Low chrome Logic Error 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper control of a resource through its lifetime in Workers
DescriptionImproper control of a resource through its lifetime in Workers
ComponentWorkers
Bug ClassLogic Error
Tracker497876969
Fix commitcef96269cafa (chromium/src) +121/-54
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
if
content/browser/worker_host/dedicated_worker_host.cc
modified
RenderFrameHost
content/browser/worker_host/dedicated_worker_host_factory_impl.h
modified
CONTENT_EXPORT
content/browser/worker_host/dedicated_worker_host_factory_impl.h
modified

Files Changed

  • content/browser/renderer_host/render_frame_host_impl.cc
  • content/browser/worker_host/dedicated_worker_host.cc
  • content/browser/worker_host/dedicated_worker_host_factory_impl.cc
  • content/browser/worker_host/dedicated_worker_host_factory_impl.h
From cef96269cafaa6f303d44ef4ef6c7f08cf8228d9 Mon Sep 17 00:00:00 2001
From: Yoshisto Yanagisawa <[email protected]>
Date: Thu, 09 Jul 2026 00:37:53 -0700
Subject: [PATCH] [DedicatedWorker] Scope DedicatedWorkerHostFactoryImpl to the creator document

DedicatedWorkerHostFactoryImpl captures its creator's StorageKey,
IsolationInfo and ClientSecurityState at bind time, so it should not
outlive the document that requested it. Convert it to a
DocumentService<blink::mojom::DedicatedWorkerHostFactory> so the
receiver is torn down when the ancestor RenderFrameHost is deleted or
commits a cross-document navigation. Both the frame-creator path
(RenderFrameHostImpl::CreateDedicatedWorkerHostFactory) and the
worker-creator path (DedicatedWorkerHost::CreateNestedDedicatedWorker)
now go through a static Create() that scopes the instance to the
ancestor RFH's current document. Existing unit tests are migrated to the
new API and a regression test is added.

Bug: 497876969
Change-Id: I2322c94b8e788e6960cd0b499b5e2eb6bd8571af
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8035382
Commit-Queue: Yoshisato Yanagisawa <[email protected]>
Reviewed-by: Rakina Zata Amni <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1659365}
---

diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index 4c38ac1..f19b1a8f 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -14859,14 +14859,13 @@
 
   // When a dedicated worker is created from the frame script, the frame is both
   // the creator and the ancestor.
-  mojo::MakeSelfOwnedReceiver(
-      std::make_unique<DedicatedWorkerHostFactoryImpl>(
-          worker_process_id,
-          /*creator=*/GetGlobalId(), GetWeakDocumentPtr(), GetStorageKey(),
-          isolation_info_, BuildClientSecurityState(),
-          policy_container_host()->policies(),
-          /*creator_coep_reporter=*/coep_reporter, GetNetworkRestrictionsID()),
-      std::move(receiver));
+  DedicatedWorkerHostFactoryImpl::Create(
+      *this, std::move(receiver), worker_process_id,
+      /*creator=*/GetGlobalId(),
+      /*ancestor_document=*/GetWeakDocumentPtr(), GetStorageKey(),
+      isolation_info_, BuildClientSecurityState(),
+      policy_container_host()->policies(),
+      /*creator_coep_reporter=*/coep_reporter, GetNetworkRestrictionsID());
 }
 
 #if BUILDFLAG(IS_ANDROID) || (BUILDFLAG(IS_IOS) && !BUILDFLAG(IS_IOS_TVOS))
diff --git a/content/browser/worker_host/dedicated_worker_host.cc b/content/browser/worker_host/dedicated_worker_host.cc
index 656b953..c7fc66c 100644
--- a/content/browser/worker_host/dedicated_worker_host.cc
+++ b/content/browser/worker_host/dedicated_worker_host.cc
@@ -857,16 +857,23 @@
 void DedicatedWorkerHost::CreateNestedDedicatedWorker(
     mojo::PendingReceiver<blink::mojom::DedicatedWorkerHostFactory> receiver) {
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
+  RenderFrameHost* ancestor_render_frame_host =
+      ancestor_document_.AsRenderFrameHostIfValid();
+  if (!ancestor_render_frame_host) {
+    // The ancestor frame may have already been closed. In that case, the worker
+    // will soon be terminated too, so abort the connection.
+    return;
+  }
+
   base::WeakPtr<CrossOriginEmbedderPolicyReporter> creator_coep_reporter =
       GetWorkerCoepReporter();
 
-  mojo::MakeSelfOwnedReceiver(
-      std::make_unique<DedicatedWorkerHostFactoryImpl>(
-          worker_process_host_->GetID(), /*creator=*/token_, ancestor_document_,
-          GetWorkerStorageKey(), isolation_info_,
-          worker_client_security_state_->Clone(), creator_policies_,
-          creator_coep_reporter, network_restrictions_id_),
-      std::move(receiver));
+  DedicatedWorkerHostFactoryImpl::Create(
+      *ancestor_render_frame_host, std::move(receiver),
+      worker_process_host_->GetID(), /*creator=*/token_, ancestor_document_,
+      GetWorkerStorageKey(), isolation_info_,
+      worker_client_security_state_->Clone(), creator_policies_,
+      creator_coep_reporter, network_restrictions_id_);
 }
 
 void DedicatedWorkerHost::CreateIdleManager(
diff --git a/content/browser/worker_host/dedicated_worker_host_factory_impl.cc b/content/browser/worker_host/dedicated_worker_host_factory_impl.cc
index e6ccdba4..b32393d1 100644
--- a/content/browser/worker_host/dedicated_worker_host_factory_impl.cc
+++ b/content/browser/worker_host/dedicated_worker_host_factory_impl.cc
@@ -47,7 +47,32 @@
 
 }  // namespace
 
+// static
+void DedicatedWorkerHostFactoryImpl::Create(
+    RenderFrameHost& ancestor_render_frame_host,
+    mojo::PendingReceiver<blink::mojom::DedicatedWorkerHostFactory> receiver,
+    ChildProcessId worker_process_id,
+    DedicatedWorkerCreator creator,
+    WeakDocumentPtr ancestor_document,
+    const blink::StorageKey& creator_storage_key,
+    const net::IsolationInfo& isolation_info,
+    network::mojom::ClientSecurityStatePtr creator_client_security_state,
+    const PolicyContainerPolicies& creator_policies,
+    base::WeakPtr<CrossOriginEmbedderPolicyReporter> creator_coep_reporter,
+    const base::UnguessableToken& creator_network_restrictions_id) {
+  // The factory deletes itself when the receiver is disconnected or when the
+  // ancestor RenderFrameHost commits a cross-document navigation.
+  new DedicatedWorkerHostFactoryImpl(
+      ancestor_render_frame_host, std::move(receiver), worker_process_id,
+      creator, std::move(ancestor_document), creator_storage_key,
+      isolation_info, std::move(creator_client_security_state),
+      creator_policies, std::move(creator_coep_reporter),
+      creator_network_restrictions_id);
+}
+
 DedicatedWorkerHostFactoryImpl::DedicatedWorkerHostFactoryImpl(
+    RenderFrameHost& ancestor_render_frame_host,
+    mojo::PendingReceiver<blink::mojom::DedicatedWorkerHostFactory> receiver,
     ChildProcessId worker_process_id,
     DedicatedWorkerCreator creator,
     WeakDocumentPtr ancestor_document,
@@ -57,7 +82,8 @@
     const PolicyContainerPolicies& creator_policies,
     base::WeakPtr<CrossOriginEmbedderPolicyReporter> creator_coep_reporter,
     const base::UnguessableToken& creator_network_restrictions_id)
-    : worker_process_id_(worker_process_id),
+    : DocumentService(ancestor_render_frame_host, std::move(receiver)),
+      worker_process_id_(worker_process_id),
       creator_(creator),
       ancestor_document_(std::move(ancestor_document)),
       creator_storage_key_(creator_storage_key),
diff --git a/content/browser/worker_host/dedicated_worker_host_factory_impl.h b/content/browser/worker_host/dedicated_worker_host_factory_impl.h
index 15e5e923..8b94d54 100644
--- a/content/browser/worker_host/dedicated_worker_host_factory_impl.h
+++ b/content/browser/worker_host/dedicated_worker_host_factory_impl.h
@@ -9,9 +9,11 @@
 #include "content/browser/renderer_host/policy_container_host.h"
 #include "content/common/content_export.h"
 #include "content/public/browser/dedicated_worker_creator.h"
+#include "content/public/browser/document_service.h"
 #include "content/public/browser/global_routing_id.h"
 #include "content/public/browser/weak_document_ptr.h"
 #include "content/public/common/child_process_id.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
 #include "mojo/public/cpp/bindings/pending_remote.h"
 #include "net/base/isolation_info.h"
 #include "net/storage_access_api/status.h"
@@ -23,23 +25,31 @@
 
 namespace content {
 
-// A factory for creating DedicatedWorkerHosts. Its lifetime is managed by the
-// renderer over mojo via SelfOwnedReceiver. It lives on the UI thread.
+class RenderFrameHost;
+
+// A factory for creating DedicatedWorkerHosts. Its lifetime is scoped to the
+// current document of the ancestor RenderFrameHost via DocumentService. It
+// lives on the UI thread.
 //
 // A factory instance creates at most one `DedicatedWorkerHost` instance.
-class CONTENT_EXPORT DedicatedWorkerHostFactoryImpl
-    : public blink::mojom::DedicatedWorkerHostFactory {
+class CONTENT_EXPORT DedicatedWorkerHostFactoryImpl final
+    : public DocumentService<blink::mojom::DedicatedWorkerHostFactory> {
  public:
   using CreateWorkerHostCallback = base::OnceCallback<void(
       const network::CrossOriginEmbedderPolicy&,
       mojo::PendingRemote<blink::mojom::BackForwardCacheControllerHost>)>;
 
+  // Creates and binds an instance scoped to `ancestor_render_frame_host`'s
+  // current document.
+  //
   // `creator_client_security_state` specifies the client security state of
   // the creator frame or worker. Must not be nullptr.
   // `creator_policies` specifies the security policies of the creator.
   // `creator_network_restrictions_id` specifies the network restrictions of
   // the creator as per its connection allowlists.
-  DedicatedWorkerHostFactoryImpl(
+  static void Create(
+      RenderFrameHost& ancestor_render_frame_host,
+      mojo::PendingReceiver<blink::mojom::DedicatedWorkerHostFactory> receiver,
       ChildProcessId worker_process_id,
       DedicatedWorkerCreator creator,
       WeakDocumentPtr ancestor_document,
@@ -55,6 +65,21 @@
   DedicatedWorkerHostFactoryImpl& operator=(
       const DedicatedWorkerHostFactoryImpl&) = delete;
 
+ private:
+  DedicatedWorkerHostFactoryImpl(
+      RenderFrameHost& ancestor_render_frame_host,
+      mojo::PendingReceiver<blink::mojom::DedicatedWorkerHostFactory> receiver,
+      ChildProcessId worker_process_id,
+      DedicatedWorkerCreator creator,
+      WeakDocumentPtr ancestor_document,
+      const blink::StorageKey& creator_storage_key,
+      const net::IsolationInfo& isolation_info,
+      network::mojom::ClientSecurityStatePtr creator_client_security_state,
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/worker_host/dedicated_worker_service_impl_unittest.cc b/content/browser/worker_host/dedicated_worker_service_impl_unittest.cc
index 4b99649..7249859 100644
--- a/content/browser/worker_host/dedicated_worker_service_impl_unittest.cc
+++ b/content/browser/worker_host/dedicated_worker_service_impl_unittest.cc
@@ -18,6 +18,7 @@
 #include "content/browser/worker_host/dedicated_worker_host.h"
 #include "content/browser/worker_host/dedicated_worker_host_factory_impl.h"
 #include "content/common/content_navigation_policy.h"
+#include "content/public/browser/back_forward_cache.h"
 #include "content/public/browser/storage_partition.h"
 #include "content/public/common/content_features.h"
 #include "content/public/test/navigation_simulator.h"
@@ -27,7 +28,6 @@
 #include "content/test/test_render_view_host.h"
 #include "content/test/test_web_contents.h"
 #include "mojo/public/cpp/bindings/remote.h"
-#include "mojo/public/cpp/bindings/self_owned_receiver.h"
 #include "mojo/public/cpp/test_support/test_utils.h"
 #include "net/base/isolation_info.h"
 #include "net/storage_access_api/status.h"
@@ -62,17 +62,15 @@
         GURL(), std::nullopt, std::nullopt, base::UnguessableToken::Create(),
         net::NetworkAnonymizationKey());
 
-    mojo::MakeSelfOwnedReceiver(
-        std::make_unique<DedicatedWorkerHostFactoryImpl>(
-            worker_process_id, /*creator=*/render_frame_host_id,
-            RenderFrameHostImpl::FromID(render_frame_host_id)
-                ->GetWeakDocumentPtr(),
-            blink::StorageKey::CreateFirstParty(origin),
-            net::IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
-            network::mojom::ClientSecurityState::New(),
-            PolicyContainerPolicies(), coep_reporter->GetWeakPtr(),
-            network::GetTestNetworkRestrictionsId()),
-        factory_.BindNewPipeAndPassReceiver());
+    DedicatedWorkerHostFactoryImpl::Create(
+        *RenderFrameHostImpl::FromID(render_frame_host_id),
+        factory_.BindNewPipeAndPassReceiver(), worker_process_id,
+        /*creator=*/render_frame_host_id,
+        RenderFrameHostImpl::FromID(render_frame_host_id)->GetWeakDocumentPtr(),
+        blink::StorageKey::CreateFirstParty(origin),
+        net::IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
+        network::mojom::ClientSecurityState::New(), PolicyContainerPolicies(),
+        coep_reporter->GetWeakPtr(), network::GetTestNetworkRestrictionsId());
 
     auto fetch_client_settings_object =
         blink::mojom::FetchClientSettingsObject::New();
@@ -368,6 +366,29 @@
   void SetUp() override { RenderViewHostImplTestHarness::SetUp(); }
 };
 
+TEST_F(DedicatedWorkerHostFactoryImplTest,
+       FactoryResetOnCrossDocumentNavigation) {
+  // Disable back/forward cache so the previous document is destroyed on
+  // navigation rather than being preserved.
+  web_contents()->GetController().GetBackForwardCache().DisableForTesting(
+      BackForwardCache::TEST_REQUIRES_NO_CACHING);
+
+  const GURL kUrlA("http://a.example.com/");
+  const GURL kUrlB("http://b.example.com/");
+
+  NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(), kUrlA);
+
+  mojo::Remote<blink::mojom::DedicatedWorkerHostFactory> factory;
+  static_cast<RenderFrameHostImpl*>(web_contents()->GetPrimaryMainFrame())
+      ->CreateDedicatedWorkerHostFactory(factory.BindNewPipeAndPassReceiver());
+  factory.FlushForTesting();
+  ASSERT_TRUE(factory.is_connected());
+
+  NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(), kUrlB);
+  factory.FlushForTesting();
+  EXPECT_FALSE(factory.is_connected());
+}
+
 TEST_F(DedicatedWorkerHostFactoryImplTest, CrossOriginScriptOriginCheck) {
   url::ScopedSchemeRegistryForTests scoped_registry;
   url::AddStandardScheme("isolated-app", url::SCHEME_WITH_HOST);
@@ -401,7 +422,8 @@
                 base::UnguessableToken::Create(),
                 net::NetworkAnonymizationKey());
 
-        return std::make_unique<DedicatedWorkerHostFactoryImpl>(
+        DedicatedWorkerHostFactoryImpl::Create(
+            *creator_rfh, factory.BindNewPipeAndPassReceiver(),
             creator_rfh->GetProcess()->GetID(),
             static_cast<RenderFrameHostImpl*>(creator_rfh)->GetGlobalId(),
             static_cast<RenderFrameHostImpl*>(creator_rfh)
@@ -440,9 +462,7 @@
         features::kEnforceDedicatedWorkerSameOriginCheck);
 
     mojo::Remote<blink::mojom::DedicatedWorkerHostFactory> factory;
-    auto factory_impl = create_factory(factory, kIwaOriginA);
-    mojo::Receiver<blink::mojom::DedicatedWorkerHostFactory> receiver(
-        factory_impl.get(), factory.BindNewPipeAndPassReceiver());
+    create_factory(factory, kIwaOriginA);
 
     mojo::test::BadMessageObserver bad_message_observer;
     start_script_load(factory, kIwaAppB);
@@ -458,9 +478,7 @@
         features::kEnforceDedicatedWorkerSameOriginCheck);
 
     mojo::Remote<blink::mojom::DedicatedWorkerHostFactory> factory;
-    auto factory_impl = create_factory(factory, kIwaOriginA);
-    mojo::Receiver<blink::mojom::DedicatedWorkerHostFactory> receiver(
-        factory_impl.get(), factory.BindNewPipeAndPassReceiver());
+    create_factory(factory, kIwaOriginA);
 
     mojo::test::BadMessageObserver bad_message_observer;
     start_script_load(factory, kIwaAppB);
@@ -477,9 +495,7 @@
         features::kEnforceDedicatedWorkerSameOriginCheck);
 
     mojo::Remote<blink::mojom::DedicatedWorkerHostFactory> factory;
-    auto factory_impl = create_factory(factory, kExtOriginA);
-    mojo::Receiver<blink::mojom::DedicatedWorkerHostFactory> receiver(
-        factory_impl.get(), factory.BindNewPipeAndPassReceiver());
+    create_factory(factory, kExtOriginA);
 
     mojo::test::BadMessageObserver bad_message_observer;
     // Cross-origin load (even to https) should be blocked for extensions now.
@@ -497,9 +513,7 @@
         features::kEnforceDedicatedWorkerSameOriginCheck);
 
     mojo::Remote<blink::mojom::DedicatedWorkerHostFactory> factory;
-    auto factory_impl = create_factory(factory, kIwaOriginA);
-    mojo::Receiver<blink::mojom::DedicatedWorkerHostFactory> receiver(
-        factory_impl.get(), factory.BindNewPipeAndPassReceiver());
+    create_factory(factory, kIwaOriginA);
 
     mojo::test::BadMessageObserver bad_message_observer;
     start_script_load(factory, kIwaAppA);
@@ -516,9 +530,7 @@
         features::kEnforceDedicatedWorkerSameOriginCheck);
 
     mojo::Remote<blink::mojom::DedicatedWorkerHostFactory> factory;
-    auto factory_impl = create_factory(factory, kIwaOriginA);
-    mojo::Receiver<blink::mojom::DedicatedWorkerHostFactory> receiver(
-        factory_impl.get(), factory.BindNewPipeAndPassReceiver());
+    create_factory(factory, kIwaOriginA);
 
     mojo::test::BadMessageObserver bad_message_observer;
     start_script_load(factory, GURL("data:text/javascript,console.log('hi')"));
@@ -553,7 +565,8 @@
                   base::UnguessableToken::Create(),
                   net::NetworkAnonymizationKey());
 
-          return std::make_unique<DedicatedWorkerHostFactoryImpl>(
+          DedicatedWorkerHostFactoryImpl::Create(
+              *opaque_rfh, factory.BindNewPipeAndPassReceiver(),
               opaque_rfh->GetProcess()->GetID(),
               static_cast<RenderFrameHostImpl*>(opaque_rfh)->GetGlobalId(),
               static_cast<RenderFrameHostImpl*>(opaque_rfh)
@@ -567,9 +580,7 @@
         };
 
     mojo::Remote<blink::mojom::DedicatedWorkerHostFactory> factory;
-    auto factory_impl = create_opaque_factory(factory);
-    mojo::Receiver<blink::mojom::DedicatedWorkerHostFactory> receiver(
-        factory_impl.get(), factory.BindNewPipeAndPassReceiver());
+    create_opaque_factory(factory);
 
     mojo::test::BadMessageObserver bad_message_observer;
     // Attempt to load a script that would normally be same-origin to the
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.