⬢ DragonFlyBSD Kernel Audit
DF-3436 / verdict.json
← back to finding ↓ download raw
{
 "finding_id": "DF-3436",
 "status": "reproduced",
 "reproduced": 1,
 "impact": "panic",
 "confidence": "certain",
 "verdict": "Module-unload lifecycle panic reproduced on the stock INVARIANTS guest and root-caused. A root loop of kldunload/kldload ipfw3_basic (nothing else running, ipfw3 rules installed) hit Fatal trap 12: page fault, fault virtual address = 0x28, supervisor read, current process Idle (netisr0), Stopped at rn_flush+0x1d: movq 0x28(%rbx),%rax in under 90 seconds. Two stacked defects: (1) ipfw3_basic_modevent (ip_fw3_basic.c:651-658) runs ip_fw3_state_modevent/ip_fw3_table_modevent MOD_UNLOAD UNCONDITIONALLY, even when ip_fw3_basic_fini() returns an error (it returns 1 while any rule still references the basic module, ip_fw3.c:189-231) - so a REFUSED unload still frees the state/table contexts and leaves the loaded module with fw3_ctx[cpu]->table_ctx dangling (never cleared); (2) ip_fw3_table_fini_dispatch (ip_fw3_table.c:576-590) calls rn_flush(table_ctx->node, ...) for ALL IPFW_TABLES_MAX ids although only CONFIGURED tables ever ran rn_inithead (ip_fw3_table.c:100-105) - unconfigured ids have node == NULL and rn_flush (radix.c:1332-1343) dereferences head->rnh_walktree with no NULL check (read at NULL+0x28 = the observed fault VA). Bouncing cycles walk the NULL/freed heads until one lands on unmapped memory. The originally-predicted state-side races (self-re-arming cleanup callout vs callout_stop at :691/:592, cleanup_dispatch dereferencing fw3_state_ctx[mycpuid] NULLed by fini at :676-677) are real by code and are fixed by the same fix.diff; the crashing instance was the table/basic side. Fix validated in two iterations: callout-only fix FAILED (same panic); full fix (rn_flush NULL guards + ctx cleared + EBUSY-without-teardown on refused unload) survived 4000 bounce cycles with no panic.",
 "exploit_chain": "",
 "evidence": [
  "panic.txt: Fatal trap 12 ... fault virtual address = 0x28 ... current process Idle ... Stopped at rn_flush+0x1d (<90s of bouncing)",
  "bounce_basic.sh: the exact trigger loop",
  "VERDICT.md: two-defect root-cause walk (refused-unload teardown + rn_flush(NULL))",
  "run.patched.log: identical bounce on the fix-v3 kernel - 4000 cycles, no panic"
 ],
 "kernel_refs": [
  "sys/net/ipfw3_basic/ip_fw3_state.c:539",
  "sys/net/ipfw3_basic/ip_fw3_state.c:542",
  "sys/net/ipfw3_basic/ip_fw3_state.c:584",
  "sys/net/ipfw3_basic/ip_fw3_state.c:592",
  "sys/net/ipfw3_basic/ip_fw3_state.c:691",
  "sys/net/ipfw3_basic/ip_fw3_basic.c:651",
  "sys/net/ipfw3_basic/ip_fw3_table.c:576",
  "sys/net/ipfw3_basic/ip_fw3_table.c:583",
  "sys/net/radix.c:1332"
 ],
 "poc_changes": "No seed. Trigger is a plain shell loop; no harness fixes needed.",
 "attempts": 1,
 "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026   root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64",
 "runtime_sec": 90,
 "guest_dirty": 1,
 "build_cmd": "(none - shell loop)",
 "run_cmd": "sh /tmp/bounce_basic.sh 4000",
 "code_hash": "",
 "notes": "Root-gated (kldunload). Related to DF-0631s rule-vs-state lifetime but distinct: this is the module lifecycle itself. The rn_flush frame indicates the table modules teardown shares the defect; a follow-up could file the table-side instance separately if the orchestrator wants per-subsystem granularity.",
 "recommended_fix": "rn_flush NULL guards for unconfigured tables + clear fw3_ctx->table_ctx after free + ipfw3_basic_modevent must not run the state/table fini when the unload is refused (return EBUSY) + callout_terminate in ip_fw3_state_fini (fix.diff).",
 "fix_status": "fixed",
 "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Sat Sep 12 16:56:02 UTC 2026   root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64",
 "fix_baseline_reproduced": 1,
 "fix_patched_reproduced": 0,
 "fix_verdict": "Two-iteration validation. Attempt 1 (kernel #2 16:27:37: callout_terminate + state-cleanup NULL guard only) FAILED: the identical bounce still panicked with the same rn_flush signature - the crashing instance was the table/basic lifecycle, not the state callout. Attempt 2 (fix.diff v2, kernel #1 16:56:02: + rn_flush NULL guards for unconfigured tables, fw3_ctx->table_ctx cleared after free, and ipfw3_basic_modevent returning EBUSY WITHOUT running the state/table fini when the unload is refused because rules still reference the module) built clean and survived the identical 4000-cycle bounce loop with no panic and the module cleanly refused/unloaded (run.patched.log). Root cause of the deterministic crash: ipfw3_basic.c:651-658 runs the sub-module fini even when ip_fw3_basic_fini() errors, and ip_fw3_table.c:576-590 rn_flush()es table heads that were never rn_inithead()d (rn_flush has no NULL check, radix.c:1332-1343).",
 "fix_evidence": [
  "run.patched.log",
  "kbuild.tail.log",
  "fix.diff",
  "panic.txt"
 ]
}