[PATCH] Bluetooth: L2CAP: Fix slab-use-after-free in l2cap_disconn_ind

From: Hojun Choi

Date: Mon Jun 29 2026 - 07:06:44 EST


l2cap_disconn_ind() runs from the hci_conn_timeout() worker without
hci_dev_lock, and reads conn->disc_reason via hcon->l2cap_data. It races
the teardown path: hci_conn_failed() frees the l2cap_conn via
hci_connect_cfm() -> l2cap_conn_del() and only afterwards drains the
worker via hci_conn_del(), so the worker can read disc_reason after the
l2cap_conn has been freed:

| BUG: KASAN: slab-use-after-free in l2cap_disconn_ind+0xd7/0xf0
| Read of size 1 at addr ffff88807ee53278 by task kworker/u9:1/4933
| l2cap_disconn_ind net/bluetooth/l2cap_core.c:7430
| hci_conn_timeout net/bluetooth/hci_conn.c:646

l2cap_conn_del() is always called under hci_dev_lock(), so hold it while
dereferencing hcon->l2cap_data and reading disc_reason. A blocking lock
cannot be used here: hci_conn_del() drains this worker with
disable_delayed_work_sync() while holding hci_dev_lock(), so the worker
would deadlock against the drain. Use a trylock; on contention return the
default HCI_ERROR_REMOTE_USER_TERM, the same reason the !conn path already
returns.

Fixes: 2950f21acb0f ("Bluetooth: Ask upper layers for HCI disconnect reason")
Reported-by: syzbot+9c40ad7c6ed7165e46e8@xxxxxxxxxxxxxxxxxxxxxxxxx
Closes: https://syzkaller.appspot.com/bug?extid=9c40ad7c6ed7165e46e8
Cc: <stable@xxxxxxxxxxxxxxx>
Signed-off-by: Hojun Choi <ghwns6743@xxxxxxxxx>
---
include/net/bluetooth/hci_core.h | 1 +
net/bluetooth/l2cap_core.c | 24 ++++++++++++++++++++----
2 files changed, 21 insertions(+), 4 deletions(-)

diff --git a/include/net/bluetooth/hci_core.h b/include/net/bluetooth/hci_core.h
index aa600fbf9a53..a5d82f9f3871 100644
--- a/include/net/bluetooth/hci_core.h
+++ b/include/net/bluetooth/hci_core.h
@@ -1733,6 +1733,7 @@ static inline struct hci_dev *hci_dev_hold(struct hci_dev *d)
}

#define hci_dev_lock(d) mutex_lock(&d->lock)
+#define hci_dev_trylock(d) mutex_trylock(&d->lock)
#define hci_dev_unlock(d) mutex_unlock(&d->lock)

#define to_hci_dev(d) container_of(d, struct hci_dev, dev)
diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
index 1fbd52165fb2..1666601ebd1f 100644
--- a/net/bluetooth/l2cap_core.c
+++ b/net/bluetooth/l2cap_core.c
@@ -7498,13 +7498,29 @@ static void l2cap_connect_cfm(struct hci_conn *hcon, u8 status)

int l2cap_disconn_ind(struct hci_conn *hcon)
{
- struct l2cap_conn *conn = hcon->l2cap_data;
+ struct hci_dev *hdev = hcon->hdev;
+ struct l2cap_conn *conn;
+ u8 reason = HCI_ERROR_REMOTE_USER_TERM;

BT_DBG("hcon %p", hcon);

- if (!conn)
- return HCI_ERROR_REMOTE_USER_TERM;
- return conn->disc_reason;
+ /* l2cap_conn_del() is always called under hci_dev_lock(), so hold
+ * it while dereferencing hcon->l2cap_data and reading disc_reason
+ * to serialize against the free. trylock because hci_conn_del()
+ * drains this worker with disable_delayed_work_sync() under
+ * hci_dev_lock(), so a blocking lock here would deadlock against
+ * that drain.
+ */
+ if (!hci_dev_trylock(hdev))
+ return reason;
+
+ conn = hcon->l2cap_data;
+ if (conn)
+ reason = conn->disc_reason;
+
+ hci_dev_unlock(hdev);
+
+ return reason;
}

static void l2cap_disconn_cfm(struct hci_conn *hcon, u8 reason)
--
2.54.0