Skip to content

Commit 721f76a

Browse files
bluetoothbotclaude
andcommitted
perf(gap): hoist per-iteration UUID length bounds check
For 16/32/128-bit Service UUID AD lists, the parse loop ran a `i + N <= end` check on every iteration to discard any malformed short tail (per Bluetooth-Devices#226). Compute the multiple-of-stride endpoint once before the loop instead, so the loop body has no per-iteration bounds branch. Malformed-tail behavior is preserved (the bits `(end - start) & ~(N-1)` truncate any 1..N-1 byte remainder). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3e6bb6f commit 721f76a

2 files changed

Lines changed: 32 additions & 28 deletions

File tree

src/bluetooth_data_tools/gap.pxd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ cpdef parse_advertisement_data(object data)
5353
offset=cython.uint,
5454
start=cython.uint,
5555
end=cython.uint,
56+
safe_end=cython.uint,
5657
i=cython.uint,
5758
tx_power_byte="unsigned char",
5859
uuid32_int=cython.uint,

src/bluetooth_data_tools/gap.py

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -247,47 +247,50 @@ def _uncached_parse_advertisement_bytes(
247247
if service_uuids is _EMPTY_SERVICE_UUIDS:
248248
service_uuids = []
249249
# Parse multiple 16-bit UUIDs (each is 2 little-endian bytes).
250-
# Decode inline to an int and look up by int key to skip the
251-
# per-iteration bytes-slice allocation.
252-
for i in range(start, end, 2):
253-
if i + 2 <= end:
254-
service_uuids.append(
255-
_cached_uint16_int_as_uuid(gap_data[i] | (gap_data[i + 1] << 8))
256-
)
250+
# Truncate to a multiple-of-2 endpoint so the loop body has no
251+
# per-iteration bounds branch; any odd tail is dropped.
252+
safe_end = start + ((end - start) & ~1)
253+
for i in range(start, safe_end, 2):
254+
service_uuids.append(
255+
_cached_uint16_int_as_uuid(gap_data[i] | (gap_data[i + 1] << 8))
256+
)
257257
elif gap_type_num in {
258258
TYPE_32BIT_SERVICE_UUID_COMPLETE,
259259
TYPE_32BIT_SERVICE_UUID_MORE_AVAILABLE,
260260
}:
261261
if service_uuids is _EMPTY_SERVICE_UUIDS:
262262
service_uuids = []
263263
# Parse multiple 32-bit UUIDs (each is 4 little-endian bytes).
264-
for i in range(start, end, 4):
265-
if i + 4 <= end:
266-
# Assemble via uint local: in Cython the shift-by-24 of an
267-
# unsigned char promotes to signed int and would yield a
268-
# negative value when bit 31 is set; assigning to a
269-
# cython.uint local recovers the unsigned 32-bit value.
270-
uuid32_int = (
271-
gap_data[i]
272-
| (gap_data[i + 1] << 8)
273-
| (gap_data[i + 2] << 16)
274-
| (gap_data[i + 3] << 24)
275-
)
276-
service_uuids.append(_cached_uint32_int_as_uuid(uuid32_int))
264+
# Truncate to a multiple-of-4 endpoint so the loop body has no
265+
# per-iteration bounds branch; any 1-3 byte tail is dropped.
266+
safe_end = start + ((end - start) & ~3)
267+
for i in range(start, safe_end, 4):
268+
# Assemble via uint local: in Cython the shift-by-24 of an
269+
# unsigned char promotes to signed int and would yield a
270+
# negative value when bit 31 is set; assigning to a
271+
# cython.uint local recovers the unsigned 32-bit value.
272+
uuid32_int = (
273+
gap_data[i]
274+
| (gap_data[i + 1] << 8)
275+
| (gap_data[i + 2] << 16)
276+
| (gap_data[i + 3] << 24)
277+
)
278+
service_uuids.append(_cached_uint32_int_as_uuid(uuid32_int))
277279
elif gap_type_num in {
278280
TYPE_128BIT_SERVICE_UUID_MORE_AVAILABLE,
279281
TYPE_128BIT_SERVICE_UUID_COMPLETE,
280282
}:
281283
if service_uuids is _EMPTY_SERVICE_UUIDS:
282284
service_uuids = []
283-
# Parse multiple 128-bit UUIDs (each is 16 bytes). The AD length
284-
# may not be a clean multiple of 16 for malformed input — skip
285-
# any trailing remainder rather than emitting a truncated UUID.
286-
for i in range(start, end, 16):
287-
if i + 16 <= end:
288-
service_uuids.append(
289-
_cached_uint128_bytes_as_uuid(gap_data[i : i + 16])
290-
)
285+
# Parse multiple 128-bit UUIDs (each is 16 bytes). Truncate to a
286+
# multiple-of-16 endpoint so the loop body has no per-iteration
287+
# bounds branch; any trailing remainder is dropped rather than
288+
# emitting a truncated UUID.
289+
safe_end = start + ((end - start) & ~15)
290+
for i in range(start, safe_end, 16):
291+
service_uuids.append(
292+
_cached_uint128_bytes_as_uuid(gap_data[i : i + 16])
293+
)
291294
elif gap_type_num == TYPE_SERVICE_DATA:
292295
splice_pos = start + 2
293296
if splice_pos > total_length or splice_pos > end:

0 commit comments

Comments
 (0)