CVE-2026-78951
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/service_worker/service_worker_single_script_update_checker.cc |
modified | |
ASSERT_TRUEcontent/browser/service_worker/service_worker_single_script_update_checker_unittest.cc |
modified | |
ServiceWorkerSingleScriptUpdateCheckerSha256ChecksumTestcontent/browser/service_worker/service_worker_single_script_update_checker_unittest.cc |
modified |
Files Changed
content/browser/service_worker/service_worker_single_script_update_checker.cccontent/browser/service_worker/service_worker_single_script_update_checker.hcontent/browser/service_worker/service_worker_single_script_update_checker_unittest.cc
Patch
From 45038ff19def4d325e7ec9a7fe3307e0dc74efd7 Mon Sep 17 00:00:00 2001
From: Yoshisto Yanagisawa <yyanagisawa@chromium.org>
Date: Mon, 06 Jul 2026 23:42:57 -0700
Subject: [PATCH] ServiceWorker: Handle re-entrant destruction in update checker
ServiceWorkerCacheWriter::MaybeWriteHeaders() can run its completion callback synchronously when the storage writer remote is disconnected. The callback chain ends up running the checker's ResultCallback, and the owner is allowed to destroy the checker from within that callback. Stop touching members in OnReceiveResponse() after WriteHeaders() returns if |this| has been destroyed.
Bug: 522957054
TAG=agy
CONV=f42520bd-6803-438a-a9c7-e7e32fcbc7e1
Change-Id: I5a9bb646212e87c5a800c5d4ad5099a66953f434
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8020222
Reviewed-by: Shunya Shishido <sisidovski@chromium.org>
Commit-Queue: Yoshisato Yanagisawa <yyanagisawa@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1657711}
---
diff --git a/content/browser/service_worker/service_worker_single_script_update_checker.cc b/content/browser/service_worker/service_worker_single_script_update_checker.cc
index afe55f7..7863c4e 100644
--- a/content/browser/service_worker/service_worker_single_script_update_checker.cc
+++ b/content/browser/service_worker/service_worker_single_script_update_checker.cc
@@ -295,7 +295,14 @@
network_accessed_ = response_head->network_accessed;
+ // WriteHeaders() may complete synchronously and run the result callback,
+ // which is allowed to destroy |this|.
+ base::WeakPtr<ServiceWorkerSingleScriptUpdateChecker> weak_this =
+ weak_factory_.GetWeakPtr();
WriteHeaders(std::move(response_head));
+ if (!weak_this) {
+ return;
+ }
if (!consumer)
return;
@@ -427,6 +434,12 @@
}
}
+void ServiceWorkerSingleScriptUpdateChecker::FlushRemotesForTesting() {
+ if (cache_writer_) {
+ cache_writer_->FlushRemotesForTesting(); // IN-TEST
+ }
+}
+
//------------------------------------------------------------------------------
void ServiceWorkerSingleScriptUpdateChecker::WriteHeaders(
diff --git a/content/browser/service_worker/service_worker_single_script_update_checker.h b/content/browser/service_worker/service_worker_single_script_update_checker.h
index 077ba46..a0c9374 100644
--- a/content/browser/service_worker/service_worker_single_script_update_checker.h
+++ b/content/browser/service_worker/service_worker_single_script_update_checker.h
@@ -191,6 +191,7 @@
const scoped_refptr<PolicyContainerHost> policy_container_host() const {
return policy_container_host_;
}
+ void FlushRemotesForTesting();
static const char* ResultToString(Result result);
diff --git a/content/browser/service_worker/service_worker_single_script_update_checker_unittest.cc b/content/browser/service_worker/service_worker_single_script_update_checker_unittest.cc
index 858a3bb..2771271 100644
--- a/content/browser/service_worker/service_worker_single_script_update_checker_unittest.cc
+++ b/content/browser/service_worker/service_worker_single_script_update_checker_unittest.cc
@@ -12,6 +12,7 @@
#include "base/functional/bind.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
+#include "base/test/run_until.h"
#include "content/browser/service_worker/service_worker_context_wrapper.h"
#include "content/browser/service_worker/service_worker_test_utils.h"
#include "content/public/test/browser_task_environment.h"
@@ -124,6 +125,45 @@
return remote;
}
+ std::unique_ptr<ServiceWorkerSingleScriptUpdateChecker>
+ CreateSingleScriptUpdateCheckerWithCallback(
+ const char* url,
+ const GURL& scope,
+ std::unique_ptr<MockServiceWorkerResourceReader> compare_reader,
+ std::unique_ptr<MockServiceWorkerResourceReader> copy_reader,
+ mojo::Remote<storage::mojom::ServiceWorkerResourceWriter> writer,
+ network::TestURLLoaderFactory* loader_factory,
+ ServiceWorkerSingleScriptUpdateChecker::ResultCallback callback) {
+ auto fetch_client_settings_object =
+ blink::mojom::FetchClientSettingsObject::New(
+ []() {
+ auto policies = blink::mojom::PolicyContainerPolicies::New();
+ policies->referrer_policy =
+ network::mojom::ReferrerPolicy::kDefault;
+ return policies;
+ }(),
+ GURL(url), blink::mojom::InsecureRequestsPolicy::kDoNotUpgrade);
+ return std::make_unique<ServiceWorkerSingleScriptUpdateChecker>(
+ GURL(url), /*is_main_script=*/true, GURL(url), scope,
+ /*force_bypass_cache=*/false, blink::mojom::ScriptType::kClassic,
+ blink::mojom::ServiceWorkerUpdateViaCache::kNone,
+ std::move(fetch_client_settings_object),
+ /*time_since_last_check=*/base::TimeDelta(), browser_context_.get(),
+ base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
+ loader_factory),
+ WrapReader(std::move(compare_reader)),
+ WrapReader(std::move(copy_reader)), std::move(writer),
+ /*writer_resource_id=*/0,
+ ServiceWorkerSingleScriptUpdateChecker::ScriptChecksumUpdateOption::
+ kDefault,
+ blink::StorageKey::Create(url::Origin::Create(scope),
+ net::SchemefulSite(scope),
+ blink::mojom::AncestorChainBit::kSameSite,
+ /*third_party_partitioning_allowed=*/true),
+ /*network_restrictions_id=*/std::nullopt, PolicyContainerPolicies(),
+ std::move(callback));
+ }
+
mojo::Remote<storage::mojom::ServiceWorkerResourceWriter> WrapWriter(
std::unique_ptr<MockServiceWorkerResourceWriter> writer) {
mojo::Remote<storage::mojom::ServiceWorkerResourceWriter> remote;
@@ -1450,6 +1490,58 @@
}
}
+// Tests that the checker can be safely destroyed from inside its result
+// callback when writing the response headers to storage fails synchronously
+// (e.g., the storage writer connection has gone away).
+TEST_F(ServiceWorkerSingleScriptUpdateCheckerTest,
+ DestroyedInCallbackOnSynchronousHeaderWriteFailure) {
+ auto loader_factory = std::make_unique<network::TestURLLoaderFactory>();
+ auto compare_reader = std::make_unique<MockServiceWorkerResourceReader>();
+ auto copy_reader = std::make_unique<MockServiceWorkerResourceReader>();
+
+ mojo::Remote<storage::mojom::ServiceWorkerResourceWriter> writer_remote;
+ auto writer_receiver = writer_remote.BindNewPipeAndPassReceiver();
+
+ std::unique_ptr<ServiceWorkerSingleScriptUpdateChecker> checker;
+ std::optional<ServiceWorkerSingleScriptUpdateChecker::Result> result;
+ checker = CreateSingleScriptUpdateCheckerWithCallback(
+ kScriptURL, GURL(kScope), std::move(compare_reader),
+ std::move(copy_reader), std::move(writer_remote), loader_factory.get(),
+ base::BindLambdaForTesting(
+ [&](const GURL&,
+ ServiceWorkerSingleScriptUpdateChecker::Result compare_result,
+ std::unique_ptr<
+ ServiceWorkerSingleScriptUpdateChecker::FailureInfo>,
+ std::unique_ptr<
+ ServiceWorkerSingleScriptUpdateChecker::PausedState>,
+ const std::optional<std::string>&) {
+ result = compare_result;
+ // The owner may destroy the checker synchronously in response to
+ // the result.
+ checker.reset();
+ }));
+
+ // Disconnect the storage writer before the network response arrives so that
+ // writing headers fails synchronously.
+ writer_receiver.reset();
+ checker->FlushRemotesForTesting();
+ ASSERT_FALSE(result.has_value());
+
+ auto head = network::mojom::URLResponseHead::New();
+ head->headers = base::MakeRefCounted<net::HttpResponseHeaders>(
+ net::HttpUtil::AssembleRawHeaders(kSuccessHeader));
+ head->headers->GetMimeType(&head->mime_type);
+ head->parsed_headers = network::mojom::ParsedHeaders::New();
+ loader_factory->SimulateResponseForPendingRequest(
+ GURL(kScriptURL), network::URLLoaderCompletionStatus(net::OK),
+ std::move(head), "abcdef");
+ ASSERT_TRUE(base::test::RunUntil([&]() { return result.has_value(); }));
+
+ EXPECT_EQ(result.value(),
+ ServiceWorkerSingleScriptUpdateChecker::Result::kFailed);
+ EXPECT_FALSE(checker);
+}
+
class ServiceWorkerSingleScriptUpdateCheckerSha256ChecksumTest
: public ServiceWorkerSingleScriptUpdateCheckerTest,
public testing::WithParamInterface<
Regression Test / PoC
diff --git a/content/browser/service_worker/service_worker_single_script_update_checker_unittest.cc b/content/browser/service_worker/service_worker_single_script_update_checker_unittest.cc
index 858a3bb..2771271 100644
--- a/content/browser/service_worker/service_worker_single_script_update_checker_unittest.cc
+++ b/content/browser/service_worker/service_worker_single_script_update_checker_unittest.cc
@@ -12,6 +12,7 @@
#include "base/functional/bind.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
+#include "base/test/run_until.h"
#include "content/browser/service_worker/service_worker_context_wrapper.h"
#include "content/browser/service_worker/service_worker_test_utils.h"
#include "content/public/test/browser_task_environment.h"
@@ -124,6 +125,45 @@
return remote;
}
+ std::unique_ptr<ServiceWorkerSingleScriptUpdateChecker>
+ CreateSingleScriptUpdateCheckerWithCallback(
+ const char* url,
+ const GURL& scope,
+ std::unique_ptr<MockServiceWorkerResourceReader> compare_reader,
+ std::unique_ptr<MockServiceWorkerResourceReader> copy_reader,
+ mojo::Remote<storage::mojom::ServiceWorkerResourceWriter> writer,
+ network::TestURLLoaderFactory* loader_factory,
+ ServiceWorkerSingleScriptUpdateChecker::ResultCallback callback) {
+ auto fetch_client_settings_object =
+ blink::mojom::FetchClientSettingsObject::New(
+ []() {
+ auto policies = blink::mojom::PolicyContainerPolicies::New();
+ policies->referrer_policy =
+ network::mojom::ReferrerPolicy::kDefault;
+ return policies;
+ }(),
+ GURL(url), blink::mojom::InsecureRequestsPolicy::kDoNotUpgrade);
+ return std::make_unique<ServiceWorkerSingleScriptUpdateChecker>(
+ GURL(url), /*is_main_script=*/true, GURL(url), scope,
+ /*force_bypass_cache=*/false, blink::mojom::ScriptType::kClassic,
+ blink::mojom::ServiceWorkerUpdateViaCache::kNone,
+ std::move(fetch_client_settings_object),
+ /*time_since_last_check=*/base::TimeDelta(), browser_context_.get(),
+ base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
+ loader_factory),
+ WrapReader(std::move(compare_reader)),
+ WrapReader(std::move(copy_reader)), std::move(writer),
+ /*writer_resource_id=*/0,
+ ServiceWorkerSingleScriptUpdateChecker::ScriptChecksumUpdateOption::
+ kDefault,
+ blink::StorageKey::Create(url::Origin::Create(scope),
+ net::SchemefulSite(scope),
+ blink::mojom::AncestorChainBit::kSameSite,
+ /*third_party_partitioning_allowed=*/true),
+ /*network_restrictions_id=*/std::nullopt, PolicyContainerPolicies(),
+ std::move(callback));
+ }
+
mojo::Remote<storage::mojom::ServiceWorkerResourceWriter> WrapWriter(
std::unique_ptr<MockServiceWorkerResourceWriter> writer) {
mojo::Remote<storage::mojom::ServiceWorkerResourceWriter> remote;
@@ -1450,6 +1490,58 @@
}
}
+// Tests that the checker can be safely destroyed from inside its result
+// callback when writing the response headers to storage fails synchronously
+// (e.g., the storage writer connection has gone away).
+TEST_F(ServiceWorkerSingleScriptUpdateCheckerTest,
+ DestroyedInCallbackOnSynchronousHeaderWriteFailure) {
+ auto loader_factory = std::make_unique<network::TestURLLoaderFactory>();
+ auto compare_reader = std::make_unique<MockServiceWorkerResourceReader>();
+ auto copy_reader = std::make_unique<MockServiceWorkerResourceReader>();
+
+ mojo::Remote<storage::mojom::ServiceWorkerResourceWriter> writer_remote;
+ auto writer_receiver = writer_remote.BindNewPipeAndPassReceiver();
+
+ std::unique_ptr<ServiceWorkerSingleScriptUpdateChecker> checker;
+ std::optional<ServiceWorkerSingleScriptUpdateChecker::Result> result;
+ checker = CreateSingleScriptUpdateCheckerWithCallback(
+ kScriptURL, GURL(kScope), std::move(compare_reader),
+ std::move(copy_reader), std::move(writer_remote), loader_factory.get(),
+ base::BindLambdaForTesting(
+ [&](const GURL&,
+ ServiceWorkerSingleScriptUpdateChecker::Result compare_result,
+ std::unique_ptr<
+ ServiceWorkerSingleScriptUpdateChecker::FailureInfo>,
+ std::unique_ptr<
+ ServiceWorkerSingleScriptUpdateChecker::PausedState>,
+ const std::optional<std::string>&) {
+ result = compare_result;
+ // The owner may destroy the checker synchronously in response to
+ // the result.
+ checker.reset();
+ }));
+
+ // Disconnect the storage writer before the network response arrives so that
+ // writing headers fails synchronously.
+ writer_receiver.reset();
+ checker->FlushRemotesForTesting();
+ ASSERT_FALSE(result.has_value());
+
+ auto head = network::mojom::URLResponseHead::New();
+ head->headers = base::MakeRefCounted<net::HttpResponseHeaders>(
+ net::HttpUtil::AssembleRawHeaders(kSuccessHeader));
+ head->headers->GetMimeType(&head->mime_type);
+ head->parsed_headers = network::mojom::ParsedHeaders::New();
+ loader_factory->SimulateResponseForPendingRequest(
+ GURL(kScriptURL), network::URLLoaderCompletionStatus(net::OK),
+ std::move(head), "abcdef");
+ ASSERT_TRUE(base::test::RunUntil([&]() { return result.has_value(); }));
+
+ EXPECT_EQ(result.value(),
+ ServiceWorkerSingleScriptUpdateChecker::Result::kFailed);
+ EXPECT_FALSE(checker);
+}
+
class ServiceWorkerSingleScriptUpdateCheckerSha256ChecksumTest
: public ServiceWorkerSingleScriptUpdateCheckerTest,
public testing::WithParamInterface<
Original Bug Report
Potential Use-After-Free in ServiceWorkerSingleScriptUpdateChecker
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A Use-After-Free vulnerability exists in the browser process when handling Service Worker script updates. If the connection to the storage process is disconnected, a synchronous callback chain can delete the ServiceWorkerSingleScriptUpdateChecker object while a method is still executing on the stack, leading to memory corruption when control unwinds.
Affected files:
content/browser/service_worker/service_worker_single_script_update_checker.cc
Estimated timestamp from git blame: 2022-01-25
Description
A potential Use-After-Free (UAF) vulnerability exists in content::ServiceWorkerSingleScriptUpdateChecker::OnReceiveResponse within the browser process. The issue stems from the fact that ServiceWorkerCacheWriter::MaybeWriteHeaders can invoke its completion callback synchronously if an immediate error occurs, such as the Mojo connection to the storage process being disconnected.
This synchronous error handling initiates a callback chain that ultimately destroys the ServiceWorkerSingleScriptUpdateChecker instance. When the call stack unwinds, execution continues in OnReceiveResponse using a dangling this pointer to write to member variables and invoke further methods.
Suggested Exploitation Steps
(Note: These are suggested steps based on code analysis; a working Proof of Concept has not been run.)
- An attacker registers a Service Worker on a controlled origin and triggers an update for it.
- The browser creates a
ServiceWorkerUpdateChecker, which in turn creates aServiceWorkerSingleScriptUpdateCheckerand stores it in astd::unique_ptr. - The update checker initiates a network request for the updated script.
- Before the network headers arrive, the attacker induces a condition that causes the Mojo pipe to the Storage process to disconnect (e.g., by exhausting resources to crash the storage process).
- When the network response headers arrive,
ServiceWorkerSingleScriptUpdateChecker::OnReceiveResponseis called. OnReceiveResponseprocesses the headers and synchronously callsWriteHeaders(), passing a callback bound with aWeakPtr.WriteHeaders()callsServiceWorkerCacheWriter::MaybeWriteHeaders().- Because the storage pipe is disconnected,
MaybeWriteHeaders()synchronously executes the callback withnet::ERR_FAILED. - The callback invokes
ServiceWorkerSingleScriptUpdateChecker::OnWriteHeadersComplete(), which callsFail(), and thenFinish(). Finish()synchronously executes its storedcallback_, signaling failure back toServiceWorkerUpdateChecker::OnOneUpdateCheckFinished().- If this is the main script, the failure is propagated to
ServiceWorkerRegisterJob::OnUpdateCheckFinished(), which synchronously aborts the job viaComplete(), resulting in the destruction of theServiceWorkerUpdateCheckerand itsstd::unique_ptr<ServiceWorkerSingleScriptUpdateChecker>. - The object is freed, and the stack unwinds back to
OnReceiveResponse()immediately after theWriteHeaders()call. - Execution continues using the dangling
thispointer. The method performs several operations on the freed memory, includingnetwork_consumer_ = std::move(consumer);and modifyingnetwork_loader_state_. This corrupts the freed memory chunk with predictable/attacker-controlled values. - Finally, it calls
MaybeStartNetworkConsumerHandleWatcher(), which interacts with the corruptednetwork_watcher_member, potentially leading to arbitrary code execution in the unsandboxed browser process.
Impact
This is a critical vulnerability because it occurs in the unsandboxed browser process. Successful exploitation of this memory corruption could lead to arbitrary Remote Code Execution (RCE) and a full sandbox escape. Note that MiraclePtr (BackupRefPtr) does not protect against this specific UAF because the dangling pointer is the implicit this pointer stored on the stack, not a heap-to-heap pointer.
Suggested Fix
There are a few ways to mitigate this:
- Asynchronous Callbacks: Ensure that
ServiceWorkerCacheWriter::MaybeWriteHeaders(and similar methods) always invoke their completion callbacks asynchronously, even for immediate errors. This prevents synchronous destruction while methods are still on the stack. - WeakPtr Checks: If synchronous callbacks are permitted,
ServiceWorkerSingleScriptUpdateChecker::OnReceiveResponsemust check ifthisis still valid after callingWriteHeaders(). This can be achieved by creating aWeakPtrtothisbefore the call and checking it immediately after the call returns. If theWeakPtrhas been invalidated, the method should return immediately.
Evaluated with Chrome root at commit: 2155cb00003ec35716a76ed3246eae995f87b7ff
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.