CVE-2025-6192
Overview
Background
- `ModuleCache`
- A
base/profilerclass that maps instruction addresses toModuleobjects, splitting them into a sortednative_modules_set and anon_native_modules_flat_set. - Non-native modules
- Regions of dynamically generated executable code (e.g., JIT output) registered at runtime via
UpdateNonNativeModules, which can be removed and moved to an inactive list when they are no longer mapped. - Sampling profiler threading
- The stack sampler reads the
ModuleCachefrom a dedicated sampling thread while the owning thread may concurrently update the non-native module set, so the two accessnon_native_modules_in parallel. - `base::AutoLock` / `GUARDED_BY`
- An RAII lock guard plus a Clang thread-safety annotation that statically requires a field to be accessed only while its lock is held.
Root Cause Analysis
The non_native_modules_ flat_set was read by GetModules and GetExistingModuleForAddress and mutated by UpdateNonNativeModules with no synchronization, even though these run on different threads. UpdateNonNativeModules reorders the to-be-removed modules to the end of the set, moves them into the inactive list, and erases the moved-from entries, which can invalidate iterators and free the underlying Module objects that a concurrent reader is simultaneously iterating or has just returned a pointer to. Because the reader could obtain and dereference a Module* whose backing storage was being erased/relocated, this is a data race that manifests as a use-after-free.
The fix introduces a mutable base::Lock lock_ that guards non_native_modules_ (GUARDED_BY(lock_)) and takes base::AutoLock around every access to that set, serializing readers against the mutating UpdateNonNativeModules. With all accesses to the container mutually excluded, no reader can observe or hold a pointer into the set while it is being modified.
non_native_modules_) was mutated on one thread while read on another with no lock, so its element pointers could be freed mid-use; the fix serializes all access behind a new base::Lock and enforces it with GUARDED_BY/LOCKS_EXCLUDED annotations.Attack Path
- Register non-native modules
Trigger runtime generation of non-native (JIT/dynamic) code so that entries are added to
non_native_modules_viaUpdateNonNativeModules. (inferred — the diff shows the mutating path but not the specific trigger.) - Induce concurrent profiling
Cause the sampling profiler to walk stacks so that
GetModules/GetExistingModuleForAddressiterate and return pointers fromnon_native_modules_on the sampling thread. (inferred) - Race the update against the read
Time an
UpdateNonNativeModulescall (which reorders, moves to inactive, and erases modules) to overlap a reader, so the reader dereferences or returns aModule*whose storage is being erased/relocated. - Use-after-free
The freed/moved
Moduleobject is accessed through the stale pointer, corrupting profiler state; further conversion of this dangling access into a controlled read/write primitive is not shown by the patch (inferred).
Impact Assessment
Module object inside the process hosting the profiler, yielding a use-after-free. The advisory classifies this as a use-after-free in Profiler (bug 421471016); it does not state in-the-wild exploitation, and any escalation to a controlled read/write, information disclosure, or code execution is (inferred) rather than demonstrated by the patch. Preconditions are the ability to drive concurrent module updates and sampling so the unsynchronized accesses overlap.Changed Functions
| Function | Change | Notes |
|---|---|---|
forbase/profiler/module_cache.cc |
modified |
Files Changed
base/profiler/module_cache.ccbase/profiler/module_cache.h
Audit Directions
- Cross-thread container sharingLook for
ModuleCache-like caches or registries whose members are read on a profiler/sampler/worker thread but mutated elsewhere without a lock orGUARDED_BYannotation. - Returned pointers into mutable containersAudit accessors that return raw
T*/iterators into sets/vectors that another code path can erase, move, or reallocate, since the returned pointer can dangle even under partial locking. - Missing thread-safety annotationsSearch
base/profilerand similar low-level utilities for fields lackingGUARDED_BYand methods lackingLOCKS_EXCLUDED/EXCLUSIVE_LOCKS_REQUIRED, which flag unsynchronized shared state.
Patch
From e9e247c09b76dc6813c70d2c7007438c830bea56 Mon Sep 17 00:00:00 2001
From: Thiabaud Engelbrecht <thiabaud@google.com>
Date: Thu, 05 Jun 2025 11:18:54 -0700
Subject: [PATCH] [ssm] Fix race condition
This CL fixes a threading issue, by using locks to prevent racy access.
Bug: 421471016
Change-Id: If41796b9eb49aa3f27df175c6be198f47947c4c0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6620528
Reviewed-by: Jean-Philippe Gravel <jpgravel@chromium.org>
Commit-Queue: Thiabaud Engelbrecht <thiabaud@google.com>
Cr-Commit-Position: refs/heads/main@{#1470083}
---
diff --git a/base/profiler/module_cache.cc b/base/profiler/module_cache.cc
index 46f0f19..31d4753 100644
--- a/base/profiler/module_cache.cc
+++ b/base/profiler/module_cache.cc
@@ -90,6 +90,7 @@
for (const std::unique_ptr<const Module>& module : native_modules_) {
result.push_back(module.get());
}
+ base::AutoLock locker(lock_);
for (const std::unique_ptr<const Module>& module : non_native_modules_) {
result.push_back(module.get());
}
@@ -103,6 +104,7 @@
flat_set<const Module*> defunct_modules_set(defunct_modules.begin(),
defunct_modules.end());
+ base::AutoLock locker(lock_);
// Reorder the modules to be removed to the last slots in the set, then move
// them to the inactive modules, then erase the moved-from modules from the
// set. This is a variation on the standard erase-remove idiom, which is
@@ -153,9 +155,12 @@
const ModuleCache::Module* ModuleCache::GetExistingModuleForAddress(
uintptr_t address) const {
- const auto non_native_module_loc = non_native_modules_.find(address);
- if (non_native_module_loc != non_native_modules_.end()) {
- return non_native_module_loc->get();
+ {
+ base::AutoLock locker(lock_);
+ const auto non_native_module_loc = non_native_modules_.find(address);
+ if (non_native_module_loc != non_native_modules_.end()) {
+ return non_native_module_loc->get();
+ }
}
const auto native_module_loc = native_modules_.find(address);
diff --git a/base/profiler/module_cache.h b/base/profiler/module_cache.h
index 0a3ca02..14de5781 100644
--- a/base/profiler/module_cache.h
+++ b/base/profiler/module_cache.h
@@ -15,6 +15,8 @@
#include "base/containers/flat_set.h"
#include "base/files/file_path.h"
#include "base/memory/raw_ptr.h"
+#include "base/synchronization/lock.h"
+#include "base/thread_annotations.h"
#include "build/build_config.h"
#if BUILDFLAG(IS_WIN)
@@ -94,8 +96,8 @@
// Gets the module containing |address| or nullptr if |address| is not within
// a module. The returned module remains owned by and has the same lifetime as
// the ModuleCache object.
- const Module* GetModuleForAddress(uintptr_t address);
- std::vector<const Module*> GetModules() const;
+ const Module* GetModuleForAddress(uintptr_t address) LOCKS_EXCLUDED(lock_);
+ std::vector<const Module*> GetModules() const LOCKS_EXCLUDED(lock_);
// Updates the set of non-native modules maintained by the
// ModuleCache. Non-native modules represent regions of non-native executable
@@ -116,7 +118,8 @@
// the same call.
void UpdateNonNativeModules(
const std::vector<const Module*>& defunct_modules,
- std::vector<std::unique_ptr<const Module>> new_modules);
+ std::vector<std::unique_ptr<const Module>> new_modules)
+ LOCKS_EXCLUDED(lock_);
// Adds a custom native module to the cache. This is intended to support
// native modules that require custom handling. In general, native modules
@@ -144,7 +147,8 @@
// NOTE: Only users that create their own modules and need control over native
// module creation should use this function. Everyone else should use
// GetModuleForAddress().
- const Module* GetExistingModuleForAddress(uintptr_t address) const;
+ const Module* GetExistingModuleForAddress(uintptr_t address) const
+ LOCKS_EXCLUDED(lock_);
private:
// Heterogenously compares modules by base address, and modules and
@@ -173,6 +177,9 @@
std::set<std::unique_ptr<const Module>, ModuleAndAddressCompare>
native_modules_;
+ // Lock to guard |non_native_modules_|.
+ mutable base::Lock lock_;
+
// Set of non-native modules currently mapped into the address space, sorted
// by base address. Represented as flat_set because std::set does not support
// extracting move-only element types prior to C++17's
@@ -182,7 +189,7 @@
// native_modules_ to support preferential lookup of non-native modules
// embedded in native modules; see comment on UpdateNonNativeModules().
base::flat_set<std::unique_ptr<const Module>, ModuleAndAddressCompare>
- non_native_modules_;
+ non_native_modules_ GUARDED_BY(lock_);
// Unsorted vector of inactive non-native modules. Inactive modules are no
// longer mapped in the address space and don't participate in address lookup,
Original Bug Report
UAF in StackSampler
Steps to reproduce the problem
Will attach details soon. See asan.txt for stack trace
Problem Description
UAF in StackSampler
Summary
UAF in StackSampler
Custom Questions
Type of crash:
browser UAF
Crash state:
See asan.txt
Additional Data
Category: Security
Chrome Channel: Canary
Regression: N/A