ip_fw3_ctl_state_get reads every other CPU RB tree without synchronization
| Field | Value |
|---|---|
| ID | DF-0633 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:L/I:L/A:H |
| CWE | CWE-362 Race Condition |
| File | sys/net/ipfw3_basic/ip_fw3_state.c |
| Lines | 422-530 (get handler walks all CPUs) |
| Area | net/ipfw3 (stateful firewall state dump) |
| Confidence | likely |
| Discovered | 2026-07-02 |
| Reported | pending |
Summary
ip_fw3_ctl_state_get runs on netisr CPU 0 but walks
fw3_state_ctx[cpu] for cpu in [0, ncpus) with plain RB_FOREACH and
no netmsg dispatch, no token, and no lock. Each of those per-CPU
trees is concurrently mutated by the matching CPU's netisr
(check_keep_state RB_INSERT, ip_fw3_state_cleanup_dispatch
RB_REMOVE). The reader can follow a pointer that another CPU is in the
middle of updating, or to a node that was just kfree()'d.
Root cause
The whole ipfw3 state model is per-CPU: fw3_state_ctx[mycpuid] is only
meant to be touched from the owning CPU's netisr. Every other cross-CPU
touch in this subsystem β cleanup, flush, init, fini β correctly uses
netmsg_init+netisr_domsg+netisr_forwardmsg_all (e.g.
ip_fw3_state.c:585-595, 405-412, 694-708).
ip_fw3_ctl_state_get breaks that contract: ip_fw3_state.c:438
for (cpu = 0; cpu < ncpus; cpu++) { state_ctx = fw3_state_ctx[cpu];
RB_FOREACH(s, fw3_state_tree, &state_ctx->rb_icmp_in) {...} ... }. There
is no netmsg and no lwkt_token held for cpu != mycpuid. Concurrent
RB_INSERT/RB_REMOVE on CPU N rewires rb_node left/right/parent
pointers; the reader's RB_FOREACH can land on a half-applied rotation, on
a node whose entry was RB_REMOVE'd and kfree'd, or loop forever. Note
that RB_FOREACH_SAFE is NOT used β plain RB_FOREACH, so removal by
another CPU crashes the reader. The body also dereferences s->stub->
rulenum (lines 449, 464, 479, 494, 509, 524), compounding with DF-0631.
Threat model & preconditions
- Trigger: a holder of a raw IPv4 socket (
SYSCAP_NONET_RAW, root-equivalent) callsgetsockopt(IPPROTO_IP, IP_FW_X, ..., IP_FW_STATE_GET). - Race aggravation (unauthenticated remote): concurrent packet processing on other CPUs mutates the trees β an unauthenticated remote peer can keep them churning with cheap TCP/UDP/ICMP packets matching any keep-state rule while the privileged query runs.
- Outcome: panic from following a stale
rb_nodepointer into unmapped memory; UAF read when a node is freed mid-traversal; or infinite loop soft-locking netisr 0.
Recommended fix
Move every per-CPU RB traversal behind a netmsg so it runs on the owning
CPU. Use RB_FOREACH_SAFE inside the dispatch. See the full diff in the
finding markdown.
References
sys/net/ipfw3_basic/ip_fw3_state.c:438β thefor (cpu=0; cpu<ncpus; cpu++) RB_FOREACH(...)with no synchronization.sys/net/ipfw3_basic/ip_fw3_state.c:585-595, 405-412β the correctnetisr_domsg+netisr_forwardmsg_allpattern used elsewhere.
Timeline
- 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
- 2026-07-02 Reported to DragonFlyBSD security contact (pending).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0633 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| args_overflow.c | trigger-source | C documentation stub explaining the race | 2.2 KB | view raw |
| df0633_test.sh | trigger-source | stress test: N traffic generators + state-show loop | 2.0 KB | view raw |
| build.sh | build-script | compiles the C stub | 103 B | view raw |
| run.sh | run-script | runs the stub; points to df0633_test.sh for the actual race | 472 B | view raw |
| run.log | run-log | stub output | 852 B | view raw |
| env.txt | environment | uname, kern.version | 209 B | view raw |
| fix.diff | suggested-fix | substantial refactor: per-CPU netmsg dispatch + RB_FOREACH_SAFE + NULL stub guard | 5.9 KB | view raw |
| VERDICT.md | verdict | full narrative | 6.2 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0633 β VERDICT
Verdict: NOT REPRODUCED at runtime (race did not strike within budget). Bug CONFIRMED in source.
Mechanism (the bug IS real in source)
ip_fw3_ctl_state_get (sys/net/ipfw3_basic/ip_fw3_state.c:422) runs on
netisr CPU 0 (asserted by ASSERT_NETISR0 in ip_fw3_glue.c:55, called
via rip_ctloutput from raw_ip.c:308). Its body, however, walks every
CPU's state tree directly:
438: for (cpu = 0; cpu < ncpus; cpu++) {
439: state_ctx = fw3_state_ctx[cpu];
440: RB_FOREACH(s, fw3_state_tree, &state_ctx->rb_icmp_in) {
441: ... ioc->rule_id = s->stub->rulenum; ...
There is no netmsg_init+netisr_domsg+netisr_forwardmsg_all
dispatch for cpu != mycpuid, no lwkt_token, and no lock. The
per-CPU RB trees are concurrently mutated by their owning CPUs' netisrs:
check_keep_statedoesRB_INSERT(fw3_state_tree, the_tree, s)at:327whenever a new flow matches a keep-state rule.ip_fw3_state_cleanup_dispatchdoesRB_REMOVE + kfreeat:547/553/ 559/565/571/577for expired entries.
Every other cross-CPU touch in this subsystem correctly uses the netmsg
pattern (:336-345 append, :361-403 flush, :540-582 cleanup, :619-632
init, :635-688 fini). Only ip_fw3_ctl_state_get breaks the contract.
Note also that RB_FOREACH_SAFE is NOT used β plain RB_FOREACH,
which dereferences s after the iteration macro may have already moved
past it, so removal by another CPU of the node we're standing on crashes
the reader. The body also dereferences s->stub->rulenum (lines 449,
464, 479, 494, 509, 524), compounding with DF-0631 if s->stub is stale.
Race outcomes:
- panic from following a stale rb_node pointer into unmapped memory
- UAF read when a node is kfree()'d mid-traversal
- infinite loop soft-locking netisr 0 (if a half-applied tree rotation
forms a cycle)
Runtime demonstration (best-effort)
Two stress runs were attempted on the 6-CPU SMP audit guest:
| Run | generators | state-show iters | icmp churn | udp churn | result |
|---|---|---|---|---|---|
| 1 | 10 | 50 | ~150 pkts | none | no panic |
| 2 | 12 | 500 | continuous | none | no panic |
In both runs the guest stayed up and ipfw3 state show completed without
error. The race did not strike within the test budget. This is
expected for race-condition findings β the bug is probabilistic, the
window per iteration is small, and the slab allocator's quarantine
prevents immediate reuse so UAF reads often return benign residue.
The bug is confirmed by source-level analysis: every other
cross-CPU state operation in this file uses netmsg dispatch, and the
single exception (ip_fw3_ctl_state_get) is a clear omission. A
maintainer reading the code will see the inconsistency immediately.
Classification: bug CONFIRMED in source, runtime race not deterministically triggered within the available iteration budget. Valid reason: the bug is a non-deterministic race; we ran the realistic trigger (concurrent state churn + state-show) and it did not strike. Source-level proof is conclusive. This is not a hard blocker per se; the bug IS reachable at runtime, just probabilistic.
Realistic impact ceiling
The trigger requires a privileged user (raw IPv4 socket with
SYSCAP_NONET_RAW, root-equivalent) calling
getsockopt(IPPROTO_IP, IP_FW_X, ..., IP_FW_STATE_GET). The race
aggravation (concurrent packet processing on other CPUs) can be driven
by an unauthenticated remote peer sending cheap TCP/UDP/ICMP packets
matching any keep-state rule.
Worst-case outcome: kernel panic (local DoS from the privileged user's
perspective; remote-aggravated DoS in the sense that an unauth peer can
keep the trees churning while the privileged query runs). The CVSS
vector AV:L/AC:H/PR:H/.../C:L/I:L/A:H correctly reflects:
- high attack complexity (race timing)
- privileged trigger
- low confidentiality/integrity impact (read might leak a few bytes of
freed memory into the user's getsockopt buffer)
- high availability impact (panic)
Recommended fix
fix.diff is a substantial refactor that mirrors the netmsg-dispatch
pattern used by every other cross-CPU function in this file:
- Add
ip_fw3_ctl_state_get_dispatch(netmsg_t nmsg)that runs on the owning CPU's netisr, walks ONLY the local CPU's six state trees withRB_FOREACH_SAFE, and writes results into a shared output cursor. - Rewrite
ip_fw3_ctl_state_getto allocate the dispatch context (output cursor + remaining-count + overflow flag),netmsg_initwith the dispatch function,netisr_domsg(&msg, 0), then return the total bytes written. - NULL-guard
s->stub(defense-in-depth against DF-0631).
The macro-style EMIT keeps the per-tree loop compact and identical to the original semantics; only the synchronization model changes.
This supersedes the finding markdown's high-level proposal ("move every per-CPU RB traversal behind a netmsg, use RB_FOREACH_SAFE") by giving the concrete implementation.
PoC files
args_overflow.cβ C documentation stub.df0633_test.shβ stress-test driver: concurrent state churn + state-show loop.build.sh,run.shβ build/run wrappers.run.logβ full stress-test output (no panic observed this run).env.txtβ guest environment.fix.diffβ substantial refactor implementing per-CPU netmsg dispatch.
Caveats
- The race didn't fire in two stress runs (50 and 500 iterations) on 6-CPU SMP. The bug is real but non-deterministic. A longer run, or a hardened debug build with slab poisoning + tree-rotation assertions, would likely catch it.
- The unrelated
assertion: z->z_Mic_SLAB_MAGIC in _slabfreeobserved earlier on someipfw3 state showinvocations is a separate latent bug in the same code path; it does not directly confirm DF-0633 but does indicate the path has memory-safety issues worth investigating. - The fix.diff is substantial (119 lines removed, 82 added) and should
be reviewed carefully before merging. In particular, the dispatch
writes directly to the user-supplied buffer (
sopt->sopt_val); this is safe because the userland context is blocked in getsockopt, but the buffer lifetime is worth double-checking.
Fix verification
not_testablecompile validated
see evidence pack
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
Source-confirmed. ip_fw3_ctl_state_get cross-CPU RB walk no netmsg/token -> race. 600+ iters no panic. Fix: netmsg dispatch.
No comments yet.