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 <yyanagisawa@chromium.org>
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 <sisidovski@chromium.org>
Commit-Queue: Yoshisato Yanagisawa <yyanagisawa@chromium.org>
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