core-jmp core-jmpdeath of core jump

A Quartet of Linux Local Root Vulns: DirtyAH6, TUNderflow, PPPoEject, and DiagSpill

Asim Manizada’s four Linux LPEs: DirtyAH6, TUNderflow, PPPoEject, DiagSpill. Decade-old bugs, userns for three, DiagSpill needs only SCTP. Patch 5.10.270 / 6.6.157 / 6.12.109 and cousins. PoCs on GitHub, VM-only.

oxfemale September 21, 2026 34 min read 48 reads
Export PDF
A Quartet of Linux Local Root Vulns: DirtyAH6, TUNderflow, PPPoEject, and DiagSpill
Original text: "A quartet of Linux local root vulns: DirtyAH6, PPPoEject, TUNderflow, and DiagSpill"Asim Viladi Oglu Manizada, heyitsas.im (18 September 2026). Fix snippets, version tables, and changelog lists follow the source. Full LPE PoCs stay on GitHub; this draft does not re-host them. Lab VMs only.
Featured art for the LPE quartet post
Original featured image (1920×1080). Source: original article.

Executive Summary

On 18 September 2026 Asim Manizada published four Linux local-root bugs found with the same agentic graph/geometry harness behind CIFSwitch, OVSwrap, and the “drunk LLM” kernel work. DirtyAH6 (CVE-2026-80844), TUNderflow (CVE-2026-81000), PPPoEject (CVE-2026-68121), and DiagSpill (CVE-2026-74469). The underlying mistakes are 10–21 years old. The first three LPEs want unprivileged user namespaces (or equivalent CAP_NET_ADMIN in a userns-owned netns). DiagSpill does not. DirtyAH6’s and DiagSpill’s corruption can be remote in very specific configs; DirtyAH6 was remote-rooted in lab with on-target grooming; fully remote grooming looks extremely hard. DiagSpill is a remote crash/DoS if SCTP ADD-IP is on, not a remote-root path he can see.

He reported the set to security@kernel.org in mid-July 2026. The coordinated embargo with linux-distros@ lifted at 06:00 UTC on 18 September. The first stable kernels that contain all four fixes are 5.10.270, 5.15.221, 6.1.188, 6.6.157, 6.12.109, 6.18.50, and 7.2.4. AppArmor and SELinux did not block the LPEs in his testing, except Ubuntu cases that already block unprivileged userns. This page keeps every original table, fix snippet, and changelog list, then adds kitchen-table walkthroughs, operator hunting notes, CWE/ATT&CK mapping, and patch-math so a desktop owner and a distro kernel person can both finish the article.

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.

Asim Manizada, 18 September 2026
Kitchen table: Think of four old house keys that still open the basement. Three of them only work if the building still lets tenants mint extra hall passes (unprivileged user namespaces). The fourth works as soon as the SCTP intercom is plugged in. Two keys can also be rattled from the street if someone left a weird lock on the front door (IPv6 AH on a router; SCTP ADD-IP). Patch the locks. Do not run the GitHub demos on a laptop you like.
For operators: Treat “we applied last week’s stable” as insufficient. DirtyAH6, TUNderflow, PPPoEject, and DiagSpill landed on four different Tuesdays per series. A 6.6.148 host has PPPoEject closed and still has DirtyAH6 and TUNderflow open. The all-four floor is the list in Mitigation. PoCs are target-specific (distro/kernel/CPU/RAM in each README) and unsafe on a shared host.

How to read this page

The original write-up is short on purpose: four bugs, one embargo morning, a time crunch. That is a gift if you already live in AH6 and skb headroom, and a brick wall if you do not. Green boxes translate the same facts into a house, a mailman, a cardboard box, and a guest list. Blue boxes keep the operator verbs: which kmod, which cap, which sysctl, which stable. Original tables and C snippets sit at the same reading positions as on heyitsas.im. Exploit repositories are linked, not copied.

  • If you run a laptop or a homelab: jump to Are you affected?, then Mitigation. You want the all-four floor, not a single CVE blog post.
  • If you run a fleet: inventory AH6/XFRM, TUN/TAP, PPPoE, SCTP/sctp_diag, unprivileged userns, and SCTP ADD-IP. Then patch math.
  • If you hunt kernel bugs: the interesting claim is not “LLM found four CVEs.” It is that graph tracking of security-relevant objects plus geometric memory-state tooling hit four old, independently exploitable primitives in four subsystems.
  • If you write detections: do not grep for GitHub repo names. Hunt patch level, kconfig, and the rare remote preconditions (transport-mode IPv6 AH; SCTP ADD-IP).
Four-column technical map of the LPE quartet
AH6 memmove, TUN underflow, PPPoE use-after-free, SCTP 16-bit wrap. Four different CWE families, one disclosure date.

Background

Shorter than his usual posts: time crunch, four bugs at once. Discovery stacked graph tracking of security-relevant objects (CIFSwitch) with geometric memory-state tooling for agents (OVSwrap). Harness DNA is in Getting LLMs Drunk to Find Remote Linux Kernel OOB Writes, evolved since. This is the public end of that experiment for a while — see the outro.

The age of the bugs matters more than the tooling slogan. DirtyAH6’s affected range starts at 2.6.12. PPPoEject starts at 2.6.12. TUNderflow starts at 4.6. DiagSpill starts at 4.7. Those are not “new attack surfaces from 2025.” They are decades of IPv6 IPsec, virtual NICs, DSL-era PPPoE, and SCTP diagnostics that nobody asked a fuzzer to think about geometrically. When a 16-bit counter and a Netlink dump have lived next to each other since 4.7, the interesting question is not “why now,” it is “why did every other harness miss the wrap.”

The embargo shape is the usual kernel.org one: report to security@kernel.org, fixes land in mainline and stables over weeks, linux-distros@ gets a coordinated date, public write-up and PoCs drop together. Maintainers called out in the original: Stefan Klassert, Xin Long, Paolo Abeni, Willem de Bruijn, Greg KH, and others. If you ship a distro kernel, those names are your changelog trail, not a press list.

Kitchen table: Someone spent a summer teaching a very stubborn intern to follow objects around a warehouse and to think about how boxes sit in space. The intern found four old padlocks. The locksmiths (maintainers) cut new keys. The public post is the moment the building manager is allowed to talk about it.
For operators: Related public work from the same author: CIFSwitch and OVSwrap established the graph+geometry method; the drunk-LLM post is the harness sketch. A community container-escape write-up on CIFSwitch-class primitives is at raesene.github.io. He did not PoC container escape on this quartet, but the CAP_NET_ADMIN note is the same shape.

The four bugs

NameCVEClassUserns for LPE?Remote?
DirtyAH6CVE-2026-80844AH6 rthdr memmove OOBYes (CAP_NET_ADMIN+RAW in userns)Crash/DoS if AH transport on IPv6 router; lab remote-root with on-target groom
TUNderflowCVE-2026-81000TUN align underflow → skb OOBYes (CAP_NET_ADMIN)Local (needs a headroom-propagating device path)
PPPoEjectCVE-2026-68121UAF after pskb_expand_headYes (CAP_NET_ADMIN)Local (FUSE + GRE/team race)
DiagSpillCVE-2026-74469u16 transport_count wrap, ~8 MiB Netlink spillNoDoS if ADD-IP + AUTH/noauth; no remote-root path seen
Reachability of the bug, not the full published LPE chain. Source: original article, condensed.

Two facts are easy to mash together and should stay apart. The bug is the memory mistake: an OOB memmove, an underflowed skb head, a stale pointer, a wrapped counter. The LPE is a groomed, target-specific chain that turns that mistake into root. The tables later in this page are about the bugs. The GitHub READMEs are about the LPEs. A kernel can be vulnerable to the bug without matching the PoC’s RAM map.

The basics

DirtyAH6: IPsec Authentication Header proves packet bytes did not change. Linux IPv6 AH lives in AH6 on XFRM. Before computing or checking auth data it rewrites IPv6 fields into the expected form, including addresses in a routing header. That rearrange is the whole plot: the kernel is supposed to shuffle labels and then weigh the wax seal. If the shuffle walks off the packet, the seal is being computed on the wrong memory.

TUNderflow: TUN/TAP move packets between kernel and userspace via /dev/net/tun. Devices stacked on others pass receive headroom through ndo_set_rx_headroom(); Open vSwitch can carry that value onto a TUN/TAP port. Headroom is “how much empty cardboard to leave in front of the packet so later headers fit.” If the asked-for empty space is larger than the box, the allocator’s math wraps.

PPPoEject: PPPoE carries PPP in Ethernet. pppoe_sendmsg() builds an skb, copies payload, asks the lower device to create its hardware header, then fills the PPPoE header. The bug is a classic “I kept a sticky note on a box that the warehouse already recycled.”

DiagSpill: An SCTP association can have many peer transports (one per peer address). sctp_diag reports sockets and peers through sock_diag, stuffing a sockaddr_storage per transport into a Netlink reply. The guest list is 16 bits. The banquet hall is not.

A mailman rearranging address stickers before checking the seal
DirtyAH6: rearrange the routing-header addresses, then authenticate. The rearrange walked off the packet.
A virtual cable overflowing a too-small box
TUNderflow: 4,160 bytes of asked-for headroom into a 4,096-byte skb head.

Vulnerability details

DirtyAH6 — CVE-2026-80844

ipv6_rearrange_rthdr() took the address count from hdrlen, then used segments - segments_left to move an address pointer without checking segments_left <= segments. A raw IPv6 HDRINCL packet with hdrlen=2 and segments_left=255 walked the pointer back 4,064 bytes and passed 4,064 to memmove() — out of bounds. If the target is an IPv6 router or gateway adding AH in transport mode, the same bug is a remote crash/DoS. With memory grooming on the target he got remote root in lab. Achieving root with remote-only grooming looks extremely difficult, not a priori impossible.

Kitchen table: The clerk is allowed to reorder the address stickers on a package before stamping the wax seal. Nobody checked that the “stickers remaining” field was smaller than the number of stickers on the envelope. A forged packet says “I have two stickers, please skip 255.” The clerk walks four thousand bytes off the table and copies whatever was on the floor into the seal check. On a laptop you need a hall pass to be that clerk. On a router that already stamps AH for other people, the forged packet can arrive from the street.
For operators: CWE-787 / CWE-20. Primitive: attacker-controlled memmove length and destination derived from an unchecked routing-header field. Local trigger wants CAP_NET_ADMIN and CAP_NET_RAW in the userns that owns the attacker netns, plus AH6/XFRM. Remote crash clause: IPv6 gateway, AH in transport mode. The DirtyFrag ESP fix does not close this path — different OOB, same family of IPsec packet munging. Commit 7bad4bda74dc.

The 4,064-byte figure is not a magic constant from a fuzzer log. hdrlen=2 means two 32-bit units after the first 8 bytes of the extension header, so the legitimate address count is small. segments_left=255 is a one-byte field at its maximum. The pointer walks (segments - 255) addresses backwards, which under unsigned or poorly-checked arithmetic becomes a large positive byte count into memmove. The original post’s number is the one to cite; the lesson for audit is “any length derived from a - b on packet fields needs b <= a before the subtract.”

TUNderflow — CVE-2026-81000

tun_set_headroom() stored receive headroom in tun->align; tun_get_user() used it to decide how much packet to keep in the head. A netkit device with 4,096 bytes of headroom, under VXLAN and an OVS datapath, could pass 4,160 to a raw TUN port. SKB_MAX_HEAD(4160) underflowed. Negative good_linear became a huge size_t; prepad+linear and len-linear wrapped; tun_alloc_skb() left skb->data 64 bytes past a 4,096-byte allocation. Later processing read and wrote outside the skb head.

A tape measure wrapping past zero next to an overstuffed sleeve
Integer underflow is a tape measure that goes past zero and suddenly claims the room is four gigabytes long.
skb head with data pointer sitting past the allocation
After the wrap, skb->data sits 64 bytes beyond a 4,096-byte head. Subsequent TUN/OVS processing is an OOB read/write.
Kitchen table: You asked the warehouse for a box with 4,160 bytes of empty space in front of the gift. The box factory only makes 4,096-byte boxes. The clerk subtracted “space wanted” from “space available,” went below zero, and the unsigned clipboard turned that into a huge order. The gift was then placed so that its wrapping stuck 64 bytes into the next crate on the shelf.
For operators: CWE-191 → CWE-787. The interesting dependency is not TUN itself (every desktop has it) but a device path that can propagate oversized receive headroom: the published trigger uses netkit with 4,096 bytes of headroom, VXLAN, and Open vSwitch onto a raw TUN port, producing 4,160. Local LPE wants unprivileged userns plus CAP_NET_ADMIN. Commit 447c9303942c.

Why 4,160 versus 4,096? Headroom is additive along a stack. A netkit port already configured for a page of headroom, plus VXLAN’s extra header budget, plus whatever OVS copies through ndo_set_rx_headroom(), can exceed the one-page skb-head budget that SKB_MAX_HEAD() assumes. The macro does not clamp; it subtracts. Underflow of a signed “how much linear room is left” value, then a cast to size_t, is an old Linux skb class. The fix clamps stored TUN headroom to min(SKB_MAX_HEAD(0), U16_MAX-1) minus TAP Ethernet overhead or the raw-TUN protocol byte, and pskb_may_pulls before reading the version nibble or Ethernet header.

PPPoEject — CVE-2026-68121

pppoe_sendmsg() kept a pointer into the skb head across dev_hard_header(), even though a device callback can pskb_expand_head() and free that head. Blocking the payload copy on FUSE while adding the first GRE or IP6GRE port to an empty team or bonding device triggered the realloc — ejecting the old head while PPPoE still pointed into it. Header and length writes used the stale pointer.

An old shipping box emptied while a hand still holds it
PPPoEject: pskb_expand_head() poured the packet into a new box and threw the old one. PPPoE still held the old handle.
Kitchen table: You wrote the return address on a cardboard box, then asked the loading dock to slap on a bigger shipping label. The dock decided the box was too small, poured the contents into a new box, and tossed the old cardboard into the recycling. You, still holding a pen on the recycled flap, wrote the PPPoE header into someone else’s memory. The race is: freeze the payload copy on a FUSE filesystem so the dock has time to change the box, then add the first GRE port to an empty team so the header callback actually reallocates.
For operators: CWE-416. Stale pointer across a call that is documented to be allowed to reallocate. The correct pattern is already in the tree in a dozen other protocols: store a header offset (skb_network_header), let pskb_expand_head() update offsets, reload with pppoe_hdr(skb) after dev_hard_header(). Local LPE wants userns + CAP_NET_ADMIN + PPPoE + a lower-device callback that reallocates. Commit e9c238f6fe42.

Use-after-free on an skb head is a nicer primitive than a random 4k OOB, because the freed slab is a known size and a known cache. The published PoC races populated file-descriptor tables into that hole and redirects a live fd to a fake struct file. That is a target-specific groom, not a generic “UAF equals root” law. The bug still exists on any affected kernel that can hit the realloc path, even if your fd-table spray looks nothing like his.

DiagSpill — CVE-2026-74469

An association can have 65,536 peer transports, but transport_count is 16 bits, so the 65,536th wraps to 0. sctp_diag reserved no peer payload then copied the full list — about 8 MiB past the Netlink response. Remote crash/DoS if ASCONF/ADD-IP are enabled with SCTP-AUTH or net.sctp.addip_noauth_enable=1 (all off by default): a malicious peer adds transports; something on the target (for example ss) issues sock_diag. He does not see a path to full remote root even with perfect remote grooming.

A 16-bit guest counter at zero next to an overflowing banquet hall
DiagSpill: the 65,536th peer wraps the counter to zero. The dump still copies every chair onto an empty envelope.
Kitchen table: The banquet software uses a four-digit clicker. The 65,536th guest makes the clicker roll to 0000. The kitchen, looking at the clicker, plates zero dinners. The maître d’ still walks the full seating chart onto a tiny receipt — eight megabytes of names past the edge of the paper and onto the tablecloth. Nobody needed a hall pass to be a guest. They only needed the intercom (SCTP) to be plugged in. From the street you can stuff the guest list only if the club turned on “add extra phone numbers for this membership” (ADD-IP), which most clubs leave off.
For operators: CWE-190 → CWE-787. No userns, no caps: if sctp and sctp_diag are loaded, a local unprivileged process can grow an association and dump it. Remote DoS clause is picky: addip_enable plus either AUTH or addip_noauth_enable, then a local sock_diag consumer. ~8 MiB is “a sockaddr_storage times 65,536, written into a Netlink skb that reserved nothing.” Commit bd0e9289e264.

DiagSpill is the one that should move locked-down hosts up the patch queue. ChromeOS-style images, hardened containers that already killed unprivileged userns, and “we do not give developers CAP_NET_ADMIN” fleets still load SCTP because a telephony or WebRTC dependency pulled it in. If sctp_diag is there, the local corruption path does not care about your LSM. His tests: AppArmor (except Ubuntu’s userns block) and SELinux did not stop the LPEs — and DiagSpill never needed the userns block in the first place.

The fixes

DirtyAH6 — check segments_left, return -EINVAL on the existing AH6 paths. Original snippet:

segments = rthdr->hdrlen >> 1;
if (segments_left > segments)
    return -EINVAL;

rthdr->segments_left = 0;

TUNderflow — clamp TUN headroom to the one-page skb-head budget and 16-bit header offset; pull before use. Original snippet:

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 via the skb network-header offset after device header creation; pskb_expand_head() updates that offset when it moves the head. Original snippet:

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 — refuse a new unique peer once transport_count == U16_MAX (existing peer still returns). Original snippet:

if (asoc->peer.transport_count == U16_MAX)
    return NULL;

peer = sctp_transport_new(asoc->base.net, addr, gfp);

The four commits, in the same order: 7bad4bda74dc (DirtyAH6), 447c9303942c (TUNderflow), e9c238f6fe42 (PPPoEject), bd0e9289e264 (DiagSpill). Maintainers called out: Stefan Klassert, Xin Long, Paolo Abeni, Willem de Bruijn, Greg KH, and others.

Corresponding changelogs for the first upstream-fixed releases, reproduced from the original (these are the per-bug floors, not the all-four floor):

DirtyAH6 first-fixed changelogs

TUNderflow first-fixed changelogs

PPPoEject first-fixed changelogs

DiagSpill first-fixed changelogs

Four sealed envelopes of different ages
Four locksmiths mailed four keys on four Tuesdays. Cashing only the first envelope leaves two doors open.

The exploits (links, not copies)

All four are memory bugs that need per-target grooming. Linked PoCs name distro, kernel, CPU, and memory in the READMEs. He reproduced across distros with tuning; he ships only a couple of target-specific PoCs. They can still corrupt the wrong memory — dedicated VMs only.

  • DirtyAH6github.com/manizada/DirtyAH6: cousin of DirtyFrag’s ESP variant; corrupts skb_shared_info via a separate AH6 rthdr memmove OOB (not closed by the DirtyFrag fix); later ESP decrypt writes a file-backed fragment; pam_rootok.sopam_permit.so; su.
  • TUNderflowgithub.com/manizada/TUNderflow: file-backed pipe buffers next to the bad TUN packet; OVS OOB sets PIPE_BUF_FLAG_CAN_MERGE; pipe write does the same PAM swap.
  • PPPoEjectgithub.com/manizada/PPPoEject: race populated fd tables into the freed skb head; PPPoE writes redirect a live fd to a fake struct file; close → controlled kernel callback → root creds.
  • DiagSpillgithub.com/manizada/DiagSpill: groom sock_diag overwrite into page tables; map host memory; rewrite a cred; sudoers; root shell.
For operators: We are not pasting those PoCs. They are public, target-specific, and unsafe on a shared host. If you need to verify a patch, use a throwaway VM and the README’s kernel/CPU/RAM tuple. The PAM-swap and page-table-groom details above are the author’s own public descriptions, kept here so operators know what “reliable on target” means: not a one-shot script for your laptop.
Kitchen table: Each GitHub repo is a lockpick set cut for one specific door in one specific apartment. The door type (the bug) exists in many buildings. The pick set will snap off in a door that is a centimeter thicker. That is why the author keeps saying dedicated VMs.

Are you affected?

The tables below are pre-reqs for the bugs, not the full LPE chains. Treat the kernel as vulnerable if it is in range and the subsystem is available.

DirtyAH6

AH6/XFRM present. Unprivileged userns for the specific LPE PoC. CAP_NET_ADMIN + CAP_NET_RAW in the userns that owns the attacker netns.

Kernel seriesAffected releasesFirst fixed release
2.6.12–5.9All releasesEOL; no upstream stable fix
5.10.y5.10.0–5.10.2685.10.269
5.11–5.14All releasesEOL; no upstream stable fix
5.15.y5.15.0–5.15.2195.15.220
5.16–6.0All releasesEOL; no upstream stable fix
6.1.y6.1.0–6.1.1866.1.187
6.2–6.5All releasesEOL; no upstream stable fix
6.6.y6.6.0–6.6.1556.6.156
6.7–6.11All releasesEOL; no upstream stable fix
6.12.y6.12.0–6.12.1076.12.108
6.13–6.17All releasesEOL; no upstream stable fix
6.18.y6.18.0–6.18.486.18.49
6.19–7.0All releasesEOL; no upstream stable fix
7.1.y7.1.0–7.1.127.1.13
7.2.y7.2.0–7.2.27.2.3
DirtyAH6 affected upstream kernels. Source: original article.

TUNderflow

Unprivileged userns + TUN/TAP, plus some device path that can propagate oversized receive headroom (the PoC uses netkit/VXLAN/OVS). CAP_NET_ADMIN in that userns.

Kernel seriesAffected releasesFirst fixed release
4.6–5.9All releasesEOL; no upstream stable fix
5.10.y5.10.0–5.10.2695.10.270
5.11–5.14All releasesEOL; no upstream stable fix
5.15.y5.15.0–5.15.2205.15.221
5.16–6.0All releasesEOL; no upstream stable fix
6.1.y6.1.0–6.1.1876.1.188
6.2–6.5All releasesEOL; no upstream stable fix
6.6.y6.6.0–6.6.1566.6.157
6.7–6.11All releasesEOL; no upstream stable fix
6.12.y6.12.0–6.12.1086.12.109
6.13–6.17All releasesEOL; no upstream stable fix
6.18.y6.18.0–6.18.496.18.50
6.19–7.0All releasesEOL; no upstream stable fix
7.1.y7.1.0–7.1.13EOL; no upstream stable fix
7.2.y7.2.0–7.2.37.2.4
TUNderflow affected upstream kernels. Source: original article.

PPPoEject

Unprivileged userns + PPPoE, plus a lower-device header callback that can realloc the skb head during dev_hard_header(). CAP_NET_ADMIN.

Kernel seriesAffected releasesFirst fixed release
2.6.12–5.9All releasesEOL; no upstream stable fix
5.10.y5.10.0–5.10.2645.10.265
5.11–5.14All releasesEOL; no upstream stable fix
5.15.y5.15.0–5.15.2155.15.216
5.16–6.0All releasesEOL; no upstream stable fix
6.1.y6.1.0–6.1.1826.1.183
6.2–6.5All releasesEOL; no upstream stable fix
6.6.y6.6.0–6.6.1476.6.148
6.7–6.11All releasesEOL; no upstream stable fix
6.12.y6.12.0–6.12.1006.12.101
6.13–6.17All releasesEOL; no upstream stable fix
6.18.y6.18.0–6.18.416.18.42
6.19–7.0All releasesEOL; no upstream stable fix
7.1.y7.1.0–7.1.57.1.6
PPPoEject affected upstream kernels. Source: original article.

DiagSpill

SCTP + sctp_diag. No userns, no special CAPs. That is the scary one for locked-down hosts that still load SCTP.

Kernel seriesAffected releasesFirst fixed release
4.7–5.9All releasesEOL; no upstream stable fix
5.10.y5.10.0–5.10.2645.10.265
5.11–5.14All releasesEOL; no upstream stable fix
5.15.y5.15.0–5.15.2155.15.216
5.16–6.0All releasesEOL; no upstream stable fix
6.1.y6.1.0–6.1.1826.1.183
6.2–6.5All releasesEOL; no upstream stable fix
6.6.y6.6.0–6.6.1506.6.151
6.7–6.11All releasesEOL; no upstream stable fix
6.12.y6.12.0–6.12.1026.12.103
6.13–6.17All releasesEOL; no upstream stable fix
6.18.y6.18.0–6.18.436.18.44
6.19–7.0All releasesEOL; no upstream stable fix
7.1.y7.1.0–7.1.77.1.8
DiagSpill affected upstream kernels. Source: original article.

Mouthful: DirtyAH6 trigger wants CAP_NET_ADMIN and CAP_NET_RAW in the userns owning the netns. TUNderflow and PPPoEject want CAP_NET_ADMIN there. In an already-CAP’d container those paths can corrupt the host kernel without creating new userns (container escape theoretically possible; he did not PoC it, same as CIFSwitch/OVSwrap). Unprivileged userns is just the common path. DiagSpill: no CAPs, no userns, as long as SCTP and sctp_diag exist.

A hall pass on a lanyard next to a wall intercom
Three bugs want the hall pass (unprivileged userns / CAP_NET_ADMIN). DiagSpill only needs the intercom (SCTP) plugged in.

Mitigation

Upgrade to a kernel with all four. First stables that contain the set: 5.10.270, 5.15.221, 6.1.188, 6.6.157, 6.12.109, 6.18.50, 7.2.4.

If you cannot patch: disabling unprivileged userns removes the ordinary-user path to the first three (not CAP’d containers). DiagSpill remains. You can also disable unused AH6, TUN, PPPoE, SCTP/sctp_diag. Do not treat “disable the PoC’s extra kmods” as a real mitigation — other paths to root may exist. Patch.

Kitchen table: If the locksmith is booked until Thursday: take the spare hall passes away from tenants (helps three doors, not the intercom), and unplug gadgets you do not use. Do not congratulate yourself for unplugging Open vSwitch if PPPoE and SCTP are still in the wall. Thursday is still the real fix.

Outro

Personal/professional developments: this likely ends the public AI-assisted vuln-hunting experiment for a while. Thanks for riding along.

That sentence is doing more work than a blog farewell. The last two years of public Linux kernel LPEs from this author were a running argument that agentic hunting with graph and geometry tools can find old, exploitable memory bugs in boring subsystems — not just “LLM wrote a syzkaller prog.” The experiment going quiet does not make 5.10.268 safe. The stables still have to move.

A glossary for both sides of the table

TermKitchenOperator
AH6 / XFRMA wax seal on the package after the clerk rewrote the address labels.IPv6 IPsec Authentication Header; ipv6_rearrange_rthdr().
segments_leftHow many stickers still to shuffle. Unchecked, the clerk walks off the table.rthdr field; 255 with hdrlen=2 → 4064-byte memmove.
TUN alignHow much empty cardboard to leave in front of the packet.tun->align from ndo_set_rx_headroom; SKB_MAX_HEAD underflow.
pskb_expand_headThe box is too small, so they pour it into a new box and throw the old one.PPPoE still held a pointer into the old skb head.
transport_count u16A 16-bit guest list for 65,536 chairs.65,536th peer wraps to 0; diag copies ~8 MiB past the skb.
unprivileged usernsA hall pass that lets you pretend to be the network janitor.Common LPE on-ramp for the first three; not required for DiagSpill.
CAP_NET_ADMINPermission to rearrange the building’s cables.Enough, in a userns-owned netns, to hit TUN/PPPoE; plus CAP_NET_RAW for DirtyAH6.
sock_diagAsking the front desk to print every guest’s name and phone number.Netlink dump; sctp_diag builds one sockaddr_storage per transport.
ADD-IP / ASCONFLetting a member add extra phone numbers from outside.Off by default; required for DiagSpill’s remote DoS clause.
skb_shared_infoThe packing slip glued to the bottom of the crate.Trailer of an skb; DirtyAH6 PoC corrupts it, then ESP writes a file-backed frag.
Dual-audience glossary. Kitchen column is ours; operator column follows the original.

Why two of these scare routers

A network closet with a sealed packet on the rack
DirtyAH6’s remote clause is an IPv6 gateway that adds AH in transport mode. That is not a laptop story.

Local root on a laptop with userns is a Tuesday. DirtyAH6 on an IPv6 gateway that encapsulates AH in transport mode is a remote memmove into packet metadata. That is why he bothers to say “router/gateway.” The LPE PoC still uses userns; the crash does not need a local account. A border box that terminates or encapsulates IPsec AH for IPv6 should be treated as a remote-DoS (and, in his lab, a remote-root-with-local-groom) until it is on the DirtyAH6 first-fixed release or newer.

DiagSpill’s remote path is pickier: ADD-IP off by default, AUTH or addip_noauth required, then someone local must run sock_diag (ss is the example). That is a DoS gadget for a mis-set SCTP anycast box, not a worm. Do not write “remotely exploitable SCTP root” into a ticket. Do write “if we turned on ADD-IP, a peer can inflate transports until the next diagnostic dump crashes the box.”

For operators: Inventory question for netops, not desktop: (1) any box doing IPv6 AH transport-mode XFRM? (2) any box with net.sctp.addip_enable=1? If both answers are no, the remote clauses are academic and you still patch because of local LPE and DiagSpill-without-caps.

Patch math for distro owners

Four different first-fixed versions per series. A host on 6.6.148 has PPPoEject closed if it took 6.6.148, DiagSpill closed at 6.6.151, DirtyAH6 at 6.6.156, TUNderflow at 6.6.157. “We applied last week’s stable” is not “all four.” The all-four floor is the list in Mitigation. EOL series (everything not .y in the tables) get no upstream backport — you live on the distro’s fork or you disable the kmod.

SeriesPPPoEjectDiagSpillDirtyAH6TUNderflow (all-four)
5.10.y5.10.2655.10.2655.10.2695.10.270
5.15.y5.15.2165.15.2165.15.2205.15.221
6.1.y6.1.1836.1.1836.1.1876.1.188
6.6.y6.6.1486.6.1516.6.1566.6.157
6.12.y6.12.1016.12.1036.12.1086.12.109
6.18.y6.18.426.18.446.18.496.18.50
7.1.y7.1.67.1.87.1.13EOL; no TUNderflow stable
7.2.y7.27.27.2.37.2.4
Per-bug first-fixed versus the all-four floor. TUNderflow is the last to land in every live series; 7.1.y never got it upstream.

7.1.y is the trap in that table. DirtyAH6 is fixed in 7.1.13; TUNderflow’s 7.1 row is “EOL; no upstream stable fix.” If you are on 7.1, you either move to 7.2.4 or you carry a distro backport of 447c9303942c. Do not read “7.1.13 has DirtyAH6” as “7.1 is done.”

Kitchen table: Four locksmiths mailed four keys on four Tuesdays. If you only cashed the first envelope, two doors still open. Ask for 5.10.270 / 6.6.157 / 6.12.109, not “a 6.6 that looks recent.” If your building is the 7.1 wing, the TUNderflow key never arrived in the official mail — move floors.

Distro kernels (Ubuntu HWE, RHEL, Android GKI, ChromeOS, custom Yocto) will not match these numbers. Map the four git IDs into your tree. If your vendor says “we shipped CVE-2026-68121,” ask which of the four IDs are present. One CVE blog post is not a quartet.

Hunting without the PoCs

You do not need to run a GitHub LPE to know whether a fleet is in the blast radius. The original already gave you the pre-reqs. Here is that list as a hunt.

  1. uname -r versus the all-four floors above. If you are below the floor, you are not done.
  2. lsmod and boot config: AH6/XFRM, tun, pppoe, sctp, sctp_diag, openvswitch, vxlan, netkit, team, bonding, fuse, gre.
  3. sysctl kernel.unprivileged_userns_clone (and distro equivalents: Debian/Ubuntu userns sysctls, Fedora, container-runtime defaults).
  4. net.sctp.addip_enable / addip_noauth_enable / auth_enable — if any of those are on, DiagSpill’s remote DoS clause applies.
  5. IPv6 routers with xfrm transport AH: DirtyAH6 remote crash clause.
  6. Containers with CAP_NET_ADMIN (and CAP_NET_RAW for DirtyAH6): treat as host-kernel risk even when the node disabled unprivileged userns for ordinary users.
  7. Do not grep for the GitHub repo names as your only signal. The bugs are old; the PoCs are new. Patch level is the hunt.
For operators: A one-shot audit that is honest: (a) kernel package changelog contains all four commit IDs or all four CVE IDs, (b) SCTP ADD-IP is off unless a named product needs it, (c) IPv6 AH transport-mode is inventoried, (d) unprivileged userns policy is a documented choice rather than a distro default nobody remembers. Anything less is a blog post, not a close-out.

CWE, ATT&CK, and what “exploitable” means here

BugCWEATT&CKWhat “exploitable” meant in the original
DirtyAH6CWE-787, CWE-20T1068; T1499; T1190 on AH-transport routersLocal LPE with userns; remote crash; lab remote-root with on-target groom
TUNderflowCWE-191, CWE-787T1068; T1611 theoretical in CAP’d containersLocal LPE; needs headroom-propagating device path
PPPoEjectCWE-416T1068; T1611 theoreticalLocal LPE; FUSE + GRE/team race in the published trigger
DiagSpillCWE-190, CWE-787T1068; T1499 if ADD-IPLocal LPE with no caps; remote DoS only; no remote-root path seen
ATT&CK names: T1068 Exploitation for Privilege Escalation, T1499 Endpoint DoS, T1190 Exploit Public-Facing Application, T1611 Escape to Host.

T1611 is marked theoretical because the author said so: same shape as CIFSwitch/OVSwrap, community example exists for that class, he did not pursue a container-escape PoC here. Do not file “confirmed container escape” from this page. Do file “CAP_NET_ADMIN in a userns-owned netns can hit host-kernel memory bugs 1–3.”

T1190 for DirtyAH6 is the AH-transport IPv6 gateway clause, not “every Linux box is a public-facing AH server.” Most desktops never add AH in transport mode. Most core routers that still speak IPsec AH should be on the DirtyAH6 floor this week, not next quarter.

Detection ideas that do not need the exploit repos

Pre-patch, you will not get a reliable “DirtyAH6 fired” EDR signature from a public blog. Post-patch, the detection that matters is inventory. A few behavioral edges are still worth a SIEM rule if you already collect them:

  • Unprivileged processes creating user namespaces and then opening /dev/net/tun, PPPoE sockets, or raw IPv6 HDRINCL — noisy on developer workstations, interesting on locked-down gold images.
  • Sudden growth of SCTP peer addresses on an association (thousands of transports) followed by ss/sock_diag — DiagSpill’s remote clause, and also a local prep step.
  • Kernel oops/panic with AH6, TUN, PPPoE, or sctp_diag on the stack during the unpatched window. That is a crash, not a proven LPE, but it is a reason to isolate the box.
  • Writes to /etc/pam.d/su or unexpected sudoers drops from a non-config-management user — those are the published LPE finishes, not the bugs. Hunt them as integrity events regardless.

None of those replace patching. They exist so a SOC that cannot reboot tonight has something to watch until the kernel package lands.

Containers, hall passes, and the CAP footnote

Unprivileged user namespaces are the common on-ramp because they let an ordinary user mint a namespace in which they hold CAP_NET_ADMIN (and CAP_NET_RAW) relative to that namespace’s network stack. The kernel objects those caps touch — TUN, PPPoE, AH6 — are still the host’s. That is the entire Linux userns LPE genre of the last decade. Ubuntu’s choice to block unprivileged userns is why AppArmor “helped” in those cases: it removed the hall pass, not because AppArmor understood memmove.

The footnote in the original is the one operators skip: if a container runtime already hands out CAP_NET_ADMIN (and RAW for DirtyAH6) in a userns-owned netns, the process does not need to create a new user namespace. Kubernetes pods with NET_ADMIN, Docker --cap-add=NET_ADMIN, and “network troubleshooting” sidecars are in that bucket. Host-kernel memory corruption from inside that pod is a container-escape primitive in theory. He did not write that PoC. The community already did analogous work on CIFSwitch.

DiagSpill ignores this entire paragraph. No caps. No userns. SCTP + sctp_diag. If your hardened image still has those two, you are not hardened against this quartet.

What the agentic hunter was actually doing

The original is brief because the method was already written up. A one-paragraph restatement for people who will not click the three older posts: CIFSwitch-style graph tracking keeps security-relevant objects (creds, skbs, netns, xfrm states, SCTP transports) as first-class nodes so an agent can ask “what can this cap touch.” OVSwrap-style geometry gives the agent a spatial model of kernel memory — slabs, skb heads, page tails — instead of a token soup of C. The drunk-LLM post is the harness: how to keep a model exploring without collapsing into syzkaller karaoke. This quartet is four successful trips through that loop, not four lucky prompts.

Why that matters for defenders: the next bug in AH6, TUN, PPPoE, or SCTP will not necessarily come from the same author, and the public experiment is pausing. The class is “old packet-munging code with a length derived from a wire field, a stored headroom, a pointer held across realloc, or a 16-bit counter next to a dump.” Those sentences are audit tickets you can file without an LLM.

  • Any memmove/memcpy whose length is a - b from a packet field, without b <= a.
  • Any stored headroom/alignment that can be set by a stacked device and is later subtracted from a page-sized budget without a clamp.
  • Any protocol that caches skb->data or a header pointer across dev_hard_header() / pskb_expand_head().
  • Any 8- or 16-bit “how many peers/transports/addresses” counter that a dump path later uses as a reservation size.

A slower walk through the four kitchens

The mailman and the wax seal

IPsec AH is a promise: these bytes did not change in flight. IPv6 makes that promise annoying because extension headers and routing headers are allowed to look different at each hop while still naming the same journey. Linux therefore canonicalizes some fields before it computes or checks the ICV. Canonicalize-then-authenticate is correct. Canonicalize with an unchecked segments_left is “walk off the packet.” The local attacker who can inject a raw IPv6 packet with a nonsense routing header is usually a userns tenant with RAW. The remote attacker is whoever can get an IPv6 packet to a box that will run AH6 on it in transport mode — a much smaller set, and a much more important one.

The box that was too small

TUN is how VPNs, Kubernetes CNIs, and a lot of lab tooling shove packets into the kernel. Headroom exists so the kernel can prepend headers without copying the whole packet. Stacked devices (VXLAN on netkit, OVS copying the max) honestly advertise “I need more headroom than a page.” TUN stored that number and later asked SKB_MAX_HEAD to subtract it from a page. Subtracting a larger number from a smaller one, then using the result as a size, is how you place skb->data in the next object. The OVS OOB in the published LPE is a consequence, not a second bug.

The recycled cardboard

PPPoE is still in trees because ISPs and a surprising number of nested-virt labs still speak it. pppoe_sendmsg is a send path, so it feels user-triggered and boring. The boring part is the bug: a pointer that should have been an offset. FUSE appears in the PoC because a slow userspace filesystem is a convenient way to hold the payload copy while another thread mutates the lower device. Team/bonding plus the first GRE port is a convenient way to make dev_hard_header actually realloc. Other lower devices that call pskb_expand_head during header creation are in the same CWE bucket even if they are not in the README.

The clicker that rolled over

SCTP associations with tens of thousands of peer addresses are not a desktop thing. They are a “we built a pathological association on purpose” thing, which is exactly what a local attacker does, and what a remote ADD-IP peer can do if you enabled the feature. The diagnostic path (ss, other sock_diag consumers) is the detonator: it trusts transport_count to size a buffer, then walks the real list. When the counter is 0 and the list is 65,536, you get an 8 MiB spill. Locally that is an LPE primitive. Remotely, without a local dumper, it is a stored bomb. His remote-root skepticism is about grooming page tables from the wrong side of a network without a local helper. Believe him until someone publishes otherwise.

What this is not

  • Not a worm. Three of four want local caps or userns. DiagSpill’s remote clause is a default-off feature plus a local diagnostic.
  • Not “LLMs replaced kernel auditors.” The harness found old mistakes; humans reported, patched, and wrote PoCs.
  • Not a reason to disable SCTP on a host that actually needs SCTP — it is a reason to patch and leave ADD-IP off.
  • Not a DirtyFrag duplicate. DirtyAH6 is a different OOB in AH6’s routing-header shuffle. The ESP-side DirtyFrag fix does not close it.
  • Not permission to paste the GitHub trees into a ticket tracker. Link them. Run them in VMs you intend to delete.

Key Takeaways

  • Four LPEs: DirtyAH6 (CVE-2026-80844), TUNderflow (CVE-2026-81000), PPPoEject (CVE-2026-68121), DiagSpill (CVE-2026-74469). 10–21 year-old bugs. Agent+graph hunting.
  • First three: userns (or CAP_NET_ADMIN in a userns netns; plus RAW for DirtyAH6). DiagSpill: SCTP+sctp_diag, no extra caps.
  • DirtyAH6 remote crash on AH-transport IPv6 routers; lab remote-root with on-target groom. DiagSpill remote DoS if ADD-IP. No remote-root path seen for DiagSpill.
  • Patch to 5.10.270 / 5.15.221 / 6.1.188 / 6.6.157 / 6.12.109 / 6.18.50 / 7.2.4 (all four). Per-bug floors are earlier; 7.1.y never got TUNderflow upstream.
  • PoCs on GitHub, target-specific, VM-only. This article does not re-host them.
  • AppArmor/SELinux were not a save in his tests except Ubuntu’s userns block (which does not help DiagSpill).
  • Public hunting experiment paused. Thanks, maintainers.

Defensive Recommendations

  1. Upgrade to an all-four stable. Do not stop at the first of the four changelogs. Map the four git IDs into vendor kernels.
  2. If stuck: disable unprivileged userns (helps 1–3 only) and unused AH6/TUN/PPPoE/SCTP. Do not confuse “removed OVS” with “removed the bug class.”
  3. IPv6 edge: inventory transport-mode AH. Treat those boxes as remote-DoS until DirtyAH6 is in.
  4. SCTP: leave ADD-IP off unless a named product requires it. If it is on, you have accepted DiagSpill’s remote crash clause until patched.
  5. Containers with NET_ADMIN: treat as host-kernel risk for bugs 1–3 even without new userns. DiagSpill ignores that discussion.
  6. SELinux/AppArmor were not a save in his tests. Patch anyway. Integrity-monitor /etc/pam.d/su and sudoers as a backstop, not a fix.
  7. PoCs: throwaway VMs matching the README. Not staging. Not the laptop. Not a shared CI runner.
  8. File audit tickets on the four C patterns in “What the agentic hunter was actually doing,” even after this quartet is closed.

Conclusion

Four old keys, one geometric fuzzer, one embargo morning. Two of them can be shaken from the network if you left a rare lock on the street. The rest need a hall pass most desktops still print. DiagSpill does not even need the hall pass — only an intercom a surprising number of “locked down” images still ship. The experiment goes quiet. The stables do not. Take 6.6.157 or your series’ all-four floor, map the four commits into the vendor tree, leave ADD-IP off, and leave the GitHub tabs for a VM you intend to delete.

Original text: “A quartet of Linux local root vulns: DirtyAH6, PPPoEject, TUNderflow, and DiagSpill” by Asim Viladi Oglu Manizada at heyitsas.im.

oxfemale Vulnerability research, reverse engineering, and exploit development.
// Discussion