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 <jkummerow@chromium.org>
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 <jkummerow@chromium.org>
Reviewed-by: Matthias Liedtke <mliedtke@chromium.org>
Commit-Queue: Jakob Kummerow <jkummerow@chromium.org>
Commit-Queue: Matthias Liedtke <mliedtke@chromium.org>
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