From 1e4e15301a0ee677b5663ce61a33426f63c32d67 Mon Sep 17 00:00:00 2001 From: cinemaONE <35371193+cinema-ONE@users.noreply.github.com> Date: Sun, 30 Aug 2026 08:32:48 +0200 Subject: [PATCH] Do not let a malformed file terminate Kodi 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 --- src/MPOPicture.cpp | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/MPOPicture.cpp b/src/MPOPicture.cpp index 21dd72b..cdc4280 100644 --- a/src/MPOPicture.cpp +++ b/src/MPOPicture.cpp @@ -10,9 +10,30 @@ #include "../lib/TinyEXIF/TinyEXIF.h" +#include #include #include +namespace +{ + +// libjpeg's default error_exit calls exit(), and libmpo installs it via +// jpeg_std_error() without overriding it, so a malformed file terminates Kodi +// rather than failing the decode. mpo_decompress_error_exit() can only replace +// the function pointer, not attach state to it, so the jump target is +// thread-local; each decoder instance is driven from one thread at a time. +thread_local std::jmp_buf s_jpegEscape; + +void MpoFatalError(j_common_ptr cinfo) +{ + char message[JMSG_LENGTH_MAX] = {}; + (*cinfo->err->format_message)(cinfo, message); + kodi::Log(ADDON_LOG_ERROR, "libjpeg: %s", message); + std::longjmp(s_jpegEscape, 1); +} + +} // namespace + MPOPicture::MPOPicture(const kodi::addon::IInstanceInfo& instance) : CInstanceImageDecoder(instance) { @@ -36,8 +57,15 @@ bool MPOPicture::SupportsFile(const std::string& file) mpo_decompress_struct mpoinfo; mpo_create_decompress(&mpoinfo); + mpo_decompress_error_exit(&mpoinfo, MpoFatalError); + if (setjmp(s_jpegEscape)) + { + mpo_destroy_decompress(&mpoinfo); + return false; + } + mpo_mem_src(&mpoinfo, buffer.data(), buffer.size()); - bool ret = mpo_read_header(&mpoinfo); + const bool ret = mpo_read_header(&mpoinfo); mpo_destroy_decompress(&mpoinfo); return ret; } @@ -139,6 +167,13 @@ bool MPOPicture::LoadImageFromMemory(const std::string& mimetype, m_data.resize(bufSize); std::copy(buffer, buffer + bufSize, m_data.begin()); mpo_create_decompress(&m_mpoinfo); + mpo_decompress_error_exit(&m_mpoinfo, MpoFatalError); + if (setjmp(s_jpegEscape)) + { + mpo_destroy_decompress(&m_mpoinfo); + return false; + } + mpo_mem_src(&m_mpoinfo, m_data.data(), m_data.size()); if (!mpo_read_header(&m_mpoinfo)) { @@ -169,6 +204,9 @@ bool MPOPicture::Decode(uint8_t* pixels, return false; } + if (setjmp(s_jpegEscape)) + return false; + size_t image = 0; while (image < m_images) {