⬢ DragonFlyBSD Kernel Audit
DF-3456 / verdict.json
← back to finding ↓ download raw
{
 "finding_id": "DF-3456",
 "status": "reproduced",
 "reproduced": 1,
 "impact": "pf-policy-bypass",
 "confidence": "certain",
 "verdict": "pf_anchor_stack[64] (pf.c:148) is a single global anchor-walk stack; the pf pfil hooks hold pf_token only SHARED (pf_ioctl.c:3199+), and DragonFly shared tokens allow concurrent holders across CPUs, so simultaneous pf rule walks tear each others frames. Demonstrated on a 6-vCPU guest with an anchor sandwich ruleset (block rules between two nested anchors): 117 (run 6) and 1 (run 8) UDP datagrams that pf block drops inside the anchor were delivered to an unprivileged receiver while the identical control block in the main ruleset dropped 100%; per-rule evaluation counters prove ~20,791 (run 6) and ~8,700 (run 8) walks silently skipped the block rule (counter gap), versus a flat/zero gap on lo-only controls and on the fixed kernel. Silent intermittent firewall rule bypass under concurrent load on any anchor-using pf ruleset; root configures pf once, unprivileged traffic triggers it.",
 "exploit_chain": "root loads an anchor-based pf ruleset (standard pfctl practice) and enables pf -> unprivileged users send concurrent UDP flows (lo + vtnet-bound DNS queries) -> two CPUs evaluate anchor rules simultaneously -> pf_step_into_anchor/pf_step_out_of_anchor write/read the same global frames -> victim walk resumes the rule list past the block rules (TAILQ_NEXT(other walks anchor rule)) -> block verdict lost, packet delivered. No memory-safety fault observed (all frame fields remain valid kernel pointers); impact is filtering integrity, not corruption of arbitrary memory.",
 "evidence": [
  "run.6.log: recv port 5555: 117 datagrams (BYPASS! should be 0) with control port 5556 at 0",
  "run.6.log counter forensics: first decoy 1,649,386 evals vs block rule 1,628,593 evals = 20,793 skipped walks; inner2 first decoy 1,628,558",
  "run8_supplement.log: second reproduction, 1 delivered datagram, counter gap +8,659 in one run",
  "run.9_fixed.log + fixed-run-2: patched kernel, 2.88M walks, 0 delivered, counters flat (2,876,466/2,876,506/2,876,509)",
  "VERDICT.md: full mechanism, pfctl workaround notes, control experiment attribution"
 ],
 "kernel_refs": [
  "sys/net/pf/pf.c:143-148",
  "sys/net/pf/pf.c:2501-2533",
  "sys/net/pf/pf.c:2535-2570",
  "sys/net/pf/pf.c:3978-3986",
  "sys/net/pf/pf_ioctl.c:3199-3263",
  "sys/kern/lwkt_token.c:726-785"
 ],
 "poc_changes": "No orchestrator seed existed; PoC designed from scratch and iterated six times: v1 ruleset misordered (pf last-match-wins, caught by control port); v2 flat anchor - concurrent walks into the SAME anchor rule write identical frames (benign); v3 revealed pfctl/kernel nested-anchor path doubling (walked anchor is spam/spam/inner1, 0 evals at the natural path); v4 empty inline anchors are optimized away by pfctl; v5 populated the actually-walked anchors via pfctl -a spam/spam/innerN -f - lo-only output walks did not tear (3.25M walks, perfectly equal counters); v6 added vtnet0-bound DNS query workers as the concurrent walk population and no state - reproduced immediately (117 bypasses). Also worked around: include <signal> -> <signal.h>, wait() prototype, ssh backgrounding of receivers, receiver port reuse between runs.",
 "attempts": 8,
 "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 (6 vCPU, stock INVARIANTS kernel for baseline; rebuilt in-guest for fix validation)",
 "runtime_sec": 5400,
 "guest_dirty": 0,
 "build_cmd": "cc -O2 -o /tmp/df3456/ab /tmp/df3456/anchor_bypass.c && cc -O2 -o /tmp/df3456/dns /tmp/df3456/dns_flood.c",
 "run_cmd": "sh setup_pf.sh (root); ./ab recv 5555 110 & ./ab recv 5556 110 & ; ./ab flood 6 90 & ./dns 24 90 400 (all as unpriv user)",
 "code_hash": "8dafbb249ced50fc9304d554bebf4e4c171c846041e99fec3392986f54c27ac2 (anchor_bypass.c); de30a1b0b4e0a1917f7f35bdcd739a0a9c6caa02df2028f28301849e28c703d4 (dns_flood.c)",
 "notes": "Auxiliary observations (documented, not filed as kernel findings): (1) pfctl inline nested anchors resolve to doubled kernel paths (spam/spam/inner1) - rules written the natural pfctl way in nested inline anchors are silently unreachable; (2) an unrelated 20k-rule single-anchor ruleset flooded by ONE sender wedged the entire guest (no panic, sshd unresponsive) - not chased, distinct from this finding. Bypass delivery rate is stochastic (117 vs 1 per 90s run) but the counter-proven tear rate was 0.4-1.3% of walks in every reproducing run; lo-only control showed ~0.01%. Config-vs-walker lifetime is properly serialized by exclusive-vs-shared pf_token - the defect is strictly walker-vs-walker.",
 "recommended_fix": "Remove the global pf_anchor_stack; give each walker (pf_match_translation, pf_test_rule, pf_test_fragment) a caller-local struct pf_anchor_stackframe astack[64] passed through pf_step_into_anchor/pf_step_out_of_anchor (see fix.diff); per-CPU arrays are NOT sufficient since a netisr thread can preempt a user-thread walk on the same CPU.",
 "fix_status": "fixed",
 "fix_kernel_uname": "DragonFly 6.5-DEVELOPMENT #0 (in-guest rebuild of X86_64_GENERIC with fix.diff applied to /usr/src/sys/net/pf/pf.c; /boot/kernel/kernel dated Sep 13 04:52, md5 667631b7029907145cc7240125e8b228; booted 04:56)",
 "fix_baseline_reproduced": 1,
 "fix_patched_reproduced": 0,
 "fix_verdict": "Rebuilt the kernel in-guest (make nativekernel, 4299 compile units, completed 04:49:31) with fix.diff (caller-local anchor walk stacks), installed and rebooted. Re-ran the exact reproducer twice (90s floods, 2.88M and 2.8M walks - more walks than the reproducing baseline runs): zero datagrams delivered on the blocked port in both runs and per-rule evaluation counters perfectly flat (no block-skip gap), versus 117+1 delivered and 20,791+8,700 skipped walks on the unpatched kernel. The bypass is gone with the fix.",
 "fix_evidence": "run.9_fixed.log (fixed run 1: 0 deliveries, counters 2,876,466 vs 2,876,506 vs 2,876,509); fixed run 2 transcript in VERDICT.md (0 deliveries, block evals >= decoy evals); kbuild_tail.txt (kernel build completion); fix.diff"
}