β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3401

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3401 Β· 2 files
FileTypeDescriptionSize
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 s pointers β†’ 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's a1->tcp_in[i] = NULL; kfree(s2) (:969-991) β†’ use-after-free read of a just-freed nat_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_testable
per-fix-DF-3401

Confirmed 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).