Linux bluetooth development
 help / color / mirror / Atom feed
* [PATCH] Bluetooth: L2CAP: Fix UAF in l2cap_chan_timeout
@ 2026-06-03 12:27 Marco Elver
  2026-06-03 13:15 ` Marco Elver
  2026-06-03 17:37 ` bluez.test.bot
  0 siblings, 2 replies; 8+ messages in thread
From: Marco Elver @ 2026-06-03 12:27 UTC (permalink / raw)
  To: elver
  Cc: Marcel Holtmann, Luiz Augusto von Dentz, linux-bluetooth,
	linux-kernel, kasan-dev, stable, Siwei Zhang,
	Luiz Augusto von Dentz

l2cap_chan_timeout() accesses chan->conn without holding a reference to
the connection object. If l2cap_conn_del() races and tears down the
connection while the timer is waiting for locks, it can result in a
use-after-free when the timer wakes up and attempts to acquire
conn->lock:

| BUG: KASAN: slab-use-after-free in instrument_atomic_read_write include/linux/instrumented.h:112 [inline]
| BUG: KASAN: slab-use-after-free in atomic_long_try_cmpxchg_acquire include/linux/atomic/atomic-instrumented.h:4456 [inline]
| BUG: KASAN: slab-use-after-free in __mutex_trylock_fast kernel/locking/mutex.c:161 [inline]
| BUG: KASAN: slab-use-after-free in mutex_lock+0x4f/0xa0 kernel/locking/mutex.c:318
| Write of size 8 at addr ffff8881298d9550 by task kworker/2:1/83
|
| CPU: 2 UID: 0 PID: 83 Comm: kworker/2:1 Not tainted 7.1.0-rc6-next-20260601-dirty #6 PREEMPT(full)
| Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.17.0-debian-1.17.0-1 04/01/2014
| Workqueue: events l2cap_chan_timeout
| Call Trace:
|  <TASK>
|  instrument_atomic_read_write include/linux/instrumented.h:112 [inline]
|  atomic_long_try_cmpxchg_acquire include/linux/atomic/atomic-instrumented.h:4456 [inline]
|  __mutex_trylock_fast kernel/locking/mutex.c:161 [inline]
|  mutex_lock+0x4f/0xa0 kernel/locking/mutex.c:318
|  l2cap_chan_timeout+0x5d/0x1b0 net/bluetooth/l2cap_core.c:422
|  process_one_work kernel/workqueue.c:3326 [inline]
|  process_scheduled_works+0x7c8/0xfb0 kernel/workqueue.c:3409
|  worker_thread+0x8a9/0xcf0 kernel/workqueue.c:3490
|  kthread+0x346/0x430 kernel/kthread.c:436
|  ret_from_fork+0x1a3/0x470 arch/x86/kernel/process.c:158
|  ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
|  </TASK>
|
| Allocated by task 320:
|  l2cap_conn_add+0xa7/0x820 net/bluetooth/l2cap_core.c:7075
|  l2cap_connect_cfm+0xdb/0xd70 net/bluetooth/l2cap_core.c:7452
|  hci_connect_cfm include/net/bluetooth/hci_core.h:2139 [inline]
|  hci_remote_features_evt+0x52f/0x9f0 net/bluetooth/hci_event.c:3760
|  hci_event_func net/bluetooth/hci_event.c:7796 [inline]
|  hci_event_packet+0x561/0xa70 net/bluetooth/hci_event.c:7847
|  hci_rx_work+0x370/0x890 net/bluetooth/hci_core.c:4040
|  process_one_work kernel/workqueue.c:3326 [inline]
|  process_scheduled_works+0x7c8/0xfb0 kernel/workqueue.c:3409
|  worker_thread+0x8a9/0xcf0 kernel/workqueue.c:3490
|  kthread+0x346/0x430 kernel/kthread.c:436
|  ret_from_fork+0x1a3/0x470 arch/x86/kernel/process.c:158
|  ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
|
| Freed by task 322:
|  hci_disconn_cfm include/net/bluetooth/hci_core.h:2154 [inline]
|  hci_conn_hash_flush+0x101/0x1f0 net/bluetooth/hci_conn.c:2736
|  hci_dev_close_sync+0x889/0xde0 net/bluetooth/hci_sync.c:5405
|  hci_dev_do_close net/bluetooth/hci_core.c:502 [inline]
|  hci_unregister_dev+0x1f7/0x370 net/bluetooth/hci_core.c:2679
|  vhci_release+0x12a/0x180 drivers/bluetooth/hci_vhci.c:690
|  __fput+0x369/0x890 fs/file_table.c:510
|  task_work_run+0x160/0x1d0 kernel/task_work.c:233
|  get_signal+0xf5b/0x1120 kernel/signal.c:2810
|  arch_do_signal_or_restart+0x4d/0x600 arch/x86/kernel/signal.c:337
|  __exit_to_user_mode_loop kernel/entry/common.c:64 [inline]
|  exit_to_user_mode_loop+0x85/0x510 kernel/entry/common.c:98
|  __exit_to_user_mode_prepare include/linux/irq-entry-common.h:207 [inline]
|  syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:230 [inline]
|  syscall_exit_to_user_mode include/linux/entry-common.h:318 [inline]
|  do_syscall_64+0x263/0x3d0 arch/x86/entry/syscall_64.c:100
|  entry_SYSCALL_64_after_hwframe+0x77/0x7f
|
| Last potentially related work creation:
|  hci_connect_cfm include/net/bluetooth/hci_core.h:2139 [inline]
|  hci_remote_features_evt+0x52f/0x9f0 net/bluetooth/hci_event.c:3760
|  hci_event_func net/bluetooth/hci_event.c:7796 [inline]
|  hci_event_packet+0x561/0xa70 net/bluetooth/hci_event.c:7847
|  hci_rx_work+0x370/0x890 net/bluetooth/hci_core.c:4040
|  process_one_work kernel/workqueue.c:3326 [inline]
|  process_scheduled_works+0x7c8/0xfb0 kernel/workqueue.c:3409
|  worker_thread+0x8a9/0xcf0 kernel/workqueue.c:3490
|  kthread+0x346/0x430 kernel/kthread.c:436
|  ret_from_fork+0x1a3/0x470 arch/x86/kernel/process.c:158
|  ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
|
| The buggy address belongs to the object at ffff8881298d9400
|  which belongs to the cache kmalloc-512 of size 512
| The buggy address is located 336 bytes inside of
|  freed 512-byte region [ffff8881298d9400, ffff8881298d9600)

Fix it by holding a reference to the connection when the channel timer
is scheduled, and releasing it when the timer is either canceled or
executes to completion.

Since l2cap_chan_del() nullifies chan->conn to disassociate the channel
during teardown, the timer handler might read NULL from chan->conn even
if it held a reference. To address this, introduce a `timer_conn` field
to `struct l2cap_chan` to store the connection pointer associated with
the active timer. The timer handler uses this field to acquire locks and
release the connection reference, and skips channel closing operations
if chan->conn has already been nullified by teardown.

Fixes: 75780ca4c6a8 ("Bluetooth: L2CAP: use chan timer to close channels in cleanup_listen()")
Cc: <stable@vger.kernel.org>
Cc: Siwei Zhang <oss@fourdim.xyz>
Cc: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
Assisted-by: Gemini:gemini-3.1-pro-preview
Reported-by: https://sashiko.dev/#/patchset/20260521021249.3258069-1-oss%40fourdim.xyz
Signed-off-by: Marco Elver <elver@google.com>
---
 include/net/bluetooth/l2cap.h | 18 ++++++++++++++++--
 net/bluetooth/l2cap_core.c    | 26 +++++++++++++++-----------
 2 files changed, 31 insertions(+), 13 deletions(-)

diff --git a/include/net/bluetooth/l2cap.h b/include/net/bluetooth/l2cap.h
index e0a1f2293679..83719777512e 100644
--- a/include/net/bluetooth/l2cap.h
+++ b/include/net/bluetooth/l2cap.h
@@ -514,6 +514,7 @@ struct l2cap_seq_list {
 
 struct l2cap_chan {
 	struct l2cap_conn	*conn;
+	struct l2cap_conn	*timer_conn; /* for chan_timer */
 	struct kref	kref;
 	atomic_t	nesting;
 
@@ -835,6 +836,9 @@ static inline void l2cap_chan_unlock(struct l2cap_chan *chan)
 	mutex_unlock(&chan->lock);
 }
 
+struct l2cap_conn *l2cap_conn_get(struct l2cap_conn *conn);
+void l2cap_conn_put(struct l2cap_conn *conn);
+
 static inline void l2cap_set_timer(struct l2cap_chan *chan,
 				   struct delayed_work *work, long timeout)
 {
@@ -843,8 +847,13 @@ static inline void l2cap_set_timer(struct l2cap_chan *chan,
 
 	/* If delayed work cancelled do not hold(chan)
 	   since it is already done with previous set_timer */
-	if (!cancel_delayed_work(work))
+	if (!cancel_delayed_work(work)) {
 		l2cap_chan_hold(chan);
+		if (work == &chan->chan_timer && chan->conn) {
+			l2cap_conn_get(chan->conn);
+			chan->timer_conn = chan->conn;
+		}
+	}
 
 	schedule_delayed_work(work, timeout);
 }
@@ -857,8 +866,13 @@ static inline bool l2cap_clear_timer(struct l2cap_chan *chan,
 	/* put(chan) if delayed work cancelled otherwise it
 	   is done in delayed work function */
 	ret = cancel_delayed_work(work);
-	if (ret)
+	if (ret) {
+		if (work == &chan->chan_timer && chan->timer_conn) {
+			l2cap_conn_put(chan->timer_conn);
+			chan->timer_conn = NULL;
+		}
 		l2cap_chan_put(chan);
+	}
 
 	return ret;
 }
diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
index c4ccfbda9d78..491b03bf6903 100644
--- a/net/bluetooth/l2cap_core.c
+++ b/net/bluetooth/l2cap_core.c
@@ -406,7 +406,7 @@ static void l2cap_chan_timeout(struct work_struct *work)
 {
 	struct l2cap_chan *chan = container_of(work, struct l2cap_chan,
 					       chan_timer.work);
-	struct l2cap_conn *conn = chan->conn;
+	struct l2cap_conn *conn = chan->timer_conn;
 	int reason;
 
 	BT_DBG("chan %p state %s", chan, state_to_string(chan->state));
@@ -421,23 +421,27 @@ static void l2cap_chan_timeout(struct work_struct *work)
 	 * this work. No need to call l2cap_chan_hold(chan) here again.
 	 */
 	l2cap_chan_lock(chan);
+	chan->timer_conn = NULL;
+
+	if (chan->conn) {
+		if (chan->state == BT_CONNECTED || chan->state == BT_CONFIG)
+			reason = ECONNREFUSED;
+		else if (chan->state == BT_CONNECT &&
+			 chan->sec_level != BT_SECURITY_SDP)
+			reason = ECONNREFUSED;
+		else
+			reason = ETIMEDOUT;
 
-	if (chan->state == BT_CONNECTED || chan->state == BT_CONFIG)
-		reason = ECONNREFUSED;
-	else if (chan->state == BT_CONNECT &&
-		 chan->sec_level != BT_SECURITY_SDP)
-		reason = ECONNREFUSED;
-	else
-		reason = ETIMEDOUT;
-
-	l2cap_chan_close(chan, reason);
+		l2cap_chan_close(chan, reason);
 
-	chan->ops->close(chan);
+		chan->ops->close(chan);
+	}
 
 	l2cap_chan_unlock(chan);
 	l2cap_chan_put(chan);
 
 	mutex_unlock(&conn->lock);
+	l2cap_conn_put(conn);
 }
 
 struct l2cap_chan *l2cap_chan_create(void)
-- 
2.54.0.1013.g208068f2d8-goog


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* Re: [PATCH] Bluetooth: L2CAP: Fix UAF in l2cap_chan_timeout
  2026-06-03 12:27 [PATCH] Bluetooth: L2CAP: Fix UAF in l2cap_chan_timeout Marco Elver
@ 2026-06-03 13:15 ` Marco Elver
  2026-06-03 17:31   ` Luiz Augusto von Dentz
  2026-06-03 17:37 ` bluez.test.bot
  1 sibling, 1 reply; 8+ messages in thread
From: Marco Elver @ 2026-06-03 13:15 UTC (permalink / raw)
  To: elver
  Cc: Marcel Holtmann, Luiz Augusto von Dentz, linux-bluetooth,
	linux-kernel, kasan-dev, stable, Siwei Zhang,
	Luiz Augusto von Dentz

On Wed, 3 Jun 2026 at 14:31, Marco Elver <elver@google.com> wrote:
>
> l2cap_chan_timeout() accesses chan->conn without holding a reference to
> the connection object. If l2cap_conn_del() races and tears down the
> connection while the timer is waiting for locks, it can result in a
> use-after-free when the timer wakes up and attempts to acquire
> conn->lock:
>
> | BUG: KASAN: slab-use-after-free in instrument_atomic_read_write include/linux/instrumented.h:112 [inline]
> | BUG: KASAN: slab-use-after-free in atomic_long_try_cmpxchg_acquire include/linux/atomic/atomic-instrumented.h:4456 [inline]
> | BUG: KASAN: slab-use-after-free in __mutex_trylock_fast kernel/locking/mutex.c:161 [inline]
> | BUG: KASAN: slab-use-after-free in mutex_lock+0x4f/0xa0 kernel/locking/mutex.c:318
> | Write of size 8 at addr ffff8881298d9550 by task kworker/2:1/83
> |
> | CPU: 2 UID: 0 PID: 83 Comm: kworker/2:1 Not tainted 7.1.0-rc6-next-20260601-dirty #6 PREEMPT(full)
> | Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.17.0-debian-1.17.0-1 04/01/2014
> | Workqueue: events l2cap_chan_timeout
> | Call Trace:
> |  <TASK>
> |  instrument_atomic_read_write include/linux/instrumented.h:112 [inline]
> |  atomic_long_try_cmpxchg_acquire include/linux/atomic/atomic-instrumented.h:4456 [inline]
> |  __mutex_trylock_fast kernel/locking/mutex.c:161 [inline]
> |  mutex_lock+0x4f/0xa0 kernel/locking/mutex.c:318
> |  l2cap_chan_timeout+0x5d/0x1b0 net/bluetooth/l2cap_core.c:422
> |  process_one_work kernel/workqueue.c:3326 [inline]
> |  process_scheduled_works+0x7c8/0xfb0 kernel/workqueue.c:3409
> |  worker_thread+0x8a9/0xcf0 kernel/workqueue.c:3490
> |  kthread+0x346/0x430 kernel/kthread.c:436
> |  ret_from_fork+0x1a3/0x470 arch/x86/kernel/process.c:158
> |  ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
> |  </TASK>
> |
> | Allocated by task 320:
> |  l2cap_conn_add+0xa7/0x820 net/bluetooth/l2cap_core.c:7075
> |  l2cap_connect_cfm+0xdb/0xd70 net/bluetooth/l2cap_core.c:7452
> |  hci_connect_cfm include/net/bluetooth/hci_core.h:2139 [inline]
> |  hci_remote_features_evt+0x52f/0x9f0 net/bluetooth/hci_event.c:3760
> |  hci_event_func net/bluetooth/hci_event.c:7796 [inline]
> |  hci_event_packet+0x561/0xa70 net/bluetooth/hci_event.c:7847
> |  hci_rx_work+0x370/0x890 net/bluetooth/hci_core.c:4040
> |  process_one_work kernel/workqueue.c:3326 [inline]
> |  process_scheduled_works+0x7c8/0xfb0 kernel/workqueue.c:3409
> |  worker_thread+0x8a9/0xcf0 kernel/workqueue.c:3490
> |  kthread+0x346/0x430 kernel/kthread.c:436
> |  ret_from_fork+0x1a3/0x470 arch/x86/kernel/process.c:158
> |  ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
> |
> | Freed by task 322:
> |  hci_disconn_cfm include/net/bluetooth/hci_core.h:2154 [inline]
> |  hci_conn_hash_flush+0x101/0x1f0 net/bluetooth/hci_conn.c:2736
> |  hci_dev_close_sync+0x889/0xde0 net/bluetooth/hci_sync.c:5405
> |  hci_dev_do_close net/bluetooth/hci_core.c:502 [inline]
> |  hci_unregister_dev+0x1f7/0x370 net/bluetooth/hci_core.c:2679
> |  vhci_release+0x12a/0x180 drivers/bluetooth/hci_vhci.c:690
> |  __fput+0x369/0x890 fs/file_table.c:510
> |  task_work_run+0x160/0x1d0 kernel/task_work.c:233
> |  get_signal+0xf5b/0x1120 kernel/signal.c:2810
> |  arch_do_signal_or_restart+0x4d/0x600 arch/x86/kernel/signal.c:337
> |  __exit_to_user_mode_loop kernel/entry/common.c:64 [inline]
> |  exit_to_user_mode_loop+0x85/0x510 kernel/entry/common.c:98
> |  __exit_to_user_mode_prepare include/linux/irq-entry-common.h:207 [inline]
> |  syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:230 [inline]
> |  syscall_exit_to_user_mode include/linux/entry-common.h:318 [inline]
> |  do_syscall_64+0x263/0x3d0 arch/x86/entry/syscall_64.c:100
> |  entry_SYSCALL_64_after_hwframe+0x77/0x7f
> |
> | Last potentially related work creation:
> |  hci_connect_cfm include/net/bluetooth/hci_core.h:2139 [inline]
> |  hci_remote_features_evt+0x52f/0x9f0 net/bluetooth/hci_event.c:3760
> |  hci_event_func net/bluetooth/hci_event.c:7796 [inline]
> |  hci_event_packet+0x561/0xa70 net/bluetooth/hci_event.c:7847
> |  hci_rx_work+0x370/0x890 net/bluetooth/hci_core.c:4040
> |  process_one_work kernel/workqueue.c:3326 [inline]
> |  process_scheduled_works+0x7c8/0xfb0 kernel/workqueue.c:3409
> |  worker_thread+0x8a9/0xcf0 kernel/workqueue.c:3490
> |  kthread+0x346/0x430 kernel/kthread.c:436
> |  ret_from_fork+0x1a3/0x470 arch/x86/kernel/process.c:158
> |  ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
> |
> | The buggy address belongs to the object at ffff8881298d9400
> |  which belongs to the cache kmalloc-512 of size 512
> | The buggy address is located 336 bytes inside of
> |  freed 512-byte region [ffff8881298d9400, ffff8881298d9600)
>
> Fix it by holding a reference to the connection when the channel timer
> is scheduled, and releasing it when the timer is either canceled or
> executes to completion.
>
> Since l2cap_chan_del() nullifies chan->conn to disassociate the channel
> during teardown, the timer handler might read NULL from chan->conn even
> if it held a reference. To address this, introduce a `timer_conn` field
> to `struct l2cap_chan` to store the connection pointer associated with
> the active timer. The timer handler uses this field to acquire locks and
> release the connection reference, and skips channel closing operations
> if chan->conn has already been nullified by teardown.
>
> Fixes: 75780ca4c6a8 ("Bluetooth: L2CAP: use chan timer to close channels in cleanup_listen()")
> Cc: <stable@vger.kernel.org>
> Cc: Siwei Zhang <oss@fourdim.xyz>
> Cc: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
> Assisted-by: Gemini:gemini-3.1-pro-preview
> Reported-by: https://sashiko.dev/#/patchset/20260521021249.3258069-1-oss%40fourdim.xyz
> Signed-off-by: Marco Elver <elver@google.com>

Sigh, Sashiko points out more problems here:
https://sashiko.dev/#/patchset/20260603123111.2334409-1-elver%40google.com

> Can this lockless read of chan->timer_conn cause a use-after-free or double
> free if another thread re-arms the timer concurrently?

I haven't analyzed this further yet, so consider this patch a
bug-report-only. If anyone finds a better fix sooner, please go ahead.

> ---
>  include/net/bluetooth/l2cap.h | 18 ++++++++++++++++--
>  net/bluetooth/l2cap_core.c    | 26 +++++++++++++++-----------
>  2 files changed, 31 insertions(+), 13 deletions(-)
>
> diff --git a/include/net/bluetooth/l2cap.h b/include/net/bluetooth/l2cap.h
> index e0a1f2293679..83719777512e 100644
> --- a/include/net/bluetooth/l2cap.h
> +++ b/include/net/bluetooth/l2cap.h
> @@ -514,6 +514,7 @@ struct l2cap_seq_list {
>
>  struct l2cap_chan {
>         struct l2cap_conn       *conn;
> +       struct l2cap_conn       *timer_conn; /* for chan_timer */
>         struct kref     kref;
>         atomic_t        nesting;
>
> @@ -835,6 +836,9 @@ static inline void l2cap_chan_unlock(struct l2cap_chan *chan)
>         mutex_unlock(&chan->lock);
>  }
>
> +struct l2cap_conn *l2cap_conn_get(struct l2cap_conn *conn);
> +void l2cap_conn_put(struct l2cap_conn *conn);
> +
>  static inline void l2cap_set_timer(struct l2cap_chan *chan,
>                                    struct delayed_work *work, long timeout)
>  {
> @@ -843,8 +847,13 @@ static inline void l2cap_set_timer(struct l2cap_chan *chan,
>
>         /* If delayed work cancelled do not hold(chan)
>            since it is already done with previous set_timer */
> -       if (!cancel_delayed_work(work))
> +       if (!cancel_delayed_work(work)) {
>                 l2cap_chan_hold(chan);
> +               if (work == &chan->chan_timer && chan->conn) {
> +                       l2cap_conn_get(chan->conn);
> +                       chan->timer_conn = chan->conn;
> +               }
> +       }
>
>         schedule_delayed_work(work, timeout);
>  }
> @@ -857,8 +866,13 @@ static inline bool l2cap_clear_timer(struct l2cap_chan *chan,
>         /* put(chan) if delayed work cancelled otherwise it
>            is done in delayed work function */
>         ret = cancel_delayed_work(work);
> -       if (ret)
> +       if (ret) {
> +               if (work == &chan->chan_timer && chan->timer_conn) {
> +                       l2cap_conn_put(chan->timer_conn);
> +                       chan->timer_conn = NULL;
> +               }
>                 l2cap_chan_put(chan);
> +       }
>
>         return ret;
>  }
> diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
> index c4ccfbda9d78..491b03bf6903 100644
> --- a/net/bluetooth/l2cap_core.c
> +++ b/net/bluetooth/l2cap_core.c
> @@ -406,7 +406,7 @@ static void l2cap_chan_timeout(struct work_struct *work)
>  {
>         struct l2cap_chan *chan = container_of(work, struct l2cap_chan,
>                                                chan_timer.work);
> -       struct l2cap_conn *conn = chan->conn;
> +       struct l2cap_conn *conn = chan->timer_conn;
>         int reason;
>
>         BT_DBG("chan %p state %s", chan, state_to_string(chan->state));
> @@ -421,23 +421,27 @@ static void l2cap_chan_timeout(struct work_struct *work)
>          * this work. No need to call l2cap_chan_hold(chan) here again.
>          */
>         l2cap_chan_lock(chan);
> +       chan->timer_conn = NULL;
> +
> +       if (chan->conn) {
> +               if (chan->state == BT_CONNECTED || chan->state == BT_CONFIG)
> +                       reason = ECONNREFUSED;
> +               else if (chan->state == BT_CONNECT &&
> +                        chan->sec_level != BT_SECURITY_SDP)
> +                       reason = ECONNREFUSED;
> +               else
> +                       reason = ETIMEDOUT;
>
> -       if (chan->state == BT_CONNECTED || chan->state == BT_CONFIG)
> -               reason = ECONNREFUSED;
> -       else if (chan->state == BT_CONNECT &&
> -                chan->sec_level != BT_SECURITY_SDP)
> -               reason = ECONNREFUSED;
> -       else
> -               reason = ETIMEDOUT;
> -
> -       l2cap_chan_close(chan, reason);
> +               l2cap_chan_close(chan, reason);
>
> -       chan->ops->close(chan);
> +               chan->ops->close(chan);
> +       }
>
>         l2cap_chan_unlock(chan);
>         l2cap_chan_put(chan);
>
>         mutex_unlock(&conn->lock);
> +       l2cap_conn_put(conn);
>  }
>
>  struct l2cap_chan *l2cap_chan_create(void)
> --
> 2.54.0.1013.g208068f2d8-goog
>

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH] Bluetooth: L2CAP: Fix UAF in l2cap_chan_timeout
  2026-06-03 13:15 ` Marco Elver
@ 2026-06-03 17:31   ` Luiz Augusto von Dentz
  2026-06-04 12:45     ` Marco Elver
  0 siblings, 1 reply; 8+ messages in thread
From: Luiz Augusto von Dentz @ 2026-06-03 17:31 UTC (permalink / raw)
  To: Marco Elver
  Cc: Marcel Holtmann, linux-bluetooth, linux-kernel, kasan-dev, stable,
	Siwei Zhang, Luiz Augusto von Dentz

Hi Marco,

On Wed, Jun 3, 2026 at 9:16 AM Marco Elver <elver@google.com> wrote:
>
> On Wed, 3 Jun 2026 at 14:31, Marco Elver <elver@google.com> wrote:
> >
> > l2cap_chan_timeout() accesses chan->conn without holding a reference to
> > the connection object. If l2cap_conn_del() races and tears down the
> > connection while the timer is waiting for locks, it can result in a
> > use-after-free when the timer wakes up and attempts to acquire
> > conn->lock:
> >
> > | BUG: KASAN: slab-use-after-free in instrument_atomic_read_write include/linux/instrumented.h:112 [inline]
> > | BUG: KASAN: slab-use-after-free in atomic_long_try_cmpxchg_acquire include/linux/atomic/atomic-instrumented.h:4456 [inline]
> > | BUG: KASAN: slab-use-after-free in __mutex_trylock_fast kernel/locking/mutex.c:161 [inline]
> > | BUG: KASAN: slab-use-after-free in mutex_lock+0x4f/0xa0 kernel/locking/mutex.c:318
> > | Write of size 8 at addr ffff8881298d9550 by task kworker/2:1/83
> > |
> > | CPU: 2 UID: 0 PID: 83 Comm: kworker/2:1 Not tainted 7.1.0-rc6-next-20260601-dirty #6 PREEMPT(full)
> > | Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.17.0-debian-1.17.0-1 04/01/2014
> > | Workqueue: events l2cap_chan_timeout
> > | Call Trace:
> > |  <TASK>
> > |  instrument_atomic_read_write include/linux/instrumented.h:112 [inline]
> > |  atomic_long_try_cmpxchg_acquire include/linux/atomic/atomic-instrumented.h:4456 [inline]
> > |  __mutex_trylock_fast kernel/locking/mutex.c:161 [inline]
> > |  mutex_lock+0x4f/0xa0 kernel/locking/mutex.c:318
> > |  l2cap_chan_timeout+0x5d/0x1b0 net/bluetooth/l2cap_core.c:422
> > |  process_one_work kernel/workqueue.c:3326 [inline]
> > |  process_scheduled_works+0x7c8/0xfb0 kernel/workqueue.c:3409
> > |  worker_thread+0x8a9/0xcf0 kernel/workqueue.c:3490
> > |  kthread+0x346/0x430 kernel/kthread.c:436
> > |  ret_from_fork+0x1a3/0x470 arch/x86/kernel/process.c:158
> > |  ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
> > |  </TASK>
> > |
> > | Allocated by task 320:
> > |  l2cap_conn_add+0xa7/0x820 net/bluetooth/l2cap_core.c:7075
> > |  l2cap_connect_cfm+0xdb/0xd70 net/bluetooth/l2cap_core.c:7452
> > |  hci_connect_cfm include/net/bluetooth/hci_core.h:2139 [inline]
> > |  hci_remote_features_evt+0x52f/0x9f0 net/bluetooth/hci_event.c:3760
> > |  hci_event_func net/bluetooth/hci_event.c:7796 [inline]
> > |  hci_event_packet+0x561/0xa70 net/bluetooth/hci_event.c:7847
> > |  hci_rx_work+0x370/0x890 net/bluetooth/hci_core.c:4040
> > |  process_one_work kernel/workqueue.c:3326 [inline]
> > |  process_scheduled_works+0x7c8/0xfb0 kernel/workqueue.c:3409
> > |  worker_thread+0x8a9/0xcf0 kernel/workqueue.c:3490
> > |  kthread+0x346/0x430 kernel/kthread.c:436
> > |  ret_from_fork+0x1a3/0x470 arch/x86/kernel/process.c:158
> > |  ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
> > |
> > | Freed by task 322:
> > |  hci_disconn_cfm include/net/bluetooth/hci_core.h:2154 [inline]
> > |  hci_conn_hash_flush+0x101/0x1f0 net/bluetooth/hci_conn.c:2736
> > |  hci_dev_close_sync+0x889/0xde0 net/bluetooth/hci_sync.c:5405
> > |  hci_dev_do_close net/bluetooth/hci_core.c:502 [inline]
> > |  hci_unregister_dev+0x1f7/0x370 net/bluetooth/hci_core.c:2679
> > |  vhci_release+0x12a/0x180 drivers/bluetooth/hci_vhci.c:690
> > |  __fput+0x369/0x890 fs/file_table.c:510
> > |  task_work_run+0x160/0x1d0 kernel/task_work.c:233
> > |  get_signal+0xf5b/0x1120 kernel/signal.c:2810
> > |  arch_do_signal_or_restart+0x4d/0x600 arch/x86/kernel/signal.c:337
> > |  __exit_to_user_mode_loop kernel/entry/common.c:64 [inline]
> > |  exit_to_user_mode_loop+0x85/0x510 kernel/entry/common.c:98
> > |  __exit_to_user_mode_prepare include/linux/irq-entry-common.h:207 [inline]
> > |  syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:230 [inline]
> > |  syscall_exit_to_user_mode include/linux/entry-common.h:318 [inline]
> > |  do_syscall_64+0x263/0x3d0 arch/x86/entry/syscall_64.c:100
> > |  entry_SYSCALL_64_after_hwframe+0x77/0x7f
> > |
> > | Last potentially related work creation:
> > |  hci_connect_cfm include/net/bluetooth/hci_core.h:2139 [inline]
> > |  hci_remote_features_evt+0x52f/0x9f0 net/bluetooth/hci_event.c:3760
> > |  hci_event_func net/bluetooth/hci_event.c:7796 [inline]
> > |  hci_event_packet+0x561/0xa70 net/bluetooth/hci_event.c:7847
> > |  hci_rx_work+0x370/0x890 net/bluetooth/hci_core.c:4040
> > |  process_one_work kernel/workqueue.c:3326 [inline]
> > |  process_scheduled_works+0x7c8/0xfb0 kernel/workqueue.c:3409
> > |  worker_thread+0x8a9/0xcf0 kernel/workqueue.c:3490
> > |  kthread+0x346/0x430 kernel/kthread.c:436
> > |  ret_from_fork+0x1a3/0x470 arch/x86/kernel/process.c:158
> > |  ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
> > |
> > | The buggy address belongs to the object at ffff8881298d9400
> > |  which belongs to the cache kmalloc-512 of size 512
> > | The buggy address is located 336 bytes inside of
> > |  freed 512-byte region [ffff8881298d9400, ffff8881298d9600)
> >
> > Fix it by holding a reference to the connection when the channel timer
> > is scheduled, and releasing it when the timer is either canceled or
> > executes to completion.
> >
> > Since l2cap_chan_del() nullifies chan->conn to disassociate the channel
> > during teardown, the timer handler might read NULL from chan->conn even
> > if it held a reference. To address this, introduce a `timer_conn` field
> > to `struct l2cap_chan` to store the connection pointer associated with
> > the active timer. The timer handler uses this field to acquire locks and
> > release the connection reference, and skips channel closing operations
> > if chan->conn has already been nullified by teardown.
> >
> > Fixes: 75780ca4c6a8 ("Bluetooth: L2CAP: use chan timer to close channels in cleanup_listen()")
> > Cc: <stable@vger.kernel.org>
> > Cc: Siwei Zhang <oss@fourdim.xyz>
> > Cc: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
> > Assisted-by: Gemini:gemini-3.1-pro-preview
> > Reported-by: https://sashiko.dev/#/patchset/20260521021249.3258069-1-oss%40fourdim.xyz
> > Signed-off-by: Marco Elver <elver@google.com>
>
> Sigh, Sashiko points out more problems here:
> https://sashiko.dev/#/patchset/20260603123111.2334409-1-elver%40google.com
>
> > Can this lockless read of chan->timer_conn cause a use-after-free or double
> > free if another thread re-arms the timer concurrently?
>
> I haven't analyzed this further yet, so consider this patch a
> bug-report-only. If anyone finds a better fix sooner, please go ahead.

I was thinking or something like the following:

diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
index c4ccfbda9d78..dfe9318272f3 100644
--- a/net/bluetooth/l2cap_core.c
+++ b/net/bluetooth/l2cap_core.c
@@ -406,17 +406,39 @@ static void l2cap_chan_timeout(struct work_struct *work)
 {
        struct l2cap_chan *chan = container_of(work, struct l2cap_chan,
                                               chan_timer.work);
-       struct l2cap_conn *conn = chan->conn;
+       struct l2cap_conn *conn;
        int reason;

        BT_DBG("chan %p state %s", chan, state_to_string(chan->state));

+       /* Hold a reference to the connection while we are processing this
+        * timeout, to prevent it from being freed out from under us by
+        * l2cap_conn_del().
+        */
+       conn = l2cap_conn_hold_unless_zero(chan->conn);
        if (!conn) {
                l2cap_chan_put(chan);
                return;
        }

        mutex_lock(&conn->lock);
+
+       /* If l2cap_chan_del() was called while waiting for conn->lock the
+        * channel shall be considered already closed and its last reference
+        * shall be released with l2cap_chan_put(chan) here.
+        *
+        * l2cap_conn_del() doesn't wait the channel's works and instead just
+        * leaves the timer reference behind which needs to be released here in
+        * order to free the channel and then l2cap_conn_put() to finally free
+        * the connection.
+        */
+       if (!chan->conn) {
+               mutex_unlock(&conn->lock);
+               l2cap_chan_put(chan);
+               l2cap_conn_put(conn);
+               return;
+       }
+
        /* __set_chan_timer() calls l2cap_chan_hold(chan) while scheduling
         * this work. No need to call l2cap_chan_hold(chan) here again.
         */
@@ -438,6 +460,8 @@ static void l2cap_chan_timeout(struct work_struct *work)
        l2cap_chan_put(chan);

        mutex_unlock(&conn->lock);
+
+       l2cap_conn_put(conn);
 }

 struct l2cap_chan *l2cap_chan_create(void)


> > ---
> >  include/net/bluetooth/l2cap.h | 18 ++++++++++++++++--
> >  net/bluetooth/l2cap_core.c    | 26 +++++++++++++++-----------
> >  2 files changed, 31 insertions(+), 13 deletions(-)
> >
> > diff --git a/include/net/bluetooth/l2cap.h b/include/net/bluetooth/l2cap.h
> > index e0a1f2293679..83719777512e 100644
> > --- a/include/net/bluetooth/l2cap.h
> > +++ b/include/net/bluetooth/l2cap.h
> > @@ -514,6 +514,7 @@ struct l2cap_seq_list {
> >
> >  struct l2cap_chan {
> >         struct l2cap_conn       *conn;
> > +       struct l2cap_conn       *timer_conn; /* for chan_timer */
> >         struct kref     kref;
> >         atomic_t        nesting;
> >
> > @@ -835,6 +836,9 @@ static inline void l2cap_chan_unlock(struct l2cap_chan *chan)
> >         mutex_unlock(&chan->lock);
> >  }
> >
> > +struct l2cap_conn *l2cap_conn_get(struct l2cap_conn *conn);
> > +void l2cap_conn_put(struct l2cap_conn *conn);
> > +
> >  static inline void l2cap_set_timer(struct l2cap_chan *chan,
> >                                    struct delayed_work *work, long timeout)
> >  {
> > @@ -843,8 +847,13 @@ static inline void l2cap_set_timer(struct l2cap_chan *chan,
> >
> >         /* If delayed work cancelled do not hold(chan)
> >            since it is already done with previous set_timer */
> > -       if (!cancel_delayed_work(work))
> > +       if (!cancel_delayed_work(work)) {
> >                 l2cap_chan_hold(chan);
> > +               if (work == &chan->chan_timer && chan->conn) {
> > +                       l2cap_conn_get(chan->conn);
> > +                       chan->timer_conn = chan->conn;
> > +               }
> > +       }
> >
> >         schedule_delayed_work(work, timeout);
> >  }
> > @@ -857,8 +866,13 @@ static inline bool l2cap_clear_timer(struct l2cap_chan *chan,
> >         /* put(chan) if delayed work cancelled otherwise it
> >            is done in delayed work function */
> >         ret = cancel_delayed_work(work);
> > -       if (ret)
> > +       if (ret) {
> > +               if (work == &chan->chan_timer && chan->timer_conn) {
> > +                       l2cap_conn_put(chan->timer_conn);
> > +                       chan->timer_conn = NULL;
> > +               }
> >                 l2cap_chan_put(chan);
> > +       }
> >
> >         return ret;
> >  }
> > diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
> > index c4ccfbda9d78..491b03bf6903 100644
> > --- a/net/bluetooth/l2cap_core.c
> > +++ b/net/bluetooth/l2cap_core.c
> > @@ -406,7 +406,7 @@ static void l2cap_chan_timeout(struct work_struct *work)
> >  {
> >         struct l2cap_chan *chan = container_of(work, struct l2cap_chan,
> >                                                chan_timer.work);
> > -       struct l2cap_conn *conn = chan->conn;
> > +       struct l2cap_conn *conn = chan->timer_conn;
> >         int reason;
> >
> >         BT_DBG("chan %p state %s", chan, state_to_string(chan->state));
> > @@ -421,23 +421,27 @@ static void l2cap_chan_timeout(struct work_struct *work)
> >          * this work. No need to call l2cap_chan_hold(chan) here again.
> >          */
> >         l2cap_chan_lock(chan);
> > +       chan->timer_conn = NULL;
> > +
> > +       if (chan->conn) {
> > +               if (chan->state == BT_CONNECTED || chan->state == BT_CONFIG)
> > +                       reason = ECONNREFUSED;
> > +               else if (chan->state == BT_CONNECT &&
> > +                        chan->sec_level != BT_SECURITY_SDP)
> > +                       reason = ECONNREFUSED;
> > +               else
> > +                       reason = ETIMEDOUT;
> >
> > -       if (chan->state == BT_CONNECTED || chan->state == BT_CONFIG)
> > -               reason = ECONNREFUSED;
> > -       else if (chan->state == BT_CONNECT &&
> > -                chan->sec_level != BT_SECURITY_SDP)
> > -               reason = ECONNREFUSED;
> > -       else
> > -               reason = ETIMEDOUT;
> > -
> > -       l2cap_chan_close(chan, reason);
> > +               l2cap_chan_close(chan, reason);
> >
> > -       chan->ops->close(chan);
> > +               chan->ops->close(chan);
> > +       }
> >
> >         l2cap_chan_unlock(chan);
> >         l2cap_chan_put(chan);
> >
> >         mutex_unlock(&conn->lock);
> > +       l2cap_conn_put(conn);
> >  }
> >
> >  struct l2cap_chan *l2cap_chan_create(void)
> > --
> > 2.54.0.1013.g208068f2d8-goog
> >



-- 
Luiz Augusto von Dentz

^ permalink raw reply related	[flat|nested] 8+ messages in thread

* RE: Bluetooth: L2CAP: Fix UAF in l2cap_chan_timeout
  2026-06-03 12:27 [PATCH] Bluetooth: L2CAP: Fix UAF in l2cap_chan_timeout Marco Elver
  2026-06-03 13:15 ` Marco Elver
@ 2026-06-03 17:37 ` bluez.test.bot
  1 sibling, 0 replies; 8+ messages in thread
From: bluez.test.bot @ 2026-06-03 17:37 UTC (permalink / raw)
  To: linux-bluetooth, elver

[-- Attachment #1: Type: text/plain, Size: 3532 bytes --]

This is automated email and please do not reply to this email!

Dear submitter,

Thank you for submitting the patches to the linux bluetooth mailing list.
This is a CI test results with your patch series:
PW Link:https://patchwork.kernel.org/project/bluetooth/list/?series=1105291

---Test result---

Test Summary:
CheckPatch                    FAIL      0.97 seconds
VerifyFixes                   PASS      0.13 seconds
VerifySignedoff               PASS      0.13 seconds
GitLint                       FAIL      0.33 seconds
SubjectPrefix                 PASS      0.12 seconds
BuildKernel                   PASS      25.11 seconds
CheckAllWarning               PASS      28.16 seconds
CheckSparse                   PASS      26.76 seconds
BuildKernel32                 PASS      24.42 seconds
TestRunnerSetup               PASS      524.47 seconds
TestRunner_l2cap-tester       PASS      58.55 seconds
IncrementalBuild              PASS      23.63 seconds

Details
##############################
Test: CheckPatch - FAIL
Desc: Run checkpatch.pl script
Output:
Bluetooth: L2CAP: Fix UAF in l2cap_chan_timeout
WARNING: Prefer a maximum 75 chars per line (possible unwrapped commit description?)
#98: 
| BUG: KASAN: slab-use-after-free in instrument_atomic_read_write include/linux/instrumented.h:112 [inline]

ERROR: Unrecognized email address: 'https://sashiko.dev/#/patchset/20260521021249.3258069-1-oss%40fourdim.xyz'
#191: 
Reported-by: https://sashiko.dev/#/patchset/20260521021249.3258069-1-oss%40fourdim.xyz

WARNING: Reported-by: should be immediately followed by Closes: with a URL to the report
#191: 
Reported-by: https://sashiko.dev/#/patchset/20260521021249.3258069-1-oss%40fourdim.xyz
Signed-off-by: Marco Elver <elver@google.com>

total: 1 errors, 2 warnings, 0 checks, 89 lines checked

NOTE: For some of the reported defects, checkpatch may be able to
      mechanically convert to the typical style using --fix or --fix-inplace.

/github/workspace/src/patch/14609364.patch has style problems, please review.

NOTE: Ignored message types: UNKNOWN_COMMIT_ID

NOTE: If any of the errors are false positives, please report
      them to the maintainer, see CHECKPATCH in MAINTAINERS.


##############################
Test: GitLint - FAIL
Desc: Run gitlint
Output:
Bluetooth: L2CAP: Fix UAF in l2cap_chan_timeout

9: B1 Line exceeds max length (107>80): "| BUG: KASAN: slab-use-after-free in instrument_atomic_read_write include/linux/instrumented.h:112 [inline]"
10: B1 Line exceeds max length (125>80): "| BUG: KASAN: slab-use-after-free in atomic_long_try_cmpxchg_acquire include/linux/atomic/atomic-instrumented.h:4456 [inline]"
11: B1 Line exceeds max length (93>80): "| BUG: KASAN: slab-use-after-free in __mutex_trylock_fast kernel/locking/mutex.c:161 [inline]"
12: B1 Line exceeds max length (84>80): "| BUG: KASAN: slab-use-after-free in mutex_lock+0x4f/0xa0 kernel/locking/mutex.c:318"
15: B1 Line exceeds max length (100>80): "| CPU: 2 UID: 0 PID: 83 Comm: kworker/2:1 Not tainted 7.1.0-rc6-next-20260601-dirty #6 PREEMPT(full)"
16: B1 Line exceeds max length (95>80): "| Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.17.0-debian-1.17.0-1 04/01/2014"
21: B1 Line exceeds max length (91>80): "|  atomic_long_try_cmpxchg_acquire include/linux/atomic/atomic-instrumented.h:4456 [inline]"
62: B1 Line exceeds max length (82>80): "|  syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:230 [inline]"


https://github.com/bluez/bluetooth-next/pull/281

---
Regards,
Linux Bluetooth


^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH] Bluetooth: L2CAP: Fix UAF in l2cap_chan_timeout
  2026-06-03 17:31   ` Luiz Augusto von Dentz
@ 2026-06-04 12:45     ` Marco Elver
  2026-06-04 14:10       ` Luiz Augusto von Dentz
  0 siblings, 1 reply; 8+ messages in thread
From: Marco Elver @ 2026-06-04 12:45 UTC (permalink / raw)
  To: Luiz Augusto von Dentz
  Cc: Marcel Holtmann, linux-bluetooth, linux-kernel, kasan-dev, stable,
	Siwei Zhang, Luiz Augusto von Dentz

On Wed, Jun 03, 2026 at 01:31PM -0400, Luiz Augusto von Dentz wrote:
> Hi Marco,
> 
> On Wed, Jun 3, 2026 at 9:16 AM Marco Elver <elver@google.com> wrote:
> >
> > On Wed, 3 Jun 2026 at 14:31, Marco Elver <elver@google.com> wrote:
> > >
> > > l2cap_chan_timeout() accesses chan->conn without holding a reference to
> > > the connection object. If l2cap_conn_del() races and tears down the
> > > connection while the timer is waiting for locks, it can result in a
> > > use-after-free when the timer wakes up and attempts to acquire
> > > conn->lock:
> > >
> > > | BUG: KASAN: slab-use-after-free in instrument_atomic_read_write include/linux/instrumented.h:112 [inline]
> > > | BUG: KASAN: slab-use-after-free in atomic_long_try_cmpxchg_acquire include/linux/atomic/atomic-instrumented.h:4456 [inline]
> > > | BUG: KASAN: slab-use-after-free in __mutex_trylock_fast kernel/locking/mutex.c:161 [inline]
> > > | BUG: KASAN: slab-use-after-free in mutex_lock+0x4f/0xa0 kernel/locking/mutex.c:318
> > > | Write of size 8 at addr ffff8881298d9550 by task kworker/2:1/83
> > > |
> > > | CPU: 2 UID: 0 PID: 83 Comm: kworker/2:1 Not tainted 7.1.0-rc6-next-20260601-dirty #6 PREEMPT(full)
> > > | Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.17.0-debian-1.17.0-1 04/01/2014
> > > | Workqueue: events l2cap_chan_timeout
> > > | Call Trace:
> > > |  <TASK>
> > > |  instrument_atomic_read_write include/linux/instrumented.h:112 [inline]
> > > |  atomic_long_try_cmpxchg_acquire include/linux/atomic/atomic-instrumented.h:4456 [inline]
> > > |  __mutex_trylock_fast kernel/locking/mutex.c:161 [inline]
> > > |  mutex_lock+0x4f/0xa0 kernel/locking/mutex.c:318
> > > |  l2cap_chan_timeout+0x5d/0x1b0 net/bluetooth/l2cap_core.c:422
> > > |  process_one_work kernel/workqueue.c:3326 [inline]
> > > |  process_scheduled_works+0x7c8/0xfb0 kernel/workqueue.c:3409
> > > |  worker_thread+0x8a9/0xcf0 kernel/workqueue.c:3490
> > > |  kthread+0x346/0x430 kernel/kthread.c:436
> > > |  ret_from_fork+0x1a3/0x470 arch/x86/kernel/process.c:158
> > > |  ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
> > > |  </TASK>
> > > |
> > > | Allocated by task 320:
> > > |  l2cap_conn_add+0xa7/0x820 net/bluetooth/l2cap_core.c:7075
> > > |  l2cap_connect_cfm+0xdb/0xd70 net/bluetooth/l2cap_core.c:7452
> > > |  hci_connect_cfm include/net/bluetooth/hci_core.h:2139 [inline]
> > > |  hci_remote_features_evt+0x52f/0x9f0 net/bluetooth/hci_event.c:3760
> > > |  hci_event_func net/bluetooth/hci_event.c:7796 [inline]
> > > |  hci_event_packet+0x561/0xa70 net/bluetooth/hci_event.c:7847
> > > |  hci_rx_work+0x370/0x890 net/bluetooth/hci_core.c:4040
> > > |  process_one_work kernel/workqueue.c:3326 [inline]
> > > |  process_scheduled_works+0x7c8/0xfb0 kernel/workqueue.c:3409
> > > |  worker_thread+0x8a9/0xcf0 kernel/workqueue.c:3490
> > > |  kthread+0x346/0x430 kernel/kthread.c:436
> > > |  ret_from_fork+0x1a3/0x470 arch/x86/kernel/process.c:158
> > > |  ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
> > > |
> > > | Freed by task 322:
> > > |  hci_disconn_cfm include/net/bluetooth/hci_core.h:2154 [inline]
> > > |  hci_conn_hash_flush+0x101/0x1f0 net/bluetooth/hci_conn.c:2736
> > > |  hci_dev_close_sync+0x889/0xde0 net/bluetooth/hci_sync.c:5405
> > > |  hci_dev_do_close net/bluetooth/hci_core.c:502 [inline]
> > > |  hci_unregister_dev+0x1f7/0x370 net/bluetooth/hci_core.c:2679
> > > |  vhci_release+0x12a/0x180 drivers/bluetooth/hci_vhci.c:690
> > > |  __fput+0x369/0x890 fs/file_table.c:510
> > > |  task_work_run+0x160/0x1d0 kernel/task_work.c:233
> > > |  get_signal+0xf5b/0x1120 kernel/signal.c:2810
> > > |  arch_do_signal_or_restart+0x4d/0x600 arch/x86/kernel/signal.c:337
> > > |  __exit_to_user_mode_loop kernel/entry/common.c:64 [inline]
> > > |  exit_to_user_mode_loop+0x85/0x510 kernel/entry/common.c:98
> > > |  __exit_to_user_mode_prepare include/linux/irq-entry-common.h:207 [inline]
> > > |  syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:230 [inline]
> > > |  syscall_exit_to_user_mode include/linux/entry-common.h:318 [inline]
> > > |  do_syscall_64+0x263/0x3d0 arch/x86/entry/syscall_64.c:100
> > > |  entry_SYSCALL_64_after_hwframe+0x77/0x7f
> > > |
> > > | Last potentially related work creation:
> > > |  hci_connect_cfm include/net/bluetooth/hci_core.h:2139 [inline]
> > > |  hci_remote_features_evt+0x52f/0x9f0 net/bluetooth/hci_event.c:3760
> > > |  hci_event_func net/bluetooth/hci_event.c:7796 [inline]
> > > |  hci_event_packet+0x561/0xa70 net/bluetooth/hci_event.c:7847
> > > |  hci_rx_work+0x370/0x890 net/bluetooth/hci_core.c:4040
> > > |  process_one_work kernel/workqueue.c:3326 [inline]
> > > |  process_scheduled_works+0x7c8/0xfb0 kernel/workqueue.c:3409
> > > |  worker_thread+0x8a9/0xcf0 kernel/workqueue.c:3490
> > > |  kthread+0x346/0x430 kernel/kthread.c:436
> > > |  ret_from_fork+0x1a3/0x470 arch/x86/kernel/process.c:158
> > > |  ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
> > > |
> > > | The buggy address belongs to the object at ffff8881298d9400
> > > |  which belongs to the cache kmalloc-512 of size 512
> > > | The buggy address is located 336 bytes inside of
> > > |  freed 512-byte region [ffff8881298d9400, ffff8881298d9600)
> > >
> > > Fix it by holding a reference to the connection when the channel timer
> > > is scheduled, and releasing it when the timer is either canceled or
> > > executes to completion.
> > >
> > > Since l2cap_chan_del() nullifies chan->conn to disassociate the channel
> > > during teardown, the timer handler might read NULL from chan->conn even
> > > if it held a reference. To address this, introduce a `timer_conn` field
> > > to `struct l2cap_chan` to store the connection pointer associated with
> > > the active timer. The timer handler uses this field to acquire locks and
> > > release the connection reference, and skips channel closing operations
> > > if chan->conn has already been nullified by teardown.
> > >
> > > Fixes: 75780ca4c6a8 ("Bluetooth: L2CAP: use chan timer to close channels in cleanup_listen()")
> > > Cc: <stable@vger.kernel.org>
> > > Cc: Siwei Zhang <oss@fourdim.xyz>
> > > Cc: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
> > > Assisted-by: Gemini:gemini-3.1-pro-preview
> > > Reported-by: https://sashiko.dev/#/patchset/20260521021249.3258069-1-oss%40fourdim.xyz
> > > Signed-off-by: Marco Elver <elver@google.com>
> >
> > Sigh, Sashiko points out more problems here:
> > https://sashiko.dev/#/patchset/20260603123111.2334409-1-elver%40google.com
> >
> > > Can this lockless read of chan->timer_conn cause a use-after-free or double
> > > free if another thread re-arms the timer concurrently?
> >
> > I haven't analyzed this further yet, so consider this patch a
> > bug-report-only. If anyone finds a better fix sooner, please go ahead.
> 
> I was thinking or something like the following:

I tested that and my repro didn't trigger the UAF here, but I still
think it has the same fundamental issue:

If the timer worker is preempted immediately after reading chan->conn
but before entering l2cap_conn_hold_unless_zero(), l2cap_conn_del() can
complete concurrently.

When the timer worker resumes, l2cap_conn_hold_unless_zero(conn) will
attempt to read conn->ref that has already been freed, resulting in
another UAF.

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH] Bluetooth: L2CAP: Fix UAF in l2cap_chan_timeout
  2026-06-04 12:45     ` Marco Elver
@ 2026-06-04 14:10       ` Luiz Augusto von Dentz
  2026-06-05 10:18         ` Marco Elver
  0 siblings, 1 reply; 8+ messages in thread
From: Luiz Augusto von Dentz @ 2026-06-04 14:10 UTC (permalink / raw)
  To: Marco Elver
  Cc: Marcel Holtmann, linux-bluetooth, linux-kernel, kasan-dev, stable,
	Siwei Zhang, Luiz Augusto von Dentz

Hi Marco,

On Thu, Jun 4, 2026 at 8:45 AM Marco Elver <elver@google.com> wrote:
>
> On Wed, Jun 03, 2026 at 01:31PM -0400, Luiz Augusto von Dentz wrote:
> > Hi Marco,
> >
> > On Wed, Jun 3, 2026 at 9:16 AM Marco Elver <elver@google.com> wrote:
> > >
> > > On Wed, 3 Jun 2026 at 14:31, Marco Elver <elver@google.com> wrote:
> > > >
> > > > l2cap_chan_timeout() accesses chan->conn without holding a reference to
> > > > the connection object. If l2cap_conn_del() races and tears down the
> > > > connection while the timer is waiting for locks, it can result in a
> > > > use-after-free when the timer wakes up and attempts to acquire
> > > > conn->lock:
> > > >
> > > > | BUG: KASAN: slab-use-after-free in instrument_atomic_read_write include/linux/instrumented.h:112 [inline]
> > > > | BUG: KASAN: slab-use-after-free in atomic_long_try_cmpxchg_acquire include/linux/atomic/atomic-instrumented.h:4456 [inline]
> > > > | BUG: KASAN: slab-use-after-free in __mutex_trylock_fast kernel/locking/mutex.c:161 [inline]
> > > > | BUG: KASAN: slab-use-after-free in mutex_lock+0x4f/0xa0 kernel/locking/mutex.c:318
> > > > | Write of size 8 at addr ffff8881298d9550 by task kworker/2:1/83
> > > > |
> > > > | CPU: 2 UID: 0 PID: 83 Comm: kworker/2:1 Not tainted 7.1.0-rc6-next-20260601-dirty #6 PREEMPT(full)
> > > > | Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.17.0-debian-1.17.0-1 04/01/2014
> > > > | Workqueue: events l2cap_chan_timeout
> > > > | Call Trace:
> > > > |  <TASK>
> > > > |  instrument_atomic_read_write include/linux/instrumented.h:112 [inline]
> > > > |  atomic_long_try_cmpxchg_acquire include/linux/atomic/atomic-instrumented.h:4456 [inline]
> > > > |  __mutex_trylock_fast kernel/locking/mutex.c:161 [inline]
> > > > |  mutex_lock+0x4f/0xa0 kernel/locking/mutex.c:318
> > > > |  l2cap_chan_timeout+0x5d/0x1b0 net/bluetooth/l2cap_core.c:422
> > > > |  process_one_work kernel/workqueue.c:3326 [inline]
> > > > |  process_scheduled_works+0x7c8/0xfb0 kernel/workqueue.c:3409
> > > > |  worker_thread+0x8a9/0xcf0 kernel/workqueue.c:3490
> > > > |  kthread+0x346/0x430 kernel/kthread.c:436
> > > > |  ret_from_fork+0x1a3/0x470 arch/x86/kernel/process.c:158
> > > > |  ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
> > > > |  </TASK>
> > > > |
> > > > | Allocated by task 320:
> > > > |  l2cap_conn_add+0xa7/0x820 net/bluetooth/l2cap_core.c:7075
> > > > |  l2cap_connect_cfm+0xdb/0xd70 net/bluetooth/l2cap_core.c:7452
> > > > |  hci_connect_cfm include/net/bluetooth/hci_core.h:2139 [inline]
> > > > |  hci_remote_features_evt+0x52f/0x9f0 net/bluetooth/hci_event.c:3760
> > > > |  hci_event_func net/bluetooth/hci_event.c:7796 [inline]
> > > > |  hci_event_packet+0x561/0xa70 net/bluetooth/hci_event.c:7847
> > > > |  hci_rx_work+0x370/0x890 net/bluetooth/hci_core.c:4040
> > > > |  process_one_work kernel/workqueue.c:3326 [inline]
> > > > |  process_scheduled_works+0x7c8/0xfb0 kernel/workqueue.c:3409
> > > > |  worker_thread+0x8a9/0xcf0 kernel/workqueue.c:3490
> > > > |  kthread+0x346/0x430 kernel/kthread.c:436
> > > > |  ret_from_fork+0x1a3/0x470 arch/x86/kernel/process.c:158
> > > > |  ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
> > > > |
> > > > | Freed by task 322:
> > > > |  hci_disconn_cfm include/net/bluetooth/hci_core.h:2154 [inline]
> > > > |  hci_conn_hash_flush+0x101/0x1f0 net/bluetooth/hci_conn.c:2736
> > > > |  hci_dev_close_sync+0x889/0xde0 net/bluetooth/hci_sync.c:5405
> > > > |  hci_dev_do_close net/bluetooth/hci_core.c:502 [inline]
> > > > |  hci_unregister_dev+0x1f7/0x370 net/bluetooth/hci_core.c:2679
> > > > |  vhci_release+0x12a/0x180 drivers/bluetooth/hci_vhci.c:690
> > > > |  __fput+0x369/0x890 fs/file_table.c:510
> > > > |  task_work_run+0x160/0x1d0 kernel/task_work.c:233
> > > > |  get_signal+0xf5b/0x1120 kernel/signal.c:2810
> > > > |  arch_do_signal_or_restart+0x4d/0x600 arch/x86/kernel/signal.c:337
> > > > |  __exit_to_user_mode_loop kernel/entry/common.c:64 [inline]
> > > > |  exit_to_user_mode_loop+0x85/0x510 kernel/entry/common.c:98
> > > > |  __exit_to_user_mode_prepare include/linux/irq-entry-common.h:207 [inline]
> > > > |  syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:230 [inline]
> > > > |  syscall_exit_to_user_mode include/linux/entry-common.h:318 [inline]
> > > > |  do_syscall_64+0x263/0x3d0 arch/x86/entry/syscall_64.c:100
> > > > |  entry_SYSCALL_64_after_hwframe+0x77/0x7f
> > > > |
> > > > | Last potentially related work creation:
> > > > |  hci_connect_cfm include/net/bluetooth/hci_core.h:2139 [inline]
> > > > |  hci_remote_features_evt+0x52f/0x9f0 net/bluetooth/hci_event.c:3760
> > > > |  hci_event_func net/bluetooth/hci_event.c:7796 [inline]
> > > > |  hci_event_packet+0x561/0xa70 net/bluetooth/hci_event.c:7847
> > > > |  hci_rx_work+0x370/0x890 net/bluetooth/hci_core.c:4040
> > > > |  process_one_work kernel/workqueue.c:3326 [inline]
> > > > |  process_scheduled_works+0x7c8/0xfb0 kernel/workqueue.c:3409
> > > > |  worker_thread+0x8a9/0xcf0 kernel/workqueue.c:3490
> > > > |  kthread+0x346/0x430 kernel/kthread.c:436
> > > > |  ret_from_fork+0x1a3/0x470 arch/x86/kernel/process.c:158
> > > > |  ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
> > > > |
> > > > | The buggy address belongs to the object at ffff8881298d9400
> > > > |  which belongs to the cache kmalloc-512 of size 512
> > > > | The buggy address is located 336 bytes inside of
> > > > |  freed 512-byte region [ffff8881298d9400, ffff8881298d9600)
> > > >
> > > > Fix it by holding a reference to the connection when the channel timer
> > > > is scheduled, and releasing it when the timer is either canceled or
> > > > executes to completion.
> > > >
> > > > Since l2cap_chan_del() nullifies chan->conn to disassociate the channel
> > > > during teardown, the timer handler might read NULL from chan->conn even
> > > > if it held a reference. To address this, introduce a `timer_conn` field
> > > > to `struct l2cap_chan` to store the connection pointer associated with
> > > > the active timer. The timer handler uses this field to acquire locks and
> > > > release the connection reference, and skips channel closing operations
> > > > if chan->conn has already been nullified by teardown.
> > > >
> > > > Fixes: 75780ca4c6a8 ("Bluetooth: L2CAP: use chan timer to close channels in cleanup_listen()")
> > > > Cc: <stable@vger.kernel.org>
> > > > Cc: Siwei Zhang <oss@fourdim.xyz>
> > > > Cc: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
> > > > Assisted-by: Gemini:gemini-3.1-pro-preview
> > > > Reported-by: https://sashiko.dev/#/patchset/20260521021249.3258069-1-oss%40fourdim.xyz
> > > > Signed-off-by: Marco Elver <elver@google.com>
> > >
> > > Sigh, Sashiko points out more problems here:
> > > https://sashiko.dev/#/patchset/20260603123111.2334409-1-elver%40google.com
> > >
> > > > Can this lockless read of chan->timer_conn cause a use-after-free or double
> > > > free if another thread re-arms the timer concurrently?
> > >
> > > I haven't analyzed this further yet, so consider this patch a
> > > bug-report-only. If anyone finds a better fix sooner, please go ahead.
> >
> > I was thinking or something like the following:
>
> I tested that and my repro didn't trigger the UAF here, but I still
> think it has the same fundamental issue:
>
> If the timer worker is preempted immediately after reading chan->conn
> but before entering l2cap_conn_hold_unless_zero(), l2cap_conn_del() can
> complete concurrently.
>
> When the timer worker resumes, l2cap_conn_hold_unless_zero(conn) will
> attempt to read conn->ref that has already been freed, resulting in
> another UAF.

I see. The window is very narrow but it is perhaps still triggerable
somehow. The only thing that comes to mind is that we would need to
take a reference of l2cap_conn with the likes of l2cap_set_timer then,
which means l2cap_chan_timeout needs to drop not only l2cap_chan but
also l2cap_conn when done, otherwise there will always be the risk of
l2cap_conn_del running while l2cap_chan_timeout is pending.

-- 
Luiz Augusto von Dentz

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH] Bluetooth: L2CAP: Fix UAF in l2cap_chan_timeout
  2026-06-04 14:10       ` Luiz Augusto von Dentz
@ 2026-06-05 10:18         ` Marco Elver
  2026-06-05 13:53           ` Luiz Augusto von Dentz
  0 siblings, 1 reply; 8+ messages in thread
From: Marco Elver @ 2026-06-05 10:18 UTC (permalink / raw)
  To: Luiz Augusto von Dentz
  Cc: Marcel Holtmann, linux-bluetooth, linux-kernel, kasan-dev, stable,
	Siwei Zhang, Luiz Augusto von Dentz

On Thu, Jun 04, 2026 at 10:10AM -0400, Luiz Augusto von Dentz wrote:
> Hi Marco,
> 
> On Thu, Jun 4, 2026 at 8:45 AM Marco Elver <elver@google.com> wrote:
> >
> > On Wed, Jun 03, 2026 at 01:31PM -0400, Luiz Augusto von Dentz wrote:
> > > Hi Marco,
> > >
> > > On Wed, Jun 3, 2026 at 9:16 AM Marco Elver <elver@google.com> wrote:
> > > >
> > > > On Wed, 3 Jun 2026 at 14:31, Marco Elver <elver@google.com> wrote:
> > > > >
> > > > > l2cap_chan_timeout() accesses chan->conn without holding a reference to
> > > > > the connection object. If l2cap_conn_del() races and tears down the
> > > > > connection while the timer is waiting for locks, it can result in a
> > > > > use-after-free when the timer wakes up and attempts to acquire
> > > > > conn->lock:
> > > > >
> > > > > | BUG: KASAN: slab-use-after-free in instrument_atomic_read_write include/linux/instrumented.h:112 [inline]
> > > > > | BUG: KASAN: slab-use-after-free in atomic_long_try_cmpxchg_acquire include/linux/atomic/atomic-instrumented.h:4456 [inline]
> > > > > | BUG: KASAN: slab-use-after-free in __mutex_trylock_fast kernel/locking/mutex.c:161 [inline]
> > > > > | BUG: KASAN: slab-use-after-free in mutex_lock+0x4f/0xa0 kernel/locking/mutex.c:318
> > > > > | Write of size 8 at addr ffff8881298d9550 by task kworker/2:1/83
> > > > > |
> > > > > | CPU: 2 UID: 0 PID: 83 Comm: kworker/2:1 Not tainted 7.1.0-rc6-next-20260601-dirty #6 PREEMPT(full)
> > > > > | Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.17.0-debian-1.17.0-1 04/01/2014
> > > > > | Workqueue: events l2cap_chan_timeout
> > > > > | Call Trace:
> > > > > |  <TASK>
> > > > > |  instrument_atomic_read_write include/linux/instrumented.h:112 [inline]
> > > > > |  atomic_long_try_cmpxchg_acquire include/linux/atomic/atomic-instrumented.h:4456 [inline]
> > > > > |  __mutex_trylock_fast kernel/locking/mutex.c:161 [inline]
> > > > > |  mutex_lock+0x4f/0xa0 kernel/locking/mutex.c:318
> > > > > |  l2cap_chan_timeout+0x5d/0x1b0 net/bluetooth/l2cap_core.c:422
> > > > > |  process_one_work kernel/workqueue.c:3326 [inline]
> > > > > |  process_scheduled_works+0x7c8/0xfb0 kernel/workqueue.c:3409
> > > > > |  worker_thread+0x8a9/0xcf0 kernel/workqueue.c:3490
> > > > > |  kthread+0x346/0x430 kernel/kthread.c:436
> > > > > |  ret_from_fork+0x1a3/0x470 arch/x86/kernel/process.c:158
> > > > > |  ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
> > > > > |  </TASK>
> > > > > |
> > > > > | Allocated by task 320:
> > > > > |  l2cap_conn_add+0xa7/0x820 net/bluetooth/l2cap_core.c:7075
> > > > > |  l2cap_connect_cfm+0xdb/0xd70 net/bluetooth/l2cap_core.c:7452
> > > > > |  hci_connect_cfm include/net/bluetooth/hci_core.h:2139 [inline]
> > > > > |  hci_remote_features_evt+0x52f/0x9f0 net/bluetooth/hci_event.c:3760
> > > > > |  hci_event_func net/bluetooth/hci_event.c:7796 [inline]
> > > > > |  hci_event_packet+0x561/0xa70 net/bluetooth/hci_event.c:7847
> > > > > |  hci_rx_work+0x370/0x890 net/bluetooth/hci_core.c:4040
> > > > > |  process_one_work kernel/workqueue.c:3326 [inline]
> > > > > |  process_scheduled_works+0x7c8/0xfb0 kernel/workqueue.c:3409
> > > > > |  worker_thread+0x8a9/0xcf0 kernel/workqueue.c:3490
> > > > > |  kthread+0x346/0x430 kernel/kthread.c:436
> > > > > |  ret_from_fork+0x1a3/0x470 arch/x86/kernel/process.c:158
> > > > > |  ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
> > > > > |
> > > > > | Freed by task 322:
> > > > > |  hci_disconn_cfm include/net/bluetooth/hci_core.h:2154 [inline]
> > > > > |  hci_conn_hash_flush+0x101/0x1f0 net/bluetooth/hci_conn.c:2736
> > > > > |  hci_dev_close_sync+0x889/0xde0 net/bluetooth/hci_sync.c:5405
> > > > > |  hci_dev_do_close net/bluetooth/hci_core.c:502 [inline]
> > > > > |  hci_unregister_dev+0x1f7/0x370 net/bluetooth/hci_core.c:2679
> > > > > |  vhci_release+0x12a/0x180 drivers/bluetooth/hci_vhci.c:690
> > > > > |  __fput+0x369/0x890 fs/file_table.c:510
> > > > > |  task_work_run+0x160/0x1d0 kernel/task_work.c:233
> > > > > |  get_signal+0xf5b/0x1120 kernel/signal.c:2810
> > > > > |  arch_do_signal_or_restart+0x4d/0x600 arch/x86/kernel/signal.c:337
> > > > > |  __exit_to_user_mode_loop kernel/entry/common.c:64 [inline]
> > > > > |  exit_to_user_mode_loop+0x85/0x510 kernel/entry/common.c:98
> > > > > |  __exit_to_user_mode_prepare include/linux/irq-entry-common.h:207 [inline]
> > > > > |  syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:230 [inline]
> > > > > |  syscall_exit_to_user_mode include/linux/entry-common.h:318 [inline]
> > > > > |  do_syscall_64+0x263/0x3d0 arch/x86/entry/syscall_64.c:100
> > > > > |  entry_SYSCALL_64_after_hwframe+0x77/0x7f
> > > > > |
> > > > > | Last potentially related work creation:
> > > > > |  hci_connect_cfm include/net/bluetooth/hci_core.h:2139 [inline]
> > > > > |  hci_remote_features_evt+0x52f/0x9f0 net/bluetooth/hci_event.c:3760
> > > > > |  hci_event_func net/bluetooth/hci_event.c:7796 [inline]
> > > > > |  hci_event_packet+0x561/0xa70 net/bluetooth/hci_event.c:7847
> > > > > |  hci_rx_work+0x370/0x890 net/bluetooth/hci_core.c:4040
> > > > > |  process_one_work kernel/workqueue.c:3326 [inline]
> > > > > |  process_scheduled_works+0x7c8/0xfb0 kernel/workqueue.c:3409
> > > > > |  worker_thread+0x8a9/0xcf0 kernel/workqueue.c:3490
> > > > > |  kthread+0x346/0x430 kernel/kthread.c:436
> > > > > |  ret_from_fork+0x1a3/0x470 arch/x86/kernel/process.c:158
> > > > > |  ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
> > > > > |
> > > > > | The buggy address belongs to the object at ffff8881298d9400
> > > > > |  which belongs to the cache kmalloc-512 of size 512
> > > > > | The buggy address is located 336 bytes inside of
> > > > > |  freed 512-byte region [ffff8881298d9400, ffff8881298d9600)
> > > > >
> > > > > Fix it by holding a reference to the connection when the channel timer
> > > > > is scheduled, and releasing it when the timer is either canceled or
> > > > > executes to completion.
> > > > >
> > > > > Since l2cap_chan_del() nullifies chan->conn to disassociate the channel
> > > > > during teardown, the timer handler might read NULL from chan->conn even
> > > > > if it held a reference. To address this, introduce a `timer_conn` field
> > > > > to `struct l2cap_chan` to store the connection pointer associated with
> > > > > the active timer. The timer handler uses this field to acquire locks and
> > > > > release the connection reference, and skips channel closing operations
> > > > > if chan->conn has already been nullified by teardown.
> > > > >
> > > > > Fixes: 75780ca4c6a8 ("Bluetooth: L2CAP: use chan timer to close channels in cleanup_listen()")
> > > > > Cc: <stable@vger.kernel.org>
> > > > > Cc: Siwei Zhang <oss@fourdim.xyz>
> > > > > Cc: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
> > > > > Assisted-by: Gemini:gemini-3.1-pro-preview
> > > > > Reported-by: https://sashiko.dev/#/patchset/20260521021249.3258069-1-oss%40fourdim.xyz
> > > > > Signed-off-by: Marco Elver <elver@google.com>
> > > >
> > > > Sigh, Sashiko points out more problems here:
> > > > https://sashiko.dev/#/patchset/20260603123111.2334409-1-elver%40google.com
> > > >
> > > > > Can this lockless read of chan->timer_conn cause a use-after-free or double
> > > > > free if another thread re-arms the timer concurrently?
> > > >
> > > > I haven't analyzed this further yet, so consider this patch a
> > > > bug-report-only. If anyone finds a better fix sooner, please go ahead.
> > >
> > > I was thinking or something like the following:
> >
> > I tested that and my repro didn't trigger the UAF here, but I still
> > think it has the same fundamental issue:
> >
> > If the timer worker is preempted immediately after reading chan->conn
> > but before entering l2cap_conn_hold_unless_zero(), l2cap_conn_del() can
> > complete concurrently.
> >
> > When the timer worker resumes, l2cap_conn_hold_unless_zero(conn) will
> > attempt to read conn->ref that has already been freed, resulting in
> > another UAF.
> 
> I see. The window is very narrow but it is perhaps still triggerable
> somehow. The only thing that comes to mind is that we would need to
> take a reference of l2cap_conn with the likes of l2cap_set_timer then,
> which means l2cap_chan_timeout needs to drop not only l2cap_chan but
> also l2cap_conn when done, otherwise there will always be the risk of
> l2cap_conn_del running while l2cap_chan_timeout is pending.

What if we tie conn's lifetime to chan? I see that 'conn' being
NULL/non-NULL is also used as a presence/not-present marker, but we
could add an explicit conn_ref?

------ >8 ------

From: Marco Elver <elver@google.com>
Date: Wed, 3 Jun 2026 18:24:56 +0200
Subject: [PATCH] Bluetooth: L2CAP: Fix UAF in channel timeout by holding conn
 ref

l2cap_chan_timeout() runs asynchronously and accesses chan->conn. If
the connection is torn down while the timer is running or pending,
chan->conn can be freed, leading to a use-after-free when the timer
worker attempts to lock conn->lock:

| BUG: KASAN: slab-use-after-free in instrument_atomic_read_write include/linux/instrumented.h:112 [inline]
| BUG: KASAN: slab-use-after-free in atomic_long_try_cmpxchg_acquire include/linux/atomic/atomic-instrumented.h:4456 [inline]
| BUG: KASAN: slab-use-after-free in __mutex_trylock_fast kernel/locking/mutex.c:161 [inline]
| BUG: KASAN: slab-use-after-free in mutex_lock+0x4f/0xa0 kernel/locking/mutex.c:318
| Write of size 8 at addr ffff8881298d9550 by task kworker/2:1/83
|
| CPU: 2 UID: 0 PID: 83 Comm: kworker/2:1 Not tainted 7.1.0-rc6-next-20260601-dirty #6 PREEMPT(full)
| Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.17.0-debian-1.17.0-1 04/01/2014
| Workqueue: events l2cap_chan_timeout
| Call Trace:
|  <TASK>
|  instrument_atomic_read_write include/linux/instrumented.h:112 [inline]
|  atomic_long_try_cmpxchg_acquire include/linux/atomic/atomic-instrumented.h:4456 [inline]
|  __mutex_trylock_fast kernel/locking/mutex.c:161 [inline]
|  mutex_lock+0x4f/0xa0 kernel/locking/mutex.c:318
|  l2cap_chan_timeout+0x5d/0x1b0 net/bluetooth/l2cap_core.c:422
|  process_one_work kernel/workqueue.c:3326 [inline]
|  process_scheduled_works+0x7c8/0xfb0 kernel/workqueue.c:3409
|  worker_thread+0x8a9/0xcf0 kernel/workqueue.c:3490
|  kthread+0x346/0x430 kernel/kthread.c:436
|  ret_from_fork+0x1a3/0x470 arch/x86/kernel/process.c:158
|  ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
|  </TASK>
|
| Allocated by task 320:
|  l2cap_conn_add+0xa7/0x820 net/bluetooth/l2cap_core.c:7075
|  l2cap_connect_cfm+0xdb/0xd70 net/bluetooth/l2cap_core.c:7452
|  hci_connect_cfm include/net/bluetooth/hci_core.h:2139 [inline]
|  hci_remote_features_evt+0x52f/0x9f0 net/bluetooth/hci_event.c:3760
|  hci_event_func net/bluetooth/hci_event.c:7796 [inline]
|  hci_event_packet+0x561/0xa70 net/bluetooth/hci_event.c:7847
|  hci_rx_work+0x370/0x890 net/bluetooth/hci_core.c:4040
|  process_one_work kernel/workqueue.c:3326 [inline]
|  process_scheduled_works+0x7c8/0xfb0 kernel/workqueue.c:3409
|  worker_thread+0x8a9/0xcf0 kernel/workqueue.c:3490
|  kthread+0x346/0x430 kernel/kthread.c:436
|  ret_from_fork+0x1a3/0x470 arch/x86/kernel/process.c:158
|  ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
|
| Freed by task 322:
|  hci_disconn_cfm include/net/bluetooth/hci_core.h:2154 [inline]
|  hci_conn_hash_flush+0x101/0x1f0 net/bluetooth/hci_conn.c:2736
|  hci_dev_close_sync+0x889/0xde0 net/bluetooth/hci_sync.c:5405
|  hci_dev_do_close net/bluetooth/hci_core.c:502 [inline]
|  hci_unregister_dev+0x1f7/0x370 net/bluetooth/hci_core.c:2679
|  vhci_release+0x12a/0x180 drivers/bluetooth/hci_vhci.c:690
|  __fput+0x369/0x890 fs/file_table.c:510
|  task_work_run+0x160/0x1d0 kernel/task_work.c:233
|  get_signal+0xf5b/0x1120 kernel/signal.c:2810
|  arch_do_signal_or_restart+0x4d/0x600 arch/x86/kernel/signal.c:337
|  __exit_to_user_mode_loop kernel/entry/common.c:64 [inline]
|  exit_to_user_mode_loop+0x85/0x510 kernel/entry/common.c:98
|  __exit_to_user_mode_prepare include/linux/irq-entry-common.h:207 [i
e]
|  syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:
[inline]
|  syscall_exit_to_user_mode include/linux/entry-common.h:318 [inline]
|  do_syscall_64+0x263/0x3d0 arch/x86/entry/syscall_64.c:100
|  entry_SYSCALL_64_after_hwframe+0x77/0x7f
|
| Last potentially related work creation:
|  hci_connect_cfm include/net/bluetooth/hci_core.h:2139 [inline]
|  hci_remote_features_evt+0x52f/0x9f0 net/bluetooth/hci_event.c:3760
|  hci_event_func net/bluetooth/hci_event.c:7796 [inline]
|  hci_event_packet+0x561/0xa70 net/bluetooth/hci_event.c:7847
|  hci_rx_work+0x370/0x890 net/bluetooth/hci_core.c:4040
|  process_one_work kernel/workqueue.c:3326 [inline]
|  process_scheduled_works+0x7c8/0xfb0 kernel/workqueue.c:3409
|  worker_thread+0x8a9/0xcf0 kernel/workqueue.c:3490
|  kthread+0x346/0x430 kernel/kthread.c:436
|  ret_from_fork+0x1a3/0x470 arch/x86/kernel/process.c:158
|  hci_conn_hash_flush+0x101/0x1f0 net/bluetooth/hci_conn.c:2736
|  hci_dev_close_sync+0x889/0xde0 net/bluetooth/hci_sync.c:5405
|  hci_dev_do_close net/bluetooth/hci_core.c:502 [inline]
|  hci_unregister_dev+0x1f7/0x370 net/bluetooth/hci_core.c:2679
|  vhci_release+0x12a/0x180 drivers/bluetooth/hci_vhci.c:690
|  __fput+0x369/0x890 fs/file_table.c:510
|  task_work_run+0x160/0x1d0 kernel/task_work.c:233
|  get_signal+0xf5b/0x1120 kernel/signal.c:2810
|  arch_do_signal_or_restart+0x4d/0x600 arch/x86/kernel/signal.c:337
|  __exit_to_user_mode_loop kernel/entry/common.c:64 [inline]
|  exit_to_user_mode_loop+0x85/0x510 kernel/entry/common.c:98
|  __exit_to_user_mode_prepare include/linux/irq-entry-common.h:207 [i
e]
|  syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:
[inline]
|  syscall_exit_to_user_mode include/linux/entry-common.h:318 [inline]
|  do_syscall_64+0x263/0x3d0 arch/x86/entry/syscall_64.c:100
|  entry_SYSCALL_64_after_hwframe+0x77/0x7f
|
| Last potentially related work creation:
|  hci_connect_cfm include/net/bluetooth/hci_core.h:2139 [inline]
|  hci_remote_features_evt+0x52f/0x9f0 net/bluetooth/hci_event.c:3760
|  hci_event_func net/bluetooth/hci_event.c:7796 [inline]
|  hci_event_packet+0x561/0xa70 net/bluetooth/hci_event.c:7847
|  hci_rx_work+0x370/0x890 net/bluetooth/hci_core.c:4040
|  process_one_work kernel/workqueue.c:3326 [inline]
|  process_scheduled_works+0x7c8/0xfb0 kernel/workqueue.c:3409
|  worker_thread+0x8a9/0xcf0 kernel/workqueue.c:3490
|  kthread+0x346/0x430 kernel/kthread.c:436
|  ret_from_fork+0x1a3/0x470 arch/x86/kernel/process.c:158
|  ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
|
| The buggy address belongs to the object at ffff8881298d9400
|  which belongs to the cache kmalloc-512 of size 512
| The buggy address is located 336 bytes inside of
|  freed 512-byte region [ffff8881298d9400, ffff8881298d9600)

Fix it by having struct l2cap_chan hold a reference to l2cap_conn
(conn_ref) when the channel is added to the connection, and releasing it
in the channel destructor. This ensures the connection remains alive as
long as the channel exists. While conn and conn_ref point to the same
object, conn being NULL indicates it being torn down, while conn_ref's
only purpose is to associate its lifetime with the parent channel.

Fixes: 75780ca4c6a8 ("Bluetooth: L2CAP: use chan timer to close channe
ls in cleanup_listen()")
Cc: <stable@vger.kernel.org>
Cc: Siwei Zhang <oss@fourdim.xyz>
Cc: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
Assisted-by: Gemini:gemini-3.1-pro-preview
Reported-by: https://sashiko.dev/#/patchset/20260521021249.3258069-1-o
ss%40fourdim.xyz
Signed-off-by: Marco Elver <elver@google.com>
---
 include/net/bluetooth/l2cap.h |  1 +
 net/bluetooth/l2cap_core.c    | 15 +++++++++++++--
 2 files changed, 14 insertions(+), 2 deletions(-)

diff --git a/include/net/bluetooth/l2cap.h b/include/net/bluetooth/l2cap.h
index e0a1f2293679..de3673149deb 100644
--- a/include/net/bluetooth/l2cap.h
+++ b/include/net/bluetooth/l2cap.h
@@ -514,6 +514,7 @@ struct l2cap_seq_list {
 
 struct l2cap_chan {
 	struct l2cap_conn	*conn;
+	struct l2cap_conn	*conn_ref;
 	struct kref	kref;
 	atomic_t	nesting;
 
diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
index c4ccfbda9d78..7f331a31b723 100644
--- a/net/bluetooth/l2cap_core.c
+++ b/net/bluetooth/l2cap_core.c
@@ -422,6 +422,9 @@ static void l2cap_chan_timeout(struct work_struct *work)
 	 */
 	l2cap_chan_lock(chan);
 
+	if (!chan->conn)
+		goto unlock;
+
 	if (chan->state == BT_CONNECTED || chan->state == BT_CONFIG)
 		reason = ECONNREFUSED;
 	else if (chan->state == BT_CONNECT &&
@@ -434,10 +437,10 @@ static void l2cap_chan_timeout(struct work_struct *work)
 
 	chan->ops->close(chan);
 
+unlock:
 	l2cap_chan_unlock(chan);
-	l2cap_chan_put(chan);
-
 	mutex_unlock(&conn->lock);
+	l2cap_chan_put(chan);
 }
 
 struct l2cap_chan *l2cap_chan_create(void)
@@ -490,6 +493,9 @@ static void l2cap_chan_destroy(struct kref *kref)
 	list_del(&chan->global_l);
 	write_unlock(&chan_list_lock);
 
+	if (chan->conn_ref)
+		l2cap_conn_put(chan->conn_ref);
+
 	kfree(chan);
 }
 
@@ -594,6 +600,7 @@ void __l2cap_chan_add(struct l2cap_conn *conn, struct l2cap_chan *chan)
 	conn->disc_reason = HCI_ERROR_REMOTE_USER_TERM;
 
 	chan->conn = conn;
+	chan->conn_ref = l2cap_conn_get(conn);
 
 	switch (chan->chan_type) {
 	case L2CAP_CHAN_CONN_ORIENTED:
@@ -3160,12 +3167,16 @@ static void l2cap_ack_timeout(struct work_struct *work)
 
 	l2cap_chan_lock(chan);
 
+	if (!chan->conn)
+		goto unlock;
+
 	frames_to_ack = __seq_offset(chan, chan->buffer_seq,
 				     chan->last_acked_seq);
 
 	if (frames_to_ack)
 		l2cap_send_rr_or_rnr(chan, 0);
 
+unlock:
 	l2cap_chan_unlock(chan);
 	l2cap_chan_put(chan);
 }
-- 
2.54.0.1032.g2f8565e1d1-goog

^ permalink raw reply related	[flat|nested] 8+ messages in thread

* Re: [PATCH] Bluetooth: L2CAP: Fix UAF in l2cap_chan_timeout
  2026-06-05 10:18         ` Marco Elver
@ 2026-06-05 13:53           ` Luiz Augusto von Dentz
  0 siblings, 0 replies; 8+ messages in thread
From: Luiz Augusto von Dentz @ 2026-06-05 13:53 UTC (permalink / raw)
  To: Marco Elver
  Cc: Marcel Holtmann, linux-bluetooth, linux-kernel, kasan-dev, stable,
	Siwei Zhang, Luiz Augusto von Dentz

Hi Marco,

On Fri, Jun 5, 2026 at 6:18 AM Marco Elver <elver@google.com> wrote:
>
> On Thu, Jun 04, 2026 at 10:10AM -0400, Luiz Augusto von Dentz wrote:
> > Hi Marco,
> >
> > On Thu, Jun 4, 2026 at 8:45 AM Marco Elver <elver@google.com> wrote:
> > >
> > > On Wed, Jun 03, 2026 at 01:31PM -0400, Luiz Augusto von Dentz wrote:
> > > > Hi Marco,
> > > >
> > > > On Wed, Jun 3, 2026 at 9:16 AM Marco Elver <elver@google.com> wrote:
> > > > >
> > > > > On Wed, 3 Jun 2026 at 14:31, Marco Elver <elver@google.com> wrote:
> > > > > >
> > > > > > l2cap_chan_timeout() accesses chan->conn without holding a reference to
> > > > > > the connection object. If l2cap_conn_del() races and tears down the
> > > > > > connection while the timer is waiting for locks, it can result in a
> > > > > > use-after-free when the timer wakes up and attempts to acquire
> > > > > > conn->lock:
> > > > > >
> > > > > > | BUG: KASAN: slab-use-after-free in instrument_atomic_read_write include/linux/instrumented.h:112 [inline]
> > > > > > | BUG: KASAN: slab-use-after-free in atomic_long_try_cmpxchg_acquire include/linux/atomic/atomic-instrumented.h:4456 [inline]
> > > > > > | BUG: KASAN: slab-use-after-free in __mutex_trylock_fast kernel/locking/mutex.c:161 [inline]
> > > > > > | BUG: KASAN: slab-use-after-free in mutex_lock+0x4f/0xa0 kernel/locking/mutex.c:318
> > > > > > | Write of size 8 at addr ffff8881298d9550 by task kworker/2:1/83
> > > > > > |
> > > > > > | CPU: 2 UID: 0 PID: 83 Comm: kworker/2:1 Not tainted 7.1.0-rc6-next-20260601-dirty #6 PREEMPT(full)
> > > > > > | Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.17.0-debian-1.17.0-1 04/01/2014
> > > > > > | Workqueue: events l2cap_chan_timeout
> > > > > > | Call Trace:
> > > > > > |  <TASK>
> > > > > > |  instrument_atomic_read_write include/linux/instrumented.h:112 [inline]
> > > > > > |  atomic_long_try_cmpxchg_acquire include/linux/atomic/atomic-instrumented.h:4456 [inline]
> > > > > > |  __mutex_trylock_fast kernel/locking/mutex.c:161 [inline]
> > > > > > |  mutex_lock+0x4f/0xa0 kernel/locking/mutex.c:318
> > > > > > |  l2cap_chan_timeout+0x5d/0x1b0 net/bluetooth/l2cap_core.c:422
> > > > > > |  process_one_work kernel/workqueue.c:3326 [inline]
> > > > > > |  process_scheduled_works+0x7c8/0xfb0 kernel/workqueue.c:3409
> > > > > > |  worker_thread+0x8a9/0xcf0 kernel/workqueue.c:3490
> > > > > > |  kthread+0x346/0x430 kernel/kthread.c:436
> > > > > > |  ret_from_fork+0x1a3/0x470 arch/x86/kernel/process.c:158
> > > > > > |  ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
> > > > > > |  </TASK>
> > > > > > |
> > > > > > | Allocated by task 320:
> > > > > > |  l2cap_conn_add+0xa7/0x820 net/bluetooth/l2cap_core.c:7075
> > > > > > |  l2cap_connect_cfm+0xdb/0xd70 net/bluetooth/l2cap_core.c:7452
> > > > > > |  hci_connect_cfm include/net/bluetooth/hci_core.h:2139 [inline]
> > > > > > |  hci_remote_features_evt+0x52f/0x9f0 net/bluetooth/hci_event.c:3760
> > > > > > |  hci_event_func net/bluetooth/hci_event.c:7796 [inline]
> > > > > > |  hci_event_packet+0x561/0xa70 net/bluetooth/hci_event.c:7847
> > > > > > |  hci_rx_work+0x370/0x890 net/bluetooth/hci_core.c:4040
> > > > > > |  process_one_work kernel/workqueue.c:3326 [inline]
> > > > > > |  process_scheduled_works+0x7c8/0xfb0 kernel/workqueue.c:3409
> > > > > > |  worker_thread+0x8a9/0xcf0 kernel/workqueue.c:3490
> > > > > > |  kthread+0x346/0x430 kernel/kthread.c:436
> > > > > > |  ret_from_fork+0x1a3/0x470 arch/x86/kernel/process.c:158
> > > > > > |  ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
> > > > > > |
> > > > > > | Freed by task 322:
> > > > > > |  hci_disconn_cfm include/net/bluetooth/hci_core.h:2154 [inline]
> > > > > > |  hci_conn_hash_flush+0x101/0x1f0 net/bluetooth/hci_conn.c:2736
> > > > > > |  hci_dev_close_sync+0x889/0xde0 net/bluetooth/hci_sync.c:5405
> > > > > > |  hci_dev_do_close net/bluetooth/hci_core.c:502 [inline]
> > > > > > |  hci_unregister_dev+0x1f7/0x370 net/bluetooth/hci_core.c:2679
> > > > > > |  vhci_release+0x12a/0x180 drivers/bluetooth/hci_vhci.c:690
> > > > > > |  __fput+0x369/0x890 fs/file_table.c:510
> > > > > > |  task_work_run+0x160/0x1d0 kernel/task_work.c:233
> > > > > > |  get_signal+0xf5b/0x1120 kernel/signal.c:2810
> > > > > > |  arch_do_signal_or_restart+0x4d/0x600 arch/x86/kernel/signal.c:337
> > > > > > |  __exit_to_user_mode_loop kernel/entry/common.c:64 [inline]
> > > > > > |  exit_to_user_mode_loop+0x85/0x510 kernel/entry/common.c:98
> > > > > > |  __exit_to_user_mode_prepare include/linux/irq-entry-common.h:207 [inline]
> > > > > > |  syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:230 [inline]
> > > > > > |  syscall_exit_to_user_mode include/linux/entry-common.h:318 [inline]
> > > > > > |  do_syscall_64+0x263/0x3d0 arch/x86/entry/syscall_64.c:100
> > > > > > |  entry_SYSCALL_64_after_hwframe+0x77/0x7f
> > > > > > |
> > > > > > | Last potentially related work creation:
> > > > > > |  hci_connect_cfm include/net/bluetooth/hci_core.h:2139 [inline]
> > > > > > |  hci_remote_features_evt+0x52f/0x9f0 net/bluetooth/hci_event.c:3760
> > > > > > |  hci_event_func net/bluetooth/hci_event.c:7796 [inline]
> > > > > > |  hci_event_packet+0x561/0xa70 net/bluetooth/hci_event.c:7847
> > > > > > |  hci_rx_work+0x370/0x890 net/bluetooth/hci_core.c:4040
> > > > > > |  process_one_work kernel/workqueue.c:3326 [inline]
> > > > > > |  process_scheduled_works+0x7c8/0xfb0 kernel/workqueue.c:3409
> > > > > > |  worker_thread+0x8a9/0xcf0 kernel/workqueue.c:3490
> > > > > > |  kthread+0x346/0x430 kernel/kthread.c:436
> > > > > > |  ret_from_fork+0x1a3/0x470 arch/x86/kernel/process.c:158
> > > > > > |  ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
> > > > > > |
> > > > > > | The buggy address belongs to the object at ffff8881298d9400
> > > > > > |  which belongs to the cache kmalloc-512 of size 512
> > > > > > | The buggy address is located 336 bytes inside of
> > > > > > |  freed 512-byte region [ffff8881298d9400, ffff8881298d9600)
> > > > > >
> > > > > > Fix it by holding a reference to the connection when the channel timer
> > > > > > is scheduled, and releasing it when the timer is either canceled or
> > > > > > executes to completion.
> > > > > >
> > > > > > Since l2cap_chan_del() nullifies chan->conn to disassociate the channel
> > > > > > during teardown, the timer handler might read NULL from chan->conn even
> > > > > > if it held a reference. To address this, introduce a `timer_conn` field
> > > > > > to `struct l2cap_chan` to store the connection pointer associated with
> > > > > > the active timer. The timer handler uses this field to acquire locks and
> > > > > > release the connection reference, and skips channel closing operations
> > > > > > if chan->conn has already been nullified by teardown.
> > > > > >
> > > > > > Fixes: 75780ca4c6a8 ("Bluetooth: L2CAP: use chan timer to close channels in cleanup_listen()")
> > > > > > Cc: <stable@vger.kernel.org>
> > > > > > Cc: Siwei Zhang <oss@fourdim.xyz>
> > > > > > Cc: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
> > > > > > Assisted-by: Gemini:gemini-3.1-pro-preview
> > > > > > Reported-by: https://sashiko.dev/#/patchset/20260521021249.3258069-1-oss%40fourdim.xyz
> > > > > > Signed-off-by: Marco Elver <elver@google.com>
> > > > >
> > > > > Sigh, Sashiko points out more problems here:
> > > > > https://sashiko.dev/#/patchset/20260603123111.2334409-1-elver%40google.com
> > > > >
> > > > > > Can this lockless read of chan->timer_conn cause a use-after-free or double
> > > > > > free if another thread re-arms the timer concurrently?
> > > > >
> > > > > I haven't analyzed this further yet, so consider this patch a
> > > > > bug-report-only. If anyone finds a better fix sooner, please go ahead.
> > > >
> > > > I was thinking or something like the following:
> > >
> > > I tested that and my repro didn't trigger the UAF here, but I still
> > > think it has the same fundamental issue:
> > >
> > > If the timer worker is preempted immediately after reading chan->conn
> > > but before entering l2cap_conn_hold_unless_zero(), l2cap_conn_del() can
> > > complete concurrently.
> > >
> > > When the timer worker resumes, l2cap_conn_hold_unless_zero(conn) will
> > > attempt to read conn->ref that has already been freed, resulting in
> > > another UAF.
> >
> > I see. The window is very narrow but it is perhaps still triggerable
> > somehow. The only thing that comes to mind is that we would need to
> > take a reference of l2cap_conn with the likes of l2cap_set_timer then,
> > which means l2cap_chan_timeout needs to drop not only l2cap_chan but
> > also l2cap_conn when done, otherwise there will always be the risk of
> > l2cap_conn_del running while l2cap_chan_timeout is pending.
>
> What if we tie conn's lifetime to chan? I see that 'conn' being
> NULL/non-NULL is also used as a presence/not-present marker, but we
> could add an explicit conn_ref?
>
> ------ >8 ------
>
> From: Marco Elver <elver@google.com>
> Date: Wed, 3 Jun 2026 18:24:56 +0200
> Subject: [PATCH] Bluetooth: L2CAP: Fix UAF in channel timeout by holding conn
>  ref
>
> l2cap_chan_timeout() runs asynchronously and accesses chan->conn. If
> the connection is torn down while the timer is running or pending,
> chan->conn can be freed, leading to a use-after-free when the timer
> worker attempts to lock conn->lock:
>
> | BUG: KASAN: slab-use-after-free in instrument_atomic_read_write include/linux/instrumented.h:112 [inline]
> | BUG: KASAN: slab-use-after-free in atomic_long_try_cmpxchg_acquire include/linux/atomic/atomic-instrumented.h:4456 [inline]
> | BUG: KASAN: slab-use-after-free in __mutex_trylock_fast kernel/locking/mutex.c:161 [inline]
> | BUG: KASAN: slab-use-after-free in mutex_lock+0x4f/0xa0 kernel/locking/mutex.c:318
> | Write of size 8 at addr ffff8881298d9550 by task kworker/2:1/83
> |
> | CPU: 2 UID: 0 PID: 83 Comm: kworker/2:1 Not tainted 7.1.0-rc6-next-20260601-dirty #6 PREEMPT(full)
> | Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.17.0-debian-1.17.0-1 04/01/2014
> | Workqueue: events l2cap_chan_timeout
> | Call Trace:
> |  <TASK>
> |  instrument_atomic_read_write include/linux/instrumented.h:112 [inline]
> |  atomic_long_try_cmpxchg_acquire include/linux/atomic/atomic-instrumented.h:4456 [inline]
> |  __mutex_trylock_fast kernel/locking/mutex.c:161 [inline]
> |  mutex_lock+0x4f/0xa0 kernel/locking/mutex.c:318
> |  l2cap_chan_timeout+0x5d/0x1b0 net/bluetooth/l2cap_core.c:422
> |  process_one_work kernel/workqueue.c:3326 [inline]
> |  process_scheduled_works+0x7c8/0xfb0 kernel/workqueue.c:3409
> |  worker_thread+0x8a9/0xcf0 kernel/workqueue.c:3490
> |  kthread+0x346/0x430 kernel/kthread.c:436
> |  ret_from_fork+0x1a3/0x470 arch/x86/kernel/process.c:158
> |  ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
> |  </TASK>
> |
> | Allocated by task 320:
> |  l2cap_conn_add+0xa7/0x820 net/bluetooth/l2cap_core.c:7075
> |  l2cap_connect_cfm+0xdb/0xd70 net/bluetooth/l2cap_core.c:7452
> |  hci_connect_cfm include/net/bluetooth/hci_core.h:2139 [inline]
> |  hci_remote_features_evt+0x52f/0x9f0 net/bluetooth/hci_event.c:3760
> |  hci_event_func net/bluetooth/hci_event.c:7796 [inline]
> |  hci_event_packet+0x561/0xa70 net/bluetooth/hci_event.c:7847
> |  hci_rx_work+0x370/0x890 net/bluetooth/hci_core.c:4040
> |  process_one_work kernel/workqueue.c:3326 [inline]
> |  process_scheduled_works+0x7c8/0xfb0 kernel/workqueue.c:3409
> |  worker_thread+0x8a9/0xcf0 kernel/workqueue.c:3490
> |  kthread+0x346/0x430 kernel/kthread.c:436
> |  ret_from_fork+0x1a3/0x470 arch/x86/kernel/process.c:158
> |  ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
> |
> | Freed by task 322:
> |  hci_disconn_cfm include/net/bluetooth/hci_core.h:2154 [inline]
> |  hci_conn_hash_flush+0x101/0x1f0 net/bluetooth/hci_conn.c:2736
> |  hci_dev_close_sync+0x889/0xde0 net/bluetooth/hci_sync.c:5405
> |  hci_dev_do_close net/bluetooth/hci_core.c:502 [inline]
> |  hci_unregister_dev+0x1f7/0x370 net/bluetooth/hci_core.c:2679
> |  vhci_release+0x12a/0x180 drivers/bluetooth/hci_vhci.c:690
> |  __fput+0x369/0x890 fs/file_table.c:510
> |  task_work_run+0x160/0x1d0 kernel/task_work.c:233
> |  get_signal+0xf5b/0x1120 kernel/signal.c:2810
> |  arch_do_signal_or_restart+0x4d/0x600 arch/x86/kernel/signal.c:337
> |  __exit_to_user_mode_loop kernel/entry/common.c:64 [inline]
> |  exit_to_user_mode_loop+0x85/0x510 kernel/entry/common.c:98
> |  __exit_to_user_mode_prepare include/linux/irq-entry-common.h:207 [i
> e]
> |  syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:
> [inline]
> |  syscall_exit_to_user_mode include/linux/entry-common.h:318 [inline]
> |  do_syscall_64+0x263/0x3d0 arch/x86/entry/syscall_64.c:100
> |  entry_SYSCALL_64_after_hwframe+0x77/0x7f
> |
> | Last potentially related work creation:
> |  hci_connect_cfm include/net/bluetooth/hci_core.h:2139 [inline]
> |  hci_remote_features_evt+0x52f/0x9f0 net/bluetooth/hci_event.c:3760
> |  hci_event_func net/bluetooth/hci_event.c:7796 [inline]
> |  hci_event_packet+0x561/0xa70 net/bluetooth/hci_event.c:7847
> |  hci_rx_work+0x370/0x890 net/bluetooth/hci_core.c:4040
> |  process_one_work kernel/workqueue.c:3326 [inline]
> |  process_scheduled_works+0x7c8/0xfb0 kernel/workqueue.c:3409
> |  worker_thread+0x8a9/0xcf0 kernel/workqueue.c:3490
> |  kthread+0x346/0x430 kernel/kthread.c:436
> |  ret_from_fork+0x1a3/0x470 arch/x86/kernel/process.c:158
> |  hci_conn_hash_flush+0x101/0x1f0 net/bluetooth/hci_conn.c:2736
> |  hci_dev_close_sync+0x889/0xde0 net/bluetooth/hci_sync.c:5405
> |  hci_dev_do_close net/bluetooth/hci_core.c:502 [inline]
> |  hci_unregister_dev+0x1f7/0x370 net/bluetooth/hci_core.c:2679
> |  vhci_release+0x12a/0x180 drivers/bluetooth/hci_vhci.c:690
> |  __fput+0x369/0x890 fs/file_table.c:510
> |  task_work_run+0x160/0x1d0 kernel/task_work.c:233
> |  get_signal+0xf5b/0x1120 kernel/signal.c:2810
> |  arch_do_signal_or_restart+0x4d/0x600 arch/x86/kernel/signal.c:337
> |  __exit_to_user_mode_loop kernel/entry/common.c:64 [inline]
> |  exit_to_user_mode_loop+0x85/0x510 kernel/entry/common.c:98
> |  __exit_to_user_mode_prepare include/linux/irq-entry-common.h:207 [i
> e]
> |  syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:
> [inline]
> |  syscall_exit_to_user_mode include/linux/entry-common.h:318 [inline]
> |  do_syscall_64+0x263/0x3d0 arch/x86/entry/syscall_64.c:100
> |  entry_SYSCALL_64_after_hwframe+0x77/0x7f
> |
> | Last potentially related work creation:
> |  hci_connect_cfm include/net/bluetooth/hci_core.h:2139 [inline]
> |  hci_remote_features_evt+0x52f/0x9f0 net/bluetooth/hci_event.c:3760
> |  hci_event_func net/bluetooth/hci_event.c:7796 [inline]
> |  hci_event_packet+0x561/0xa70 net/bluetooth/hci_event.c:7847
> |  hci_rx_work+0x370/0x890 net/bluetooth/hci_core.c:4040
> |  process_one_work kernel/workqueue.c:3326 [inline]
> |  process_scheduled_works+0x7c8/0xfb0 kernel/workqueue.c:3409
> |  worker_thread+0x8a9/0xcf0 kernel/workqueue.c:3490
> |  kthread+0x346/0x430 kernel/kthread.c:436
> |  ret_from_fork+0x1a3/0x470 arch/x86/kernel/process.c:158
> |  ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
> |
> | The buggy address belongs to the object at ffff8881298d9400
> |  which belongs to the cache kmalloc-512 of size 512
> | The buggy address is located 336 bytes inside of
> |  freed 512-byte region [ffff8881298d9400, ffff8881298d9600)
>
> Fix it by having struct l2cap_chan hold a reference to l2cap_conn
> (conn_ref) when the channel is added to the connection, and releasing it
> in the channel destructor. This ensures the connection remains alive as
> long as the channel exists. While conn and conn_ref point to the same
> object, conn being NULL indicates it being torn down, while conn_ref's
> only purpose is to associate its lifetime with the parent channel.
>
> Fixes: 75780ca4c6a8 ("Bluetooth: L2CAP: use chan timer to close channe
> ls in cleanup_listen()")
> Cc: <stable@vger.kernel.org>
> Cc: Siwei Zhang <oss@fourdim.xyz>
> Cc: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
> Assisted-by: Gemini:gemini-3.1-pro-preview
> Reported-by: https://sashiko.dev/#/patchset/20260521021249.3258069-1-o
> ss%40fourdim.xyz
> Signed-off-by: Marco Elver <elver@google.com>
> ---
>  include/net/bluetooth/l2cap.h |  1 +
>  net/bluetooth/l2cap_core.c    | 15 +++++++++++++--
>  2 files changed, 14 insertions(+), 2 deletions(-)
>
> diff --git a/include/net/bluetooth/l2cap.h b/include/net/bluetooth/l2cap.h
> index e0a1f2293679..de3673149deb 100644
> --- a/include/net/bluetooth/l2cap.h
> +++ b/include/net/bluetooth/l2cap.h
> @@ -514,6 +514,7 @@ struct l2cap_seq_list {
>
>  struct l2cap_chan {
>         struct l2cap_conn       *conn;
> +       struct l2cap_conn       *conn_ref;
>         struct kref     kref;
>         atomic_t        nesting;
>
> diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
> index c4ccfbda9d78..7f331a31b723 100644
> --- a/net/bluetooth/l2cap_core.c
> +++ b/net/bluetooth/l2cap_core.c
> @@ -422,6 +422,9 @@ static void l2cap_chan_timeout(struct work_struct *work)
>          */
>         l2cap_chan_lock(chan);
>
> +       if (!chan->conn)
> +               goto unlock;
> +
>         if (chan->state == BT_CONNECTED || chan->state == BT_CONFIG)
>                 reason = ECONNREFUSED;
>         else if (chan->state == BT_CONNECT &&
> @@ -434,10 +437,10 @@ static void l2cap_chan_timeout(struct work_struct *work)
>
>         chan->ops->close(chan);
>
> +unlock:
>         l2cap_chan_unlock(chan);
> -       l2cap_chan_put(chan);
> -
>         mutex_unlock(&conn->lock);
> +       l2cap_chan_put(chan);
>  }
>
>  struct l2cap_chan *l2cap_chan_create(void)
> @@ -490,6 +493,9 @@ static void l2cap_chan_destroy(struct kref *kref)
>         list_del(&chan->global_l);
>         write_unlock(&chan_list_lock);
>
> +       if (chan->conn_ref)
> +               l2cap_conn_put(chan->conn_ref);
> +
>         kfree(chan);
>  }
>
> @@ -594,6 +600,7 @@ void __l2cap_chan_add(struct l2cap_conn *conn, struct l2cap_chan *chan)
>         conn->disc_reason = HCI_ERROR_REMOTE_USER_TERM;
>
>         chan->conn = conn;
> +       chan->conn_ref = l2cap_conn_get(conn);
>
>         switch (chan->chan_type) {
>         case L2CAP_CHAN_CONN_ORIENTED:
> @@ -3160,12 +3167,16 @@ static void l2cap_ack_timeout(struct work_struct *work)
>
>         l2cap_chan_lock(chan);
>
> +       if (!chan->conn)
> +               goto unlock;
> +
>         frames_to_ack = __seq_offset(chan, chan->buffer_seq,
>                                      chan->last_acked_seq);
>
>         if (frames_to_ack)
>                 l2cap_send_rr_or_rnr(chan, 0);
>
> +unlock:
>         l2cap_chan_unlock(chan);
>         l2cap_chan_put(chan);
>  }
> --
> 2.54.0.1032.g2f8565e1d1-goog

Looks good, please a spim a patch since just pasting like the above
doesn't seem to trigger PW and CI/CD run.

-- 
Luiz Augusto von Dentz

^ permalink raw reply	[flat|nested] 8+ messages in thread

end of thread, other threads:[~2026-06-05 13:53 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-03 12:27 [PATCH] Bluetooth: L2CAP: Fix UAF in l2cap_chan_timeout Marco Elver
2026-06-03 13:15 ` Marco Elver
2026-06-03 17:31   ` Luiz Augusto von Dentz
2026-06-04 12:45     ` Marco Elver
2026-06-04 14:10       ` Luiz Augusto von Dentz
2026-06-05 10:18         ` Marco Elver
2026-06-05 13:53           ` Luiz Augusto von Dentz
2026-06-03 17:37 ` bluez.test.bot

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox