Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactRace condition in V8
DescriptionRace condition in V8
ComponentV8
Sub-componentSandbox
Bug ClassRace
Tracker554619028
Fix commit5a79228e0ac5 (v8/v8) +46/-26
CISA KEVNot listed
Creditedflyyy
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
if
src/objects/js-array-buffer-inl.h
modified
if
src/objects/js-array-buffer.cc
modified

Files Changed

  • src/sandbox/external-pointer-table-inl.h
  • src/objects/js-array-buffer.h
  • src/base/atomic-utils.h
  • src/objects/js-array-buffer-inl.h
  • src/objects/js-array-buffer.cc
From 5a79228e0ac524c389cfd2c216d47d38ae3adae2 Mon Sep 17 00:00:00 2001
From: Omer Katz <[email protected]>
Date: Wed, 09 Sep 2026 15:12:11 +0200
Subject: [PATCH] [sandbox] Avoid double read of JSArrayBuffer::extension_

Bug: 554619028
Change-Id: Id2e89d46581acddba586286ab3cb1980bb617019
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8365887
Reviewed-by: Maksim Ivanov <[email protected]>
Reviewed-by: Michael Lippautz <[email protected]>
Commit-Queue: Omer Katz <[email protected]>
Cr-Commit-Position: refs/heads/main@{#109748}
---
diff --git a/src/base/atomic-utils.h b/src/base/atomic-utils.h
index 3605230..4959538 100644
--- a/src/base/atomic-utils.h
+++ b/src/base/atomic-utils.h
@@ -112,8 +112,15 @@
   template <typename T>
   static T SeqCst_Swap(T* addr, std::remove_reference_t<T> new_value) {
     static_assert(sizeof(T) <= sizeof(AtomicStorageType));
-    return base::SeqCst_AtomicExchange(
-        to_storage_addr(addr), cast_helper<T>::to_storage_type(new_value));
+    return cast_helper<T>::to_return_type(base::SeqCst_AtomicExchange(
+        to_storage_addr(addr), cast_helper<T>::to_storage_type(new_value)));
+  }
+
+  template <typename T>
+  static T Relaxed_Swap(T* addr, std::remove_reference_t<T> new_value) {
+    static_assert(sizeof(T) <= sizeof(AtomicStorageType));
+    return cast_helper<T>::to_return_type(base::Relaxed_AtomicExchange(
+        to_storage_addr(addr), cast_helper<T>::to_storage_type(new_value)));
   }
 
   template <typename T>
diff --git a/src/objects/js-array-buffer-inl.h b/src/objects/js-array-buffer-inl.h
index 5e90240..79c39f0 100644
--- a/src/objects/js-array-buffer-inl.h
+++ b/src/objects/js-array-buffer-inl.h
@@ -169,6 +169,23 @@
   WriteBarrier::ForArrayBufferExtension(this, extension);
 }
 
+ArrayBufferExtension* JSArrayBuffer::extract_extension(
+    Isolate* isolate,
+    const DisallowGarbageCollection& disallow_gc V8_LIFETIME_BOUND) {
+#if V8_COMPRESS_POINTERS
+  ExternalPointerHandle handle =
+      base::AsAtomic32::Relaxed_Load(extension_handle_location());
+  if (handle == kNullExternalPointerHandle) {
+    return nullptr;
+  }
+  return reinterpret_cast<ArrayBufferExtension*>(
+      isolate->external_pointer_table().Exchange(handle, kNullAddress,
+                                                 kArrayBufferExtensionTag));
+#else
+  return base::AsAtomicPointer::Relaxed_Swap(extension_location(), nullptr);
+#endif  // V8_COMPRESS_POINTERS
+}
+
 #if V8_COMPRESS_POINTERS
 ExternalPointerHandle* JSArrayBuffer::extension_handle_location() const {
   return reinterpret_cast<ExternalPointerHandle*>(extension_.storage_address());
diff --git a/src/objects/js-array-buffer.cc b/src/objects/js-array-buffer.cc
index 9c10ef1..5c44f10 100644
--- a/src/objects/js-array-buffer.cc
+++ b/src/objects/js-array-buffer.cc
@@ -217,14 +217,19 @@
 void JSArrayBuffer::DetachInternal(DirectHandle<JSArrayBuffer> array_buffer,
                                    bool force_for_wasm_memory,
                                    Isolate* isolate) {
-  ArrayBufferExtension* extension = array_buffer->extension();
-
-  if (extension) {
+  {
     DisallowGarbageCollection disallow_gc;
-    isolate->heap()->DetachArrayBufferExtension(extension);
-    std::shared_ptr<BackingStore> backing_store =
-        array_buffer->RemoveExtension();
-    CHECK_IMPLIES(force_for_wasm_memory, backing_store->is_wasm_memory());
+    ArrayBufferExtension* extension =
+        array_buffer->extract_extension(isolate, disallow_gc);
+    if (extension) {
+      isolate->heap()->DetachArrayBufferExtension(extension);
+      // Prevent concurrent detachment vs background copying if an attacker
+      // swapped the extension pointer to a shared buffer's extension.
+      SBXCHECK(!extension->is_shared());
+      std::shared_ptr<BackingStore> backing_store =
+          extension->RemoveBackingStore();
+      CHECK_IMPLIES(force_for_wasm_memory, backing_store->is_wasm_memory());
+    }
   }
 
   array_buffer->set_was_detached(true, kReleaseStore);
@@ -331,19 +336,6 @@
   return extension;
 }
 
-std::shared_ptr<BackingStore> JSArrayBuffer::RemoveExtension() {
-  ArrayBufferExtension* extension = this->extension();
-  DCHECK_NOT_NULL(extension);
-  // Prevent concurrent detachment vs background copying if an attacker
-  // swapped the extension pointer to a shared buffer's extension.
-  SBXCHECK(!extension->is_shared());
-  auto result = extension->RemoveBackingStore();
-  // Remove pointer to extension such that the next GC will free it
-  // automatically.
-  set_extension(nullptr);
-  return result;
-}
-
 Handle<JSArrayBuffer> JSTypedArray::GetBuffer(Isolate* isolate) {
   DirectHandle<JSTypedArray> self(this, isolate);
   DCHECK(IsTypedArrayOrRabGsabTypedArrayElementsKind(self->GetElementsKind()));
diff --git a/src/objects/js-array-buffer.h b/src/objects/js-array-buffer.h
index ec73e6f..0376f9f 100644
--- a/src/objects/js-array-buffer.h
+++ b/src/objects/js-array-buffer.h
@@ -56,6 +56,12 @@
   inline ArrayBufferExtension* extension() const;
   inline void set_extension(ArrayBufferExtension* value);
   inline void init_extension();
+  // Returns the current extension and resets it to nullptr. Expects a witness
+  // DisallowGarbageCollection to make sure the returned extension isn't freed
+  // while a pointer to it is held on stack.
+  inline ArrayBufferExtension* extract_extension(
+      Isolate* isolate,
+      const DisallowGarbageCollection& disallow_gc V8_LIFETIME_BOUND);
 
   // [bit_field]: boolean flags
   inline uint32_t bit_field() const;
@@ -183,9 +189,6 @@
   V8_EXPORT_PRIVATE ArrayBufferExtension* CreateExtension(
       Isolate* isolate, std::shared_ptr<BackingStore> backing_store);
 
-  // Frees the associated ArrayBufferExtension and returns its backing store.
-  std::shared_ptr<BackingStore> RemoveExtension();
-
   //
   // Serializer/deserializer support.
   //
diff --git a/src/sandbox/external-pointer-table-inl.h b/src/sandbox/external-pointer-table-inl.h
index b718e5d..a6ba845 100644
--- a/src/sandbox/external-pointer-table-inl.h
+++ b/src/sandbox/external-pointer-table-inl.h
@@ -210,8 +210,9 @@
 Address ExternalPointerTable::Exchange(ExternalPointerHandle handle,
                                        Address value, ExternalPointerTag tag) {
   DCHECK_NE(kNullExternalPointerHandle, handle);
-  DCHECK(!IsManagedExternalPointerType(tag));
   uint32_t index = HandleToIndex(handle);
+  FreeManagedResourceIfPresent(index);
+  TakeOwnershipOfManagedResourceIfNecessary(value, handle, tag);
   return at(index).ExchangeExternalPointer(value, tag);
 }
 
Loading diff…

Original Bug Report

reported by [email protected]

JSArrayBuffer::Detach double-reads extension_ EPT handle enabling backing store theft and uaf

Steps to reproduce the problem

Build d8 with v8_enable_memory_corruption_api=true, run: ./d8 –expose-memory-corruption-api –allow-natives-syntax d2-minimal-crash.js

Problem Description

JSArrayBuffer::Detach() reads extension_ EPT handle twice (line 220 and line 335), enabling backing store theft via sandbox-internal handle swap. In Detach(), extension_ is read at line 220 for DetachArrayBufferExtension(). RemoveExtension() (line 226) reads it again at line 335. The handle is stored in-cage at JSArrayBuffer+44. An attacker with sandbox-internal read/write can swap the handle between the two reads. Both reads use kArrayBufferExtensionTag, so the tag check passes. RemoveExtension() then calls RemoveBackingStore() on the wrong extension, freeing the victim’s backing store while byteLength stays unchanged (dangling). SBXCHECK(!is_shared()) at line 338 only blocks shared buffers, not non-shared swaps. Same pattern as issue 532204454 (ExternalString::DisposeResource double-read), fixed in commit 7d1fb25f997. VERSION Chrome 153.0.8010.12 stable, Linux x86-64. Also confirmed on V8 main 7cef5429de3 (2026-08-29). REPRODUCTION Build d8 with v8_enable_memory_corruption_api=true, run: ./d8 –expose-memory-corruption-api –allow-natives-syntax d2-minimal-crash.js CRASH SIGSEGV SEGV_ACCERR at freed backing store (PartitionAlloc) Crashing instruction: movzx r8d,BYTE PTR [r8+rbx*1] in Builtins_KeyedLoadIC_Megamorphic+7878 R8=freed backing_store address Stack: KeyedLoadIC_Megamorphic -> InterpreterEntryTrampoline FIX Pass extension pointer from first read (line 220) directly to RemoveExtension(), eliminating second read. Or use EPT exchange like 7d1fb25f997. CREDIT: flyyy

Summary

JSArrayBuffer::Detach double-reads extension_ EPT handle enabling backing store theft and uaf

Custom Questions

Type of crash:

render

Crash state:

Signal: SIGSEGV (signal 11), SEGV_ACCERR Fault Address: 0x3d2900052000 (freed backing store in PartitionAlloc region)

Crashing instruction (from gdb with symbols): => 0x5555570df906 <Builtins_KeyedLoadIC_Megamorphic+7878>: movzx r8d, BYTE PTR [r8+rbx*1]

Registers at crash: RIP = 0x5555570df906 (Builtins_KeyedLoadIC_Megamorphic+7878) R8 = 0x3d2900052000 (freed backing_store pointer) RBX = 0x0 RAX = 0x0

Stack trace (gdb, with symbols): #0 Builtins_KeyedLoadIC_Megamorphic+7878 (d8) <- reading freed backing_store via TypedArray element load #1 0x00002a01005a01 (interpreted JS frame) #6 Builtins_InterpreterEntryTrampoline (d8) #10 Builtins_InterpreterEntryTrampoline (d8)

Root cause call path: JSArrayBuffer::Detach() -> array_buffer->extension() // READ #1 (line 220) -> heap()->DetachArrayBufferExtension(ext) // operates on swapped extension -> array_buffer->RemoveExtension() -> this->extension() // READ #2 (line 335) -> extension->RemoveBackingStore() // frees victim’s backing store -> this->set_extension(nullptr) // clears wrong EPT entry

Reporter credit:

flyyy

Additional Data

Category: Security
Chrome Channel: Stable
Regression: N/A \

View on issue tracker