⬢ DragonFlyBSD Kernel Audit
DF-5752 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-5752",
  "status": "reproduced",
  "verdict": "Reproduced deterministically in two stages on the DF guest. Stage 1 (every rx burst >= 2 packets): hci_input_event/acl/sco and hci_complete_sco hand each mbuf to netisr_queue() (embedding the netmsg inside the mbuf, sys/net/netisr.c:424-431) while hci_intr()/hci_event() free the mbufs during the first btintr() drain, leaving 1..N-1 netmsgs MSGF_QUEUED on the netisr port through freed memory (21 dangling observations observed per 8-packet burst; hci_disables IF_DRAIN is a second instance). Stage 2 (chunk reuse during the window, e.g. a same-CPU controller rx interrupt during the drain): netisr_queue() re-initialises a still-queued netmsg and TAILQ_INSERT_TAILs the same node twice -> panic link elm ... next->prev != elm in lwkt_spin_getport <- netmsg_service_loop. On kernels without the queue assertion this is silent corruption of the lwkt port queue including the dispatch-through-freed-memory path (msg->nm_dispatch). Fix validated: schednetisr() kick instead of netisr_queue() (plus btintr replying the kick per the ngintr contract) gives dangling_observations=0 and no panic on the previously-crashing trigger.",
  "reproduced": true,
  "impact": "panic",
  "confidence": "certain",
  "exploit_chain": "remote/local rx burst (>=2 packets, any controller traffic) -> mbufs freed with netmsgs still port-queued -> netisr thread operates TAILQ linkage and nm_dispatch out of freed objcache chunks (works by luck while chunks stay intact) -> chunk reuse during the window re-initialises a live netmsg and double-links the node -> port queue corruption (panic on assert kernels); escalation path on non-assert kernels: controlled re-init/double-queue of the node and, via slab recycling, dispatch through attacker-influenced memory",
  "evidence": "run.log: 16+ DF-5752 PROOF ... already freed by hci_event() ... still had MSGF_QUEUED set (flags=0x4) lines (21 observations per burst) and the panic link elm 0xfffff80117da8428 next->prev != elm / lwkt_spin_getport+0x134 / netmsg_service_loop+0xaf; fix_run.log: post-fix dangling_observations=0 on both baseline and fire, no panic; setup.log: md5s proving verbatim production sources",
  "kernel_refs": [
    "sys/netbt/hci_unit.c:437",
    "sys/netbt/hci_unit.c:459",
    "sys/netbt/hci_unit.c:481",
    "sys/netbt/hci_unit.c:543",
    "sys/netbt/hci_unit.c:252",
    "sys/netbt/bt_input.c:32",
    "sys/netbt/hci_event.c:229",
    "sys/net/netisr.c:424",
    "sys/kern/lwkt_msgport.c:655"
  ],
  "poc_changes": "harness tracks every injected mbuf + embedded lmsg; the hci_mtap() hook (called by hci_intr for every packet before hci_event frees the previous ones) records freed-with-MSGF_QUEUED observations in memory (netisr-context kprintf gets dropped by the serial driver) and re-injects 3 events mid-drain from netisr context to force same-cpu objcache LIFO reuse of chunks with queued netmsgs; probe sysctl prints the mbuf/netmsg layout (mh_netmsg@40, nm_dispatch@104) which killed the initial pkthdr-overlay theory and redirected the PoC to the double-queue corruption",
  "attempts": 4,
  "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": 900,
  "guest_dirty": false,
  "build_cmd": "cd /root/hciuu && sh build.sh",
  "run_cmd": "kldload ./hci_unit_harness.ko; sysctl hw.hciu_setup=1; sysctl hw.hciu_fire=2; sysctl hw.hciu_fire=1",
  "code_hash": "d6b73fd72e85527a98201eb9da96d1256e9d1bda0ddd017d55267f708cf018dc",
  "notes": "netbt is not compiled into GENERIC; any third-party/ported controller driver (ubt class) attaching via hci_attach() enables the path. The trigger is unauthenticated remote (any rx burst from a peer). The DF-5736 rig (128 back-to-back injections) has been unknowingly exercising the works-by-luck window. Distinct from DF-0683 (hci_disable unlocked queue mutation vs hci_intr): the fix for that does not fix this, and vice versa; also covers the hci_disable IF_DRAIN free path (hci_unit.c:252-262).",
  "recommended_fix": "Replace the four netisr_queue(NETISR_BLUETOOTH, m) calls with schednetisr(NETISR_BLUETOOTH) and reply the kick message in btintr() (ngintr contract)",
  "fix_status": "fixed",
  "fix_baseline_reproduced": true,
  "fix_patched_reproduced": false,
  "fix_verdict": "Applied fix.diff to the guest /usr/src/sys/netbt (verbatim-source module rebuild): post-fix fire=2 and fire=1 both report dangling_observations=0 and no panic; the module kept draining correctly across repeated kicks (mtap_calls=8/11) proving the replymsg contract was needed and works; the 1500-round DF-5753 fix-check ran in the same boot without any netisr anomaly.",
  "fix_evidence": "findings/poc/DF-5752/fix_run.log: [harness] done: dangling_observations=0 reuse_hits=0 mtap_calls=8 (baseline) and dangling_observations=0 reuse_hits=2 mtap_calls=11 (fire, previously the panic trigger) with the system surviving; contrast run.log panic"
}