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 <annasato@chromium.org>
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 <annasato@chromium.org>
Reviewed-by: Rakina Zata Amni <rakina@chromium.org>
Commit-Queue: Rakina Zata Amni <rakina@chromium.org>
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;