High chrome UAF 🔧 Commit mapped

Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Agents
DescriptionUse after free in Agents
ComponentAgents
Bug ClassUAF
Tracker483569512
Fix commitcf01b963406f (chromium/src) +91/-7
CISA KEVNot listed
CreditedSyn4pse
Disclosed2026-03-10

Changed Functions

FunctionChangeNotes
TEST_F
third_party/blink/renderer/core/script_tools/model_context_test.cc
modified
ReentrantListener
third_party/blink/renderer/core/script_tools/model_context_test.cc
modified

Files Changed

  • third_party/blink/renderer/core/script_tools/model_context.cc
  • third_party/blink/renderer/core/script_tools/model_context_test.cc
From cf01b963406f63ce3fbf5d5b1383e5dca34682fa Mon Sep 17 00:00:00 2001
From: Mason Freed <masonf@chromium.org>
Date: Wed, 11 Feb 2026 17:23:05 -0800
Subject: [PATCH] Fix UAF in ModelContext::CancelTool due to iterator invalidation

ModelContext::CancelTool held a HashMap iterator across a synchronous
DispatchEvent call for the 'toolcancel' event. Because this event
dispatches to JavaScript, a script listener could re-enter the
ModelContext (e.g., by calling executeTool()) and cause the
pending_executions_ map to rehash or mutate. This invalidated the
iterator held by CancelTool, leading to a UAF when the iterator was
subsequently used to access the callback or erase the entry.

Note from the bug: the recent fix in
https://chromium-review.googlesource.com/c/chromium/src/+/7531756
originally trying to fix the UaF, but the later commit in
https://chromium-review.googlesource.com/c/chromium/src/+/7540370
introduces this UAF.

Fixed: 483569512
Change-Id: Ic97b93f1f40ee09dc2742266b1b80bc5b554b876
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7569343
Reviewed-by: Ben Greenstein <bengr@chromium.org>
Commit-Queue: Ben Greenstein <bengr@chromium.org>
Auto-Submit: Mason Freed <masonf@chromium.org>
Commit-Queue: Mason Freed <masonf@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1583638}
---

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 6d07706..6cc73e5 100644
--- a/third_party/blink/renderer/core/script_tools/model_context.cc
+++ b/third_party/blink/renderer/core/script_tools/model_context.cc
@@ -254,17 +254,24 @@
 }
 
 void ModelContext::CancelTool(uint32_t execution_id) {
+  auto it = pending_executions_.find(execution_id);
+  if (it == pending_executions_.end()) {
+    return;
+  }
+  String tool_name = it->value.tool_name;
+
+  if (LocalDOMWindow* window = document_->domWindow()) {
+    // This is a synchronous, non-cancelable event. Note that this can re-enter
+    // JavaScript and modify `pending_executions_`.
+    window->DispatchEvent(
+        *WebMCPEvent::Create(event_type_names::kToolcancel, tool_name));
+  }
+
+  // The pending_executions_ map might have been rehashed during DispatchEvent.
   auto pending_execution = pending_executions_.find(execution_id);
   if (pending_execution == pending_executions_.end()) {
     return;
   }
-
-  if (LocalDOMWindow* window = document_->domWindow()) {
-    // This is a synchronous, non-cancelable event.
-    window->DispatchEvent(*WebMCPEvent::Create(
-        event_type_names::kToolcancel, pending_execution->value.tool_name));
-  }
-
   task_runner_->PostTask(
       FROM_HERE,
       blink::BindOnce(std::move(pending_execution->value.callback),
diff --git a/third_party/blink/renderer/core/script_tools/model_context_test.cc b/third_party/blink/renderer/core/script_tools/model_context_test.cc
index 7373e36..cb304cc 100644
--- a/third_party/blink/renderer/core/script_tools/model_context_test.cc
+++ b/third_party/blink/renderer/core/script_tools/model_context_test.cc
@@ -16,6 +16,7 @@
 #include "third_party/blink/public/web/web_script_source.h"
 #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
 #include "third_party/blink/renderer/core/dom/abort_controller.h"
+#include "third_party/blink/renderer/core/dom/events/native_event_listener.h"
 #include "third_party/blink/renderer/core/frame/local_dom_window.h"
 #include "third_party/blink/renderer/core/frame/web_local_frame_impl.h"
 #include "third_party/blink/renderer/core/script_tools/model_context_supplement.h"
@@ -72,6 +73,7 @@
 
  private:
   ScopedWebMCPForTest scoped_webmcp_{true};
+  ScopedWebMCPTestingForTest scoped_webmcp_testing_{true};
 };
 
 TEST_F(ModelContextTest, ExecuteTool) {
@@ -965,4 +967,79 @@
   EXPECT_EQ(EvalJsString("window.select_val"), "3");
 }
 
+class ReentrantListener : public NativeEventListener {
+ public:
+  explicit ReentrantListener(ModelContext* model_context)
+      : model_context_(model_context) {}
+  void Invoke(ExecutionContext*, Event*) override {
+    // Trigger HashMap modification by adding a new execution.
+    model_context_->ExecuteTool("echo", "{}", nullptr, base::DoNothing());
+  }
+  void Trace(Visitor* visitor) const override {
+    visitor->Trace(model_context_);
+    NativeEventListener::Trace(visitor);
+  }
+
+ private:
+  Member<ModelContext> model_context_;
+};
+
+TEST_F(ModelContextTest, CancelToolReentrancy) {
+  SimRequest main_resource("https://example.com/", "text/html");
+  LoadURL("https://example.com/");
+  v8::HandleScope handle_scope(Window().GetIsolate());
+  ScriptState::Scope script_scope(
+      ToScriptStateForMainWorld(Window().GetFrame()));
+
+  main_resource.Complete(R"(
+    <body>
+    <script>
+    async function hang(obj) {
+      return new Promise(() => {});
+    }
+
+    navigator.modelContext.registerTool({
+      execute: hang,
+      name: "hang",
+      description: "never resolves",
+    });
+
+    // We also need another tool that can be executed.
+    navigator.modelContext.registerTool({
+      execute: async () => "done",
+      name: "echo",
+      description: "echo",
+    });
+  </script>
+)");
+
+  auto* model_context =
+      ModelContextSupplement::modelContext(*Window().navigator());
+  ASSERT_TRUE(model_context);
+
+  Window().addEventListener(
+      event_type_names::kToolcancel,
+      MakeGarbageCollected<ReentrantListener>(model_context), false);
+
+  base::RunLoop run_loop;
+
+  std::optional<uint32_t> execution_id = model_context->ExecuteTool(
+      "hang", "{}", /* signal= */ nullptr,
+      base::BindLambdaForTesting(
+          [&](base::expected<WebString, WebDocument::ScriptToolError> res) {
+            EXPECT_FALSE(res.has_value());
+            EXPECT_EQ(res.error(),
+                      WebDocument::ScriptToolError::kToolCancelled);
+            run_loop.Quit();
+          }));
+
+  ASSERT_TRUE(execution_id.has_value());
+
+  // This should trigger the toolcancel event, which re-enters and modifies
+  // pending_executions_.
+  model_context->CancelTool(*execution_id);
+
+  run_loop.Run();
+}
+
 }  // namespace blink
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/renderer/core/script_tools/model_context_test.cc b/third_party/blink/renderer/core/script_tools/model_context_test.cc
index 7373e36..cb304cc 100644
--- a/third_party/blink/renderer/core/script_tools/model_context_test.cc
+++ b/third_party/blink/renderer/core/script_tools/model_context_test.cc
@@ -16,6 +16,7 @@
 #include "third_party/blink/public/web/web_script_source.h"
 #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
 #include "third_party/blink/renderer/core/dom/abort_controller.h"
+#include "third_party/blink/renderer/core/dom/events/native_event_listener.h"
 #include "third_party/blink/renderer/core/frame/local_dom_window.h"
 #include "third_party/blink/renderer/core/frame/web_local_frame_impl.h"
 #include "third_party/blink/renderer/core/script_tools/model_context_supplement.h"
@@ -72,6 +73,7 @@
 
  private:
   ScopedWebMCPForTest scoped_webmcp_{true};
+  ScopedWebMCPTestingForTest scoped_webmcp_testing_{true};
 };
 
 TEST_F(ModelContextTest, ExecuteTool) {
@@ -965,4 +967,79 @@
   EXPECT_EQ(EvalJsString("window.select_val"), "3");
 }
 
+class ReentrantListener : public NativeEventListener {
+ public:
+  explicit ReentrantListener(ModelContext* model_context)
+      : model_context_(model_context) {}
+  void Invoke(ExecutionContext*, Event*) override {
+    // Trigger HashMap modification by adding a new execution.
+    model_context_->ExecuteTool("echo", "{}", nullptr, base::DoNothing());
+  }
+  void Trace(Visitor* visitor) const override {
+    visitor->Trace(model_context_);
+    NativeEventListener::Trace(visitor);
+  }
+
+ private:
+  Member<ModelContext> model_context_;
+};
+
+TEST_F(ModelContextTest, CancelToolReentrancy) {
+  SimRequest main_resource("https://example.com/", "text/html");
+  LoadURL("https://example.com/");
+  v8::HandleScope handle_scope(Window().GetIsolate());
+  ScriptState::Scope script_scope(
+      ToScriptStateForMainWorld(Window().GetFrame()));
+
+  main_resource.Complete(R"(
+    <body>
+    <script>
+    async function hang(obj) {
+      return new Promise(() => {});
+    }
+
+    navigator.modelContext.registerTool({
+      execute: hang,
+      name: "hang",
+      description: "never resolves",
+    });
+
+    // We also need another tool that can be executed.
+    navigator.modelContext.registerTool({
+      execute: async () => "done",
+      name: "echo",
+      description: "echo",
+    });
+  </script>
+)");
+
+  auto* model_context =
+      ModelContextSupplement::modelContext(*Window().navigator());
+  ASSERT_TRUE(model_context);
+
+  Window().addEventListener(
+      event_type_names::kToolcancel,
+      MakeGarbageCollected<ReentrantListener>(model_context), false);
+
+  base::RunLoop run_loop;
+
+  std::optional<uint32_t> execution_id = model_context->ExecuteTool(
+      "hang", "{}", /* signal= */ nullptr,
+      base::BindLambdaForTesting(
+          [&](base::expected<WebString, WebDocument::ScriptToolError> res) {
+            EXPECT_FALSE(res.has_value());
+            EXPECT_EQ(res.error(),
+                      WebDocument::ScriptToolError::kToolCancelled);
+            run_loop.Quit();
+          }));
+
+  ASSERT_TRUE(execution_id.has_value());
+
+  // This should trigger the toolcancel event, which re-enters and modifies
+  // pending_executions_.
+  model_context->CancelTool(*execution_id);
+
+  run_loop.Run();
+}
+
 }  // namespace blink
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.