ip_fw3_ctl_nat_get_record walks all CPUs' lockless per-CPU NAT state with no synchronization; break-vs-continue truncation
Summary
get_record runs in netisr0 but RB_FOREACHs and scans alias pointer arrays of every other netisr CPU's cfg_nat - the per-CPU lockless structures their owning threads mutate - with no token or netmsg. Races yield garbage s pointers (stale/freed-heap contents in the records) or malformed-traversal panic; tcp_in[i] read races NULL-store+kfree -> UAF read. Also a break-vs-continue truncates show-all output. Root-issued GET_RECORD racing live NAT traffic on SMP: limited stale-heap disclosure to root or panic. Fix: dispatch per-CPU snapshot collection to the owning CPUs via netmsgs.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3401 Β· 2 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.1 KB | β raw | |
| verdict.json | β | 1.7 KB | view raw |
DF-3401 β ip_fw3_ctl_nat_get_record walks every CPU's lockless per-CPU NAT state with no serialization
What
sys/net/ipfw3_nat/ip_fw3_nat.c:585-694
(ip_fw3_ctl_nat_get_record): after dumping cpu0's ICMP states, the
function iterates
for (cpu = 0; cpu < ncpus; cpu++) {
nat_ctx = ip_fw3_nat_ctx[cpu];
... RB_FOREACH(s, state_tree, &the->rb_tcp_out) {...}
... for (i = 0; i < ALIAS_RANGE; i++) { s2 = a1->tcp_in[i]; ... }
}
from the calling thread (netisr0), touching the per-CPU, deliberately
lockless RB trees and alias pointer arrays of all other netisr
CPUs, which their owning netisr threads mutate concurrently
(ip_fw3_nat inserts/removes on packet processing;
nat_cleanup_func_dispatch frees expired entries on every CPU). No
token, no netmsg dispatch to the owning CPU, nothing. The file's own
header comment (:96-102) documents the lockless per-CPU design that
this routine violates.
Consequences while ipfw3 nat show state races live traffic:
- RB traversal of a tree being rebalanced/rotated β garbage
spointers βs->src_addr... reads of freed/foreign memory (stale-heap contents copied to the root caller as "state records"), or a walk that never terminates; tcp_in[i]/udp_in[i]read racing the cleanup callout'sa1->tcp_in[i] = NULL; kfree(s2)(:969-991) β use-after-free read of a just-freednat_state2.
Also, :534 uses break instead of continue when
nat_ctx->nats[n] == NULL, so with ioc_nat_id == 0 (show all) the
ICMP loop stops at the first unused NAT id β functional, but evidence
this path was never carefully reviewed.
Impact
Requires root to issue IP_FW_NAT_GET_RECORD while NAT traffic flows (6-CPU window). Worst case: kernel panic (malformed traversal) or limited stale-heap disclosure to the already-root caller. Low / likely. Not verified on the guest (Low β Phase V skipped per contract).
Fix
Dispatch the per-CPU snapshot collection to each owning CPU via a
netmsg (the file's own netisr_forwardmsg_all pattern), or take the
per-CPU state under a lock shared with the data path.
Fix verification
not_testableConfirmed kernel references
Detail
Evidence (decisive lines)
['findings/poc/DF-3401/README.md']
PoC changes
n/a (Phase V skipped for this Low finding per batch contract)
Verified recommended fix
Dispatch per-CPU snapshot collection to the owning CPUs via netisr netmsgs (the file's own netisr_forwardmsg_all pattern) instead of direct cross-CPU walks
Verdict
ip_fw3_ctl_nat_get_record (ip_fw3_nat.c:585-694) runs in netisr0 but walks the per-CPU lockless RB trees and alias pointer arrays of ALL other netisr CPUs with no token/netmsg -- racing the owning CPUs' state inserts/removals and the cleanup callout's kfree (:961-996). Race outcomes: RB traversal through rebalancing tree -> garbage s pointer -> stale/freed-heap contents copied into the root caller's records or malformed-traversal panic; tcp_in[i]/udp_in[i] read racing 'a1->tcp_in[i]=NULL; kfree(s2)' -> UAF read. Also :534 'break' where 'continue' is meant truncates show-all output. Root-issued observability racing live NAT traffic; Low/likely; Phase V skipped (Low).
No comments yet.