Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Blink
DescriptionUse after free in Blink
ComponentBlink
Bug ClassUAF
Tracker502089411
Fix commit7428099311de (chromium/src) +12/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/controller/oom_intervention_impl.cc
modified
for
third_party/blink/renderer/controller/oom_intervention_impl.cc
modified
for
third_party/blink/renderer/core/inspector/inspector_memory_agent.cc
modified

Files Changed

  • third_party/blink/renderer/controller/oom_intervention_impl.cc
  • third_party/blink/renderer/core/inspector/inspector_memory_agent.cc
From 7428099311de1034955118520c81f7acf867ec93 Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <[email protected]>
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 <[email protected]>
Reviewed-by: Steve Kobes <[email protected]>
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);
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • third_party/blink/renderer/controller/oom_intervention_impl.cc
  • third_party/blink/renderer/core/page/page.cc
  • third_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.

  1. An attacker creates a page with an iframe.
  2. In the iframe, JS intercepts a navigation using the Navigation API and adds an abort event listener.
  3. 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.
  4. The OomInterventionImpl loop begins processing the parent frame and calls ForciblyPurgeV8Memory() -> StopAllLoaders(), which cascades to the child frame.
  5. The child frame cancels the ongoing navigation, synchronously firing the abort event and executing the attacker’s JS callback.
  6. Inside the callback, the attacker calls window.open("about:blank") multiple times.
  7. window.open() executes a synchronous IPC to the browser, which synchronously tells the renderer to create a new WebViewImpl and Page.
  8. Page::CreateOrdinary() inserts the new page into Page::OrdinaryPages(). Exceeding the load factor triggers HashTable::RehashTo.
  9. RehashTo allocates a new backing store and calls cppgc::subtle::FreeUnreferencedObject() to explicitly free the old backing store.
  10. The attacker allocates cppgc-managed memory to reclaim the freed backing store, filling it with a forged WeakMember<Page> pointing to a fake Page object.
  11. The JS callback completes, and the C++ stack unwinds back to the outer OomInterventionImpl::Check loop.
  12. The iterator advances, dereferencing its dangling raw pointer to read the attacker’s forged WeakMember<Page>.
  13. The loop calls DynamicTo<LocalFrame>(page->MainFrame()). DynamicTo performs 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 into cppgc (Oilpan) managed memory.
  • WTF Modification Guards: The concurrent modification checks in WTF::HashTable are strictly guarded by #if DCHECK_IS_ON(). They compile out in Release builds and do not prevent exploitation.
  • Exploitability: The OomInterventionImpl path 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.

View on issue tracker