CVE-2026-95280
Overview
Background
- `NativeModule`
- V8’s per-module container for compiled WebAssembly code and associated metadata, shared across the threads that compile, execute, and debug a Wasm module.
- `WasmModuleSourceMap`
- a parsed source map that maps generated Wasm offsets back to original source locations, attached to a
NativeModuleand consumed by DevTools when debugging. - `allocation_mutex_`
- the
NativeModulemutex (taken viabase::RecursiveMutexGuard) that serializes access to the module’s mutable allocation-related member group. - Data race / UAF
- concurrent unsynchronized reads/writes to shared state; here a
std::unique_ptrbeing moved from multiple threads can free memory another thread still references.
Root Cause Analysis
NativeModule::SetWasmSourceMap assigned source_map_ = std::move(source_map) with no lock, and source_map_ lived outside the group of members protected by allocation_mutex_. When two threads set a source map on the same NativeModule concurrently, both could std::move into source_map_: the assignment destroys the previously held WasmModuleSourceMap while another thread may still be overwriting or reading it, producing a data race and a potential use-after-free / double-free of the source map object. GetWasmSourceMap also read source_map_.get() without holding the mutex, so a reader could observe a torn pointer or dereference an object being concurrently freed by a writer.
The fix renames the setter to SetWasmSourceMapIfUnset, takes base::RecursiveMutexGuard guard(&allocation_mutex_), and returns early if source_map_ is already set, making the operation both synchronized and idempotent. GetWasmSourceMap now takes the same guard so readers no longer race with writers. source_map_ was moved into the mutex-protected member group to make the invariant explicit.
std::unique_ptr member that can be assigned from more than one thread is a latent use-after-free, because the move-assignment silently frees the prior pointee; adding the mutex plus an idempotent set-if-unset both serializes writes and prevents any legitimate second write from clobbering a live source map.Attack Path
- Trigger concurrent source map installs
Cause a Wasm module to have its source map set from two code paths at nearly the same time so both reach the unguarded
SetWasmSourceMapon the sameNativeModule. - Race the writes
Both threads execute
source_map_ = std::move(...)concurrently; one frees theWasmModuleSourceMapthe other just stored or is storing. - Race a reader
A thread calling the unguarded
GetWasmSourceMapobtains a pointer to aWasmModuleSourceMapthat a writer thread is concurrently freeing. - Use freed memory
The stale
WasmModuleSourceMap*is dereferenced (or the object double-freed), giving an attacker use-after-free primitives if the freed region is reclaimed with controlled data.
Impact Assessment
WasmModuleSourceMap still in use, potentially leading to memory corruption. Reachability is constrained: source maps are installed and consumed through the DevTools/debugging path rather than ordinary script execution, so triggering the race in practice requires the module to have a source map set concurrently, which narrows exposure relative to a purely script-reachable bug.Files Changed
src/wasm/wasm-code-manager.ccsrc/wasm/wasm-code-manager.h
Audit Directions
- Unguarded `unique_ptr` membersSearch
NativeModule(and other shared V8 objects) forstd::unique_ptr/pointer members assigned outside their mutex; any that can be written from more than one thread risk the same move-assignment UAF. - Getter/setter mutex asymmetryFlag getters like the old
GetWasmSourceMapthat read a member without the lock its setter should hold; readers racing writers is as exploitable as racing writes. - Set-once semanticsFor fields intended to be initialized once, prefer an idempotent set-if-unset under a lock over plain assignment, so re-entry or duplicate installation cannot free a live object.
Patch
From 695bea76a654b7f11bcfde1d898cf44716f83648 Mon Sep 17 00:00:00 2001 From: Jakob Kummerow <[email protected]> Date: Tue, 15 Sep 2026 16:01:53 +0200 Subject: [PATCH] [wasm] Fix race in SetWasmSourceMap Setting a source map lacked synchronization, so multiple threads trying to do that at the same time could clash. Fix that by adding a mutex guard. Fixed: 560406548 Change-Id: Ib562b77429b3921ea7f0f81ae4882a7c9bbbc68d Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8412287 Auto-Submit: Jakob Kummerow <[email protected]> Reviewed-by: Matthias Liedtke <[email protected]> Commit-Queue: Jakob Kummerow <[email protected]> Commit-Queue: Matthias Liedtke <[email protected]> Cr-Commit-Position: refs/heads/main@{#109844} --- diff --git a/src/wasm/wasm-code-manager.cc b/src/wasm/wasm-code-manager.cc index c3c7924..dbfcfa9 100644 --- a/src/wasm/wasm-code-manager.cc +++ b/src/wasm/wasm-code-manager.cc @@ -431,7 +431,7 @@ load_wasm_source_map(v8_isolate, external_url_string.c_str()); if (source_map_str.IsEmpty()) return; - native_module_->SetWasmSourceMap( + native_module_->SetWasmSourceMapIfUnset( std::make_unique<WasmModuleSourceMap>(v8_isolate, source_map_str)); } @@ -1741,12 +1741,15 @@ code_table_[declared_function_index(module(), index)]->tier() == tier; } -void NativeModule::SetWasmSourceMap( +void NativeModule::SetWasmSourceMapIfUnset( std::unique_ptr<WasmModuleSourceMap> source_map) { + base::RecursiveMutexGuard guard(&allocation_mutex_); + if (source_map_) return; source_map_ = std::move(source_map); } WasmModuleSourceMap* NativeModule::GetWasmSourceMap() const { + base::RecursiveMutexGuard guard(&allocation_mutex_); return source_map_.get(); } diff --git a/src/wasm/wasm-code-manager.h b/src/wasm/wasm-code-manager.h index 136c12d..bab5eca 100644 --- a/src/wasm/wasm-code-manager.h +++ b/src/wasm/wasm-code-manager.h @@ -736,7 +736,9 @@ bool HasCode(uint32_t index) const; bool HasCodeWithTier(uint32_t index, ExecutionTier tier) const; - void SetWasmSourceMap(std::unique_ptr<WasmModuleSourceMap> source_map); + // Protected by {allocation_mutex_}. + // Does nothing if {source_map_} is already populated. + void SetWasmSourceMapIfUnset(std::unique_ptr<WasmModuleSourceMap> source_map); WasmModuleSourceMap* GetWasmSourceMap() const V8_LIFETIME_BOUND; Address jump_table_start() const { @@ -1048,8 +1050,6 @@ // tasks can keep this alive. std::shared_ptr<const WasmModule> module_; - std::unique_ptr<WasmModuleSourceMap> source_map_; - // Wire bytes, held in a shared_ptr so they can be kept alive by the // {WireBytesStorage}, held by background compile tasks. std::shared_ptr<base::OwnedVector<const uint8_t>> wire_bytes_; @@ -1125,6 +1125,8 @@ // mutex. std::unique_ptr<DebugInfo> debug_info_; + std::unique_ptr<WasmModuleSourceMap> source_map_; + std::unique_ptr<NamesProvider> names_provider_; // {WasmCodeAllocator} manages all code reservations and allocations for this