## https://sploitus.com/exploit?id=2F23EA09-56C6-54AD-B6E1-C8743D01A747
# Copy Fail 2 โ Electric Boogaloo
Unprivileged local privilege escalation on Linux via the `xfrm` ESP-in-UDP receive
fast path and `MSG_SPLICE_PAGES`. Same primitive shape as
[Copy Fail / CVE-2026-31431](https://copy.fail/) (Theori/Xint, April 2026), in a
different kernel subsystem.
Where Copy Fail walked through `algif_aead` + `splice()` to land a 4-byte write
into the page cache, this one rides `udp_sendmsg(MSG_SPLICE_PAGES)` โ
`xfrm_input` โ `esp_input` no-COW fast path to land an **arbitrary-length
keystream-derived write into any page-cache page the attacker can `splice()`
from a readable file**. From there it's a 1-byte `/etc/passwd` UID flip and a
`su` to a known user, swapped for a SUID-root shell drop.
## Bug
Mainline fix: [`netdev/net.git f4c50a4034`](https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net.git/commit/?id=f4c50a4034e62ab75f1d5cdd191dd5f9c77fdff4)
(2026-05-05, `Cc: stable@vger.kernel.org`).
> `MSG_SPLICE_PAGES` can attach pages from a pipe directly to an skb. TCP marks
> such skbs with `SKBFL_SHARED_FRAG` after `skb_splice_from_iter()`, so later
> paths that may modify packet data can first make a private copy. The
> IPv4/IPv6 datagram append paths did not set this flag when splicing pages
> into UDP skbs.
>
> That leaves an ESP-in-UDP packet made from shared pipe pages looking like an
> ordinary uncloned nonlinear skb. ESP input then takes the no-COW fast path
> for uncloned skbs without a frag\_list and decrypts in place over data that
> is not owned privately by the skb.
`Fixes:` chain spans:
- `cac2661c53f3` (esp4 no-COW, 2017)
- `03e2a30f6a27` (esp6 no-COW, 2017)
- `7da0dde68486` (UDP `MSG_SPLICE_PAGES`, 2023)
- `6d8192bd69bb` (UDP6 `MSG_SPLICE_PAGES`, 2023)
Reported-by: Hyunwoo Kim (imv4bel) and Kuan-Ting Chen (h3xrabbit).
## Why this isn't just a re-skin of Copy Fail
| | Copy Fail (CVE-2026-31431) | This bug |
|---|---|---|
| Subsystem | `algif_aead` (AF_ALG userspace crypto) | `xfrm` ESP-in-UDP receive |
| Trigger | `splice()` page-cache pages into AEAD destination scatterlist | `splice()` page-cache pages into UDP skb via `MSG_SPLICE_PAGES`, loopback into `esp_input` |
| Primitive | controlled 4-byte write into page cache | keystream-derived N-byte write into page cache (N โค ESP payload len) |
| Module gate | mitigation effective only when `CONFIG_CRYPTO_USER_API_AEAD=m` | **none** โ `xfrm` is always built-in on distros |
| `modprobe.d` blocklist works? | sometimes | no |
| Needs xfrm SA install | no | yes โ `CAP_NET_ADMIN` in netns (userns gives this) |
The two are siblings in the same bug class โ _kernel AEAD running in-place over
attacker-shared page-cache pages_ โ discovered independently in different
subsystems within weeks of each other. The xfrm version has no `=m` knob to
hide behind, which makes it strictly more reachable on hardened distros.
## The primitive
The fast path in `esp_input()`:
```c
} else if (!skb_has_frag_list(skb)) { // โ pre-fix
nfrags = skb_shinfo(skb)->nr_frags;
nfrags++;
goto skip_cow; // skip skb_cow_data โ decrypt in place
}
```
When the receive skb's frags are page-cache pages (because the sender used
`udp_sendmsg(MSG_SPLICE_PAGES)` without `SKBFL_SHARED_FRAG` ever being set on
the datagram path), the in-place AEAD decrypt writes plaintext into pages the
attacker still maps via the originating pipe โ i.e. into the page cache of
whatever readable file the attacker `splice()`'d from.
```
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ attacker process (uid=1001, no privs) โ
โ โ
โ splice(/etc/passwd, off=N, len=1) โ pipe โ
โ โโโโบ pipe buf (page-cache page of /etc/passwd) โ
โ โ
โ splice(pipe โ udp_sock to 127.0.0.1:4500) โ
โ kernel sets MSG_SPLICE_PAGES โ
โ โ โ
โ ip_append_data โ skb frag = same page-cache page โ
โ (pre-fix: SKBFL_SHARED_FRAG NOT set โ fast path eligible) โ
โ โ โ
โ loopback xmit โ udp_rcv โ udp_encap_rcv (UDP_ENCAP_ESPINUDP)โ
โ โ โ
โ xfrm_input โ esp_input โ no-COW fast path โ
โ โ โ
โ AES-GCM decrypt IN PLACE over page-cache page โ
โ โ โ
โ /etc/passwd page cache mutated โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
```
Plaintext written = `ciphertext_byte XOR keystream(K, IV)`. The attacker owns
the SA key, so the keystream is fully attacker-determined โ but solving "find K
such that AES-CTR outputs *specific bytes*" requires inverting AES.
For 1-byte targets at chosen offsets, that's not needed: brute-force the IV
against a fixed K until `keystream[0]` XORs the original byte to the desired
value. ~256 trials average per byte. ~30 ms per fire on a laptop.
## Demo
```text
=== Setup
=== Stage 1-3 โ page-cache write โ su exp โ drop SUID rsh โ restore /etc/passwd (all under sudo -u np)
[+] IV found (after 168 trials): a800000000000000 keystream[0]=0x01 โ plain=0x30
[+] post byte at offset 2729 = 0x30 (was 0x31, wanted 0x30) match=YES
/etc/passwd page cache:
exp:x:0000:1000:Audit dev account:/home/exp:/bin/bash
/etc/passwd restored:
exp:x:1000:1000:Audit dev account:/home/exp:/bin/bash
SUID rsh: -rwsr-xr-x 1 root root 821416 May 7 15:20 /var/tmp/.rs
=== Spawning root shell via SUID helper at /var/tmp/.rs
root@target:~# id
uid=0(root) gid=0(root) groups=0(root),...
root@target:~# head -3 /etc/shadow
root:*:20566:0:99999:7:::
daemon:*:20566:0:99999:7:::
bin:*:20566:0:99999:7:::
```
## Build & run
Tested on Ubuntu 26.04 LTS / Resolute (kernel `7.0.0-15-generic`).
```bash
sudo apt-get install -y libssl-dev gcc
sudo modprobe esp4 xfrm_user
gcc -O2 -Wall copyfail2.c -o copyfail2 -lcrypto
gcc -O2 rsh.c -o rsh
# Run as a sudo-group user; chain itself fires as a different unprivileged user.
./run.sh
```
`run.sh` expects:
- `aa-rootns` available at `/home/exp/aa-rootns-tools/aa-rootns` (single-binary
bypass for `kernel.apparmor_restrict_unprivileged_userns=1` โ sets up a
CLONE\_NEWUSER+CLONE\_NEWNET environment with a full 41-cap bitmap)
- two unprivileged user accounts: a `sudo`-group user (e.g. `exp`) for the
one-time `modprobe`/backup setup, and a clean-unpriv user (e.g. `np`) that
runs the actual exploit
- both users sharing the same known password (script defaults to `fuzz`)
These map onto the kCTF/lab-VM convention; tweak the script's `P=` and user
names for your target.
`copyfail2` itself takes ` ` and
performs a single 1-byte page-cache write. `run.sh` orchestrates the rest.
## Why hardened-kernel mitigations don't apply
Most modern userspace-LPE defenses are slab-/heap-shaped:
- `CONFIG_RANDOM_KMALLOC_CACHES`
- `CONFIG_INIT_ON_ALLOC_DEFAULT_ON`
- `CONFIG_SLAB_FREELIST_RANDOM`
- `CONFIG_HARDENED_USERCOPY`
- `CONFIG_SLAB_VIRTUAL` / SLAB\_BUCKETS
- KASLR
This is a **page-cache write**, not a slab UAF. None of the above touch it.
`kernel.apparmor_restrict_unprivileged_userns=1` (Resolute/Ubuntu's anti-userns
hardening) is bypassed by the `aa-rootns` harness via the
`crun โ chrome` AppArmor profile re-exec + Ambient cap launder.
The original Copy Fail's standard mitigation is `install algif_aead /bin/true`
in `modprobe.d` โ useless on this bug because `xfrm`/ESP is always built-in on
distros.
## Affected
Confirmed vulnerable as of 2026-05-07:
- Mainline 7.0.x and 7.1-rc2
- Resolute 7.0.0-15 (Ubuntu 26.04 LTS HWE kernel `7.0.0-15-generic`)
Any kernel that has all four `Fixes:` commits (i.e. โฅ late-2023 mainline) and
hasn't pulled `f4c50a4034` from `netdev/net.git` is in scope. As of writing
this, no major distro has shipped the backport.
## Disclosure
Bug was reported privately by Hyunwoo Kim and Kuan-Ting Chen and fixed in
mainline `netdev/net.git` on 2026-05-05 with `Cc: stable@vger.kernel.org`. This
repo demonstrates exploitation post-fix-publication. No new information about
the bug itself is disclosed here that isn't already in the public commit
message.
## Files
| | |
|---|---|
| `copyfail2.c` | Page-cache write primitive: brute-forces IV, builds ESP-in-UDP frame, splices target file's page-cache page into the UDP skb, fires the bug. |
| `rsh.c` | SUID-root โ `setresuid(0); execv("/bin/bash", argv)` helper. |
| `run.sh` | Orchestrates setup, mutation, escalation, SUID drop, and `/etc/passwd` restore. Drops you into an interactive root shell at the end. |
## Credits
The bug itself, the upstream fix, and the prior art that made the connection
obvious all belong to other people. This repo is just exploitation work on top
of their findings.
- **Hyunwoo Kim** ([@imv4bel](https://github.com/imv4bel)) and **Kuan-Ting Chen**
([@h3xrabbit](https://github.com/h3xrabbit)) โ reported and tested the bug,
authored / drove the upstream fix.
- **Steffen Klassert** โ IPsec maintainer, signed off and posted the fix to
`netdev/net.git` on 2026-05-05.
- **Brad Spengler** ([@spendergrsec](https://twitter.com/spendergrsec) /
grsecurity) โ pointed out the structural parallel between this bug and Copy
Fail (CVE-2026-31431) before most people had read the commit. The "copyfail
2: electric boogaloo" framing in this repo is a direct nod to that. Brad has
also been the one publicly noting that the standard `algif_aead` modprobe
mitigation for the original Copy Fail is a no-op on enterprise distros that
ship `CONFIG_CRYPTO_USER_API_AEAD=y` โ the same observation generalizes
here, except `xfrm`/ESP has no `=m` knob to fall back to.
- **Theori / Xint** โ original Copy Fail discovery and write-up. This bug is
the same primitive class in a different subsystem; the conceptual
vocabulary ("decrypt-in-place over splice'd page-cache pages") is theirs.
## References
- [Copy Fail (CVE-2026-31431) โ copy.fail](https://copy.fail/)
- [Theori/Xint write-up](https://xint.io/blog/copy-fail-linux-distributions)
- [Theori PoC](https://github.com/theori-io/copy-fail-CVE-2026-31431)
- [netdev/net.git fix `f4c50a4034`](https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net.git/commit/?id=f4c50a4034e62ab75f1d5cdd191dd5f9c77fdff4)
- [Brad Spengler / @spendergrsec](https://twitter.com/spendergrsec)