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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0683 Β· 9 files
FileTypeDescriptionSize
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
VERDICT.md verdict source-level race analysis (3 race variants) + why not testable here
↓ download 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:337 reads eventqlen > 0.
  • hci_disable:253 zeroes eventqlen.
  • hci_intr:338 IF_DEQUEUE returns NULL (queue drained by IF_DRAIN).
  • hci_intr:339 eventqlen-- underflows to 65535 (uint16_t).
  • hci_intr:342 KKASSERT(m != NULL) panics β€” or m->m_pkthdr.len at :345 NULL-derefs.

Race (b) β€” IF_DRAIN vs IF_DEQUEUE mbuf lifetime

  • Concurrent IF_DRAIN and IF_DEQUEUE on the same ifqueue (a singly-linked mbuf list with no synchronization) corrupts the list / frees an mbuf still referenced by the other side β†’ UAF.
  • hci_intr:395 does TAILQ_FOREACH(link, &unit->hci_links, hl_next) after releasing hci_devlock at :390.
  • hci_disable:238-245 does TAILQ_REMOVE + kfree on the same links with no lock β†’ UAF on struct hci_link.

Trigger (theoretical)

  • SIOCSBTFLAGS (sys/netbt/hci_ioctl.c:224) toggling BTF_UP while BT traffic is being processed (root-only).
  • Driver detach / kldunload while 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_disable is 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-deref m->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_testable

compile 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.