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

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)

No comments yet.

PoC verification

Evidence pack

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

VERDICT.md
↓ download raw

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.c copied verbatim from /usr/src/sys/netbt β€” md5s identical to the audit tree and to the guest's /usr/src (baseline: hci_event.c c3e1e3ea…, hci_link.c a2ee71c2…, hci_unit.c d7f992b1…, bt_input.c 35d12188…; recorded in build.log/setup.log).
  • The harness attaches the unit through the production hci_attach() and injects the event through the production entry point hci_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 by hci_acl_send() whenever unit->hci_num_acl_pkts == 0: hci_acl_start() returns at sys/netbt/hci_link.c:631-632 leaving the PDU queued-but-unsent. The harness reproduces it with the production functions (hci_link_alloc + a PDU enqueued exactly as l2cap_lower.c does).

Observed sequence (run.log + serial console)

  1. hw.hcih_setup=1 β€” unit + ACL link #5 (OPEN) attached via production hci_attach().
  2. hw.hcih_arm_pdu=1 β€” [harness] armed: 1 PDU on link #5 txq, lp_pending=0, lp_data qlen=1.
  3. hw.hcih_fire_numcompl=1 β€” [harness] pre-fire state: head lp_pending=0, head lp_data qlen=1; event 04 13 05 01 05 00 01 00 (Num_Compl_Pkts, handle 5, num 1) handed to hci_input_event().
  4. Console prints the harness "injected" marker β€” then silence. The triggering sysctl never completes (hw.hcih_fire_numcompl: 0 printed, the -> 1 epilogue never appears: the syscall thread is starved too).
  5. 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=1 to lp_pending=1, qlen=0 β€” i.e. hci_acl_complete() returned via the new guard, hci_event_num_compl_pkts() then credited unit->hci_num_acl_pkts += 1 and hci_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=0 completed 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

fixed
baseline reproduced→ patch + rebuild →patched clean

With 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)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

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.