From cace4a98ad2e76035d07382f689c279dc6eafe97 Mon Sep 17 00:00:00 2001 From: Aleksander Wasaznik Date: Mon, 6 Oct 2025 16:02:41 +0200 Subject: [PATCH] Bluetooth: Host: Invoke tx callbacks when channel is deleted When bt_l2cap_send_pdu() succeeds, it transfers buffer ownership to the stack, which must eventually invoke the provided callback. This contract is honored in all paths where transmission becomes impossible: - Normal transmission: callback invoked with err=0 after HCI Number of Completed Packets event (tx_notify_process) - Send errors (after tx allocated): callback invoked with err=-ESHUTDOWN via conn_tx_destroy - Send errors (before tx allocated): callback invoked with the specific error code in send_buf error_return path - Connection disconnect: callbacks invoked with err=-ESHUTDOWN via process_unack_tx -> conn_tx_destroy for all PDUs in tx_pending However, when a channel is deleted (l2cap_chan_del), PDUs remaining in the tx_queue are dropped without invoking their callbacks, violating the ownership contract. Fix this by extracting and invoking any non-NULL callbacks from the closure stored in buf->user_data before releasing the buffers. The callback is invoked with err=-ESHUTDOWN, making this path analogous to process_unack_tx: both drain queues of unsent PDUs when transmission becomes impossible due to external events (channel deletion vs connection disconnect). The only difference is the buffer lifecycle stage - in l2cap_chan_del, PDUs are still in tx_queue (closure in buf->user_data), while in process_unack_tx, they've progressed to tx_pending (callback in bt_conn_tx struct). Note: conn_tx_destroy() cannot be used here because no bt_conn_tx struct has been allocated yet - the closure is still in buf->user_data. Signed-off-by: Aleksander Wasaznik --- subsys/bluetooth/host/l2cap.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/subsys/bluetooth/host/l2cap.c b/subsys/bluetooth/host/l2cap.c index 5772ca662ce81..6dcdfe04e541b 100644 --- a/subsys/bluetooth/host/l2cap.c +++ b/subsys/bluetooth/host/l2cap.c @@ -278,6 +278,21 @@ static void l2cap_chan_del(struct bt_l2cap_chan *chan) * `l2cap_chan_destroy()` as it is not called for fixed channels. */ while ((buf = k_fifo_get(&le_chan->tx_queue, K_NO_WAIT))) { + bt_conn_tx_cb_t cb = closure_cb(buf->user_data); + + if (cb) { + void *user_data = closure_data(buf->user_data); + + /* When bt_l2cap_send_pdu() succeeds, the stack takes ownership + * and must invoke the callback eventually. Since these PDUs will + * never be transmitted, invoke the callback now with an error. + * Note: We cannot use conn_tx_destroy() here because no bt_conn_tx + * struct has been allocated yet - the closure is still in the + * buf->user_data. + */ + cb(chan->conn, user_data, -ESHUTDOWN); + } + net_buf_unref(buf); }