Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in V8
DescriptionUse after free in V8
ComponentV8
Bug ClassUAF
Tracker511255112
Fix commit1dba9760bf96 (v8/v8) +33/-10
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
for
src/inspector/v8-console-agent-impl.cc
modified
if
src/inspector/v8-console-message.cc
modified
for
src/inspector/v8-runtime-agent-impl.cc
modified

Files Changed

  • src/inspector/v8-console-agent-impl.cc
  • src/inspector/v8-console-message.cc
  • src/inspector/v8-inspector-impl.cc
  • src/inspector/v8-inspector-impl.h
  • src/inspector/v8-runtime-agent-impl.cc
From 1dba9760bf9601b35fe0f1ab07c9f228ef554cff Mon Sep 17 00:00:00 2001
From: Kim-Anh Tran <[email protected]>
Date: Mon, 18 May 2026 01:41:17 -0700
Subject: [PATCH] Fix potential UAF when accessing V8ConsoleMessageStorage

During console message handling a V8ConsoleMessageStorage may be removed
and recreated in a nested message loop (kicked off by a 'debugger' call
in JS code), leading to a UAF if we don't check the identity of the
storage object itself.

This adds the identity checks for storage whenever JS may have changed
the storage object.

Bug: 511255112
Change-Id: I121eebd536f5e36f2df19467e0cb996a0f8962ac
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7853603
Reviewed-by: Benedikt Meurer <[email protected]>
Commit-Queue: Benedikt Meurer <[email protected]>
Cr-Commit-Position: refs/heads/main@{#107470}
---

diff --git a/src/inspector/v8-console-agent-impl.cc b/src/inspector/v8-console-agent-impl.cc
index 8a79d9c..1fc082c 100644
--- a/src/inspector/v8-console-agent-impl.cc
+++ b/src/inspector/v8-console-agent-impl.cc
@@ -62,11 +62,16 @@
           m_session->contextGroupId());
   // The message queue can be cleared by a getter during message formatting.
   // Make a copy of the message to avoid a UAF.
-  const auto& messages = storage->messages();
-  const size_t size = messages.size();
+  // Also, the storage itself can be destroyed and recreated, so re-fetch the
+  // storage on each iteration.
+  size_t size = storage->messages().size();
   for (size_t i = 0; i < size; ++i) {
-    if (i >= messages.size()) break;
-    V8ConsoleMessage message = *messages[i];
+    if (m_session->inspector()->consoleMessageStorage(
+            m_session->contextGroupId()) != storage) {
+      break;
+    }
+    if (i >= storage->messages().size()) break;
+    V8ConsoleMessage message = *storage->messages()[i];
     if (!reportMessage(&message, false)) {
       break;
     }
diff --git a/src/inspector/v8-console-message.cc b/src/inspector/v8-console-message.cc
index 66d12ad..f6993b0 100644
--- a/src/inspector/v8-console-message.cc
+++ b/src/inspector/v8-console-message.cc
@@ -321,8 +321,11 @@
   // Protect against reentrant debugger calls via interrupts.
   v8::debug::PostponeInterruptsScope no_interrupts(inspector->isolate());
 
+  V8ConsoleMessageStorage* storage =
+      inspector->consoleMessageStorage(contextGroupId);
+  if (!storage) return;
+
   if (m_origin == V8MessageOrigin::kException) {
-    if (!inspector->hasConsoleMessageStorage(contextGroupId)) return;
     v8::HandleScope scope(inspector->isolate());
     auto maybeScriptOrigin =
         v8::debug::GetScriptOrigin(inspector->isolate(), m_scriptId);
@@ -367,7 +370,7 @@
   if (m_origin == V8MessageOrigin::kConsole) {
     std::unique_ptr<protocol::Array<protocol::Runtime::RemoteObject>>
         arguments = wrapArguments(session, generatePreview);
-    if (!inspector->hasConsoleMessageStorage(contextGroupId)) return;
+    if (inspector->consoleMessageStorage(contextGroupId) != storage) return;
     if (!arguments) {
       arguments =
           std::make_unique<protocol::Array<protocol::Runtime::RemoteObject>>();
diff --git a/src/inspector/v8-inspector-impl.cc b/src/inspector/v8-inspector-impl.cc
index 34be36d..77bd811 100644
--- a/src/inspector/v8-inspector-impl.cc
+++ b/src/inspector/v8-inspector-impl.cc
@@ -138,6 +138,15 @@
   return storageIt->second.get();
 }
 
+V8ConsoleMessageStorage* V8InspectorImpl::consoleMessageStorage(
+    int contextGroupId) {
+  auto storageIt = m_consoleStorageMap.find(contextGroupId);
+  if (storageIt == m_consoleStorageMap.end()) {
+    return nullptr;
+  }
+  return storageIt->second.get();
+}
+
 bool V8InspectorImpl::hasConsoleMessageStorage(int contextGroupId) {
   auto storageIt = m_consoleStorageMap.find(contextGroupId);
   return storageIt != m_consoleStorageMap.end();
diff --git a/src/inspector/v8-inspector-impl.h b/src/inspector/v8-inspector-impl.h
index cf4e63c..f0e9ab4 100644
--- a/src/inspector/v8-inspector-impl.h
+++ b/src/inspector/v8-inspector-impl.h
@@ -133,6 +133,7 @@
   void muteExceptions(int contextGroupId);
   void unmuteExceptions(int contextGroupId);
   V8ConsoleMessageStorage* ensureConsoleMessageStorage(int contextGroupId);
+  V8ConsoleMessageStorage* consoleMessageStorage(int contextGroupId);
   bool hasConsoleMessageStorage(int contextGroupId);
   void discardInspectedContext(int contextGroupId, int contextId);
   void disconnect(V8InspectorSessionImpl*);
diff --git a/src/inspector/v8-runtime-agent-impl.cc b/src/inspector/v8-runtime-agent-impl.cc
index 226a6c7..e7a003b 100644
--- a/src/inspector/v8-runtime-agent-impl.cc
+++ b/src/inspector/v8-runtime-agent-impl.cc
@@ -1125,11 +1125,16 @@
       m_inspector->ensureConsoleMessageStorage(m_session->contextGroupId());
   // The message queue can be cleared by a getter during message formatting.
   // Make a copy of the message to avoid a UAF.
-  const auto& messages = storage->messages();
-  const size_t size = messages.size();
+  // Also, the storage itself can be destroyed and recreated, so re-fetch the
+  // storage on each iteration.
+  size_t size = storage->messages().size();
   for (size_t i = 0; i < size; ++i) {
-    if (i >= messages.size()) break;
-    V8ConsoleMessage message = *messages[i];
+    if (m_inspector->consoleMessageStorage(m_session->contextGroupId()) !=
+        storage) {
+      break;
+    }
+    if (i >= storage->messages().size()) break;
+    V8ConsoleMessage message = *storage->messages()[i];
     if (!reportMessage(&message, false)) {
       break;
     }
Loading diff…

Original Bug Report

reported by [email protected]

UAF in V8 Inspector via identity-blind hasConsoleMessageStorage bypass

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

Overview: A potential Use-After-Free exists in V8 Inspector’s console message handling when DevTools is open. An attacker could bypass the hasConsoleMessageStorage liveness guard by using a debugger; statement in a custom object getter to trigger a nested message loop. During this pause, an attacker-initiated navigation can destroy the original V8ConsoleMessageStorage and recreate a new one, leading to UAF read/write operations when the getter returns.

Affected files:

  • v8/src/inspector/v8-console-message.cc
  • v8/src/inspector/v8-runtime-agent-impl.cc
  • v8/src/inspector/v8-inspector-impl.cc

Estimated timestamp from git blame: 2026-02-24

Summary

A potential Use-After-Free (UAF) vulnerability has been identified in the V8 Inspector component. The issue stems from the hasConsoleMessageStorage guard, which is used to verify the liveness of a V8ConsoleMessageStorage instance after a JavaScript callout. This guard only checks for the presence of a context group ID in a map, failing to verify the identity of the specific storage object itself. If the storage is destroyed and then re-created for the same context group ID during a callout, the guard will return a false positive, leading to subsequent operations on a freed object.

Technical Details

There are two identified vectors for this vulnerability: a UAF write in V8ConsoleMessageStorage::addMessage and a UAF read in V8RuntimeAgentImpl::enable.

Vector 1: UAF Write in V8ConsoleMessageStorage::addMessage

  1. V8ConsoleMessageStorage::addMessage logs a console message and calls m_inspector->forEachSession. This triggers V8RuntimeAgentImpl::reportMessage.
  2. reportMessage calls ValueMirror::create (via wrapArguments or wrapException) to wrap the object. For an Error object, ValueMirror::create attempts to access the properties name, message, and stack via getErrorProperty.
  3. While getErrorProperty avoids direct user-defined getters, it falls back to object->Get(...) if the property is not an ‘own’ property. This allows an attacker to execute custom JavaScript synchronously by placing a Proxy with a get trap on the object’s prototype chain.
  4. The attacker places a debugger; statement inside this get trap. This forces a synchronous pause, spinning up a nested message loop in V8.
  5. During this nested loop, the renderer continues to process high-priority IPCs. The attacker arranges for a navigation or frame detachment IPC to arrive at this moment. This triggers V8InspectorImpl::resetContextGroup, which erases the V8ConsoleMessageStorage object (this) from m_consoleStorageMap, freeing it.
  6. Still within the nested loop (e.g., via DevTools evaluation), the attacker logs a new message to the identical context group. This invokes V8InspectorImpl::ensureConsoleMessageStorage, which allocates a new storage object and inserts it into m_consoleStorageMap using the same contextGroupId.
  7. The attacker resumes execution, unwinding the stack back to V8ConsoleMessageStorage::addMessage.
  8. addMessage executes its liveness guard: if (!inspector->hasConsoleMessageStorage(contextGroupId)) return;. Because a new object was created in Step 6, the ID exists in the map, and the guard incorrectly returns true.
  9. addMessage proceeds to call m_messages.push_back(...) on the freed this pointer, resulting in a UAF write.

Vector 2: UAF Read in V8RuntimeAgentImpl::enable

  1. V8RuntimeAgentImpl::enable caches a pointer to the storage and a reference to its message deque: const auto& messages = storage->messages();.
  2. It iterates over this deque, calling reportMessage.
  3. If reportMessage triggers the same debugger; nested loop sequence described above, the original storage and its deque are freed, and a new storage is created.
  4. The hasConsoleMessageStorage guard at the end of reportMessage falsely returns true.
  5. The loop continues to the next iteration, accessing messages.size() and messages[i] using the dangling reference, leading to a UAF read.

Impact

This vulnerability could lead to memory corruption within the renderer process. An attacker could leverage the UAF write to corrupt the heap, potentially achieving Remote Code Execution (RCE) within the renderer sandbox. Exploitation requires DevTools to be open and the user to visit a malicious site.

(Note: These are suggested steps based on static analysis; our tooling agent cannot execute code to verify the exploit.)

Suggested Fix

To prevent this vulnerability, the liveness checks must verify the identity of the storage object, not just the existence of the group ID.

  1. For V8ConsoleMessageStorage::addMessage: Instead of relying on hasConsoleMessageStorage(contextGroupId), capture a base::WeakPtr (or equivalent weak reference mechanism available in V8) to this before the forEachSession call. Check if the weak pointer is still valid after the call returns.
  2. For V8RuntimeAgentImpl::enable: Do not cache raw references or pointers across calls that can execute JavaScript. Instead, maintain an index and fetch the storage and message at that index dynamically on each iteration, verifying the storage object hasn’t been unexpectedly swapped out or deleted.

Evaluated with Chrome root at commit: eca8648a4e1cdfdda68c495a6003059fed641955


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