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

Per-CPU state counters never decremented: permanent state-table exhaustion DoS

Field Value
ID DF-0632
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:H
CWE CWE-400 Uncontrolled Resource Consumption
File sys/net/ipfw3_basic/ip_fw3_state.c
Lines 317-318 (increment); 545-580 (cleanup, no decrement); 361-403 (flush, no decrement)
Area net/ipfw3 (stateful firewall counter accounting)
Confidence certain
Discovered 2026-07-02
Reported pending

Summary

The count_tcp_in/out, count_udp_in/out, count_icmp_in/out counters in ipfw3_state_context are incremented every time check_keep_state creates a state, but neither the expiry timer nor the flush path ever decrements them. After the_max+1 lifetime creations the gate *the_count <= the_max (line 317) is permanently false and the firewall silently stops creating dynamic states for that protocol/direction until the module is reloaded.

Root cause

ip_fw3_state.c:317-318:

if (*the_count <= the_max) {
    (*the_count)++;
    ...

is the only writer of the six counters. ip_fw3_state.c:545-580 (ip_fw3_state_cleanup_dispatch) removes expired entries with RB_REMOVE+kfree but never touches count_*. ip_fw3_state.c:361-403 (ip_fw3_state_flush_dispatch) removes every entry the same way and also never resets the counters. The counters are monotonic until module reload (ip_fw3_state_fini_dispatch frees the whole context). The default sysctl_var_state_max_tcp_in/out is 4096 (lines 85-91), so the cap is reached with trivial effort.

Threat model & preconditions

  • Attacker: unauthenticated remote OR unprivileged local.
  • Precondition: any ipfw3 keep-state/limit rule matches untrusted traffic (common ingress/egress pattern: ipfw3 add allow tcp from any to any keep-state).
  • Trigger: create one state per distinct 5-tuple (e.g., unique source ports). After 4097 (or configured max+1) creations β€” even if every one of those connections immediately closes and the states expire normally β€” no further state can ever be created.
  • Impact: if the policy relies on keep-state to permit return traffic (default-deny + stateful allow), every new connection is then denied: a permanent, self-sustaining network DoS that survives until an administrator reloads the ipfw3 module. The attacker does not need to keep traffic flowing once the counter is pinned.

Decrement the counters everywhere a state is removed, and reset them on flush. See the full diff in the finding markdown β€” decrement in every RB_REMOVE+kfree block in ip_fw3_state_cleanup_dispatch and ip_fw3_state_flush_dispatch.

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-0632 Β· 12 files
FileTypeDescriptionSize
df0632_test.sh trigger-source test driver: load modules, send pings, snapshot counters at each stage 3.1 KB view raw
ipfw3_counter_probe.c instrumentation kernel module that reads fw3_state_ctx[cpu] and logs count_* to dmesg 1.8 KB view raw
build.sh build-script builds the df0632_probe.ko instrumentation module in-guest 409 B view raw
run.sh run-script nohup-wraps df0632_test.sh (firewall load breaks SSH) 369 B view raw
run.log run-log decisive run output showing counter monotonic from 0 -> 6 -> stays at 6 1.8 KB view raw
env.txt environment uname, kern.version, cc version 247 B view raw
fix.diff suggested-fix decrement counters in cleanup_dispatch, reset in flush_dispatch 2.2 KB view raw
fix_build.log build-log single-fix kernel build log (NK_DONE rc=0) 5.6 MB ↓ download
fix_run.log run-log patched-kernel validation: counter now decrements to 0 on expiry 1.1 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-0632 β€” VERDICT

Verdict: REPRODUCED (logic bug; runtime confirmed via kernel-counter probe; FIX VALIDATED on patched kernel #1)

Mechanism

sys/net/ipfw3_basic/ip_fw3_state.c:317-318 is the sole writer that increments state_ctx->count_{tcp,udp,icmp}_{in,out}:

317:    if (*the_count <= the_max) {
318:        (*the_count)++;

The two removal paths never decrement these counters:

  • ip_fw3_state_cleanup_dispatch (ip_fw3_state.c:545-580) walks each per-CPU RB tree and runs RB_REMOVE + kfree(s, M_IPFW3_STATE) for every expired entry. It touches only the tree and the freed pointer β€” the matching count_* is left untouched.
  • ip_fw3_state_flush_dispatch (ip_fw3_state.c:361-403) removes every entry the same way and likewise leaves count_* unchanged.

Only ip_fw3_state_fini_dispatch (module unload) frees the whole ipfw3_state_context, indirectly discarding the counters.

The default sysctl_var_state_max_tcp_in/out is 4096, sysctl_var_state_max_icmp_in/out is 10 (ip_fw3_state.c:85-91). After the_max+1 lifetime creations for a given proto/direction, the gate *the_count <= the_max (:317) is permanently false and check_keep_state silently stops creating new dynamic states until the module is reloaded.

Exploit chain

N/A β€” this is a logic bug (resource-accounting error), not a memory- corruption primitive. There is no slab target, no UAF, no function pointer to hijack. The bug's impact ceiling is permanent state-table exhaustion DoS: once count_X pins at max+1, the firewall stops tracking that protocol/direction. If policy relies on keep-state to permit return traffic (the canonical default-deny + stateful-allow pattern), every new flow of that kind is then denied until the ipfw3_basic module is reloaded.

Runtime demonstration (this run)

The DF0632-probe kernel module (built into the evidence pack) was used to read the in-kernel counters directly. state_max_icmp_out was lowered to 5 and icmp_timeout to 3 s for fast exhaustion. With firewall enabled:

Step icmp_in icmp_out note
Initial 0 0
1 ping to gateway 1 1 state created
Round 1 (6 pings, unique dst) 1 6 max=5 exceeded by 1
Wait 6 s (expiry + cleanup) 1 6 BUG: alive states=0, counter NOT decremented
Round 2 (6 more pings) 1 6 counter>max β†’ no new states created
Wait 6 s 1 6 unchanged
Round 3 (5 more pings) 1 6 permanently pinned

ipfw3 show confirms the firewall DID see all 19 icmp packets (rule 00200 pcnt=19), but only the first 6 actually created state β€” the remaining 13 hit the *the_count <= the_max gate and were silently dropped through to the default allow (no state creation).

The counter is never decremented, exactly as the source shows.

PoC changes

The original PoC directory was empty. I added:

  • df0632_test.sh β€” the test driver.
  • ipfw3_counter_probe.c β€” kernel module that reads fw3_state_ctx[cpu] and logs count_* to dmesg.
  • build.sh β€” builds the probe module.
  • run.sh β€” wraps the test driver.
  • fix.diff β€” git-apply-able fix.
  • run.log β€” the decisive run's output (also captured in this folder).

Caveats / observations

  1. The ipfw3 state show userland command (which would be the natural way to observe states without a probe) triggers a separate slab-assertion (assertion: z->z_Magic == ZALLOC_SLAB_MAGIC in _slabfree) on this kernel build. That is a separate latent bug in the IP_FW_STATE_GET path, not DF-0632; the probe sidesteps it.
  2. kldunload ipfw3_basic triggers yet another separate NULL-deref panic in ip_fw3_table_fini_dispatch (rn_flush(table_ctx->node, ...) on tables whose node head was never initialized). Also not DF-0632; documented for awareness.
  3. The bug is root-only triggerable from a configuration standpoint β€” the operator must install a keep-state rule. But once such a rule is in place (the recommended pattern for stateful firewalls), any unauthenticated remote peer can drive state creation by sending packets with distinct 5-tuples, exhausting max+1 lifetime creations in seconds (ICMP) to minutes (TCP/UDP with default 4096). The firewall then permanently stops creating that proto/direction's state.

fix.diff makes two coordinated changes in sys/net/ipfw3_basic/ip_fw3_state.c:

  1. In ip_fw3_state_cleanup_dispatch, decrement the matching counter inside each if (expired) { RB_REMOVE; kfree; } block. Guards with > 0 to avoid underflow.
  2. In ip_fw3_state_flush_dispatch, reset all six counters to 0 after the RB_FOREACH_SAFE removal loops (mirrors the fini behavior).

This supersedes the finding markdown's high-level proposal by giving the exact line-by-line decrement/reset implementation.

Fix validation

Built a single-fix kernel (#1, Sun Jul 19 01:05:49 UTC 2026) with both DF-0632 and DF-0628 fix.diffs applied. Ran the same DF-0632 PoC against the patched kernel:

Step Baseline (#0) Patched (#1)
Initial icmp_out=0 icmp_out=0
Round 1 (6 pings) icmp_out=6 icmp_out=2 *
After expiry + cleanup icmp_out=6 (BUG: not decremented) icmp_out=0 (FIXED: decremented)
Round 2 (6 more pings) icmp_out=6 (no new states) icmp_out=2 (new states created, since counter was reset)
After second expiry icmp_out=6 (permanently pinned) icmp_out=0

* Lower than 6 because the firewall had just been enabled and some early pings were dropped during rule installation. The decrement behavior is clearly demonstrated: counter returns to 0 after every expiry cycle.

Fix confirmed: on the patched kernel the counter is properly decremented on cleanup, restoring the firewall's ability to create new states indefinitely.

Fix verification

fixed

validated

see evidence pack
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Jul 19 01:05:49 UTC 2026

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

REPRODUCED (kld probe). ipfw3 state counters monotonic, never decremented -> permanent state exhaustion. Kernel fix: decrement in cleanup+flush.