CVE-2026-6361
Overview
Background
- `CFX_PSRenderer::DrawDIBits`
- the PDFium routine (named in the commit) that emits a device-independent bitmap into a PostScript stream, including a fallback path that copies raw pixel rows into an allocated buffer.
- `FX_SAFE_UINT32`
- PDFium’s checked-arithmetic integer type that tracks overflow and exposes
IsValid()/ValueOrDie()so multiplications that wrap can be detected before use. - `FX_Alloc`
- PDFium’s heap allocator; here
FX_Alloc(uint8_t, output_size)reservesoutput_sizebytes to hold the bitmap’s pixel data. - Pitch / output size
src_pitch = width * bytes_per_pixelis the per-row byte count andoutput_size = height * src_pitchis the total buffer size the row-copy loop expects.
Root Cause Analysis
In the non-JPEG fallback branch of DrawDIBits, the buffer size was computed with native signed int arithmetic: int src_pitch = width * bytes_per_pixel; and output_size = height * src_pitch;. For a bitmap with large width, height, and bytes_per_pixel, these multiplications overflow 32-bit int, yielding a small or wrapped output_size. FX_Alloc(uint8_t, output_size) then allocates a buffer far smaller than the true pixel data, while the subsequent for (int row = 0; row < height; row++) loop copies height scanlines of real width into it, writing past the end of the allocation — a heap buffer overflow. The invariant that the allocation size equals the number of bytes actually written was violated by unchecked integer overflow.
The fix recomputes the sizes with FX_SAFE_UINT32 (safe_pitch = bytes_per_pixel; safe_pitch *= width; safe_output_size = safe_pitch; safe_output_size *= height;), and if !safe_output_size.IsValid() it writes "\nQ\n" and returns false before any allocation or copy, otherwise extracting the validated sizes via ValueOrDie().
int multiplication, so an overflowed output_size under-allocated the buffer the row loop then overran; the fix performs the same arithmetic in FX_SAFE_UINT32 and bails out on overflow before allocating.Attack Path
- Supply a crafted image
Provide a PDF (or print job) containing a bitmap whose
width,height, andbytes_per_pixelmakewidth * bytes_per_pixel * heightexceed 32-bit range. - Reach the PostScript fallback
Cause rendering through
CFX_PSRenderer::DrawDIBitswhere JPEG encoding is not taken, entering theelsebranch that computessrc_pitch/output_size. - Under-allocate via overflow
The unchecked
intmultiplications wrap to a smalloutput_size, andFX_Alloc(uint8_t, output_size)returns an undersized heap buffer. - Heap overflow in the copy loop
The
for (int row ...; row < height; ...)loop writesheightfull scanlines into the undersized buffer, overflowing the heap allocation (a write primitive; any further escalation is(inferred)).
Impact Assessment
DrawDIBits PostScript path with image dimensions that overflow the 32-bit size computation; the Win32 file path (core/fxge/win32/) indicates this is the Windows PostScript print path. The advisory rates this high severity and reports no in-the-wild exploitation; escalation of the overflow to code execution is (inferred) and not shown by the diff.Changed Functions
| Function | Change | Notes |
|---|---|---|
forcore/fxge/win32/cfx_psrenderer.cpp |
modified |
Files Changed
core/fxge/win32/cfx_psrenderer.cpp
Audit Directions
- Width × height × bpp allocationsAudit image/bitmap buffer sizing across
core/fxgeand codecs for native-integerpitch * heightstyle multiplications; these should useFX_SAFE_*and validate beforeFX_Alloc. - Alloc size vs. copy count mismatchFlag any allocation whose size is computed separately from the loop bound that fills it, where an overflow in one but not the other desynchronizes them.
- `int` for byte countsReview use of signed
intto hold pitches and buffer sizes derived from attacker-influenced dimensions; prefer checked unsigned types throughout the computation.
Patch
From bce2e672827992dcd7289e2872ca10c5c20bf056 Mon Sep 17 00:00:00 2001 From: Tom Sepez <[email protected]> Date: Tue, 07 Apr 2026 15:50:30 -0700 Subject: [PATCH] Use safe arithmetic in CFX_PSRenderer::DrawDIBits() Hardening suggestion from the AI bot. Bug: 500036290 Change-Id: Ie521629d06ba944f610b941a8c9e9505fa29aea7 Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/145731 Reviewed-by: Lei Zhang <[email protected]> Commit-Queue: Tom Sepez <[email protected]> --- diff --git a/core/fxge/win32/cfx_psrenderer.cpp b/core/fxge/win32/cfx_psrenderer.cpp index b38f1a2..b8710e5 100644 --- a/core/fxge/win32/cfx_psrenderer.cpp +++ b/core/fxge/win32/cfx_psrenderer.cpp @@ -620,8 +620,16 @@ encoder_iface_->pJpegEncodeFunc(bitmap, &output_buf, &output_size)) { filter = "/DCTDecode filter "; } else { - int src_pitch = width * bytes_per_pixel; - output_size = height * src_pitch; + FX_SAFE_UINT32 safe_pitch = bytes_per_pixel; + safe_pitch *= width; + FX_SAFE_UINT32 safe_output_size = safe_pitch; + safe_output_size *= height; + if (!safe_output_size.IsValid()) { + WriteString("\nQ\n"); + return false; + } + uint32_t src_pitch = safe_pitch.ValueOrDie(); + output_size = safe_output_size.ValueOrDie(); output_buf = FX_Alloc(uint8_t, output_size); for (int row = 0; row < height; row++) { const uint8_t* src_scan = bitmap->GetScanline(row).data();
Original Bug Report
Potential Integer Overflow leading to OOB Write in CFX_PSRenderer::DrawDIBits
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the security team.
Overview: A potential integer overflow exists in PDFium’s PostScript renderer when calculating the memory required for image conversion. When printing a specially crafted PDF with massive dimensions, this calculation overflows, leading to a severely undersized heap allocation. Subsequent rendering loops write attacker-controlled data extensively out-of-bounds, potentially allowing for remote code execution within the sandboxed utility process.
Affected files:
third_party/pdfium/core/fxge/win32/cfx_psrenderer.cppthird_party/pdfium/core/fpdfapi/page/cpdf_pageimagecache.cppthird_party/pdfium/core/fpdfapi/page/cpdf_dib.cpp
Estimated timestamp from git blame: 2024-08-09
Description
A potential integer overflow vulnerability exists in CFX_PSRenderer::DrawDIBits (third_party/pdfium/core/fxge/win32/cfx_psrenderer.cpp). When a PDF is printed to a PostScript printer, this function allocates a heap buffer to hold converted image data. The calculation for the buffer size relies on standard 32-bit signed integer math (int). If an image is sufficiently large, the mathematical product of height and src_pitch exceeds INT_MAX, wraps around, and truncates to a small positive value, resulting in an undersized buffer allocation.
Vulnerability Details
In CFX_PSRenderer::DrawDIBits, the buffer size is calculated without safe math wrappers:
int src_pitch = width * bytes_per_pixel;
output_size = height * src_pitch;
output_buf = FX_Alloc(uint8_t, output_size);
While upstream PDFium checks (like in CPDF_DIB::LoadInternal) validate image dimensions using FX_SAFE_UINT32, an attacker can bypass these limitations by utilizing an image configuration that expands later in the pipeline. For example, a 16-bit Grayscale image with dimensions 11,075 x 131,071 requires approximately 2.9 GB uncompressed, which fits perfectly within a valid 32-bit unsigned integer.
Because this image exceeds the 60 MB kHugeImageSize caching threshold (CPDF_PageImageCache::Entry::ContinueGetCachedBitmap), PDFium evaluates it lazily (realize_hint=false), avoiding an immediate allocation.
When CFX_PSRenderer::DrawDIBits processes the image, the 16-bpc format is promoted to a 24-bpp BGR format (bytes_per_pixel = 3). The renderer calculates src_pitch as 11,075 * 3 = 33,225. It then calculates output_size as 131,071 * 33,225, which equals 4,354,833,975. This value overflows a 32-bit signed int and truncates to exactly 59,866,679 bytes. FX_Alloc subsequently allocates this severely undersized 59.8 MB buffer.
In the subsequent copy loop:
for (int row = 0; row < height; row++) {
uint8_t* dest_scan = UNSAFE_TODO(output_buf + row * src_pitch);
// ... copies pixels to dest_scan
}
- Forward OOB Write: Starting around row 1,802, the offset
row * 33,225exceeds the allocated 59.8 MB, causing massive forward out-of-bounds writes of attacker-controlled pixel data. - Backward OOB Write: At row 64,635, the signed 32-bit integer offset calculation overflows
INT_MAX(64,635 * 33,225 = 2,147,497,875), wrapping to a negative value (-2,147,469,421). This negative offset is sign-extended and added to theoutput_bufpointer, resulting in a backward out-of-bounds write roughly 2.14 GB before the buffer.
Potential Attack Steps
Note: These are potential steps based on code analysis, as our tooling agent cannot execute code to verify a working proof of concept.
- An attacker crafts a malicious PDF containing an
ImageXObject. - The image specifies
Width= 11,075,Height= 131,071,BitsPerComponent= 16, andColorSpace=DeviceGray, backed by aFlateDecodestream containing carefully structured malicious pixel payloads. - The attacker convinces a victim to open the PDF in Chrome and print it to a PostScript-capable printer on a Windows machine.
- PDFium processes the image lazily due to its large uncompressed footprint (~2.9GB).
- During PostScript rendering, the image is promoted to 24-bpp, triggering the integer overflow in
CFX_PSRenderer::DrawDIBits. - The rendering loop copies the attacker’s decompressed pixel stream wildly out of bounds.
- While writing across gigabytes of memory is likely to hit an unmapped guard page and crash, an attacker might exploit race conditions by overwriting critical structures (like IPC objects or vtables) accessed by concurrent threads within the
kPdfConversionutility process before the crash occurs, potentially achieving Remote Code Execution.
Suggested Fix
The memory calculation in CFX_PSRenderer::DrawDIBits should utilize FX_SAFE_UINT32 to prevent integer overflow and abort rendering if the required size exceeds representable limits.
FX_SAFE_UINT32 safe_src_pitch = width;
safe_src_pitch *= bytes_per_pixel;
FX_SAFE_UINT32 safe_output_size = safe_src_pitch;
safe_output_size *= height;
if (!safe_output_size.IsValid()) {
return false; // or handle error accordingly
}
int src_pitch = safe_src_pitch.ValueOrDie();
size_t output_size = safe_output_size.ValueOrDie();
Evaluated with Chrome root at commit: f200f57a19490707ff8bc7aa5de3cbc443a3afad
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.