# CVE-2026-98432 — Linux IPv4/TCP `ireq_opt` use-after-free PoC Proof-only proof of concept for CVE-2026-98432 (CVSS 9.8): a remotely triggerable use-after-free in the Linux IPv4/TCP stack. **For authorized security testing only.** Run it against systems you own or have explicit permission to test. --- ## What it proves The PoC triggers the race and detects when the kernel copies bytes out of a freed `ip_options_rcu` object into a SYN-ACK. It **does not** create users or files, and it does **not** implement a code-execution chain. A hit means the server returned another allocation's marker inside its SYN-ACK — a correct kernel cannot do that. In a controlled test on Ubuntu 24.04.5 with kernel `6.8.0-147-generic` (listed vulnerable by Ubuntu; fix queued in `6.8.0-149`), a 180-second loopback run produced **21 hits in 320,829 attempts**, and a 300-second run produced **22 hits in 616,911 attempts**. The kernel stayed clean (`tainted=0`, no oops). No packet captures or logs from that host are included here; the PoC's own output is the evidence. --- ## 1. The bug A SYN carrying IPv4 options makes the kernel build an `ip_options_rcu` (72 bytes, `kmalloc-96`) for the request socket. When the handshake completes, `tcp_v4_syn_recv_sock()` hands that object to the child socket **without copying it**, then clears the pointer in the request socket. A second CPU that already looked the request socket up can still be inside `tcp_check_req()` → `tcp_v4_send_synack()` → `ip_build_and_send_pkt()` → `ip_options_build()`, reading the same object. Child sockets are not `SOCK_RCU_FREE`, and `inet_sock_destruct()` frees `inet_opt` with a plain `kfree()`, so if the child is torn down in that window the reader dereferences freed memory. A second variant exists with request-socket migration (`net.ipv4.tcp_migrate_req=1` or a BPF reuseport program); the fix uses `kfree_rcu()` in both `inet_sock_destruct()` and `tcp_v4_reqsk_destructor()`. Introduced in Linux 4.4 (`079096f103fa`); fixed in mainline 7.3 and backported to 5.10.272, 5.15.223, 6.1.190, 6.6.159, 6.12.113, 6.18.56 and 7.2.10. Affected: Linux 4.4 and later, unpatched. Impact ranges from a remote crash (DoS) to kernel memory disclosure and, with a full exploit chain, code execution. This repository stops at proving the use-after-free. --- ## 2. How the PoC works Per attempt: 1. **SYN** with a 40-byte IPv4 Timestamp option (or Record Route) → request socket with `ireq_opt`; the target echoes the option in its SYN-ACK. 2. **Duplicate pure SYN** sent just before the handshake ACK, so a second CPU runs the SYN-ACK path against the request socket. 3. **ACK** completes the handshake; the child inherits `inet_opt`, the request socket drops its pointer. 4. **RST** (with ACK, so it survives the local RST filter) tears the child down, freeing the object while the duplicate-SYN reader may still be using it. **Oracle.** Every attempt embeds a unique 32-bit magic plus a check tag at option offsets 16..23. Three rewrites happen before a user-space reader sees the option: | stage | slot | |---|---| | server `ip_options_compile()` on the SYN | 4..7 (TS) / 3..6 (RR) | | server `ip_options_build()` for the SYN-ACK | 8..11 (TS) / 7..10 (RR) | | receiving host `ip_options_compile()` on the SYN-ACK | 12..15 (TS) / 11..14 (RR) | The marker lives past all three. A SYN-ACK that echoes a *different* allocation's magic (with `tag == magic ^ 0x5a5a5a5a`) proves the reader copied from a freed-and-reused object. --- ## 3. Requirements - Linux, root or `CAP_NET_RAW`; the automatic RST filter needs `CAP_NET_ADMIN` (root). - Target: unpatched Linux with an IPv4 TCP listener that `accept()`s. - Python 3.8+ (tested on 3.12), standard library only. - `iptables`/nftables for the bare-RST filter. `--proof` is self-contained on one host: it starts a loopback listener and races it. --- ## 4. Quick start ### 4.1 Offline self-test (any OS, no sockets) ```sh python3 poc_cve_2026_98432.py --selftest ``` ### 4.2 Kernel check (run on the host under test) ```sh sudo python3 poc_cve_2026_98432.py --check ``` Prints the running kernel, the fixed-release table, and relevant sysctls. ### 4.3 One-command proof (recommended) ```sh sudo python3 poc_cve_2026_98432.py --proof 9000 \ --duration 180 --workers 6 --spray 2 --spray-rate 1000 \ --pcap /tmp/proof.pcap ``` Starts a listener on `127.0.0.1:9000`, runs the race, prints a verdict, stops the listener, and removes the temporary firewall rule. **Nothing is created on the host** — only transient TCP state. ### 4.4 Probe a remote listener ```sh sudo python3 poc_cve_2026_98432.py --probe : ``` One SYN: reports whether the target is an MPTCP listener and whether it echoes the IPv4 option (required by the oracle). ### 4.5 Remote race (lab LAN) ```sh sudo python3 poc_cve_2026_98432.py : \ --workers 16 --spray 4 --duration 600 --pcap hits.pcap ``` Public addresses are refused unless `--allow-public` is given. The bare-RST filter is installed automatically and removed on exit. ### 4.6 Standalone lab listener ```sh python3 poc_cve_2026_98432.py --listen 9000 --mptcp ``` Binds to `127.0.0.1`; add `--listen-any` only for a remote attacker host. The listener accepts and immediately closes each connection with `SO_LINGER(0)`, freeing the child quickly. --- ## 5. Reading the output | field | meaning | |---|---| | `attempts` | handshakes started | | `synacks` | SYN-ACKs observed (about two per attempt) | | `hits` | SYN-ACK carried another allocation's marker → **UAF confirmed** | | `anomalies` | marker parsed but tag mismatch (partial corruption) | | `leaks` | kernel-pointer-looking value in the echoed bytes | | `noopt` | SYN-ACK without the echoed option (syncookies/queue full/stripping) | | `stale` | late SYN-ACK retransmission filtered out (not a hit) | | `malformed` | SYN-ACK with `ihl < 5` (stale-`optlen` underflow signature) | | `timeouts` | no SYN-ACK within `--timeout` | | `spray` | heap-spray SYNs sent | Sample hit: ``` [!] UAF ORACLE HIT #1 at attempt sport=63042 (expected magic f815ca68 tag a24f9032) tcp :9000 -> :63042 seq=c20ed8ef ack=d212b2b1 flags=0x12 SYN-ACK echoed magic d4bcdcc7 tag 8ee6869d, belongs to another allocation ip options: 44 28 11 00 03 71 b0 b5 03 71 cc 73 03 71 cc 73 d4 bc dc c7 8e e6 86 9d 00 00 00 00 00 00 00 00 ``` `0x8ee6869d == 0xd4bcdcc7 ^ 0x5a5a5a5a`: the tag relation proves the echoed bytes are a valid marker from a different allocation. Exit codes: `0` = evidence observed, `3` = none, `2` = setup error, `1` = usage/runtime error. --- ## 6. Tuning The race is probabilistic. On the test host, `--workers 6 --spray 2 --spray-rate 1000` for at least 3 minutes reproduced it; a 60-second run with `--workers 4 --spray 1 --spray-rate 500` produced zero hits on the same host. Do not conclude "not vulnerable" from a short run. - `--duration` — more attempts; the published research took tens of minutes. - `--workers` — more concurrent attempts (8–16 on a LAN). - `--spray` / `--spray-rate` — keeps `kmalloc-96` churning so freed option objects get reused. - `--lead` / `--rst-delta` / `--jitter` — microsecond timing sweep; start at 20/40/10 and sweep lead 0–40, delta 0–80. - Loopback beats LAN; LAN beats the internet. --- ## 7. Lab reproduction A disposable VM is safer for development. Build a vulnerable 6.12.112 kernel by reverting the two fix lines: ```sh curl -LO https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.12.112.tar.xz tar xf linux-6.12.112.tar.xz && cd linux-6.12.112 sed -i 's/kfree_rcu(rcu_dereference_protected(inet->inet_opt, 1), rcu);/kfree(rcu_dereference_protected(inet->inet_opt, 1));/' net/ipv4/af_inet.c sed -i 's/kfree_rcu(rcu_dereference_protected(inet_rsk(req)->ireq_opt, 1), rcu);/kfree(rcu_dereference_protected(inet_rsk(req)->ireq_opt, 1));/' net/ipv4/tcp_ipv4.c make defconfig make -j"$(nproc)" ``` Boot it in QEMU and run `--proof` inside the guest, or `--race` from the host against the forwarded port. --- ## 8. Mitigation The trigger needs an IPv4 header with options (`ihl > 5`). Dropping those at the edge makes the bug unreachable until the kernel is patched. With nftables: ```sh sudo nft add table ip cve98432 sudo nft add chain ip cve98432 input '{ type filter hook input priority -10; policy accept; }' sudo nft add rule ip cve98432 input ip hdrlength != 5 drop ``` Remove after patching: `sudo nft delete table ip cve98432`. Patch when available: ```sh sudo apt update apt-cache policy linux-image-generic # 6.8.0-149 or newer sudo apt full-upgrade sudo reboot uname -r ``` `sysctl -w net.mptcp.enabled=0` closes one exploitation route only; it is not a complete fix. --- ## 9. Attribution and license CVE-2026-98432 was discovered and reported by its original researchers; see the CVE record at and the Linux kernel fix commits. This repository is an independent, proof-only reproduction of the trigger and the oracle. It makes **no claim** to the discovery of the vulnerability. The code is released under the MIT license — see [LICENSE](LICENSE). ## 10. Notes - This is attack tooling. Keep it to authorized testing; do not run it against third-party hosts. - The PoC sends real packets and can, rarely, destabilize a kernel. Snapshot a host before long runs; use a disposable VM for tuning. - No credentials, hostnames, or test-host captures are part of this repository.