Low chrome Memory Corruption 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in Actor
DescriptionMemory corruption in Actor
ComponentActor
Sub-componentWebMCP
Bug ClassMemory Corruption
Tracker524316342
Fix commitcff8d8fcf36b (chromium/src) +165/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
for
components/optimization_guide/content/browser/page_content_proto_util.cc
modified
if
components/optimization_guide/content/browser/page_content_proto_util.cc
modified
IN_PROC_BROWSER_TEST_F
chrome/browser/actor/tools/script_tool_browsertest.cc
modified
TEST_F
components/optimization_guide/content/browser/page_content_proto_util_unittest.cc
modified

Files Changed

  • chrome/browser/actor/tools/script_tool_browsertest.cc
  • components/optimization_guide/content/browser/page_content_proto_provider.cc
  • components/optimization_guide/content/browser/page_content_proto_util.h
  • chrome/browser/actor/tools/script_tool_host.cc
  • chrome/browser/actor/tools/script_tool_request.cc
  • components/optimization_guide/content/browser/page_content_proto_util_unittest.cc
  • components/optimization_guide/content/browser/page_content_proto_util.cc
From cff8d8fcf36bab59f3cbe85cd6e0499b0a536b51 Mon Sep 17 00:00:00 2001
From: Dominic Farolino <dom@chromium.org>
Date: Fri, 25 Sep 2026 16:45:57 -0700
Subject: [PATCH] WebMCP: Enforce permission policy in actor stack

Currently, `ScriptToolHost::TimeOfUseValidation()` and
`ConvertFrameData` do not verify in the browser process whether
`blink::features::kWebMCP` and `PermissionsPolicyFeature::kTools` are
enabled for the target `RenderFrameHost(Impl)`.

Because `Permissions-Policy: tools` defaults to `EnableForSelf`,
cross-origin subframes are disallowed from using WebMCP tools unless
explicitly delegated `allow="tools"`. Without browser-side checks:
1. A compromised renderer in a disallowed frame could populate
   `mojom_frame_data.script_tools` in `GetAIPageContent` and advertise
   tools to the agent in `AnnotatedPageContent`.
2. If the actor dispatched a `ScriptToolAction` targeting a disallowed
   frame, `ScriptToolHost::Invoke()` would grant browser-verified
   transient user activation (`NotifyUserActivation(kActorWebMCP)`) to
   that frame before invoking the tool.

This CL enforces the `kWebMCP` feature and
`PermissionsPolicyFeature::kTools` on the `RenderFrameHost` at booth
tool/APC registration time (`GetRenderFrameInfo()` and
`ConvertFrameData()`) and tool execution time
(`ScriptToolHost::TimeOfUseValidation()`).

This has the benefit of making it impossible for a compromised renderer
without the `tools` permission to advertise tools to the actor stack,
and therefore makes it impossible for it to trick the agent into
executing a fake tool and providing the frame with user activation.

R=mfoltz

Bug: 524316342
Change-Id: Ic44b4bb8e927f889a1be75048e11ae03e6e2c323
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8466242
Reviewed-by: Shivani Sharma <shivanisha@chromium.org>
Reviewed-by: Mark Foltz <mfoltz@chromium.org>
Commit-Queue: Dominic Farolino <dom@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1705317}
---
diff --git a/chrome/browser/actor/tools/script_tool_host.cc b/chrome/browser/actor/tools/script_tool_host.cc
index 4da719a..c7834512 100644
--- a/chrome/browser/actor/tools/script_tool_host.cc
+++ b/chrome/browser/actor/tools/script_tool_host.cc
@@ -26,7 +26,9 @@
 #include "content/public/browser/web_contents.h"
 #include "content/public/browser/web_contents_user_data.h"
 #include "mojo/public/cpp/bindings/message.h"
+#include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom-shared.h"
 #include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
+#include "third_party/blink/public/common/features_generated.h"
 #include "third_party/blink/public/mojom/frame/user_activation_notification_type.mojom-shared.h"
 
 namespace actor {
@@ -97,6 +99,12 @@
     return MakeResult(mojom::ActionResultCode::kTabWentAway);
   }
 
+  CHECK(base::FeatureList::IsEnabled(blink::features::kWebMCP));
+  if (!target_rfh->IsFeatureEnabled(
+          network::mojom::PermissionsPolicyFeature::kTools)) {
+    return MakeResult(mojom::ActionResultCode::kScriptToolInvocationFailed);
+  }
+
   target_frame_tree_node_id_ = target_rfh->GetFrameTreeNodeId();
   target_document_ = target_rfh->GetWeakDocumentPtr();
   return MakeOkResult();
@@ -126,6 +134,9 @@
   InitializePendingResult();
   auto* frame = target_document_.AsRenderFrameHostIfValid();
   CHECK(frame);
+  CHECK(base::FeatureList::IsEnabled(blink::features::kWebMCP));
+  CHECK(frame->IsFeatureEnabled(
+      network::mojom::PermissionsPolicyFeature::kTools));
 
   // Provide transient user activation to the frame for the tool invocation.
   if (base::FeatureList::IsEnabled(
@@ -360,6 +371,19 @@
     return;
   }
 
+  // The new document must also be allowed to use WebMCP. It may not be, even
+  // though it is same-origin with the original target document, if it was
+  // served with `Permissions-Policy: tools=()`, or if its embedder revoked
+  // `allow="tools"` before the navigation.
+  if (!new_host->IsFeatureEnabled(
+          network::mojom::PermissionsPolicyFeature::kTools)) {
+    PostErrorResult(std::move(tool_done_callback_),
+                    mojom::ActionResultCode::kScriptToolInvocationFailed,
+                    base::StrCat({"Tools permission disabled in new document: ",
+                                  new_host->GetLastCommittedURL().spec()}));
+    return;
+  }
+
   lifecycle_ = Lifecycle::kPendingResultFromNewDocument;
 
   new_document_ = new_host->GetWeakDocumentPtr();
diff --git a/chrome/browser/actor/tools/script_tool_request.cc b/chrome/browser/actor/tools/script_tool_request.cc
index fc385ee..b98b9f684 100644
--- a/chrome/browser/actor/tools/script_tool_request.cc
+++ b/chrome/browser/actor/tools/script_tool_request.cc
@@ -14,6 +14,7 @@
 #include "chrome/common/actor/actor_constants.h"
 #include "components/actor/core/actor_features.h"
 #include "components/actor/public/mojom/actor_types.mojom.h"
+#include "third_party/blink/public/common/features_generated.h"
 
 namespace actor {
 
@@ -42,6 +43,13 @@
 ToolRequest::CreateToolResult ScriptToolRequest::CreateTool(
     TaskId task_id,
     ToolDelegate& tool_delegate) const {
+  if (!base::FeatureList::IsEnabled(blink::features::kWebMCP)) {
+    return {/*tool=*/nullptr,
+            MakeResult(mojom::ActionResultCode::kScriptToolInvocationFailed,
+                       /*requires_page_stabilization=*/false,
+                       "WebMCP is not enabled.")};
+  }
+
   if (!GetTabHandle().Get()) {
     return {/*tool=*/nullptr, MakeResult(mojom::ActionResultCode::kTabWentAway,
                                          /*requires_page_stabilization=*/false,
diff --git a/components/optimization_guide/content/browser/page_content_proto_provider.cc b/components/optimization_guide/content/browser/page_content_proto_provider.cc
index da09d13d..3b7dec2 100644
--- a/components/optimization_guide/content/browser/page_content_proto_provider.cc
+++ b/components/optimization_guide/content/browser/page_content_proto_provider.cc
@@ -46,7 +46,9 @@
 #include "services/metrics/public/cpp/metrics_utils.h"
 #include "services/metrics/public/cpp/ukm_builders.h"
 #include "services/metrics/public/cpp/ukm_recorder.h"
+#include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom-shared.h"
 #include "services/service_manager/public/cpp/interface_provider.h"
+#include "third_party/blink/public/common/features_generated.h"
 #include "third_party/blink/public/mojom/content_extraction/ai_page_content.mojom.h"
 #include "ui/gfx/geometry/rect.h"
 
@@ -323,6 +325,10 @@
   render_frame_info.source_origin = render_frame_host->GetLastCommittedOrigin();
   render_frame_info.url = render_frame_host->GetLastCommittedURL();
   render_frame_info.media_data = ComputeMediaData(render_frame_host);
+  render_frame_info.script_tools_are_allowed =
+      base::FeatureList::IsEnabled(blink::features::kWebMCP) &&
+      render_frame_host->IsFeatureEnabled(
+          network::mojom::PermissionsPolicyFeature::kTools);
 
   if (base::FeatureList::IsEnabled(
           features::kAnnotatedPageContentVerifyPopupProcess)) {
diff --git a/components/optimization_guide/content/browser/page_content_proto_util.cc b/components/optimization_guide/content/browser/page_content_proto_util.cc
index a7983ea..a22ce5e 100644
--- a/components/optimization_guide/content/browser/page_content_proto_util.cc
+++ b/components/optimization_guide/content/browser/page_content_proto_util.cc
@@ -1053,8 +1053,23 @@
       metadata.frame_metadata.back()->has_media_transcripts = true;
     }
   }
-  for (const auto& tool : mojom_frame_data.script_tools) {
-    ConvertScriptTool(*tool, proto_frame_data->add_script_tools());
+  // Silently drop `script_tools` rather than killing the renderer if
+  // `!render_frame_info.script_tools_are_allowed` and
+  // `!mojom_frame_data.script_tools.empty()`. `GetAIPageContent` is requested
+  // over an unassociated `blink.mojom.AIPageContentAgent` pipe and
+  // `GetRenderFrameInfo()` is evaluated asynchronously on the `RenderFrameHost`
+  // only after all subframes have responded. If a frame legitimately sends
+  // `script_tools` from a document where `Permissions-Policy: tools` is
+  // enabled, and later commits a navigation that reuses the same
+  // `RenderFrameHost` to a document with `Permissions-Policy: tools=()` while
+  // the browser is still waiting on other subframes, `script_tools_are_allowed`
+  // will be false by the time conversion runs. `RenderFrameHost` reuse happens
+  // when RenderDocument is disabled, *and* also independently when navigating
+  // away from the initial empty document.
+  if (render_frame_info.script_tools_are_allowed) {
+    for (const auto& tool : mojom_frame_data.script_tools) {
+      ConvertScriptTool(*tool, proto_frame_data->add_script_tools());
+    }
   }
 
   // Accessibility focus is tracked globally in the browser, so it should be set
diff --git a/components/optimization_guide/content/browser/page_content_proto_util.h b/components/optimization_guide/content/browser/page_content_proto_util.h
index 2fa71da6..59fa3ce 100644
--- a/components/optimization_guide/content/browser/page_content_proto_util.h
+++ b/components/optimization_guide/content/browser/page_content_proto_util.h
@@ -80,6 +80,15 @@
 
   // The trusted screen bounds of the active popup widget in DIPs.
   gfx::Rect popup_bounds_in_dips;
+
+  // Whether the browser process has verified that this frame is allowed to
+  // expose WebMCP script tools (i.e., the WebMCP feature is enabled and the
+  // frame's Permissions-Policy allows the `tools` feature). This ensures a
+  // compromised renderer in a frame where `Permissions-Policy: tools` is
+  // disabled (such as a cross-origin iframe without `allow="tools"`) cannot
+  // trick the browser into advertising or registering tools with the actor
+  // stack.
+  bool script_tools_are_allowed = false;
 };
 
 struct TargetNodeInfo {
diff --git a/chrome/browser/actor/tools/script_tool_browsertest.cc b/chrome/browser/actor/tools/script_tool_browsertest.cc
index 307ca97c..5a24678 100644
--- a/chrome/browser/actor/tools/script_tool_browsertest.cc
+++ b/chrome/browser/actor/tools/script_tool_browsertest.cc
@@ -33,6 +33,7 @@
 #include "content/public/test/test_navigation_observer.h"
 #include "content/public/test/test_navigation_throttle.h"
 #include "content/public/test/test_navigation_throttle_inserter.h"
+#include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom-shared.h"
 #include "third_party/blink/public/common/features.h"
 #include "third_party/blink/public/common/features_generated.h"
 
@@ -966,4 +967,52 @@
             PageContentExtractionPolicy::kSkipped);
 }
 
+IN_PROC_BROWSER_TEST_F(ActorToolsTestScriptTool, PermissionsPolicyDisabled) {
+  // Embed a cross-origin iframe without `allow="tools"`. Since the default
+  // allowlist for `Permissions-Policy: tools` is `EnableForSelf`, the
+  // cross-origin iframe has `PermissionsPolicyFeature::kTools` disabled.
+  content::RenderFrameHost& subframe =
+      NavigateSubframe(embedded_https_test_server().GetURL(
+                           "example.com", "/actor/simple_iframe.html"),
+                       embedded_https_test_server().GetURL(
+                           "sub.other.com", "/actor/script_tool.html"));
+  EXPECT_FALSE(subframe.HasTransientUserActivation());
+
+  auto action =
+      MakeScriptToolRequest(subframe, "echo", R"JSON({"text": "test"})JSON");
+  RunScriptToolExpectingError(
+      std::move(action), mojom::ActionResultCode::kScriptToolInvocationFailed);
+
+  // Ensure the browser process did not grant transient user activation to the
+  // disallowed frame.
+  EXPECT_FALSE(subframe.HasTransientUserActivation());
+}
+
+// A declarative tool whose result comes from a new, same-origin document must
+// fail if the new document has `PermissionsPolicyFeature::kTools` disabled.
+IN_PROC_BROWSER_TEST_F(ActorToolsTestScriptTool,
+                       PermissionsPolicyDisabledInCrossDocumentResult) {
+  // The same-origin iframe has `PermissionsPolicyFeature::kTools` enabled by
+  // default.
+  content::RenderFrameHost& subframe = NavigateSubframe(
+      embedded_https_test_server().GetURL("example.com",
+                                          "/actor/simple_iframe.html"),
+      embedded_https_test_server().GetURL(
+          "example.com", "/actor/declarative_script_tool_cross_document.html"));
+  ASSERT_TRUE(subframe.IsFeatureEnabled(
+      network::mojom::PermissionsPolicyFeature::kTools));
+
+  // Revoke `tools` from the iframe. The updated container policy only applies
+  // to the document created by the tool's form submission.
+  ASSERT_TRUE(
+      content::ExecJs(web_contents(),
+                      "document.getElementById('iframe').setAttribute('allow', "
+                      "\"tools 'none'\");"));
+
+  auto action = MakeScriptToolRequest(subframe, "declarative_tool",
+                                      R"JSON({"echo": "hello world"})JSON");
+  RunScriptToolExpectingError(
+      std::move(action), mojom::ActionResultCode::kScriptToolInvocationFailed);
+}
+
 }  // namespace actor
diff --git a/components/optimization_guide/content/browser/page_content_proto_util_unittest.cc b/components/optimization_guide/content/browser/page_content_proto_util_unittest.cc
index bdf085c7..dbcde462 100644
--- a/components/optimization_guide/content/browser/page_content_proto_util_unittest.cc
+++ b/components/optimization_guide/content/browser/page_content_proto_util_unittest.cc
@@ -3406,5 +3406,57 @@
                                  .enable_otp_redaction = true,
                              }));
 
+TEST_F(PageContentProtoUtilTest, ScriptToolsFilteredByRenderFrameInfo) {
+  auto main_frame_token = CreateFrameToken();
+  auto root_content = CreatePageContent();
+  auto tool = blink::mojom::ScriptTool::New();
+  tool->name = "forged_tool";
+  tool->description = "forged description";
+  tool->annotations = blink::mojom::ScriptToolAnnotations::New();
+  root_content->frame_data->script_tools.push_back(std::move(tool));
+
+  AIPageContentMap page_content_map;
+  page_content_map[main_frame_token] = std::move(root_content);
+
+  bool allow_script_tools = false;
+  auto get_render_frame_info = base::BindLambdaForTesting(
+      [&](int child_process_id,
+          blink::FrameToken token) -> std::optional<RenderFrameInfo> {
+        RenderFrameInfo render_frame_info;
+        render_frame_info.global_frame_token = main_frame_token;
+        render_frame_info.source_origin =
+            url::Origin::Create(GURL("https://example.com"));
+        render_frame_info.url = GURL("https://example.com");
+        render_frame_info.serialized_server_token = token.ToString();
+        render_frame_info.script_tools_are_allowed = allow_script_tools;
+        return render_frame_info;
+      });
+
+  {
+    AIPageContentResult page_content;
+    FrameTokenSet frame_token_set;
+    ASSERT_TRUE(ConvertAIPageContentToProto(
+                    blink::mojom::AIPageContentOptions::New(), main_frame_token,
+                    page_content_map, get_render_frame_info, frame_token_set,
+                    page_content)
+                    .has_value());
+    EXPECT_TRUE(page_content.proto.main_frame_data().script_tools().empty());
+  }
+
+  allow_script_tools = true;
+  {
+    AIPageContentResult page_content;
+    FrameTokenSet frame_token_set;
+    ASSERT_TRUE(ConvertAIPageContentToProto(
+                    blink::mojom::AIPageContentOptions::New(), main_frame_token,
+                    page_content_map, get_render_frame_info, frame_token_set,
+                    page_content)
+                    .has_value());
+    ASSERT_EQ(page_content.proto.main_frame_data().script_tools().size(), 1);
+    EXPECT_EQ(page_content.proto.main_frame_data().script_tools(0).name(),
+              "forged_tool");
+  }
+}
+
 }  // namespace
 }  // namespace optimization_guide
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/actor/tools/script_tool_browsertest.cc b/chrome/browser/actor/tools/script_tool_browsertest.cc
index 307ca97c..5a24678 100644
--- a/chrome/browser/actor/tools/script_tool_browsertest.cc
+++ b/chrome/browser/actor/tools/script_tool_browsertest.cc
@@ -33,6 +33,7 @@
 #include "content/public/test/test_navigation_observer.h"
 #include "content/public/test/test_navigation_throttle.h"
 #include "content/public/test/test_navigation_throttle_inserter.h"
+#include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom-shared.h"
 #include "third_party/blink/public/common/features.h"
 #include "third_party/blink/public/common/features_generated.h"
 
@@ -966,4 +967,52 @@
             PageContentExtractionPolicy::kSkipped);
 }
 
+IN_PROC_BROWSER_TEST_F(ActorToolsTestScriptTool, PermissionsPolicyDisabled) {
+  // Embed a cross-origin iframe without `allow="tools"`. Since the default
+  // allowlist for `Permissions-Policy: tools` is `EnableForSelf`, the
+  // cross-origin iframe has `PermissionsPolicyFeature::kTools` disabled.
+  content::RenderFrameHost& subframe =
+      NavigateSubframe(embedded_https_test_server().GetURL(
+                           "example.com", "/actor/simple_iframe.html"),
+                       embedded_https_test_server().GetURL(
+                           "sub.other.com", "/actor/script_tool.html"));
+  EXPECT_FALSE(subframe.HasTransientUserActivation());
+
+  auto action =
+      MakeScriptToolRequest(subframe, "echo", R"JSON({"text": "test"})JSON");
+  RunScriptToolExpectingError(
+      std::move(action), mojom::ActionResultCode::kScriptToolInvocationFailed);
+
+  // Ensure the browser process did not grant transient user activation to the
+  // disallowed frame.
+  EXPECT_FALSE(subframe.HasTransientUserActivation());
+}
+
+// A declarative tool whose result comes from a new, same-origin document must
+// fail if the new document has `PermissionsPolicyFeature::kTools` disabled.
+IN_PROC_BROWSER_TEST_F(ActorToolsTestScriptTool,
+                       PermissionsPolicyDisabledInCrossDocumentResult) {
+  // The same-origin iframe has `PermissionsPolicyFeature::kTools` enabled by
+  // default.
+  content::RenderFrameHost& subframe = NavigateSubframe(
+      embedded_https_test_server().GetURL("example.com",
+                                          "/actor/simple_iframe.html"),
+      embedded_https_test_server().GetURL(
+          "example.com", "/actor/declarative_script_tool_cross_document.html"));
+  ASSERT_TRUE(subframe.IsFeatureEnabled(
+      network::mojom::PermissionsPolicyFeature::kTools));
+
+  // Revoke `tools` from the iframe. The updated container policy only applies
+  // to the document created by the tool's form submission.
+  ASSERT_TRUE(
+      content::ExecJs(web_contents(),
+                      "document.getElementById('iframe').setAttribute('allow', "
+                      "\"tools 'none'\");"));
+
+  auto action = MakeScriptToolRequest(subframe, "declarative_tool",
+                                      R"JSON({"echo": "hello world"})JSON");
+  RunScriptToolExpectingError(
+      std::move(action), mojom::ActionResultCode::kScriptToolInvocationFailed);
+}
+
 }  // namespace actor
diff --git a/components/optimization_guide/content/browser/page_content_proto_util_unittest.cc b/components/optimization_guide/content/browser/page_content_proto_util_unittest.cc
index bdf085c7..dbcde462 100644
--- a/components/optimization_guide/content/browser/page_content_proto_util_unittest.cc
+++ b/components/optimization_guide/content/browser/page_content_proto_util_unittest.cc
@@ -3406,5 +3406,57 @@
                                  .enable_otp_redaction = true,
                              }));
 
+TEST_F(PageContentProtoUtilTest, ScriptToolsFilteredByRenderFrameInfo) {
+  auto main_frame_token = CreateFrameToken();
+  auto root_content = CreatePageContent();
+  auto tool = blink::mojom::ScriptTool::New();
+  tool->name = "forged_tool";
+  tool->description = "forged description";
+  tool->annotations = blink::mojom::ScriptToolAnnotations::New();
+  root_content->frame_data->script_tools.push_back(std::move(tool));
+
+  AIPageContentMap page_content_map;
+  page_content_map[main_frame_token] = std::move(root_content);
+
+  bool allow_script_tools = false;
+  auto get_render_frame_info = base::BindLambdaForTesting(
+      [&](int child_process_id,
+          blink::FrameToken token) -> std::optional<RenderFrameInfo> {
+        RenderFrameInfo render_frame_info;
+        render_frame_info.global_frame_token = main_frame_token;
+        render_frame_info.source_origin =
+            url::Origin::Create(GURL("https://example.com"));
+        render_frame_info.url = GURL("https://example.com");
+        render_frame_info.serialized_server_token = token.ToString();
+        render_frame_info.script_tools_are_allowed = allow_script_tools;
+        return render_frame_info;
+      });
+
+  {
+    AIPageContentResult page_content;
+    FrameTokenSet frame_token_set;
+    ASSERT_TRUE(ConvertAIPageContentToProto(
+                    blink::mojom::AIPageContentOptions::New(), main_frame_token,
+                    page_content_map, get_render_frame_info, frame_token_set,
+                    page_content)
+                    .has_value());
+    EXPECT_TRUE(page_content.proto.main_frame_data().script_tools().empty());
+  }
+
+  allow_script_tools = true;
+  {
+    AIPageContentResult page_content;
+    FrameTokenSet frame_token_set;
+    ASSERT_TRUE(ConvertAIPageContentToProto(
+                    blink::mojom::AIPageContentOptions::New(), main_frame_token,
+                    page_content_map, get_render_frame_info, frame_token_set,
+                    page_content)
+                    .has_value());
+    ASSERT_EQ(page_content.proto.main_frame_data().script_tools().size(), 1);
+    EXPECT_EQ(page_content.proto.main_frame_data().script_tools(0).name(),
+              "forged_tool");
+  }
+}
+
 }  // namespace
 }  // namespace optimization_guide
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.