CVE-2026-78952
Overview
Background
- Crashpad `ProcessMemory`
- the Crashpad abstraction the crash handler uses to read another (crashing) process’s memory. Its public
Read,ReadCStringandReadCStringSizeLimitedhelpers are built on a platform-specificReadUpTo(address, size, buffer)primitive that returns the number of bytes copied. - `ReadUpTo` contract
ReadUpTomay return fewer bytes than requested (a short read), but never more thansize. Callers subtract the returned count from their remaining size and advance their destination pointer by it.- `ERROR_PARTIAL_COPY`
- the Windows error
ReadProcessMemoryreports when only part of the requested range could be read. The WindowsReadUpTothen retries with a “short read” up to the end of the first page. - `base::CheckedNumeric`
- Chromium’s checked-arithmetic wrapper. Overflow or underflow marks the value invalid, and
AssignIfValid/IsValidlet the caller detect that instead of wrapping silently.
Root Cause Analysis
On Windows, when ReadProcessMemory failed with ERROR_PARTIAL_COPY, process_memory_win.cc retried with short_read = GetPageSize() - (address & (GetPageSize() - 1)), the number of bytes to the end of the page. It never limited that to the caller’s requested size. When size was smaller than the page remainder, the retry let ReadProcessMemory write up to a page’s worth of target memory into a buffer sized for only size bytes. That is an out-of-bounds write in the reading (Crashpad handler) process, and it also broke the ReadUpTo contract by reporting more bytes than requested. The generic callers in process_memory.cc relied on that contract. One of them only DCHECK_LEd the returned count before local_size -= bytes_read, so in release builds the subtraction could wrap. It would then keep reading with an enormous remaining size while advancing buffer_c past the end of the buffer. The C-string reader likewise used the returned count without checking it against read_size.
The fix clamps the Windows retry to std::min(size, page_remainder) and replaces the debug-only check with CheckedNumeric subtraction in both generic callers. Either caller now fails with “read more bytes than requested” if a backend over-reports.
DCHECK. The patch bounds the retry by size and makes the callers enforce bytes_read <= remaining with checked arithmetic in release builds.Attack Path
- Crashpad reads a target process
the Crashpad handler, which runs as a separate process, reads memory from a client process (for example while producing a crash dump) through
ProcessMemory::ReadorReadCString*, which call the WindowsReadUpTo. - Force a partial copy
the memory layout or protections of the target process make the initial
ReadProcessMemoryfail withERROR_PARTIAL_COPYfor a small request that does not extend to the end of its page. An attacker who controls the target process, such as a compromised renderer, could arrange this (inferred). - Oversized retry
before the fix, the fallback re-read
page_remainderbytes, which can exceedsize, into the caller’sbuffer, writing past its end in the handler process. - Caller bookkeeping corrupted
the over-reported count made
local_size -= bytes_readwrap in release builds and pushedbuffer_cfurther out of bounds, which could extend the out-of-bounds write (inferred from the removedDCHECK_LEand the added regression tests).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/crashpad/crashpad/util/process/process_memory.cc |
modified | |
MockProcessMemoryOverflowthird_party/crashpad/crashpad/util/process/process_memory_test.cc |
modified | |
TESTthird_party/crashpad/crashpad/util/process/process_memory_test.cc |
modified |
Files Changed
third_party/crashpad/crashpad/util/process/process_memory.ccthird_party/crashpad/crashpad/util/process/process_memory_test.ccthird_party/crashpad/crashpad/util/process/process_memory_win.cc
Audit Directions
- Platform `ReadUpTo` backendscheck every OS backend of
ProcessMemory::ReadUpTo(Linux/Android ptrace or/proc/pid/mem, macOS/Fuchsia) and any retry or fallback path. The length passed to the kernel or OS read must never exceed the caller’ssize. - Debug-only checks on cross-process datalook for
DCHECKs guarding byte counts, sizes or offsets derived from another process’s memory or from OS APIs in Crashpad and other crash/minidump code. In release builds those checks disappear, so subtraction and pointer advancement can wrap. - Snapshot readers built on `ProcessMemory`audit Crashpad’s module, thread, exception and annotation snapshot readers that size buffers from target-controlled structures. A hostile crashing process controls those values, and the handler runs with higher privilege.
Patch
From c155e094d71e6c6ca452e5140d6996bbd76b36f1 Mon Sep 17 00:00:00 2001 From: Will Harris <[email protected]> Date: Wed, 19 Aug 2026 08:43:11 -0700 Subject: [PATCH] Fix overflow in crashpad Clamp retry of short reads on Windows to the correct size. Also, reject invalid sizes larger than the requested remaining size when advancing the destination pointer or subtracting the returned count. Bug: 545820931 Change-Id: I6c78b1f9a1ea4eeb54e6e5c7069ca4ea24febb06 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8267322 Reviewed-by: Mark Mentovai <[email protected]> Commit-Queue: Will Harris <[email protected]> Cr-Commit-Position: refs/heads/main@{#1682346} --- diff --git a/third_party/crashpad/crashpad/util/process/process_memory.cc b/third_party/crashpad/crashpad/util/process/process_memory.cc index 8983773..7e2c901f0 100644 --- a/third_party/crashpad/crashpad/util/process/process_memory.cc +++ b/third_party/crashpad/crashpad/util/process/process_memory.cc @@ -20,6 +20,8 @@ #include "base/check_op.h" #include "base/logging.h" +#include "base/numerics/safe_conversions.h" +#include "base/numerics/safe_math.h" #include "util/numeric/safe_assignment.h" namespace crashpad { @@ -41,8 +43,13 @@ LOG(ERROR) << "short read"; return false; } - DCHECK_LE(static_cast<size_t>(bytes_read), local_size); - local_size -= bytes_read; + + base::CheckedNumeric<size_t> new_local_size = local_size; + new_local_size -= bytes_read; + if (!new_local_size.AssignIfValid(&local_size)) { + LOG(ERROR) << "read more bytes than requested"; + return false; + } address += bytes_read; buffer_c += bytes_read; } @@ -78,6 +85,13 @@ break; } + base::CheckedNumeric<size_t> checked_read_size = read_size; + checked_read_size -= bytes_read; + if (!checked_read_size.IsValid()) { + LOG(ERROR) << "read more bytes than requested"; + return false; + } + char* nul = static_cast<char*>(memchr(buffer, '\0', bytes_read)); if (nul != nullptr) { string->append(buffer, nul - buffer); diff --git a/third_party/crashpad/crashpad/util/process/process_memory_win.cc b/third_party/crashpad/crashpad/util/process/process_memory_win.cc index c120827a..21c2fd3 100644 --- a/third_party/crashpad/crashpad/util/process/process_memory_win.cc +++ b/third_party/crashpad/crashpad/util/process/process_memory_win.cc @@ -60,8 +60,9 @@ if (GetLastError() == ERROR_PARTIAL_COPY) { // If we can not read the entire section, perform a short read of the first // page instead. This is necessary to support ReadCString(). - size_t short_read = + const size_t page_remainder = base::GetPageSize() - (address & (base::GetPageSize() - 1)); + const size_t short_read = std::min(size, page_remainder); success = ReadProcessMemory(handle_, reinterpret_cast<void*>(address), buffer, diff --git a/third_party/crashpad/crashpad/util/process/process_memory_test.cc b/third_party/crashpad/crashpad/util/process/process_memory_test.cc index 07b4253..82307c98 100644 --- a/third_party/crashpad/crashpad/util/process/process_memory_test.cc +++ b/third_party/crashpad/crashpad/util/process/process_memory_test.cc @@ -601,6 +601,38 @@ test.RunAgainstChild(); } +class MockProcessMemoryOverflow : public ProcessMemory { + public: + MockProcessMemoryOverflow() = default; + ~MockProcessMemoryOverflow() override = default; + + protected: + ssize_t ReadUpTo(VMAddress address, + size_t size, + void* buffer) const override { + // Maliciously return more bytes than requested to test the bounds checking. + return size + 10; + } +}; + +TEST(ProcessMemory, ReadOverflowFailsSafely) { + MockProcessMemoryOverflow memory; + char buffer[16]; + EXPECT_FALSE(memory.Read(0x1000, 16, buffer)); +} + +TEST(ProcessMemory, ReadCStringOverflowFailsSafely) { + MockProcessMemoryOverflow memory; + std::string result; + EXPECT_FALSE(memory.ReadCString(0x1000, &result)); +} + +TEST(ProcessMemory, ReadCStringSizeLimitedOverflowFailsSafely) { + MockProcessMemoryOverflow memory; + std::string result; + EXPECT_FALSE(memory.ReadCStringSizeLimited(0x1000, 16, &result)); +} + } // namespace } // namespace test } // namespace crashpad
Regression Test / PoC
diff --git a/third_party/crashpad/crashpad/util/process/process_memory_test.cc b/third_party/crashpad/crashpad/util/process/process_memory_test.cc
index 07b4253..82307c98 100644
--- a/third_party/crashpad/crashpad/util/process/process_memory_test.cc
+++ b/third_party/crashpad/crashpad/util/process/process_memory_test.cc
@@ -601,6 +601,38 @@
test.RunAgainstChild();
}
+class MockProcessMemoryOverflow : public ProcessMemory {
+ public:
+ MockProcessMemoryOverflow() = default;
+ ~MockProcessMemoryOverflow() override = default;
+
+ protected:
+ ssize_t ReadUpTo(VMAddress address,
+ size_t size,
+ void* buffer) const override {
+ // Maliciously return more bytes than requested to test the bounds checking.
+ return size + 10;
+ }
+};
+
+TEST(ProcessMemory, ReadOverflowFailsSafely) {
+ MockProcessMemoryOverflow memory;
+ char buffer[16];
+ EXPECT_FALSE(memory.Read(0x1000, 16, buffer));
+}
+
+TEST(ProcessMemory, ReadCStringOverflowFailsSafely) {
+ MockProcessMemoryOverflow memory;
+ std::string result;
+ EXPECT_FALSE(memory.ReadCString(0x1000, &result));
+}
+
+TEST(ProcessMemory, ReadCStringSizeLimitedOverflowFailsSafely) {
+ MockProcessMemoryOverflow memory;
+ std::string result;
+ EXPECT_FALSE(memory.ReadCStringSizeLimited(0x1000, 16, &result));
+}
+
} // namespace
} // namespace test
} // namespace crashpad
Original Bug Report
Windows Crashpad ReadUpTo() retry can exceed the caller's buffer size
VULNERABILITY DETAILS
On Windows, ProcessMemoryWin::ReadUpTo() handles
ERROR_PARTIAL_COPY by retrying ReadProcessMemory() with the bytes remaining
in the source page. It does not clamp that retry to the destination size:
BOOL success = ReadProcessMemory(
handle_, reinterpret_cast<void*>(address), buffer, size, &size_out);
if (GetLastError() == ERROR_PARTIAL_COPY) {
size_t short_read =
base::GetPageSize() - (address & (base::GetPageSize() - 1));
success = ReadProcessMemory(handle_,
reinterpret_cast<void*>(address),
buffer,
short_read,
&size_out);
if (success)
return base::checked_cast<ssize_t>(size_out);
}
For example, a source at page offset 0x9d0 makes the retry size 0x630
even when the caller supplied a 16-byte destination.
One production callsite is
ExceptionSnapshotWin::InitializeFromExceptionPointers(), which reads an
EXCEPTION_POINTERS64 into a 16-byte stack object. The attached current-main
reproducer uses a source at offset 0x900. The retry copies 0x700 bytes,
overwrites the saved return address, and transfers control to the source’s
final qword, 0x0000000041414141.
The bug is also reachable at a heap destination. Our full-chain reproduction (reported separately) uses 48 renderer-controlled source pages to provide 48 retry opportunities in
one request. A successful 0x168-byte retry into a 0xb8-byte allocation
corrupts an adjacent SnapshotMinidumpMemoryWriter. That chain executes code
in the unsandboxed Crashpad handler on signed Chrome Beta.
A compromised renderer can create the required transition. A thread created
with NtCreateThreadEx(..., flags=0x42) remains runnable while Crashpad
snapshots the renderer. The renderer controls the registered exception-data
pointer and retains the event used to request a non-crash dump. Its live
thread changes a source page between PAGE_NOACCESS and PAGE_READWRITE
during the request, making the initial read fail and the oversized retry
succeed.
Suggested fix:
const size_t page_remainder =
base::GetPageSize() - (address & (base::GetPageSize() - 1));
const size_t short_read = std::min(size, page_remainder);
ProcessMemory::Read() should also reject a ReadUpTo() result larger than
the requested remaining size before advancing the destination pointer or
subtracting the returned count.
It’s not clear whether allowing renderer threads to remain alive during a crashpad snapshot is itself an invariant violation. If that is intended behavior then there are likely to be other crashpad sites that need to be audited for safety under concurrent modifications of renderer state. If that is not intended behavior then some way of reliably quiescing the renderer process or making a snapshot atomic is needed.
VERSION
Chrome Version:
- 151.0.7922.72 Stable: natural Crashpad-handler
/GSfast-fail - 152.0.7977.30 Beta: heap-callsite full-chain code execution
- Chromium 153.0.7991.0 at revision
56e554842ef240aae8c44bd178f3ddce51bb7448: controlled saved-return overwrite
Source inspection confirmed the vulnerable implementation in Stable
151.0.7922.109, Beta 152.0.7977.30, and Chromium main revision
dc5fc94bddd3b8a4c78b52a72be77f5bd6c3e727 on 2026-08-13.
Operating System: Windows 11 Enterprise Evaluation 10.0.26200 x64 and Windows Server 2022 build 20348 x64
REPRODUCTION CASE
From a clean Windows Chromium checkout:
git checkout 56e554842ef240aae8c44bd178f3ddce51bb7448
gclient sync --revision src@56e554842ef240aae8c44bd178f3ddce51bb7448 -D
git apply current-source-renderer-trigger.patch
mkdir out\crashpad_read_retry_poc
Create out\crashpad_read_retry_poc\args.gn:
target_cpu = "x64"
is_debug = false
is_component_build = false
is_official_build = true
dcheck_always_on = false
symbol_level = 2
blink_symbol_level = 0
v8_symbol_level = 0
chrome_pgo_phase = 0
use_remoteexec = false
use_reclient = false
use_siso = false
Build and open the normal browser:
gn gen out\crashpad_read_retry_poc
autoninja -C out\crashpad_read_retry_poc chrome
out\crashpad_read_retry_poc\chrome.exe --user-data-dir=%TEMP%\crashpad-read-retry-poc --no-first-run --no-default-browser-check about:blank
Starting the renderer invokes the trigger. It uses a renderer-owned shadow loader page as an early snapshot cue, then keeps the controlled source page transitioning for the rest of the request. The primary Crashpad handler exits with an execute access violation at the controlled address:
ExceptionCode: c0000005 (Access violation)
ExceptionAddress: 0000000041414141
Attempt to execute non-executable address 0000000041414141
rip=0000000041414141
current-main-controlled-return-crash.txt contains the postmortem debugger
record. Report-0.utf8.txt is the natural failure record from signed Stable
151.0.7922.72; that build reports handler fast-fail 0xc0000409, subcode 2.
The full-chain reproduction uses the same ReadUpTo() defect at its heap
callsite. Its preferred configuration completed 30 of 30 fresh-profile,
ordinary-browser trials and launched cmd /k calc.exe as a child of the
Crashpad handler.
FOR CRASHES, PLEASE INCLUDE THE FOLLOWING ADDITIONAL INFORMATION
Type of crash: Crashpad handler process
Crash State:
- Current Chromium: execute access violation at controlled
RIP=0x41414141 - Signed Stable:
0xc0000409, fast-fail subcode0x2(FAST_FAIL_STACK_COOKIE_CHECK_FAILURE)
Client ID (if relevant): N/A
CREDIT INFORMATION
Reporter credit: Brendan Dolan-Gavitt, XBOW