[PATCH v2] Bluetooth: hci_qca: Reset the in-band sleep state on power off

From: Birk Skyum

Date: Wed Oct 14 2026 - 14:05:52 EST


qca_power_off() cuts the power of the controller but leaves the host
side of the in-band sleep (IBS) protocol as it was. tx_ibs_state and
rx_ibs_state are initialised in qca_open() only, so a controller that
qca_setup() powers on again meets whatever state the last session ended
in.

If that state is HCI_IBS_TX_AWAKE, the host sends the frames after the
next setup without a wake indication. qca_hci_shutdown() stops both IBS
timers, but the pre-shutdown command it sends afterwards goes through
qca_enqueue() and arms tx_idle_timer again for 2 s, so a power on within
those 2 s finds the state at AWAKE. On a OnePlus Pad 3 (WCN7880) this
happens with a power off and on within 2 s, and at every boot of a board
without local-bd-address when user space sets the address right after
the controller registers. The controller comes back asleep, and the
first command after it has gone to sleep breaks the link:

Bluetooth: hci0: Frame reassembly failed (-84)
Bluetooth: hci0: Opcode 0x0c56 failed: -110
Bluetooth: hci0: crash the soc to collect controller dump

The timer left armed can also send HCI_IBS_SLEEP_IND in the middle of
the next setup.

If the state is HCI_IBS_TX_WAKING, the host is waiting for a wake
acknowledgment, and qca_hci_shutdown() has deleted the timer that
sends the wake indication again. The setup still works, because frames
bypass IBS while QCA_IBS_DISABLED is set. Once the flag is cleared,
qca_enqueue() puts every frame on tx_wait_q and nothing takes it off:

Bluetooth: hci0: QCA setup on UART is completed
Bluetooth: hci0: Opcode 0x0c03 failed: -110

A restore from hibernation gets there on a Lenovo Yoga Slim 7x
(WCN7850). The controller has been through a power cycle, while the
restored kernel holds the state from before the image was taken. Its
wake indications go unanswered, the commands behind them time out, and
the core closes the device and sets it up again. From then on
tx_ibs_state reads 1 in debugfs and Bluetooth stays dead until hci_uart
is reloaded.

When the controller loses power, cancel the IBS work, wait for the IBS
timers, drop what was queued and put both sides back to asleep, which
is where qca_open() starts. The timer callbacks take hci_ibs_lock, so
the timers are deleted outside of it. All callers of qca_power_off()
can sleep, and the work items do not call it.

Fixes: 3e4be65eb82c ("Bluetooth: hci_qca: Add poweroff support during hci down for wcn3990")
Cc: stable@xxxxxxxxxxxxxxx
Assisted-by: Claude:claude-opus-5-5
Co-developed-by: Vladyslav Tymoshchuk <tvlad744@xxxxxxxxx>
Signed-off-by: Vladyslav Tymoshchuk <tvlad744@xxxxxxxxx>
Signed-off-by: Birk Skyum <birk.skyum@xxxxx>
---
Changes since v1
- Cancel both IBS work items and wait for both timers before the reset,
with the timers deleted outside hci_ibs_lock, and flush after that, as
the Sashiko review of v1 pointed out (Vladyslav)
- Describe the AWAKE trigger next to the WAKING one
- Add Fixes: and Cc: stable
- v1 is at https://lore.kernel.org/r/179163212446.46972.2306254853726299632@xxxxx

Vladyslav tested this version on a OnePlus Pad 3 (SM8750, WCN7880). In
five quick power cycles four fail without it and none with it [1].

On a Lenovo Yoga Slim 7x with v7.3-rc7 and hci_uart rebuilt, ten quick
power cycles (power on 1 s after the power off, then a scan) pass with
the stock module, with v1 and with v2, so this board does not show the
AWAKE failure. Right after the power off, debugfs reads tx_ibs_state=2
and rx_ibs_state=1 with the stock module and 0 and 0 with v1 and v2.

The hibernation runs were done with v1 (stock dead after four restores
out of four, back by itself after two out of two) and are not repeated
with v2. Those kernels also carried the hibernation changes in [2], [3]
and [4]. v2 is based on bluetooth-next, where it builds without
warnings (W=1).

[1] https://lore.kernel.org/r/20261014162025.3733-1-tvlad744@xxxxxxxxx
[2] https://lore.kernel.org/r/179157746908.73485.2343672865542690797@xxxxx
[3] https://lore.kernel.org/r/179158127177.70185.5691100900886332545@xxxxx
[4] https://lore.kernel.org/r/179163141354.18006.80461347454353634@xxxxx

drivers/bluetooth/hci_qca.c | 17 +++++++++++++++--
1 file changed, 15 insertions(+), 2 deletions(-)

diff --git a/drivers/bluetooth/hci_qca.c b/drivers/bluetooth/hci_qca.c
index 7089e9b63..0d3b5f198 100644
--- a/drivers/bluetooth/hci_qca.c
+++ b/drivers/bluetooth/hci_qca.c
@@ -2229,12 +2229,25 @@ static void qca_power_off(struct hci_uart *hu)
struct qca_power *power;

/* From this point we go into power off state. But serial port may
- * still be open, stop queueing the IBS data and flush all the buffered
- * data in skb's.
+ * still be open, stop queueing the IBS data.
*/
spin_lock_irqsave(&qca->hci_ibs_lock, flags);
set_bit(QCA_IBS_DISABLED, &qca->flags);
+ spin_unlock_irqrestore(&qca->hci_ibs_lock, flags);
+
+ /* The controller loses the IBS state with its power and comes back
+ * with both sides asleep, as qca_open() assumes. Stop the IBS work and
+ * timers, drop what they queued and start over from there.
+ */
+ cancel_work_sync(&qca->ws_awake_device);
+ cancel_work_sync(&qca->ws_awake_rx);
+ timer_delete_sync(&qca->wake_retrans_timer);
+ timer_delete_sync(&qca->tx_idle_timer);
+
+ spin_lock_irqsave(&qca->hci_ibs_lock, flags);
qca_flush(hu);
+ qca->tx_ibs_state = HCI_IBS_TX_ASLEEP;
+ qca->rx_ibs_state = HCI_IBS_RX_ASLEEP;
spin_unlock_irqrestore(&qca->hci_ibs_lock, flags);

/* Non-serdev device usually is powered by external power
--
2.55.0