From 051e0e671cb5cdc1d6ec96b2715d20b448cd7d81 Mon Sep 17 00:00:00 2001 From: Jon Gilmore <7232986+JonGilmore@users.noreply.github.com> Date: Thu, 2 Apr 2026 08:10:12 -0500 Subject: [PATCH 1/2] 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 --- .../bt/host/bluedroid/hci/packet_fragmenter.c | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/components/bt/host/bluedroid/hci/packet_fragmenter.c b/components/bt/host/bluedroid/hci/packet_fragmenter.c index 0827ecb4fe..548e0df56d 100644 --- a/components/bt/host/bluedroid/hci/packet_fragmenter.c +++ b/components/bt/host/bluedroid/hci/packet_fragmenter.c @@ -149,20 +149,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) { From 03c7e8720290d6636ade06bb08b7340df33f06d8 Mon Sep 17 00:00:00 2001 From: Jin Cheng Date: Wed, 8 Apr 2026 20:13:37 +0800 Subject: [PATCH 2/2] fix(bt/bluedroid): moved L2CAP length check into start packets only --- .../bt/host/bluedroid/hci/packet_fragmenter.c | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/components/bt/host/bluedroid/hci/packet_fragmenter.c b/components/bt/host/bluedroid/hci/packet_fragmenter.c index 548e0df56d..ab10c34de6 100644 --- a/components/bt/host/bluedroid/hci/packet_fragmenter.c +++ b/components/bt/host/bluedroid/hci/packet_fragmenter.c @@ -159,22 +159,10 @@ static void reassemble_and_dispatch(BT_HDR *packet) STREAM_TO_UINT16(handle, stream); STREAM_TO_UINT16(acl_length, stream); - 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 */ - } + uint8_t boundary_flag = GET_BOUNDARY_FLAG(handle); + handle = handle & HANDLE_MASK; BT_HDR *partial_packet = (BT_HDR *)hash_map_get(partial_packets, (void *)(uintptr_t)handle); @@ -185,6 +173,14 @@ static void reassemble_and_dispatch(BT_HDR *packet) osi_free(partial_packet); } + /* 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); /* Check for integer overflow in length calculation */ if (l2cap_length > (UINT16_MAX - L2CAP_HEADER_SIZE - HCI_ACL_PREAMBLE_SIZE)) { HCI_TRACE_ERROR("L2CAP length too large: %u", l2cap_length);