CVE-2026-11694
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/modules/service_worker/service_worker_global_scope.cc |
modified |
Files Changed
third_party/blink/renderer/modules/service_worker/service_worker_global_scope.ccthird_party/blink/renderer/modules/service_worker/service_worker_global_scope.h
Patch
From fba6d00c1dd5690baf23180f392332cc26354413 Mon Sep 17 00:00:00 2001 From: Takashi Nakayama <[email protected]> Date: Mon, 01 Jun 2026 07:29:24 -0700 Subject: [PATCH] [ServiceWorker] Disallow collided UnguessableToken in SWGlobalScope This is a follow-up CL of crrev.com/c/7889215 to prevent UaF by design. Duplicated UnguessabkeToken(s) passed to SWGlobalScope has been regarded as logic errors and emitted DwoC to investigate the reasons. However, the DwoC are not observed anymore and therefore should now be regarded as fatal errors. This CL cleanups the DWoC and simplifies the structure of the class members to clarify the invariants. Bug: 1492640,517705966 Change-Id: I856f865c1223e6db614d7152d109c0a8ca7a6fd1 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7889376 Commit-Queue: Takashi Nakayama <[email protected]> Reviewed-by: Shunya Shishido <[email protected]> Cr-Commit-Position: refs/heads/main@{#1639365} --- diff --git a/third_party/blink/renderer/modules/service_worker/service_worker_global_scope.cc b/third_party/blink/renderer/modules/service_worker/service_worker_global_scope.cc index a9c231d..55abfb86 100644 --- a/third_party/blink/renderer/modules/service_worker/service_worker_global_scope.cc +++ b/third_party/blink/renderer/modules/service_worker/service_worker_global_scope.cc @@ -30,12 +30,12 @@ #include "third_party/blink/renderer/modules/service_worker/service_worker_global_scope.h" +#include <stdint.h> + #include <algorithm> #include <memory> #include <utility> -#include "base/debug/crash_logging.h" -#include "base/debug/dump_without_crashing.h" #include "base/feature_list.h" #include "base/functional/callback_helpers.h" #include "base/memory/ptr_util.h" @@ -137,6 +137,7 @@ #include "third_party/blink/renderer/platform/bindings/source_location.h" #include "third_party/blink/renderer/platform/bindings/v8_binding.h" #include "third_party/blink/renderer/platform/bindings/v8_throw_exception.h" +#include "third_party/blink/renderer/platform/heap/collection_support/heap_hash_map.h" #include "third_party/blink/renderer/platform/heap/garbage_collected.h" #include "third_party/blink/renderer/platform/loader/fetch/fetch_client_settings_object_snapshot.h" #include "third_party/blink/renderer/platform/loader/fetch/memory_cache.h" @@ -147,6 +148,7 @@ #include "third_party/blink/renderer/platform/weborigin/kurl.h" #include "third_party/blink/renderer/platform/weborigin/security_policy.h" #include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h" +#include "third_party/blink/renderer/platform/wtf/hash_map.h" #include "third_party/blink/renderer/platform/wtf/text/strcat.h" #include "third_party/perfetto/include/perfetto/tracing/track_event_args.h" @@ -2780,13 +2782,11 @@ std::optional<mojo::PendingRemote<network::mojom::blink::URLLoaderFactory>> ServiceWorkerGlobalScope::FindRaceNetworkRequestURLLoaderFactory( const base::UnguessableToken& token) { - std::unique_ptr<RaceNetworkRequestInfo> result = - race_network_requests_.Take(String(token.ToString())); - if (result) { - race_network_request_fetch_event_ids_.erase(result->fetch_event_id); - return std::optional< - mojo::PendingRemote<network::mojom::blink::URLLoaderFactory>>( - std::move(result->url_loader_factory)); + if (RaceNetworkRequestInfo result = + race_network_requests_.Take(String(token.ToString())); + result.IsValid()) { + fetch_event_ids_to_token_map_.erase(result.fetch_event_id); + return std::move(result.url_loader_factory); } return std::nullopt; } @@ -2798,58 +2798,22 @@ url_loader_factory, const KURL& request_url) { auto race_network_request_token = String(token.ToString()); - auto info = std::make_unique<RaceNetworkRequestInfo>( - fetch_event_id, race_network_request_token, - std::move(url_loader_factory)); - RaceNetworkRequestInfo* info_raw = info.get(); + RaceNetworkRequestInfo info{ + .fetch_event_id = fetch_event_id, + .url_loader_factory = std::move(url_loader_factory)}; auto insert_result = race_network_requests_.insert(race_network_request_token, std::move(info)); - // WTF::HashMap::insert does not consume |info| on a duplicate key; in that - // case |info| (and |info_raw|) is freed at scope exit. Only publish the raw - // pointer into the secondary index after the owning insert succeeds. - if (insert_result.is_new_entry) { - race_network_request_fetch_event_ids_.insert(fetch_event_id, info_raw); - } - - // DumpWithoutCrashing if the token is empty, or not inserted as a new entry - // to |race_network_request_loader_factories_|. - // TODO(crbug.com/1492640) Remove DumpWithoutCrashing once we collect data - // and identify the cause. - static bool has_dumped_without_crashing_for_empty_token = false; - static bool has_dumped_without_crashing_for_not_new_entry = false; - if (!has_dumped_without_crashing_for_empty_token && token.is_empty()) { - has_dumped_without_crashing_for_empty_token = true; - SCOPED_CRASH_KEY_BOOL("SWGlobalScope", "empty_race_token", - token.is_empty()); - SCOPED_CRASH_KEY_STRING64("SWGlobalScope", "race_token_string", - token.ToString()); - SCOPED_CRASH_KEY_BOOL("SWGlobalScope", "race_insert_new_entry", - insert_result.is_new_entry); - SCOPED_CRASH_KEY_STRING256("SWGlobalScope", "race_request_url", - request_url.GetString().Utf8()); - base::debug::DumpWithoutCrashing(); - } - if (!has_dumped_without_crashing_for_not_new_entry && - !insert_result.is_new_entry) { - has_dumped_without_crashing_for_not_new_entry = true; - SCOPED_CRASH_KEY_BOOL("SWGlobalScope", "empty_race_token", - token.is_empty()); - SCOPED_CRASH_KEY_STRING64("SWGlobalScope", "race_token_string", - token.ToString()); - SCOPED_CRASH_KEY_BOOL("SWGlobalScope", "race_insert_new_entry", - insert_result.is_new_entry); - SCOPED_CRASH_KEY_STRING256("SWGlobalScope", "race_request_url", - request_url.GetString().Utf8()); - base::debug::DumpWithoutCrashing(); - } + CHECK(insert_result.is_new_entry) << "Collided UnguessableToken"; + fetch_event_ids_to_token_map_.insert(fetch_event_id, + std::move(race_network_request_token)); } void ServiceWorkerGlobalScope::RemoveItemFromRaceNetworkRequests( int fetch_event_id) { - RaceNetworkRequestInfo* info = - race_network_request_fetch_event_ids_.Take(fetch_event_id); - if (info) { - race_network_requests_.erase(info->token); + if (const String token_to_remove = + fetch_event_ids_to_token_map_.Take(fetch_event_id); + !token_to_remove.empty()) { + race_network_requests_.erase(token_to_remove); } } diff --git a/third_party/blink/renderer/modules/service_worker/service_worker_global_scope.h b/third_party/blink/renderer/modules/service_worker/service_worker_global_scope.h index 06b80cb..16f0649 100644 --- a/third_party/blink/renderer/modules/service_worker/service_worker_global_scope.h +++ b/third_party/blink/renderer/modules/service_worker/service_worker_global_scope.h @@ -810,15 +810,14 @@ struct RaceNetworkRequestInfo { int fetch_event_id; - String token; mojo::PendingRemote<network::mojom::blink::URLLoaderFactory> url_loader_factory; + bool IsValid() const { return url_loader_factory.is_valid(); } }; // TODO(crbug.com/918702) HashMap cannot use base::UnguessableToken as a // key. As a workaround uses String as a key instead. - HashMap<String, std::unique_ptr<RaceNetworkRequestInfo>> - race_network_requests_; - HashMap<int, RaceNetworkRequestInfo*> race_network_request_fetch_event_ids_; + HashMap<String, RaceNetworkRequestInfo> race_network_requests_; + HashMap<int, String> fetch_event_ids_to_token_map_; HeapMojoAssociatedRemote<mojom::blink::AssociatedInterfaceProvider> remote_associated_interfaces_{this};
Original Bug Report
Renderer Use-After-Free in ServiceWorkerGlobalScope::RemoveItemFromRaceNetworkRequests
Project Fortify, 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 potential Use-After-Free (UAF) vulnerability exists in the Service Worker global scope due to a key-collision ownership discrepancy. When duplicate tokens are processed in a race network request, a secondary lookup map retains a raw pointer to a temporary object that has been deallocated. Subsequent event resolution dereferences this dangling pointer, leading to potential memory corruption and remote code execution in the sandboxed renderer process.
Affected files:
third_party/blink/renderer/modules/service_worker/service_worker_global_scope.ccthird_party/blink/renderer/modules/service_worker/service_worker_global_scope.h
Estimated timestamp from git blame: 2023-10-24
Description
There is a potential Use-After-Free (UAF) vulnerability in the Service Worker thread of the Blink renderer process. The bug stems from how duplicate network tokens are handled within ServiceWorkerGlobalScope::InsertNewItemToRaceNetworkRequests.
When inserting a new race network request, a raw pointer to a heap-allocated RaceNetworkRequestInfo structure is added to the secondary lookup map race_network_request_fetch_event_ids_ before ownership of the managing std::unique_ptr is transferred to the primary race_network_requests_ map. If a key collision occurs (due to duplicate tokens), WTF::HashMap::insert does not consume or move the std::unique_ptr. When the function scope exits, the std::unique_ptr is destroyed, freeing the underlying RaceNetworkRequestInfo allocation. However, the secondary map still retains the dangling raw pointer.
Upon completion or abortion of the associated event, RemoveItemFromRaceNetworkRequests retrieves this dangling raw pointer and dereferences its member fields, leading to memory corruption.
Code References
In third_party/blink/renderer/modules/service_worker/service_worker_global_scope.cc:
void ServiceWorkerGlobalScope::InsertNewItemToRaceNetworkRequests(
int fetch_event_id,
const base::UnguessableToken& token, ...)
{
auto race_network_request_token = String(token.ToString());
auto info = std::make_unique<RaceNetworkRequestInfo>(
fetch_event_id, race_network_request_token,
std::move(url_loader_factory));
race_network_request_fetch_event_ids_.insert(fetch_event_id, info.get()); // [1] Stores raw pointer to info
auto insert_result = race_network_requests_.insert(
race_network_request_token, std::move(info)); // [2] insert fails if duplicate token
...
} // [3] info dtor is invoked and deletes the object if not moved in [2]
And inside the cleanup path:
void ServiceWorkerGlobalScope::RemoveItemFromRaceNetworkRequests(int fetch_event_id) {
RaceNetworkRequestInfo* info =
race_network_request_fetch_event_ids_.Take(fetch_event_id); // [4] Returns the dangling raw pointer
if (info) {
race_network_requests_.erase(info->token); // [5] Use-After-Free on info->token
}
}
Potential Trigger Steps
(Note: These are potential steps modeled via code tracing, as our tooling does not currently run exploit code.)
- A compromised renderer process holds a direct
blink::mojom::ControllerServiceWorkerMojo remote. - The attacker triggers two
DispatchFetchEventForSubresourcecalls with differentfetch_event_ids but using the exact same non-zeroservice_worker_race_network_request_tokeninparams->request. - The first call populates the maps normally.
- The second call hits the key collision inside
InsertNewItemToRaceNetworkRequests. The secondRaceNetworkRequestInfois deleted at the end of the method, leaving a dangling pointer inrace_network_request_fetch_event_ids_under the second event’s ID. - The attacker sprays the PartitionAlloc heap to reclaim the freed size bucket with controlled data.
- The attacker completes or aborts the second fetch event, triggering
RemoveItemFromRaceNetworkRequestsand invoking the UAF.
Proposed Fix
To resolve this issue, only add the entry to the secondary lookup map race_network_request_fetch_event_ids_ after a successful insertion into the primary map has occurred:
void ServiceWorkerGlobalScope::InsertNewItemToRaceNetworkRequests(
int fetch_event_id,
const base::UnguessableToken& token,
mojo::PendingRemote<network::mojom::blink::URLLoaderFactory>
url_loader_factory,
const KURL& request_url) {
auto race_network_request_token = String(token.ToString());
auto info = std::make_unique<RaceNetworkRequestInfo>(
fetch_event_id, race_network_request_token,
std::move(url_loader_factory));
auto insert_result = race_network_requests_.insert(race_network_request_token,
std::move(info));
if (insert_result.is_new_entry) {
race_network_request_fetch_event_ids_.insert(
fetch_event_id, insert_result.stored_value->value.get());
}
...
Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379
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.