[PATCH] Bluetooth: L2CAP: take chan_l lock in ecred defer recvmsg path to fix list corruption
From: Doruk Tan Ozturk
Date: Tue Jul 14 2026 - 08:52:29 EST
When a deferred L2CAP_MODE_EXT_FLOWCTL connection is accepted,
l2cap_sock_recvmsg() (BT_CONNECT2 + BT_SK_DEFER_SETUP branch) calls
__l2cap_ecred_conn_rsp_defer() while holding only lock_sock(sk). That
function walks conn->chan_l via __l2cap_chan_list_id() and, on the
authorization/refuse path, removes channels with l2cap_chan_del() ->
list_del(&chan->list) -- all without conn->lock.
conn->chan_l is serialised by conn->lock and is concurrently mutated by
the RX worker, which processes inbound signalling (e.g. an
L2CAP_DISCONN_REQ -> l2cap_chan_del()) under conn->lock. A lockless
traversal/removal racing the RX worker's list_del() corrupts the list:
list_del corruption, ...->next is LIST_POISON1 (dead000000000100)
WARNING lib/list_debug.c:56 __list_del_entry_valid_or_report
l2cap_chan_del+0xdb/0x1140
__l2cap_ecred_conn_rsp_defer+0x61d/0x700
l2cap_sock_recvmsg+0x758/0x8b0
Oops: general protection fault
KASAN: maybe wild-memory-access in range [dead000000000100-...]
__l2cap_ecred_conn_rsp_defer+0x3c6/0x700
l2cap_sock_recvmsg+0x758/0x8b0
This is the recvmsg-path sibling of commit 41c2713b204e ("Bluetooth:
L2CAP: Fix possible crash on l2cap_ecred_conn_rsp"), which addressed the
same conn->chan_l manipulation on the signalling (l2cap_ecred_conn_rsp)
side; the deferred-accept path was left unguarded.
Take conn->lock around __l2cap_ecred_conn_rsp_defer(). The established
lock order is conn->lock -> chan->lock -> sk_lock (the RX worker reaches
the socket via l2cap_chan_del() -> l2cap_sock_teardown_cb() ->
lock_sock_nested()), so the socket lock is dropped before conn->lock is
taken, mirroring l2cap_sock_shutdown() and l2cap_sock_cleanup_listen().
The conn is pinned with l2cap_conn_hold_unless_zero() across the
unlocked window. Only the EXT_FLOWCTL branch needs this; the LE and
BR/EDR defer paths respond for a single channel and do not walk
conn->chan_l.
Found by 0sec (https://0sec.ai).
Fixes: 15f02b910562 ("Bluetooth: L2CAP: Add initial code for Enhanced Credit Based Mode")
Cc: stable@xxxxxxxxxxxxxxx
Assisted-by: 0sec
Signed-off-by: Doruk Tan Ozturk <doruk@xxxxxxx>
---
net/bluetooth/l2cap_sock.c | 34 ++++++++++++++++++++++++++++++++--
1 file changed, 32 insertions(+), 2 deletions(-)
diff --git a/net/bluetooth/l2cap_sock.c b/net/bluetooth/l2cap_sock.c
index 735167f73f31..796b9a4eee24 100644
--- a/net/bluetooth/l2cap_sock.c
+++ b/net/bluetooth/l2cap_sock.c
@@ -1227,9 +1227,39 @@ static int l2cap_sock_recvmsg(struct socket *sock, struct msghdr *msg,
if (sk->sk_state == BT_CONNECT2 && test_bit(BT_SK_DEFER_SETUP,
&bt_sk(sk)->flags)) {
if (pi->chan->mode == L2CAP_MODE_EXT_FLOWCTL) {
+ struct l2cap_chan *chan = pi->chan;
+ struct l2cap_conn *conn;
+
sk->sk_state = BT_CONNECTED;
- pi->chan->state = BT_CONNECTED;
- __l2cap_ecred_conn_rsp_defer(pi->chan);
+ chan->state = BT_CONNECTED;
+
+ /* __l2cap_ecred_conn_rsp_defer() walks and mutates
+ * conn->chan_l (via __l2cap_chan_list_id() and
+ * l2cap_chan_del()), which is serialised by conn->lock
+ * and is concurrently modified by the RX worker. The
+ * established lock order is
+ * conn->lock -> chan->lock -> sk_lock, so the socket
+ * lock must be dropped before taking conn->lock to
+ * avoid inverting it (lockdep deadlock). Pin the conn
+ * across the unlocked window.
+ */
+ conn = l2cap_conn_hold_unless_zero(chan->conn);
+ release_sock(sk);
+ if (conn) {
+ mutex_lock(&conn->lock);
+ /* The RX worker may have torn the channel down
+ * (FLAG_DEL, removed from conn->chan_l) while the
+ * socket lock was dropped; skip the response in
+ * that case. conn->lock below serialises the
+ * chan_l walk against the RX worker's
+ * l2cap_chan_del().
+ */
+ if (!test_bit(FLAG_DEL, &chan->flags))
+ __l2cap_ecred_conn_rsp_defer(chan);
+ mutex_unlock(&conn->lock);
+ l2cap_conn_put(conn);
+ }
+ lock_sock(sk);
} else if (bdaddr_type_is_le(pi->chan->src_type)) {
sk->sk_state = BT_CONNECTED;
pi->chan->state = BT_CONNECTED;
--
2.43.0