Re: [PATCH] Bluetooth: L2CAP: Fix UAF in l2cap_chan_timeout
From: Luiz Augusto von Dentz
Date: Fri Jun 05 2026 - 09:53:56 EST
Hi Marco,
On Fri, Jun 5, 2026 at 6:18 AM Marco Elver <elver@xxxxxxxxxx> 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@xxxxxxxxxx> 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@xxxxxxxxxx> wrote:
> > > > >
> > > > > On Wed, 3 Jun 2026 at 14:31, Marco Elver <elver@xxxxxxxxxx> 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@xxxxxxxxxxxxxxx>
> > > > > > Cc: Siwei Zhang <oss@xxxxxxxxxxx>
> > > > > > Cc: Luiz Augusto von Dentz <luiz.von.dentz@xxxxxxxxx>
> > > > > > 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@xxxxxxxxxx>
> > > > >
> > > > > 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@xxxxxxxxxx>
> 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@xxxxxxxxxxxxxxx>
> Cc: Siwei Zhang <oss@xxxxxxxxxxx>
> Cc: Luiz Augusto von Dentz <luiz.von.dentz@xxxxxxxxx>
> 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@xxxxxxxxxx>
> ---
> 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