CVE-2026-95339
Overview
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 incontent/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
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.
Attack Path
- Register a worker
Page script registers/activates a ServiceWorker and drives it toward a lifecycle transition (start, stop, or
skipWaiting). - Fill the queue
The page issues operations that enqueue multiple callbacks into a container like
pending_cache_storage_requests_orinflight_requests_. - Trigger dispatch A web-reachable event causes the browser to iterate that container and run the queued callbacks.
- Re-enter and mutate
One callback synchronously re-enters the same
ServiceWorkerVersion/EmbeddedWorkerInstance, mutating or clearing the container mid-iteration and freeing entries. - Use freed memory Continued iteration dereferences the invalidated iterator/freed entry, yielding a controllable browser-process UAF.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
forcontent/browser/service_worker/embedded_worker_instance.cc |
modified | |
ifcontent/browser/service_worker/embedded_worker_instance.cc |
modified | |
ifcontent/browser/service_worker/service_worker_unregister_job.cc |
modified | |
forcontent/browser/service_worker/service_worker_version.cc |
modified | |
TEST_Pcontent/browser/service_worker/service_worker_job_unittest.cc |
modified |
Files Changed
content/browser/service_worker/service_worker_version.hcontent/browser/service_worker/service_worker_register_job.cccontent/browser/service_worker/service_worker_unregister_job.hcontent/browser/service_worker/service_worker_context_core.cccontent/browser/service_worker/service_worker_job_unittest.cccontent/browser/service_worker/service_worker_unregister_job.cccontent/browser/service_worker/service_worker_version.cccontent/browser/service_worker/embedded_worker_instance.cccontent/browser/service_worker/service_worker_version_unittest.cc
Audit Directions
- Iterate-then-invoke on membersFlag 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 orderingAudit queue processors that
.Run()a front/queued callback before removing it; the entry must be detached from the live container first. - Web-reachable re-entrancyFor 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.
Patch
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)
Regression Test / PoC
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