High watch CVSS 8.3 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in Crashpad
DescriptionOut of bounds write in Crashpad
ComponentCrashpad
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.
Tracker545820931
Fix commitc155e094d71e (chromium/src) +50/-3
CWECWE-787 (Out-of-bounds write)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedBrendan Dolan-Gavitt, XBOW
Reported2026-08-13 (per vendor advisory)
Fix landed2026-08-19 (public git)
Disclosed2026-08-25
Patch gap6 days — fix public in source before the advisory

Background

Crashpad `ProcessMemory`
the Crashpad abstraction the crash handler uses to read another (crashing) process’s memory. Its public Read, ReadCString and ReadCStringSizeLimited helpers are built on a platform-specific ReadUpTo(address, size, buffer) primitive that returns the number of bytes copied.
`ReadUpTo` contract
ReadUpTo may return fewer bytes than requested (a short read), but never more than size. Callers subtract the returned count from their remaining size and advance their destination pointer by it.
`ERROR_PARTIAL_COPY`
the Windows error ReadProcessMemory reports when only part of the requested range could be read. The Windows ReadUpTo then 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 / IsValid let 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.

Key insight
The Windows partial-copy fallback computed its read length from the page boundary alone and ignored the caller’s buffer size. Its callers trusted the byte count with only a DCHECK. The patch bounds the retry by size and makes the callers enforce bytes_read <= remaining with checked arithmetic in release builds.

Attack Path

  1. 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::Read or ReadCString*, which call the Windows ReadUpTo.
  2. Force a partial copy the memory layout or protections of the target process make the initial ReadProcessMemory fail with ERROR_PARTIAL_COPY for 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).
  3. Oversized retry before the fix, the fallback re-read page_remainder bytes, which can exceed size, into the caller’s buffer, writing past its end in the handler process.
  4. Caller bookkeeping corrupted the over-reported count made local_size -= bytes_read wrap in release builds and pushed buffer_c further out of bounds, which could extend the out-of-bounds write (inferred from the removed DCHECK_LE and the added regression tests).

Impact Assessment

The out-of-bounds write happens in the Crashpad handler process on Windows. That process is not part of the renderer sandbox, and the written bytes come from the memory of the process being read. Reaching it plausibly needs control over a process Crashpad inspects, such as a compromised renderer (inferred). Turning the heap/stack overwrite into code execution outside the sandbox (a sandbox escape) is not shown by the patch and is (inferred). The advisory gives no in-the-wild exploitation status.

Changed Functions

FunctionChangeNotes
if
third_party/crashpad/crashpad/util/process/process_memory.cc
modified
MockProcessMemoryOverflow
third_party/crashpad/crashpad/util/process/process_memory_test.cc
modified
TEST
third_party/crashpad/crashpad/util/process/process_memory_test.cc
modified

Files Changed

  • third_party/crashpad/crashpad/util/process/process_memory.cc
  • third_party/crashpad/crashpad/util/process/process_memory_test.cc
  • third_party/crashpad/crashpad/util/process/process_memory_win.cc

Audit Directions

  • Platform `ReadUpTo` backends
    check 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’s size.
  • Debug-only checks on cross-process data
    look 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.
From c155e094d71e6c6ca452e5140d6996bbd76b36f1 Mon Sep 17 00:00:00 2001
From: Will Harris <wfh@chromium.org>
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 <mark@chromium.org>
Commit-Queue: Will Harris <wfh@chromium.org>
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
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by mo...@xbow.com

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 /GS fast-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 subcode 0x2 (FAST_FAIL_STACK_COOKIE_CHECK_FAILURE)

Client ID (if relevant): N/A

CREDIT INFORMATION

Reporter credit: Brendan Dolan-Gavitt, XBOW

View on issue tracker