net/bluetooth/l2cap_core.c | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+)
l2cap_sig_channel() walks every L2CAP signaling command packed in
one inbound ACL frame and dispatches each to l2cap_bredr_sig_cmd().
The L2CAP_ECHO_REQ handler unconditionally calls
l2cap_send_cmd(conn, ident, L2CAP_ECHO_RSP, cmd_len, data);
per request, with no per-PDU cap and no per-connection rate limit
anywhere in the path (HCI -> hci_conn -> l2cap_sig_channel ->
l2cap_bredr_sig_cmd).
A peer that packs N L2CAP_ECHO_REQ commands (4 bytes each) into a
single inbound signaling PDU forces the kernel to emit N separate
ACL frames carrying L2CAP_ECHO_RSP. Measured between two
unmodified upstream kernels over real radio (Intel AX210 attacker,
Intel BE200 target):
N=1 : 13 B in, 13 B out, 1 ACL frame back, 23 ms wall
N=4 : 25 B in, 52 B out, 4 ACL frames back, 12 ms wall
N=16 : 73 B in, 208 B out, 16 ACL frames back, 28 ms wall
N=48 : 201 B in, 624 B out, 48 ACL frames back, 67 ms wall
N=168 : 681 B in, 2184 B out, 168 ACL frames back, 220 ms wall
The 10x latency growth at N=168 vs N=1 (220ms vs 23ms) is the
target's signaling work-queue and radio TX queue saturating on the
burst. A sustained flood (50 inbound PDUs of 168 packed echoes
each) drives ~840 ECHO_RSPs/sec from the target until the kernel's
input ACL queue back-pressures the attacker; during the attack
window the target's BT radio is fully occupied answering echoes
and any other ongoing BT traffic (audio, HID, file transfer) is
starved. bluetoothd CPU stays at 0% throughout -- the entire path
is kernel-side.
L2CAP signaling channel CID 0x0001 is pre-auth, so no pairing is
required to reach the bug. Reproducible from any device with a
programmable Bluetooth radio within range of a target running an
unpatched kernel with CONFIG_BT_BREDR=y (effectively every
Linux distribution that ships Bluetooth).
Cap the number of echoes answered per inbound signaling PDU to
L2CAP_SIG_ECHO_BURST (4). Subsequent ECHO_REQs in the same PDU
are silently dropped. Legitimate clients (l2ping, BlueZ test
suite, specification-compliant L2CAP) send one ECHO_REQ per PDU
and are unaffected. Worst-case amplification ratio is now 4:1
per PDU, bounded.
Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Signed-off-by: Michael Bommarito <michael.bommarito@gmail.com>
Cc: stable@vger.kernel.org
Assisted-by: Claude:claude-opus-4-7
---
net/bluetooth/l2cap_core.c | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
index 77dec104a9c36..1c1179fddb8ba 100644
--- a/net/bluetooth/l2cap_core.c
+++ b/net/bluetooth/l2cap_core.c
@@ -5626,11 +5626,25 @@ static inline void l2cap_sig_send_rej(struct l2cap_conn *conn, u16 ident)
l2cap_send_cmd(conn, ident, L2CAP_COMMAND_REJ, sizeof(rej), &rej);
}
+/*
+ * Maximum L2CAP_ECHO_REQ commands handled in a single inbound
+ * signaling PDU. An inbound PDU may legitimately pack multiple
+ * commands, but a peer that packs many L2CAP_ECHO_REQs into one
+ * PDU triggers an N:1 outbound ECHO_RSP storm that saturates the
+ * link's signaling and ACL TX paths. Cap the number of echoes
+ * answered per inbound PDU to a small burst. Subsequent ECHO_REQs
+ * in the same PDU are silently dropped; the peer can pace itself
+ * with one ECHO_REQ per PDU (the l2ping shape and the only legit
+ * use that has ever been observed in the wild).
+ */
+#define L2CAP_SIG_ECHO_BURST 4
+
static inline void l2cap_sig_channel(struct l2cap_conn *conn,
struct sk_buff *skb)
{
struct hci_conn *hcon = conn->hcon;
struct l2cap_cmd_hdr *cmd;
+ unsigned int echo_count = 0;
int err;
l2cap_raw_recv(conn, skb);
@@ -5656,6 +5670,13 @@ static inline void l2cap_sig_channel(struct l2cap_conn *conn,
continue;
}
+ if (cmd->code == L2CAP_ECHO_REQ &&
+ ++echo_count > L2CAP_SIG_ECHO_BURST) {
+ /* Drop excess echoes packed into one PDU. */
+ skb_pull(skb, len);
+ continue;
+ }
+
err = l2cap_bredr_sig_cmd(conn, cmd, len, skb->data);
if (err) {
BT_ERR("Wrong link type (%d)", err);
--
2.53.0
Hi Michael,
Thanks for the patch. The ECHO_REQ cap addresses one amplification
vector, but the same N:1 amplification appears reachable via the
unknown-command path in the same loop.
When l2cap_bredr_sig_cmd() returns an error for an unrecognised command
code, the loop calls l2cap_sig_send_rej() unconditionally:
err = l2cap_bredr_sig_cmd(conn, cmd, len, skb->data);
if (err) {
BT_ERR("Wrong link type (%d)", err);
l2cap_sig_send_rej(conn, cmd->ident);
}
A peer that packs N commands with an unknown code (e.g. 0xFF, len = 0)
into a single signaling PDU would trigger N calls to
l2cap_sig_send_rej(), sending N COMMAND_REJ responses,
bypassing the new L2CAP_SIG_ECHO_BURST limit entirely. This produces
the same TX queue saturation the patch intends to prevent, and also
floods the kernel log with N BT_ERR() writes per PDU.
A response counter covering all outbound responses generated per PDU
would close both paths. Something like:
int resp_count = 0;
/* in the error path: */
if (err) {
if (++resp_count <= L2CAP_SIG_ECHO_BURST) {
BT_ERR("Wrong link type (%d)", err);
l2cap_sig_send_rej(conn, cmd->ident);
}
continue;
}
/* existing echo path: */
if (cmd->code == L2CAP_ECHO_REQ &&
++resp_count > L2CAP_SIG_ECHO_BURST) {
skb_pull(skb, len);
continue;
}
Using pr_warn_ratelimited() instead of BT_ERR() in the loop would
also reduce log amplification independent of the skb path.
Regards,
Muhammad Bilal
On Sun, May 17, 2026 at 9:00 PM Muhammad Bilal <meatuni001@gmail.com> wrote:
> A response counter covering all outbound responses generated per PDU
> would close both paths. Something like:
>
> int resp_count = 0;
>
> /* in the error path: */
> if (err) {
> if (++resp_count <= L2CAP_SIG_ECHO_BURST) {
> BT_ERR("Wrong link type (%d)", err);
> l2cap_sig_send_rej(conn, cmd->ident);
> }
> continue;
> }
>
> /* existing echo path: */
> if (cmd->code == L2CAP_ECHO_REQ &&
> ++resp_count > L2CAP_SIG_ECHO_BURST) {
> skb_pull(skb, len);
> continue;
> }
>
> Using pr_warn_ratelimited() instead of BT_ERR() in the loop would
> also reduce log amplification independent of the skb path.
Thanks, good catch on both! Luiz reminded me about
pr_warn_ratelimited before and I missed that again.
I'll give others an opportunity to weigh in and then send an updated v2.
Thanks,
Mike
Hi Michael,
On Sun, May 17, 2026 at 9:03 PM Michael Bommarito
<michael.bommarito@gmail.com> wrote:
>
> On Sun, May 17, 2026 at 9:00 PM Muhammad Bilal <meatuni001@gmail.com> wrote:
> > A response counter covering all outbound responses generated per PDU
> > would close both paths. Something like:
> >
> > int resp_count = 0;
> >
> > /* in the error path: */
> > if (err) {
> > if (++resp_count <= L2CAP_SIG_ECHO_BURST) {
> > BT_ERR("Wrong link type (%d)", err);
> > l2cap_sig_send_rej(conn, cmd->ident);
> > }
> > continue;
> > }
> >
> > /* existing echo path: */
> > if (cmd->code == L2CAP_ECHO_REQ &&
> > ++resp_count > L2CAP_SIG_ECHO_BURST) {
> > skb_pull(skb, len);
> > continue;
> > }
> >
> > Using pr_warn_ratelimited() instead of BT_ERR() in the loop would
> > also reduce log amplification independent of the skb path.
>
> Thanks, good catch on both! Luiz reminded me about
> pr_warn_ratelimited before and I missed that again.
>
> I'll give others an opportunity to weigh in and then send an updated v2.
Hmm, the spec does not have support limitation though, it just says:
Implementations shall be able to handle the reception of multiple
commands in an L2CAP packet sent over fixed channel CID 0x0001.
[] https://www.bluetooth.com/wp-content/uploads/Files/Specification/HTML/Core-62/out/en/host/logical-link-control-and-adaptation-protocol-specification.html#UUID-32a25a06-4aa4-c6c7-77c5-dcfe3682355d
Perhaps we should define our Signalling MTU:
Signaling MTU (MTUsig)
The maximum size of command information that the L2CAP layer entity is
capable of accepting. The MTUsig, refers to the signaling channel only
and corresponds to the maximum size of a C-frame, omitting the size of
the Basic L2CAP header. The MTUsig value of a peer is discovered when
a C-frame that is too large is rejected by the peer.
The minimum MTU seems to be 48, so we could probably use that as a
limit for the overall packet. If the response exceeds this limit, we
should reject it as recommended by the spec, but I think we need to
reject the whole packet rather than the cmd CID.
> Thanks,
> Mike
--
Luiz Augusto von Dentz
Hi Michael,
On Mon, May 18, 2026 at 5:27 PM Luiz Augusto von Dentz
<luiz.dentz@gmail.com> wrote:
>
> Hi Michael,
>
> On Sun, May 17, 2026 at 9:03 PM Michael Bommarito
> <michael.bommarito@gmail.com> wrote:
> >
> > On Sun, May 17, 2026 at 9:00 PM Muhammad Bilal <meatuni001@gmail.com> wrote:
> > > A response counter covering all outbound responses generated per PDU
> > > would close both paths. Something like:
> > >
> > > int resp_count = 0;
> > >
> > > /* in the error path: */
> > > if (err) {
> > > if (++resp_count <= L2CAP_SIG_ECHO_BURST) {
> > > BT_ERR("Wrong link type (%d)", err);
> > > l2cap_sig_send_rej(conn, cmd->ident);
> > > }
> > > continue;
> > > }
> > >
> > > /* existing echo path: */
> > > if (cmd->code == L2CAP_ECHO_REQ &&
> > > ++resp_count > L2CAP_SIG_ECHO_BURST) {
> > > skb_pull(skb, len);
> > > continue;
> > > }
> > >
> > > Using pr_warn_ratelimited() instead of BT_ERR() in the loop would
> > > also reduce log amplification independent of the skb path.
> >
> > Thanks, good catch on both! Luiz reminded me about
> > pr_warn_ratelimited before and I missed that again.
> >
> > I'll give others an opportunity to weigh in and then send an updated v2.
>
> Hmm, the spec does not have support limitation though, it just says:
>
> Implementations shall be able to handle the reception of multiple
> commands in an L2CAP packet sent over fixed channel CID 0x0001.
> [] https://www.bluetooth.com/wp-content/uploads/Files/Specification/HTML/Core-62/out/en/host/logical-link-control-and-adaptation-protocol-specification.html#UUID-32a25a06-4aa4-c6c7-77c5-dcfe3682355d
>
> Perhaps we should define our Signalling MTU:
>
> Signaling MTU (MTUsig)
>
> The maximum size of command information that the L2CAP layer entity is
> capable of accepting. The MTUsig, refers to the signaling channel only
> and corresponds to the maximum size of a C-frame, omitting the size of
> the Basic L2CAP header. The MTUsig value of a peer is discovered when
> a C-frame that is too large is rejected by the peer.
>
> The minimum MTU seems to be 48, so we could probably use that as a
> limit for the overall packet. If the response exceeds this limit, we
> should reject it as recommended by the spec, but I think we need to
> reject the whole packet rather than the cmd CID.
Just confirmed, the packet as whole needs to be rejected:
When multiple commands are included in an L2CAP packet and the packet
exceeds the signaling MTU (MTUsig) of the receiver, a single
L2CAP_COMMAND_REJECT_RSP packet shall be sent in response. The
identifier shall match the first request command in the L2CAP packet.
If only responses are recognized, the packet shall be silently
discarded.
[] https://www.bluetooth.com/wp-content/uploads/Files/Specification/HTML/Core-62/out/en/host/logical-link-control-and-adaptation-protocol-specification.html#UUID-692db0dd-8ed5-693f-d300-4fd7af07e340
So we need to reject with L2CAP_REJ_MTU_EXCEEDED.
> > Thanks,
> > Mike
>
>
>
> --
> Luiz Augusto von Dentz
--
Luiz Augusto von Dentz
On Mon, May 18, 2026 at 5:37 PM Luiz Augusto von Dentz <luiz.dentz@gmail.com> wrote: Two questions on this: > > Implementations shall be able to handle the reception of multiple > > commands in an L2CAP packet sent over fixed channel CID 0x0001. > > [] https://www.bluetooth.com/wp-content/uploads/Files/Specification/HTML/Core-62/out/en/host/logical-link-control-and-adaptation-protocol-specification.html#UUID-32a25a06-4aa4-c6c7-77c5-dcfe3682355d > > > > Perhaps we should define our Signalling MTU: > > > > Signaling MTU (MTUsig) > ... I went to add the constant in l2cap.h and noticed that our const names don't quite align with the spec names. Do you want me to call this MAX_MTU to be consistent with the const names or SIG_MTU to stick to the standard? > So we need to reject with L2CAP_REJ_MTU_EXCEEDED. It looks like this would be our first use of L2CAP_REJ_MTU_EXCEEDED anywhere, which seemed suspicious. Can you think of any other locations offhand where we might want to check for an MTU guard? Thanks, Mike
Hi Michael, On Mon, May 18, 2026 at 8:43 PM Michael Bommarito <michael.bommarito@gmail.com> wrote: > > On Mon, May 18, 2026 at 5:37 PM Luiz Augusto von Dentz > <luiz.dentz@gmail.com> wrote: > > Two questions on this: > > > > Implementations shall be able to handle the reception of multiple > > > commands in an L2CAP packet sent over fixed channel CID 0x0001. > > > [] https://www.bluetooth.com/wp-content/uploads/Files/Specification/HTML/Core-62/out/en/host/logical-link-control-and-adaptation-protocol-specification.html#UUID-32a25a06-4aa4-c6c7-77c5-dcfe3682355d > > > > > > Perhaps we should define our Signalling MTU: > > > > > > Signaling MTU (MTUsig) > > ... > > I went to add the constant in l2cap.h and noticed that our const names > don't quite align with the spec names. Do you want me to call this > MAX_MTU to be consistent with the const names or SIG_MTU to stick to > the standard? I guess L2CAP_SIG_MTU is fine. > > > So we need to reject with L2CAP_REJ_MTU_EXCEEDED. > > It looks like this would be our first use of L2CAP_REJ_MTU_EXCEEDED > anywhere, which seemed suspicious. Can you think of any other > locations offhand where we might want to check for an MTU guard? This is our first use. I don't think this error existed initially; it was probably added to handle cases where signalling channels need to indicate their MTU. > Thanks, > Mike -- Luiz Augusto von Dentz
© 2016 - 2026 Red Hat, Inc.