[PATCH] Bluetooth: 6LoWPAN: Serialize multicast sends with peer removal

From: Cen Zhang

Date: Thu Oct 08 2026 - 00:28:09 EST


A peer's channel must remain alive until send_mcast_pkt() finishes using
it. The multicast walk protects the lowpan_peer with RCU, but peer_del()
defers only the peer allocation. On disconnect, l2cap_chan_del() drops
the connection-list reference, chan_close_cb() removes the peer and drops
the original channel reference, and l2cap_conn_del() drops its temporary
reference. A ready channel can then be freed while multicast transmit
still holds the peer, causing send_pkt() to write chan->data after free.
Another peer can keep the interface up, so the last-peer shutdown does
not drain this transmission.

Hold devices_lock across the multicast walk and send_pkt() calls so that
peer removal cannot release the channel reference until the send finishes.
Use the bottom-half-safe variants for all devices_lock critical sections
because transmit runs in softirq context. This prevents transmit from
interrupting a process-context lock holder on the same CPU and deadlocking.
The LE send path uses atomic allocations and does not acquire the channel
or connection mutexes, preserving the existing lock order and teardown
sequence.

The reported access and release path were:

[Thu Oct 1 11:52:32 2026] BUG: KASAN: slab-use-after-free in
send_pkt+0x2c5/0x300
[Thu Oct 1 11:52:32 2026] Write of size 8 at addr ffff88810be0f4a0
by task python3/535
[...]
[Thu Oct 1 11:52:32 2026] Freed by task 502:
[...]
[Thu Oct 1 11:52:32 2026] l2cap_chan_put+0x273/0x3a0
[Thu Oct 1 11:52:32 2026] l2cap_conn_del+0x36d/0x770
[Thu Oct 1 11:52:32 2026] l2cap_disconn_cfm+0x87/0xd0
[Thu Oct 1 11:52:32 2026] hci_disconn_complete_evt+0x319/0xa30
[...]

Fixes: 90305829635d ("Bluetooth: 6lowpan: Converting rwlocks to use RCU")
Assisted-by: LLM
Signed-off-by: Cen Zhang <zzzccc427@xxxxxxxxx>
---
diff --git a/net/bluetooth/6lowpan.c b/net/bluetooth/6lowpan.c
index 836add41f5..8732132c47 100644
--- a/net/bluetooth/6lowpan.c
+++ b/net/bluetooth/6lowpan.c
@@ -471,6 +471,11 @@
struct lowpan_btle_dev *entry;
int err = 0;

+ /*
+ * Peer removal drops the channel reference, so RCU alone is not
+ * enough.
+ */
+ spin_lock_bh(&devices_lock);
rcu_read_lock();

list_for_each_entry_rcu(entry, &bt_6lowpan_devices, list) {
@@ -502,6 +507,7 @@
}

rcu_read_unlock();
+ spin_unlock_bh(&devices_lock);

return err;
}
@@ -657,10 +663,10 @@

lowpan_iphc_uncompress_eui48_lladdr(&peer->peer_addr, peer->lladdr);

- spin_lock(&devices_lock);
+ spin_lock_bh(&devices_lock);
INIT_LIST_HEAD(&peer->list);
peer_add(dev, peer);
- spin_unlock(&devices_lock);
+ spin_unlock_bh(&devices_lock);

/* Notifying peers about us needs to be done without locks held */
if (new_netdev)
@@ -695,17 +701,17 @@
(*dev)->hdev = chan->conn->hcon->hdev;
INIT_LIST_HEAD(&(*dev)->peers);

- spin_lock(&devices_lock);
+ spin_lock_bh(&devices_lock);
INIT_LIST_HEAD(&(*dev)->list);
list_add_rcu(&(*dev)->list, &bt_6lowpan_devices);
- spin_unlock(&devices_lock);
+ spin_unlock_bh(&devices_lock);

err = lowpan_register_netdev(netdev, LOWPAN_LLTYPE_BTLE);
if (err < 0) {
BT_INFO("register_netdev failed %d", err);
- spin_lock(&devices_lock);
+ spin_lock_bh(&devices_lock);
list_del_rcu(&(*dev)->list);
- spin_unlock(&devices_lock);
+ spin_unlock_bh(&devices_lock);
free_netdev(netdev);
goto out;
}
@@ -788,7 +794,7 @@

BT_DBG("chan %p conn %p", chan, chan->conn);

- spin_lock(&devices_lock);
+ spin_lock_bh(&devices_lock);

list_for_each_entry_rcu(entry, &bt_6lowpan_devices, list) {
dev = lowpan_btle_dev(entry->netdev);
@@ -808,7 +814,7 @@
}

if (!err && last && dev && !atomic_read(&dev->peer_count)) {
- spin_unlock(&devices_lock);
+ spin_unlock_bh(&devices_lock);

cancel_delayed_work_sync(&dev->notify_peers);

@@ -817,7 +823,7 @@
INIT_WORK(&entry->delete_netdev, delete_netdev);
schedule_work(&entry->delete_netdev);
} else {
- spin_unlock(&devices_lock);
+ spin_unlock_bh(&devices_lock);
}
}

@@ -918,18 +924,18 @@

BT_DBG("conn %p dst type %u", conn, dst_type);

- spin_lock(&devices_lock);
+ spin_lock_bh(&devices_lock);

peer = lookup_peer(conn);
if (!peer) {
- spin_unlock(&devices_lock);
+ spin_unlock_bh(&devices_lock);
return -ENOENT;
}

chan = peer->chan;
l2cap_chan_hold(chan);

- spin_unlock(&devices_lock);
+ spin_unlock_bh(&devices_lock);

BT_DBG("peer %p chan %p", peer, chan);

@@ -1053,7 +1059,7 @@

nchans = 0;

- spin_lock(&devices_lock);
+ spin_lock_bh(&devices_lock);

list_for_each_entry_rcu(entry, &bt_6lowpan_devices, list) {
list_for_each_entry_rcu(peer, &entry->peers, list) {
@@ -1070,7 +1076,7 @@
}

done:
- spin_unlock(&devices_lock);
+ spin_unlock_bh(&devices_lock);

for (i = 0; i < nchans; ++i) {
l2cap_chan_close_unlocked(chans[i], ENOENT);
@@ -1195,7 +1201,7 @@
struct lowpan_btle_dev *entry;
struct lowpan_peer *peer;

- spin_lock(&devices_lock);
+ spin_lock_bh(&devices_lock);

list_for_each_entry(entry, &bt_6lowpan_devices, list) {
list_for_each_entry(peer, &entry->peers, list)
@@ -1203,7 +1209,7 @@
&peer->chan->dst, peer->chan->dst_type);
}

- spin_unlock(&devices_lock);
+ spin_unlock_bh(&devices_lock);

return 0;
}
@@ -1269,7 +1275,7 @@

switch (event) {
case NETDEV_UNREGISTER:
- spin_lock(&devices_lock);
+ spin_lock_bh(&devices_lock);
list_for_each_entry(entry, &bt_6lowpan_devices, list) {
if (entry->netdev == netdev) {
BT_DBG("Unregistered netdev %s %p",
@@ -1278,7 +1284,7 @@
break;
}
}
- spin_unlock(&devices_lock);
+ spin_unlock_bh(&devices_lock);
break;
}