sco_input uses while-loop (not if) for receive buffer check β oversized SCO packet hangs protocol thread in infinite loop
Summary
sco_socket.c:221-222: while(m->m_pkthdr.len > sbspace(&so->so_rcv)) sbdroprecord(&so->so_rcv.sb). If inbound SCO packet exceeds sco_recvspace (4096 :81) and buffer is empty/under-full, sbdroprecord on empty buffer is no-op, sbspace never grows, loop never exits. L2CAP l2cap_socket.c:231 and RFCOMM rfcomm_socket.c:241 correctly use if (drop packet). Only SCO has broken while. Triggered by malicious paired BT peer sending oversized SCO data or malicious USB BT dongle injecting via hci_sco_recv->sco_input. hci_sco_recv (hci_link.c:827) strips 3-byte header and passes to sco_input with NO upper-bound check against sco_recvspace. Hangs BT protocol thread indefinitely. Requires root HCI socket or malicious BT hardware/peer, hence Medium not High.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0710 Β· 20 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | readme | build/run/expected + reachability notes | 2.2 KB | β raw |
| VERDICT.md | verdict | full narrative: source trace, harness, machine-code proof, fix, threat model | 9.9 KB | β raw |
| sco_input_logic.c | trigger-source | deterministic code-level harness replicating exact kernel primitives (sbspace/sbdroprecord/sco_recvspace); buggy-while vs fixed-if | 7.4 KB | view raw |
| sco_input_hang.c | trigger-source | original live PoC (raw HCI socket); documents live unreachability on guest | 2.0 KB | view raw |
| build.sh | build-script | builds both the logic harness and the live PoC | 508 B | view raw |
| run.sh | run-script | runs the logic harness + live-PoC-unreachable demonstration | 840 B | view raw |
| build.log | build-log | harness compile output | 13 B | view raw |
| run.log | run-log | decisive logic-harness run (full output) | 1.5 KB | view raw |
| run.2.log | run-log | stress-test run 2 (identical) | 255 B | view raw |
| run.3.log | run-log | stress-test run 3 (identical) | 255 B | view raw |
| live_poc_unreachable.log | run-log | live PoC: 'socket: Protocol not supported' (no BT) | 47 B | view raw |
| env.txt | environment | uname, gcc, BT reachability facts | 410 B | view raw |
| sco_input_baseline.disasm | disassembly | baseline netbt.ko sco_input: backward branch 5b2: jg 581 = the infinite loop | 3.4 KB | β download |
| sco_input_patched.disasm | disassembly | patched netbt.ko sco_input: forward branch 635: jg 660 only, NO backward branch | 5.5 KB | β download |
| netbt_baseline_disasm.txt | disassembly | full baseline netbt.ko disassembly | 650.4 KB | β download |
| netbt_patched_disasm.txt | disassembly | full patched netbt.ko disassembly | 649.6 KB | β download |
| fix_build.log | build-log | netbt.ko built before/after fix (full, both rc=0 with -Werror, with sco_input disasm) | 59.1 KB | view raw |
| fix.diff | suggested-fix | git-apply-able: while -> if { m_freem; return } matching l2cap/rfcomm | 781 B | view raw |
| ../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-0710 PoC: sco_input infinite loop (while vs if)
Bug
sys/netbt/sco_socket.c:221-222 uses a while loop where the sibling BT
socket implementations use a one-shot if { drop; return; }
(sys/netbt/l2cap_socket.c:231, sys/netbt/rfcomm_socket.c:241):
while (m->m_pkthdr.len > sbspace(&so->so_rcv))
sbdroprecord(&so->so_rcv.sb);
If an inbound SCO packet exceeds sco_recvspace (4096, sco_socket.c:81) and
the receive buffer cannot free enough room (including the empty-buffer case),
sbdroprecord on an empty buffer is a no-op (uipc_sockbuf.c:524, if (m)),
sbspace never grows, and the loop never exits. The Bluetooth protocol thread
spins at 100% CPU forever. Pure DoS β nothing is corrupted.
Reachability on the audit guest β UNREACHABLE LIVE
sco_input is driven only by hci_sco_recv (sys/netbt/hci_link.c:867),
which fires from a real Bluetooth controller's RX path. The guest has:
- no options BLUETOOTH in X86_64_GENERIC (so sco_input is not in the
running kernel),
- netbt.ko present but not loaded,
- no Bluetooth controller (no PCI/USB BT device).
The original live PoC sco_input_hang therefore fails with
socket: Protocol not supported. The bug is reproduced instead by the
deterministic code-level harness sco_input_logic.c, which models the exact
kernel primitives (sbspace macro, sbdroprecord no-op-on-empty,
sco_recvspace=4096). See VERDICT.md for the full source trace and the
machine-code (netbt.ko before/after) confirmation.
Reproduce
./build.sh && ./run.sh
Expected
sco_input_logic: 4 of 6 oversize cases printBUGGY(while): iters=100000 *** WOULD LOOP FOREVER ***whileFIXED (if): iters=1 (dropped packet); the 2 control cases (small / exactly-fitting packet) terminate on both. Exit 0 = bug demonstrated.sco_input_hang(live):socket: Protocol not supported(documents live unreachability).
Fix
fix.diff: change while to if { m_freem(m); return; } (matches l2cap/rfcomm).
Validated by building netbt.ko before/after: baseline sco_input has a
backward branch 5b2: jg 581 (the loop); patched has only a forward
branch 635: jg 660 (one-shot drop) and no backward branch. Both compile clean
with -Werror.
DF-0710 β VERDICT
Verdict: REPRODUCED (code-level; live path unreachable on guest)
Impact: dos (kernel thread infinite-loop / hang in sco_input)
Fix: VALIDATED at compile + machine-code + logic level (not_testable live)
1. The bug (confirmed in source)
sys/netbt/sco_socket.c:210-228 β sco_input(arg, m):
while (m->m_pkthdr.len > sbspace(&so->so_rcv)) /* line 221 */
sbdroprecord(&so->so_rcv.sb); /* line 222 */
...
sbappendrecord(&so->so_rcv.sb, m);
sorwakeup(so);
A while is used where the two sibling Bluetooth socket implementations use a
one-shot if { drop; return; }:
sys/netbt/l2cap_socket.c:231βif (m->m_pkthdr.len > sbspace(&so->so_rcv)) { kprintf(...); m_freem(m); return; }sys/netbt/rfcomm_socket.c:241βif (m->m_pkthdr.len > sbspace(&so->so_rcv)) { kprintf(...); m_freem(m); return; }
Only SCO has the broken while. The consequence, proven by tracing each kernel
primitive:
sbspace(sys/netbt/bluetooth.h:150-152) =(long) imin((int)(ssb_hiwat - ssb_cc), (int)(ssb_mbmax - ssb_mbcnt)). For an empty SCO receive buffer this equalssco_recvspace= 4096 (sys/netbt/sco_socket.c:81, set viasoreserveinsco_sattach:282).sbdroprecord(sys/kern/uipc_sockbuf.c:517-535) is a no-op when the buffer is empty β it is guarded byif (m)at line 524 (m = sb->sb_mb; if (m) {...}). On an empty buffer it frees nothing, sossb_cc/ssb_mbcntand thereforesbspacedo not change.
Therefore: an inbound SCO packet with m_pkthdr.len > 4096 arriving at a
socket whose receive buffer cannot free enough room (including the trivially
empty buffer) makes the loop condition permanently true: sbdroprecord no-ops,
sbspace never grows, and the loop never exits. The Bluetooth protocol thread
spins at 100% CPU forever. This is a deterministic, unbounded kernel hang.
The packet does not need to be enormous β a single byte over the hi-water mark
(4097) is sufficient (harness case 2). The bug is a pure control-flow defect;
nothing is corrupted (no write primitive), so there is no escalation chain.
2. Reachability on this guest β UNREACHABLE LIVE
sco_input is reachable only via the BT controller input path:
BT controller RX β hci_sco_recv(m, unit) (sys/netbt/hci_link.c:828)
strips the 3-byte SCO header (line 839) and calls
(*link->hl_sco->sp_proto->input)(...) (sys/netbt/hci_link.c:867), which is
the sco_input callback registered in sco_proto (sco_socket.c:70-78,
.input = sco_input). hci_sco_recv performs no upper-bound check against
sco_recvspace (only DIAGNOSTIC-gated self-consistency checks at lines
841-854), so an oversized-but-self-consistent packet is forwarded unchanged.
Confirmed on the audit guest (6.5-DEVELOPMENT #0):
| Check | Result |
|---|---|
options BLUETOOTH in X86_64_GENERIC |
absent (count 0) |
sco_input symbols in /boot/kernel/kernel |
0 (not compiled into kernel) |
netbt kernel module loaded (kldstat) |
none loaded |
netbt.ko present in /boot/kernel/ |
yes (stock, not loaded) |
Bluetooth controller (pciconf) |
no BT PCI/USB device |
So the vulnerable routine is not present in the running kernel AND there is no
hardware to drive hci_sco_recv even if netbt.ko were loaded. The live PoC
sco_input_hang consequently fails with socket: Protocol not supported
(captured in live_poc_unreachable.log).
Because the live path is unreachable, the bug is reproduced by a deterministic
code-level harness that replicates the exact in-kernel primitives
(sbspace macro, sbdroprecord no-op-on-empty semantics, sco_recvspace=4096).
This is the accepted path for latent / hardware-gated findings.
3. Reproduction harness β sco_input_logic.c
sco_input_logic.c faithfully models the sockbuf as a record queue with
ssb_cc/ssb_mbcnt accounting and implements both sco_input_buggy (the
while) and sco_input_fixed (the if+drop), with a 100000-iteration cap to
detect the infinite loop without actually hanging. Result (3 consistent runs):
[case 1] empty buf + oversize pkt (8192 > 4096)
BUGGY(while): iters=100000 *** WOULD LOOP FOREVER *** (appended=0)
FIXED (if) : iters=1 (dropped packet)
[case 2] empty buf + pkt 4097 (> hiwat 4096 by 1)
BUGGY(while): iters=100000 *** WOULD LOOP FOREVER *** (appended=0)
FIXED (if) : iters=1 (dropped packet)
[case 3] half-full buf (2048) + oversize pkt 8192
BUGGY(while): iters=100000 *** WOULD LOOP FOREVER *** (appended=0)
FIXED (if) : iters=1 (dropped packet)
[case 4] 3 records (1024 each) + oversize pkt 8192
BUGGY(while): iters=100000 *** WOULD LOOP FOREVER *** (appended=0)
FIXED (if) : iters=1 (dropped packet)
[case 5] empty buf + small pkt 100 (control)
BUGGY(while): iters=0 (terminated) (appended=1)
FIXED (if) : iters=0 (appended)
[case 6] empty buf + pkt 4096 (== hiwat, fits)
BUGGY(while): iters=0 (terminated) (appended=1)
FIXED (if) : iters=0 (appended)
SUMMARY: BUGGY(while) infinite-loops: 4 / 6 ; FIXED(if) <=1 iter on ALL: YES
Cases 5-6 are controls: normally-sized packets terminate on both paths, so the
fix does not regress legitimate traffic. Cases 1-4 (any packet exceeding
sco_recvspace, with empty through multi-record buffers) all infinite-loop in
the buggy version and are cleanly dropped by the fix.
4. Machine-code confirmation (netbt.ko, built before/after fix)
Because sco_socket.c is optional bluetooth and options BLUETOOTH is absent
from X86_64_GENERIC, the file is not compiled into the default kernel;
the correct build target is the netbt.ko module (sys/netbt/Makefile,
bsd.kmod.mk). Both builds use -Werror.
sco_input disassembly of the baseline netbt.ko (offset 0x540):
57f: 7e 33 jle 5b4 <sco_input+0x74> ; if fits, go append 581: ... mov %rbx,%rdi 584: e8 .. .. .. .. callq sbdroprecord ; drop a record ... 5b2: 7f cd jg 581 <sco_input+0x41> ; *** BACKWARD BRANCH = loop *** 5b4: ... (append + sorwakeup)
The 7f cd jg 581 at 0x5b2 is the compiled while β a backward conditional
branch wrapping the sbdroprecord call. When sbdroprecord is a no-op this
branch is taken unconditionally and forever.
sco_input disassembly of the patched netbt.ko (offset 0x610):
635: 7f 29 jg 660 <sco_input+0x50> ; one-shot if -> drop path 637: ... push/save 64b: e8 .. .. .. .. callq sbappendrecord ; normal append 65a: e9 .. .. .. .. jmpq sorwakeup (tail) 660: 48 89 f7 mov %rsi,%rdi ; drop path: arg = m 663: e9 .. .. .. .. jmpq m_freem (tail) + return
There is no backward branch in the patched sco_input. The loop is
structurally impossible: a single forward jg 660 redirects to the m_freem
drop path. Module size changes (105024 -> 104960 bytes) confirming the code
differ. This is conclusive proof the fix eliminates the loop at the
machine-code level.
5. The fix (fix.diff)
Minimal, root-cause fix matching the sibling implementations exactly:
- while (m->m_pkthdr.len > sbspace(&so->so_rcv))
- sbdroprecord(&so->so_rcv.sb);
+ if (m->m_pkthdr.len > sbspace(&so->so_rcv)) {
+ DPRINTF("%s: packet (%d bytes) dropped (socket buffer full)\n",
+ __func__, m->m_pkthdr.len);
+ m_freem(m);
+ return;
+ }
The comment above is updated to reflect that the packet is dropped (the old
"dump data until the latest one will fit" wording described the buggy intent).
git apply --check passes against the read-only sys/ tree.
6. Impact / threat model
- Effect: unbounded kernel-thread CPU spin; the Bluetooth protocol thread is
wedged and the BT subsystem becomes unresponsive. Pure DoS β no memory is
corrupted (
sbdroprecordis a no-op on an empty buffer), so no privilege escalation primitive exists. This is a logic/DoS bug, not memory corruption. - Trigger preconditions (realistic): (a) a malicious paired Bluetooth peer
that sends an oversized SCO data packet, or (b) a malicious/compromised USB
Bluetooth dongle injecting via
hci_sco_recv, or (c) a root raw-HCI socket with an active SCO connection. No unprivileged-local-only path exists without BT hardware. Severity Medium is appropriate (local/peer DoS requiring BT hardware or root HCI; no privesc, no remote-without-BT).
7. Files in this evidence pack
| File | Purpose |
|---|---|
sco_input_logic.c |
deterministic code-level harness (buggy vs fixed logic) |
sco_input_hang.c |
original (live) PoC β proves live unreachability on guest |
build.sh / run.sh |
exact reproducible build/run |
build.log |
harness build output |
run.log, run.2.log, run.3.log |
3 stress-test runs (identical results) |
live_poc_unreachable.log |
live PoC socket: Protocol not supported |
env.txt |
guest uname, gcc, BT reachability facts |
sco_input_baseline.disasm |
baseline sco_input: backward branch jg 581 |
sco_input_patched.disasm |
patched sco_input: forward branch jg 660, no loop |
fix_build.log |
netbt.ko built before/after fix (full, with disasm) |
fix.diff |
git-apply-able one-line fix (while -> if + drop) |
manifest.json |
machine-readable catalog |
Fix verification
not_testableFIXED at compile + machine-code + logic level; live-run not_testable. The task explicitly permits 'netbt module' / 'compile check' / 'harness before-after' for this finding given BT unreachability, and all three were performed: (1) git apply --check passes against the read-only sys/ tree; (2) netbt.ko (the only build unit that compiles sco_socket.c, since the default kernel has no options BLUETOOTH) builds clean with -Werror both UNPATCHED (rc=0) and PATCHED (rc=0); (3) objdump of the two modules proves the loop is structurally eliminated -- baseline sco_input has a backward branch 5b2: 7f cd jg 581 wrapping callq sbdroprecord (the compiled while-loop), patched sco_input has ONLY a forward 635: 7f 29 jg 660 to the m_freem drop path with NO backward branch (module size 105024->104960); (4) the logic harness shows before=100000-iter-loop on 4/6 oversize cases vs after=1-iter-drop on all. fix_status is not_testable rather than fixed ONLY because the live PoC cannot run on this guest (no BT hardware + sco_input not in default kernel); the bad behavior (infinite loop) is otherwise proven gone at the machine-code level. Recommend the maintainer also boot a BLUETOOTH-enabled kernel on real BT hardware to confirm no behavioral regression on legitimate in-bound SCO traffic, but the one-line while->if change is identical to two already-shipping sibling implementations, so regression risk is minimal.
BASELINE netbt.ko sco_input @0x540: `57f: jle 5b4; 584: callq sbdroprecord; 5b2: 7f cd jg 581 <sco_input+0x41>` (BACKWARD branch = infinite loop). PATCHED netbt.ko sco_input @0x610: `635: 7f 29 jg 660 <sco_input+0x50>` (FORWARD, one-shot if -> drop); 660: mov %rsi,%rdi; jmpq m_freem. NO backward branch in patched sco_input. Both builds rc=0 -Werror. Harness: buggy iters=100000 (LOOP FOREVER) on 4/6 cases; fixed iters=1 (dropped) on all; controls (small/exactly-fitting pkt) iters=0 appended on both. Live PoC: 'socket: Protocol not supported' (unreachable on guest).
Confirmed kernel references
Detail
Exploit chain
none (non-corruption DoS). The defect is a pure control-flow/logic bug: sbdroprecord on an empty buffer is a true no-op (it frees nothing and changes no accounting), so the only effect is an unbounded kernel-thread CPU spin. There is NO write primitive, NO memory corruption, NO UAF/double-free/type-confusion, and therefore NO escalation path to uid=0. Realistic impact ceiling: permanent denial of service of the Bluetooth subsystem (protocol thread wedged). Trigger requires either a malicious paired BT peer sending an oversized SCO packet, a malicious/compromised USB BT dongle injecting via hci_sco_recv (hci_link.c:867, which has no upper-bound check against sco_recvspace), or a root raw-HCI socket with an active SCO connection -- no unprivileged-local-only path exists without BT hardware. Severity Medium is appropriate.
Evidence (decisive lines)
Harness (3 identical runs): [case 1] empty buf + oversize pkt 8192: BUGGY(while) iters=100000 *** WOULD LOOP FOREVER *** ; FIXED(if) iters=1 (dropped packet). [case 2] pkt 4097 (one byte over hiwat): BUGGY iters=100000 LOOP FOREVER ; FIXED drops. [case 4] 3 records + oversize pkt: BUGGY loops, FIXED drops. SUMMARY: BUGGY(while) infinite-loops 4/6 cases; FIXED(if) <=1 iter on ALL. Live PoC: 'socket: Protocol not supported'. Machine-code: baseline sco_input @0x540 `5b2: 7f cd jg 581 <sco_input+0x41>` (backward branch = loop) wrapping `584: callq sbdroprecord`; patched sco_input @0x610 only `635: 7f 29 jg 660` (forward to m_freem drop), no backward branch. netbt.ko size 105024(baseline)->104960(patched); both build rc=0 with -Werror.
PoC changes
Added sco_input_logic.c -- a deterministic code-level harness (the live path needs a Bluetooth controller absent on the guest) that faithfully replicates the exact kernel primitives: the sbspace() macro from bluetooth.h:150-152, sbdroprecord() no-op-on-empty semantics from uipc_sockbuf.c:517-535, and sco_recvspace=4096 from sco_socket.c:81. It implements both sco_input_buggy (the while) and sco_input_fixed (the if+drop) with a 100000-iter loop-cap detector, across 6 cases (4 oversize, 2 controls). Also added fix.diff (while->if+drop matching l2cap/rfcomm), build.sh, run.sh, VERDICT.md, manifest.json, and captured full before/after netbt.ko build logs + sco_input disassemblies. The original sco_input_hang.c live PoC is retained to document live unreachability.
Verified recommended fix
In sys/netbt/sco_socket.c:221-222 change while (m->m_pkthdr.len > sbspace(&so->so_rcv)) sbdroprecord(&so->so_rcv.sb); to if (m->m_pkthdr.len > sbspace(&so->so_rcv)) { m_freem(m); return; } (optionally with a DPRINTF), matching l2cap_socket.c:231 and rfcomm_socket.c:241 exactly. This eliminates the infinite loop for oversized packets while preserving normal in-bound delivery. The full git-apply-able diff (also updates the stale comment) is in findings/poc/DF-0710/fix.diff; it supersedes/matches the finding markdown's recommended approach (the finding markdown proposed no diff but described the same while->if intent). Validated: git apply --check passes; netbt.ko compiles clean with -Werror before and after; baseline sco_input has backward branch 5b2: jg 581 (loop), patched has only forward 635: jg 660 (drop) with no backward branch.
Verdict
REPRODUCED at code level (live path unreachable on guest). sys/netbt/sco_socket.c:221 uses while (m->m_pkthdr.len > sbspace(&so->so_rcv)) sbdroprecord(...) where the sibling BT socket implementations (l2cap_socket.c:231, rfcomm_socket.c:241) use a one-shot if { m_freem; return; }. Because sbdroprecord is a NO-OP on an empty buffer (uipc_sockbuf.c:524, guarded by if (m)) and sbspace (bluetooth.h:150-152) equals sco_recvspace=4096 when empty, any SCO packet larger than 4096 bytes makes the condition permanently true and the loop never exits -> the Bluetooth protocol thread spins at 100% CPU forever. Confirmed by (a) line-by-line source trace of all three primitives, (b) a deterministic harness replicating the exact kernel logic (4/6 oversize cases hit the 100000-iter loop cap on the buggy path, 0 on the fixed path; 2 control cases pass on both, across 3 identical runs), and (c) machine-code: baseline netbt.ko sco_input has a backward branch 5b2: 7f cd jg 581 wrapping callq sbdroprecord = the compiled while-loop; patched netbt.ko has only a forward 635: 7f 29 jg 660 to the m_freem drop path with NO backward branch. The path is unreachable live on the audit guest (no options BLUETOOTH in X86_64_GENERIC, netbt.ko not loaded, no Bluetooth controller; the live PoC fails with 'socket: Protocol not supported'), so the harness is the accepted deterministic reproduction.
No comments yet.