DF-3439 / verdict.json
{ "finding_id": "DF-3439", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "check_skipto() jumps through a per-rule cached next_rule pointer that is never invalidated on rule deletion (ip_fw3_delete_rule frees the target; no caller clears caches). Reproduced three ways on the stock INVARIANTS guest: (1) ICMP stays 100% denied after the only deny rule is deleted from a default-accept chain (stale freed rule still executing); (2) once the freed M_IPFW3 chunk is reused by ordinary kernel activity, every packet through the stale cache hits garbage -> ipfw: ouch! console flood and a total ssh/traffic blackhole; (3) with debug.use_weird_array=1 (freed-chunk poison), the first packet after the delete panics: Fatal trap 9 GPF at ip_fw3_chk+0x1a4 (wild filter_funcs[module][opcode] indirect call from poisoned freed bytes). Live-memory dumps (libkvm /dev/kmem) show rule100->next_rule == &rule200 before the delete and still set after the free. Fix (clear targeting next_rule caches in delete_rule_dispatch) validated on a rebuilt kernel: cache cleared, post-delete ping passes via fresh lookup, no flood, no panic.", "exploit_chain": "root adds skipto rule + target rule (routine firewall config); unprivileged packet populates the cache; root deletes the target rule (routine churn); next packet: UAF read of freed rule header + instructions -> filter_funcs[module][opcode] indirect call with freed-chunk indices (chunk refillable with attacker-chosen rule instruction words) and pcnt++/bcnt+= UAF writes on terminal actions. Setup needs firewall-admin; trigger is any packet; demonstrated impact: deny-all availability loss + deterministic panic; controlled-call escalation would additionally require same-zone refill choreography (not pursued; setup is already root-gated).", "evidence": [ "findings/poc/DF-3439/panic.txt \u2014 Fatal trap 9 at ip_fw3_chk+0x1a4 after del+ping with use_weird_array=1", "findings/poc/DF-3439/run.log \u2014 phase-by-phase: live deny, cache dump (next_rule==&rule200), post-delete deny-all, ouch-flood", "findings/poc/DF-3439/blackhole_console.txt \u2014 ouch!, skip past end of rules, denying packet flood after freed-chunk reuse", "findings/poc/DF-3439/fix.diff + VERDICT.md \u2014 fix validation: kernel #1 clears caches, post-delete ping 0% loss, no panic" ], "kernel_refs": [ "sys/net/ipfw3_basic/ip_fw3_basic.c:175-185", "sys/net/ipfw3_basic/ip_fw3_basic.c:104-124", "sys/net/ipfw3/ip_fw3.c:757-768", "sys/net/ipfw3/ip_fw3.c:824-841", "sys/net/ipfw3/ip_fw3.c:485-507", "sys/net/ipfw3/ip_fw3.c:573-578" ], "poc_changes": "Original audit sketch had no runnable PoC. Wrote ipfw3x (raw IP_FW_X setsockopt client; the shipped /sbin/ipfw3 userland has no skipto parser), krules (libkvm /dev/kmem rule-chain walker to image the cache before/after delete), and a poison run using debug.use_weird_array. Two early PoC encodings were wrong (ipfw_insn is 2 words wide, LEN_OF_IPFWINSN==2; struct ipfw_ioc_rule pads to 48 so instruction arrays must be byte-packed at offset 36) \u2014 a misencoded 2nd instruction itself demonstrated the known DF-0473 unvalidated dispatch (call NULL, Fatal trap 12 RIP=0) before the encoding was fixed and the UAF runs captured.", "attempts": 8, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (baseline) / #1 rebuilt with fix.diff", "runtime_sec": 5400, "guest_dirty": 0, "build_cmd": "cc -O -o ipfw3x ipfw3x.c && cc -O -o krules krules.c -lkvm (fix kernel: cd /usr/src && make -j6 nativekernel && make installkernel)", "run_cmd": "./run.sh (phases: kldload ipfw3 ipfw3_basic; add 200 protodeny 1; add 100 skipto 200; ping; del 200; ping; krules dumps)", "code_hash": "01bc1b1c0bf8e58055699e24f9e375597b13a1ccfec5ee32981d75fca097e507", "notes": "Impact panic per the honest-status table (write-capable UAF: panic proven, uid0 not pursued because setup requires firewall-admin root). Also note the ip_fw3_chk dispatch at ip_fw3.c:506 has no bounds check on module/opcode (DF-0473, known) \u2014 this finding adds a dangling-pointer route to it. guest reset with-src performed after the fix-validation run.", "recommended_fix": "In delete_rule_dispatch(), walk the chain and NULL every ->next_rule equal to the rule about to be freed (see fix.diff); mirror in any future rule-move/flush paths.", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #1: Sat Sep 12 18:50:09 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Identical PoC phases on the fixed kernel: pre-delete behavior unchanged (deny works, cache populated), after delete the cache is cleared (krules shows next_rule=0x0 or re-cached onto the live default rule), post-delete ping passes (0% loss, fresh lookup to default accept), no ouch flood, no panic, guest stays up.", "fix_evidence": "findings/poc/DF-3439/VERDICT.md (Fix validation section); run.log tail" } |