CVE-2026-11164
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/controller/oom_intervention_impl.cc |
modified | |
forthird_party/blink/renderer/controller/oom_intervention_impl.cc |
modified | |
forthird_party/blink/renderer/core/inspector/inspector_memory_agent.cc |
modified |
Files Changed
third_party/blink/renderer/controller/oom_intervention_impl.ccthird_party/blink/renderer/core/inspector/inspector_memory_agent.cc
Patch
From 7428099311de1034955118520c81f7acf867ec93 Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <apaseltiner@chromium.org>
Date: Wed, 22 Apr 2026 15:19:54 -0700
Subject: [PATCH] Fix Use-After-Free when iterating over Page::OrdinaryPages()
Iterating directly over Page::OrdinaryPages() while calling methods that
can synchronously execute JavaScript (like ForciblyPurgeV8Memory) is
unsafe. Re-entrant JS can create new pages, triggering a rehash of the
underlying HeapHashSet and invalidating the iterator.
This CL fixes the issue by copying the collection into a Page::PageSet
before iteration. Using Page::PageSet (the actual return type of
OrdinaryPages()) is the established pattern for safely iterating
over this set when synchronous JS is possible.
Fixed: 502089411
Change-Id: Ibf6c9b95f92465b968a39308e29745f6d5ef4a36
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7785937
Commit-Queue: Andrew Paseltiner <apaseltiner@chromium.org>
Reviewed-by: Steve Kobes <skobes@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1619129}
---
diff --git a/third_party/blink/renderer/controller/oom_intervention_impl.cc b/third_party/blink/renderer/controller/oom_intervention_impl.cc
index c6e13dc..10ab8c4c 100644
--- a/third_party/blink/renderer/controller/oom_intervention_impl.cc
+++ b/third_party/blink/renderer/controller/oom_intervention_impl.cc
@@ -123,7 +123,12 @@
base::debug::SetCrashKeyString(GetStateCrashKey(), "during");
if (navigate_ads_enabled_ || purge_v8_memory_enabled_) {
- for (const auto& page : Page::OrdinaryPages()) {
+ // Copy Page::OrdinaryPages() to avoid UAF. Synchronous JS
+ // execution during iteration can create new pages, which causes rehashing
+ // of the OrdinaryPages() set and invalidates the iterator.
+ // See crbug.com/502089411
+ Page::PageSet pages(Page::OrdinaryPages());
+ for (const auto& page : pages) {
for (Frame* frame = page->MainFrame(); frame;
frame = frame->Tree().TraverseNext()) {
auto* local_frame = DynamicTo<LocalFrame>(frame);
diff --git a/third_party/blink/renderer/core/inspector/inspector_memory_agent.cc b/third_party/blink/renderer/core/inspector/inspector_memory_agent.cc
index d99c4e8f..5069e8fe 100644
--- a/third_party/blink/renderer/core/inspector/inspector_memory_agent.cc
+++ b/third_party/blink/renderer/core/inspector/inspector_memory_agent.cc
@@ -68,7 +68,12 @@
}
protocol::Response InspectorMemoryAgent::forciblyPurgeJavaScriptMemory() {
- for (const auto& page : Page::OrdinaryPages()) {
+ // Copy Page::OrdinaryPages() to avoid UAF. Synchronous JS
+ // execution during iteration can create new pages, which causes rehashing
+ // of the OrdinaryPages() set and invalidates the iterator.
+ // See crbug.com/502089411
+ Page::PageSet pages(Page::OrdinaryPages());
+ for (const auto& page : pages) {
for (Frame* frame = page->MainFrame(); frame;
frame = frame->Tree().TraverseNext()) {
LocalFrame* local_frame = DynamicTo<LocalFrame>(frame);
Original Bug Report
Use-After-Free in Page::OrdinaryPages() iteration via synchronous Navigation API abort
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.
Overview: A potential Use-After-Free (UAF) vulnerability exists when iterating over Page::OrdinaryPages() in OomInterventionImpl::Check and InspectorMemoryAgent::forciblyPurgeJavaScriptMemory. Synchronous JavaScript execution during the loop can create new pages, causing a rehash that explicitly frees the underlying HeapHashSet backing store. The iterator subsequently dereferences this freed memory, which can lead to type confusion and potential Remote Code Execution (RCE).
Affected files:
third_party/blink/renderer/core/inspector/inspector_memory_agent.ccthird_party/blink/renderer/controller/oom_intervention_impl.ccthird_party/blink/renderer/core/page/page.ccthird_party/blink/renderer/core/frame/local_frame.cc
Estimated timestamp from git blame: 2020-06-26
Overview
A potential Use-After-Free (UAF) vulnerability exists during the iteration of Page::OrdinaryPages(). The vulnerability is triggered when LocalFrame::ForciblyPurgeV8Memory() stops loaders on a frame tree, leading to synchronous JavaScript execution in child frames via the Navigation API. This re-entrant JS can modify the Page::OrdinaryPages() collection by opening new windows, invalidating the iterator and causing a UAF.
Root Cause Analysis
OomInterventionImpl::Check() (on low-end Android devices) and InspectorMemoryAgent::forciblyPurgeJavaScriptMemory() iterate over the global page collection using a range-based for loop:
for (const auto& page : Page::OrdinaryPages())
Page::OrdinaryPages() returns a HeapHashSet<WeakMember<Page>>. The C++ range-based for loop uses a HashTableConstIterator, which stores a raw pointer (position_) directly into the HeapHashSet’s backing store array managed by cppgc.
Inside the loop, ForciblyPurgeV8Memory() is called on parent frames. This method explicitly calls Loader().StopAllLoaders(/*abort_client=*/true). FrameLoader::StopAllLoaders recursively calls StopAllLoaders on child frames before the child frame’s context is marked as destroyed.
If the child frame has an ongoing intercepted navigation (via the Navigation API), stopping the loader triggers NavigationApi::InformAboutCanceledNavigation(). This results in the synchronous dispatch of an abort event on the AbortSignal and a navigateerror event. Because the child’s script context is still valid and no ScriptForbiddenScope is active, the attacker’s JavaScript executes synchronously.
Potential Exploit Sequence
Note: These are suggested steps based on static analysis.
- An attacker creates a page with an
iframe. - In the
iframe, JS intercepts a navigation using the Navigation API and adds anabortevent listener. - The attacker triggers an OOM condition (e.g., allocating large typed arrays), prompting the browser to call
OomInterventionImpl::Check()on the renderer’s main thread. - The
OomInterventionImplloop begins processing the parent frame and callsForciblyPurgeV8Memory()->StopAllLoaders(), which cascades to the child frame. - The child frame cancels the ongoing navigation, synchronously firing the
abortevent and executing the attacker’s JS callback. - Inside the callback, the attacker calls
window.open("about:blank")multiple times. window.open()executes a synchronous IPC to the browser, which synchronously tells the renderer to create a newWebViewImplandPage.Page::CreateOrdinary()inserts the new page intoPage::OrdinaryPages(). Exceeding the load factor triggersHashTable::RehashTo.RehashToallocates a new backing store and callscppgc::subtle::FreeUnreferencedObject()to explicitly free the old backing store.- The attacker allocates cppgc-managed memory to reclaim the freed backing store, filling it with a forged
WeakMember<Page>pointing to a fakePageobject. - The JS callback completes, and the C++ stack unwinds back to the outer
OomInterventionImpl::Checkloop. - The iterator advances, dereferencing its dangling raw pointer to read the attacker’s forged
WeakMember<Page>. - The loop calls
DynamicTo<LocalFrame>(page->MainFrame()).DynamicToperforms a virtual call (frame->IsLocalFrame()), which jumps to an attacker-controlled address via the fake object’s vtable, granting RCE.
Mitigations & Exploitability
- MiraclePtr: Does not protect this vulnerability because the dangling pointer (
position_) is a raw C++ stack pointer pointing intocppgc(Oilpan) managed memory. - WTF Modification Guards: The concurrent modification checks in
WTF::HashTableare strictly guarded by#if DCHECK_IS_ON(). They compile out in Release builds and do not prevent exploitation. - Exploitability: The
OomInterventionImplpath is web-accessible and reachable without special permissions on platforms where the intervention is enabled (low-end Android).
Suggested Fix
Do not iterate over Page::OrdinaryPages() directly when calling methods that can synchronously execute JavaScript or run nested event loops.
Instead, copy the collection before iterating:
HeapVector<Member<Page>> pages;
CopyToVector(Page::OrdinaryPages(), pages);
for (auto& page : pages) {
// ...
}
Alternatively, enforce a ScriptForbiddenScope during the iteration, though this may cause legitimate assertions to fire if script execution is an expected side-effect of ForciblyPurgeV8Memory().
Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b
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.