
Proof-only PoC for CVE-2026-98432, a Linux IPv4/TCP ireq_opt use-after-free: race trigger and heap-reuse oracle that confirms the UAF without code execution.
ireq_opt use-after-free PoCProof-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.
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.
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.
Per attempt:
ireq_opt; the target echoes the option in its SYN-ACK.inet_opt, the request
socket drops its pointer.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.
CAP_NET_RAW; the automatic RST filter needs
CAP_NET_ADMIN (root).accept()s.iptables/nftables for the bare-RST filter.--proof is self-contained on one host: it starts a loopback listener and
races it.
python3 poc_cve_2026_98432.py --selftest
sudo python3 poc_cve_2026_98432.py --check
Prints the running kernel, the fixed-release table, and relevant sysctls.
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.
sudo python3 poc_cve_2026_98432.py --probe <TARGET>:<PORT>
One SYN: reports whether the target is an MPTCP listener and whether it echoes the IPv4 option (required by the oracle).
sudo python3 poc_cve_2026_98432.py <TARGET>:<PORT> \
--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.
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.
| 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 <TARGET>:9000 -> <CLIENT>: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.
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.A disposable VM is safer for development. Build a vulnerable 6.12.112 kernel by reverting the two fix lines:
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)"