Re: [PATCH v2] Bluetooth: SCO: give the socket its own sco_conn reference

Pauli Virtanen <[email protected]> Sat, 25 Jul 2026 14:37:04 +0300
Newsgroups org.kernel.vger.linux-bluetooth,org.kernel.vger.linux-kernel,org.kernel.vger.stable
Message-ID <[email protected]>
Hi,

to, 2026-07-23 kello 20:29 -0300, Aldo Ariel Panzardo kirjoitti:
> sco_conn_del() drops a reference it does not own. It takes one transient
> reference via sco_conn_hold_unless_zero() and releases it with the
> sco_conn_put() that follows sco_sock_hold(); the additional put in the
> !sk branch releases a second one:
> 
>     conn = sco_conn_hold_unless_zero(conn);
>     ...
>     sk = sco_sock_hold(conn);
>     sco_conn_unlock(conn);
>     sco_conn_put(conn);
> 
>     if (!sk) {
>             sco_conn_put(conn);
>             return;
>     }
> 
> When close() races the controller's Disconnection Complete, sco_chan_del()
> clears conn->sk and drops the socket's reference while sco_conn_del() is
> running. sco_conn_del() then sees sk == NULL, its own put drops the count
> to zero and frees the conn, and the second put writes to the freed kref:
> 
>     BUG: KASAN: slab-use-after-free in sco_conn_put.part.0+0x1a/0x190
>     Write of size 4 at addr ffff8881099dec74 by task kworker/u17:3/413
>     Workqueue: hci1 hci_rx_work
>     Call Trace:
>      sco_conn_put.part.0+0x1a/0x190
>      hci_disconn_complete_evt+0x1ee/0x3e0
>      hci_event_packet+0x54a/0x650
>      hci_rx_work+0x321/0x3d0
>     Allocated by task 413:
>      sco_conn_add+0x72/0x1a0
>      sco_connect_cfm+0x88/0x670
>     Freed by task 413:
>      sco_conn_del.isra.0+0x3f/0xf0
>      hci_disconn_complete_evt+0x1ee/0x3e0
>     refcount_t: underflow; use-after-free.
> 
> Simply deleting the extra put is not enough, because the reference it
> releases is not always accounted for elsewhere. __sco_chan_add() stores
> the connection in the socket without taking a reference:
> 
>     sco_pi(sk)->conn = conn;
> 
> so the socket inherits whatever reference its caller happened to hold.
> That works out for sco_conn_ready(), which takes an explicit
> sco_conn_hold() beforehand and whose caller puts its own reference, and
> for the success path of sco_connect(), where the reference returned by
> sco_conn_add() is silently handed over and later released by
> sco_sock_destruct(). It does not work out for the two error paths of
> sco_connect(): if the socket state changed while the lock was dropped, or
> if sco_chan_add() returns -EBUSY, the reference from sco_conn_add() is
> never released and the connection is leaked. The extra put in
> sco_conn_del() is what eventually reclaims those orphans, which is why
> removing it in isolation trades a use-after-free for a leak.
> 
> Make the ownership explicit instead. __sco_chan_add() now takes the
> socket's reference itself, sco_connect() releases the one it got from
> sco_conn_add() on every path, and the now redundant hold in
> sco_conn_ready() is dropped. With the socket holding a counted reference,
> a connection can no longer reach zero while conn->sk is set, so
> sco_conn_free() no longer has to clear sco_pi(conn->sk)->conn. Every
> reference then has exactly one owner: the one sco_conn_add() returns
> belongs to its caller, the socket's is taken and released with the
> channel, and sco_conn_del() and sco_sock_timeout() only ever hold
> transient ones.
> 
> Fixes: e6720779ae61 ("Bluetooth: SCO: Use kref to track lifetime of sco_conn")
> Cc: [email protected]
> Signed-off-by: Aldo Ariel Panzardo <[email protected]>
> ---
> v2:
>  - Do not just delete the extra put: make the socket own its reference,
>    balance sco_connect()'s error paths and drop the redundant hold in
>    sco_conn_ready(), per Pauli Virtanen's review.
>  - Drop the now unreachable sco_pi(conn->sk)->conn clearing in
>    sco_conn_free().
>  - Indent the quoted code with spaces so gitlint stops complaining.
> 
> On hci_conn_drop() vs hci_connect_sco(), which was also asked about: the
> reference hci_connect_sco() returns is released by hci_conn_drop() on
> each error path of sco_connect(), and on the success path it is handed to
> the connection and released by sco_conn_free(). That side looks balanced.
> There is a separate asymmetry that this patch does not touch: when
> sco_conn_add() returns a connection that already existed for the hcon,
> hci_connect_sco() has taken a fresh hci_conn reference but sco_conn_free()
> only ever issues one hci_conn_drop(). That looks like a pre-existing
> hci_conn leak rather than an sco_conn one; I did not want to fold it into
> this fix.

There's a double drop on the error paths now, so it'll probably hit
WARN_ON() if these are reached, but it's not fatal.

The SCO refcounting rule probably should be that sco_conn owns one
hci_conn_hold reference all of its lifetime.

hci_connect_sco() returns a hci_conn with a new hci_conn_hold reference
given to the caller.

sco_connect_cfm() never gives callee a hci_conn_hold refcount.

So probably (this will have to be thought out better and maybe separate
patch):

diff --git a/net/bluetooth/sco.c b/net/bluetooth/sco.c
index 5cab7e2fb898..0e1dd6a8e6d7 100644
--- a/net/bluetooth/sco.c
+++ b/net/bluetooth/sco.c
@@ -185,6 +185,8 @@ static void sco_sock_clear_timer(struct sock *sk)
 }
 
 /* ---- SCO connections ---- */
+
+/* Consumes hci_conn_hold refcount */
 static struct sco_conn *sco_conn_add(struct hci_conn *hcon)
 {
 	struct sco_conn *conn = hcon->sco_data;
@@ -195,6 +197,9 @@ static struct sco_conn *sco_conn_add(struct
hci_conn *hcon)
 			sco_conn_lock(conn);
 			conn->hcon = hcon;
 			sco_conn_unlock(conn);
+		} else {
+			/* We already own the refcount */
+			hci_conn_drop(hcon);
 		}
 		return conn;
 	}
@@ -362,7 +367,6 @@ static int sco_connect(struct sock *sk)
 	if (sk->sk_state != BT_OPEN && sk->sk_state != BT_BOUND) {
 		release_sock(sk);
 		sco_conn_put(conn);
-		hci_conn_drop(hcon);
 		err = -EBADFD;
 		goto unlock;
 	}
@@ -371,7 +375,6 @@ static int sco_connect(struct sock *sk)
 	sco_conn_put(conn);
 	if (err) {
 		release_sock(sk);
-		hci_conn_drop(hcon);
 		goto unlock;
 	}
 
@@ -1449,7 +1452,6 @@ static void sco_conn_ready(struct sco_conn *conn)
 		bacpy(&sco_pi(sk)->src, &conn->hcon->src);
 		bacpy(&sco_pi(sk)->dst, &conn->hcon->dst);
 
-		hci_conn_hold(conn->hcon);
 		__sco_chan_add(conn, sk, parent);
 
 		if (test_bit(BT_SK_DEFER_SETUP, &bt_sk(parent)-
>flags))
@@ -1505,7 +1507,7 @@ static void sco_connect_cfm(struct hci_conn
*hcon, __u8 status)
 	if (!status) {
 		struct sco_conn *conn;
 
-		conn = sco_conn_add(hcon);
+		conn = sco_conn_add(hci_conn_hold(hcon));
 		if (conn) {
 			sco_conn_ready(conn);
 			sco_conn_put(conn);

> 
> Testing: the original defect reproduced 45 times across 2 independent
> runs on unmodified v7.2-rc1-240-g71dfdfb0209b with KASAN, driven through
> /dev/vhci by racing close() of an SCO socket against an injected
> Disconnection Complete; both KASAN and the refcount_t underflow fired
> every time. The BlueZ CI ran sco-tester against v1 with no regression.
> 
>  net/bluetooth/sco.c | 12 ++++--------
>  1 file changed, 4 insertions(+), 8 deletions(-)
> 
> diff --git a/net/bluetooth/sco.c b/net/bluetooth/sco.c
> index fcc597be5bbd..21f829575803 100644
> --- a/net/bluetooth/sco.c
> +++ b/net/bluetooth/sco.c
> @@ -81,9 +81,6 @@ static void sco_conn_free(struct kref *r
>  
>  	BT_DBG("conn %p", conn);
>  
> -	if (conn->sk)
> -		sco_pi(conn->sk)->conn = NULL;
> -
>  	if (conn->hcon) {
>  		conn->hcon->sco_data = NULL;
>  		hci_conn_drop(conn->hcon);
> @@ -265,10 +262,8 @@ static void sco_conn_del(struct hci_conn
>  	sco_conn_unlock(conn);
>  	sco_conn_put(conn);
>  
> -	if (!sk) {
> -		sco_conn_put(conn);
> +	if (!sk)
>  		return;
> -	}
>  
>  	/* Kill socket */
>  	lock_sock(sk);
> @@ -283,7 +278,7 @@ static void __sco_chan_add(struct sco_co
>  {
>  	BT_DBG("conn %p", conn);
>  
> -	sco_pi(sk)->conn = conn;
> +	sco_pi(sk)->conn = sco_conn_hold(conn);
>  	conn->sk = sk;
>  
>  	if (parent)
> @@ -366,12 +361,14 @@ static int sco_connect(struct sock *sk)
>  	 */
>  	if (sk->sk_state != BT_OPEN && sk->sk_state != BT_BOUND) {
>  		release_sock(sk);
> +		sco_conn_put(conn);
>  		hci_conn_drop(hcon);
>  		err = -EBADFD;
>  		goto unlock;
>  	}
>  
>  	err = sco_chan_add(conn, sk, NULL);
> +	sco_conn_put(conn);
>  	if (err) {
>  		release_sock(sk);
>  		hci_conn_drop(hcon);
> @@ -1439,7 +1436,6 @@ static void sco_conn_ready(struct sco_co
>  		bacpy(&sco_pi(sk)->src, &conn->hcon->src);
>  		bacpy(&sco_pi(sk)->dst, &conn->hcon->dst);
>  
> -		sco_conn_hold(conn);
>  		hci_conn_hold(conn->hcon);
>  		__sco_chan_add(conn, sk, parent);
>  

-- 
Pauli Virtanen