High watch CVSS 7.5 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
7.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in ServiceWorker
DescriptionUse after free in ServiceWorker
ComponentServiceWorker
Sub-componentServiceWorker
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker517705966
Fix commitfba6d00c1dd5 (chromium/src) +22/-59
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-29 (per vendor advisory)
Fix landed2026-06-01 (public git)
Disclosed2026-06-08
Patch gap7 days — fix public in source before the advisory

Background

`ServiceWorkerGlobalScope`
The renderer-side (Blink) global execution context of a service worker, which manages RaceNetworkRequest bookkeeping.
RaceNetworkRequest
An optimization that races a network request against the service worker’s fetch handler; each in-flight race is tracked by a RaceNetworkRequestInfo keyed by an UnguessableToken (stored as a String) and cross-indexed by fetch_event_id.
Dual-map bookkeeping
Pre-patch, race_network_requests_ held owning std::unique_ptr<RaceNetworkRequestInfo> by token while race_network_request_fetch_event_ids_ held a raw RaceNetworkRequestInfo* by fetch_event_id.
`UnguessableToken` collision
Two race requests mapping to the same token string, a condition previously logged with DumpWithoutCrashing (DWoC) for investigation.

Root Cause Analysis

The pre-patch design maintained two parallel maps where one owned the RaceNetworkRequestInfo via unique_ptr and the other stored a raw back-pointer (info_raw) into it, keyed differently (token vs. fetch_event_id). If an UnguessableToken collided, WTF::HashMap::insert would not consume the new info (the existing entry stays), so ownership and the two indices could desynchronize, leaving a raw pointer that outlives or dangles relative to the owning entry — a lifetime hazard the DWoC instrumentation was added to study. Rather than continue tolerating collisions, this follow-up CL treats a duplicated token as a fatal logic error via CHECK(insert_result.is_new_entry), and restructures the members so race_network_requests_ stores RaceNetworkRequestInfo by value and the secondary map (fetch_event_ids_to_token_map_) stores only the token String instead of a raw pointer. Eliminating the raw RaceNetworkRequestInfo* back-index removes the dangling-pointer class entirely, and the CHECK guarantees the token→info map stays one-to-one, so FindRaceNetworkRequestURLLoaderFactory and RemoveItemFromRaceNetworkRequests can no longer operate on a stale or freed entry. This “prevents UaF by design” as the commit states.
Key insight
The core mistake was cross-indexing an owned object with a raw back-pointer that could dangle when an UnguessableToken collided; the fix removes the raw-pointer index (storing values and token strings instead) and makes a collision a fatal CHECK rather than a survivable, silently-logged state.

Attack Path

  1. Drive RaceNetworkRequest creation A controlled service worker with race-network-request enabled issues fetches so InsertNewItemToRaceNetworkRequests is called, populating both maps with a fetch_event_id and token. (inferred)
  2. Induce a token collision Cause two race requests to map to the same UnguessableToken string so WTF::HashMap::insert reports !is_new_entry, which pre-patch left the maps inconsistent instead of aborting. (inferred)
  3. Desynchronize ownership and raw index The owning unique_ptr entry and the raw RaceNetworkRequestInfo* in race_network_request_fetch_event_ids_ diverge, so one map references memory whose owner has been (or will be) freed. (inferred)
  4. Dereference a dangling entry A later FindRaceNetworkRequestURLLoaderFactory or RemoveItemFromRaceNetworkRequests follows the stale raw pointer or freed info, yielding a use-after-free in the renderer. (inferred)
  5. Exploitation The freed RaceNetworkRequestInfo (holding a PendingRemote<URLLoaderFactory>) could be reallocated and abused. (inferred)

Impact Assessment

An attacker gains a use-after-free in the renderer process within a service worker’s RaceNetworkRequest bookkeeping, contingent on triggering a duplicated UnguessableToken in the RaceNetworkAndFetch/RaceNetworkAndCache path. The advisory classifies this as a use-after-free in ServiceWorker with no exploited-in-the-wild status stated; the specific dereference chain and any escalation are (inferred) since the patch is a hardening/restructure rather than a demonstrated sink.

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

Audit Directions

  • Parallel maps with raw back-pointers
    Audit any bookkeeping that owns an object in one container while storing a raw pointer to it in another; prefer value storage or keys instead of raw pointers to avoid desynchronized lifetimes.
  • Non-consuming inserts on duplicate keys
    Review WTF::HashMap::insert (and similar) call sites that move-construct an argument, since a duplicate key does not consume the argument and can leave secondary state inconsistent.
  • DumpWithoutCrashing on "impossible" states
    Treat DWoC-instrumented invariants (e.g., duplicate UnguessableToken) as latent memory-safety bugs; verify the code path is actually unreachable rather than merely rare, and promote to CHECK where safe.
From fba6d00c1dd5690baf23180f392332cc26354413 Mon Sep 17 00:00:00 2001
From: Takashi Nakayama <tnak@chromium.org>
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 <tnak@chromium.org>
Reviewed-by: Shunya Shishido <sisidovski@chromium.org>
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 vm...@google.com

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