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
- flowrace.c [text/plain] 25330 bytes · preview