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)
PoC verification
Evidence pack
findings/poc/DF-3337 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
fixedBaseline (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)
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.
No comments yet.