⬢ DragonFlyBSD Kernel Audit
DF-3337 / verdict.json
← back to finding ↓ download raw
{
 "finding_id": "DF-3337",
 "status": "reproduced",
 "reproduced": 1,
 "impact": "dos",
 "confidence": "certain",
 "verdict": "REPRODUCED: pfr_insert_kentry() executes rn_addroute() + pfrkt_cnt++ on the packet path, which holds pf_token only SHARED (pf_check_in/out), so multiple CPUs mutate the same pfr_ktable radix head concurrently and unsynchronized. On the stock INVARIANTS kernel a 12-worker synproxy+max-src-conn-rate+overload flood (sources = the connections themselves, i.e. remotely triggerable on a real edge firewall) produced (a) the kernels own integrity detector pfr_get_addrs: corruption detected (-1) (pf_table.c:586) in 2 of 3 sessions, (b) one session silently losing 2900 of 3072 inserts in a round (radix nodes orphaned/mislinked; pfr_insert_kentrys rv is ignored by the caller), and (c) post-corruption packet-processing collapse (~650 state-searches/s, flooders never finishing, table count drifting between consecutive walks). The sequential control with the same binary loses nothing (MISSING=0), isolating concurrency as the cause. Escalation to uid=0 was not pursued: the primitive is structural mislinking of legitimately-allocated radix nodes (pointer-valued stores into valid objects), not a controlled write; a deeper rn_delete/UAF chain is plausible but undemonstrated. fix.diff (spinlock around the packet-path mutators) rebuilt as a module and validated: identical flood shows MISSING=0 in all rounds and no corruption prints.",
 "exploit_chain": "root loads an overload rule (standard anti-bruteforce config) -> >=2 remote/local sources exceed max-src-conn-rate simultaneously -> pf_src_connlimit() on multiple CPUs (pf_token SHARED) -> concurrent pfr_lookup_addr/rn_addroute/pfrkt_cnt++ on the same table -> radix mislink/orphan + count drift -> integrity-detector trip / mass silent entry loss / packet-path throughput collapse (DoS). Controlled memory write not demonstrated.",
 "evidence": [
  "run.log: round 2 WALK=0 with pfr_get_addrs: corruption detected (-1) + hot session MISSING=2900/3072",
  "control_sequential.log: W=1 MISSING=0 (same binary, all sources fired: 47 blocking prints)",
  "fix_run.log: patched pf.ko, W=12: 4/4 rounds MISSING=0, 0 corruption prints",
  "fix.diff / fix_build.log: spinlock patch, module-only build rc=0",
  "panic_unload_incidental.txt: separate pf.c purge-path panic observed during work (NOT DF-3337 evidence)"
 ],
 "kernel_refs": [
  "sys/net/pf/pf_table.c:861",
  "sys/net/pf/pf_table.c:873",
  "sys/net/pf/pf_table.c:981",
  "sys/net/pf/pf_table.c:878",
  "sys/net/pf/pf_table.c:2059",
  "sys/net/pf/pf_ioctl.c:3205",
  "sys/net/pf/pf.c:513",
  "sys/net/pf/pf.c:4613",
  "sys/net/pf/pf.c:620",
  "sys/net/radix.c:1"
 ],
 "poc_changes": "PoC had to be engineered past four DragonFly specifics: (1) loopback sources require exact /32 lo0 aliases or bind/connect silently emit no packets; (2) source-tracking nodes and their rate thresholds are PER-CPU (pf.c tree_src_tracking[cpu]) so 10 connections per source are needed to guarantee a threshold trip (pigeonhole over 6 netisr CPUs) - with only 2 conns just ~6% of sources fire; (3) plain keep-state rarely reaches the connlimit call site on this stack - synproxy state fires deterministically on the client ACK (pf.c:4931); (4) short state timeouts + PROXY:DST expiry trigger an unrelated purge-thread panic (pf_send_tcp KKASSERT), so states are pinned with 86400s timeouts and a 65536-entry preload widens every rn_addroute window. Detection uses the kernels own walk-vs-count integrity detector plus per-round walk counting around a pfctl -T replace reset.",
 "attempts": 9,
 "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": 1260,
 "guest_dirty": 0,
 "build_cmd": "cd /tmp/poc/DF-3337 && sh build.sh  (cc -O2 -Wall -o flooder flooder.c)",
 "run_cmd": "cd /tmp/poc/DF-3337 && W=12 N=256 R=4 sh run.sh ; baseline: same on stock pf.ko; control: W=1 N=16 R=1",
 "code_hash": "cc68eca28fc2cf6329dbf106f2328e034e0d76093b606fe3781e9dc25862c1e5",
 "notes": "adjacent same-family issues observed, not filed as findings: (a) pfr_update_stats() also increments kt->pfrkt_packets/bytes and pfrkt_match/nomatch non-atomically on the shared path (counter drift only); (b) SEPARATE pf.c bug discovered during verification: purging/unloading states in PF_TCPS_PROXY_DST calls pf_send_tcp -> ip_output from the purge thread / ioctl thread, tripping the netisr KKASSERT (netisr2.h:138 thread %p cpu%d is not within netisr_ncpus) -> guaranteed panic; remotely triggerable by connecting to a synproxy-protected service whose backend never answers (states expire into PROXY:DST then get purged); evidence in panic_unload_incidental.txt - recommend a dedicated pf.c audit finding. Guest reset to with-src snapshot after validation (guest_dirty=0).",
 "recommended_fix": "Serialize the packet-path table mutators: spin_lock around pfr_insert_kentry()s lookup+create+rn_addroute+count and around pfr_update_stats()s pfrke_counters kmalloc/swap (fix.diff), or move overload handling into an exclusively-serialized task as OpenBSD/FreeBSD do.",
 "fix_status": "fixed",
 "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #0 (same kernel image; pf.ko module rebuilt from patched source, sha256 a28ddf120f967896309617498bbf82c2f18a2da139eb68d4e3a81407f8e80093 vs stock fa8a8dc186eb944632c178493b28e772a4d0acb94865e9bada5701bf8b1886ff)",
 "fix_baseline_reproduced": 1,
 "fix_patched_reproduced": 0,
 "fix_verdict": "Baseline (stock pf.ko, same guest+params): corruption-detector trip + up to 2900/3072 lost inserts + throughput collapse. Patched (fix.diff module-only rebuild): 4/4 rounds MISSING=0, zero corruption detected prints, flood completes at full speed. fix_status=fixed.",
 "fix_evidence": "fix_build.log (make rc=0, -Werror clean), fix_run.log (W=12 N=256 R=4 all clean), run.log (baseline dirty), control_sequential.log (single-threaded control clean)"
}