DF-3337 / verdict.json
{ "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)" } |