Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

ipv6: make ipv6_renew_options() interrupt/kernel safe

At present the ipv6_renew_options_kern() function ends up calling into
access_ok() which is problematic if done from inside an interrupt as
access_ok() calls WARN_ON_IN_IRQ() on some (all?) architectures
(x86-64 is affected). Example warning/backtrace is shown below:

WARNING: CPU: 1 PID: 3144 at lib/usercopy.c:11 _copy_from_user+0x85/0x90
...
Call Trace:
<IRQ>
ipv6_renew_option+0xb2/0xf0
ipv6_renew_options+0x26a/0x340
ipv6_renew_options_kern+0x2c/0x40
calipso_req_setattr+0x72/0xe0
netlbl_req_setattr+0x126/0x1b0
selinux_netlbl_inet_conn_request+0x80/0x100
selinux_inet_conn_request+0x6d/0xb0
security_inet_conn_request+0x32/0x50
tcp_conn_request+0x35f/0xe00
? __lock_acquire+0x250/0x16c0
? selinux_socket_sock_rcv_skb+0x1ae/0x210
? tcp_rcv_state_process+0x289/0x106b
tcp_rcv_state_process+0x289/0x106b
? tcp_v6_do_rcv+0x1a7/0x3c0
tcp_v6_do_rcv+0x1a7/0x3c0
tcp_v6_rcv+0xc82/0xcf0
ip6_input_finish+0x10d/0x690
ip6_input+0x45/0x1e0
? ip6_rcv_finish+0x1d0/0x1d0
ipv6_rcv+0x32b/0x880
? ip6_make_skb+0x1e0/0x1e0
__netif_receive_skb_core+0x6f2/0xdf0
? process_backlog+0x85/0x250
? process_backlog+0x85/0x250
? process_backlog+0xec/0x250
process_backlog+0xec/0x250
net_rx_action+0x153/0x480
__do_softirq+0xd9/0x4f7
do_softirq_own_stack+0x2a/0x40
</IRQ>
...

While not present in the backtrace, ipv6_renew_option() ends up calling
access_ok() via the following chain:

access_ok()
_copy_from_user()
copy_from_user()
ipv6_renew_option()

The fix presented in this patch is to perform the userspace copy
earlier in the call chain such that it is only called when the option
data is actually coming from userspace; that place is
do_ipv6_setsockopt(). Not only does this solve the problem seen in
the backtrace above, it also allows us to simplify the code quite a
bit by removing ipv6_renew_options_kern() completely. We also take
this opportunity to cleanup ipv6_renew_options()/ipv6_renew_option()
a small amount as well.

This patch is heavily based on a rough patch by Al Viro. I've taken
his original patch, converted a kmemdup() call in do_ipv6_setsockopt()
to a memdup_user() call, made better use of the e_inval jump target in
the same function, and cleaned up the use ipv6_renew_option() by
ipv6_renew_options().

CC: Al Viro <viro@zeniv.linux.org.uk>
Signed-off-by: Paul Moore <paul@paul-moore.com>
Signed-off-by: David S. Miller <davem@davemloft.net>

authored by

Paul Moore and committed by
David S. Miller
a9ba23d4 0df8adbb

+53 -103
+1 -8
include/net/ipv6.h
··· 355 355 struct ipv6_txoptions *ipv6_renew_options(struct sock *sk, 356 356 struct ipv6_txoptions *opt, 357 357 int newtype, 358 - struct ipv6_opt_hdr __user *newopt, 359 - int newoptlen); 360 - struct ipv6_txoptions * 361 - ipv6_renew_options_kern(struct sock *sk, 362 - struct ipv6_txoptions *opt, 363 - int newtype, 364 - struct ipv6_opt_hdr *newopt, 365 - int newoptlen); 358 + struct ipv6_opt_hdr *newopt); 366 359 struct ipv6_txoptions *ipv6_fixup_options(struct ipv6_txoptions *opt_space, 367 360 struct ipv6_txoptions *opt); 368 361
+3 -6
net/ipv6/calipso.c
··· 799 799 { 800 800 struct ipv6_txoptions *old = txopt_get(inet6_sk(sk)), *txopts; 801 801 802 - txopts = ipv6_renew_options_kern(sk, old, IPV6_HOPOPTS, 803 - hop, hop ? ipv6_optlen(hop) : 0); 802 + txopts = ipv6_renew_options(sk, old, IPV6_HOPOPTS, hop); 804 803 txopt_put(old); 805 804 if (IS_ERR(txopts)) 806 805 return PTR_ERR(txopts); ··· 1221 1222 if (IS_ERR(new)) 1222 1223 return PTR_ERR(new); 1223 1224 1224 - txopts = ipv6_renew_options_kern(sk, req_inet->ipv6_opt, IPV6_HOPOPTS, 1225 - new, new ? ipv6_optlen(new) : 0); 1225 + txopts = ipv6_renew_options(sk, req_inet->ipv6_opt, IPV6_HOPOPTS, new); 1226 1226 1227 1227 kfree(new); 1228 1228 ··· 1258 1260 if (calipso_opt_del(req_inet->ipv6_opt->hopopt, &new)) 1259 1261 return; /* Nothing to do */ 1260 1262 1261 - txopts = ipv6_renew_options_kern(sk, req_inet->ipv6_opt, IPV6_HOPOPTS, 1262 - new, new ? ipv6_optlen(new) : 0); 1263 + txopts = ipv6_renew_options(sk, req_inet->ipv6_opt, IPV6_HOPOPTS, new); 1263 1264 1264 1265 if (!IS_ERR(txopts)) { 1265 1266 txopts = xchg(&req_inet->ipv6_opt, txopts);
+30 -81
net/ipv6/exthdrs.c
··· 1015 1015 } 1016 1016 EXPORT_SYMBOL_GPL(ipv6_dup_options); 1017 1017 1018 - static int ipv6_renew_option(void *ohdr, 1019 - struct ipv6_opt_hdr __user *newopt, int newoptlen, 1020 - int inherit, 1021 - struct ipv6_opt_hdr **hdr, 1022 - char **p) 1018 + static void ipv6_renew_option(int renewtype, 1019 + struct ipv6_opt_hdr **dest, 1020 + struct ipv6_opt_hdr *old, 1021 + struct ipv6_opt_hdr *new, 1022 + int newtype, char **p) 1023 1023 { 1024 - if (inherit) { 1025 - if (ohdr) { 1026 - memcpy(*p, ohdr, ipv6_optlen((struct ipv6_opt_hdr *)ohdr)); 1027 - *hdr = (struct ipv6_opt_hdr *)*p; 1028 - *p += CMSG_ALIGN(ipv6_optlen(*hdr)); 1029 - } 1030 - } else { 1031 - if (newopt) { 1032 - if (copy_from_user(*p, newopt, newoptlen)) 1033 - return -EFAULT; 1034 - *hdr = (struct ipv6_opt_hdr *)*p; 1035 - if (ipv6_optlen(*hdr) > newoptlen) 1036 - return -EINVAL; 1037 - *p += CMSG_ALIGN(newoptlen); 1038 - } 1039 - } 1040 - return 0; 1024 + struct ipv6_opt_hdr *src; 1025 + 1026 + src = (renewtype == newtype ? new : old); 1027 + if (!src) 1028 + return; 1029 + 1030 + memcpy(*p, src, ipv6_optlen(src)); 1031 + *dest = (struct ipv6_opt_hdr *)*p; 1032 + *p += CMSG_ALIGN(ipv6_optlen(*dest)); 1041 1033 } 1042 1034 1043 1035 /** ··· 1055 1063 */ 1056 1064 struct ipv6_txoptions * 1057 1065 ipv6_renew_options(struct sock *sk, struct ipv6_txoptions *opt, 1058 - int newtype, 1059 - struct ipv6_opt_hdr __user *newopt, int newoptlen) 1066 + int newtype, struct ipv6_opt_hdr *newopt) 1060 1067 { 1061 1068 int tot_len = 0; 1062 1069 char *p; 1063 1070 struct ipv6_txoptions *opt2; 1064 - int err; 1065 1071 1066 1072 if (opt) { 1067 1073 if (newtype != IPV6_HOPOPTS && opt->hopopt) ··· 1072 1082 tot_len += CMSG_ALIGN(ipv6_optlen(opt->dst1opt)); 1073 1083 } 1074 1084 1075 - if (newopt && newoptlen) 1076 - tot_len += CMSG_ALIGN(newoptlen); 1085 + if (newopt) 1086 + tot_len += CMSG_ALIGN(ipv6_optlen(newopt)); 1077 1087 1078 1088 if (!tot_len) 1079 1089 return NULL; ··· 1088 1098 opt2->tot_len = tot_len; 1089 1099 p = (char *)(opt2 + 1); 1090 1100 1091 - err = ipv6_renew_option(opt ? opt->hopopt : NULL, newopt, newoptlen, 1092 - newtype != IPV6_HOPOPTS, 1093 - &opt2->hopopt, &p); 1094 - if (err) 1095 - goto out; 1096 - 1097 - err = ipv6_renew_option(opt ? opt->dst0opt : NULL, newopt, newoptlen, 1098 - newtype != IPV6_RTHDRDSTOPTS, 1099 - &opt2->dst0opt, &p); 1100 - if (err) 1101 - goto out; 1102 - 1103 - err = ipv6_renew_option(opt ? opt->srcrt : NULL, newopt, newoptlen, 1104 - newtype != IPV6_RTHDR, 1105 - (struct ipv6_opt_hdr **)&opt2->srcrt, &p); 1106 - if (err) 1107 - goto out; 1108 - 1109 - err = ipv6_renew_option(opt ? opt->dst1opt : NULL, newopt, newoptlen, 1110 - newtype != IPV6_DSTOPTS, 1111 - &opt2->dst1opt, &p); 1112 - if (err) 1113 - goto out; 1101 + ipv6_renew_option(IPV6_HOPOPTS, &opt2->hopopt, 1102 + (opt ? opt->hopopt : NULL), 1103 + newopt, newtype, &p); 1104 + ipv6_renew_option(IPV6_RTHDRDSTOPTS, &opt2->dst0opt, 1105 + (opt ? opt->dst0opt : NULL), 1106 + newopt, newtype, &p); 1107 + ipv6_renew_option(IPV6_RTHDR, 1108 + (struct ipv6_opt_hdr **)&opt2->srcrt, 1109 + (opt ? (struct ipv6_opt_hdr *)opt->srcrt : NULL), 1110 + newopt, newtype, &p); 1111 + ipv6_renew_option(IPV6_DSTOPTS, &opt2->dst1opt, 1112 + (opt ? opt->dst1opt : NULL), 1113 + newopt, newtype, &p); 1114 1114 1115 1115 opt2->opt_nflen = (opt2->hopopt ? ipv6_optlen(opt2->hopopt) : 0) + 1116 1116 (opt2->dst0opt ? ipv6_optlen(opt2->dst0opt) : 0) + ··· 1108 1128 opt2->opt_flen = (opt2->dst1opt ? ipv6_optlen(opt2->dst1opt) : 0); 1109 1129 1110 1130 return opt2; 1111 - out: 1112 - sock_kfree_s(sk, opt2, opt2->tot_len); 1113 - return ERR_PTR(err); 1114 - } 1115 - 1116 - /** 1117 - * ipv6_renew_options_kern - replace a specific ext hdr with a new one. 1118 - * 1119 - * @sk: sock from which to allocate memory 1120 - * @opt: original options 1121 - * @newtype: option type to replace in @opt 1122 - * @newopt: new option of type @newtype to replace (kernel-mem) 1123 - * @newoptlen: length of @newopt 1124 - * 1125 - * See ipv6_renew_options(). The difference is that @newopt is 1126 - * kernel memory, rather than user memory. 1127 - */ 1128 - struct ipv6_txoptions * 1129 - ipv6_renew_options_kern(struct sock *sk, struct ipv6_txoptions *opt, 1130 - int newtype, struct ipv6_opt_hdr *newopt, 1131 - int newoptlen) 1132 - { 1133 - struct ipv6_txoptions *ret_val; 1134 - const mm_segment_t old_fs = get_fs(); 1135 - 1136 - set_fs(KERNEL_DS); 1137 - ret_val = ipv6_renew_options(sk, opt, newtype, 1138 - (struct ipv6_opt_hdr __user *)newopt, 1139 - newoptlen); 1140 - set_fs(old_fs); 1141 - return ret_val; 1142 1131 } 1143 1132 1144 1133 struct ipv6_txoptions *ipv6_fixup_options(struct ipv6_txoptions *opt_space,
+19 -8
net/ipv6/ipv6_sockglue.c
··· 398 398 case IPV6_DSTOPTS: 399 399 { 400 400 struct ipv6_txoptions *opt; 401 + struct ipv6_opt_hdr *new = NULL; 402 + 403 + /* hop-by-hop / destination options are privileged option */ 404 + retv = -EPERM; 405 + if (optname != IPV6_RTHDR && !ns_capable(net->user_ns, CAP_NET_RAW)) 406 + break; 401 407 402 408 /* remove any sticky options header with a zero option 403 409 * length, per RFC3542. ··· 415 409 else if (optlen < sizeof(struct ipv6_opt_hdr) || 416 410 optlen & 0x7 || optlen > 8 * 255) 417 411 goto e_inval; 418 - 419 - /* hop-by-hop / destination options are privileged option */ 420 - retv = -EPERM; 421 - if (optname != IPV6_RTHDR && !ns_capable(net->user_ns, CAP_NET_RAW)) 422 - break; 412 + else { 413 + new = memdup_user(optval, optlen); 414 + if (IS_ERR(new)) { 415 + retv = PTR_ERR(new); 416 + break; 417 + } 418 + if (unlikely(ipv6_optlen(new) > optlen)) { 419 + kfree(new); 420 + goto e_inval; 421 + } 422 + } 423 423 424 424 opt = rcu_dereference_protected(np->opt, 425 425 lockdep_sock_is_held(sk)); 426 - opt = ipv6_renew_options(sk, opt, optname, 427 - (struct ipv6_opt_hdr __user *)optval, 428 - optlen); 426 + opt = ipv6_renew_options(sk, opt, optname, new); 427 + kfree(new); 429 428 if (IS_ERR(opt)) { 430 429 retv = PTR_ERR(opt); 431 430 break;