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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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