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 <tsepez@google.com>
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 <thestig@chromium.org>
Commit-Queue: Tom Sepez <tsepez@chromium.org>
---
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)