CVE-2026-14393
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forsrc/inspector/v8-console-agent-impl.cc |
modified | |
ifsrc/inspector/v8-console-message.cc |
modified | |
forsrc/inspector/v8-runtime-agent-impl.cc |
modified |
Files Changed
src/inspector/v8-console-agent-impl.ccsrc/inspector/v8-console-message.ccsrc/inspector/v8-inspector-impl.ccsrc/inspector/v8-inspector-impl.hsrc/inspector/v8-runtime-agent-impl.cc
Patch
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; }
Original Bug Report
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.ccv8/src/inspector/v8-runtime-agent-impl.ccv8/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
V8ConsoleMessageStorage::addMessagelogs a console message and callsm_inspector->forEachSession. This triggersV8RuntimeAgentImpl::reportMessage.reportMessagecallsValueMirror::create(viawrapArgumentsorwrapException) to wrap the object. For anErrorobject,ValueMirror::createattempts to access the propertiesname,message, andstackviagetErrorProperty.- While
getErrorPropertyavoids direct user-defined getters, it falls back toobject->Get(...)if the property is not an ‘own’ property. This allows an attacker to execute custom JavaScript synchronously by placing aProxywith agettrap on the object’s prototype chain. - The attacker places a
debugger;statement inside thisgettrap. This forces a synchronous pause, spinning up a nested message loop in V8. - 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 theV8ConsoleMessageStorageobject (this) fromm_consoleStorageMap, freeing it. - 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 intom_consoleStorageMapusing the samecontextGroupId. - The attacker resumes execution, unwinding the stack back to
V8ConsoleMessageStorage::addMessage. addMessageexecutes 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 returnstrue.addMessageproceeds to callm_messages.push_back(...)on the freedthispointer, resulting in a UAF write.
Vector 2: UAF Read in V8RuntimeAgentImpl::enable
V8RuntimeAgentImpl::enablecaches a pointer to the storage and a reference to its message deque:const auto& messages = storage->messages();.- It iterates over this deque, calling
reportMessage. - If
reportMessagetriggers the samedebugger;nested loop sequence described above, the original storage and its deque are freed, and a new storage is created. - The
hasConsoleMessageStorageguard at the end ofreportMessagefalsely returnstrue. - The loop continues to the next iteration, accessing
messages.size()andmessages[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.
- For
V8ConsoleMessageStorage::addMessage: Instead of relying onhasConsoleMessageStorage(contextGroupId), capture abase::WeakPtr(or equivalent weak reference mechanism available in V8) tothisbefore theforEachSessioncall. Check if the weak pointer is still valid after the call returns. - 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.