Medium chrome UAF 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in V8
DescriptionUse after free in V8
ComponentV8
Bug ClassUAF
Tracker532921336
Fix commita777999ee486 (v8/v8) +91/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Background

`Atomics.wait`
a JavaScript API that blocks the calling agent on a SharedArrayBuffer location until it is notified or times out, implemented in V8 via futex emulation.
`FutexWaitListNode`
a per-Isolate singleton node (reached through isolate->futex_wait_list_node()) that links an isolate into the global FutexWaitList while it is blocked in a wait.
`FutexEmulation::WaitSyncImpl`
the synchronous wait implementation that inserts the node into FutexWaitList and periodically drops the list mutex to run isolate->stack_guard()->HandleInterrupts().
Re-entrancy
nested execution of JavaScript on the same thread that occurs while an outer call is still on the stack, here triggered by an interrupt (e.g. DevTools Runtime.evaluate) fired during a wait.

Root Cause Analysis

The vulnerable path is FutexEmulation::WaitSyncImpl, which registers the isolate’s single FutexWaitListNode into the shared FutexWaitList and then, while still logically “waiting”, releases the FutexWaitList mutex to call isolate->stack_guard()->HandleInterrupts(). The implicit invariant is that a given FutexWaitListNode is linked into the wait list at most once at a time, but nothing enforced this against same-thread re-entrancy. During the interrupt window, re-entrant JavaScript (for example Atomics.wait invoked from a DevTools Runtime.evaluate interrupt on a frozen worker) reaches WaitSyncImpl again and re-uses the same singleton node, double-adding it to the list and corrupting the intrusive linked-list pointers, which produces a use-after-free.

The fix adds an in_sync_wait_ flag on FutexWaitListNode, sets it via a scoped InSyncWaitScope RAII guard on entry to a synchronous wait, and makes the second, re-entrant entry observe node->IsInSyncWait() and cleanly throw a TypeError (MessageTemplate::kAtomicsOperationNotAllowed) instead of re-inserting the node. This works because it detects that the thread is already inside a wait scope before any list mutation occurs, eliminating the double-add entirely.

Key insight
The single core mistake is treating the per-isolate FutexWaitListNode as if it could only ever be in one active synchronous wait, while WaitSyncImpl re-enters JavaScript via HandleInterrupts() and lets a nested Atomics.wait re-use the same node. The fix guards each synchronous wait with an in_sync_wait_ flag (InSyncWaitScope) so a re-entrant call is rejected with a TypeError before it can double-add the node.

Attack Path

  1. Block on a synchronous wait Attacker-controlled JS calls Atomics.wait on a SharedArrayBuffer, causing WaitSyncImpl to insert the isolate’s FutexWaitListNode and mark it waiting.
  2. Force an interrupt window While the node is waiting, WaitSyncImpl releases the FutexWaitList mutex and calls HandleInterrupts(), opening a re-entrancy window on the same thread.
  3. Re-enter via interrupt A same-thread interrupt (e.g. DevTools Runtime.evaluate on a frozen worker) runs nested JS that calls Atomics.wait again, reaching WaitSyncImpl with the same singleton node.
  4. Corrupt the wait list The re-entrant call double-adds the already-linked FutexWaitListNode to FutexWaitList, corrupting its intrusive next/prev pointers.
  5. Trigger use-after-free Subsequent list traversal or unlink operations dereference the corrupted pointers, yielding a use-after-free on the node’s memory.

Impact Assessment

An attacker gains a use-after-free in the V8 heap/wait-list bookkeeping within the renderer (or worker) process running the JavaScript, which can be leveraged toward memory corruption. Preconditions include the ability to run JavaScript that uses SharedArrayBuffer/Atomics.wait and a mechanism to re-enter JS during the wait’s interrupt window (such as a DevTools Runtime.evaluate interrupt on a frozen worker). The severity is rated medium, reflecting the specific re-entrancy conditions required to reach the corrupting path.

Changed Functions

FunctionChangeNotes
InSyncWaitScope
src/execution/futex-emulation.cc
modified
if
src/execution/futex-emulation.cc
modified
ReentrantWaitThread
test/cctest/test-api.cc
modified
TEST
test/cctest/test-api.cc
modified

Files Changed

  • src/execution/futex-emulation.cc
  • src/execution/futex-emulation.h
  • test/cctest/test-api.cc

Audit Directions

  • Interrupt-window re-entrancy
    Audit every code path that drops a mutex to run HandleInterrupts() or otherwise re-enters JS, and verify that singleton per-isolate state cannot be mutated again by a nested call.
  • Per-isolate singleton reuse
    Review other per-Isolate singleton nodes/objects linked into shared lists to confirm they cannot be double-inserted when the same thread re-enters the owning operation.
  • Lock-dropping invariants
    Check that invariants assumed to hold across a temporary mutex release (such as “this node is linked at most once”) are re-validated after HandleInterrupts() or defended by a scope flag rather than only by DCHECKs.
From a777999ee48633dc5587b01440180753a09eac31 Mon Sep 17 00:00:00 2001
From: Leszek Swirski <leszeks@chromium.org>
Date: Fri, 17 Jul 2026 15:31:42 +0200
Subject: [PATCH] [execution] Throw TypeError on re-entrant Atomics.wait calls

Each Isolate owns a single per-isolate FutexWaitListNode instance
accessed via isolate->futex_wait_list_node().

When FutexEmulation::WaitSyncImpl unlocks the FutexWaitList mutex during
isolate->stack_guard()->HandleInterrupts(), re-entrant JS execution
(e.g., via DevTools Runtime.evaluate on frozen workers) can call
Atomics.wait again on the same thread.

This re-entrancy reuses the same singleton FutexWaitListNode,
double-adding it to FutexWaitList and corrupting list pointers, leading
to Use-After-Free.

This CL checks if node->waiting_ is already true on entry to
WaitSyncImpl, and throws a JS TypeError
(MessageTemplate::kAtomicsOperationNotAllowed) to reject re-entrant
synchronous wait calls cleanly.

Bug: 532921336
Change-Id: Id2d2eab9a7fb19f5ce15246692ac2c3693bdc2cb
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8086043
Commit-Queue: Olivier Flückiger <olivf@chromium.org>
Auto-Submit: Leszek Swirski <leszeks@chromium.org>
Reviewed-by: Olivier Flückiger <olivf@chromium.org>
Cr-Commit-Position: refs/heads/main@{#108975}
---

diff --git a/src/execution/futex-emulation.cc b/src/execution/futex-emulation.cc
index 00e0cb4..3d0c691 100644
--- a/src/execution/futex-emulation.cc
+++ b/src/execution/futex-emulation.cc
@@ -324,6 +324,23 @@
                            rel_timeout_ns, CallType::kIsWasm);
 }
 
+namespace {
+class InSyncWaitScope {
+ public:
+  explicit InSyncWaitScope(FutexWaitListNode* node) : node_(node) {
+    DCHECK(!node_->IsInSyncWait());
+    node_->SetInSyncWait(true);
+  }
+  ~InSyncWaitScope() {
+    DCHECK(node_->IsInSyncWait());
+    node_->SetInSyncWait(false);
+  }
+
+ private:
+  FutexWaitListNode* node_;
+};
+}  // namespace
+
 #if V8_ENABLE_WEBASSEMBLY
 Tagged<Object> FutexEmulation::WaitWasmManagedObject(
     Isolate* isolate, Tagged<HeapObject> object, int32_t offset,
@@ -334,6 +351,12 @@
       base::TimeDelta::FromNanoseconds(rel_timeout_ns);
 
   FutexWaitListNode* node = isolate->futex_wait_list_node();
+  if (node->IsInSyncWait()) {
+    return isolate->Throw(*isolate->factory()->NewTypeError(
+        MessageTemplate::kAtomicsOperationNotAllowed,
+        isolate->factory()->NewStringFromAsciiChecked("Atomics.wait")));
+  }
+  InSyncWaitScope wait_scope(node);
 
   bool use_timeout = rel_timeout_ns >= 0;
 
@@ -401,6 +424,13 @@
   FutexWaitList* wait_list = GetWaitList();
   FutexWaitListNode* node = isolate->futex_wait_list_node();
 
+  if (node->IsInSyncWait()) {
+    return isolate->Throw(*isolate->factory()->NewTypeError(
+        MessageTemplate::kAtomicsOperationNotAllowed,
+        isolate->factory()->NewStringFromAsciiChecked("Atomics.wait")));
+  }
+  InSyncWaitScope wait_scope(node);
+
   base::TimeTicks timeout_time;
   if (use_timeout) {
     base::TimeTicks current_time = base::TimeTicks::Now();
@@ -430,9 +460,10 @@
     NoGarbageCollectionMutexGuard& lock_guard, bool use_timeout,
     base::TimeTicks timeout_time, T value, T loaded_value,
     std::optional<void*> wait_location) {
+  DCHECK(!node->IsWaiting());
+
   DirectHandle<Object> result;
   if (loaded_value != value) {
-    DCHECK(!node->waiting_);
     return direct_handle(Smi::FromInt(WaitReturnValue::kNotEqualValue),
                          isolate);
   }
diff --git a/src/execution/futex-emulation.h b/src/execution/futex-emulation.h
index 8c1bb3d..baad1a2 100644
--- a/src/execution/futex-emulation.h
+++ b/src/execution/futex-emulation.h
@@ -64,6 +64,9 @@
   void NotifyWake();
 
   bool IsAsync() const { return async_state_ != nullptr; }
+  bool IsWaiting() const { return waiting_.load(std::memory_order_relaxed); }
+  bool IsInSyncWait() const { return in_sync_wait_; }
+  void SetInSyncWait(bool v) { in_sync_wait_ = v; }
 
   // Returns false if the cancelling failed, true otherwise.
   bool CancelTimeoutTask();
@@ -139,9 +142,13 @@
   // this node is alive.
   void* wait_location_ = nullptr;
 
-  // waiting_ and interrupted_ are protected by `GetWaitList()::mutex()`.
-  bool waiting_ = false;
+  // waiting_ is std::atomic<bool> to allow safe relaxed reads outside mutex
+  // locks.
+  std::atomic<bool> waiting_{false};
   bool interrupted_ = false;
+  // in_sync_wait_ tracks whether the isolate thread is executing inside a
+  // WaitSync scope. Modified exclusively by the isolate thread itself.
+  bool in_sync_wait_ = false;
 
   // State used for an async wait; nullptr on sync waits.
   const std::unique_ptr<AsyncState> async_state_;
diff --git a/test/cctest/test-api.cc b/test/cctest/test-api.cc
index 854af64..35eb589 100644
--- a/test/cctest/test-api.cc
+++ b/test/cctest/test-api.cc
@@ -26207,6 +26207,56 @@
   timeout_thread.Join();
 }
 
+namespace {
+class ReentrantWaitThread : public v8::base::Thread {
+ public:
+  explicit ReentrantWaitThread(v8::Isolate* isolate)
+      : Thread(Options("ReentrantWaitThread")), isolate_(isolate) {}
+
+  static void InterruptCallback(v8::Isolate* isolate, void* data) {
+    v8::HandleScope scope(isolate);
+    v8::TryCatch try_catch(isolate);
+    CompileRun(
+        "var sab2 = new SharedArrayBuffer(4);"
+        "var i32a2 = new Int32Array(sab2);"
+        "Atomics.wait(i32a2, 0, 0, 10);");
+
+    CHECK(try_catch.HasCaught());
+    v8::String::Utf8Value exception_msg(isolate, try_catch.Exception());
+    CHECK_NOT_NULL(strstr(*exception_msg, "cannot be called in this context"));
+  }
+
+  void Run() override {
+    i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate_);
+    // Wait until main thread enters WaitSyncImpl and marks node->waiting_ =
+    // true
+    while (!i_isolate->futex_wait_list_node()->IsWaiting()) {
+      v8::base::OS::Sleep(v8::base::TimeDelta::FromMilliseconds(1));
+    }
+    isolate_->RequestInterrupt(InterruptCallback, nullptr);
+  }
+
+ private:
+  v8::Isolate* isolate_;
+};
+}  // namespace
+
+TEST(FutexReentrantWait) {
+  v8::Isolate* isolate = CcTest::isolate();
+  v8::HandleScope scope(isolate);
+  LocalContext env;
+
+  ReentrantWaitThread thread(isolate);
+  CHECK(thread.Start());
+
+  CompileRun(
+      "var ab = new SharedArrayBuffer(4);"
+      "var i32a = new Int32Array(ab);"
+      "Atomics.wait(i32a, 0, 0, 500);");
+
+  thread.Join();
+}
+
 TEST(StackCheckTermination) {
   v8::Isolate* isolate = CcTest::isolate();
   i::Isolate* i_isolate = CcTest::i_isolate();
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.