CVE-2026-106284
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifprinting/emf_win.cc |
modified | |
TESTprinting/emf_win_unittest.cc |
modified |
Files Changed
printing/emf_win_unittest.ccprinting/emf_win.cc
Patch
From 91c5000a563ae5bd2fe97e20c0c62db229d3d039 Mon Sep 17 00:00:00 2001 From: Lei Zhang <[email protected]> Date: Sat, 12 Sep 2026 00:11:52 -0700 Subject: [PATCH] Printing: Verify record size in PostScriptMetaFile::SafePlayback() In PostScriptMetaFile::SafePlayback(), EMR_GDICOMMENT records could potentially be too small: - Emf::Enumerator::EnhMetaFileProc() only enforces record->nSize >= 8 (kMinEnhMetaRecordSize), but an EMRGDICOMMENT structure requires 12 bytes for its header before comment Data starts. - SafePlayback() directly dereferenced a 16-bit payload length from comment->Data without checking if the record or comment contained at least 2 bytes. - comment->cbData was not validated against the actual record size, which could allow ExtEscape() to read past the end of the record. Fix this by: - Adding a GetGdiCommentData() helper that validates that the record span is at least sizeof(EMRGDICOMMENT) and that comment->cbData fits within the record span. - Using base::span in SafePlayback() to verify that comment data contains at least 2 bytes before decoding the payload length. - Using base::U16FromNativeEndian() to safely read the 16-bit payload size without alignment hazards. - Validating that the expected data size matches the comment data span size before passing it to ExtEscape(). - Adding unit test coverage in PostScriptMetaFileTest for undersized comment records. TAG=agy CONV=33d756ea-4edb-43ab-a6c9-b245813aa16c Bug: 517804731 Change-Id: Ie6e13aad70a2bc0753cd0e4ecb0cd7b41c00337d Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8395682 Reviewed-by: Will Harris <[email protected]> Commit-Queue: Lei Zhang <[email protected]> Cr-Commit-Position: refs/heads/main@{#1696826} --- diff --git a/printing/emf_win.cc b/printing/emf_win.cc index 64e60935..3db474d 100644 --- a/printing/emf_win.cc +++ b/printing/emf_win.cc @@ -11,6 +11,8 @@ #include "base/check_op.h" #include "base/compiler_specific.h" +#include "base/containers/span.h" +#include "base/numerics/byte_conversions.h" #include "base/numerics/checked_math.h" #include "base/numerics/safe_conversions.h" #include "printing/mojom/print.mojom.h" @@ -93,6 +95,23 @@ return record_span.subspan(sdib_record->offBitsSrc, sdib_record->cbBitsSrc); } +base::span<const uint8_t> GetGdiCommentData( + base::span<const uint8_t> record_span) { + if (record_span.size() < sizeof(EMRGDICOMMENT)) { + return {}; + } + + const auto* comment = + reinterpret_cast<const EMRGDICOMMENT*>(record_span.data()); + base::CheckedNumeric<uint32_t> end_data = offsetof(EMRGDICOMMENT, Data); + end_data += comment->cbData; + if (!end_data.IsValid() || end_data.ValueOrDie() > record_span.size()) { + return {}; + } + + return record_span.subspan(offsetof(EMRGDICOMMENT, Data), comment->cbData); +} + } // namespace Emf::Emf() = default; @@ -238,21 +257,31 @@ continue; } - const auto* comment = reinterpret_cast<const EMRGDICOMMENT*>(emf_record); - const char* data = reinterpret_cast<const char*>(comment->Data); + // SAFETY: `Emf::Enumerator::EnhMetaFileProc()` verified that `nSize` fits + // within the remaining metafile buffer. + base::span<const uint8_t> record_span = UNSAFE_BUFFERS(base::span( + reinterpret_cast<const uint8_t*>(emf_record), emf_record->nSize)); + base::span<const uint8_t> data_span = GetGdiCommentData(record_span); + if (data_span.size() < sizeof(uint16_t)) { + continue; + } + // First uint16_t element in `data` holds the size of the rest of `data`, // which is the actual PostScript data. Windows requires this payload size + // PS data structure. - const uint16_t ps_payload_size = *reinterpret_cast<const uint16_t*>(data); + const uint16_t ps_payload_size = + base::U16FromNativeEndian(data_span.first<sizeof(uint16_t)>()); const uint32_t data_size = 2 + ps_payload_size; // Assume value used in PDFium's core/fxge/win32/cpsoutput.cpp is not going // to change. static constexpr uint16_t kExpectedPdfiumMax = 1024 + 2; - if (data_size != comment->cbData || data_size > kExpectedPdfiumMax) { + if (data_size != data_span.size() || data_size > kExpectedPdfiumMax) { continue; } - int ret = ExtEscape(hdc, PASSTHROUGH, data_size, data, 0, nullptr); + int ret = + ExtEscape(hdc, PASSTHROUGH, static_cast<int>(data_size), + reinterpret_cast<const char*>(data_span.data()), 0, nullptr); DCHECK_EQ(ps_payload_size, ret); } return true; diff --git a/printing/emf_win_unittest.cc b/printing/emf_win_unittest.cc index d749287..61276bdd 100644 --- a/printing/emf_win_unittest.cc +++ b/printing/emf_win_unittest.cc @@ -203,4 +203,33 @@ EXPECT_EQ(remaining_size, 0u); } +TEST(PostScriptMetaFileTest, SafePlaybackUndersizedComment) { + std::vector<char> data; + { + PostScriptMetaFile emf; + EXPECT_TRUE(emf.Init()); + EXPECT_TRUE(emf.context()); + // Add `EMR_GDICOMMENT` records where the comment size field is deliberately + // wrong/undersized (< sizeof(uint16_t)). A valid PostScript comment + // requires at least 2 bytes for the payload size header, so a size of 0 or + // 1 cannot hold a valid payload size. + EXPECT_TRUE(::GdiComment(emf.context(), 0, nullptr)); + uint8_t byte = 0; + EXPECT_TRUE(::GdiComment(emf.context(), 1, &byte)); + ::Rectangle(emf.context(), 10, 10, 190, 190); + EXPECT_TRUE(emf.FinishDocument()); + uint32_t size = emf.GetDataSize(); + EXPECT_GT(size, 0u); + EXPECT_TRUE(emf.GetDataAsVector(&data)); + } + + PostScriptMetaFile emf; + EXPECT_TRUE(emf.InitFromData(base::as_byte_span(data))); + base::win::ScopedCreateDC hdc(CreateCompatibleDC(nullptr)); + ASSERT_TRUE(hdc.is_valid()); + // `SafePlayback()` should safely skip the undersized comment records and must + // not crash from an out-of-bounds read. + EXPECT_TRUE(emf.SafePlayback(hdc.get())); +} + } // namespace printing
Regression Test / PoC
diff --git a/printing/emf_win_unittest.cc b/printing/emf_win_unittest.cc
index d749287..61276bdd 100644
--- a/printing/emf_win_unittest.cc
+++ b/printing/emf_win_unittest.cc
@@ -203,4 +203,33 @@
EXPECT_EQ(remaining_size, 0u);
}
+TEST(PostScriptMetaFileTest, SafePlaybackUndersizedComment) {
+ std::vector<char> data;
+ {
+ PostScriptMetaFile emf;
+ EXPECT_TRUE(emf.Init());
+ EXPECT_TRUE(emf.context());
+ // Add `EMR_GDICOMMENT` records where the comment size field is deliberately
+ // wrong/undersized (< sizeof(uint16_t)). A valid PostScript comment
+ // requires at least 2 bytes for the payload size header, so a size of 0 or
+ // 1 cannot hold a valid payload size.
+ EXPECT_TRUE(::GdiComment(emf.context(), 0, nullptr));
+ uint8_t byte = 0;
+ EXPECT_TRUE(::GdiComment(emf.context(), 1, &byte));
+ ::Rectangle(emf.context(), 10, 10, 190, 190);
+ EXPECT_TRUE(emf.FinishDocument());
+ uint32_t size = emf.GetDataSize();
+ EXPECT_GT(size, 0u);
+ EXPECT_TRUE(emf.GetDataAsVector(&data));
+ }
+
+ PostScriptMetaFile emf;
+ EXPECT_TRUE(emf.InitFromData(base::as_byte_span(data)));
+ base::win::ScopedCreateDC hdc(CreateCompatibleDC(nullptr));
+ ASSERT_TRUE(hdc.is_valid());
+ // `SafePlayback()` should safely skip the undersized comment records and must
+ // not crash from an out-of-bounds read.
+ EXPECT_TRUE(emf.SafePlayback(hdc.get()));
+}
+
} // namespace printing
Original Bug Report
Heap OOB Read in PostScriptMetaFile::SafePlayback due to missing record bounds check
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential heap out-of-bounds read exists in PostScriptMetaFile::SafePlayback when parsing malformed GDI comment records from an EMF stream. By manipulating the record’s physical size (nSize) relative to its claimed comment data size (cbData), a compromised utility process could trigger an out-of-bounds read of up to 1022 bytes. This could lead to a crash of the privileged browser process (Denial of Service) or an information leak on Windows.
Affected files:
printing/emf_win.cc
Estimated timestamp from git blame: 2026-03-25
Summary
There is a potential Out-of-Bounds (OOB) heap read vulnerability in PostScriptMetaFile::SafePlayback inside printing/emf_win.cc. The function processes EMR_GDICOMMENT records from an EMF blob returned by the sandboxed PDF conversion utility process (kPdfConversion). However, it fails to perform proper bounds checking on the size of the record payload relative to the actual physical record boundary (emf_record->nSize). A compromised utility process could return a crafted EMF record that triggers a heap out-of-bounds read of up to 1022 bytes in the privileged browser process during print operations on Windows.
Vulnerability Analysis
In PostScriptMetaFile::SafePlayback (printing/emf_win.cc):
bool PostScriptMetaFile::SafePlayback(HDC hdc) const {
Emf::Enumerator emf_enum(*this, nullptr, nullptr);
for (const Emf::Record& record : emf_enum) {
const ENHMETARECORD* emf_record = record.record();
if (emf_record->iType != EMR_GDICOMMENT) {
continue;
}
const auto* comment = reinterpret_cast<const EMRGDICOMMENT*>(emf_record);
const char* data = reinterpret_cast<const char*>(comment->Data);
const uint16_t ps_payload_size = *reinterpret_cast<const uint16_t*>(data);
const uint32_t data_size = 2 + ps_payload_size;
static constexpr uint16_t kExpectedPdfiumMax = 1024 + 2;
if (data_size != comment->cbData || data_size > kExpectedPdfiumMax) {
continue;
}
int ret = ExtEscape(hdc, PASSTHROUGH, data_size, data, 0, nullptr);
DCHECK_EQ(ps_payload_size, ret);
}
return true;
}
The function reads ps_payload_size from comment->Data and compares data_size = 2 + ps_payload_size to comment->cbData and kExpectedPdfiumMax (1026). This is a consistency check on attacker-controlled values, not a physical bounds validation. Specifically, it never verifies that data_size (or comment->cbData) is less than or equal to emf_record->nSize - 12 (the actual space allocated for the payload inside the EMF record).
By default on Windows, out-of-process (OOP) printing is disabled because kEnableOopPrintDrivers is defined as FEATURE_DISABLED_BY_DEFAULT (see printing/printing_features.cc). Consequently, SafePlayback executes inside the privileged browser process.
A compromised utility process can return a crafted EMF blob with an EMR_GDICOMMENT record where nSize is small (e.g., 16 bytes, meaning comment->Data only has 4 bytes of valid allocation), but cbData is 1026 and ps_payload_size is 1024. All conditions in the validation check will be satisfied:
data_size == comment->cbData(1026 == 1026) -> Truedata_size <= 1026-> True
Subsequently, ExtEscape(hdc, PASSTHROUGH, 1026, data, 0, nullptr) is called, which instructs GDI to read 1026 bytes starting from data. Since the allocated record has nSize = 16, only the first 4 bytes are valid, leading to a heap out-of-bounds read of up to 1022 bytes beyond the record boundaries.
Potential Steps to Trigger
Note: The following are potential steps that an attacker could follow to trigger this vulnerability, as our automated analysis tooling cannot execute code dynamically.
- Compromise the
kPdfConversionutility process (for example, through a separate parser exploit in PDFium). - Lure the victim to trigger a print operation on a page to a PostScript-compatible printer (or a ‘Generic / Text Only’ driver) to ensure the printer mode is set to PostScript.
- Intercept the Mojo
ConvertPagereply from the utility process to the browser, returning a crafted EMF blob inside theReadOnlySharedMemoryRegioncontaining:- A valid
ENHMETAHEADER - An
EMR_GDICOMMENTrecord withiType = 70(EMR_GDICOMMENT),nSize = 16,cbData = 1026, and the first 2 bytes ofDataset to0x0400(little-endian uint16_t representing 1024). - A valid
EMR_EOFrecord.
- A valid
- When the browser process receives this EMF blob, it will invoke
PostScriptMetaFile::SafePlayback. GDI parses the record usingEnumEnhMetaFileand passes it to the safe playback loop. - The browser process executes the consistent check, passes it, and calls
ExtEscapewith a size of 1026, triggering an out-of-bounds heap read of up to 1022 bytes in the browser process.
Suggested Fix
Ensure that the size of the comment data does not exceed the boundaries defined by the record size (nSize). Specifically, verify that emf_record->nSize is large enough to contain the EMRGDICOMMENT struct header and the claimed comment->cbData payload before accessing the structure’s fields:
if (emf_record->nSize < sizeof(EMRGDICOMMENT) ||
comment->cbData > emf_record->nSize - offsetof(EMRGDICOMMENT, Data)) {
continue;
}
Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.