Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in ServiceWorker
DescriptionUse after free in ServiceWorker
ComponentServiceWorker
Bug ClassUAF
Tracker517705966
Fix commitfba6d00c1dd5 (chromium/src) +22/-59
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-08

Changed Functions

FunctionChangeNotes
if
third_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.cc
  • third_party/blink/renderer/modules/service_worker/service_worker_global_scope.h
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};
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • third_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.)

  1. A compromised renderer process holds a direct blink::mojom::ControllerServiceWorker Mojo remote.
  2. The attacker triggers two DispatchFetchEventForSubresource calls with different fetch_event_ids but using the exact same non-zero service_worker_race_network_request_token in params->request.
  3. The first call populates the maps normally.
  4. The second call hits the key collision inside InsertNewItemToRaceNetworkRequests. The second RaceNetworkRequestInfo is deleted at the end of the method, leaving a dangling pointer in race_network_request_fetch_event_ids_ under the second event’s ID.
  5. The attacker sprays the PartitionAlloc heap to reclaim the freed size bucket with controlled data.
  6. The attacker completes or aborts the second fetch event, triggering RemoveItemFromRaceNetworkRequests and 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.

View on issue tracker