Linux bluetooth development
 help / color / mirror / Atom feed
From: Ali Firas <alishmery18@gmail.com>
To: luiz.dentz@gmail.com, marcel@holtmann.org
Cc: linux-bluetooth@vger.kernel.org, linux-kernel@vger.kernel.org,
	Ali Firas <alishmery18@gmail.com>
Subject: [PATCH] Bluetooth: SCO: avoid deadlock in sco_conn_free()
Date: Thu, 20 Aug 2026 20:46:33 +0300	[thread overview]
Message-ID: <20260820174633.1570398-1-alishmery18@gmail.com> (raw)

sco_conn_free() (the sco_conn kref release) synchronously cancels the
connection timeout work with disable_delayed_work_sync() while the caller
holds lock_sock(sk).  The timeout work, sco_sock_timeout(), itself takes
lock_sock(sk).  When a teardown path drops the connection's last reference
under the socket lock -- e.g. sco_connect_cfm() -> hci_conn_failed() on a
failed link, or a Disconnection Complete via sco_conn_del() -- the ensuing
sco_conn_free() waits for a sco_sock_timeout() that is blocked on the same
socket lock: an AB/BA deadlock.

On a PROVE_LOCKING kernel, lockdep reports:

  WARNING: possible circular locking dependency detected
  kworker/u9:1 is trying to acquire lock:
   ((work_completion)(&(&conn->timeout_work)->work)), at: __flush_work
  but task is already holding lock:
   (sk_lock-AF_BLUETOOTH-BTPROTO_SCO), at: sco_conn_del
  -> #1 (sk_lock-AF_BLUETOOTH-BTPROTO_SCO):
         lock_sock_nested ; sco_sock_timeout ; process_one_work
  -> #0 ((work_completion)(&conn->timeout_work)):
         __flush_work ; disable_delayed_work_sync ; sco_conn_put ;
         sco_chan_del ; sco_conn_del ; sco_connect_cfm

The bug is reachable by an unprivileged local process: opening an
AF_BLUETOOTH/BTPROTO_SCO socket needs no capability, and a short
SO_SNDTIMEO makes the timeout fire during connection setup/teardown.
(syzbot logged the same chain in 2022, closed obsolete with no reproducer.)

The identical problem in the ISO transport was fixed in commit 200fa1629c57
("Bluetooth: ISO: avoid deadlocks in iso_sock_timeout") by moving the
timeout work out of the refcounted, freed-under-lock iso_conn and into
iso_pinfo, so its lifetime follows the socket, and by cancelling it outside
lock_sock().  Do the same for SCO: move timeout_work into struct sco_pinfo,
drop the sync cancel from sco_conn_free(), and add sco_sock_disable_timer()
-- which asserts the socket lock is NOT held -- on the teardown paths
(sco_conn_del(), sco_sock_kill(), sco_sock_close()).

Note that merely making the cancel asynchronous is not sufficient: with
disable_delayed_work() the work can still run against freed memory, which
KASAN reports as a use-after-free in sco_conn_hold_unless_zero().  The
timer's lifetime has to follow the socket, as it now does for ISO.

Reproducer available on request.

Fixes: e6720779ae61 ("Bluetooth: SCO: Use kref to track lifetime of sco_conn")
Assisted-by: Claude:claude-opus-4-6 [claude-code]
Signed-off-by: Ali Firas <alishmery18@gmail.com>
---
 net/bluetooth/sco.c | 76 +++++++++++++++++----------------------------
 1 file changed, 28 insertions(+), 48 deletions(-)

diff --git a/net/bluetooth/sco.c b/net/bluetooth/sco.c
index 3d4362a09df4..c87ad72af783 100644
--- a/net/bluetooth/sco.c
+++ b/net/bluetooth/sco.c
@@ -46,8 +46,6 @@ struct sco_conn {
 	spinlock_t	lock;
 	struct sock	*sk;
 
-	struct delayed_work	timeout_work;
-
 	unsigned int    mtu;
 	struct kref	ref;
 };
@@ -69,6 +67,7 @@ struct sco_pinfo {
 	__u16		setting;
 	struct bt_codec codec;
 	struct sco_conn	*conn;
+	struct delayed_work	timeout_work;
 };
 
 /* ---- SCO timers ---- */
@@ -89,9 +88,6 @@ static void sco_conn_free(struct kref *ref)
 		hci_conn_drop(conn->hcon);
 	}
 
-	/* Ensure no more work items will run since hci_conn has been dropped */
-	disable_delayed_work_sync(&conn->timeout_work);
-
 	kfree(conn);
 }
 
@@ -138,53 +134,45 @@ static struct sock *sco_sock_hold(struct sco_conn *conn)
 
 static void sco_sock_timeout(struct work_struct *work)
 {
-	struct sco_conn *conn = container_of(work, struct sco_conn,
-					     timeout_work.work);
-	struct sock *sk;
-
-	conn = sco_conn_hold_unless_zero(conn);
-	if (!conn)
-		return;
-
-	sco_conn_lock(conn);
-	if (!conn->hcon) {
-		sco_conn_unlock(conn);
-		sco_conn_put(conn);
-		return;
-	}
-	sk = sco_sock_hold(conn);
-	sco_conn_unlock(conn);
-	sco_conn_put(conn);
-
-	if (!sk)
-		return;
+	struct sco_pinfo *pi = container_of(work, struct sco_pinfo,
+					    timeout_work.work);
+	struct sock *sk = &pi->bt.sk;
 
 	BT_DBG("sock %p state %d", sk, sk->sk_state);
 
 	lock_sock(sk);
-	sk->sk_err = ETIMEDOUT;
-	sk->sk_state_change(sk);
+	if (!sock_flag(sk, SOCK_ZAPPED)) {
+		sk->sk_err = ETIMEDOUT;
+		sk->sk_state_change(sk);
+	}
 	release_sock(sk);
-	sock_put(sk);
 }
 
 static void sco_sock_set_timer(struct sock *sk, long timeout)
 {
+	lockdep_assert(lockdep_sock_is_held(sk));
+
+	cancel_delayed_work(&sco_pi(sk)->timeout_work);
+
 	if (!sco_pi(sk)->conn)
 		return;
 
 	BT_DBG("sock %p state %d timeout %ld", sk, sk->sk_state, timeout);
-	cancel_delayed_work(&sco_pi(sk)->conn->timeout_work);
-	schedule_delayed_work(&sco_pi(sk)->conn->timeout_work, timeout);
+	schedule_delayed_work(&sco_pi(sk)->timeout_work, timeout);
 }
 
 static void sco_sock_clear_timer(struct sock *sk)
 {
-	if (!sco_pi(sk)->conn)
-		return;
+	BT_DBG("sock %p state %d", sk, sk->sk_state);
+	cancel_delayed_work(&sco_pi(sk)->timeout_work);
+}
+
+static void sco_sock_disable_timer(struct sock *sk)
+{
+	lockdep_assert(!lockdep_sock_is_held(sk));
 
 	BT_DBG("sock %p state %d", sk, sk->sk_state);
-	cancel_delayed_work(&sco_pi(sk)->conn->timeout_work);
+	disable_delayed_work_sync(&sco_pi(sk)->timeout_work);
 }
 
 /* ---- SCO connections ---- */
@@ -214,7 +202,6 @@ static struct sco_conn *sco_conn_add(struct hci_conn *hcon)
 
 	kref_init(&conn->ref);
 	spin_lock_init(&conn->lock);
-	INIT_DELAYED_WORK(&conn->timeout_work, sco_sock_timeout);
 
 	hcon->sco_data = conn;
 	conn->hcon = hcon;
@@ -274,9 +261,10 @@ static void sco_conn_del(struct hci_conn *hcon, int err)
 	if (!sk)
 		return;
 
+	sco_sock_disable_timer(sk);
+
 	/* Kill socket */
 	lock_sock(sk);
-	sco_sock_clear_timer(sk);
 	sco_chan_del(sk, err);
 	release_sock(sk);
 	sock_put(sk);
@@ -532,6 +520,8 @@ static void sco_sock_kill(struct sock *sk)
 
 	BT_DBG("sk %p state %d", sk, sk->sk_state);
 
+	sco_sock_disable_timer(sk);
+
 	/* Sock is dead, so set conn->sk to NULL to avoid possible UAF */
 	lock_sock(sk);
 	if (sco_pi(sk)->conn) {
@@ -574,23 +564,11 @@ static void __sco_sock_close(struct sock *sk)
 /* Must be called on unlocked socket. */
 static void sco_sock_close(struct sock *sk)
 {
-	struct sco_conn *conn;
-
-	lock_sock(sk);
-	conn = sco_pi(sk)->conn;
-	if (conn)
-		sco_conn_hold(conn);
-	release_sock(sk);
-
-	if (conn)
-		disable_delayed_work_sync(&conn->timeout_work);
+	sco_sock_disable_timer(sk);
 
 	lock_sock(sk);
 	__sco_sock_close(sk);
 	release_sock(sk);
-
-	if (conn)
-		sco_conn_put(conn);
 }
 
 static void sco_sock_init(struct sock *sk, struct sock *parent)
@@ -622,6 +600,8 @@ static struct sock *sco_sock_alloc(struct net *net, struct socket *sock,
 	sk->sk_destruct = sco_sock_destruct;
 	sk->sk_sndtimeo = SCO_CONN_TIMEOUT;
 
+	INIT_DELAYED_WORK(&sco_pi(sk)->timeout_work, sco_sock_timeout);
+
 	sco_pi(sk)->setting = BT_VOICE_CVSD_16BIT;
 	sco_pi(sk)->codec.id = BT_CODEC_CVSD;
 	sco_pi(sk)->codec.cid = 0xffff;
-- 
2.53.0


             reply	other threads:[~2026-08-20 17:46 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-20 17:46 Ali Firas [this message]
2026-08-20 18:30 ` Bluetooth: SCO: avoid deadlock in sco_conn_free() bluez.test.bot

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260820174633.1570398-1-alishmery18@gmail.com \
    --to=alishmery18@gmail.com \
    --cc=linux-bluetooth@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=luiz.dentz@gmail.com \
    --cc=marcel@holtmann.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox