Skip to content

Stop malformed MPO files terminating Kodi, and bounds-check the MP parser - #73

Merged
garbear merged 1 commit into
xbmc:Piersfrom
cinema-ONE:mpo-jpeg-error-handler
Aug 30, 2026
Merged

garbear merged 1 commit into
xbmc:Piersfrom
cinema-ONE:mpo-jpeg-error-handler

Conversation

@cinema-ONE

Copy link
Copy Markdown
Contributor

Two related fixes for malformed MPO files, found by fuzzing libmpo as it actually ships.

A malformed file terminates Kodi

libmpo installs libjpeg's error handler with jpeg_std_error() and never overrides error_exit, whose default implementation calls exit(). This add-on never called mpo_decompress_error_exit() to replace it, so a crafted .mpo takes the whole application down rather than failing the decode — no memory corruption required, and completely reliable.

The first commit installs a handler that logs the libjpeg message and jumps back to the caller. libmpo's API can only replace the handler's function pointer, not attach state to it, so the jump target is thread-local.

The MP parser reads out of bounds

mpf_getbyte() is the primitive every 16- and 32-bit reader in the MP extension parser is built on:

unsigned int mpf_getbyte (MPFbuffer_ptr b)
{
    assert(b->_cur < b->_size);   /* the only bounds check */
    return b->buffer[b->_cur++];
}

NDEBUG removes that assert from release builds, so the parser reads from a file-controlled offset with nothing checking it. It is the single choke point — the only other buffer[...] accesses in libmpo are two constant writes in the data source.

This only became reachable once the exit() above stopped firing first. With the handler installed, fuzzing found a heap-buffer-overflow read within minutes:

READ of size 1
  #0 mpf_getbyte      src/mpo.c:32
  #1 mpf_getint16     src/mpo.c:43
  #2 MPExtReadTag     src/mpo.c:253
  #3 MPExtReadMPF     src/mpo.c:402
  #4 MPExtReadAPP02   src/dmpo.c:66
  #9 mpo_read_header  src/dmpo.c:123

The second commit bounds-checks the cursor in mpf_getbyte(), clamps the file-supplied offset in mpf_seek(), and stops mpf_dc_rewindc() taking the cursor negative — mpf_getbyte() alone cannot recover from a cursor already out of range.

Before After
Known bad inputs exit(), then heap overflow both parse cleanly
Fuzzing (ASan + UBSan, -DNDEBUG) crash within minutes 12,750 runs, 0 crashes

On patching vendored code

This is a downstream change to lib/libmpo, which I would normally avoid — drift is how the vendored TinyEXIF here ended up five years behind. But libmpo's upstream is finished: the last code commit is 63ada10 from August 2017, and the only commit since is a README edit in 2020. There is nothing to sync to and nobody to report to, so the patch is recorded in lib/kodi-libmpo-note.txt for whoever looks next.

Builds clean, and composes with #71 — I built both together.


Written by my AI co-author (Claude Code); posted from my account.

@greptile-apps

greptile-apps Bot commented Aug 30, 2026

Copy link
Copy Markdown

Greptile Summary

This PR prevents malformed MPO input from invoking libjpeg's process-terminating default error handler and adds cursor bounds handling to the vendored MP parser.

  • Installs a thread-local setjmp/longjmp recovery path around MPO header and image decoding.
  • Clamps parser seeks and prevents reads or rewinds outside the MP buffer.
  • Documents the downstream libmpo modifications.
  • The EOF behavior still permits a truncated MP extension to produce a successful zero-image load.

Confidence Score: 4/5

The malformed-input hardening should not merge until truncated MP metadata is propagated as a parse failure instead of a successful zero-image decode.

The bounds check removes the out-of-bounds read, but returning synthetic zero bytes without parser error state lets a reachable truncated APP2 segment pass header loading with invalid image metadata.

Files Needing Attention: lib/libmpo/src/mpo.c and src/MPOPicture.cpp

Important Files Changed

Filename Overview
lib/libmpo/src/mpo.c Adds cursor bounds handling, but EOF is represented as unlimited zero bytes and malformed MP metadata can consequently be accepted.
src/MPOPicture.cpp Replaces libjpeg's process-terminating fatal handler with per-thread recovery around header parsing and decoding.
lib/kodi-libmpo-note.txt Records the purpose and downstream status of the vendored parser changes.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Malformed MPO input] --> B[mpo_read_header]
  B --> C[MP APP2 parser]
  C -->|cursor in bounds| D[Read MP fields]
  C -->|cursor at EOF| E[mpf_getbyte returns 0]
  E --> F[Missing fields become zero]
  F --> G[MPExtReadMPF reports success]
  G --> H[Load succeeds with zero images]
  B -->|libjpeg fatal error| I[MpoFatalError]
  I --> J[longjmp to caller]
  J --> K[Decode or load returns false]
Loading

Reviews (1): Last reviewed commit: "Bounds-check the MP parser's cursor" | Re-trigger Greptile

Comment thread lib/libmpo/src/mpo.c Outdated
assert(b->_cur < b->_size);
/* Kodi downstream: this assert was the only bounds check on every read in
the MP parser, and NDEBUG removes it from the builds we ship. */
if(b->_cur < 0 || b->_cur >= b->_size)return 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 EOF Produces Successful Parse

When an APP2 MP extension ends before NumberOfImages is fully available, mpf_getbyte supplies zero for each missing byte without recording an error, while MPExtReadMPF still reports success. LoadImageFromMemory consequently accepts a zero-image MPO, reports zero width, and Decode returns success without writing any pixels.

libmpo installs libjpeg's error handler with jpeg_std_error() and never
overrides error_exit, whose default implementation calls exit(). A malformed
MPO therefore takes the whole application down rather than failing the
decode, and the add-on never called mpo_decompress_error_exit() to replace it.

Found by fuzzing libmpo as it actually ships. With NDEBUG - which Release
builds define - a crafted file reaches exit() from mpo_read_header(). Without
NDEBUG the same input trips one of libmpo's asserts instead; those asserts are
the only validation of the attacker-controlled offsets in that parser, and
they are compiled out of the builds we ship.

libmpo's API can only replace the handler's function pointer, not attach
state to it, so the jump target is thread-local.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@cinema-ONE
cinema-ONE force-pushed the mpo-jpeg-error-handler branch from de3c55a to 1e4e153 Compare August 30, 2026 10:59
@garbear
garbear merged commit eabad85 into xbmc:Piers Aug 30, 2026
8 checks passed
@garbear

garbear commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Thanks!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants