CVE-2026-93381
Overview
Background
- libjpeg `error_exit`
- the callback libjpeg invokes on a fatal error, which by contract must never return to the library.
- `setjmp`/`longjmp`
- the C non-local-jump pair used to save a recovery point and unwind straight out of libjpeg back to it when an error fires.
- `JpegModule::JpegEncode`
- PDFium’s Windows-only JPEG compression routine that streams bitmap scanlines into a growable output buffer tracked by a
jpeg_destination_mgr. - `jpeg_common_error_fatal`
- PDFium’s fatal-error handler (pre-existing, declared in
jpeg_common.h) that the patch now wires aserror_exitso a fatal error performs alongjmpinstead of returning.
Root Cause Analysis
Pre-patch, JpegModule::JpegEncode (guarded by BUILDFLAG(IS_WIN)) installed jpeg_common_error_do_nothing as the error_exit member of its jpeg_error_mgr. That handler is a no-op that simply returns, which violates libjpeg’s core contract that error_exit must not return to the library. When a fatal error arose inside jpeg_start_compress, jpeg_write_scanlines, or jpeg_finish_compress, libjpeg therefore resumed executing on inconsistent internal state instead of aborting, and continued writing through the jpeg_destination_mgr — the buffer-overflow condition the advisory reports (the exact out-of-bounds sink is not shown in the diff and is (inferred)).
The fix rebuilds the compress object in new C wrappers (jpeg_common_create_compress and friends) that each place a setjmp(jpeg_compress->jmpbuf) guard and set error_exit = jpeg_common_error_fatal, so a fatal error longjmps back into the wrapper, which returns FALSE/-1; JpegEncode then checks every wrapper’s result and returns false cleanly. This matches the decompression architecture and additionally moves the destination and line buffers to std::unique_ptr<uint8_t, FxFreeDeleter> so the new early-return paths cannot leak.
error_exit that returned into libjpeg after a fatal error, letting the library keep operating (and writing) on corrupt state; the fix catches those fatal errors with setjmp/longjmp in the C wrappers and aborts the encode instead of continuing.Attack Path
- Deliver a crafted image
an attacker supplies a document whose bitmap content PDFium re-encodes to JPEG on Windows through
JpegModule::JpegEncode. - Provoke a fatal libjpeg error
image dimensions, component counts, or compression parameters trigger a fatal error inside
jpeg_start_compress,jpeg_write_scanlines, orjpeg_finish_compress. - `error_exit` returns instead of aborting
the old
jpeg_common_error_do_nothinghandler returns control to libjpeg, which resumes compression on inconsistent internal state rather than unwinding. -
Out-of-bounds write
(inferred)— libjpeg continues emitting bytes into thejpeg_destination_mgroutput buffer while its bookkeeping is invalid, overflowing the allocation. -
Corruption leveraged
(inferred)— the resulting heap corruption may be shaped toward code execution in the process hosting PDFium.
Impact Assessment
(inferred). No exploited-in-the-wild status is stated in the advisory.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcore/fxcodec/jpeg/jpegmodule.cpp |
modified | |
forcore/fxcodec/jpeg/jpegmodule.cpp |
modified | |
whilecore/fxcodec/jpeg/jpegmodule.cpp |
modified |
Files Changed
core/fxcodec/jpeg/jpegmodule.cppcore/fxcodec/jpeg/jpeg_common.hcore/fxcodec/jpeg/jpeg_common.c
Audit Directions
- Returning error callbackssearch for any native-library error/abort callback (especially libjpeg
error_exit) stubbed as a no-op that returns, since libraries built tolongjmp/abort will run on corrupt state if the handler returns. - Incomplete `setjmp` coverageverify every C entry point that can raise a fatal error is wrapped by a
setjmpguard, mirroring the decompression path, so no compression/decoding call can fail silently. - Raw buffers across error pathsaudit
FX_Alloc/FX_TryAlloc/FX_Reallocbuffers not owned bystd::unique_ptr<..., FxFreeDeleter>, which can leak or dangle once new early-return error paths are introduced.
Patch
From 0edc2c51443c64b3589c2a919a0c1076c9d5547b Mon Sep 17 00:00:00 2001 From: Tom Sepez <[email protected]> Date: Tue, 08 Sep 2026 15:45:50 -0700 Subject: [PATCH] Improve error handling in libjpeg encoding Avoid ignoring fatal errors during Windows JPEG compression by using longjmp to catch failures in libjpeg C wrapper functions, matching the decompression architecture. -- use modern memory ownership model. -- avoid array of [1]. TAG=agy CONV=2e125a78-6471-43e2-9eed-d2106f60cbec Bug: 556853443 Change-Id: I2e5eadc41d277f2098ebc91be91d67578c10301f Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/157030 Reviewed-by: Lei Zhang <[email protected]> Commit-Queue: Tom Sepez <[email protected]> --- diff --git a/core/fxcodec/jpeg/jpeg_common.c b/core/fxcodec/jpeg/jpeg_common.c index d2798f6..9f06ecf 100644 --- a/core/fxcodec/jpeg/jpeg_common.c +++ b/core/fxcodec/jpeg/jpeg_common.c @@ -88,6 +88,63 @@ } #if BUILDFLAG(IS_WIN) +boolean jpeg_common_create_compress(JpegCompressCommon* jpeg_compress) { + if (setjmp(jpeg_compress->jmpbuf) == -1) { + return FALSE; + } + jpeg_compress->cinfo.err = &jpeg_compress->error_mgr; + jpeg_compress->cinfo.client_data = jpeg_compress; + jpeg_compress->error_mgr.error_exit = jpeg_common_error_fatal; + jpeg_compress->error_mgr.emit_message = jpeg_common_error_do_nothing_int; + jpeg_compress->error_mgr.output_message = jpeg_common_error_do_nothing; + jpeg_compress->error_mgr.format_message = jpeg_common_error_do_nothing_char; + jpeg_compress->error_mgr.reset_error_mgr = jpeg_common_error_do_nothing; + jpeg_create_compress(&jpeg_compress->cinfo); + jpeg_compress->dest_mgr.init_destination = jpeg_common_dest_do_nothing; + jpeg_compress->dest_mgr.term_destination = jpeg_common_dest_do_nothing; + jpeg_compress->dest_mgr.empty_output_buffer = jpeg_common_dest_empty; + jpeg_compress->cinfo.dest = &jpeg_compress->dest_mgr; + return TRUE; +} + +void jpeg_common_destroy_compress(JpegCompressCommon* jpeg_compress) { + jpeg_destroy_compress(&jpeg_compress->cinfo); +} + +boolean jpeg_common_set_defaults(JpegCompressCommon* jpeg_compress) { + if (setjmp(jpeg_compress->jmpbuf) == -1) { + return FALSE; + } + jpeg_set_defaults(&jpeg_compress->cinfo); + return TRUE; +} + +boolean jpeg_common_start_compress(JpegCompressCommon* jpeg_compress, + boolean write_all_tables) { + if (setjmp(jpeg_compress->jmpbuf) == -1) { + return FALSE; + } + jpeg_start_compress(&jpeg_compress->cinfo, write_all_tables); + return TRUE; +} + +int jpeg_common_write_scanlines(JpegCompressCommon* jpeg_compress, + JSAMPROW* scanlines, + unsigned int num_lines) { + if (setjmp(jpeg_compress->jmpbuf) == -1) { + return -1; + } + return (int)jpeg_write_scanlines(&jpeg_compress->cinfo, scanlines, num_lines); +} + +boolean jpeg_common_finish_compress(JpegCompressCommon* jpeg_compress) { + if (setjmp(jpeg_compress->jmpbuf) == -1) { + return FALSE; + } + jpeg_finish_compress(&jpeg_compress->cinfo); + return TRUE; +} + void jpeg_common_dest_do_nothing(j_compress_ptr cinfo) {} boolean jpeg_common_dest_empty(j_compress_ptr cinfo) { diff --git a/core/fxcodec/jpeg/jpeg_common.h b/core/fxcodec/jpeg/jpeg_common.h index b82ea60..88b3331 100644 --- a/core/fxcodec/jpeg/jpeg_common.h +++ b/core/fxcodec/jpeg/jpeg_common.h @@ -62,6 +62,24 @@ void jpeg_common_error_fatal(j_common_ptr cinfo); #if BUILDFLAG(IS_WIN) +struct JpegCompressCommon { + jmp_buf jmpbuf; + struct jpeg_compress_struct cinfo; + struct jpeg_error_mgr error_mgr; + struct jpeg_destination_mgr dest_mgr; +}; +typedef struct JpegCompressCommon JpegCompressCommon; + +boolean jpeg_common_create_compress(JpegCompressCommon* jpeg_compress); +void jpeg_common_destroy_compress(JpegCompressCommon* jpeg_compress); +boolean jpeg_common_set_defaults(JpegCompressCommon* jpeg_compress); +boolean jpeg_common_start_compress(JpegCompressCommon* jpeg_compress, + boolean write_all_tables); +int jpeg_common_write_scanlines(JpegCompressCommon* jpeg_compress, + JSAMPROW* scanlines, + unsigned int num_lines); +boolean jpeg_common_finish_compress(JpegCompressCommon* jpeg_compress); + void jpeg_common_dest_do_nothing(j_compress_ptr cinfo); boolean jpeg_common_dest_empty(j_compress_ptr cinfo); #endif // BUILDFLAG(IS_WIN) diff --git a/core/fxcodec/jpeg/jpegmodule.cpp b/core/fxcodec/jpeg/jpegmodule.cpp index 5fd613d..bf71bdc 100644 --- a/core/fxcodec/jpeg/jpegmodule.cpp +++ b/core/fxcodec/jpeg/jpegmodule.cpp @@ -19,11 +19,10 @@ #endif #if BUILDFLAG(IS_WIN) -#include <type_traits> - #include "core/fxcodec/jpeg/jpeg_common.h" #include "core/fxcrt/compiler_specific.h" #include "core/fxcrt/fx_memory.h" +#include "core/fxcrt/fx_memory_wrappers.h" #include "core/fxcrt/fx_safe_types.h" #include "core/fxcrt/numerics/safe_conversions.h" #include "core/fxcrt/retain_ptr.h" @@ -33,6 +32,22 @@ namespace fxcodec { +#if BUILDFLAG(IS_WIN) +namespace { + +struct ScopedJpegCompressCommon { + ~ScopedJpegCompressCommon() { + if (created) { + jpeg_common_destroy_compress(&common); + } + } + JpegCompressCommon common = {}; + bool created = false; +}; + +} // namespace +#endif // BUILDFLAG(IS_WIN) + // static std::unique_ptr<ScanlineDecoder> JpegModule::CreateDecoder( pdfium::span<const uint8_t> src_span, @@ -64,17 +79,6 @@ bool JpegModule::JpegEncode(const RetainPtr<const CFX_DIBBase>& pSource, uint8_t** dest_buf, size_t* dest_size) { - jpeg_error_mgr jerr; - jerr.error_exit = jpeg_common_error_do_nothing; - jerr.emit_message = jpeg_common_error_do_nothing_int; - jerr.output_message = jpeg_common_error_do_nothing; - jerr.format_message = jpeg_common_error_do_nothing_char; - jerr.reset_error_mgr = jpeg_common_error_do_nothing; - - jpeg_compress_struct cinfo = {}; // Aggregate initialization. - static_assert(std::is_aggregate_v<decltype(cinfo)>); - cinfo.err = &jerr; - jpeg_create_compress(&cinfo); const int bytes_per_pixel = pSource->GetBPP() / 8; uint32_t nComponents = bytes_per_pixel >= 3 ? 3 : 1; uint32_t pitch = pSource->GetPitch(); @@ -88,48 +92,57 @@ return false; } + static constexpr uint32_t kMinTryBufLen = 1024; uint32_t dest_buf_length = safe_buf_len.ValueOrDie(); - *dest_buf = FX_TryAlloc(uint8_t, dest_buf_length); - const int MIN_TRY_BUF_LEN = 1024; - while (!(*dest_buf) && dest_buf_length > MIN_TRY_BUF_LEN) { - dest_buf_length >>= 1; - *dest_buf = FX_TryAlloc(uint8_t, dest_buf_length); + std::unique_ptr<uint8_t, FxFreeDeleter> local_dest; + for (; dest_buf_length >= kMinTryBufLen; dest_buf_length >>= 1) { + local_dest.reset(FX_TryAlloc(uint8_t, dest_buf_length)); + if (local_dest) { + break; + } } - if (!(*dest_buf)) { + if (!local_dest) { return false; } - jpeg_destination_mgr dest; - dest.init_destination = jpeg_common_dest_do_nothing; - dest.term_destination = jpeg_common_dest_do_nothing; - dest.empty_output_buffer = jpeg_common_dest_empty; - dest.next_output_byte = *dest_buf; - dest.free_in_buffer = dest_buf_length; - cinfo.dest = &dest; - cinfo.image_width = width; - cinfo.image_height = height; - cinfo.input_components = nComponents; - if (nComponents == 1) { - cinfo.in_color_space = JCS_GRAYSCALE; - } else if (nComponents == 3) { - cinfo.in_color_space = JCS_RGB; - } else { - cinfo.in_color_space = JCS_CMYK; - } - uint8_t* line_buf = nullptr; - if (nComponents > 1) { - line_buf = FX_Alloc2D(uint8_t, width, nComponents); + ScopedJpegCompressCommon compress_common; + compress_common.created = + jpeg_common_create_compress(&compress_common.common); + if (!compress_common.created) { + return false; } - jpeg_set_defaults(&cinfo); - jpeg_start_compress(&cinfo, TRUE); - JSAMPROW row_pointer[1]; + compress_common.common.dest_mgr.next_output_byte = local_dest.get(); + compress_common.common.dest_mgr.free_in_buffer = dest_buf_length; + compress_common.common.cinfo.image_width = width; + compress_common.common.cinfo.image_height = height; + compress_common.common.cinfo.input_components = nComponents; + if (nComponents == 1) { + compress_common.common.cinfo.in_color_space = JCS_GRAYSCALE; + } else if (nComponents == 3) { + compress_common.common.cinfo.in_color_space = JCS_RGB; + } else { + compress_common.common.cinfo.in_color_space = JCS_CMYK; + } + + std::unique_ptr<uint8_t, FxFreeDeleter> line_buf; + if (nComponents > 1) { + line_buf.reset(FX_Alloc2D(uint8_t, width, nComponents)); + } + + if (!jpeg_common_set_defaults(&compress_common.common) || + !jpeg_common_start_compress(&compress_common.common, TRUE)) { + return false; + } + + JSAMPROW row_pointer; JDIMENSION row; - while (cinfo.next_scanline < cinfo.image_height) { + while (compress_common.common.cinfo.next_scanline < + compress_common.common.cinfo.image_height) { pdfium::span<const uint8_t> src_scan = - pSource->GetScanline(cinfo.next_scanline); + pSource->GetScanline(compress_common.common.cinfo.next_scanline); if (nComponents > 1) { - uint8_t* dest_scan = line_buf; + uint8_t* dest_scan = line_buf.get(); if (nComponents == 3) { UNSAFE_TODO({ for (uint32_t i = 0; i < width; i++) { @@ -146,29 +159,36 @@ } }); } - row_pointer[0] = line_buf; + row_pointer = line_buf.get(); } else { - row_pointer[0] = const_cast<uint8_t*>(src_scan.data()); + row_pointer = const_cast<uint8_t*>(src_scan.data()); } - row = cinfo.next_scanline; - jpeg_write_scanlines(&cinfo, row_pointer, 1); + row = compress_common.common.cinfo.next_scanline; + if (jpeg_common_write_scanlines(&compress_common.common, &row_pointer, 1) < + 0) { + return false; + } UNSAFE_TODO({ - if (cinfo.next_scanline == row) { + if (compress_common.common.cinfo.next_scanline == row) { static constexpr size_t kJpegBlockSize = 1048576; - *dest_buf = - FX_Realloc(uint8_t, *dest_buf, dest_buf_length + kJpegBlockSize); - dest.next_output_byte = - *dest_buf + dest_buf_length - dest.free_in_buffer; + local_dest.reset(FX_Realloc(uint8_t, local_dest.release(), + dest_buf_length + kJpegBlockSize)); + compress_common.common.dest_mgr.next_output_byte = + local_dest.get() + dest_buf_length - + compress_common.common.dest_mgr.free_in_buffer; dest_buf_length += kJpegBlockSize; - dest.free_in_buffer += kJpegBlockSize; + compress_common.common.dest_mgr.free_in_buffer += kJpegBlockSize; } }); } - jpeg_finish_compress(&cinfo); - jpeg_destroy_compress(&cinfo); - FX_Free(line_buf); - *dest_size = dest_buf_length - static_cast<size_t>(dest.free_in_buffer); + if (!jpeg_common_finish_compress(&compress_common.common)) { + return false; + } + *dest_size = + dest_buf_length - + static_cast<size_t>(compress_common.common.dest_mgr.free_in_buffer); + *dest_buf = local_dest.release(); return true; } #endif // BUILDFLAG(IS_WIN)