⬢ DragonFlyBSD Kernel Audit
DF-3453 / verdict.json
← back to finding ↓ download raw
{
 "finding_id": "DF-3453",
 "status": "reproduced",
 "reproduced": 1,
 "impact": "panic",
 "confidence": "likely",
 "verdict": "netmsg_service_sync() only synchronizes the netisr threads (netreglist is populated solely with netisr_threads, netisr.c:229), but pfil_run_hooks() walkers also execute on softclock (ip_mroute tbf_reprocess_q callout -> tbf_process_q -> tbf_send_packet -> ip_output -> pfil_run_hooks PFIL_OUT) and on IFF_IDIRECT driver interrupt threads (bridge_pfil). pfil_list_free() after the COW swap recycles the packet_filter_hook entries and list head an in-flight walk on those threads still iterates, ending in an indirect call through freed pfh->pfil_func. Reproduced on the stock INVARIANTS guest: the plain mroute feeder (no test modules) panics with the kernels own ASSERT_NETISR_NCPUS inside ip_output on the tbf_reprocess_q/softclock path (panic.txt) - an unconditional root-configured panic on default INVARIANTS builds. On a non-INVARIANTS kernel the same flood runs ip_output->pfil_run_hooks on softclock for minutes (97% CPU in the walk, production semantics), and instrumentation proves the freed hook chunks are returned by the very next same-size kmalloc on netisr0 (spray-hit 2/2), i.e. the poisoning half of the UAF works exactly as designed. The final wild-call step could not be triggered on this 6-vCPU guest because the only sustainable tbf walker (softclock 0) shares CPU0 with the pinned mutation thread (netisr0) and LWKT non-preemption serializes them, while softclock-N walkers are starved of tokens by netisr-N inline tbf_process_q - a configuration coincidence, not a pfil.c synchronization. Both triggers are root-gated: no unprivileged path, severity Low (reliability/UAF class, root racing the kernel). fix.diff adds a pfil_token held across the walk and across the swap->free section.",
 "exploit_chain": "",
 "evidence": [
  "panic.txt: stock-kernel panic backtrace ip_output<-tbf_send_packet<-tbf_process_q<-tbf_reprocess_q<-softclock_handler (thread ... is not within netisr_ncpus 6)",
  "run.log: [softclock 0] 96-97% CPU parked inside pfil walk on the non-INVARIANTS kernel; net.prhook_n_spray_hit=2/2 (first kmalloc(36,M_IFADDR) after pfil_remove_hook returns a just-freed packet_filter_hook chunk); k=1..5 flow probes show walker CPU pinning",
  "VERDICT.md: full serialization analysis of why the end-to-end wild call is blocked on this guest (CPU0 LWKT non-preemption; token starvation of non-zero softclock walkers)",
  "fix.diff: pfil_token around pfil_run_hooks walk and around the dispatch swap+free sections"
 ],
 "kernel_refs": [
  "sys/net/pfil.c:47",
  "sys/net/pfil.c:110",
  "sys/net/pfil.c:238",
  "sys/net/pfil.c:244",
  "sys/net/pfil.c:346",
  "sys/net/pfil.c:351",
  "sys/net/netisr.c:229",
  "sys/net/netisr.c:292",
  "sys/netinet/ip_output.c:185",
  "sys/netinet/ip_output.c:529",
  "sys/net/ip_mroute/ip_mroute.c:1874",
  "sys/net/ip_mroute/ip_mroute.c:1931",
  "sys/kern/kern_timeout.c:423",
  "sys/net/if_ethersubr.c:1457",
  "sys/net/bridge/if_bridge.c:4097"
 ],
 "poc_changes": "Seed had no runnable PoC. Built: prhook.ko (pfil hook module: softclock-only multicast busy-delay to widen the in-flight walk window, sysctl-driven COW add/remove mutation on netisr0, M_IFADDR 36-byte 0x41 spray, instrumentation counters proving 2/2 freed-chunk reclaim) and pfilrace_feeder (MRT_INIT + 2 phyint vifs + rate-limited vif1 + MFC + throttled multicast flood; parameterized rate and per-flow source-port pinning). Discovered en route: the tbf callout arms on the feeding flows CPU; tsleep parking deadlocks softclock inside its own callout wheel; ipfw3_basic unload panics in rn_flush (follow-up).",
 "attempts": 7,
 "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 (stock INVARIANTS, panic.txt) and DragonFly 6.5-DEVELOPMENT #0: Sun Sep 13 01:13:18 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/PRNOINV x86_64 (in-guest non-INVARIANTS build, race instrumentation)",
 "runtime_sec": 4950,
 "guest_dirty": 0,
 "build_cmd": "cd /root/pfilrace && cc -O2 -o pfilrace_feeder pfilrace_feeder.c && cd prhook && make -m /usr/src/share/mk",
 "run_cmd": "kldload ip_mroute; kldload /root/pfilrace/prhook/prhook.ko; sysctl -w net.prhook_delay_ms=150; sysctl -w net.prhook_toggle=1; /root/pfilrace/pfilrace_feeder 10.0.2.15 1000 & sleep 5; sysctl -w net.prhook_toggle=1",
 "code_hash": "f271a24a45ae4192f9143e028553afa7e4728f84d0da33dee3d727569718c6ef",
 "notes": "Both triggers root-gated (MRT_INIT on a raw IGMP socket + pfil module load/unload) - no unprivileged trigger; Low severity, filed as reliability/UAF-class with a recharacterization of the COW + netmsg_service_sync is sufficient assumption from the ipfw2 pass-2 review. The INVARIANTS panic is itself a clean root-configured DoS on default builds. Guest was reset to the clean-source snapshot after the run (guest_dirty=0). Out-of-batch follow-up: rn_flush NULL-deref during kldunload ipfw3_basic on the non-INVARIANTS kernel (sys/net/ipfw3_basic/ip_fw3_table.c teardown).",
 "recommended_fix": "Hold a dedicated LWKT token (pfil_token) across the pfil_run_hooks walk and across the add/remove dispatch swap->free section so list frees exclude all walkers, not only netisr threads",
 "fix_status": "not_testable",
 "fix_kernel_uname": "",
 "fix_baseline_reproduced": 0,
 "fix_patched_reproduced": 0,
 "fix_verdict": "fix.diff authored (pfil_token) but not kernel-built/validated: the end-to-end UAF could not be triggered on this guest (same-CPU LWKT serialization pins the only sustainable walker against the netisr0 mutation), so a patched-vs-baseline comparison has no observable signal here. The INVARIANTS panic reproduced instead lives at ip_output.c:185s assert (mroute callout-context send), a different code site unaffected by the pfil token; fixing that site belongs with ip_mroute (drain tbf via a netmsg on the protocol thread, like igmp/carp/pfsync timers do).",
 "fix_evidence": "fix.diff; VERDICT.md Fix section"
}