Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in ServiceWorker
DescriptionUse after free in ServiceWorker
ComponentServiceWorker
Sub-componentServiceWorker
Bug ClassUAF
Tracker499365667
Fix commit09a6b40648a2 (chromium/src) +167/-9
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
if
content/browser/service_worker/service_worker_client.cc
modified
GURL
content/browser/service_worker/service_worker_context_core.h
modified
ServiceWorkerFencedFrameProcessAllocationBrowserTest
content/browser/service_worker/service_worker_browsertest.cc
modified
TEST_F
content/browser/service_worker/service_worker_container_host_unittest.cc
modified
TEST_F
content/browser/service_worker/service_worker_context_unittest.cc
modified

Files Changed

  • content/browser/service_worker/service_worker_container_host.cc
  • content/browser/service_worker/service_worker_client.cc
  • content/browser/service_worker/service_worker_context_core.cc
  • content/browser/service_worker/service_worker_context_unittest.cc
  • content/browser/service_worker/service_worker_context_core.h
  • content/browser/service_worker/service_worker_client.h
  • content/browser/service_worker/service_worker_browsertest.cc
  • content/browser/service_worker/service_worker_container_host_unittest.cc
From 09a6b40648a206fb714d6e4c8372761c7866c477 Mon Sep 17 00:00:00 2001
From: Shunya Shishido <[email protected]>
Date: Thu, 17 Sep 2026 17:53:08 -0700
Subject: [PATCH] ServiceWorker: cache ancestor frame type at navigation commit

ServiceWorkerContextCore::RegisterServiceWorker derived the ancestor
frame type for a new registration by looking up the requesting
RenderFrameHost when the Register IPC was processed. The RenderFrameHost
can already be gone at that point because the container host outlives
it, so the lookup could silently fall back to kNormalFrame even for a
document that was nested inside a fenced frame.

Capture the ancestor frame type on ServiceWorkerClient at
CommitResponse() time, when the RenderFrameHost is still alive, and
forward the cached value through
ServiceWorkerContainerHostForClient::Register to RegisterServiceWorker
as an explicit argument. Browser-initiated registrations and existing
direct callers default to kNormalFrame.

TAG=agy
CONV=1726966b-c0ad-45d5-94b5-82a80621712d

Co-authored-by: Martin Verde <[email protected]>
Bug: 499365667
Change-Id: I1e90f23d42d9b634943fa376e07488db0d58bca8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8410929
Reviewed-by: Yoshisato Yanagisawa <[email protected]>
Commit-Queue: Shunya Shishido <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1700337}
---
diff --git a/content/browser/service_worker/service_worker_client.cc b/content/browser/service_worker/service_worker_client.cc
index 3bbfd77f..2f4a155 100644
--- a/content/browser/service_worker/service_worker_client.cc
+++ b/content/browser/service_worker/service_worker_client.cc
@@ -504,6 +504,14 @@
     auto* rfh = RenderFrameHostImpl::FromID(*rfh_id);
     // `rfh` may be null in tests (but it should not happen in production).
     if (rfh) {
+      // Cache the ancestor frame type (whether the frame is nested within a
+      // fenced frame) at commit time while the RenderFrameHost is still alive.
+      // This caches it on the ServiceWorkerClient, so it remains accessible
+      // even if the RenderFrameHost is later destroyed before a service
+      // worker registration IPC is processed.
+      if (rfh->IsNestedWithinFencedFrame()) {
+        ancestor_frame_type_ = blink::mojom::AncestorFrameType::kFencedFrame;
+      }
       rfh->AddServiceWorkerClient(client_uuid(),
                                   weak_ptr_factory_.GetWeakPtr());
       // A window client hosted by a privileged WebContents (see //chrome's
diff --git a/content/browser/service_worker/service_worker_client.h b/content/browser/service_worker/service_worker_client.h
index e2a0819..f5691ba 100644
--- a/content/browser/service_worker/service_worker_client.h
+++ b/content/browser/service_worker/service_worker_client.h
@@ -27,6 +27,7 @@
 #include "services/network/public/mojom/cross_origin_embedder_policy.mojom-forward.h"
 #include "services/network/public/mojom/document_isolation_policy.mojom-forward.h"
 #include "third_party/blink/public/common/storage_key/storage_key.h"
+#include "third_party/blink/public/mojom/service_worker/service_worker_ancestor_frame_type.mojom.h"
 #include "third_party/blink/public/mojom/service_worker/service_worker_client.mojom-forward.h"
 #include "third_party/blink/public/mojom/service_worker/service_worker_container.mojom.h"
 #include "third_party/blink/public/mojom/service_worker/service_worker_registration.mojom-forward.h"
@@ -288,6 +289,14 @@
   // for more details.
   GlobalRenderFrameHostId GetRenderFrameHostId() const;
 
+  blink::mojom::AncestorFrameType ancestor_frame_type() const {
+    return ancestor_frame_type_;
+  }
+  void SetAncestorFrameTypeForTesting(
+      blink::mojom::AncestorFrameType ancestor_frame_type) {
+    ancestor_frame_type_ = ancestor_frame_type;
+  }
+
   // For service worker clients. For window clients, this is not populated until
   // after navigation commit.
   ChildProcessId GetProcessId() const;
@@ -678,6 +687,11 @@
   // CommitResponse().
   FrameTreeNodeId ongoing_navigation_frame_tree_node_id_;
 
+  // Set in CommitResponse() so it remains available after the RenderFrameHost
+  // is destroyed.
+  blink::mojom::AncestorFrameType ancestor_frame_type_ =
+      blink::mojom::AncestorFrameType::kNormalFrame;
+
   // URLLoaderFactory used for navigation preload etc.
   // Only set/used for clients for prefetch.
   scoped_refptr<network::SharedURLLoaderFactory>
diff --git a/content/browser/service_worker/service_worker_container_host.cc b/content/browser/service_worker/service_worker_container_host.cc
index b347612..695f7a3 100644
--- a/content/browser/service_worker/service_worker_container_host.cc
+++ b/content/browser/service_worker/service_worker_container_host.cc
@@ -222,7 +222,8 @@
                      weak_ptr_factory_.GetWeakPtr(), GURL(script_url),
                      GURL(options->scope), std::move(wrapped_callback),
                      trace_id, mojo::GetBadMessageCallback()),
-      global_frame_id, policy_container_policies_);
+      global_frame_id, policy_container_policies_,
+      service_worker_client().ancestor_frame_type());
 }
 
 void ServiceWorkerContainerHostForClient::GetRegistration(
diff --git a/content/browser/service_worker/service_worker_context_core.cc b/content/browser/service_worker/service_worker_context_core.cc
index 82a53d4..b09031ed2 100644
--- a/content/browser/service_worker/service_worker_context_core.cc
+++ b/content/browser/service_worker/service_worker_context_core.cc
@@ -561,7 +561,8 @@
         outside_fetch_client_settings_object,
     RegistrationCallback callback,
     const GlobalRenderFrameHostId& requesting_frame_id,
-    const PolicyContainerPolicies& policy_container_policies) {
+    const PolicyContainerPolicies& policy_container_policies,
+    const blink::mojom::AncestorFrameType ancestor_frame_type) {
   CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M159);
   std::string error_message;
   if (!IsValidRegisterRequest(script_url, options.scope, key, &error_message)) {
@@ -571,12 +572,6 @@
     return;
   }
 
-  auto* render_frame_host = RenderFrameHostImpl::FromID(requesting_frame_id);
-  const blink::mojom::AncestorFrameType ancestor_frame_type =
-      render_frame_host && render_frame_host->IsNestedWithinFencedFrame()
-          ? blink::mojom::AncestorFrameType::kFencedFrame
-          : blink::mojom::AncestorFrameType::kNormalFrame;
-
   was_service_worker_registered_ = true;
   job_coordinator_->Register(
       script_url, options, key, std::move(outside_fetch_client_settings_object),
diff --git a/content/browser/service_worker/service_worker_context_core.h b/content/browser/service_worker/service_worker_context_core.h
index fc8eb0fc..8cc39483 100644
--- a/content/browser/service_worker/service_worker_context_core.h
+++ b/content/browser/service_worker/service_worker_context_core.h
@@ -36,6 +36,7 @@
 #include "mojo/public/cpp/bindings/associated_receiver_set.h"
 #include "mojo/public/cpp/bindings/receiver.h"
 #include "third_party/blink/public/common/storage_key/storage_key.h"
+#include "third_party/blink/public/mojom/service_worker/service_worker_ancestor_frame_type.mojom.h"
 #include "third_party/blink/public/mojom/service_worker/service_worker_registration.mojom-forward.h"
 
 class GURL;
@@ -424,7 +425,9 @@
           outside_fetch_client_settings_object,
       RegistrationCallback callback,
       const GlobalRenderFrameHostId& requesting_frame_id,
-      const PolicyContainerPolicies& policy_container_policies);
+      const PolicyContainerPolicies& policy_container_policies,
+      blink::mojom::AncestorFrameType ancestor_frame_type =
+          blink::mojom::AncestorFrameType::kNormalFrame);
 
   // If `is_immediate` is true, unregister clears the active worker from the
   // registration without waiting for the controlled clients to unload.
diff --git a/content/browser/service_worker/service_worker_browsertest.cc b/content/browser/service_worker/service_worker_browsertest.cc
index 426378eb..af23881b 100644
--- a/content/browser/service_worker/service_worker_browsertest.cc
+++ b/content/browser/service_worker/service_worker_browsertest.cc
@@ -4883,6 +4883,53 @@
             EvalJs(fenced_frame, "backgroundFetchFromServiceWorker()"));
 }
 
+// Tests that a service worker registered from a nested iframe within a fenced
+// frame is correctly classified with the kFencedFrame ancestor frame type.
+// This is verified by ensuring the service worker is barred from performing
+// background fetch, which is restricted in fenced frames.
+IN_PROC_BROWSER_TEST_F(ServiceWorkerFencedFrameBrowserTest,
+                       AncestorFrameTypeIsStoredForNestedFrameInFencedFrame) {
+  WorkerRunningStatusObserver observer(public_context());
+
+  // Navigate the main frame to a page that can register service workers.
+  EXPECT_TRUE(NavigateToURL(shell(),
+                            embedded_test_server()->GetURL(
+                                "/service_worker/create_service_worker.html")));
+  const GURL kFencedFrameUrl =
+      embedded_test_server()->GetURL("/service_worker/fenced_frame.html");
+
+  // Create a fenced frame nested inside the main frame.
+  RenderFrameHost* fenced_frame = fenced_frame_test_helper().CreateFencedFrame(
+      shell()->web_contents()->GetPrimaryMainFrame(), kFencedFrameUrl);
+
+  // Inside the fenced frame, programmatically create a nested iframe.
+  EXPECT_TRUE(ExecJs(fenced_frame,
+                     "const iframe = document.createElement('iframe');"
+                     "iframe.src = '/service_worker/fenced_frame.html';"
+                     "document.body.appendChild(iframe);"));
+  WaitForLoadStop(shell()->web_contents());
+
+  // Retrieve the RenderFrameHost of the newly created nested iframe and
+  // verify it is indeed recognized as being nested within a fenced frame.
+  RenderFrameHost* nested_iframe = ChildFrameAt(fenced_frame, 0);
+  ASSERT_TRUE(nested_iframe);
+  EXPECT_TRUE(nested_iframe->IsNestedWithinFencedFrame());
+
+  // Register the service worker from the nested iframe context.
+  EXPECT_EQ("ok - service worker registered",
+            EvalJs(nested_iframe, "RegisterServiceWorker()"));
+  observer.WaitUntilRunning();
+
+  // Verify that the service worker is blocked from calling background
+  // fetch, confirming that the registration stored the kFencedFrame ancestor
+  // frame type for this worker.
+  constexpr char kExpectedError[] =
+      "Failed to execute 'fetch' on 'BackgroundFetchManager': "
+      "backgroundFetch is not allowed in fenced frames.";
+  EXPECT_EQ(kExpectedError,
+            EvalJs(nested_iframe, "backgroundFetchFromServiceWorker()"));
+}
+
 class ServiceWorkerFencedFrameProcessAllocationBrowserTest
     : public ServiceWorkerFencedFrameBrowserTest,
       public testing::WithParamInterface<bool> {
diff --git a/content/browser/service_worker/service_worker_container_host_unittest.cc b/content/browser/service_worker/service_worker_container_host_unittest.cc
index 76add65..07722f30 100644
--- a/content/browser/service_worker/service_worker_container_host_unittest.cc
+++ b/content/browser/service_worker/service_worker_container_host_unittest.cc
@@ -49,6 +49,7 @@
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/blink/public/common/storage_key/storage_key.h"
 #include "third_party/blink/public/common/tokens/tokens.h"
+#include "third_party/blink/public/mojom/service_worker/service_worker_ancestor_frame_type.mojom.h"
 #include "third_party/blink/public/mojom/service_worker/service_worker_object.mojom.h"
 #include "third_party/blink/public/mojom/service_worker/service_worker_registration.mojom.h"
 #include "third_party/blink/public/mojom/service_worker/service_worker_registration_options.mojom.h"
@@ -703,6 +704,43 @@
   SetBrowserClientForTesting(old_browser_client);
 }
 
+// Tests that the container host's registration logic correctly forwards and
+// stores the ancestor frame type (specifically kFencedFrame) that was cached
+// on the ServiceWorkerClient.
+TEST_F(ServiceWorkerContainerHostTest, Register_AncestorFrameType) {
+  // Set up a ServiceWorkerClient and its associated container host for
+  // a mock window client.
+  CommittedServiceWorkerClient service_worker_client =
+      PrepareServiceWorkerContainerHost(GURL("https://www.example.com/foo"));
+
+  // Manually set the ancestor frame type of the client to kFencedFrame.
+  // This simulates the client having been determined to be nested within a
+  // fenced frame during navigation commit.
+  service_worker_client->SetAncestorFrameTypeForTesting(
+      blink::mojom::AncestorFrameType::kFencedFrame);
+
+  // Trigger registration of a service worker using the container host's
+  // Mojo interface. This exercises the path that forwards the cached ancestor
+  // frame type from the client to the context registration logic.
+  const GURL kScope("https://www.example.com/new_scope");
+  EXPECT_EQ(blink::mojom::ServiceWorkerErrorType::kNone,
+            Register(service_worker_client.host_remote().get(), kScope,
+                     GURL("https://www.example.com/bar")));
+
+  // Verify that the resulting service worker registration was correctly
+  // created and populated with the correct ancestor frame type (kFencedFrame).
+  scoped_refptr<ServiceWorkerRegistration> registration;
+  for (const auto& [id, reg] : context_->GetLiveRegistrations()) {
+    if (reg->scope() == kScope) {
+      registration = reg;
+      break;
+    }
+  }
+  ASSERT_TRUE(registration);
+  EXPECT_EQ(registration->ancestor_frame_type(),
+            blink::mojom::AncestorFrameType::kFencedFrame);
+}
+
 TEST_F(ServiceWorkerContainerHostTest, AllowServiceWorker) {
   // Create an active version.
   scoped_refptr<ServiceWorkerVersion> version =
diff --git a/content/browser/service_worker/service_worker_context_unittest.cc b/content/browser/service_worker/service_worker_context_unittest.cc
index d040d20..57145c1f 100644
--- a/content/browser/service_worker/service_worker_context_unittest.cc
+++ b/content/browser/service_worker/service_worker_context_unittest.cc
@@ -849,6 +849,58 @@
   EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorAbort, failure_status);
 }
 
+// Verification test for the case where a Service Worker registration request is
+// processed after the requesting frame has already been detached or destroyed.
+// This test ensures that the ancestor frame type (e.g., kFencedFrame) passed
+// from the client is preserved, instead of falling back to kNormalFrame due to
+// a failed frame lookup.
+TEST_F(ServiceWorkerContextTest, Register_AncestorFrameTypeWithDetachedFrame) {
+  GURL scope("https://www.example.com/");
+  const blink::StorageKey key =
+      blink::StorageKey::CreateFirstParty(url::Origin::Create(scope));
+  GURL script_url("https://www.example.com/service_worker.js");
+  blink::mojom::ServiceWorkerRegistrationOptions options;
+  options.scope = scope;
+
+  base::RunLoop run_loop;
+  bool called = false;
+  int64_t registration_id = blink::mojom::kInvalidServiceWorkerRegistrationId;
+
+  // Request Service Worker registration.
+  // Note: We pass a fake, non-existent `GlobalRenderFrameHostId`
+  // (child_id=1234, frame_routing_id=5678) as `requesting_frame_id`. Because
+  // this ID does not exist in the test's process or routing registry, any
+  // browser-side lookup using `RenderFrameHostImpl::FromID()` will return
+  // nullptr. This simulates the case where the frame was destroyed or detached
+  // before the Register IPC arrived at the browser process. We explicitly pass
+  // the ancestor frame type as `kFencedFrame`.
+  context()->RegisterServiceWorker(
+      script_url, key, options, CreateFetchClientSettingsObject(),
+      base::BindLambdaForTesting(
+          [&](blink::ServiceWorkerStatusCode result_status,
+              const std::string& /*status_message*/,
+              int64_t result_registration_id) {
+            EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk, result_status);
+            called = true;
+            registration_id = result_registration_id;
+            run_loop.Quit();
+          }),
+      /*requesting_frame_id=*/
+      GlobalRenderFrameHostId(/*child_id=*/1234, /*frame_routing_id=*/5678),
+      PolicyContainerPolicies(), blink::mojom::AncestorFrameType::kFencedFrame);
+  run_loop.Run();
+
+  // Verify the registration succeeded and that the stored ancestor
+  // frame type matches the passed `kFencedFrame` value, rather than having
+  // defaulted to `kNormalFrame` due to the failed frame lookup.
+  EXPECT_TRUE(called);
+  scoped_refptr<ServiceWorkerRegistration> registration =
+      context()->GetLiveRegistration(registration_id);
+  ASSERT_TRUE(registration);
+  EXPECT_EQ(blink::mojom::AncestorFrameType::kFencedFrame,
+            registration->ancestor_frame_type());
+}
+
 // Test registration when the service worker rejects the install event. The
 // registration callback should indicate success, but there should be no waiting
 // or active worker in the registration.
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/service_worker/service_worker_browsertest.cc b/content/browser/service_worker/service_worker_browsertest.cc
index 426378eb..af23881b 100644
--- a/content/browser/service_worker/service_worker_browsertest.cc
+++ b/content/browser/service_worker/service_worker_browsertest.cc
@@ -4883,6 +4883,53 @@
             EvalJs(fenced_frame, "backgroundFetchFromServiceWorker()"));
 }
 
+// Tests that a service worker registered from a nested iframe within a fenced
+// frame is correctly classified with the kFencedFrame ancestor frame type.
+// This is verified by ensuring the service worker is barred from performing
+// background fetch, which is restricted in fenced frames.
+IN_PROC_BROWSER_TEST_F(ServiceWorkerFencedFrameBrowserTest,
+                       AncestorFrameTypeIsStoredForNestedFrameInFencedFrame) {
+  WorkerRunningStatusObserver observer(public_context());
+
+  // Navigate the main frame to a page that can register service workers.
+  EXPECT_TRUE(NavigateToURL(shell(),
+                            embedded_test_server()->GetURL(
+                                "/service_worker/create_service_worker.html")));
+  const GURL kFencedFrameUrl =
+      embedded_test_server()->GetURL("/service_worker/fenced_frame.html");
+
+  // Create a fenced frame nested inside the main frame.
+  RenderFrameHost* fenced_frame = fenced_frame_test_helper().CreateFencedFrame(
+      shell()->web_contents()->GetPrimaryMainFrame(), kFencedFrameUrl);
+
+  // Inside the fenced frame, programmatically create a nested iframe.
+  EXPECT_TRUE(ExecJs(fenced_frame,
+                     "const iframe = document.createElement('iframe');"
+                     "iframe.src = '/service_worker/fenced_frame.html';"
+                     "document.body.appendChild(iframe);"));
+  WaitForLoadStop(shell()->web_contents());
+
+  // Retrieve the RenderFrameHost of the newly created nested iframe and
+  // verify it is indeed recognized as being nested within a fenced frame.
+  RenderFrameHost* nested_iframe = ChildFrameAt(fenced_frame, 0);
+  ASSERT_TRUE(nested_iframe);
+  EXPECT_TRUE(nested_iframe->IsNestedWithinFencedFrame());
+
+  // Register the service worker from the nested iframe context.
+  EXPECT_EQ("ok - service worker registered",
+            EvalJs(nested_iframe, "RegisterServiceWorker()"));
+  observer.WaitUntilRunning();
+
+  // Verify that the service worker is blocked from calling background
+  // fetch, confirming that the registration stored the kFencedFrame ancestor
+  // frame type for this worker.
+  constexpr char kExpectedError[] =
+      "Failed to execute 'fetch' on 'BackgroundFetchManager': "
+      "backgroundFetch is not allowed in fenced frames.";
+  EXPECT_EQ(kExpectedError,
+            EvalJs(nested_iframe, "backgroundFetchFromServiceWorker()"));
+}
+
 class ServiceWorkerFencedFrameProcessAllocationBrowserTest
     : public ServiceWorkerFencedFrameBrowserTest,
       public testing::WithParamInterface<bool> {
diff --git a/content/browser/service_worker/service_worker_container_host_unittest.cc b/content/browser/service_worker/service_worker_container_host_unittest.cc
index 76add65..07722f30 100644
--- a/content/browser/service_worker/service_worker_container_host_unittest.cc
+++ b/content/browser/service_worker/service_worker_container_host_unittest.cc
@@ -49,6 +49,7 @@
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/blink/public/common/storage_key/storage_key.h"
 #include "third_party/blink/public/common/tokens/tokens.h"
+#include "third_party/blink/public/mojom/service_worker/service_worker_ancestor_frame_type.mojom.h"
 #include "third_party/blink/public/mojom/service_worker/service_worker_object.mojom.h"
 #include "third_party/blink/public/mojom/service_worker/service_worker_registration.mojom.h"
 #include "third_party/blink/public/mojom/service_worker/service_worker_registration_options.mojom.h"
@@ -703,6 +704,43 @@
   SetBrowserClientForTesting(old_browser_client);
 }
 
+// Tests that the container host's registration logic correctly forwards and
+// stores the ancestor frame type (specifically kFencedFrame) that was cached
+// on the ServiceWorkerClient.
+TEST_F(ServiceWorkerContainerHostTest, Register_AncestorFrameType) {
+  // Set up a ServiceWorkerClient and its associated container host for
+  // a mock window client.
+  CommittedServiceWorkerClient service_worker_client =
+      PrepareServiceWorkerContainerHost(GURL("https://www.example.com/foo"));
+
+  // Manually set the ancestor frame type of the client to kFencedFrame.
+  // This simulates the client having been determined to be nested within a
+  // fenced frame during navigation commit.
+  service_worker_client->SetAncestorFrameTypeForTesting(
+      blink::mojom::AncestorFrameType::kFencedFrame);
+
+  // Trigger registration of a service worker using the container host's
+  // Mojo interface. This exercises the path that forwards the cached ancestor
+  // frame type from the client to the context registration logic.
+  const GURL kScope("https://www.example.com/new_scope");
+  EXPECT_EQ(blink::mojom::ServiceWorkerErrorType::kNone,
+            Register(service_worker_client.host_remote().get(), kScope,
+                     GURL("https://www.example.com/bar")));
+
+  // Verify that the resulting service worker registration was correctly
+  // created and populated with the correct ancestor frame type (kFencedFrame).
+  scoped_refptr<ServiceWorkerRegistration> registration;
+  for (const auto& [id, reg] : context_->GetLiveRegistrations()) {
+    if (reg->scope() == kScope) {
+      registration = reg;
+      break;
+    }
+  }
+  ASSERT_TRUE(registration);
+  EXPECT_EQ(registration->ancestor_frame_type(),
+            blink::mojom::AncestorFrameType::kFencedFrame);
+}
+
 TEST_F(ServiceWorkerContainerHostTest, AllowServiceWorker) {
   // Create an active version.
   scoped_refptr<ServiceWorkerVersion> version =
diff --git a/content/browser/service_worker/service_worker_context_unittest.cc b/content/browser/service_worker/service_worker_context_unittest.cc
index d040d20..57145c1f 100644
--- a/content/browser/service_worker/service_worker_context_unittest.cc
+++ b/content/browser/service_worker/service_worker_context_unittest.cc
@@ -849,6 +849,58 @@
   EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorAbort, failure_status);
 }
 
+// Verification test for the case where a Service Worker registration request is
+// processed after the requesting frame has already been detached or destroyed.
+// This test ensures that the ancestor frame type (e.g., kFencedFrame) passed
+// from the client is preserved, instead of falling back to kNormalFrame due to
+// a failed frame lookup.
+TEST_F(ServiceWorkerContextTest, Register_AncestorFrameTypeWithDetachedFrame) {
+  GURL scope("https://www.example.com/");
+  const blink::StorageKey key =
+      blink::StorageKey::CreateFirstParty(url::Origin::Create(scope));
+  GURL script_url("https://www.example.com/service_worker.js");
+  blink::mojom::ServiceWorkerRegistrationOptions options;
+  options.scope = scope;
+
+  base::RunLoop run_loop;
+  bool called = false;
+  int64_t registration_id = blink::mojom::kInvalidServiceWorkerRegistrationId;
+
+  // Request Service Worker registration.
+  // Note: We pass a fake, non-existent `GlobalRenderFrameHostId`
+  // (child_id=1234, frame_routing_id=5678) as `requesting_frame_id`. Because
+  // this ID does not exist in the test's process or routing registry, any
+  // browser-side lookup using `RenderFrameHostImpl::FromID()` will return
+  // nullptr. This simulates the case where the frame was destroyed or detached
+  // before the Register IPC arrived at the browser process. We explicitly pass
+  // the ancestor frame type as `kFencedFrame`.
+  context()->RegisterServiceWorker(
+      script_url, key, options, CreateFetchClientSettingsObject(),
+      base::BindLambdaForTesting(
+          [&](blink::ServiceWorkerStatusCode result_status,
+              const std::string& /*status_message*/,
+              int64_t result_registration_id) {
+            EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk, result_status);
+            called = true;
+            registration_id = result_registration_id;
+            run_loop.Quit();
+          }),
+      /*requesting_frame_id=*/
+      GlobalRenderFrameHostId(/*child_id=*/1234, /*frame_routing_id=*/5678),
+      PolicyContainerPolicies(), blink::mojom::AncestorFrameType::kFencedFrame);
+  run_loop.Run();
+
+  // Verify the registration succeeded and that the stored ancestor
+  // frame type matches the passed `kFencedFrame` value, rather than having
+  // defaulted to `kNormalFrame` due to the failed frame lookup.
+  EXPECT_TRUE(called);
+  scoped_refptr<ServiceWorkerRegistration> registration =
+      context()->GetLiveRegistration(registration_id);
+  ASSERT_TRUE(registration);
+  EXPECT_EQ(blink::mojom::AncestorFrameType::kFencedFrame,
+            registration->ancestor_frame_type());
+}
+
 // Test registration when the service worker rejects the install event. The
 // registration callback should indicate success, but there should be no waiting
 // or active worker in the registration.
Loading diff…

Original Bug Report

reported by [email protected]

Race Condition in ServiceWorker Registration Bypasses Fenced Frame Isolation

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 without the security team.

Overview: A race condition exists if a Fenced Frame is destroyed while processing a Service Worker registration IPC. The browser process fails to look up the detached frame, securely defaults to classifying the worker as a non-fenced kNormalFrame, and permanently bypasses Fenced Frame process isolation and restricted API checks.

Affected files:

  • content/browser/service_worker/service_worker_context_core.cc
  • content/browser/service_worker/service_worker_process_manager.cc

Estimated timestamp from git blame: 2022-06-12

Summary

A potential race condition in ServiceWorkerContextCore::RegisterServiceWorker allows a compromised renderer or an attacker-controlled Fenced Frame to register a Service Worker that is incorrectly classified as a normal (non-fenced) worker. By destroying the Fenced Frame via the top-level document immediately after calling navigator.serviceWorker.register(), an attacker can cause a browser-side frame lookup to fail. The browser then insecurely defaults to an unrestricted state (kNormalFrame), completely bypassing Fenced Frame Site Isolation and API restrictions.

Technical Details

When a Service Worker registration request is sent from a renderer, it is processed via the ServiceWorkerContainerHost Mojo interface. The registration reaches ServiceWorkerContextCore::RegisterServiceWorker, which attempts to securely classify the requester using its GlobalRenderFrameHostId:

// content/browser/service_worker/service_worker_context_core.cc
void ServiceWorkerContextCore::RegisterServiceWorker(
    ...,
    const GlobalRenderFrameHostId& requesting_frame_id,
    ...) {
  // ...
  auto* render_frame_host = RenderFrameHostImpl::FromID(requesting_frame_id);
  const blink::mojom::AncestorFrameType ancestor_frame_type =
      render_frame_host && render_frame_host->IsNestedWithinFencedFrame()
          ? blink::mojom::AncestorFrameType::kFencedFrame
          : blink::mojom::AncestorFrameType::kNormalFrame;
  // ...
}

The Service Worker Register IPC and the FrameHost::Detach IPC (sent when a frame is removed) travel over different Mojo pipes. If an attacker times the removal of a Fenced Frame perfectly, the frame is destroyed just before the Register IPC is processed.

Because the receiver’s lifetime is tied to the Mojo pipe and not synchronously to the RenderFrameHostImpl destructor, the Register IPC is still processed. However, RenderFrameHostImpl::FromID(requesting_frame_id) returns nullptr. The ternary operator then silently falls back to blink::mojom::AncestorFrameType::kNormalFrame.

This incorrect, elevated status is permanently saved into the Service Worker registration database. When the worker is subsequently started, ServiceWorkerProcessManager::AllocateWorkerProcess reads this status:

// content/browser/service_worker/service_worker_process_manager.cc
  const bool is_fenced =
      ancestor_frame_type == blink::mojom::AncestorFrameType::kFencedFrame &&
      SiteIsolationPolicy::IsProcessIsolationForFencedFramesEnabled();

Because ancestor_frame_type is kNormalFrame, is_fenced evaluates to false. The Service Worker is allocated in a standard renderer process, bypassing Fenced Frame process isolation. Furthermore, API restrictions (like the Push API in PushMessagingManager::Subscribe) rely on this status and will incorrectly permit restricted operations, allowing exfiltration of partitioned data.

Suggested Reproduction Steps

Note: These are potential steps as we currently lack an active testing environment to run a Proof of Concept.

  1. Host a page that embeds a cross-site Fenced Frame.
  2. Inside the Fenced Frame, execute navigator.serviceWorker.register('/sw.js').
  3. Immediately after the call, have the top-level parent page remove the Fenced Frame from the DOM (fenced_frame.remove()).
  4. If the race condition is won (the Detach IPC beats the Register IPC processing), the Service Worker is saved as a kNormalFrame worker.
  5. Verify that the resulting Service Worker starts in a non-isolated process and can successfully subscribe to the Push API or use Background Sync.

Proposed Fix

Do not default to kNormalFrame upon a frame lookup failure. If the registration originates from a window client and render_frame_host is nullptr (due to the frame being destroyed or detached mid-flight), the registration request should either be aborted, or the ancestor_frame_type should be reliably cached earlier in the ServiceWorkerClient’s lifetime (e.g., during CommitResponse) and passed explicitly to RegisterServiceWorker.

Evaluated with Chrome root at commit: 09ec9e7cc4d24823d20b6d37cf3d282734f6bf0f


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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