* [PATCH v9 0/1] Bluetooth: L2CAP: Fix use-after-free in l2cap_sock_new_connection_cb()
@ 2026-06-03 15:06 Siwei Zhang
2026-06-03 15:06 ` [PATCH v9 1/1] " Siwei Zhang
0 siblings, 1 reply; 20+ messages in thread
From: Siwei Zhang @ 2026-06-03 15:06 UTC (permalink / raw)
To: Marcel Holtmann, Luiz Augusto von Dentz; +Cc: linux-bluetooth, Siwei Zhang
Compared to v2, addresses comments on https://sashiko.dev/#/patchset/20260415204842.2363950-1-oss%40fourdim.xyz .
Compared to v3, rebase against bluetooth-next.
Compared to v4, allocate the channel outside the function and pass it in as an argument to avoid the use-after-free.
Compared to v5, extract the channel init to a separate function.
Compared to v6, balance puts and holds on chans.
Compared to v7, rebase against bluetooth-next and refactor the chan refcounting.
Compared to v8, adopt the philosophy of one assignment one reference. Make refcounting easier to follow.
Siwei Zhang (1):
Bluetooth: L2CAP: Fix use-after-free in l2cap_sock_new_connection_cb()
include/net/bluetooth/l2cap.h | 10 +++--
net/bluetooth/6lowpan.c | 31 +++++++------
net/bluetooth/l2cap_core.c | 41 ++++++++++++-----
net/bluetooth/l2cap_sock.c | 83 +++++++++++++++++++++--------------
net/bluetooth/smp.c | 17 ++++---
5 files changed, 113 insertions(+), 69 deletions(-)
--
2.54.0
^ permalink raw reply [flat|nested] 20+ messages in thread
* [PATCH v9 1/1] Bluetooth: L2CAP: Fix use-after-free in l2cap_sock_new_connection_cb()
2026-06-03 15:06 [PATCH v9 0/1] Bluetooth: L2CAP: Fix use-after-free in l2cap_sock_new_connection_cb() Siwei Zhang
@ 2026-06-03 15:06 ` Siwei Zhang
2026-06-03 17:40 ` bluez.test.bot
2026-06-03 18:17 ` [PATCH v9 1/1] " Luiz Augusto von Dentz
0 siblings, 2 replies; 20+ messages in thread
From: Siwei Zhang @ 2026-06-03 15:06 UTC (permalink / raw)
To: Marcel Holtmann, Luiz Augusto von Dentz; +Cc: linux-bluetooth, Siwei Zhang
l2cap_sock_new_connection_cb() accesses l2cap_pi(sk)->chan after
release_sock(parent). Once the parent lock is released, the child
socket sk can be freed by another task.
Allocate the channel outside the func to prevent this.
Fixes: 8ffb929098a5 ("Bluetooth: Remove parent socket usage from l2cap_core.c")
Cc: stable@kernel.org
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Siwei Zhang <oss@fourdim.xyz>
---
include/net/bluetooth/l2cap.h | 10 +++--
net/bluetooth/6lowpan.c | 31 +++++++------
net/bluetooth/l2cap_core.c | 41 ++++++++++++-----
net/bluetooth/l2cap_sock.c | 83 +++++++++++++++++++++--------------
net/bluetooth/smp.c | 17 ++++---
5 files changed, 113 insertions(+), 69 deletions(-)
diff --git a/include/net/bluetooth/l2cap.h b/include/net/bluetooth/l2cap.h
index e0a1f2293679..7f5e4647f6e0 100644
--- a/include/net/bluetooth/l2cap.h
+++ b/include/net/bluetooth/l2cap.h
@@ -620,7 +620,9 @@ struct l2cap_chan {
struct l2cap_ops {
char *name;
- struct l2cap_chan *(*new_connection) (struct l2cap_chan *chan);
+ int (*new_connection)(struct l2cap_conn *conn,
+ struct l2cap_chan *chan,
+ struct l2cap_chan *new_chan);
int (*recv) (struct l2cap_chan * chan,
struct sk_buff *skb);
void (*teardown) (struct l2cap_chan *chan, int err);
@@ -884,9 +886,11 @@ static inline __u16 __next_seq(struct l2cap_chan *chan, __u16 seq)
return (seq + 1) % (chan->tx_win_max + 1);
}
-static inline struct l2cap_chan *l2cap_chan_no_new_connection(struct l2cap_chan *chan)
+static inline int l2cap_chan_no_new_connection(struct l2cap_conn *conn,
+ struct l2cap_chan *chan,
+ struct l2cap_chan *new_chan)
{
- return NULL;
+ return -EOPNOTSUPP;
}
static inline int l2cap_chan_no_recv(struct l2cap_chan *chan, struct sk_buff *skb)
diff --git a/net/bluetooth/6lowpan.c b/net/bluetooth/6lowpan.c
index cb1e329d66fd..94863af97a44 100644
--- a/net/bluetooth/6lowpan.c
+++ b/net/bluetooth/6lowpan.c
@@ -624,6 +624,15 @@ static bool is_bt_6lowpan(struct hci_conn *hcon)
return true;
}
+static void chan_init(struct l2cap_chan *chan)
+{
+ l2cap_chan_set_defaults(chan);
+
+ chan->chan_type = L2CAP_CHAN_CONN_ORIENTED;
+ chan->mode = L2CAP_MODE_LE_FLOWCTL;
+ chan->imtu = 1280;
+}
+
static struct l2cap_chan *chan_create(void)
{
struct l2cap_chan *chan;
@@ -632,11 +641,7 @@ static struct l2cap_chan *chan_create(void)
if (!chan)
return NULL;
- l2cap_chan_set_defaults(chan);
-
- chan->chan_type = L2CAP_CHAN_CONN_ORIENTED;
- chan->mode = L2CAP_MODE_LE_FLOWCTL;
- chan->imtu = 1280;
+ chan_init(chan);
return chan;
}
@@ -745,19 +750,19 @@ static inline void chan_ready_cb(struct l2cap_chan *chan)
ifup(dev->netdev);
}
-static inline struct l2cap_chan *chan_new_conn_cb(struct l2cap_chan *pchan)
+static inline int chan_new_conn_cb(struct l2cap_conn *conn,
+ struct l2cap_chan *pchan,
+ struct l2cap_chan *chan)
{
- struct l2cap_chan *chan;
-
- chan = chan_create();
- if (!chan)
- return NULL;
-
+ chan_init(chan);
chan->ops = pchan->ops;
+ /* Take the conn list reference; see l2cap_new_connection(). */
+ __l2cap_chan_add(conn, chan);
+
BT_DBG("chan %p pchan %p", chan, pchan);
- return chan;
+ return 0;
}
static void unregister_dev(struct lowpan_btle_dev *dev)
diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
index c4ccfbda9d78..62acf90837fb 100644
--- a/net/bluetooth/l2cap_core.c
+++ b/net/bluetooth/l2cap_core.c
@@ -4007,6 +4007,31 @@ static inline int l2cap_command_rej(struct l2cap_conn *conn,
return 0;
}
+/* Allocate and initialise a channel for an incoming connection.
+ *
+ * ->new_connection() initialises the channel and links it into @conn with
+ * __l2cap_chan_add(). The l2cap_chan_create() reference becomes the one owned
+ * by the parent subsystem (l2cap_pi(sk)->chan, conn->smp or peer->chan) and is
+ * released by its teardown callback; the conn list reference is released by
+ * l2cap_chan_del().
+ */
+static struct l2cap_chan *l2cap_new_connection(struct l2cap_conn *conn,
+ struct l2cap_chan *pchan)
+{
+ struct l2cap_chan *chan;
+
+ chan = l2cap_chan_create();
+ if (!chan)
+ return NULL;
+
+ if (pchan->ops->new_connection(conn, pchan, chan) < 0) {
+ l2cap_chan_put(chan);
+ return NULL;
+ }
+
+ return chan;
+}
+
static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
u8 *data, u8 rsp_code)
{
@@ -4053,7 +4078,7 @@ static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
goto response;
}
- chan = pchan->ops->new_connection(pchan);
+ chan = l2cap_new_connection(conn, pchan);
if (!chan)
goto response;
@@ -4071,8 +4096,6 @@ static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
chan->psm = psm;
chan->dcid = scid;
- __l2cap_chan_add(conn, chan);
-
dcid = chan->scid;
__set_chan_timer(chan, chan->ops->get_sndtimeo(chan));
@@ -4955,7 +4978,7 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
goto response_unlock;
}
- chan = pchan->ops->new_connection(pchan);
+ chan = l2cap_new_connection(conn, pchan);
if (!chan) {
result = L2CAP_CR_LE_NO_MEM;
goto response_unlock;
@@ -4970,8 +4993,6 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
chan->omtu = mtu;
chan->remote_mps = mps;
- __l2cap_chan_add(conn, chan);
-
l2cap_le_flowctl_init(chan, __le16_to_cpu(req->credits));
dcid = chan->scid;
@@ -5179,7 +5200,7 @@ static inline int l2cap_ecred_conn_req(struct l2cap_conn *conn,
continue;
}
- chan = pchan->ops->new_connection(pchan);
+ chan = l2cap_new_connection(conn, pchan);
if (!chan) {
result = L2CAP_CR_LE_NO_MEM;
continue;
@@ -5194,8 +5215,6 @@ static inline int l2cap_ecred_conn_req(struct l2cap_conn *conn,
chan->omtu = mtu;
chan->remote_mps = mps;
- __l2cap_chan_add(conn, chan);
-
l2cap_ecred_init(chan, __le16_to_cpu(req->credits));
/* Init response */
@@ -7470,14 +7489,12 @@ static void l2cap_connect_cfm(struct hci_conn *hcon, u8 status)
goto next;
l2cap_chan_lock(pchan);
- chan = pchan->ops->new_connection(pchan);
+ chan = l2cap_new_connection(conn, pchan);
if (chan) {
bacpy(&chan->src, &hcon->src);
bacpy(&chan->dst, &hcon->dst);
chan->src_type = bdaddr_src_type(hcon);
chan->dst_type = dst_type;
-
- __l2cap_chan_add(conn, chan);
}
l2cap_chan_unlock(pchan);
diff --git a/net/bluetooth/l2cap_sock.c b/net/bluetooth/l2cap_sock.c
index 025329636353..87f4c0db5c0c 100644
--- a/net/bluetooth/l2cap_sock.c
+++ b/net/bluetooth/l2cap_sock.c
@@ -46,7 +46,8 @@ static struct bt_sock_list l2cap_sk_list = {
static const struct proto_ops l2cap_sock_ops;
static void l2cap_sock_init(struct sock *sk, struct sock *parent);
static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
- int proto, gfp_t prio, int kern);
+ int proto, gfp_t prio, int kern,
+ struct l2cap_chan *chan);
static void l2cap_sock_cleanup_listen(struct sock *parent);
bool l2cap_is_socket(struct socket *sock)
@@ -1287,6 +1288,23 @@ static int l2cap_sock_recvmsg(struct socket *sock, struct msghdr *msg,
return err;
}
+/* Release the sock's ref on chan and clear the pointer so that the ref is
+ * dropped exactly once even if both l2cap_sock_kill() and
+ * l2cap_sock_destruct() run. Setting chan->data to NULL first stops any other
+ * task from dereferencing the now-dead sock pointer.
+ */
+static void l2cap_sock_put_chan(struct sock *sk)
+{
+ struct l2cap_chan *chan = l2cap_pi(sk)->chan;
+
+ if (!chan)
+ return;
+
+ chan->data = NULL;
+ l2cap_pi(sk)->chan = NULL;
+ l2cap_chan_put(chan);
+}
+
/* Kill socket (only if zapped and orphan)
* Must be called on unlocked socket, with l2cap channel lock.
*/
@@ -1297,13 +1315,9 @@ static void l2cap_sock_kill(struct sock *sk)
BT_DBG("sk %p state %s", sk, state_to_string(sk->sk_state));
- /* Sock is dead, so set chan data to NULL, avoid other task use invalid
- * sock pointer.
- */
- l2cap_pi(sk)->chan->data = NULL;
- /* Kill poor orphan */
+ l2cap_sock_put_chan(sk);
- l2cap_chan_put(l2cap_pi(sk)->chan);
+ /* Kill poor orphan */
sock_set_flag(sk, SOCK_DEAD);
sock_put(sk);
}
@@ -1546,12 +1560,14 @@ static void l2cap_sock_cleanup_listen(struct sock *parent)
}
}
-static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
+static int l2cap_sock_new_connection_cb(struct l2cap_conn *conn,
+ struct l2cap_chan *chan,
+ struct l2cap_chan *new_chan)
{
struct sock *sk, *parent = chan->data;
if (!parent)
- return NULL;
+ return -EINVAL;
lock_sock(parent);
@@ -1559,25 +1575,33 @@ static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
if (sk_acceptq_is_full(parent)) {
BT_DBG("backlog full %d", parent->sk_ack_backlog);
release_sock(parent);
- return NULL;
+ return -ENOBUFS;
}
sk = l2cap_sock_alloc(sock_net(parent), NULL, BTPROTO_L2CAP,
- GFP_ATOMIC, 0);
+ GFP_ATOMIC, 0, new_chan);
if (!sk) {
release_sock(parent);
- return NULL;
- }
+ return -ENOMEM;
+ }
bt_sock_reclassify_lock(sk, BTPROTO_L2CAP);
l2cap_sock_init(sk, parent);
+ /* Link the channel into conn before exposing the new socket via the
+ * accept queue. Once release_sock() below drops the parent lock the
+ * socket may be freed by another task, dropping its reference on
+ * new_chan; the conn list reference taken here keeps new_chan alive so
+ * the caller can safely use it after ->new_connection() returns.
+ */
+ __l2cap_chan_add(conn, new_chan);
+
bt_accept_enqueue(parent, sk, false);
release_sock(parent);
- return l2cap_pi(sk)->chan;
+ return 0;
}
static int l2cap_sock_recv_cb(struct l2cap_chan *chan, struct sk_buff *skb)
@@ -1874,10 +1898,7 @@ static void l2cap_sock_destruct(struct sock *sk)
BT_DBG("sk %p", sk);
- if (l2cap_pi(sk)->chan) {
- l2cap_pi(sk)->chan->data = NULL;
- l2cap_chan_put(l2cap_pi(sk)->chan);
- }
+ l2cap_sock_put_chan(sk);
list_for_each_entry_safe(rx_busy, next, &l2cap_pi(sk)->rx_busy, list) {
kfree_skb(rx_busy->skb);
@@ -1978,10 +1999,10 @@ static struct proto l2cap_proto = {
};
static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
- int proto, gfp_t prio, int kern)
+ int proto, gfp_t prio, int kern,
+ struct l2cap_chan *chan)
{
struct sock *sk;
- struct l2cap_chan *chan;
sk = bt_sock_alloc(net, sock, &l2cap_proto, proto, prio, kern);
if (!sk)
@@ -1992,16 +2013,7 @@ static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
INIT_LIST_HEAD(&l2cap_pi(sk)->rx_busy);
- chan = l2cap_chan_create();
- if (!chan) {
- sk_free(sk);
- if (sock)
- sock->sk = NULL;
- return NULL;
- }
-
- l2cap_chan_hold(chan);
-
+ /* The sock takes ownership of the caller's reference on chan. */
l2cap_pi(sk)->chan = chan;
return sk;
@@ -2011,6 +2023,7 @@ static int l2cap_sock_create(struct net *net, struct socket *sock, int protocol,
int kern)
{
struct sock *sk;
+ struct l2cap_chan *chan;
BT_DBG("sock %p", sock);
@@ -2025,10 +2038,16 @@ static int l2cap_sock_create(struct net *net, struct socket *sock, int protocol,
sock->ops = &l2cap_sock_ops;
- sk = l2cap_sock_alloc(net, sock, protocol, GFP_ATOMIC, kern);
- if (!sk)
+ chan = l2cap_chan_create();
+ if (!chan)
return -ENOMEM;
+ sk = l2cap_sock_alloc(net, sock, protocol, GFP_ATOMIC, kern, chan);
+ if (!sk) {
+ l2cap_chan_put(chan);
+ return -ENOMEM;
+ }
+
l2cap_sock_init(sk, NULL);
bt_sock_link(&l2cap_sk_list, sk);
return 0;
diff --git a/net/bluetooth/smp.c b/net/bluetooth/smp.c
index 1739c1989dbd..2d31c3c7bbc0 100644
--- a/net/bluetooth/smp.c
+++ b/net/bluetooth/smp.c
@@ -3204,16 +3204,12 @@ static const struct l2cap_ops smp_chan_ops = {
.get_sndtimeo = l2cap_chan_no_get_sndtimeo,
};
-static inline struct l2cap_chan *smp_new_conn_cb(struct l2cap_chan *pchan)
+static inline int smp_new_conn_cb(struct l2cap_conn *conn,
+ struct l2cap_chan *pchan,
+ struct l2cap_chan *chan)
{
- struct l2cap_chan *chan;
-
BT_DBG("pchan %p", pchan);
- chan = l2cap_chan_create();
- if (!chan)
- return NULL;
-
chan->chan_type = pchan->chan_type;
chan->ops = &smp_chan_ops;
chan->scid = pchan->scid;
@@ -3229,9 +3225,12 @@ static inline struct l2cap_chan *smp_new_conn_cb(struct l2cap_chan *pchan)
*/
atomic_set(&chan->nesting, L2CAP_NESTING_SMP);
- BT_DBG("created chan %p", chan);
+ /* Take the conn list reference; see l2cap_new_connection(). */
+ __l2cap_chan_add(conn, chan);
- return chan;
+ BT_DBG("initialised chan %p", chan);
+
+ return 0;
}
static const struct l2cap_ops smp_root_chan_ops = {
--
2.54.0
^ permalink raw reply related [flat|nested] 20+ messages in thread* RE: Bluetooth: L2CAP: Fix use-after-free in l2cap_sock_new_connection_cb()
2026-06-03 15:06 ` [PATCH v9 1/1] " Siwei Zhang
@ 2026-06-03 17:40 ` bluez.test.bot
2026-06-03 18:17 ` [PATCH v9 1/1] " Luiz Augusto von Dentz
1 sibling, 0 replies; 20+ messages in thread
From: bluez.test.bot @ 2026-06-03 17:40 UTC (permalink / raw)
To: linux-bluetooth, oss
[-- Attachment #1: Type: text/plain, Size: 1150 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=1105389
---Test result---
Test Summary:
CheckPatch PASS 1.55 seconds
VerifyFixes PASS 0.10 seconds
VerifySignedoff PASS 0.07 seconds
GitLint PASS 0.32 seconds
SubjectPrefix PASS 0.07 seconds
BuildKernel PASS 26.94 seconds
CheckAllWarning PASS 29.57 seconds
CheckSparse PASS 27.88 seconds
BuildKernel32 PASS 26.28 seconds
TestRunnerSetup PASS 580.30 seconds
TestRunner_l2cap-tester PASS 60.03 seconds
TestRunner_smp-tester PASS 23.45 seconds
TestRunner_6lowpan-tester PASS 22.67 seconds
IncrementalBuild PASS 25.76 seconds
https://github.com/bluez/bluetooth-next/pull/283
---
Regards,
Linux Bluetooth
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH v9 1/1] Bluetooth: L2CAP: Fix use-after-free in l2cap_sock_new_connection_cb()
2026-06-03 15:06 ` [PATCH v9 1/1] " Siwei Zhang
2026-06-03 17:40 ` bluez.test.bot
@ 2026-06-03 18:17 ` Luiz Augusto von Dentz
2026-06-04 15:52 ` Siwei Zhang
1 sibling, 1 reply; 20+ messages in thread
From: Luiz Augusto von Dentz @ 2026-06-03 18:17 UTC (permalink / raw)
To: Siwei Zhang; +Cc: Marcel Holtmann, linux-bluetooth
Hi Siwei,
On Wed, Jun 3, 2026 at 11:09 AM Siwei Zhang <oss@fourdim.xyz> wrote:
>
> l2cap_sock_new_connection_cb() accesses l2cap_pi(sk)->chan after
> release_sock(parent). Once the parent lock is released, the child
> socket sk can be freed by another task.
>
> Allocate the channel outside the func to prevent this.
>
> Fixes: 8ffb929098a5 ("Bluetooth: Remove parent socket usage from l2cap_core.c")
> Cc: stable@kernel.org
> Assisted-by: Claude:claude-opus-4-8
> Signed-off-by: Siwei Zhang <oss@fourdim.xyz>
> ---
> include/net/bluetooth/l2cap.h | 10 +++--
> net/bluetooth/6lowpan.c | 31 +++++++------
> net/bluetooth/l2cap_core.c | 41 ++++++++++++-----
> net/bluetooth/l2cap_sock.c | 83 +++++++++++++++++++++--------------
> net/bluetooth/smp.c | 17 ++++---
> 5 files changed, 113 insertions(+), 69 deletions(-)
>
> diff --git a/include/net/bluetooth/l2cap.h b/include/net/bluetooth/l2cap.h
> index e0a1f2293679..7f5e4647f6e0 100644
> --- a/include/net/bluetooth/l2cap.h
> +++ b/include/net/bluetooth/l2cap.h
> @@ -620,7 +620,9 @@ struct l2cap_chan {
> struct l2cap_ops {
> char *name;
>
> - struct l2cap_chan *(*new_connection) (struct l2cap_chan *chan);
> + int (*new_connection)(struct l2cap_conn *conn,
> + struct l2cap_chan *chan,
> + struct l2cap_chan *new_chan);
> int (*recv) (struct l2cap_chan * chan,
> struct sk_buff *skb);
> void (*teardown) (struct l2cap_chan *chan, int err);
> @@ -884,9 +886,11 @@ static inline __u16 __next_seq(struct l2cap_chan *chan, __u16 seq)
> return (seq + 1) % (chan->tx_win_max + 1);
> }
>
> -static inline struct l2cap_chan *l2cap_chan_no_new_connection(struct l2cap_chan *chan)
> +static inline int l2cap_chan_no_new_connection(struct l2cap_conn *conn,
> + struct l2cap_chan *chan,
> + struct l2cap_chan *new_chan)
> {
> - return NULL;
> + return -EOPNOTSUPP;
> }
>
> static inline int l2cap_chan_no_recv(struct l2cap_chan *chan, struct sk_buff *skb)
> diff --git a/net/bluetooth/6lowpan.c b/net/bluetooth/6lowpan.c
> index cb1e329d66fd..94863af97a44 100644
> --- a/net/bluetooth/6lowpan.c
> +++ b/net/bluetooth/6lowpan.c
> @@ -624,6 +624,15 @@ static bool is_bt_6lowpan(struct hci_conn *hcon)
> return true;
> }
>
> +static void chan_init(struct l2cap_chan *chan)
> +{
> + l2cap_chan_set_defaults(chan);
> +
> + chan->chan_type = L2CAP_CHAN_CONN_ORIENTED;
> + chan->mode = L2CAP_MODE_LE_FLOWCTL;
> + chan->imtu = 1280;
> +}
> +
> static struct l2cap_chan *chan_create(void)
> {
> struct l2cap_chan *chan;
> @@ -632,11 +641,7 @@ static struct l2cap_chan *chan_create(void)
> if (!chan)
> return NULL;
>
> - l2cap_chan_set_defaults(chan);
> -
> - chan->chan_type = L2CAP_CHAN_CONN_ORIENTED;
> - chan->mode = L2CAP_MODE_LE_FLOWCTL;
> - chan->imtu = 1280;
> + chan_init(chan);
>
> return chan;
> }
> @@ -745,19 +750,19 @@ static inline void chan_ready_cb(struct l2cap_chan *chan)
> ifup(dev->netdev);
> }
>
> -static inline struct l2cap_chan *chan_new_conn_cb(struct l2cap_chan *pchan)
> +static inline int chan_new_conn_cb(struct l2cap_conn *conn,
> + struct l2cap_chan *pchan,
> + struct l2cap_chan *chan)
> {
> - struct l2cap_chan *chan;
> -
> - chan = chan_create();
> - if (!chan)
> - return NULL;
> -
> + chan_init(chan);
> chan->ops = pchan->ops;
>
> + /* Take the conn list reference; see l2cap_new_connection(). */
> + __l2cap_chan_add(conn, chan);
> +
> BT_DBG("chan %p pchan %p", chan, pchan);
>
> - return chan;
> + return 0;
> }
>
> static void unregister_dev(struct lowpan_btle_dev *dev)
> diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
> index c4ccfbda9d78..62acf90837fb 100644
> --- a/net/bluetooth/l2cap_core.c
> +++ b/net/bluetooth/l2cap_core.c
> @@ -4007,6 +4007,31 @@ static inline int l2cap_command_rej(struct l2cap_conn *conn,
> return 0;
> }
>
> +/* Allocate and initialise a channel for an incoming connection.
> + *
> + * ->new_connection() initialises the channel and links it into @conn with
> + * __l2cap_chan_add(). The l2cap_chan_create() reference becomes the one owned
> + * by the parent subsystem (l2cap_pi(sk)->chan, conn->smp or peer->chan) and is
> + * released by its teardown callback; the conn list reference is released by
> + * l2cap_chan_del().
> + */
> +static struct l2cap_chan *l2cap_new_connection(struct l2cap_conn *conn,
> + struct l2cap_chan *pchan)
> +{
> + struct l2cap_chan *chan;
> +
> + chan = l2cap_chan_create();
> + if (!chan)
> + return NULL;
> +
> + if (pchan->ops->new_connection(conn, pchan, chan) < 0) {
> + l2cap_chan_put(chan);
> + return NULL;
> + }
I don't quite get why we can't just place __l2cap_chan_add here
instead of having it called by new_connection callbacks?
> +
> + return chan;
> +}
> +
> static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
> u8 *data, u8 rsp_code)
> {
> @@ -4053,7 +4078,7 @@ static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
> goto response;
> }
>
> - chan = pchan->ops->new_connection(pchan);
> + chan = l2cap_new_connection(conn, pchan);
> if (!chan)
> goto response;
>
> @@ -4071,8 +4096,6 @@ static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
> chan->psm = psm;
> chan->dcid = scid;
>
> - __l2cap_chan_add(conn, chan);
> -
> dcid = chan->scid;
>
> __set_chan_timer(chan, chan->ops->get_sndtimeo(chan));
> @@ -4955,7 +4978,7 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
> goto response_unlock;
> }
>
> - chan = pchan->ops->new_connection(pchan);
> + chan = l2cap_new_connection(conn, pchan);
> if (!chan) {
> result = L2CAP_CR_LE_NO_MEM;
> goto response_unlock;
> @@ -4970,8 +4993,6 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
> chan->omtu = mtu;
> chan->remote_mps = mps;
>
> - __l2cap_chan_add(conn, chan);
> -
> l2cap_le_flowctl_init(chan, __le16_to_cpu(req->credits));
>
> dcid = chan->scid;
> @@ -5179,7 +5200,7 @@ static inline int l2cap_ecred_conn_req(struct l2cap_conn *conn,
> continue;
> }
>
> - chan = pchan->ops->new_connection(pchan);
> + chan = l2cap_new_connection(conn, pchan);
> if (!chan) {
> result = L2CAP_CR_LE_NO_MEM;
> continue;
> @@ -5194,8 +5215,6 @@ static inline int l2cap_ecred_conn_req(struct l2cap_conn *conn,
> chan->omtu = mtu;
> chan->remote_mps = mps;
>
> - __l2cap_chan_add(conn, chan);
> -
> l2cap_ecred_init(chan, __le16_to_cpu(req->credits));
>
> /* Init response */
> @@ -7470,14 +7489,12 @@ static void l2cap_connect_cfm(struct hci_conn *hcon, u8 status)
> goto next;
>
> l2cap_chan_lock(pchan);
> - chan = pchan->ops->new_connection(pchan);
> + chan = l2cap_new_connection(conn, pchan);
> if (chan) {
> bacpy(&chan->src, &hcon->src);
> bacpy(&chan->dst, &hcon->dst);
> chan->src_type = bdaddr_src_type(hcon);
> chan->dst_type = dst_type;
> -
> - __l2cap_chan_add(conn, chan);
> }
>
> l2cap_chan_unlock(pchan);
> diff --git a/net/bluetooth/l2cap_sock.c b/net/bluetooth/l2cap_sock.c
> index 025329636353..87f4c0db5c0c 100644
> --- a/net/bluetooth/l2cap_sock.c
> +++ b/net/bluetooth/l2cap_sock.c
> @@ -46,7 +46,8 @@ static struct bt_sock_list l2cap_sk_list = {
> static const struct proto_ops l2cap_sock_ops;
> static void l2cap_sock_init(struct sock *sk, struct sock *parent);
> static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
> - int proto, gfp_t prio, int kern);
> + int proto, gfp_t prio, int kern,
> + struct l2cap_chan *chan);
> static void l2cap_sock_cleanup_listen(struct sock *parent);
>
> bool l2cap_is_socket(struct socket *sock)
> @@ -1287,6 +1288,23 @@ static int l2cap_sock_recvmsg(struct socket *sock, struct msghdr *msg,
> return err;
> }
>
> +/* Release the sock's ref on chan and clear the pointer so that the ref is
> + * dropped exactly once even if both l2cap_sock_kill() and
> + * l2cap_sock_destruct() run. Setting chan->data to NULL first stops any other
> + * task from dereferencing the now-dead sock pointer.
> + */
> +static void l2cap_sock_put_chan(struct sock *sk)
> +{
> + struct l2cap_chan *chan = l2cap_pi(sk)->chan;
> +
> + if (!chan)
> + return;
> +
> + chan->data = NULL;
> + l2cap_pi(sk)->chan = NULL;
> + l2cap_chan_put(chan);
> +}
> +
> /* Kill socket (only if zapped and orphan)
> * Must be called on unlocked socket, with l2cap channel lock.
> */
> @@ -1297,13 +1315,9 @@ static void l2cap_sock_kill(struct sock *sk)
>
> BT_DBG("sk %p state %s", sk, state_to_string(sk->sk_state));
>
> - /* Sock is dead, so set chan data to NULL, avoid other task use invalid
> - * sock pointer.
> - */
> - l2cap_pi(sk)->chan->data = NULL;
> - /* Kill poor orphan */
> + l2cap_sock_put_chan(sk);
>
> - l2cap_chan_put(l2cap_pi(sk)->chan);
> + /* Kill poor orphan */
> sock_set_flag(sk, SOCK_DEAD);
> sock_put(sk);
> }
> @@ -1546,12 +1560,14 @@ static void l2cap_sock_cleanup_listen(struct sock *parent)
> }
> }
>
> -static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
> +static int l2cap_sock_new_connection_cb(struct l2cap_conn *conn,
> + struct l2cap_chan *chan,
> + struct l2cap_chan *new_chan)
> {
> struct sock *sk, *parent = chan->data;
>
> if (!parent)
> - return NULL;
> + return -EINVAL;
>
> lock_sock(parent);
>
> @@ -1559,25 +1575,33 @@ static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
> if (sk_acceptq_is_full(parent)) {
> BT_DBG("backlog full %d", parent->sk_ack_backlog);
> release_sock(parent);
> - return NULL;
> + return -ENOBUFS;
> }
>
> sk = l2cap_sock_alloc(sock_net(parent), NULL, BTPROTO_L2CAP,
> - GFP_ATOMIC, 0);
> + GFP_ATOMIC, 0, new_chan);
> if (!sk) {
> release_sock(parent);
> - return NULL;
> - }
> + return -ENOMEM;
> + }
>
> bt_sock_reclassify_lock(sk, BTPROTO_L2CAP);
>
> l2cap_sock_init(sk, parent);
>
> + /* Link the channel into conn before exposing the new socket via the
> + * accept queue. Once release_sock() below drops the parent lock the
> + * socket may be freed by another task, dropping its reference on
> + * new_chan; the conn list reference taken here keeps new_chan alive so
> + * the caller can safely use it after ->new_connection() returns.
> + */
> + __l2cap_chan_add(conn, new_chan);
> +
> bt_accept_enqueue(parent, sk, false);
>
> release_sock(parent);
>
> - return l2cap_pi(sk)->chan;
> + return 0;
> }
>
> static int l2cap_sock_recv_cb(struct l2cap_chan *chan, struct sk_buff *skb)
> @@ -1874,10 +1898,7 @@ static void l2cap_sock_destruct(struct sock *sk)
>
> BT_DBG("sk %p", sk);
>
> - if (l2cap_pi(sk)->chan) {
> - l2cap_pi(sk)->chan->data = NULL;
> - l2cap_chan_put(l2cap_pi(sk)->chan);
> - }
> + l2cap_sock_put_chan(sk);
>
> list_for_each_entry_safe(rx_busy, next, &l2cap_pi(sk)->rx_busy, list) {
> kfree_skb(rx_busy->skb);
> @@ -1978,10 +1999,10 @@ static struct proto l2cap_proto = {
> };
>
> static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
> - int proto, gfp_t prio, int kern)
> + int proto, gfp_t prio, int kern,
> + struct l2cap_chan *chan)
> {
> struct sock *sk;
> - struct l2cap_chan *chan;
>
> sk = bt_sock_alloc(net, sock, &l2cap_proto, proto, prio, kern);
> if (!sk)
> @@ -1992,16 +2013,7 @@ static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
>
> INIT_LIST_HEAD(&l2cap_pi(sk)->rx_busy);
>
> - chan = l2cap_chan_create();
> - if (!chan) {
> - sk_free(sk);
> - if (sock)
> - sock->sk = NULL;
> - return NULL;
> - }
> -
> - l2cap_chan_hold(chan);
> -
> + /* The sock takes ownership of the caller's reference on chan. */
> l2cap_pi(sk)->chan = chan;
>
> return sk;
> @@ -2011,6 +2023,7 @@ static int l2cap_sock_create(struct net *net, struct socket *sock, int protocol,
> int kern)
> {
> struct sock *sk;
> + struct l2cap_chan *chan;
>
> BT_DBG("sock %p", sock);
>
> @@ -2025,10 +2038,16 @@ static int l2cap_sock_create(struct net *net, struct socket *sock, int protocol,
>
> sock->ops = &l2cap_sock_ops;
>
> - sk = l2cap_sock_alloc(net, sock, protocol, GFP_ATOMIC, kern);
> - if (!sk)
> + chan = l2cap_chan_create();
> + if (!chan)
> return -ENOMEM;
>
> + sk = l2cap_sock_alloc(net, sock, protocol, GFP_ATOMIC, kern, chan);
> + if (!sk) {
> + l2cap_chan_put(chan);
> + return -ENOMEM;
> + }
> +
> l2cap_sock_init(sk, NULL);
> bt_sock_link(&l2cap_sk_list, sk);
> return 0;
> diff --git a/net/bluetooth/smp.c b/net/bluetooth/smp.c
> index 1739c1989dbd..2d31c3c7bbc0 100644
> --- a/net/bluetooth/smp.c
> +++ b/net/bluetooth/smp.c
> @@ -3204,16 +3204,12 @@ static const struct l2cap_ops smp_chan_ops = {
> .get_sndtimeo = l2cap_chan_no_get_sndtimeo,
> };
>
> -static inline struct l2cap_chan *smp_new_conn_cb(struct l2cap_chan *pchan)
> +static inline int smp_new_conn_cb(struct l2cap_conn *conn,
> + struct l2cap_chan *pchan,
> + struct l2cap_chan *chan)
> {
> - struct l2cap_chan *chan;
> -
> BT_DBG("pchan %p", pchan);
>
> - chan = l2cap_chan_create();
> - if (!chan)
> - return NULL;
> -
> chan->chan_type = pchan->chan_type;
> chan->ops = &smp_chan_ops;
> chan->scid = pchan->scid;
> @@ -3229,9 +3225,12 @@ static inline struct l2cap_chan *smp_new_conn_cb(struct l2cap_chan *pchan)
> */
> atomic_set(&chan->nesting, L2CAP_NESTING_SMP);
>
> - BT_DBG("created chan %p", chan);
> + /* Take the conn list reference; see l2cap_new_connection(). */
> + __l2cap_chan_add(conn, chan);
>
> - return chan;
> + BT_DBG("initialised chan %p", chan);
> +
> + return 0;
> }
>
> static const struct l2cap_ops smp_root_chan_ops = {
> --
> 2.54.0
>
--
Luiz Augusto von Dentz
^ permalink raw reply [flat|nested] 20+ messages in thread* Re: [PATCH v9 1/1] Bluetooth: L2CAP: Fix use-after-free in l2cap_sock_new_connection_cb()
2026-06-03 18:17 ` [PATCH v9 1/1] " Luiz Augusto von Dentz
@ 2026-06-04 15:52 ` Siwei Zhang
2026-06-08 13:28 ` Siwei Zhang
0 siblings, 1 reply; 20+ messages in thread
From: Siwei Zhang @ 2026-06-04 15:52 UTC (permalink / raw)
To: Luiz Augusto von Dentz; +Cc: linux-bluetooth
Hi Luiz,
On Wed, Jun 3, 2026, at 2:17 PM, Luiz Augusto von Dentz wrote:
> Hi Siwei,
>
> On Wed, Jun 3, 2026 at 11:09 AM Siwei Zhang <oss@fourdim.xyz> wrote:
>>
>> l2cap_sock_new_connection_cb() accesses l2cap_pi(sk)->chan after
>> release_sock(parent). Once the parent lock is released, the child
>> socket sk can be freed by another task.
>>
>> Allocate the channel outside the func to prevent this.
>>
>> Fixes: 8ffb929098a5 ("Bluetooth: Remove parent socket usage from l2cap_core.c")
>> Cc: stable@kernel.org
>> Assisted-by: Claude:claude-opus-4-8
>> Signed-off-by: Siwei Zhang <oss@fourdim.xyz>
>> ---
>> include/net/bluetooth/l2cap.h | 10 +++--
>> net/bluetooth/6lowpan.c | 31 +++++++------
>> net/bluetooth/l2cap_core.c | 41 ++++++++++++-----
>> net/bluetooth/l2cap_sock.c | 83 +++++++++++++++++++++--------------
>> net/bluetooth/smp.c | 17 ++++---
>> 5 files changed, 113 insertions(+), 69 deletions(-)
>>
>> diff --git a/include/net/bluetooth/l2cap.h b/include/net/bluetooth/l2cap.h
>> index e0a1f2293679..7f5e4647f6e0 100644
>> --- a/include/net/bluetooth/l2cap.h
>> +++ b/include/net/bluetooth/l2cap.h
>> @@ -620,7 +620,9 @@ struct l2cap_chan {
>> struct l2cap_ops {
>> char *name;
>>
>> - struct l2cap_chan *(*new_connection) (struct l2cap_chan *chan);
>> + int (*new_connection)(struct l2cap_conn *conn,
>> + struct l2cap_chan *chan,
>> + struct l2cap_chan *new_chan);
>> int (*recv) (struct l2cap_chan * chan,
>> struct sk_buff *skb);
>> void (*teardown) (struct l2cap_chan *chan, int err);
>> @@ -884,9 +886,11 @@ static inline __u16 __next_seq(struct l2cap_chan *chan, __u16 seq)
>> return (seq + 1) % (chan->tx_win_max + 1);
>> }
>>
>> -static inline struct l2cap_chan *l2cap_chan_no_new_connection(struct l2cap_chan *chan)
>> +static inline int l2cap_chan_no_new_connection(struct l2cap_conn *conn,
>> + struct l2cap_chan *chan,
>> + struct l2cap_chan *new_chan)
>> {
>> - return NULL;
>> + return -EOPNOTSUPP;
>> }
>>
>> static inline int l2cap_chan_no_recv(struct l2cap_chan *chan, struct sk_buff *skb)
>> diff --git a/net/bluetooth/6lowpan.c b/net/bluetooth/6lowpan.c
>> index cb1e329d66fd..94863af97a44 100644
>> --- a/net/bluetooth/6lowpan.c
>> +++ b/net/bluetooth/6lowpan.c
>> @@ -624,6 +624,15 @@ static bool is_bt_6lowpan(struct hci_conn *hcon)
>> return true;
>> }
>>
>> +static void chan_init(struct l2cap_chan *chan)
>> +{
>> + l2cap_chan_set_defaults(chan);
>> +
>> + chan->chan_type = L2CAP_CHAN_CONN_ORIENTED;
>> + chan->mode = L2CAP_MODE_LE_FLOWCTL;
>> + chan->imtu = 1280;
>> +}
>> +
>> static struct l2cap_chan *chan_create(void)
>> {
>> struct l2cap_chan *chan;
>> @@ -632,11 +641,7 @@ static struct l2cap_chan *chan_create(void)
>> if (!chan)
>> return NULL;
>>
>> - l2cap_chan_set_defaults(chan);
>> -
>> - chan->chan_type = L2CAP_CHAN_CONN_ORIENTED;
>> - chan->mode = L2CAP_MODE_LE_FLOWCTL;
>> - chan->imtu = 1280;
>> + chan_init(chan);
>>
>> return chan;
>> }
>> @@ -745,19 +750,19 @@ static inline void chan_ready_cb(struct l2cap_chan *chan)
>> ifup(dev->netdev);
>> }
>>
>> -static inline struct l2cap_chan *chan_new_conn_cb(struct l2cap_chan *pchan)
>> +static inline int chan_new_conn_cb(struct l2cap_conn *conn,
>> + struct l2cap_chan *pchan,
>> + struct l2cap_chan *chan)
>> {
>> - struct l2cap_chan *chan;
>> -
>> - chan = chan_create();
>> - if (!chan)
>> - return NULL;
>> -
>> + chan_init(chan);
>> chan->ops = pchan->ops;
>>
>> + /* Take the conn list reference; see l2cap_new_connection(). */
>> + __l2cap_chan_add(conn, chan);
>> +
>> BT_DBG("chan %p pchan %p", chan, pchan);
>>
>> - return chan;
>> + return 0;
>> }
>>
>> static void unregister_dev(struct lowpan_btle_dev *dev)
>> diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
>> index c4ccfbda9d78..62acf90837fb 100644
>> --- a/net/bluetooth/l2cap_core.c
>> +++ b/net/bluetooth/l2cap_core.c
>> @@ -4007,6 +4007,31 @@ static inline int l2cap_command_rej(struct l2cap_conn *conn,
>> return 0;
>> }
>>
>> +/* Allocate and initialise a channel for an incoming connection.
>> + *
>> + * ->new_connection() initialises the channel and links it into @conn with
>> + * __l2cap_chan_add(). The l2cap_chan_create() reference becomes the one owned
>> + * by the parent subsystem (l2cap_pi(sk)->chan, conn->smp or peer->chan) and is
>> + * released by its teardown callback; the conn list reference is released by
>> + * l2cap_chan_del().
>> + */
>> +static struct l2cap_chan *l2cap_new_connection(struct l2cap_conn *conn,
>> + struct l2cap_chan *pchan)
>> +{
>> + struct l2cap_chan *chan;
>> +
>> + chan = l2cap_chan_create();
>> + if (!chan)
>> + return NULL;
>> +
>> + if (pchan->ops->new_connection(conn, pchan, chan) < 0) {
>> + l2cap_chan_put(chan);
>> + return NULL;
>> + }
>
> I don't quite get why we can't just place __l2cap_chan_add here
> instead of having it called by new_connection callbacks?
>
It's specifically the l2cap_sock_new_connection_cb() case - the very
use-after-free this patch fixes. The __l2cap_chan_add() has to happen while
the parent lock is still held, and only the callback holds that lock.
The reference counting on the new child chan starts at one ref,
owned by the new socket:
/* l2cap_new_connection() */
chan = l2cap_chan_create(); /* refcount = 1 */
if (!chan)
return NULL;
pchan->ops->new_connection(conn, pchan, chan);
and inside the socket callback:
/* l2cap_sock_new_connection_cb() */
lock_sock(parent);
...
sk = l2cap_sock_alloc(..., new_chan); /* sk owns the chan_create ref */
...
l2cap_sock_init(sk, parent);
__l2cap_chan_add(conn, new_chan); /* (A) conn list takes a ref */
bt_accept_enqueue(parent, sk, false); /* (B) sk now on accept queue */
release_sock(parent); /* (C) parent lock dropped */
return 0;
The moment we hit (C), sk is reachable through the parent's accept queue, so
another task can grab and tear it down:
accept() -> l2cap_sock_kill() -> l2cap_sock_put_chan()
chan->data = NULL;
l2cap_chan_put(chan); /* drops the sk's chan ref */
If __l2cap_chan_add() at (A) hadn't already taken the conn list reference,
that put would drop the last ref and free new_chan. Control then returns up
to l2cap_new_connection(), which hands the now-freed chan back to
l2cap_connect():
/* l2cap_connect() - runs after the callback returns */
chan = l2cap_new_connection(conn, pchan);
if (!chan)
goto response;
...
bacpy(&chan->src, &conn->hcon->src); /* <-- UAF on freed chan */
chan->psm = psm;
chan->dcid = scid;
The conn list reference taken at (A), before (C), is what keeps new_chan
alive across the release_sock() window so l2cap_connect() can keep using it.
So __l2cap_chan_add() can't move out to l2cap_new_connection(): by the time
the callback returns, the parent lock is already dropped and chan may already
be freed - which is exactly the race. It has to be taken inside the callback,
under the parent lock, before the socket is exposed.
The other callbacks (6lowpan, smp) have no equivalent lock-drop window.
I kept __l2cap_chan_add() inside all of the ->new_connection() callbacks
just to keep the "callback links the channel into conn" contract uniform.
>> +
>> + return chan;
>> +}
>> +
>> static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
>> u8 *data, u8 rsp_code)
>> {
>> @@ -4053,7 +4078,7 @@ static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
>> goto response;
>> }
>>
>> - chan = pchan->ops->new_connection(pchan);
>> + chan = l2cap_new_connection(conn, pchan);
>> if (!chan)
>> goto response;
>>
>> @@ -4071,8 +4096,6 @@ static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
>> chan->psm = psm;
>> chan->dcid = scid;
>>
>> - __l2cap_chan_add(conn, chan);
>> -
>> dcid = chan->scid;
>>
>> __set_chan_timer(chan, chan->ops->get_sndtimeo(chan));
>> @@ -4955,7 +4978,7 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
>> goto response_unlock;
>> }
>>
>> - chan = pchan->ops->new_connection(pchan);
>> + chan = l2cap_new_connection(conn, pchan);
>> if (!chan) {
>> result = L2CAP_CR_LE_NO_MEM;
>> goto response_unlock;
>> @@ -4970,8 +4993,6 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
>> chan->omtu = mtu;
>> chan->remote_mps = mps;
>>
>> - __l2cap_chan_add(conn, chan);
>> -
>> l2cap_le_flowctl_init(chan, __le16_to_cpu(req->credits));
>>
>> dcid = chan->scid;
>> @@ -5179,7 +5200,7 @@ static inline int l2cap_ecred_conn_req(struct l2cap_conn *conn,
>> continue;
>> }
>>
>> - chan = pchan->ops->new_connection(pchan);
>> + chan = l2cap_new_connection(conn, pchan);
>> if (!chan) {
>> result = L2CAP_CR_LE_NO_MEM;
>> continue;
>> @@ -5194,8 +5215,6 @@ static inline int l2cap_ecred_conn_req(struct l2cap_conn *conn,
>> chan->omtu = mtu;
>> chan->remote_mps = mps;
>>
>> - __l2cap_chan_add(conn, chan);
>> -
>> l2cap_ecred_init(chan, __le16_to_cpu(req->credits));
>>
>> /* Init response */
>> @@ -7470,14 +7489,12 @@ static void l2cap_connect_cfm(struct hci_conn *hcon, u8 status)
>> goto next;
>>
>> l2cap_chan_lock(pchan);
>> - chan = pchan->ops->new_connection(pchan);
>> + chan = l2cap_new_connection(conn, pchan);
>> if (chan) {
>> bacpy(&chan->src, &hcon->src);
>> bacpy(&chan->dst, &hcon->dst);
>> chan->src_type = bdaddr_src_type(hcon);
>> chan->dst_type = dst_type;
>> -
>> - __l2cap_chan_add(conn, chan);
>> }
>>
>> l2cap_chan_unlock(pchan);
>> diff --git a/net/bluetooth/l2cap_sock.c b/net/bluetooth/l2cap_sock.c
>> index 025329636353..87f4c0db5c0c 100644
>> --- a/net/bluetooth/l2cap_sock.c
>> +++ b/net/bluetooth/l2cap_sock.c
>> @@ -46,7 +46,8 @@ static struct bt_sock_list l2cap_sk_list = {
>> static const struct proto_ops l2cap_sock_ops;
>> static void l2cap_sock_init(struct sock *sk, struct sock *parent);
>> static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
>> - int proto, gfp_t prio, int kern);
>> + int proto, gfp_t prio, int kern,
>> + struct l2cap_chan *chan);
>> static void l2cap_sock_cleanup_listen(struct sock *parent);
>>
>> bool l2cap_is_socket(struct socket *sock)
>> @@ -1287,6 +1288,23 @@ static int l2cap_sock_recvmsg(struct socket *sock, struct msghdr *msg,
>> return err;
>> }
>>
>> +/* Release the sock's ref on chan and clear the pointer so that the ref is
>> + * dropped exactly once even if both l2cap_sock_kill() and
>> + * l2cap_sock_destruct() run. Setting chan->data to NULL first stops any other
>> + * task from dereferencing the now-dead sock pointer.
>> + */
>> +static void l2cap_sock_put_chan(struct sock *sk)
>> +{
>> + struct l2cap_chan *chan = l2cap_pi(sk)->chan;
>> +
>> + if (!chan)
>> + return;
>> +
>> + chan->data = NULL;
>> + l2cap_pi(sk)->chan = NULL;
>> + l2cap_chan_put(chan);
>> +}
>> +
>> /* Kill socket (only if zapped and orphan)
>> * Must be called on unlocked socket, with l2cap channel lock.
>> */
>> @@ -1297,13 +1315,9 @@ static void l2cap_sock_kill(struct sock *sk)
>>
>> BT_DBG("sk %p state %s", sk, state_to_string(sk->sk_state));
>>
>> - /* Sock is dead, so set chan data to NULL, avoid other task use invalid
>> - * sock pointer.
>> - */
>> - l2cap_pi(sk)->chan->data = NULL;
>> - /* Kill poor orphan */
>> + l2cap_sock_put_chan(sk);
>>
>> - l2cap_chan_put(l2cap_pi(sk)->chan);
>> + /* Kill poor orphan */
>> sock_set_flag(sk, SOCK_DEAD);
>> sock_put(sk);
>> }
>> @@ -1546,12 +1560,14 @@ static void l2cap_sock_cleanup_listen(struct sock *parent)
>> }
>> }
>>
>> -static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
>> +static int l2cap_sock_new_connection_cb(struct l2cap_conn *conn,
>> + struct l2cap_chan *chan,
>> + struct l2cap_chan *new_chan)
>> {
>> struct sock *sk, *parent = chan->data;
>>
>> if (!parent)
>> - return NULL;
>> + return -EINVAL;
>>
>> lock_sock(parent);
>>
>> @@ -1559,25 +1575,33 @@ static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
>> if (sk_acceptq_is_full(parent)) {
>> BT_DBG("backlog full %d", parent->sk_ack_backlog);
>> release_sock(parent);
>> - return NULL;
>> + return -ENOBUFS;
>> }
>>
>> sk = l2cap_sock_alloc(sock_net(parent), NULL, BTPROTO_L2CAP,
>> - GFP_ATOMIC, 0);
>> + GFP_ATOMIC, 0, new_chan);
>> if (!sk) {
>> release_sock(parent);
>> - return NULL;
>> - }
>> + return -ENOMEM;
>> + }
>>
>> bt_sock_reclassify_lock(sk, BTPROTO_L2CAP);
>>
>> l2cap_sock_init(sk, parent);
>>
>> + /* Link the channel into conn before exposing the new socket via the
>> + * accept queue. Once release_sock() below drops the parent lock the
>> + * socket may be freed by another task, dropping its reference on
>> + * new_chan; the conn list reference taken here keeps new_chan alive so
>> + * the caller can safely use it after ->new_connection() returns.
>> + */
>> + __l2cap_chan_add(conn, new_chan);
>> +
>> bt_accept_enqueue(parent, sk, false);
>>
>> release_sock(parent);
>>
>> - return l2cap_pi(sk)->chan;
>> + return 0;
>> }
>>
>> static int l2cap_sock_recv_cb(struct l2cap_chan *chan, struct sk_buff *skb)
>> @@ -1874,10 +1898,7 @@ static void l2cap_sock_destruct(struct sock *sk)
>>
>> BT_DBG("sk %p", sk);
>>
>> - if (l2cap_pi(sk)->chan) {
>> - l2cap_pi(sk)->chan->data = NULL;
>> - l2cap_chan_put(l2cap_pi(sk)->chan);
>> - }
>> + l2cap_sock_put_chan(sk);
>>
>> list_for_each_entry_safe(rx_busy, next, &l2cap_pi(sk)->rx_busy, list) {
>> kfree_skb(rx_busy->skb);
>> @@ -1978,10 +1999,10 @@ static struct proto l2cap_proto = {
>> };
>>
>> static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
>> - int proto, gfp_t prio, int kern)
>> + int proto, gfp_t prio, int kern,
>> + struct l2cap_chan *chan)
>> {
>> struct sock *sk;
>> - struct l2cap_chan *chan;
>>
>> sk = bt_sock_alloc(net, sock, &l2cap_proto, proto, prio, kern);
>> if (!sk)
>> @@ -1992,16 +2013,7 @@ static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
>>
>> INIT_LIST_HEAD(&l2cap_pi(sk)->rx_busy);
>>
>> - chan = l2cap_chan_create();
>> - if (!chan) {
>> - sk_free(sk);
>> - if (sock)
>> - sock->sk = NULL;
>> - return NULL;
>> - }
>> -
>> - l2cap_chan_hold(chan);
>> -
>> + /* The sock takes ownership of the caller's reference on chan. */
>> l2cap_pi(sk)->chan = chan;
>>
>> return sk;
>> @@ -2011,6 +2023,7 @@ static int l2cap_sock_create(struct net *net, struct socket *sock, int protocol,
>> int kern)
>> {
>> struct sock *sk;
>> + struct l2cap_chan *chan;
>>
>> BT_DBG("sock %p", sock);
>>
>> @@ -2025,10 +2038,16 @@ static int l2cap_sock_create(struct net *net, struct socket *sock, int protocol,
>>
>> sock->ops = &l2cap_sock_ops;
>>
>> - sk = l2cap_sock_alloc(net, sock, protocol, GFP_ATOMIC, kern);
>> - if (!sk)
>> + chan = l2cap_chan_create();
>> + if (!chan)
>> return -ENOMEM;
>>
>> + sk = l2cap_sock_alloc(net, sock, protocol, GFP_ATOMIC, kern, chan);
>> + if (!sk) {
>> + l2cap_chan_put(chan);
>> + return -ENOMEM;
>> + }
>> +
>> l2cap_sock_init(sk, NULL);
>> bt_sock_link(&l2cap_sk_list, sk);
>> return 0;
>> diff --git a/net/bluetooth/smp.c b/net/bluetooth/smp.c
>> index 1739c1989dbd..2d31c3c7bbc0 100644
>> --- a/net/bluetooth/smp.c
>> +++ b/net/bluetooth/smp.c
>> @@ -3204,16 +3204,12 @@ static const struct l2cap_ops smp_chan_ops = {
>> .get_sndtimeo = l2cap_chan_no_get_sndtimeo,
>> };
>>
>> -static inline struct l2cap_chan *smp_new_conn_cb(struct l2cap_chan *pchan)
>> +static inline int smp_new_conn_cb(struct l2cap_conn *conn,
>> + struct l2cap_chan *pchan,
>> + struct l2cap_chan *chan)
>> {
>> - struct l2cap_chan *chan;
>> -
>> BT_DBG("pchan %p", pchan);
>>
>> - chan = l2cap_chan_create();
>> - if (!chan)
>> - return NULL;
>> -
>> chan->chan_type = pchan->chan_type;
>> chan->ops = &smp_chan_ops;
>> chan->scid = pchan->scid;
>> @@ -3229,9 +3225,12 @@ static inline struct l2cap_chan *smp_new_conn_cb(struct l2cap_chan *pchan)
>> */
>> atomic_set(&chan->nesting, L2CAP_NESTING_SMP);
>>
>> - BT_DBG("created chan %p", chan);
>> + /* Take the conn list reference; see l2cap_new_connection(). */
>> + __l2cap_chan_add(conn, chan);
>>
>> - return chan;
>> + BT_DBG("initialised chan %p", chan);
>> +
>> + return 0;
>> }
>>
>> static const struct l2cap_ops smp_root_chan_ops = {
>> --
>> 2.54.0
>>
>
>
> --
> Luiz Augusto von Dentz
Best,
Siwei
^ permalink raw reply [flat|nested] 20+ messages in thread* Re: [PATCH v9 1/1] Bluetooth: L2CAP: Fix use-after-free in l2cap_sock_new_connection_cb()
2026-06-04 15:52 ` Siwei Zhang
@ 2026-06-08 13:28 ` Siwei Zhang
2026-06-10 15:57 ` Luiz Augusto von Dentz
0 siblings, 1 reply; 20+ messages in thread
From: Siwei Zhang @ 2026-06-08 13:28 UTC (permalink / raw)
To: Luiz Augusto von Dentz; +Cc: linux-bluetooth
Hi Luiz,
On Thu, Jun 4, 2026, at 11:52 AM, Siwei Zhang wrote:
> Hi Luiz,
>
> On Wed, Jun 3, 2026, at 2:17 PM, Luiz Augusto von Dentz wrote:
>> Hi Siwei,
>>
>> On Wed, Jun 3, 2026 at 11:09 AM Siwei Zhang <oss@fourdim.xyz> wrote:
>>>
>>> l2cap_sock_new_connection_cb() accesses l2cap_pi(sk)->chan after
>>> release_sock(parent). Once the parent lock is released, the child
>>> socket sk can be freed by another task.
>>>
>>> Allocate the channel outside the func to prevent this.
>>>
>>> Fixes: 8ffb929098a5 ("Bluetooth: Remove parent socket usage from l2cap_core.c")
>>> Cc: stable@kernel.org
>>> Assisted-by: Claude:claude-opus-4-8
>>> Signed-off-by: Siwei Zhang <oss@fourdim.xyz>
>>> ---
>>> include/net/bluetooth/l2cap.h | 10 +++--
>>> net/bluetooth/6lowpan.c | 31 +++++++------
>>> net/bluetooth/l2cap_core.c | 41 ++++++++++++-----
>>> net/bluetooth/l2cap_sock.c | 83 +++++++++++++++++++++--------------
>>> net/bluetooth/smp.c | 17 ++++---
>>> 5 files changed, 113 insertions(+), 69 deletions(-)
>>>
>>> diff --git a/include/net/bluetooth/l2cap.h b/include/net/bluetooth/l2cap.h
>>> index e0a1f2293679..7f5e4647f6e0 100644
>>> --- a/include/net/bluetooth/l2cap.h
>>> +++ b/include/net/bluetooth/l2cap.h
>>> @@ -620,7 +620,9 @@ struct l2cap_chan {
>>> struct l2cap_ops {
>>> char *name;
>>>
>>> - struct l2cap_chan *(*new_connection) (struct l2cap_chan *chan);
>>> + int (*new_connection)(struct l2cap_conn *conn,
>>> + struct l2cap_chan *chan,
>>> + struct l2cap_chan *new_chan);
>>> int (*recv) (struct l2cap_chan * chan,
>>> struct sk_buff *skb);
>>> void (*teardown) (struct l2cap_chan *chan, int err);
>>> @@ -884,9 +886,11 @@ static inline __u16 __next_seq(struct l2cap_chan *chan, __u16 seq)
>>> return (seq + 1) % (chan->tx_win_max + 1);
>>> }
>>>
>>> -static inline struct l2cap_chan *l2cap_chan_no_new_connection(struct l2cap_chan *chan)
>>> +static inline int l2cap_chan_no_new_connection(struct l2cap_conn *conn,
>>> + struct l2cap_chan *chan,
>>> + struct l2cap_chan *new_chan)
>>> {
>>> - return NULL;
>>> + return -EOPNOTSUPP;
>>> }
>>>
>>> static inline int l2cap_chan_no_recv(struct l2cap_chan *chan, struct sk_buff *skb)
>>> diff --git a/net/bluetooth/6lowpan.c b/net/bluetooth/6lowpan.c
>>> index cb1e329d66fd..94863af97a44 100644
>>> --- a/net/bluetooth/6lowpan.c
>>> +++ b/net/bluetooth/6lowpan.c
>>> @@ -624,6 +624,15 @@ static bool is_bt_6lowpan(struct hci_conn *hcon)
>>> return true;
>>> }
>>>
>>> +static void chan_init(struct l2cap_chan *chan)
>>> +{
>>> + l2cap_chan_set_defaults(chan);
>>> +
>>> + chan->chan_type = L2CAP_CHAN_CONN_ORIENTED;
>>> + chan->mode = L2CAP_MODE_LE_FLOWCTL;
>>> + chan->imtu = 1280;
>>> +}
>>> +
>>> static struct l2cap_chan *chan_create(void)
>>> {
>>> struct l2cap_chan *chan;
>>> @@ -632,11 +641,7 @@ static struct l2cap_chan *chan_create(void)
>>> if (!chan)
>>> return NULL;
>>>
>>> - l2cap_chan_set_defaults(chan);
>>> -
>>> - chan->chan_type = L2CAP_CHAN_CONN_ORIENTED;
>>> - chan->mode = L2CAP_MODE_LE_FLOWCTL;
>>> - chan->imtu = 1280;
>>> + chan_init(chan);
>>>
>>> return chan;
>>> }
>>> @@ -745,19 +750,19 @@ static inline void chan_ready_cb(struct l2cap_chan *chan)
>>> ifup(dev->netdev);
>>> }
>>>
>>> -static inline struct l2cap_chan *chan_new_conn_cb(struct l2cap_chan *pchan)
>>> +static inline int chan_new_conn_cb(struct l2cap_conn *conn,
>>> + struct l2cap_chan *pchan,
>>> + struct l2cap_chan *chan)
>>> {
>>> - struct l2cap_chan *chan;
>>> -
>>> - chan = chan_create();
>>> - if (!chan)
>>> - return NULL;
>>> -
>>> + chan_init(chan);
>>> chan->ops = pchan->ops;
>>>
>>> + /* Take the conn list reference; see l2cap_new_connection(). */
>>> + __l2cap_chan_add(conn, chan);
>>> +
>>> BT_DBG("chan %p pchan %p", chan, pchan);
>>>
>>> - return chan;
>>> + return 0;
>>> }
>>>
>>> static void unregister_dev(struct lowpan_btle_dev *dev)
>>> diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
>>> index c4ccfbda9d78..62acf90837fb 100644
>>> --- a/net/bluetooth/l2cap_core.c
>>> +++ b/net/bluetooth/l2cap_core.c
>>> @@ -4007,6 +4007,31 @@ static inline int l2cap_command_rej(struct l2cap_conn *conn,
>>> return 0;
>>> }
>>>
>>> +/* Allocate and initialise a channel for an incoming connection.
>>> + *
>>> + * ->new_connection() initialises the channel and links it into @conn with
>>> + * __l2cap_chan_add(). The l2cap_chan_create() reference becomes the one owned
>>> + * by the parent subsystem (l2cap_pi(sk)->chan, conn->smp or peer->chan) and is
>>> + * released by its teardown callback; the conn list reference is released by
>>> + * l2cap_chan_del().
>>> + */
>>> +static struct l2cap_chan *l2cap_new_connection(struct l2cap_conn *conn,
>>> + struct l2cap_chan *pchan)
>>> +{
>>> + struct l2cap_chan *chan;
>>> +
>>> + chan = l2cap_chan_create();
>>> + if (!chan)
>>> + return NULL;
>>> +
>>> + if (pchan->ops->new_connection(conn, pchan, chan) < 0) {
>>> + l2cap_chan_put(chan);
>>> + return NULL;
>>> + }
>>
>> I don't quite get why we can't just place __l2cap_chan_add here
>> instead of having it called by new_connection callbacks?
>>
>
> It's specifically the l2cap_sock_new_connection_cb() case - the very
> use-after-free this patch fixes. The __l2cap_chan_add() has to happen while
> the parent lock is still held, and only the callback holds that lock.
>
> The reference counting on the new child chan starts at one ref,
> owned by the new socket:
>
> /* l2cap_new_connection() */
> chan = l2cap_chan_create(); /* refcount = 1 */
> if (!chan)
> return NULL;
>
> pchan->ops->new_connection(conn, pchan, chan);
>
> and inside the socket callback:
>
> /* l2cap_sock_new_connection_cb() */
> lock_sock(parent);
> ...
> sk = l2cap_sock_alloc(..., new_chan); /* sk owns the chan_create ref */
> ...
> l2cap_sock_init(sk, parent);
>
> __l2cap_chan_add(conn, new_chan); /* (A) conn list takes a ref */
> bt_accept_enqueue(parent, sk, false); /* (B) sk now on accept queue */
>
> release_sock(parent); /* (C) parent lock dropped */
> return 0;
>
> The moment we hit (C), sk is reachable through the parent's accept queue, so
> another task can grab and tear it down:
>
> accept() -> l2cap_sock_kill() -> l2cap_sock_put_chan()
> chan->data = NULL;
> l2cap_chan_put(chan); /* drops the sk's chan ref */
>
> If __l2cap_chan_add() at (A) hadn't already taken the conn list reference,
> that put would drop the last ref and free new_chan. Control then returns up
> to l2cap_new_connection(), which hands the now-freed chan back to
> l2cap_connect():
>
> /* l2cap_connect() - runs after the callback returns */
> chan = l2cap_new_connection(conn, pchan);
> if (!chan)
> goto response;
> ...
> bacpy(&chan->src, &conn->hcon->src); /* <-- UAF on freed chan */
> chan->psm = psm;
> chan->dcid = scid;
>
> The conn list reference taken at (A), before (C), is what keeps new_chan
> alive across the release_sock() window so l2cap_connect() can keep using it.
>
> So __l2cap_chan_add() can't move out to l2cap_new_connection(): by the time
> the callback returns, the parent lock is already dropped and chan may already
> be freed - which is exactly the race. It has to be taken inside the callback,
> under the parent lock, before the socket is exposed.
>
> The other callbacks (6lowpan, smp) have no equivalent lock-drop window.
> I kept __l2cap_chan_add() inside all of the ->new_connection() callbacks
> just to keep the "callback links the channel into conn" contract uniform.
>
Could you please check this?
Just remind you in case you miss this.
>>> +
>>> + return chan;
>>> +}
>>> +
>>> static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
>>> u8 *data, u8 rsp_code)
>>> {
>>> @@ -4053,7 +4078,7 @@ static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
>>> goto response;
>>> }
>>>
>>> - chan = pchan->ops->new_connection(pchan);
>>> + chan = l2cap_new_connection(conn, pchan);
>>> if (!chan)
>>> goto response;
>>>
>>> @@ -4071,8 +4096,6 @@ static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
>>> chan->psm = psm;
>>> chan->dcid = scid;
>>>
>>> - __l2cap_chan_add(conn, chan);
>>> -
>>> dcid = chan->scid;
>>>
>>> __set_chan_timer(chan, chan->ops->get_sndtimeo(chan));
>>> @@ -4955,7 +4978,7 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
>>> goto response_unlock;
>>> }
>>>
>>> - chan = pchan->ops->new_connection(pchan);
>>> + chan = l2cap_new_connection(conn, pchan);
>>> if (!chan) {
>>> result = L2CAP_CR_LE_NO_MEM;
>>> goto response_unlock;
>>> @@ -4970,8 +4993,6 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
>>> chan->omtu = mtu;
>>> chan->remote_mps = mps;
>>>
>>> - __l2cap_chan_add(conn, chan);
>>> -
>>> l2cap_le_flowctl_init(chan, __le16_to_cpu(req->credits));
>>>
>>> dcid = chan->scid;
>>> @@ -5179,7 +5200,7 @@ static inline int l2cap_ecred_conn_req(struct l2cap_conn *conn,
>>> continue;
>>> }
>>>
>>> - chan = pchan->ops->new_connection(pchan);
>>> + chan = l2cap_new_connection(conn, pchan);
>>> if (!chan) {
>>> result = L2CAP_CR_LE_NO_MEM;
>>> continue;
>>> @@ -5194,8 +5215,6 @@ static inline int l2cap_ecred_conn_req(struct l2cap_conn *conn,
>>> chan->omtu = mtu;
>>> chan->remote_mps = mps;
>>>
>>> - __l2cap_chan_add(conn, chan);
>>> -
>>> l2cap_ecred_init(chan, __le16_to_cpu(req->credits));
>>>
>>> /* Init response */
>>> @@ -7470,14 +7489,12 @@ static void l2cap_connect_cfm(struct hci_conn *hcon, u8 status)
>>> goto next;
>>>
>>> l2cap_chan_lock(pchan);
>>> - chan = pchan->ops->new_connection(pchan);
>>> + chan = l2cap_new_connection(conn, pchan);
>>> if (chan) {
>>> bacpy(&chan->src, &hcon->src);
>>> bacpy(&chan->dst, &hcon->dst);
>>> chan->src_type = bdaddr_src_type(hcon);
>>> chan->dst_type = dst_type;
>>> -
>>> - __l2cap_chan_add(conn, chan);
>>> }
>>>
>>> l2cap_chan_unlock(pchan);
>>> diff --git a/net/bluetooth/l2cap_sock.c b/net/bluetooth/l2cap_sock.c
>>> index 025329636353..87f4c0db5c0c 100644
>>> --- a/net/bluetooth/l2cap_sock.c
>>> +++ b/net/bluetooth/l2cap_sock.c
>>> @@ -46,7 +46,8 @@ static struct bt_sock_list l2cap_sk_list = {
>>> static const struct proto_ops l2cap_sock_ops;
>>> static void l2cap_sock_init(struct sock *sk, struct sock *parent);
>>> static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
>>> - int proto, gfp_t prio, int kern);
>>> + int proto, gfp_t prio, int kern,
>>> + struct l2cap_chan *chan);
>>> static void l2cap_sock_cleanup_listen(struct sock *parent);
>>>
>>> bool l2cap_is_socket(struct socket *sock)
>>> @@ -1287,6 +1288,23 @@ static int l2cap_sock_recvmsg(struct socket *sock, struct msghdr *msg,
>>> return err;
>>> }
>>>
>>> +/* Release the sock's ref on chan and clear the pointer so that the ref is
>>> + * dropped exactly once even if both l2cap_sock_kill() and
>>> + * l2cap_sock_destruct() run. Setting chan->data to NULL first stops any other
>>> + * task from dereferencing the now-dead sock pointer.
>>> + */
>>> +static void l2cap_sock_put_chan(struct sock *sk)
>>> +{
>>> + struct l2cap_chan *chan = l2cap_pi(sk)->chan;
>>> +
>>> + if (!chan)
>>> + return;
>>> +
>>> + chan->data = NULL;
>>> + l2cap_pi(sk)->chan = NULL;
>>> + l2cap_chan_put(chan);
>>> +}
>>> +
>>> /* Kill socket (only if zapped and orphan)
>>> * Must be called on unlocked socket, with l2cap channel lock.
>>> */
>>> @@ -1297,13 +1315,9 @@ static void l2cap_sock_kill(struct sock *sk)
>>>
>>> BT_DBG("sk %p state %s", sk, state_to_string(sk->sk_state));
>>>
>>> - /* Sock is dead, so set chan data to NULL, avoid other task use invalid
>>> - * sock pointer.
>>> - */
>>> - l2cap_pi(sk)->chan->data = NULL;
>>> - /* Kill poor orphan */
>>> + l2cap_sock_put_chan(sk);
>>>
>>> - l2cap_chan_put(l2cap_pi(sk)->chan);
>>> + /* Kill poor orphan */
>>> sock_set_flag(sk, SOCK_DEAD);
>>> sock_put(sk);
>>> }
>>> @@ -1546,12 +1560,14 @@ static void l2cap_sock_cleanup_listen(struct sock *parent)
>>> }
>>> }
>>>
>>> -static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
>>> +static int l2cap_sock_new_connection_cb(struct l2cap_conn *conn,
>>> + struct l2cap_chan *chan,
>>> + struct l2cap_chan *new_chan)
>>> {
>>> struct sock *sk, *parent = chan->data;
>>>
>>> if (!parent)
>>> - return NULL;
>>> + return -EINVAL;
>>>
>>> lock_sock(parent);
>>>
>>> @@ -1559,25 +1575,33 @@ static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
>>> if (sk_acceptq_is_full(parent)) {
>>> BT_DBG("backlog full %d", parent->sk_ack_backlog);
>>> release_sock(parent);
>>> - return NULL;
>>> + return -ENOBUFS;
>>> }
>>>
>>> sk = l2cap_sock_alloc(sock_net(parent), NULL, BTPROTO_L2CAP,
>>> - GFP_ATOMIC, 0);
>>> + GFP_ATOMIC, 0, new_chan);
>>> if (!sk) {
>>> release_sock(parent);
>>> - return NULL;
>>> - }
>>> + return -ENOMEM;
>>> + }
>>>
>>> bt_sock_reclassify_lock(sk, BTPROTO_L2CAP);
>>>
>>> l2cap_sock_init(sk, parent);
>>>
>>> + /* Link the channel into conn before exposing the new socket via the
>>> + * accept queue. Once release_sock() below drops the parent lock the
>>> + * socket may be freed by another task, dropping its reference on
>>> + * new_chan; the conn list reference taken here keeps new_chan alive so
>>> + * the caller can safely use it after ->new_connection() returns.
>>> + */
>>> + __l2cap_chan_add(conn, new_chan);
>>> +
>>> bt_accept_enqueue(parent, sk, false);
>>>
>>> release_sock(parent);
>>>
>>> - return l2cap_pi(sk)->chan;
>>> + return 0;
>>> }
>>>
>>> static int l2cap_sock_recv_cb(struct l2cap_chan *chan, struct sk_buff *skb)
>>> @@ -1874,10 +1898,7 @@ static void l2cap_sock_destruct(struct sock *sk)
>>>
>>> BT_DBG("sk %p", sk);
>>>
>>> - if (l2cap_pi(sk)->chan) {
>>> - l2cap_pi(sk)->chan->data = NULL;
>>> - l2cap_chan_put(l2cap_pi(sk)->chan);
>>> - }
>>> + l2cap_sock_put_chan(sk);
>>>
>>> list_for_each_entry_safe(rx_busy, next, &l2cap_pi(sk)->rx_busy, list) {
>>> kfree_skb(rx_busy->skb);
>>> @@ -1978,10 +1999,10 @@ static struct proto l2cap_proto = {
>>> };
>>>
>>> static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
>>> - int proto, gfp_t prio, int kern)
>>> + int proto, gfp_t prio, int kern,
>>> + struct l2cap_chan *chan)
>>> {
>>> struct sock *sk;
>>> - struct l2cap_chan *chan;
>>>
>>> sk = bt_sock_alloc(net, sock, &l2cap_proto, proto, prio, kern);
>>> if (!sk)
>>> @@ -1992,16 +2013,7 @@ static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
>>>
>>> INIT_LIST_HEAD(&l2cap_pi(sk)->rx_busy);
>>>
>>> - chan = l2cap_chan_create();
>>> - if (!chan) {
>>> - sk_free(sk);
>>> - if (sock)
>>> - sock->sk = NULL;
>>> - return NULL;
>>> - }
>>> -
>>> - l2cap_chan_hold(chan);
>>> -
>>> + /* The sock takes ownership of the caller's reference on chan. */
>>> l2cap_pi(sk)->chan = chan;
>>>
>>> return sk;
>>> @@ -2011,6 +2023,7 @@ static int l2cap_sock_create(struct net *net, struct socket *sock, int protocol,
>>> int kern)
>>> {
>>> struct sock *sk;
>>> + struct l2cap_chan *chan;
>>>
>>> BT_DBG("sock %p", sock);
>>>
>>> @@ -2025,10 +2038,16 @@ static int l2cap_sock_create(struct net *net, struct socket *sock, int protocol,
>>>
>>> sock->ops = &l2cap_sock_ops;
>>>
>>> - sk = l2cap_sock_alloc(net, sock, protocol, GFP_ATOMIC, kern);
>>> - if (!sk)
>>> + chan = l2cap_chan_create();
>>> + if (!chan)
>>> return -ENOMEM;
>>>
>>> + sk = l2cap_sock_alloc(net, sock, protocol, GFP_ATOMIC, kern, chan);
>>> + if (!sk) {
>>> + l2cap_chan_put(chan);
>>> + return -ENOMEM;
>>> + }
>>> +
>>> l2cap_sock_init(sk, NULL);
>>> bt_sock_link(&l2cap_sk_list, sk);
>>> return 0;
>>> diff --git a/net/bluetooth/smp.c b/net/bluetooth/smp.c
>>> index 1739c1989dbd..2d31c3c7bbc0 100644
>>> --- a/net/bluetooth/smp.c
>>> +++ b/net/bluetooth/smp.c
>>> @@ -3204,16 +3204,12 @@ static const struct l2cap_ops smp_chan_ops = {
>>> .get_sndtimeo = l2cap_chan_no_get_sndtimeo,
>>> };
>>>
>>> -static inline struct l2cap_chan *smp_new_conn_cb(struct l2cap_chan *pchan)
>>> +static inline int smp_new_conn_cb(struct l2cap_conn *conn,
>>> + struct l2cap_chan *pchan,
>>> + struct l2cap_chan *chan)
>>> {
>>> - struct l2cap_chan *chan;
>>> -
>>> BT_DBG("pchan %p", pchan);
>>>
>>> - chan = l2cap_chan_create();
>>> - if (!chan)
>>> - return NULL;
>>> -
>>> chan->chan_type = pchan->chan_type;
>>> chan->ops = &smp_chan_ops;
>>> chan->scid = pchan->scid;
>>> @@ -3229,9 +3225,12 @@ static inline struct l2cap_chan *smp_new_conn_cb(struct l2cap_chan *pchan)
>>> */
>>> atomic_set(&chan->nesting, L2CAP_NESTING_SMP);
>>>
>>> - BT_DBG("created chan %p", chan);
>>> + /* Take the conn list reference; see l2cap_new_connection(). */
>>> + __l2cap_chan_add(conn, chan);
>>>
>>> - return chan;
>>> + BT_DBG("initialised chan %p", chan);
>>> +
>>> + return 0;
>>> }
>>>
>>> static const struct l2cap_ops smp_root_chan_ops = {
>>> --
>>> 2.54.0
>>>
>>
>>
>> --
>> Luiz Augusto von Dentz
>
> Best,
> Siwei
Best,
Siwei
^ permalink raw reply [flat|nested] 20+ messages in thread* Re: [PATCH v9 1/1] Bluetooth: L2CAP: Fix use-after-free in l2cap_sock_new_connection_cb()
2026-06-08 13:28 ` Siwei Zhang
@ 2026-06-10 15:57 ` Luiz Augusto von Dentz
2026-06-10 16:32 ` Siwei Zhang
0 siblings, 1 reply; 20+ messages in thread
From: Luiz Augusto von Dentz @ 2026-06-10 15:57 UTC (permalink / raw)
To: Siwei Zhang; +Cc: linux-bluetooth
Hi Siwei,
On Mon, Jun 8, 2026 at 9:29 AM Siwei Zhang <oss@fourdim.xyz> wrote:
>
> Hi Luiz,
>
> On Thu, Jun 4, 2026, at 11:52 AM, Siwei Zhang wrote:
> > Hi Luiz,
> >
> > On Wed, Jun 3, 2026, at 2:17 PM, Luiz Augusto von Dentz wrote:
> >> Hi Siwei,
> >>
> >> On Wed, Jun 3, 2026 at 11:09 AM Siwei Zhang <oss@fourdim.xyz> wrote:
> >>>
> >>> l2cap_sock_new_connection_cb() accesses l2cap_pi(sk)->chan after
> >>> release_sock(parent). Once the parent lock is released, the child
> >>> socket sk can be freed by another task.
> >>>
> >>> Allocate the channel outside the func to prevent this.
> >>>
> >>> Fixes: 8ffb929098a5 ("Bluetooth: Remove parent socket usage from l2cap_core.c")
> >>> Cc: stable@kernel.org
> >>> Assisted-by: Claude:claude-opus-4-8
> >>> Signed-off-by: Siwei Zhang <oss@fourdim.xyz>
> >>> ---
> >>> include/net/bluetooth/l2cap.h | 10 +++--
> >>> net/bluetooth/6lowpan.c | 31 +++++++------
> >>> net/bluetooth/l2cap_core.c | 41 ++++++++++++-----
> >>> net/bluetooth/l2cap_sock.c | 83 +++++++++++++++++++++--------------
> >>> net/bluetooth/smp.c | 17 ++++---
> >>> 5 files changed, 113 insertions(+), 69 deletions(-)
> >>>
> >>> diff --git a/include/net/bluetooth/l2cap.h b/include/net/bluetooth/l2cap.h
> >>> index e0a1f2293679..7f5e4647f6e0 100644
> >>> --- a/include/net/bluetooth/l2cap.h
> >>> +++ b/include/net/bluetooth/l2cap.h
> >>> @@ -620,7 +620,9 @@ struct l2cap_chan {
> >>> struct l2cap_ops {
> >>> char *name;
> >>>
> >>> - struct l2cap_chan *(*new_connection) (struct l2cap_chan *chan);
> >>> + int (*new_connection)(struct l2cap_conn *conn,
> >>> + struct l2cap_chan *chan,
> >>> + struct l2cap_chan *new_chan);
> >>> int (*recv) (struct l2cap_chan * chan,
> >>> struct sk_buff *skb);
> >>> void (*teardown) (struct l2cap_chan *chan, int err);
> >>> @@ -884,9 +886,11 @@ static inline __u16 __next_seq(struct l2cap_chan *chan, __u16 seq)
> >>> return (seq + 1) % (chan->tx_win_max + 1);
> >>> }
> >>>
> >>> -static inline struct l2cap_chan *l2cap_chan_no_new_connection(struct l2cap_chan *chan)
> >>> +static inline int l2cap_chan_no_new_connection(struct l2cap_conn *conn,
> >>> + struct l2cap_chan *chan,
> >>> + struct l2cap_chan *new_chan)
> >>> {
> >>> - return NULL;
> >>> + return -EOPNOTSUPP;
> >>> }
> >>>
> >>> static inline int l2cap_chan_no_recv(struct l2cap_chan *chan, struct sk_buff *skb)
> >>> diff --git a/net/bluetooth/6lowpan.c b/net/bluetooth/6lowpan.c
> >>> index cb1e329d66fd..94863af97a44 100644
> >>> --- a/net/bluetooth/6lowpan.c
> >>> +++ b/net/bluetooth/6lowpan.c
> >>> @@ -624,6 +624,15 @@ static bool is_bt_6lowpan(struct hci_conn *hcon)
> >>> return true;
> >>> }
> >>>
> >>> +static void chan_init(struct l2cap_chan *chan)
> >>> +{
> >>> + l2cap_chan_set_defaults(chan);
> >>> +
> >>> + chan->chan_type = L2CAP_CHAN_CONN_ORIENTED;
> >>> + chan->mode = L2CAP_MODE_LE_FLOWCTL;
> >>> + chan->imtu = 1280;
> >>> +}
> >>> +
> >>> static struct l2cap_chan *chan_create(void)
> >>> {
> >>> struct l2cap_chan *chan;
> >>> @@ -632,11 +641,7 @@ static struct l2cap_chan *chan_create(void)
> >>> if (!chan)
> >>> return NULL;
> >>>
> >>> - l2cap_chan_set_defaults(chan);
> >>> -
> >>> - chan->chan_type = L2CAP_CHAN_CONN_ORIENTED;
> >>> - chan->mode = L2CAP_MODE_LE_FLOWCTL;
> >>> - chan->imtu = 1280;
> >>> + chan_init(chan);
> >>>
> >>> return chan;
> >>> }
> >>> @@ -745,19 +750,19 @@ static inline void chan_ready_cb(struct l2cap_chan *chan)
> >>> ifup(dev->netdev);
> >>> }
> >>>
> >>> -static inline struct l2cap_chan *chan_new_conn_cb(struct l2cap_chan *pchan)
> >>> +static inline int chan_new_conn_cb(struct l2cap_conn *conn,
> >>> + struct l2cap_chan *pchan,
> >>> + struct l2cap_chan *chan)
> >>> {
> >>> - struct l2cap_chan *chan;
> >>> -
> >>> - chan = chan_create();
> >>> - if (!chan)
> >>> - return NULL;
> >>> -
> >>> + chan_init(chan);
> >>> chan->ops = pchan->ops;
> >>>
> >>> + /* Take the conn list reference; see l2cap_new_connection(). */
> >>> + __l2cap_chan_add(conn, chan);
> >>> +
> >>> BT_DBG("chan %p pchan %p", chan, pchan);
> >>>
> >>> - return chan;
> >>> + return 0;
> >>> }
> >>>
> >>> static void unregister_dev(struct lowpan_btle_dev *dev)
> >>> diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
> >>> index c4ccfbda9d78..62acf90837fb 100644
> >>> --- a/net/bluetooth/l2cap_core.c
> >>> +++ b/net/bluetooth/l2cap_core.c
> >>> @@ -4007,6 +4007,31 @@ static inline int l2cap_command_rej(struct l2cap_conn *conn,
> >>> return 0;
> >>> }
> >>>
> >>> +/* Allocate and initialise a channel for an incoming connection.
> >>> + *
> >>> + * ->new_connection() initialises the channel and links it into @conn with
> >>> + * __l2cap_chan_add(). The l2cap_chan_create() reference becomes the one owned
> >>> + * by the parent subsystem (l2cap_pi(sk)->chan, conn->smp or peer->chan) and is
> >>> + * released by its teardown callback; the conn list reference is released by
> >>> + * l2cap_chan_del().
> >>> + */
> >>> +static struct l2cap_chan *l2cap_new_connection(struct l2cap_conn *conn,
> >>> + struct l2cap_chan *pchan)
> >>> +{
> >>> + struct l2cap_chan *chan;
> >>> +
> >>> + chan = l2cap_chan_create();
> >>> + if (!chan)
> >>> + return NULL;
> >>> +
> >>> + if (pchan->ops->new_connection(conn, pchan, chan) < 0) {
> >>> + l2cap_chan_put(chan);
> >>> + return NULL;
> >>> + }
> >>
> >> I don't quite get why we can't just place __l2cap_chan_add here
> >> instead of having it called by new_connection callbacks?
> >>
> >
> > It's specifically the l2cap_sock_new_connection_cb() case - the very
> > use-after-free this patch fixes. The __l2cap_chan_add() has to happen while
> > the parent lock is still held, and only the callback holds that lock.
> >
> > The reference counting on the new child chan starts at one ref,
> > owned by the new socket:
> >
> > /* l2cap_new_connection() */
> > chan = l2cap_chan_create(); /* refcount = 1 */
> > if (!chan)
> > return NULL;
> >
> > pchan->ops->new_connection(conn, pchan, chan);
> >
> > and inside the socket callback:
> >
> > /* l2cap_sock_new_connection_cb() */
> > lock_sock(parent);
> > ...
> > sk = l2cap_sock_alloc(..., new_chan); /* sk owns the chan_create ref */
> > ...
> > l2cap_sock_init(sk, parent);
> >
> > __l2cap_chan_add(conn, new_chan); /* (A) conn list takes a ref */
> > bt_accept_enqueue(parent, sk, false); /* (B) sk now on accept queue */
> >
> > release_sock(parent); /* (C) parent lock dropped */
> > return 0;
> >
> > The moment we hit (C), sk is reachable through the parent's accept queue, so
> > another task can grab and tear it down:
> >
> > accept() -> l2cap_sock_kill() -> l2cap_sock_put_chan()
> > chan->data = NULL;
> > l2cap_chan_put(chan); /* drops the sk's chan ref */
> >
> > If __l2cap_chan_add() at (A) hadn't already taken the conn list reference,
> > that put would drop the last ref and free new_chan. Control then returns up
> > to l2cap_new_connection(), which hands the now-freed chan back to
> > l2cap_connect():
> >
> > /* l2cap_connect() - runs after the callback returns */
> > chan = l2cap_new_connection(conn, pchan);
> > if (!chan)
> > goto response;
> > ...
> > bacpy(&chan->src, &conn->hcon->src); /* <-- UAF on freed chan */
> > chan->psm = psm;
> > chan->dcid = scid;
> >
> > The conn list reference taken at (A), before (C), is what keeps new_chan
> > alive across the release_sock() window so l2cap_connect() can keep using it.
> >
> > So __l2cap_chan_add() can't move out to l2cap_new_connection(): by the time
> > the callback returns, the parent lock is already dropped and chan may already
> > be freed - which is exactly the race. It has to be taken inside the callback,
> > under the parent lock, before the socket is exposed.
If you do after l2cap_new_connection, yes, but what if you do before
you call ->new_connect:
diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
index 3991c179b98f..cd864f400985 100644
--- a/net/bluetooth/l2cap_core.c
+++ b/net/bluetooth/l2cap_core.c
@@ -4027,8 +4027,10 @@ static struct l2cap_chan
*l2cap_new_connection(struct l2cap_conn *conn,
if (!chan)
return NULL;
+ __l2cap_chan_add(conn, chan);
+
if (pchan->ops->new_connection(conn, pchan, chan) < 0) {
- l2cap_chan_put(chan);
+ l2cap_chan_del(chan, 0);
return NULL;
}
This means we have to call l2cap_chan_del if new_connection fails, but
we can eliminate other functions having to call __l2cap_chan_add with
this. If the issue is then having l2cap_chan_del called concurrently,
we can add a l2cap_chan_hold; actually, we may need a reference anyway
since sk can be released and call l2cap_chan_del, releasing both
references.
> > The other callbacks (6lowpan, smp) have no equivalent lock-drop window.
> > I kept __l2cap_chan_add() inside all of the ->new_connection() callbacks
> > just to keep the "callback links the channel into conn" contract uniform.
> >
>
> Could you please check this?
> Just remind you in case you miss this.
>
> >>> +
> >>> + return chan;
> >>> +}
> >>> +
> >>> static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
> >>> u8 *data, u8 rsp_code)
> >>> {
> >>> @@ -4053,7 +4078,7 @@ static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
> >>> goto response;
> >>> }
> >>>
> >>> - chan = pchan->ops->new_connection(pchan);
> >>> + chan = l2cap_new_connection(conn, pchan);
> >>> if (!chan)
> >>> goto response;
> >>>
> >>> @@ -4071,8 +4096,6 @@ static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
> >>> chan->psm = psm;
> >>> chan->dcid = scid;
> >>>
> >>> - __l2cap_chan_add(conn, chan);
> >>> -
> >>> dcid = chan->scid;
> >>>
> >>> __set_chan_timer(chan, chan->ops->get_sndtimeo(chan));
> >>> @@ -4955,7 +4978,7 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
> >>> goto response_unlock;
> >>> }
> >>>
> >>> - chan = pchan->ops->new_connection(pchan);
> >>> + chan = l2cap_new_connection(conn, pchan);
> >>> if (!chan) {
> >>> result = L2CAP_CR_LE_NO_MEM;
> >>> goto response_unlock;
> >>> @@ -4970,8 +4993,6 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
> >>> chan->omtu = mtu;
> >>> chan->remote_mps = mps;
> >>>
> >>> - __l2cap_chan_add(conn, chan);
> >>> -
> >>> l2cap_le_flowctl_init(chan, __le16_to_cpu(req->credits));
> >>>
> >>> dcid = chan->scid;
> >>> @@ -5179,7 +5200,7 @@ static inline int l2cap_ecred_conn_req(struct l2cap_conn *conn,
> >>> continue;
> >>> }
> >>>
> >>> - chan = pchan->ops->new_connection(pchan);
> >>> + chan = l2cap_new_connection(conn, pchan);
> >>> if (!chan) {
> >>> result = L2CAP_CR_LE_NO_MEM;
> >>> continue;
> >>> @@ -5194,8 +5215,6 @@ static inline int l2cap_ecred_conn_req(struct l2cap_conn *conn,
> >>> chan->omtu = mtu;
> >>> chan->remote_mps = mps;
> >>>
> >>> - __l2cap_chan_add(conn, chan);
> >>> -
> >>> l2cap_ecred_init(chan, __le16_to_cpu(req->credits));
> >>>
> >>> /* Init response */
> >>> @@ -7470,14 +7489,12 @@ static void l2cap_connect_cfm(struct hci_conn *hcon, u8 status)
> >>> goto next;
> >>>
> >>> l2cap_chan_lock(pchan);
> >>> - chan = pchan->ops->new_connection(pchan);
> >>> + chan = l2cap_new_connection(conn, pchan);
> >>> if (chan) {
> >>> bacpy(&chan->src, &hcon->src);
> >>> bacpy(&chan->dst, &hcon->dst);
> >>> chan->src_type = bdaddr_src_type(hcon);
> >>> chan->dst_type = dst_type;
> >>> -
> >>> - __l2cap_chan_add(conn, chan);
> >>> }
> >>>
> >>> l2cap_chan_unlock(pchan);
> >>> diff --git a/net/bluetooth/l2cap_sock.c b/net/bluetooth/l2cap_sock.c
> >>> index 025329636353..87f4c0db5c0c 100644
> >>> --- a/net/bluetooth/l2cap_sock.c
> >>> +++ b/net/bluetooth/l2cap_sock.c
> >>> @@ -46,7 +46,8 @@ static struct bt_sock_list l2cap_sk_list = {
> >>> static const struct proto_ops l2cap_sock_ops;
> >>> static void l2cap_sock_init(struct sock *sk, struct sock *parent);
> >>> static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
> >>> - int proto, gfp_t prio, int kern);
> >>> + int proto, gfp_t prio, int kern,
> >>> + struct l2cap_chan *chan);
> >>> static void l2cap_sock_cleanup_listen(struct sock *parent);
> >>>
> >>> bool l2cap_is_socket(struct socket *sock)
> >>> @@ -1287,6 +1288,23 @@ static int l2cap_sock_recvmsg(struct socket *sock, struct msghdr *msg,
> >>> return err;
> >>> }
> >>>
> >>> +/* Release the sock's ref on chan and clear the pointer so that the ref is
> >>> + * dropped exactly once even if both l2cap_sock_kill() and
> >>> + * l2cap_sock_destruct() run. Setting chan->data to NULL first stops any other
> >>> + * task from dereferencing the now-dead sock pointer.
> >>> + */
> >>> +static void l2cap_sock_put_chan(struct sock *sk)
> >>> +{
> >>> + struct l2cap_chan *chan = l2cap_pi(sk)->chan;
> >>> +
> >>> + if (!chan)
> >>> + return;
> >>> +
> >>> + chan->data = NULL;
> >>> + l2cap_pi(sk)->chan = NULL;
> >>> + l2cap_chan_put(chan);
> >>> +}
> >>> +
> >>> /* Kill socket (only if zapped and orphan)
> >>> * Must be called on unlocked socket, with l2cap channel lock.
> >>> */
> >>> @@ -1297,13 +1315,9 @@ static void l2cap_sock_kill(struct sock *sk)
> >>>
> >>> BT_DBG("sk %p state %s", sk, state_to_string(sk->sk_state));
> >>>
> >>> - /* Sock is dead, so set chan data to NULL, avoid other task use invalid
> >>> - * sock pointer.
> >>> - */
> >>> - l2cap_pi(sk)->chan->data = NULL;
> >>> - /* Kill poor orphan */
> >>> + l2cap_sock_put_chan(sk);
> >>>
> >>> - l2cap_chan_put(l2cap_pi(sk)->chan);
> >>> + /* Kill poor orphan */
> >>> sock_set_flag(sk, SOCK_DEAD);
> >>> sock_put(sk);
> >>> }
> >>> @@ -1546,12 +1560,14 @@ static void l2cap_sock_cleanup_listen(struct sock *parent)
> >>> }
> >>> }
> >>>
> >>> -static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
> >>> +static int l2cap_sock_new_connection_cb(struct l2cap_conn *conn,
> >>> + struct l2cap_chan *chan,
> >>> + struct l2cap_chan *new_chan)
> >>> {
> >>> struct sock *sk, *parent = chan->data;
> >>>
> >>> if (!parent)
> >>> - return NULL;
> >>> + return -EINVAL;
> >>>
> >>> lock_sock(parent);
> >>>
> >>> @@ -1559,25 +1575,33 @@ static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
> >>> if (sk_acceptq_is_full(parent)) {
> >>> BT_DBG("backlog full %d", parent->sk_ack_backlog);
> >>> release_sock(parent);
> >>> - return NULL;
> >>> + return -ENOBUFS;
> >>> }
> >>>
> >>> sk = l2cap_sock_alloc(sock_net(parent), NULL, BTPROTO_L2CAP,
> >>> - GFP_ATOMIC, 0);
> >>> + GFP_ATOMIC, 0, new_chan);
> >>> if (!sk) {
> >>> release_sock(parent);
> >>> - return NULL;
> >>> - }
> >>> + return -ENOMEM;
> >>> + }
> >>>
> >>> bt_sock_reclassify_lock(sk, BTPROTO_L2CAP);
> >>>
> >>> l2cap_sock_init(sk, parent);
> >>>
> >>> + /* Link the channel into conn before exposing the new socket via the
> >>> + * accept queue. Once release_sock() below drops the parent lock the
> >>> + * socket may be freed by another task, dropping its reference on
> >>> + * new_chan; the conn list reference taken here keeps new_chan alive so
> >>> + * the caller can safely use it after ->new_connection() returns.
> >>> + */
> >>> + __l2cap_chan_add(conn, new_chan);
> >>> +
> >>> bt_accept_enqueue(parent, sk, false);
> >>>
> >>> release_sock(parent);
> >>>
> >>> - return l2cap_pi(sk)->chan;
> >>> + return 0;
> >>> }
> >>>
> >>> static int l2cap_sock_recv_cb(struct l2cap_chan *chan, struct sk_buff *skb)
> >>> @@ -1874,10 +1898,7 @@ static void l2cap_sock_destruct(struct sock *sk)
> >>>
> >>> BT_DBG("sk %p", sk);
> >>>
> >>> - if (l2cap_pi(sk)->chan) {
> >>> - l2cap_pi(sk)->chan->data = NULL;
> >>> - l2cap_chan_put(l2cap_pi(sk)->chan);
> >>> - }
> >>> + l2cap_sock_put_chan(sk);
> >>>
> >>> list_for_each_entry_safe(rx_busy, next, &l2cap_pi(sk)->rx_busy, list) {
> >>> kfree_skb(rx_busy->skb);
> >>> @@ -1978,10 +1999,10 @@ static struct proto l2cap_proto = {
> >>> };
> >>>
> >>> static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
> >>> - int proto, gfp_t prio, int kern)
> >>> + int proto, gfp_t prio, int kern,
> >>> + struct l2cap_chan *chan)
> >>> {
> >>> struct sock *sk;
> >>> - struct l2cap_chan *chan;
> >>>
> >>> sk = bt_sock_alloc(net, sock, &l2cap_proto, proto, prio, kern);
> >>> if (!sk)
> >>> @@ -1992,16 +2013,7 @@ static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
> >>>
> >>> INIT_LIST_HEAD(&l2cap_pi(sk)->rx_busy);
> >>>
> >>> - chan = l2cap_chan_create();
> >>> - if (!chan) {
> >>> - sk_free(sk);
> >>> - if (sock)
> >>> - sock->sk = NULL;
> >>> - return NULL;
> >>> - }
> >>> -
> >>> - l2cap_chan_hold(chan);
> >>> -
> >>> + /* The sock takes ownership of the caller's reference on chan. */
> >>> l2cap_pi(sk)->chan = chan;
> >>>
> >>> return sk;
> >>> @@ -2011,6 +2023,7 @@ static int l2cap_sock_create(struct net *net, struct socket *sock, int protocol,
> >>> int kern)
> >>> {
> >>> struct sock *sk;
> >>> + struct l2cap_chan *chan;
> >>>
> >>> BT_DBG("sock %p", sock);
> >>>
> >>> @@ -2025,10 +2038,16 @@ static int l2cap_sock_create(struct net *net, struct socket *sock, int protocol,
> >>>
> >>> sock->ops = &l2cap_sock_ops;
> >>>
> >>> - sk = l2cap_sock_alloc(net, sock, protocol, GFP_ATOMIC, kern);
> >>> - if (!sk)
> >>> + chan = l2cap_chan_create();
> >>> + if (!chan)
> >>> return -ENOMEM;
> >>>
> >>> + sk = l2cap_sock_alloc(net, sock, protocol, GFP_ATOMIC, kern, chan);
> >>> + if (!sk) {
> >>> + l2cap_chan_put(chan);
> >>> + return -ENOMEM;
> >>> + }
> >>> +
> >>> l2cap_sock_init(sk, NULL);
> >>> bt_sock_link(&l2cap_sk_list, sk);
> >>> return 0;
> >>> diff --git a/net/bluetooth/smp.c b/net/bluetooth/smp.c
> >>> index 1739c1989dbd..2d31c3c7bbc0 100644
> >>> --- a/net/bluetooth/smp.c
> >>> +++ b/net/bluetooth/smp.c
> >>> @@ -3204,16 +3204,12 @@ static const struct l2cap_ops smp_chan_ops = {
> >>> .get_sndtimeo = l2cap_chan_no_get_sndtimeo,
> >>> };
> >>>
> >>> -static inline struct l2cap_chan *smp_new_conn_cb(struct l2cap_chan *pchan)
> >>> +static inline int smp_new_conn_cb(struct l2cap_conn *conn,
> >>> + struct l2cap_chan *pchan,
> >>> + struct l2cap_chan *chan)
> >>> {
> >>> - struct l2cap_chan *chan;
> >>> -
> >>> BT_DBG("pchan %p", pchan);
> >>>
> >>> - chan = l2cap_chan_create();
> >>> - if (!chan)
> >>> - return NULL;
> >>> -
> >>> chan->chan_type = pchan->chan_type;
> >>> chan->ops = &smp_chan_ops;
> >>> chan->scid = pchan->scid;
> >>> @@ -3229,9 +3225,12 @@ static inline struct l2cap_chan *smp_new_conn_cb(struct l2cap_chan *pchan)
> >>> */
> >>> atomic_set(&chan->nesting, L2CAP_NESTING_SMP);
> >>>
> >>> - BT_DBG("created chan %p", chan);
> >>> + /* Take the conn list reference; see l2cap_new_connection(). */
> >>> + __l2cap_chan_add(conn, chan);
> >>>
> >>> - return chan;
> >>> + BT_DBG("initialised chan %p", chan);
> >>> +
> >>> + return 0;
> >>> }
> >>>
> >>> static const struct l2cap_ops smp_root_chan_ops = {
> >>> --
> >>> 2.54.0
> >>>
> >>
> >>
> >> --
> >> Luiz Augusto von Dentz
> >
> > Best,
> > Siwei
>
> Best,
> Siwei
--
Luiz Augusto von Dentz
^ permalink raw reply related [flat|nested] 20+ messages in thread* Re: [PATCH v9 1/1] Bluetooth: L2CAP: Fix use-after-free in l2cap_sock_new_connection_cb()
2026-06-10 15:57 ` Luiz Augusto von Dentz
@ 2026-06-10 16:32 ` Siwei Zhang
2026-06-10 16:59 ` Luiz Augusto von Dentz
0 siblings, 1 reply; 20+ messages in thread
From: Siwei Zhang @ 2026-06-10 16:32 UTC (permalink / raw)
To: Luiz Augusto von Dentz; +Cc: linux-bluetooth
On Wed, Jun 10, 2026, at 11:57 AM, Luiz Augusto von Dentz wrote:
> Hi Siwei,
>
> On Mon, Jun 8, 2026 at 9:29 AM Siwei Zhang <oss@fourdim.xyz> wrote:
>>
>> Hi Luiz,
>>
>> On Thu, Jun 4, 2026, at 11:52 AM, Siwei Zhang wrote:
>> > Hi Luiz,
>> >
>> > On Wed, Jun 3, 2026, at 2:17 PM, Luiz Augusto von Dentz wrote:
>> >> Hi Siwei,
>> >>
>> >> On Wed, Jun 3, 2026 at 11:09 AM Siwei Zhang <oss@fourdim.xyz> wrote:
>> >>>
>> >>> l2cap_sock_new_connection_cb() accesses l2cap_pi(sk)->chan after
>> >>> release_sock(parent). Once the parent lock is released, the child
>> >>> socket sk can be freed by another task.
>> >>>
>> >>> Allocate the channel outside the func to prevent this.
>> >>>
>> >>> Fixes: 8ffb929098a5 ("Bluetooth: Remove parent socket usage from l2cap_core.c")
>> >>> Cc: stable@kernel.org
>> >>> Assisted-by: Claude:claude-opus-4-8
>> >>> Signed-off-by: Siwei Zhang <oss@fourdim.xyz>
>> >>> ---
>> >>> include/net/bluetooth/l2cap.h | 10 +++--
>> >>> net/bluetooth/6lowpan.c | 31 +++++++------
>> >>> net/bluetooth/l2cap_core.c | 41 ++++++++++++-----
>> >>> net/bluetooth/l2cap_sock.c | 83 +++++++++++++++++++++--------------
>> >>> net/bluetooth/smp.c | 17 ++++---
>> >>> 5 files changed, 113 insertions(+), 69 deletions(-)
>> >>>
>> >>> diff --git a/include/net/bluetooth/l2cap.h b/include/net/bluetooth/l2cap.h
>> >>> index e0a1f2293679..7f5e4647f6e0 100644
>> >>> --- a/include/net/bluetooth/l2cap.h
>> >>> +++ b/include/net/bluetooth/l2cap.h
>> >>> @@ -620,7 +620,9 @@ struct l2cap_chan {
>> >>> struct l2cap_ops {
>> >>> char *name;
>> >>>
>> >>> - struct l2cap_chan *(*new_connection) (struct l2cap_chan *chan);
>> >>> + int (*new_connection)(struct l2cap_conn *conn,
>> >>> + struct l2cap_chan *chan,
>> >>> + struct l2cap_chan *new_chan);
>> >>> int (*recv) (struct l2cap_chan * chan,
>> >>> struct sk_buff *skb);
>> >>> void (*teardown) (struct l2cap_chan *chan, int err);
>> >>> @@ -884,9 +886,11 @@ static inline __u16 __next_seq(struct l2cap_chan *chan, __u16 seq)
>> >>> return (seq + 1) % (chan->tx_win_max + 1);
>> >>> }
>> >>>
>> >>> -static inline struct l2cap_chan *l2cap_chan_no_new_connection(struct l2cap_chan *chan)
>> >>> +static inline int l2cap_chan_no_new_connection(struct l2cap_conn *conn,
>> >>> + struct l2cap_chan *chan,
>> >>> + struct l2cap_chan *new_chan)
>> >>> {
>> >>> - return NULL;
>> >>> + return -EOPNOTSUPP;
>> >>> }
>> >>>
>> >>> static inline int l2cap_chan_no_recv(struct l2cap_chan *chan, struct sk_buff *skb)
>> >>> diff --git a/net/bluetooth/6lowpan.c b/net/bluetooth/6lowpan.c
>> >>> index cb1e329d66fd..94863af97a44 100644
>> >>> --- a/net/bluetooth/6lowpan.c
>> >>> +++ b/net/bluetooth/6lowpan.c
>> >>> @@ -624,6 +624,15 @@ static bool is_bt_6lowpan(struct hci_conn *hcon)
>> >>> return true;
>> >>> }
>> >>>
>> >>> +static void chan_init(struct l2cap_chan *chan)
>> >>> +{
>> >>> + l2cap_chan_set_defaults(chan);
>> >>> +
>> >>> + chan->chan_type = L2CAP_CHAN_CONN_ORIENTED;
>> >>> + chan->mode = L2CAP_MODE_LE_FLOWCTL;
>> >>> + chan->imtu = 1280;
>> >>> +}
>> >>> +
>> >>> static struct l2cap_chan *chan_create(void)
>> >>> {
>> >>> struct l2cap_chan *chan;
>> >>> @@ -632,11 +641,7 @@ static struct l2cap_chan *chan_create(void)
>> >>> if (!chan)
>> >>> return NULL;
>> >>>
>> >>> - l2cap_chan_set_defaults(chan);
>> >>> -
>> >>> - chan->chan_type = L2CAP_CHAN_CONN_ORIENTED;
>> >>> - chan->mode = L2CAP_MODE_LE_FLOWCTL;
>> >>> - chan->imtu = 1280;
>> >>> + chan_init(chan);
>> >>>
>> >>> return chan;
>> >>> }
>> >>> @@ -745,19 +750,19 @@ static inline void chan_ready_cb(struct l2cap_chan *chan)
>> >>> ifup(dev->netdev);
>> >>> }
>> >>>
>> >>> -static inline struct l2cap_chan *chan_new_conn_cb(struct l2cap_chan *pchan)
>> >>> +static inline int chan_new_conn_cb(struct l2cap_conn *conn,
>> >>> + struct l2cap_chan *pchan,
>> >>> + struct l2cap_chan *chan)
>> >>> {
>> >>> - struct l2cap_chan *chan;
>> >>> -
>> >>> - chan = chan_create();
>> >>> - if (!chan)
>> >>> - return NULL;
>> >>> -
>> >>> + chan_init(chan);
ref1: chan->chan_type = L2CAP_CHAN_CONN_ORIENTED; set
in chan_init.
>> >>> chan->ops = pchan->ops;
>> >>>
>> >>> + /* Take the conn list reference; see l2cap_new_connection(). */
>> >>> + __l2cap_chan_add(conn, chan);
>> >>> +
>> >>> BT_DBG("chan %p pchan %p", chan, pchan);
>> >>>
>> >>> - return chan;
>> >>> + return 0;
>> >>> }
>> >>>
>> >>> static void unregister_dev(struct lowpan_btle_dev *dev)
>> >>> diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
>> >>> index c4ccfbda9d78..62acf90837fb 100644
>> >>> --- a/net/bluetooth/l2cap_core.c
>> >>> +++ b/net/bluetooth/l2cap_core.c
>> >>> @@ -4007,6 +4007,31 @@ static inline int l2cap_command_rej(struct l2cap_conn *conn,
>> >>> return 0;
>> >>> }
>> >>>
>> >>> +/* Allocate and initialise a channel for an incoming connection.
>> >>> + *
>> >>> + * ->new_connection() initialises the channel and links it into @conn with
>> >>> + * __l2cap_chan_add(). The l2cap_chan_create() reference becomes the one owned
>> >>> + * by the parent subsystem (l2cap_pi(sk)->chan, conn->smp or peer->chan) and is
>> >>> + * released by its teardown callback; the conn list reference is released by
>> >>> + * l2cap_chan_del().
>> >>> + */
>> >>> +static struct l2cap_chan *l2cap_new_connection(struct l2cap_conn *conn,
>> >>> + struct l2cap_chan *pchan)
>> >>> +{
>> >>> + struct l2cap_chan *chan;
>> >>> +
>> >>> + chan = l2cap_chan_create();
>> >>> + if (!chan)
>> >>> + return NULL;
>> >>> +
>> >>> + if (pchan->ops->new_connection(conn, pchan, chan) < 0) {
>> >>> + l2cap_chan_put(chan);
>> >>> + return NULL;
>> >>> + }
>> >>
>> >> I don't quite get why we can't just place __l2cap_chan_add here
>> >> instead of having it called by new_connection callbacks?
>> >>
>> >
>> > It's specifically the l2cap_sock_new_connection_cb() case - the very
>> > use-after-free this patch fixes. The __l2cap_chan_add() has to happen while
>> > the parent lock is still held, and only the callback holds that lock.
>> >
>> > The reference counting on the new child chan starts at one ref,
>> > owned by the new socket:
>> >
>> > /* l2cap_new_connection() */
>> > chan = l2cap_chan_create(); /* refcount = 1 */
>> > if (!chan)
>> > return NULL;
>> >
>> > pchan->ops->new_connection(conn, pchan, chan);
>> >
>> > and inside the socket callback:
>> >
>> > /* l2cap_sock_new_connection_cb() */
>> > lock_sock(parent);
>> > ...
>> > sk = l2cap_sock_alloc(..., new_chan); /* sk owns the chan_create ref */
>> > ...
>> > l2cap_sock_init(sk, parent);
>> >
>> > __l2cap_chan_add(conn, new_chan); /* (A) conn list takes a ref */
>> > bt_accept_enqueue(parent, sk, false); /* (B) sk now on accept queue */
>> >
>> > release_sock(parent); /* (C) parent lock dropped */
>> > return 0;
>> >
>> > The moment we hit (C), sk is reachable through the parent's accept queue, so
>> > another task can grab and tear it down:
>> >
>> > accept() -> l2cap_sock_kill() -> l2cap_sock_put_chan()
>> > chan->data = NULL;
>> > l2cap_chan_put(chan); /* drops the sk's chan ref */
>> >
>> > If __l2cap_chan_add() at (A) hadn't already taken the conn list reference,
>> > that put would drop the last ref and free new_chan. Control then returns up
>> > to l2cap_new_connection(), which hands the now-freed chan back to
>> > l2cap_connect():
>> >
>> > /* l2cap_connect() - runs after the callback returns */
>> > chan = l2cap_new_connection(conn, pchan);
>> > if (!chan)
>> > goto response;
>> > ...
>> > bacpy(&chan->src, &conn->hcon->src); /* <-- UAF on freed chan */
>> > chan->psm = psm;
>> > chan->dcid = scid;
>> >
>> > The conn list reference taken at (A), before (C), is what keeps new_chan
>> > alive across the release_sock() window so l2cap_connect() can keep using it.
>> >
>> > So __l2cap_chan_add() can't move out to l2cap_new_connection(): by the time
>> > the callback returns, the parent lock is already dropped and chan may already
>> > be freed - which is exactly the race. It has to be taken inside the callback,
>> > under the parent lock, before the socket is exposed.
>
> If you do after l2cap_new_connection, yes, but what if you do before
> you call ->new_connect:
>
> diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
> index 3991c179b98f..cd864f400985 100644
> --- a/net/bluetooth/l2cap_core.c
> +++ b/net/bluetooth/l2cap_core.c
> @@ -4027,8 +4027,10 @@ static struct l2cap_chan
> *l2cap_new_connection(struct l2cap_conn *conn,
> if (!chan)
> return NULL;
>
> + __l2cap_chan_add(conn, chan);
> +
> if (pchan->ops->new_connection(conn, pchan, chan) < 0) {
> - l2cap_chan_put(chan);
> + l2cap_chan_del(chan, 0);
> return NULL;
> }
>
> This means we have to call l2cap_chan_del if new_connection fails, but
> we can eliminate other functions having to call __l2cap_chan_add with
> this. If the issue is then having l2cap_chan_del called concurrently,
> we can add a l2cap_chan_hold; actually, we may need a reference anyway
> since sk can be released and call l2cap_chan_del, releasing both
> references.
>
In:
void __l2cap_chan_add(struct l2cap_conn *conn, struct l2cap_chan *chan)
{
...
switch (chan->chan_type) {
case L2CAP_CHAN_CONN_ORIENTED:
...
break;
case L2CAP_CHAN_CONN_LESS:
...
break;
case L2CAP_CHAN_FIXED:
...
break;
default:
...
}
}
Apparently it depends on chan->chan_type and it is set by ref1.
It is only set in cb function.
>> > The other callbacks (6lowpan, smp) have no equivalent lock-drop window.
>> > I kept __l2cap_chan_add() inside all of the ->new_connection() callbacks
>> > just to keep the "callback links the channel into conn" contract uniform.
>> >
>>
>> Could you please check this?
>> Just remind you in case you miss this.
>>
>> >>> +
>> >>> + return chan;
>> >>> +}
>> >>> +
>> >>> static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
>> >>> u8 *data, u8 rsp_code)
>> >>> {
>> >>> @@ -4053,7 +4078,7 @@ static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
>> >>> goto response;
>> >>> }
>> >>>
>> >>> - chan = pchan->ops->new_connection(pchan);
>> >>> + chan = l2cap_new_connection(conn, pchan);
>> >>> if (!chan)
>> >>> goto response;
>> >>>
>> >>> @@ -4071,8 +4096,6 @@ static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
>> >>> chan->psm = psm;
>> >>> chan->dcid = scid;
>> >>>
>> >>> - __l2cap_chan_add(conn, chan);
>> >>> -
>> >>> dcid = chan->scid;
>> >>>
>> >>> __set_chan_timer(chan, chan->ops->get_sndtimeo(chan));
>> >>> @@ -4955,7 +4978,7 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
>> >>> goto response_unlock;
>> >>> }
>> >>>
>> >>> - chan = pchan->ops->new_connection(pchan);
>> >>> + chan = l2cap_new_connection(conn, pchan);
>> >>> if (!chan) {
>> >>> result = L2CAP_CR_LE_NO_MEM;
>> >>> goto response_unlock;
>> >>> @@ -4970,8 +4993,6 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
>> >>> chan->omtu = mtu;
>> >>> chan->remote_mps = mps;
>> >>>
>> >>> - __l2cap_chan_add(conn, chan);
>> >>> -
>> >>> l2cap_le_flowctl_init(chan, __le16_to_cpu(req->credits));
>> >>>
>> >>> dcid = chan->scid;
>> >>> @@ -5179,7 +5200,7 @@ static inline int l2cap_ecred_conn_req(struct l2cap_conn *conn,
>> >>> continue;
>> >>> }
>> >>>
>> >>> - chan = pchan->ops->new_connection(pchan);
>> >>> + chan = l2cap_new_connection(conn, pchan);
>> >>> if (!chan) {
>> >>> result = L2CAP_CR_LE_NO_MEM;
>> >>> continue;
>> >>> @@ -5194,8 +5215,6 @@ static inline int l2cap_ecred_conn_req(struct l2cap_conn *conn,
>> >>> chan->omtu = mtu;
>> >>> chan->remote_mps = mps;
>> >>>
>> >>> - __l2cap_chan_add(conn, chan);
>> >>> -
>> >>> l2cap_ecred_init(chan, __le16_to_cpu(req->credits));
>> >>>
>> >>> /* Init response */
>> >>> @@ -7470,14 +7489,12 @@ static void l2cap_connect_cfm(struct hci_conn *hcon, u8 status)
>> >>> goto next;
>> >>>
>> >>> l2cap_chan_lock(pchan);
>> >>> - chan = pchan->ops->new_connection(pchan);
>> >>> + chan = l2cap_new_connection(conn, pchan);
>> >>> if (chan) {
>> >>> bacpy(&chan->src, &hcon->src);
>> >>> bacpy(&chan->dst, &hcon->dst);
>> >>> chan->src_type = bdaddr_src_type(hcon);
>> >>> chan->dst_type = dst_type;
>> >>> -
>> >>> - __l2cap_chan_add(conn, chan);
>> >>> }
>> >>>
>> >>> l2cap_chan_unlock(pchan);
>> >>> diff --git a/net/bluetooth/l2cap_sock.c b/net/bluetooth/l2cap_sock.c
>> >>> index 025329636353..87f4c0db5c0c 100644
>> >>> --- a/net/bluetooth/l2cap_sock.c
>> >>> +++ b/net/bluetooth/l2cap_sock.c
>> >>> @@ -46,7 +46,8 @@ static struct bt_sock_list l2cap_sk_list = {
>> >>> static const struct proto_ops l2cap_sock_ops;
>> >>> static void l2cap_sock_init(struct sock *sk, struct sock *parent);
>> >>> static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
>> >>> - int proto, gfp_t prio, int kern);
>> >>> + int proto, gfp_t prio, int kern,
>> >>> + struct l2cap_chan *chan);
>> >>> static void l2cap_sock_cleanup_listen(struct sock *parent);
>> >>>
>> >>> bool l2cap_is_socket(struct socket *sock)
>> >>> @@ -1287,6 +1288,23 @@ static int l2cap_sock_recvmsg(struct socket *sock, struct msghdr *msg,
>> >>> return err;
>> >>> }
>> >>>
>> >>> +/* Release the sock's ref on chan and clear the pointer so that the ref is
>> >>> + * dropped exactly once even if both l2cap_sock_kill() and
>> >>> + * l2cap_sock_destruct() run. Setting chan->data to NULL first stops any other
>> >>> + * task from dereferencing the now-dead sock pointer.
>> >>> + */
>> >>> +static void l2cap_sock_put_chan(struct sock *sk)
>> >>> +{
>> >>> + struct l2cap_chan *chan = l2cap_pi(sk)->chan;
>> >>> +
>> >>> + if (!chan)
>> >>> + return;
>> >>> +
>> >>> + chan->data = NULL;
>> >>> + l2cap_pi(sk)->chan = NULL;
>> >>> + l2cap_chan_put(chan);
>> >>> +}
>> >>> +
>> >>> /* Kill socket (only if zapped and orphan)
>> >>> * Must be called on unlocked socket, with l2cap channel lock.
>> >>> */
>> >>> @@ -1297,13 +1315,9 @@ static void l2cap_sock_kill(struct sock *sk)
>> >>>
>> >>> BT_DBG("sk %p state %s", sk, state_to_string(sk->sk_state));
>> >>>
>> >>> - /* Sock is dead, so set chan data to NULL, avoid other task use invalid
>> >>> - * sock pointer.
>> >>> - */
>> >>> - l2cap_pi(sk)->chan->data = NULL;
>> >>> - /* Kill poor orphan */
>> >>> + l2cap_sock_put_chan(sk);
>> >>>
>> >>> - l2cap_chan_put(l2cap_pi(sk)->chan);
>> >>> + /* Kill poor orphan */
>> >>> sock_set_flag(sk, SOCK_DEAD);
>> >>> sock_put(sk);
>> >>> }
>> >>> @@ -1546,12 +1560,14 @@ static void l2cap_sock_cleanup_listen(struct sock *parent)
>> >>> }
>> >>> }
>> >>>
>> >>> -static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
>> >>> +static int l2cap_sock_new_connection_cb(struct l2cap_conn *conn,
>> >>> + struct l2cap_chan *chan,
>> >>> + struct l2cap_chan *new_chan)
>> >>> {
>> >>> struct sock *sk, *parent = chan->data;
>> >>>
>> >>> if (!parent)
>> >>> - return NULL;
>> >>> + return -EINVAL;
>> >>>
>> >>> lock_sock(parent);
>> >>>
>> >>> @@ -1559,25 +1575,33 @@ static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
>> >>> if (sk_acceptq_is_full(parent)) {
>> >>> BT_DBG("backlog full %d", parent->sk_ack_backlog);
>> >>> release_sock(parent);
>> >>> - return NULL;
>> >>> + return -ENOBUFS;
>> >>> }
>> >>>
>> >>> sk = l2cap_sock_alloc(sock_net(parent), NULL, BTPROTO_L2CAP,
>> >>> - GFP_ATOMIC, 0);
>> >>> + GFP_ATOMIC, 0, new_chan);
>> >>> if (!sk) {
>> >>> release_sock(parent);
>> >>> - return NULL;
>> >>> - }
>> >>> + return -ENOMEM;
>> >>> + }
>> >>>
>> >>> bt_sock_reclassify_lock(sk, BTPROTO_L2CAP);
>> >>>
>> >>> l2cap_sock_init(sk, parent);
>> >>>
>> >>> + /* Link the channel into conn before exposing the new socket via the
>> >>> + * accept queue. Once release_sock() below drops the parent lock the
>> >>> + * socket may be freed by another task, dropping its reference on
>> >>> + * new_chan; the conn list reference taken here keeps new_chan alive so
>> >>> + * the caller can safely use it after ->new_connection() returns.
>> >>> + */
>> >>> + __l2cap_chan_add(conn, new_chan);
>> >>> +
>> >>> bt_accept_enqueue(parent, sk, false);
>> >>>
>> >>> release_sock(parent);
>> >>>
>> >>> - return l2cap_pi(sk)->chan;
>> >>> + return 0;
>> >>> }
>> >>>
>> >>> static int l2cap_sock_recv_cb(struct l2cap_chan *chan, struct sk_buff *skb)
>> >>> @@ -1874,10 +1898,7 @@ static void l2cap_sock_destruct(struct sock *sk)
>> >>>
>> >>> BT_DBG("sk %p", sk);
>> >>>
>> >>> - if (l2cap_pi(sk)->chan) {
>> >>> - l2cap_pi(sk)->chan->data = NULL;
>> >>> - l2cap_chan_put(l2cap_pi(sk)->chan);
>> >>> - }
>> >>> + l2cap_sock_put_chan(sk);
>> >>>
>> >>> list_for_each_entry_safe(rx_busy, next, &l2cap_pi(sk)->rx_busy, list) {
>> >>> kfree_skb(rx_busy->skb);
>> >>> @@ -1978,10 +1999,10 @@ static struct proto l2cap_proto = {
>> >>> };
>> >>>
>> >>> static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
>> >>> - int proto, gfp_t prio, int kern)
>> >>> + int proto, gfp_t prio, int kern,
>> >>> + struct l2cap_chan *chan)
>> >>> {
>> >>> struct sock *sk;
>> >>> - struct l2cap_chan *chan;
>> >>>
>> >>> sk = bt_sock_alloc(net, sock, &l2cap_proto, proto, prio, kern);
>> >>> if (!sk)
>> >>> @@ -1992,16 +2013,7 @@ static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
>> >>>
>> >>> INIT_LIST_HEAD(&l2cap_pi(sk)->rx_busy);
>> >>>
>> >>> - chan = l2cap_chan_create();
>> >>> - if (!chan) {
>> >>> - sk_free(sk);
>> >>> - if (sock)
>> >>> - sock->sk = NULL;
>> >>> - return NULL;
>> >>> - }
>> >>> -
>> >>> - l2cap_chan_hold(chan);
>> >>> -
>> >>> + /* The sock takes ownership of the caller's reference on chan. */
>> >>> l2cap_pi(sk)->chan = chan;
>> >>>
>> >>> return sk;
>> >>> @@ -2011,6 +2023,7 @@ static int l2cap_sock_create(struct net *net, struct socket *sock, int protocol,
>> >>> int kern)
>> >>> {
>> >>> struct sock *sk;
>> >>> + struct l2cap_chan *chan;
>> >>>
>> >>> BT_DBG("sock %p", sock);
>> >>>
>> >>> @@ -2025,10 +2038,16 @@ static int l2cap_sock_create(struct net *net, struct socket *sock, int protocol,
>> >>>
>> >>> sock->ops = &l2cap_sock_ops;
>> >>>
>> >>> - sk = l2cap_sock_alloc(net, sock, protocol, GFP_ATOMIC, kern);
>> >>> - if (!sk)
>> >>> + chan = l2cap_chan_create();
>> >>> + if (!chan)
>> >>> return -ENOMEM;
>> >>>
>> >>> + sk = l2cap_sock_alloc(net, sock, protocol, GFP_ATOMIC, kern, chan);
>> >>> + if (!sk) {
>> >>> + l2cap_chan_put(chan);
>> >>> + return -ENOMEM;
>> >>> + }
>> >>> +
>> >>> l2cap_sock_init(sk, NULL);
>> >>> bt_sock_link(&l2cap_sk_list, sk);
>> >>> return 0;
>> >>> diff --git a/net/bluetooth/smp.c b/net/bluetooth/smp.c
>> >>> index 1739c1989dbd..2d31c3c7bbc0 100644
>> >>> --- a/net/bluetooth/smp.c
>> >>> +++ b/net/bluetooth/smp.c
>> >>> @@ -3204,16 +3204,12 @@ static const struct l2cap_ops smp_chan_ops = {
>> >>> .get_sndtimeo = l2cap_chan_no_get_sndtimeo,
>> >>> };
>> >>>
>> >>> -static inline struct l2cap_chan *smp_new_conn_cb(struct l2cap_chan *pchan)
>> >>> +static inline int smp_new_conn_cb(struct l2cap_conn *conn,
>> >>> + struct l2cap_chan *pchan,
>> >>> + struct l2cap_chan *chan)
>> >>> {
>> >>> - struct l2cap_chan *chan;
>> >>> -
>> >>> BT_DBG("pchan %p", pchan);
>> >>>
>> >>> - chan = l2cap_chan_create();
>> >>> - if (!chan)
>> >>> - return NULL;
>> >>> -
>> >>> chan->chan_type = pchan->chan_type;
>> >>> chan->ops = &smp_chan_ops;
>> >>> chan->scid = pchan->scid;
>> >>> @@ -3229,9 +3225,12 @@ static inline struct l2cap_chan *smp_new_conn_cb(struct l2cap_chan *pchan)
>> >>> */
>> >>> atomic_set(&chan->nesting, L2CAP_NESTING_SMP);
>> >>>
>> >>> - BT_DBG("created chan %p", chan);
>> >>> + /* Take the conn list reference; see l2cap_new_connection(). */
>> >>> + __l2cap_chan_add(conn, chan);
>> >>>
>> >>> - return chan;
>> >>> + BT_DBG("initialised chan %p", chan);
>> >>> +
>> >>> + return 0;
>> >>> }
>> >>>
>> >>> static const struct l2cap_ops smp_root_chan_ops = {
>> >>> --
>> >>> 2.54.0
>> >>>
>> >>
>> >>
>> >> --
>> >> Luiz Augusto von Dentz
>> >
>> > Best,
>> > Siwei
>>
>> Best,
>> Siwei
>
>
>
> --
> Luiz Augusto von Dentz
Best,
Siwei
^ permalink raw reply [flat|nested] 20+ messages in thread* Re: [PATCH v9 1/1] Bluetooth: L2CAP: Fix use-after-free in l2cap_sock_new_connection_cb()
2026-06-10 16:32 ` Siwei Zhang
@ 2026-06-10 16:59 ` Luiz Augusto von Dentz
2026-06-10 17:02 ` Siwei Zhang
0 siblings, 1 reply; 20+ messages in thread
From: Luiz Augusto von Dentz @ 2026-06-10 16:59 UTC (permalink / raw)
To: Siwei Zhang; +Cc: linux-bluetooth
Hi Siwei,
On Wed, Jun 10, 2026 at 12:33 PM Siwei Zhang <oss@fourdim.xyz> wrote:
>
>
>
> On Wed, Jun 10, 2026, at 11:57 AM, Luiz Augusto von Dentz wrote:
> > Hi Siwei,
> >
> > On Mon, Jun 8, 2026 at 9:29 AM Siwei Zhang <oss@fourdim.xyz> wrote:
> >>
> >> Hi Luiz,
> >>
> >> On Thu, Jun 4, 2026, at 11:52 AM, Siwei Zhang wrote:
> >> > Hi Luiz,
> >> >
> >> > On Wed, Jun 3, 2026, at 2:17 PM, Luiz Augusto von Dentz wrote:
> >> >> Hi Siwei,
> >> >>
> >> >> On Wed, Jun 3, 2026 at 11:09 AM Siwei Zhang <oss@fourdim.xyz> wrote:
> >> >>>
> >> >>> l2cap_sock_new_connection_cb() accesses l2cap_pi(sk)->chan after
> >> >>> release_sock(parent). Once the parent lock is released, the child
> >> >>> socket sk can be freed by another task.
> >> >>>
> >> >>> Allocate the channel outside the func to prevent this.
> >> >>>
> >> >>> Fixes: 8ffb929098a5 ("Bluetooth: Remove parent socket usage from l2cap_core.c")
> >> >>> Cc: stable@kernel.org
> >> >>> Assisted-by: Claude:claude-opus-4-8
> >> >>> Signed-off-by: Siwei Zhang <oss@fourdim.xyz>
> >> >>> ---
> >> >>> include/net/bluetooth/l2cap.h | 10 +++--
> >> >>> net/bluetooth/6lowpan.c | 31 +++++++------
> >> >>> net/bluetooth/l2cap_core.c | 41 ++++++++++++-----
> >> >>> net/bluetooth/l2cap_sock.c | 83 +++++++++++++++++++++--------------
> >> >>> net/bluetooth/smp.c | 17 ++++---
> >> >>> 5 files changed, 113 insertions(+), 69 deletions(-)
> >> >>>
> >> >>> diff --git a/include/net/bluetooth/l2cap.h b/include/net/bluetooth/l2cap.h
> >> >>> index e0a1f2293679..7f5e4647f6e0 100644
> >> >>> --- a/include/net/bluetooth/l2cap.h
> >> >>> +++ b/include/net/bluetooth/l2cap.h
> >> >>> @@ -620,7 +620,9 @@ struct l2cap_chan {
> >> >>> struct l2cap_ops {
> >> >>> char *name;
> >> >>>
> >> >>> - struct l2cap_chan *(*new_connection) (struct l2cap_chan *chan);
> >> >>> + int (*new_connection)(struct l2cap_conn *conn,
> >> >>> + struct l2cap_chan *chan,
> >> >>> + struct l2cap_chan *new_chan);
> >> >>> int (*recv) (struct l2cap_chan * chan,
> >> >>> struct sk_buff *skb);
> >> >>> void (*teardown) (struct l2cap_chan *chan, int err);
> >> >>> @@ -884,9 +886,11 @@ static inline __u16 __next_seq(struct l2cap_chan *chan, __u16 seq)
> >> >>> return (seq + 1) % (chan->tx_win_max + 1);
> >> >>> }
> >> >>>
> >> >>> -static inline struct l2cap_chan *l2cap_chan_no_new_connection(struct l2cap_chan *chan)
> >> >>> +static inline int l2cap_chan_no_new_connection(struct l2cap_conn *conn,
> >> >>> + struct l2cap_chan *chan,
> >> >>> + struct l2cap_chan *new_chan)
> >> >>> {
> >> >>> - return NULL;
> >> >>> + return -EOPNOTSUPP;
> >> >>> }
> >> >>>
> >> >>> static inline int l2cap_chan_no_recv(struct l2cap_chan *chan, struct sk_buff *skb)
> >> >>> diff --git a/net/bluetooth/6lowpan.c b/net/bluetooth/6lowpan.c
> >> >>> index cb1e329d66fd..94863af97a44 100644
> >> >>> --- a/net/bluetooth/6lowpan.c
> >> >>> +++ b/net/bluetooth/6lowpan.c
> >> >>> @@ -624,6 +624,15 @@ static bool is_bt_6lowpan(struct hci_conn *hcon)
> >> >>> return true;
> >> >>> }
> >> >>>
> >> >>> +static void chan_init(struct l2cap_chan *chan)
> >> >>> +{
> >> >>> + l2cap_chan_set_defaults(chan);
> >> >>> +
> >> >>> + chan->chan_type = L2CAP_CHAN_CONN_ORIENTED;
> >> >>> + chan->mode = L2CAP_MODE_LE_FLOWCTL;
> >> >>> + chan->imtu = 1280;
> >> >>> +}
> >> >>> +
> >> >>> static struct l2cap_chan *chan_create(void)
> >> >>> {
> >> >>> struct l2cap_chan *chan;
> >> >>> @@ -632,11 +641,7 @@ static struct l2cap_chan *chan_create(void)
> >> >>> if (!chan)
> >> >>> return NULL;
> >> >>>
> >> >>> - l2cap_chan_set_defaults(chan);
> >> >>> -
> >> >>> - chan->chan_type = L2CAP_CHAN_CONN_ORIENTED;
> >> >>> - chan->mode = L2CAP_MODE_LE_FLOWCTL;
> >> >>> - chan->imtu = 1280;
> >> >>> + chan_init(chan);
> >> >>>
> >> >>> return chan;
> >> >>> }
> >> >>> @@ -745,19 +750,19 @@ static inline void chan_ready_cb(struct l2cap_chan *chan)
> >> >>> ifup(dev->netdev);
> >> >>> }
> >> >>>
> >> >>> -static inline struct l2cap_chan *chan_new_conn_cb(struct l2cap_chan *pchan)
> >> >>> +static inline int chan_new_conn_cb(struct l2cap_conn *conn,
> >> >>> + struct l2cap_chan *pchan,
> >> >>> + struct l2cap_chan *chan)
> >> >>> {
> >> >>> - struct l2cap_chan *chan;
> >> >>> -
> >> >>> - chan = chan_create();
> >> >>> - if (!chan)
> >> >>> - return NULL;
> >> >>> -
> >> >>> + chan_init(chan);
>
> ref1: chan->chan_type = L2CAP_CHAN_CONN_ORIENTED; set
> in chan_init.
>
> >> >>> chan->ops = pchan->ops;
> >> >>>
> >> >>> + /* Take the conn list reference; see l2cap_new_connection(). */
> >> >>> + __l2cap_chan_add(conn, chan);
> >> >>> +
> >> >>> BT_DBG("chan %p pchan %p", chan, pchan);
> >> >>>
> >> >>> - return chan;
> >> >>> + return 0;
> >> >>> }
> >> >>>
> >> >>> static void unregister_dev(struct lowpan_btle_dev *dev)
> >> >>> diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
> >> >>> index c4ccfbda9d78..62acf90837fb 100644
> >> >>> --- a/net/bluetooth/l2cap_core.c
> >> >>> +++ b/net/bluetooth/l2cap_core.c
> >> >>> @@ -4007,6 +4007,31 @@ static inline int l2cap_command_rej(struct l2cap_conn *conn,
> >> >>> return 0;
> >> >>> }
> >> >>>
> >> >>> +/* Allocate and initialise a channel for an incoming connection.
> >> >>> + *
> >> >>> + * ->new_connection() initialises the channel and links it into @conn with
> >> >>> + * __l2cap_chan_add(). The l2cap_chan_create() reference becomes the one owned
> >> >>> + * by the parent subsystem (l2cap_pi(sk)->chan, conn->smp or peer->chan) and is
> >> >>> + * released by its teardown callback; the conn list reference is released by
> >> >>> + * l2cap_chan_del().
> >> >>> + */
> >> >>> +static struct l2cap_chan *l2cap_new_connection(struct l2cap_conn *conn,
> >> >>> + struct l2cap_chan *pchan)
> >> >>> +{
> >> >>> + struct l2cap_chan *chan;
> >> >>> +
> >> >>> + chan = l2cap_chan_create();
> >> >>> + if (!chan)
> >> >>> + return NULL;
> >> >>> +
> >> >>> + if (pchan->ops->new_connection(conn, pchan, chan) < 0) {
> >> >>> + l2cap_chan_put(chan);
> >> >>> + return NULL;
> >> >>> + }
> >> >>
> >> >> I don't quite get why we can't just place __l2cap_chan_add here
> >> >> instead of having it called by new_connection callbacks?
> >> >>
> >> >
> >> > It's specifically the l2cap_sock_new_connection_cb() case - the very
> >> > use-after-free this patch fixes. The __l2cap_chan_add() has to happen while
> >> > the parent lock is still held, and only the callback holds that lock.
> >> >
> >> > The reference counting on the new child chan starts at one ref,
> >> > owned by the new socket:
> >> >
> >> > /* l2cap_new_connection() */
> >> > chan = l2cap_chan_create(); /* refcount = 1 */
> >> > if (!chan)
> >> > return NULL;
> >> >
> >> > pchan->ops->new_connection(conn, pchan, chan);
> >> >
> >> > and inside the socket callback:
> >> >
> >> > /* l2cap_sock_new_connection_cb() */
> >> > lock_sock(parent);
> >> > ...
> >> > sk = l2cap_sock_alloc(..., new_chan); /* sk owns the chan_create ref */
> >> > ...
> >> > l2cap_sock_init(sk, parent);
> >> >
> >> > __l2cap_chan_add(conn, new_chan); /* (A) conn list takes a ref */
> >> > bt_accept_enqueue(parent, sk, false); /* (B) sk now on accept queue */
> >> >
> >> > release_sock(parent); /* (C) parent lock dropped */
> >> > return 0;
> >> >
> >> > The moment we hit (C), sk is reachable through the parent's accept queue, so
> >> > another task can grab and tear it down:
> >> >
> >> > accept() -> l2cap_sock_kill() -> l2cap_sock_put_chan()
> >> > chan->data = NULL;
> >> > l2cap_chan_put(chan); /* drops the sk's chan ref */
> >> >
> >> > If __l2cap_chan_add() at (A) hadn't already taken the conn list reference,
> >> > that put would drop the last ref and free new_chan. Control then returns up
> >> > to l2cap_new_connection(), which hands the now-freed chan back to
> >> > l2cap_connect():
> >> >
> >> > /* l2cap_connect() - runs after the callback returns */
> >> > chan = l2cap_new_connection(conn, pchan);
> >> > if (!chan)
> >> > goto response;
> >> > ...
> >> > bacpy(&chan->src, &conn->hcon->src); /* <-- UAF on freed chan */
> >> > chan->psm = psm;
> >> > chan->dcid = scid;
> >> >
> >> > The conn list reference taken at (A), before (C), is what keeps new_chan
> >> > alive across the release_sock() window so l2cap_connect() can keep using it.
> >> >
> >> > So __l2cap_chan_add() can't move out to l2cap_new_connection(): by the time
> >> > the callback returns, the parent lock is already dropped and chan may already
> >> > be freed - which is exactly the race. It has to be taken inside the callback,
> >> > under the parent lock, before the socket is exposed.
> >
> > If you do after l2cap_new_connection, yes, but what if you do before
> > you call ->new_connect:
> >
> > diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
> > index 3991c179b98f..cd864f400985 100644
> > --- a/net/bluetooth/l2cap_core.c
> > +++ b/net/bluetooth/l2cap_core.c
> > @@ -4027,8 +4027,10 @@ static struct l2cap_chan
> > *l2cap_new_connection(struct l2cap_conn *conn,
> > if (!chan)
> > return NULL;
> >
> > + __l2cap_chan_add(conn, chan);
> > +
> > if (pchan->ops->new_connection(conn, pchan, chan) < 0) {
> > - l2cap_chan_put(chan);
> > + l2cap_chan_del(chan, 0);
> > return NULL;
> > }
> >
> > This means we have to call l2cap_chan_del if new_connection fails, but
> > we can eliminate other functions having to call __l2cap_chan_add with
> > this. If the issue is then having l2cap_chan_del called concurrently,
> > we can add a l2cap_chan_hold; actually, we may need a reference anyway
> > since sk can be released and call l2cap_chan_del, releasing both
> > references.
> >
>
> In:
>
> void __l2cap_chan_add(struct l2cap_conn *conn, struct l2cap_chan *chan)
> {
> ...
> switch (chan->chan_type) {
> case L2CAP_CHAN_CONN_ORIENTED:
> ...
> break;
> case L2CAP_CHAN_CONN_LESS:
> ...
> break;
> case L2CAP_CHAN_FIXED:
> ...
> break;
> default:
> ...
> }
> }
>
> Apparently it depends on chan->chan_type and it is set by ref1.
> It is only set in cb function.
Ok, this probably needs fixing in 6lowpan. These defaults should come
from the parent channel, similar to how it's done in l2cap_sock_init.
Perhaps we should promote that logic to l2cap_chan_set_defaults
passing pchan so it can initialize the channel before calling
__l2cap_chan_add.
^ permalink raw reply [flat|nested] 20+ messages in thread* Re: [PATCH v9 1/1] Bluetooth: L2CAP: Fix use-after-free in l2cap_sock_new_connection_cb()
2026-06-10 16:59 ` Luiz Augusto von Dentz
@ 2026-06-10 17:02 ` Siwei Zhang
2026-06-10 17:12 ` Luiz Augusto von Dentz
0 siblings, 1 reply; 20+ messages in thread
From: Siwei Zhang @ 2026-06-10 17:02 UTC (permalink / raw)
To: Luiz Augusto von Dentz; +Cc: linux-bluetooth
On Wed, Jun 10, 2026, at 12:59 PM, Luiz Augusto von Dentz wrote:
> Hi Siwei,
>
> On Wed, Jun 10, 2026 at 12:33 PM Siwei Zhang <oss@fourdim.xyz> wrote:
>>
>>
>>
>> On Wed, Jun 10, 2026, at 11:57 AM, Luiz Augusto von Dentz wrote:
>> > Hi Siwei,
>> >
>> > On Mon, Jun 8, 2026 at 9:29 AM Siwei Zhang <oss@fourdim.xyz> wrote:
>> >>
>> >> Hi Luiz,
>> >>
>> >> On Thu, Jun 4, 2026, at 11:52 AM, Siwei Zhang wrote:
>> >> > Hi Luiz,
>> >> >
>> >> > On Wed, Jun 3, 2026, at 2:17 PM, Luiz Augusto von Dentz wrote:
>> >> >> Hi Siwei,
>> >> >>
>> >> >> On Wed, Jun 3, 2026 at 11:09 AM Siwei Zhang <oss@fourdim.xyz> wrote:
>> >> >>>
>> >> >>> l2cap_sock_new_connection_cb() accesses l2cap_pi(sk)->chan after
>> >> >>> release_sock(parent). Once the parent lock is released, the child
>> >> >>> socket sk can be freed by another task.
>> >> >>>
>> >> >>> Allocate the channel outside the func to prevent this.
>> >> >>>
>> >> >>> Fixes: 8ffb929098a5 ("Bluetooth: Remove parent socket usage from l2cap_core.c")
>> >> >>> Cc: stable@kernel.org
>> >> >>> Assisted-by: Claude:claude-opus-4-8
>> >> >>> Signed-off-by: Siwei Zhang <oss@fourdim.xyz>
>> >> >>> ---
>> >> >>> include/net/bluetooth/l2cap.h | 10 +++--
>> >> >>> net/bluetooth/6lowpan.c | 31 +++++++------
>> >> >>> net/bluetooth/l2cap_core.c | 41 ++++++++++++-----
>> >> >>> net/bluetooth/l2cap_sock.c | 83 +++++++++++++++++++++--------------
>> >> >>> net/bluetooth/smp.c | 17 ++++---
>> >> >>> 5 files changed, 113 insertions(+), 69 deletions(-)
>> >> >>>
>> >> >>> diff --git a/include/net/bluetooth/l2cap.h b/include/net/bluetooth/l2cap.h
>> >> >>> index e0a1f2293679..7f5e4647f6e0 100644
>> >> >>> --- a/include/net/bluetooth/l2cap.h
>> >> >>> +++ b/include/net/bluetooth/l2cap.h
>> >> >>> @@ -620,7 +620,9 @@ struct l2cap_chan {
>> >> >>> struct l2cap_ops {
>> >> >>> char *name;
>> >> >>>
>> >> >>> - struct l2cap_chan *(*new_connection) (struct l2cap_chan *chan);
>> >> >>> + int (*new_connection)(struct l2cap_conn *conn,
>> >> >>> + struct l2cap_chan *chan,
>> >> >>> + struct l2cap_chan *new_chan);
>> >> >>> int (*recv) (struct l2cap_chan * chan,
>> >> >>> struct sk_buff *skb);
>> >> >>> void (*teardown) (struct l2cap_chan *chan, int err);
>> >> >>> @@ -884,9 +886,11 @@ static inline __u16 __next_seq(struct l2cap_chan *chan, __u16 seq)
>> >> >>> return (seq + 1) % (chan->tx_win_max + 1);
>> >> >>> }
>> >> >>>
>> >> >>> -static inline struct l2cap_chan *l2cap_chan_no_new_connection(struct l2cap_chan *chan)
>> >> >>> +static inline int l2cap_chan_no_new_connection(struct l2cap_conn *conn,
>> >> >>> + struct l2cap_chan *chan,
>> >> >>> + struct l2cap_chan *new_chan)
>> >> >>> {
>> >> >>> - return NULL;
>> >> >>> + return -EOPNOTSUPP;
>> >> >>> }
>> >> >>>
>> >> >>> static inline int l2cap_chan_no_recv(struct l2cap_chan *chan, struct sk_buff *skb)
>> >> >>> diff --git a/net/bluetooth/6lowpan.c b/net/bluetooth/6lowpan.c
>> >> >>> index cb1e329d66fd..94863af97a44 100644
>> >> >>> --- a/net/bluetooth/6lowpan.c
>> >> >>> +++ b/net/bluetooth/6lowpan.c
>> >> >>> @@ -624,6 +624,15 @@ static bool is_bt_6lowpan(struct hci_conn *hcon)
>> >> >>> return true;
>> >> >>> }
>> >> >>>
>> >> >>> +static void chan_init(struct l2cap_chan *chan)
>> >> >>> +{
>> >> >>> + l2cap_chan_set_defaults(chan);
>> >> >>> +
>> >> >>> + chan->chan_type = L2CAP_CHAN_CONN_ORIENTED;
>> >> >>> + chan->mode = L2CAP_MODE_LE_FLOWCTL;
>> >> >>> + chan->imtu = 1280;
>> >> >>> +}
>> >> >>> +
>> >> >>> static struct l2cap_chan *chan_create(void)
>> >> >>> {
>> >> >>> struct l2cap_chan *chan;
>> >> >>> @@ -632,11 +641,7 @@ static struct l2cap_chan *chan_create(void)
>> >> >>> if (!chan)
>> >> >>> return NULL;
>> >> >>>
>> >> >>> - l2cap_chan_set_defaults(chan);
>> >> >>> -
>> >> >>> - chan->chan_type = L2CAP_CHAN_CONN_ORIENTED;
>> >> >>> - chan->mode = L2CAP_MODE_LE_FLOWCTL;
>> >> >>> - chan->imtu = 1280;
>> >> >>> + chan_init(chan);
>> >> >>>
>> >> >>> return chan;
>> >> >>> }
>> >> >>> @@ -745,19 +750,19 @@ static inline void chan_ready_cb(struct l2cap_chan *chan)
>> >> >>> ifup(dev->netdev);
>> >> >>> }
>> >> >>>
>> >> >>> -static inline struct l2cap_chan *chan_new_conn_cb(struct l2cap_chan *pchan)
>> >> >>> +static inline int chan_new_conn_cb(struct l2cap_conn *conn,
>> >> >>> + struct l2cap_chan *pchan,
>> >> >>> + struct l2cap_chan *chan)
>> >> >>> {
>> >> >>> - struct l2cap_chan *chan;
>> >> >>> -
>> >> >>> - chan = chan_create();
>> >> >>> - if (!chan)
>> >> >>> - return NULL;
>> >> >>> -
>> >> >>> + chan_init(chan);
>>
>> ref1: chan->chan_type = L2CAP_CHAN_CONN_ORIENTED; set
>> in chan_init.
>>
>> >> >>> chan->ops = pchan->ops;
>> >> >>>
>> >> >>> + /* Take the conn list reference; see l2cap_new_connection(). */
>> >> >>> + __l2cap_chan_add(conn, chan);
>> >> >>> +
>> >> >>> BT_DBG("chan %p pchan %p", chan, pchan);
>> >> >>>
>> >> >>> - return chan;
>> >> >>> + return 0;
>> >> >>> }
>> >> >>>
>> >> >>> static void unregister_dev(struct lowpan_btle_dev *dev)
>> >> >>> diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
>> >> >>> index c4ccfbda9d78..62acf90837fb 100644
>> >> >>> --- a/net/bluetooth/l2cap_core.c
>> >> >>> +++ b/net/bluetooth/l2cap_core.c
>> >> >>> @@ -4007,6 +4007,31 @@ static inline int l2cap_command_rej(struct l2cap_conn *conn,
>> >> >>> return 0;
>> >> >>> }
>> >> >>>
>> >> >>> +/* Allocate and initialise a channel for an incoming connection.
>> >> >>> + *
>> >> >>> + * ->new_connection() initialises the channel and links it into @conn with
>> >> >>> + * __l2cap_chan_add(). The l2cap_chan_create() reference becomes the one owned
>> >> >>> + * by the parent subsystem (l2cap_pi(sk)->chan, conn->smp or peer->chan) and is
>> >> >>> + * released by its teardown callback; the conn list reference is released by
>> >> >>> + * l2cap_chan_del().
>> >> >>> + */
>> >> >>> +static struct l2cap_chan *l2cap_new_connection(struct l2cap_conn *conn,
>> >> >>> + struct l2cap_chan *pchan)
>> >> >>> +{
>> >> >>> + struct l2cap_chan *chan;
>> >> >>> +
>> >> >>> + chan = l2cap_chan_create();
>> >> >>> + if (!chan)
>> >> >>> + return NULL;
>> >> >>> +
>> >> >>> + if (pchan->ops->new_connection(conn, pchan, chan) < 0) {
>> >> >>> + l2cap_chan_put(chan);
>> >> >>> + return NULL;
>> >> >>> + }
>> >> >>
>> >> >> I don't quite get why we can't just place __l2cap_chan_add here
>> >> >> instead of having it called by new_connection callbacks?
>> >> >>
>> >> >
>> >> > It's specifically the l2cap_sock_new_connection_cb() case - the very
>> >> > use-after-free this patch fixes. The __l2cap_chan_add() has to happen while
>> >> > the parent lock is still held, and only the callback holds that lock.
>> >> >
>> >> > The reference counting on the new child chan starts at one ref,
>> >> > owned by the new socket:
>> >> >
>> >> > /* l2cap_new_connection() */
>> >> > chan = l2cap_chan_create(); /* refcount = 1 */
>> >> > if (!chan)
>> >> > return NULL;
>> >> >
>> >> > pchan->ops->new_connection(conn, pchan, chan);
>> >> >
>> >> > and inside the socket callback:
>> >> >
>> >> > /* l2cap_sock_new_connection_cb() */
>> >> > lock_sock(parent);
>> >> > ...
>> >> > sk = l2cap_sock_alloc(..., new_chan); /* sk owns the chan_create ref */
>> >> > ...
>> >> > l2cap_sock_init(sk, parent);
>> >> >
>> >> > __l2cap_chan_add(conn, new_chan); /* (A) conn list takes a ref */
>> >> > bt_accept_enqueue(parent, sk, false); /* (B) sk now on accept queue */
>> >> >
>> >> > release_sock(parent); /* (C) parent lock dropped */
>> >> > return 0;
>> >> >
>> >> > The moment we hit (C), sk is reachable through the parent's accept queue, so
>> >> > another task can grab and tear it down:
>> >> >
>> >> > accept() -> l2cap_sock_kill() -> l2cap_sock_put_chan()
>> >> > chan->data = NULL;
>> >> > l2cap_chan_put(chan); /* drops the sk's chan ref */
>> >> >
>> >> > If __l2cap_chan_add() at (A) hadn't already taken the conn list reference,
>> >> > that put would drop the last ref and free new_chan. Control then returns up
>> >> > to l2cap_new_connection(), which hands the now-freed chan back to
>> >> > l2cap_connect():
>> >> >
>> >> > /* l2cap_connect() - runs after the callback returns */
>> >> > chan = l2cap_new_connection(conn, pchan);
>> >> > if (!chan)
>> >> > goto response;
>> >> > ...
>> >> > bacpy(&chan->src, &conn->hcon->src); /* <-- UAF on freed chan */
>> >> > chan->psm = psm;
>> >> > chan->dcid = scid;
>> >> >
>> >> > The conn list reference taken at (A), before (C), is what keeps new_chan
>> >> > alive across the release_sock() window so l2cap_connect() can keep using it.
>> >> >
>> >> > So __l2cap_chan_add() can't move out to l2cap_new_connection(): by the time
>> >> > the callback returns, the parent lock is already dropped and chan may already
>> >> > be freed - which is exactly the race. It has to be taken inside the callback,
>> >> > under the parent lock, before the socket is exposed.
>> >
>> > If you do after l2cap_new_connection, yes, but what if you do before
>> > you call ->new_connect:
>> >
>> > diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
>> > index 3991c179b98f..cd864f400985 100644
>> > --- a/net/bluetooth/l2cap_core.c
>> > +++ b/net/bluetooth/l2cap_core.c
>> > @@ -4027,8 +4027,10 @@ static struct l2cap_chan
>> > *l2cap_new_connection(struct l2cap_conn *conn,
>> > if (!chan)
>> > return NULL;
>> >
>> > + __l2cap_chan_add(conn, chan);
>> > +
>> > if (pchan->ops->new_connection(conn, pchan, chan) < 0) {
>> > - l2cap_chan_put(chan);
>> > + l2cap_chan_del(chan, 0);
>> > return NULL;
>> > }
>> >
>> > This means we have to call l2cap_chan_del if new_connection fails, but
>> > we can eliminate other functions having to call __l2cap_chan_add with
>> > this. If the issue is then having l2cap_chan_del called concurrently,
>> > we can add a l2cap_chan_hold; actually, we may need a reference anyway
>> > since sk can be released and call l2cap_chan_del, releasing both
>> > references.
>> >
>>
>> In:
>>
>> void __l2cap_chan_add(struct l2cap_conn *conn, struct l2cap_chan *chan)
>> {
>> ...
>> switch (chan->chan_type) {
>> case L2CAP_CHAN_CONN_ORIENTED:
>> ...
>> break;
>> case L2CAP_CHAN_CONN_LESS:
>> ...
>> break;
>> case L2CAP_CHAN_FIXED:
>> ...
>> break;
>> default:
>> ...
>> }
>> }
>>
>> Apparently it depends on chan->chan_type and it is set by ref1.
>> It is only set in cb function.
>
> Ok, this probably needs fixing in 6lowpan. These defaults should come
> from the parent channel, similar to how it's done in l2cap_sock_init.
> Perhaps we should promote that logic to l2cap_chan_set_defaults
> passing pchan so it can initialize the channel before calling
> __l2cap_chan_add.
I can fix that in a separate patch.
Could you please merge it first?
Best,
Siwei
^ permalink raw reply [flat|nested] 20+ messages in thread* Re: [PATCH v9 1/1] Bluetooth: L2CAP: Fix use-after-free in l2cap_sock_new_connection_cb()
2026-06-10 17:02 ` Siwei Zhang
@ 2026-06-10 17:12 ` Luiz Augusto von Dentz
2026-06-10 17:13 ` Siwei Zhang
0 siblings, 1 reply; 20+ messages in thread
From: Luiz Augusto von Dentz @ 2026-06-10 17:12 UTC (permalink / raw)
To: Siwei Zhang; +Cc: linux-bluetooth
Hi Siwei,
On Wed, Jun 10, 2026 at 1:03 PM Siwei Zhang <oss@fourdim.xyz> wrote:
>
>
> On Wed, Jun 10, 2026, at 12:59 PM, Luiz Augusto von Dentz wrote:
> > Hi Siwei,
> >
> > On Wed, Jun 10, 2026 at 12:33 PM Siwei Zhang <oss@fourdim.xyz> wrote:
> >>
> >>
> >>
> >> On Wed, Jun 10, 2026, at 11:57 AM, Luiz Augusto von Dentz wrote:
> >> > Hi Siwei,
> >> >
> >> > On Mon, Jun 8, 2026 at 9:29 AM Siwei Zhang <oss@fourdim.xyz> wrote:
> >> >>
> >> >> Hi Luiz,
> >> >>
> >> >> On Thu, Jun 4, 2026, at 11:52 AM, Siwei Zhang wrote:
> >> >> > Hi Luiz,
> >> >> >
> >> >> > On Wed, Jun 3, 2026, at 2:17 PM, Luiz Augusto von Dentz wrote:
> >> >> >> Hi Siwei,
> >> >> >>
> >> >> >> On Wed, Jun 3, 2026 at 11:09 AM Siwei Zhang <oss@fourdim.xyz> wrote:
> >> >> >>>
> >> >> >>> l2cap_sock_new_connection_cb() accesses l2cap_pi(sk)->chan after
> >> >> >>> release_sock(parent). Once the parent lock is released, the child
> >> >> >>> socket sk can be freed by another task.
> >> >> >>>
> >> >> >>> Allocate the channel outside the func to prevent this.
> >> >> >>>
> >> >> >>> Fixes: 8ffb929098a5 ("Bluetooth: Remove parent socket usage from l2cap_core.c")
> >> >> >>> Cc: stable@kernel.org
> >> >> >>> Assisted-by: Claude:claude-opus-4-8
> >> >> >>> Signed-off-by: Siwei Zhang <oss@fourdim.xyz>
> >> >> >>> ---
> >> >> >>> include/net/bluetooth/l2cap.h | 10 +++--
> >> >> >>> net/bluetooth/6lowpan.c | 31 +++++++------
> >> >> >>> net/bluetooth/l2cap_core.c | 41 ++++++++++++-----
> >> >> >>> net/bluetooth/l2cap_sock.c | 83 +++++++++++++++++++++--------------
> >> >> >>> net/bluetooth/smp.c | 17 ++++---
> >> >> >>> 5 files changed, 113 insertions(+), 69 deletions(-)
> >> >> >>>
> >> >> >>> diff --git a/include/net/bluetooth/l2cap.h b/include/net/bluetooth/l2cap.h
> >> >> >>> index e0a1f2293679..7f5e4647f6e0 100644
> >> >> >>> --- a/include/net/bluetooth/l2cap.h
> >> >> >>> +++ b/include/net/bluetooth/l2cap.h
> >> >> >>> @@ -620,7 +620,9 @@ struct l2cap_chan {
> >> >> >>> struct l2cap_ops {
> >> >> >>> char *name;
> >> >> >>>
> >> >> >>> - struct l2cap_chan *(*new_connection) (struct l2cap_chan *chan);
> >> >> >>> + int (*new_connection)(struct l2cap_conn *conn,
> >> >> >>> + struct l2cap_chan *chan,
> >> >> >>> + struct l2cap_chan *new_chan);
> >> >> >>> int (*recv) (struct l2cap_chan * chan,
> >> >> >>> struct sk_buff *skb);
> >> >> >>> void (*teardown) (struct l2cap_chan *chan, int err);
> >> >> >>> @@ -884,9 +886,11 @@ static inline __u16 __next_seq(struct l2cap_chan *chan, __u16 seq)
> >> >> >>> return (seq + 1) % (chan->tx_win_max + 1);
> >> >> >>> }
> >> >> >>>
> >> >> >>> -static inline struct l2cap_chan *l2cap_chan_no_new_connection(struct l2cap_chan *chan)
> >> >> >>> +static inline int l2cap_chan_no_new_connection(struct l2cap_conn *conn,
> >> >> >>> + struct l2cap_chan *chan,
> >> >> >>> + struct l2cap_chan *new_chan)
> >> >> >>> {
> >> >> >>> - return NULL;
> >> >> >>> + return -EOPNOTSUPP;
> >> >> >>> }
> >> >> >>>
> >> >> >>> static inline int l2cap_chan_no_recv(struct l2cap_chan *chan, struct sk_buff *skb)
> >> >> >>> diff --git a/net/bluetooth/6lowpan.c b/net/bluetooth/6lowpan.c
> >> >> >>> index cb1e329d66fd..94863af97a44 100644
> >> >> >>> --- a/net/bluetooth/6lowpan.c
> >> >> >>> +++ b/net/bluetooth/6lowpan.c
> >> >> >>> @@ -624,6 +624,15 @@ static bool is_bt_6lowpan(struct hci_conn *hcon)
> >> >> >>> return true;
> >> >> >>> }
> >> >> >>>
> >> >> >>> +static void chan_init(struct l2cap_chan *chan)
> >> >> >>> +{
> >> >> >>> + l2cap_chan_set_defaults(chan);
> >> >> >>> +
> >> >> >>> + chan->chan_type = L2CAP_CHAN_CONN_ORIENTED;
> >> >> >>> + chan->mode = L2CAP_MODE_LE_FLOWCTL;
> >> >> >>> + chan->imtu = 1280;
> >> >> >>> +}
> >> >> >>> +
> >> >> >>> static struct l2cap_chan *chan_create(void)
> >> >> >>> {
> >> >> >>> struct l2cap_chan *chan;
> >> >> >>> @@ -632,11 +641,7 @@ static struct l2cap_chan *chan_create(void)
> >> >> >>> if (!chan)
> >> >> >>> return NULL;
> >> >> >>>
> >> >> >>> - l2cap_chan_set_defaults(chan);
> >> >> >>> -
> >> >> >>> - chan->chan_type = L2CAP_CHAN_CONN_ORIENTED;
> >> >> >>> - chan->mode = L2CAP_MODE_LE_FLOWCTL;
> >> >> >>> - chan->imtu = 1280;
> >> >> >>> + chan_init(chan);
> >> >> >>>
> >> >> >>> return chan;
> >> >> >>> }
> >> >> >>> @@ -745,19 +750,19 @@ static inline void chan_ready_cb(struct l2cap_chan *chan)
> >> >> >>> ifup(dev->netdev);
> >> >> >>> }
> >> >> >>>
> >> >> >>> -static inline struct l2cap_chan *chan_new_conn_cb(struct l2cap_chan *pchan)
> >> >> >>> +static inline int chan_new_conn_cb(struct l2cap_conn *conn,
> >> >> >>> + struct l2cap_chan *pchan,
> >> >> >>> + struct l2cap_chan *chan)
> >> >> >>> {
> >> >> >>> - struct l2cap_chan *chan;
> >> >> >>> -
> >> >> >>> - chan = chan_create();
> >> >> >>> - if (!chan)
> >> >> >>> - return NULL;
> >> >> >>> -
> >> >> >>> + chan_init(chan);
> >>
> >> ref1: chan->chan_type = L2CAP_CHAN_CONN_ORIENTED; set
> >> in chan_init.
> >>
> >> >> >>> chan->ops = pchan->ops;
> >> >> >>>
> >> >> >>> + /* Take the conn list reference; see l2cap_new_connection(). */
> >> >> >>> + __l2cap_chan_add(conn, chan);
> >> >> >>> +
> >> >> >>> BT_DBG("chan %p pchan %p", chan, pchan);
> >> >> >>>
> >> >> >>> - return chan;
> >> >> >>> + return 0;
> >> >> >>> }
> >> >> >>>
> >> >> >>> static void unregister_dev(struct lowpan_btle_dev *dev)
> >> >> >>> diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
> >> >> >>> index c4ccfbda9d78..62acf90837fb 100644
> >> >> >>> --- a/net/bluetooth/l2cap_core.c
> >> >> >>> +++ b/net/bluetooth/l2cap_core.c
> >> >> >>> @@ -4007,6 +4007,31 @@ static inline int l2cap_command_rej(struct l2cap_conn *conn,
> >> >> >>> return 0;
> >> >> >>> }
> >> >> >>>
> >> >> >>> +/* Allocate and initialise a channel for an incoming connection.
> >> >> >>> + *
> >> >> >>> + * ->new_connection() initialises the channel and links it into @conn with
> >> >> >>> + * __l2cap_chan_add(). The l2cap_chan_create() reference becomes the one owned
> >> >> >>> + * by the parent subsystem (l2cap_pi(sk)->chan, conn->smp or peer->chan) and is
> >> >> >>> + * released by its teardown callback; the conn list reference is released by
> >> >> >>> + * l2cap_chan_del().
> >> >> >>> + */
> >> >> >>> +static struct l2cap_chan *l2cap_new_connection(struct l2cap_conn *conn,
> >> >> >>> + struct l2cap_chan *pchan)
> >> >> >>> +{
> >> >> >>> + struct l2cap_chan *chan;
> >> >> >>> +
> >> >> >>> + chan = l2cap_chan_create();
> >> >> >>> + if (!chan)
> >> >> >>> + return NULL;
> >> >> >>> +
> >> >> >>> + if (pchan->ops->new_connection(conn, pchan, chan) < 0) {
> >> >> >>> + l2cap_chan_put(chan);
> >> >> >>> + return NULL;
> >> >> >>> + }
> >> >> >>
> >> >> >> I don't quite get why we can't just place __l2cap_chan_add here
> >> >> >> instead of having it called by new_connection callbacks?
> >> >> >>
> >> >> >
> >> >> > It's specifically the l2cap_sock_new_connection_cb() case - the very
> >> >> > use-after-free this patch fixes. The __l2cap_chan_add() has to happen while
> >> >> > the parent lock is still held, and only the callback holds that lock.
> >> >> >
> >> >> > The reference counting on the new child chan starts at one ref,
> >> >> > owned by the new socket:
> >> >> >
> >> >> > /* l2cap_new_connection() */
> >> >> > chan = l2cap_chan_create(); /* refcount = 1 */
> >> >> > if (!chan)
> >> >> > return NULL;
> >> >> >
> >> >> > pchan->ops->new_connection(conn, pchan, chan);
> >> >> >
> >> >> > and inside the socket callback:
> >> >> >
> >> >> > /* l2cap_sock_new_connection_cb() */
> >> >> > lock_sock(parent);
> >> >> > ...
> >> >> > sk = l2cap_sock_alloc(..., new_chan); /* sk owns the chan_create ref */
> >> >> > ...
> >> >> > l2cap_sock_init(sk, parent);
> >> >> >
> >> >> > __l2cap_chan_add(conn, new_chan); /* (A) conn list takes a ref */
> >> >> > bt_accept_enqueue(parent, sk, false); /* (B) sk now on accept queue */
> >> >> >
> >> >> > release_sock(parent); /* (C) parent lock dropped */
> >> >> > return 0;
> >> >> >
> >> >> > The moment we hit (C), sk is reachable through the parent's accept queue, so
> >> >> > another task can grab and tear it down:
> >> >> >
> >> >> > accept() -> l2cap_sock_kill() -> l2cap_sock_put_chan()
> >> >> > chan->data = NULL;
> >> >> > l2cap_chan_put(chan); /* drops the sk's chan ref */
> >> >> >
> >> >> > If __l2cap_chan_add() at (A) hadn't already taken the conn list reference,
> >> >> > that put would drop the last ref and free new_chan. Control then returns up
> >> >> > to l2cap_new_connection(), which hands the now-freed chan back to
> >> >> > l2cap_connect():
> >> >> >
> >> >> > /* l2cap_connect() - runs after the callback returns */
> >> >> > chan = l2cap_new_connection(conn, pchan);
> >> >> > if (!chan)
> >> >> > goto response;
> >> >> > ...
> >> >> > bacpy(&chan->src, &conn->hcon->src); /* <-- UAF on freed chan */
> >> >> > chan->psm = psm;
> >> >> > chan->dcid = scid;
> >> >> >
> >> >> > The conn list reference taken at (A), before (C), is what keeps new_chan
> >> >> > alive across the release_sock() window so l2cap_connect() can keep using it.
> >> >> >
> >> >> > So __l2cap_chan_add() can't move out to l2cap_new_connection(): by the time
> >> >> > the callback returns, the parent lock is already dropped and chan may already
> >> >> > be freed - which is exactly the race. It has to be taken inside the callback,
> >> >> > under the parent lock, before the socket is exposed.
> >> >
> >> > If you do after l2cap_new_connection, yes, but what if you do before
> >> > you call ->new_connect:
> >> >
> >> > diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
> >> > index 3991c179b98f..cd864f400985 100644
> >> > --- a/net/bluetooth/l2cap_core.c
> >> > +++ b/net/bluetooth/l2cap_core.c
> >> > @@ -4027,8 +4027,10 @@ static struct l2cap_chan
> >> > *l2cap_new_connection(struct l2cap_conn *conn,
> >> > if (!chan)
> >> > return NULL;
> >> >
> >> > + __l2cap_chan_add(conn, chan);
> >> > +
> >> > if (pchan->ops->new_connection(conn, pchan, chan) < 0) {
> >> > - l2cap_chan_put(chan);
> >> > + l2cap_chan_del(chan, 0);
> >> > return NULL;
> >> > }
> >> >
> >> > This means we have to call l2cap_chan_del if new_connection fails, but
> >> > we can eliminate other functions having to call __l2cap_chan_add with
> >> > this. If the issue is then having l2cap_chan_del called concurrently,
> >> > we can add a l2cap_chan_hold; actually, we may need a reference anyway
> >> > since sk can be released and call l2cap_chan_del, releasing both
> >> > references.
> >> >
> >>
> >> In:
> >>
> >> void __l2cap_chan_add(struct l2cap_conn *conn, struct l2cap_chan *chan)
> >> {
> >> ...
> >> switch (chan->chan_type) {
> >> case L2CAP_CHAN_CONN_ORIENTED:
> >> ...
> >> break;
> >> case L2CAP_CHAN_CONN_LESS:
> >> ...
> >> break;
> >> case L2CAP_CHAN_FIXED:
> >> ...
> >> break;
> >> default:
> >> ...
> >> }
> >> }
> >>
> >> Apparently it depends on chan->chan_type and it is set by ref1.
> >> It is only set in cb function.
> >
> > Ok, this probably needs fixing in 6lowpan. These defaults should come
> > from the parent channel, similar to how it's done in l2cap_sock_init.
> > Perhaps we should promote that logic to l2cap_chan_set_defaults
> > passing pchan so it can initialize the channel before calling
> > __l2cap_chan_add.
>
> I can fix that in a separate patch.
> Could you please merge it first?
Merge it first, and then rework the order? Not sure what is the
benefit of doing that, it is a somewhat substatial change either way
so I don't think it will make it much easier to lets say backport.
> Best,
> Siwei
--
Luiz Augusto von Dentz
^ permalink raw reply [flat|nested] 20+ messages in thread* Re: [PATCH v9 1/1] Bluetooth: L2CAP: Fix use-after-free in l2cap_sock_new_connection_cb()
2026-06-10 17:12 ` Luiz Augusto von Dentz
@ 2026-06-10 17:13 ` Siwei Zhang
0 siblings, 0 replies; 20+ messages in thread
From: Siwei Zhang @ 2026-06-10 17:13 UTC (permalink / raw)
To: Luiz Augusto von Dentz; +Cc: linux-bluetooth
On Wed, Jun 10, 2026, at 1:12 PM, Luiz Augusto von Dentz wrote:
> Hi Siwei,
>
> On Wed, Jun 10, 2026 at 1:03 PM Siwei Zhang <oss@fourdim.xyz> wrote:
>>
>>
>> On Wed, Jun 10, 2026, at 12:59 PM, Luiz Augusto von Dentz wrote:
>> > Hi Siwei,
>> >
>> > On Wed, Jun 10, 2026 at 12:33 PM Siwei Zhang <oss@fourdim.xyz> wrote:
>> >>
>> >>
>> >>
>> >> On Wed, Jun 10, 2026, at 11:57 AM, Luiz Augusto von Dentz wrote:
>> >> > Hi Siwei,
>> >> >
>> >> > On Mon, Jun 8, 2026 at 9:29 AM Siwei Zhang <oss@fourdim.xyz> wrote:
>> >> >>
>> >> >> Hi Luiz,
>> >> >>
>> >> >> On Thu, Jun 4, 2026, at 11:52 AM, Siwei Zhang wrote:
>> >> >> > Hi Luiz,
>> >> >> >
>> >> >> > On Wed, Jun 3, 2026, at 2:17 PM, Luiz Augusto von Dentz wrote:
>> >> >> >> Hi Siwei,
>> >> >> >>
>> >> >> >> On Wed, Jun 3, 2026 at 11:09 AM Siwei Zhang <oss@fourdim.xyz> wrote:
>> >> >> >>>
>> >> >> >>> l2cap_sock_new_connection_cb() accesses l2cap_pi(sk)->chan after
>> >> >> >>> release_sock(parent). Once the parent lock is released, the child
>> >> >> >>> socket sk can be freed by another task.
>> >> >> >>>
>> >> >> >>> Allocate the channel outside the func to prevent this.
>> >> >> >>>
>> >> >> >>> Fixes: 8ffb929098a5 ("Bluetooth: Remove parent socket usage from l2cap_core.c")
>> >> >> >>> Cc: stable@kernel.org
>> >> >> >>> Assisted-by: Claude:claude-opus-4-8
>> >> >> >>> Signed-off-by: Siwei Zhang <oss@fourdim.xyz>
>> >> >> >>> ---
>> >> >> >>> include/net/bluetooth/l2cap.h | 10 +++--
>> >> >> >>> net/bluetooth/6lowpan.c | 31 +++++++------
>> >> >> >>> net/bluetooth/l2cap_core.c | 41 ++++++++++++-----
>> >> >> >>> net/bluetooth/l2cap_sock.c | 83 +++++++++++++++++++++--------------
>> >> >> >>> net/bluetooth/smp.c | 17 ++++---
>> >> >> >>> 5 files changed, 113 insertions(+), 69 deletions(-)
>> >> >> >>>
>> >> >> >>> diff --git a/include/net/bluetooth/l2cap.h b/include/net/bluetooth/l2cap.h
>> >> >> >>> index e0a1f2293679..7f5e4647f6e0 100644
>> >> >> >>> --- a/include/net/bluetooth/l2cap.h
>> >> >> >>> +++ b/include/net/bluetooth/l2cap.h
>> >> >> >>> @@ -620,7 +620,9 @@ struct l2cap_chan {
>> >> >> >>> struct l2cap_ops {
>> >> >> >>> char *name;
>> >> >> >>>
>> >> >> >>> - struct l2cap_chan *(*new_connection) (struct l2cap_chan *chan);
>> >> >> >>> + int (*new_connection)(struct l2cap_conn *conn,
>> >> >> >>> + struct l2cap_chan *chan,
>> >> >> >>> + struct l2cap_chan *new_chan);
>> >> >> >>> int (*recv) (struct l2cap_chan * chan,
>> >> >> >>> struct sk_buff *skb);
>> >> >> >>> void (*teardown) (struct l2cap_chan *chan, int err);
>> >> >> >>> @@ -884,9 +886,11 @@ static inline __u16 __next_seq(struct l2cap_chan *chan, __u16 seq)
>> >> >> >>> return (seq + 1) % (chan->tx_win_max + 1);
>> >> >> >>> }
>> >> >> >>>
>> >> >> >>> -static inline struct l2cap_chan *l2cap_chan_no_new_connection(struct l2cap_chan *chan)
>> >> >> >>> +static inline int l2cap_chan_no_new_connection(struct l2cap_conn *conn,
>> >> >> >>> + struct l2cap_chan *chan,
>> >> >> >>> + struct l2cap_chan *new_chan)
>> >> >> >>> {
>> >> >> >>> - return NULL;
>> >> >> >>> + return -EOPNOTSUPP;
>> >> >> >>> }
>> >> >> >>>
>> >> >> >>> static inline int l2cap_chan_no_recv(struct l2cap_chan *chan, struct sk_buff *skb)
>> >> >> >>> diff --git a/net/bluetooth/6lowpan.c b/net/bluetooth/6lowpan.c
>> >> >> >>> index cb1e329d66fd..94863af97a44 100644
>> >> >> >>> --- a/net/bluetooth/6lowpan.c
>> >> >> >>> +++ b/net/bluetooth/6lowpan.c
>> >> >> >>> @@ -624,6 +624,15 @@ static bool is_bt_6lowpan(struct hci_conn *hcon)
>> >> >> >>> return true;
>> >> >> >>> }
>> >> >> >>>
>> >> >> >>> +static void chan_init(struct l2cap_chan *chan)
>> >> >> >>> +{
>> >> >> >>> + l2cap_chan_set_defaults(chan);
>> >> >> >>> +
>> >> >> >>> + chan->chan_type = L2CAP_CHAN_CONN_ORIENTED;
>> >> >> >>> + chan->mode = L2CAP_MODE_LE_FLOWCTL;
>> >> >> >>> + chan->imtu = 1280;
>> >> >> >>> +}
>> >> >> >>> +
>> >> >> >>> static struct l2cap_chan *chan_create(void)
>> >> >> >>> {
>> >> >> >>> struct l2cap_chan *chan;
>> >> >> >>> @@ -632,11 +641,7 @@ static struct l2cap_chan *chan_create(void)
>> >> >> >>> if (!chan)
>> >> >> >>> return NULL;
>> >> >> >>>
>> >> >> >>> - l2cap_chan_set_defaults(chan);
>> >> >> >>> -
>> >> >> >>> - chan->chan_type = L2CAP_CHAN_CONN_ORIENTED;
>> >> >> >>> - chan->mode = L2CAP_MODE_LE_FLOWCTL;
>> >> >> >>> - chan->imtu = 1280;
>> >> >> >>> + chan_init(chan);
>> >> >> >>>
>> >> >> >>> return chan;
>> >> >> >>> }
>> >> >> >>> @@ -745,19 +750,19 @@ static inline void chan_ready_cb(struct l2cap_chan *chan)
>> >> >> >>> ifup(dev->netdev);
>> >> >> >>> }
>> >> >> >>>
>> >> >> >>> -static inline struct l2cap_chan *chan_new_conn_cb(struct l2cap_chan *pchan)
>> >> >> >>> +static inline int chan_new_conn_cb(struct l2cap_conn *conn,
>> >> >> >>> + struct l2cap_chan *pchan,
>> >> >> >>> + struct l2cap_chan *chan)
>> >> >> >>> {
>> >> >> >>> - struct l2cap_chan *chan;
>> >> >> >>> -
>> >> >> >>> - chan = chan_create();
>> >> >> >>> - if (!chan)
>> >> >> >>> - return NULL;
>> >> >> >>> -
>> >> >> >>> + chan_init(chan);
>> >>
>> >> ref1: chan->chan_type = L2CAP_CHAN_CONN_ORIENTED; set
>> >> in chan_init.
>> >>
>> >> >> >>> chan->ops = pchan->ops;
>> >> >> >>>
>> >> >> >>> + /* Take the conn list reference; see l2cap_new_connection(). */
>> >> >> >>> + __l2cap_chan_add(conn, chan);
>> >> >> >>> +
>> >> >> >>> BT_DBG("chan %p pchan %p", chan, pchan);
>> >> >> >>>
>> >> >> >>> - return chan;
>> >> >> >>> + return 0;
>> >> >> >>> }
>> >> >> >>>
>> >> >> >>> static void unregister_dev(struct lowpan_btle_dev *dev)
>> >> >> >>> diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
>> >> >> >>> index c4ccfbda9d78..62acf90837fb 100644
>> >> >> >>> --- a/net/bluetooth/l2cap_core.c
>> >> >> >>> +++ b/net/bluetooth/l2cap_core.c
>> >> >> >>> @@ -4007,6 +4007,31 @@ static inline int l2cap_command_rej(struct l2cap_conn *conn,
>> >> >> >>> return 0;
>> >> >> >>> }
>> >> >> >>>
>> >> >> >>> +/* Allocate and initialise a channel for an incoming connection.
>> >> >> >>> + *
>> >> >> >>> + * ->new_connection() initialises the channel and links it into @conn with
>> >> >> >>> + * __l2cap_chan_add(). The l2cap_chan_create() reference becomes the one owned
>> >> >> >>> + * by the parent subsystem (l2cap_pi(sk)->chan, conn->smp or peer->chan) and is
>> >> >> >>> + * released by its teardown callback; the conn list reference is released by
>> >> >> >>> + * l2cap_chan_del().
>> >> >> >>> + */
>> >> >> >>> +static struct l2cap_chan *l2cap_new_connection(struct l2cap_conn *conn,
>> >> >> >>> + struct l2cap_chan *pchan)
>> >> >> >>> +{
>> >> >> >>> + struct l2cap_chan *chan;
>> >> >> >>> +
>> >> >> >>> + chan = l2cap_chan_create();
>> >> >> >>> + if (!chan)
>> >> >> >>> + return NULL;
>> >> >> >>> +
>> >> >> >>> + if (pchan->ops->new_connection(conn, pchan, chan) < 0) {
>> >> >> >>> + l2cap_chan_put(chan);
>> >> >> >>> + return NULL;
>> >> >> >>> + }
>> >> >> >>
>> >> >> >> I don't quite get why we can't just place __l2cap_chan_add here
>> >> >> >> instead of having it called by new_connection callbacks?
>> >> >> >>
>> >> >> >
>> >> >> > It's specifically the l2cap_sock_new_connection_cb() case - the very
>> >> >> > use-after-free this patch fixes. The __l2cap_chan_add() has to happen while
>> >> >> > the parent lock is still held, and only the callback holds that lock.
>> >> >> >
>> >> >> > The reference counting on the new child chan starts at one ref,
>> >> >> > owned by the new socket:
>> >> >> >
>> >> >> > /* l2cap_new_connection() */
>> >> >> > chan = l2cap_chan_create(); /* refcount = 1 */
>> >> >> > if (!chan)
>> >> >> > return NULL;
>> >> >> >
>> >> >> > pchan->ops->new_connection(conn, pchan, chan);
>> >> >> >
>> >> >> > and inside the socket callback:
>> >> >> >
>> >> >> > /* l2cap_sock_new_connection_cb() */
>> >> >> > lock_sock(parent);
>> >> >> > ...
>> >> >> > sk = l2cap_sock_alloc(..., new_chan); /* sk owns the chan_create ref */
>> >> >> > ...
>> >> >> > l2cap_sock_init(sk, parent);
>> >> >> >
>> >> >> > __l2cap_chan_add(conn, new_chan); /* (A) conn list takes a ref */
>> >> >> > bt_accept_enqueue(parent, sk, false); /* (B) sk now on accept queue */
>> >> >> >
>> >> >> > release_sock(parent); /* (C) parent lock dropped */
>> >> >> > return 0;
>> >> >> >
>> >> >> > The moment we hit (C), sk is reachable through the parent's accept queue, so
>> >> >> > another task can grab and tear it down:
>> >> >> >
>> >> >> > accept() -> l2cap_sock_kill() -> l2cap_sock_put_chan()
>> >> >> > chan->data = NULL;
>> >> >> > l2cap_chan_put(chan); /* drops the sk's chan ref */
>> >> >> >
>> >> >> > If __l2cap_chan_add() at (A) hadn't already taken the conn list reference,
>> >> >> > that put would drop the last ref and free new_chan. Control then returns up
>> >> >> > to l2cap_new_connection(), which hands the now-freed chan back to
>> >> >> > l2cap_connect():
>> >> >> >
>> >> >> > /* l2cap_connect() - runs after the callback returns */
>> >> >> > chan = l2cap_new_connection(conn, pchan);
>> >> >> > if (!chan)
>> >> >> > goto response;
>> >> >> > ...
>> >> >> > bacpy(&chan->src, &conn->hcon->src); /* <-- UAF on freed chan */
>> >> >> > chan->psm = psm;
>> >> >> > chan->dcid = scid;
>> >> >> >
>> >> >> > The conn list reference taken at (A), before (C), is what keeps new_chan
>> >> >> > alive across the release_sock() window so l2cap_connect() can keep using it.
>> >> >> >
>> >> >> > So __l2cap_chan_add() can't move out to l2cap_new_connection(): by the time
>> >> >> > the callback returns, the parent lock is already dropped and chan may already
>> >> >> > be freed - which is exactly the race. It has to be taken inside the callback,
>> >> >> > under the parent lock, before the socket is exposed.
>> >> >
>> >> > If you do after l2cap_new_connection, yes, but what if you do before
>> >> > you call ->new_connect:
>> >> >
>> >> > diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
>> >> > index 3991c179b98f..cd864f400985 100644
>> >> > --- a/net/bluetooth/l2cap_core.c
>> >> > +++ b/net/bluetooth/l2cap_core.c
>> >> > @@ -4027,8 +4027,10 @@ static struct l2cap_chan
>> >> > *l2cap_new_connection(struct l2cap_conn *conn,
>> >> > if (!chan)
>> >> > return NULL;
>> >> >
>> >> > + __l2cap_chan_add(conn, chan);
>> >> > +
>> >> > if (pchan->ops->new_connection(conn, pchan, chan) < 0) {
>> >> > - l2cap_chan_put(chan);
>> >> > + l2cap_chan_del(chan, 0);
>> >> > return NULL;
>> >> > }
>> >> >
>> >> > This means we have to call l2cap_chan_del if new_connection fails, but
>> >> > we can eliminate other functions having to call __l2cap_chan_add with
>> >> > this. If the issue is then having l2cap_chan_del called concurrently,
>> >> > we can add a l2cap_chan_hold; actually, we may need a reference anyway
>> >> > since sk can be released and call l2cap_chan_del, releasing both
>> >> > references.
>> >> >
>> >>
>> >> In:
>> >>
>> >> void __l2cap_chan_add(struct l2cap_conn *conn, struct l2cap_chan *chan)
>> >> {
>> >> ...
>> >> switch (chan->chan_type) {
>> >> case L2CAP_CHAN_CONN_ORIENTED:
>> >> ...
>> >> break;
>> >> case L2CAP_CHAN_CONN_LESS:
>> >> ...
>> >> break;
>> >> case L2CAP_CHAN_FIXED:
>> >> ...
>> >> break;
>> >> default:
>> >> ...
>> >> }
>> >> }
>> >>
>> >> Apparently it depends on chan->chan_type and it is set by ref1.
>> >> It is only set in cb function.
>> >
>> > Ok, this probably needs fixing in 6lowpan. These defaults should come
>> > from the parent channel, similar to how it's done in l2cap_sock_init.
>> > Perhaps we should promote that logic to l2cap_chan_set_defaults
>> > passing pchan so it can initialize the channel before calling
>> > __l2cap_chan_add.
>>
>> I can fix that in a separate patch.
>> Could you please merge it first?
>
> Merge it first, and then rework the order? Not sure what is the
> benefit of doing that, it is a somewhat substatial change either way
> so I don't think it will make it much easier to lets say backport.
>
Will do a patch v10.
>> Best,
>> Siwei
>
>
>
> --
> Luiz Augusto von Dentz
Best,
Siwei
^ permalink raw reply [flat|nested] 20+ messages in thread
* [PATCH v10 1/1] Bluetooth: L2CAP: Fix use-after-free in l2cap_sock_new_connection_cb()
@ 2026-06-11 15:08 Siwei Zhang
2026-06-11 19:09 ` bluez.test.bot
0 siblings, 1 reply; 20+ messages in thread
From: Siwei Zhang @ 2026-06-11 15:08 UTC (permalink / raw)
To: Luiz Augusto von Dentz; +Cc: linux-bluetooth, Siwei Zhang
l2cap_sock_new_connection_cb() returned l2cap_pi(sk)->chan after
release_sock(parent). Once the parent lock is dropped the newly
enqueued child socket sk is reachable via the accept queue, so another
task can accept and free it before the callback dereferences sk,
resulting in a use-after-free.
Rework the ->new_connection() op so the core, rather than the callback,
owns the child channel's lifetime. The op now receives a pre-allocated
new_chan and returns an errno instead of allocating and returning a
channel. l2cap_new_connection() allocates the child channel and links
it into the conn list via __l2cap_chan_add() before invoking the
callback, so the conn-list reference keeps the channel alive once
release_sock(parent) exposes the socket to other tasks.
Channel configuration that was duplicated in l2cap_sock_init() and the
various new_connection callbacks is consolidated into
l2cap_chan_set_defaults(), which now inherits from the parent channel
when one is supplied.
Fixes: 8ffb929098a5 ("Bluetooth: Remove parent socket usage from l2cap_core.c")
Cc: stable@kernel.org
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Siwei Zhang <oss@fourdim.xyz>
---
include/net/bluetooth/l2cap.h | 10 ++--
net/bluetooth/6lowpan.c | 18 +-----
net/bluetooth/l2cap_core.c | 78 ++++++++++++++++++++-----
net/bluetooth/l2cap_sock.c | 103 ++++++++++++++++------------------
net/bluetooth/smp.c | 27 ++-------
5 files changed, 127 insertions(+), 109 deletions(-)
diff --git a/include/net/bluetooth/l2cap.h b/include/net/bluetooth/l2cap.h
index e0a1f2293679..27acfedf5c79 100644
--- a/include/net/bluetooth/l2cap.h
+++ b/include/net/bluetooth/l2cap.h
@@ -620,7 +620,8 @@ struct l2cap_chan {
struct l2cap_ops {
char *name;
- struct l2cap_chan *(*new_connection) (struct l2cap_chan *chan);
+ int (*new_connection)(struct l2cap_chan *chan,
+ struct l2cap_chan *new_chan);
int (*recv) (struct l2cap_chan * chan,
struct sk_buff *skb);
void (*teardown) (struct l2cap_chan *chan, int err);
@@ -884,9 +885,10 @@ static inline __u16 __next_seq(struct l2cap_chan *chan, __u16 seq)
return (seq + 1) % (chan->tx_win_max + 1);
}
-static inline struct l2cap_chan *l2cap_chan_no_new_connection(struct l2cap_chan *chan)
+static inline int l2cap_chan_no_new_connection(struct l2cap_chan *chan,
+ struct l2cap_chan *new_chan)
{
- return NULL;
+ return -EOPNOTSUPP;
}
static inline int l2cap_chan_no_recv(struct l2cap_chan *chan, struct sk_buff *skb)
@@ -963,7 +965,7 @@ int l2cap_chan_send(struct l2cap_chan *chan, struct msghdr *msg, size_t len,
void l2cap_chan_busy(struct l2cap_chan *chan, int busy);
void l2cap_chan_rx_avail(struct l2cap_chan *chan, ssize_t rx_avail);
int l2cap_chan_check_security(struct l2cap_chan *chan, bool initiator);
-void l2cap_chan_set_defaults(struct l2cap_chan *chan);
+void l2cap_chan_set_defaults(struct l2cap_chan *chan, struct l2cap_chan *pchan);
int l2cap_ertm_init(struct l2cap_chan *chan);
void l2cap_chan_add(struct l2cap_conn *conn, struct l2cap_chan *chan);
void __l2cap_chan_add(struct l2cap_conn *conn, struct l2cap_chan *chan);
diff --git a/net/bluetooth/6lowpan.c b/net/bluetooth/6lowpan.c
index cb1e329d66fd..6e57b4b95c94 100644
--- a/net/bluetooth/6lowpan.c
+++ b/net/bluetooth/6lowpan.c
@@ -632,7 +632,7 @@ static struct l2cap_chan *chan_create(void)
if (!chan)
return NULL;
- l2cap_chan_set_defaults(chan);
+ l2cap_chan_set_defaults(chan, NULL);
chan->chan_type = L2CAP_CHAN_CONN_ORIENTED;
chan->mode = L2CAP_MODE_LE_FLOWCTL;
@@ -745,21 +745,6 @@ static inline void chan_ready_cb(struct l2cap_chan *chan)
ifup(dev->netdev);
}
-static inline struct l2cap_chan *chan_new_conn_cb(struct l2cap_chan *pchan)
-{
- struct l2cap_chan *chan;
-
- chan = chan_create();
- if (!chan)
- return NULL;
-
- chan->ops = pchan->ops;
-
- BT_DBG("chan %p pchan %p", chan, pchan);
-
- return chan;
-}
-
static void unregister_dev(struct lowpan_btle_dev *dev)
{
struct hci_dev *hdev = READ_ONCE(dev->hdev);
@@ -901,7 +886,6 @@ static long chan_get_sndtimeo_cb(struct l2cap_chan *chan)
static const struct l2cap_ops bt_6lowpan_chan_ops = {
.name = "L2CAP 6LoWPAN channel",
- .new_connection = chan_new_conn_cb,
.recv = chan_recv_cb,
.close = chan_close_cb,
.state_change = chan_state_change_cb,
diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
index c4ccfbda9d78..78ea7cdcc906 100644
--- a/net/bluetooth/l2cap_core.c
+++ b/net/bluetooth/l2cap_core.c
@@ -519,7 +519,10 @@ void l2cap_chan_put(struct l2cap_chan *c)
}
EXPORT_SYMBOL_GPL(l2cap_chan_put);
-void l2cap_chan_set_defaults(struct l2cap_chan *chan)
+/* Initialise @chan with default values, inheriting from the parent channel
+ * @pchan when it is given.
+ */
+void l2cap_chan_set_defaults(struct l2cap_chan *chan, struct l2cap_chan *pchan)
{
chan->fcs = L2CAP_FCS_CRC16;
chan->max_tx = L2CAP_DEFAULT_MAX_TX;
@@ -533,6 +536,31 @@ void l2cap_chan_set_defaults(struct l2cap_chan *chan)
chan->retrans_timeout = L2CAP_DEFAULT_RETRANS_TO;
chan->monitor_timeout = L2CAP_DEFAULT_MONITOR_TO;
+ if (pchan) {
+ BT_DBG("chan %p pchan %p", chan, pchan);
+
+ chan->chan_type = pchan->chan_type;
+ chan->imtu = pchan->imtu;
+ chan->omtu = pchan->omtu;
+ chan->mode = pchan->mode;
+ chan->fcs = pchan->fcs;
+ chan->max_tx = pchan->max_tx;
+ chan->tx_win = pchan->tx_win;
+ chan->tx_win_max = pchan->tx_win_max;
+ chan->sec_level = pchan->sec_level;
+ chan->conf_state = pchan->conf_state;
+ chan->flags = pchan->flags;
+ chan->tx_credits = pchan->tx_credits;
+ chan->rx_credits = pchan->rx_credits;
+
+ if (chan->chan_type == L2CAP_CHAN_FIXED) {
+ chan->scid = pchan->scid;
+ chan->dcid = pchan->scid;
+ }
+
+ return;
+ }
+
chan->conf_state = 0;
set_bit(CONF_NOT_COMPLETE, &chan->conf_state);
@@ -4007,6 +4035,38 @@ static inline int l2cap_command_rej(struct l2cap_conn *conn,
return 0;
}
+/* Allocate and initialise a channel for an incoming connection.
+ *
+ * The channel inherits its configuration from @pchan and is linked into @conn
+ * before ->new_connection() runs, so the conn list reference keeps it alive if
+ * the callback exposes it (e.g. via the socket accept queue) before this
+ * returns. The l2cap_chan_create() reference is taken over by the subsystem on
+ * success and dropped here on failure.
+ */
+static struct l2cap_chan *l2cap_new_connection(struct l2cap_conn *conn,
+ struct l2cap_chan *pchan)
+{
+ struct l2cap_chan *chan;
+
+ chan = l2cap_chan_create();
+ if (!chan)
+ return NULL;
+
+ l2cap_chan_set_defaults(chan, pchan);
+ chan->ops = pchan->ops;
+
+ __l2cap_chan_add(conn, chan);
+
+ if (pchan->ops->new_connection &&
+ pchan->ops->new_connection(pchan, chan) < 0) {
+ l2cap_chan_del(chan, 0);
+ l2cap_chan_put(chan);
+ return NULL;
+ }
+
+ return chan;
+}
+
static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
u8 *data, u8 rsp_code)
{
@@ -4053,7 +4113,7 @@ static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
goto response;
}
- chan = pchan->ops->new_connection(pchan);
+ chan = l2cap_new_connection(conn, pchan);
if (!chan)
goto response;
@@ -4071,8 +4131,6 @@ static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
chan->psm = psm;
chan->dcid = scid;
- __l2cap_chan_add(conn, chan);
-
dcid = chan->scid;
__set_chan_timer(chan, chan->ops->get_sndtimeo(chan));
@@ -4955,7 +5013,7 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
goto response_unlock;
}
- chan = pchan->ops->new_connection(pchan);
+ chan = l2cap_new_connection(conn, pchan);
if (!chan) {
result = L2CAP_CR_LE_NO_MEM;
goto response_unlock;
@@ -4970,8 +5028,6 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
chan->omtu = mtu;
chan->remote_mps = mps;
- __l2cap_chan_add(conn, chan);
-
l2cap_le_flowctl_init(chan, __le16_to_cpu(req->credits));
dcid = chan->scid;
@@ -5179,7 +5235,7 @@ static inline int l2cap_ecred_conn_req(struct l2cap_conn *conn,
continue;
}
- chan = pchan->ops->new_connection(pchan);
+ chan = l2cap_new_connection(conn, pchan);
if (!chan) {
result = L2CAP_CR_LE_NO_MEM;
continue;
@@ -5194,8 +5250,6 @@ static inline int l2cap_ecred_conn_req(struct l2cap_conn *conn,
chan->omtu = mtu;
chan->remote_mps = mps;
- __l2cap_chan_add(conn, chan);
-
l2cap_ecred_init(chan, __le16_to_cpu(req->credits));
/* Init response */
@@ -7470,14 +7524,12 @@ static void l2cap_connect_cfm(struct hci_conn *hcon, u8 status)
goto next;
l2cap_chan_lock(pchan);
- chan = pchan->ops->new_connection(pchan);
+ chan = l2cap_new_connection(conn, pchan);
if (chan) {
bacpy(&chan->src, &hcon->src);
bacpy(&chan->dst, &hcon->dst);
chan->src_type = bdaddr_src_type(hcon);
chan->dst_type = dst_type;
-
- __l2cap_chan_add(conn, chan);
}
l2cap_chan_unlock(pchan);
diff --git a/net/bluetooth/l2cap_sock.c b/net/bluetooth/l2cap_sock.c
index 025329636353..7370f2b4d28e 100644
--- a/net/bluetooth/l2cap_sock.c
+++ b/net/bluetooth/l2cap_sock.c
@@ -46,7 +46,8 @@ static struct bt_sock_list l2cap_sk_list = {
static const struct proto_ops l2cap_sock_ops;
static void l2cap_sock_init(struct sock *sk, struct sock *parent);
static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
- int proto, gfp_t prio, int kern);
+ int proto, gfp_t prio, int kern,
+ struct l2cap_chan *chan);
static void l2cap_sock_cleanup_listen(struct sock *parent);
bool l2cap_is_socket(struct socket *sock)
@@ -1287,6 +1288,23 @@ static int l2cap_sock_recvmsg(struct socket *sock, struct msghdr *msg,
return err;
}
+/* Release the sock's ref on chan and clear the pointer so that the ref is
+ * dropped exactly once even if both l2cap_sock_kill() and
+ * l2cap_sock_destruct() run. Setting chan->data to NULL first stops any other
+ * task from dereferencing the now-dead sock pointer.
+ */
+static void l2cap_sock_put_chan(struct sock *sk)
+{
+ struct l2cap_chan *chan = l2cap_pi(sk)->chan;
+
+ if (!chan)
+ return;
+
+ chan->data = NULL;
+ l2cap_pi(sk)->chan = NULL;
+ l2cap_chan_put(chan);
+}
+
/* Kill socket (only if zapped and orphan)
* Must be called on unlocked socket, with l2cap channel lock.
*/
@@ -1297,13 +1315,9 @@ static void l2cap_sock_kill(struct sock *sk)
BT_DBG("sk %p state %s", sk, state_to_string(sk->sk_state));
- /* Sock is dead, so set chan data to NULL, avoid other task use invalid
- * sock pointer.
- */
- l2cap_pi(sk)->chan->data = NULL;
- /* Kill poor orphan */
+ l2cap_sock_put_chan(sk);
- l2cap_chan_put(l2cap_pi(sk)->chan);
+ /* Kill poor orphan */
sock_set_flag(sk, SOCK_DEAD);
sock_put(sk);
}
@@ -1546,12 +1560,13 @@ static void l2cap_sock_cleanup_listen(struct sock *parent)
}
}
-static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
+static int l2cap_sock_new_connection_cb(struct l2cap_chan *chan,
+ struct l2cap_chan *new_chan)
{
struct sock *sk, *parent = chan->data;
if (!parent)
- return NULL;
+ return -EINVAL;
lock_sock(parent);
@@ -1559,25 +1574,28 @@ static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
if (sk_acceptq_is_full(parent)) {
BT_DBG("backlog full %d", parent->sk_ack_backlog);
release_sock(parent);
- return NULL;
+ return -ENOBUFS;
}
sk = l2cap_sock_alloc(sock_net(parent), NULL, BTPROTO_L2CAP,
- GFP_ATOMIC, 0);
+ GFP_ATOMIC, 0, new_chan);
if (!sk) {
release_sock(parent);
- return NULL;
- }
+ return -ENOMEM;
+ }
bt_sock_reclassify_lock(sk, BTPROTO_L2CAP);
l2cap_sock_init(sk, parent);
+ /* The conn list reference taken by l2cap_new_connection() keeps new_chan
+ * alive once release_sock() lets another task free this socket.
+ */
bt_accept_enqueue(parent, sk, false);
release_sock(parent);
- return l2cap_pi(sk)->chan;
+ return 0;
}
static int l2cap_sock_recv_cb(struct l2cap_chan *chan, struct sk_buff *skb)
@@ -1874,10 +1892,7 @@ static void l2cap_sock_destruct(struct sock *sk)
BT_DBG("sk %p", sk);
- if (l2cap_pi(sk)->chan) {
- l2cap_pi(sk)->chan->data = NULL;
- l2cap_chan_put(l2cap_pi(sk)->chan);
- }
+ l2cap_sock_put_chan(sk);
list_for_each_entry_safe(rx_busy, next, &l2cap_pi(sk)->rx_busy, list) {
kfree_skb(rx_busy->skb);
@@ -1910,30 +1925,12 @@ static void l2cap_sock_init(struct sock *sk, struct sock *parent)
BT_DBG("sk %p", sk);
if (parent) {
- struct l2cap_chan *pchan = l2cap_pi(parent)->chan;
-
sk->sk_type = parent->sk_type;
bt_sk(sk)->flags = bt_sk(parent)->flags;
- chan->chan_type = pchan->chan_type;
- chan->imtu = pchan->imtu;
- chan->omtu = pchan->omtu;
- chan->conf_state = pchan->conf_state;
- chan->mode = pchan->mode;
- chan->fcs = pchan->fcs;
- chan->max_tx = pchan->max_tx;
- chan->tx_win = pchan->tx_win;
- chan->tx_win_max = pchan->tx_win_max;
- chan->sec_level = pchan->sec_level;
- chan->flags = pchan->flags;
- chan->tx_credits = pchan->tx_credits;
- chan->rx_credits = pchan->rx_credits;
-
- if (chan->chan_type == L2CAP_CHAN_FIXED) {
- chan->scid = pchan->scid;
- chan->dcid = pchan->scid;
- }
-
+ /* Channel configuration is inherited from the parent by
+ * l2cap_new_connection().
+ */
security_sk_clone(parent, sk);
} else {
switch (sk->sk_type) {
@@ -1959,7 +1956,7 @@ static void l2cap_sock_init(struct sock *sk, struct sock *parent)
chan->mode = L2CAP_MODE_BASIC;
}
- l2cap_chan_set_defaults(chan);
+ l2cap_chan_set_defaults(chan, NULL);
}
/* Default config options */
@@ -1978,10 +1975,10 @@ static struct proto l2cap_proto = {
};
static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
- int proto, gfp_t prio, int kern)
+ int proto, gfp_t prio, int kern,
+ struct l2cap_chan *chan)
{
struct sock *sk;
- struct l2cap_chan *chan;
sk = bt_sock_alloc(net, sock, &l2cap_proto, proto, prio, kern);
if (!sk)
@@ -1992,16 +1989,7 @@ static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
INIT_LIST_HEAD(&l2cap_pi(sk)->rx_busy);
- chan = l2cap_chan_create();
- if (!chan) {
- sk_free(sk);
- if (sock)
- sock->sk = NULL;
- return NULL;
- }
-
- l2cap_chan_hold(chan);
-
+ /* The sock takes ownership of the caller's reference on chan. */
l2cap_pi(sk)->chan = chan;
return sk;
@@ -2011,6 +1999,7 @@ static int l2cap_sock_create(struct net *net, struct socket *sock, int protocol,
int kern)
{
struct sock *sk;
+ struct l2cap_chan *chan;
BT_DBG("sock %p", sock);
@@ -2025,10 +2014,16 @@ static int l2cap_sock_create(struct net *net, struct socket *sock, int protocol,
sock->ops = &l2cap_sock_ops;
- sk = l2cap_sock_alloc(net, sock, protocol, GFP_ATOMIC, kern);
- if (!sk)
+ chan = l2cap_chan_create();
+ if (!chan)
return -ENOMEM;
+ sk = l2cap_sock_alloc(net, sock, protocol, GFP_ATOMIC, kern, chan);
+ if (!sk) {
+ l2cap_chan_put(chan);
+ return -ENOMEM;
+ }
+
l2cap_sock_init(sk, NULL);
bt_sock_link(&l2cap_sk_list, sk);
return 0;
diff --git a/net/bluetooth/smp.c b/net/bluetooth/smp.c
index 1739c1989dbd..d3f9207a026d 100644
--- a/net/bluetooth/smp.c
+++ b/net/bluetooth/smp.c
@@ -3204,34 +3204,19 @@ static const struct l2cap_ops smp_chan_ops = {
.get_sndtimeo = l2cap_chan_no_get_sndtimeo,
};
-static inline struct l2cap_chan *smp_new_conn_cb(struct l2cap_chan *pchan)
+static inline int smp_new_conn_cb(struct l2cap_chan *chan,
+ struct l2cap_chan *new_chan)
{
- struct l2cap_chan *chan;
-
- BT_DBG("pchan %p", pchan);
-
- chan = l2cap_chan_create();
- if (!chan)
- return NULL;
-
- chan->chan_type = pchan->chan_type;
- chan->ops = &smp_chan_ops;
- chan->scid = pchan->scid;
- chan->dcid = chan->scid;
- chan->imtu = pchan->imtu;
- chan->omtu = pchan->omtu;
- chan->mode = pchan->mode;
+ new_chan->ops = &smp_chan_ops;
/* Other L2CAP channels may request SMP routines in order to
* change the security level. This means that the SMP channel
* lock must be considered in its own category to avoid lockdep
* warnings.
*/
- atomic_set(&chan->nesting, L2CAP_NESTING_SMP);
-
- BT_DBG("created chan %p", chan);
+ atomic_set(&new_chan->nesting, L2CAP_NESTING_SMP);
- return chan;
+ return 0;
}
static const struct l2cap_ops smp_root_chan_ops = {
@@ -3291,7 +3276,7 @@ static struct l2cap_chan *smp_add_cid(struct hci_dev *hdev, u16 cid)
l2cap_add_scid(chan, cid);
- l2cap_chan_set_defaults(chan);
+ l2cap_chan_set_defaults(chan, NULL);
if (cid == L2CAP_CID_SMP) {
u8 bdaddr_type;
--
2.54.0
^ permalink raw reply related [flat|nested] 20+ messages in thread* RE: Bluetooth: L2CAP: Fix use-after-free in l2cap_sock_new_connection_cb()
2026-06-11 15:08 [PATCH v10 " Siwei Zhang
@ 2026-06-11 19:09 ` bluez.test.bot
0 siblings, 0 replies; 20+ messages in thread
From: bluez.test.bot @ 2026-06-11 19:09 UTC (permalink / raw)
To: linux-bluetooth, oss
[-- Attachment #1: Type: text/plain, Size: 6509 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=1110137
---Test result---
Test Summary:
CheckPatch PASS 1.67 seconds
VerifyFixes PASS 0.13 seconds
VerifySignedoff PASS 0.12 seconds
GitLint PASS 0.32 seconds
SubjectPrefix PASS 0.12 seconds
BuildKernel FAIL 25.18 seconds
CheckAllWarning FAIL 28.90 seconds
CheckSparse FAIL 26.64 seconds
BuildKernel32 FAIL 24.50 seconds
TestRunnerSetup FAIL 510.15 seconds
TestRunner_l2cap-tester FAIL 0.13 seconds
TestRunner_smp-tester FAIL 0.13 seconds
TestRunner_6lowpan-tester FAIL 0.13 seconds
IncrementalBuild PASS 23.83 seconds
Details
##############################
Test: BuildKernel - FAIL
Desc: Build Kernel for Bluetooth
Output:
net/bluetooth/6lowpan.c:768:13: error: redefinition of ‘unregister_dev’
768 | static void unregister_dev(struct lowpan_btle_dev *dev)
| ^~~~~~~~~~~~~~
net/bluetooth/6lowpan.c:748:13: note: previous definition of ‘unregister_dev’ was here
748 | static void unregister_dev(struct lowpan_btle_dev *dev)
| ^~~~~~~~~~~~~~
net/bluetooth/6lowpan.c:748:13: warning: ‘unregister_dev’ defined but not used [-Wunused-function]
make[4]: *** [scripts/Makefile.build:289: net/bluetooth/6lowpan.o] Error 1
make[3]: *** [scripts/Makefile.build:548: net/bluetooth] Error 2
make[2]: *** [scripts/Makefile.build:548: net] Error 2
make[2]: *** Waiting for unfinished jobs....
make[1]: *** [/github/workspace/src/src/Makefile:2143: .] Error 2
make: *** [Makefile:248: __sub-make] Error 2
##############################
Test: CheckAllWarning - FAIL
Desc: Run linux kernel with all warning enabled
Output:
net/bluetooth/6lowpan.c:768:13: error: redefinition of ‘unregister_dev’
768 | static void unregister_dev(struct lowpan_btle_dev *dev)
| ^~~~~~~~~~~~~~
net/bluetooth/6lowpan.c:748:13: note: previous definition of ‘unregister_dev’ was here
748 | static void unregister_dev(struct lowpan_btle_dev *dev)
| ^~~~~~~~~~~~~~
net/bluetooth/6lowpan.c:748:13: warning: ‘unregister_dev’ defined but not used [-Wunused-function]
make[4]: *** [scripts/Makefile.build:289: net/bluetooth/6lowpan.o] Error 1
make[4]: *** Waiting for unfinished jobs....
make[3]: *** [scripts/Makefile.build:548: net/bluetooth] Error 2
make[2]: *** [scripts/Makefile.build:548: net] Error 2
make[2]: *** Waiting for unfinished jobs....
make[1]: *** [/github/workspace/src/src/Makefile:2143: .] Error 2
make: *** [Makefile:248: __sub-make] Error 2
##############################
Test: CheckSparse - FAIL
Desc: Run sparse tool with linux kernel
Output:
/github/workspace/src/src/Makefile:1246: C=1 specified, but sparse is not available or not up to date
/github/workspace/src/src/Makefile:1246: C=1 specified, but sparse is not available or not up to date
net/bluetooth/6lowpan.c:768:13: error: redefinition of ‘unregister_dev’
768 | static void unregister_dev(struct lowpan_btle_dev *dev)
| ^~~~~~~~~~~~~~
net/bluetooth/6lowpan.c:748:13: note: previous definition of ‘unregister_dev’ was here
748 | static void unregister_dev(struct lowpan_btle_dev *dev)
| ^~~~~~~~~~~~~~
net/bluetooth/6lowpan.c:748:13: warning: ‘unregister_dev’ defined but not used [-Wunused-function]
make[4]: *** [scripts/Makefile.build:289: net/bluetooth/6lowpan.o] Error 1
make[4]: *** Waiting for unfinished jobs....
make[3]: *** [scripts/Makefile.build:548: net/bluetooth] Error 2
make[2]: *** [scripts/Makefile.build:548: net] Error 2
make[2]: *** Waiting for unfinished jobs....
make[1]: *** [/github/workspace/src/src/Makefile:2143: .] Error 2
make: *** [Makefile:248: __sub-make] Error 2
##############################
Test: BuildKernel32 - FAIL
Desc: Build 32bit Kernel for Bluetooth
Output:
net/bluetooth/6lowpan.c:768:13: error: redefinition of ‘unregister_dev’
768 | static void unregister_dev(struct lowpan_btle_dev *dev)
| ^~~~~~~~~~~~~~
net/bluetooth/6lowpan.c:748:13: note: previous definition of ‘unregister_dev’ was here
748 | static void unregister_dev(struct lowpan_btle_dev *dev)
| ^~~~~~~~~~~~~~
net/bluetooth/6lowpan.c:748:13: warning: ‘unregister_dev’ defined but not used [-Wunused-function]
make[4]: *** [scripts/Makefile.build:289: net/bluetooth/6lowpan.o] Error 1
make[3]: *** [scripts/Makefile.build:548: net/bluetooth] Error 2
make[2]: *** [scripts/Makefile.build:548: net] Error 2
make[2]: *** Waiting for unfinished jobs....
make[1]: *** [/github/workspace/src/src/Makefile:2143: .] Error 2
make: *** [Makefile:248: __sub-make] Error 2
##############################
Test: TestRunnerSetup - FAIL
Desc: Setup kernel and bluez for test-runner
Output:
Kernel:
net/bluetooth/6lowpan.c:768:13: error: redefinition of ‘unregister_dev’
768 | static void unregister_dev(struct lowpan_btle_dev *dev)
| ^~~~~~~~~~~~~~
net/bluetooth/6lowpan.c:748:13: note: previous definition of ‘unregister_dev’ was here
748 | static void unregister_dev(struct lowpan_btle_dev *dev)
| ^~~~~~~~~~~~~~
net/bluetooth/6lowpan.c:748:13: warning: ‘unregister_dev’ defined but not used [-Wunused-function]
make[4]: *** [scripts/Makefile.build:289: net/bluetooth/6lowpan.o] Error 1
make[3]: *** [scripts/Makefile.build:548: net/bluetooth] Error 2
make[2]: *** [scripts/Makefile.build:548: net] Error 2
make[2]: *** Waiting for unfinished jobs....
make[1]: *** [/github/workspace/src/src/Makefile:2143: .] Error 2
make: *** [Makefile:248: __sub-make] Error 2
##############################
Test: TestRunner_l2cap-tester - FAIL
Desc: Run l2cap-tester with test-runner
Output:
No kernel image found
##############################
Test: TestRunner_smp-tester - FAIL
Desc: Run smp-tester with test-runner
Output:
No kernel image found
##############################
Test: TestRunner_6lowpan-tester - FAIL
Desc: Run 6lowpan-tester with test-runner
Output:
No kernel image found
https://github.com/bluez/bluetooth-next/pull/304
---
Regards,
Linux Bluetooth
^ permalink raw reply [flat|nested] 20+ messages in thread
* [PATCH v8 1/1] Bluetooth: L2CAP: Fix use-after-free in l2cap_sock_new_connection_cb()
@ 2026-06-02 12:55 Siwei Zhang
2026-06-02 15:27 ` bluez.test.bot
0 siblings, 1 reply; 20+ messages in thread
From: Siwei Zhang @ 2026-06-02 12:55 UTC (permalink / raw)
To: Marcel Holtmann, Luiz Augusto von Dentz; +Cc: linux-bluetooth, Siwei Zhang
l2cap_sock_new_connection_cb() accesses l2cap_pi(sk)->chan after
release_sock(parent). Once the parent lock is released, the child
socket sk can be freed by another task.
Allocate the channel outside the func to prevent this.
Fixes: 8ffb929098a5 ("Bluetooth: Remove parent socket usage from l2cap_core.c")
Cc: stable@kernel.org
Assisted-by: Claude:claude-opus-4-6
Signed-off-by: Siwei Zhang <oss@fourdim.xyz>
---
include/net/bluetooth/l2cap.h | 8 ++--
net/bluetooth/6lowpan.c | 32 +++++++++------
net/bluetooth/l2cap_core.c | 60 ++++++++++++++++++++-------
net/bluetooth/l2cap_sock.c | 76 ++++++++++++++++++++++-------------
net/bluetooth/smp.c | 18 ++++-----
5 files changed, 126 insertions(+), 68 deletions(-)
diff --git a/include/net/bluetooth/l2cap.h b/include/net/bluetooth/l2cap.h
index e0a1f2293679..ffd006187324 100644
--- a/include/net/bluetooth/l2cap.h
+++ b/include/net/bluetooth/l2cap.h
@@ -620,7 +620,8 @@ struct l2cap_chan {
struct l2cap_ops {
char *name;
- struct l2cap_chan *(*new_connection) (struct l2cap_chan *chan);
+ int (*new_connection)(struct l2cap_chan *chan,
+ struct l2cap_chan *new_chan);
int (*recv) (struct l2cap_chan * chan,
struct sk_buff *skb);
void (*teardown) (struct l2cap_chan *chan, int err);
@@ -884,9 +885,10 @@ static inline __u16 __next_seq(struct l2cap_chan *chan, __u16 seq)
return (seq + 1) % (chan->tx_win_max + 1);
}
-static inline struct l2cap_chan *l2cap_chan_no_new_connection(struct l2cap_chan *chan)
+static inline int l2cap_chan_no_new_connection(struct l2cap_chan *chan,
+ struct l2cap_chan *new_chan)
{
- return NULL;
+ return -EOPNOTSUPP;
}
static inline int l2cap_chan_no_recv(struct l2cap_chan *chan, struct sk_buff *skb)
diff --git a/net/bluetooth/6lowpan.c b/net/bluetooth/6lowpan.c
index cb1e329d66fd..40f118168015 100644
--- a/net/bluetooth/6lowpan.c
+++ b/net/bluetooth/6lowpan.c
@@ -624,6 +624,15 @@ static bool is_bt_6lowpan(struct hci_conn *hcon)
return true;
}
+static void chan_init(struct l2cap_chan *chan)
+{
+ l2cap_chan_set_defaults(chan);
+
+ chan->chan_type = L2CAP_CHAN_CONN_ORIENTED;
+ chan->mode = L2CAP_MODE_LE_FLOWCTL;
+ chan->imtu = 1280;
+}
+
static struct l2cap_chan *chan_create(void)
{
struct l2cap_chan *chan;
@@ -632,11 +641,7 @@ static struct l2cap_chan *chan_create(void)
if (!chan)
return NULL;
- l2cap_chan_set_defaults(chan);
-
- chan->chan_type = L2CAP_CHAN_CONN_ORIENTED;
- chan->mode = L2CAP_MODE_LE_FLOWCTL;
- chan->imtu = 1280;
+ chan_init(chan);
return chan;
}
@@ -745,19 +750,20 @@ static inline void chan_ready_cb(struct l2cap_chan *chan)
ifup(dev->netdev);
}
-static inline struct l2cap_chan *chan_new_conn_cb(struct l2cap_chan *pchan)
+static inline int chan_new_conn_cb(struct l2cap_chan *pchan,
+ struct l2cap_chan *chan)
{
- struct l2cap_chan *chan;
-
- chan = chan_create();
- if (!chan)
- return NULL;
-
+ chan_init(chan);
chan->ops = pchan->ops;
+ /* Take a reference to match the put in chan_close_cb(). The caller
+ * drops its own local reference after __l2cap_chan_add().
+ */
+ l2cap_chan_hold(chan);
+
BT_DBG("chan %p pchan %p", chan, pchan);
- return chan;
+ return 0;
}
static void unregister_dev(struct lowpan_btle_dev *dev)
diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
index c4ccfbda9d78..31fb715fd830 100644
--- a/net/bluetooth/l2cap_core.c
+++ b/net/bluetooth/l2cap_core.c
@@ -4007,6 +4007,36 @@ static inline int l2cap_command_rej(struct l2cap_conn *conn,
return 0;
}
+/* Allocate and initialise a channel for an incoming connection. The returned
+ * channel carries the caller's local reference, which must be dropped once
+ * __l2cap_chan_add() has pinned it via the conn list.
+ */
+static struct l2cap_chan *l2cap_new_connection(struct l2cap_chan *pchan)
+{
+ struct l2cap_chan *chan;
+
+ chan = l2cap_chan_create();
+ if (!chan)
+ return NULL;
+
+ if (pchan->ops->new_connection(pchan, chan) < 0) {
+ l2cap_chan_put(chan);
+ return NULL;
+ }
+
+ return chan;
+}
+
+/* Link a channel from l2cap_new_connection() into conn and release the local
+ * reference it carried. __l2cap_chan_add() pins the channel via the conn list,
+ * so it remains valid after this returns.
+ */
+static void l2cap_chan_add_new(struct l2cap_conn *conn, struct l2cap_chan *chan)
+{
+ __l2cap_chan_add(conn, chan);
+ l2cap_chan_put(chan);
+}
+
static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
u8 *data, u8 rsp_code)
{
@@ -4053,7 +4083,7 @@ static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
goto response;
}
- chan = pchan->ops->new_connection(pchan);
+ chan = l2cap_new_connection(pchan);
if (!chan)
goto response;
@@ -4071,7 +4101,7 @@ static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
chan->psm = psm;
chan->dcid = scid;
- __l2cap_chan_add(conn, chan);
+ l2cap_chan_add_new(conn, chan);
dcid = chan->scid;
@@ -4883,6 +4913,7 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
struct l2cap_le_conn_rsp rsp;
struct l2cap_chan *chan, *pchan;
u16 dcid, scid, credits, mtu, mps;
+ u16 rsp_mtu, rsp_mps;
__le16 psm;
u8 result;
@@ -4895,6 +4926,8 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
psm = req->psm;
dcid = 0;
credits = 0;
+ rsp_mtu = 0;
+ rsp_mps = 0;
if (mtu < 23 || mps < 23)
return -EPROTO;
@@ -4955,7 +4988,7 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
goto response_unlock;
}
- chan = pchan->ops->new_connection(pchan);
+ chan = l2cap_new_connection(pchan);
if (!chan) {
result = L2CAP_CR_LE_NO_MEM;
goto response_unlock;
@@ -4970,12 +5003,14 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
chan->omtu = mtu;
chan->remote_mps = mps;
- __l2cap_chan_add(conn, chan);
+ l2cap_chan_add_new(conn, chan);
l2cap_le_flowctl_init(chan, __le16_to_cpu(req->credits));
dcid = chan->scid;
credits = chan->rx_credits;
+ rsp_mtu = chan->imtu;
+ rsp_mps = chan->mps;
__set_chan_timer(chan, chan->ops->get_sndtimeo(chan));
@@ -5003,13 +5038,8 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
return 0;
response:
- if (chan) {
- rsp.mtu = cpu_to_le16(chan->imtu);
- rsp.mps = cpu_to_le16(chan->mps);
- } else {
- rsp.mtu = 0;
- rsp.mps = 0;
- }
+ rsp.mtu = cpu_to_le16(rsp_mtu);
+ rsp.mps = cpu_to_le16(rsp_mps);
rsp.dcid = cpu_to_le16(dcid);
rsp.credits = cpu_to_le16(credits);
@@ -5179,7 +5209,7 @@ static inline int l2cap_ecred_conn_req(struct l2cap_conn *conn,
continue;
}
- chan = pchan->ops->new_connection(pchan);
+ chan = l2cap_new_connection(pchan);
if (!chan) {
result = L2CAP_CR_LE_NO_MEM;
continue;
@@ -5194,7 +5224,7 @@ static inline int l2cap_ecred_conn_req(struct l2cap_conn *conn,
chan->omtu = mtu;
chan->remote_mps = mps;
- __l2cap_chan_add(conn, chan);
+ l2cap_chan_add_new(conn, chan);
l2cap_ecred_init(chan, __le16_to_cpu(req->credits));
@@ -7470,14 +7500,14 @@ static void l2cap_connect_cfm(struct hci_conn *hcon, u8 status)
goto next;
l2cap_chan_lock(pchan);
- chan = pchan->ops->new_connection(pchan);
+ chan = l2cap_new_connection(pchan);
if (chan) {
bacpy(&chan->src, &hcon->src);
bacpy(&chan->dst, &hcon->dst);
chan->src_type = bdaddr_src_type(hcon);
chan->dst_type = dst_type;
- __l2cap_chan_add(conn, chan);
+ l2cap_chan_add_new(conn, chan);
}
l2cap_chan_unlock(pchan);
diff --git a/net/bluetooth/l2cap_sock.c b/net/bluetooth/l2cap_sock.c
index 025329636353..745b012890ee 100644
--- a/net/bluetooth/l2cap_sock.c
+++ b/net/bluetooth/l2cap_sock.c
@@ -46,7 +46,8 @@ static struct bt_sock_list l2cap_sk_list = {
static const struct proto_ops l2cap_sock_ops;
static void l2cap_sock_init(struct sock *sk, struct sock *parent);
static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
- int proto, gfp_t prio, int kern);
+ int proto, gfp_t prio, int kern,
+ struct l2cap_chan *chan);
static void l2cap_sock_cleanup_listen(struct sock *parent);
bool l2cap_is_socket(struct socket *sock)
@@ -1292,18 +1293,25 @@ static int l2cap_sock_recvmsg(struct socket *sock, struct msghdr *msg,
*/
static void l2cap_sock_kill(struct sock *sk)
{
+ struct l2cap_chan *chan = l2cap_pi(sk)->chan;
+
if (!sock_flag(sk, SOCK_ZAPPED) || sk->sk_socket)
return;
BT_DBG("sk %p state %s", sk, state_to_string(sk->sk_state));
- /* Sock is dead, so set chan data to NULL, avoid other task use invalid
- * sock pointer.
+ /* Release the sock's ref on chan and clear the pointer so that
+ * l2cap_sock_destruct() does not drop it a second time. Setting
+ * chan->data to NULL first stops any other task from dereferencing
+ * the now-dead sock pointer.
*/
- l2cap_pi(sk)->chan->data = NULL;
- /* Kill poor orphan */
+ if (chan) {
+ chan->data = NULL;
+ l2cap_pi(sk)->chan = NULL;
+ l2cap_chan_put(chan);
+ }
- l2cap_chan_put(l2cap_pi(sk)->chan);
+ /* Kill poor orphan */
sock_set_flag(sk, SOCK_DEAD);
sock_put(sk);
}
@@ -1383,7 +1391,9 @@ static int l2cap_sock_shutdown(struct socket *sock, int how)
sock_hold(sk);
/* prevent chan structure from being freed whilst unlocked */
- chan = l2cap_chan_hold_unless_zero(l2cap_pi(sk)->chan);
+ chan = l2cap_pi(sk)->chan;
+ if (chan)
+ chan = l2cap_chan_hold_unless_zero(chan);
if (!chan)
goto shutdown_already;
@@ -1546,12 +1556,13 @@ static void l2cap_sock_cleanup_listen(struct sock *parent)
}
}
-static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
+static int l2cap_sock_new_connection_cb(struct l2cap_chan *chan,
+ struct l2cap_chan *new_chan)
{
struct sock *sk, *parent = chan->data;
if (!parent)
- return NULL;
+ return -EINVAL;
lock_sock(parent);
@@ -1559,15 +1570,15 @@ static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
if (sk_acceptq_is_full(parent)) {
BT_DBG("backlog full %d", parent->sk_ack_backlog);
release_sock(parent);
- return NULL;
+ return -ENOBUFS;
}
sk = l2cap_sock_alloc(sock_net(parent), NULL, BTPROTO_L2CAP,
- GFP_ATOMIC, 0);
+ GFP_ATOMIC, 0, new_chan);
if (!sk) {
release_sock(parent);
- return NULL;
- }
+ return -ENOMEM;
+ }
bt_sock_reclassify_lock(sk, BTPROTO_L2CAP);
@@ -1577,7 +1588,7 @@ static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
release_sock(parent);
- return l2cap_pi(sk)->chan;
+ return 0;
}
static int l2cap_sock_recv_cb(struct l2cap_chan *chan, struct sk_buff *skb)
@@ -1875,8 +1886,11 @@ static void l2cap_sock_destruct(struct sock *sk)
BT_DBG("sk %p", sk);
if (l2cap_pi(sk)->chan) {
- l2cap_pi(sk)->chan->data = NULL;
- l2cap_chan_put(l2cap_pi(sk)->chan);
+ struct l2cap_chan *chan = l2cap_pi(sk)->chan;
+
+ chan->data = NULL;
+ l2cap_pi(sk)->chan = NULL;
+ l2cap_chan_put(chan);
}
list_for_each_entry_safe(rx_busy, next, &l2cap_pi(sk)->rx_busy, list) {
@@ -1978,10 +1992,10 @@ static struct proto l2cap_proto = {
};
static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
- int proto, gfp_t prio, int kern)
+ int proto, gfp_t prio, int kern,
+ struct l2cap_chan *chan)
{
struct sock *sk;
- struct l2cap_chan *chan;
sk = bt_sock_alloc(net, sock, &l2cap_proto, proto, prio, kern);
if (!sk)
@@ -1992,14 +2006,11 @@ static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
INIT_LIST_HEAD(&l2cap_pi(sk)->rx_busy);
- chan = l2cap_chan_create();
- if (!chan) {
- sk_free(sk);
- if (sock)
- sock->sk = NULL;
- return NULL;
- }
-
+ /* The sock owns a single ref on chan. It is released by whichever of
+ * l2cap_sock_kill() or l2cap_sock_destruct() runs first; that path
+ * clears l2cap_pi(sk)->chan so the ref is dropped exactly once. The
+ * caller keeps its own ref independent of this one.
+ */
l2cap_chan_hold(chan);
l2cap_pi(sk)->chan = chan;
@@ -2011,6 +2022,7 @@ static int l2cap_sock_create(struct net *net, struct socket *sock, int protocol,
int kern)
{
struct sock *sk;
+ struct l2cap_chan *chan;
BT_DBG("sock %p", sock);
@@ -2025,9 +2037,17 @@ static int l2cap_sock_create(struct net *net, struct socket *sock, int protocol,
sock->ops = &l2cap_sock_ops;
- sk = l2cap_sock_alloc(net, sock, protocol, GFP_ATOMIC, kern);
- if (!sk)
+ chan = l2cap_chan_create();
+ if (!chan)
+ return -ENOMEM;
+
+ sk = l2cap_sock_alloc(net, sock, protocol, GFP_ATOMIC, kern, chan);
+ if (!sk) {
+ l2cap_chan_put(chan);
return -ENOMEM;
+ }
+ /* Sock has taken its own refs on chan; drop the chan_create() ref. */
+ l2cap_chan_put(chan);
l2cap_sock_init(sk, NULL);
bt_sock_link(&l2cap_sk_list, sk);
diff --git a/net/bluetooth/smp.c b/net/bluetooth/smp.c
index 1739c1989dbd..887a2fc34391 100644
--- a/net/bluetooth/smp.c
+++ b/net/bluetooth/smp.c
@@ -3204,16 +3204,11 @@ static const struct l2cap_ops smp_chan_ops = {
.get_sndtimeo = l2cap_chan_no_get_sndtimeo,
};
-static inline struct l2cap_chan *smp_new_conn_cb(struct l2cap_chan *pchan)
+static inline int smp_new_conn_cb(struct l2cap_chan *pchan,
+ struct l2cap_chan *chan)
{
- struct l2cap_chan *chan;
-
BT_DBG("pchan %p", pchan);
- chan = l2cap_chan_create();
- if (!chan)
- return NULL;
-
chan->chan_type = pchan->chan_type;
chan->ops = &smp_chan_ops;
chan->scid = pchan->scid;
@@ -3229,9 +3224,14 @@ static inline struct l2cap_chan *smp_new_conn_cb(struct l2cap_chan *pchan)
*/
atomic_set(&chan->nesting, L2CAP_NESTING_SMP);
- BT_DBG("created chan %p", chan);
+ /* Take a reference to match the put in smp_teardown_cb(). The caller
+ * drops its own local reference after __l2cap_chan_add().
+ */
+ l2cap_chan_hold(chan);
- return chan;
+ BT_DBG("initialised chan %p", chan);
+
+ return 0;
}
static const struct l2cap_ops smp_root_chan_ops = {
--
2.54.0
^ permalink raw reply related [flat|nested] 20+ messages in thread* [PATCH v7 1/1] Bluetooth: L2CAP: Fix use-after-free in l2cap_sock_new_connection_cb()
@ 2026-06-01 14:03 Siwei Zhang
2026-06-01 18:50 ` bluez.test.bot
0 siblings, 1 reply; 20+ messages in thread
From: Siwei Zhang @ 2026-06-01 14:03 UTC (permalink / raw)
To: Marcel Holtmann, Luiz Augusto von Dentz; +Cc: linux-bluetooth, Siwei Zhang
l2cap_sock_new_connection_cb() accesses l2cap_pi(sk)->chan after
release_sock(parent). Once the parent lock is released, the child
socket sk can be freed by another task.
Allocate the channel outside the func to prevent this.
Fixes: 8ffb929098a5 ("Bluetooth: Remove parent socket usage from l2cap_core.c")
Cc: stable@kernel.org
Assisted-by: Claude:claude-opus-4-6
Signed-off-by: Siwei Zhang <oss@fourdim.xyz>
---
include/net/bluetooth/l2cap.h | 8 +++--
net/bluetooth/6lowpan.c | 32 +++++++++++--------
net/bluetooth/l2cap_core.c | 60 ++++++++++++++++++++++++++---------
net/bluetooth/l2cap_sock.c | 48 ++++++++++++++++------------
net/bluetooth/smp.c | 18 +++++------
5 files changed, 106 insertions(+), 60 deletions(-)
diff --git a/include/net/bluetooth/l2cap.h b/include/net/bluetooth/l2cap.h
index 5172afee5494..f7a11e6431f0 100644
--- a/include/net/bluetooth/l2cap.h
+++ b/include/net/bluetooth/l2cap.h
@@ -619,7 +619,8 @@ struct l2cap_chan {
struct l2cap_ops {
char *name;
- struct l2cap_chan *(*new_connection) (struct l2cap_chan *chan);
+ int (*new_connection)(struct l2cap_chan *chan,
+ struct l2cap_chan *new_chan);
int (*recv) (struct l2cap_chan * chan,
struct sk_buff *skb);
void (*teardown) (struct l2cap_chan *chan, int err);
@@ -883,9 +884,10 @@ static inline __u16 __next_seq(struct l2cap_chan *chan, __u16 seq)
return (seq + 1) % (chan->tx_win_max + 1);
}
-static inline struct l2cap_chan *l2cap_chan_no_new_connection(struct l2cap_chan *chan)
+static inline int l2cap_chan_no_new_connection(struct l2cap_chan *chan,
+ struct l2cap_chan *new_chan)
{
- return NULL;
+ return -EOPNOTSUPP;
}
static inline int l2cap_chan_no_recv(struct l2cap_chan *chan, struct sk_buff *skb)
diff --git a/net/bluetooth/6lowpan.c b/net/bluetooth/6lowpan.c
index 23a229ab6a33..284eefb73e94 100644
--- a/net/bluetooth/6lowpan.c
+++ b/net/bluetooth/6lowpan.c
@@ -622,6 +622,15 @@ static bool is_bt_6lowpan(struct hci_conn *hcon)
return true;
}
+static void chan_init(struct l2cap_chan *chan)
+{
+ l2cap_chan_set_defaults(chan);
+
+ chan->chan_type = L2CAP_CHAN_CONN_ORIENTED;
+ chan->mode = L2CAP_MODE_LE_FLOWCTL;
+ chan->imtu = 1280;
+}
+
static struct l2cap_chan *chan_create(void)
{
struct l2cap_chan *chan;
@@ -630,11 +639,7 @@ static struct l2cap_chan *chan_create(void)
if (!chan)
return NULL;
- l2cap_chan_set_defaults(chan);
-
- chan->chan_type = L2CAP_CHAN_CONN_ORIENTED;
- chan->mode = L2CAP_MODE_LE_FLOWCTL;
- chan->imtu = 1280;
+ chan_init(chan);
return chan;
}
@@ -743,19 +748,20 @@ static inline void chan_ready_cb(struct l2cap_chan *chan)
ifup(dev->netdev);
}
-static inline struct l2cap_chan *chan_new_conn_cb(struct l2cap_chan *pchan)
+static inline int chan_new_conn_cb(struct l2cap_chan *pchan,
+ struct l2cap_chan *chan)
{
- struct l2cap_chan *chan;
-
- chan = chan_create();
- if (!chan)
- return NULL;
-
+ chan_init(chan);
chan->ops = pchan->ops;
+ /* Take a reference to match the put in chan_close_cb(). The caller
+ * drops its own local reference after __l2cap_chan_add().
+ */
+ l2cap_chan_hold(chan);
+
BT_DBG("chan %p pchan %p", chan, pchan);
- return chan;
+ return 0;
}
static void unregister_dev(struct lowpan_btle_dev *dev)
diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
index fdccd62ccca8..3edf6680899e 100644
--- a/net/bluetooth/l2cap_core.c
+++ b/net/bluetooth/l2cap_core.c
@@ -4005,6 +4005,36 @@ static inline int l2cap_command_rej(struct l2cap_conn *conn,
return 0;
}
+/* Allocate and initialise a channel for an incoming connection. The returned
+ * channel carries the caller's local reference, which must be dropped once
+ * __l2cap_chan_add() has pinned it via the conn list.
+ */
+static struct l2cap_chan *l2cap_new_connection(struct l2cap_chan *pchan)
+{
+ struct l2cap_chan *chan;
+
+ chan = l2cap_chan_create();
+ if (!chan)
+ return NULL;
+
+ if (pchan->ops->new_connection(pchan, chan) < 0) {
+ l2cap_chan_put(chan);
+ return NULL;
+ }
+
+ return chan;
+}
+
+/* Link a channel from l2cap_new_connection() into conn and release the local
+ * reference it carried. __l2cap_chan_add() pins the channel via the conn list,
+ * so it remains valid after this returns.
+ */
+static void l2cap_chan_add_new(struct l2cap_conn *conn, struct l2cap_chan *chan)
+{
+ __l2cap_chan_add(conn, chan);
+ l2cap_chan_put(chan);
+}
+
static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
u8 *data, u8 rsp_code)
{
@@ -4051,7 +4081,7 @@ static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
goto response;
}
- chan = pchan->ops->new_connection(pchan);
+ chan = l2cap_new_connection(pchan);
if (!chan)
goto response;
@@ -4069,7 +4099,7 @@ static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
chan->psm = psm;
chan->dcid = scid;
- __l2cap_chan_add(conn, chan);
+ l2cap_chan_add_new(conn, chan);
dcid = chan->scid;
@@ -4881,6 +4911,7 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
struct l2cap_le_conn_rsp rsp;
struct l2cap_chan *chan, *pchan;
u16 dcid, scid, credits, mtu, mps;
+ u16 rsp_mtu, rsp_mps;
__le16 psm;
u8 result;
@@ -4893,6 +4924,8 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
psm = req->psm;
dcid = 0;
credits = 0;
+ rsp_mtu = 0;
+ rsp_mps = 0;
if (mtu < 23 || mps < 23)
return -EPROTO;
@@ -4953,7 +4986,7 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
goto response_unlock;
}
- chan = pchan->ops->new_connection(pchan);
+ chan = l2cap_new_connection(pchan);
if (!chan) {
result = L2CAP_CR_LE_NO_MEM;
goto response_unlock;
@@ -4968,12 +5001,14 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
chan->omtu = mtu;
chan->remote_mps = mps;
- __l2cap_chan_add(conn, chan);
+ l2cap_chan_add_new(conn, chan);
l2cap_le_flowctl_init(chan, __le16_to_cpu(req->credits));
dcid = chan->scid;
credits = chan->rx_credits;
+ rsp_mtu = chan->imtu;
+ rsp_mps = chan->mps;
__set_chan_timer(chan, chan->ops->get_sndtimeo(chan));
@@ -5001,13 +5036,8 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
return 0;
response:
- if (chan) {
- rsp.mtu = cpu_to_le16(chan->imtu);
- rsp.mps = cpu_to_le16(chan->mps);
- } else {
- rsp.mtu = 0;
- rsp.mps = 0;
- }
+ rsp.mtu = cpu_to_le16(rsp_mtu);
+ rsp.mps = cpu_to_le16(rsp_mps);
rsp.dcid = cpu_to_le16(dcid);
rsp.credits = cpu_to_le16(credits);
@@ -5177,7 +5207,7 @@ static inline int l2cap_ecred_conn_req(struct l2cap_conn *conn,
continue;
}
- chan = pchan->ops->new_connection(pchan);
+ chan = l2cap_new_connection(pchan);
if (!chan) {
result = L2CAP_CR_LE_NO_MEM;
continue;
@@ -5192,7 +5222,7 @@ static inline int l2cap_ecred_conn_req(struct l2cap_conn *conn,
chan->omtu = mtu;
chan->remote_mps = mps;
- __l2cap_chan_add(conn, chan);
+ l2cap_chan_add_new(conn, chan);
l2cap_ecred_init(chan, __le16_to_cpu(req->credits));
@@ -7399,14 +7429,14 @@ static void l2cap_connect_cfm(struct hci_conn *hcon, u8 status)
goto next;
l2cap_chan_lock(pchan);
- chan = pchan->ops->new_connection(pchan);
+ chan = l2cap_new_connection(pchan);
if (chan) {
bacpy(&chan->src, &hcon->src);
bacpy(&chan->dst, &hcon->dst);
chan->src_type = bdaddr_src_type(hcon);
chan->dst_type = dst_type;
- __l2cap_chan_add(conn, chan);
+ l2cap_chan_add_new(conn, chan);
}
l2cap_chan_unlock(pchan);
diff --git a/net/bluetooth/l2cap_sock.c b/net/bluetooth/l2cap_sock.c
index dede550d6031..598f24c8f704 100644
--- a/net/bluetooth/l2cap_sock.c
+++ b/net/bluetooth/l2cap_sock.c
@@ -46,7 +46,8 @@ static struct bt_sock_list l2cap_sk_list = {
static const struct proto_ops l2cap_sock_ops;
static void l2cap_sock_init(struct sock *sk, struct sock *parent);
static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
- int proto, gfp_t prio, int kern);
+ int proto, gfp_t prio, int kern,
+ struct l2cap_chan *chan);
static void l2cap_sock_cleanup_listen(struct sock *parent);
bool l2cap_is_socket(struct socket *sock)
@@ -1507,12 +1508,13 @@ static void l2cap_sock_cleanup_listen(struct sock *parent)
}
}
-static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
+static int l2cap_sock_new_connection_cb(struct l2cap_chan *chan,
+ struct l2cap_chan *new_chan)
{
struct sock *sk, *parent = chan->data;
if (!parent)
- return NULL;
+ return -EINVAL;
lock_sock(parent);
@@ -1520,15 +1522,15 @@ static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
if (sk_acceptq_is_full(parent)) {
BT_DBG("backlog full %d", parent->sk_ack_backlog);
release_sock(parent);
- return NULL;
+ return -ENOBUFS;
}
sk = l2cap_sock_alloc(sock_net(parent), NULL, BTPROTO_L2CAP,
- GFP_ATOMIC, 0);
+ GFP_ATOMIC, 0, new_chan);
if (!sk) {
release_sock(parent);
- return NULL;
- }
+ return -ENOMEM;
+ }
bt_sock_reclassify_lock(sk, BTPROTO_L2CAP);
@@ -1538,7 +1540,7 @@ static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
release_sock(parent);
- return l2cap_pi(sk)->chan;
+ return 0;
}
static int l2cap_sock_recv_cb(struct l2cap_chan *chan, struct sk_buff *skb)
@@ -1939,10 +1941,10 @@ static struct proto l2cap_proto = {
};
static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
- int proto, gfp_t prio, int kern)
+ int proto, gfp_t prio, int kern,
+ struct l2cap_chan *chan)
{
struct sock *sk;
- struct l2cap_chan *chan;
sk = bt_sock_alloc(net, sock, &l2cap_proto, proto, prio, kern);
if (!sk)
@@ -1953,14 +1955,11 @@ static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
INIT_LIST_HEAD(&l2cap_pi(sk)->rx_busy);
- chan = l2cap_chan_create();
- if (!chan) {
- sk_free(sk);
- if (sock)
- sock->sk = NULL;
- return NULL;
- }
-
+ /* The sock owns two refs on chan, matching the puts in
+ * l2cap_sock_kill() and l2cap_sock_destruct(). The caller keeps
+ * its own ref independent of these.
+ */
+ l2cap_chan_hold(chan);
l2cap_chan_hold(chan);
l2cap_pi(sk)->chan = chan;
@@ -1972,6 +1971,7 @@ static int l2cap_sock_create(struct net *net, struct socket *sock, int protocol,
int kern)
{
struct sock *sk;
+ struct l2cap_chan *chan;
BT_DBG("sock %p", sock);
@@ -1986,10 +1986,18 @@ static int l2cap_sock_create(struct net *net, struct socket *sock, int protocol,
sock->ops = &l2cap_sock_ops;
- sk = l2cap_sock_alloc(net, sock, protocol, GFP_ATOMIC, kern);
- if (!sk)
+ chan = l2cap_chan_create();
+ if (!chan)
return -ENOMEM;
+ sk = l2cap_sock_alloc(net, sock, protocol, GFP_ATOMIC, kern, chan);
+ if (!sk) {
+ l2cap_chan_put(chan);
+ return -ENOMEM;
+ }
+ /* Sock has taken its own refs on chan; drop the chan_create() ref. */
+ l2cap_chan_put(chan);
+
l2cap_sock_init(sk, NULL);
bt_sock_link(&l2cap_sk_list, sk);
return 0;
diff --git a/net/bluetooth/smp.c b/net/bluetooth/smp.c
index 1739c1989dbd..887a2fc34391 100644
--- a/net/bluetooth/smp.c
+++ b/net/bluetooth/smp.c
@@ -3204,16 +3204,11 @@ static const struct l2cap_ops smp_chan_ops = {
.get_sndtimeo = l2cap_chan_no_get_sndtimeo,
};
-static inline struct l2cap_chan *smp_new_conn_cb(struct l2cap_chan *pchan)
+static inline int smp_new_conn_cb(struct l2cap_chan *pchan,
+ struct l2cap_chan *chan)
{
- struct l2cap_chan *chan;
-
BT_DBG("pchan %p", pchan);
- chan = l2cap_chan_create();
- if (!chan)
- return NULL;
-
chan->chan_type = pchan->chan_type;
chan->ops = &smp_chan_ops;
chan->scid = pchan->scid;
@@ -3229,9 +3224,14 @@ static inline struct l2cap_chan *smp_new_conn_cb(struct l2cap_chan *pchan)
*/
atomic_set(&chan->nesting, L2CAP_NESTING_SMP);
- BT_DBG("created chan %p", chan);
+ /* Take a reference to match the put in smp_teardown_cb(). The caller
+ * drops its own local reference after __l2cap_chan_add().
+ */
+ l2cap_chan_hold(chan);
- return chan;
+ BT_DBG("initialised chan %p", chan);
+
+ return 0;
}
static const struct l2cap_ops smp_root_chan_ops = {
--
2.54.0
^ permalink raw reply related [flat|nested] 20+ messages in thread* [PATCH v6 1/1] Bluetooth: L2CAP: Fix use-after-free in l2cap_sock_new_connection_cb()
@ 2026-05-29 16:54 Siwei Zhang
2026-05-29 18:21 ` bluez.test.bot
0 siblings, 1 reply; 20+ messages in thread
From: Siwei Zhang @ 2026-05-29 16:54 UTC (permalink / raw)
To: Marcel Holtmann, Luiz Augusto von Dentz; +Cc: linux-bluetooth, Siwei Zhang
l2cap_sock_new_connection_cb() accesses l2cap_pi(sk)->chan after
release_sock(parent). Once the parent lock is released, the child
socket sk can be freed by another task.
Allocate the channel outside the func to prevent this.
Fixes: 8ffb929098a5 ("Bluetooth: Remove parent socket usage from l2cap_core.c")
Cc: stable@kernel.org
Assisted-by: Claude:claude-opus-4-6
Signed-off-by: Siwei Zhang <oss@fourdim.xyz>
---
include/net/bluetooth/l2cap.h | 8 +++--
net/bluetooth/6lowpan.c | 27 ++++++++--------
net/bluetooth/l2cap_core.c | 58 ++++++++++++++++++++++++++++-------
net/bluetooth/l2cap_sock.c | 48 +++++++++++++++++------------
net/bluetooth/smp.c | 13 +++-----
5 files changed, 98 insertions(+), 56 deletions(-)
diff --git a/include/net/bluetooth/l2cap.h b/include/net/bluetooth/l2cap.h
index 5172afee5494..f7a11e6431f0 100644
--- a/include/net/bluetooth/l2cap.h
+++ b/include/net/bluetooth/l2cap.h
@@ -619,7 +619,8 @@ struct l2cap_chan {
struct l2cap_ops {
char *name;
- struct l2cap_chan *(*new_connection) (struct l2cap_chan *chan);
+ int (*new_connection)(struct l2cap_chan *chan,
+ struct l2cap_chan *new_chan);
int (*recv) (struct l2cap_chan * chan,
struct sk_buff *skb);
void (*teardown) (struct l2cap_chan *chan, int err);
@@ -883,9 +884,10 @@ static inline __u16 __next_seq(struct l2cap_chan *chan, __u16 seq)
return (seq + 1) % (chan->tx_win_max + 1);
}
-static inline struct l2cap_chan *l2cap_chan_no_new_connection(struct l2cap_chan *chan)
+static inline int l2cap_chan_no_new_connection(struct l2cap_chan *chan,
+ struct l2cap_chan *new_chan)
{
- return NULL;
+ return -EOPNOTSUPP;
}
static inline int l2cap_chan_no_recv(struct l2cap_chan *chan, struct sk_buff *skb)
diff --git a/net/bluetooth/6lowpan.c b/net/bluetooth/6lowpan.c
index 23a229ab6a33..a5830b5746d8 100644
--- a/net/bluetooth/6lowpan.c
+++ b/net/bluetooth/6lowpan.c
@@ -622,6 +622,15 @@ static bool is_bt_6lowpan(struct hci_conn *hcon)
return true;
}
+static void chan_init(struct l2cap_chan *chan)
+{
+ l2cap_chan_set_defaults(chan);
+
+ chan->chan_type = L2CAP_CHAN_CONN_ORIENTED;
+ chan->mode = L2CAP_MODE_LE_FLOWCTL;
+ chan->imtu = 1280;
+}
+
static struct l2cap_chan *chan_create(void)
{
struct l2cap_chan *chan;
@@ -630,11 +639,7 @@ static struct l2cap_chan *chan_create(void)
if (!chan)
return NULL;
- l2cap_chan_set_defaults(chan);
-
- chan->chan_type = L2CAP_CHAN_CONN_ORIENTED;
- chan->mode = L2CAP_MODE_LE_FLOWCTL;
- chan->imtu = 1280;
+ chan_init(chan);
return chan;
}
@@ -743,19 +748,15 @@ static inline void chan_ready_cb(struct l2cap_chan *chan)
ifup(dev->netdev);
}
-static inline struct l2cap_chan *chan_new_conn_cb(struct l2cap_chan *pchan)
+static inline int chan_new_conn_cb(struct l2cap_chan *pchan,
+ struct l2cap_chan *chan)
{
- struct l2cap_chan *chan;
-
- chan = chan_create();
- if (!chan)
- return NULL;
-
+ chan_init(chan);
chan->ops = pchan->ops;
BT_DBG("chan %p pchan %p", chan, pchan);
- return chan;
+ return 0;
}
static void unregister_dev(struct lowpan_btle_dev *dev)
diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
index fdccd62ccca8..505f32034971 100644
--- a/net/bluetooth/l2cap_core.c
+++ b/net/bluetooth/l2cap_core.c
@@ -4051,10 +4051,16 @@ static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
goto response;
}
- chan = pchan->ops->new_connection(pchan);
+ chan = l2cap_chan_create();
if (!chan)
goto response;
+ if (pchan->ops->new_connection(pchan, chan) < 0) {
+ l2cap_chan_put(chan);
+ chan = NULL;
+ goto response;
+ }
+
/* For certain devices (ex: HID mouse), support for authentication,
* pairing and bonding is optional. For such devices, inorder to avoid
* the ACL alive for too long after L2CAP disconnection, reset the ACL
@@ -4132,6 +4138,10 @@ static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
chan->num_conf_req++;
}
+ /* Drop our local ref; __l2cap_chan_add() pinned chan via the conn list. */
+ if (chan)
+ l2cap_chan_put(chan);
+
l2cap_chan_unlock(pchan);
l2cap_chan_put(pchan);
}
@@ -4881,6 +4891,7 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
struct l2cap_le_conn_rsp rsp;
struct l2cap_chan *chan, *pchan;
u16 dcid, scid, credits, mtu, mps;
+ u16 rsp_mtu, rsp_mps;
__le16 psm;
u8 result;
@@ -4893,6 +4904,8 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
psm = req->psm;
dcid = 0;
credits = 0;
+ rsp_mtu = 0;
+ rsp_mps = 0;
if (mtu < 23 || mps < 23)
return -EPROTO;
@@ -4953,12 +4966,19 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
goto response_unlock;
}
- chan = pchan->ops->new_connection(pchan);
+ chan = l2cap_chan_create();
if (!chan) {
result = L2CAP_CR_LE_NO_MEM;
goto response_unlock;
}
+ if (pchan->ops->new_connection(pchan, chan) < 0) {
+ l2cap_chan_put(chan);
+ chan = NULL;
+ result = L2CAP_CR_LE_NO_MEM;
+ goto response_unlock;
+ }
+
bacpy(&chan->src, &conn->hcon->src);
bacpy(&chan->dst, &conn->hcon->dst);
chan->src_type = bdaddr_src_type(conn->hcon);
@@ -4974,6 +4994,8 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
dcid = chan->scid;
credits = chan->rx_credits;
+ rsp_mtu = chan->imtu;
+ rsp_mps = chan->mps;
__set_chan_timer(chan, chan->ops->get_sndtimeo(chan));
@@ -4993,6 +5015,9 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
result = L2CAP_CR_LE_SUCCESS;
}
+ /* Drop our local ref; __l2cap_chan_add() pinned chan via the conn list. */
+ l2cap_chan_put(chan);
+
response_unlock:
l2cap_chan_unlock(pchan);
l2cap_chan_put(pchan);
@@ -5001,13 +5026,8 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
return 0;
response:
- if (chan) {
- rsp.mtu = cpu_to_le16(chan->imtu);
- rsp.mps = cpu_to_le16(chan->mps);
- } else {
- rsp.mtu = 0;
- rsp.mps = 0;
- }
+ rsp.mtu = cpu_to_le16(rsp_mtu);
+ rsp.mps = cpu_to_le16(rsp_mps);
rsp.dcid = cpu_to_le16(dcid);
rsp.credits = cpu_to_le16(credits);
@@ -5177,12 +5197,18 @@ static inline int l2cap_ecred_conn_req(struct l2cap_conn *conn,
continue;
}
- chan = pchan->ops->new_connection(pchan);
+ chan = l2cap_chan_create();
if (!chan) {
result = L2CAP_CR_LE_NO_MEM;
continue;
}
+ if (pchan->ops->new_connection(pchan, chan) < 0) {
+ l2cap_chan_put(chan);
+ result = L2CAP_CR_LE_NO_MEM;
+ continue;
+ }
+
bacpy(&chan->src, &conn->hcon->src);
bacpy(&chan->dst, &conn->hcon->dst);
chan->src_type = bdaddr_src_type(conn->hcon);
@@ -5217,6 +5243,9 @@ static inline int l2cap_ecred_conn_req(struct l2cap_conn *conn,
} else {
l2cap_chan_ready(chan);
}
+
+ /* Drop our local ref; __l2cap_chan_add() pinned chan via the conn list. */
+ l2cap_chan_put(chan);
}
unlock:
@@ -7399,7 +7428,11 @@ static void l2cap_connect_cfm(struct hci_conn *hcon, u8 status)
goto next;
l2cap_chan_lock(pchan);
- chan = pchan->ops->new_connection(pchan);
+ chan = l2cap_chan_create();
+ if (chan && pchan->ops->new_connection(pchan, chan) < 0) {
+ l2cap_chan_put(chan);
+ chan = NULL;
+ }
if (chan) {
bacpy(&chan->src, &hcon->src);
bacpy(&chan->dst, &hcon->dst);
@@ -7407,6 +7440,9 @@ static void l2cap_connect_cfm(struct hci_conn *hcon, u8 status)
chan->dst_type = dst_type;
__l2cap_chan_add(conn, chan);
+
+ /* Drop our local ref; __l2cap_chan_add() pinned chan via the conn list. */
+ l2cap_chan_put(chan);
}
l2cap_chan_unlock(pchan);
diff --git a/net/bluetooth/l2cap_sock.c b/net/bluetooth/l2cap_sock.c
index dede550d6031..598f24c8f704 100644
--- a/net/bluetooth/l2cap_sock.c
+++ b/net/bluetooth/l2cap_sock.c
@@ -46,7 +46,8 @@ static struct bt_sock_list l2cap_sk_list = {
static const struct proto_ops l2cap_sock_ops;
static void l2cap_sock_init(struct sock *sk, struct sock *parent);
static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
- int proto, gfp_t prio, int kern);
+ int proto, gfp_t prio, int kern,
+ struct l2cap_chan *chan);
static void l2cap_sock_cleanup_listen(struct sock *parent);
bool l2cap_is_socket(struct socket *sock)
@@ -1507,12 +1508,13 @@ static void l2cap_sock_cleanup_listen(struct sock *parent)
}
}
-static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
+static int l2cap_sock_new_connection_cb(struct l2cap_chan *chan,
+ struct l2cap_chan *new_chan)
{
struct sock *sk, *parent = chan->data;
if (!parent)
- return NULL;
+ return -EINVAL;
lock_sock(parent);
@@ -1520,15 +1522,15 @@ static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
if (sk_acceptq_is_full(parent)) {
BT_DBG("backlog full %d", parent->sk_ack_backlog);
release_sock(parent);
- return NULL;
+ return -ENOBUFS;
}
sk = l2cap_sock_alloc(sock_net(parent), NULL, BTPROTO_L2CAP,
- GFP_ATOMIC, 0);
+ GFP_ATOMIC, 0, new_chan);
if (!sk) {
release_sock(parent);
- return NULL;
- }
+ return -ENOMEM;
+ }
bt_sock_reclassify_lock(sk, BTPROTO_L2CAP);
@@ -1538,7 +1540,7 @@ static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
release_sock(parent);
- return l2cap_pi(sk)->chan;
+ return 0;
}
static int l2cap_sock_recv_cb(struct l2cap_chan *chan, struct sk_buff *skb)
@@ -1939,10 +1941,10 @@ static struct proto l2cap_proto = {
};
static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
- int proto, gfp_t prio, int kern)
+ int proto, gfp_t prio, int kern,
+ struct l2cap_chan *chan)
{
struct sock *sk;
- struct l2cap_chan *chan;
sk = bt_sock_alloc(net, sock, &l2cap_proto, proto, prio, kern);
if (!sk)
@@ -1953,14 +1955,11 @@ static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
INIT_LIST_HEAD(&l2cap_pi(sk)->rx_busy);
- chan = l2cap_chan_create();
- if (!chan) {
- sk_free(sk);
- if (sock)
- sock->sk = NULL;
- return NULL;
- }
-
+ /* The sock owns two refs on chan, matching the puts in
+ * l2cap_sock_kill() and l2cap_sock_destruct(). The caller keeps
+ * its own ref independent of these.
+ */
+ l2cap_chan_hold(chan);
l2cap_chan_hold(chan);
l2cap_pi(sk)->chan = chan;
@@ -1972,6 +1971,7 @@ static int l2cap_sock_create(struct net *net, struct socket *sock, int protocol,
int kern)
{
struct sock *sk;
+ struct l2cap_chan *chan;
BT_DBG("sock %p", sock);
@@ -1986,10 +1986,18 @@ static int l2cap_sock_create(struct net *net, struct socket *sock, int protocol,
sock->ops = &l2cap_sock_ops;
- sk = l2cap_sock_alloc(net, sock, protocol, GFP_ATOMIC, kern);
- if (!sk)
+ chan = l2cap_chan_create();
+ if (!chan)
return -ENOMEM;
+ sk = l2cap_sock_alloc(net, sock, protocol, GFP_ATOMIC, kern, chan);
+ if (!sk) {
+ l2cap_chan_put(chan);
+ return -ENOMEM;
+ }
+ /* Sock has taken its own refs on chan; drop the chan_create() ref. */
+ l2cap_chan_put(chan);
+
l2cap_sock_init(sk, NULL);
bt_sock_link(&l2cap_sk_list, sk);
return 0;
diff --git a/net/bluetooth/smp.c b/net/bluetooth/smp.c
index 1739c1989dbd..25cb5dc580bf 100644
--- a/net/bluetooth/smp.c
+++ b/net/bluetooth/smp.c
@@ -3204,16 +3204,11 @@ static const struct l2cap_ops smp_chan_ops = {
.get_sndtimeo = l2cap_chan_no_get_sndtimeo,
};
-static inline struct l2cap_chan *smp_new_conn_cb(struct l2cap_chan *pchan)
+static inline int smp_new_conn_cb(struct l2cap_chan *pchan,
+ struct l2cap_chan *chan)
{
- struct l2cap_chan *chan;
-
BT_DBG("pchan %p", pchan);
- chan = l2cap_chan_create();
- if (!chan)
- return NULL;
-
chan->chan_type = pchan->chan_type;
chan->ops = &smp_chan_ops;
chan->scid = pchan->scid;
@@ -3229,9 +3224,9 @@ static inline struct l2cap_chan *smp_new_conn_cb(struct l2cap_chan *pchan)
*/
atomic_set(&chan->nesting, L2CAP_NESTING_SMP);
- BT_DBG("created chan %p", chan);
+ BT_DBG("initialised chan %p", chan);
- return chan;
+ return 0;
}
static const struct l2cap_ops smp_root_chan_ops = {
--
2.54.0
^ permalink raw reply related [flat|nested] 20+ messages in thread* RE: Bluetooth: L2CAP: Fix use-after-free in l2cap_sock_new_connection_cb()
2026-05-29 16:54 [PATCH v6 1/1] " Siwei Zhang
@ 2026-05-29 18:21 ` bluez.test.bot
0 siblings, 0 replies; 20+ messages in thread
From: bluez.test.bot @ 2026-05-29 18:21 UTC (permalink / raw)
To: linux-bluetooth, oss
[-- Attachment #1: Type: text/plain, Size: 11948 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=1103029
---Test result---
Test Summary:
CheckPatch PASS 1.68 seconds
VerifyFixes PASS 0.14 seconds
VerifySignedoff PASS 0.14 seconds
GitLint PASS 0.35 seconds
SubjectPrefix PASS 0.13 seconds
BuildKernel PASS 25.47 seconds
CheckAllWarning PASS 27.77 seconds
CheckSparse PASS 26.54 seconds
BuildKernel32 PASS 24.52 seconds
TestRunnerSetup PASS 530.14 seconds
TestRunner_l2cap-tester FAIL 55.83 seconds
TestRunner_smp-tester FAIL 23.82 seconds
TestRunner_6lowpan-tester FAIL 22.93 seconds
IncrementalBuild PASS 25.60 seconds
Details
##############################
Test: TestRunner_l2cap-tester - FAIL
Desc: Run l2cap-tester with test-runner
Output:
WARNING: held lock freed!
7.1.0-rc1-g5f2fcd0833d0 #1 Not tainted
-------------------------
memcheck-amd64-/34 is freeing memory ffff888002762000-ffff8880027627ff, with a lock still held there!
ffff888002762508 (&chan->lock#4){+.+.}-{4:4}, at: l2cap_conn_del+0x33b/0x660
5 locks held by memcheck-amd64-/34:
#0: ffff8880026e0e80 (&hdev->req_lock){+.+.}-{4:4}, at: hci_dev_do_close+0x5d/0x90
#1: ffff8880026e00b0 (&hdev->lock){+.+.}-{4:4}, at: hci_dev_close_sync+0x2ca/0xfa0
#2: ffffffffb92bc278 (hci_cb_list_lock){+.+.}-{4:4}, at: hci_conn_hash_flush+0xed/0x230
#3: ffff8880022ec2f0 (&conn->lock){+.+.}-{4:4}, at: l2cap_conn_del+0x9b/0x660
#4: ffff888002762508 (&chan->lock#4){+.+.}-{4:4}, at: l2cap_conn_del+0x33b/0x660
stack backtrace:
CPU: 0 UID: 0 PID: 34 Comm: memcheck-amd64- Not tainted 7.1.0-rc1-g5f2fcd0833d0 #1 PREEMPT(lazy)
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.13.0-1ubuntu1.1 04/01/2014
Call Trace:
<TASK>
debug_check_no_locks_freed+0x102/0x140
? l2cap_chan_del+0x220/0x770
kfree+0x22b/0x4c0
? _raw_write_unlock+0x1e/0x40
l2cap_chan_del+0x220/0x770
l2cap_conn_del+0x346/0x660
hci_conn_hash_flush+0x135/0x230
hci_dev_close_sync+0x4f8/0xfa0
hci_dev_do_close+0x65/0x90
hci_unregister_dev+0x254/0x510
vhci_release+0x183/0x240
__fput+0x356/0x9c0
? lock_is_held_type+0x9b/0x110
fput_close_sync+0xd6/0x180
? __pfx_fput_close_sync+0x10/0x10
__x64_sys_close+0x78/0xd0
do_syscall_64+0x9f/0x560
entry_SYSCALL_64_after_hwframe+0x74/0x7c
RIP: 0033:0x5805ccc9
Code: 00 e8 eb a4 fe ff 66 2e 0f 1f 84 00 00 00 00 00 90 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <c3> 66 0f 1f 44 00 00 f3 0f 1e fa 8d 87 ff 0f 00 00 89 fa 89 ff 3d
RSP: 002b:0000001002db9d08 EFLAGS: 00000206 ORIG_RAX: 0000000000000003
RAX: ffffffffffffffda RBX: 0000001002386d90 RCX: 000000005805ccc9
RDX: 0000000000000000 RSI: 0000000000000000 RDI: 0000000000000007
...
BUG: KASAN: slab-use-after-free in l2cap_chan_del+0x6ee/0x770
Write of size 8 at addr ffff888002762000 by task memcheck-amd64-/34
CPU: 0 UID: 0 PID: 34 Comm: memcheck-amd64- Not tainted 7.1.0-rc1-g5f2fcd0833d0 #1 PREEMPT(lazy)
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.13.0-1ubuntu1.1 04/01/2014
Call Trace:
<TASK>
print_report+0x108/0x5c0
? __virt_addr_valid+0x21c/0x3f0
? l2cap_chan_del+0x6ee/0x770
kasan_report+0x94/0xc0
? l2cap_chan_del+0x6ee/0x770
l2cap_chan_del+0x6ee/0x770
l2cap_conn_del+0x346/0x660
hci_conn_hash_flush+0x135/0x230
hci_dev_close_sync+0x4f8/0xfa0
hci_dev_do_close+0x65/0x90
hci_unregister_dev+0x254/0x510
vhci_release+0x183/0x240
__fput+0x356/0x9c0
? lock_is_held_type+0x9b/0x110
fput_close_sync+0xd6/0x180
? __pfx_fput_close_sync+0x10/0x10
__x64_sys_close+0x78/0xd0
do_syscall_64+0x9f/0x560
entry_SYSCALL_64_after_hwframe+0x74/0x7c
RIP: 0033:0x5805ccc9
Code: 00 e8 eb a4 fe ff 66 2e 0f 1f 84 00 00 00 00 00 90 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <c3> 66 0f 1f 44 00 00 f3 0f 1e fa 8d 87 ff 0f 00 00 89 fa 89 ff 3d
RSP: 002b:0000001002db9d08 EFLAGS: 00000206 ORIG_RAX: 0000000000000003
RAX: ffffffffffffffda RBX: 0000001002386d90 RCX: 000000005805ccc9
RDX: 0000000000000000 RSI: 0000000000000000 RDI: 0000000000000007
RBP: 0000000000000001 R08: 0000000005229e80 R09: 0000000000000040
R10: 00000000000002fa R11: 0000000000000206 R12: 0000001002386e20
R13: 0000001002008360 R14: 0000000000000003 R15: 0000001002db9d70
Total: 96, Passed: 96 (100.0%), Failed: 0, Not Run: 0
##############################
Test: TestRunner_smp-tester - FAIL
Desc: Run smp-tester with test-runner
Output:
WARNING: held lock freed!
7.1.0-rc1-g5f2fcd0833d0 #1 Not tainted
-------------------------
memcheck-amd64-/34 is freeing memory ffff8880021b6000-ffff8880021b67ff, with a lock still held there!
ffff8880021b6508 (&chan->lock#3){+.+.}-{4:4}, at: l2cap_conn_del+0x33b/0x660
5 locks held by memcheck-amd64-/34:
#0: ffff8880024e4e80 (&hdev->req_lock){+.+.}-{4:4}, at: hci_dev_do_close+0x5d/0x90
#1: ffff8880024e40b0 (&hdev->lock){+.+.}-{4:4}, at: hci_dev_close_sync+0x2ca/0xfa0
#2: ffffffffb6ebc278 (hci_cb_list_lock){+.+.}-{4:4}, at: hci_conn_hash_flush+0xed/0x230
#3: ffff8880022fb2f0 (&conn->lock){+.+.}-{4:4}, at: l2cap_conn_del+0x9b/0x660
#4: ffff8880021b6508 (&chan->lock#3){+.+.}-{4:4}, at: l2cap_conn_del+0x33b/0x660
stack backtrace:
CPU: 0 UID: 0 PID: 34 Comm: memcheck-amd64- Not tainted 7.1.0-rc1-g5f2fcd0833d0 #1 PREEMPT(lazy)
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.13.0-1ubuntu1.1 04/01/2014
Call Trace:
<TASK>
debug_check_no_locks_freed+0x102/0x140
? l2cap_chan_del+0x220/0x770
kfree+0x22b/0x4c0
? _raw_write_unlock+0x1e/0x40
l2cap_chan_del+0x220/0x770
l2cap_conn_del+0x346/0x660
hci_conn_hash_flush+0x135/0x230
hci_dev_close_sync+0x4f8/0xfa0
hci_dev_do_close+0x65/0x90
hci_unregister_dev+0x254/0x510
vhci_release+0x183/0x240
__fput+0x356/0x9c0
? lock_is_held_type+0x9b/0x110
fput_close_sync+0xd6/0x180
? __pfx_fput_close_sync+0x10/0x10
__x64_sys_close+0x78/0xd0
do_syscall_64+0x9f/0x560
entry_SYSCALL_64_after_hwframe+0x74/0x7c
RIP: 0033:0x5805ccc9
Code: 00 e8 eb a4 fe ff 66 2e 0f 1f 84 00 00 00 00 00 90 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <c3> 66 0f 1f 44 00 00 f3 0f 1e fa 8d 87 ff 0f 00 00 89 fa 89 ff 3d
RSP: 002b:0000001002db5d08 EFLAGS: 00000206 ORIG_RAX: 0000000000000003
RAX: ffffffffffffffda RBX: 0000001002386d90 RCX: 000000005805ccc9
RDX: 0000000000000000 RSI: 0000000000000000 RDI: 000000000000000a
...
BUG: KASAN: slab-use-after-free in l2cap_chan_del+0x6ee/0x770
Write of size 8 at addr ffff8880021b6000 by task memcheck-amd64-/34
CPU: 0 UID: 0 PID: 34 Comm: memcheck-amd64- Not tainted 7.1.0-rc1-g5f2fcd0833d0 #1 PREEMPT(lazy)
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.13.0-1ubuntu1.1 04/01/2014
Call Trace:
<TASK>
print_report+0x108/0x5c0
? __virt_addr_valid+0x21c/0x3f0
? l2cap_chan_del+0x6ee/0x770
kasan_report+0x94/0xc0
? l2cap_chan_del+0x6ee/0x770
l2cap_chan_del+0x6ee/0x770
l2cap_conn_del+0x346/0x660
hci_conn_hash_flush+0x135/0x230
hci_dev_close_sync+0x4f8/0xfa0
hci_dev_do_close+0x65/0x90
hci_unregister_dev+0x254/0x510
vhci_release+0x183/0x240
__fput+0x356/0x9c0
? lock_is_held_type+0x9b/0x110
fput_close_sync+0xd6/0x180
? __pfx_fput_close_sync+0x10/0x10
__x64_sys_close+0x78/0xd0
do_syscall_64+0x9f/0x560
entry_SYSCALL_64_after_hwframe+0x74/0x7c
RIP: 0033:0x5805ccc9
Code: 00 e8 eb a4 fe ff 66 2e 0f 1f 84 00 00 00 00 00 90 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <c3> 66 0f 1f 44 00 00 f3 0f 1e fa 8d 87 ff 0f 00 00 89 fa 89 ff 3d
RSP: 002b:0000001002db5d08 EFLAGS: 00000206 ORIG_RAX: 0000000000000003
RAX: ffffffffffffffda RBX: 0000001002386d90 RCX: 000000005805ccc9
RDX: 0000000000000000 RSI: 0000000000000000 RDI: 000000000000000a
RBP: 0000000000000001 R08: 0000000004c198e0 R09: 0000000000000040
R10: 0000000000000002 R11: 0000000000000206 R12: 0000001002386e20
R13: 0000001002008360 R14: 0000000000000003 R15: 0000001002db5d70
Total: 8, Passed: 8 (100.0%), Failed: 0, Not Run: 0
##############################
Test: TestRunner_6lowpan-tester - FAIL
Desc: Run 6lowpan-tester with test-runner
Output:
WARNING: held lock freed!
7.1.0-rc1-g5f2fcd0833d0 #1 Not tainted
-------------------------
memcheck-amd64-/34 is freeing memory ffff8880026af000-ffff8880026af7ff, with a lock still held there!
ffff8880026af508 (&chan->lock#4){+.+.}-{4:4}, at: l2cap_conn_del+0x33b/0x660
5 locks held by memcheck-amd64-/34:
#0: ffff8880025ece80 (&hdev->req_lock){+.+.}-{4:4}, at: hci_dev_do_close+0x5d/0x90
#1: ffff8880025ec0b0 (&hdev->lock){+.+.}-{4:4}, at: hci_dev_close_sync+0x2ca/0xfa0
#2: ffffffff84ebc278 (hci_cb_list_lock){+.+.}-{4:4}, at: hci_conn_hash_flush+0xed/0x230
#3: ffff888001fcbaf0 (&conn->lock){+.+.}-{4:4}, at: l2cap_conn_del+0x9b/0x660
#4: ffff8880026af508 (&chan->lock#4){+.+.}-{4:4}, at: l2cap_conn_del+0x33b/0x660
stack backtrace:
CPU: 0 UID: 0 PID: 34 Comm: memcheck-amd64- Not tainted 7.1.0-rc1-g5f2fcd0833d0 #1 PREEMPT(lazy)
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.13.0-1ubuntu1.1 04/01/2014
Call Trace:
<TASK>
debug_check_no_locks_freed+0x102/0x140
? l2cap_chan_del+0x220/0x770
kfree+0x22b/0x4c0
? _raw_write_unlock+0x1e/0x40
l2cap_chan_del+0x220/0x770
l2cap_conn_del+0x346/0x660
hci_conn_hash_flush+0x135/0x230
hci_dev_close_sync+0x4f8/0xfa0
hci_dev_do_close+0x65/0x90
hci_unregister_dev+0x254/0x510
vhci_release+0x183/0x240
__fput+0x356/0x9c0
? lock_is_held_type+0x9b/0x110
fput_close_sync+0xd6/0x180
? __pfx_fput_close_sync+0x10/0x10
__x64_sys_close+0x78/0xd0
do_syscall_64+0x9f/0x560
entry_SYSCALL_64_after_hwframe+0x74/0x7c
RIP: 0033:0x5805ccc9
Code: 00 e8 eb a4 fe ff 66 2e 0f 1f 84 00 00 00 00 00 90 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <c3> 66 0f 1f 44 00 00 f3 0f 1e fa 8d 87 ff 0f 00 00 89 fa 89 ff 3d
RSP: 002b:0000001002cb5d08 EFLAGS: 00000206 ORIG_RAX: 0000000000000003
RAX: ffffffffffffffda RBX: 0000001002386d90 RCX: 000000005805ccc9
RDX: 0000000000000000 RSI: 0000000000000000 RDI: 0000000000000007
...
BUG: KASAN: slab-use-after-free in l2cap_chan_del+0x6ee/0x770
Write of size 8 at addr ffff8880026af000 by task memcheck-amd64-/34
CPU: 0 UID: 0 PID: 34 Comm: memcheck-amd64- Not tainted 7.1.0-rc1-g5f2fcd0833d0 #1 PREEMPT(lazy)
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.13.0-1ubuntu1.1 04/01/2014
Call Trace:
<TASK>
print_report+0x108/0x5c0
? __virt_addr_valid+0x21c/0x3f0
? l2cap_chan_del+0x6ee/0x770
kasan_report+0x94/0xc0
? l2cap_chan_del+0x6ee/0x770
l2cap_chan_del+0x6ee/0x770
l2cap_conn_del+0x346/0x660
hci_conn_hash_flush+0x135/0x230
hci_dev_close_sync+0x4f8/0xfa0
hci_dev_do_close+0x65/0x90
hci_unregister_dev+0x254/0x510
vhci_release+0x183/0x240
__fput+0x356/0x9c0
? lock_is_held_type+0x9b/0x110
fput_close_sync+0xd6/0x180
? __pfx_fput_close_sync+0x10/0x10
__x64_sys_close+0x78/0xd0
do_syscall_64+0x9f/0x560
entry_SYSCALL_64_after_hwframe+0x74/0x7c
RIP: 0033:0x5805ccc9
Code: 00 e8 eb a4 fe ff 66 2e 0f 1f 84 00 00 00 00 00 90 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <c3> 66 0f 1f 44 00 00 f3 0f 1e fa 8d 87 ff 0f 00 00 89 fa 89 ff 3d
RSP: 002b:0000001002cb5d08 EFLAGS: 00000206 ORIG_RAX: 0000000000000003
RAX: ffffffffffffffda RBX: 0000001002386d90 RCX: 000000005805ccc9
RDX: 0000000000000000 RSI: 0000000000000000 RDI: 0000000000000007
RBP: 0000000000000001 R08: 0000000004c205f0 R09: 0000000000000040
R10: 0000000000000002 R11: 0000000000000206 R12: 0000001002386e20
R13: 0000001002008360 R14: 0000000000000003 R15: 0000001002cb5d70
Total: 8, Passed: 8 (100.0%), Failed: 0, Not Run: 0
https://github.com/bluez/bluetooth-next/pull/256
---
Regards,
Linux Bluetooth
^ permalink raw reply [flat|nested] 20+ messages in thread
* [PATCH v5 1/1] Bluetooth: L2CAP: Fix use-after-free in l2cap_sock_new_connection_cb()
@ 2026-05-20 16:20 Siwei Zhang
2026-05-20 18:07 ` bluez.test.bot
0 siblings, 1 reply; 20+ messages in thread
From: Siwei Zhang @ 2026-05-20 16:20 UTC (permalink / raw)
To: Marcel Holtmann, Luiz Augusto von Dentz; +Cc: linux-bluetooth, Siwei Zhang
l2cap_sock_new_connection_cb() accesses l2cap_pi(sk)->chan after
release_sock(parent). Once the parent lock is released, the child
socket sk can be freed by another task.
Allocate the channel outside the func to prevent this.
Fixes: 8ffb929098a5 ("Bluetooth: Remove parent socket usage from l2cap_core.c")
Cc: stable@kernel.org
Assisted-by: Claude:claude-opus-4-6
Signed-off-by: Siwei Zhang <oss@fourdim.xyz>
---
include/net/bluetooth/l2cap.h | 8 +++--
net/bluetooth/6lowpan.c | 14 ++++-----
net/bluetooth/l2cap_core.c | 58 ++++++++++++++++++++++++++++-------
net/bluetooth/l2cap_sock.c | 48 +++++++++++++++++------------
net/bluetooth/smp.c | 13 +++-----
5 files changed, 91 insertions(+), 50 deletions(-)
diff --git a/include/net/bluetooth/l2cap.h b/include/net/bluetooth/l2cap.h
index 5172afee5494..f7a11e6431f0 100644
--- a/include/net/bluetooth/l2cap.h
+++ b/include/net/bluetooth/l2cap.h
@@ -619,7 +619,8 @@ struct l2cap_chan {
struct l2cap_ops {
char *name;
- struct l2cap_chan *(*new_connection) (struct l2cap_chan *chan);
+ int (*new_connection)(struct l2cap_chan *chan,
+ struct l2cap_chan *new_chan);
int (*recv) (struct l2cap_chan * chan,
struct sk_buff *skb);
void (*teardown) (struct l2cap_chan *chan, int err);
@@ -883,9 +884,10 @@ static inline __u16 __next_seq(struct l2cap_chan *chan, __u16 seq)
return (seq + 1) % (chan->tx_win_max + 1);
}
-static inline struct l2cap_chan *l2cap_chan_no_new_connection(struct l2cap_chan *chan)
+static inline int l2cap_chan_no_new_connection(struct l2cap_chan *chan,
+ struct l2cap_chan *new_chan)
{
- return NULL;
+ return -EOPNOTSUPP;
}
static inline int l2cap_chan_no_recv(struct l2cap_chan *chan, struct sk_buff *skb)
diff --git a/net/bluetooth/6lowpan.c b/net/bluetooth/6lowpan.c
index 23a229ab6a33..286c0b45055b 100644
--- a/net/bluetooth/6lowpan.c
+++ b/net/bluetooth/6lowpan.c
@@ -743,19 +743,19 @@ static inline void chan_ready_cb(struct l2cap_chan *chan)
ifup(dev->netdev);
}
-static inline struct l2cap_chan *chan_new_conn_cb(struct l2cap_chan *pchan)
+static inline int chan_new_conn_cb(struct l2cap_chan *pchan,
+ struct l2cap_chan *chan)
{
- struct l2cap_chan *chan;
-
- chan = chan_create();
- if (!chan)
- return NULL;
+ l2cap_chan_set_defaults(chan);
+ chan->chan_type = L2CAP_CHAN_CONN_ORIENTED;
+ chan->mode = L2CAP_MODE_LE_FLOWCTL;
+ chan->imtu = 1280;
chan->ops = pchan->ops;
BT_DBG("chan %p pchan %p", chan, pchan);
- return chan;
+ return 0;
}
static void unregister_dev(struct lowpan_btle_dev *dev)
diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
index fdccd62ccca8..505f32034971 100644
--- a/net/bluetooth/l2cap_core.c
+++ b/net/bluetooth/l2cap_core.c
@@ -4051,10 +4051,16 @@ static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
goto response;
}
- chan = pchan->ops->new_connection(pchan);
+ chan = l2cap_chan_create();
if (!chan)
goto response;
+ if (pchan->ops->new_connection(pchan, chan) < 0) {
+ l2cap_chan_put(chan);
+ chan = NULL;
+ goto response;
+ }
+
/* For certain devices (ex: HID mouse), support for authentication,
* pairing and bonding is optional. For such devices, inorder to avoid
* the ACL alive for too long after L2CAP disconnection, reset the ACL
@@ -4132,6 +4138,10 @@ static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
chan->num_conf_req++;
}
+ /* Drop our local ref; __l2cap_chan_add() pinned chan via the conn list. */
+ if (chan)
+ l2cap_chan_put(chan);
+
l2cap_chan_unlock(pchan);
l2cap_chan_put(pchan);
}
@@ -4881,6 +4891,7 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
struct l2cap_le_conn_rsp rsp;
struct l2cap_chan *chan, *pchan;
u16 dcid, scid, credits, mtu, mps;
+ u16 rsp_mtu, rsp_mps;
__le16 psm;
u8 result;
@@ -4893,6 +4904,8 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
psm = req->psm;
dcid = 0;
credits = 0;
+ rsp_mtu = 0;
+ rsp_mps = 0;
if (mtu < 23 || mps < 23)
return -EPROTO;
@@ -4953,12 +4966,19 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
goto response_unlock;
}
- chan = pchan->ops->new_connection(pchan);
+ chan = l2cap_chan_create();
if (!chan) {
result = L2CAP_CR_LE_NO_MEM;
goto response_unlock;
}
+ if (pchan->ops->new_connection(pchan, chan) < 0) {
+ l2cap_chan_put(chan);
+ chan = NULL;
+ result = L2CAP_CR_LE_NO_MEM;
+ goto response_unlock;
+ }
+
bacpy(&chan->src, &conn->hcon->src);
bacpy(&chan->dst, &conn->hcon->dst);
chan->src_type = bdaddr_src_type(conn->hcon);
@@ -4974,6 +4994,8 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
dcid = chan->scid;
credits = chan->rx_credits;
+ rsp_mtu = chan->imtu;
+ rsp_mps = chan->mps;
__set_chan_timer(chan, chan->ops->get_sndtimeo(chan));
@@ -4993,6 +5015,9 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
result = L2CAP_CR_LE_SUCCESS;
}
+ /* Drop our local ref; __l2cap_chan_add() pinned chan via the conn list. */
+ l2cap_chan_put(chan);
+
response_unlock:
l2cap_chan_unlock(pchan);
l2cap_chan_put(pchan);
@@ -5001,13 +5026,8 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
return 0;
response:
- if (chan) {
- rsp.mtu = cpu_to_le16(chan->imtu);
- rsp.mps = cpu_to_le16(chan->mps);
- } else {
- rsp.mtu = 0;
- rsp.mps = 0;
- }
+ rsp.mtu = cpu_to_le16(rsp_mtu);
+ rsp.mps = cpu_to_le16(rsp_mps);
rsp.dcid = cpu_to_le16(dcid);
rsp.credits = cpu_to_le16(credits);
@@ -5177,12 +5197,18 @@ static inline int l2cap_ecred_conn_req(struct l2cap_conn *conn,
continue;
}
- chan = pchan->ops->new_connection(pchan);
+ chan = l2cap_chan_create();
if (!chan) {
result = L2CAP_CR_LE_NO_MEM;
continue;
}
+ if (pchan->ops->new_connection(pchan, chan) < 0) {
+ l2cap_chan_put(chan);
+ result = L2CAP_CR_LE_NO_MEM;
+ continue;
+ }
+
bacpy(&chan->src, &conn->hcon->src);
bacpy(&chan->dst, &conn->hcon->dst);
chan->src_type = bdaddr_src_type(conn->hcon);
@@ -5217,6 +5243,9 @@ static inline int l2cap_ecred_conn_req(struct l2cap_conn *conn,
} else {
l2cap_chan_ready(chan);
}
+
+ /* Drop our local ref; __l2cap_chan_add() pinned chan via the conn list. */
+ l2cap_chan_put(chan);
}
unlock:
@@ -7399,7 +7428,11 @@ static void l2cap_connect_cfm(struct hci_conn *hcon, u8 status)
goto next;
l2cap_chan_lock(pchan);
- chan = pchan->ops->new_connection(pchan);
+ chan = l2cap_chan_create();
+ if (chan && pchan->ops->new_connection(pchan, chan) < 0) {
+ l2cap_chan_put(chan);
+ chan = NULL;
+ }
if (chan) {
bacpy(&chan->src, &hcon->src);
bacpy(&chan->dst, &hcon->dst);
@@ -7407,6 +7440,9 @@ static void l2cap_connect_cfm(struct hci_conn *hcon, u8 status)
chan->dst_type = dst_type;
__l2cap_chan_add(conn, chan);
+
+ /* Drop our local ref; __l2cap_chan_add() pinned chan via the conn list. */
+ l2cap_chan_put(chan);
}
l2cap_chan_unlock(pchan);
diff --git a/net/bluetooth/l2cap_sock.c b/net/bluetooth/l2cap_sock.c
index dede550d6031..598f24c8f704 100644
--- a/net/bluetooth/l2cap_sock.c
+++ b/net/bluetooth/l2cap_sock.c
@@ -46,7 +46,8 @@ static struct bt_sock_list l2cap_sk_list = {
static const struct proto_ops l2cap_sock_ops;
static void l2cap_sock_init(struct sock *sk, struct sock *parent);
static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
- int proto, gfp_t prio, int kern);
+ int proto, gfp_t prio, int kern,
+ struct l2cap_chan *chan);
static void l2cap_sock_cleanup_listen(struct sock *parent);
bool l2cap_is_socket(struct socket *sock)
@@ -1507,12 +1508,13 @@ static void l2cap_sock_cleanup_listen(struct sock *parent)
}
}
-static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
+static int l2cap_sock_new_connection_cb(struct l2cap_chan *chan,
+ struct l2cap_chan *new_chan)
{
struct sock *sk, *parent = chan->data;
if (!parent)
- return NULL;
+ return -EINVAL;
lock_sock(parent);
@@ -1520,15 +1522,15 @@ static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
if (sk_acceptq_is_full(parent)) {
BT_DBG("backlog full %d", parent->sk_ack_backlog);
release_sock(parent);
- return NULL;
+ return -ENOBUFS;
}
sk = l2cap_sock_alloc(sock_net(parent), NULL, BTPROTO_L2CAP,
- GFP_ATOMIC, 0);
+ GFP_ATOMIC, 0, new_chan);
if (!sk) {
release_sock(parent);
- return NULL;
- }
+ return -ENOMEM;
+ }
bt_sock_reclassify_lock(sk, BTPROTO_L2CAP);
@@ -1538,7 +1540,7 @@ static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
release_sock(parent);
- return l2cap_pi(sk)->chan;
+ return 0;
}
static int l2cap_sock_recv_cb(struct l2cap_chan *chan, struct sk_buff *skb)
@@ -1939,10 +1941,10 @@ static struct proto l2cap_proto = {
};
static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
- int proto, gfp_t prio, int kern)
+ int proto, gfp_t prio, int kern,
+ struct l2cap_chan *chan)
{
struct sock *sk;
- struct l2cap_chan *chan;
sk = bt_sock_alloc(net, sock, &l2cap_proto, proto, prio, kern);
if (!sk)
@@ -1953,14 +1955,11 @@ static struct sock *l2cap_sock_alloc(struct net *net, struct socket *sock,
INIT_LIST_HEAD(&l2cap_pi(sk)->rx_busy);
- chan = l2cap_chan_create();
- if (!chan) {
- sk_free(sk);
- if (sock)
- sock->sk = NULL;
- return NULL;
- }
-
+ /* The sock owns two refs on chan, matching the puts in
+ * l2cap_sock_kill() and l2cap_sock_destruct(). The caller keeps
+ * its own ref independent of these.
+ */
+ l2cap_chan_hold(chan);
l2cap_chan_hold(chan);
l2cap_pi(sk)->chan = chan;
@@ -1972,6 +1971,7 @@ static int l2cap_sock_create(struct net *net, struct socket *sock, int protocol,
int kern)
{
struct sock *sk;
+ struct l2cap_chan *chan;
BT_DBG("sock %p", sock);
@@ -1986,10 +1986,18 @@ static int l2cap_sock_create(struct net *net, struct socket *sock, int protocol,
sock->ops = &l2cap_sock_ops;
- sk = l2cap_sock_alloc(net, sock, protocol, GFP_ATOMIC, kern);
- if (!sk)
+ chan = l2cap_chan_create();
+ if (!chan)
return -ENOMEM;
+ sk = l2cap_sock_alloc(net, sock, protocol, GFP_ATOMIC, kern, chan);
+ if (!sk) {
+ l2cap_chan_put(chan);
+ return -ENOMEM;
+ }
+ /* Sock has taken its own refs on chan; drop the chan_create() ref. */
+ l2cap_chan_put(chan);
+
l2cap_sock_init(sk, NULL);
bt_sock_link(&l2cap_sk_list, sk);
return 0;
diff --git a/net/bluetooth/smp.c b/net/bluetooth/smp.c
index 1739c1989dbd..25cb5dc580bf 100644
--- a/net/bluetooth/smp.c
+++ b/net/bluetooth/smp.c
@@ -3204,16 +3204,11 @@ static const struct l2cap_ops smp_chan_ops = {
.get_sndtimeo = l2cap_chan_no_get_sndtimeo,
};
-static inline struct l2cap_chan *smp_new_conn_cb(struct l2cap_chan *pchan)
+static inline int smp_new_conn_cb(struct l2cap_chan *pchan,
+ struct l2cap_chan *chan)
{
- struct l2cap_chan *chan;
-
BT_DBG("pchan %p", pchan);
- chan = l2cap_chan_create();
- if (!chan)
- return NULL;
-
chan->chan_type = pchan->chan_type;
chan->ops = &smp_chan_ops;
chan->scid = pchan->scid;
@@ -3229,9 +3224,9 @@ static inline struct l2cap_chan *smp_new_conn_cb(struct l2cap_chan *pchan)
*/
atomic_set(&chan->nesting, L2CAP_NESTING_SMP);
- BT_DBG("created chan %p", chan);
+ BT_DBG("initialised chan %p", chan);
- return chan;
+ return 0;
}
static const struct l2cap_ops smp_root_chan_ops = {
--
2.54.0
^ permalink raw reply related [flat|nested] 20+ messages in thread* [PATCH RESEND v4 1/1] Bluetooth: L2CAP: Fix use-after-free in l2cap_sock_new_connection_cb()
@ 2026-05-11 17:09 Siwei Zhang
2026-05-11 18:49 ` bluez.test.bot
0 siblings, 1 reply; 20+ messages in thread
From: Siwei Zhang @ 2026-05-11 17:09 UTC (permalink / raw)
To: Marcel Holtmann, Luiz Augusto von Dentz; +Cc: linux-bluetooth, Siwei Zhang
l2cap_sock_new_connection_cb() accesses l2cap_pi(sk)->chan after
release_sock(parent). Once the parent lock is released, the child
socket sk can be freed by another task.
Save the channel pointer into a local variable while the parent lock
is still held to prevent this.
Fixes: 8ffb929098a5 ("Bluetooth: Remove parent socket usage from l2cap_core.c")
Cc: stable@kernel.org
Assisted-by: Claude:claude-opus-4-7
Signed-off-by: Siwei Zhang <oss@fourdim.xyz>
---
net/bluetooth/6lowpan.c | 5 +++++
net/bluetooth/l2cap_core.c | 12 ++++++++++++
net/bluetooth/l2cap_sock.c | 13 ++++++++++++-
net/bluetooth/smp.c | 5 +++++
4 files changed, 34 insertions(+), 1 deletion(-)
diff --git a/net/bluetooth/6lowpan.c b/net/bluetooth/6lowpan.c
index 23a229ab6a33..71c1c04b61e5 100644
--- a/net/bluetooth/6lowpan.c
+++ b/net/bluetooth/6lowpan.c
@@ -755,6 +755,11 @@ static inline struct l2cap_chan *chan_new_conn_cb(struct l2cap_chan *pchan)
BT_DBG("chan %p pchan %p", chan, pchan);
+ /* Match the put that the caller of ops->new_connection() performs
+ * once it is done with the returned channel pointer.
+ */
+ l2cap_chan_hold(chan);
+
return chan;
}
diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
index 7701528f1167..0f6c3c651207 100644
--- a/net/bluetooth/l2cap_core.c
+++ b/net/bluetooth/l2cap_core.c
@@ -4071,6 +4071,9 @@ static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
__l2cap_chan_add(conn, chan);
+ /* Drop the ops->new_connection() ref; conn list now pins chan. */
+ l2cap_chan_put(chan);
+
dcid = chan->scid;
__set_chan_timer(chan, chan->ops->get_sndtimeo(chan));
@@ -4970,6 +4973,9 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
__l2cap_chan_add(conn, chan);
+ /* Drop the ops->new_connection() ref; conn list now pins chan. */
+ l2cap_chan_put(chan);
+
l2cap_le_flowctl_init(chan, __le16_to_cpu(req->credits));
dcid = chan->scid;
@@ -5194,6 +5200,9 @@ static inline int l2cap_ecred_conn_req(struct l2cap_conn *conn,
__l2cap_chan_add(conn, chan);
+ /* Drop the ops->new_connection() ref; conn list now pins chan. */
+ l2cap_chan_put(chan);
+
l2cap_ecred_init(chan, __le16_to_cpu(req->credits));
/* Init response */
@@ -7407,6 +7416,9 @@ static void l2cap_connect_cfm(struct hci_conn *hcon, u8 status)
chan->dst_type = dst_type;
__l2cap_chan_add(conn, chan);
+
+ /* Drop the ops->new_connection() ref; conn list now pins chan. */
+ l2cap_chan_put(chan);
}
l2cap_chan_unlock(pchan);
diff --git a/net/bluetooth/l2cap_sock.c b/net/bluetooth/l2cap_sock.c
index cf590a67d364..295c79cf5cf3 100644
--- a/net/bluetooth/l2cap_sock.c
+++ b/net/bluetooth/l2cap_sock.c
@@ -1497,6 +1497,7 @@ static void l2cap_sock_cleanup_listen(struct sock *parent)
static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
{
struct sock *sk, *parent = chan->data;
+ struct l2cap_chan *child_chan;
if (!parent)
return NULL;
@@ -1523,9 +1524,19 @@ static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
bt_accept_enqueue(parent, sk, false);
+ child_chan = l2cap_pi(sk)->chan;
+
+ /* Pin the channel for the caller. Once release_sock(parent) returns,
+ * userspace can accept(2) and immediately close(2) the child socket,
+ * which would drop the socket's references on the channel and free
+ * it before the caller (e.g. l2cap_connect_req()) is done using the
+ * returned pointer. The matching put is the caller's responsibility.
+ */
+ l2cap_chan_hold(child_chan);
+
release_sock(parent);
- return l2cap_pi(sk)->chan;
+ return child_chan;
}
static int l2cap_sock_recv_cb(struct l2cap_chan *chan, struct sk_buff *skb)
diff --git a/net/bluetooth/smp.c b/net/bluetooth/smp.c
index 1739c1989dbd..9796c3030434 100644
--- a/net/bluetooth/smp.c
+++ b/net/bluetooth/smp.c
@@ -3231,6 +3231,11 @@ static inline struct l2cap_chan *smp_new_conn_cb(struct l2cap_chan *pchan)
BT_DBG("created chan %p", chan);
+ /* Match the put that the caller of ops->new_connection() performs
+ * once it is done with the returned channel pointer.
+ */
+ l2cap_chan_hold(chan);
+
return chan;
}
--
2.54.0
^ permalink raw reply related [flat|nested] 20+ messages in thread* RE: Bluetooth: L2CAP: Fix use-after-free in l2cap_sock_new_connection_cb()
2026-05-11 17:09 [PATCH RESEND v4 1/1] " Siwei Zhang
@ 2026-05-11 18:49 ` bluez.test.bot
0 siblings, 0 replies; 20+ messages in thread
From: bluez.test.bot @ 2026-05-11 18:49 UTC (permalink / raw)
To: linux-bluetooth, oss
[-- Attachment #1: Type: text/plain, Size: 1708 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=1092984
---Test result---
Test Summary:
CheckPatch PASS 1.40 seconds
GitLint FAIL 0.34 seconds
SubjectPrefix PASS 0.13 seconds
BuildKernel PASS 26.78 seconds
CheckAllWarning PASS 27.90 seconds
CheckSparse PASS 26.85 seconds
BuildKernel32 PASS 24.71 seconds
TestRunnerSetup PASS 524.97 seconds
TestRunner_l2cap-tester PASS 376.41 seconds
TestRunner_smp-tester PASS 18.41 seconds
TestRunner_6lowpan-tester PASS 51.29 seconds
IncrementalBuild PASS 23.95 seconds
Details
##############################
Test: GitLint - FAIL
Desc: Run gitlint
Output:
[RESEND,v4,1/1] Bluetooth: L2CAP: Fix use-after-free in l2cap_sock_new_connection_cb()
WARNING: I3 - ignore-body-lines: gitlint will be switching from using Python regex 'match' (match beginning) to 'search' (match anywhere) semantics. Please review your ignore-body-lines.regex option accordingly. To remove this warning, set general.regex-style-search=True. More details: https://jorisroovers.github.io/gitlint/configuration/#regex-style-search
1: T1 Title exceeds max length (86>80): "[RESEND,v4,1/1] Bluetooth: L2CAP: Fix use-after-free in l2cap_sock_new_connection_cb()"
https://github.com/bluez/bluetooth-next/pull/172
---
Regards,
Linux Bluetooth
^ permalink raw reply [flat|nested] 20+ messages in thread
* [PATCH 1/1] Bluetooth: L2CAP: Fix use-after-free in l2cap_sock_new_connection_cb()
@ 2026-05-11 4:51 Siwei Zhang
2026-05-11 8:32 ` bluez.test.bot
0 siblings, 1 reply; 20+ messages in thread
From: Siwei Zhang @ 2026-05-11 4:51 UTC (permalink / raw)
To: Marcel Holtmann, Luiz Augusto von Dentz; +Cc: linux-bluetooth, Siwei Zhang
l2cap_sock_new_connection_cb() accesses l2cap_pi(sk)->chan after
release_sock(parent). Once the parent lock is released, the child
socket sk can be freed by another task.
Save the channel pointer into a local variable while the parent lock
is still held to prevent this.
Fixes: 8ffb929098a5 ("Bluetooth: Remove parent socket usage from l2cap_core.c")
Cc: stable@kernel.org
Assisted-by: Claude:claude-opus-4-7
Signed-off-by: Siwei Zhang <oss@fourdim.xyz>
---
net/bluetooth/6lowpan.c | 5 +++++
net/bluetooth/l2cap_core.c | 12 ++++++++++++
net/bluetooth/l2cap_sock.c | 13 ++++++++++++-
net/bluetooth/smp.c | 5 +++++
4 files changed, 34 insertions(+), 1 deletion(-)
diff --git a/net/bluetooth/6lowpan.c b/net/bluetooth/6lowpan.c
index 23a229ab6a33..71c1c04b61e5 100644
--- a/net/bluetooth/6lowpan.c
+++ b/net/bluetooth/6lowpan.c
@@ -755,6 +755,11 @@ static inline struct l2cap_chan *chan_new_conn_cb(struct l2cap_chan *pchan)
BT_DBG("chan %p pchan %p", chan, pchan);
+ /* Match the put that the caller of ops->new_connection() performs
+ * once it is done with the returned channel pointer.
+ */
+ l2cap_chan_hold(chan);
+
return chan;
}
diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
index 7701528f1167..0f6c3c651207 100644
--- a/net/bluetooth/l2cap_core.c
+++ b/net/bluetooth/l2cap_core.c
@@ -4071,6 +4071,9 @@ static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
__l2cap_chan_add(conn, chan);
+ /* Drop the ops->new_connection() ref; conn list now pins chan. */
+ l2cap_chan_put(chan);
+
dcid = chan->scid;
__set_chan_timer(chan, chan->ops->get_sndtimeo(chan));
@@ -4970,6 +4973,9 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
__l2cap_chan_add(conn, chan);
+ /* Drop the ops->new_connection() ref; conn list now pins chan. */
+ l2cap_chan_put(chan);
+
l2cap_le_flowctl_init(chan, __le16_to_cpu(req->credits));
dcid = chan->scid;
@@ -5194,6 +5200,9 @@ static inline int l2cap_ecred_conn_req(struct l2cap_conn *conn,
__l2cap_chan_add(conn, chan);
+ /* Drop the ops->new_connection() ref; conn list now pins chan. */
+ l2cap_chan_put(chan);
+
l2cap_ecred_init(chan, __le16_to_cpu(req->credits));
/* Init response */
@@ -7407,6 +7416,9 @@ static void l2cap_connect_cfm(struct hci_conn *hcon, u8 status)
chan->dst_type = dst_type;
__l2cap_chan_add(conn, chan);
+
+ /* Drop the ops->new_connection() ref; conn list now pins chan. */
+ l2cap_chan_put(chan);
}
l2cap_chan_unlock(pchan);
diff --git a/net/bluetooth/l2cap_sock.c b/net/bluetooth/l2cap_sock.c
index cf590a67d364..295c79cf5cf3 100644
--- a/net/bluetooth/l2cap_sock.c
+++ b/net/bluetooth/l2cap_sock.c
@@ -1497,6 +1497,7 @@ static void l2cap_sock_cleanup_listen(struct sock *parent)
static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
{
struct sock *sk, *parent = chan->data;
+ struct l2cap_chan *child_chan;
if (!parent)
return NULL;
@@ -1523,9 +1524,19 @@ static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
bt_accept_enqueue(parent, sk, false);
+ child_chan = l2cap_pi(sk)->chan;
+
+ /* Pin the channel for the caller. Once release_sock(parent) returns,
+ * userspace can accept(2) and immediately close(2) the child socket,
+ * which would drop the socket's references on the channel and free
+ * it before the caller (e.g. l2cap_connect_req()) is done using the
+ * returned pointer. The matching put is the caller's responsibility.
+ */
+ l2cap_chan_hold(child_chan);
+
release_sock(parent);
- return l2cap_pi(sk)->chan;
+ return child_chan;
}
static int l2cap_sock_recv_cb(struct l2cap_chan *chan, struct sk_buff *skb)
diff --git a/net/bluetooth/smp.c b/net/bluetooth/smp.c
index 1739c1989dbd..9796c3030434 100644
--- a/net/bluetooth/smp.c
+++ b/net/bluetooth/smp.c
@@ -3231,6 +3231,11 @@ static inline struct l2cap_chan *smp_new_conn_cb(struct l2cap_chan *pchan)
BT_DBG("created chan %p", chan);
+ /* Match the put that the caller of ops->new_connection() performs
+ * once it is done with the returned channel pointer.
+ */
+ l2cap_chan_hold(chan);
+
return chan;
}
--
2.54.0
^ permalink raw reply related [flat|nested] 20+ messages in thread* [PATCH 1/1] Bluetooth: L2CAP: Fix use-after-free in l2cap_sock_new_connection_cb()
@ 2026-05-11 3:18 Siwei Zhang
2026-05-11 4:21 ` bluez.test.bot
0 siblings, 1 reply; 20+ messages in thread
From: Siwei Zhang @ 2026-05-11 3:18 UTC (permalink / raw)
To: Marcel Holtmann, Luiz Augusto von Dentz; +Cc: linux-bluetooth, Siwei Zhang
l2cap_sock_new_connection_cb() accesses l2cap_pi(sk)->chan after
release_sock(parent). Once the parent lock is released, the child
socket sk can be freed by another task.
Save the channel pointer into a local variable while the parent lock
is still held to prevent this.
Fixes: 8ffb929098a5 ("Bluetooth: Remove parent socket usage from l2cap_core.c")
Cc: stable@kernel.org
Assisted-by: Claude:claude-opus-4-7
Signed-off-by: Siwei Zhang <oss@fourdim.xyz>
---
net/bluetooth/6lowpan.c | 5 +++++
net/bluetooth/l2cap_core.c | 12 ++++++++++++
net/bluetooth/l2cap_sock.c | 13 ++++++++++++-
net/bluetooth/smp.c | 5 +++++
4 files changed, 34 insertions(+), 1 deletion(-)
diff --git a/net/bluetooth/6lowpan.c b/net/bluetooth/6lowpan.c
index 2f03b780b40d..bbe67bd73f9c 100644
--- a/net/bluetooth/6lowpan.c
+++ b/net/bluetooth/6lowpan.c
@@ -755,6 +755,11 @@ static inline struct l2cap_chan *chan_new_conn_cb(struct l2cap_chan *pchan)
BT_DBG("chan %p pchan %p", chan, pchan);
+ /* Match the put that the caller of ops->new_connection() performs
+ * once it is done with the returned channel pointer.
+ */
+ l2cap_chan_hold(chan);
+
return chan;
}
diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
index 95c65fece39b..fc663386872c 100644
--- a/net/bluetooth/l2cap_core.c
+++ b/net/bluetooth/l2cap_core.c
@@ -4071,6 +4071,9 @@ static void l2cap_connect(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd,
__l2cap_chan_add(conn, chan);
+ /* Drop the ops->new_connection() ref; conn list now pins chan. */
+ l2cap_chan_put(chan);
+
dcid = chan->scid;
__set_chan_timer(chan, chan->ops->get_sndtimeo(chan));
@@ -4978,6 +4981,9 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
__l2cap_chan_add(conn, chan);
+ /* Drop the ops->new_connection() ref; conn list now pins chan. */
+ l2cap_chan_put(chan);
+
l2cap_le_flowctl_init(chan, __le16_to_cpu(req->credits));
dcid = chan->scid;
@@ -5202,6 +5208,9 @@ static inline int l2cap_ecred_conn_req(struct l2cap_conn *conn,
__l2cap_chan_add(conn, chan);
+ /* Drop the ops->new_connection() ref; conn list now pins chan. */
+ l2cap_chan_put(chan);
+
l2cap_ecred_init(chan, __le16_to_cpu(req->credits));
/* Init response */
@@ -7402,6 +7411,9 @@ static void l2cap_connect_cfm(struct hci_conn *hcon, u8 status)
chan->dst_type = dst_type;
__l2cap_chan_add(conn, chan);
+
+ /* Drop the ops->new_connection() ref; conn list now pins chan. */
+ l2cap_chan_put(chan);
}
l2cap_chan_unlock(pchan);
diff --git a/net/bluetooth/l2cap_sock.c b/net/bluetooth/l2cap_sock.c
index 71e8c1b45bce..355fad9e2955 100644
--- a/net/bluetooth/l2cap_sock.c
+++ b/net/bluetooth/l2cap_sock.c
@@ -1497,6 +1497,7 @@ static void l2cap_sock_cleanup_listen(struct sock *parent)
static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
{
struct sock *sk, *parent = chan->data;
+ struct l2cap_chan *child_chan;
lock_sock(parent);
@@ -1520,9 +1521,19 @@ static struct l2cap_chan *l2cap_sock_new_connection_cb(struct l2cap_chan *chan)
bt_accept_enqueue(parent, sk, false);
+ child_chan = l2cap_pi(sk)->chan;
+
+ /* Pin the channel for the caller. Once release_sock(parent) returns,
+ * userspace can accept(2) and immediately close(2) the child socket,
+ * which would drop the socket's references on the channel and free
+ * it before the caller (e.g. l2cap_connect_req()) is done using the
+ * returned pointer. The matching put is the caller's responsibility.
+ */
+ l2cap_chan_hold(child_chan);
+
release_sock(parent);
- return l2cap_pi(sk)->chan;
+ return child_chan;
}
static int l2cap_sock_recv_cb(struct l2cap_chan *chan, struct sk_buff *skb)
diff --git a/net/bluetooth/smp.c b/net/bluetooth/smp.c
index 98f1da4f5f55..32761d3d252e 100644
--- a/net/bluetooth/smp.c
+++ b/net/bluetooth/smp.c
@@ -3261,6 +3261,11 @@ static inline struct l2cap_chan *smp_new_conn_cb(struct l2cap_chan *pchan)
BT_DBG("created chan %p", chan);
+ /* Match the put that the caller of ops->new_connection() performs
+ * once it is done with the returned channel pointer.
+ */
+ l2cap_chan_hold(chan);
+
return chan;
}
--
2.54.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
end of thread, other threads:[~2026-06-11 19:09 UTC | newest]
Thread overview: 20+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-03 15:06 [PATCH v9 0/1] Bluetooth: L2CAP: Fix use-after-free in l2cap_sock_new_connection_cb() Siwei Zhang
2026-06-03 15:06 ` [PATCH v9 1/1] " Siwei Zhang
2026-06-03 17:40 ` bluez.test.bot
2026-06-03 18:17 ` [PATCH v9 1/1] " Luiz Augusto von Dentz
2026-06-04 15:52 ` Siwei Zhang
2026-06-08 13:28 ` Siwei Zhang
2026-06-10 15:57 ` Luiz Augusto von Dentz
2026-06-10 16:32 ` Siwei Zhang
2026-06-10 16:59 ` Luiz Augusto von Dentz
2026-06-10 17:02 ` Siwei Zhang
2026-06-10 17:12 ` Luiz Augusto von Dentz
2026-06-10 17:13 ` Siwei Zhang
-- strict thread matches above, loose matches on Subject: below --
2026-06-11 15:08 [PATCH v10 " Siwei Zhang
2026-06-11 19:09 ` bluez.test.bot
2026-06-02 12:55 [PATCH v8 1/1] " Siwei Zhang
2026-06-02 15:27 ` bluez.test.bot
2026-06-01 14:03 [PATCH v7 1/1] " Siwei Zhang
2026-06-01 18:50 ` bluez.test.bot
2026-05-29 16:54 [PATCH v6 1/1] " Siwei Zhang
2026-05-29 18:21 ` bluez.test.bot
2026-05-20 16:20 [PATCH v5 1/1] " Siwei Zhang
2026-05-20 18:07 ` bluez.test.bot
2026-05-11 17:09 [PATCH RESEND v4 1/1] " Siwei Zhang
2026-05-11 18:49 ` bluez.test.bot
2026-05-11 4:51 [PATCH 1/1] " Siwei Zhang
2026-05-11 8:32 ` bluez.test.bot
2026-05-11 3:18 [PATCH 1/1] " Siwei Zhang
2026-05-11 4:21 ` 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