Stop malformed MPO files terminating Kodi, and bounds-check the MP parser - #73
Conversation
|
| 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]
Reviews (1): Last reviewed commit: "Bounds-check the MP parser's cursor" | Re-trigger Greptile
| 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; |
There was a problem hiding this comment.
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.
8f40c0e to
de3c55a
Compare
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>
de3c55a to
1e4e153
Compare
|
Thanks! |
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 overrideserror_exit, whose default implementation callsexit(). This add-on never calledmpo_decompress_error_exit()to replace it, so a crafted.mpotakes 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:NDEBUGremoves 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 otherbuffer[...]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:The second commit bounds-checks the cursor in
mpf_getbyte(), clamps the file-supplied offset inmpf_seek(), and stopsmpf_dc_rewindc()taking the cursor negative —mpf_getbyte()alone cannot recover from a cursor already out of range.exit(), then heap overflow-DNDEBUG)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 is63ada10from 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 inlib/kodi-libmpo-note.txtfor 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.