Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in GWP-ASan
DescriptionOut of bounds read in GWP-ASan
ComponentGWP-ASan
Bug ClassOOB
Tracker502768780
Fix commit21db15bc04ff (chromium/src) +32/-8
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
components/gwp_asan/crash_handler/crash_analyzer.cc
modified
TEST_P
components/gwp_asan/crash_handler/crash_analyzer_unittest.cc
modified

Files Changed

  • components/gwp_asan/client/lightweight_detector/poison_metadata_recorder.h
  • components/gwp_asan/crash_handler/crash_analyzer.cc
  • components/gwp_asan/crash_handler/crash_analyzer.h
  • components/gwp_asan/crash_handler/crash_analyzer_unittest.cc
From 21db15bc04ff587cbbabe0f4f7253ad291917261 Mon Sep 17 00:00:00 2001
From: Sergei Glazunov <[email protected]>
Date: Thu, 16 Apr 2026 03:37:10 -0700
Subject: [PATCH] gwp_asan: Pass correct trace length limit to ReadAllocationInfo

ReadAllocationInfo used to hardcode AllocatorState::kMaxPackedTraceLength
for bounds checking. This is correct for the classic allocator but incorrect
for the Lightweight UAF Detector, which uses a smaller buffer.

This CL updates ReadAllocationInfo to accept the maximum trace length as
an argument and updates callers to pass the appropriate limit.

Bug: 502768780
Change-Id: I3d8777e8abc40d5b75df1c22932946aac2a7c2aa
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7758513
Auto-Submit: Sergei Glazunov <[email protected]>
Reviewed-by: Samuel Groß <[email protected]>
Commit-Queue: Samuel Groß <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1615732}
---

diff --git a/components/gwp_asan/client/lightweight_detector/poison_metadata_recorder.h b/components/gwp_asan/client/lightweight_detector/poison_metadata_recorder.h
index 185a83f5..2260b929 100644
--- a/components/gwp_asan/client/lightweight_detector/poison_metadata_recorder.h
+++ b/components/gwp_asan/client/lightweight_detector/poison_metadata_recorder.h
@@ -17,6 +17,7 @@
 
 namespace gwp_asan::internal {
 FORWARD_DECLARE_TEST(LightweightDetectorAnalyzerTest, InternalError);
+FORWARD_DECLARE_TEST(LightweightDetectorAnalyzerTest, InvalidTraceLength);
 }  // namespace gwp_asan::internal
 
 namespace gwp_asan::internal::lud {
@@ -64,6 +65,9 @@
   FRIEND_TEST_ALL_PREFIXES(
       ::gwp_asan::internal::LightweightDetectorAnalyzerTest,
       InternalError);
+  FRIEND_TEST_ALL_PREFIXES(
+      ::gwp_asan::internal::LightweightDetectorAnalyzerTest,
+      InvalidTraceLength);
 };
 
 extern template class EXPORT_TEMPLATE_DECLARE(GWP_ASAN_EXPORT)
diff --git a/components/gwp_asan/crash_handler/crash_analyzer.cc b/components/gwp_asan/crash_handler/crash_analyzer.cc
index 56af968b..0953a77 100644
--- a/components/gwp_asan/crash_handler/crash_analyzer.cc
+++ b/components/gwp_asan/crash_handler/crash_analyzer.cc
@@ -387,7 +387,8 @@
       metadata.dealloc.trace_len) {
     ReadAllocationInfo(metadata.deallocation_stack_trace,
                        /* stack_trace_offset = */ 0, metadata.dealloc,
-                       proto->mutable_deallocation());
+                       proto->mutable_deallocation(),
+                       LightweightDetectorState::kMaxPackedTraceLength);
   }
 
   ReportHistogram(Crash_Allocator_PARTITIONALLOC,
@@ -533,12 +534,14 @@
     if (metadata.alloc.tid != base::kInvalidThreadId ||
         metadata.alloc.trace_len) {
       ReadAllocationInfo(metadata.stack_trace_pool, 0, metadata.alloc,
-                         proto->mutable_allocation());
+                         proto->mutable_allocation(),
+                         AllocatorState::kMaxPackedTraceLength);
     }
     if (metadata.dealloc.tid != base::kInvalidThreadId ||
         metadata.dealloc.trace_len) {
       ReadAllocationInfo(metadata.stack_trace_pool, metadata.alloc.trace_len,
-                         metadata.dealloc, proto->mutable_deallocation());
+                         metadata.dealloc, proto->mutable_deallocation(),
+                         AllocatorState::kMaxPackedTraceLength);
     }
   }
 
@@ -550,7 +553,8 @@
     const uint8_t* stack_trace,
     size_t stack_trace_offset,
     const AllocationInfo& slot_info,
-    gwp_asan::Crash_AllocationInfo* proto_info) {
+    gwp_asan::Crash_AllocationInfo* proto_info,
+    size_t max_trace_length) {
   if (slot_info.tid != base::kInvalidThreadId) {
     // The PlatformThreadId will match the Crashpad tid in terms of the bit
     // values, however it can differ in bitwidth and sign. To make this uniform,
@@ -564,9 +568,8 @@
   if (!slot_info.trace_len || !slot_info.trace_collected)
     return;
 
-  if (slot_info.trace_len > AllocatorState::kMaxPackedTraceLength ||
-      stack_trace_offset + slot_info.trace_len >
-          AllocatorState::kMaxPackedTraceLength) {
+  if (slot_info.trace_len > max_trace_length ||
+      stack_trace_offset + slot_info.trace_len > max_trace_length) {
     DLOG(ERROR) << "Stack trace length is corrupted: " << slot_info.trace_len;
     return;
   }
diff --git a/components/gwp_asan/crash_handler/crash_analyzer.h b/components/gwp_asan/crash_handler/crash_analyzer.h
index 1410909a..540de0f8 100644
--- a/components/gwp_asan/crash_handler/crash_analyzer.h
+++ b/components/gwp_asan/crash_handler/crash_analyzer.h
@@ -116,7 +116,8 @@
   static void ReadAllocationInfo(const uint8_t* stack_trace,
                                  size_t stack_trace_offset,
                                  const AllocationInfo& slot_info,
-                                 gwp_asan::Crash_AllocationInfo* proto_info);
+                                 gwp_asan::Crash_AllocationInfo* proto_info,
+                                 size_t max_trace_length);
 
   // This method analyzes the AllocatorState of the crashing process. If the
   // exception is related to the Lightweight UAF Detector it fills out the
diff --git a/components/gwp_asan/crash_handler/crash_analyzer_unittest.cc b/components/gwp_asan/crash_handler/crash_analyzer_unittest.cc
index d681247d..fb3c5a5 100644
--- a/components/gwp_asan/crash_handler/crash_analyzer_unittest.cc
+++ b/components/gwp_asan/crash_handler/crash_analyzer_unittest.cc
@@ -339,6 +339,22 @@
             CrashAnalyzer::LightweightDetectorModeToGwpAsanMode(GetParam()));
 }
 
+TEST_P(LightweightDetectorAnalyzerTest, InvalidTraceLength) {
+  uint64_t alloc;
+  ASSERT_TRUE(lud::PoisonMetadataRecorder::Get());
+  lud::PoisonMetadataRecorder::Get()->RecordAndZap(&alloc, sizeof(alloc));
+  InitializeSnapshot(alloc);
+
+  // Corrupt the trace_len to be larger than 90 but less than 400.
+  lud::PoisonMetadataRecorder::Get()->metadata_[0].dealloc.trace_len = 400;
+
+  base::HistogramTester histogram_tester;
+  gwp_asan::Crash proto;
+  bool proto_present =
+      CrashAnalyzer::GetExceptionInfo(process_snapshot_, &proto);
+  ASSERT_TRUE(proto_present);
+}
+
 INSTANTIATE_TEST_SUITE_P(
     VaryLightweightDetectorMode,
     LightweightDetectorAnalyzerTest,
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/gwp_asan/crash_handler/crash_analyzer_unittest.cc b/components/gwp_asan/crash_handler/crash_analyzer_unittest.cc
index d681247d..fb3c5a5 100644
--- a/components/gwp_asan/crash_handler/crash_analyzer_unittest.cc
+++ b/components/gwp_asan/crash_handler/crash_analyzer_unittest.cc
@@ -339,6 +339,22 @@
             CrashAnalyzer::LightweightDetectorModeToGwpAsanMode(GetParam()));
 }
 
+TEST_P(LightweightDetectorAnalyzerTest, InvalidTraceLength) {
+  uint64_t alloc;
+  ASSERT_TRUE(lud::PoisonMetadataRecorder::Get());
+  lud::PoisonMetadataRecorder::Get()->RecordAndZap(&alloc, sizeof(alloc));
+  InitializeSnapshot(alloc);
+
+  // Corrupt the trace_len to be larger than 90 but less than 400.
+  lud::PoisonMetadataRecorder::Get()->metadata_[0].dealloc.trace_len = 400;
+
+  base::HistogramTester histogram_tester;
+  gwp_asan::Crash proto;
+  bool proto_present =
+      CrashAnalyzer::GetExceptionInfo(process_snapshot_, &proto);
+  ASSERT_TRUE(proto_present);
+}
+
 INSTANTIATE_TEST_SUITE_P(
     VaryLightweightDetectorMode,
     LightweightDetectorAnalyzerTest,
Loading diff…

Original Bug Report

reported by [email protected]

Potential Out-of-Bounds Heap Read in iOS CrashAnalyzer via Lightweight UAF Detector

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 Chrome Security team. Please see go/chrome-ai-generated-security-bugs-faq for more information.

Overview: The GWP-ASan CrashAnalyzer uses an incorrect bounds limit when parsing Lightweight UAF Detector crashes, allowing an attacker to read past the end of the metadata buffer. On iOS, a compromised app extension can provide a crafted intermediate crash dump to trigger this out-of-bounds read in the highly-privileged browser process. The leaked heap memory is encoded and written back to the shared minidump directory, allowing the extension to reconstruct the sensitive data.

Affected files:

  • components/gwp_asan/crash_handler/crash_analyzer.cc
  • components/gwp_asan/common/lightweight_detector_state.h
  • components/gwp_asan/common/allocator_state.h

Estimated timestamp from git blame: 2026-03-17

Background

On iOS, Crashpad operates in-process and writes intermediate crash dumps to a directory shared between the main Chrome browser process and its app extensions (such as the Share or Credential Provider extensions) via an App Group container. When the main browser process starts, it reads these intermediate dumps to generate full minidumps.

The Vulnerability

There is a bounds checking mismatch in CrashAnalyzer::ReadAllocationInfo (components/gwp_asan/crash_handler/crash_analyzer.cc) when it processes GWP-ASan stack traces. The function is shared between the classic allocator and the Lightweight UAF Detector mode.

When verifying the slot_info.trace_len before calling Unpack(), the code checks against AllocatorState::kMaxPackedTraceLength (which is 400 bytes):

if (slot_info.trace_len > AllocatorState::kMaxPackedTraceLength ||
    stack_trace_offset + slot_info.trace_len >
        AllocatorState::kMaxPackedTraceLength) {
  return;
}

However, when analyzing a Lightweight UAF Detector crash, the source buffer is LightweightDetectorState::SlotMetadata::deallocation_stack_trace, which has a maximum size of LightweightDetectorState::kMaxPackedTraceLength (90 bytes).

If a compromised app extension sets trace_len to 400 in a crafted intermediate dump, the check passes. Unpack() will then read 400 bytes starting from the 90-byte buffer.

Potential Exploitation Steps (Theoretical)

Our tooling agent suggests the following steps an attacker might take to exploit this vulnerability. Note that these are potential steps, as we do not yet have a working proof of concept that has been successfully run.

  1. Craft the Dump: A compromised app extension creates a fake ProcessSnapshotIOSIntermediateDump in the shared App Group Crashpad directory (Crashpad/pending-serialized-ios-dump/).
  2. Fake State: The dump annotations designate a Lightweight UAF Detector crash. The dump provides a fake LightweightDetectorState with num_metadata set to 1.
  3. Fake Metadata Array: The dump provides a 136-byte payload representing a single LightweightDetectorState::SlotMetadata object (size on 64-bit iOS). Inside this object, dealloc.trace_len is set to 400, and id is set to 0x41414141.
  4. Exception Address Triggers Analysis: The dump’s ARM64 CPU context specifies an exception address of 0xEFED414141418000, which the analyzer decodes to match the metadata id of 0x41414141, triggering stack trace extraction.
  5. Trigger OOB Read: The browser process allocates a 136-byte heap buffer for the SlotMetadata array (std::make_unique<LightweightDetectorState::SlotMetadata[]>(1)) and copies the attacker’s payload into it. ReadAllocationInfo validates the 400-byte length against the incorrect 400-byte maximum and calls Unpack().
  6. Memory Exfiltration: Unpack() reads 400 bytes starting at the 90-byte deallocation_stack_trace field. It reads the 90 bytes of the array, the remaining 30 bytes of the SlotMetadata struct, and 280 bytes of out-of-bounds heap memory adjacent to the SlotMetadata allocation.
  7. Data Reconstruction: Unpack successfully decodes the OOB bytes (assuming they don’t violate VarInt formatting rules, which is likely for random heap data) and writes them to the gwp_asan::Crash protobuf. The browser serializes the protobuf into the final minidump stored in the shared App Group directory.
  8. The app extension reads the new minidump and perfectly reverses the mathematical VarInt and ZigZag encoding applied by Unpack(), reconstructing the raw 280 bytes of browser process heap memory.

Suggested Fix

ReadAllocationInfo should be modified to accept the correct maximum buffer size as an argument, or infer it based on the current mode, rather than hardcoding AllocatorState::kMaxPackedTraceLength.

void CrashAnalyzer::ReadAllocationInfo(
    const uint8_t* stack_trace,
    size_t stack_trace_offset,
    const AllocationInfo& slot_info,
    gwp_asan::Crash_AllocationInfo* proto_info,
    size_t max_trace_length) { // Pass the correct limit

Evaluated with Chrome root at commit: 661452647ddb2827305122ff3273bd5dea403f09


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.

View on issue tracker