Num_Compl_Pkts completion credit wedges hci_acl_complete() in a no-progress infinite loop: NETISR thread (holding the mplock) spins forever -> total system hang from one 8-byte event
Summary
The attacker-controlled completion count passes unvalidated into hci_acl_complete: when the head PDU has lp_pending==0 with data still queued (the routine full-controller-buffer state), the loop executes num -= 0 forever with no state change - the bluetooth NETISR thread holding the MPLOCK spins permanently: guest-verified total system hang from one well-formed 8-byte event (probes timeout, no panic, no recovery). Distinct from DF-0558 (parse bounds) - a completion-accounting state-machine defect on well-formed input. no-progress guard + lc_pending underflow protection fix validated on a patched rebuild. Also documented: KKASSERT is a no-op on non-INVARIANTS kernels, so the pass-1 DF-0559/0560 'panic guards' silently vanish into OOB instead (aggravation, root cause DF-0561).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5734 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | β | 15.4 KB | view raw | |
| hci_stub.c | β | 2.5 KB | view raw | |
| Makefile | β | 535 B | β download | |
| build.sh | β | 83 B | view raw | |
| run.sh | β | 193 B | view raw | |
| xfer.sh | β | 801 B | view raw | |
| build.log | β | 463 B | view raw | |
| run.log | β | 1.5 KB | view raw | |
| fix.diff | β | 2.7 KB | view raw | |
| fix_run.log | β | 1.6 KB | view raw | |
| fix_build.log | β | 7.3 KB | view raw | |
| fix_guard_panic_serial.txt | β | 2.7 KB | view raw | |
| README.md | β | 3.6 KB | β raw | |
| VERDICT.md | β | 5.9 KB | β raw | |
| verdict.json | β | 3.8 KB | view raw | |
| manifest.json | β | 1.1 KB | view raw | |
| env.txt | β | 667 B | view raw |
DF-5734 β Num_Compl_Pkts β hci_acl_complete() no-progress livelock (permanent system hang)
File: sys/netbt/hci_event.c (pass 2) + sink sys/netbt/hci_link.c
Severity: High β CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:C/C:N/I:N/A:H (CWE-835)
Verdict: REPRODUCED β one injected 8-byte HCI event permanently wedges the kernel.
Root cause
hci_event_num_compl_pkts() (sys/netbt/hci_event.c:380-405) takes the
attacker-controlled compl_pkts count from the event and passes it unvalidated
to hci_acl_complete(link, num) (sys/netbt/hci_event.c:393).
hci_acl_complete() (sys/netbt/hci_link.c:708-748) loops:
while (num > 0) {
pdu = TAILQ_FIRST(&link->hl_txq);
if (pdu == NULL) { ...; return; }
if (num >= pdu->lp_pending) {
num -= pdu->lp_pending;
pdu->lp_pending = 0;
if (IF_QEMPTY(&pdu->lp_data)) { ...remove/complete... }
/* else: NOTHING β pdu stays, lp_pending now 0 */
} else { ... }
}
If the head PDU has lp_pending == 0 with data still queued β the normal
state whenever a PDU is queued while the controller reported zero free buffers
(hci_acl_start() returns early at sys/netbt/hci_link.c:631-632) β then
num -= 0 never decrements num, the queue never changes, and the loop never
terminates. Any num >= 1 wedges the bluetooth NETISR thread
(btintr() β hci_intr() β hci_event(), sys/netbt/bt_input.c:31-40,
sys/netbt/hci_unit.c:329-352) while it holds the mplock
(sys/netbt/bt_input.c:36), starving the kernel. This is distinct from the
pass-1 parse-loop findings (DF-0558): different function, different root cause
(no-progress iteration vs. unbounded parse).
Also fixed here: chan->lc_pending-- (sys/netbt/hci_link.c:734) is a uint8_t
that can underflow to 0xFF when completion credit exceeds outstanding fragments,
permanently stalling the L2CAP channel (l2cap_lower.c:174,
l2cap_upper.c:244 both gate on lc_pending == 0).
Threat model
The event source is the Bluetooth controller (or anything able to inject HCI events to the host: malicious/compromised controller firmware, hostile USB device presenting a bt controller, HCI transport injection). No authentication exists on the HCI transport. Net effect from ONE well-formed 8-byte event: total system unavailability (hard reset required).
Build & run (inside the guest, as root)
cd /root/hcih && sh build.sh # copies VERBATIM /usr/src/sys/netbt sources,
# builds hci_event_harness.ko
kldload ./hci_event_harness.ko
sysctl hw.hcih_setup=1 # attach fake unit + ACL link #5 (OPEN)
sysctl hw.hcih_arm_pdu=1 # queue 1 unsent PDU fragment, lp_pending=0
sysctl hw.hcih_fire_numcompl=1 # inject Num_Compl_Pkts(handle=5, num=1)
Expected result (baseline)
Console prints the pre-fire state, the injection marker β then goes silent;
the guest stops responding entirely (ssh banner timeout, vm.sh status β
down). No panic: a livelock. See run.log.
Fix validation
Apply fix.diff (progress guard in hci_acl_complete + lc_pending
underflow guard) to /usr/src/sys/netbt, rebuild the harness (build.sh copies
the patched sources verbatim), rerun the identical trigger: the sysctl
returns (SYSCTL_RC=0), the guest stays alive and keeps processing further
triggers (fix_run.log). The guard's execution is directly evidenced by the
intermediate fix build (fix_guard_panic_serial.txt): its diagnostic
device_get_nameunit() call faulted on the harness's fake device_t, proving
the new code path is reached in netisr context. See VERDICT.md for the full
narrative.
DF-5734 VERDICT β REPRODUCED (permanent kernel livelock / total system hang)
Bottom line
Reproduced, deterministic, root-caused, fix validated. A single
well-formed 8-byte HCI Num_Compl_Pkts event β deliverable by any
malicious/compromised Bluetooth controller or HCI-transport injector β drives
hci_event_num_compl_pkts() (sys/netbt/hci_event.c:380-405) into
hci_acl_complete(link, num) (sys/netbt/hci_link.c:708-748) whose completion
loop can make no progress when the head PDU of the link txq has
lp_pending == 0 with data still queued (the routine state for a PDU waiting
for free controller buffers). The loop decrements num by 0 forever; the
bluetooth NETISR thread (btintr, sys/netbt/bt_input.c:31-40) spins
permanently while holding the mplock, and the guest becomes totally
unresponsive β no panic, no recovery short of reset.
Why the injected path is the production path
- The KLD compiles
hci_event.c,hci_link.c,hci_misc.c,hci_unit.c,bt_input.ccopied verbatim from/usr/src/sys/netbtβ md5s identical to the audit tree and to the guest's/usr/src(baseline: hci_event.cc3e1e3eaβ¦, hci_link.ca2ee71c2β¦, hci_unit.cd7f992b1β¦, bt_input.c35d12188β¦; recorded inbuild.log/setup.log). - The harness attaches the unit through the production
hci_attach()and injects the event through the production entry pointhci_input_event()βnetisr(NETISR_BLUETOOTH)βbtintr()βhci_intr()βhci_event()β the exact path a controller driver (ubt/btuart) feeds. - The stuck precondition (PDU queued,
lp_pending == 0, data queued) is the routine production state created byhci_acl_send()wheneverunit->hci_num_acl_pkts == 0:hci_acl_start()returns atsys/netbt/hci_link.c:631-632leaving the PDU queued-but-unsent. The harness reproduces it with the production functions (hci_link_alloc+ a PDU enqueued exactly asl2cap_lower.cdoes).
Observed sequence (run.log + serial console)
hw.hcih_setup=1β unit + ACL link #5 (OPEN) attached via productionhci_attach().hw.hcih_arm_pdu=1β[harness] armed: 1 PDU on link #5 txq, lp_pending=0, lp_data qlen=1.hw.hcih_fire_numcompl=1β[harness] pre-fire state: head lp_pending=0, head lp_data qlen=1; event04 13 05 01 05 00 01 00(Num_Compl_Pkts, handle 5, num 1) handed tohci_input_event().- Console prints the harness "injected" marker β then silence. The
triggering
sysctlnever completes (hw.hcih_fire_numcompl: 0printed, the-> 1epilogue never appears: the syscall thread is starved too). - Post-trigger probes:
Connection timed out during banner exchange(x2),vm.sh statusβ down. No panic signature anywhere on the serial console β this is a livelock, not a crash.
Distinction from the pass-1 family
DF-0558 (unbounded num_con_handles parse loop), DF-0559/0560/0561 all
concern parsing trust (event length fields / KKASSERT guards) with
short/malformed packets. DF-5734 is a different bug class in a different
loop: the packet here is well-formed; the defect is the completion
accounting state machine (hci_acl_complete) lacking a progress guarantee.
Both bugs are reachable from the same event, but neither subsumes the other;
the fixes are disjoint.
Impact ceiling β availability, not memory corruption
The primitive is an infinite spin in a softint thread holding the mplock: no
attacker-controlled memory write occurs before the spin (the loop body only
touches num, pdu->lp_pending and the PDU list head). There is no
slab-groom/forge route from this bug. Adjacent accounting corruption
(chan->lc_pending uint8_t underflow, sys/netbt/hci_link.c:734) only
poisons flow control (channel stall), not memory layout. Impact = permanent,
unauthenticated total-system DoS from the controller side.
Fix validation
fix.diff adds a no-progress guard (if (pdu->lp_pending == 0) return;) and
an lc_pending underflow guard. Applied to the guest's /usr/src/sys/netbt,
harness rebuilt against the patched sources, identical trigger re-run:
SYSCTL_RC=0,ALIVE_AFTER_3S, guest fully responsive (fix_run.log).- Link-state transition observed on the fixed build proves the guard path
executes: after the event, the armed PDU moved from
lp_pending=0, qlen=1tolp_pending=1, qlen=0β i.e.hci_acl_complete()returned via the new guard,hci_event_num_compl_pkts()then creditedunit->hci_num_acl_pkts += 1andhci_acl_start()transmitted the fragment (sys/netbt/hci_link.c:646-669). - Direct proof the guard is reached in netisr context: the first fix-build
attempt used
device_get_nameunit()in the guard's diagnostic kprintf and panicked inside that call evaluating the argument (fix_guard_panic_serial.txt:Stopped at device_get_nameunit: movq 0x58(%rdi),%rax) β the guard ran; the print argument was a harness artifact (fake device_t). The final guard print (device-independent) does not appear in msgbuf β netisr-context kprintf output is dropped by the console layer on this guest β but the state transition above is the behavioral proof. - Normal completion still works post-fix: a follow-up Num_Compl_Pkts on a PDU
with
lp_pending=1, qlen=0completed it and freed the PDU cleanly (guest stayed alive through three more triggers on the same boot).
Reproduction & stress
Baseline wedge reproduced on the first firing; repeated probes confirmed permanence (no recovery over ~2 minutes). Post-fix, the same trigger ran cleanly twice on one boot alongside the DF-5735/DF-5736 triggers.
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
fixedWith fix.diff applied to /usr/src/sys/netbt and the harness rebuilt against the patched sources, the identical trigger completes (SYSCTL_RC=0) and the guest stays alive; the armed PDU transitions lp_pending 0->1/qlen 1->0 via the guard-return + hci_acl_start path, proving the new early-return executes; normal completion of a pending PDU still works.
fix_run.log (F1 FIXED RUN block: SYSCTL_RC=0, ALIVE_AFTER_3S, subsequent triggers on same boot); fix_guard_panic_serial.txt (guard kprintf arg evaluation fault = guard reached in netisr context)
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log: console sequence (pre-fire state lp_pending=0/qlen=1 -> inject -> silence) + ssh banner timeouts + vm.sh status down; VERDICT.md: verbatim-build md5s and state-transition proof of the guard; fix_run.log: SYSCTL_RC=0 + ALIVE_AFTER_3S on patched build; fix_guard_panic_serial.txt: guard reached in netisr context (fault in its kprintf arg evaluation)
PoC changes
harness wrote m_copyback2 results inside KKASSERT() which is a no-op without INVARIANTS - copyback never ran; replaced with real error checks. netisr_register needs 3 args on this tree. Stubbed l2cap_send_{connect_req,connect_rsp,config_req} and sco_detach for the verbatim link layer.
Verified recommended fix
hci_acl_complete(): return when the head PDU has lp_pending==0 (no-progress guard); guard chan->lc_pending decrement against underflow
Verdict
A single well-formed 8-byte HCI Num_Compl_Pkts event drives hci_event_num_compl_pkts() (sys/netbt/hci_event.c:380-405) into hci_acl_complete() (sys/netbt/hci_link.c:708-748), whose completion loop makes no progress when the link's head PDU has lp_pending==0 with data queued (routine state while controller buffers are full): num -= 0 forever, the bluetooth NETISR thread spins holding the mplock, and the guest becomes totally unresponsive (no panic, recovery only by reset). Distinct from the pass-1 parse-loop findings (DF-0558/0559/0560/0561): this is a completion-accounting state-machine defect triggered by a WELL-FORMED event. Fix (no-progress guard + lc_pending underflow guard) validated on the guest: identical trigger returns cleanly and the guest stays alive.
No comments yet.