rawv6_send_hdrinc() saves a pointer to the dst/rt6_info before calling l3mdev_ip6_out(): skb_dst_set(skb, &rt->dst); *dstp = NULL; skb = l3mdev_ip6_out(sk, skb); When the egress device is enslaved to a VRF with a non-default qdisc (e.g. fq), l3mdev_ip6_out() takes the vrf_ip6_out_redirect() path, which does: skb_dst_drop(skb); // drops refcount on old dst -> call_rcu skb_dst_set(skb, dst); // installs VRF's own rt6 The old dst entry (the one `rt` still points to) now has refcount 0 and is queued for RCU-deferred destruction via dst_destroy_rcu(). After l3mdev_ip6_out() returns, rawv6_send_hdrinc() continues to use the stale `rt` pointer: IP6_INC_STATS(net, rt->rt6i_idev, ...); // UAF read err = NF_HOOK(..., rt->dst.dev, ...); // UAF read With expedited RCU grace periods (rcupdate.rcu_expedited=1) and CONFIG_PREEMPT=y, the RCU callback fires quickly enough to free the object before the stale accesses, producing a slab-use-after-free detectable by KASAN. Fix this by reloading `rt` from skb_dst(skb) after l3mdev_ip6_out() returns, so subsequent code uses the (possibly replaced) dst entry. Fixes: a8e3e1a9f020 ("net: l3mdev: Add hook in output path") Signed-off-by: Zhou Qiuyue --- net/ipv6/raw.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/net/ipv6/raw.c b/net/ipv6/raw.c index XXXXXXX..XXXXXXX 100644 --- a/net/ipv6/raw.c +++ b/net/ipv6/raw.c @@ -648,6 +648,13 @@ static int rawv6_send_hdrinc(struct sock *sk, struct msghdr *msg, int length, if (unlikely(!skb)) return 0; + /* + * l3mdev_ip6_out() may have replaced skb's dst via + * vrf_ip6_out_redirect(), in which case the original dst has been + * released and may be freed asynchronously. Reload rt from the + * current skb dst. + */ + rt = dst_rt6_info(skb_dst(skb)); + /* Acquire rcu_read_lock() in case we need to use rt->rt6i_idev * in the error path. Since skb has been freed, the dst could * have been queued for deletion. -- 2.43.0