Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in WebMCP
DescriptionUse after free in WebMCP
ComponentWebMCP
Bug ClassUAF
Tracker522064153
Fix commit4e58c2cff37c (chromium/src) +8/-9
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-23

Files Changed

  • third_party/blink/renderer/core/script_tools/model_context.cc
From 4e58c2cff37c50c6b6298bb514d0a8d6cb256484 Mon Sep 17 00:00:00 2001
From: Ben Greenstein <bengr@google.com>
Date: Mon, 29 Jun 2026 16:51:15 -0700
Subject: [PATCH] [WebMCP] Take pending execution before running completion

ModelContext::OnToolExecuted held an iterator into pending_executions_
while invoking probe::WebMCPToolFailed / WebMCPToolResponded and the
stored completion callback. Both of these can re-enter ModelContext (via
DevTools wrapping the thrown error, or via the caller's callback) and
mutate pending_executions_, which can rehash the table and invalidate
the iterator before it is erased.

Use HashMap::Take() to move the entry out of the map before making any
potentially re-entrant calls, matching the existing handling in
CancelTool.

Bug: 522064153
Change-Id: I4a439c4f9c93984f0164a63814c3c576b7e7bcf1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7987973
Reviewed-by: Dominic Farolino <dom@chromium.org>
Commit-Queue: Ben Greenstein <bengr@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1654471}
---

diff --git a/third_party/blink/renderer/core/script_tools/model_context.cc b/third_party/blink/renderer/core/script_tools/model_context.cc
index f9e5ddcc..b43715b 100644
--- a/third_party/blink/renderer/core/script_tools/model_context.cc
+++ b/third_party/blink/renderer/core/script_tools/model_context.cc
@@ -512,13 +512,12 @@
 
   // The pending_executions_ map might have been rehashed during DispatchEvent.
   auto pending_execution =
-      pending_executions_.find(String(invocation_id.ToString()));
-  if (pending_execution == pending_executions_.end()) {
+      pending_executions_.Take(String(invocation_id.ToString()));
+  if (pending_execution.callback.is_null()) {
     return false;
   }
-  OnToolFailed(std::move(pending_execution->value.callback), invocation_id,
+  OnToolFailed(std::move(pending_execution.callback), invocation_id,
                ScriptToolError(ScriptToolErrorCode::kToolCancelled));
-  pending_executions_.erase(pending_execution);
   return true;
 }
 
@@ -734,20 +733,20 @@
 void ModelContext::OnToolExecuted(
     const base::UnguessableToken& invocation_id,
     base::expected<String, std::pair<ScriptValue, ScriptState*>> result) {
-  auto it = pending_executions_.find(String(invocation_id.ToString()));
-  if (it == pending_executions_.end()) {
+  auto pending_execution =
+      pending_executions_.Take(String(invocation_id.ToString()));
+  if (pending_execution.callback.is_null()) {
     return;
   }
 
   if (result.has_value()) {
     probe::WebMCPToolResponded(document_, result.value(), invocation_id);
-    std::move(it->value.callback).Run(result.value());
+    std::move(pending_execution.callback).Run(result.value());
   } else {
     ScriptToolError error(ScriptToolErrorCode::kToolInvocationFailed);
     probe::WebMCPToolFailed(document_, error, invocation_id, result.error());
-    std::move(it->value.callback).Run(base::unexpected(error));
+    std::move(pending_execution.callback).Run(base::unexpected(error));
   }
-  pending_executions_.erase(it);
 }
 
 void ModelContext::MaybeRecordToolCount() {
Loading diff…

Original Bug Report

reported by rj...@google.com

Potential Use-After-Free in ModelContext::OnToolExecuted via Synchronous JS Re-entrancy

Flapjack, 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: A potential Use-After-Free exists in ModelContext::OnToolExecuted because a WTF::HashMap iterator is held across a DevTools probe that can execute synchronous user JavaScript. An attacker can use an AbortSignal inside this JS callback to re-entrantly modify the map, causing its backing store to shrink and be freed before the iterator is accessed. This dangling pointer could be leveraged to execute arbitrary code in the renderer process.

Affected files:

  • third_party/blink/renderer/core/script_tools/model_context.cc

Estimated timestamp from git blame: 2026-03-23

Summary

A potential Use-After-Free (UAF) vulnerability exists in ModelContext::OnToolExecuted within third_party/blink/renderer/core/script_tools/model_context.cc. The vulnerability occurs because a raw WTF::HashMap iterator is held across a call to probe::WebMCPToolFailed. This probe can trigger synchronous execution of user-controlled JavaScript when the DevTools inspector is open. This re-entrancy allows an attacker to manipulate the map and free its backing store, invalidating the iterator before it is subsequently used to execute a callback.

Vulnerability Details

In ModelContext::OnToolExecuted, an iterator it is obtained from the pending_executions_ map:

void ModelContext::OnToolExecuted(
    const base::UnguessableToken& invocation_id,
    base::expected<String, std::pair<ScriptValue, ScriptState*>> result) {
  auto it = pending_executions_.find(String(invocation_id.ToString()));
  // ...
  if (result.has_value()) {
    // ...
  } else {
    ScriptToolError error(ScriptToolErrorCode::kToolInvocationFailed);
    probe::WebMCPToolFailed(document_, error, invocation_id, result.error());
    std::move(it->value.callback).Run(base::unexpected(error));
  }
  pending_executions_.erase(it);
}

When a tool execution fails and probe::WebMCPToolFailed is called, the DevTools inspector (if attached) invokes InspectorWebMCPAgent::WebMCPToolFailed. This method attempts to format the JavaScript exception for the inspector protocol using v8_session_->wrapObject.

V8’s wrapObject delegates to descriptionForError (v8/src/inspector/value-mirror.cc), which attempts to fetch the name and stack properties of the error object. It does this via getErrorProperty(). While getErrorProperty() includes checks to prevent the execution of getters defined directly on the object (via GetOwnPropertyDescriptor), it falls back to standard property access (object->Get()) if the property is not found. For standard JS Error objects, the name property is inherited from Error.prototype. Therefore, standard property access is used, which evaluates getters on the prototype chain without any ScriptId safety checks.

An attacker can define a malicious getter on Error.prototype.name. When this getter executes synchronously, the attacker can invoke abortController.abort() on previously registered dummy tools. This triggers ModelContext::CancelTool, which calls pending_executions_.erase(). If enough tools are erased, WTF::HashMap::Shrink() is triggered, allocating a new backing buffer and freeing the old one via PartitionAlloc.

When control returns to ModelContext::OnToolExecuted, the iterator it continues to point into the freed backing buffer. The code then uses this dangling pointer to execute the tool’s callback (std::move(it->value.callback).Run(...)).

Potential Exploitation Steps

  1. The attacker ensures DevTools is open and attached to the page.
  2. The attacker defines a malicious JavaScript getter: Object.defineProperty(Error.prototype, 'name', { get: function() { ... } });.
  3. The attacker registers a large number of dummy WebMCP tools via the ModelContext API, passing an AbortSignal for each.
  4. The attacker triggers execution of all dummy tools, growing the pending_executions_ map’s backing store.
  5. The attacker executes a target tool, adding it to the map.
  6. The attacker intentionally causes the target tool’s execution to fail by throwing a standard JS Error.
  7. The failure triggers ModelContext::OnToolExecuted, which fetches the iterator it and invokes the DevTools probe.
  8. The probe accesses the error’s name property, synchronously triggering the attacker’s getter.
  9. Inside the getter, the attacker aborts all the dummy tools. This causes pending_executions_ to erase the dummy entries and shrink, freeing the backing buffer.
  10. Still inside the getter, the attacker sprays the PartitionAlloc heap to replace the freed backing buffer with attacker-controlled data, forging a base::OnceCallback at the target tool’s index.
  11. The getter returns. OnToolExecuted resumes and executes std::move(it->value.callback).Run(...) using the dangling iterator, hijacking the instruction pointer and achieving arbitrary Remote Code Execution (RCE) in the renderer process.

Suggested Fix

Do not hold the WTF::HashMap iterator across the probe::WebMCPToolFailed call, as the map can be mutated during the synchronous callback. The element should be removed from the map before the probe is invoked and the callback is executed. For example, use HashMap::Take() to extract the PendingExecution struct by value:

  auto pending_execution = pending_executions_.Take(String(invocation_id.ToString()));
  // Check if pending_execution is valid...
  
  if (result.has_value()) {
    probe::WebMCPToolResponded(document_, result.value(), invocation_id);
    std::move(pending_execution.callback).Run(result.value());
  } else {
    ScriptToolError error(ScriptToolErrorCode::kToolInvocationFailed);
    probe::WebMCPToolFailed(document_, error, invocation_id, result.error());
    std::move(pending_execution.callback).Run(base::unexpected(error));
  }

Evaluated with Chrome root at commit: 2155cb00003ec35716a76ed3246eae995f87b7ff


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