[PATCH] Bluetooth: btusb_qcom: check reassembly bounds and pull results
From: Noah Weinberger
Date: Sun Nov 08 2026 - 17:34:20 EST
Hello,
Three hardening gaps in the new Qualcomm multi-subsystem transport, all
of them patterns the sibling paths already handle:
1. btusb_qcom_recv_intr() computes the expected payload length from
the device-supplied header and re-enters the fill loop without
checking it against the skb tailroom. The bulk path
(btusb_qcom_recv_bulk) rejects hci_skb_expect(skb) >
QPERI_MAX_FRAME_SIZE - skb->len; the interrupt path relies solely on
the u8 width of the two length fields and a three-byte margin.
2. btqcom_recv_vendor_pkt() dereferences skb_pull_data(skb, 1)
without a NULL check.
3. btusb_qcom_send_frame() does the same for vendor packets on the TX
path.
None of these is reachable with the current in-tree producers (the
empty-vendor-packet invariant is established several call-hops away),
so this is regression protection for brand-new code rather than a live
bug fix. The patch mirrors the bulk-path check and adds the NULL
checks.
Affected version/commit: mainline fa4ad798f6f681031775266044ba1b0cf3c670bd (v7.4-rc1, 2026-11-07), drivers/bluetooth/btusb_qcom.c:4387-4462 (recv_intr), 2624-2628 (recv_vendor_pkt), 4258-4262 (send_frame).
New driver merged in this window.
This was found during an assisted (AI-supported) static source review of
mainline fa4ad798f6f681031775266044ba1b0cf3c670bd (v7.4-rc1, 2026-11-07); every code path cited below was re-verified by the
reporter against that tree. The patch below has NOT been compiled and has
NOT been runtime tested, and no reproducer was produced - the triggering
conditions are described above instead. Please treat this as a report for
verification rather than a battle-tested fix.
Reported-by: Noah Weinberger <noahweinberger2003@xxxxxxxxx>
---
diff --git a/drivers/bluetooth/btusb_qcom.c b/drivers/bluetooth/btusb_qcom.c
index cc4cfcd..3c41e60 100644
--- a/drivers/bluetooth/btusb_qcom.c
+++ b/drivers/bluetooth/btusb_qcom.c
@@ -2623,7 +2623,13 @@ out_free_skb:
/* Implements hdev->recv_vendor_pkt(). */
static void btqcom_recv_vendor_pkt(struct hci_dev *hdev, struct sk_buff *skb)
{
- hci_skb_pkt_type(skb) = *(const u8 *)skb_pull_data(skb, 1);
+ const u8 *pkt = skb_pull_data(skb, 1);
+
+ if (!pkt) {
+ kfree_skb(skb);
+ return;
+ }
+ hci_skb_pkt_type(skb) = *pkt;
switch (hci_skb_pkt_type(skb)) {
case QPERI_EVENT_PKT:
@@ -4257,8 +4263,13 @@ int btusb_qcom_send_frame(struct hci_dev *hdev, struct sk_buff *skb)
pkt_type = hci_skb_pkt_type(skb);
is_vendor = pkt_type == HCI_VENDOR_PKT;
/* an empty vendor packet has no path to reach here */
- if (is_vendor)
- hci_skb_pkt_type(skb) = *(const u8 *)skb_pull_data(skb, 1);
+ if (is_vendor) {
+ const u8 *pkt = skb_pull_data(skb, 1);
+
+ if (!pkt)
+ return -EILSEQ;
+ hci_skb_pkt_type(skb) = *pkt;
+ }
pkt_type = hci_skb_pkt_type(skb);
need_lock = pkt_type == HCI_COMMAND_PKT ||
@@ -4438,5 +4449,13 @@ struct sk_buff *btusb_qcom_recv_intr(struct hci_dev *hdev, struct sk_buff *skb,
else if (pkt_type == HCI_EVENT_PKT)
hci_skb_expect(skb) = hci_event_hdr(skb)->plen;
+ if (hci_skb_expect(skb) >
+ QPERI_MAX_EVENT_SIZE - skb->len) {
+ dev_kfree_skb_irq(skb);
+ skb = NULL;
+ *err = -EILSEQ;
+ break;
+ }
+
if (hci_skb_expect(skb))
continue;