β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3337

pfr_insert_kentry() mutates the table radix tree (rn_addroute + pfrkt_cnt++) on the packet path, which holds pf_token only SHARED: multi-CPU overload floods race and corrupt the tree

Summary

pf_src_connlimit calls pfr_insert_kentry from the TCP state machine, which runs under lwkt_gettoken_shared(&pf_token). pfr_insert_kentry walks the table radix tree, inserts into it (pfr_route_kentry -> rn_addroute) and increments pfrkt_cnt - with no serialization between CPUs holding the token shared; sys/net/radix.c has no internal locking. A remote flood against any rule configured with 'overload <table>' + 'max-src-conn-rate' (the standard anti-bruteforce/synproxy pattern) makes multiple CPUs mutate the same radix head simultaneously: nodes mislinked/orphaned, counts torn, and the kernel's own integrity detector fires ('pfr_get_addrs: corruption detected (-1)'). VERIFIED on guest: W=12 N=256 R=4 flood - silent loss of up to 2900/3072 inserts per round (orphaned radix nodes; rv ignored by caller), pfrkt_cnt/walk divergence tripping the corruption detector, and post-corruption packet-processing collapse (~650 state-searches/s) - remote DoS with real kernel heap corruption; sequential control (W=1) loses nothing, isolating concurrency. Patched module 4/4 rounds clean (spinlock around packet-path mutators incl. pfr_update_stats' counter alloc). Unauthenticated remote - triggering sources are the attacker's own connections exceeding the rate limit on an already-configured overload rule; no local account, no /dev/pf access. uid0 chain not developed (structural mislinking of legitimately-allocated nodes, not a controlled write). Upstream-grade alternative: move overload handling into an exclusively-serialized task as OpenBSD/FreeBSD do.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3337 Β· 9 files
FileTypeDescriptionSize
flooder.c trigger-source 10-conn-per-source synproxy flood from 127.(16+w).x.y aliases 3.2 KB view raw
run.sh run-script alias setup, preload, round loop with per-round integrity check, pfctl -T replace reset 137 B view raw
run.log baseline-log stock pf.ko concurrent runs: corruption-detector trips + missing entries 2.4 KB view raw
control_sequential.log control-log sequential arm: MISSING=0 958 B view raw
fix.diff fix spinlock serialization of pfr_insert_kentry + pfr_update_stats counters swap 1.8 KB view raw
fix_build.log fix-build-log module-only make rc=0 2.6 KB view raw
fix_run.log fix-run-log patched: 4/4 rounds MISSING=0 470 B view raw
panic_unload_incidental.txt incident-log separate pf.c purge-path panic observed during verification (NOT DF-3337) 2.6 KB view raw
verdict.json verdict machine verdict 5.9 KB view raw

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

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_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)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0 (same kernel image; pf.ko module rebuilt from patched source, sha256 a28ddf120f967896309617498bbf82c2f18a2da139eb68d4e3a81407f8e80093 vs stock fa8a8dc186eb944632c178493b28e772a4d0acb94865e9bada5701bf8b1886ff)

Confirmed kernel references

Detail

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 (decisive lines)

["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)']

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 kernel's own walk-vs-count integrity detector plus per-round walk counting around a pfctl -T replace reset.

Verified 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.

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 kernel's 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_kentry's 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.