High watch CVSS 8.8 chrome OOB 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactBuffer overflow in PDFium
DescriptionBuffer overflow in PDFium
ComponentPDFium
Bug ClassOOB
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker556853443
Fix commit0edc2c51443c (pdfium) +154/-59
CWECWE-122 (Heap buffer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedSeungMyung Lee (@sm1ee), Siung kim (@ksw9722)
Reported2026-09-03 (per vendor advisory)
Fix landed2026-09-08 (public git)
Disclosed2026-09-17
Patch gap9 days — fix public in source before the advisory

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 as error_exit so a fatal error performs a longjmp instead 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.

Key insight
The single mistake was using a no-op 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

  1. Deliver a crafted image an attacker supplies a document whose bitmap content PDFium re-encodes to JPEG on Windows through JpegModule::JpegEncode.
  2. Provoke a fatal libjpeg error image dimensions, component counts, or compression parameters trigger a fatal error inside jpeg_start_compress, jpeg_write_scanlines, or jpeg_finish_compress.
  3. `error_exit` returns instead of aborting the old jpeg_common_error_do_nothing handler returns control to libjpeg, which resumes compression on inconsistent internal state rather than unwinding.
  4. Out-of-bounds write (inferred) — libjpeg continues emitting bytes into the jpeg_destination_mgr output buffer while its bookkeeping is invalid, overflowing the allocation.
  5. Corruption leveraged (inferred) — the resulting heap corruption may be shaped toward code execution in the process hosting PDFium.

Impact Assessment

An attacker gains a buffer overflow in the process that runs PDFium’s Windows JPEG encoder, the preconditions being an attacker-controlled image that reaches the encode path and induces a libjpeg fatal error. The advisory classifies this only as “Buffer overflow in PDFium” (bug class OOB); any escalation to memory-read/write primitives or code execution is (inferred). No exploited-in-the-wild status is stated in the advisory.

Changed Functions

FunctionChangeNotes
if
core/fxcodec/jpeg/jpegmodule.cpp
modified
for
core/fxcodec/jpeg/jpegmodule.cpp
modified
while
core/fxcodec/jpeg/jpegmodule.cpp
modified

Files Changed

  • core/fxcodec/jpeg/jpegmodule.cpp
  • core/fxcodec/jpeg/jpeg_common.h
  • core/fxcodec/jpeg/jpeg_common.c

Audit Directions

  • Returning error callbacks
    search for any native-library error/abort callback (especially libjpeg error_exit) stubbed as a no-op that returns, since libraries built to longjmp/abort will run on corrupt state if the handler returns.
  • Incomplete `setjmp` coverage
    verify every C entry point that can raise a fatal error is wrapped by a setjmp guard, mirroring the decompression path, so no compression/decoding call can fail silently.
  • Raw buffers across error paths
    audit FX_Alloc/FX_TryAlloc/FX_Realloc buffers not owned by std::unique_ptr<..., FxFreeDeleter>, which can leak or dangle once new early-return error paths are introduced.
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)
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.