β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5754

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5754 Β· 17 files
FileTypeDescriptionSize
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).

VERDICT.md
↓ download raw

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_list in btintr() (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), whose goto another loop re-acquires lockmgr(&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_unit gains hci_refcnt; a global hci_unit_lock spinlock guards the list and the counts.
  • btintr() snapshots the (referenced) unit list under the spinlock and drops the references after hci_intr().
  • hci_detach() removes the unit under the spinlock and waits the reference count out before kfree(). (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 in disable.)

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

fixed
baseline reproduced→ patch + rebuild →patched clean

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.

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)
↓ fix.diffper-fix-DF-5754

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.