-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 NEFARIOUSPLAN-CANONICAL-V1 {"body_md":"## The store value is a netlink attribute\n\nMost kernel write primitives have to bend the world to choose what gets written. Dirty Frag does not. The attacker registers an XFRM Security Association via `xfrm_user` netlink and supplies `XFRMA_REPLAY_ESN_VAL`. One field of that attribute, `seq_hi`, is the high 32 bits of the extended sequence number. The attacker types it.\n\nThe kernel will faithfully copy that 32-bit integer into the AEAD scratch buffer during `crypto_authenc_esn_decrypt`, then write it back to the destination scatterlist as part of an ESN reordering dance:\n\n```\nscatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1)\n```\n\nIf `dst` is a kernel page that backs a file in the page cache, those four bytes land in the page cache. The on-disk file is untouched. AIDE, tripwire, auditd, every inotify watcher in the world: nothing fires. The next process that reads the file gets the modified bytes from cache.\n\n`/usr/bin/su` is a setuid binary. The kernel checks the setuid bit at `execve` time and trusts the page cache for the bytes that follow. V4bel's PoC patches a four-byte window inside `su` to make it skip the password prompt. The disk is clean. The exploit is a write that nothing on the system can see.\n\n## Two correct decisions made years apart\n\nThe interesting part is not the sink. The interesting part is the route the four bytes take to reach it.\n\nIn January 2017, commit `cac2661c53f3` added a fast path to `esp_input` for non-ESN, non-fragmented skbs. The branch was guarded by the right invariants for 2017. Every IPsec receive path back then assembled its skb out of locally-allocated pages, and `skb_has_shared_frag()` reliably caught the cases where someone had pinned an external page into the skb's frag list. Linearisation was expensive, and skipping it when nothing was shared was a sensible optimisation.\n\nIn February 2023, commit `7c8a61d9ee1d` introduced `MSG_SPLICE_PAGES` for UDP. The point of the change was to let userspace `splice()` or `vmsplice()` arbitrary pipe pages into a UDP socket without copying them. `__ip_append_data` and `__ip6_append_data` learned to attach those pages directly to the outgoing skb's frag array. The flag set on those frags is `SKBFL_PURE_ZEROCOPY`. The flag that is _not_ set is `SKBFL_SHARED_FRAG`.\n\nThat asymmetry is the whole bug.\n\n`skb_has_shared_frag()` returns false on a `MSG_SPLICE_PAGES` skb. The 2017 fast path in `esp_input` therefore declines to call `skb_linearize`. The skb proceeds into the AEAD pipeline with frags that point into a different process's pipe, which itself points at a page in the kernel page cache for `/usr/bin/su`. `crypto_authenc_esn_decrypt` walks the destination scatterlist, locates the four-byte ESN reordering window, and writes the attacker's `seq_hi` directly into the page cache.\n\nNeither commit was wrong on its own. The 2017 commit was guarded for the world it lived in. The 2023 commit added a new way for skbs to enter that world, and the guard did not know about it. There is no individual whose mistake produced this bug. There is a six-year reviewer gap between two correct patches.\n\n## The crypto sink has now produced two CVEs\n\nBefore Dirty Frag there was Copy Fail, CVE-2026-31431. The same `crypto_authenc_esn_decrypt` write happens in Copy Fail too. The difference is the route in. Copy Fail uses `algif_aead`, the userspace AF_ALG socket that lets unprivileged processes drive a kernel AEAD transform directly. The PoC is a sixty-line Python script that opens a socket of family 38 (`AF_ALG`), binds it to `aead/authencesn(hmac(sha256),cbc(aes))`, splices `/usr/bin/su` in as the destination via `splice()` between a pipe and the socket fd, and lets the kernel walk the AEAD scatterlist into the page cache.\n\nTwo CVEs. Different syscalls. Different subsystems. Same four-byte write at the same line of `crypto/authencesn.c`.\n\nThe sink is doing exactly what it was designed to do. `scatterwalk_map_and_copy` does not ask whether the destination scatterlist points at a frag attached by the network stack, by `algif_aead`, by `splice`, or by a kernel module's bounce buffer. It writes. Every caller of `crypto_authenc_esn_decrypt` is a potential delivery vector for the same primitive, and the policy that pages backing read-only setuid binaries should not be writable destinations was never encoded at the sink. It was encoded, separately and incompletely, at every caller.\n\n## The patch is in two files\n\nLook at where the upstream fix landed. The cve.org record cites four programFiles entries:\n\n```\nnet/ipv4/esp4.c\nnet/ipv4/ip_output.c\nnet/ipv6/esp6.c\nnet/ipv6/ip6_output.c\n```\n\n`esp4.c` and `esp6.c` get the linearisation fix: drop the `skb_has_shared_frag()` guard for the ESN case and unconditionally linearise. `ip_output.c` and `ip6_output.c` get the labelling fix: when `__ip_append_data` attaches a `MSG_SPLICE_PAGES` page, also set `SKBFL_SHARED_FRAG`, so every existing consumer that already checks the flag will start working again.\n\nTwo patches for one bug because one patch would not have been enough. Fixing only `esp_input` would leave every other 2017-era branch that trusts `skb_has_shared_frag()` exposed to the same primitive via the same `MSG_SPLICE_PAGES` route. Fixing only `ip_output.c` would close this delivery vector and leave four other in-tree paths that build skb frags directly without consulting the IP output code at all.\n\nThe fix is split because the bug is a missing edge between two subsystems, and you cannot fill an edge from one side.\n\n## The half that defeats Ubuntu has no upstream patch\n\nV4bel's chain is two CVEs. CVE-2026-43284 is one half. The other half is CVE-2026-43500, a use-after-free in RxRPC, the kernel module that implements the AFS RPC transport. Why both?\n\nBecause Ubuntu 24.04 ships AppArmor with `unprivileged_userns_apparmor_policy=1`. An unprivileged process cannot `unshare(CLONE_NEWUSER | CLONE_NEWNET)` to gain the `CAP_NET_ADMIN` it needs to register an XFRM SA. The `xfrm_user` route is closed on Ubuntu without root.\n\n`rxrpc.ko` is loaded by default on Ubuntu. Triggering the RxRPC use-after-free does not require namespace creation. The RxRPC bug elevates to the point where you can reach `xfrm_user` with the right capability set, and then the page-cache write proceeds.\n\nOn RHEL 10, AlmaLinux 10, Rocky 10, the inverse holds. Userns are usable without AppArmor blocking, so the XFRM half is reachable directly. RxRPC is not loaded by default. The chain dispatches per distribution: whichever half is unblocked is the half the exploit uses, and the other half is dormant.\n\nCVE-2026-43284, the one with the patch, is the half that the Ubuntu kernel was already mitigating in user space via AppArmor. The half that defeats Ubuntu is CVE-2026-43500, which is the unpatched RxRPC use-after-free that is upstream as of this writing. Distribution mitigation summaries that report \"patched\" against CVE-2026-43284 are not lying. They are reporting on the half of the bug that was already harder on their kernel.\n\nDebian shipped 7.0.4-1 to sid and backported via DSA-6253-1 to trixie and DLA-4572-1 to bullseye. AlmaLinux pushed both fixes to 9 and 10 production. Proxmox issued PSA-2026-00019. Rocky, Amazon Linux 2023, NixOS, RHEL: no advisories at the time of writing for CVE-2026-43500. The xfrm-ESP half travels well. The RxRPC half travels alone.\n\n## What the PoC does not say\n\nThe exp.c that V4bel published, and that 6abc and 0xBlackash forked verbatim, is a 67kB C program. The first hundred lines are entirely setup: it carves out a 192-byte x86_64 shellcode ELF whose entry point at 0x400078 calls `setuid(0); setgid(0); execve(\"/bin/sh\", ...)`, opens `/usr/bin/su` read-only, sets up pipes, and primes the XFRM netlink configuration.\n\nThe shellcode is not the interesting part. The interesting part is the 32-bit value the exploit places into `XFRMA_REPLAY_ESN_VAL.seq_hi` at the moment the AEAD walk crosses the four-byte window inside `su`'s text segment. The exploit does not need to upload code. It only needs to corrupt the four bytes of `su` that decide whether to call `pam_authenticate`, and it needs to do that without touching the disk.\n\nThe PoC weighs 67kB because most of it is the framework for placing four specific bytes at a specific page offset. The vulnerability itself is a single 32-bit netlink attribute.\n\nThe thing the PoC does not advertise: it works on a kernel that an EDR is watching. There is no `ptrace`, no `process_vm_writev`, no `/proc//mem` write, no kernel module load, no `bpf()` syscall, no `ftrace` manipulation. The system call sequence is `socket(AF_NETLINK)`, `sendmsg`, `splice`, `sendmsg`, `recv`, `execve(\"/usr/bin/su\")`. The audit subsystem sees a normal `su` invocation that succeeds. Forensics on the disk shows no modified files. The page cache containing the modified `su` evicts on memory pressure, after which the disk version reloads and the evidence is gone.\n\nThis is the property the Dirty Frag name preserves from Dirty Pipe and Dirty COW: a write that targets the page cache, not the disk, leaves no artifact that the standard incident-response toolchain knows to look for.\n\n## The mitigation script ships separately\n\nA third-party mitigation script published by scriptzteam blacklists `esp4`, `esp6`, and `rxrpc` via modprobe. The README warns that this disables IPsec ESP, which is to say strongSwan, libreswan, and most enterprise VPN configurations, and disables AFS. The script is correct. It is also revealing.\n\nDefending against Dirty Frag without the upstream patches means turning off IPsec. The patched-half is the half whose mitigation is \"blacklist `esp4` and `esp6` modules and stop running VPNs\". The unpatched half is the half whose mitigation is \"blacklist `rxrpc` and stop running AFS\". For a great many production systems, both of those are non-options. The mitigation script ships with a warning because the cost of the workaround is high enough that the script's author wants to make sure you read it before you run it.\n\nThe cost of the workaround is the price of the missing upstream patch. CVE-2026-43500 has no fix. As long as it does not, the modprobe blacklist is what stands between an Ubuntu fleet and an unauthenticated local privilege escalation.\n\n## The reviewer gap is the real artifact\n\nSix years passed between the 2017 commit that introduced the vulnerable branch and the 2023 commit that armed it. Nine years between the 2017 commit and the CVE. There is no review process in the kernel that runs against `esp_input` every time someone touches `__ip_append_data`. There is no static analyser that knows the relationship between `SKBFL_SHARED_FRAG` and `skb_has_shared_frag()` and the `crypto_authenc_esn` reordering write. The relationship existed only in the head of whoever wrote the 2017 patch, and that person had no obligation, and probably no occasion, to revisit it in 2023.\n\nThis is what the emergent-primitive pattern looks like in the kernel. Two correct patches compose into one wrong primitive over a six-year window, and the fix has to be in both files at once because each file individually is doing a thing that was correct under its own local invariants.\n\nIt is also what the design-debt-driver pattern looks like at the AEAD sink. `crypto_authenc_esn_decrypt` is now confirmed to deliver attacker-controlled four-byte writes to wherever the destination scatterlist resolves, regardless of route. Copy Fail came in via `algif_aead`. Dirty Frag came in via `MSG_SPLICE_PAGES` plus xfrm. Whichever syscall arrives next at this sink will produce the third CVE in the series.","closing_line":"The CVE record is the patch's name. The bug's name is Dirty Frag.","hook_md":"CVE-2026-43284 patches a branch added to `esp_input` in January 2017. The branch was correct in 2017. It was correct again in 2018. It was correct in 2020 and in 2022. The thing that broke it was added to a different file in 2023, and nobody who reviewed the 2023 change was reading `esp_input`.\n\nThe patch lives at the consumer end of the chain. It linearises a fragmented skb before letting `crypto_authenc_esn` write four bytes of recovered sequence-number state into a scatterlist destination. That is the entire fix. Four bytes, written in the wrong place, was a local privilege escalation against every distribution kernel from 6.12 through 6.19.\n\nThe CVE record is for the half of the bug that has a patch. The half that does not have a patch is the half that works on Ubuntu.","post_id":194,"slug":"dirty-frag-patched-half-ubuntu-already-mitigates","title":"Dirty Frag: the patched half is the half Ubuntu already mitigated","type":"initial","unreadable_sentence":"CVE-2026-43284 is the half of Dirty Frag that does not work on Ubuntu. The other half does."} -----BEGIN PGP SIGNATURE----- iHUEARYIAB0WIQRf0htP5+SjynlxywneZjl4jgkQJgUCaf9FSAAKCRDeZjl4jgkQ JjamAQDakVOixAhxdA0H13IOlewjpCKo76ow23gWwCdV8QvNdAEAhbIdqV/EO7i7 zK2Yav1RDVFXp/HQZikezWT/c4xg8gk= =pxrB -----END PGP SIGNATURE-----