5 분 소요

Summary

I found and fixed a Linux kernel XFRM policy use-after-free now tracked as CVE-2026-53239. The upstream fix is commit 7f2d76c9c032, titled xfrm: policy: fix use-after-free on inexact bin in xfrm_policy_bysel_ctx().

XFRM is the Linux kernel framework behind IPsec transformations. The XFRM policy database decides which flows should be transformed, passed, or blocked; the kernel comments describe struct xfrm_policy as an SPD entry and struct xfrm_state as a security association. This bug was not in the cryptographic transform itself. It was in policy database bookkeeping.

The vulnerable function, xfrm_policy_bysel_ctx(), is the selector-based policy lookup path used by XFRM netlink operations such as policy deletion. Given a selector, direction, type, mark, and optional security context, it finds the matching policy. When called for deletion, it also unlinks the selected policy and prunes now-empty inexact policy bins.

The bug was in net/xfrm/xfrm_policy.c. xfrm_policy_bysel_ctx() could keep a raw pointer to an inexact policy bin after dropping xfrm_policy_lock; a concurrent XFRM hash rebuild could free that bin before the original path later pruned it.

The bug is scored as CVSS 7.8 (High) by the kernel.org CNA.

Root cause

The vulnerable path was a race between policy deletion and policy hash rebuilding.

On the deletion side, xfrm_policy_bysel_ctx() looked up an inexact-bin pointer while holding xfrm_policy_lock, unlinked the selected policy, dropped the lock, killed the policy, and then called xfrm_policy_inexact_prune_bin() with the saved bin pointer.

On the rebuild side, xfrm_hash_rebuild() could take the same lock and flush inexact bins. That flush path could release the bin with kfree_rcu(). If this happened during the unlocked window, the deletion path later used a stale bin pointer.

In short:

CPU0: XFRM_MSG_DELPOLICY              CPU1: XFRM_MSG_NEWSPDINFO
--------------------------------      --------------------------------
xfrm_policy_bysel_ctx()
  lock xfrm_policy_lock
  bin = xfrm_policy_inexact_lookup()
  __xfrm_policy_unlink(pol)
  unlock xfrm_policy_lock
  xfrm_policy_kill(ret)
                                      xfrm_hash_rebuild()
                                        lock xfrm_policy_lock
                                        __xfrm_policy_inexact_flush()
                                          kfree_rcu(bin)
                                        unlock xfrm_policy_lock
  xfrm_policy_inexact_prune_bin(bin)

The last line is the stale access.

Evidence

My local evidence was a KASAN-confirmed lifetime violation on a debug kernel, with timing adjusted to force the RCU grace period to complete before the stale access. The report showed a slab use-after-free in xfrm_policy_bysel_ctx(), reached from xfrm_get_policy() and xfrm_user_rcv_msg().

The short excerpt below is from the local KASAN report:

BUG: KASAN: slab-use-after-free in xfrm_policy_bysel_ctx.cold+0x59/0xb8
Read of size 8 at addr ffff8881153c4700 by task repro_xfrm/387

Call Trace:
  xfrm_policy_bysel_ctx.cold+0x59/0xb8
  xfrm_get_policy+0x7df/0xc00
  xfrm_user_rcv_msg+0x41b/0x950
  netlink_rcv_skb+0x16d/0x420
  xfrm_netlink_rcv+0x76/0x90

That caveat matters: the bug shape is real, but the local reproducer used debug timing to make the race observable. I do not claim a public exploit from this evidence.

Fix

The fix keeps the bin prune inside the original critical section. Instead of saving the bin pointer, dropping the lock, and then re-acquiring the lock through xfrm_policy_inexact_prune_bin(), the patch calls __xfrm_policy_inexact_prune_bin() while xfrm_policy_lock is still held.

Conceptually, the change is:

if (bin && delete)
        __xfrm_policy_inexact_prune_bin(bin, false);
spin_unlock_bh(&net->xfrm.xfrm_policy_lock);

if (ret && delete)
        xfrm_policy_kill(ret);

The old wrapper xfrm_policy_inexact_prune_bin() became unused and was removed.

Exploit

After the sanitizer proof, I ran a separate exploitability pass on a vulnerable non-KASAN Linux 6.12.91 kernel in a QEMU snapshot VM.

The attack surface I tested was local XFRM netlink. The receiver checks CAP_NET_ADMIN for XFRM messages; in this context that means the capability to administer networking objects in the relevant network namespace. The trigger was reachable from uid/gid 65534 by entering a user and network namespace and using the namespace-local CAP_NET_ADMIN. I did not identify a remote attack path for this bug.

The first run exercised the original race for 120 seconds with 8 policy-hash rebuild workers and 8 policy add/delete workers. XFRM netlink remained operational, the guest stayed alive, and post-run dmesg and serial logs contained no Oops, panic, warning, RCU stall, or general protection fault. No privilege transition or namespace escape occurred.

I then tested whether the stale pointer could be made more useful by extending the natural window and increasing replacement pressure. One run created traffic-backed policy teardown with an unresolved XFRM template and veth traffic; it completed 65,671 add/delete iterations and 16,811,776 UDP sends over 120 seconds without a kernel fault. A second run added same-type XFRM inexact-bin churn so that replacement pressure targeted the same unaccounted kmalloc-96 cache as the freed xfrm_pol_inexact_bin; it completed 21,364 victim iterations, 10,938,368 UDP sends, and 4,948,445 same-type add/delete operations over 180 seconds, again with an empty post-run dmesg.

The object I was trying to replace is allocated in xfrm_policy_inexact_alloc_bin():

struct xfrm_pol_inexact_bin {
        struct xfrm_pol_inexact_key k;
        struct rhash_head head;
        struct hlist_head hhead;
        seqcount_spinlock_t count;
        struct rb_root root_d;
        struct rb_root root_s;
        struct list_head inexact_bins;
        struct rcu_head rcu;
};

bin = kzalloc(sizeof(*bin), GFP_ATOMIC);

GFP_ATOMIC is the non-sleeping allocation mode used when the caller cannot block, for example in bottom-half or interrupt-like contexts. That detail matters for replacement: it places the object on the normal, unaccounted kmalloc path, not the accounted path commonly used for some user-triggered heap sprays.

The layout and runtime cache placement were:

$ pahole -C xfrm_pol_inexact_bin vmlinux-exploit
/* size: 88, cachelines: 2, members: 8 */

$ cat /sys/kernel/slab/kmalloc-96/object_size
96

The main blocker was primitive quality. A useful replacement has to survive the invariants of an XFRM inexact bin: the namespace key, rhashtable node, list node, RB roots, sequence counter, and RCU callback storage all sit in the object that the stale prune path will later treat as live. If the slot is replaced by arbitrary bytes, the likely outcome is an invalid pointer path or a list/rhashtable consistency failure, not a clean credential overwrite. If the slot is replaced by another XFRM bin, the replacement is structurally valid but still not a convenient arbitrary read/write or control-flow object.

The generic sprays I checked were also a poor fit. For example, message-style sprays use accounted allocation paths, while the XFRM bin was in the unaccounted kmalloc-96 cache in my guest. In the same-type replacement run, kmalloc-96 activity increased while kmalloc-cg-96 stayed flat, but the run still produced no crash or privilege effect.

These experiments did not produce LPE, RCE, container escape, a reliable crash, or a controlled read/write primitive. The result is therefore a conservative exploitability finding: the bug is a confirmed UAF with namespace-local reachability, but I do not claim a practical exploit from the evidence I have.

Timeline

  • 2026-05-28: I recorded the candidate as an XFRM policy inexact-bin UAF.
  • 2026-06-09: The v2 patch was tracked locally as accepted/applied to the XFRM/IPsec tree.
  • 2026-06-25: CVE-2026-53239 was published by kernel.org/NVD.
  • 2026-06-28: The CVE record was updated with the CNA CVSS v3.1 score.
  • 2026-07-01: I published this note.

Mitigation

Upgrade to a kernel that includes the upstream or stable fix for CVE-2026-53239. The public CVE record references commit 7f2d76c9c03257c0782afef9d95321fa04096f60 and several stable backports.

References