* [PATCH] Bluetooth: 6lowpan: Defer peer channel release until RCU cleanup
@ 2026-05-09 17:37 Zhang Cen
2026-05-10 1:43 ` Hillf Danton
2026-05-10 4:04 ` [PATCH v2] Bluetooth: 6lowpan: Fix peer and channel lifetime during teardown Zhang Cen
0 siblings, 2 replies; 8+ messages in thread
From: Zhang Cen @ 2026-05-09 17:37 UTC (permalink / raw)
To: Marcel Holtmann, Luiz Augusto von Dentz
Cc: linux-bluetooth, linux-kernel, zerocling0077, Zhang Cen
A 6LoWPAN peer stores the protocol-owned L2CAP channel reference in
peer->chan. chan_close_cb() removes the peer from the RCU list, but it
also drops that reference immediately. The peer object can still be seen
by in-flight RCU readers, and some paths keep using peer->chan after the
lookup has finished.
That lets transmit and disconnect paths dereference, lock, or send
through a channel whose last reference was released by the close path.
Defer the peer-owned l2cap_chan_put() until the peer RCU callback so a
peer that remains observable through RCU still carries a live channel
reference. Then take temporary channel references in the unicast,
multicast, and explicit disconnect paths before they keep using the
channel after the lookup window closes.
If the reader reaches step 3 after teardown reaches step 4, it can hit a
freed l2cap_chan.
The buggy scenario involves two paths, with each column showing the order within that path:
L2CAP peer teardown: Concurrent peer reader:
1. l2cap_conn_del() or another 1. A reader such as
close path takes a temporary __peer_lookup_conn(),
hold on the channel setup_header(),
send_mcast_pkt(), or
bt_6lowpan_disconnect()
traverses the peer list and
reads peer->chan
2. l2cap_chan_del() drops the 2. The reader keeps using the
connection-owned channel raw channel pointer after the
reference before 6LoWPAN lookup or after only RCU
close handling finishes protection remains
3. chan_close_cb() removes the 3. It dereferences channel
matching lowpan_peer from the fields or calls send or close
peer list operations through that
pointer
4. The original chan_close_cb()
also dropped the peer-owned
l2cap_chan reference, and the
close caller later released
its temporary hold
A peer reader can still observe the lowpan_peer while chan_close_cb()
has already consumed the peer-owned channel reference and the close
caller is releasing the last temporary hold, leaving peer->chan stale
before the peer's RCU grace period ends.
lowpan_peer objects stay RCU-visible after peer_del() removes them from
the list.
Signed-off-by: Zhang Cen <rollkingzzc@gmail.com>
---
diff --git a/net/bluetooth/6lowpan.c b/net/bluetooth/6lowpan.c
index 2f03b780b40d..ea3ee6929101 100644
--- a/net/bluetooth/6lowpan.c
+++ b/net/bluetooth/6lowpan.c
@@ -95,13 +95,20 @@ static inline void peer_add(struct lowpan_btle_dev *dev,
atomic_inc(&dev->peer_count);
}
+static void peer_free_rcu(struct rcu_head *rcu)
+{
+ struct lowpan_peer *peer = container_of(rcu, struct lowpan_peer, rcu);
+
+ l2cap_chan_put(peer->chan);
+ kfree(peer);
+ module_put(THIS_MODULE);
+}
+
static inline bool peer_del(struct lowpan_btle_dev *dev,
struct lowpan_peer *peer)
{
list_del_rcu(&peer->list);
- kfree_rcu(peer, rcu);
-
- module_put(THIS_MODULE);
+ call_rcu(&peer->rcu, peer_free_rcu);
if (atomic_dec_and_test(&dev->peer_count)) {
BT_DBG("last peer");
@@ -137,9 +144,32 @@ __peer_lookup_conn(struct lowpan_btle_dev *dev, struct l2cap_conn *conn)
return NULL;
}
-static inline struct lowpan_peer *peer_lookup_dst(struct lowpan_btle_dev *dev,
- struct in6_addr *daddr,
- struct sk_buff *skb)
+static struct l2cap_chan *lookup_peer_chan(struct l2cap_conn *conn)
+{
+ struct lowpan_btle_dev *entry;
+ struct lowpan_peer *peer;
+ struct l2cap_chan *chan = NULL;
+
+ rcu_read_lock();
+
+ list_for_each_entry_rcu(entry, &bt_6lowpan_devices, list) {
+ peer = __peer_lookup_conn(entry, conn);
+ if (peer) {
+ chan = peer->chan;
+ l2cap_chan_hold(chan);
+ break;
+ }
+ }
+
+ rcu_read_unlock();
+
+ return chan;
+}
+
+static int peer_lookup_dst(struct lowpan_btle_dev *dev, struct in6_addr *daddr,
+ struct sk_buff *skb, u8 *lladdr,
+ bdaddr_t *peer_addr, u8 *peer_addr_type,
+ struct l2cap_chan **chan)
{
struct rt6_info *rt = dst_rt6_info(skb_dst(skb));
int count = atomic_read(&dev->peer_count);
@@ -175,13 +205,20 @@ static inline struct lowpan_peer *peer_lookup_dst(struct lowpan_btle_dev *dev,
rcu_read_lock();
list_for_each_entry_rcu(peer, &dev->peers, list) {
+ struct l2cap_chan *pchan = peer->chan;
+
BT_DBG("dst addr %pMR dst type %u ip %pI6c",
- &peer->chan->dst, peer->chan->dst_type,
+ &pchan->dst, pchan->dst_type,
&peer->peer_addr);
if (!ipv6_addr_cmp(&peer->peer_addr, nexthop)) {
+ memcpy(lladdr, peer->lladdr, ETH_ALEN);
+ *peer_addr = pchan->dst;
+ *peer_addr_type = pchan->dst_type;
+ l2cap_chan_hold(pchan);
+ *chan = pchan;
rcu_read_unlock();
- return peer;
+ return 0;
}
}
@@ -190,9 +227,16 @@ static inline struct lowpan_peer *peer_lookup_dst(struct lowpan_btle_dev *dev,
if (neigh) {
list_for_each_entry_rcu(peer, &dev->peers, list) {
if (!memcmp(neigh->ha, peer->lladdr, ETH_ALEN)) {
+ struct l2cap_chan *pchan = peer->chan;
+
+ memcpy(lladdr, peer->lladdr, ETH_ALEN);
+ *peer_addr = pchan->dst;
+ *peer_addr_type = pchan->dst_type;
+ l2cap_chan_hold(pchan);
+ *chan = pchan;
neigh_release(neigh);
rcu_read_unlock();
- return peer;
+ return 0;
}
}
neigh_release(neigh);
@@ -200,7 +244,7 @@ static inline struct lowpan_peer *peer_lookup_dst(struct lowpan_btle_dev *dev,
rcu_read_unlock();
- return NULL;
+ return -ENOENT;
}
static struct lowpan_peer *lookup_peer(struct l2cap_conn *conn)
@@ -379,7 +423,7 @@ static int setup_header(struct sk_buff *skb, struct net_device *netdev,
struct in6_addr ipv6_daddr;
struct ipv6hdr *hdr;
struct lowpan_btle_dev *dev;
- struct lowpan_peer *peer;
+ u8 peer_lladdr[ETH_ALEN];
u8 *daddr;
int err, status = 0;
@@ -388,9 +432,9 @@ static int setup_header(struct sk_buff *skb, struct net_device *netdev,
dev = lowpan_btle_dev(netdev);
memcpy(&ipv6_daddr, &hdr->daddr, sizeof(ipv6_daddr));
+ lowpan_cb(skb)->chan = NULL;
if (ipv6_addr_is_multicast(&ipv6_daddr)) {
- lowpan_cb(skb)->chan = NULL;
daddr = NULL;
} else {
BT_DBG("dest IP %pI6c", &ipv6_daddr);
@@ -400,16 +444,15 @@ static int setup_header(struct sk_buff *skb, struct net_device *netdev,
* or user set route) so get peer according to
* the destination address.
*/
- peer = peer_lookup_dst(dev, &ipv6_daddr, skb);
- if (!peer) {
+ err = peer_lookup_dst(dev, &ipv6_daddr, skb, peer_lladdr,
+ peer_addr, peer_addr_type,
+ &lowpan_cb(skb)->chan);
+ if (err) {
BT_DBG("no such peer");
- return -ENOENT;
+ return err;
}
- daddr = peer->lladdr;
- *peer_addr = peer->chan->dst;
- *peer_addr_type = peer->chan->dst_type;
- lowpan_cb(skb)->chan = peer->chan;
+ daddr = peer_lladdr;
status = 1;
}
@@ -417,8 +460,13 @@ static int setup_header(struct sk_buff *skb, struct net_device *netdev,
lowpan_header_compress(skb, netdev, daddr, dev->netdev->dev_addr);
err = dev_hard_header(skb, netdev, ETH_P_IPV6, NULL, NULL, 0);
- if (err < 0)
+ if (err < 0) {
+ if (lowpan_cb(skb)->chan) {
+ l2cap_chan_put(lowpan_cb(skb)->chan);
+ lowpan_cb(skb)->chan = NULL;
+ }
return err;
+ }
return status;
}
@@ -483,15 +531,23 @@ static int send_mcast_pkt(struct sk_buff *skb, struct net_device *netdev)
dev = lowpan_btle_dev(entry->netdev);
list_for_each_entry_rcu(pentry, &dev->peers, list) {
+ struct l2cap_chan *chan = pentry->chan;
int ret;
local_skb = skb_clone(skb, GFP_ATOMIC);
+ if (!local_skb) {
+ err = -ENOMEM;
+ continue;
+ }
+
+ l2cap_chan_hold(chan);
BT_DBG("xmit %s to %pMR type %u IP %pI6c chan %p",
netdev->name,
- &pentry->chan->dst, pentry->chan->dst_type,
- &pentry->peer_addr, pentry->chan);
- ret = send_pkt(pentry->chan, local_skb, netdev);
+ &chan->dst, chan->dst_type,
+ &pentry->peer_addr, chan);
+ ret = send_pkt(chan, local_skb, netdev);
+ l2cap_chan_put(chan);
if (ret < 0)
err = ret;
@@ -530,10 +586,14 @@ static netdev_tx_t bt_xmit(struct sk_buff *skb, struct net_device *netdev)
if (err) {
if (lowpan_cb(skb)->chan) {
+ struct l2cap_chan *chan = lowpan_cb(skb)->chan;
+
BT_DBG("xmit %s to %pMR type %u IP %pI6c chan %p",
netdev->name, &addr, addr_type,
- &lowpan_cb(skb)->addr, lowpan_cb(skb)->chan);
- err = send_pkt(lowpan_cb(skb)->chan, skb, netdev);
+ &lowpan_cb(skb)->addr, chan);
+ err = send_pkt(chan, skb, netdev);
+ l2cap_chan_put(chan);
+ lowpan_cb(skb)->chan = NULL;
} else {
err = -ENOENT;
}
@@ -802,8 +862,6 @@ static void chan_close_cb(struct l2cap_chan *chan)
last ? "last " : "1 ", peer);
BT_DBG("chan %p orig refcnt %u", chan,
kref_read(&chan->kref));
-
- l2cap_chan_put(chan);
break;
}
}
@@ -917,19 +975,20 @@ static int bt_6lowpan_connect(bdaddr_t *addr, u8 dst_type)
static int bt_6lowpan_disconnect(struct l2cap_conn *conn, u8 dst_type)
{
- struct lowpan_peer *peer;
+ struct l2cap_chan *chan;
BT_DBG("conn %p dst type %u", conn, dst_type);
- peer = lookup_peer(conn);
- if (!peer)
+ chan = lookup_peer_chan(conn);
+ if (!chan)
return -ENOENT;
- BT_DBG("peer %p chan %p", peer, peer->chan);
+ BT_DBG("chan %p", chan);
- l2cap_chan_lock(peer->chan);
- l2cap_chan_close(peer->chan, ENOENT);
- l2cap_chan_unlock(peer->chan);
+ l2cap_chan_lock(chan);
+ l2cap_chan_close(chan, ENOENT);
+ l2cap_chan_unlock(chan);
+ l2cap_chan_put(chan);
return 0;
}
^ permalink raw reply related [flat|nested] 8+ messages in thread
* Re: [PATCH] Bluetooth: 6lowpan: Defer peer channel release until RCU cleanup
2026-05-09 17:37 [PATCH] Bluetooth: 6lowpan: Defer peer channel release until RCU cleanup Zhang Cen
@ 2026-05-10 1:43 ` Hillf Danton
2026-05-10 4:01 ` c Z
2026-05-10 4:04 ` [PATCH v2] Bluetooth: 6lowpan: Fix peer and channel lifetime during teardown Zhang Cen
1 sibling, 1 reply; 8+ messages in thread
From: Hillf Danton @ 2026-05-10 1:43 UTC (permalink / raw)
To: Zhang Cen
Cc: Marcel Holtmann, Luiz Augusto von Dentz, linux-bluetooth,
linux-kernel, zerocling0077
On Sun, 10 May 2026 01:37:45 +0800 Zhang Cen wrote:
> A 6LoWPAN peer stores the protocol-owned L2CAP channel reference in
> peer->chan. chan_close_cb() removes the peer from the RCU list, but it
> also drops that reference immediately. The peer object can still be seen
> by in-flight RCU readers, and some paths keep using peer->chan after the
> lookup has finished.
>
> That lets transmit and disconnect paths dereference, lock, or send
> through a channel whose last reference was released by the close path.
>
Sounds like the race between close and lookup paths
chan_close_cb() setup_header()
--- ---
peer_del()
list_del_rcu(&peer->list);
kfree_rcu(peer, rcu);
l2cap_chan_put(c); // free chan
peer_lookup_dst(dev, &ipv6_daddr, skb);
rcu_read_lock();
peer = find peer;
rcu_read_unlock();
if (peer)
addr = peer->chan->dst; //uaf
and on the lookup side, a) peer is unsafe outside rcu lock and
b) chan is used after put.
To fix the race, in addition to move l2cap_chan_put() to the rcu
callback as this patch does, refcount should be added to peer to
make the lookup path safe.
chan_close_cb() setup_header()
--- ---
peer_del()
list_del_rcu(&peer->list);
call_rcu(&peer->rcu, peer_free_rcu);
if (peer->refcount-- == 0) {
//alternatively schedule workqueue if task context needed
l2cap_chan_put(c);
kfree(peer);
module_put(THIS_MODULE);
}
peer_lookup_dst(dev, &ipv6_daddr, skb);
rcu_read_lock();
peer = find peer;
if (peer)
peer->refcount++;
rcu_read_unlock();
if (peer) {
addr = peer->chan->dst;
if (peer->refcount-- == 0)
schedule workqueue to free peer;
}
> Defer the peer-owned l2cap_chan_put() until the peer RCU callback so a
> peer that remains observable through RCU still carries a live channel
> reference. Then take temporary channel references in the unicast,
> multicast, and explicit disconnect paths before they keep using the
> channel after the lookup window closes.
>
> If the reader reaches step 3 after teardown reaches step 4, it can hit a
> freed l2cap_chan.
>
> The buggy scenario involves two paths, with each column showing the order within that path:
>
> L2CAP peer teardown: Concurrent peer reader:
> 1. l2cap_conn_del() or another 1. A reader such as
> close path takes a temporary __peer_lookup_conn(),
> hold on the channel setup_header(),
> send_mcast_pkt(), or
> bt_6lowpan_disconnect()
> traverses the peer list and
> reads peer->chan
> 2. l2cap_chan_del() drops the 2. The reader keeps using the
> connection-owned channel raw channel pointer after the
> reference before 6LoWPAN lookup or after only RCU
> close handling finishes protection remains
> 3. chan_close_cb() removes the 3. It dereferences channel
> matching lowpan_peer from the fields or calls send or close
> peer list operations through that
> pointer
> 4. The original chan_close_cb()
> also dropped the peer-owned
> l2cap_chan reference, and the
> close caller later released
> its temporary hold
>
> A peer reader can still observe the lowpan_peer while chan_close_cb()
> has already consumed the peer-owned channel reference and the close
> caller is releasing the last temporary hold, leaving peer->chan stale
> before the peer's RCU grace period ends.
>
> lowpan_peer objects stay RCU-visible after peer_del() removes them from
> the list.
>
> Signed-off-by: Zhang Cen <rollkingzzc@gmail.com>
>
> ---
> diff --git a/net/bluetooth/6lowpan.c b/net/bluetooth/6lowpan.c
> index 2f03b780b40d..ea3ee6929101 100644
> --- a/net/bluetooth/6lowpan.c
> +++ b/net/bluetooth/6lowpan.c
> @@ -95,13 +95,20 @@ static inline void peer_add(struct lowpan_btle_dev *dev,
> atomic_inc(&dev->peer_count);
> }
>
> +static void peer_free_rcu(struct rcu_head *rcu)
> +{
> + struct lowpan_peer *peer = container_of(rcu, struct lowpan_peer, rcu);
> +
> + l2cap_chan_put(peer->chan);
> + kfree(peer);
> + module_put(THIS_MODULE);
> +}
> +
> static inline bool peer_del(struct lowpan_btle_dev *dev,
> struct lowpan_peer *peer)
> {
> list_del_rcu(&peer->list);
> - kfree_rcu(peer, rcu);
> -
> - module_put(THIS_MODULE);
> + call_rcu(&peer->rcu, peer_free_rcu);
>
> if (atomic_dec_and_test(&dev->peer_count)) {
> BT_DBG("last peer");
> @@ -137,9 +144,32 @@ __peer_lookup_conn(struct lowpan_btle_dev *dev, struct l2cap_conn *conn)
> return NULL;
> }
>
> -static inline struct lowpan_peer *peer_lookup_dst(struct lowpan_btle_dev *dev,
> - struct in6_addr *daddr,
> - struct sk_buff *skb)
> +static struct l2cap_chan *lookup_peer_chan(struct l2cap_conn *conn)
> +{
> + struct lowpan_btle_dev *entry;
> + struct lowpan_peer *peer;
> + struct l2cap_chan *chan = NULL;
> +
> + rcu_read_lock();
> +
> + list_for_each_entry_rcu(entry, &bt_6lowpan_devices, list) {
> + peer = __peer_lookup_conn(entry, conn);
> + if (peer) {
> + chan = peer->chan;
> + l2cap_chan_hold(chan);
> + break;
> + }
> + }
> +
> + rcu_read_unlock();
> +
> + return chan;
> +}
> +
> +static int peer_lookup_dst(struct lowpan_btle_dev *dev, struct in6_addr *daddr,
> + struct sk_buff *skb, u8 *lladdr,
> + bdaddr_t *peer_addr, u8 *peer_addr_type,
> + struct l2cap_chan **chan)
> {
> struct rt6_info *rt = dst_rt6_info(skb_dst(skb));
> int count = atomic_read(&dev->peer_count);
> @@ -175,13 +205,20 @@ static inline struct lowpan_peer *peer_lookup_dst(struct lowpan_btle_dev *dev,
> rcu_read_lock();
>
> list_for_each_entry_rcu(peer, &dev->peers, list) {
> + struct l2cap_chan *pchan = peer->chan;
> +
> BT_DBG("dst addr %pMR dst type %u ip %pI6c",
> - &peer->chan->dst, peer->chan->dst_type,
> + &pchan->dst, pchan->dst_type,
> &peer->peer_addr);
>
> if (!ipv6_addr_cmp(&peer->peer_addr, nexthop)) {
> + memcpy(lladdr, peer->lladdr, ETH_ALEN);
> + *peer_addr = pchan->dst;
> + *peer_addr_type = pchan->dst_type;
> + l2cap_chan_hold(pchan);
> + *chan = pchan;
> rcu_read_unlock();
> - return peer;
> + return 0;
> }
> }
>
> @@ -190,9 +227,16 @@ static inline struct lowpan_peer *peer_lookup_dst(struct lowpan_btle_dev *dev,
> if (neigh) {
> list_for_each_entry_rcu(peer, &dev->peers, list) {
> if (!memcmp(neigh->ha, peer->lladdr, ETH_ALEN)) {
> + struct l2cap_chan *pchan = peer->chan;
> +
> + memcpy(lladdr, peer->lladdr, ETH_ALEN);
> + *peer_addr = pchan->dst;
> + *peer_addr_type = pchan->dst_type;
> + l2cap_chan_hold(pchan);
> + *chan = pchan;
> neigh_release(neigh);
> rcu_read_unlock();
> - return peer;
> + return 0;
> }
> }
> neigh_release(neigh);
> @@ -200,7 +244,7 @@ static inline struct lowpan_peer *peer_lookup_dst(struct lowpan_btle_dev *dev,
>
> rcu_read_unlock();
>
> - return NULL;
> + return -ENOENT;
> }
>
> static struct lowpan_peer *lookup_peer(struct l2cap_conn *conn)
> @@ -379,7 +423,7 @@ static int setup_header(struct sk_buff *skb, struct net_device *netdev,
> struct in6_addr ipv6_daddr;
> struct ipv6hdr *hdr;
> struct lowpan_btle_dev *dev;
> - struct lowpan_peer *peer;
> + u8 peer_lladdr[ETH_ALEN];
> u8 *daddr;
> int err, status = 0;
>
> @@ -388,9 +432,9 @@ static int setup_header(struct sk_buff *skb, struct net_device *netdev,
> dev = lowpan_btle_dev(netdev);
>
> memcpy(&ipv6_daddr, &hdr->daddr, sizeof(ipv6_daddr));
> + lowpan_cb(skb)->chan = NULL;
>
> if (ipv6_addr_is_multicast(&ipv6_daddr)) {
> - lowpan_cb(skb)->chan = NULL;
> daddr = NULL;
> } else {
> BT_DBG("dest IP %pI6c", &ipv6_daddr);
> @@ -400,16 +444,15 @@ static int setup_header(struct sk_buff *skb, struct net_device *netdev,
> * or user set route) so get peer according to
> * the destination address.
> */
> - peer = peer_lookup_dst(dev, &ipv6_daddr, skb);
> - if (!peer) {
> + err = peer_lookup_dst(dev, &ipv6_daddr, skb, peer_lladdr,
> + peer_addr, peer_addr_type,
> + &lowpan_cb(skb)->chan);
> + if (err) {
> BT_DBG("no such peer");
> - return -ENOENT;
> + return err;
> }
>
> - daddr = peer->lladdr;
> - *peer_addr = peer->chan->dst;
> - *peer_addr_type = peer->chan->dst_type;
> - lowpan_cb(skb)->chan = peer->chan;
> + daddr = peer_lladdr;
>
> status = 1;
> }
> @@ -417,8 +460,13 @@ static int setup_header(struct sk_buff *skb, struct net_device *netdev,
> lowpan_header_compress(skb, netdev, daddr, dev->netdev->dev_addr);
>
> err = dev_hard_header(skb, netdev, ETH_P_IPV6, NULL, NULL, 0);
> - if (err < 0)
> + if (err < 0) {
> + if (lowpan_cb(skb)->chan) {
> + l2cap_chan_put(lowpan_cb(skb)->chan);
> + lowpan_cb(skb)->chan = NULL;
> + }
> return err;
> + }
>
> return status;
> }
> @@ -483,15 +531,23 @@ static int send_mcast_pkt(struct sk_buff *skb, struct net_device *netdev)
> dev = lowpan_btle_dev(entry->netdev);
>
> list_for_each_entry_rcu(pentry, &dev->peers, list) {
> + struct l2cap_chan *chan = pentry->chan;
> int ret;
>
> local_skb = skb_clone(skb, GFP_ATOMIC);
> + if (!local_skb) {
> + err = -ENOMEM;
> + continue;
> + }
> +
> + l2cap_chan_hold(chan);
>
> BT_DBG("xmit %s to %pMR type %u IP %pI6c chan %p",
> netdev->name,
> - &pentry->chan->dst, pentry->chan->dst_type,
> - &pentry->peer_addr, pentry->chan);
> - ret = send_pkt(pentry->chan, local_skb, netdev);
> + &chan->dst, chan->dst_type,
> + &pentry->peer_addr, chan);
> + ret = send_pkt(chan, local_skb, netdev);
> + l2cap_chan_put(chan);
> if (ret < 0)
> err = ret;
>
> @@ -530,10 +586,14 @@ static netdev_tx_t bt_xmit(struct sk_buff *skb, struct net_device *netdev)
>
> if (err) {
> if (lowpan_cb(skb)->chan) {
> + struct l2cap_chan *chan = lowpan_cb(skb)->chan;
> +
> BT_DBG("xmit %s to %pMR type %u IP %pI6c chan %p",
> netdev->name, &addr, addr_type,
> - &lowpan_cb(skb)->addr, lowpan_cb(skb)->chan);
> - err = send_pkt(lowpan_cb(skb)->chan, skb, netdev);
> + &lowpan_cb(skb)->addr, chan);
> + err = send_pkt(chan, skb, netdev);
> + l2cap_chan_put(chan);
> + lowpan_cb(skb)->chan = NULL;
> } else {
> err = -ENOENT;
> }
> @@ -802,8 +862,6 @@ static void chan_close_cb(struct l2cap_chan *chan)
> last ? "last " : "1 ", peer);
> BT_DBG("chan %p orig refcnt %u", chan,
> kref_read(&chan->kref));
> -
> - l2cap_chan_put(chan);
> break;
> }
> }
> @@ -917,19 +975,20 @@ static int bt_6lowpan_connect(bdaddr_t *addr, u8 dst_type)
>
> static int bt_6lowpan_disconnect(struct l2cap_conn *conn, u8 dst_type)
> {
> - struct lowpan_peer *peer;
> + struct l2cap_chan *chan;
>
> BT_DBG("conn %p dst type %u", conn, dst_type);
>
> - peer = lookup_peer(conn);
> - if (!peer)
> + chan = lookup_peer_chan(conn);
> + if (!chan)
> return -ENOENT;
>
> - BT_DBG("peer %p chan %p", peer, peer->chan);
> + BT_DBG("chan %p", chan);
>
> - l2cap_chan_lock(peer->chan);
> - l2cap_chan_close(peer->chan, ENOENT);
> - l2cap_chan_unlock(peer->chan);
> + l2cap_chan_lock(chan);
> + l2cap_chan_close(chan, ENOENT);
> + l2cap_chan_unlock(chan);
> + l2cap_chan_put(chan);
>
> return 0;
> }
>
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH] Bluetooth: 6lowpan: Defer peer channel release until RCU cleanup
2026-05-10 1:43 ` Hillf Danton
@ 2026-05-10 4:01 ` c Z
0 siblings, 0 replies; 8+ messages in thread
From: c Z @ 2026-05-10 4:01 UTC (permalink / raw)
To: Hillf Danton
Cc: Marcel Holtmann, Luiz Augusto von Dentz, linux-bluetooth,
linux-kernel, zerocling0077
Hillf Danton <hdanton@sina.com> 于2026年5月10日周日 09:44写道:
>
> On Sun, 10 May 2026 01:37:45 +0800 Zhang Cen wrote:
> > A 6LoWPAN peer stores the protocol-owned L2CAP channel reference in
> > peer->chan. chan_close_cb() removes the peer from the RCU list, but it
> > also drops that reference immediately. The peer object can still be seen
> > by in-flight RCU readers, and some paths keep using peer->chan after the
> > lookup has finished.
> >
> > That lets transmit and disconnect paths dereference, lock, or send
> > through a channel whose last reference was released by the close path.
> >
> Sounds like the race between close and lookup paths
>
> chan_close_cb() setup_header()
> --- ---
> peer_del()
> list_del_rcu(&peer->list);
> kfree_rcu(peer, rcu);
> l2cap_chan_put(c); // free chan
>
> peer_lookup_dst(dev, &ipv6_daddr, skb);
> rcu_read_lock();
> peer = find peer;
> rcu_read_unlock();
> if (peer)
> addr = peer->chan->dst; //uaf
>
> and on the lookup side, a) peer is unsafe outside rcu lock and
> b) chan is used after put.
>
> To fix the race, in addition to move l2cap_chan_put() to the rcu
> callback as this patch does, refcount should be added to peer to
> make the lookup path safe.
>
> chan_close_cb() setup_header()
> --- ---
> peer_del()
> list_del_rcu(&peer->list);
> call_rcu(&peer->rcu, peer_free_rcu);
> if (peer->refcount-- == 0) {
> //alternatively schedule workqueue if task context needed
> l2cap_chan_put(c);
> kfree(peer);
> module_put(THIS_MODULE);
> }
> peer_lookup_dst(dev, &ipv6_daddr, skb);
> rcu_read_lock();
> peer = find peer;
> if (peer)
> peer->refcount++;
> rcu_read_unlock();
> if (peer) {
> addr = peer->chan->dst;
> if (peer->refcount-- == 0)
> schedule workqueue to free peer;
> }
>
Hi Hillf,
Thanks for looking at this and for spelling out the lookup-side lifetime
issue.
I agree that v2 should make the peer lifetime explicit. v1 moves the
peer-owned l2cap_chan_put() behind the RCU grace period, and it also
changes the unicast transmit lookup to copy the peer fields / take a
channel reference before dropping RCU. But the broader point still
stands: helpers such as lookup_peer() should not hand a raw lowpan_peer
to users that continue after rcu_read_unlock().
I'll prepare v2 so the peer object has its own refcount. The plan is to:
* add a refcount/kref to struct lowpan_peer and initialize it with the
peer-list owner reference;
* make RCU lookups that return a peer acquire a reference before
unlocking, using the usual non-zero refcount pattern;
* make peer_del() only unlink the peer and schedule an RCU callback
which drops the owner reference after the grace period;
* release the peer-owned L2CAP channel reference and module reference
from the final peer put; and
* audit the remaining lookup_peer()/peer_lookup_dst() users so every
post-lookup use either holds a peer reference or only uses data copied
while still under RCU.
Thanks,
Zhang Cen
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v2] Bluetooth: 6lowpan: Fix peer and channel lifetime during teardown
2026-05-09 17:37 [PATCH] Bluetooth: 6lowpan: Defer peer channel release until RCU cleanup Zhang Cen
2026-05-10 1:43 ` Hillf Danton
@ 2026-05-10 4:04 ` Zhang Cen
2026-05-11 16:20 ` Luiz Augusto von Dentz
2026-05-11 17:18 ` [PATCH v3] " Zhang Cen
1 sibling, 2 replies; 8+ messages in thread
From: Zhang Cen @ 2026-05-10 4:04 UTC (permalink / raw)
To: Marcel Holtmann, Luiz Augusto von Dentz
Cc: linux-bluetooth, linux-kernel, Hillf Danton, zerocling0077,
hanguidong02, Zhang Cen
6LoWPAN peers keep a protocol-owned L2CAP channel reference in
peer->chan. chan_close_cb() removes the peer from the RCU list when the
channel is closed, but the old code also dropped that channel reference
before the peer object stopped being observable by in-flight RCU readers.
That can leave peer readers with a live lowpan_peer that still points at
an l2cap_chan whose last reference has already been released. Some
lookup helpers also returned raw peer pointers after dropping RCU
protection, so keeping only the channel alive until the RCU callback
still leaves the peer lifetime rules implicit.
Make the peer lifetime explicit. Give each lowpan_peer a refcount, have
RCU lookups take a peer reference before returning a peer outside the
read-side critical section, and make peer_del() only unlink the peer and
schedule an RCU callback to drop the list owner reference after the grace
period. Release the peer-owned L2CAP channel reference and module
reference from the final peer put.
Keep temporary channel references in transmit and disconnect paths that
cache or send through peer->chan after lookup.
The buggy scenario involves two paths, with each column showing the order within that path:
L2CAP peer teardown: Concurrent peer reader:
1. A close path holds a 1. A reader such as
temporary reference to the lookup_peer(),
channel peer_lookup_dst(),
send_mcast_pkt(), or
bt_6lowpan_disconnect()
observes the peer
2. l2cap_chan_del() drops the 2. The reader keeps using the
connection-owned channel raw peer or peer->chan after
reference RCU lookup protection ends
3. chan_close_cb() removes the 3. The reader reads channel
lowpan_peer from the peer fields, locks the channel, or
list sends through the channel
pointer
4. The old chan_close_cb()
dropped the peer-owned
channel reference before the
peer RCU grace period ended
5. The close caller releases its
temporary channel reference
If the reader reaches its channel use after teardown releases the peer-
owned channel reference and the close caller releases its temporary
reference, it can use a freed channel; raw peer returns can also outlive
the peer RCU callback.
lowpan_peer objects remain RCU-observable after peer_del() unlinks them.
Signed-off-by: Zhang Cen <rollkingzzc@gmail.com>
---
diff --git a/net/bluetooth/6lowpan.c b/net/bluetooth/6lowpan.c
index 2f03b780b40d..7334a78f5bfb 100644
--- a/net/bluetooth/6lowpan.c
+++ b/net/bluetooth/6lowpan.c
@@ -9,6 +9,7 @@
#include <linux/etherdevice.h>
#include <linux/module.h>
#include <linux/debugfs.h>
+#include <linux/refcount.h>
#include <net/ipv6.h>
#include <net/ip6_route.h>
@@ -61,6 +62,7 @@ enum {
struct lowpan_peer {
struct list_head list;
struct rcu_head rcu;
+ refcount_t refcnt;
struct l2cap_chan *chan;
/* peer addresses in various formats */
@@ -95,13 +97,33 @@ static inline void peer_add(struct lowpan_btle_dev *dev,
atomic_inc(&dev->peer_count);
}
+static inline bool lowpan_peer_get_unless_zero(struct lowpan_peer *peer)
+{
+ return refcount_inc_not_zero(&peer->refcnt);
+}
+
+static void lowpan_peer_put(struct lowpan_peer *peer)
+{
+ if (!refcount_dec_and_test(&peer->refcnt))
+ return;
+
+ l2cap_chan_put(peer->chan);
+ kfree(peer);
+ module_put(THIS_MODULE);
+}
+
+static void lowpan_peer_put_rcu(struct rcu_head *rcu)
+{
+ struct lowpan_peer *peer = container_of(rcu, struct lowpan_peer, rcu);
+
+ lowpan_peer_put(peer);
+}
+
static inline bool peer_del(struct lowpan_btle_dev *dev,
struct lowpan_peer *peer)
{
list_del_rcu(&peer->list);
- kfree_rcu(peer, rcu);
-
- module_put(THIS_MODULE);
+ call_rcu(&peer->rcu, lowpan_peer_put_rcu);
if (atomic_dec_and_test(&dev->peer_count)) {
BT_DBG("last peer");
@@ -179,7 +201,8 @@ static inline struct lowpan_peer *peer_lookup_dst(struct lowpan_btle_dev *dev,
&peer->chan->dst, peer->chan->dst_type,
&peer->peer_addr);
- if (!ipv6_addr_cmp(&peer->peer_addr, nexthop)) {
+ if (!ipv6_addr_cmp(&peer->peer_addr, nexthop) &&
+ lowpan_peer_get_unless_zero(peer)) {
rcu_read_unlock();
return peer;
}
@@ -189,7 +212,8 @@ static inline struct lowpan_peer *peer_lookup_dst(struct lowpan_btle_dev *dev,
neigh = __ipv6_neigh_lookup(dev->netdev, nexthop);
if (neigh) {
list_for_each_entry_rcu(peer, &dev->peers, list) {
- if (!memcmp(neigh->ha, peer->lladdr, ETH_ALEN)) {
+ if (!memcmp(neigh->ha, peer->lladdr, ETH_ALEN) &&
+ lowpan_peer_get_unless_zero(peer)) {
neigh_release(neigh);
rcu_read_unlock();
return peer;
@@ -212,8 +236,9 @@ static struct lowpan_peer *lookup_peer(struct l2cap_conn *conn)
list_for_each_entry_rcu(entry, &bt_6lowpan_devices, list) {
peer = __peer_lookup_conn(entry, conn);
- if (peer)
+ if (peer && lowpan_peer_get_unless_zero(peer))
break;
+ peer = NULL;
}
rcu_read_unlock();
@@ -361,8 +386,10 @@ static int chan_recv_cb(struct l2cap_chan *chan, struct sk_buff *skb)
return -ENOENT;
dev = lookup_dev(chan->conn);
- if (!dev || !dev->netdev)
+ if (!dev || !dev->netdev) {
+ lowpan_peer_put(peer);
return -ENOENT;
+ }
err = recv_pkt(skb, dev->netdev, peer);
if (err) {
@@ -370,6 +397,8 @@ static int chan_recv_cb(struct l2cap_chan *chan, struct sk_buff *skb)
err = -EAGAIN;
}
+ lowpan_peer_put(peer);
+
return err;
}
@@ -380,6 +409,8 @@ static int setup_header(struct sk_buff *skb, struct net_device *netdev,
struct ipv6hdr *hdr;
struct lowpan_btle_dev *dev;
struct lowpan_peer *peer;
+ struct l2cap_chan *chan;
+ u8 peer_lladdr[ETH_ALEN];
u8 *daddr;
int err, status = 0;
@@ -388,9 +419,9 @@ static int setup_header(struct sk_buff *skb, struct net_device *netdev,
dev = lowpan_btle_dev(netdev);
memcpy(&ipv6_daddr, &hdr->daddr, sizeof(ipv6_daddr));
+ lowpan_cb(skb)->chan = NULL;
if (ipv6_addr_is_multicast(&ipv6_daddr)) {
- lowpan_cb(skb)->chan = NULL;
daddr = NULL;
} else {
BT_DBG("dest IP %pI6c", &ipv6_daddr);
@@ -406,10 +437,15 @@ static int setup_header(struct sk_buff *skb, struct net_device *netdev,
return -ENOENT;
}
- daddr = peer->lladdr;
- *peer_addr = peer->chan->dst;
- *peer_addr_type = peer->chan->dst_type;
- lowpan_cb(skb)->chan = peer->chan;
+ chan = peer->chan;
+ l2cap_chan_hold(chan);
+ memcpy(peer_lladdr, peer->lladdr, ETH_ALEN);
+ *peer_addr = chan->dst;
+ *peer_addr_type = chan->dst_type;
+ lowpan_cb(skb)->chan = chan;
+ lowpan_peer_put(peer);
+
+ daddr = peer_lladdr;
status = 1;
}
@@ -417,8 +453,13 @@ static int setup_header(struct sk_buff *skb, struct net_device *netdev,
lowpan_header_compress(skb, netdev, daddr, dev->netdev->dev_addr);
err = dev_hard_header(skb, netdev, ETH_P_IPV6, NULL, NULL, 0);
- if (err < 0)
+ if (err < 0) {
+ if (lowpan_cb(skb)->chan) {
+ l2cap_chan_put(lowpan_cb(skb)->chan);
+ lowpan_cb(skb)->chan = NULL;
+ }
return err;
+ }
return status;
}
@@ -483,15 +524,23 @@ static int send_mcast_pkt(struct sk_buff *skb, struct net_device *netdev)
dev = lowpan_btle_dev(entry->netdev);
list_for_each_entry_rcu(pentry, &dev->peers, list) {
+ struct l2cap_chan *chan = pentry->chan;
int ret;
local_skb = skb_clone(skb, GFP_ATOMIC);
+ if (!local_skb) {
+ err = -ENOMEM;
+ continue;
+ }
+
+ l2cap_chan_hold(chan);
BT_DBG("xmit %s to %pMR type %u IP %pI6c chan %p",
netdev->name,
- &pentry->chan->dst, pentry->chan->dst_type,
- &pentry->peer_addr, pentry->chan);
- ret = send_pkt(pentry->chan, local_skb, netdev);
+ &chan->dst, chan->dst_type,
+ &pentry->peer_addr, chan);
+ ret = send_pkt(chan, local_skb, netdev);
+ l2cap_chan_put(chan);
if (ret < 0)
err = ret;
@@ -530,10 +579,14 @@ static netdev_tx_t bt_xmit(struct sk_buff *skb, struct net_device *netdev)
if (err) {
if (lowpan_cb(skb)->chan) {
+ struct l2cap_chan *chan = lowpan_cb(skb)->chan;
+
BT_DBG("xmit %s to %pMR type %u IP %pI6c chan %p",
netdev->name, &addr, addr_type,
- &lowpan_cb(skb)->addr, lowpan_cb(skb)->chan);
- err = send_pkt(lowpan_cb(skb)->chan, skb, netdev);
+ &lowpan_cb(skb)->addr, chan);
+ err = send_pkt(chan, skb, netdev);
+ l2cap_chan_put(chan);
+ lowpan_cb(skb)->chan = NULL;
} else {
err = -ENOENT;
}
@@ -649,6 +702,7 @@ static struct l2cap_chan *add_peer_chan(struct l2cap_chan *chan,
if (!peer)
return NULL;
+ refcount_set(&peer->refcnt, 1);
peer->chan = chan;
baswap((void *)peer->lladdr, &chan->dst);
@@ -802,8 +856,6 @@ static void chan_close_cb(struct l2cap_chan *chan)
last ? "last " : "1 ", peer);
BT_DBG("chan %p orig refcnt %u", chan,
kref_read(&chan->kref));
-
- l2cap_chan_put(chan);
break;
}
}
@@ -918,6 +970,7 @@ static int bt_6lowpan_connect(bdaddr_t *addr, u8 dst_type)
static int bt_6lowpan_disconnect(struct l2cap_conn *conn, u8 dst_type)
{
struct lowpan_peer *peer;
+ struct l2cap_chan *chan;
BT_DBG("conn %p dst type %u", conn, dst_type);
@@ -925,11 +978,16 @@ static int bt_6lowpan_disconnect(struct l2cap_conn *conn, u8 dst_type)
if (!peer)
return -ENOENT;
- BT_DBG("peer %p chan %p", peer, peer->chan);
+ chan = peer->chan;
+ l2cap_chan_hold(chan);
+
+ BT_DBG("peer %p chan %p", peer, chan);
+ lowpan_peer_put(peer);
- l2cap_chan_lock(peer->chan);
- l2cap_chan_close(peer->chan, ENOENT);
- l2cap_chan_unlock(peer->chan);
+ l2cap_chan_lock(chan);
+ l2cap_chan_close(chan, ENOENT);
+ l2cap_chan_unlock(chan);
+ l2cap_chan_put(chan);
return 0;
}
@@ -1169,6 +1227,7 @@ static ssize_t lowpan_control_write(struct file *fp,
peer = lookup_peer(conn);
if (peer) {
BT_DBG("6LoWPAN connection already exists");
+ lowpan_peer_put(peer);
return -EALREADY;
}
^ permalink raw reply related [flat|nested] 8+ messages in thread
* Re: [PATCH v2] Bluetooth: 6lowpan: Fix peer and channel lifetime during teardown
2026-05-10 4:04 ` [PATCH v2] Bluetooth: 6lowpan: Fix peer and channel lifetime during teardown Zhang Cen
@ 2026-05-11 16:20 ` Luiz Augusto von Dentz
2026-05-11 17:11 ` Cen Zhang
2026-05-11 17:18 ` [PATCH v3] " Zhang Cen
1 sibling, 1 reply; 8+ messages in thread
From: Luiz Augusto von Dentz @ 2026-05-11 16:20 UTC (permalink / raw)
To: Zhang Cen
Cc: Marcel Holtmann, linux-bluetooth, linux-kernel, Hillf Danton,
zerocling0077, hanguidong02
Hi,
On Sun, May 10, 2026 at 12:04 AM Zhang Cen <rollkingzzc@gmail.com> wrote:
>
> 6LoWPAN peers keep a protocol-owned L2CAP channel reference in
> peer->chan. chan_close_cb() removes the peer from the RCU list when the
> channel is closed, but the old code also dropped that channel reference
> before the peer object stopped being observable by in-flight RCU readers.
>
> That can leave peer readers with a live lowpan_peer that still points at
> an l2cap_chan whose last reference has already been released. Some
> lookup helpers also returned raw peer pointers after dropping RCU
> protection, so keeping only the channel alive until the RCU callback
> still leaves the peer lifetime rules implicit.
>
> Make the peer lifetime explicit. Give each lowpan_peer a refcount, have
> RCU lookups take a peer reference before returning a peer outside the
> read-side critical section, and make peer_del() only unlink the peer and
> schedule an RCU callback to drop the list owner reference after the grace
> period. Release the peer-owned L2CAP channel reference and module
> reference from the final peer put.
>
> Keep temporary channel references in transmit and disconnect paths that
> cache or send through peer->chan after lookup.
>
> The buggy scenario involves two paths, with each column showing the order within that path:
>
> L2CAP peer teardown: Concurrent peer reader:
> 1. A close path holds a 1. A reader such as
> temporary reference to the lookup_peer(),
> channel peer_lookup_dst(),
> send_mcast_pkt(), or
> bt_6lowpan_disconnect()
> observes the peer
> 2. l2cap_chan_del() drops the 2. The reader keeps using the
> connection-owned channel raw peer or peer->chan after
> reference RCU lookup protection ends
> 3. chan_close_cb() removes the 3. The reader reads channel
> lowpan_peer from the peer fields, locks the channel, or
> list sends through the channel
> pointer
> 4. The old chan_close_cb()
> dropped the peer-owned
> channel reference before the
> peer RCU grace period ended
> 5. The close caller releases its
> temporary channel reference
>
> If the reader reaches its channel use after teardown releases the peer-
> owned channel reference and the close caller releases its temporary
> reference, it can use a freed channel; raw peer returns can also outlive
> the peer RCU callback.
>
> lowpan_peer objects remain RCU-observable after peer_del() unlinks them.
>
> Signed-off-by: Zhang Cen <rollkingzzc@gmail.com>
>
> ---
> diff --git a/net/bluetooth/6lowpan.c b/net/bluetooth/6lowpan.c
> index 2f03b780b40d..7334a78f5bfb 100644
> --- a/net/bluetooth/6lowpan.c
> +++ b/net/bluetooth/6lowpan.c
> @@ -9,6 +9,7 @@
> #include <linux/etherdevice.h>
> #include <linux/module.h>
> #include <linux/debugfs.h>
> +#include <linux/refcount.h>
>
> #include <net/ipv6.h>
> #include <net/ip6_route.h>
> @@ -61,6 +62,7 @@ enum {
> struct lowpan_peer {
> struct list_head list;
> struct rcu_head rcu;
> + refcount_t refcnt;
> struct l2cap_chan *chan;
>
> /* peer addresses in various formats */
> @@ -95,13 +97,33 @@ static inline void peer_add(struct lowpan_btle_dev *dev,
> atomic_inc(&dev->peer_count);
> }
>
> +static inline bool lowpan_peer_get_unless_zero(struct lowpan_peer *peer)
> +{
> + return refcount_inc_not_zero(&peer->refcnt);
> +}
> +
> +static void lowpan_peer_put(struct lowpan_peer *peer)
> +{
> + if (!refcount_dec_and_test(&peer->refcnt))
> + return;
> +
> + l2cap_chan_put(peer->chan);
> + kfree(peer);
> + module_put(THIS_MODULE);
> +}
> +
> +static void lowpan_peer_put_rcu(struct rcu_head *rcu)
> +{
> + struct lowpan_peer *peer = container_of(rcu, struct lowpan_peer, rcu);
> +
> + lowpan_peer_put(peer);
> +}
> +
> static inline bool peer_del(struct lowpan_btle_dev *dev,
> struct lowpan_peer *peer)
> {
> list_del_rcu(&peer->list);
> - kfree_rcu(peer, rcu);
> -
> - module_put(THIS_MODULE);
> + call_rcu(&peer->rcu, lowpan_peer_put_rcu);
>
> if (atomic_dec_and_test(&dev->peer_count)) {
> BT_DBG("last peer");
> @@ -179,7 +201,8 @@ static inline struct lowpan_peer *peer_lookup_dst(struct lowpan_btle_dev *dev,
> &peer->chan->dst, peer->chan->dst_type,
> &peer->peer_addr);
>
> - if (!ipv6_addr_cmp(&peer->peer_addr, nexthop)) {
> + if (!ipv6_addr_cmp(&peer->peer_addr, nexthop) &&
> + lowpan_peer_get_unless_zero(peer)) {
> rcu_read_unlock();
> return peer;
> }
> @@ -189,7 +212,8 @@ static inline struct lowpan_peer *peer_lookup_dst(struct lowpan_btle_dev *dev,
> neigh = __ipv6_neigh_lookup(dev->netdev, nexthop);
> if (neigh) {
> list_for_each_entry_rcu(peer, &dev->peers, list) {
> - if (!memcmp(neigh->ha, peer->lladdr, ETH_ALEN)) {
> + if (!memcmp(neigh->ha, peer->lladdr, ETH_ALEN) &&
> + lowpan_peer_get_unless_zero(peer)) {
> neigh_release(neigh);
> rcu_read_unlock();
> return peer;
> @@ -212,8 +236,9 @@ static struct lowpan_peer *lookup_peer(struct l2cap_conn *conn)
>
> list_for_each_entry_rcu(entry, &bt_6lowpan_devices, list) {
> peer = __peer_lookup_conn(entry, conn);
> - if (peer)
> + if (peer && lowpan_peer_get_unless_zero(peer))
> break;
> + peer = NULL;
> }
>
> rcu_read_unlock();
> @@ -361,8 +386,10 @@ static int chan_recv_cb(struct l2cap_chan *chan, struct sk_buff *skb)
> return -ENOENT;
>
> dev = lookup_dev(chan->conn);
> - if (!dev || !dev->netdev)
> + if (!dev || !dev->netdev) {
> + lowpan_peer_put(peer);
> return -ENOENT;
> + }
>
> err = recv_pkt(skb, dev->netdev, peer);
> if (err) {
> @@ -370,6 +397,8 @@ static int chan_recv_cb(struct l2cap_chan *chan, struct sk_buff *skb)
> err = -EAGAIN;
> }
>
> + lowpan_peer_put(peer);
> +
> return err;
> }
>
> @@ -380,6 +409,8 @@ static int setup_header(struct sk_buff *skb, struct net_device *netdev,
> struct ipv6hdr *hdr;
> struct lowpan_btle_dev *dev;
> struct lowpan_peer *peer;
> + struct l2cap_chan *chan;
> + u8 peer_lladdr[ETH_ALEN];
> u8 *daddr;
> int err, status = 0;
>
> @@ -388,9 +419,9 @@ static int setup_header(struct sk_buff *skb, struct net_device *netdev,
> dev = lowpan_btle_dev(netdev);
>
> memcpy(&ipv6_daddr, &hdr->daddr, sizeof(ipv6_daddr));
> + lowpan_cb(skb)->chan = NULL;
>
> if (ipv6_addr_is_multicast(&ipv6_daddr)) {
> - lowpan_cb(skb)->chan = NULL;
> daddr = NULL;
> } else {
> BT_DBG("dest IP %pI6c", &ipv6_daddr);
> @@ -406,10 +437,15 @@ static int setup_header(struct sk_buff *skb, struct net_device *netdev,
> return -ENOENT;
> }
>
> - daddr = peer->lladdr;
> - *peer_addr = peer->chan->dst;
> - *peer_addr_type = peer->chan->dst_type;
> - lowpan_cb(skb)->chan = peer->chan;
> + chan = peer->chan;
> + l2cap_chan_hold(chan);
> + memcpy(peer_lladdr, peer->lladdr, ETH_ALEN);
> + *peer_addr = chan->dst;
> + *peer_addr_type = chan->dst_type;
> + lowpan_cb(skb)->chan = chan;
> + lowpan_peer_put(peer);
> +
> + daddr = peer_lladdr;
>
> status = 1;
> }
> @@ -417,8 +453,13 @@ static int setup_header(struct sk_buff *skb, struct net_device *netdev,
> lowpan_header_compress(skb, netdev, daddr, dev->netdev->dev_addr);
>
> err = dev_hard_header(skb, netdev, ETH_P_IPV6, NULL, NULL, 0);
> - if (err < 0)
> + if (err < 0) {
> + if (lowpan_cb(skb)->chan) {
> + l2cap_chan_put(lowpan_cb(skb)->chan);
> + lowpan_cb(skb)->chan = NULL;
> + }
> return err;
> + }
>
> return status;
> }
> @@ -483,15 +524,23 @@ static int send_mcast_pkt(struct sk_buff *skb, struct net_device *netdev)
> dev = lowpan_btle_dev(entry->netdev);
>
> list_for_each_entry_rcu(pentry, &dev->peers, list) {
> + struct l2cap_chan *chan = pentry->chan;
> int ret;
>
> local_skb = skb_clone(skb, GFP_ATOMIC);
> + if (!local_skb) {
> + err = -ENOMEM;
> + continue;
> + }
> +
> + l2cap_chan_hold(chan);
>
> BT_DBG("xmit %s to %pMR type %u IP %pI6c chan %p",
> netdev->name,
> - &pentry->chan->dst, pentry->chan->dst_type,
> - &pentry->peer_addr, pentry->chan);
> - ret = send_pkt(pentry->chan, local_skb, netdev);
> + &chan->dst, chan->dst_type,
> + &pentry->peer_addr, chan);
> + ret = send_pkt(chan, local_skb, netdev);
> + l2cap_chan_put(chan);
> if (ret < 0)
> err = ret;
>
> @@ -530,10 +579,14 @@ static netdev_tx_t bt_xmit(struct sk_buff *skb, struct net_device *netdev)
>
> if (err) {
> if (lowpan_cb(skb)->chan) {
> + struct l2cap_chan *chan = lowpan_cb(skb)->chan;
> +
> BT_DBG("xmit %s to %pMR type %u IP %pI6c chan %p",
> netdev->name, &addr, addr_type,
> - &lowpan_cb(skb)->addr, lowpan_cb(skb)->chan);
> - err = send_pkt(lowpan_cb(skb)->chan, skb, netdev);
> + &lowpan_cb(skb)->addr, chan);
> + err = send_pkt(chan, skb, netdev);
> + l2cap_chan_put(chan);
> + lowpan_cb(skb)->chan = NULL;
> } else {
> err = -ENOENT;
> }
> @@ -649,6 +702,7 @@ static struct l2cap_chan *add_peer_chan(struct l2cap_chan *chan,
> if (!peer)
> return NULL;
>
> + refcount_set(&peer->refcnt, 1);
> peer->chan = chan;
>
> baswap((void *)peer->lladdr, &chan->dst);
> @@ -802,8 +856,6 @@ static void chan_close_cb(struct l2cap_chan *chan)
> last ? "last " : "1 ", peer);
> BT_DBG("chan %p orig refcnt %u", chan,
> kref_read(&chan->kref));
> -
> - l2cap_chan_put(chan);
> break;
> }
> }
> @@ -918,6 +970,7 @@ static int bt_6lowpan_connect(bdaddr_t *addr, u8 dst_type)
> static int bt_6lowpan_disconnect(struct l2cap_conn *conn, u8 dst_type)
> {
> struct lowpan_peer *peer;
> + struct l2cap_chan *chan;
>
> BT_DBG("conn %p dst type %u", conn, dst_type);
>
> @@ -925,11 +978,16 @@ static int bt_6lowpan_disconnect(struct l2cap_conn *conn, u8 dst_type)
> if (!peer)
> return -ENOENT;
>
> - BT_DBG("peer %p chan %p", peer, peer->chan);
> + chan = peer->chan;
> + l2cap_chan_hold(chan);
> +
> + BT_DBG("peer %p chan %p", peer, chan);
> + lowpan_peer_put(peer);
>
> - l2cap_chan_lock(peer->chan);
> - l2cap_chan_close(peer->chan, ENOENT);
> - l2cap_chan_unlock(peer->chan);
> + l2cap_chan_lock(chan);
> + l2cap_chan_close(chan, ENOENT);
> + l2cap_chan_unlock(chan);
> + l2cap_chan_put(chan);
>
> return 0;
> }
> @@ -1169,6 +1227,7 @@ static ssize_t lowpan_control_write(struct file *fp,
> peer = lookup_peer(conn);
> if (peer) {
> BT_DBG("6LoWPAN connection already exists");
> + lowpan_peer_put(peer);
> return -EALREADY;
> }
AI got quite a few issues with this change:
https://sashiko.dev/#/patchset/20260509173745.413473-1-rollkingzzc%40gmail.com
--
Luiz Augusto von Dentz
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH v2] Bluetooth: 6lowpan: Fix peer and channel lifetime during teardown
2026-05-11 16:20 ` Luiz Augusto von Dentz
@ 2026-05-11 17:11 ` Cen Zhang
0 siblings, 0 replies; 8+ messages in thread
From: Cen Zhang @ 2026-05-11 17:11 UTC (permalink / raw)
To: Luiz Augusto von Dentz
Cc: Marcel Holtmann, linux-bluetooth, linux-kernel, Hillf Danton,
zerocling0077, hanguidong02
Hi Luiz,
Thanks for pointing this out.
I'll take another look at the lifetime and context issues raised there
and rework the patch accordingly. I'll send a v3 after addressing them.
Thanks,
Zhang Cen
Luiz Augusto von Dentz <luiz.dentz@gmail.com> 于2026年5月12日周二 00:20写道:
>
> Hi,
>
> On Sun, May 10, 2026 at 12:04 AM Zhang Cen <rollkingzzc@gmail.com> wrote:
> >
> > 6LoWPAN peers keep a protocol-owned L2CAP channel reference in
> > peer->chan. chan_close_cb() removes the peer from the RCU list when the
> > channel is closed, but the old code also dropped that channel reference
> > before the peer object stopped being observable by in-flight RCU readers.
> >
> > That can leave peer readers with a live lowpan_peer that still points at
> > an l2cap_chan whose last reference has already been released. Some
> > lookup helpers also returned raw peer pointers after dropping RCU
> > protection, so keeping only the channel alive until the RCU callback
> > still leaves the peer lifetime rules implicit.
> >
> > Make the peer lifetime explicit. Give each lowpan_peer a refcount, have
> > RCU lookups take a peer reference before returning a peer outside the
> > read-side critical section, and make peer_del() only unlink the peer and
> > schedule an RCU callback to drop the list owner reference after the grace
> > period. Release the peer-owned L2CAP channel reference and module
> > reference from the final peer put.
> >
> > Keep temporary channel references in transmit and disconnect paths that
> > cache or send through peer->chan after lookup.
> >
> > The buggy scenario involves two paths, with each column showing the order within that path:
> >
> > L2CAP peer teardown: Concurrent peer reader:
> > 1. A close path holds a 1. A reader such as
> > temporary reference to the lookup_peer(),
> > channel peer_lookup_dst(),
> > send_mcast_pkt(), or
> > bt_6lowpan_disconnect()
> > observes the peer
> > 2. l2cap_chan_del() drops the 2. The reader keeps using the
> > connection-owned channel raw peer or peer->chan after
> > reference RCU lookup protection ends
> > 3. chan_close_cb() removes the 3. The reader reads channel
> > lowpan_peer from the peer fields, locks the channel, or
> > list sends through the channel
> > pointer
> > 4. The old chan_close_cb()
> > dropped the peer-owned
> > channel reference before the
> > peer RCU grace period ended
> > 5. The close caller releases its
> > temporary channel reference
> >
> > If the reader reaches its channel use after teardown releases the peer-
> > owned channel reference and the close caller releases its temporary
> > reference, it can use a freed channel; raw peer returns can also outlive
> > the peer RCU callback.
> >
> > lowpan_peer objects remain RCU-observable after peer_del() unlinks them.
> >
> > Signed-off-by: Zhang Cen <rollkingzzc@gmail.com>
> >
> > ---
> > diff --git a/net/bluetooth/6lowpan.c b/net/bluetooth/6lowpan.c
> > index 2f03b780b40d..7334a78f5bfb 100644
> > --- a/net/bluetooth/6lowpan.c
> > +++ b/net/bluetooth/6lowpan.c
> > @@ -9,6 +9,7 @@
> > #include <linux/etherdevice.h>
> > #include <linux/module.h>
> > #include <linux/debugfs.h>
> > +#include <linux/refcount.h>
> >
> > #include <net/ipv6.h>
> > #include <net/ip6_route.h>
> > @@ -61,6 +62,7 @@ enum {
> > struct lowpan_peer {
> > struct list_head list;
> > struct rcu_head rcu;
> > + refcount_t refcnt;
> > struct l2cap_chan *chan;
> >
> > /* peer addresses in various formats */
> > @@ -95,13 +97,33 @@ static inline void peer_add(struct lowpan_btle_dev *dev,
> > atomic_inc(&dev->peer_count);
> > }
> >
> > +static inline bool lowpan_peer_get_unless_zero(struct lowpan_peer *peer)
> > +{
> > + return refcount_inc_not_zero(&peer->refcnt);
> > +}
> > +
> > +static void lowpan_peer_put(struct lowpan_peer *peer)
> > +{
> > + if (!refcount_dec_and_test(&peer->refcnt))
> > + return;
> > +
> > + l2cap_chan_put(peer->chan);
> > + kfree(peer);
> > + module_put(THIS_MODULE);
> > +}
> > +
> > +static void lowpan_peer_put_rcu(struct rcu_head *rcu)
> > +{
> > + struct lowpan_peer *peer = container_of(rcu, struct lowpan_peer, rcu);
> > +
> > + lowpan_peer_put(peer);
> > +}
> > +
> > static inline bool peer_del(struct lowpan_btle_dev *dev,
> > struct lowpan_peer *peer)
> > {
> > list_del_rcu(&peer->list);
> > - kfree_rcu(peer, rcu);
> > -
> > - module_put(THIS_MODULE);
> > + call_rcu(&peer->rcu, lowpan_peer_put_rcu);
> >
> > if (atomic_dec_and_test(&dev->peer_count)) {
> > BT_DBG("last peer");
> > @@ -179,7 +201,8 @@ static inline struct lowpan_peer *peer_lookup_dst(struct lowpan_btle_dev *dev,
> > &peer->chan->dst, peer->chan->dst_type,
> > &peer->peer_addr);
> >
> > - if (!ipv6_addr_cmp(&peer->peer_addr, nexthop)) {
> > + if (!ipv6_addr_cmp(&peer->peer_addr, nexthop) &&
> > + lowpan_peer_get_unless_zero(peer)) {
> > rcu_read_unlock();
> > return peer;
> > }
> > @@ -189,7 +212,8 @@ static inline struct lowpan_peer *peer_lookup_dst(struct lowpan_btle_dev *dev,
> > neigh = __ipv6_neigh_lookup(dev->netdev, nexthop);
> > if (neigh) {
> > list_for_each_entry_rcu(peer, &dev->peers, list) {
> > - if (!memcmp(neigh->ha, peer->lladdr, ETH_ALEN)) {
> > + if (!memcmp(neigh->ha, peer->lladdr, ETH_ALEN) &&
> > + lowpan_peer_get_unless_zero(peer)) {
> > neigh_release(neigh);
> > rcu_read_unlock();
> > return peer;
> > @@ -212,8 +236,9 @@ static struct lowpan_peer *lookup_peer(struct l2cap_conn *conn)
> >
> > list_for_each_entry_rcu(entry, &bt_6lowpan_devices, list) {
> > peer = __peer_lookup_conn(entry, conn);
> > - if (peer)
> > + if (peer && lowpan_peer_get_unless_zero(peer))
> > break;
> > + peer = NULL;
> > }
> >
> > rcu_read_unlock();
> > @@ -361,8 +386,10 @@ static int chan_recv_cb(struct l2cap_chan *chan, struct sk_buff *skb)
> > return -ENOENT;
> >
> > dev = lookup_dev(chan->conn);
> > - if (!dev || !dev->netdev)
> > + if (!dev || !dev->netdev) {
> > + lowpan_peer_put(peer);
> > return -ENOENT;
> > + }
> >
> > err = recv_pkt(skb, dev->netdev, peer);
> > if (err) {
> > @@ -370,6 +397,8 @@ static int chan_recv_cb(struct l2cap_chan *chan, struct sk_buff *skb)
> > err = -EAGAIN;
> > }
> >
> > + lowpan_peer_put(peer);
> > +
> > return err;
> > }
> >
> > @@ -380,6 +409,8 @@ static int setup_header(struct sk_buff *skb, struct net_device *netdev,
> > struct ipv6hdr *hdr;
> > struct lowpan_btle_dev *dev;
> > struct lowpan_peer *peer;
> > + struct l2cap_chan *chan;
> > + u8 peer_lladdr[ETH_ALEN];
> > u8 *daddr;
> > int err, status = 0;
> >
> > @@ -388,9 +419,9 @@ static int setup_header(struct sk_buff *skb, struct net_device *netdev,
> > dev = lowpan_btle_dev(netdev);
> >
> > memcpy(&ipv6_daddr, &hdr->daddr, sizeof(ipv6_daddr));
> > + lowpan_cb(skb)->chan = NULL;
> >
> > if (ipv6_addr_is_multicast(&ipv6_daddr)) {
> > - lowpan_cb(skb)->chan = NULL;
> > daddr = NULL;
> > } else {
> > BT_DBG("dest IP %pI6c", &ipv6_daddr);
> > @@ -406,10 +437,15 @@ static int setup_header(struct sk_buff *skb, struct net_device *netdev,
> > return -ENOENT;
> > }
> >
> > - daddr = peer->lladdr;
> > - *peer_addr = peer->chan->dst;
> > - *peer_addr_type = peer->chan->dst_type;
> > - lowpan_cb(skb)->chan = peer->chan;
> > + chan = peer->chan;
> > + l2cap_chan_hold(chan);
> > + memcpy(peer_lladdr, peer->lladdr, ETH_ALEN);
> > + *peer_addr = chan->dst;
> > + *peer_addr_type = chan->dst_type;
> > + lowpan_cb(skb)->chan = chan;
> > + lowpan_peer_put(peer);
> > +
> > + daddr = peer_lladdr;
> >
> > status = 1;
> > }
> > @@ -417,8 +453,13 @@ static int setup_header(struct sk_buff *skb, struct net_device *netdev,
> > lowpan_header_compress(skb, netdev, daddr, dev->netdev->dev_addr);
> >
> > err = dev_hard_header(skb, netdev, ETH_P_IPV6, NULL, NULL, 0);
> > - if (err < 0)
> > + if (err < 0) {
> > + if (lowpan_cb(skb)->chan) {
> > + l2cap_chan_put(lowpan_cb(skb)->chan);
> > + lowpan_cb(skb)->chan = NULL;
> > + }
> > return err;
> > + }
> >
> > return status;
> > }
> > @@ -483,15 +524,23 @@ static int send_mcast_pkt(struct sk_buff *skb, struct net_device *netdev)
> > dev = lowpan_btle_dev(entry->netdev);
> >
> > list_for_each_entry_rcu(pentry, &dev->peers, list) {
> > + struct l2cap_chan *chan = pentry->chan;
> > int ret;
> >
> > local_skb = skb_clone(skb, GFP_ATOMIC);
> > + if (!local_skb) {
> > + err = -ENOMEM;
> > + continue;
> > + }
> > +
> > + l2cap_chan_hold(chan);
> >
> > BT_DBG("xmit %s to %pMR type %u IP %pI6c chan %p",
> > netdev->name,
> > - &pentry->chan->dst, pentry->chan->dst_type,
> > - &pentry->peer_addr, pentry->chan);
> > - ret = send_pkt(pentry->chan, local_skb, netdev);
> > + &chan->dst, chan->dst_type,
> > + &pentry->peer_addr, chan);
> > + ret = send_pkt(chan, local_skb, netdev);
> > + l2cap_chan_put(chan);
> > if (ret < 0)
> > err = ret;
> >
> > @@ -530,10 +579,14 @@ static netdev_tx_t bt_xmit(struct sk_buff *skb, struct net_device *netdev)
> >
> > if (err) {
> > if (lowpan_cb(skb)->chan) {
> > + struct l2cap_chan *chan = lowpan_cb(skb)->chan;
> > +
> > BT_DBG("xmit %s to %pMR type %u IP %pI6c chan %p",
> > netdev->name, &addr, addr_type,
> > - &lowpan_cb(skb)->addr, lowpan_cb(skb)->chan);
> > - err = send_pkt(lowpan_cb(skb)->chan, skb, netdev);
> > + &lowpan_cb(skb)->addr, chan);
> > + err = send_pkt(chan, skb, netdev);
> > + l2cap_chan_put(chan);
> > + lowpan_cb(skb)->chan = NULL;
> > } else {
> > err = -ENOENT;
> > }
> > @@ -649,6 +702,7 @@ static struct l2cap_chan *add_peer_chan(struct l2cap_chan *chan,
> > if (!peer)
> > return NULL;
> >
> > + refcount_set(&peer->refcnt, 1);
> > peer->chan = chan;
> >
> > baswap((void *)peer->lladdr, &chan->dst);
> > @@ -802,8 +856,6 @@ static void chan_close_cb(struct l2cap_chan *chan)
> > last ? "last " : "1 ", peer);
> > BT_DBG("chan %p orig refcnt %u", chan,
> > kref_read(&chan->kref));
> > -
> > - l2cap_chan_put(chan);
> > break;
> > }
> > }
> > @@ -918,6 +970,7 @@ static int bt_6lowpan_connect(bdaddr_t *addr, u8 dst_type)
> > static int bt_6lowpan_disconnect(struct l2cap_conn *conn, u8 dst_type)
> > {
> > struct lowpan_peer *peer;
> > + struct l2cap_chan *chan;
> >
> > BT_DBG("conn %p dst type %u", conn, dst_type);
> >
> > @@ -925,11 +978,16 @@ static int bt_6lowpan_disconnect(struct l2cap_conn *conn, u8 dst_type)
> > if (!peer)
> > return -ENOENT;
> >
> > - BT_DBG("peer %p chan %p", peer, peer->chan);
> > + chan = peer->chan;
> > + l2cap_chan_hold(chan);
> > +
> > + BT_DBG("peer %p chan %p", peer, chan);
> > + lowpan_peer_put(peer);
> >
> > - l2cap_chan_lock(peer->chan);
> > - l2cap_chan_close(peer->chan, ENOENT);
> > - l2cap_chan_unlock(peer->chan);
> > + l2cap_chan_lock(chan);
> > + l2cap_chan_close(chan, ENOENT);
> > + l2cap_chan_unlock(chan);
> > + l2cap_chan_put(chan);
> >
> > return 0;
> > }
> > @@ -1169,6 +1227,7 @@ static ssize_t lowpan_control_write(struct file *fp,
> > peer = lookup_peer(conn);
> > if (peer) {
> > BT_DBG("6LoWPAN connection already exists");
> > + lowpan_peer_put(peer);
> > return -EALREADY;
> > }
>
> AI got quite a few issues with this change:
>
> https://sashiko.dev/#/patchset/20260509173745.413473-1-rollkingzzc%40gmail.com
>
> --
> Luiz Augusto von Dentz
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v3] Bluetooth: 6lowpan: Fix peer and channel lifetime during teardown
2026-05-10 4:04 ` [PATCH v2] Bluetooth: 6lowpan: Fix peer and channel lifetime during teardown Zhang Cen
2026-05-11 16:20 ` Luiz Augusto von Dentz
@ 2026-05-11 17:18 ` Zhang Cen
1 sibling, 0 replies; 8+ messages in thread
From: Zhang Cen @ 2026-05-11 17:18 UTC (permalink / raw)
To: Marcel Holtmann, Luiz Augusto von Dentz
Cc: linux-bluetooth, linux-kernel, zerocling0077, 2045gemini,
Zhang Cen
A 6LoWPAN peer keeps a protocol-owned L2CAP channel reference in
peer->chan. chan_close_cb() removes the peer from the RCU list when the
channel is closed, but the old code also dropped that channel reference
before the peer object stopped being observable by in-flight RCU readers.
The buggy scenario involves two paths, with each column showing the order
within that path:
close path: lookup/transmit path:
l2cap_conn_del() or another setup_header(), send_mcast_pkt(),
close path holds a temporary bt_6lowpan_disconnect(), or
channel reference chan_recv_cb() observes a peer
l2cap_chan_del() drops the the reader leaves the RCU read
connection-owned channel reference side critical section, or keeps
using peer->chan inside it
chan_close_cb() removes the peer the reader reads channel fields,
from the peer list locks the channel, or sends
through peer->chan
the old chan_close_cb() dropped
the peer-owned channel reference
the close caller releases its
temporary channel reference
If the close path consumes the peer-owned channel reference before a
reader finishes with peer->chan, the reader can use a freed l2cap_chan.
The lookup helpers that returned raw peer pointers after rcu_read_unlock()
also let callers outlive the peer object's kfree_rcu() lifetime.
Give lowpan_peer its own refcount and make RCU lookups acquire a peer
reference before returning a peer to code that runs outside the read-side
critical section. Keep the peer-owned L2CAP channel reference attached to
the peer until the last peer reference is dropped.
The last peer put can happen from an RCU callback or from the network
transmit path, so do not drop the final L2CAP channel reference directly
from lowpan_peer_put(). Queue final peer destruction to a dedicated
workqueue and release the channel and module references there. Store the
peer, rather than a temporary channel reference, in the skb control block
for unicast transmit, and put the peer after the send path is done.
Multicast continues to use peer->chan only while it is inside the RCU
read-side critical section.
Wait for queued RCU callbacks and peer-destroy work during module exit so
asynchronous peer destruction cannot outlive the module text.
Signed-off-by: Zhang Cen <rollkingzzc@gmail.com>
---
net/bluetooth/6lowpan.c | 128 ++++++++++++++++++++++++++++++++--------
1 file changed, 104 insertions(+), 24 deletions(-)
diff --git a/net/bluetooth/6lowpan.c b/net/bluetooth/6lowpan.c
index 23a229ab6..7a1b0d7b0 100644
--- a/net/bluetooth/6lowpan.c
+++ b/net/bluetooth/6lowpan.c
@@ -9,6 +9,8 @@
#include <linux/etherdevice.h>
#include <linux/module.h>
#include <linux/debugfs.h>
+#include <linux/refcount.h>
+#include <linux/workqueue.h>
#include <net/ipv6.h>
#include <net/ip6_route.h>
@@ -29,10 +31,12 @@ static struct dentry *lowpan_control_debugfs;
#define IFACE_NAME_TEMPLATE "bt%d"
+struct lowpan_peer;
+
struct skb_cb {
struct in6_addr addr;
struct in6_addr gw;
- struct l2cap_chan *chan;
+ struct lowpan_peer *peer;
};
#define lowpan_cb(skb) ((struct skb_cb *)((skb)->cb))
@@ -52,6 +56,7 @@ static bool enable_6lowpan;
*/
static struct l2cap_chan *listen_chan;
static DEFINE_MUTEX(set_lock);
+static struct workqueue_struct *lowpan_peer_wq;
enum {
LOWPAN_PEER_CLOSING,
@@ -61,6 +66,8 @@ enum {
struct lowpan_peer {
struct list_head list;
struct rcu_head rcu;
+ struct work_struct destroy_work;
+ refcount_t refcnt;
struct l2cap_chan *chan;
/* peer addresses in various formats */
@@ -95,13 +102,39 @@ static inline void peer_add(struct lowpan_btle_dev *dev,
atomic_inc(&dev->peer_count);
}
+static inline bool lowpan_peer_get_unless_zero(struct lowpan_peer *peer)
+{
+ return refcount_inc_not_zero(&peer->refcnt);
+}
+
+static void lowpan_peer_destroy(struct work_struct *work)
+{
+ struct lowpan_peer *peer = container_of(work, struct lowpan_peer,
+ destroy_work);
+
+ l2cap_chan_put(peer->chan);
+ kfree(peer);
+ module_put(THIS_MODULE);
+}
+
+static void lowpan_peer_put(struct lowpan_peer *peer)
+{
+ if (refcount_dec_and_test(&peer->refcnt))
+ queue_work(lowpan_peer_wq, &peer->destroy_work);
+}
+
+static void lowpan_peer_put_rcu(struct rcu_head *rcu)
+{
+ struct lowpan_peer *peer = container_of(rcu, struct lowpan_peer, rcu);
+
+ lowpan_peer_put(peer);
+}
+
static inline bool peer_del(struct lowpan_btle_dev *dev,
struct lowpan_peer *peer)
{
list_del_rcu(&peer->list);
- kfree_rcu(peer, rcu);
-
- module_put(THIS_MODULE);
+ call_rcu(&peer->rcu, lowpan_peer_put_rcu);
if (atomic_dec_and_test(&dev->peer_count)) {
BT_DBG("last peer");
@@ -179,7 +212,8 @@ static inline struct lowpan_peer *peer_lookup_dst(struct lowpan_btle_dev *dev,
&peer->chan->dst, peer->chan->dst_type,
&peer->peer_addr);
- if (!ipv6_addr_cmp(&peer->peer_addr, nexthop)) {
+ if (!ipv6_addr_cmp(&peer->peer_addr, nexthop) &&
+ lowpan_peer_get_unless_zero(peer)) {
rcu_read_unlock();
return peer;
}
@@ -189,7 +223,8 @@ static inline struct lowpan_peer *peer_lookup_dst(struct lowpan_btle_dev *dev,
neigh = __ipv6_neigh_lookup(dev->netdev, nexthop);
if (neigh) {
list_for_each_entry_rcu(peer, &dev->peers, list) {
- if (!memcmp(neigh->ha, peer->lladdr, ETH_ALEN)) {
+ if (!memcmp(neigh->ha, peer->lladdr, ETH_ALEN) &&
+ lowpan_peer_get_unless_zero(peer)) {
neigh_release(neigh);
rcu_read_unlock();
return peer;
@@ -212,8 +247,9 @@ static struct lowpan_peer *lookup_peer(struct l2cap_conn *conn)
list_for_each_entry_rcu(entry, &bt_6lowpan_devices, list) {
peer = __peer_lookup_conn(entry, conn);
- if (peer)
+ if (peer && lowpan_peer_get_unless_zero(peer))
break;
+ peer = NULL;
}
rcu_read_unlock();
@@ -361,8 +397,10 @@ static int chan_recv_cb(struct l2cap_chan *chan, struct sk_buff *skb)
return -ENOENT;
dev = lookup_dev(chan->conn);
- if (!dev || !dev->netdev)
+ if (!dev || !dev->netdev) {
+ lowpan_peer_put(peer);
return -ENOENT;
+ }
err = recv_pkt(skb, dev->netdev, peer);
if (err) {
@@ -370,6 +408,8 @@ static int chan_recv_cb(struct l2cap_chan *chan, struct sk_buff *skb)
err = -EAGAIN;
}
+ lowpan_peer_put(peer);
+
return err;
}
@@ -380,6 +420,8 @@ static int setup_header(struct sk_buff *skb, struct net_device *netdev,
struct ipv6hdr *hdr;
struct lowpan_btle_dev *dev;
struct lowpan_peer *peer;
+ struct l2cap_chan *chan;
+ u8 peer_lladdr[ETH_ALEN];
u8 *daddr;
int err, status = 0;
@@ -388,9 +430,9 @@ static int setup_header(struct sk_buff *skb, struct net_device *netdev,
dev = lowpan_btle_dev(netdev);
memcpy(&ipv6_daddr, &hdr->daddr, sizeof(ipv6_daddr));
+ lowpan_cb(skb)->peer = NULL;
if (ipv6_addr_is_multicast(&ipv6_daddr)) {
- lowpan_cb(skb)->chan = NULL;
daddr = NULL;
} else {
BT_DBG("dest IP %pI6c", &ipv6_daddr);
@@ -406,10 +448,13 @@ static int setup_header(struct sk_buff *skb, struct net_device *netdev,
return -ENOENT;
}
- daddr = peer->lladdr;
- *peer_addr = peer->chan->dst;
- *peer_addr_type = peer->chan->dst_type;
- lowpan_cb(skb)->chan = peer->chan;
+ chan = peer->chan;
+ memcpy(peer_lladdr, peer->lladdr, ETH_ALEN);
+ *peer_addr = chan->dst;
+ *peer_addr_type = chan->dst_type;
+ lowpan_cb(skb)->peer = peer;
+
+ daddr = peer_lladdr;
status = 1;
}
@@ -417,8 +462,13 @@ static int setup_header(struct sk_buff *skb, struct net_device *netdev,
lowpan_header_compress(skb, netdev, daddr, dev->netdev->dev_addr);
err = dev_hard_header(skb, netdev, ETH_P_IPV6, NULL, NULL, 0);
- if (err < 0)
+ if (err < 0) {
+ if (lowpan_cb(skb)->peer) {
+ lowpan_peer_put(lowpan_cb(skb)->peer);
+ lowpan_cb(skb)->peer = NULL;
+ }
return err;
+ }
return status;
}
@@ -486,6 +536,10 @@ static int send_mcast_pkt(struct sk_buff *skb, struct net_device *netdev)
int ret;
local_skb = skb_clone(skb, GFP_ATOMIC);
+ if (!local_skb) {
+ err = -ENOMEM;
+ continue;
+ }
BT_DBG("xmit %s to %pMR type %u IP %pI6c chan %p",
netdev->name,
@@ -529,11 +583,17 @@ static netdev_tx_t bt_xmit(struct sk_buff *skb, struct net_device *netdev)
}
if (err) {
- if (lowpan_cb(skb)->chan) {
+ struct lowpan_peer *peer = lowpan_cb(skb)->peer;
+
+ if (peer) {
+ struct l2cap_chan *chan = peer->chan;
+
BT_DBG("xmit %s to %pMR type %u IP %pI6c chan %p",
netdev->name, &addr, addr_type,
- &lowpan_cb(skb)->addr, lowpan_cb(skb)->chan);
- err = send_pkt(lowpan_cb(skb)->chan, skb, netdev);
+ &lowpan_cb(skb)->addr, chan);
+ err = send_pkt(chan, skb, netdev);
+ lowpan_peer_put(peer);
+ lowpan_cb(skb)->peer = NULL;
} else {
err = -ENOENT;
}
@@ -649,6 +709,8 @@ static struct l2cap_chan *add_peer_chan(struct l2cap_chan *chan,
if (!peer)
return NULL;
+ INIT_WORK(&peer->destroy_work, lowpan_peer_destroy);
+ refcount_set(&peer->refcnt, 1);
peer->chan = chan;
baswap((void *)peer->lladdr, &chan->dst);
@@ -822,8 +884,6 @@ static void chan_close_cb(struct l2cap_chan *chan)
last ? "last " : "1 ", peer);
BT_DBG("chan %p orig refcnt %u", chan,
kref_read(&chan->kref));
-
- l2cap_chan_put(chan);
break;
}
}
@@ -938,6 +998,7 @@ static int bt_6lowpan_connect(bdaddr_t *addr, u8 dst_type)
static int bt_6lowpan_disconnect(struct l2cap_conn *conn, u8 dst_type)
{
struct lowpan_peer *peer;
+ struct l2cap_chan *chan;
BT_DBG("conn %p dst type %u", conn, dst_type);
@@ -945,11 +1006,14 @@ static int bt_6lowpan_disconnect(struct l2cap_conn *conn, u8 dst_type)
if (!peer)
return -ENOENT;
- BT_DBG("peer %p chan %p", peer, peer->chan);
+ chan = peer->chan;
+
+ BT_DBG("peer %p chan %p", peer, chan);
- l2cap_chan_lock(peer->chan);
- l2cap_chan_close(peer->chan, ENOENT);
- l2cap_chan_unlock(peer->chan);
+ l2cap_chan_lock(chan);
+ l2cap_chan_close(chan, ENOENT);
+ l2cap_chan_unlock(chan);
+ lowpan_peer_put(peer);
return 0;
}
@@ -1189,6 +1253,7 @@ static ssize_t lowpan_control_write(struct file *fp,
peer = lookup_peer(conn);
if (peer) {
BT_DBG("6LoWPAN connection already exists");
+ lowpan_peer_put(peer);
return -EALREADY;
}
@@ -1320,6 +1385,12 @@ static struct notifier_block bt_6lowpan_dev_notifier = {
static int __init bt_6lowpan_init(void)
{
+ int err;
+
+ lowpan_peer_wq = alloc_ordered_workqueue("bt_6lowpan_peer", 0);
+ if (!lowpan_peer_wq)
+ return -ENOMEM;
+
lowpan_enable_debugfs = debugfs_create_file_unsafe("6lowpan_enable",
0644, bt_debugfs,
NULL,
@@ -1328,7 +1399,14 @@ static int __init bt_6lowpan_init(void)
bt_debugfs, NULL,
&lowpan_control_fops);
- return register_netdevice_notifier(&bt_6lowpan_dev_notifier);
+ err = register_netdevice_notifier(&bt_6lowpan_dev_notifier);
+ if (err) {
+ debugfs_remove(lowpan_enable_debugfs);
+ debugfs_remove(lowpan_control_debugfs);
+ destroy_workqueue(lowpan_peer_wq);
+ }
+
+ return err;
}
static void __exit bt_6lowpan_exit(void)
@@ -1346,6 +1424,8 @@ static void __exit bt_6lowpan_exit(void)
disconnect_devices();
unregister_netdevice_notifier(&bt_6lowpan_dev_notifier);
+ rcu_barrier();
+ destroy_workqueue(lowpan_peer_wq);
}
module_init(bt_6lowpan_init);
--
2.43.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH v3] Bluetooth: 6lowpan: Fix peer and channel lifetime during teardown
@ 2026-05-11 17:23 Zhang Cen
0 siblings, 0 replies; 8+ messages in thread
From: Zhang Cen @ 2026-05-11 17:23 UTC (permalink / raw)
To: Marcel Holtmann, Luiz Augusto von Dentz
Cc: linux-bluetooth, linux-kernel, Hillf Danton, zerocling0077,
2045gemini
A 6LoWPAN peer keeps a protocol-owned L2CAP channel reference in
peer->chan. chan_close_cb() removes the peer from the RCU list when the
channel is closed, but the old code also dropped that channel reference
before the peer object stopped being observable by in-flight RCU readers.
The buggy scenario involves two paths, with each column showing the order
within that path:
close path: lookup/transmit path:
l2cap_conn_del() or another setup_header(), send_mcast_pkt(),
close path holds a temporary bt_6lowpan_disconnect(), or
channel reference chan_recv_cb() observes a peer
l2cap_chan_del() drops the the reader leaves the RCU read
connection-owned channel reference side critical section, or keeps
using peer->chan inside it
chan_close_cb() removes the peer the reader reads channel fields,
from the peer list locks the channel, or sends
through peer->chan
the old chan_close_cb() dropped
the peer-owned channel reference
the close caller releases its
temporary channel reference
If the close path consumes the peer-owned channel reference before a
reader finishes with peer->chan, the reader can use a freed l2cap_chan.
The lookup helpers that returned raw peer pointers after rcu_read_unlock()
also let callers outlive the peer object's kfree_rcu() lifetime.
Give lowpan_peer its own refcount and make RCU lookups acquire a peer
reference before returning a peer to code that runs outside the read-side
critical section. Keep the peer-owned L2CAP channel reference attached to
the peer until the last peer reference is dropped.
The last peer put can happen from an RCU callback or from the network
transmit path, so do not drop the final L2CAP channel reference directly
from lowpan_peer_put(). Queue final peer destruction to a dedicated
workqueue and release the channel and module references there. Store the
peer, rather than a temporary channel reference, in the skb control block
for unicast transmit, and put the peer after the send path is done.
Multicast continues to use peer->chan only while it is inside the RCU
read-side critical section.
Wait for queued RCU callbacks and peer-destroy work during module exit so
asynchronous peer destruction cannot outlive the module text.
Signed-off-by: Zhang Cen <rollkingzzc@gmail.com>
---
net/bluetooth/6lowpan.c | 128 ++++++++++++++++++++++++++++++++--------
1 file changed, 104 insertions(+), 24 deletions(-)
diff --git a/net/bluetooth/6lowpan.c b/net/bluetooth/6lowpan.c
index 23a229ab6..7a1b0d7b0 100644
--- a/net/bluetooth/6lowpan.c
+++ b/net/bluetooth/6lowpan.c
@@ -9,6 +9,8 @@
#include <linux/etherdevice.h>
#include <linux/module.h>
#include <linux/debugfs.h>
+#include <linux/refcount.h>
+#include <linux/workqueue.h>
#include <net/ipv6.h>
#include <net/ip6_route.h>
@@ -29,10 +31,12 @@ static struct dentry *lowpan_control_debugfs;
#define IFACE_NAME_TEMPLATE "bt%d"
+struct lowpan_peer;
+
struct skb_cb {
struct in6_addr addr;
struct in6_addr gw;
- struct l2cap_chan *chan;
+ struct lowpan_peer *peer;
};
#define lowpan_cb(skb) ((struct skb_cb *)((skb)->cb))
@@ -52,6 +56,7 @@ static bool enable_6lowpan;
*/
static struct l2cap_chan *listen_chan;
static DEFINE_MUTEX(set_lock);
+static struct workqueue_struct *lowpan_peer_wq;
enum {
LOWPAN_PEER_CLOSING,
@@ -61,6 +66,8 @@ enum {
struct lowpan_peer {
struct list_head list;
struct rcu_head rcu;
+ struct work_struct destroy_work;
+ refcount_t refcnt;
struct l2cap_chan *chan;
/* peer addresses in various formats */
@@ -95,13 +102,39 @@ static inline void peer_add(struct lowpan_btle_dev *dev,
atomic_inc(&dev->peer_count);
}
+static inline bool lowpan_peer_get_unless_zero(struct lowpan_peer *peer)
+{
+ return refcount_inc_not_zero(&peer->refcnt);
+}
+
+static void lowpan_peer_destroy(struct work_struct *work)
+{
+ struct lowpan_peer *peer = container_of(work, struct lowpan_peer,
+ destroy_work);
+
+ l2cap_chan_put(peer->chan);
+ kfree(peer);
+ module_put(THIS_MODULE);
+}
+
+static void lowpan_peer_put(struct lowpan_peer *peer)
+{
+ if (refcount_dec_and_test(&peer->refcnt))
+ queue_work(lowpan_peer_wq, &peer->destroy_work);
+}
+
+static void lowpan_peer_put_rcu(struct rcu_head *rcu)
+{
+ struct lowpan_peer *peer = container_of(rcu, struct lowpan_peer, rcu);
+
+ lowpan_peer_put(peer);
+}
+
static inline bool peer_del(struct lowpan_btle_dev *dev,
struct lowpan_peer *peer)
{
list_del_rcu(&peer->list);
- kfree_rcu(peer, rcu);
-
- module_put(THIS_MODULE);
+ call_rcu(&peer->rcu, lowpan_peer_put_rcu);
if (atomic_dec_and_test(&dev->peer_count)) {
BT_DBG("last peer");
@@ -179,7 +212,8 @@ static inline struct lowpan_peer *peer_lookup_dst(struct lowpan_btle_dev *dev,
&peer->chan->dst, peer->chan->dst_type,
&peer->peer_addr);
- if (!ipv6_addr_cmp(&peer->peer_addr, nexthop)) {
+ if (!ipv6_addr_cmp(&peer->peer_addr, nexthop) &&
+ lowpan_peer_get_unless_zero(peer)) {
rcu_read_unlock();
return peer;
}
@@ -189,7 +223,8 @@ static inline struct lowpan_peer *peer_lookup_dst(struct lowpan_btle_dev *dev,
neigh = __ipv6_neigh_lookup(dev->netdev, nexthop);
if (neigh) {
list_for_each_entry_rcu(peer, &dev->peers, list) {
- if (!memcmp(neigh->ha, peer->lladdr, ETH_ALEN)) {
+ if (!memcmp(neigh->ha, peer->lladdr, ETH_ALEN) &&
+ lowpan_peer_get_unless_zero(peer)) {
neigh_release(neigh);
rcu_read_unlock();
return peer;
@@ -212,8 +247,9 @@ static struct lowpan_peer *lookup_peer(struct l2cap_conn *conn)
list_for_each_entry_rcu(entry, &bt_6lowpan_devices, list) {
peer = __peer_lookup_conn(entry, conn);
- if (peer)
+ if (peer && lowpan_peer_get_unless_zero(peer))
break;
+ peer = NULL;
}
rcu_read_unlock();
@@ -361,8 +397,10 @@ static int chan_recv_cb(struct l2cap_chan *chan, struct sk_buff *skb)
return -ENOENT;
dev = lookup_dev(chan->conn);
- if (!dev || !dev->netdev)
+ if (!dev || !dev->netdev) {
+ lowpan_peer_put(peer);
return -ENOENT;
+ }
err = recv_pkt(skb, dev->netdev, peer);
if (err) {
@@ -370,6 +408,8 @@ static int chan_recv_cb(struct l2cap_chan *chan, struct sk_buff *skb)
err = -EAGAIN;
}
+ lowpan_peer_put(peer);
+
return err;
}
@@ -380,6 +420,8 @@ static int setup_header(struct sk_buff *skb, struct net_device *netdev,
struct ipv6hdr *hdr;
struct lowpan_btle_dev *dev;
struct lowpan_peer *peer;
+ struct l2cap_chan *chan;
+ u8 peer_lladdr[ETH_ALEN];
u8 *daddr;
int err, status = 0;
@@ -388,9 +430,9 @@ static int setup_header(struct sk_buff *skb, struct net_device *netdev,
dev = lowpan_btle_dev(netdev);
memcpy(&ipv6_daddr, &hdr->daddr, sizeof(ipv6_daddr));
+ lowpan_cb(skb)->peer = NULL;
if (ipv6_addr_is_multicast(&ipv6_daddr)) {
- lowpan_cb(skb)->chan = NULL;
daddr = NULL;
} else {
BT_DBG("dest IP %pI6c", &ipv6_daddr);
@@ -406,10 +448,13 @@ static int setup_header(struct sk_buff *skb, struct net_device *netdev,
return -ENOENT;
}
- daddr = peer->lladdr;
- *peer_addr = peer->chan->dst;
- *peer_addr_type = peer->chan->dst_type;
- lowpan_cb(skb)->chan = peer->chan;
+ chan = peer->chan;
+ memcpy(peer_lladdr, peer->lladdr, ETH_ALEN);
+ *peer_addr = chan->dst;
+ *peer_addr_type = chan->dst_type;
+ lowpan_cb(skb)->peer = peer;
+
+ daddr = peer_lladdr;
status = 1;
}
@@ -417,8 +462,13 @@ static int setup_header(struct sk_buff *skb, struct net_device *netdev,
lowpan_header_compress(skb, netdev, daddr, dev->netdev->dev_addr);
err = dev_hard_header(skb, netdev, ETH_P_IPV6, NULL, NULL, 0);
- if (err < 0)
+ if (err < 0) {
+ if (lowpan_cb(skb)->peer) {
+ lowpan_peer_put(lowpan_cb(skb)->peer);
+ lowpan_cb(skb)->peer = NULL;
+ }
return err;
+ }
return status;
}
@@ -486,6 +536,10 @@ static int send_mcast_pkt(struct sk_buff *skb, struct net_device *netdev)
int ret;
local_skb = skb_clone(skb, GFP_ATOMIC);
+ if (!local_skb) {
+ err = -ENOMEM;
+ continue;
+ }
BT_DBG("xmit %s to %pMR type %u IP %pI6c chan %p",
netdev->name,
@@ -529,11 +583,17 @@ static netdev_tx_t bt_xmit(struct sk_buff *skb, struct net_device *netdev)
}
if (err) {
- if (lowpan_cb(skb)->chan) {
+ struct lowpan_peer *peer = lowpan_cb(skb)->peer;
+
+ if (peer) {
+ struct l2cap_chan *chan = peer->chan;
+
BT_DBG("xmit %s to %pMR type %u IP %pI6c chan %p",
netdev->name, &addr, addr_type,
- &lowpan_cb(skb)->addr, lowpan_cb(skb)->chan);
- err = send_pkt(lowpan_cb(skb)->chan, skb, netdev);
+ &lowpan_cb(skb)->addr, chan);
+ err = send_pkt(chan, skb, netdev);
+ lowpan_peer_put(peer);
+ lowpan_cb(skb)->peer = NULL;
} else {
err = -ENOENT;
}
@@ -649,6 +709,8 @@ static struct l2cap_chan *add_peer_chan(struct l2cap_chan *chan,
if (!peer)
return NULL;
+ INIT_WORK(&peer->destroy_work, lowpan_peer_destroy);
+ refcount_set(&peer->refcnt, 1);
peer->chan = chan;
baswap((void *)peer->lladdr, &chan->dst);
@@ -822,8 +884,6 @@ static void chan_close_cb(struct l2cap_chan *chan)
last ? "last " : "1 ", peer);
BT_DBG("chan %p orig refcnt %u", chan,
kref_read(&chan->kref));
-
- l2cap_chan_put(chan);
break;
}
}
@@ -938,6 +998,7 @@ static int bt_6lowpan_connect(bdaddr_t *addr, u8 dst_type)
static int bt_6lowpan_disconnect(struct l2cap_conn *conn, u8 dst_type)
{
struct lowpan_peer *peer;
+ struct l2cap_chan *chan;
BT_DBG("conn %p dst type %u", conn, dst_type);
@@ -945,11 +1006,14 @@ static int bt_6lowpan_disconnect(struct l2cap_conn *conn, u8 dst_type)
if (!peer)
return -ENOENT;
- BT_DBG("peer %p chan %p", peer, peer->chan);
+ chan = peer->chan;
+
+ BT_DBG("peer %p chan %p", peer, chan);
- l2cap_chan_lock(peer->chan);
- l2cap_chan_close(peer->chan, ENOENT);
- l2cap_chan_unlock(peer->chan);
+ l2cap_chan_lock(chan);
+ l2cap_chan_close(chan, ENOENT);
+ l2cap_chan_unlock(chan);
+ lowpan_peer_put(peer);
return 0;
}
@@ -1189,6 +1253,7 @@ static ssize_t lowpan_control_write(struct file *fp,
peer = lookup_peer(conn);
if (peer) {
BT_DBG("6LoWPAN connection already exists");
+ lowpan_peer_put(peer);
return -EALREADY;
}
@@ -1320,6 +1385,12 @@ static struct notifier_block bt_6lowpan_dev_notifier = {
static int __init bt_6lowpan_init(void)
{
+ int err;
+
+ lowpan_peer_wq = alloc_ordered_workqueue("bt_6lowpan_peer", 0);
+ if (!lowpan_peer_wq)
+ return -ENOMEM;
+
lowpan_enable_debugfs = debugfs_create_file_unsafe("6lowpan_enable",
0644, bt_debugfs,
NULL,
@@ -1328,7 +1399,14 @@ static int __init bt_6lowpan_init(void)
bt_debugfs, NULL,
&lowpan_control_fops);
- return register_netdevice_notifier(&bt_6lowpan_dev_notifier);
+ err = register_netdevice_notifier(&bt_6lowpan_dev_notifier);
+ if (err) {
+ debugfs_remove(lowpan_enable_debugfs);
+ debugfs_remove(lowpan_control_debugfs);
+ destroy_workqueue(lowpan_peer_wq);
+ }
+
+ return err;
}
static void __exit bt_6lowpan_exit(void)
@@ -1346,6 +1424,8 @@ static void __exit bt_6lowpan_exit(void)
disconnect_devices();
unregister_netdevice_notifier(&bt_6lowpan_dev_notifier);
+ rcu_barrier();
+ destroy_workqueue(lowpan_peer_wq);
}
module_init(bt_6lowpan_init);
--
2.43.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
end of thread, other threads:[~2026-05-11 17:23 UTC | newest]
Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-09 17:37 [PATCH] Bluetooth: 6lowpan: Defer peer channel release until RCU cleanup Zhang Cen
2026-05-10 1:43 ` Hillf Danton
2026-05-10 4:01 ` c Z
2026-05-10 4:04 ` [PATCH v2] Bluetooth: 6lowpan: Fix peer and channel lifetime during teardown Zhang Cen
2026-05-11 16:20 ` Luiz Augusto von Dentz
2026-05-11 17:11 ` Cen Zhang
2026-05-11 17:18 ` [PATCH v3] " Zhang Cen
-- strict thread matches above, loose matches on Subject: below --
2026-05-11 17:23 Zhang Cen
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox