Bluetooth: Controller: nRF5340 v4.4: received PDU written into shared empty PDU buffer, encrypted link lost with MIC failure (backport #115651)
Describe the bug
On nRF5340 with the open source controller (BT_LL_SW_SPLIT, hci_ipc on the network core) from v4.4, a central with two connections loses its encrypted connection within a minute or two of normal traffic. The other connection is unencrypted.
Over-the-air captures and controller tracing show that a PDU received on the unencrypted connection ends up in the shared empty PDU buffer (_pkt_empty in hal/nrf5/radio/radio.c). From then on, every "empty" PDU on every connection is transmitted with that content:
- Encrypted link. The corrupted empty PDU is encrypted and sent. The ACK is processed through the empty PDU path, so the CCM counter does not advance and the next "empty" PDU goes out with the same ciphertext and a new SN. The peer decrypts it with the next packet counter, gets a MIC failure and silently leaves. The central reports
0x08connection timeout. - New connections. They start with this garbage data PDU instead of an empty PDU or
LL_FEATURE_REQ. The peer does not respond and the central reports0x3E. - Recovery. Only a reset of the network core recovers, because the buffer is in
.bss.
With commit d2c101979016 ("Bluetooth: Controller: Fix race in software radio switch timer", #115651) cherry-picked onto v4.4.2, the problem no longer reproduces. That commit is in main but not in v4.4-branch, while the commit it fixes, 5fe53aaa84b0 ("Use PPI/DPPI to start s/w switch timer", #101856), is in v4.4-branch.
Request: please backport #115651 to v4.4-branch.
Regression
- This is a regression.
Steps to reproduce
The application keeps encrypted ATT traffic on the camera link (a GATT read every 200 ms) and disconnects and reconnects the heart rate sensor every ~20 s, so GATT discovery runs on the unencrypted link again and again.
| Network core | Runs | Result |
|---|---|---|
| v4.4.2 | 4 | encrypted link lost in every run, within 32–92 s of the start of each capture |
| v4.4.2 + d2c101979016 | 1 | no failure for 16 min 41 s, until the camera powered itself off |
| v4.4.2 + empty PDU header restore (see below) | 1 | no failure for 16 min, until the camera powered itself off |
The configuration is otherwise identical (.config diff of the network core images is empty apart from the change under test).
Relevant log output
### 1. Same ciphertext with a new SN on the encrypted link
`mic_failure_same_ciphertext.pcap`, frames 105/107/109 (events 12274–12276):
#105 C->P hdr 02 len 13 37 28 2f 38 a8 4b 53 e6 c3 cf de 5d 75 SN 0
#106 P->C NESN 1 (ACK)
#107 C->P hdr 0e len 13 37 28 2f 38 a8 4b 53 e6 c3 cf de 5d 75 SN 1 same ciphertext and MIC
#108 P->C last packet from the peer
#109 C->P hdr 02 len 13 37 28 2f 38 a8 4b 53 e6 c3 cf de 5d 75 SN 0 repeated until supervision timeout
### 2. The empty PDU buffer contains a PDU received from the peer
When the central reconnects without encryption, the corrupted "empty" PDU is visible in plaintext.
`corrupted_empty_pdu_new_conn.pcap`: the first central PDU of a new connection, repeated 6 times until `0x3E`:
llid 2, len 26: 16 00 04 00 05 01 0d 00 37 2a 0e 00 02 29 0f 00 03 28 10 00 38 2a 11 00 00 28
= ATT Find Information Response: 0x000D 2A37 (HR Measurement), 0x000E 2902, 0x000F 2803, 0x0010 2A38, 0x0011 2800
Our device has no Heart Rate service. This is the heart rate sensor's response to our CCC discovery, i.e. a PDU **received** on the unencrypted link.
`corrupted_empty_pdu_new_conn_2.pcap` (another run) shows the same effect with `llid 2, len 7: 03 00 04 00 02 f7 00` (ATT Exchange MTU Request, MTU 247).
### 3. Controller trace: where the buffer gets corrupted
`trace_excerpt.txt`. The instrumentation (not upstream) checks `_pkt_empty` for `len != 0` in central prepare, `lll_conn_isr_tx()`, `lll_conn_isr_rx()` and `isr_done()`:
2210 1056.43 h1 ev29 PREP e=1 len=0 empty PDU still intact, selected for Tx on the unencrypted link
2211 1056.46 h1 ev29 TX_SET len=0
2212 1058.99 h1 ev29 ISR_TX 2.53 ms after TX_SET; 2.26-2.35 ms in the surrounding h1 events
2213 1058.99 !!EMPTY_PDU_CORRUPT len=26 found at lll_conn_isr_tx() entry
2214 1059.20 h1 ev29 NO_RX
2216 1099.98 h0 ev376 PREP e=1 len=26 encrypted link now sends the corrupted empty PDU
On the unencrypted link the radio transmits directly from `_pkt_empty`. At `lll_conn_isr_tx()` entry the buffer already holds 26 octets, and the ISR runs ~240 µs later than in the neighbouring events. Together with evidence 2, this means **the radio received the peer's response into the Tx packet buffer before `lll_conn_isr_tx()` set the Rx packet pointer**, and the ISR only ran after that reception ended.
This is consistent with the race described in #115651, where the next RXEN starts while the switch timer is being reset. With that commit applied the failure is no longer observed. I did not prove the exact sequence inside the switch timer.Impact
Major – Severely degrades functionality; workaround is difficult or unavailable.
Environment
- Zephyr v4.4.2-126-ga5b396444925 (
v4.4-branch). - nRF5340, custom board; network core:
samples/bluetooth/hci_ipc. - Relevant network core configuration:
CONFIG_BT_CTLR_RADIO_ENABLE_FAST=y, hence noBT_CTLR_TIFS_HWand software Tx/Rx switch with a separate timer (!BT_CTLR_SW_SWITCH_SINGLE_TIMER);CONFIG_BT_MAX_CONN=4,CONFIG_BT_BUF_ACL_TX_COUNT=10,CONFIG_BT_CTLR_DATA_LENGTH_MAX=27,CONFIG_BT_CTLR_PRIVACY=y,CONFIG_BT_CTLR_ASSERT_DEBUG=n.
- Central with two links, both 50 ms interval, 1M PHY:
- encrypted link to a GoPro HERO13;
- unencrypted link to a heart rate sensor (Magene).
- Sniffer: nRF Sniffer for Bluetooth LE 4.1.1 following the camera.
Additional Context
- The assert does not catch it.
LL_ASSERT_ERR(!radio_is_ready())inlll_conn_isr_tx()is evaluated afterlll_isr_tx_status_reset()has clearedEVENTS_READY. As the comment inlll_conn_rx_pkt_set()notes, a READY that happened before the ISR ran cannot be detected. A reception that already completed into the Tx buffer goes unnoticed. - Hardening proposal, independent of the root cause. Restore the empty PDU header each time
lll_conn_pdu_tx_prep()selects it, so that a corruption like this cannot persist and affect all connections:On its own this change also made the failure disappear in the stress test (table above). I can open a PR if this is considered useful.p = (void *)radio_pkt_empty_get(); p->ll_id = PDU_DATA_LLID_DATA_CONTINUE; p->len = 0U;
Source: zephyrproject-rtos/zephyr