Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in DevTools
DescriptionUse after free in DevTools
ComponentDevTools
Bug ClassUAF
Tracker525331547
Fix commit50302ef33bc0 (chromium/src) +114/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
for
third_party/blink/renderer/core/inspector/inspector_emulation_agent.cc
modified
InspectorEmulationAgentTest
third_party/blink/renderer/core/inspector/inspector_emulation_agent_test.cc
modified
TEST_F
third_party/blink/renderer/core/inspector/inspector_emulation_agent_test.cc
modified
MutatingBodyLoader
third_party/blink/renderer/core/inspector/inspector_emulation_agent_test.cc
modified
if
third_party/blink/renderer/core/inspector/inspector_emulation_agent_test.cc
modified
for
third_party/blink/renderer/core/inspector/inspector_emulation_agent_test.cc
modified

Files Changed

  • third_party/blink/renderer/core/inspector/inspector_emulation_agent.cc
  • third_party/blink/renderer/core/inspector/inspector_emulation_agent_test.cc
From 50302ef33bc0c9f9e7313821c936a9dd8ce8aa15 Mon Sep 17 00:00:00 2001
From: Peter Kvitek <[email protected]>
Date: Fri, 19 Jun 2026 06:17:35 -0700
Subject: [PATCH] [DevTools] Improve Emulation.setVirtualTimePolicy robustness

Bug: 525331547
Change-Id: I15ce4a9dd7fea2ad4fce5cc36a7dcc863cc185d1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7964078
Auto-Submit: Peter Kvitek <[email protected]>
Commit-Queue: Dmitry Gozman <[email protected]>
Reviewed-by: Dmitry Gozman <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1649637}
---

diff --git a/third_party/blink/renderer/core/inspector/inspector_emulation_agent.cc b/third_party/blink/renderer/core/inspector/inspector_emulation_agent.cc
index 4163430..c6d6e4b 100644
--- a/third_party/blink/renderer/core/inspector/inspector_emulation_agent.cc
+++ b/third_party/blink/renderer/core/inspector/inspector_emulation_agent.cc
@@ -600,9 +600,16 @@
         budget_amount,
         BindOnce(&InspectorEmulationAgent::VirtualTimeBudgetExpired,
                  WrapWeakPersistent(this)));
-    for (DocumentLoader* loader : pending_document_loaders_)
+    // SetDefersLoading() can synchronously run author script (virtual time
+    // forces kForceSynchronousParsing) which may spin a nested message loop
+    // and re-enter WillCommitLoad(), mutating |pending_document_loaders_|.
+    // Move to a local snapshot first so the live iterators cannot be
+    // invalidated.
+    HeapVector<Member<DocumentLoader>> loaders =
+        std::move(pending_document_loaders_);
+    for (DocumentLoader* loader : loaders) {
       loader->SetDefersLoading(LoaderFreezeMode::kNone);
-    pending_document_loaders_.clear();
+    }
   }
 
   if (max_virtual_time_task_starvation_count.value_or(0)) {
diff --git a/third_party/blink/renderer/core/inspector/inspector_emulation_agent_test.cc b/third_party/blink/renderer/core/inspector/inspector_emulation_agent_test.cc
index 0d2d3e60..8d190fe 100644
--- a/third_party/blink/renderer/core/inspector/inspector_emulation_agent_test.cc
+++ b/third_party/blink/renderer/core/inspector/inspector_emulation_agent_test.cc
@@ -14,9 +14,12 @@
 #include "third_party/blink/renderer/core/frame/web_local_frame_impl.h"
 #include "third_party/blink/renderer/core/inspector/inspector_session_state.h"
 #include "third_party/blink/renderer/core/inspector/protocol/protocol.h"
+#include "third_party/blink/renderer/core/loader/document_loader.h"
 #include "third_party/blink/renderer/platform/scheduler/public/page_scheduler.h"
 #include "third_party/blink/renderer/platform/scheduler/public/thread_cpu_throttler.h"
 #include "third_party/blink/renderer/platform/testing/task_environment.h"
+#include "third_party/blink/renderer/platform/testing/url_loader_test_delegate.h"
+#include "third_party/blink/renderer/platform/testing/url_test_helpers.h"
 #include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
 #include "third_party/inspector_protocol/crdtp/span.h"
 
@@ -53,8 +56,6 @@
   void FlushProtocolNotifications() override {}
 };
 
-}  // namespace
-
 class InspectorEmulationAgentTest : public testing::Test {};
 
 TEST_F(InspectorEmulationAgentTest, ModifiesAcceptHeader) {
@@ -152,4 +153,106 @@
 }
 #endif
 
+class MutatingBodyLoader : public WebNavigationBodyLoader {
+ public:
+  MutatingBodyLoader(InspectorEmulationAgent* agent,
+                     LocalFrame* frame,
+                     Persistent<DocumentLoader>* loader2)
+      : agent_(agent), frame_(frame), loader2_(loader2) {}
+
+  void SetTestReady(bool ready) { test_ready_ = ready; }
+
+  void SetDefersLoading(WebLoaderFreezeMode mode) override {
+    if (test_ready_ && mode == WebLoaderFreezeMode::kNone && !mutated_) {
+      mutated_ = true;
+      double base_ms = 0;
+      agent_->setVirtualTimePolicy(
+          protocol::Emulation::VirtualTimePolicyEnum::Pause, std::nullopt,
+          std::nullopt, std::nullopt, &base_ms);
+
+      auto params2 = std::make_unique<WebNavigationParams>();
+      params2->url = WebURL(KURL("https://example.com/test2.html"));
+      *loader2_ = MakeGarbageCollected<DocumentLoader>(
+          frame_, kWebNavigationTypeOther, std::move(params2), nullptr,
+          nullptr);
+
+      for (int i = 0; i < 50; ++i) {
+        agent_->WillCommitLoad(frame_, *loader2_);
+      }
+    }
+  }
+
+  void StartLoadingBody(Client*) override {}
+  BodyLoaderType GetType() const override { return BodyLoaderType::kStatic; }
+
+ private:
+  Persistent<InspectorEmulationAgent> agent_;
+  Persistent<LocalFrame> frame_;
+  Persistent<DocumentLoader>* loader2_;
+  bool mutated_ = false;
+  bool test_ready_ = false;
+};
+
+TEST_F(InspectorEmulationAgentTest, VirtualTimePolicyIteratorInvalidation) {
+  test::TaskEnvironment task_environment;
+  frame_test_helpers::WebViewHelper helper;
+  WebViewImpl* web_view = helper.Initialize();
+  WebLocalFrameImpl* web_frame = web_view->MainFrameImpl();
+  LocalFrame* frame = web_frame->GetFrame();
+  auto* virtual_time_controller =
+      web_view->Scheduler()->GetVirtualTimeController();
+
+  DummyFrontendChannel channel;
+  protocol::UberDispatcher dispatcher(&channel);
+  auto reattach_state = mojom::blink::DevToolsSessionState::New();
+  InspectorSessionState session_state(std::move(reattach_state));
+
+  auto* agent = MakeGarbageCollected<InspectorEmulationAgent>(
+      web_frame, *virtual_time_controller);
+  agent->Init(frame->GetProbeSink(), &dispatcher, &session_state);
+
+  double base_ms = 0;
+  agent->setVirtualTimePolicy(protocol::Emulation::VirtualTimePolicyEnum::Pause,
+                              std::nullopt, std::nullopt, std::nullopt,
+                              &base_ms);
+
+  Persistent<DocumentLoader> loader2;
+  auto body_loader =
+      std::make_unique<MutatingBodyLoader>(agent, frame, &loader2);
+  MutatingBodyLoader* body_loader_ptr = body_loader.get();
+
+  auto params1 = std::make_unique<WebNavigationParams>();
+  params1->url = WebURL(KURL("https://example.com/test1.html"));
+  params1->body_loader = std::move(body_loader);
+
+  auto* loader1 = MakeGarbageCollected<DocumentLoader>(
+      frame, kWebNavigationTypeOther, std::move(params1), nullptr, nullptr);
+  loader1->StartLoading();
+
+  agent->WillCommitLoad(frame, loader1);
+
+  body_loader_ptr->SetTestReady(true);
+
+  agent->setVirtualTimePolicy(
+      protocol::Emulation::VirtualTimePolicyEnum::Advance, 100.0, std::nullopt,
+      std::nullopt, &base_ms);
+
+  loader1->SetSentDidFinishLoad();
+  loader1->StopLoading();
+  loader1->DetachFromFrame(false);
+
+  if (loader2) {
+    loader2->SetSentDidFinishLoad();
+    loader2->StopLoading();
+    loader2->DetachFromFrame(false);
+  }
+
+  virtual_time_controller->DisableVirtualTimeForTesting();
+  agent->disable();
+  agent->Dispose();
+  helper.Reset();
+}
+
+}  // namespace
+
 }  // namespace blink
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/renderer/core/inspector/inspector_emulation_agent_test.cc b/third_party/blink/renderer/core/inspector/inspector_emulation_agent_test.cc
index 0d2d3e60..8d190fe 100644
--- a/third_party/blink/renderer/core/inspector/inspector_emulation_agent_test.cc
+++ b/third_party/blink/renderer/core/inspector/inspector_emulation_agent_test.cc
@@ -14,9 +14,12 @@
 #include "third_party/blink/renderer/core/frame/web_local_frame_impl.h"
 #include "third_party/blink/renderer/core/inspector/inspector_session_state.h"
 #include "third_party/blink/renderer/core/inspector/protocol/protocol.h"
+#include "third_party/blink/renderer/core/loader/document_loader.h"
 #include "third_party/blink/renderer/platform/scheduler/public/page_scheduler.h"
 #include "third_party/blink/renderer/platform/scheduler/public/thread_cpu_throttler.h"
 #include "third_party/blink/renderer/platform/testing/task_environment.h"
+#include "third_party/blink/renderer/platform/testing/url_loader_test_delegate.h"
+#include "third_party/blink/renderer/platform/testing/url_test_helpers.h"
 #include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
 #include "third_party/inspector_protocol/crdtp/span.h"
 
@@ -53,8 +56,6 @@
   void FlushProtocolNotifications() override {}
 };
 
-}  // namespace
-
 class InspectorEmulationAgentTest : public testing::Test {};
 
 TEST_F(InspectorEmulationAgentTest, ModifiesAcceptHeader) {
@@ -152,4 +153,106 @@
 }
 #endif
 
+class MutatingBodyLoader : public WebNavigationBodyLoader {
+ public:
+  MutatingBodyLoader(InspectorEmulationAgent* agent,
+                     LocalFrame* frame,
+                     Persistent<DocumentLoader>* loader2)
+      : agent_(agent), frame_(frame), loader2_(loader2) {}
+
+  void SetTestReady(bool ready) { test_ready_ = ready; }
+
+  void SetDefersLoading(WebLoaderFreezeMode mode) override {
+    if (test_ready_ && mode == WebLoaderFreezeMode::kNone && !mutated_) {
+      mutated_ = true;
+      double base_ms = 0;
+      agent_->setVirtualTimePolicy(
+          protocol::Emulation::VirtualTimePolicyEnum::Pause, std::nullopt,
+          std::nullopt, std::nullopt, &base_ms);
+
+      auto params2 = std::make_unique<WebNavigationParams>();
+      params2->url = WebURL(KURL("https://example.com/test2.html"));
+      *loader2_ = MakeGarbageCollected<DocumentLoader>(
+          frame_, kWebNavigationTypeOther, std::move(params2), nullptr,
+          nullptr);
+
+      for (int i = 0; i < 50; ++i) {
+        agent_->WillCommitLoad(frame_, *loader2_);
+      }
+    }
+  }
+
+  void StartLoadingBody(Client*) override {}
+  BodyLoaderType GetType() const override { return BodyLoaderType::kStatic; }
+
+ private:
+  Persistent<InspectorEmulationAgent> agent_;
+  Persistent<LocalFrame> frame_;
+  Persistent<DocumentLoader>* loader2_;
+  bool mutated_ = false;
+  bool test_ready_ = false;
+};
+
+TEST_F(InspectorEmulationAgentTest, VirtualTimePolicyIteratorInvalidation) {
+  test::TaskEnvironment task_environment;
+  frame_test_helpers::WebViewHelper helper;
+  WebViewImpl* web_view = helper.Initialize();
+  WebLocalFrameImpl* web_frame = web_view->MainFrameImpl();
+  LocalFrame* frame = web_frame->GetFrame();
+  auto* virtual_time_controller =
+      web_view->Scheduler()->GetVirtualTimeController();
+
+  DummyFrontendChannel channel;
+  protocol::UberDispatcher dispatcher(&channel);
+  auto reattach_state = mojom::blink::DevToolsSessionState::New();
+  InspectorSessionState session_state(std::move(reattach_state));
+
+  auto* agent = MakeGarbageCollected<InspectorEmulationAgent>(
+      web_frame, *virtual_time_controller);
+  agent->Init(frame->GetProbeSink(), &dispatcher, &session_state);
+
+  double base_ms = 0;
+  agent->setVirtualTimePolicy(protocol::Emulation::VirtualTimePolicyEnum::Pause,
+                              std::nullopt, std::nullopt, std::nullopt,
+                              &base_ms);
+
+  Persistent<DocumentLoader> loader2;
+  auto body_loader =
+      std::make_unique<MutatingBodyLoader>(agent, frame, &loader2);
+  MutatingBodyLoader* body_loader_ptr = body_loader.get();
+
+  auto params1 = std::make_unique<WebNavigationParams>();
+  params1->url = WebURL(KURL("https://example.com/test1.html"));
+  params1->body_loader = std::move(body_loader);
+
+  auto* loader1 = MakeGarbageCollected<DocumentLoader>(
+      frame, kWebNavigationTypeOther, std::move(params1), nullptr, nullptr);
+  loader1->StartLoading();
+
+  agent->WillCommitLoad(frame, loader1);
+
+  body_loader_ptr->SetTestReady(true);
+
+  agent->setVirtualTimePolicy(
+      protocol::Emulation::VirtualTimePolicyEnum::Advance, 100.0, std::nullopt,
+      std::nullopt, &base_ms);
+
+  loader1->SetSentDidFinishLoad();
+  loader1->StopLoading();
+  loader1->DetachFromFrame(false);
+
+  if (loader2) {
+    loader2->SetSentDidFinishLoad();
+    loader2->StopLoading();
+    loader2->DetachFromFrame(false);
+  }
+
+  virtual_time_controller->DisableVirtualTimeForTesting();
+  agent->disable();
+  agent->Dispose();
+  helper.Reset();
+}
+
+}  // namespace
+
 }  // namespace blink
Loading diff…

Original Bug Report

reported by [email protected]

UAF in InspectorEmulationAgent::setVirtualTimePolicy due to reentrant vector mutation

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: During execution of InspectorEmulationAgent::setVirtualTimePolicy, unfreezing pending loaders can synchronously execute author scripts and spin a nested message loop via a scripted print request. If the virtual time budget expires during this nested loop, the policy reverts to Pause, allowing other concurrent navigation commits to mutate pending_document_loaders_. This results in vector reallocation and iterator invalidation, potentially leading to a Use-After-Free (UAF) in the renderer process.

Affected files:

  • third_party/blink/renderer/core/inspector/inspector_emulation_agent.cc
  • third_party/blink/renderer/core/inspector/inspector_emulation_agent.h

Estimated timestamp from git blame: 2020-11-11

Potential Use-After-Free (UAF) in InspectorEmulationAgent::setVirtualTimePolicy

Summary

There is a potential Use-After-Free (UAF) vulnerability in InspectorEmulationAgent::setVirtualTimePolicy due to synchronous script reentrancy and reentrant mutation of the pending_document_loaders_ vector during iteration.

Root Cause Analysis

In InspectorEmulationAgent::setVirtualTimePolicy (third_party/blink/renderer/core/inspector/inspector_emulation_agent.cc:603-605), the agent iterates over pending_document_loaders_ (a HeapVector<Member<DocumentLoader>>) directly with a range-based for loop:

for (DocumentLoader* loader : pending_document_loaders_)
  loader->SetDefersLoading(LoaderFreezeMode::kNone);
pending_document_loaders_.clear();

When loader->SetDefersLoading(LoaderFreezeMode::kNone) is called on a loader, it propagates down to NavigationBodyLoader::SetDefersLoading which synchronously invokes OnReadable, reads from the Mojo data pipe, and triggers synchronous HTML parsing (kForceSynchronousParsing is enabled when virtual time is active).

This allows inline script blocks inside the parsing document to execute synchronously on the same stack. If this script spins a nested message loop, asynchronous tasks can run. If the virtual time budget expires inside the nested loop, VirtualTimeBudgetExpired is called and reverts virtual_time_policy_ to Pause.

When any concurrent navigation commit message is processed during the nested loop, InspectorEmulationAgent::WillCommitLoad is invoked. Because the policy was reverted to Pause, the loader is frozen and appended to pending_document_loaders_ via push_back:

void InspectorEmulationAgent::WillCommitLoad(LocalFrame*, DocumentLoader* loader) {
  if (virtual_time_policy_.Get() != protocol::Emulation::VirtualTimePolicyEnum::Pause) return;
  loader->SetDefersLoading(LoaderFreezeMode::kStrict);
  pending_document_loaders_.push_back(loader);
}

This push_back mutates the active pending_document_loaders_ vector, potentially reallocating its backing store. When the nested loop exits and control returns to the range-based for loop, the loop’s iterators are invalidated, resulting in a Use-After-Free of the Member<DocumentLoader> backing storage.

Note: Since our tooling agent currently lacks the ability to execute code, these are potential steps and findings derived from code flow analysis.

Suggested Potential Exploitation Steps

An attacker might attempt to exploit this potential vulnerability using the following path:

  1. Target page runs under a CDP automation session where virtual time is managed (e.g. headless tests).
  2. The CDP client pauses virtual time (policy: "pause"), causing multiple frame navigations to freeze and append their DocumentLoaders to pending_document_loaders_.
  3. The CDP client resumes virtual time (policy: "advance") with a small budget.
  4. During the iteration, the first unpaused loader synchronously parses an inline script containing parent.print(). This script targets a fully loaded parent frame to bypass LocalDOMWindow::print’s loading check, triggering PrintRenderFrameHelper::RequestPrintPreview.
  5. The scripted print request spins a nested base::RunLoop with nestable tasks allowed.
  6. Inside the nested loop, the virtual time budget expires, reverting the policy to Pause.
  7. Also during the nested loop, a queued navigation commit dispatches, calling WillCommitLoad and invoking pending_document_loaders_.push_back(), causing vector reallocation.
  8. The print loop is terminated/dismissed, and the range-for resumes with invalidated iterators, leading to UAF.

To resolve this issue, adopt the pattern used to fix other reentrancy bugs in loader vectors (such as document_loader.cc:1946-1960) by snapshotting the collection before iterating. Move the elements out of pending_document_loaders_ using std::move or std::exchange before the loop, preventing any reentrant mutations from invalidating active iterators.

HeapVector<Member<DocumentLoader>> pending_loaders = std::move(pending_document_loaders_);
for (DocumentLoader* loader : pending_loaders)
  loader->SetDefersLoading(LoaderFreezeMode::kNone);

Evaluated with Chrome root at commit: 75203b87cbf6681eb7c7dda8e1d0bf781538c76a


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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