* [PATCH 0/5] Bluetooth: 6LoWPAN lifecycle fixes
@ 2026-09-21 15:38 Cen Zhang
2026-09-21 15:38 ` [PATCH 1/5] Bluetooth: L2CAP: ignore close requests for deleted channels Cen Zhang
` (5 more replies)
0 siblings, 6 replies; 12+ messages in thread
From: Cen Zhang @ 2026-09-21 15:38 UTC (permalink / raw)
To: Tejun Heo, Lai Jiangshan, Marcel Holtmann,
Luiz Augusto von Dentz, Marco Elver, Jukka Rissanen
Cc: linux-kernel, linux-bluetooth, baijiaju1990, jjzuming, zzzccc427
6LoWPAN shares L2CAP channels between connection setup, published peers
and packet transmission, while teardown also schedules asynchronous
cleanup. These paths currently disagree on when channel ownership, peer
visibility and module lifetime end. The resulting failures range from
leaked or unusable connections and peers surviving disable to transmit
use-after-free and execution of unloaded module code.
This series establishes a consistent ownership and teardown order across
those paths. Shared L2CAP and workqueue changes provide the lifetime
guarantees needed by the following 6LoWPAN changes, which make failed setup
recoverable and stop users of a peer before releasing its resources.
Keeping the changes together avoids fixing one lifetime while shortening
another.
The timer support adds one ordered workqueue per L2CAP connection and
serializes that connection's channel timers.
Assisted-by: LLM
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH 1/5] Bluetooth: L2CAP: ignore close requests for deleted channels
2026-09-21 15:38 [PATCH 0/5] Bluetooth: 6LoWPAN lifecycle fixes Cen Zhang
@ 2026-09-21 15:38 ` Cen Zhang
2026-09-21 16:56 ` Pauli Virtanen
2026-09-21 15:38 ` [PATCH 2/5] workqueue: add support for module-owned work Cen Zhang
` (4 subsequent siblings)
5 siblings, 1 reply; 12+ messages in thread
From: Cen Zhang @ 2026-09-21 15:38 UTC (permalink / raw)
To: Tejun Heo, Lai Jiangshan, Marcel Holtmann,
Luiz Augusto von Dentz, Marco Elver, Jukka Rissanen
Cc: linux-kernel, linux-bluetooth, baijiaju1990, jjzuming, zzzccc427
l2cap_chan_del() keeps chan->conn alive until channel destruction and
marks removal with FLAG_DEL, but does not necessarily change the state.
A caller retaining a temporary reference can therefore enter
l2cap_chan_close_unlocked() after deletion and follow a stale connected
state, rearming the channel timer and sending another disconnection
request for a channel no longer on the connection list.
Check FLAG_DEL after taking the channel and connection locks so that
concurrent deletion is serialized with the decision to close. Return
without further timer or signaling work when the channel is deleted.
Fixes: b66774b48dd9 ("Bluetooth: L2CAP: Fix UAF in channel timeout by holding conn ref")
Assisted-by: LLM
Signed-off-by: Cen Zhang <zzzccc427@gmail.com>
---
net/bluetooth/l2cap_core.c | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
index 49a998804908..65e957fdc7ae 100644
--- a/net/bluetooth/l2cap_core.c
+++ b/net/bluetooth/l2cap_core.c
@@ -939,8 +939,10 @@ void l2cap_chan_close_unlocked(struct l2cap_chan *chan, int reason)
have_conn = l2cap_chan_lock_conn(chan);
- /* Context analysis: consider chan->conn->lock held also if conn NULL */
- context_unsafe(__l2cap_chan_close(chan, reason));
+ if (!test_bit(FLAG_DEL, &chan->flags)) {
+ /* Consider chan->conn->lock held also if conn NULL */
+ context_unsafe(__l2cap_chan_close(chan, reason));
+ }
l2cap_chan_unlock_conn(chan, have_conn);
}
base-commit: 019debf20bfd648b40ba10377ee0168db5eb241e
--
2.43.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH 2/5] workqueue: add support for module-owned work
2026-09-21 15:38 [PATCH 0/5] Bluetooth: 6LoWPAN lifecycle fixes Cen Zhang
2026-09-21 15:38 ` [PATCH 1/5] Bluetooth: L2CAP: ignore close requests for deleted channels Cen Zhang
@ 2026-09-21 15:38 ` Cen Zhang
2026-09-21 16:16 ` Tejun Heo
2026-09-21 15:39 ` [PATCH 3/5] Bluetooth: L2CAP: drain channel timers on connection teardown Cen Zhang
` (3 subsequent siblings)
5 siblings, 1 reply; 12+ messages in thread
From: Cen Zhang @ 2026-09-21 15:38 UTC (permalink / raw)
To: Tejun Heo, Lai Jiangshan, Marcel Holtmann,
Luiz Augusto von Dentz, Marco Elver, Jukka Rissanen
Cc: linux-kernel, linux-bluetooth, baijiaju1990, jjzuming, zzzccc427
Queueing a callback on a system workqueue does not take a reference to
the module containing that callback. A caller which releases its last
module reference after queueing work can therefore leave a callback in
unloaded text. Releasing the reference from the callback itself also
leaves its return path unprotected.
Add module_work and schedule_module_work() to hold the callback's owner
from queueing until the callback returns. Run the dispatch and final
module_put() in workqueue core, which remains present when the callback's
module is unloaded. Cache the function and owner before invoking the
callback so that it can free the containing work item.
6LoWPAN needs this for deferred network-device deletion after removing
the last peer.
Assisted-by: LLM
Signed-off-by: Cen Zhang <zzzccc427@gmail.com>
---
include/linux/workqueue.h | 15 ++++++++++++++
kernel/workqueue.c | 43 +++++++++++++++++++++++++++++++++++++++
2 files changed, 58 insertions(+)
diff --git a/include/linux/workqueue.h b/include/linux/workqueue.h
index c8a36423cb34..9920796c8822 100644
--- a/include/linux/workqueue.h
+++ b/include/linux/workqueue.h
@@ -128,6 +128,14 @@ struct rcu_work {
struct workqueue_struct *wq;
};
+struct module;
+
+struct module_work {
+ struct work_struct work;
+ struct module *owner;
+ work_func_t func;
+};
+
enum wq_affn_scope {
WQ_AFFN_DFL, /* use system default */
WQ_AFFN_CPU, /* one pod per CPU */
@@ -220,6 +228,11 @@ static inline struct rcu_work *to_rcu_work(struct work_struct *work)
return container_of(work, struct rcu_work, work);
}
+static inline struct module_work *to_module_work(struct work_struct *work)
+{
+ return container_of(work, struct module_work, work);
+}
+
struct execute_work {
struct work_struct work;
};
@@ -634,6 +647,8 @@ extern void __flush_workqueue(struct workqueue_struct *wq);
extern void drain_workqueue(struct workqueue_struct *wq);
extern int schedule_on_each_cpu(work_func_t func);
+bool schedule_module_work(struct module_work *mwork, work_func_t func,
+ struct module *owner);
int execute_in_process_context(work_func_t fn, struct execute_work *);
diff --git a/kernel/workqueue.c b/kernel/workqueue.c
index 1ae3732a2c51..1a16bc5dfb68 100644
--- a/kernel/workqueue.c
+++ b/kernel/workqueue.c
@@ -48,6 +48,7 @@
#include <linux/hashtable.h>
#include <linux/rculist.h>
#include <linux/nodemask.h>
+#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/uaccess.h>
#include <linux/sched/isolation.h>
@@ -4808,6 +4809,48 @@ int execute_in_process_context(work_func_t fn, struct execute_work *ew)
}
EXPORT_SYMBOL_GPL(execute_in_process_context);
+static void module_work_func(struct work_struct *work)
+{
+ struct module_work *mwork = to_module_work(work);
+ struct module *owner = mwork->owner;
+ work_func_t func = mwork->func;
+
+ func(work);
+ module_put(owner);
+}
+
+/**
+ * schedule_module_work - schedule work owned by a module
+ * @mwork: module work to schedule
+ * @func: work function to schedule
+ * @owner: module owning @func
+ *
+ * Take a reference to @owner before scheduling @func. The reference is
+ * released by workqueue core after the callback returns. The callback may
+ * free @mwork. @mwork must not be pending.
+ *
+ * Return: %false if the module is being removed or the work could not be
+ * queued, %true otherwise.
+ */
+bool schedule_module_work(struct module_work *mwork, work_func_t func,
+ struct module *owner)
+{
+ if (!try_module_get(owner))
+ return false;
+
+ INIT_WORK(&mwork->work, module_work_func);
+ mwork->owner = owner;
+ mwork->func = func;
+
+ if (!schedule_work(&mwork->work)) {
+ module_put(owner);
+ return false;
+ }
+
+ return true;
+}
+EXPORT_SYMBOL_GPL(schedule_module_work);
+
/**
* free_workqueue_attrs - free a workqueue_attrs
* @attrs: workqueue_attrs to free
--
2.43.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH 3/5] Bluetooth: L2CAP: drain channel timers on connection teardown
2026-09-21 15:38 [PATCH 0/5] Bluetooth: 6LoWPAN lifecycle fixes Cen Zhang
2026-09-21 15:38 ` [PATCH 1/5] Bluetooth: L2CAP: ignore close requests for deleted channels Cen Zhang
2026-09-21 15:38 ` [PATCH 2/5] workqueue: add support for module-owned work Cen Zhang
@ 2026-09-21 15:39 ` Cen Zhang
2026-09-21 15:54 ` Luiz Augusto von Dentz
2026-09-21 15:39 ` [PATCH 4/5] Bluetooth: 6lowpan: handle channel setup failure and callback lifetime Cen Zhang
` (2 subsequent siblings)
5 siblings, 1 reply; 12+ messages in thread
From: Cen Zhang @ 2026-09-21 15:39 UTC (permalink / raw)
To: Tejun Heo, Lai Jiangshan, Marcel Holtmann,
Luiz Augusto von Dentz, Marco Elver, Jukka Rissanen
Cc: linux-kernel, linux-bluetooth, baijiaju1990, jjzuming, zzzccc427
L2CAP channel timers run on system_percpu_wq. Connection deletion cancels
them without waiting because callbacks may need conn->lock, but HCI
unregister does not drain that workqueue after releasing the lock. A
running callback can therefore outlive HCI-driver unregister. If it drops
the last channel reference and releases a protocol module, it still has
to return through Bluetooth code after those dependencies are gone.
Individual channel deletion has another cancellation race. It cancels
timers before calling the socket teardown callback, which can wait for
sk lock. A concurrent recvmsg holding that lock can clear local busy and
rearm the monitor timer. Deletion then unlinks the channel, so connection
teardown can no longer find that delayed work through the channel list.
Give each connection an ordered timer workqueue. Serialize timer queueing
with channel and connection stop flags, and stop each channel before
canceling its timers or calling teardown. At connection deletion, stop
queueing for the connection, cancel all four timer types, then drop
conn->lock and drain running callbacks before releasing the connection.
This lets callbacks acquire their locks, observe FLAG_DEL and return
before HCI unregister completes.
The channel stop flag also ensures that no unlinked channel can leave
delayed work behind on the new queue. The queue remains allocated until
the drained connection is freed. This adds one workqueue per connection
and serializes that connection's channel timers.
Assisted-by: LLM
Signed-off-by: Cen Zhang <zzzccc427@gmail.com>
---
include/net/bluetooth/l2cap.h | 23 ++++++++++++++++++++++-
net/bluetooth/l2cap_core.c | 34 ++++++++++++++++++++++++++++++----
2 files changed, 52 insertions(+), 5 deletions(-)
diff --git a/include/net/bluetooth/l2cap.h b/include/net/bluetooth/l2cap.h
index efb9b7f422d1..b4af087a0a81 100644
--- a/include/net/bluetooth/l2cap.h
+++ b/include/net/bluetooth/l2cap.h
@@ -611,6 +611,7 @@ struct l2cap_chan {
void *data;
const struct l2cap_ops *ops;
+ bool timers_stopped; /* protected by conn->timer_lock */
struct mutex lock;
};
@@ -636,6 +637,10 @@ struct l2cap_conn {
struct sk_buff_head pending_rx;
struct work_struct pending_rx_work;
+ struct workqueue_struct *timer_workqueue;
+ spinlock_t timer_lock; /* protects timer scheduling */
+
+ bool timers_stopped __guarded_by(&timer_lock);
struct delayed_work id_addr_timer;
@@ -856,13 +861,29 @@ static inline void l2cap_chan_unlock(struct l2cap_chan *chan)
static inline void l2cap_set_timer(struct l2cap_chan *chan,
struct delayed_work *work, long timeout)
{
+ struct l2cap_conn *conn = chan->conn;
+ unsigned long flags;
+ bool pending;
+
BT_DBG("chan %p state %s timeout %ld", chan,
state_to_string(chan->state), timeout);
+ if (WARN_ON_ONCE(!conn))
+ return;
+
+ spin_lock_irqsave(&conn->timer_lock, flags);
+ if (conn->timers_stopped || chan->timers_stopped) {
+ spin_unlock_irqrestore(&conn->timer_lock, flags);
+ return;
+ }
+
l2cap_chan_hold(chan);
/* put(chan) if timer was already queued so it already has a ref */
- if (mod_delayed_work(system_percpu_wq, work, timeout))
+ pending = mod_delayed_work(conn->timer_workqueue, work, timeout);
+ spin_unlock_irqrestore(&conn->timer_lock, flags);
+
+ if (pending)
l2cap_chan_put(chan);
}
diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
index 65e957fdc7ae..0df7bda54473 100644
--- a/net/bluetooth/l2cap_core.c
+++ b/net/bluetooth/l2cap_core.c
@@ -686,9 +686,21 @@ void l2cap_chan_add(struct l2cap_conn *conn, struct l2cap_chan *chan)
void l2cap_chan_del(struct l2cap_chan *chan, int err)
{
+ struct l2cap_conn *conn = chan->conn;
+ unsigned long flags;
+
lockdep_assert(!chan->conn || lockdep_is_held(&chan->conn->lock));
+ if (conn) {
+ spin_lock_irqsave(&conn->timer_lock, flags);
+ chan->timers_stopped = true;
+ spin_unlock_irqrestore(&conn->timer_lock, flags);
+ }
+
__clear_chan_timer(chan);
+ __clear_retrans_timer(chan);
+ __clear_monitor_timer(chan);
+ __clear_ack_timer(chan);
BT_DBG("chan %p, err %d, state %s", chan, err,
state_to_string(chan->state));
@@ -723,10 +735,6 @@ void l2cap_chan_del(struct l2cap_chan *chan, int err)
break;
case L2CAP_MODE_ERTM:
- __clear_retrans_timer(chan);
- __clear_monitor_timer(chan);
- __clear_ack_timer(chan);
-
skb_queue_purge(&chan->srej_q);
l2cap_seq_list_free(&chan->srej_list);
@@ -1879,6 +1887,7 @@ static void l2cap_conn_del(struct hci_conn *hcon, int err)
{
struct l2cap_conn *conn = hcon->l2cap_data;
struct l2cap_chan *chan, *l;
+ unsigned long flags;
if (!conn)
return;
@@ -1891,6 +1900,9 @@ static void l2cap_conn_del(struct hci_conn *hcon, int err)
cancel_work_sync(&conn->pending_rx_work);
mutex_lock(&conn->lock);
+ spin_lock_irqsave(&conn->timer_lock, flags);
+ conn->timers_stopped = true;
+ spin_unlock_irqrestore(&conn->timer_lock, flags);
kfree_skb(conn->rx_skb);
@@ -1925,6 +1937,11 @@ static void l2cap_conn_del(struct hci_conn *hcon, int err)
spin_unlock(&hcon->proto_lock);
mutex_unlock(&conn->lock);
+
+ /* Channel deletion canceled pending timers. Drop conn->lock before
+ * waiting for running callbacks so they can acquire it and return.
+ */
+ drain_workqueue(conn->timer_workqueue);
l2cap_conn_put(conn);
}
@@ -1932,6 +1949,7 @@ static void l2cap_conn_free(struct kref *ref)
{
struct l2cap_conn *conn = container_of(ref, struct l2cap_conn, ref);
+ destroy_workqueue(conn->timer_workqueue);
hci_conn_put(conn->hcon);
kfree(conn);
}
@@ -7416,6 +7434,14 @@ static struct l2cap_conn *l2cap_conn_add(struct hci_conn *hcon)
return NULL;
}
+ conn->timer_workqueue = alloc_ordered_workqueue("l2cap", WQ_MEM_RECLAIM);
+ if (!conn->timer_workqueue) {
+ kfree(conn);
+ hci_chan_del(hchan);
+ return NULL;
+ }
+ spin_lock_init(&conn->timer_lock);
+
kref_init(&conn->ref);
conn->hchan = hchan;
--
2.43.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH 4/5] Bluetooth: 6lowpan: handle channel setup failure and callback lifetime
2026-09-21 15:38 [PATCH 0/5] Bluetooth: 6LoWPAN lifecycle fixes Cen Zhang
` (2 preceding siblings ...)
2026-09-21 15:39 ` [PATCH 3/5] Bluetooth: L2CAP: drain channel timers on connection teardown Cen Zhang
@ 2026-09-21 15:39 ` Cen Zhang
2026-09-21 15:39 ` [PATCH 5/5] Bluetooth: 6lowpan: quiesce peers before channel deletion Cen Zhang
2026-09-21 16:07 ` [PATCH 0/5] Bluetooth: 6LoWPAN lifecycle fixes Luiz Augusto von Dentz
5 siblings, 0 replies; 12+ messages in thread
From: Cen Zhang @ 2026-09-21 15:39 UTC (permalink / raw)
To: Tejun Heo, Lai Jiangshan, Marcel Holtmann,
Luiz Augusto von Dentz, Marco Elver, Jukka Rissanen
Cc: linux-kernel, linux-bluetooth, baijiaju1990, jjzuming, zzzccc427
L2CAP marks a channel connected before invoking its ready callback.
6LoWPAN can then fail to allocate or publish a peer, leaving an unusable
connected channel or a network device without a peer. Deletion before
peer publication also leaks the channel's initial reference: close only
releases it after finding a peer.
The peer's module reference ends too early as well. Removing the last
peer releases it while the close callback is still executing, allowing
bluetooth_6lowpan to be unloaded before that callback returns. Deferred
network-device deletion also needs protection through callback return.
Let ready return an error and handle it in the LE CoC and ECRED request
and response paths. Allocate the peer before setting up a network device,
and publish it before bringing the device up. Track ownership of the
initial reference explicitly and release it once from teardown, even
when no peer was published. Hold separate request-handler references
through rollback, unlock and response construction.
Hold the operations owner's module reference for accepted and outgoing
channels until channel destruction. Keep the listener itself unpinned so
module exit can close it; check BT_LISTEN under the parent lock and take
a temporary owner reference before invoking new_connection. Use
module-owned work for deferred network-device deletion.
A close/unload race produced this instruction-fetch fault:
[ 101.222759] BUG: unable to handle page fault for address: ffffffffc040199f
[ 101.224102] #PF: supervisor instruction fetch in kernel mode
[ 101.225119] #PF: error_code(0x0010) - not-present page
[ 101.226980] Oops: Oops: 0010 [#1] SMP KASAN NOPTI
[... omitted ...]
[ 101.232306] Workqueue: hci1 hci_rx_work
[ 101.233015] RIP: 0010:0xffffffffc040199f
[ 101.233780] Code: Unable to access opcode bytes at 0xffffffffc0401975.
[... omitted ...]
[ 101.275265] Modules linked in: [last unloaded: bluetooth_6lowpan(O)]
[ 101.276117] CR2: ffffffffc040199f
[... omitted ...]
[ 101.287138] Kernel panic - not syncing: Fatal exception
Fixes: 6b8d4a6a0314 ("Bluetooth: 6LoWPAN: Use connected oriented channel instead of fixed one")
Assisted-by: LLM
Signed-off-by: Cen Zhang <zzzccc427@gmail.com>
---
include/net/bluetooth/l2cap.h | 10 +++-
net/bluetooth/6lowpan.c | 86 ++++++++++++++++++++------------
net/bluetooth/l2cap_core.c | 94 +++++++++++++++++++++++++++++------
net/bluetooth/l2cap_sock.c | 6 ++-
net/bluetooth/smp.c | 4 +-
5 files changed, 148 insertions(+), 52 deletions(-)
diff --git a/include/net/bluetooth/l2cap.h b/include/net/bluetooth/l2cap.h
index b4af087a0a81..7194dc570ee8 100644
--- a/include/net/bluetooth/l2cap.h
+++ b/include/net/bluetooth/l2cap.h
@@ -611,6 +611,8 @@ struct l2cap_chan {
void *data;
const struct l2cap_ops *ops;
+ struct module *ops_owner;
+ bool ops_owner_pinned;
bool timers_stopped; /* protected by conn->timer_lock */
struct mutex lock;
};
@@ -669,7 +671,7 @@ struct l2cap_ops {
__must_hold(&chan->lock);
void (*state_change) (struct l2cap_chan *chan,
int state, int err);
- void (*ready) (struct l2cap_chan *chan)
+ int (*ready)(struct l2cap_chan *chan)
__must_hold(&chan->lock)
__must_hold(&chan->conn->lock);
void (*defer) (struct l2cap_chan *chan);
@@ -764,6 +766,7 @@ enum {
FLAG_ECRED_CONN_REQ_SENT,
FLAG_PENDING_SECURITY,
FLAG_HOLD_HCI_CONN,
+ FLAG_RELEASE_CREATOR,
FLAG_DEL,
};
@@ -948,8 +951,9 @@ static inline void l2cap_chan_no_close(struct l2cap_chan *chan)
{
}
-static inline void l2cap_chan_no_ready(struct l2cap_chan *chan)
+static inline int l2cap_chan_no_ready(struct l2cap_chan *chan)
{
+ return 0;
}
static inline void l2cap_chan_no_state_change(struct l2cap_chan *chan,
@@ -994,6 +998,8 @@ int l2cap_add_psm(struct l2cap_chan *chan, bdaddr_t *src, __le16 psm);
int l2cap_add_scid(struct l2cap_chan *chan, __u16 scid);
struct l2cap_chan *l2cap_chan_create(void);
+bool l2cap_chan_set_ops(struct l2cap_chan *chan,
+ const struct l2cap_ops *ops, struct module *owner);
void l2cap_chan_close_unlocked(struct l2cap_chan *chan, int reason)
__must_not_hold(&chan->lock);
int l2cap_chan_connect(struct l2cap_chan *chan, __le16 psm, u16 cid,
diff --git a/net/bluetooth/6lowpan.c b/net/bluetooth/6lowpan.c
index 836add41f5d1..5c49dc146086 100644
--- a/net/bluetooth/6lowpan.c
+++ b/net/bluetooth/6lowpan.c
@@ -52,6 +52,7 @@ static bool enable_6lowpan;
*/
static struct l2cap_chan *listen_chan;
static DEFINE_MUTEX(set_lock);
+static const struct l2cap_ops bt_6lowpan_chan_ops;
enum {
LOWPAN_PEER_CLOSING,
@@ -78,7 +79,7 @@ struct lowpan_btle_dev {
struct list_head peers;
atomic_t peer_count; /* number of items in peers list */
- struct work_struct delete_netdev;
+ struct module_work delete_netdev;
struct delayed_work notify_peers;
};
@@ -101,8 +102,6 @@ static inline bool peer_del(struct lowpan_btle_dev *dev,
list_del_rcu(&peer->list);
kfree_rcu(peer, rcu);
- module_put(THIS_MODULE);
-
if (atomic_dec_and_test(&dev->peer_count)) {
BT_DBG("last peer");
return true;
@@ -641,16 +640,10 @@ static struct l2cap_chan *chan_create(void)
return chan;
}
-static struct l2cap_chan *add_peer_chan(struct l2cap_chan *chan,
- struct lowpan_btle_dev *dev,
- bool new_netdev)
+static void add_peer_chan(struct l2cap_chan *chan,
+ struct lowpan_btle_dev *dev,
+ struct lowpan_peer *peer, bool new_netdev)
{
- struct lowpan_peer *peer;
-
- peer = kzalloc_obj(*peer, GFP_ATOMIC);
- if (!peer)
- return NULL;
-
peer->chan = chan;
baswap((void *)peer->lladdr, &chan->dst);
@@ -666,8 +659,6 @@ static struct l2cap_chan *add_peer_chan(struct l2cap_chan *chan,
if (new_netdev)
INIT_DELAYED_WORK(&dev->notify_peers, do_notify_peers);
schedule_delayed_work(&dev->notify_peers, msecs_to_jiffies(100));
-
- return peer->chan;
}
static int setup_netdev(struct l2cap_chan *chan, struct lowpan_btle_dev **dev)
@@ -721,30 +712,37 @@ static int setup_netdev(struct l2cap_chan *chan, struct lowpan_btle_dev **dev)
return err;
}
-static inline void chan_ready_cb(struct l2cap_chan *chan)
+static inline int chan_ready_cb(struct l2cap_chan *chan)
__must_hold(&chan->lock)
__must_hold(&chan->conn->lock)
{
struct lowpan_btle_dev *dev;
+ struct lowpan_peer *peer;
bool new_netdev = false;
+ int err;
+
+ peer = kzalloc_obj(*peer, GFP_ATOMIC);
+ if (!peer)
+ return -ENOMEM;
dev = lookup_dev(chan->conn);
BT_DBG("chan %p conn %p dev %p", chan, chan->conn, dev);
if (!dev) {
- if (setup_netdev(chan, &dev) < 0) {
- l2cap_chan_del(chan, -ENOENT);
- return;
- }
+ err = setup_netdev(chan, &dev);
+ if (err < 0)
+ goto free_peer;
new_netdev = true;
}
- if (!try_module_get(THIS_MODULE))
- return;
-
- add_peer_chan(chan, dev, new_netdev);
+ add_peer_chan(chan, dev, peer, new_netdev);
ifup(dev->netdev);
+ return 0;
+
+free_peer:
+ kfree(peer);
+ return err;
}
static void unregister_dev(struct lowpan_btle_dev *dev)
@@ -771,7 +769,7 @@ static void delete_netdev(struct work_struct *work)
{
struct lowpan_btle_dev *entry = container_of(work,
struct lowpan_btle_dev,
- delete_netdev);
+ delete_netdev.work);
unregister_dev(entry);
@@ -785,6 +783,7 @@ static void chan_close_cb(struct l2cap_chan *chan)
struct lowpan_peer *peer;
int err = -ENOENT;
bool last = false;
+ bool queued;
BT_DBG("chan %p conn %p", chan, chan->conn);
@@ -799,10 +798,6 @@ static void chan_close_cb(struct l2cap_chan *chan)
BT_DBG("dev %p removing %speer %p", dev,
last ? "last " : "1 ", peer);
- BT_DBG("chan %p orig refcnt %u", chan,
- kref_read(&chan->kref));
-
- l2cap_chan_put(chan);
break;
}
}
@@ -814,8 +809,9 @@ static void chan_close_cb(struct l2cap_chan *chan)
ifdown(dev->netdev);
- INIT_WORK(&entry->delete_netdev, delete_netdev);
- schedule_work(&entry->delete_netdev);
+ queued = schedule_module_work(&entry->delete_netdev,
+ delete_netdev, THIS_MODULE);
+ WARN_ON_ONCE(!queued);
} else {
spin_unlock(&devices_lock);
}
@@ -874,8 +870,27 @@ static long chan_get_sndtimeo_cb(struct l2cap_chan *chan)
return L2CAP_CONN_TIMEOUT;
}
+static int chan_new_connection_cb(struct l2cap_chan *chan,
+ struct l2cap_chan *new_chan)
+{
+ if (!l2cap_chan_set_ops(new_chan, &bt_6lowpan_chan_ops, THIS_MODULE))
+ return -ENODEV;
+
+ set_bit(FLAG_RELEASE_CREATOR, &new_chan->flags);
+ return 0;
+}
+
+static void chan_teardown_cb(struct l2cap_chan *chan, int err)
+{
+ chan->state = BT_CLOSED;
+
+ if (test_and_clear_bit(FLAG_RELEASE_CREATOR, &chan->flags))
+ l2cap_chan_put(chan);
+}
+
static const struct l2cap_ops bt_6lowpan_chan_ops = {
.name = "L2CAP 6LoWPAN channel",
+ .new_connection = chan_new_connection_cb,
.recv = chan_recv_cb,
.close = chan_close_cb,
.state_change = chan_state_change_cb,
@@ -885,7 +900,7 @@ static const struct l2cap_ops bt_6lowpan_chan_ops = {
.get_sndtimeo = chan_get_sndtimeo_cb,
.alloc_skb = chan_alloc_skb_cb,
- .teardown = l2cap_chan_no_teardown,
+ .teardown = chan_teardown_cb,
.defer = l2cap_chan_no_defer,
.set_shutdown = l2cap_chan_no_set_shutdown,
};
@@ -899,7 +914,12 @@ static int bt_6lowpan_connect(bdaddr_t *addr, u8 dst_type)
if (!chan)
return -EINVAL;
- chan->ops = &bt_6lowpan_chan_ops;
+ if (!l2cap_chan_set_ops(chan, &bt_6lowpan_chan_ops, THIS_MODULE)) {
+ l2cap_chan_put(chan);
+ return -ENODEV;
+ }
+
+ set_bit(FLAG_RELEASE_CREATOR, &chan->flags);
err = l2cap_chan_connect(chan, cpu_to_le16(L2CAP_PSM_IPSP), 0,
addr, dst_type, L2CAP_CONN_TIMEOUT);
@@ -952,7 +972,9 @@ static struct l2cap_chan *bt_6lowpan_listen(void)
if (!chan)
return NULL;
+ /* The listener is closed by module_exit(), so it must not self-pin. */
chan->ops = &bt_6lowpan_chan_ops;
+ chan->ops_owner = THIS_MODULE;
chan->state = BT_LISTEN;
chan->src_type = BDADDR_LE_PUBLIC;
diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
index 0df7bda54473..f6a87a44d8a6 100644
--- a/net/bluetooth/l2cap_core.c
+++ b/net/bluetooth/l2cap_core.c
@@ -481,9 +481,27 @@ struct l2cap_chan *l2cap_chan_create(void)
}
EXPORT_SYMBOL_GPL(l2cap_chan_create);
+bool l2cap_chan_set_ops(struct l2cap_chan *chan,
+ const struct l2cap_ops *ops, struct module *owner)
+{
+ if (WARN_ON_ONCE(chan->ops_owner))
+ return false;
+
+ if (!try_module_get(owner))
+ return false;
+
+ chan->ops = ops;
+ chan->ops_owner = owner;
+ chan->ops_owner_pinned = true;
+ return true;
+}
+EXPORT_SYMBOL_GPL(l2cap_chan_set_ops);
+
static void l2cap_chan_destroy(struct kref *kref)
{
struct l2cap_chan *chan = container_of(kref, struct l2cap_chan, kref);
+ struct module *ops_owner = chan->ops_owner;
+ bool ops_owner_pinned = chan->ops_owner_pinned;
BT_DBG("chan %p", chan);
@@ -495,6 +513,8 @@ static void l2cap_chan_destroy(struct kref *kref)
l2cap_conn_put(chan->conn);
kfree(chan);
+ if (ops_owner_pinned)
+ module_put(ops_owner);
}
void l2cap_chan_hold(struct l2cap_chan *c)
@@ -1352,7 +1372,7 @@ void l2cap_send_conn_req(struct l2cap_chan *chan)
l2cap_send_cmd(conn, chan->ident, L2CAP_CONN_REQ, sizeof(req), &req);
}
-static void l2cap_chan_ready(struct l2cap_chan *chan)
+static int l2cap_chan_ready(struct l2cap_chan *chan)
__must_hold(&chan->lock)
__must_hold(&chan->conn->lock)
{
@@ -1361,7 +1381,7 @@ static void l2cap_chan_ready(struct l2cap_chan *chan)
* procedure is complete.
*/
if (chan->state == BT_CONNECTED)
- return;
+ return 0;
/* This clears all conf flags, including CONF_NOT_COMPLETE */
chan->conf_state = 0;
@@ -1377,7 +1397,7 @@ static void l2cap_chan_ready(struct l2cap_chan *chan)
chan->state = BT_CONNECTED;
- chan->ops->ready(chan);
+ return chan->ops->ready(chan);
}
static void l2cap_le_connect(struct l2cap_chan *chan)
@@ -4241,10 +4261,20 @@ static struct l2cap_chan *l2cap_new_connection(struct l2cap_conn *conn,
__must_hold(&pchan->lock)
{
struct l2cap_chan *chan;
+ struct module *owner;
+
+ if (pchan->state != BT_LISTEN)
+ return NULL;
+
+ owner = pchan->ops_owner;
+ if (!try_module_get(owner))
+ return NULL;
chan = l2cap_chan_create();
- if (!chan)
+ if (!chan) {
+ module_put(owner);
return NULL;
+ }
l2cap_chan_lock(chan);
@@ -4260,10 +4290,12 @@ static struct l2cap_chan *l2cap_new_connection(struct l2cap_conn *conn,
l2cap_chan_del(chan, 0);
l2cap_chan_unlock(chan);
l2cap_chan_put(chan);
+ module_put(owner);
return NULL;
}
l2cap_chan_unlock(chan);
+ module_put(owner);
return chan;
}
@@ -5011,7 +5043,7 @@ static int l2cap_le_connect_rsp(struct l2cap_conn *conn,
struct hci_conn *hcon = conn->hcon;
u16 dcid, mtu, mps, credits, result;
struct l2cap_chan *chan;
- int err, sec_level;
+ int err, ready_err, sec_level;
if (cmd_len < sizeof(*rsp))
return -EPROTO;
@@ -5056,7 +5088,11 @@ static int l2cap_le_connect_rsp(struct l2cap_conn *conn,
chan->omtu = mtu;
chan->remote_mps = mps;
chan->tx_credits = credits;
- l2cap_chan_ready(chan);
+ ready_err = l2cap_chan_ready(chan);
+ if (ready_err < 0) {
+ l2cap_send_disconn_req(chan, -ready_err);
+ l2cap_chan_del(chan, -ready_err);
+ }
break;
case L2CAP_CR_LE_AUTHENTICATION:
@@ -5180,9 +5216,10 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
{
struct l2cap_le_conn_req *req = (struct l2cap_le_conn_req *) data;
struct l2cap_le_conn_rsp rsp;
- struct l2cap_chan *chan, *pchan;
+ struct l2cap_chan *chan, *chan_ref = NULL, *pchan;
u16 dcid, scid, credits, mtu, mps;
__le16 psm;
+ int err;
u8 result;
if (cmd_len != sizeof(*req))
@@ -5260,6 +5297,9 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
goto response_unlock;
}
+ /* ->ready() may delete the channel. */
+ l2cap_chan_hold(chan);
+ chan_ref = chan;
l2cap_chan_lock(chan);
lockdep_assert_held(&chan->conn->lock);
@@ -5293,18 +5333,26 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
result = L2CAP_CR_PEND;
chan->ops->defer(chan);
} else {
- l2cap_chan_ready(chan);
- result = L2CAP_CR_LE_SUCCESS;
+ err = l2cap_chan_ready(chan);
+ if (err < 0) {
+ l2cap_chan_del(chan, -err);
+ chan = NULL;
+ dcid = 0;
+ credits = 0;
+ result = L2CAP_CR_LE_NO_MEM;
+ } else {
+ result = L2CAP_CR_LE_SUCCESS;
+ }
}
- l2cap_chan_unlock(chan);
+ l2cap_chan_unlock(chan_ref);
response_unlock:
l2cap_chan_unlock(pchan);
l2cap_chan_put(pchan);
if (result == L2CAP_CR_PEND)
- return 0;
+ goto done;
response:
if (chan) {
@@ -5321,6 +5369,10 @@ static int l2cap_le_connect_req(struct l2cap_conn *conn,
l2cap_send_cmd(conn, cmd->ident, L2CAP_LE_CONN_RSP, sizeof(rsp), &rsp);
+done:
+ if (chan_ref)
+ l2cap_chan_put(chan_ref);
+
return 0;
}
@@ -5385,7 +5437,7 @@ static inline int l2cap_ecred_conn_req(struct l2cap_conn *conn,
u16 mtu, mps;
__le16 psm;
u8 result, rsp_len = 0;
- int i, num_scid = 0;
+ int err, i, num_scid = 0;
bool defer = false;
if (!enable_ecred)
@@ -5493,6 +5545,8 @@ static inline int l2cap_ecred_conn_req(struct l2cap_conn *conn,
continue;
}
+ /* ->ready() may delete the channel. */
+ l2cap_chan_hold(chan);
l2cap_chan_lock(chan);
lockdep_assert_held(&chan->conn->lock);
@@ -5527,10 +5581,16 @@ static inline int l2cap_ecred_conn_req(struct l2cap_conn *conn,
defer = true;
chan->ops->defer(chan);
} else {
- l2cap_chan_ready(chan);
+ err = l2cap_chan_ready(chan);
+ if (err < 0) {
+ l2cap_chan_del(chan, -err);
+ pdu->dcid[i] = 0;
+ result = L2CAP_CR_LE_NO_MEM;
+ }
}
l2cap_chan_unlock(chan);
+ l2cap_chan_put(chan);
}
unlock:
@@ -5558,7 +5618,7 @@ static inline int l2cap_ecred_conn_rsp(struct l2cap_conn *conn,
struct hci_conn *hcon = conn->hcon;
u16 mtu, mps, credits, result;
struct l2cap_chan *chan, *tmp;
- int err = 0, sec_level;
+ int err = 0, ready_err, sec_level;
int i = 0;
if (cmd_len < sizeof(*rsp))
@@ -5673,7 +5733,11 @@ static inline int l2cap_ecred_conn_rsp(struct l2cap_conn *conn,
chan->omtu = mtu;
chan->remote_mps = mps;
chan->tx_credits = credits;
- l2cap_chan_ready(chan);
+ ready_err = l2cap_chan_ready(chan);
+ if (ready_err < 0) {
+ l2cap_send_disconn_req(chan, -ready_err);
+ l2cap_chan_del(chan, -ready_err);
+ }
break;
}
diff --git a/net/bluetooth/l2cap_sock.c b/net/bluetooth/l2cap_sock.c
index 278adb05c4c9..7a631581950f 100644
--- a/net/bluetooth/l2cap_sock.c
+++ b/net/bluetooth/l2cap_sock.c
@@ -1825,13 +1825,13 @@ static struct sk_buff *l2cap_sock_alloc_skb_cb(struct l2cap_chan *chan,
return skb;
}
-static void l2cap_sock_ready_cb(struct l2cap_chan *chan)
+static int l2cap_sock_ready_cb(struct l2cap_chan *chan)
{
struct sock *sk = chan->data;
struct sock *parent;
if (!sk)
- return;
+ return 0;
lock_sock(sk);
@@ -1846,6 +1846,8 @@ static void l2cap_sock_ready_cb(struct l2cap_chan *chan)
parent->sk_data_ready(parent);
release_sock(sk);
+
+ return 0;
}
static void l2cap_sock_defer_cb(struct l2cap_chan *chan)
diff --git a/net/bluetooth/smp.c b/net/bluetooth/smp.c
index 0badb93a5725..28e32e8cf3b6 100644
--- a/net/bluetooth/smp.c
+++ b/net/bluetooth/smp.c
@@ -3155,7 +3155,7 @@ static void smp_resume_cb(struct l2cap_chan *chan)
smp_distribute_keys(smp);
}
-static void smp_ready_cb(struct l2cap_chan *chan)
+static int smp_ready_cb(struct l2cap_chan *chan)
{
struct l2cap_conn *conn = chan->conn;
struct hci_conn *hcon = conn->hcon;
@@ -3172,6 +3172,8 @@ static void smp_ready_cb(struct l2cap_chan *chan)
if (hcon->type == ACL_LINK && test_bit(HCI_CONN_ENCRYPT, &hcon->flags))
bredr_pairing(chan);
+
+ return 0;
}
static int smp_recv_cb(struct l2cap_chan *chan, struct sk_buff *skb)
--
2.43.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH 5/5] Bluetooth: 6lowpan: quiesce peers before channel deletion
2026-09-21 15:38 [PATCH 0/5] Bluetooth: 6LoWPAN lifecycle fixes Cen Zhang
` (3 preceding siblings ...)
2026-09-21 15:39 ` [PATCH 4/5] Bluetooth: 6lowpan: handle channel setup failure and callback lifetime Cen Zhang
@ 2026-09-21 15:39 ` Cen Zhang
2026-09-21 16:07 ` [PATCH 0/5] Bluetooth: 6LoWPAN lifecycle fixes Luiz Augusto von Dentz
5 siblings, 0 replies; 12+ messages in thread
From: Cen Zhang @ 2026-09-21 15:39 UTC (permalink / raw)
To: Tejun Heo, Lai Jiangshan, Marcel Holtmann,
Luiz Augusto von Dentz, Marco Elver, Jukka Rissanen
Cc: linux-kernel, linux-bluetooth, baijiaju1990, jjzuming, zzzccc427
6LoWPAN removes a peer and drops its channel reference from the close
callback. A transmitter which already observed the RCU-published peer
can still use the freed channel. Keeping the allocation alive alone is
insufficient: a transmitter which passed the deletion check can enqueue
a packet after L2CAP has purged the transmit queue.
Move peer cleanup to teardown, before FLAG_DEL and the transmit purge.
Mark the channel closed, unlink the peer under devices_lock, then drop
the lock and wait for network RCU readers with synchronize_net(). Free
the peer and release the initial channel reference only after those
transmitters have returned. Queue last-peer network-device deletion
before releasing the channel's ownership reference.
Disabling 6LoWPAN must also close the listener before sweeping existing
peers. Otherwise a request holding the old listener can publish a child
after the sweep. Serialize the transition with set_lock: requests which
complete admission before listener teardown are included in the sweep,
and requests which reach the closed listener are rejected.
The transmit/removal race produced this report:
[ 59.413897] BUG: KASAN: slab-use-after-free in send_pkt+0x3b1/0x3e0
[ 59.415014] Write of size 8 at addr ffff88810cf0e4a0 by task python3/583
[ ... report excerpt omitted ... ]
[ 59.490444] Freed by task 504:
[ ... report excerpt omitted ... ]
[ 59.493046] kfree+0x307/0x580
[ 59.493497] l2cap_chan_put+0x273/0x3a0
[ 59.494020] l2cap_disconnect_req+0x613/0x890
[ ... report excerpt omitted ... ]
Fixes: 6b8d4a6a0314 ("Bluetooth: 6LoWPAN: Use connected oriented channel instead of fixed one")
Assisted-by: LLM
Signed-off-by: Cen Zhang <zzzccc427@gmail.com>
---
net/bluetooth/6lowpan.c | 64 +++++++++++++++++++++--------------------
1 file changed, 33 insertions(+), 31 deletions(-)
diff --git a/net/bluetooth/6lowpan.c b/net/bluetooth/6lowpan.c
index 5c49dc146086..60d2c6d1be99 100644
--- a/net/bluetooth/6lowpan.c
+++ b/net/bluetooth/6lowpan.c
@@ -61,7 +61,6 @@ enum {
struct lowpan_peer {
struct list_head list;
- struct rcu_head rcu;
struct l2cap_chan *chan;
/* peer addresses in various formats */
@@ -100,7 +99,6 @@ static inline bool peer_del(struct lowpan_btle_dev *dev,
struct lowpan_peer *peer)
{
list_del_rcu(&peer->list);
- kfree_rcu(peer, rcu);
if (atomic_dec_and_test(&dev->peer_count)) {
BT_DBG("last peer");
@@ -776,16 +774,15 @@ static void delete_netdev(struct work_struct *work)
/* The entry pointer is deleted by the netdev destructor. */
}
-static void chan_close_cb(struct l2cap_chan *chan)
+static void chan_teardown_cb(struct l2cap_chan *chan, int err)
{
struct lowpan_btle_dev *entry;
struct lowpan_btle_dev *dev = NULL;
- struct lowpan_peer *peer;
- int err = -ENOENT;
+ struct lowpan_peer *peer = NULL;
bool last = false;
- bool queued;
BT_DBG("chan %p conn %p", chan, chan->conn);
+ chan->state = BT_CLOSED;
spin_lock(&devices_lock);
@@ -794,7 +791,6 @@ static void chan_close_cb(struct l2cap_chan *chan)
peer = __peer_lookup_chan(dev, chan);
if (peer) {
last = peer_del(dev, peer);
- err = 0;
BT_DBG("dev %p removing %speer %p", dev,
last ? "last " : "1 ", peer);
@@ -802,19 +798,28 @@ static void chan_close_cb(struct l2cap_chan *chan)
}
}
- if (!err && last && dev && !atomic_read(&dev->peer_count)) {
- spin_unlock(&devices_lock);
+ spin_unlock(&devices_lock);
- cancel_delayed_work_sync(&dev->notify_peers);
+ if (peer) {
+ /* ndo_start_xmit() holds network RCU while using peer->chan. */
+ synchronize_net();
+ kfree(peer);
- ifdown(dev->netdev);
+ if (last && dev) {
+ bool queued;
- queued = schedule_module_work(&entry->delete_netdev,
- delete_netdev, THIS_MODULE);
- WARN_ON_ONCE(!queued);
- } else {
- spin_unlock(&devices_lock);
+ cancel_delayed_work_sync(&dev->notify_peers);
+
+ ifdown(dev->netdev);
+
+ queued = schedule_module_work(&entry->delete_netdev,
+ delete_netdev, THIS_MODULE);
+ WARN_ON_ONCE(!queued);
+ }
}
+
+ if (test_and_clear_bit(FLAG_RELEASE_CREATOR, &chan->flags))
+ l2cap_chan_put(chan);
}
static void chan_state_change_cb(struct l2cap_chan *chan, int state, int err)
@@ -873,6 +878,9 @@ static long chan_get_sndtimeo_cb(struct l2cap_chan *chan)
static int chan_new_connection_cb(struct l2cap_chan *chan,
struct l2cap_chan *new_chan)
{
+ if (chan->state != BT_LISTEN)
+ return -EINVAL;
+
if (!l2cap_chan_set_ops(new_chan, &bt_6lowpan_chan_ops, THIS_MODULE))
return -ENODEV;
@@ -880,19 +888,11 @@ static int chan_new_connection_cb(struct l2cap_chan *chan,
return 0;
}
-static void chan_teardown_cb(struct l2cap_chan *chan, int err)
-{
- chan->state = BT_CLOSED;
-
- if (test_and_clear_bit(FLAG_RELEASE_CREATOR, &chan->flags))
- l2cap_chan_put(chan);
-}
-
static const struct l2cap_ops bt_6lowpan_chan_ops = {
.name = "L2CAP 6LoWPAN channel",
.new_connection = chan_new_connection_cb,
.recv = chan_recv_cb,
- .close = chan_close_cb,
+ .close = l2cap_chan_no_close,
.state_change = chan_state_change_cb,
.ready = chan_ready_cb,
.resume = chan_resume_cb,
@@ -1103,20 +1103,22 @@ static void disconnect_all_peers(void)
static void do_enable_set(bool flag)
{
- if (!flag || enable_6lowpan != flag)
- /* Disconnect existing connections if 6lowpan is
- * disabled
- */
- disconnect_all_peers();
+ bool disconnect;
+
+ mutex_lock(&set_lock);
+ disconnect = !flag || enable_6lowpan != flag;
enable_6lowpan = flag;
- mutex_lock(&set_lock);
if (listen_chan) {
l2cap_chan_close_unlocked(listen_chan, 0);
l2cap_chan_put(listen_chan);
+ listen_chan = NULL;
}
+ if (disconnect)
+ disconnect_all_peers();
+
listen_chan = bt_6lowpan_listen();
mutex_unlock(&set_lock);
}
--
2.43.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH 3/5] Bluetooth: L2CAP: drain channel timers on connection teardown
2026-09-21 15:39 ` [PATCH 3/5] Bluetooth: L2CAP: drain channel timers on connection teardown Cen Zhang
@ 2026-09-21 15:54 ` Luiz Augusto von Dentz
0 siblings, 0 replies; 12+ messages in thread
From: Luiz Augusto von Dentz @ 2026-09-21 15:54 UTC (permalink / raw)
To: Cen Zhang
Cc: Tejun Heo, Lai Jiangshan, Marcel Holtmann, Marco Elver,
Jukka Rissanen, linux-kernel, linux-bluetooth, baijiaju1990,
jjzuming
Hi Cen,
On Mon, Sep 21, 2026 at 11:39 AM Cen Zhang <zzzccc427@gmail.com> wrote:
>
> L2CAP channel timers run on system_percpu_wq. Connection deletion cancels
> them without waiting because callbacks may need conn->lock, but HCI
> unregister does not drain that workqueue after releasing the lock. A
> running callback can therefore outlive HCI-driver unregister. If it drops
> the last channel reference and releases a protocol module, it still has
> to return through Bluetooth code after those dependencies are gone.
>
> Individual channel deletion has another cancellation race. It cancels
> timers before calling the socket teardown callback, which can wait for
> sk lock. A concurrent recvmsg holding that lock can clear local busy and
> rearm the monitor timer. Deletion then unlinks the channel, so connection
> teardown can no longer find that delayed work through the channel list.
How about disabling the work instead of introducing another workqueue for it?
> Give each connection an ordered timer workqueue. Serialize timer queueing
> with channel and connection stop flags, and stop each channel before
> canceling its timers or calling teardown. At connection deletion, stop
> queueing for the connection, cancel all four timer types, then drop
> conn->lock and drain running callbacks before releasing the connection.
> This lets callbacks acquire their locks, observe FLAG_DEL and return
> before HCI unregister completes.
>
> The channel stop flag also ensures that no unlinked channel can leave
> delayed work behind on the new queue. The queue remains allocated until
> the drained connection is freed. This adds one workqueue per connection
> and serializes that connection's channel timers.
>
> Assisted-by: LLM
> Signed-off-by: Cen Zhang <zzzccc427@gmail.com>
> ---
> include/net/bluetooth/l2cap.h | 23 ++++++++++++++++++++++-
> net/bluetooth/l2cap_core.c | 34 ++++++++++++++++++++++++++++++----
> 2 files changed, 52 insertions(+), 5 deletions(-)
>
> diff --git a/include/net/bluetooth/l2cap.h b/include/net/bluetooth/l2cap.h
> index efb9b7f422d1..b4af087a0a81 100644
> --- a/include/net/bluetooth/l2cap.h
> +++ b/include/net/bluetooth/l2cap.h
> @@ -611,6 +611,7 @@ struct l2cap_chan {
>
> void *data;
> const struct l2cap_ops *ops;
> + bool timers_stopped; /* protected by conn->timer_lock */
> struct mutex lock;
> };
>
> @@ -636,6 +637,10 @@ struct l2cap_conn {
>
> struct sk_buff_head pending_rx;
> struct work_struct pending_rx_work;
> + struct workqueue_struct *timer_workqueue;
> + spinlock_t timer_lock; /* protects timer scheduling */
> +
> + bool timers_stopped __guarded_by(&timer_lock);
>
> struct delayed_work id_addr_timer;
>
> @@ -856,13 +861,29 @@ static inline void l2cap_chan_unlock(struct l2cap_chan *chan)
> static inline void l2cap_set_timer(struct l2cap_chan *chan,
> struct delayed_work *work, long timeout)
> {
> + struct l2cap_conn *conn = chan->conn;
> + unsigned long flags;
> + bool pending;
> +
> BT_DBG("chan %p state %s timeout %ld", chan,
> state_to_string(chan->state), timeout);
>
> + if (WARN_ON_ONCE(!conn))
> + return;
> +
> + spin_lock_irqsave(&conn->timer_lock, flags);
> + if (conn->timers_stopped || chan->timers_stopped) {
> + spin_unlock_irqrestore(&conn->timer_lock, flags);
> + return;
> + }
> +
> l2cap_chan_hold(chan);
>
> /* put(chan) if timer was already queued so it already has a ref */
> - if (mod_delayed_work(system_percpu_wq, work, timeout))
> + pending = mod_delayed_work(conn->timer_workqueue, work, timeout);
> + spin_unlock_irqrestore(&conn->timer_lock, flags);
> +
> + if (pending)
> l2cap_chan_put(chan);
> }
>
> diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
> index 65e957fdc7ae..0df7bda54473 100644
> --- a/net/bluetooth/l2cap_core.c
> +++ b/net/bluetooth/l2cap_core.c
> @@ -686,9 +686,21 @@ void l2cap_chan_add(struct l2cap_conn *conn, struct l2cap_chan *chan)
>
> void l2cap_chan_del(struct l2cap_chan *chan, int err)
> {
> + struct l2cap_conn *conn = chan->conn;
> + unsigned long flags;
> +
> lockdep_assert(!chan->conn || lockdep_is_held(&chan->conn->lock));
>
> + if (conn) {
> + spin_lock_irqsave(&conn->timer_lock, flags);
> + chan->timers_stopped = true;
> + spin_unlock_irqrestore(&conn->timer_lock, flags);
> + }
> +
> __clear_chan_timer(chan);
> + __clear_retrans_timer(chan);
> + __clear_monitor_timer(chan);
> + __clear_ack_timer(chan);
>
> BT_DBG("chan %p, err %d, state %s", chan, err,
> state_to_string(chan->state));
> @@ -723,10 +735,6 @@ void l2cap_chan_del(struct l2cap_chan *chan, int err)
> break;
>
> case L2CAP_MODE_ERTM:
> - __clear_retrans_timer(chan);
> - __clear_monitor_timer(chan);
> - __clear_ack_timer(chan);
> -
> skb_queue_purge(&chan->srej_q);
>
> l2cap_seq_list_free(&chan->srej_list);
> @@ -1879,6 +1887,7 @@ static void l2cap_conn_del(struct hci_conn *hcon, int err)
> {
> struct l2cap_conn *conn = hcon->l2cap_data;
> struct l2cap_chan *chan, *l;
> + unsigned long flags;
>
> if (!conn)
> return;
> @@ -1891,6 +1900,9 @@ static void l2cap_conn_del(struct hci_conn *hcon, int err)
> cancel_work_sync(&conn->pending_rx_work);
>
> mutex_lock(&conn->lock);
> + spin_lock_irqsave(&conn->timer_lock, flags);
> + conn->timers_stopped = true;
> + spin_unlock_irqrestore(&conn->timer_lock, flags);
>
> kfree_skb(conn->rx_skb);
>
> @@ -1925,6 +1937,11 @@ static void l2cap_conn_del(struct hci_conn *hcon, int err)
> spin_unlock(&hcon->proto_lock);
>
> mutex_unlock(&conn->lock);
> +
> + /* Channel deletion canceled pending timers. Drop conn->lock before
> + * waiting for running callbacks so they can acquire it and return.
> + */
> + drain_workqueue(conn->timer_workqueue);
> l2cap_conn_put(conn);
> }
>
> @@ -1932,6 +1949,7 @@ static void l2cap_conn_free(struct kref *ref)
> {
> struct l2cap_conn *conn = container_of(ref, struct l2cap_conn, ref);
>
> + destroy_workqueue(conn->timer_workqueue);
> hci_conn_put(conn->hcon);
> kfree(conn);
> }
> @@ -7416,6 +7434,14 @@ static struct l2cap_conn *l2cap_conn_add(struct hci_conn *hcon)
> return NULL;
> }
>
> + conn->timer_workqueue = alloc_ordered_workqueue("l2cap", WQ_MEM_RECLAIM);
> + if (!conn->timer_workqueue) {
> + kfree(conn);
> + hci_chan_del(hchan);
> + return NULL;
> + }
> + spin_lock_init(&conn->timer_lock);
> +
> kref_init(&conn->ref);
> conn->hchan = hchan;
>
> --
> 2.43.0
>
--
Luiz Augusto von Dentz
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH 0/5] Bluetooth: 6LoWPAN lifecycle fixes
2026-09-21 15:38 [PATCH 0/5] Bluetooth: 6LoWPAN lifecycle fixes Cen Zhang
` (4 preceding siblings ...)
2026-09-21 15:39 ` [PATCH 5/5] Bluetooth: 6lowpan: quiesce peers before channel deletion Cen Zhang
@ 2026-09-21 16:07 ` Luiz Augusto von Dentz
2026-09-21 16:59 ` Cen Zhang
5 siblings, 1 reply; 12+ messages in thread
From: Luiz Augusto von Dentz @ 2026-09-21 16:07 UTC (permalink / raw)
To: Cen Zhang
Cc: Tejun Heo, Lai Jiangshan, Marcel Holtmann, Marco Elver,
Jukka Rissanen, linux-kernel, linux-bluetooth, baijiaju1990,
jjzuming
Hi Cen,
On Mon, Sep 21, 2026 at 11:39 AM Cen Zhang <zzzccc427@gmail.com> wrote:
>
> 6LoWPAN shares L2CAP channels between connection setup, published peers
> and packet transmission, while teardown also schedules asynchronous
> cleanup. These paths currently disagree on when channel ownership, peer
> visibility and module lifetime end. The resulting failures range from
> leaked or unusable connections and peers surviving disable to transmit
> use-after-free and execution of unloaded module code.
>
> This series establishes a consistent ownership and teardown order across
> those paths. Shared L2CAP and workqueue changes provide the lifetime
> guarantees needed by the following 6LoWPAN changes, which make failed setup
> recoverable and stop users of a peer before releasing its resources.
> Keeping the changes together avoids fixing one lifetime while shortening
> another.
>
> The timer support adds one ordered workqueue per L2CAP connection and
> serializes that connection's channel timers.
>
> Assisted-by: LLM
Something suggests none of this was tested, as IPSP was never
mentioned once in this set.
--
Luiz Augusto von Dentz
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH 2/5] workqueue: add support for module-owned work
2026-09-21 15:38 ` [PATCH 2/5] workqueue: add support for module-owned work Cen Zhang
@ 2026-09-21 16:16 ` Tejun Heo
2026-09-21 17:03 ` Cen Zhang
0 siblings, 1 reply; 12+ messages in thread
From: Tejun Heo @ 2026-09-21 16:16 UTC (permalink / raw)
To: Cen Zhang
Cc: Lai Jiangshan, Marcel Holtmann, Luiz Augusto von Dentz,
Marco Elver, Jukka Rissanen, linux-kernel, linux-bluetooth,
baijiaju1990, jjzuming
On Mon, Sep 21, 2026 at 11:38:59PM +0800, Cen Zhang wrote:
> Queueing a callback on a system workqueue does not take a reference to
> the module containing that callback. A caller which releases its last
> module reference after queueing work can therefore leave a callback in
> unloaded text. Releasing the reference from the callback itself also
> leaves its return path unprotected.
>
> Add module_work and schedule_module_work() to hold the callback's owner
> from queueing until the callback returns. Run the dispatch and final
> module_put() in workqueue core, which remains present when the callback's
> module is unloaded. Cache the function and owner before invoking the
> callback so that it can free the containing work item.
>
> 6LoWPAN needs this for deferred network-device deletion after removing
> the last peer.
>
> Assisted-by: LLM
> Signed-off-by: Cen Zhang <zzzccc427@gmail.com>
Please provide justification on why this needs to be done this way when
everyone else can flush delayed items before unloading.
Thanks.
--
tejun
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH 1/5] Bluetooth: L2CAP: ignore close requests for deleted channels
2026-09-21 15:38 ` [PATCH 1/5] Bluetooth: L2CAP: ignore close requests for deleted channels Cen Zhang
@ 2026-09-21 16:56 ` Pauli Virtanen
0 siblings, 0 replies; 12+ messages in thread
From: Pauli Virtanen @ 2026-09-21 16:56 UTC (permalink / raw)
To: Cen Zhang, Tejun Heo, Lai Jiangshan, Marcel Holtmann,
Luiz Augusto von Dentz, Marco Elver, Jukka Rissanen
Cc: linux-kernel, linux-bluetooth, baijiaju1990, jjzuming
Hi,
ma, 2026-09-21 kello 23:38 +0800, Cen Zhang kirjoitti:
> l2cap_chan_del() keeps chan->conn alive until channel destruction and
> marks removal with FLAG_DEL, but does not necessarily change the state.
> A caller retaining a temporary reference can therefore enter
> l2cap_chan_close_unlocked() after deletion and follow a stale connected
> state, rearming the channel timer and sending another disconnection
> request for a channel no longer on the connection list.
>
> Check FLAG_DEL after taking the channel and connection locks so that
> concurrent deletion is serialized with the decision to close. Return
> without further timer or signaling work when the channel is deleted.
>
> Fixes: b66774b48dd9 ("Bluetooth: L2CAP: Fix UAF in channel timeout by holding conn ref")
> Assisted-by: LLM
> Signed-off-by: Cen Zhang <zzzccc427@gmail.com>
> ---
> net/bluetooth/l2cap_core.c | 6 ++++--
> 1 file changed, 4 insertions(+), 2 deletions(-)
>
> diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
> index 49a998804908..65e957fdc7ae 100644
> --- a/net/bluetooth/l2cap_core.c
> +++ b/net/bluetooth/l2cap_core.c
> @@ -939,8 +939,10 @@ void l2cap_chan_close_unlocked(struct l2cap_chan *chan, int reason)
>
> have_conn = l2cap_chan_lock_conn(chan);
>
> - /* Context analysis: consider chan->conn->lock held also if conn NULL */
> - context_unsafe(__l2cap_chan_close(chan, reason));
> + if (!test_bit(FLAG_DEL, &chan->flags)) {
> + /* Consider chan->conn->lock held also if conn NULL */
> + context_unsafe(__l2cap_chan_close(chan, reason));
This probably would better be further down in __l2cap_chan_close()
together with the other state check.
> + }
>
> l2cap_chan_unlock_conn(chan, have_conn);
> }
>
> base-commit: 019debf20bfd648b40ba10377ee0168db5eb241e
--
Pauli Virtanen
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH 0/5] Bluetooth: 6LoWPAN lifecycle fixes
2026-09-21 16:07 ` [PATCH 0/5] Bluetooth: 6LoWPAN lifecycle fixes Luiz Augusto von Dentz
@ 2026-09-21 16:59 ` Cen Zhang
0 siblings, 0 replies; 12+ messages in thread
From: Cen Zhang @ 2026-09-21 16:59 UTC (permalink / raw)
To: Luiz Augusto von Dentz
Cc: Tejun Heo, Lai Jiangshan, Marcel Holtmann, Marco Elver,
Jukka Rissanen, linux-kernel, linux-bluetooth, baijiaju1990,
jjzuming
Hi Luiz,
Luiz Augusto von Dentz <luiz.dentz@gmail.com> 于2026年9月22日周二 00:07写道:
>
> Something suggests none of this was tested, as IPSP was never
> mentioned once in this set.
>
Thank you for the feedback, and sorry that the patch series is not clear enough.
This series addresses six Bluetooth 6LoWPAN/IPSP lifetime bugs found
by our tools.
They were not submitted as static guesses: each bug was confirmed by dynamic
execution in a controlled, instrumented kernel. Two of them produced
native reports:
a KASAN use-after-free in send_pkt(), and an instruction-fetch fault
after unloading
bluetooth_6lowpan. The other four were confirmed as semantic or state-management
bugs where the harmful state or broken invariant was observed at runtime.
Because these bugs are related, I tried to fix them together in one series.
However, this turned out to be more difficult than I expected, and the
current result is not good enough. I will try either to narrow the fixes or
to send detailed bug reports first and work through the solutions together.
Do you think it would be better to start from separate bug reports, or from
narrower patches for a smaller subset of these issues?
For reference, the six bugs were:
- A transmit race between ndo_start_xmit() and peer/channel removal,
producing a native KASAN use-after-free report in send_pkt().
- A module-lifetime bug where Bluetooth 6LoWPAN callback code could keep
executing or return through module text after the module reference was
dropped, producing a native instruction-fetch-fault report after unload.
- A pre-publication setup failure where an IPSP channel could be deleted
before lowpan_peer publication, leaving the creator-side channel
reference without an owner.
- A close-path race where local close could continue from stale connected
state after concurrent deletion had set FLAG_DEL, rearming the timer and
sending another disconnect for the deleted channel.
- A listener/disable race where a request that had already looked up the
listener could still create and publish a child peer after 6LoWPAN had
been disabled and existing peers swept.
- A ready/publication failure where add_peer_chan() could fail while the
void ready callback returned normally, leaving a connected channel
without a peer and running ifup() on a zero-peer netdev.
Best regards,
Cen Zhang
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH 2/5] workqueue: add support for module-owned work
2026-09-21 16:16 ` Tejun Heo
@ 2026-09-21 17:03 ` Cen Zhang
0 siblings, 0 replies; 12+ messages in thread
From: Cen Zhang @ 2026-09-21 17:03 UTC (permalink / raw)
To: Tejun Heo
Cc: Lai Jiangshan, Marcel Holtmann, Luiz Augusto von Dentz,
Marco Elver, Jukka Rissanen, linux-kernel, linux-bluetooth,
baijiaju1990, jjzuming
Hi Tejun,
Tejun Heo <tj@kernel.org> 于2026年9月22日周二 00:16写道:
>
> Please provide justification on why this needs to be done this way when
> everyone else can flush delayed items before unloading.
>
Thank you for the review, and sorry for the extra noise.
I will revisit whether the 6LoWPAN unload path can use the existing workqueue
flush/cancellation primitives while preserving the required module lifetime,
rather than adding a new workqueue API.
Thanks again for your patience.
Best regards,
Cen Zhang
^ permalink raw reply [flat|nested] 12+ messages in thread
end of thread, other threads:[~2026-09-21 17:03 UTC | newest]
Thread overview: 12+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-21 15:38 [PATCH 0/5] Bluetooth: 6LoWPAN lifecycle fixes Cen Zhang
2026-09-21 15:38 ` [PATCH 1/5] Bluetooth: L2CAP: ignore close requests for deleted channels Cen Zhang
2026-09-21 16:56 ` Pauli Virtanen
2026-09-21 15:38 ` [PATCH 2/5] workqueue: add support for module-owned work Cen Zhang
2026-09-21 16:16 ` Tejun Heo
2026-09-21 17:03 ` Cen Zhang
2026-09-21 15:39 ` [PATCH 3/5] Bluetooth: L2CAP: drain channel timers on connection teardown Cen Zhang
2026-09-21 15:54 ` Luiz Augusto von Dentz
2026-09-21 15:39 ` [PATCH 4/5] Bluetooth: 6lowpan: handle channel setup failure and callback lifetime Cen Zhang
2026-09-21 15:39 ` [PATCH 5/5] Bluetooth: 6lowpan: quiesce peers before channel deletion Cen Zhang
2026-09-21 16:07 ` [PATCH 0/5] Bluetooth: 6LoWPAN lifecycle fixes Luiz Augusto von Dentz
2026-09-21 16:59 ` Cen Zhang
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®