hci_detach() frees struct hci_unit with no lifetime protection for in-flight kernel processing: btintr()'s unit-list walk and hci_intr()'s devlock loop (and in-flight driver rx, and hci_unit_lookup's unreferenced pointers) operate on the freed unit -> UAF through hci_devlock
Summary
hci_detach removes the unit under a same-CPU crit (only defers interrupts locally) and kfrees it, while btintr walks the same list and hci_intr's per-packet loop re-acquires hci_devlock on the netisr thread: reproduced 'Fatal trap 12 read at 0x90 in lockmgr_exclusive' at round <=1 of an attach/flood/detach loop. Every hci_unit_lookup consumer (sockets, ioctls) also retains a raw pointer for the socket lifetime. Distinct from DF-0683 (the faulting object is the unit itself). USB hot-unplug or kldunload trigger; chunk reuse turns the stale lock into arbitrary-address read/write. refcount+spinlock+referenced-walk fix validated 300/300 rounds clean.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5754 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | β | 22.3 KB | view raw | |
| hci_stub.c | β | 2.4 KB | view raw | |
| Makefile | β | 624 B | β download | |
| build.sh | β | 84 B | view raw | |
| run.sh | β | 160 B | view raw | |
| xfer.sh | β | 801 B | view raw | |
| build.log | β | 13.0 KB | view raw | |
| setup.log | β | 871 B | view raw | |
| run.log | β | 1.1 KB | view raw | |
| fix_run.log | β | 13.1 KB | view raw | |
| panic.txt | β | 582 B | view raw | |
| fix.diff | β | 3.9 KB | view raw | |
| README.md | β | 2.1 KB | β raw | |
| VERDICT.md | β | 4.5 KB | β raw | |
| manifest.json | β | 1.3 KB | view raw | |
| verdict.json | β | 4.7 KB | view raw | |
| env.txt | β | 206 B | view raw |
DF-5754 β hci_detach frees the unit with no lifetime protection for in-flight netisr processing: unit UAF
File: sys/netbt/hci_unit.c (pass 2) β hci_attach/hci_detach,
unit list, hci_unit_lookup
Trigger surface: driver detach (kldunload / USB hot-unplug) racing
rx traffic
Build (inside the guest, as root)
cd /root/hciuu sh build.sh # verbatim production netbt sources (md5s recorded)
Run (inside the guest, as root)
kldload /root/hciuu/hci_unit_harness.ko
sysctl hw.hciu_setup=1 # base unit (also registers netisr)
sysctl hw.hciud_fire=300 # 300 rounds of [attach + flood + detach]
# (hciud_fire=2 uses the slow inquiry-16
# payload of the original baseline)
Expected (baseline, unfixed)
The guest dies within the first rounds:
Fatal trap 12: page fault while in kernel mode cpuid = 0 fault virtual address = 0x90 fault code = supervisor read data, page not present current process = Idle Stopped at lockmgr_exclusive+0x5d: orl (%r14),%esi
hci_detach() (hci_unit.c:120-130) runs hci_disable(), removes the
unit from hci_unit_list (under a crit_enter() that only defers
interrupts on the current CPU) and kfrees it β while btintr()
(bt_input.c:37) is walking the same list and hci_intr() is mid-drain on
the netisr thread (goto another re-locks &unit->hci_devlock on every
packet, hci_unit.c:335). The freed unit's hci_devlock is then locked
through freed (reused) memory. The driver-rx side
(hci_input_event(unit) in flight during detach) has the same lifetime
hole; hci_unit_lookup() (hci_unit.c:265-279) likewise returns raw,
unreferenced unit pointers that sockets and upper layers retain.
Fix check
Apply fix.diff (spinlock-guarded list + hci_refcnt acquired by the
netisr walker via a snapshot, hci_detach() waiting the count out
before kfree; the driver remains responsible for quiescing its own rx
before detach, as documented in the diff) and re-run hciud_fire=300:
all 300 rounds survive, guest up (fix_run.log).
DF-5754 β VERDICT
Status: reproduced (use-after-free through the freed unit's hci_devlock, netisr/detach race). Impact: panic (UAF read/write on the unit object). Confidence: certain (race window widened by flood; the faulting instruction and object are exact).
Root cause
hci_detach() (sys/netbt/hci_unit.c:120-130):
crit_enter();
hci_disable(unit);
TAILQ_REMOVE(&hci_unit_list, unit, hci_next);
crit_exit();
kfree(unit, M_BLUETOOTH);
There is no reference counting on struct hci_unit anywhere in
netbt, and the crit_enter() only defers interrupts on the detaching
CPU β it does not stop the netisr thread (possibly another CPU) that is
- walking
hci_unit_listinbtintr()(bt_input.c:37,TAILQ_FOREACH(unit, &hci_unit_list, hci_next) hci_intr(unit);), and - inside
hci_intr(unit)(hci_unit.c:329-411), whosegoto anotherloop re-acquireslockmgr(&unit->hci_devlock, LK_EXCLUSIVE)on every packet β i.e. writes to the unit object it may be executing past its lifetime.
When hci_detach() runs under rx load (controller detaching while
traffic is in flight β USB hot-unplug or driver module unload), the unit
is freed underneath the walker: the next lockmgr() operates on the
freed (and reusable) chunk. The same lifetime hole covers driver rx in
flight (hci_input_event(unit, m) taking the same devlock) and every
hci_unit_lookup() consumer (hci_unit.c:265-279 returns a raw pointer
that sockets/upper layers retain for the socket lifetime:
hci_socket.c:503, l2cap_upper.c:161, sco_upper.c:139, hci_ioctl.c:151).
Reproduction
Rig: 300 rounds of [hci_attach() (production) β flood-inject
hci_input_event() from a kthread β hci_detach() (production)]. The
baseline (continuous flood, inquiry-16 payload) died in round 0/1:
Fatal trap 12: page fault while in kernel mode cpuid = 0 fault virtual address = 0x90 fault code = supervisor read data, page not present current process = Idle (kernel/netisr thread) Stopped at lockmgr_exclusive+0x5d: orl (%r14),%esi
lockmgr_exclusive reading through r14 (the lock pointer loaded from
the freed unit) faulting at 0x90 = a garbage lock address out of freed
memory β the lockmgr(&unit->hci_devlock, ...) in hci_intr/
hci_input_event on a unit freed by hci_detach. This is distinct from
DF-0683 (hci_disable mutating the queues/link list without devlock):
the fault here is on the unit object's lock itself, which only
hci_detach's kfree can have freed.
Fix validation
fix.diff:
struct hci_unitgainshci_refcnt; a globalhci_unit_lockspinlock guards the list and the counts.btintr()snapshots the (referenced) unit list under the spinlock and drops the references afterhci_intr().hci_detach()removes the unit under the spinlock and waits the reference count out beforekfree(). (The controller driver remains responsible for quiescing its own rx callbacks before detach β same contract as before, now documented; the rig's fake driver honours it by stopping and draining its injector indisable.)
Post-fix: hciud_fire=300 (filler-packet flood keeps the netisr walker
busy during every detach) β all 300 rounds survived, guest up
(fix_run.log). One earlier fix-check iteration also ran 100+ rounds
clean of the unit-lifetime signature before tripping the separate,
already-known DF-0683 (hci_disable's unlocked drain racing hci_intr's
queue processing β different function, different mechanism, queue/memo
corruption rather than the unit UAF); the shipped rig mode 1 avoids
allocating memos during the race to keep the two findings separable,
and the final 300-round run completed with no incident at all.
Rig-evolution note (honesty)
The rig's driver-quiesce emulation initially had a TOCTOU (enable-flag
check before the inflight increment) that let one injector call slip
past fake_disable()'s drain β a rig bug producing the same
lockmgr_exclusive signature during fix-check; fixed by incrementing
g_d_inflight before checking the flag. The baseline panic (above) was
captured before that change and reflects the genuinely unprotected
kernel (flood continuous through detach).
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedApplied 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.
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)
Confirmed kernel references
Detail
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 (decisive lines)
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
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 driver's 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
Verified 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()
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 unit's 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.
No comments yet.