xfrm: bpf_skb_set_xfrm_info()/xfrmi_xmit2() pass dst_orig through a shared percpu metadata_dst scratch -- permanent reference leak, double-consumption UAF and cross-packet if_id/link confusion

From: 徐晗翔 <hidden>
Date: 2026-10-29 02:54:45
Also in: bpf, lkml

bpf_skb_set_xfrm_info() parks the packet's original dst in a field of
a global percpu metadata_dst scratch (xfrm_bpf_md_dst) and
xfrmi_xmit2() later picks it up with a plain read-then-NULL sequence
-- but nothing enforces that only one skb per CPU is inside that
set-to-xmit interval: the only guard checks the *current* skb's dst,
not the scratch's occupancy, so a second packet setting on the same
CPU overwrites the previous dst_orig reference (leaked forever: the
scratch is never destroyed), and two concurrent xmit2 consumers can
both take the single parked reference (the refcount is then short one
holder, ending in a use-after-free), and the if_id/link values ride
the same scratch so packet A is looked up with packet B's XFRM policy
selectors.  Verified on a pristine Linux 7.3 KASAN kernel against the
native object layer (real percpu metadata_dst, real dst_entry/rcuref,
real skb): the leak variant hits at 0.48% per cycle in a natural
two-thread same-CPU run (1,926 leaks in 400,000 cycles, confirmed
independently by the kernel's own "Objects remaining on
__kmem_cache_shutdown" at module exit) and 1,015 cross-packet route
confusions; the double-consumption variant, driven deterministically,
produces the predicted rcuref "imbalanced put()" warning and then a
KASAN use-after-free read whose free stack is dst_destroy from
rcu_core -- i.e. the first skb's release destroyed the dst while the
second skb was still using it.

The scratch pattern assumes "the CPU is processing at most one such
packet", which qdisc queueing and redirect immediately break: packets
complete set on CPU0 and execute xmit on other CPUs (or later, after
preemption), while all of them share the same scratch copy selected
at set time.  Triggering needs a TC cls program calling
bpf_skb_set_xfrm_info() and redirecting to a collect_md xfrm
interface (root / CAP_BPF + CAP_NET_ADMIN to install); no race with
any administrative action is required, and the sending rate and queue
depth are fully attacker-controlled.  Fix direction: stop passing
dst_orig through the shared scratch -- allocate a per-packet
metadata_dst in bpf_skb_set_xfrm_info() (a minimal alternative is
xchg()-based hand-off on both sides); the sibling kfunc
bpf_skb_set_tunnel_key() has the same shared-scratch shape for its
tun_info and deserves the same treatment.

The detailed description:

* Test environment / affected version
  Linux version 7.3.0 (gcc 13.3.0, GNU ld 2.42) #1 SMP PREEMPT_DYNAMIC
  -- vanilla mainline 7.3 release tarball, KASAN enabled, QEMU/KVM
  x86_64 (8 vCPU).  Fidelity note, stated plainly: the object layer
  is fully native (the real percpu metadata_dst allocator, real
  dst_entry objects with real rcuref, real skbs); because the test
  kernel has CONFIG_BPF_SYSCALL/CONFIG_XFRM_INTERFACE off, the two
  call sites themselves cannot execute in the VM, so the attached
  module replicates the exact statement sequences of
  bpf_skb_set_xfrm_info() (xfrm_interface_bpf.c:64-94) and the
  collect_md branch of xfrmi_xmit2() (xfrm_interface_core.c:434-455)
  line-by-line against those native objects.
* The two sites
  net/xfrm/xfrm_interface_bpf.c:64-94 (writer, TC cls kfunc):
      if (unlikely(skb_metadata_dst(skb)))   /* :72-73 guards only the
                                                CURRENT skb */
              return -EINVAL;
      md_dst = this_cpu_ptr(xfrm_bpf_md_dst);   /* :80 shared scratch
                                                   (net/core/filter.c:
                                                   6249, never freed) */
      info = &md_dst->u.xfrm_info;
      info->if_id = from->if_id;             /* :84 */
      info->link = from->link;               /* :85 */
      skb_dst_force(skb);                    /* :85 real reference on
                                                a NOREF dst */
      info->dst_orig = skb_dst(skb);         /* :87 unconditional
                                                overwrite: previous
                                                parked ref LEAKS */
      dst_hold(md_dst); skb_dst_set(skb, md_dst);  /* :89-90 */
  net/xfrm/xfrm_interface_core.c:434-455 (reader, collect_md xmit):
      if (md_info->dst_orig) {               /* read */
              dst = md_info->dst_orig;       /* take */
              skb_dst_set(skb, dst);
              md_info->dst_orig = NULL;      /* clear -- the three
                                                steps are not atomic
                                                and the scratch copy
                                                is shared by every
                                                skb that set on this
                                                CPU */
              dst_release(tmp_dst);
      }

* Failure modes and how they played out in verification
  1. Overwrite leak (interleaving: A sets, B sets on the same CPU
     before A's xmit): A's dst_orig reference is overwritten and
     never released.  Natural run: leaked=1926 of 400,000 cycles
     (0.48%); each leak pins a route/device/neighbour; the kernel's
     own cache shutdown check independently reported the surviving
     objects.  The scratch is a global percpu that is never
     destroyed, so metadata_dst_free()'s release path is unreachable
     -- the leak is permanent.
  2. Double consumption (A and B both parked/queued, their xmit2
     consumers run concurrently): both pass the read test before
     either NULLs; one real reference is now claimed by two skbs.
     Deterministic run: refs_held shows two claims on one reference;
     after the first skb releases, the dst is destroyed
     (dead=1); the second skb's release trips
     "rcuref - imbalanced put()" (lib/rcuref.c:266 -- the exact
     WARN the rcuref design predicts), and touching the object
     afterwards gives
        BUG: KASAN: slab-use-after-free (Read)
        freed by: kmem_cache_free <- dst_destroy <- rcu_core
     i.e. the second skb used the dst across its destruction.
     (rcuref correctly prevents the double free; it cannot protect
     the use side.)
  3. Cross-packet policy confusion: if_id/link share the scratch and
     are overwritten along with dst_orig, and xfrmi_xmit2 performs
     xfrm_lookup_with_ifid() with md_info->if_id -- packet A is
     routed and policy-matched with packet B's if_id/link
     (cross-interface / cross-policy-domain decision).  Natural run:
     confused(other R)=1015 hits (packet received another packet's
     route dst).

* Reproducer (attached)
  flowrace.c, external module, action=casec_ctl (deterministic
  leak + double-consumption phases, prints the refcounts at each
  step) and action=casec_nat (two kthreads bound to the same CPU,
  400k cycles, random 0-40us set->xmit spread standing in for qdisc
  queueing; prints leak/confusion counters; module exit lets the
  kernel's shutdown check confirm the leaks).  Baseline action
  flowsanity: clean.  (The module also carries the flow_indr A/B
  verification actions; they are inert unless selected.)
  insmod flowrace.ko action=casec_ctl
  insmod flowrace.ko action=casec_nat

Regards,
Hanxiang Xu
xuhx@hust.edu.cn

Attachments

Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help