CVE-2016-3841 (C, published 2016-08-06T20:59Z)
Description: value: The IPv6 stack in the Linux kernel before 4.3.3 mishandles options data, which allows local users to gain privileges or cause a denial of service (use-after-free and system crash) via a crafted sendmsg system call.
Severity: HIGH (CVSS v3 base score: 7.3)
Weakness: CWE-416 \u2014 Use After Free
The product reuses or references memory after it has been freed. At some point afterward, the memory may be allocated again and saved in another pointer, while the original pointer references a location somewhere within the new allocation. Any operations using the original pointer are no longer valid because the memory "belongs" to the code that operates on the new pointer.
Repository: https://github.com/torvalds/linux
Files changed:
include/linux/ipv6.h
include/net/ipv6.h
net/dccp/ipv6.c
net/ipv6/af_inet6.c
net/ipv6/datagram.c
net/ipv6/exthdrs.c
net/ipv6/inet6_connection_sock.c
net/ipv6/ipv6_sockglue.c
net/ipv6/raw.c
net/ipv6/syncookies.c
net/ipv6/tcp_ipv6.c
net/ipv6/udp.c
net/l2tp/l2tp_ip6.c
Commit message: ipv6: add complete rcu protection around np->opt

This patch addresses multiple problems :

UDP/RAW sendmsg() need to get a stable struct ipv6_txoptions
while socket is not locked : Other threads can change np->opt
concurrently. Dmitry posted a syzkaller
(http://github.com/google/syzkaller) program desmonstrating
use-after-free.

Starting with TCP/DCCP lockless listeners, tcp_v6_syn_recv_sock()
and dccp_v6_request_recv_sock() also need to use RCU protection
to dereference np->opt once (before calling ipv6_dup_options())

This patch adds full RCU protection to np->opt

Reported-by: Dmitry Vyukov <dvyukov@google.com>
Signed-off-by: Eric Dumazet <edumazet@google.com>
Acked-by: Hannes Frederic Sowa <hannes@stressinduktion.org>
Signed-off-by: David S. Miller <davem@davemloft.net>


Vulnerable code:
	struct ipv6_txoptions	*opt;

	final_p = fl6_update_dst(&fl6, np->opt, &final);
		err = ip6_xmit(sk, skb, &fl6, np->opt, np->tclass);
	if (np->opt != NULL)
		newnp->opt = ipv6_dup_options(newsk, np->opt);

	if (newnp->opt != NULL)
		inet_csk(newsk)->icsk_ext_hdr_len = (newnp->opt->opt_nflen +
						     newnp->opt->opt_flen);
	final_p = fl6_update_dst(&fl6, np->opt, &final);
	if (np->opt != NULL)
		icsk->icsk_ext_hdr_len = (np->opt->opt_flen +
					  np->opt->opt_nflen);
	opt = xchg(&np->opt, NULL);
	if (opt)
		sock_kfree_s(sk, opt, opt->tot_len);
		final_p = fl6_update_dst(&fl6, np->opt, &final);
	opt = flowlabel ? flowlabel->opt : np->opt;

	final_p = fl6_update_dst(fl6, np->opt, &final);
	final_p = fl6_update_dst(fl6, np->opt, &final);
	res = ip6_xmit(sk, skb, &fl6, np->opt, np->tclass);
	opt = xchg(&inet6_sk(sk)->opt, opt);
			opt = xchg(&np->opt, NULL);
			if (opt)
				sock_kfree_s(sk, opt, opt->tot_len);
		opt = ipv6_renew_options(sk, np->opt, optname,
		if (opt)
			sock_kfree_s(sk, opt, opt->tot_len);
		if (opt)
			sock_kfree_s(sk, opt, opt->tot_len);
		len = ipv6_getsockopt_sticky(sk, np->opt,
					     optname, optval, len);
	if (!opt)
		opt = np->opt;
		final_p = fl6_update_dst(&fl6, np->opt, &final);
	final_p = fl6_update_dst(&fl6, np->opt, &final);
	if (np->opt)
		icsk->icsk_ext_hdr_len = (np->opt->opt_flen +
					  np->opt->opt_nflen);
		err = ip6_xmit(sk, skb, fl6, np->opt, np->tclass);
	if (np->opt)
		newnp->opt = ipv6_dup_options(newsk, np->opt);

	if (newnp->opt)
		inet_csk(newsk)->icsk_ext_hdr_len = (newnp->opt->opt_nflen +
						     newnp->opt->opt_flen);
	if (!opt)
		opt = np->opt;
	if (opt == NULL)
		opt = np->opt;

Fixed code:
	struct ipv6_txoptions __rcu	*opt;
	atomic_t		refcnt;
	struct rcu_head		rcu;
static inline struct ipv6_txoptions *txopt_get(const struct ipv6_pinfo *np)
{
	struct ipv6_txoptions *opt;

	rcu_read_lock();
	opt = rcu_dereference(np->opt);
	if (opt && !atomic_inc_not_zero(&opt->refcnt))
		opt = NULL;
	rcu_read_unlock();
	return opt;
}

static inline void txopt_put(struct ipv6_txoptions *opt)
{
	if (opt && atomic_dec_and_test(&opt->refcnt))
		kfree_rcu(opt, rcu);
}

	rcu_read_lock();
	final_p = fl6_update_dst(&fl6, rcu_dereference(np->opt), &final);
	rcu_read_unlock();
		rcu_read_lock();
		err = ip6_xmit(sk, skb, &fl6, rcu_dereference(np->opt),
			       np->tclass);
		rcu_read_unlock();
	struct ipv6_txoptions *opt;
	opt = rcu_dereference(np->opt);
	if (opt) {
		opt = ipv6_dup_options(newsk, opt);
		RCU_INIT_POINTER(newnp->opt, opt);
	}
	if (opt)
		inet_csk(newsk)->icsk_ext_hdr_len = opt->opt_nflen +
						    opt->opt_flen;
	struct ipv6_txoptions *opt;
	opt = rcu_dereference_protected(np->opt, sock_owned_by_user(sk));
	final_p = fl6_update_dst(&fl6, opt, &final);
	if (opt)
		icsk->icsk_ext_hdr_len = opt->opt_flen + opt->opt_nflen;
	opt = xchg((__force struct ipv6_txoptions **)&np->opt, NULL);
	if (opt) {
		atomic_sub(opt->tot_len, &sk->sk_omem_alloc);
		txopt_put(opt);
	}
		rcu_read_lock();
		final_p = fl6_update_dst(&fl6, rcu_dereference(np->opt),
					 &final);
		rcu_read_unlock();
	rcu_read_lock();
	opt = flowlabel ? flowlabel->opt : rcu_dereference(np->opt);
	rcu_read_unlock();
		atomic_set(&opt2->refcnt, 1);
	atomic_set(&opt2->refcnt, 1);
	rcu_read_lock();
	final_p = fl6_update_dst(fl6, rcu_dereference(np->opt), &final);
	rcu_read_unlock();
	rcu_read_lock();
	final_p = fl6_update_dst(fl6, rcu_dereference(np->opt), &final);
	rcu_read_unlock();
	res = ip6_xmit(sk, skb, &fl6, rcu_dereference(np->opt),
		       np->tclass);
	opt = xchg((__force struct ipv6_txoptions **)&inet6_sk(sk)->opt,
		   opt);
			opt = xchg((__force struct ipv6_txoptions **)&np->opt,
				   NULL);
			if (opt) {
				atomic_sub(opt->tot_len, &sk->sk_omem_alloc);
				txopt_put(opt);
			}
		opt = rcu_dereference_protected(np->opt, sock_owned_by_user(sk));
		opt = ipv6_renew_options(sk, opt, optname,
		if (opt) {
			atomic_sub(opt->tot_len, &sk->sk_omem_alloc);
			txopt_put(opt);
		}
		atomic_set(&opt->refcnt, 1);
		if (opt) {
			atomic_sub(opt->tot_len, &sk->sk_omem_alloc);
			txopt_put(opt);
		}
		struct ipv6_txoptions *opt;
		opt = rcu_dereference_protected(np->opt, sock_owned_by_user(sk));
		len = ipv6_getsockopt_sticky(sk, opt, optname, optval, len);
	struct ipv6_txoptions *opt_to_free = NULL;
	if (!opt) {
		opt = txopt_get(np);
		opt_to_free = opt;
		}
	txopt_put(opt_to_free);
		final_p = fl6_update_dst(&fl6, rcu_dereference(np->opt), &final);
	struct ipv6_txoptions *opt;
	opt = rcu_dereference_protected(np->opt, sock_owned_by_user(sk));
	final_p = fl6_update_dst(&fl6, opt, &final);
	if (opt)
		icsk->icsk_ext_hdr_len = opt->opt_flen +
					 opt->opt_nflen;
		err = ip6_xmit(sk, skb, fl6, rcu_dereference(np->opt),
			       np->tclass);
	struct ipv6_txoptions *opt;
	opt = rcu_dereference(np->opt);
	if (opt) {
		opt = ipv6_dup_options(newsk, opt);
		RCU_INIT_POINTER(newnp->opt, opt);
	}
	if (opt)
		inet_csk(newsk)->icsk_ext_hdr_len = opt->opt_nflen +
						    opt->opt_flen;
	struct ipv6_txoptions *opt_to_free = NULL;
	if (!opt) {
		opt = txopt_get(np);
		opt_to_free = opt;
	}
	txopt_put(opt_to_free);
	struct ipv6_txoptions *opt_to_free = NULL;
	if (!opt) {
		opt = txopt_get(np);
		opt_to_free = opt;
	}
	txopt_put(opt_to_free);