syzbot
WARNING in l2cap_chan_le_send_credits
Status:
moderation: reported on 2026/11/13 00:18
Subsystems:
bluetooth
Labels:
prio:high
[Documentation on labels]
Reported-by: syzbot+6a690206ef86757de567@syzkaller.appspotmail.com
First crash: 1h49m, last: 1h49m
✨ AI-generated root cause summary:
In net/bluetooth/l2cap_core.c, l2cap_chan_le_send_credits() allocates an L2CAP transaction identifier via l2cap_get_ident() and unconditionally calls ida_free(&conn->tx_ida, ident) after transmission under the assumption that L2CAP_LE_CREDITS never expects a response. However, if the remote peer rejects the packet with L2CAP_COMMAND_REJ or sends a signaling response bearing that identifier, the asynchronous RX signaling handler l2cap_put_ident() sees the ID as currently allocated and frees it first. When l2cap_chan_le_send_credits() then executes ida_free() on the already-released identifier, ida_free() detects that the ID is not allocated and triggers the warning in lib/idr.c.
▶
▼
✨ Full AI root cause analysis
Root Cause Analysis
1. Functions, Structs, and Files Involved
net/bluetooth/l2cap_core.c:
l2cap_chan_le_send_credits(): Allocates and immediately releases the transaction identifier for outgoing credit packets.
l2cap_get_ident(): Allocates an ID from conn->tx_ida using ida_alloc_range().
l2cap_put_ident(): Frees an ID from conn->tx_ida when receiving responses or rejection packets.
struct l2cap_conn: Holds the connection state and struct ida tx_ida.
lib/idr.c:
ida_free(): Frees an allocated ID in an IDA instance and issues WARN(1, "ida_free called for id=%d which is not allocated.\n", id) if the ID's bit is not set.
2. Detailed Technical Mechanism
-
Transaction Identifier Management:
Commit 6c3ea155e5ee replaced the cyclic counter for L2CAP signaling transaction identifiers with an IDA structure (conn->tx_ida) to track in-flight requests and reuse IDs once freed by responses in l2cap_put_ident().
Commit 6e1930ece855 observed that L2CAP_LE_CREDITS is an unacknowledged indication/credit packet without an explicit response. To prevent exhausting the 1–255 ID range, l2cap_chan_le_send_credits() was changed to allocate an identifier and then immediately free it:
ident = l2cap_get_ident(conn);
l2cap_send_cmd(conn, ident, L2CAP_LE_CREDITS, sizeof(pkt), &pkt);
if (ident > 0)
ida_free(&conn->tx_ida, ident);
-
The Race Condition / Lifecycle Violation:
l2cap_chan_le_send_credits() is invoked along the socket read path (e.g., l2cap_sock_recvmsg() -> l2cap_publish_rx_avail() -> l2cap_chan_rx_avail()) holding only lock_sock(sk). Neither chan->lock nor conn->lock is held.
- When
l2cap_chan_le_send_credits() allocates ident (here, ident = 2) via l2cap_get_ident(conn), bit 2 is set in conn->tx_ida.
l2cap_send_cmd(conn, ident, L2CAP_LE_CREDITS, ...) builds and transmits the frame.
- Per the Bluetooth Core Specification, a remote peer or virtual HCI controller (such as syzkaller's
vhci) may respond to any unrecognized, malformed, or rejected signaling command with L2CAP_COMMAND_REJ, quoting the rejected command's identifier (cmd->ident = 2).
- When the RX signaling handler (
l2cap_le_sig_cmd() or l2cap_bredr_sig_cmd()) processes the incoming frame, it invokes:
l2cap_put_ident(conn, cmd->code, cmd->ident);
Inside l2cap_put_ident():
case L2CAP_COMMAND_REJ:
...
if (ida_find_first_range(&conn->tx_ida, id, id) >= 0)
ida_free(&conn->tx_ida, id);
- Because
ident = 2 is still marked allocated in conn->tx_ida during the transmission window, ida_find_first_range(&conn->tx_ida, 2, 2) succeeds and l2cap_put_ident() executes ida_free(&conn->tx_ida, 2).
- Once execution resumes in
l2cap_chan_le_send_credits(), it executes its own unconditional ida_free(&conn->tx_ida, 2).
ida_free() inspects conn->tx_ida, finds bit 2 already unset (0), and triggers the warning:
ida_free called for id=2 which is not allocated.
WARNING: lib/idr.c:594 at ida_free+0x24c/0x2d4 lib/idr.c:594
Sample crash report:
* Struck through repros no longer work on HEAD.