DF-5754 / verdict.json
{ "finding_id": "DF-5754", "status": "reproduced", "verdict": "Reproduced as a use-after-free on the unit object: hci_detach() (hci_unit.c:120-130) removes the unit from hci_unit_list under a crit_enter() that only defers interrupts on the detaching CPU and kfree()s it, while btintr() (bt_input.c:37) walks the same list and hci_intr()s per-packet loop re-acquires lockmgr(&unit->hci_devlock, LK_EXCLUSIVE) (hci_unit.c:335) on the netisr thread - under rx load the freed units lock is then operated through freed/reused memory. Baseline rig (300 rounds of hci_attach + flood + hci_detach) faulted in round 0/1: Fatal trap 12, read at 0x90, Stopped at lockmgr_exclusive+0x5d, current process Idle. Distinct from DF-0683 (queue/list mutation inside hci_disable racing hci_intr): the faulting object here is the unit itself, freed only by hci_detach. hci_unit_lookup() (hci_unit.c:265-279) likewise returns raw unreferenced pointers retained by sockets/upper layers. Fix validated: spinlock-guarded list + hci_refcnt taken by the btintr snapshot walker + hci_detach waiting the count before kfree -> all 300 flood-detach rounds clean.", "reproduced": true, "impact": "panic", "confidence": "certain", "exploit_chain": "controller detach (USB hot-unplug / kldunload of the driver) while rx traffic is in flight -> kfree(unit) underneath btintr()/hci_intr() (or an in-flight hci_input_event) -> lockmgr operates on freed M_BLUETOOTH chunk; chunk reuse (next hci_attach / link alloc, same zone) turns the stale lock pointer into arbitrary-address read/write in lockmgr; escalation requires root-or-physical detach trigger, so the practical impact is a privileged-context memory-corruption/DoS, plus the unrefcounted hci_unit_lookup pointers dangling in sockets after any detach", "evidence": "panic.txt / run.log: Fatal trap 12 ... fault virtual address = 0x90, supervisor read ... current process = Idle ... Stopped at lockmgr_exclusive+0x5d: orl (%r14),%esi after round 0 survived; fix_run.log: post-fix rounds through 280 and all rounds done without a hit with guest alive", "kernel_refs": [ "sys/netbt/hci_unit.c:123", "sys/netbt/hci_unit.c:126", "sys/netbt/hci_unit.c:129", "sys/netbt/hci_unit.c:335", "sys/netbt/hci_unit.c:265", "sys/netbt/bt_input.c:37" ], "poc_changes": "rig attaches/detaches production units in a loop while a kthread floods production hci_input_event() (the driver-rx stand-in); the fake drivers disable() stops and drains the injector exactly as a real controller driver must (increment-before-check discipline after a rig TOCTOU initially let one call slip past the drain - documented in VERDICT.md); fix-check mode uses memo-free filler packets to keep the known DF-0683 (hci_disable drain race) separable from this unit-lifetime race", "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": 600, "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.hciud_fire=300", "code_hash": "d6b73fd72e85527a98201eb9da96d1256e9d1bda0ddd017d55267f708cf018dc", "notes": "Trigger requires driver detach (root via kldunload, or physical hot-unplug), hence Medium severity despite the clean UAF. An earlier fix-check iteration survived 100+ rounds free of the unit-lifetime signature and then tripped the separate known DF-0683 (hci_disables unlocked drain) - different function/mechanism; final shipped rig avoids that surface and completed 300 rounds with no incident. The DF-5752 fix (schednetisr) and this fix both touch btintr(); the shipped per-finding diffs apply independently to pristine sources but must be merged (fix5752s replymsg line + fix5754s snapshot walk) when combined, as done in the validated combined tree.", "recommended_fix": "Guard hci_unit_list with a spinlock, give struct hci_unit a reference count acquired by the btintr() snapshot walker, and make hci_detach() wait the count out before kfree()", "fix_status": "fixed", "fix_baseline_reproduced": true, "fix_patched_reproduced": false, "fix_verdict": "Applied fix.diff (hci.h hci_refcnt member + prototypes; hci_unit.c spinlock-guarded attach/detach with refcnt wait + hci_unit_ref/hci_unit_rele; bt_input.c snapshot walk under the spinlock, merged with the DF-5752 replymsg line), rebuilt, rebooted, re-ran hciud_fire=300: rounds 0..280 all survived, all rounds done without a hit, guest alive and responsive.", "fix_evidence": "findings/poc/DF-5754/fix_run.log: round 280 survived and all rounds done without a hit with the guest still up (post-fix); contrast panic.txt (lockmgr_exclusive UAF at round <=1 pre-fix)" } |