CVE-2026-91749
Overview
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 viaRemoveLockObserver(), whichLockManagercalls back (e.g. on lock contention). - `StoragePartitionImpl`
- The storage context that owns the
LockManager, obtained bySharedWorkerHost::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.
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
- Create a shared worker
An attacker page instantiates a shared worker, causing
SharedWorkerHost::CreateLockManager()to register the host as a Web Locks observer viaAddLockObserver(). - Kill the renderer process
The renderer hosting the worker is made to exit so that
site_instance_->HasProcess()becomes false while theSharedWorkerHostis still pending destruction. - Destroy the host with removal skipped
~SharedWorkerHost()runs but, because of theHasProcess()guard, never callsRemoveLockObserver(), leaving a dangling pointer to the freed host inLockManager::client_observer_map_. - Trigger lock contention
Subsequent Web Locks activity causes
LockManagerto dereference the stale observer pointer (e.g. viaOnLockContention()), producing a browser-process heap use-after-free that can be groomed for control.
Impact Assessment
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.cccontent/browser/locks/lock_manager.hcontent/browser/devtools/shared_worker_devtools_agent_host_unittest.cccontent/browser/worker_host/shared_worker_host_unittest.cc
Audit Directions
- Conditional deregistrationLook 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 teardownAudit accessors like
GetStoragePartitionImpl()that reach throughGetProcessHost(); prefer owner-held references (e.g.service_->storage_partition()) that survive renderer exit. - Raw pointer observer mapsSearch browser-process containers still storing bare
Observer*pointers and migrate them toraw_ptr<>to surface use-after-free regressions.
Patch
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;
Regression Test / PoC
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;