[PATCH RFC] Bluetooth: Add generic support for vendor HCI frames
From: Zijun Hu
Date: Mon Jul 20 2026 - 01:14:27 EST
For Qualcomm multi-subsystem BT chips, the transport wire carries both
BT-HCI and PERI-HCI frames. PERI is a subsystem on the chip. Take the
upcoming QCC2072 support as an example:
Packet type BT-HCI indicator PERI-HCI indicator
--------------------------------------------------------------
CMD (Host -> Controller) 0x01 0x31
ACL Data (bidirectional) 0x02 0x32
EVENT (Controller -> Host) 0x04 0x34
To generically support vendor HCI frames:
- Show them in btmon logs as they appear on the wire.
- Allow userspace to send/receive them via HCI_CHANNEL_USER, with a
socket option to control RX, defaulting off to eliminate regression
risk for existing applications.
- Add hdev->recv_vendor() to handle vendor frames in hci_rx_work() like
BT frame handlers.
- Add hci_send_vendor_frame() API similar to existing __hci_cmd_send().
Signed-off-by: Zijun Hu <zijun.hu@xxxxxxxxxxxxxxxx>
---
Hi Luiz
To address the below concerns you raised on previous
discussion, and inspired from your suggestion to add hdev callback:
- Not use safer skb helper like skb_pull_data()
- It will skip sending to the monitor, making debugging much harder
Looking forward to your further comments.
Why hdev->is_vendor() instead of multiplexing vendor frames under a
virtual HCI_VENDOR_PKT + vendor indicator byte ?
1) The virtual HCI_VENDOR_PKT isn't used by the core itself
currently, and may be removed later.
2) It makes full use of the indicator (hci_skb_pkt_type(skb)) space,
which is large enough to accommodate vendor frames without an
extra layer of nesting inside HCI_VENDOR_PKT.
3) It lets userspace and btmon logs see the exact same frame as it
appears on the wire.
Previous discussion link:
https://lore.kernel.org/all/CABBYNZJpdeoZ16bObLRPhng2dfyNeK9ix9_-z_hMZhJ0QxTxGg@xxxxxxxxxxxxxx
https://lore.kernel.org/all/CABBYNZJ0zoVVVxuM48L=Km==gnrQuskrbjB6q_aNV0KEEY3+5w@xxxxxxxxxxxxxx
---
include/net/bluetooth/bluetooth.h | 2 +
include/net/bluetooth/hci.h | 1 +
include/net/bluetooth/hci_core.h | 13 +++++++
include/net/bluetooth/hci_mon.h | 2 +
net/bluetooth/hci_core.c | 22 +++++++++++
net/bluetooth/hci_sock.c | 78 ++++++++++++++++++++++++++++++++++-----
6 files changed, 108 insertions(+), 10 deletions(-)
diff --git a/include/net/bluetooth/bluetooth.h b/include/net/bluetooth/bluetooth.h
index b624da5026f5..d9870a43a2c4 100644
--- a/include/net/bluetooth/bluetooth.h
+++ b/include/net/bluetooth/bluetooth.h
@@ -256,6 +256,8 @@ struct bt_codecs {
#define BT_SCM_PKT_SEQNUM 0x05
+#define BT_RCV_VENDOR_PKT 23
+
__printf(1, 2)
void bt_info(const char *fmt, ...);
__printf(1, 2)
diff --git a/include/net/bluetooth/hci.h b/include/net/bluetooth/hci.h
index 50f0eef71fb1..ac97550cbdba 100644
--- a/include/net/bluetooth/hci.h
+++ b/include/net/bluetooth/hci.h
@@ -398,6 +398,7 @@ enum {
/* HCI socket flags */
enum {
HCI_SOCK_TRUSTED,
+ HCI_SOCK_RCV_VENDOR_PKT,
HCI_MGMT_INDEX_EVENTS,
HCI_MGMT_UNCONF_INDEX_EVENTS,
HCI_MGMT_EXT_INDEX_EVENTS,
diff --git a/include/net/bluetooth/hci_core.h b/include/net/bluetooth/hci_core.h
index e7133ff87fbf..20a455d90ef3 100644
--- a/include/net/bluetooth/hci_core.h
+++ b/include/net/bluetooth/hci_core.h
@@ -645,6 +645,8 @@ struct hci_dev {
int (*setup)(struct hci_dev *hdev);
int (*shutdown)(struct hci_dev *hdev);
int (*send)(struct hci_dev *hdev, struct sk_buff *skb);
+ bool (*is_vendor)(struct hci_dev *hdev, const struct sk_buff *skb);
+ void (*recv_vendor)(struct hci_dev *hdev, struct sk_buff *skb);
void (*notify)(struct hci_dev *hdev, unsigned int evt);
void (*hw_error)(struct hci_dev *hdev, u8 code);
int (*post_init)(struct hci_dev *hdev);
@@ -660,6 +662,15 @@ struct hci_dev {
u8 (*classify_pkt_type)(struct hci_dev *hdev, struct sk_buff *skb);
};
+static inline bool hci_is_vendor_frame(struct hci_dev *hdev,
+ const struct sk_buff *skb)
+{
+ if (!hdev->is_vendor)
+ return false;
+
+ return hdev->is_vendor(hdev, skb);
+}
+
#define hci_set_quirk(hdev, nr) set_bit((nr), (hdev)->quirk_flags)
#define hci_clear_quirk(hdev, nr) clear_bit((nr), (hdev)->quirk_flags)
#define hci_test_quirk(hdev, nr) test_bit((nr), (hdev)->quirk_flags)
@@ -2327,6 +2338,8 @@ static inline int hci_check_conn_params(u16 min, u16 max, u16 latency,
int hci_register_cb(struct hci_cb *hcb);
int hci_unregister_cb(struct hci_cb *hcb);
+int hci_send_vendor_frame(struct hci_dev *hdev, struct sk_buff *skb);
+
int __hci_cmd_send(struct hci_dev *hdev, u16 opcode, u32 plen,
const void *param);
diff --git a/include/net/bluetooth/hci_mon.h b/include/net/bluetooth/hci_mon.h
index 4b2a0af4ed58..7710688c0d30 100644
--- a/include/net/bluetooth/hci_mon.h
+++ b/include/net/bluetooth/hci_mon.h
@@ -50,6 +50,8 @@ struct hci_mon_hdr {
#define HCI_MON_ISO_RX_PKT 19
#define HCI_MON_DRV_TX_PKT 20
#define HCI_MON_DRV_RX_PKT 21
+#define HCI_MON_VENDOR_TX_PKT 22
+#define HCI_MON_VENDOR_RX_PKT 23
struct hci_mon_new_index {
__u8 type;
diff --git a/net/bluetooth/hci_core.c b/net/bluetooth/hci_core.c
index 1e324c05af24..c12c180e8293 100644
--- a/net/bluetooth/hci_core.c
+++ b/net/bluetooth/hci_core.c
@@ -2920,6 +2920,8 @@ int hci_recv_frame(struct hci_dev *hdev, struct sk_buff *skb)
case HCI_DRV_PKT:
break;
default:
+ if (hci_is_vendor_frame(hdev, skb))
+ break;
kfree_skb(skb);
return -EINVAL;
}
@@ -3053,6 +3055,18 @@ static int hci_send_conn_frame(struct hci_dev *hdev, struct hci_conn *conn,
return hci_send_frame(hdev, skb);
}
+int hci_send_vendor_frame(struct hci_dev *hdev, struct sk_buff *skb)
+{
+ if (hci_is_vendor_frame(hdev, skb))
+ return hci_send_frame(hdev, skb);
+
+ bt_dev_err(hdev, "invalid vendor frame with pkt_type 0x%2.2x",
+ hci_skb_pkt_type(skb));
+ kfree_skb(skb);
+ return -EINVAL;
+}
+EXPORT_SYMBOL(hci_send_vendor_frame);
+
/* Send HCI command */
int hci_send_cmd(struct hci_dev *hdev, __u16 opcode, __u32 plen,
const void *param)
@@ -4066,6 +4080,14 @@ static void hci_rx_work(struct work_struct *work)
break;
default:
+ if (hci_is_vendor_frame(hdev, skb)) {
+ BT_DBG("%s Vendor packet with type 0x%2.2x",
+ hdev->name, hci_skb_pkt_type(skb));
+ if (hdev->recv_vendor) {
+ hdev->recv_vendor(hdev, skb);
+ break;
+ }
+ }
kfree_skb(skb);
break;
}
diff --git a/net/bluetooth/hci_sock.c b/net/bluetooth/hci_sock.c
index 070ca388f9ac..903498e6db3d 100644
--- a/net/bluetooth/hci_sock.c
+++ b/net/bluetooth/hci_sock.c
@@ -229,12 +229,16 @@ void hci_send_to_sock(struct hci_dev *hdev, struct sk_buff *skb)
} else if (hci_pi(sk)->channel == HCI_CHANNEL_USER) {
if (!bt_cb(skb)->incoming)
continue;
- if (hci_skb_pkt_type(skb) != HCI_EVENT_PKT &&
- hci_skb_pkt_type(skb) != HCI_ACLDATA_PKT &&
- hci_skb_pkt_type(skb) != HCI_SCODATA_PKT &&
- hci_skb_pkt_type(skb) != HCI_ISODATA_PKT &&
- hci_skb_pkt_type(skb) != HCI_DRV_PKT)
+ if (hci_is_vendor_frame(hdev, skb)) {
+ if (!hci_sock_test_flag(sk, HCI_SOCK_RCV_VENDOR_PKT))
+ continue;
+ } else if (hci_skb_pkt_type(skb) != HCI_EVENT_PKT &&
+ hci_skb_pkt_type(skb) != HCI_ACLDATA_PKT &&
+ hci_skb_pkt_type(skb) != HCI_SCODATA_PKT &&
+ hci_skb_pkt_type(skb) != HCI_ISODATA_PKT &&
+ hci_skb_pkt_type(skb) != HCI_DRV_PKT) {
continue;
+ }
} else {
/* Don't send frame to other channel types */
continue;
@@ -359,6 +363,7 @@ void hci_send_to_monitor(struct hci_dev *hdev, struct sk_buff *skb)
struct sk_buff *skb_copy = NULL;
struct hci_mon_hdr *hdr;
__le16 opcode;
+ bool is_vendor_frame = false;
if (!atomic_read(&monitor_promisc))
return;
@@ -400,7 +405,26 @@ void hci_send_to_monitor(struct hci_dev *hdev, struct sk_buff *skb)
opcode = cpu_to_le16(HCI_MON_VENDOR_DIAG);
break;
default:
- return;
+ is_vendor_frame = hci_is_vendor_frame(hdev, skb);
+ if (!is_vendor_frame)
+ return;
+ if (bt_cb(skb)->incoming)
+ opcode = cpu_to_le16(HCI_MON_VENDOR_RX_PKT);
+ else
+ opcode = cpu_to_le16(HCI_MON_VENDOR_TX_PKT);
+ break;
+ }
+
+ if (is_vendor_frame) {
+ skb_copy = __pskb_copy_fclone(skb, HCI_MON_HDR_SIZE + 1,
+ GFP_ATOMIC, true);
+ if (!skb_copy)
+ return;
+
+ *(u8 *)skb_push(skb_copy, 1) = hci_skb_pkt_type(skb);
+ hdr = skb_push(skb_copy, HCI_MON_HDR_SIZE);
+ hdr->len = cpu_to_le16(skb->len + 1);
+ goto out_comm;
}
/* Create a private copy with headroom */
@@ -408,14 +432,15 @@ void hci_send_to_monitor(struct hci_dev *hdev, struct sk_buff *skb)
if (!skb_copy)
return;
- hci_sock_copy_creds(skb->sk, skb_copy);
-
/* Put header before the data */
hdr = skb_push(skb_copy, HCI_MON_HDR_SIZE);
+ hdr->len = cpu_to_le16(skb->len);
+
+out_comm:
hdr->opcode = opcode;
hdr->index = cpu_to_le16(hdev->id);
- hdr->len = cpu_to_le16(skb->len);
+ hci_sock_copy_creds(skb->sk, skb_copy);
hci_send_to_channel(HCI_CHANNEL_MONITOR, skb_copy,
HCI_SOCK_TRUSTED, NULL);
kfree_skb(skb_copy);
@@ -1868,7 +1893,8 @@ static int hci_sock_sendmsg(struct socket *sock, struct msghdr *msg,
hci_skb_pkt_type(skb) != HCI_ACLDATA_PKT &&
hci_skb_pkt_type(skb) != HCI_SCODATA_PKT &&
hci_skb_pkt_type(skb) != HCI_ISODATA_PKT &&
- hci_skb_pkt_type(skb) != HCI_DRV_PKT) {
+ hci_skb_pkt_type(skb) != HCI_DRV_PKT &&
+ !hci_is_vendor_frame(hdev, skb)) {
err = -EINVAL;
goto drop;
}
@@ -2017,6 +2043,7 @@ static int hci_sock_setsockopt(struct socket *sock, int level, int optname,
{
struct sock *sk = sock->sk;
int err = 0;
+ int opt_int;
u16 opt;
BT_DBG("sk %p, opt %d", sk, optname);
@@ -2050,6 +2077,23 @@ static int hci_sock_setsockopt(struct socket *sock, int level, int optname,
hci_pi(sk)->mtu = opt;
break;
+ case BT_RCV_VENDOR_PKT:
+ if (hci_pi(sk)->channel != HCI_CHANNEL_USER) {
+ err = -ENOPROTOOPT;
+ break;
+ }
+
+ err = copy_safe_from_sockptr(&opt_int, sizeof(opt_int),
+ optval, optlen);
+ if (err)
+ break;
+
+ if (opt_int)
+ hci_sock_set_flag(sk, HCI_SOCK_RCV_VENDOR_PKT);
+ else
+ hci_sock_clear_flag(sk, HCI_SOCK_RCV_VENDOR_PKT);
+ break;
+
default:
err = -ENOPROTOOPT;
break;
@@ -2132,6 +2176,7 @@ static int hci_sock_getsockopt(struct socket *sock, int level, int optname,
{
struct sock *sk = sock->sk;
int err = 0;
+ int opt_int;
u16 mtu;
BT_DBG("sk %p, opt %d", sk, optname);
@@ -2153,6 +2198,19 @@ static int hci_sock_getsockopt(struct socket *sock, int level, int optname,
err = -EFAULT;
break;
+ case BT_RCV_VENDOR_PKT:
+ if (hci_pi(sk)->channel != HCI_CHANNEL_USER) {
+ err = -ENOPROTOOPT;
+ break;
+ }
+
+ opt_int = hci_sock_test_flag(sk, HCI_SOCK_RCV_VENDOR_PKT) ?
+ 1 : 0;
+ if (copy_to_iter(&opt_int, sizeof(opt_int),
+ &sopt->iter_out) != sizeof(opt_int))
+ err = -EFAULT;
+ break;
+
default:
err = -ENOPROTOOPT;
break;
---
base-commit: 7f02cdbb4cf17df8aa1977eea0cf490c472a706d
change-id: 20260719-support_vendor_hci-1fbb4449ab8e
Best regards,
--
Zijun Hu <zijun.hu@xxxxxxxxxxxxxxxx>