home intel cve-2025-54957-dolby-udc-integer-overflow-oob-write
CVE Analysis 2025-10-20 · 9 min read

CVE-2025-54957: Integer Wraparound OOB Write in Dolby UDC DD+ Decoder

An integer wraparound in evo_priv.c's length calculation allows a malformed DD+ bitstream to trigger an out-of-bounds heap write in Dolby UDC 4.5–4.13, exploitable 0-click via Android audio transcription.

#integer-overflow#buffer-overflow#memory-corruption#audio-codec#dolby-udc
Technical mode — for security professionals
▶ Attack flow — CVE-2025-54957 · Memory Corruption
ATTACKERRemote / unauthMEMORY CORRUPTIOCVE-2025-54957Cross-platform · CRITICALCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2025-54957 is a critical (CVSS 9.8) integer wraparound leading to an out-of-bounds heap write in the Dolby Unified Decoder Component (UDC), versions 4.5 through 4.13. The bug lives in evo_priv.c, the file responsible for parsing Evolution (EVO) supplementary data embedded in Dolby Digital Plus (DD+/EAC-3) bitstreams. Because Android's Google Messages automatically decodes incoming audio attachments for transcription with no user interaction, this vulnerability sits squarely in the 0-click attack surface of the vast majority of Android devices in active use.

The vulnerability was reported to Google by Natalie Silvanovich and Ivan Fratric of Project Zero. It was fixed as of January 5, 2026, and was used as the first stage of a full 0-click exploit chain against the Pixel 9, achieving arbitrary code execution in the mediacodec context. The exploit chain details appear across three Project Zero blog posts; this analysis focuses on the root cause and exploitation mechanics of the memory corruption primitive itself.

Root cause: The EVO data length calculation in evo_priv.c uses an integer type narrow enough to wrap around on attacker-controlled input, producing a smaller-than-required heap allocation whose subsequent bounds check is defeated by the same wrapped value, allowing an unbounded out-of-bounds write.

Affected Component

The Dolby Unified Decoder (UDC) is a shared library distributed by Dolby and integrated by OEMs into the Android media stack. It provides hardware-agnostic software decoding for DD (AC-3) and DD+ (EAC-3) audio. On Pixel 9 it runs under the mediacodec process, which operates inside a restricted seccomp/SELinux sandbox. The EVO (Evolution data) subsystem, implemented in evo_priv.c, parses auxiliary data blocks appended to standard DD+ frames — things like object audio metadata and spatial cues — and stages them into internal buffers for downstream processing.

Affected versions: Dolby UDC 4.5 – 4.13. Any platform shipping these library versions is affected; this includes the majority of Android OEM devices in the field.

Root Cause Analysis

The EVO parser reads a caller-controlled length field from the bitstream and uses it to compute how many bytes to write into a staging buffer. The calculation is performed in a type that is too narrow, so sufficiently large attacker-supplied values wrap to a small positive number. The allocation is then sized by the wrapped value. The subsequent bounds check before the write uses the same wrapped (small) value, so even though the write is "within bounds" according to the check, the actual write target extends far past the end of the allocation.


/* evo_priv.c — simplified decompiled pseudocode, Dolby UDC 4.x */

typedef struct {
    uint8_t  *evo_buf;        // staging buffer for EVO payload
    uint32_t  evo_buf_size;   // allocated size of evo_buf
    uint32_t  evo_written;    // bytes already written
} evo_state_t;

/*
 * evo_write_data — copies EVO payload from the bitstream into evo_buf.
 * `elem_count`  : number of EVO elements, read directly from bitstream (attacker-controlled)
 * `elem_size`   : per-element size in bytes, also bitstream-derived (attacker-controlled)
 */
int evo_write_data(evo_state_t *state, uint16_t elem_count, uint16_t elem_size,
                   const uint8_t *src)
{
    /* BUG: both operands are uint16_t; product silently wraps mod 2^16
     *      e.g. elem_count=0x0101, elem_size=0x0100 → 0x0101*0x0100 = 0x10100
     *      truncated to uint16_t → 0x0100 (256 bytes)
     *      actual data to copy: 0x10100 bytes
     */
    uint16_t total_len = elem_count * elem_size;   // BUG: integer wraparound here

    /* Allocation is sized by the *wrapped* total_len — too small */
    if (state->evo_buf == NULL) {
        state->evo_buf      = malloc(total_len);   // allocates only 256 bytes
        state->evo_buf_size = total_len;           // stores the wrapped size
    }

    /* Bounds check uses the same wrapped value — check passes trivially
     * because evo_written (0) + total_len (0x100) <= evo_buf_size (0x100)
     * but the real copy is elem_count * elem_size = 0x10100 bytes        */
    if (state->evo_written + total_len > state->evo_buf_size) {
        return EVO_ERR_OVERFLOW;   // never reached — check defeated by wraparound
    }

    /* Out-of-bounds write: copies 0x10100 bytes into a 0x100-byte buffer */
    memcpy(state->evo_buf + state->evo_written, src, elem_count * elem_size); // BUG: OOB write
    state->evo_written += total_len;
    return EVO_OK;
}

The critical observation is that total_len is used for both the allocation size and the bounds check ceiling, but the memcpy length argument is re-derived as elem_count * elem_size without the truncation. This means the allocation and guard are coherent with each other but both disagree with the actual copy length.

Memory Layout

The following illustrates the heap state on a Pixel 9 (jemalloc-based allocator in mediacodec) when the bug triggers with elem_count=0x0101, elem_size=0x0100.


/* Internal EVO state struct — relevant fields */
struct evo_state_t {
    /* +0x00 */ uint8_t  *evo_buf;       // ptr to staging allocation
    /* +0x08 */ uint32_t  evo_buf_size;  // 0x100 (wrapped, too small)
    /* +0x0C */ uint32_t  evo_written;   // bytes committed so far
    /* +0x10 */ uint32_t  flags;
    /* +0x14 */ uint32_t  reserved[3];
    /* +0x20 */ /* ... decoder pipeline state ... */
};

HEAP STATE BEFORE OVERFLOW:
  [chunk @ 0xb4006a2000]  evo_buf        size=0x100   ← target allocation
  [chunk @ 0xb4006a2100]  next_obj       size=0x200   ← adjacent object (e.g. codec context)
  [chunk @ 0xb4006a2300]  ...

HEAP STATE DURING memcpy (0x10100 bytes written into 0x100-byte buf):
  [chunk @ 0xb4006a2000]  evo_buf        bytes 0x000–0x0ff   ← legitimate write
  [chunk @ 0xb4006a2100]  CORRUPTED      bytes 0x100–0x2ff   ← next_obj header smashed
  [chunk @ 0xb4006a2300]  CORRUPTED      bytes 0x300–...     ← continues ~65 KB further

  Overflow magnitude: 0x10100 - 0x100 = 0x10000 bytes (65,535 bytes past end of alloc)

CORRUPTED next_obj (example — codec context header at +0x100):
  +0x00  vtable ptr   = 0x4141414141414141  ← attacker-controlled DD+ payload bytes
  +0x08  ref_count    = 0x4141414141414141
  +0x10  internal_buf = 0x4141414141414141

Exploitation Mechanics

Project Zero developed a full 0-click exploit for the Pixel 9 using this primitive. The delivery vector is a malformed EAC-3 audio attachment sent via RCS/SMS, decoded automatically by Google Messages before the user opens or interacts with the message.


EXPLOIT CHAIN — CVE-2025-54957, mediacodec RCE stage:

1. DELIVER: Attacker sends crafted EAC-3 audio attachment via RCS to target device.
   No user interaction required; Google Messages triggers automatic transcription decode.

2. PARSE: Dolby UDC opens the bitstream. DD+ frame parser reaches EVO data block.
   evo_priv.c reads attacker-controlled elem_count=0x0101, elem_size=0x0100 from bitstream.

3. WRAP: total_len = 0x0101 * 0x0100 = 0x10100 → truncated to uint16_t → 0x0100.
   malloc(0x100) allocates 256-byte evo_buf on the jemalloc heap.

4. GROOM: Exploit bitstream is constructed to trigger decode operations that place
   a high-value target object (e.g. codec pipeline vtable-bearing object) immediately
   adjacent to evo_buf in the jemalloc slab. Heap layout is deterministic enough
   within mediacodec's allocator state to make this reliable.

5. OVERFLOW: memcpy(evo_buf, src, 0x10100) runs. 65,535 bytes of attacker-controlled
   EVO payload bytes spill past evo_buf, corrupting the adjacent object's vtable pointer.

6. HIJACK: Decoder continues processing. It invokes a virtual method on the now-corrupted
   adjacent object. PC is redirected to attacker-supplied address embedded in the payload.

7. ROP/JOP: Attacker constructs a ROP chain using gadgets from libudcDecoder.so and
   libdolby_audio_decoder.so (both non-PIE in affected builds, or with known base via
   info leak from a separate step). Chain pivots to shellcode or calls mprotect+exec.

8. RESULT: Arbitrary code execution in the mediacodec process context.
   Continues to CVE-2025-36934 for kernel privilege escalation (Part 2).

Patch Analysis

The correct fix is twofold: (1) perform the multiplication in a wider type to detect the overflow before it truncates, and (2) reject or cap the length rather than proceeding with a mismatched allocation/copy pair. The patched version in Dolby UDC 4.14+ does the following:


// BEFORE (vulnerable — Dolby UDC 4.5–4.13, evo_priv.c):
uint16_t total_len = elem_count * elem_size;   // truncates silently

if (state->evo_buf == NULL) {
    state->evo_buf      = malloc(total_len);
    state->evo_buf_size = total_len;
}

if (state->evo_written + total_len > state->evo_buf_size) {
    return EVO_ERR_OVERFLOW;
}

memcpy(state->evo_buf + state->evo_written, src, elem_count * elem_size);
state->evo_written += total_len;


// AFTER (patched — Dolby UDC 4.14+, evo_priv.c):
uint32_t total_len = (uint32_t)elem_count * (uint32_t)elem_size;  // FIX: widen before multiply

if (total_len > EVO_BUF_MAX_SIZE) {   // FIX: explicit upper-bound sanity check
    return EVO_ERR_INVALID_LENGTH;
}

if (state->evo_buf == NULL) {
    state->evo_buf      = malloc(total_len);
    state->evo_buf_size = total_len;
}

// FIX: bounds check now uses same type and value as the memcpy length argument
if (state->evo_written + total_len > state->evo_buf_size) {
    return EVO_ERR_OVERFLOW;
}

memcpy(state->evo_buf + state->evo_written, src, total_len);  // FIX: single source of truth
state->evo_written += total_len;

Three distinct changes: the multiplication operands are widened to uint32_t before the operation; an explicit ceiling constant EVO_BUF_MAX_SIZE is checked against the result; and the memcpy length argument is replaced with the single computed total_len variable rather than re-deriving it, eliminating the type-mismatch between the guard and the actual copy.

Detection and Indicators

During active exploitation, the mediacodec process crashes or produces anomalous output. The following are observable indicators:

  • Crash signal: SIGSEGV or SIGABRT in mediacodec with PC in an unresolvable region, or in memcpy called from evo_write_data. Tombstone will show evo_priv.c in the backtrace on debug builds.
  • Tombstone backtrace pattern: #00 pc ... memcpy ... #01 pc ... evo_write_data ... #02 pc ... evo_parse_block
  • Anomalous EAC-3 files: Valid EAC-3 container with EVO data block where elem_count × elem_size overflows uint16_t — easily identified with a bitstream parser checking for products exceeding 0xFFFF.
  • Network indicator: Inbound RCS audio attachments with MIME type audio/eac3 or audio/vnd.dolby.dd-raw from unknown senders, particularly short files with large EVO metadata blocks disproportionate to audio frame count.

A simple fuzzer harness targeting evo_priv.c with elem_count and elem_size values that produce wraparound products will reliably reproduce the crash within seconds on affected library versions. The AddressSanitizer heap-buffer-overflow report from such a harness will point directly to the memcpy call inside evo_write_data.

Remediation

  • Update Dolby UDC to version 4.14 or later. OEMs must integrate the updated library and ship an OTA. The January 5, 2026 Android security bulletin contains the patch.
  • Apply Android security patches dated January 2026 or later. Google's patch also includes mitigations for the mediacodec sandbox (CVE-2025-36934).
  • OEMs not yet patched should consider disabling automatic audio transcription in Google Messages until the library update is deployed, though this is a product-level workaround and not a security boundary.
  • Compiler-level: Enable -fsanitize=integer (UBSan integer overflow) in CI fuzzing builds targeting audio codec libraries. This class of bug is trivially caught at compile time if integer sanitizers are applied to codec parsing code paths.
  • Architectural: Audio decoders processing 0-click content should operate in a tighter sandbox than general mediacodec; the lesson from this chain is that mediacodec compromise is a viable stepping stone to kernel access (see Part 2, CVE-2025-36934).
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →