← All notes

Our own researchAct

Ask the list, not the field: our use-after-free in the Linux kernel

Fuzzing found a use-after-free in the code that holds IPsec security associations. The patch is ours and it is now in every maintained stable branch. The crash is the least interesting part. The question the code was asking is the interesting part.

Published
Topics
Linux kernel · CVE · fuzzing · use-after-free · disclosure
0xfrm_state crashes in 7.1 million fuzzing executions once the patch was applied

This one is ours. A fuzzing run against the code that holds IPsec security associations turned up a use-after-free, we wrote the patch, and it now ships in every maintained stable branch of Linux. We are writing it up because the shape of the mistake is more useful than the crash.

The question the code was asking

When the kernel deletes an IPsec state, it has to take that object off four hash lists. For two of them, the code decided whether to unhash by looking at a value: does the state have a sequence number, does it have an SPI. That is a proxy for the question that actually matters, which is whether the object is on the list.

Most of the time the proxy and the truth agree. Under concurrency they can drift apart, and when they do, the delete path either skips an unhash it needed or performs one it should not have. A second delete of the same object then writes through a pointer that was poisoned the first time. The same function already asked the right question for the two lists added more recently. The older ones were never brought into line.

Before and after, the two guarded lists
before:
if (x->km.seq)
        hlist_del_rcu(&x->byseq);
if (x->id.spi)
        hlist_del_rcu(&x->byspi);

after:
if (!hlist_unhashed(&x->byseq))
        hlist_del_init_rcu(&x->byseq);
if (!hlist_unhashed(&x->byspi))
        hlist_del_init_rcu(&x->byspi);

Two changes, and they do different jobs. Asking the list instead of the field makes the decision track reality. Re-initialising the node on delete makes a second delete a harmless no-op rather than a write through poison.

A field is a claim about state

This is the same failure we keep writing about in agent systems, one layer down. A field that says the object has an SPI is a claim about where the object is. The list is the fact. Code that trusts the claim works until something changes one without the other, and concurrency is exactly that something.

An agent that trusts a tool's description instead of what the tool does, a permission check that trusts a label instead of the resource, a delete path that trusts a scalar instead of the list: different layers, one habit. Trust is the attack surface, including the trust a function places in its own bookkeeping.

What the patch deliberately does not do

It does not chase down the single interleaving that frees the object twice. It makes the delete idempotent and the guard truthful, so the consequence goes away whichever path gets there first. The maintainers accepted it on those terms. A fix for the one race we happened to catch would have been narrower and would have left the next one in place.

The part that never makes it into a CVE

The first version did not apply. It went out through a web mail client, which stripped every tab from the diff, and it had no Fixes: tag. The maintainer asked for the tag, the stable maintainer replied with the form letter pointing at the stable rules, and the resend through git send-email was applied the next morning.

That tag is not paperwork. It is the mechanism that carries a fix into older trees, and without it this patch would have landed in mainline and stopped there. Every downstream fix in the ledger exists because one line named the commits that introduced the bug.

Where a kernel bug ends up

A crash found in cloud virtual machines now appears in an industrial controller advisory and in long-term enterprise kernels that most people assume are frozen. That is the practical point for anyone buying hardware: your exposure is not your own kernel version. It is every appliance running Linux that you did not build, patched on its vendor's schedule rather than upstream's.

What to actually do

  • Move to the first fixed release of your stable branch. The ledger lists them. Long-term branches got the fix weeks after mainline, so check rather than assume.
  • If you cannot patch yet, look at unprivileged user namespaces. The low-privilege rating rests on them. Hosts that do not need them lose that path.
  • Ask your appliance vendors where they stand. Controllers, firewalls and storage boxes run kernels you never see. One industrial vendor has listed this as affected with no fixed firmware yet.
  • If you review kernel or systems code, look for proxy guards. Any condition that tests a value as a stand-in for membership, ownership or state is worth replacing with a test of the thing itself.

E/01What is confirmed
  • CVE-2026-46116, assigned by the Linux kernel CNA and published 28 May 2026. Fix commit 14acf9652e56, "xfrm: defensively unhash xfrm_state lists in __xfrm_state_delete", written, reported and tested by Michał Kosiorek of ArkLink. Applied to the ipsec tree by its maintainer on 30 April 2026.
  • Found with syzkaller (746545b8) against Linux 6.12.47 built with KASAN and KCOV, on 32 QEMU/KVM guests on AWS c5.metal. Nine unique crash signatures in about nine hours, all inside the xfrm_state lifecycle. The main one is an 8-byte slab-use-after-free write in __xfrm_state_delete, reached from network namespace cleanup.
  • Patch size: 6 insertions and 6 deletions in net/xfrm/xfrm_state.c. Re-running the same harness on the patched kernel for 1 h 16 min gave 7.1 million executions across 32 guests and zero xfrm_state use-after-free or out-of-bounds hits, with the xfrm_state slab still being allocated and freed.
  • Fixed releases: mainline 7.1-rc3 (10 May 2026); stable 7.0.7, 6.18.30 and 6.12.88 (14 May); 6.6.140 (17 May); 6.1.176 and 5.15.210 (19 June); 5.10.261 (24 July). The patch failed to apply automatically to 6.6 and older on 12 May and was backported by hand.
  • The CVE record marks every kernel from 2.6.19 up to those releases as affected, following the patch's two Fixes: tags.
  • Kernel CNA score: CVSS 3.1 base 7.8, AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H. Its stated reason for PR:L is that xfrm checks CAP_NET_ADMIN against the network namespace's user namespace, which an unprivileged user can obtain with unshare -Urn. Red Hat scores it 7.0 with AC:H and rates it Important. NVD status on 7 October 2026: Undergoing Analysis.
  • Three weakness labels for one bug: CWE-416 from NVD, CWE-763 from Red Hat, CWE-20 from Siemens.
  • Downstream: the NVD record lists 23 Red Hat errata, including the realtime kernel for RHEL 7 Extended Lifecycle Support. Siemens SSA-019113 V1.1 (8 September 2026) lists the GNU/Linux subsystem of the SIMATIC S7-1500 CPU 1518(F)-4 PN/DP MFP firmware V3.1.6 as affected, in an advisory covering 432 CVEs.
E/02What we do not yet know
  • Whether it is exploitable beyond a crash. We did not build an exploit. The kernel CNA's C:H and I:H describe what the primitive could allow, not something that has been demonstrated, and Red Hat's AC:H disagrees on how reliable the race is.
  • Whether the race is reachable on every kernel the record marks affected. The range follows the Fixes: tags. We reproduced on 6.12.47 only, and confirmed the same code path on mainline and the ipsec tree.
  • No minimal standalone reproducer is published. The evidence is the fuzzing harness and the before and after crash counts.
  • Neither score accounts for hosts that disable unprivileged user namespaces, where the PR:L reasoning does not hold. We have not measured how common that configuration is.
  • Siemens lists no fixed firmware for this CVE in the advisory and gives no date for one.