High watch CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Profiler
DescriptionUse after free in Profiler
ComponentProfiler
Sub-componentSsm
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
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.
Tracker421471016
Fix commite9e247c09b76 (chromium/src) +20/-8
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedChaoyuan Peng (@ret2happy)
Reported2025-05-31 · bounty $4000 (per vendor advisory)
Fix landed2025-06-05 (public git)
Disclosed2025-06-17
Patch gap12 days — fix public in source before the advisory

Background

`ModuleCache`
A base/profiler class that maps instruction addresses to Module objects, splitting them into a sorted native_modules_ set and a non_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 ModuleCache from a dedicated sampling thread while the owning thread may concurrently update the non-native module set, so the two access non_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.

Key insight
A concurrently shared container (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

  1. Register non-native modules Trigger runtime generation of non-native (JIT/dynamic) code so that entries are added to non_native_modules_ via UpdateNonNativeModules. (inferred — the diff shows the mutating path but not the specific trigger.)
  2. Induce concurrent profiling Cause the sampling profiler to walk stacks so that GetModules / GetExistingModuleForAddress iterate and return pointers from non_native_modules_ on the sampling thread. (inferred)
  3. Race the update against the read Time an UpdateNonNativeModules call (which reorders, moves to inactive, and erases modules) to overlap a reader, so the reader dereferences or returns a Module* whose storage is being erased/relocated.
  4. Use-after-free The freed/moved Module object 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

An attacker who can influence non-native module registration and profiling activity can win a race that dereferences a freed 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

FunctionChangeNotes
for
base/profiler/module_cache.cc
modified

Files Changed

  • base/profiler/module_cache.cc
  • base/profiler/module_cache.h

Audit Directions

  • Cross-thread container sharing
    Look for ModuleCache-like caches or registries whose members are read on a profiler/sampler/worker thread but mutated elsewhere without a lock or GUARDED_BY annotation.
  • Returned pointers into mutable containers
    Audit 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 annotations
    Search base/profiler and similar low-level utilities for fields lacking GUARDED_BY and methods lacking LOCKS_EXCLUDED/EXCLUSIVE_LOCKS_REQUIRED, which flag unsynchronized shared state.
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,
Loading diff…

Original Bug Report

reported by ha...@gmail.com

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

View on issue tracker
Uploaded PoC files