Low watch ✔︎ verified CVSS 9.6 chrome UAF 🔧 Commit mapped

Overview

Low
Severity
9.6
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 — revised by independent review by AI
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.
Independent review by AIConfirmed (tier revised from patch-now)
Reviewer notes: Mechanism matches the diff. But 'receiver' and 'factory' are locals, not members. The UT simulates teardown by a manual DetachedFromRequest, so a real web-triggerable path is not shown. Low severity contradicts the 'default' and 'powerful' framing; triggering needs a precisely timed navigation cancel inside the callback.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker522418913
Fix commite13a7ad5bf74 (chromium/src) +47/-3
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-06-10 (per vendor advisory)
Fix landed2026-07-01 (public git)
Disclosed2026-08-25
Patch gap55 days — fix public in source before the advisory

Background

`ServiceWorkerMainResourceLoader`
the browser-side URLLoader that drives a main-resource (navigation) request through a controlling service worker and, on fallback, hands the request to the default network factory.
`Fallback()`
the method invoked when the service worker declines to handle the request; it unbinds the Mojo receiver_ and runs fallback_callback_ to obtain the default URLLoaderFactory.
`DetachedFromRequest()`
a teardown entry point that, when the loader’s receiver_ is no longer bound, executes delete this.
`base::WeakPtr` / `AsWeakPtr()`
a weak reference that becomes null once the pointed-to object is destroyed, used to detect reentrant self-destruction.

Root Cause Analysis

Fallback() first calls receiver_.Unbind(), then invokes std::move(fallback_callback_).Run(...) to get the default factory. Running that callback can synchronously tear down the navigation loader stack that owns the ServiceWorkerMainResourceLoader; that teardown calls DetachedFromRequest(), and because receiver_ is already unbound, DetachedFromRequest() performs delete this. The pre-patch code assumed the callback returned with this still alive and immediately dereferenced member state (using the returned factory and passing receiver, request_id_, options_, etc. into factory->CreateLoaderAndStart), so when the callback destroyed the loader the subsequent member access was a use-after-free.

The fix captures base::WeakPtr<ServiceWorkerMainResourceLoader> weak_this = AsWeakPtr() before running the callback and returns early if !weak_this, ensuring no member of a freed this is touched. This works because the weak pointer is invalidated by the destructor, giving a reliable post-callback liveness check.

Key insight
The core mistake was continuing to use this after running a callback that can synchronously delete the loader (via DetachedFromRequest() once receiver_ is unbound); the fix adds a WeakPtr liveness check and an early return to abort when the loader has been destroyed.

Attack Path

  1. Navigate a service-worker-controlled scope A request enters ServiceWorkerMainResourceLoader for a document controlled by a service worker configured to fall back to the network.
  2. Enter `Fallback()` and unbind the receiver The loader calls receiver_.Unbind(), arming the condition under which DetachedFromRequest() will delete this.
  3. Synchronously tear down the loader from the callback While fallback_callback_ runs, the navigation loader stack is torn down (e.g. the navigation is cancelled), calling DetachedFromRequest(), which deletes the ServiceWorkerMainResourceLoader — exactly what the added DetachedDuringFallbackCallback test simulates via loader_.release()->DetachedFromRequest().
  4. Use-after-free on return Pre-patch, control returned into Fallback() and dereferenced freed members (factory, receiver, request_id_, options_) to call CreateLoaderAndStart, operating on freed heap memory (inferred).

Impact Assessment

An attacker who can cause a service-worker fallback navigation to be cancelled at the right moment gains a use-after-free of a ServiceWorkerMainResourceLoader in the browser process, which is a powerful sandbox-external corruption target; the advisory classifies this as a use-after-free in ServiceWorker. Preconditions are a controlling service worker that triggers network fallback plus the ability to tear down the navigation while the fallback callback is on the stack. No exploited-in-the-wild status or severity beyond the UAF classification is stated, and escalation to code execution is (inferred).

Changed Functions

FunctionChangeNotes
if
content/browser/service_worker/service_worker_main_resource_loader.cc
modified
TEST_F
content/browser/service_worker/service_worker_main_resource_loader_unittest.cc
modified

Files Changed

  • content/browser/service_worker/service_worker_main_resource_loader.cc
  • content/browser/service_worker/service_worker_main_resource_loader_unittest.cc

Audit Directions

  • Callback-then-use after receiver unbind
    Search for methods that unbind a Mojo receiver and then run a client/owner callback before touching this, where the callback can reenter destruction paths.
  • `DetachedFromRequest`-style delete-on-unbound
    Audit teardown functions whose delete this is gated only on receiver_ being unbound, since any prior unbind widens the window for reentrant deletion.
  • Missing `WeakPtr` guards around synchronous callbacks
    Look across the loader/interceptor stack for std::move(callback_).Run(...) followed by member access without a liveness check.
From e13a7ad5bf7400250dae3fbd081abfdd414321c4 Mon Sep 17 00:00:00 2001
From: Keita Suzuki <[email protected]>
Date: Tue, 30 Jun 2026 19:38:37 -0700
Subject: [PATCH] ServiceWorker: Handle loader detach during fallback callback

ServiceWorkerMainResourceLoader::Fallback() unbinds its Mojo receiver
before running |fallback_callback_|. The callback may synchronously tear
down the navigation loader stack, which calls DetachedFromRequest() and,
with the receiver already unbound, deletes |this|. Guard against this by
taking a WeakPtr before running the callback and returning early if the
loader is gone when it returns.

Add a unit test that detaches the loader from inside the fallback
callback.

TAG=agy
CONV=7fc40a18-110b-4e65-83f9-4a0b829923eb

Bug: 522418913
Change-Id: Ib9e38a6c36d671348ee3d697ffc33b02f52f021e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8017182
Commit-Queue: Keita Suzuki <[email protected]>
Reviewed-by: Shunya Shishido <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1655219}
---
diff --git a/content/browser/service_worker/service_worker_main_resource_loader.cc b/content/browser/service_worker/service_worker_main_resource_loader.cc
index cf6c9e44..09e5d55 100644
--- a/content/browser/service_worker/service_worker_main_resource_loader.cc
+++ b/content/browser/service_worker/service_worker_main_resource_loader.cc
@@ -1062,9 +1062,16 @@
       receiver_.Unbind();
 
   if (fallback_callback_) {
-    if (network::mojom::URLLoaderFactory* factory =
-            std::move(fallback_callback_)
-                .Run(std::move(response_header_params))) {
+    // Running the fallback callback may synchronously destroy the owner of
+    // `this`, which in turn calls `DetachedFromRequest()`. Since `receiver_`
+    // is no longer bound at this point, that triggers `delete this`.
+    base::WeakPtr<ServiceWorkerMainResourceLoader> weak_this = AsWeakPtr();
+    network::mojom::URLLoaderFactory* factory =
+        std::move(fallback_callback_).Run(std::move(response_header_params));
+    if (!weak_this) {
+      return;
+    }
+    if (factory) {
       // Fallback to the default factory, and pass the original parameters/mojo
       // pipes of the initial request received in `StartRequest()`.
       factory->CreateLoaderAndStart(std::move(receiver), request_id_, options_,
diff --git a/content/browser/service_worker/service_worker_main_resource_loader_unittest.cc b/content/browser/service_worker/service_worker_main_resource_loader_unittest.cc
index c24765f9..1f187ec 100644
--- a/content/browser/service_worker/service_worker_main_resource_loader_unittest.cc
+++ b/content/browser/service_worker/service_worker_main_resource_loader_unittest.cc
@@ -1447,6 +1447,43 @@
   EXPECT_FALSE(loader);
 }
 
+// Tests that the loader handles being detached while running the fallback
+// callback. This can happen if the navigation is cancelled while the fallback
+// callback is on the stack.
+TEST_F(ServiceWorkerMainResourceLoaderTest, DetachedDuringFallbackCallback) {
+  service_worker_->RespondWithFallback();
+  SetupNetworkResponse();
+
+  std::unique_ptr<network::ResourceRequest> request = CreateRequest();
+  service_worker_client_ = std::make_unique<ScopedServiceWorkerClient>(
+      CreateServiceWorkerClient(helper_->context(), request->url));
+  service_worker_client()->AddMatchingRegistration(registration_.get());
+  service_worker_client()->SetControllerRegistration(
+      registration_, /*notify_controllerchange=*/false);
+
+  base::RunLoop run_loop;
+  loader_ = std::make_unique<ServiceWorkerMainResourceLoader>(
+      base::BindLambdaForTesting(
+          [&](ResponseHeadUpdateParams) -> network::mojom::URLLoaderFactory* {
+            // Simulate the owning interceptor being torn down while the
+            // fallback callback is running.
+            loader_.release()->DetachedFromRequest();
+            run_loop.Quit();
+            return fake_url_loader_factory_.get();
+          }),
+      /*fetch_event_client_id=*/"", service_worker_client()->AsWeakPtr(),
+      /*find_registration_start_time=*/base::TimeTicks::Now());
+  base::WeakPtr<ServiceWorkerMainResourceLoader> loader = loader_->AsWeakPtr();
+  loader_->StartRequest(loader_remote_.BindNewPipeAndPassReceiver(),
+                        /*request_id=*/0, /*options=*/0, *request,
+                        client_.CreateRemote(),
+                        net::MutableNetworkTrafficAnnotationTag());
+  run_loop.Run();
+
+  // The loader should have been deleted.
+  EXPECT_FALSE(loader);
+}
+
 TEST_F(ServiceWorkerMainResourceLoaderTest, ConnectionErrorDuringFetchEvent) {
   service_worker_->DeferResponse();
   StartRequest(CreateRequest());
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/service_worker/service_worker_main_resource_loader_unittest.cc b/content/browser/service_worker/service_worker_main_resource_loader_unittest.cc
index c24765f9..1f187ec 100644
--- a/content/browser/service_worker/service_worker_main_resource_loader_unittest.cc
+++ b/content/browser/service_worker/service_worker_main_resource_loader_unittest.cc
@@ -1447,6 +1447,43 @@
   EXPECT_FALSE(loader);
 }
 
+// Tests that the loader handles being detached while running the fallback
+// callback. This can happen if the navigation is cancelled while the fallback
+// callback is on the stack.
+TEST_F(ServiceWorkerMainResourceLoaderTest, DetachedDuringFallbackCallback) {
+  service_worker_->RespondWithFallback();
+  SetupNetworkResponse();
+
+  std::unique_ptr<network::ResourceRequest> request = CreateRequest();
+  service_worker_client_ = std::make_unique<ScopedServiceWorkerClient>(
+      CreateServiceWorkerClient(helper_->context(), request->url));
+  service_worker_client()->AddMatchingRegistration(registration_.get());
+  service_worker_client()->SetControllerRegistration(
+      registration_, /*notify_controllerchange=*/false);
+
+  base::RunLoop run_loop;
+  loader_ = std::make_unique<ServiceWorkerMainResourceLoader>(
+      base::BindLambdaForTesting(
+          [&](ResponseHeadUpdateParams) -> network::mojom::URLLoaderFactory* {
+            // Simulate the owning interceptor being torn down while the
+            // fallback callback is running.
+            loader_.release()->DetachedFromRequest();
+            run_loop.Quit();
+            return fake_url_loader_factory_.get();
+          }),
+      /*fetch_event_client_id=*/"", service_worker_client()->AsWeakPtr(),
+      /*find_registration_start_time=*/base::TimeTicks::Now());
+  base::WeakPtr<ServiceWorkerMainResourceLoader> loader = loader_->AsWeakPtr();
+  loader_->StartRequest(loader_remote_.BindNewPipeAndPassReceiver(),
+                        /*request_id=*/0, /*options=*/0, *request,
+                        client_.CreateRemote(),
+                        net::MutableNetworkTrafficAnnotationTag());
+  run_loop.Run();
+
+  // The loader should have been deleted.
+  EXPECT_FALSE(loader);
+}
+
 TEST_F(ServiceWorkerMainResourceLoaderTest, ConnectionErrorDuringFetchEvent) {
   service_worker_->DeferResponse();
   StartRequest(CreateRequest());
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.