
PoC, non-destructive version scanner, KASAN crash verifier, and Suricata/Sigma detection rules for the Linux kernel TCP/IPv4 IP-options use-after-free (CVE-2026-98432).
Check · Exploit · Detection · PCAP · Impact · Write-up
Disclaimer: This tool is provided for authorized security testing and educational purposes only. Only use against systems you own or have explicit written permission to test. Unauthorized access to computer systems is illegal.
CWE-416 | Use-After-Free | Linux 4.4 through pre-fix versions | Remote pre-auth, no user interaction
A race condition in the Linux kernel's TCP/IPv4 connection handling allows a remote unauthenticated attacker to trigger a use-after-free by racing SYN retransmission (carrying IP options) against socket destruction. The ip_options_rcu allocation is freed via kfree() when a child socket is destroyed, but a concurrent SYN retransmit on another CPU still holds an RCU read-side reference to the freed memory. When ip_options_build() reads the stale optlen field and copies data into the SYNACK, it leaks kernel heap contents to the attacker. With controlled heap grooming, this enables full remote kernel code execution. CVSS 9.8. Lost races do not crash the kernel, allowing unlimited retries.
Author: al4n4n (Dadb0d)
Write-up: blog.al4n4n.com/CVE-2026-98432-Linux-Kernel-TCP-Race-UAF/
Fixed in: 5.10.272, 5.15.223, 6.1.190, 6.6.159, 6.12.113, 6.18.56, 7.2.10, 7.3+
The vulnerability lives in tcp_v4_syn_recv_sock() in net/ipv4/tcp_ipv4.c. When a TCP connection completes the three-way handshake, this function creates a child socket from the request socket. It transfers the IP options by moving a pointer rather than copying:
inet_opt = rcu_dereference(ireq->ireq_opt);
RCU_INIT_POINTER(newinet->inet_opt, inet_opt); // pointer moved, not copied
// ...
ireq->ireq_opt = NULL;
The inet_opt pointer now belongs to the child socket. When that socket is later destroyed, inet_sock_destruct() calls kfree(inet_opt). But the request socket's destructor (tcp_v4_reqsk_destructor()) also calls kfree(ireq_opt) using the same pattern. The fix changes both to kfree_rcu(), deferring the free until the RCU grace period ends.
The race window: after the child socket takes ownership and before the request socket clears its pointer, a retransmitted SYN on another CPU enters tcp_check_req() → tcp_v4_send_synack() → ip_build_and_send_pkt() → ip_options_build(), which reads the freed ip_options_rcu:
memcpy(iph + sizeof(struct iphdr), opt->__data, opt->optlen);
If the freed memory has been reclaimed by the SLUB allocator and overwritten, opt->optlen may contain a larger value, causing an over-read that copies kernel heap bytes into the SYNACK packet's IP options field. These bytes are sent to the attacker.
The ip_options_rcu structure (with a Record Route option) is 72 bytes, placing it in the kmalloc-96 SLUB cache. This cache is shared with many common kernel allocations, making heap grooming straightforward.
kfree() call (pre-fix)ip hdrlength > 5| File | Purpose |
|---|---|
setup.py | Lab environment: builds KASAN-enabled kernel, boots QEMU VM |
check.py | Non-destructive scanner: kernel version fingerprinting via SSH |
poc.py | Race condition trigger: SYN+IP options → handshake → RST → retransmit |
verify.py | Crash verification dashboard with KASAN report parsing |
detection/cve-2026-98432.rules | Suricata rules for network-level detection |
detection/cve-2026-98432.yml | Sigma rule for KASAN/kernel crash log detection |
capture/CVE-2026-98432.pcap | Synthetic PCAP of exploit chain (30 packets, plain TCP) |
poster.png | Repository poster image |
CVE-2026-98432/
├── README.md
├── LICENSE
├── poster.png
├── setup.py # Lab environment setup (QEMU + KASAN kernel)
├── check.py # Non-destructive vulnerability scanner
├── poc.py # Race condition trigger (requires root)
├── verify.py # Crash verification dashboard
├── capture/
│ ├── CVE-2026-98432.pcap # Synthetic exploit PCAP
│ └── README.md # PCAP documentation
└── detection/
├── cve-2026-98432.rules # Suricata detection rules
└── cve-2026-98432.yml # Sigma detection rule
# Build KASAN-enabled kernel and boot QEMU VM (first run ~30 min)
python3 setup.py
# Re-boot existing lab
python3 setup.py --boot-only
# Tear down
python3 setup.py --teardown
# Scan a host for the vulnerability
python3 check.py 192.168.1.100
# JSON output for pipeline integration
python3 check.py 192.168.1.100 --json
# Custom SSH port
python3 check.py 192.168.1.100 --ssh-port 10022
# Trigger the race condition (requires root for raw sockets)
sudo python3 poc.py <target-ip> <target-port>
# Increase parallelism and attempts
sudo python3 poc.py <target-ip> <target-port> --threads 4 --attempts 2000
# Full verification with dashboard output
sudo python3 verify.py <target-ip> <target-port> --ssh-port 10022
# More rounds for higher confidence
sudo python3 verify.py <target-ip> <target-port> --rounds 10
╔══════════════════════════════════════════════════════════════╗
║ CVE-2026-98432 — TCP Race Condition UAF ║
║ Non-Destructive Vulnerability Scanner ║
╚══════════════════════════════════════════════════════════════╝
target: 192.168.1.100
time: 2026-11-01T12:00:00Z
kernel: 6.12.112
ssh: SSH-2.0-OpenSSH_9.2 Debian-...
distro: Debian
[+] VULNERABLE (confidence: high)
vulnerable (6.12.x fixed in 6.12.113)
╔══════════════════════════════════════════════════════════════╗
║ CVE-2026-98432 — TCP Race Condition UAF ║
║ Proof of Concept ║
╚══════════════════════════════════════════════════════════════╝
target: 192.168.1.100:9999
source: 192.168.1.1
threads: 2
attempts: 500
ip option: Record Route (39 bytes → kmalloc-96)
[>] Phase 1: Heap spray (filling kmalloc-96 freelist)...
[+] Heap spray complete
[>] Phase 2: Race condition trigger...
[500/500] race windows opened: 487