CVE-2026-7922
Overview
Background
- `ServiceWorkerVersion`
- the browser-process object tracking a running service worker’s inflight requests and their timeouts.
- `request_timeouts_`
- an ordered container of pending request timeout entries; each inflight request stores an iterator (
timeout_iter) pointing into it. - `InflightRequest::timeout_iter`
- a member that references the request’s entry in
request_timeouts_; the fix changes its use so it is treated as an optional (std::optional) that can be cleared. - Kill switch feature flag
kServiceWorkerOptionalTimeoutIterator, abase::Featureenabled by default that gates the safe optional-iterator behavior so the fix can be disabled remotely if needed.
Root Cause Analysis
Verify before building a PoC
What the commit does not establish: This CL is only the default-enabled kill switch; commit says core fix landed in crrev.com/c/7727546, so the dangling-iterator root cause is consistent with but not fully proven by this diff.
When a request’s timeout fires, the timeout-processing loop erases the corresponding entry from request_timeouts_ (it = request_timeouts_.erase(it)), which invalidates any iterator still pointing at that entry.
Pre-patch, the matching InflightRequest::timeout_iter was left pointing at the now-erased element, so when the request later finished, the finish path executed request_timeouts_.erase(*request->timeout_iter) using that stale iterator—a use of an invalidated iterator that dereferences/erases freed container memory. This CL is the merge-safe kill switch around the real fix: under the enabled flag, the timeout loop looks up the corresponding InflightRequest and sets request->timeout_iter = std::nullopt before erasing, and the finish path only erases when request->timeout_iter.has_value(), so a timed-out request never re-erases through a dangling iterator. When the flag is disabled the code falls back to the previous behavior—request_timeouts_.erase(request->timeout_iter.value_or({}))—which the added comment explicitly marks “Maybe unsafe,” restoring the vulnerable path only if the kill switch is flipped.
InflightRequest’s stored timeout iterator was not cleared when the timeout loop erased its request_timeouts_ entry, so the later finish path erased through a dangling iterator; the fix (gated by kServiceWorkerOptionalTimeoutIterator) nulls out timeout_iter on timeout and only erases when it still has a value.Attack Path
- Drive inflight requests to timeout
Cause a service worker to have inflight requests whose entries in
request_timeouts_are processed by the timeout loop, which erases each entry and (pre-patch) leaves the request’stimeout_iterdangling. - Complete the timed-out request
Let the same request subsequently finish so the finish path runs
request_timeouts_.erase(*request->timeout_iter)using the now-invalidated iterator. - Trigger the use-after-free
The erase dereferences the stale iterator into freed
request_timeouts_node memory, producing the use-after-free (inferred: exact heap layout and any resulting primitive not shown in the diff).
Impact Assessment
request_timeouts_ iterator/node memory in the browser process. The advisory classifies this as a use-after-free in ServiceWorker; no exploited-in-the-wild status is stated. This CL only adds the default-enabled kill switch for the underlying fix (landed in a separate CL), and any escalation to a read/write primitive or code execution beyond the iterator misuse is (inferred).Files Changed
content/browser/service_worker/service_worker_version.cccontent/common/features.cccontent/common/features.h
Audit Directions
- Iterators stored across mutationsAudit any structure that caches container iterators (or pointers) as members and later erases through them; ensure the cached iterator is invalidated whenever the container element is removed elsewhere.
- Dual removal paths for one entryLook for objects that can be removed by two independent code paths (timeout vs. completion, cancel vs. finish) where one path fails to notify the other that the entry is gone.
- `value_or({})` / default-constructed handlesFlag fallbacks that substitute a default-constructed iterator/handle for a missing value, which can silently operate on an invalid or unrelated container position.
Patch
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);
Original Bug Report
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.cccontent/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:
- 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 thetimeout_iterpointers stored in the correspondingInflightRequestobjects. - Second Loop (Callback Execution): It loops through the copied data and calls
MaybeTimeoutRequest, which runs theerror_callbackfor 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.
- From a compromised renderer or malicious website, register a Service Worker.
- 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_REQUESTkeep-alive). - 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.
- The browser’s timeout timer fires.
OnTimeoutTimererases both requests from therequest_timeouts_set, freeing both nodes. OnTimeoutTimerfires the error callback for the first request.- The callback synchronously finishes the second request.
FinishRequestattempts to erase the second request from therequest_timeouts_set using its dangling iterator. - By grooming the heap beforehand, the attacker reclaims the freed
std::setnode. When theerasetree-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.
- Modify
InflightRequestto handle an invalid iterator state (e.g., by setting it torequest_timeouts_.end()). - In
OnTimeoutTimer, when erasing elements from the set, look up theInflightRequestininflight_requests_and update itstimeout_itertorequest_timeouts_.end(). - In
FinishRequestWithFetchCount, check ifrequest->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.