98e3a15c06760ec363c5e25620b9469a5572f84f [M153] Fix Browser-process Heap-UAF in LockManager caused by stale SharedWorkerHost
The bug at a glance
This is an M153 security cherry-pick fixing a confirmed browser-process heap-use-after-free: SharedWorkerHost always registered as a Web Locks observer but only deregistered when its renderer process still had a process, leaving a dangling Observer* in LockManager::client_observer_map_ that is dereferenced during later lock contention; browser-process UAF reachable from web content is high severity.
A page can spawn a shared worker that acquires a Web Lock (registering the host as a lock observer), then cause the renderer process to exit before the SharedWorkerHost is destroyed, leaving a stale observer pointer in the browser’s LockManager that is dereferenced on the next lock-contention event, giving an attacker a use-after-free in the privileged browser process.
Root cause
SharedWorkerHost::CreateLockManager() unconditionally called LockManager::AddLockObserver(token, this), but ~SharedWorkerHost() only called RemoveLockObserver inside an if (site_instance_->HasProcess()) block. When the host outlived its renderer process, that block was skipped and the observer was never removed, so LockManager::client_observer_map_ retained a dangling Observer* to the freed host; a later lock-contention notification dereferenced it (heap-UAF). The pre-patch GetStoragePartitionImpl() also derived the storage partition from GetProcessHost(), which is invalid once the process is gone. The fix moves the RemoveLockObserver call out of the HasProcess()-guarded block so deregistration is unconditional, repoints GetStoragePartitionImpl() to service_->storage_partition() (valid for the host’s whole lifetime), and hardens client_observer_map_ to raw_ptr for MiraclePtr protection.
Key code
Unconditional lock-observer deregistration moved out of HasProcess() guard
+ if (auto* lock_manager = GetStoragePartitionImpl()->GetLockManager()) {
+ lock_manager->RemoveLockObserver(token().value());
+ }
+
if (site_instance_->HasProcess()) {
Patch walkthrough
content/browser/worker_host/shared_worker_host.cc— Moves RemoveLockObserver out of the if (site_instance_->HasProcess()) block so the stale observer is always deregistered in ~SharedWorkerHost(), and changes GetStoragePartitionImpl() to return service_->storage_partition() (valid for the full host lifetime) instead of deriving it from the possibly-gone process host; this is the core UAF fix.content/browser/locks/lock_manager.h— Changes client_observer_map_ from std::map value Observer* to raw_ptr, adding MiraclePtr (BackupRefPtr) protection that turns a dangling-pointer dereference into a safe crash rather than exploitable UAF.content/browser/worker_host/shared_worker_host_unittest.cc— Constructs the test SharedWorkerServiceImpl with a real default StoragePartition so GetStoragePartitionImpl() via service_ works, exercising the fixed code path.content/browser/devtools/shared_worker_devtools_agent_host_unittest.cc— Updates two SharedWorkerServiceImpl constructions to pass real storage partitions (regular and incognito) consistent with the storage-partition sourcing change.
Background
SharedWorkerHost — Browser-process object that manages a shared worker’s lifetime and brokers its capabilities, including Web Locks.
LockManager / Web Locks — Browser-process implementation of the Web Locks API where clients register as observers to be notified of lock contention.
raw_ptr / MiraclePtr — Chromium’s BackupRefPtr-backed smart pointer that detects dangling-pointer dereferences and converts many UAFs into non-exploitable crashes.
StoragePartition lifetime — Deriving the partition from the process host is unsafe after the renderer exits; the SharedWorkerService’s partition stays valid for the host’s full lifetime.
Vulnerability window
- Reachability — A web page creates a shared worker, which in the browser instantiates a SharedWorkerHost and calls CreateLockManager(), registering the host as a Web Locks observer.
- Trigger — The renderer process backing the worker exits (crash/kill/navigation teardown) so site_instance_->HasProcess() becomes false before ~SharedWorkerHost() runs.
- Primitive — ~SharedWorkerHost() skips RemoveLockObserver, leaving a dangling Observer* for the freed host in LockManager::client_observer_map_.
- Escalation — A subsequent lock-contention notification dereferences the stale observer, a browser-process heap-UAF potentially leveraged for corruption (inferred).
Proof of concept
Conceptually: open a page that starts a shared worker which requests a Web Lock (registering its host as a lock observer in the browser), then force the worker’s renderer process to terminate so the host outlives its process, then from another same-partition client acquire/contend on the same lock scope to trigger a notification to the now-stale observer. No exploit code is provided; the fix deregisters unconditionally and hardens the map pointer.
Exploitation
- Register — Shared worker requests a Web Lock, causing CreateLockManager() to add its host as an observer.
- Free — Renderer process exits so SharedWorkerHost is torn down without deregistering, freeing the host while the Observer* lingers.
- Reuse — Lock contention invokes the dangling observer pointer, dereferencing freed browser-process memory (inferred).
- Control — Heap grooming to reclaim the freed slot would be needed to turn the UAF into controlled corruption (inferred).
Detection & hunting
For defenders and detection engineers:
- grep for AddLockObserver / RemoveLockObserver pairing and ‘if (site_instance_->HasProcess())’ guards around cleanup — Flag any observer/registration added unconditionally but removed only under a conditional (process-alive) guard in a destructor.
- ASAN heap-use-after-free in LockManager:: with SharedWorkerHost in the free stack* — Hunt crash reports/fuzzer logs showing LockManager dereferences of a freed SharedWorkerHost observer.
Audit directions
- *content/browser/worker_host and other Host destructors — Audit for registrations (observers, callbacks, mojo receivers) done unconditionally but unregistered only inside HasProcess()/IsAlive() conditionals, a recurring stale-pointer pattern.
- GetStoragePartition() derived from GetProcessHost() — Look for other hosts that fetch the storage partition via the process host, which becomes invalid after the renderer exits.