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

Overview

Critical
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Workers
DescriptionUse after free in Workers
ComponentWorkers
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: Needs worker renderer process to exit before host teardown (crash/kill); not shown to be trivially web-triggerable, so 'default' overstated. raw_ptr is BRP/MiraclePtr mitigation, not detection. Wrong-partition claim inferred. Sandbox-escape/RCE strength asserted, not shown.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker558456602
Fix commita4c300ba041d (chromium/src) +19/-11
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
CreditedWinD39 - Huynh Dinh Vu
Reported2026-09-08 (per vendor advisory)
Fix landed2026-09-09 (public git)
Disclosed2026-09-15
Patch gap6 days — fix public in source before the advisory

Background

`SharedWorkerHost`
The browser-process object that manages a shared worker’s lifetime and, in CreateLockManager(), registers itself as a Web Locks observer.
`LockManager`
The browser-process service that tracks Web Locks and stores registered observers in its client_observer_map_.
Web Locks observer
A client registered via AddLockObserver() and expected to be torn down via RemoveLockObserver(), which LockManager calls back (e.g. on lock contention).
`StoragePartitionImpl`
The storage context that owns the LockManager, obtained by SharedWorkerHost::GetStoragePartitionImpl().

Root Cause Analysis

SharedWorkerHost::CreateLockManager() unconditionally calls AddLockObserver(token().value(), this), registering the host as an observer in LockManager::client_observer_map_, but ~SharedWorkerHost() called the matching RemoveLockObserver() only inside an if (site_instance_->HasProcess()) block. When a SharedWorkerHost was destroyed after its renderer process had already exited, HasProcess() returned false and deregistration was skipped, leaving a dangling raw pointer to the freed host in client_observer_map_. Compounding this, GetStoragePartitionImpl() derived the partition from GetProcessHost()->GetStoragePartition(), which is unreliable once the process is gone, so even a deregistration attempt could target the wrong partition.

The fix moves the RemoveLockObserver() call out of the HasProcess() guard so it always runs, and reworks GetStoragePartitionImpl() to return service_->storage_partition(), which stays valid for the host’s entire lifetime; client_observer_map_ is additionally hardened to store raw_ptr<Observer> so misuse is caught rather than silently exploited.

Key insight
The single mistake was gating observer removal on site_instance_->HasProcess() while observer registration was unconditional, so a host outliving its process left a stale entry in LockManager::client_observer_map_; the fix always removes the observer and sources the partition from the process-independent service_->storage_partition().

Attack Path

  1. Create a shared worker An attacker page instantiates a shared worker, causing SharedWorkerHost::CreateLockManager() to register the host as a Web Locks observer via AddLockObserver().
  2. Kill the renderer process The renderer hosting the worker is made to exit so that site_instance_->HasProcess() becomes false while the SharedWorkerHost is still pending destruction.
  3. Destroy the host with removal skipped ~SharedWorkerHost() runs but, because of the HasProcess() guard, never calls RemoveLockObserver(), leaving a dangling pointer to the freed host in LockManager::client_observer_map_.
  4. Trigger lock contention Subsequent Web Locks activity causes LockManager to dereference the stale observer pointer (e.g. via OnLockContention()), producing a browser-process heap use-after-free that can be groomed for control.

Impact Assessment

An attacker who can script a shared worker and force its renderer to exit gains a use-after-free on a freed SharedWorkerHost pointer held in the browser-process LockManager::client_observer_map_. Because the dangling access occurs in the privileged browser process during Web Locks contention, it is a strong primitive toward sandbox escape or remote code execution, with the only precondition being the ability to create a shared worker and provoke process teardown.

Files Changed

  • content/browser/worker_host/shared_worker_host.cc
  • content/browser/locks/lock_manager.h
  • content/browser/devtools/shared_worker_devtools_agent_host_unittest.cc
  • content/browser/worker_host/shared_worker_host_unittest.cc

Audit Directions

  • Conditional deregistration
    Look for observer or callback removal gated on a liveness check (like HasProcess()) while the corresponding registration is unconditional, since divergent lifetimes leave stale entries.
  • Process-derived resources at teardown
    Audit accessors like GetStoragePartitionImpl() that reach through GetProcessHost(); prefer owner-held references (e.g. service_->storage_partition()) that survive renderer exit.
  • Raw pointer observer maps
    Search browser-process containers still storing bare Observer* pointers and migrate them to raw_ptr<> to surface use-after-free regressions.
From a4c300ba041d326214536d4bef6d9fa1b73024f7 Mon Sep 17 00:00:00 2001
From: Anna Sato <[email protected]>
Date: Wed, 09 Sep 2026 06:39:46 -0700
Subject: [PATCH] Fix Browser-process Heap-UAF in LockManager caused by stale SharedWorkerHost

SharedWorkerHost unconditionally registers as a Web Locks observer in
CreateLockManager(), but only removed itself in ~SharedWorkerHost()
inside an if (site_instance_->HasProcess()) block. When the worker host
is destroyed after its renderer process has exited, deregistration was
skipped, leaving a dangling pointer in LockManager::client_observer_map_
that triggered a heap-use-after-free during subsequent lock contention.

This CL fixes the issue by updating SharedWorkerHost::GetStoragePartitionImpl()
to return service_->storage_partition(), which is valid throughout the
entire lifetime of SharedWorkerHost, and unconditionally removing the
lock observer in ~SharedWorkerHost(). In addition, client_observer_map_
in LockManager is hardened to use raw_ptr<Observer>.

TAG=agy
CONV=7314e936-8382-47a3-b488-db116be9ff5e

Bug: 558456602
Change-Id: I911a92dc8012917036ab96ddae0ae596e0c58fad
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8370922
Auto-Submit: Anna Sato <[email protected]>
Reviewed-by: Rakina Zata Amni <[email protected]>
Commit-Queue: Rakina Zata Amni <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1694548}
---
diff --git a/content/browser/locks/lock_manager.h b/content/browser/locks/lock_manager.h
index b64da17..e224f9f 100644
--- a/content/browser/locks/lock_manager.h
+++ b/content/browser/locks/lock_manager.h
@@ -119,7 +119,7 @@
   int64_t next_lock_id_ = 0;
   std::map<LockGroupIdType, LockGroupState> lock_groups_;
 
-  base::flat_map<std::string, Observer*> client_observer_map_;
+  base::flat_map<std::string, raw_ptr<Observer>> client_observer_map_;
 
   SEQUENCE_CHECKER(sequence_checker_);
   base::WeakPtrFactory<LockManager> weak_ptr_factory_{this};
diff --git a/content/browser/worker_host/shared_worker_host.cc b/content/browser/worker_host/shared_worker_host.cc
index 03f9f0a..ec04cbd 100644
--- a/content/browser/worker_host/shared_worker_host.cc
+++ b/content/browser/worker_host/shared_worker_host.cc
@@ -226,6 +226,10 @@
     }
   }
 
+  if (auto* lock_manager = GetStoragePartitionImpl()->GetLockManager()) {
+    lock_manager->RemoveLockObserver(token().value());
+  }
+
   if (site_instance_->HasProcess()) {
     // Send any final reports and allow the reporting configuration to be
     // removed.
@@ -238,10 +242,6 @@
 
     GetProcessHost()->RemoveObserver(this);
 
-    if (auto* lock_manager = GetStoragePartitionImpl()->GetLockManager()) {
-      lock_manager->RemoveLockObserver(token().value());
-    }
-
     GetStoragePartitionImpl()->ClearNetworkRestrictionsAfterDelay({
         network_restrictions_id_,
     });
@@ -623,7 +623,7 @@
   GetStoragePartitionImpl()->BindLockManager(
       GetWorkerStorageKey(), token().value(), std::move(receiver));
   GetStoragePartitionImpl()->GetLockManager()->AddLockObserver(token().value(),
-                                                            this);
+                                                               this);
 }
 
 bool SharedWorkerHost::OnLockContention() {
@@ -969,8 +969,7 @@
 }
 
 StoragePartitionImpl* SharedWorkerHost::GetStoragePartitionImpl() {
-  return static_cast<StoragePartitionImpl*>(
-      GetProcessHost()->GetStoragePartition());
+  return service_->storage_partition();
 }
 
 std::vector<GlobalRenderFrameHostId>
diff --git a/content/browser/devtools/shared_worker_devtools_agent_host_unittest.cc b/content/browser/devtools/shared_worker_devtools_agent_host_unittest.cc
index 27ddb8e..68ea745 100644
--- a/content/browser/devtools/shared_worker_devtools_agent_host_unittest.cc
+++ b/content/browser/devtools/shared_worker_devtools_agent_host_unittest.cc
@@ -12,6 +12,7 @@
 #include "content/browser/devtools/shared_worker_devtools_manager.h"
 #include "content/browser/renderer_host/render_process_host_impl.h"
 #include "content/browser/site_instance_impl.h"
+#include "content/browser/storage_partition_impl.h"
 #include "content/browser/worker_host/shared_worker_host.h"
 #include "content/browser/worker_host/shared_worker_service_impl.h"
 #include "content/public/browser/shared_worker_instance.h"
@@ -56,7 +57,10 @@
   RenderProcessHost* rph1 = site_instance1->GetOrCreateProcessForTesting();
   ASSERT_TRUE(rph1->Init());
 
-  SharedWorkerServiceImpl service1(nullptr, nullptr);
+  SharedWorkerServiceImpl service1(
+      static_cast<StoragePartitionImpl*>(
+          regular_context.GetDefaultStoragePartition()),
+      nullptr);
   SharedWorkerInstance instance1(
       kWorkerUrl, blink::mojom::ScriptType::kClassic,
       network::mojom::CredentialsMode::kSameOrigin, "name", storage_key,
@@ -81,7 +85,10 @@
   RenderProcessHost* rph2 = site_instance2->GetOrCreateProcessForTesting();
   ASSERT_TRUE(rph2->Init());
 
-  SharedWorkerServiceImpl service2(nullptr, nullptr);
+  SharedWorkerServiceImpl service2(
+      static_cast<StoragePartitionImpl*>(
+          incognito_context.GetDefaultStoragePartition()),
+      nullptr);
   SharedWorkerInstance instance2(
       kWorkerUrl, blink::mojom::ScriptType::kClassic,
       network::mojom::CredentialsMode::kSameOrigin, "name", storage_key,
diff --git a/content/browser/worker_host/shared_worker_host_unittest.cc b/content/browser/worker_host/shared_worker_host_unittest.cc
index 68eb091d..932f3ba 100644
--- a/content/browser/worker_host/shared_worker_host_unittest.cc
+++ b/content/browser/worker_host/shared_worker_host_unittest.cc
@@ -21,6 +21,7 @@
 #include "content/browser/service_worker/service_worker_context_core.h"
 #include "content/browser/service_worker/service_worker_main_resource_handle.h"
 #include "content/browser/site_instance_impl.h"
+#include "content/browser/storage_partition_impl.h"
 #include "content/browser/worker_host/mock_shared_worker.h"
 #include "content/browser/worker_host/shared_worker_connector_impl.h"
 #include "content/browser/worker_host/shared_worker_service_impl.h"
@@ -74,7 +75,8 @@
   }
 
   SharedWorkerHostTest()
-      : service_(nullptr /* storage_partition */,
+      : service_(static_cast<StoragePartitionImpl*>(
+                     browser_context_.GetDefaultStoragePartition()),
                  nullptr /* service_worker_context */) {}
 
   SharedWorkerHostTest(const SharedWorkerHostTest&) = delete;
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/devtools/shared_worker_devtools_agent_host_unittest.cc b/content/browser/devtools/shared_worker_devtools_agent_host_unittest.cc
index 27ddb8e..68ea745 100644
--- a/content/browser/devtools/shared_worker_devtools_agent_host_unittest.cc
+++ b/content/browser/devtools/shared_worker_devtools_agent_host_unittest.cc
@@ -12,6 +12,7 @@
 #include "content/browser/devtools/shared_worker_devtools_manager.h"
 #include "content/browser/renderer_host/render_process_host_impl.h"
 #include "content/browser/site_instance_impl.h"
+#include "content/browser/storage_partition_impl.h"
 #include "content/browser/worker_host/shared_worker_host.h"
 #include "content/browser/worker_host/shared_worker_service_impl.h"
 #include "content/public/browser/shared_worker_instance.h"
@@ -56,7 +57,10 @@
   RenderProcessHost* rph1 = site_instance1->GetOrCreateProcessForTesting();
   ASSERT_TRUE(rph1->Init());
 
-  SharedWorkerServiceImpl service1(nullptr, nullptr);
+  SharedWorkerServiceImpl service1(
+      static_cast<StoragePartitionImpl*>(
+          regular_context.GetDefaultStoragePartition()),
+      nullptr);
   SharedWorkerInstance instance1(
       kWorkerUrl, blink::mojom::ScriptType::kClassic,
       network::mojom::CredentialsMode::kSameOrigin, "name", storage_key,
@@ -81,7 +85,10 @@
   RenderProcessHost* rph2 = site_instance2->GetOrCreateProcessForTesting();
   ASSERT_TRUE(rph2->Init());
 
-  SharedWorkerServiceImpl service2(nullptr, nullptr);
+  SharedWorkerServiceImpl service2(
+      static_cast<StoragePartitionImpl*>(
+          incognito_context.GetDefaultStoragePartition()),
+      nullptr);
   SharedWorkerInstance instance2(
       kWorkerUrl, blink::mojom::ScriptType::kClassic,
       network::mojom::CredentialsMode::kSameOrigin, "name", storage_key,
diff --git a/content/browser/worker_host/shared_worker_host_unittest.cc b/content/browser/worker_host/shared_worker_host_unittest.cc
index 68eb091d..932f3ba 100644
--- a/content/browser/worker_host/shared_worker_host_unittest.cc
+++ b/content/browser/worker_host/shared_worker_host_unittest.cc
@@ -21,6 +21,7 @@
 #include "content/browser/service_worker/service_worker_context_core.h"
 #include "content/browser/service_worker/service_worker_main_resource_handle.h"
 #include "content/browser/site_instance_impl.h"
+#include "content/browser/storage_partition_impl.h"
 #include "content/browser/worker_host/mock_shared_worker.h"
 #include "content/browser/worker_host/shared_worker_connector_impl.h"
 #include "content/browser/worker_host/shared_worker_service_impl.h"
@@ -74,7 +75,8 @@
   }
 
   SharedWorkerHostTest()
-      : service_(nullptr /* storage_partition */,
+      : service_(static_cast<StoragePartitionImpl*>(
+                     browser_context_.GetDefaultStoragePartition()),
                  nullptr /* service_worker_context */) {}
 
   SharedWorkerHostTest(const SharedWorkerHostTest&) = delete;
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.