CVE-2026-45515 AI-generated analysis
platform/packages/modules/Bluetooth, system/stack/a2dp (A2DP sink Opus vendor codec decoder)
Passes the correct frame_size (the number of samples per channel the output buffer can hold, A2DP_OPUS_DECODE_BUFFER_LENGTH / (A2DP_OPUS_CODEC_OUTPUT_CHS * sizeof(int16_t))) to both opus_decode() calls in a2dp_vendor_opus_decoder_decode_packet instead of the buffer size in bytes. It also adds a channel-count check, removes the per-frame re-decode loop and makes frame_len use the configured stereo output.
Root cause
In system/stack/a2dp/a2dp_vendor_opus_decoder.cc, a2dp_vendor_opus_decoder_decode_packet called opus_decode(handle, pBuffer, bufferSize, a2dp_opus_decoder_cb.decode_buf, A2DP_OPUS_DECODE_BUFFER_LENGTH, 0). Per the commit message, opus_decode expects samples per channel as its frame_size, but A2DP_OPUS_DECODE_BUFFER_LENGTH is the buffer size in bytes. The output is stereo int16 (the commit message gives a fixed channel count of 2), so the bound was larger than decode_buf's real capacity by the factor A2DP_OPUS_CODEC_OUTPUT_CHS * sizeof(int16_t). A crafted Opus packet with a large frame duration could therefore overflow the heap buffer. The new test uses a real 60 ms, 2880 samples per channel packet. The packet-loss-concealment call opus_decode(handle, NULL, 0, ...) had the same wrong bound. The code also had three other problems: it looped numFrames times decoding the same whole packet, it computed frame_len from the packet's numChannels instead of the configured output channel count, and it never validated numChannels.
How the fix works
- system/stack/a2dp/a2dp_vendor_opus_decoder.cc:a2dp_vendor_opus_decoder_decode_packet: both opus_decode() calls (the normal decode and the NULL/0 concealment retry) now pass A2DP_OPUS_DECODE_BUFFER_LENGTH / (A2DP_OPUS_CODEC_OUTPUT_CHS * sizeof(int16_t)) as frame_size. The decoder's bound is now the number of stereo int16 samples per channel that decode_buf can hold.
- Same function: adds an early return false with an error log when opus_packet_get_nb_channels() returns anything other than 1 or 2. This also rejects negative error codes stored in the uint32_t numChannels.
- Same function: removes the 'for (frame = 0; frame < numFrames; ++frame)' loop, which re-decoded the entire packet and called decode_callback numFrames times. The packet is now decoded exactly once.
- Same function: frame_len passed to decode_callback is now ret_val * A2DP_OPUS_CODEC_OUTPUT_CHS * sizeof(decode_buf[0]) (configured stereo output) instead of ret_val * numChannels (the packet's channel count). The unused 'num_frames = pBuffer[0] & 0xf' is dropped, and two LOG_ERROR lines are replaced by one LOG_INFO line.
- system/stack/test/a2dp/a2dp_opus_unittest.cc: adds <opus.h> and a new test, decode_packet_60ms_overflow_repro. It encodes a 60 ms stereo 48 kHz Opus packet, prepends the 1-byte A2DP header and passes it to decoder_iface_->decode_packet().
Completeness
- Both opus_decode() call sites in the function (the primary decode and the concealment retry) get the corrected frame_size. The shown material contains no other opus_decode calls.
- The bound comes from the constant A2DP_OPUS_CODEC_OUTPUT_CHS (stereo), and a code comment says Opus is always configured as stereo. The fix is only correct if the decoder handle created in a2dp_vendor_opus_decoder_init uses that channel count and decode_buf is allocated with A2DP_OPUS_DECODE_BUFFER_LENGTH bytes. The diff shows neither.
- The fix adds no explicit post-decode check that ret_val * A2DP_OPUS_CODEC_OUTPUT_CHS * sizeof(int16_t) <= A2DP_OPUS_DECODE_BUFFER_LENGTH. It relies entirely on opus_decode honoring frame_size.
- Existing input handling in the same function is unchanged. pBuffer = p_buf->data + p_buf->offset + 1 and bufferSize = p_buf->len - 1 are computed after only a p_buf->len == 0 check. A packet with len == 1 therefore gives bufferSize 0, and opus_packet_get_nb_channels(pBuffer) is still called on the byte after the A2DP header. The fix adds no minimum-length check.
- numFrames, frameSize and frameLen are still computed, now only for logging. Error returns from those opus_packet_* helpers are stored in uint32_t and not validated, but they no longer affect control flow.
Behavior changes
- opus_decode now receives a frame_size that matches decode_buf's capacity. When the primary decode returns an error, the concealment call runs with the same corrected bound, and the packet is dropped (return false) only if that call also fails.
- Packets reporting a channel count other than 1 or 2 are rejected with 'Invalid numChannels' and return false.
- A multi-frame Opus packet is decoded and delivered to decode_callback once instead of numFrames times.
- The decode_callback length is always computed with the stereo output channel count, so mono-flagged packets now report the stereo byte length.
- Per-packet diagnostic logging drops from two LOG_ERROR lines to one LOG_INFO line.
Per major
- Android 14: Same fix: commit 4f7ed05b1660 in android-security-14.0.0_r29 (RESTRICT AUTOMERGE cherry-pick of e87caba4b8df). It uses the printf-style LOG_ERROR/LOG_INFO macros and also reformats the frameSize/frameLen lines. In the test file, <stdio.h> is replaced by <opus.h>.
- Android 15: Same fix: commit b837ddeb7125 in android-security-15.0.0_r17. Identical logic, adapted to fmt-style log::error/log::info logging. The test adds <stdio.h> and <opus.h>.
- Android 16: Same fix: commit 5eefb7cb0b0c in android-security-16.0.0_r8. Identical logic with log::* logging. The hunk is smaller because the frameSize/frameLen lines were already single-line on this branch.
- Android 17: Same fix: commit 3b78dbef3ec6 in android-security-17.0.0_r1. The hunks are the same as Android 16, and the test file has the same blob indexes. The decoder file's base blob differs.
Unknowns
- The numeric values of A2DP_OPUS_DECODE_BUFFER_LENGTH and A2DP_OPUS_CODEC_OUTPUT_CHS, where they are defined, and how decode_buf is allocated in a2dp_vendor_opus_decoder_init are not shown.
- How opus_decode handles a packet whose duration exceeds frame_size (error code or other behavior), and whether the concealment retry then succeeds, are not shown.
- The bulletin lists the issue as EoP. The material does not state the privilege boundary crossed or the process in which the decoder runs.
- The call path from the AVDTP/A2DP sink media receive path into a2dp_vendor_opus_decoder_decode_packet is not shown in the material (only the header declaration and the unit test).