Critical patch-now ~ plausible CVSS 9.6 chrome UAF 🔧 Commit mapped 🧬 A nested callout frees the object, then it is used

Overview

Critical
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in ServiceWorker
DescriptionUse after free in ServiceWorker
ComponentServiceWorker
Bug ClassUAF
Action tierpatch-now — corruption primitive, web-reachable in default config (plausible) — upheld by independent review by AI
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.
Independent review by AIPlausible
Reviewer notes: Diff is a multi-site re-entrancy cleanup; it does not show which site actually caused the advisory's UAF, yet the analysis states every site freed memory as fact. Omits the UnregisterJob AddCallback-after-resolution change (late callbacks previously never ran).
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker548585299
Fix commit869b676e94ba (chromium/src) +213/-39
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedAndrew Boni
Reported2026-08-18 (per vendor advisory)
Fix landed2026-08-24 (public git)
Disclosed2026-09-22
Patch gap29 days — fix public in source before the advisory

Background

ServiceWorker
a browser-process-managed background script whose lifecycle (register, start, stop, skipWaiting, status changes) is driven by web-reachable page APIs and dispatched through callbacks in content/browser/service_worker/.
Re-entrant dispatch
invoking a callback while still iterating a member container, where the callback synchronously calls back into the same object and mutates that container.
UAF (use-after-free)
dereferencing memory (here, container entries or iterators) after it has been freed or invalidated by a nested mutation.

Root Cause Analysis

Verify before building a PoC

This analysis is consistent with the fixing commit but not fully provable from it: part of the vulnerable path lives in code the fix commit does not touch. Confirm the trigger and the sink against the source before relying on it for exploit development.
What the commit does not establish: Diff is a multi-site re-entrancy cleanup; it does not show which site actually caused the advisory's UAF, yet the analysis states every site freed memory as fact. Omits the UnregisterJob AddCallback-after-resolution change (late callbacks previously never ran).

Multiple ServiceWorker dispatch sites ran queued callbacks by iterating directly over a member container (e.g. for (auto& request : pending_cache_storage_requests_) or looping inflight_requests_) while the container was still live on the object. Because ServiceWorker lifecycle events are web-reachable, a callback could synchronously trigger a nested/re-entrant path — a status change, stop, or promise resolution — that mutates or clears the very container being iterated. That mutation invalidates the active iterator and frees the entries it points at, so continued iteration dereferences freed memory. In ServiceWorkerContextCore::AddWarmUpRequest the front callback was even run before pop_front(), so a re-entrant enqueue/dequeue could corrupt the deque around the in-flight element. The same anti-pattern appeared in SetStatus (pending_skip_waiting_requests_), OnStoppedInternal/FinishStartWorker (error_callbacks/inflight_requests_), and the register/unregister job promise resolvers.

The result is a browser-process use-after-free reachable from ordinary page script.

Key insight
The bug is not a single site but a repeated pattern: invoking a callback that can re-enter the same object while its callback container is still the live, iterated member. Snapshotting to a local copy (or popping the entry before invoking) severs the aliasing so nested mutation can no longer invalidate the in-progress traversal.

Attack Path

  1. Register a worker Page script registers/activates a ServiceWorker and drives it toward a lifecycle transition (start, stop, or skipWaiting).
  2. Fill the queue The page issues operations that enqueue multiple callbacks into a container like pending_cache_storage_requests_ or inflight_requests_.
  3. Trigger dispatch A web-reachable event causes the browser to iterate that container and run the queued callbacks.
  4. Re-enter and mutate One callback synchronously re-enters the same ServiceWorkerVersion/EmbeddedWorkerInstance, mutating or clearing the container mid-iteration and freeing entries.
  5. Use freed memory Continued iteration dereferences the invalidated iterator/freed entry, yielding a controllable browser-process UAF.

Impact Assessment

This is a use-after-free in the privileged browser process, reachable from unprivileged web content because ServiceWorker lifecycle transitions are page-controllable. Exploitation can corrupt browser-process heap state, enabling potential remote code execution or sandbox escape from a compromised or malicious renderer. The web-reachability and lifecycle timing give an attacker practical control over when the re-entrant free occurs.

Changed Functions

FunctionChangeNotes
for
content/browser/service_worker/embedded_worker_instance.cc
modified
if
content/browser/service_worker/embedded_worker_instance.cc
modified
if
content/browser/service_worker/service_worker_unregister_job.cc
modified
for
content/browser/service_worker/service_worker_version.cc
modified
TEST_P
content/browser/service_worker/service_worker_job_unittest.cc
modified

Files Changed

  • content/browser/service_worker/service_worker_version.h
  • content/browser/service_worker/service_worker_register_job.cc
  • content/browser/service_worker/service_worker_unregister_job.h
  • content/browser/service_worker/service_worker_context_core.cc
  • content/browser/service_worker/service_worker_job_unittest.cc
  • content/browser/service_worker/service_worker_unregister_job.cc
  • content/browser/service_worker/service_worker_version.cc
  • content/browser/service_worker/embedded_worker_instance.cc
  • content/browser/service_worker/service_worker_version_unittest.cc

Audit Directions

  • Iterate-then-invoke on members
    Flag any for (auto& x : member_container_) { std::move(x.cb).Run(); } where the callback can re-enter the owning object; require a local swap/copy or pop-before-invoke.
  • Run-before-pop ordering
    Audit queue processors that .Run() a front/queued callback before removing it; the entry must be detached from the live container first.
  • Web-reachable re-entrancy
    For ServiceWorker lifecycle handlers (SetStatus, stop/finish-start, promise resolution), trace whether a dispatched callback can synchronously mutate the same container, and verify each dispatch site snapshots its callbacks.
From 869b676e94ba96b3e4bc0c272aeafe6c321328d2 Mon Sep 17 00:00:00 2001
From: Yoshisto Yanagisawa <[email protected]>
Date: Mon, 24 Aug 2026 00:27:13 -0700
Subject: [PATCH] Clean up callback handling and queue processing in ServiceWorker

This CL updates callback and queue handling across
content/browser/service_worker:

1. ServiceWorkerVersion::SetStatus:
   - Use a local vector when running pending_skip_waiting_requests_.
2. ServiceWorkerVersion::OnStoppedInternal / FinishStartWorker:
   - Extract error_callbacks from inflight_requests_ and clear the map
     before running callbacks.
   - Extend is_running_start_callbacks_ to cover warm_up_callbacks_
     dispatch.
3. ServiceWorkerRegisterJob::ResolvePromise:
   - Use a local vector when running callbacks.
4. ServiceWorkerUnregisterJob:
   - Post a task if AddCallback is called after promise resolution.
   - Use a local vector when running callbacks in ResolvePromise.
5. ServiceWorkerContextCore::AddWarmUpRequest:
   - Pop front before invoking callback to maintain queue consistency.
6. EmbeddedWorkerInstance::BindCacheStorageInternal:
   - Use a local vector and drain loop when processing pending cache
     storage requests.
7. Unit Tests:
   - Add unit tests for nested callback dispatches in ServiceWorkerVersionTest
     and ServiceWorkerJobTest.

TAG=agy
CONV=e4a12cd9-683b-4158-8a4e-43915800ca0a

Bug: 548585299
Change-Id: I77beadcc99c44798de8f94760678ba26c5952dee
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8278110
Reviewed-by: Shunya Shishido <[email protected]>
Commit-Queue: Yoshisato Yanagisawa <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1684613}
---
diff --git a/content/browser/service_worker/embedded_worker_instance.cc b/content/browser/service_worker/embedded_worker_instance.cc
index 95051090..d754be1 100644
--- a/content/browser/service_worker/embedded_worker_instance.cc
+++ b/content/browser/service_worker/embedded_worker_instance.cc
@@ -1162,30 +1162,34 @@
     return;
   }
 
-  for (auto& request : pending_cache_storage_requests_) {
-    mojo::PendingRemote<network::mojom::CrossOriginEmbedderPolicyReporter>
-        coep_reporter_remote;
-    if (coep_reporter_) {
-      coep_reporter_->Clone(
-          coep_reporter_remote.InitWithNewPipeAndPassReceiver());
+  while (!pending_cache_storage_requests_.empty()) {
+    std::vector<CacheStorageRequest> requests;
+    requests.swap(pending_cache_storage_requests_);
+    for (auto& request : requests) {
+      mojo::PendingRemote<network::mojom::CrossOriginEmbedderPolicyReporter>
+          coep_reporter_remote;
+      if (coep_reporter_) {
+        coep_reporter_->Clone(
+            coep_reporter_remote.InitWithNewPipeAndPassReceiver());
+      }
+
+      mojo::PendingRemote<network::mojom::DocumentIsolationPolicyReporter>
+          dip_reporter_remote;
+      if (dip_reporter_) {
+        dip_reporter_->Clone(
+            dip_reporter_remote.InitWithNewPipeAndPassReceiver());
+      }
+
+      auto* rph = RenderProcessHost::FromID(process_id());
+      if (!rph) {
+        return;
+      }
+
+      rph->BindCacheStorage(*coep, std::move(coep_reporter_remote), *dip,
+                            std::move(dip_reporter_remote), request.bucket,
+                            std::move(request.receiver));
     }
-
-    mojo::PendingRemote<network::mojom::DocumentIsolationPolicyReporter>
-        dip_reporter_remote;
-    if (dip_reporter_) {
-      dip_reporter_->Clone(
-          dip_reporter_remote.InitWithNewPipeAndPassReceiver());
-    }
-
-    auto* rph = RenderProcessHost::FromID(process_id());
-    if (!rph)
-      return;
-
-    rph->BindCacheStorage(*coep, std::move(coep_reporter_remote), *dip,
-                          std::move(dip_reporter_remote), request.bucket,
-                          std::move(request.receiver));
   }
-  pending_cache_storage_requests_.clear();
 }
 
 mojo::PendingRemote<network::mojom::CrossOriginEmbedderPolicyReporter>
diff --git a/content/browser/service_worker/service_worker_context_core.cc b/content/browser/service_worker/service_worker_context_core.cc
index 84411bac..93c132e 100644
--- a/content/browser/service_worker/service_worker_context_core.cc
+++ b/content/browser/service_worker/service_worker_context_core.cc
@@ -758,8 +758,8 @@
   while (warm_up_requests_.size() > kRequestQueueLength) {
     auto [front_url, front_key, front_callback] =
         std::move(warm_up_requests_.front());
-    std::move(front_callback).Run();
     warm_up_requests_.pop_front();
+    std::move(front_callback).Run();
   }
 }
 
diff --git a/content/browser/service_worker/service_worker_register_job.cc b/content/browser/service_worker/service_worker_register_job.cc
index fb0ec557..613da18e 100644
--- a/content/browser/service_worker/service_worker_register_job.cc
+++ b/content/browser/service_worker/service_worker_register_job.cc
@@ -979,11 +979,12 @@
 
   is_promise_resolved_ = true;
   promise_resolved_status_ = status;
-  promise_resolved_status_message_ = status_message,
+  promise_resolved_status_message_ = status_message;
   promise_resolved_registration_ = registration;
-  for (RegistrationCallback& callback : callbacks_)
+  std::vector<RegistrationCallback> callbacks;
+  callbacks.swap(callbacks_);
+  for (RegistrationCallback& callback : callbacks)
     std::move(callback).Run(status, status_message, registration);
-  callbacks_.clear();
 }
 
 void ServiceWorkerRegisterJob::AddRegistrationToMatchingContainerHosts(
diff --git a/content/browser/service_worker/service_worker_unregister_job.cc b/content/browser/service_worker/service_worker_unregister_job.cc
index 726363e..6ad16de 100644
--- a/content/browser/service_worker/service_worker_unregister_job.cc
+++ b/content/browser/service_worker/service_worker_unregister_job.cc
@@ -8,6 +8,7 @@
 
 #include "base/functional/bind.h"
 #include "base/memory/weak_ptr.h"
+#include "base/task/single_thread_task_runner.h"
 #include "content/browser/service_worker/service_worker_context_core.h"
 #include "content/browser/service_worker/service_worker_job_coordinator.h"
 #include "content/browser/service_worker/service_worker_registration.h"
@@ -35,7 +36,14 @@
 ServiceWorkerUnregisterJob::~ServiceWorkerUnregisterJob() = default;
 
 void ServiceWorkerUnregisterJob::AddCallback(UnregistrationCallback callback) {
-  callbacks_.emplace_back(std::move(callback));
+  if (!is_promise_resolved_) {
+    callbacks_.emplace_back(std::move(callback));
+    return;
+  }
+  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+      FROM_HERE,
+      base::BindOnce(std::move(callback), promise_resolved_registration_id_,
+                     promise_resolved_status_));
 }
 
 void ServiceWorkerUnregisterJob::Start() {
@@ -108,9 +116,12 @@
     blink::ServiceWorkerStatusCode status) {
   DCHECK(!is_promise_resolved_);
   is_promise_resolved_ = true;
-  for (UnregistrationCallback& callback : callbacks_)
+  promise_resolved_registration_id_ = registration_id;
+  promise_resolved_status_ = status;
+  std::vector<UnregistrationCallback> callbacks;
+  callbacks.swap(callbacks_);
+  for (UnregistrationCallback& callback : callbacks)
     std::move(callback).Run(registration_id, status);
-  callbacks_.clear();
 }
 
 }  // namespace content
diff --git a/content/browser/service_worker/service_worker_unregister_job.h b/content/browser/service_worker/service_worker_unregister_job.h
index ec2bbd1..e9de285 100644
--- a/content/browser/service_worker/service_worker_unregister_job.h
+++ b/content/browser/service_worker/service_worker_unregister_job.h
@@ -77,6 +77,10 @@
   const ServiceWorkerRegistration::DeleteInitiator initiator_;
   std::vector<UnregistrationCallback> callbacks_;
   bool is_promise_resolved_ = false;
+  int64_t promise_resolved_registration_id_ =
+      blink::mojom::kInvalidServiceWorkerRegistrationId;
+  blink::ServiceWorkerStatusCode promise_resolved_status_ =
+      blink::ServiceWorkerStatusCode::kOk;
   base::WeakPtrFactory<ServiceWorkerUnregisterJob> weak_factory_{this};
 };
 }  // namespace content
diff --git a/content/browser/service_worker/service_worker_version.cc b/content/browser/service_worker/service_worker_version.cc
index 397a23dd..ef0bdca 100644
--- a/content/browser/service_worker/service_worker_version.cc
+++ b/content/browser/service_worker/service_worker_version.cc
@@ -405,21 +405,25 @@
       case ACTIVATING:
         // Do nothing until ACTIVATED time.
         break;
-      case ACTIVATED:
+      case ACTIVATED: {
         // Resolve skip waiting promises.
         ClearTick(&skip_waiting_time_);
-        for (SkipWaitingCallback& callback : pending_skip_waiting_requests_) {
+        std::vector<SkipWaitingCallback> callbacks;
+        callbacks.swap(pending_skip_waiting_requests_);
+        for (SkipWaitingCallback& callback : callbacks) {
           std::move(callback).Run(true);
         }
-        pending_skip_waiting_requests_.clear();
         break;
-      case REDUNDANT:
+      }
+      case REDUNDANT: {
         // Fail any pending skip waiting requests since this version is dead.
-        for (SkipWaitingCallback& callback : pending_skip_waiting_requests_) {
+        std::vector<SkipWaitingCallback> callbacks;
+        callbacks.swap(pending_skip_waiting_requests_);
+        for (SkipWaitingCallback& callback : callbacks) {
           std::move(callback).Run(false);
         }
-        pending_skip_waiting_requests_.clear();
         break;
+      }
     }
   }
 
@@ -3296,6 +3300,7 @@
   // Let all message callbacks fail (this will also fire and clear all
   // callbacks for events).
   // TODO(kinuko): Consider if we want to add queue+resend mechanism here.
+  std::vector<StatusCallback> error_callbacks;
   base::IDMap<std::unique_ptr<InflightRequest>>::iterator iter(
       &inflight_requests_);
   while (!iter.IsAtEnd()) {
@@ -3305,14 +3310,17 @@
         perfetto::TerminatingFlow::FromPointer(iter.GetCurrentValue(),
                                                "ServiceWorkerVersion::Request"),
         "Error", "Worker Stopped");
-    std::move(iter.GetCurrentValue()->error_callback)
-        .Run(blink::ServiceWorkerStatusCode::kErrorFailed);
+    error_callbacks.push_back(
+        std::move(iter.GetCurrentValue()->error_callback));
     iter.Advance();
   }
   inflight_requests_.Clear();
   request_timeouts_.clear();
   external_request_uuid_to_request_id_.clear();
   latest_external_keepalive_sequence_number_ = 0;
+  for (auto& callback : error_callbacks) {
+    std::move(callback).Run(blink::ServiceWorkerStatusCode::kErrorFailed);
+  }
   service_worker_remote_.reset();
   is_endpoint_ready_ = false;
   remote_controller_.reset();
@@ -3340,19 +3348,19 @@
 
 void ServiceWorkerVersion::FinishStartWorker(
     blink::ServiceWorkerStatusCode status) {
+  is_running_start_callbacks_ = true;
   std::vector<StatusCallback> callbacks;
   callbacks.swap(start_callbacks_);
-  is_running_start_callbacks_ = true;
   for (auto& callback : callbacks) {
     std::move(callback).Run(status);
   }
-  is_running_start_callbacks_ = false;
 
   std::vector<StatusCallback> warm_up_callbacks;
   warm_up_callbacks.swap(warm_up_callbacks_);
   for (auto& callback : warm_up_callbacks) {
     std::move(callback).Run(status);
   }
+  is_running_start_callbacks_ = false;
 }
 
 void ServiceWorkerVersion::CleanUpExternalRequest(
diff --git a/content/browser/service_worker/service_worker_version.h b/content/browser/service_worker/service_worker_version.h
index 8eb9a7a6..eb484801 100644
--- a/content/browser/service_worker/service_worker_version.h
+++ b/content/browser/service_worker/service_worker_version.h
@@ -116,6 +116,7 @@
 FORWARD_DECLARE_TEST(ServiceWorkerVersionTest,
                      DefaultTimeoutRequestDoesNotAffectMaxTimeoutRequest);
 FORWARD_DECLARE_TEST(ServiceWorkerVersionTest, Doom);
+FORWARD_DECLARE_TEST(ServiceWorkerVersionTest, SetStatus_ReentrantSkipWaiting);
 }  // namespace service_worker_version_unittest
 
 FORWARD_DECLARE_TEST(ServiceWorkerRegistryTest, ScriptResponseTime);
@@ -910,6 +911,9 @@
   FRIEND_TEST_ALL_PREFIXES(
       service_worker_version_unittest::ServiceWorkerVersionTest,
       Doom);
+  FRIEND_TEST_ALL_PREFIXES(
+      service_worker_version_unittest::ServiceWorkerVersionTest,
+      SetStatus_ReentrantSkipWaiting);
   FRIEND_TEST_ALL_PREFIXES(ServiceWorkerRegistryTest, ScriptResponseTime);
   FRIEND_TEST_ALL_PREFIXES(ServiceWorkerBrowserTest,
                            WarmUpAndStartServiceWorker);
diff --git a/content/browser/service_worker/service_worker_job_unittest.cc b/content/browser/service_worker/service_worker_job_unittest.cc
index f7d7459..406b1c3 100644
--- a/content/browser/service_worker/service_worker_job_unittest.cc
+++ b/content/browser/service_worker/service_worker_job_unittest.cc
@@ -2688,4 +2688,94 @@
   observer.RunUntilStatusChange(version.get(), ServiceWorkerVersion::ACTIVATED);
 }
 
+TEST_P(ServiceWorkerJobTest, Unregister_ReentrantUnregister) {
+  GURL scope("https://www.example.com/");
+  const blink::StorageKey key = GetTestStorageKey(scope);
+  blink::mojom::ServiceWorkerRegistrationOptions options;
+  options.scope = scope;
+
+  scoped_refptr<ServiceWorkerRegistration> registration =
+      RunRegisterJob(GURL("https://www.example.com/service_worker.js"), key,
+                     options);
+  ASSERT_TRUE(registration);
+
+  base::RunLoop run_loop;
+  bool callback1_called = false;
+  bool callback2_called = false;
+
+  job_coordinator()->Unregister(
+      scope, key, /*is_immediate=*/false,
+      ServiceWorkerRegistration::DeleteInitiator::kTest,
+      base::BindLambdaForTesting(
+          [&](int64_t registration_id, blink::ServiceWorkerStatusCode status) {
+            EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk, status);
+            callback1_called = true;
+            // Call Unregister from within the completion callback.
+            job_coordinator()->Unregister(
+                scope, key, /*is_immediate=*/false,
+                ServiceWorkerRegistration::DeleteInitiator::kTest,
+                base::BindLambdaForTesting(
+                    [&](int64_t id2, blink::ServiceWorkerStatusCode status2) {
+                      EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk, status2);
+                      callback2_called = true;
+                      run_loop.Quit();
+                    }));
+          }));
+
+  run_loop.Run();
+  EXPECT_TRUE(callback1_called);
+  EXPECT_TRUE(callback2_called);
+}
+
+TEST_P(ServiceWorkerJobTest, Register_ReentrantRegister) {
+  GURL scope("https://www.example.com/");
+  const blink::StorageKey key = GetTestStorageKey(scope);
+  blink::mojom::ServiceWorkerRegistrationOptions options;
+  options.scope = scope;
+
+  base::RunLoop run_loop;
+  bool callback1_called = false;
+  bool callback2_called = false;
+
+  scoped_refptr<ServiceWorkerRegistration> registration1;
+  scoped_refptr<ServiceWorkerRegistration> registration2;
+
+  job_coordinator()->Register(
+      GURL("https://www.example.com/service_worker.js"), options, key,
+      CreateFetchClientSettingsObject(),
+      /*requesting_frame_id=*/GlobalRenderFrameHostId(),
+      /*ancestor_frame_type=*/blink::mojom::AncestorFrameType::kNormalFrame,
+      base::BindLambdaForTesting(
+          [&](blink::ServiceWorkerStatusCode status,
+              const std::string& status_message,
+              ServiceWorkerRegistration* registration) {
+            EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk, status);
+            callback1_called = true;
+            registration1 = registration;
+
+            // Trigger another Register job from within the completion callback.
+            job_coordinator()->Register(
+                GURL("https://www.example.com/service_worker.js"), options, key,
+                CreateFetchClientSettingsObject(),
+                /*requesting_frame_id=*/GlobalRenderFrameHostId(),
+                /*ancestor_frame_type=*/
+                blink::mojom::AncestorFrameType::kNormalFrame,
+                base::BindLambdaForTesting(
+                    [&](blink::ServiceWorkerStatusCode status2,
+                        const std::string& status_message2,
+                        ServiceWorkerRegistration* reg2) {
+                      EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk, status2);
+                      callback2_called = true;
+                      registration2 = reg2;
+                      run_loop.Quit();
+                    }),
+                PolicyContainerPolicies());
+          }),
+      PolicyContainerPolicies());
+
+  run_loop.Run();
+  EXPECT_TRUE(callback1_called);
+  EXPECT_TRUE(callback2_called);
+}
+
 }  // namespace content
diff --git a/content/browser/service_worker/service_worker_version_unittest.cc b/content/browser/service_worker/service_worker_version_unittest.cc
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/service_worker/service_worker_job_unittest.cc b/content/browser/service_worker/service_worker_job_unittest.cc
index f7d7459..406b1c3 100644
--- a/content/browser/service_worker/service_worker_job_unittest.cc
+++ b/content/browser/service_worker/service_worker_job_unittest.cc
@@ -2688,4 +2688,94 @@
   observer.RunUntilStatusChange(version.get(), ServiceWorkerVersion::ACTIVATED);
 }
 
+TEST_P(ServiceWorkerJobTest, Unregister_ReentrantUnregister) {
+  GURL scope("https://www.example.com/");
+  const blink::StorageKey key = GetTestStorageKey(scope);
+  blink::mojom::ServiceWorkerRegistrationOptions options;
+  options.scope = scope;
+
+  scoped_refptr<ServiceWorkerRegistration> registration =
+      RunRegisterJob(GURL("https://www.example.com/service_worker.js"), key,
+                     options);
+  ASSERT_TRUE(registration);
+
+  base::RunLoop run_loop;
+  bool callback1_called = false;
+  bool callback2_called = false;
+
+  job_coordinator()->Unregister(
+      scope, key, /*is_immediate=*/false,
+      ServiceWorkerRegistration::DeleteInitiator::kTest,
+      base::BindLambdaForTesting(
+          [&](int64_t registration_id, blink::ServiceWorkerStatusCode status) {
+            EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk, status);
+            callback1_called = true;
+            // Call Unregister from within the completion callback.
+            job_coordinator()->Unregister(
+                scope, key, /*is_immediate=*/false,
+                ServiceWorkerRegistration::DeleteInitiator::kTest,
+                base::BindLambdaForTesting(
+                    [&](int64_t id2, blink::ServiceWorkerStatusCode status2) {
+                      EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk, status2);
+                      callback2_called = true;
+                      run_loop.Quit();
+                    }));
+          }));
+
+  run_loop.Run();
+  EXPECT_TRUE(callback1_called);
+  EXPECT_TRUE(callback2_called);
+}
+
+TEST_P(ServiceWorkerJobTest, Register_ReentrantRegister) {
+  GURL scope("https://www.example.com/");
+  const blink::StorageKey key = GetTestStorageKey(scope);
+  blink::mojom::ServiceWorkerRegistrationOptions options;
+  options.scope = scope;
+
+  base::RunLoop run_loop;
+  bool callback1_called = false;
+  bool callback2_called = false;
+
+  scoped_refptr<ServiceWorkerRegistration> registration1;
+  scoped_refptr<ServiceWorkerRegistration> registration2;
+
+  job_coordinator()->Register(
+      GURL("https://www.example.com/service_worker.js"), options, key,
+      CreateFetchClientSettingsObject(),
+      /*requesting_frame_id=*/GlobalRenderFrameHostId(),
+      /*ancestor_frame_type=*/blink::mojom::AncestorFrameType::kNormalFrame,
+      base::BindLambdaForTesting(
+          [&](blink::ServiceWorkerStatusCode status,
+              const std::string& status_message,
+              ServiceWorkerRegistration* registration) {
+            EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk, status);
+            callback1_called = true;
+            registration1 = registration;
+
+            // Trigger another Register job from within the completion callback.
+            job_coordinator()->Register(
+                GURL("https://www.example.com/service_worker.js"), options, key,
+                CreateFetchClientSettingsObject(),
+                /*requesting_frame_id=*/GlobalRenderFrameHostId(),
+                /*ancestor_frame_type=*/
+                blink::mojom::AncestorFrameType::kNormalFrame,
+                base::BindLambdaForTesting(
+                    [&](blink::ServiceWorkerStatusCode status2,
+                        const std::string& status_message2,
+                        ServiceWorkerRegistration* reg2) {
+                      EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk, status2);
+                      callback2_called = true;
+                      registration2 = reg2;
+                      run_loop.Quit();
+                    }),
+                PolicyContainerPolicies());
+          }),
+      PolicyContainerPolicies());
+
+  run_loop.Run();
+  EXPECT_TRUE(callback1_called);
+  EXPECT_TRUE(callback2_called);
+}
+
 }  // namespace content
diff --git a/content/browser/service_worker/service_worker_version_unittest.cc b/content/browser/service_worker/service_worker_version_unittest.cc
index 424a21e..32d3601 100644
--- a/content/browser/service_worker/service_worker_version_unittest.cc
+++ b/content/browser/service_worker/service_worker_version_unittest.cc
@@ -2982,5 +2982,57 @@
       bad_message_observer.WaitForBadMessage());
 }
 
+TEST_P(ServiceWorkerVersionTest, SetStatus_ReentrantSkipWaiting) {
+  registration_->SetWaitingVersion(version_);
+  version_->SetStatus(ServiceWorkerVersion::INSTALLED);
+  bool callback1_called = false;
+  bool callback2_called = false;
+
+  version_->SkipWaiting(base::BindLambdaForTesting([&](bool success) {
+    EXPECT_TRUE(success);
+    callback1_called = true;
+    // Call SkipWaiting from inside the callback.
+    version_->SkipWaiting(base::BindLambdaForTesting([&](bool success2) {
+      EXPECT_TRUE(success2);
+      callback2_called = true;
+    }));
+  }));
+
+  // Transitioning to ACTIVATED resolves skip waiting promises.
+  version_->SetStatus(ServiceWorkerVersion::ACTIVATED);
+  EXPECT_TRUE(callback1_called);
+  EXPECT_TRUE(callback2_called);
+}
+
+TEST_P(ServiceWorkerVersionTest, OnStopped_MultipleInflightRequests) {
+  ASSERT_EQ(blink::ServiceWorkerStatusCode::kOk,
+            StartServiceWorker(version_.get()));
+  version_->SetStatus(ServiceWorkerVersion::ACTIVATED);
+
+  bool error_callback1_called = false;
+  bool error_callback2_called = false;
+  base::RunLoop run_loop;
+
+  version_->StartRequest(
+      ServiceWorkerMetrics::EventType::FETCH_MAIN_FRAME,
+      base::BindLambdaForTesting([&](blink::ServiceWorkerStatusCode status) {
+        EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorFailed, status);
+        error_callback1_called = true;
+      }));
+
+  version_->StartRequest(
+      ServiceWorkerMetrics::EventType::SYNC,
+      base::BindLambdaForTesting([&](blink::ServiceWorkerStatusCode status) {
+        EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorFailed, status);
+        error_callback2_called = true;
+      }));
+
+  version_->StopWorker(run_loop.QuitClosure());
+  run_loop.Run();
+
+  EXPECT_TRUE(error_callback1_called);
+  EXPECT_TRUE(error_callback2_called);
+}
+
 }  // namespace service_worker_version_unittest
 }  // namespace content
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.