[PATCH v2] Bluetooth: L2CAP: Fix list corruption in ecred defer recvmsg path
Doruk Tan Ozturk <[email protected]> Sun, 2 Aug 2026 15:20:29 +0200
| Newsgroups | org.kernel.vger.linux-bluetooth,org.kernel.vger.linux-kernel,org.kernel.vger.stable |
|---|---|
| Message-ID | <[email protected]> |
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. Every other
walker of the list holds that lock: l2cap_chan_list() takes it around
__l2cap_chan_list(), and the signalling handlers reach the list from
l2cap_recv_frame(), which runs with it held. The deferred-accept path
from l2cap_sock_recvmsg() is the only one that does not, so a peer
disconnect landing during the walk leaves it on a poisoned entry:
list_del corruption, ffff88810420c480->next is LIST_POISON1
(dead000000000100)
WARNING: CPU: 1 PID: 88 at lib/list_debug.c:56
__list_del_entry_valid_or_report+0xd6/0x140
l2cap_chan_del+0x7c/0x7c0
__l2cap_ecred_conn_rsp_defer+0x333/0x340
l2cap_sock_recvmsg+0x338/0x340
sock_recvmsg+0xec/0xf0
__sys_recvfrom+0x14c/0x1f0
BUG: KASAN: wild-memory-access in
__l2cap_ecred_conn_rsp_defer+0x1c0/0x340
Read of size 8 at addr dead000000000100 by task race/95
__l2cap_ecred_conn_rsp_defer+0x1c0/0x340
l2cap_sock_recvmsg+0x338/0x340
sock_recvmsg+0xec/0xf0
__sys_recvfrom+0x14c/0x1f0
Oops: general protection fault, probably for non-canonical
address 0xdead000000000100
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(). 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.
Reproduced with hci_vhci on a KASAN + PROVE_LOCKING kernel: a peer sends
L2CAP_ECRED_CONN_REQ over LE, userspace accepts the deferred channels,
and an L2CAP_DISCONN_REQ for a sibling channel races the recvmsg() that
completes the accept. 7 of 10 unpatched boots reproduced it; 10 patched
boots gave neither a splat nor a lockdep report. Well-formed traffic is
unaffected: the response is built from the same channels with the same
contents, and the only case now skipped is a channel the RX worker has
already removed from conn->chan_l, for which no response is meaningful.
Found by 0sec (https://0sec.ai).
Fixes: 15f02b910562 ("Bluetooth: L2CAP: Add initial code for Enhanced Credit Based Mode")
Cc: [email protected]
Assisted-by: 0sec:multi-model
Signed-off-by: Doruk Tan Ozturk <[email protected]>
---
v2: Rewrite the commit message. v1 called this the recvmsg-path sibling
of 41c2713b204e; that commit fixes iterator invalidation in a path
that already runs under conn->lock, not a missing lock, so the
reference is dropped and the invariant is stated directly instead.
v1 also cited l2cap_sock_cleanup_listen() as precedent for taking
conn->lock, which is backwards: it deliberately avoids conn->lock
because it runs under the parent sk lock. Only l2cap_sock_shutdown()
is cited now. The splat is quoted from an actual run. Shorten the
subject to 80 columns and use the AGENT_NAME:MODEL_VERSION form for
Assisted-by. The only code change from v1 is four comment lines on
why chan needs no extra reference across the unlocked window.
Note for stable: this uses FLAG_DEL, added by b66774b48dd9
("Bluetooth: L2CAP: Fix UAF in channel timeout by holding conn ref",
v7.2-rc1). Trees without that commit need it first; the patch does
not build otherwise.
v1: https://lore.kernel.org/linux-bluetooth/[email protected]/
net/bluetooth/l2cap_sock.c | 37 +++++++++++++++++++++++++++++++++++--
1 file changed, 35 insertions(+), 2 deletions(-)
diff --git a/net/bluetooth/l2cap_sock.c b/net/bluetooth/l2cap_sock.c
index 735167f73f312..af35608791994 100644
--- a/net/bluetooth/l2cap_sock.c
+++ b/net/bluetooth/l2cap_sock.c
@@ -1227,9 +1227,42 @@ 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; chan needs no extra
+ * reference because the socket holds one until
+ * sk->sk_socket is cleared, which cannot happen while
+ * this call is in progress.
+ */
+ 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