Low watch CVSS 3.1 chrome Logic Error 🔧 Commit mapped

Overview

Low
Severity
3.1
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
Sub-componentDedicatedWorker
Bug ClassLogic Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
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.
Tracker497876969
Fix commitcef96269cafa (chromium/src) +121/-54
CWECWE-664
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-03-30 (per vendor advisory)
Fix landed2026-07-09 (public git)
Disclosed2026-08-25
Patch gap47 days — fix public in source before the advisory

Background

`DedicatedWorkerHostFactoryImpl`
A browser-process (UI-thread) Mojo interface implementation that manufactures DedicatedWorkerHosts on behalf of a creator frame or worker, capturing that creator’s StorageKey, IsolationInfo, and ClientSecurityState at bind time.
`MakeSelfOwnedReceiver` / `SelfOwnedReceiver`
A Mojo lifetime model in which the implementation object lives until its own message pipe is closed, independent of any document or frame lifetime.
`DocumentService<T>`
A Mojo receiver base class that binds an implementation to a specific RenderFrameHost’s current document and automatically destroys it when that document is deleted or the frame commits a cross-document navigation.
`WeakDocumentPtr` / `ancestor_document_`
A weak reference to the document that created the worker; the factory holds it to route later worker creation to the correct frame.

Root Cause Analysis

DedicatedWorkerHostFactoryImpl snapshots security-critical state (StorageKey, IsolationInfo, ClientSecurityState, policies, network restrictions) belonging to its creator document at construction, so its validity is tied to that document. Before the patch, both entry points — RenderFrameHostImpl::CreateDedicatedWorkerHostFactory() and DedicatedWorkerHost::CreateNestedDedicatedWorker() — bound the factory with mojo::MakeSelfOwnedReceiver, meaning the factory’s lifetime was governed only by the renderer-controlled message pipe and could outlive the creator document (for example across a cross-document navigation). A factory that outlived its document would keep vending workers using the stale captured security context, an improper resource-lifetime condition (the bug class is a logic/lifetime error).

The fix converts DedicatedWorkerHostFactoryImpl to a DocumentService<blink::mojom::DedicatedWorkerHostFactory>, routes both call sites through a static Create() that scopes the instance to the ancestor RenderFrameHost’s current document, and — in the nested case — first resolves ancestor_document_.AsRenderFrameHostIfValid() and aborts (returns) if the ancestor frame is already gone. DocumentService then tears the receiver down on document destruction or cross-document navigation, so the factory can no longer serve requests with a stale context (confirmed by the added FactoryResetOnCrossDocumentNavigation test, which asserts the factory disconnects after navigating A→B).

Key insight
The factory captured a document’s security context but was given a SelfOwnedReceiver lifetime decoupled from that document, letting a renderer keep it alive past the document’s teardown; converting it to DocumentService (plus a null-ancestor abort in the nested path) binds its lifetime to the document whose context it holds.

Attack Path

  1. Obtain a worker factory A renderer requests a DedicatedWorkerHostFactory from its frame (CreateDedicatedWorkerHostFactory) or nested worker path, causing the browser to bind a DedicatedWorkerHostFactoryImpl that captures the current document’s StorageKey/IsolationInfo/ClientSecurityState.
  2. Keep the pipe open across a navigation Because the receiver was self-owned, the renderer holds the factory pipe open while the frame commits a cross-document navigation, so the factory survives destruction of the document whose security context it captured.
  3. Vend a worker with stale context The renderer then drives the still-connected factory to create a DedicatedWorkerHost, which is initialized with the previous document’s captured StorageKey/isolation/security state rather than the new document’s. (inferred: use of the mismatched context to access storage or network resources scoped to the old document.)
  4. Fixed behavior After the patch, the DocumentService binding disconnects the factory on the cross-document commit (and CreateNestedDedicatedWorker returns early when the ancestor frame is gone), so step 3 cannot occur.

Impact Assessment

A compromised or malicious renderer could keep a worker factory alive past its creator document and spawn workers stamped with that document’s stale security context (storage key, isolation info, client security state), a browser-process lifetime/logic flaw that could enable access to resources under an incorrect origin/isolation identity (inferred). The advisory classifies it as “improper control of a resource through its lifetime” in Workers and does not report in-the-wild exploitation; any concrete cross-origin data access is (inferred) beyond what the patch demonstrates. Preconditions are the ability to hold a Mojo pipe open across a navigation, i.e. a cooperating or compromised renderer.

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
TEST_F
content/browser/worker_host/dedicated_worker_service_impl_unittest.cc
modified

Files Changed

  • content/browser/worker_host/dedicated_worker_host_factory_impl.h
  • content/browser/worker_host/dedicated_worker_service_impl_unittest.cc
  • 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

Audit Directions

  • Captured-context objects with self-owned lifetimes
    Search for mojo::MakeSelfOwnedReceiver on implementations that snapshot a document’s or frame’s security state (StorageKey, IsolationInfo, ClientSecurityState, policies); these should generally be DocumentService/DocumentUserData so they die with their context.
  • Ancestor/creator weak pointers used without validity checks
    Review paths that dereference or bind against a WeakDocumentPtr/creator frame (e.g. nested worker or nested factory creation) and confirm they check AsRenderFrameHostIfValid() and abort when the ancestor is gone.
  • Lifetime mismatch across navigation
    For any browser-side Mojo interface exposed to renderers, verify a cross-document navigation of the owning frame tears down or rebinds the receiver, rather than leaving it serving with pre-navigation state.
From cef96269cafaa6f303d44ef4ef6c7f08cf8228d9 Mon Sep 17 00:00:00 2001
From: Yoshisto Yanagisawa <yyanagisawa@chromium.org>
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 <yyanagisawa@chromium.org>
Reviewed-by: Rakina Zata Amni <rakina@chromium.org>
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,
+      const PolicyContainerPolicies& creator_policies,
+      base::WeakPtr<CrossOriginEmbedderPolicyReporter> creator_coep_reporter,
+      const base::UnguessableToken& creator_network_restrictions_id);
+
+  // `this` can only be destroyed by DocumentService.
   ~DedicatedWorkerHostFactoryImpl() override;
 
   // blink::mojom::DedicatedWorkerHostFactory:
@@ -69,7 +94,6 @@
           client,
       net::StorageAccessApiStatus storage_access_api_status) override;
 
- private:
   // The ID of the RenderProcessHost where the worker will live.
   const ChildProcessId worker_process_id_;
 
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…

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.