Overview

High
Severity
7.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactRace condition in V8
DescriptionRace condition in V8
ComponentV8
Sub-componentWASM
Bug ClassRace
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.
Tracker560406548
Fix commit695bea76a654 (v8/v8) +10/-5
CWECWE-362 (Race condition)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-09-12 (per vendor advisory)
Fix landed2026-09-15 (public git)
Disclosed2026-09-22
Patch gap7 days — fix public in source before the advisory

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 NativeModule and consumed by DevTools when debugging.
`allocation_mutex_`
the NativeModule mutex (taken via base::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_ptr being 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.

Key insight
A 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

  1. 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 SetWasmSourceMap on the same NativeModule.
  2. Race the writes Both threads execute source_map_ = std::move(...) concurrently; one frees the WasmModuleSourceMap the other just stored or is storing.
  3. Race a reader A thread calling the unguarded GetWasmSourceMap obtains a pointer to a WasmModuleSourceMap that a writer thread is concurrently freeing.
  4. 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

This is a race-condition use-after-free in V8’s Wasm code manager that could corrupt or free a 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.cc
  • src/wasm/wasm-code-manager.h

Audit Directions

  • Unguarded `unique_ptr` members
    Search NativeModule (and other shared V8 objects) for std::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 asymmetry
    Flag getters like the old GetWasmSourceMap that read a member without the lock its setter should hold; readers racing writers is as exploitable as racing writes.
  • Set-once semantics
    For 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.
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
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.