hci_disable modifies queues and link list without hci_devlock, racing with hci_intr (panic / mbuf UAF)
Summary
hci_disable (:217-263): line 250 comment "no need to hold hci_devlock the driver is disabled" but hci_intr (:328-411) is netisr-driven (bt_input.c:btintr iterates hci_unit_list calls hci_intr unconditionally) NOT driver-driven keeps running after driver disable. hci_disable line 231 calls (*hci_if->disable)() then line 232 clears BTF_RUNNING then :238-245 TAILQ_FOREACH hci_link_free (TAILQ_REMOVE+kfree from hci_links) then :252-262 IF_DRAIN all ifqueues + zeroes eventqlen/aclrxqlen/scorxqlen NONE under hci_devlock. Race (a): hci_intr :337 reads eventqlen>0 hci_disable :253 zeroes it hci_intr :339 decrements uint16_t underflow to 65535 next iter IF_DEQUEUE returns NULL KKASSERT(m!=NULL) :342 panic or NULL deref m->m_pkthdr.len :345. Race (b): IF_DRAIN and IF_DEQUEUE race same mbuf linked list no sync corrupted queue/freed-mbuf UAF. Race (c): hci_intr :395 TAILQ_FOREACH(hci_links) after releasing lock :390 hci_disable :238-245 TAILQ_REMOVE+kfree same links UAF on struct hci_link. Attacker: root SIOCSBTFLAGS hci_ioctl.c:224 or driver detach/kldunload toggle BTF_UP while BT traffic flowing. Impact: panic A:H + limited UAF C:L/I:L heap-grooming speculative. Fix: acquire hci_devlock around ALL hci_disable teardown + NULL-check after IF_DEQUEUE in hci_intr.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0683 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| build.sh | build-script | no-op (no live PoC possible) | 300 B | view raw |
| run.sh | run-script | no-op (no live PoC possible) | 228 B | view raw |
| VERDICT.md | verdict | source-level race analysis (3 race variants) + why not testable here | 4.9 KB | β raw |
| run.log | run-log | guest checks: no BT HW, no hci_unit, hci_disable unreachable | 579 B | view raw |
| fix.diff | suggested-fix | git-apply-able: lockmgr(hci_devlock) around hci_disable teardown + NULL-check m in hci_intr | 1.3 KB | view raw |
| env.txt | environment | uname, cc, no BT modules/devices | 339 B | view raw |
| fix_build.log | build-log | compile-validation: kernel+module build with fix applied, rc=0, no errors | 5.6 MB | β download |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0683 β hci_disable (hci_unit.c) teardown races hci_intr (no lock)
Verdict
SOURCE-CONFIRMED, NOT TESTABLE ON THIS GUEST. The race is unambiguous in
source; the trigger requires a registered hci_unit, which requires Bluetooth
hardware (or a virtual BT adapter) β absent from this VM. There is also no
unprivileged trigger: the realistic race window needs SIOCSBTFLAGS (root) or
driver detach (kldunload/unplug) concurrent with active Bluetooth traffic.
Mechanism
sys/netbt/hci_unit.c:217-263 hci_disable:
void
hci_disable(struct hci_unit *unit)
{
...
(*unit->hci_if->disable)(unit->hci_dev);
unit->hci_flags &= ~BTF_RUNNING;
/* close down any links ... */
for (acl = 0 ; acl < 2 ; acl++) { // :238
next = TAILQ_FIRST(&unit->hci_links);
while ((link = next) != NULL) {
next = TAILQ_NEXT(link, hl_next);
if (acl || link->hl_type != HCI_LINK_ACL)
hci_link_free(link, ECONNABORTED); // TAILQ_REMOVE + kfree
}
}
while ((memo = LIST_FIRST(&unit->hci_memos)) != NULL)
hci_memo_free(memo);
/* (no need to hold hci_devlock, the driver is disabled) */ // :250
IF_DRAIN(&unit->hci_eventq); // :252
unit->hci_eventqlen = 0;
IF_DRAIN(&unit->hci_aclrxq);
unit->hci_aclrxqlen = 0;
IF_DRAIN(&unit->hci_scorxq);
unit->hci_scorxqlen = 0;
IF_DRAIN(&unit->hci_cmdwait);
IF_DRAIN(&unit->hci_scodone);
}
The comment at :250 ("no need to hold hci_devlock, the driver is disabled")
is wrong: hci_intr (sys/netbt/hci_unit.c:328-411) is netisr-driven
β btintr in sys/netbt/bt_input.c walks hci_unit_list and calls
hci_intr unconditionally on every BT interrupt β and does take
hci_devlock (:335) around its eventqlen/queue TAILQ touches. After
hci_disable returns, in-flight netisr work can still be running concurrently
on another CPU. The teardown happens without hci_devlock, so:
Race (a) β counter underflow + NULL deref
hci_intr:337readseventqlen > 0.hci_disable:253zeroeseventqlen.hci_intr:338IF_DEQUEUEreturns NULL (queue drained byIF_DRAIN).hci_intr:339eventqlen--underflows to 65535 (uint16_t).hci_intr:342KKASSERT(m != NULL)panics β orm->m_pkthdr.lenat:345NULL-derefs.
Race (b) β IF_DRAIN vs IF_DEQUEUE mbuf lifetime
- Concurrent
IF_DRAINandIF_DEQUEUEon the sameifqueue(a singly-linked mbuf list with no synchronization) corrupts the list / frees an mbuf still referenced by the other side β UAF.
Race (c) β hci_links TAILQ walk UAF
hci_intr:395doesTAILQ_FOREACH(link, &unit->hci_links, hl_next)after releasinghci_devlockat:390.hci_disable:238-245doesTAILQ_REMOVE+kfreeon the same links with no lock β UAF onstruct hci_link.
Trigger (theoretical)
SIOCSBTFLAGS(sys/netbt/hci_ioctl.c:224) togglingBTF_UPwhile BT traffic is being processed (root-only).- Driver detach /
kldunloadwhile BT traffic flowing (root-only).
Both paths require root AND a live hci_unit (Bluetooth hardware or virtual
adapter). The guest has neither.
Why not testable here
- No Bluetooth hardware / virtual adapter in the VM β no registered
hci_unitβhci_disableis never called. - Even with HW, the race needs careful timing of (unprivileged?) traffic vs (root-only) SIOCSBTFLAGS.
So this finding is not live-testable on this guest. The source-level race is unambiguous and high-confidence.
Realistic impact ceiling
- Panic (DoS) via
KKASSERT(m != NULL)or NULL-derefm->m_pkthdr.lenβ Race (a). Deterministic-ish on a busy BT link. - Speculative UAF on
struct hci_link/ freed mbufs (Races b, c). With heap grooming, possibly escalatable, but the precondition (root + BT HW) makes this a rootβkernel hardening gap rather than an unprivileged privesc.
Realistic class: Medium local DoS (root-triggered, BT-HW-dependent) with speculative UAF that would need a lot of work to convert.
Fix
sys/netbt/hci_unit.c:
1. Acquire hci_devlock around the entire teardown in hci_disable (links,
memos, queue drains, counter zeroes). Removes the races by making the
teardown atomic w.r.t. hci_intr.
2. Defense-in-depth in hci_intr: after IF_DEQUEUE returns NULL despite
*rxqlen > 0, skip the KKASSERT and re-enter (goto another) instead of
panicking β so a race that somehow survives the lock doesn't crash.
See fix.diff (git-apply-able, applies cleanly, compiles, kernel boots).
Fix validation (Phase 8)
fix_status: not_testable. The fix.diff applies and compiles as part
of the single-fix-combined kernel (6.5-DEVELOPMENT #1 built and booted). The
race itself cannot be reproduced on a guest without Bluetooth hardware, so no
live before/after measurement is possible here. The fix is the textbook
correct approach for "concurrent teardown vs consumer" races.
Fix verification
not_testablecompile validated
see evidence pack
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
Source-confirmed. hci_disable teardown no lock vs hci_intr netisr -> race/UAF. No BT HW.
No comments yet.