TLDR: Four more Linux LPEs; two of the corruption bugs are reachable remotely under very specific circumstances, with one theoretically remote-groomable to remote root.
Background #
This will be shorter than usual because a) I’m under a time crunch and b) we are covering 4 vulnerabilities at once.
These were discovered by combining the graph-based tracking of security-relevant objects/properties used in CIFSwitch with the tooling to enable agents to think ‘geometrically’ about the memory state, as seen in OVSwrap. See those posts’ Background sections for more info.
The harness design is captured, in broad strokes, in Getting LLMs Drunk to Find Remote Linux Kernel OOB Writes (and More), though it’s evolved considerably since.
The vulnerabilities #
The vulnerabilities are DirtyAH6 (CVE-2026-80844), TUNderflow (CVE-2026-81000), PPPoEject (CVE-2026-68121), and DiagSpill (CVE-2026-74469).
The underlying bugs have been around for 10-21 years. The first three LPEs require unprivileged user namespaces*; DiagSpill does not. In some very specific circumstances, DirtyAH6’s and DiagSpill’s corruption bugs are remotely exploitable (see below for further details).
I reported these to security@kernel.org mid-July, with the fixes landing over the past weeks. Per the coordinated embargo with linux-distros@, we agreed to publish the writeup/PoCs on September 18, 6am UTC, so the affected system owners can prioritize and apply the patches. Huge thanks to the many maintainers involved in the patching/coordination process: Stefan Klassert, Xin Long, Paolo Abeni, Willem de Bruijn, Greg KH, and many others.
The basics #
Some background on the kernel subsystems involved in each of the vulnerabilities:
DirtyAH6: IPsec’s Authentication Header (AH) checks that packet data has
not changed. Linux implements its IPv6 side in AH6, using the kernel’s XFRM
code; before calculating or checking authentication data, AH6 changes some
IPv6 fields into the expected form, including addresses in a routing header.
TUNderflow: TUN and TAP are virtual network devices that move packets
between the kernel and userspace through /dev/net/tun. Network devices
built on top of other devices can pass down the receive headroom they need
through ndo_set_rx_headroom(), and Open vSwitch can carry that value from
another port to a TUN or TAP port.
PPPoEject: PPPoE carries PPP sessions in Ethernet frames. On send,
pppoe_sendmsg() builds an skb, copies in the payload, and asks the lower
network device to create its hardware header before filling in the PPPoE
header.
DiagSpill: An SCTP association can have many peer transports, one for each
peer address. sctp_diag reports SCTP socket and peer information through
sock_diag, building a Netlink reply with one sockaddr_storage for each
transport.
Vulnerability details #
DirtyAH6: ipv6_rearrange_rthdr() would get the number of addresses from
hdrlen, then use segments - segments_left to move an address pointer without
first checking that segments_left <= segments. So, a raw IPv6 HDRINCL packet with
hdrlen=2 and segments_left=255 moved the pointer back 4,064 bytes
and passed a 4,064-byte length to memmove(), causing an out-of-bounds access.
AH in transport mode,
the same bug can be turned into a remote crash/DoS; with memory grooming on the
target, I was also able to turn it into remote root in my lab. Achieving root with remote-only grooming looks extremely difficult, but it isn’t a priori impossible.
TUNderflow: tun_set_headroom() stored receive headroom directly in
tun->align, while tun_get_user() also used it to choose how much packet
data to keep in the head. A netkit device with 4,096 bytes of configured
headroom, under a VXLAN device and an Open vSwitch datapath, could pass 4,160
bytes to a raw TUN port. Then, SKB_MAX_HEAD(4160) would underflow. The negative
good_linear value became a huge positive size_t; prepad + linear and
len - linear then wrapped, and tun_alloc_skb() left skb->data 64 bytes
beyond its 4,096-byte allocation. The packet processing afterward would then read/write outside the skb head.
PPPoEject: pppoe_sendmsg() kept a pointer into the skb head across
dev_hard_header(), even though a device callback could call
pskb_expand_head() and free that head. Blocking the payload copy on FUSE
while adding the first GRE/IP6GRE port to an empty team or bonding
device triggered the reallocation, effectively ejecting the old skb head
while PPPoE still held a pointer into it. The subsequent header and length
writes used that stale pointer.
DiagSpill: An SCTP association can have 65,536 peer transports, but
transport_count is 16 bits – so the 65,536th transport wrapped it to 0.
sctp_diag then reserved no peer payload but copied the full list, spilling about 8 MiB past the end of the Netlink response.
ASCONF/ADD-IP are enabled with either SCTP-AUTH or
net.sctp.addip_noauth_enable=1 (all of these are disabled by default) – a
malicious peer adds enough transports, and something on the target (e.g., ss)
issues the sock_diag request that triggers the overwrite. I don’t see a path
to full remote root, even assuming perfect remote memory grooming.
The fixes #
DirtyAH6: Check segments_left before changing the header or the
address pointer, and pass -EINVAL through the existing AH6 error paths:
segments = rthdr->hdrlen >> 1;
if (segments_left > segments)
return -EINVAL;
rthdr->segments_left = 0;TUNderflow: Bound the headroom stored by TUN to the one-page
skb-head budget and the largest allowed 16-bit header offset, while leaving
enough data in the head for the raw-TUN protocol byte or a complete TAP
Ethernet header. Then make sure those bytes are present before using them:
max_headroom = min_t(size_t, SKB_MAX_HEAD(0), U16_MAX - 1);
if ((tun->flags & TUN_TYPE_MASK) == IFF_TAP)
max_headroom -= ETH_HLEN + NET_IP_ALIGN;
else
max_headroom -= 1;
tun->align = clamp_t(int, new_hr, NET_SKB_PAD, max_headroom);
...
case IFF_TUN:
if (tun->flags & IFF_NO_PI) {
u8 ip_version;
if (!pskb_may_pull(skb, 1)) {
err = -EINVAL;
goto drop;
}
ip_version = skb->data[0] >> 4;
...
}
...
break;
case IFF_TAP:
if (!pskb_may_pull(skb, ETH_HLEN)) {
err = -ENOMEM;
drop_reason = SKB_DROP_REASON_HDR_TRUNC;
goto drop;
}PPPoEject: Reload the PPPoE header through the skb’s
network-header offset after device header creation; have pskb_expand_head() update that offset when it moves the head:
dev_hard_header(skb, dev, ETH_P_PPP_SES,
po->pppoe_pa.remote, NULL, total_len);
ph = pppoe_hdr(skb);
memcpy(ph, &hdr, sizeof(struct pppoe_hdr));DiagSpill: Reject a new unique peer once
transport_count == U16_MAX. The check happens after looking for an
existing peer, so an address already on the list still returns its existing
transport at the limit:
if (asoc->peer.transport_count == U16_MAX)
return NULL;
peer = sctp_transport_new(asoc->base.net, addr, gfp);The corresponding changelogs for the first upstream-fixed releases are:
DirtyAH6
TUNderflow
PPPoEject
The exploits #
These are all memory bugs that require per-target grooming for to-root escalation, so the linked PoCs specify the distro/kernel/cpu/memory they’re tuned for (see the READMEs) – I’ve managed to repro across a range of distros with some tuning, but I’m releasing only the minimal couple-target-specific PoCs for simplicity.
While these are generally reliable on their targets, they may still corrupt the wrong memory, so run these in dedicated VMs/throwaway hosts only:
DirtyAH6: The PoC is a cousin of
DirtyFrag’s ESP variant: it corrupts skb_shared_info via a separate AH6 routing-header
memmove() OOB (not addressed by the DirtyFrag fix), then makes a later ESP
decrypt write into a file-backed fragment, replacing pam_rootok.so with
pam_permit.so, getting a root shell with su.
TUNderflow: The PoC arranges
file-backed pipe buffers next to the bad TUN packet. The Open vSwitch
out-of-bounds write then sets PIPE_BUF_FLAG_CAN_MERGE on one of them, after which a
pipe write replaces pam_rootok.so with pam_permit.so in /etc/pam.d/su, with su used again for the root shell.
PPPoEject: The PoC races populated
fd tables into the freed skb head and uses the PPPoE writes to
redirect one live entry to an attacker-built fake struct file. Closing that
fd calls a controlled kernel callback, installs root credentials, and
opens a root shell.
DiagSpill: The PoC grooms the
sock_diag overwrite into page tables, uses the corrupted page
tables to map host memory, finds and rewrites a credential object, installs
a sudoers rule, and opens a root shell.
Are you affected? + Mitigation #
The bug reachability pre-reqs #
The below are the pre-reqs for the actual bugs, not full LPE chains (which are captured in full in the above-linked PoCs’ READMEs). Nevertheless, it’s a good idea to treat your kernel as vulnerable if it’s in the list of vulnerable versions below and has the relevant subsystems available:
DirtyAH6: Reachable on the affected kernels as long as AH6/XFRM support is available on the affected kernels below; unprivileged user namespaces* are required for the specific LPE PoC above.
DirtyAH6 affected upstream kernels
| Kernel series | Affected releases | First fixed release |
|---|---|---|
| 2.6.12–5.9 | All releases | EOL; no upstream stable fix |
| 5.10.y | 5.10.0–5.10.268 | 5.10.269 |
| 5.11–5.14 | All releases | EOL; no upstream stable fix |
| 5.15.y | 5.15.0–5.15.219 | 5.15.220 |
| 5.16–6.0 | All releases | EOL; no upstream stable fix |
| 6.1.y | 6.1.0–6.1.186 | 6.1.187 |
| 6.2–6.5 | All releases | EOL; no upstream stable fix |
| 6.6.y | 6.6.0–6.6.155 | 6.6.156 |
| 6.7–6.11 | All releases | EOL; no upstream stable fix |
| 6.12.y | 6.12.0–6.12.107 | 6.12.108 |
| 6.13–6.17 | All releases | EOL; no upstream stable fix |
| 6.18.y | 6.18.0–6.18.48 | 6.18.49 |
| 6.19–7.0 | All releases | EOL; no upstream stable fix |
| 7.1.y | 7.1.0–7.1.12 | 7.1.13 |
| 7.2.y | 7.2.0–7.2.2 | 7.2.3 |
TUNderflow: Reachable on the affected kernels if unprivileged user namespaces are enabled*
and TUN/TAP support is available, with some network-device path that can
propagate oversized receive headroom to it.
TUNderflow affected upstream kernels
| Kernel series | Affected releases | First fixed release |
|---|---|---|
| 4.6–5.9 | All releases | EOL; no upstream stable fix |
| 5.10.y | 5.10.0–5.10.269 | 5.10.270 |
| 5.11–5.14 | All releases | EOL; no upstream stable fix |
| 5.15.y | 5.15.0–5.15.220 | 5.15.221 |
| 5.16–6.0 | All releases | EOL; no upstream stable fix |
| 6.1.y | 6.1.0–6.1.187 | 6.1.188 |
| 6.2–6.5 | All releases | EOL; no upstream stable fix |
| 6.6.y | 6.6.0–6.6.156 | 6.6.157 |
| 6.7–6.11 | All releases | EOL; no upstream stable fix |
| 6.12.y | 6.12.0–6.12.108 | 6.12.109 |
| 6.13–6.17 | All releases | EOL; no upstream stable fix |
| 6.18.y | 6.18.0–6.18.49 | 6.18.50 |
| 6.19–7.0 | All releases | EOL; no upstream stable fix |
| 7.1.y | 7.1.0–7.1.13 | EOL; no upstream stable fix |
| 7.2.y | 7.2.0–7.2.3 | 7.2.4 |
PPPoEject: Reachable on the affected kernels as long as unprivileged user namespaces are enabled*
and PPPoE support is available, with a lower-device header callback that can
reallocate the skb head during dev_hard_header().
PPPoEject affected upstream kernels
| Kernel series | Affected releases | First fixed release |
|---|---|---|
| 2.6.12–5.9 | All releases | EOL; no upstream stable fix |
| 5.10.y | 5.10.0–5.10.264 | 5.10.265 |
| 5.11–5.14 | All releases | EOL; no upstream stable fix |
| 5.15.y | 5.15.0–5.15.215 | 5.15.216 |
| 5.16–6.0 | All releases | EOL; no upstream stable fix |
| 6.1.y | 6.1.0–6.1.182 | 6.1.183 |
| 6.2–6.5 | All releases | EOL; no upstream stable fix |
| 6.6.y | 6.6.0–6.6.147 | 6.6.148 |
| 6.7–6.11 | All releases | EOL; no upstream stable fix |
| 6.12.y | 6.12.0–6.12.100 | 6.12.101 |
| 6.13–6.17 | All releases | EOL; no upstream stable fix |
| 6.18.y | 6.18.0–6.18.41 | 6.18.42 |
| 6.19–7.0 | All releases | EOL; no upstream stable fix |
| 7.1.y | 7.1.0–7.1.5 | 7.1.6 |
DiagSpill: Reachable on the affected kernels as long as SCTP and sctp_diag support is available (no unprivileged user namespace/CAP requirements).
DiagSpill affected upstream kernels
| Kernel series | Affected releases | First fixed release |
|---|---|---|
| 4.7–5.9 | All releases | EOL; no upstream stable fix |
| 5.10.y | 5.10.0–5.10.264 | 5.10.265 |
| 5.11–5.14 | All releases | EOL; no upstream stable fix |
| 5.15.y | 5.15.0–5.15.215 | 5.15.216 |
| 5.16–6.0 | All releases | EOL; no upstream stable fix |
| 6.1.y | 6.1.0–6.1.182 | 6.1.183 |
| 6.2–6.5 | All releases | EOL; no upstream stable fix |
| 6.6.y | 6.6.0–6.6.150 | 6.6.151 |
| 6.7–6.11 | All releases | EOL; no upstream stable fix |
| 6.12.y | 6.12.0–6.12.102 | 6.12.103 |
| 6.13–6.17 | All releases | EOL; no upstream stable fix |
| 6.18.y | 6.18.0–6.18.43 | 6.18.44 |
| 6.19–7.0 | All releases | EOL; no upstream stable fix |
| 7.1.y | 7.1.0–7.1.7 | 7.1.8 |
AppArmor (except for the Ubuntu cases of blocking unprivileged user namespaces themselves)/SELinux do not block the exploits in my testing.
* A mouthful of a note: the DirtyAH6 corruption trigger requires
CAP_NET_ADMIN and CAP_NET_RAW in the user namespace that owns the
attacker-controlled network ns. The TUNderflow and PPPoEject
corruption triggers require only CAP_NET_ADMIN in the same user ns.
In an appropriately-CAP'd container or a process in general,
these paths can corrupt the host kernel (possibly leading to a
container escape, etc.) with just the specified CAPs, without
creating new user namespaces. Unprivileged user namespaces are just the most common path.
And so while a container escape is theoretically possible with these primitives, just as with CIFSwitch (see a community example) and OVSwrap, I did not pursue that direction with the PoC.
Finally, note that for DiagSpill no special CAPs or unprivileged user namespaces are required at all -- so long as SCTP and sctp_diag are available, the bug can corrupt the host kernel.Mitigation #
Upgrade to a kernel containing all four fixes. The first stable releases with
all four are 5.10.270, 5.15.221, 6.1.188, 6.6.157, 6.12.109,
6.18.50, and 7.2.4.
If patching is not possible, disabling unprivileged user namespaces removes the ordinary-user path to
the first three (but doesn’t protect against appropriately-CAP’d containers/other processes) – DiagSpill remains reachable. You can also disable the relevant
functionality (if unused) to cut out the underlying bug: AH6, TUN, PPPoE, and SCTP/sctp_diag. I wouldn’t recommend disabling PoC-specific kmods only as a proper mitigation, as there could be other paths to root – as always, patching is much preferred.
Outro #
Given some personal/professional developments, this likely concludes the AI-assisted vulnerability hunting experiment – at least in its public form – for some while. Thanks for riding along!