CVE-2026-11183
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/gwp_asan/crash_handler/crash_analyzer.cc |
modified | |
TEST_Pcomponents/gwp_asan/crash_handler/crash_analyzer_unittest.cc |
modified |
Files Changed
components/gwp_asan/client/lightweight_detector/poison_metadata_recorder.hcomponents/gwp_asan/crash_handler/crash_analyzer.cccomponents/gwp_asan/crash_handler/crash_analyzer.hcomponents/gwp_asan/crash_handler/crash_analyzer_unittest.cc
Patch
From 21db15bc04ff587cbbabe0f4f7253ad291917261 Mon Sep 17 00:00:00 2001
From: Sergei Glazunov <glazunov@google.com>
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 <glazunov@google.com>
Reviewed-by: Samuel Groß <saelo@chromium.org>
Commit-Queue: Samuel Groß <saelo@chromium.org>
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,
Regression Test / PoC
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,
Original Bug Report
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.cccomponents/gwp_asan/common/lightweight_detector_state.hcomponents/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.
- Craft the Dump: A compromised app extension creates a fake
ProcessSnapshotIOSIntermediateDumpin the shared App Group Crashpad directory (Crashpad/pending-serialized-ios-dump/). - Fake State: The dump annotations designate a Lightweight UAF Detector crash. The dump provides a fake
LightweightDetectorStatewithnum_metadataset to 1. - Fake Metadata Array: The dump provides a 136-byte payload representing a single
LightweightDetectorState::SlotMetadataobject (size on 64-bit iOS). Inside this object,dealloc.trace_lenis set to 400, andidis set to0x41414141. - Exception Address Triggers Analysis: The dump’s ARM64 CPU context specifies an exception address of
0xEFED414141418000, which the analyzer decodes to match the metadataidof0x41414141, triggering stack trace extraction. - Trigger OOB Read: The browser process allocates a 136-byte heap buffer for the
SlotMetadataarray (std::make_unique<LightweightDetectorState::SlotMetadata[]>(1)) and copies the attacker’s payload into it.ReadAllocationInfovalidates the 400-byte length against the incorrect 400-byte maximum and callsUnpack(). - Memory Exfiltration:
Unpack()reads 400 bytes starting at the 90-bytedeallocation_stack_tracefield. It reads the 90 bytes of the array, the remaining 30 bytes of theSlotMetadatastruct, and 280 bytes of out-of-bounds heap memory adjacent to theSlotMetadataallocation. - Data Reconstruction:
Unpacksuccessfully decodes the OOB bytes (assuming they don’t violate VarInt formatting rules, which is likely for random heap data) and writes them to thegwp_asan::Crashprotobuf. The browser serializes the protobuf into the final minidump stored in the shared App Group directory. - 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.