⬢ DragonFlyBSD Kernel Audit
DF-3340 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-3340",
  "status": "reproduced",
  "reproduced": 1,
  "impact": "panic",
  "confidence": "certain",
  "verdict": "pf_unlink_state() sends a spoofed RST via pf_send_tcp()->ip_output() whenever a synproxy state is unlinked in PF_TCPS_PROXY_DST (backend never answered). On state expiry this runs on pf_purge_thread, a non-netisr thread, tripping the guaranteed INVARIANTS assert ASSERT_NETISR_NCPUS at ip_output.c:185 (thread %p cpu%d is not within netisr_ncpus %d). Reproduced on the reference INVARIANTS guest ~6 s after a single TCP connect to a synproxy rule with a dead backend (tcp.first shortened to 5 s; default 120 s) - backtrace ip_output<-pf_send_tcp<-pf_unlink_state<-pf_purge_expired_states<-pf_purge_thread (panic.txt). The same send panics from DIOCCLRSTATES/DIOCKILLSTATES/kldunload (root-only; the unload variant is DF-3337s incidental panic). Trigger is remotely reachable and unauthenticated against any synproxy-protected port whose backend is down, but the panic requires an INVARIANTS kernel: the assert compiles out on production GENERIC, leaving an architecture violation (network output from the purge thread under exclusive pf_consistency_lock) with no demonstrated crash. Not a memory-corruption primitive; no uid0 chain. fix.diff (defer via heap netmsg to netisr, rtfree_async pattern, fast path kept for netisr callers, in-flight drain in pf_unload) validated on a rebuilt in-guest pf.ko: expiry trigger survives with the spoofed RST still emitted on lo0, unload with live PROXY:DST state clean, 8-state stress clean, serial console clean.",
  "exploit_chain": "",
  "evidence": "panic.txt (purge-thread backtrace); run.log (PROXY:DST state then connection death); fix_run.log (SURVIVED + tcpdump Flags [R.] from 127.0.0.1.8080 at t+4.7s = deferred RST); fix_unload_stress.log (kldunload + 8-state stress); fix.diff; fix_build.log",
  "kernel_refs": [
    "sys/net/pf/pf.c:1483",
    "sys/net/pf/pf.c:1487",
    "sys/net/pf/pf.c:1262",
    "sys/net/pf/pf.c:1591",
    "sys/net/pf/pf.c:2255",
    "sys/net/pf/pf.c:4935",
    "sys/net/pf/pf_ioctl.c:1591",
    "sys/net/pf/pf_ioctl.c:3071",
    "sys/netinet/ip_output.c:185",
    "sys/net/netisr2.h:136"
  ],
  "poc_changes": "Lead only asserted the unload-context panic (DF-3337 incidental). Built the minimal expiry rig (synproxy on lo0:8080, no backend, tcp.first 5) to reach the remotely-triggerable purge-thread context; verified PF_TCPS_PROXY_DST entry and RST-ignoring behavior via pfctl -s state and tcpdump; fix authored after verification and validated in the same session.",
  "attempts": 3,
  "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": 1580,
  "guest_dirty": 0,
  "build_cmd": "guest: cd /usr/src && patch -p1 < fix.diff && cd sys/net/pf && make obj && make   (pf.ko, -Werror clean)",
  "run_cmd": "guest root: sh /tmp/df3340_trigger.sh   (baseline panic) / sh /tmp/df3340_fixcheck.sh + kldunload variant (fixed)",
  "code_hash": "bd7a58a960f0d7738e28c6a74a0bb7a39762f9ea68adfd348c9ac5c2bcc987d6",
  "notes": "Stock default config ships the assert: options INVARIANTS at sys/config/X86_64_GENERIC:56; the reference guest boots exactly that config (uname /usr/obj/usr/src/sys/X86_64_GENERIC) -> default-config remote-unauth DoS (High). Kernels custom-built without INVARIANTS compile the assert out; impact there is an architecture violation without demonstrated crash. Root-only contexts (pfctl -F states / pfctl -k / kldunload) panic identically pre-fix. pass-2 known findings DF-3263..DF-3268 not re-reported. File pass-2 status left untouched; scoped task_log entries written.",
  "fix_status": "fixed",
  "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul  2 06:02:54 UTC 2026  root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC  x86_64 (module-only fix: pf.ko rebuilt from patched /usr/src/sys/net/pf)",
  "fix_baseline_reproduced": 1,
  "fix_patched_reproduced": 0,
  "fix_verdict": "Single-fix validation on the same guest/kernel: baseline stock pf.ko panicked deterministically on the expiry purge (panic.txt); pf.ko rebuilt with only fix.diff applied survived the identical trigger, purged the state, and still emitted the spoofed RST from netisr context (lo0 tcpdump Flags [R.] at t+4.7s). The unload context (kldunload with live PROXY:DST state, DF-3337s panic) also survived, and an 8-state stress run left the serial console clean. Bounded in-flight netmsg drain added to pf_unload so the module cannot be freed under a queued netmsg.",
  "fix_evidence": "fix_run.log / fix_unload_stress.log: expiry + unload + 8-state stress all survive on patched pf.ko; deferred RST still emitted (lo0 tcpdump); baseline panicked at the same point.",
  "recommended_fix": "Defer the synproxy PROXY:DST RST in pf_unlink_state() to a netisr via a parameter-copying heap netmsg (rtfree_async pattern), keep direct send when already on a netisr thread, and drain in-flight netmsgs in pf_unload()."
}