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

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) calls getsockopt(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_node pointer into unmapped memory; UAF read when a node is freed mid-traversal; or infinite loop soft-locking netisr 0.

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

Timeline

  • 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
  • 2026-07-02 Reported to DragonFlyBSD security contact (pending).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0633 Β· 10 files
FileTypeDescriptionSize
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
VERDICT.md verdict full narrative
↓ download 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_state does RB_INSERT(fw3_state_tree, the_tree, s) at :327 whenever a new flow matches a keep-state rule.
  • ip_fw3_state_cleanup_dispatch does RB_REMOVE + kfree at :547/553/ 559/565/571/577 for 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)

fix.diff is a substantial refactor that mirrors the netmsg-dispatch pattern used by every other cross-CPU function in this file:

  1. 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 with RB_FOREACH_SAFE, and writes results into a shared output cursor.
  2. Rewrite ip_fw3_ctl_state_get to allocate the dispatch context (output cursor + remaining-count + overflow flag), netmsg_init with the dispatch function, netisr_domsg(&msg, 0), then return the total bytes written.
  3. 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 _slabfree observed earlier on some ipfw3 state show invocations 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_testable

compile 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.