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.
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 currentmaster(pl_mpeg.hSHA-2563a8cb30c83c2a1147719c30fe0c8b93da2987aa43140077c575b39aaa75fc2c9, 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.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 frommb_row/mb_colwith no lower-bound clamp:On the crafted stream
diunderflows to a small negative value, sod[di]reads before the plane. The #64 fix boundsmacroblock_addressbut not this per-block prediction index.Suggested fix: clamp/validate
dito[0, plane_size - block extent]beforePLM_BLOCK_SET, or reject whenmb_row/mb_colfall 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.The bounds check at 3359-3361 validates only
si/di:But the half-pel interpolation cases read up to
s[si + dw + 1]:When
siis at/nearmax_address,si + dw + 1runs 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(anddi + dw + block_sizeon the write side), not justsi <= max_address.Reproduce
Harness reads a stream from stdin and decodes all video+audio frames:
Reproducers (286 bytes each, afl-tmin minimized). Decode base64 to reproduce:
A.mpg (
sha256 78803129…, triggers Bug A):B.mpg (
sha256 0760bafd…, triggers Bug B):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.