CVE-2026-49921 AI-generated analysis
platform/packages/modules/Bluetooth, system/stack/sdp (SDP server: sdp_server.cc, sdp_utils.cc, sdp_db.cc)
Replaces the fixed 400-byte (SDP_MAX_ATTR_LEN) heap buffer in sdpu_build_partial_attrib_entry with an allocation sized to the serialized attribute. It also rejects attributes whose payload exceeds SDP_MAX_ATTR_LEN in that helper, in both SDP server request handlers and at database insertion, and adds an offset-vs-length check before the partial memcpy.
Root cause
sdpu_build_partial_attrib_entry (system/stack/sdp/sdp_utils.cc) allocated exactly SDP_MAX_ATTR_LEN (400) bytes. It then called sdpu_build_attrib_entry to serialize the whole attribute (type/length header plus p_attr->len payload bytes) into that buffer, with no check that the serialized entry fits. The only guard was in the callers process_service_attr_req and process_service_search_attr_req (sdp_server.cc): a check 'attr_len >= SDP_MAX_ATTR_LEN' that existed only in the 'rem_len < attr_len' (first partial send) branch. It was absent from the continuation branch ('if (p_ccb->cont_info.attr_offset)'), which also calls sdpu_build_partial_attrib_entry. In addition, SDP_AddAttributeToRecord (sdp_db.cc) only truncated against the record pad (SDP_MAX_PAD_LEN - free_pad_ptr), so an attribute with a payload longer than SDP_MAX_ATTR_LEN could be stored in the local database. An oversized attribute reaching the helper overflowed the 400-byte heap buffer during serialization. The following memcpy from &p_attr_buff[*offset] could then read beyond the buffer, and the caller check (attr_len < attr_offset) did not reject *offset == attr_len.
How the fix works
- system/stack/sdp/sdp_utils.cc:sdpu_build_partial_attrib_entry: first computes attr_len = sdpu_get_attrib_entry_len(p_attr). It returns p_out unchanged if p_attr->len > SDP_MAX_ATTR_LEN, allocates osi_malloc(attr_len) (the exact serialized size instead of a fixed 400) and returns on nullptr, and only then calls sdpu_build_attrib_entry. It adds 'if (*offset >= attr_len)', which frees the buffer and returns p_out, so the memcpy source range always lies inside the allocation. The existing clamp of 'len' to SDP_MAX_ATTR_LEN is kept.
- system/stack/sdp/sdp_server.cc:process_service_attr_req and process_service_search_attr_req (14): after attr_len = sdpu_get_attrib_entry_len(p_attr), a new 'if (p_attr->len > SDP_MAX_ATTR_LEN)' check logs, calls sdpu_build_n_send_error(p_ccb, trans_num, SDP_NO_RESOURCES, NULL) and returns. It runs before the continuation, first-partial and full-entry branches. The old 'attr_len >= SDP_MAX_ATTR_LEN' checks inside the first-partial branches, which compared the full entry length rather than the payload and covered only one branch, are removed. This is most likely the 'mistaken bounds checks' correction that the commit message mentions.
- system/stack/sdp/sdp_db.cc:SDP_AddAttributeToRecord: the overflow condition becomes 'attr_len > SDP_MAX_ATTR_LEN || free_pad_ptr + attr_len >= SDP_MAX_PAD_LEN'. For TEXT_STR_DESC_TYPE the value is truncated to max_allowed_len = min(SDP_MAX_ATTR_LEN, SDP_MAX_PAD_LEN - free_pad_ptr) and NUL-terminated at p_val[max_allowed_len - 1]. Other types get attr_len = 0 as before. This keeps attributes with a payload over 400 bytes out of the local server database.
- Tests: stack_sdp_test.cc adds write_read_max_attr_len_slicing (a 400-byte attribute served over continuation chunks with max_list_len 50) and SDP_AddAttribute__exceed_max_attr_len_truncation. stack_sdp_utils_test.cc adds exceeds_max_attr_len and within_bounds tests for sdpu_build_partial_attrib_entry. On 14 it also maps the osi_malloc/osi_free mocks to malloc/free in the fixture.
Completeness
- Covers all three layers shown: the helper itself, both server handlers that call it (process_service_attr_req and process_service_search_attr_req), and database insertion via SDP_AddAttributeToRecord.
- The helper is protected independently of its callers: it rejects payloads over SDP_MAX_ATTR_LEN, allocates exactly attr_len bytes and rejects *offset >= attr_len.
- The full-entry branch in the handlers (sdpu_build_attrib_entry writing directly into p_ccb->rsp_list when rem_len >= attr_len) is unchanged in body, but the new p_attr->len check now runs before it (on 14 after attr_len is computed, on 15+ at the top of the if (p_attr) block).
- When sdpu_build_partial_attrib_entry rejects, it returns p_out without advancing *offset. On 14, in the continuation branch, if the offset is below attr_len the caller breaks with cont_info.attr_offset unchanged. If *offset == attr_len, the caller sees attr_offset == attr_len and resets attr_offset to 0. There is no caller-side error response for the helper's offset rejection.
- sdp_db.cc still writes a NUL terminator into the caller-supplied p_val buffer during truncation (existing behavior, only the index computation changed).
Behavior changes
- On 14, ServiceAttribute and ServiceSearchAttribute requests that reach a stored attribute with a payload over SDP_MAX_ATTR_LEN now get an SDP_NO_RESOURCES error response on every path. Before, this happened only in the first-partial branch and was based on entry length.
- On 14, attributes whose serialized entry length is >= 400 but whose payload is <= 400 were rejected with SDP_NO_RESOURCES when they needed partial sending. They are now served across continuation chunks (exercised for a 400-byte payload by write_read_max_attr_len_slicing).
- SDP_AddAttribute now truncates TEXT_STR_DESC_TYPE values to at most SDP_MAX_ATTR_LEN (400) bytes, logging a warning. It stores other types longer than that with attr_len = 0.
- sdpu_build_partial_attrib_entry allocates attr_len bytes instead of a fixed 400. It returns p_out unchanged (no copy, offset unchanged) for oversized attributes, allocation failure, or *offset >= attr_len.
Per major
- Android 14: adapted fix (51b0cc33b16c in android-security-14.0.0_r29). The new p_attr->len check is inserted after the attr_len computation in both handlers, and the old 'attr_len >= SDP_MAX_ATTR_LEN' checks in the partial branches are removed. In sdpu_build_partial_attrib_entry the offset check comes after the len clamp. Uses SDP_TRACE_ERROR logging and SDP_NO_RESOURCES. 7/8 lines of the patched region are present at the tag.
- Android 15: adapted fix (c738041c8ce8 in android-security-15.0.0_r17). Same three-layer change, but the p_attr->len check sits at the top of the 'if (p_attr)' block, before the AVRCP/HFP version tweaks. The offset check comes before the len clamp, and logging uses log::error/log::warn. The diff does not show removal of the old partial-branch checks. 8/8 lines of the patched region are present.
- Android 16: adapted fix (947e8bbe7dc6 in android-security-16.0.0_r8). Same shape as 15, using tSDP_STATUS::SDP_NO_RESOURCES and reinterpret_cast. The test uses mock_stack_l2cap_interface_ EXPECT_CALL. 7/8 lines of the patched region are present.
- Android 17: adapted fix (4a9e8875e210 in android-security-17.0.0_r1). Same shape as 16. The database function is the static sdp_add_attribute_to_record instead of SDP_AddAttributeToRecord. 7/8 lines of the patched region are present.
Unknowns
- The numeric value of SDP_MAX_PAD_LEN, and therefore how far beyond 400 bytes a stored attribute could have been before the fix.
- Which local profiles or app-facing APIs can register an attribute longer than SDP_MAX_ATTR_LEN. The material only shows the stack-level SDP_AddAttribute entry point.
- Whether the old 'attr_len >= SDP_MAX_ATTR_LEN' checks in the partial branches still exist on 15/16/17 (their diffs do not show removal).
- Why the per-major resolver reports 7/8 patched-region lines on 14, 16 and 17 (not verifiable from the material).
- The width of tSDP_ATTRIBUTE::len relative to the uint16_t attr_len returned by sdpu_get_attrib_entry_len.