fix(bt/bluedroid): fix ACL reassembly dropping valid continuation fragments

The minimum length check in `reassemble_and_dispatch()` applied the START packet minimum (`HCI_ACL_PREAMBLE_SIZE + L2CAP_LENGTH_SIZE` = 8 bytes) to **all** ACL packets, including continuation fragments. Continuation fragments only carry the 4-byte ACL preamble (handle + length) with no L2CAP header, so small but valid continuations (5-7 bytes) were incorrectly rejected as "too short."

This caused the first L2CAP PDU in a rapid burst of BLE GATT indications to be silently dropped. The partial reassembly was orphaned, then discarded when the next indication's START fragment arrived, producing:

```
E BT_HCI: ACL packet too short (len=5)
W BT_HCI: reassemble_and_dispatch found unfinished packet for handle with start packet. Dropping old.
```

Parse the ACL preamble first (requires only 4 bytes) to determine the boundary flag, then apply the L2CAP length check only to START packets. Continuation packets are now accepted with the correct minimum of `HCI_ACL_PREAMBLE_SIZE` (4 bytes).

- ESP32-S3 connected to a BLE peripheral that fragments indications at 40 bytes per L2CAP PDU
- Peripheral sends 8+ indications within ~200ms (burst of state changes)
- The final continuation fragment of the first indication is small (5-6 bytes after type stripping)
- 100% reproducible on every burst; confirmed on ESP-IDF 5.5.3, 5.5.4, and 6.0.0

Verified on ESP32-S3 with a Sub-Zero wall oven (SO3050PESP, firmware 8.5):
- **Before fix:** First indication in every burst lost (ACL reassembly failure)
- **After fix:** All indications in burst delivered correctly, including when the final continuation fragment is 5-6 bytes

Closes https://github.com/espressif/esp-idf/issues/18414
This commit is contained in:
Jon Gilmore
2026-04-02 08:10:12 -05:00
committed by Jin Cheng
parent 2994fca5ba
commit 25ddfaab1c
@@ -150,20 +150,33 @@ static void reassemble_and_dispatch(BT_HDR *packet)
uint16_t l2cap_length;
uint16_t acl_length __attribute__((unused));
if (packet->len < HCI_ACL_PREAMBLE_SIZE + L2CAP_LENGTH_SIZE) {
/* All ACL packets need at least the 4-byte HCI ACL preamble (handle + length) */
if (packet->len < HCI_ACL_PREAMBLE_SIZE) {
HCI_TRACE_ERROR("ACL packet too short (len=%u)\n", packet->len);
osi_free(packet);
return;
}
STREAM_TO_UINT16(handle, stream);
STREAM_TO_UINT16(acl_length, stream);
STREAM_TO_UINT16(l2cap_length, stream);
assert(acl_length == packet->len - HCI_ACL_PREAMBLE_SIZE);
uint8_t boundary_flag = GET_BOUNDARY_FLAG(handle);
handle = handle & HANDLE_MASK;
assert(acl_length == packet->len - HCI_ACL_PREAMBLE_SIZE);
if (boundary_flag == START_PACKET_BOUNDARY) {
/* START packets must also contain the L2CAP header (length field) */
if (packet->len < HCI_ACL_PREAMBLE_SIZE + L2CAP_LENGTH_SIZE) {
HCI_TRACE_ERROR("ACL START packet too short for L2CAP header (len=%u)\n", packet->len);
osi_free(packet);
return;
}
STREAM_TO_UINT16(l2cap_length, stream);
} else {
l2cap_length = 0; /* Not used for continuation packets */
}
BT_HDR *partial_packet = (BT_HDR *)hash_map_get(partial_packets, (void *)(uintptr_t)handle);
if (boundary_flag == START_PACKET_BOUNDARY) {