CVE-2012-3552 (C, published 2012-10-03T11:02Z)
Description: value: Race condition in the IP implementation in the Linux kernel before 3.0 might allow remote attackers to cause a denial of service (slab corruption and system crash) by sending packets to an application that sets socket options during the handling of network traffic.
Severity: HIGH (CVSS v3 base score: 5.9)
Weakness: CWE-362 \u2014 Concurrent Execution using Shared Resource with Improper Synchronization ('Race Condition')
The product contains a code sequence that can run concurrently with other code, and the code sequence requires temporary, exclusive access to a shared resource, but a timing window exists in which the shared resource can be modified by another code sequence that is operating concurrently.
Repository: https://github.com/torvalds/linux
Files changed:
include/net/inet_sock.h
include/net/ip.h
net/dccp/ipv4.c
net/dccp/ipv6.c
net/ipv4/af_inet.c
net/ipv4/cipso_ipv4.c
net/ipv4/icmp.c
net/ipv4/inet_connection_sock.c
net/ipv4/ip_options.c
net/ipv4/ip_output.c
net/ipv4/ip_sockglue.c
net/ipv4/raw.c
net/ipv4/syncookies.c
net/ipv4/tcp_ipv4.c
net/ipv4/udp.c
net/ipv6/tcp_ipv6.c
net/l2tp/l2tp_ip.c
Commit message: inet: add RCU protection to inet->opt

We lack proper synchronization to manipulate inet->opt ip_options

Problem is ip_make_skb() calls ip_setup_cork() and
ip_setup_cork() possibly makes a copy of ipc->opt (struct ip_options),
without any protection against another thread manipulating inet->opt.

Another thread can change inet->opt pointer and free old one under us.

Use RCU to protect inet->opt (changed to inet->inet_opt).

Instead of handling atomic refcounts, just copy ip_options when
necessary, to avoid cache line dirtying.

We cant insert an rcu_head in struct ip_options since its included in
skb->cb[], so this patch is large because I had to introduce a new
ip_options_rcu structure.

Signed-off-by: Eric Dumazet <eric.dumazet@gmail.com>
Cc: Herbert Xu <herbert@gondor.apana.org.au>
Signed-off-by: David S. Miller <davem@davemloft.net>


Vulnerable code:
#define optlength(opt) (sizeof(struct ip_options) + opt->optlen)
	struct ip_options	*opt;
	struct ip_options	*opt;
	struct ip_options	*opt;
					      struct ip_options *opt);
extern void ip_options_build(struct sk_buff *skb, struct ip_options *opt, __be32 daddr, struct rtable *rt, int is_frag);
extern int ip_options_get(struct net *net, struct ip_options **optp,
extern int ip_options_get_from_user(struct net *net, struct ip_options **optp,
	if (inet->opt != NULL && inet->opt->srr) {
		nexthop = inet->opt->faddr;
	if (inet->opt == NULL || !inet->opt->srr)
	if (inet->opt != NULL)
		inet_csk(sk)->icsk_ext_hdr_len = inet->opt->optlen;
	newinet->opt	   = ireq->opt;
	newinet->opt = NULL;
	kfree(inet->opt);
	if (inet->opt && inet->opt->srr)
		daddr = inet->opt->faddr;
	if (inet->opt && inet->opt->srr)
		daddr = inet->opt->faddr;
	struct ip_options *opt = NULL;
	memcpy(opt->__data, buf, buf_len);
	opt->optlen = opt_len;
	opt->cipso = sizeof(struct iphdr);
		if (sk_inet->opt)
			sk_conn->icsk_ext_hdr_len -= sk_inet->opt->optlen;
		sk_conn->icsk_ext_hdr_len += opt->optlen;
	opt = xchg(&sk_inet->opt, opt);
	kfree(opt);
	struct ip_options *opt = NULL;
	memcpy(opt->__data, buf, buf_len);
	opt->optlen = opt_len;
	opt->cipso = sizeof(struct iphdr);
	kfree(opt);
static int cipso_v4_delopt(struct ip_options **opt_ptr)
	struct ip_options *opt = *opt_ptr;
	if (opt->srr || opt->rr || opt->ts || opt->router_alert) {
		cipso_off = opt->cipso - sizeof(struct iphdr);
		cipso_ptr = &opt->__data[cipso_off];
		if (opt->srr > opt->cipso)
			opt->srr -= cipso_len;
		if (opt->rr > opt->cipso)
			opt->rr -= cipso_len;
		if (opt->ts > opt->cipso)
			opt->ts -= cipso_len;
		if (opt->router_alert > opt->cipso)
			opt->router_alert -= cipso_len;
		opt->cipso = 0;
			opt->optlen - cipso_off - cipso_len);
		while (iter < opt->optlen)
			if (opt->__data[iter] != IPOPT_NOP) {
				iter += opt->__data[iter + 1];
		hdr_delta = opt->optlen;
		opt->optlen = (optlen_new + 3) & ~3;
		hdr_delta -= opt->optlen;
		hdr_delta = opt->optlen;
		kfree(opt);
	struct ip_options *opt;
	opt = sk_inet->opt;
	if (opt == NULL || opt->cipso == 0)
	hdr_delta = cipso_v4_delopt(&sk_inet->opt);
	struct ip_options *opt;
	if (opt == NULL || opt->cipso == 0)
	struct ip_options *opt;
	opt = inet_sk(sk)->opt;
	if (opt == NULL || opt->cipso == 0)
		return -ENOMSG;

	return cipso_v4_getattr(opt->__data + opt->cipso - sizeof(struct iphdr),
				secattr);
	struct ip_options replyopts;
	unsigned char  optbuf[40];
	if (ip_options_echo(&icmp_param->replyopts, skb))
	if (icmp_param->replyopts.optlen) {
		ipc.opt = &icmp_param->replyopts;
		if (ipc.opt->srr)
			daddr = icmp_param->replyopts.faddr;
		.daddr = (param->replyopts.srr ?
			  param->replyopts.faddr : iph->saddr),
	if (ip_options_echo(&icmp_param.replyopts, skb_in))
	ipc.opt = &icmp_param.replyopts;
	room -= sizeof(struct iphdr) + icmp_param.replyopts.optlen;
	struct ip_options *opt = inet_rsk(req)->opt;
			   (opt && opt->srr) ? opt->faddr : ireq->rmt_addr,
	if (opt && opt->is_strictroute && rt->rt_dst != rt->rt_gateway)
void ip_options_build(struct sk_buff * skb, struct ip_options * opt,
int ip_options_echo(struct ip_options * dopt, struct sk_buff * skb)
	struct ip_options *sopt;
	if (sopt->optlen == 0) {
		dopt->optlen = 0;
	}
		unsigned char * start = sptr+sopt->srr;
static struct ip_options *ip_options_get_alloc(const int optlen)
	return kzalloc(sizeof(struct ip_options) + ((optlen + 3) & ~3),
static int ip_options_get_finish(struct net *net, struct ip_options **optp,
				 struct ip_options *opt, int optlen)
		opt->__data[optlen++] = IPOPT_END;
	opt->optlen = optlen;
	if (optlen && ip_options_compile(net, opt, NULL)) {
int ip_options_get_from_user(struct net *net, struct ip_options **optp,
	struct ip_options *opt = ip_options_get_alloc(optlen);
	if (optlen && copy_from_user(opt->__data, data, optlen)) {
int ip_options_get(struct net *net, struct ip_options **optp,
	struct ip_options *opt = ip_options_get_alloc(optlen);
		memcpy(opt->__data, data, optlen);
			  __be32 saddr, __be32 daddr, struct ip_options *opt)
	skb_push(skb, sizeof(struct iphdr) + (opt ? opt->optlen : 0));
	if (opt && opt->optlen) {
		iph->ihl += opt->optlen>>2;
		ip_options_build(skb, opt, daddr, rt, 0);
	struct ip_options *opt = inet->opt;
		if(opt && opt->srr)
			daddr = opt->faddr;
	if (opt && opt->is_strictroute && rt->rt_dst != rt->rt_gateway)
	skb_push(skb, sizeof(struct iphdr) + (opt ? opt->optlen : 0));
	if (opt && opt->optlen) {
		iph->ihl += opt->optlen >> 2;
		ip_options_build(skb, opt, inet->inet_daddr, rt, 0);
	struct ip_options *opt;
		memcpy(cork->opt, opt, sizeof(struct ip_options) + opt->optlen);
	struct {
		struct ip_options	opt;
		char			data[40];
	} replyopts;
	if (ip_options_echo(&replyopts.opt, skb))
	if (replyopts.opt.optlen) {
		if (ipc.opt->srr)
			daddr = replyopts.opt.faddr;
		struct ip_options *opt = NULL;
				if (inet->opt)
					icsk->icsk_ext_hdr_len -= inet->opt->optlen;
					icsk->icsk_ext_hdr_len += opt->optlen;
		opt = xchg(&inet->opt, opt);
		kfree(opt);
		struct ip_options * opt = (struct ip_options *)optbuf;
		if (inet->opt)
			memcpy(optbuf, inet->opt,
			       sizeof(struct ip_options)+
			       inet->opt->optlen);
	if (!ipc.opt)
		ipc.opt = inet->opt;
		if (ipc.opt->srr) {
			daddr = ipc.opt->faddr;
		int opt_size = sizeof(struct ip_options) + opt->optlen;
		if (ireq->opt != NULL && ip_options_echo(ireq->opt, skb)) {
	if (inet->opt && inet->opt->srr) {
		nexthop = inet->opt->faddr;
	if (!inet->opt || !inet->opt->srr)
	if (inet->opt)
		inet_csk(sk)->icsk_ext_hdr_len = inet->opt->optlen;
static struct ip_options *tcp_v4_save_options(struct sock *sk,
					      struct sk_buff *skb)
	struct ip_options *opt = &(IPCB(skb)->opt);
	struct ip_options *dopt = NULL;
		int opt_size = optlength(opt);
			if (ip_options_echo(dopt, skb)) {
	newinet->opt	      = ireq->opt;
	if (newinet->opt)
		inet_csk(newsk)->icsk_ext_hdr_len = newinet->opt->optlen;
	if (!ipc.opt)
		ipc.opt = inet->opt;
	if (ipc.opt && ipc.opt->srr) {
		faddr = ipc.opt->faddr;
	    (ipc.opt && ipc.opt->is_strictroute)) {
	newinet->opt = NULL;
	struct ip_options *opt = inet->opt;
		if (opt && opt->srr)
			daddr = opt->faddr;

Fixed code:
struct ip_options_rcu {
	struct rcu_head rcu;
	struct ip_options opt;
};

struct ip_options_data {
	struct ip_options_rcu	opt;
	char			data[40];
};
	struct ip_options_rcu	*opt;
	struct ip_options_rcu __rcu	*inet_opt;
	struct ip_options_rcu	*opt;
					      struct ip_options_rcu *opt);
extern void ip_options_build(struct sk_buff *skb, struct ip_options *opt,
			     __be32 daddr, struct rtable *rt, int is_frag);
extern int ip_options_get(struct net *net, struct ip_options_rcu **optp,
extern int ip_options_get_from_user(struct net *net, struct ip_options_rcu **optp,
	struct ip_options_rcu *inet_opt;

	inet_opt = rcu_dereference_protected(inet->inet_opt,
					     sock_owned_by_user(sk));
	if (inet_opt != NULL && inet_opt->opt.srr) {
		nexthop = inet_opt->opt.faddr;
	if (inet_opt == NULL || !inet_opt->opt.srr)
	if (inet_opt)
		inet_csk(sk)->icsk_ext_hdr_len = inet_opt->opt.optlen;
	newinet->inet_opt	= ireq->opt;
	newinet->inet_opt = NULL;
	kfree(rcu_dereference_protected(inet->inet_opt, 1));
	struct ip_options_rcu *inet_opt;
	inet_opt = rcu_dereference_protected(inet->inet_opt,
					     sock_owned_by_user(sk));
	if (inet_opt && inet_opt->opt.srr)
		daddr = inet_opt->opt.faddr;
	struct ip_options_rcu *inet_opt;
	rcu_read_lock();
	inet_opt = rcu_dereference(inet->inet_opt);
	if (inet_opt && inet_opt->opt.srr)
		daddr = inet_opt->opt.faddr;
	rcu_read_unlock();
static void opt_kfree_rcu(struct rcu_head *head)
{
	kfree(container_of(head, struct ip_options_rcu, rcu));
}

	struct ip_options_rcu *old, *opt = NULL;
	memcpy(opt->opt.__data, buf, buf_len);
	opt->opt.optlen = opt_len;
	opt->opt.cipso = sizeof(struct iphdr);

	old = rcu_dereference_protected(sk_inet->inet_opt, sock_owned_by_user(sk));
		if (old)
			sk_conn->icsk_ext_hdr_len -= old->opt.optlen;
		sk_conn->icsk_ext_hdr_len += opt->opt.optlen;
	rcu_assign_pointer(sk_inet->inet_opt, opt);
	if (old)
		call_rcu(&old->rcu, opt_kfree_rcu);
	struct ip_options_rcu *opt = NULL;
	memcpy(opt->opt.__data, buf, buf_len);
	opt->opt.optlen = opt_len;
	opt->opt.cipso = sizeof(struct iphdr);
	if (opt)
		call_rcu(&opt->rcu, opt_kfree_rcu);
static int cipso_v4_delopt(struct ip_options_rcu **opt_ptr)
	struct ip_options_rcu *opt = *opt_ptr;
	if (opt->opt.srr || opt->opt.rr || opt->opt.ts || opt->opt.router_alert) {
		cipso_off = opt->opt.cipso - sizeof(struct iphdr);
		cipso_ptr = &opt->opt.__data[cipso_off];
		if (opt->opt.srr > opt->opt.cipso)
			opt->opt.srr -= cipso_len;
		if (opt->opt.rr > opt->opt.cipso)
			opt->opt.rr -= cipso_len;
		if (opt->opt.ts > opt->opt.cipso)
			opt->opt.ts -= cipso_len;
		if (opt->opt.router_alert > opt->opt.cipso)
			opt->opt.router_alert -= cipso_len;
		opt->opt.cipso = 0;
			opt->opt.optlen - cipso_off - cipso_len);
		while (iter < opt->opt.optlen)
			if (opt->opt.__data[iter] != IPOPT_NOP) {
				iter += opt->opt.__data[iter + 1];
		hdr_delta = opt->opt.optlen;
		opt->opt.optlen = (optlen_new + 3) & ~3;
		hdr_delta -= opt->opt.optlen;
		hdr_delta = opt->opt.optlen;
		call_rcu(&opt->rcu, opt_kfree_rcu);
	struct ip_options_rcu *opt;
	opt = rcu_dereference_protected(sk_inet->inet_opt, 1);
	if (opt == NULL || opt->opt.cipso == 0)
	hdr_delta = cipso_v4_delopt(&sk_inet->inet_opt);
	struct ip_options_rcu *opt;
	if (opt == NULL || opt->opt.cipso == 0)
	struct ip_options_rcu *opt;
	int res = -ENOMSG;
	rcu_read_lock();
	opt = rcu_dereference(inet_sk(sk)->inet_opt);
	if (opt && opt->opt.cipso)
		res = cipso_v4_getattr(opt->opt.__data +
						opt->opt.cipso -
						sizeof(struct iphdr),
				       secattr);
	rcu_read_unlock();
	return res;
	struct ip_options_data replyopts;
	if (ip_options_echo(&icmp_param->replyopts.opt.opt, skb))
	if (icmp_param->replyopts.opt.opt.optlen) {
		ipc.opt = &icmp_param->replyopts.opt;
		if (ipc.opt->opt.srr)
			daddr = icmp_param->replyopts.opt.opt.faddr;
		.daddr = (param->replyopts.opt.opt.srr ?
			  param->replyopts.opt.opt.faddr : iph->saddr),
	if (ip_options_echo(&icmp_param.replyopts.opt.opt, skb_in))
	ipc.opt = &icmp_param.replyopts.opt;
	room -= sizeof(struct iphdr) + icmp_param.replyopts.opt.opt.optlen;
	struct ip_options_rcu *opt = inet_rsk(req)->opt;
			   (opt && opt->opt.srr) ? opt->opt.faddr : ireq->rmt_addr,
	if (opt && opt->opt.is_strictroute && rt->rt_dst != rt->rt_gateway)
void ip_options_build(struct sk_buff *skb, struct ip_options *opt,
int ip_options_echo(struct ip_options *dopt, struct sk_buff *skb)
	const struct ip_options *sopt;
	if (sopt->optlen == 0)
		unsigned char *start = sptr+sopt->srr;
static struct ip_options_rcu *ip_options_get_alloc(const int optlen)
	return kzalloc(sizeof(struct ip_options_rcu) + ((optlen + 3) & ~3),
static int ip_options_get_finish(struct net *net, struct ip_options_rcu **optp,
				 struct ip_options_rcu *opt, int optlen)
		opt->opt.__data[optlen++] = IPOPT_END;
	opt->opt.optlen = optlen;
	if (optlen && ip_options_compile(net, &opt->opt, NULL)) {
int ip_options_get_from_user(struct net *net, struct ip_options_rcu **optp,
	struct ip_options_rcu *opt = ip_options_get_alloc(optlen);
	if (optlen && copy_from_user(opt->opt.__data, data, optlen)) {
int ip_options_get(struct net *net, struct ip_options_rcu **optp,
	struct ip_options_rcu *opt = ip_options_get_alloc(optlen);
		memcpy(opt->opt.__data, data, optlen);
			  __be32 saddr, __be32 daddr, struct ip_options_rcu *opt)
	skb_push(skb, sizeof(struct iphdr) + (opt ? opt->opt.optlen : 0));
	if (opt && opt->opt.optlen) {
		iph->ihl += opt->opt.optlen>>2;
		ip_options_build(skb, &opt->opt, daddr, rt, 0);
	struct ip_options_rcu *inet_opt;
	inet_opt = rcu_dereference(inet->inet_opt);
		if (inet_opt && inet_opt->opt.srr)
			daddr = inet_opt->opt.faddr;
	if (inet_opt && inet_opt->opt.is_strictroute && rt->rt_dst != rt->rt_gateway)
	skb_push(skb, sizeof(struct iphdr) + (inet_opt ? inet_opt->opt.optlen : 0));
	if (inet_opt && inet_opt->opt.optlen) {
		iph->ihl += inet_opt->opt.optlen >> 2;
		ip_options_build(skb, &inet_opt->opt, inet->inet_daddr, rt, 0);
	struct ip_options_rcu *opt;
		memcpy(cork->opt, &opt->opt, sizeof(struct ip_options) + opt->opt.optlen);
	struct ip_options_data replyopts;
	if (ip_options_echo(&replyopts.opt.opt, skb))
	if (replyopts.opt.opt.optlen) {
		if (replyopts.opt.opt.srr)
			daddr = replyopts.opt.opt.faddr;
static void opt_kfree_rcu(struct rcu_head *head)
{
	kfree(container_of(head, struct ip_options_rcu, rcu));
}

		struct ip_options_rcu *old, *opt = NULL;

		old = rcu_dereference_protected(inet->inet_opt,
						sock_owned_by_user(sk));
				if (old)
					icsk->icsk_ext_hdr_len -= old->opt.optlen;
					icsk->icsk_ext_hdr_len += opt->opt.optlen;
		rcu_assign_pointer(inet->inet_opt, opt);
		if (old)
			call_rcu(&old->rcu, opt_kfree_rcu);
		struct ip_options *opt = (struct ip_options *)optbuf;
		struct ip_options_rcu *inet_opt;

		inet_opt = rcu_dereference_protected(inet->inet_opt,
						     sock_owned_by_user(sk));
		if (inet_opt)
			memcpy(optbuf, &inet_opt->opt,
			       sizeof(struct ip_options) +
			       inet_opt->opt.optlen);
	struct ip_options_data opt_copy;
	if (!ipc.opt) {
		struct ip_options_rcu *inet_opt;

		rcu_read_lock();
		inet_opt = rcu_dereference(inet->inet_opt);
		if (inet_opt) {
			memcpy(&opt_copy, inet_opt,
			       sizeof(*inet_opt) + inet_opt->opt.optlen);
			ipc.opt = &opt_copy.opt;
		}
		rcu_read_unlock();
	}
		if (ipc.opt->opt.srr) {
			daddr = ipc.opt->opt.faddr;
		int opt_size = sizeof(struct ip_options_rcu) + opt->optlen;
		if (ireq->opt != NULL && ip_options_echo(&ireq->opt->opt, skb)) {
	struct ip_options_rcu *inet_opt;
	inet_opt = rcu_dereference_protected(inet->inet_opt,
					     sock_owned_by_user(sk));
	if (inet_opt && inet_opt->opt.srr) {
		nexthop = inet_opt->opt.faddr;
	if (!inet_opt || !inet_opt->opt.srr)
	if (inet_opt)
		inet_csk(sk)->icsk_ext_hdr_len = inet_opt->opt.optlen;
static struct ip_options_rcu *tcp_v4_save_options(struct sock *sk,
						  struct sk_buff *skb)
	const struct ip_options *opt = &(IPCB(skb)->opt);
	struct ip_options_rcu *dopt = NULL;
		int opt_size = sizeof(*dopt) + opt->optlen;

			if (ip_options_echo(&dopt->opt, skb)) {
	struct ip_options_rcu *inet_opt;
	inet_opt	      = ireq->opt;
	rcu_assign_pointer(newinet->inet_opt, inet_opt);
	if (inet_opt)
		inet_csk(newsk)->icsk_ext_hdr_len = inet_opt->opt.optlen;
	struct ip_options_data opt_copy;
	if (!ipc.opt) {
		struct ip_options_rcu *inet_opt;

		rcu_read_lock();
		inet_opt = rcu_dereference(inet->inet_opt);
		if (inet_opt) {
			memcpy(&opt_copy, inet_opt,
			       sizeof(*inet_opt) + inet_opt->opt.optlen);
			ipc.opt = &opt_copy.opt;
		}
		rcu_read_unlock();
	}
	if (ipc.opt && ipc.opt->opt.srr) {
		faddr = ipc.opt->opt.faddr;
	    (ipc.opt && ipc.opt->opt.is_strictroute)) {
	newinet->inet_opt = NULL;
		struct ip_options_rcu *inet_opt;

		inet_opt = rcu_dereference_protected(inet->inet_opt,
						     sock_owned_by_user(sk));

		if (inet_opt && inet_opt->opt.srr)
			daddr = inet_opt->opt.faddr;