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) | 65 B | view raw |
| run.sh | run-script | no-op (no live PoC possible) | 89 B | view raw |
| VERDICT.md | verdict | source-level race analysis (3 race variants) + why not testable here | 5.3 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 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
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.