Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in ServiceWorker
DescriptionUse after free in ServiceWorker
ComponentServiceWorker
Bug ClassUAF
Tracker499449324
Fix commit2f10b2e5550a (chromium/src) +19/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-05

Files Changed

  • content/browser/service_worker/service_worker_version.cc
  • content/common/features.cc
  • content/common/features.h
From 2f10b2e5550a0ab15b7a4bcfb5edb73f5cbbd187 Mon Sep 17 00:00:00 2001
From: Takashi Nakayama <[email protected]>
Date: Thu, 09 Apr 2026 05:25:00 -0700
Subject: [PATCH] Wrap SW optional timeout iterator behind a feature flag

This CL introduces the killswitch for the security bug fix implemented
in crrev.com/c/7727546 for safe merge.

Bug: 499449324
Change-Id: I319ae1653e6b344d35e4ee6044f01dfe83b10ff2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7742380
Reviewed-by: Yoshisato Yanagisawa <[email protected]>
Commit-Queue: Rakina Zata Amni <[email protected]>
Reviewed-by: Rakina Zata Amni <[email protected]>
Auto-Submit: Takashi Nakayama <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1612121}
---

diff --git a/content/browser/service_worker/service_worker_version.cc b/content/browser/service_worker/service_worker_version.cc
index 40c7b8b..8b934de 100644
--- a/content/browser/service_worker/service_worker_version.cc
+++ b/content/browser/service_worker/service_worker_version.cc
@@ -838,8 +838,14 @@
   // ServiceWorkerVersion::Request
   TRACE_EVENT_END("ServiceWorker", perfetto::Track::FromPointer(request),
                   "Handled", was_handled);
-  if (request->timeout_iter.has_value()) {
-    request_timeouts_.erase(*request->timeout_iter);
+  if (base::FeatureList::IsEnabled(
+          features::kServiceWorkerOptionalTimeoutIterator)) {
+    if (request->timeout_iter.has_value()) {
+      request_timeouts_.erase(*request->timeout_iter);
+    }
+  } else {
+    // Equivalent to the previous, non-optional iterator behavior. Maybe unsafe.
+    request_timeouts_.erase(request->timeout_iter.value_or({}));
   }
   inflight_requests_.Remove(request_id);
   // TODO(crbug.com/40864997): remove the following DCHECK when the cause
@@ -2782,9 +2788,12 @@
     timed_out_infos.push_back(*it);
     // Erase the entry from `request_timeouts_` and update `InflightRequest`
     // accordingly.
-    InflightRequest* request = inflight_requests_.Lookup(it->id);
-    CHECK(request);
-    request->timeout_iter = std::nullopt;
+    if (base::FeatureList::IsEnabled(
+            features::kServiceWorkerOptionalTimeoutIterator)) {
+      InflightRequest* request = inflight_requests_.Lookup(it->id);
+      CHECK(request);
+      request->timeout_iter = std::nullopt;
+    }
     it = request_timeouts_.erase(it);
   }
 
diff --git a/content/common/features.cc b/content/common/features.cc
index 5fc3aec..f1c295b2 100644
--- a/content/common/features.cc
+++ b/content/common/features.cc
@@ -721,6 +721,10 @@
 BASE_FEATURE(kServiceWorkerClientUrlIsCreationUrl,
              base::FEATURE_ENABLED_BY_DEFAULT);
 
+// Kill switch for crbug.com/499449324.
+BASE_FEATURE(kServiceWorkerOptionalTimeoutIterator,
+             base::FEATURE_ENABLED_BY_DEFAULT);
+
 BASE_FEATURE(kServiceWorkerWindowClientInitiator,
              base::FEATURE_ENABLED_BY_DEFAULT);
 
diff --git a/content/common/features.h b/content/common/features.h
index 9ac4403..f0a8d22 100644
--- a/content/common/features.h
+++ b/content/common/features.h
@@ -223,6 +223,7 @@
 CONTENT_EXPORT BASE_DECLARE_FEATURE(
     kServiceWorkerSuppressTimeoutWhenPaymentWindowOpen);
 CONTENT_EXPORT BASE_DECLARE_FEATURE(kServiceWorkerClientUrlIsCreationUrl);
+CONTENT_EXPORT BASE_DECLARE_FEATURE(kServiceWorkerOptionalTimeoutIterator);
 CONTENT_EXPORT BASE_DECLARE_FEATURE(kServiceWorkerWindowClientInitiator);
 CONTENT_EXPORT BASE_DECLARE_FEATURE(
     kServiceWorkerSoftUpdateOnFunctionalEvent);
Loading diff…

Original Bug Report

reported by [email protected]

Potential Use-After-Free in ServiceWorkerVersion via dangling timeout iterators

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 without the security team.

Overview: A potential Use-After-Free vulnerability exists in the browser process due to improper handling of std::set iterators in ServiceWorkerVersion::OnTimeoutTimer. When multiple requests time out simultaneously, error callbacks can synchronously trigger the cancellation of other timed-out requests, leading to the use of a dangling iterator. This heap corruption occurs in the privileged browser process and could potentially lead to a sandbox escape.

Affected files:

  • content/browser/service_worker/service_worker_version.cc
  • content/browser/service_worker/service_worker_version.h

Estimated timestamp from git blame: 2025-08-26

Summary

A potential Use-After-Free (UAF) vulnerability has been identified in ServiceWorkerVersion::OnTimeoutTimer within content/browser/service_worker/service_worker_version.cc. The flaw occurs when multiple Service Worker requests time out simultaneously. During timeout processing, std::set iterators are invalidated early, but can still be accessed if an error callback synchronously finishes another timed-out request.

Because this vulnerability triggers a UAF on internal libc++ tree nodes, it bypasses MiraclePtr (BRP) protections. Successful exploitation could lead to arbitrary memory read/write and a sandbox escape from the browser process.

Root Cause Analysis

The ServiceWorkerVersion class tracks active requests in inflight_requests_ (a base::IDMap) and their expiration times in request_timeouts_ (a std::set). Each InflightRequest stores a timeout_iter pointing to its corresponding entry in the request_timeouts_ set.

When the timeout timer fires, ServiceWorkerVersion::OnTimeoutTimer executes in two phases:

  1. First Loop (Iterator Invalidation): It iterates through request_timeouts_, copies the data of all expired requests into a vector, and immediately erases them from the set (it = request_timeouts_.erase(it)). This frees the underlying red-black tree nodes and invalidates the timeout_iter pointers stored in the corresponding InflightRequest objects.
  2. Second Loop (Callback Execution): It loops through the copied data and calls MaybeTimeoutRequest, which runs the error_callback for each timed-out request.

A developer comment explicitly acknowledges that these error callbacks can “synchronously finish another request”. If the error callback for Request A synchronously triggers the completion or cancellation of Request B (e.g., an external keep-alive request or an associated fetch/sync request), ServiceWorkerVersion::FinishRequest(B) is invoked.

If Request B also timed out in the same cycle, it has not yet been processed by the second loop. Therefore, it still exists in inflight_requests_. When FinishRequest (via FinishRequestWithFetchCount) handles Request B, it executes request_timeouts_.erase(request->timeout_iter);.

Since Request B’s timeout_iter was already invalidated during the first loop, this results in a UAF. The std::set::erase function attempts to read from and rebalance the freed node, corrupting the heap.

Potential Attack Steps

Note: These are suggested steps based on code analysis; our tooling agent does not yet have the ability to run working exploit code.

  1. From a compromised renderer or malicious website, register a Service Worker.
  2. Trigger two concurrent requests that are coupled in a way where the failure of one triggers the cancellation of the other (e.g., an Extension event and its associated EXTERNAL_REQUEST keep-alive).
  3. Intentionally stall the Service Worker’s execution (e.g., via infinite loops or unresolved Promises) to ensure both requests hit their maximum expiration time simultaneously.
  4. The browser’s timeout timer fires. OnTimeoutTimer erases both requests from the request_timeouts_ set, freeing both nodes.
  5. OnTimeoutTimer fires the error callback for the first request.
  6. The callback synchronously finishes the second request. FinishRequest attempts to erase the second request from the request_timeouts_ set using its dangling iterator.
  7. By grooming the heap beforehand, the attacker reclaims the freed std::set node. When the erase tree-rebalancing algorithm executes, it processes the attacker-controlled memory, leading to arbitrary memory read/write and eventual Remote Code Execution (RCE).

Suggested Fix

To prevent the dangling iterator dereference, ensure that when an element is erased from request_timeouts_, the timeout_iter in the corresponding InflightRequest is properly invalidated.

  1. Modify InflightRequest to handle an invalid iterator state (e.g., by setting it to request_timeouts_.end()).
  2. In OnTimeoutTimer, when erasing elements from the set, look up the InflightRequest in inflight_requests_ and update its timeout_iter to request_timeouts_.end().
  3. In FinishRequestWithFetchCount, check if request->timeout_iter != request_timeouts_.end() before attempting to erase it.

Evaluated with Chrome root at commit: 09ec9e7cc4d24823d20b6d37cf3d282734f6bf0f


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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