High patch-now ✔︎ verified CVSS 8.8 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactHeap buffer overflow in PDFium
DescriptionHeap buffer overflow in PDFium
ComponentPDFium
Bug ClassOOB
Action tierpatch-now — corruption primitive, file-reachable in default config (confirmed) — upheld by independent review by AI
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.
Independent review by AIConfirmed
Reviewer notes: none -- diff adds the td_bitspersample != BITS_IN_JSAMPLE clause in tif_jpeg.c and commit message states it prevents under-sized scanline buffers/write overflow, matching the claimed heap-write OOB.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker496907110
Fix commitca8a943c247c (pdfium) +28/-1
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
Credited86ac1f1587b71893ed2ad792cd7dde32
Reported2026-03-27 (per vendor advisory)
Fix landed2026-03-27 (public git)
Disclosed2026-04-15
Patch gap19 days — fix public in source before the advisory

Background

libtiff `tif_jpeg.c`
The JPEG-in-TIFF codec module bundled in PDFium’s third_party/libtiff, which decodes JPEG-compressed TIFF strips/tiles by driving libjpeg.
`td_bitspersample`
The TIFF BitsPerSample tag value read from the (attacker-controlled) file, used to size decode/scanline buffers.
`BITS_IN_JSAMPLE`
A compile-time constant giving the sample width (typically 8) that the libjpeg build is configured to produce into a JSAMPLE.
`data_precision`
The bit depth declared inside the JPEG stream header (sp->cinfo.d.data_precision), which must match both the TIFF metadata and the compiled sample size for buffers to be correctly sized.

Root Cause Analysis

In the #else branch (the build where data_precision and bits_in_jsample are not force-assigned) within third_party/libtiff/tif_jpeg.c, the precision validation only rejected inputs when sp->cinfo.d.data_precision != td->td_bitspersample—that is, it merely required the JPEG header precision to agree with the TIFF BitsPerSample tag. It never checked either value against BITS_IN_JSAMPLE, the width the decoder actually writes per sample. The violated invariant is that the decoded sample width must equal the width the scanline buffers were allocated for (BITS_IN_JSAMPLE); a file could set both td_bitspersample and data_precision consistently to, say, 12 while the build expects 8, passing the check yet causing libjpeg to emit wider samples than the undersized buffer holds.

The fix tightens the guard to if (td->td_bitspersample != BITS_IN_JSAMPLE || sp->cinfo.d.data_precision != td->td_bitspersample), so any mismatch between the TIFF precision and the compiled BITS_IN_JSAMPLE now triggers TIFFErrorExtR(...) and return (0) before decoding proceeds. Per the upstream commit message, the under-sized scanline buffers otherwise lead to write buffer overflows in jdcolor.c/null_convert when decoding malformed inputs. The enclosing function is not named inside the changed hunk; the @@ header TIFFJPEGIsFullStripRequired is only a nearby location anchor, and the edited code belongs to the JPEG decode-setup path that assigns cinfo.d.data_precision.

Key insight
The core mistake was validating the JPEG precision only against the TIFF tag and never against BITS_IN_JSAMPLE, so matching-but-wrong precisions slipped through; the fix adds the td->td_bitspersample != BITS_IN_JSAMPLE clause so the decoder rejects any precision its scanline buffers were not sized for.

Attack Path

  1. Craft a malicious TIFF An attacker builds a JPEG-compressed TIFF whose BitsPerSample and embedded JPEG data_precision are equal to each other but differ from the decoder’s compiled BITS_IN_JSAMPLE (e.g. both 12 vs. an 8-bit build).
  2. Pass the weak check The pre-patch condition data_precision != td_bitspersample evaluates false because the two agree, so decoding setup proceeds without error.
  3. Allocate undersized buffers Scanline/decode buffers are sized for BITS_IN_JSAMPLE-width samples, smaller than the actual samples libjpeg will produce.
  4. Overflow on decode libjpeg’s color conversion (jdcolor.c/null_convert) writes wider samples than the buffer holds, causing a heap write buffer overflow (sink stated by the upstream commit message, not shown in this diff).

Impact Assessment

An attacker who gets PDFium to decode a crafted JPEG-in-TIFF image (e.g. embedded in a PDF the victim opens) gains a heap buffer write overflow in the PDF-rendering process, with the sole precondition of the victim processing the document. The vendor advisory rates this high severity and labels it a heap buffer overflow in PDFium; no exploited-in-the-wild status is indicated. Any progression to code execution or sandbox escape is (inferred) and not demonstrated by this validation-tightening diff.

Files Changed

  • third_party/libtiff/0034-tiff-jpeg-overflow.patch
  • third_party/libtiff/README.pdfium
  • third_party/libtiff/tif_jpeg.c

Audit Directions

  • Metadata-vs-compile-constant mismatches
    Audit codecs that size buffers from file-declared bit depths; verify the declared precision is validated against the compiled sample/output width, not merely against another file-controlled field.
  • Self-consistent but out-of-range inputs
    Look for validation that only cross-checks two attacker-supplied values against each other, which an attacker can trivially satisfy; add an absolute bound against the implementation’s real limits.
  • JPEG/color-conversion sinks
    Review libjpeg integration points (jdcolor.c, null_convert, scanline buffer allocation) and parallel bundled libraries for other precision/component-count assumptions that could undersize decode buffers.
From ca8a943c247c208fd7a9cd21b4de049f22b93070 Mon Sep 17 00:00:00 2001
From: Lei Zhang <[email protected]>
Date: Fri, 27 Mar 2026 14:52:16 -0700
Subject: [PATCH] Patch an overflow in libtiff

Apply fix [1] from upstream, which is not in the most recent versioned
release.

[1] https://gitlab.com/libtiff/libtiff/-/commit/0f726d9

Bug: 496907110
Change-Id: Ic8665879ebdd4445f473e9a1e156cfc42c294d51
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/145550
Reviewed-by: Andy Phan <[email protected]>
Commit-Queue: Lei Zhang <[email protected]>
---

diff --git a/third_party/libtiff/0034-tiff-jpeg-overflow.patch b/third_party/libtiff/0034-tiff-jpeg-overflow.patch
new file mode 100644
index 0000000..f03fa94
--- /dev/null
+++ b/third_party/libtiff/0034-tiff-jpeg-overflow.patch
@@ -0,0 +1,25 @@
+commit 0f726d9477a11e15eb67ca349c03907f6cfb82a9
+Author: Mikhail Khachaiants <[email protected]>
+Date:   Mon Dec 1 22:26:34 2025 +0200
+
+    tif_jpeg: reject mismatched JPEG data precision to avoid write overflow
+    
+    Ensure TIFF BitsPerSample matches both BITS_IN_JSAMPLE and the JPEG
+    header data_precision for JPEG-compressed images. This prevents
+    under-sized scanline buffers that can lead to write buffer overflows
+    in jdcolor.c/null_convert when decoding malformed inputs.
+
+diff --git a/libtiff/tif_jpeg.c b/libtiff/tif_jpeg.c
+index aba5f99b..4d6370b5 100644
+--- a/libtiff/tif_jpeg.c
++++ b/libtiff/tif_jpeg.c
+@@ -1282,7 +1282,8 @@ int TIFFJPEGIsFullStripRequired(TIFF *tif)
+     sp->cinfo.d.data_precision = td->td_bitspersample;
+     sp->cinfo.d.bits_in_jsample = td->td_bitspersample;
+ #else
+-    if (sp->cinfo.d.data_precision != td->td_bitspersample)
++    if (td->td_bitspersample != BITS_IN_JSAMPLE ||
++        sp->cinfo.d.data_precision != td->td_bitspersample)
+     {
+         TIFFErrorExtR(tif, module, "Improper JPEG data precision");
+         return (0);
diff --git a/third_party/libtiff/README.pdfium b/third_party/libtiff/README.pdfium
index 9953e76..e3f352d 100644
--- a/third_party/libtiff/README.pdfium
+++ b/third_party/libtiff/README.pdfium
@@ -19,3 +19,4 @@
 0028-nstrips-OOM.patch: return error for excess number of tiles/strips.
 0031-safe_size_ingtStripContig.patch: return error if the size to read overflow from int32.
 0033-avail-out-overflow.patch: signed comparison in PixarLogDecode().
+0034-tiff-jpeg-overflow.patch: reject mismatched JPEG data precision.
diff --git a/third_party/libtiff/tif_jpeg.c b/third_party/libtiff/tif_jpeg.c
index 5281457..a9764f0 100644
--- a/third_party/libtiff/tif_jpeg.c
+++ b/third_party/libtiff/tif_jpeg.c
@@ -1287,7 +1287,8 @@
     sp->cinfo.d.data_precision = td->td_bitspersample;
     sp->cinfo.d.bits_in_jsample = td->td_bitspersample;
 #else
-    if (sp->cinfo.d.data_precision != td->td_bitspersample)
+    if (td->td_bitspersample != BITS_IN_JSAMPLE ||
+        sp->cinfo.d.data_precision != td->td_bitspersample)
     {
         TIFFErrorExtR(tif, module, "Improper JPEG data precision");
         return (0);
Loading diff…

Original Bug Report

reported by [email protected]

OOB write in PDFium TIFF decoder via lossless JPEG data precision mismatch

Summary

A heap buffer overflow write exists in PDFium’s TIFF image decoder when processing a TIFF file that uses JPEG compression with a lossless JPEG codestream (SOF3) whose data precision is less than 8 bits. The bundled libtiff calculates the strip buffer size based on the TIFF BitsPerSample tag (e.g. 2 bits per sample), while the bundled libjpeg-turbo, which has lossless JPEG support enabled in Chromium, outputs decoded samples at 8 bits each. This mismatch causes libjpeg-turbo to write 4x more data than the allocated buffer can hold, producing an attacker-controlled heap overflow in the renderer process. The bug affects all platforms and is reachable through XFA PDF forms when the PdfXfaSupport feature is enabled. No user interaction beyond opening a PDF is required.

Bisect

Introducing Commit: 4aa3b725c (PDFium)

  • Date: 2025-10-23
  • Author: Lei Zhang
  • Review: Upgrade libtiff from 4.7.0 to 4.7.1

Root Cause

PDFium’s bundled libtiff validates JPEG data precision in JPEGPreDecode by comparing only the JPEG header’s data_precision against the TIFF tag td_bitspersample:

// third_party/pdfium/third_party/libtiff/tif_jpeg.c
if (sp->cinfo.d.data_precision != td->td_bitspersample)
{
    TIFFErrorExtR(tif, module, "Improper JPEG data precision");
    return (0);
}

This check passes when both values are 2, but it does not verify that the precision equals BITS_IN_JSAMPLE (8), which is the output sample width of libjpeg-turbo. The upstream libtiff fix 0f726d9 adds exactly this check, but the PDFium copy has not incorporated it.

Separately, Chromium’s bundled libjpeg-turbo 3.1.0 defines D_LOSSLESS_SUPPORTED in jmorecfg.h, enabling the lossless JPEG decoding path. This path accepts data_precision values from 2 through 16 in jdinput.c:

// third_party/libjpeg_turbo/src/jdinput.c
#ifdef D_LOSSLESS_SUPPORTED
  if (cinfo->master->lossless) {
    if (cinfo->data_precision < 2 || cinfo->data_precision > 16)
      ERREXIT1(cinfo, JERR_BAD_PRECISION, cinfo->data_precision);
  } else

The post-processing controller in jdpostct.c also accepts precision 2 through 8 when BITS_IN_JSAMPLE == 8:

// third_party/libjpeg_turbo/src/jdpostct.c
#if BITS_IN_JSAMPLE == 8
    if (cinfo->data_precision > BITS_IN_JSAMPLE || cinfo->data_precision < 2)
#endif
      ERREXIT1(cinfo, JERR_BAD_PRECISION, cinfo->data_precision);

A precision of 2 passes all of these checks. The lossless decoder then writes one JSAMPLE (8-bit unsigned char) per output sample through null_convert in jdcolor.c. Meanwhile, libtiff computes the strip buffer size via TIFFScanlineSize(tif), which packs samples at 2 bits each. For a 64-pixel wide, single-component image, the scanline buffer is ceil(64 * 2 / 8) = 16 bytes, but libjpeg-turbo writes 64 * 1 = 64 bytes per row. When JPEGDecode advances the buffer pointer by sp->bytesperline (16 bytes) after the first row and calls jpeg_read_scanlines again, the second row’s 64-byte write extends 48 bytes past the end of the 64-byte strip buffer.

Exploitability

The overflow size is directly controlled by the attacker through the TIFF image dimensions and the JPEG data precision value. For a width of W pixels and a precision of P bits, each scanline overflows by W - ceil(W * P / 8) bytes. Lowering the precision increases the ratio; at precision 2, the overflow is 75% of the image width per row, and additional rows multiply the total. The written bytes are the decoded lossless JPEG sample values, which the attacker encodes into the Huffman stream and therefore fully controls. Combined with the ability to choose arbitrary image dimensions to target specific heap bucket sizes, this gives an attacker a controlled, variable-length heap write primitive in the renderer process.

Reproduce

Tested on Chromium commit 46afa4acada327a9d297f780fc811554a9eecf99 on macOS arm64 and Ubuntu 22.04. The bug is platform-independent. No source modifications are required.

Check out the commit and configure an ASAN build:

is_asan = true
is_debug = false
dcheck_always_on = false

Then build:

autoninja -C out/asan chrome

Generate the malicious PDF using the attached Python script, or use the poc.pdf directly, then open it in Chrome with XFA support enabled:

python3 gen_poc.py

out/asan/Chromium.app/Contents/MacOS/Chromium --user-data-dir=./userdata --enable-features=PdfXfaSupport poc.pdf

The renderer process will abort with ERROR: AddressSanitizer: heap-buffer-overflow, WRITE of size 1, in null_convert (jdcolor.c). The full ASAN trace is in asan.txt.

ASAN output

=================================================================
==34815==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6070001174a0 at pc 0x00035de48638 bp 0x00016b0ec230 sp 0x00016b0ec228
WRITE of size 1 at 0x6070001174a0 thread T0
==34815==WARNING: invalid path to external symbolizer!
==34815==WARNING: Failed to use and restart external symbolizer!
    #0 0x00035de48634 in null_convert+0xfd8 (/Users/test/Desktop/src/chromium/src/out/asan/Chromium.app/Contents/Frameworks/Chromium Framework.framework/Versions/148.0.7759.0/Chromium Framework:arm64+0x13d78634)
    #1 0x00035ddbd638 in sep_upsample+0x300 (/Users/test/Desktop/src/chromium/src/out/asan/Chromium.app/Contents/Frameworks/Chromium Framework.framework/Versions/148.0.7759.0/Chromium Framework:arm64+0x13ced638)
    #2 0x00035de5aa7c in process_data_simple_main+0x138 (/Users/test/Desktop/src/chromium/src/out/asan/Chromium.app/Contents/Frameworks/Chromium Framework.framework/Versions/148.0.7759.0/Chromium Framework:arm64+0x13d8aa7c)
    #3 0x00035ddb8d20 in chromium_jpeg_read_scanlines+0x324 (/Users/test/Desktop/src/chromium/src/out/asan/Chromium.app/Contents/Frameworks/Chromium Framework.framework/Versions/148.0.7759.0/Chromium Framework:arm64+0x13ce8d20)
    #4 0x00035b3375f0 in TIFFjpeg_read_scanlines+0x34 (/Users/test/Desktop/src/chromium/src/out/asan/Chromium.app/Contents/Frameworks/Chromium Framework.framework/Versions/148.0.7759.0/Chromium Framework:arm64+0x112675f0)
    #5 0x00035b332fac in JPEGDecode+0x1f8 (/Users/test/Desktop/src/chromium/src/out/asan/Chromium.app/Contents/Frameworks/Chromium Framework.framework/Versions/148.0.7759.0/Chromium Framework:arm64+0x11262fac)
......

The complete untruncated log is in the attached asan.txt.

References

Credit

Please use 86ac1f1587b71893ed2ad792cd7dde32 as the credit for this vulnerability. Thank you.

View on issue tracker