
Ansible role and Python script that detect Linux kernel exposure to CVE-2026-98432 and apply an nftables rule dropping inbound IPv4 SYNs carrying IP options.
A use-after-free in the Linux kernel's IPv4 TCP code
(CVE-2026-98432, CVSS 9.8):
IPv4 options saved off an incoming SYN can be freed while another CPU is
still reading them to build a SYNACK, because two destructor paths free
them with a plain kfree() instead of waiting for an RCU grace period.
The upstream fix is
e3f33b0a1d89
("ipv4: free inet_opt and ireq_opt after an RCU grace period").
As of 2026-10-29 no fixed kernel is released for any Ubuntu series. Every affected status on Ubuntu's tracker reads "work in progress" or plain vulnerable, and the page lists no mitigation. This pack is the mitigation.
Both racing paths read ireq->ireq_opt, the IPv4 options the kernel saved
from the incoming SYN (tcp_v4_save_options()): the SYNACK build reads
them while child-socket teardown can free them, and request-socket
migration (net.ipv4.tcp_migrate_req=1, or a
BPF_SK_REUSEPORT_SELECT_OR_MIGRATE program) shares then frees the same
options. When a SYN carries no IPv4 options, ireq_opt is NULL: there is
nothing to free and nothing to read. We read the fix commit before relying
on that; both halves start from a SYN that carried IPv4 options.
So the pack installs one nftables rule that drops inbound IPv4 TCP SYNs carrying IP options, in a table of its own so it never collides with Docker's, the substrate's or the distribution's rules:
table inet samp_cve_2026_98432 {
chain prerouting {
type filter hook prerouting priority -300; policy accept;
ip hdrlength > 5 tcp flags & (syn | ack) == syn counter drop \
comment "CVE-2026-98432: drop inbound IPv4 SYN carrying IP options"
}
}
Verified on a real kernel before release: an IPv4 SYN carrying options hits the counter and is dropped; a plain SYN passes untouched. TCP traffic almost never carries IP options (several option types are dropped by many networks as a matter of course, per RFC 7126), so the operational cost of the rule is close to nil.
pve kernel,
which Ubuntu's page does not cover) report unknown: read their
advisories. The rule itself would protect any Linux host; the verdict
tables are what is Ubuntu-only.The rule lives in kernel memory and is not persisted to disk. A
mitigation pack must not write to persistence paths (units, cron), and an
nftables include file is only loaded where the distribution's nftables
service is enabled, which on these estates it is not uniformly. So: after
a reboot, a detection reads the rule absent (rule_live=0,
persistent=0 on every result line) and the next apply restores it.
Re-running the apply after reboots is the operating model, and the result
line never claims otherwise.
Read-only, stages itself in a private temporary directory it removes, and reports:
uname -r, mapped to its package version)
against Ubuntu's fixed versions per release and flavour. No fix released
for the box's release: no_fix. A fixed kernel released but not
running: vulnerable, with restart_needed=1 when it is installed and
waiting for a reboot (an installed fix is never reported fixed).fixed with workaround: true on
the result line while the rule covers an unfixed kernel; plain fixed
once the running kernel is fixed.not_affected for releases the tracker clears, unknown for releases
and distributions the tables do not cover.0 clean (fixed, not_affected), 2 exposed or
undecided (no_fix, vulnerable, unknown: never clean), 1 the run
itself failed, 130 interrupted.syn_options_mitigation/files/samp_common.py is a byte-for-byte copy of
samp-common 1.0.4 (the dpkg, rpm and apk version
comparisons, the result line, config loading). Its sha256 is in
samp.yaml, and tests/test_samp_pack.py fails if the copy differs. Never
edit it here; re-vendor it with samp-common's scripts/vendor.py.samp/v<version> (this one:
samp/v1.0.1), signed with a hardware-backed key. The substrate
imports a signed tag, never a branch.One script step, one result line. While the running kernel is unfixed, it
loads the rule; once a fixed kernel is running, it removes the rule and
its table. It edits no files beyond the script, library and config it
deploys (declared in samp.yaml touches), so the result line's backup
list is empty. Removing the mitigation by hand is one command:
nft delete table inet samp_cve_2026_98432
ansible-playbook -i inventory play.yml # a play that runs the role
python3 syn_options_mitigator.py --config syn_options_config.yaml # detect
sudo python3 syn_options_mitigator.py --config syn_options_config.yaml --apply
When Ubuntu releases fixed kernels, paste the versions from the tracker
into syn_options_fixed_versions in the role defaults (the work-in-progress
versions are already there in a comment) and release the change as the next
signed tag; detection then starts reporting vulnerable/restart_needed
against them, and the apply removes the rule from boxes that reboot into a
fixed kernel.
python3 -m pytest tests/
Unit tests fake every external command; the contract test
(tests/test_samp_pack.py, samp-common's) checks the manifest against the
role, runs a real detection from a scratch directory and holds the apply
entry to the one-result-line and needrestart rules.
MIT. See LICENSE.
PRs and improvements welcome -> please fork and submit a merge request.