From d23afdf1645e57723bef36ed313f0101d0a41d59 Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Sat, 21 Feb 2026 11:00:42 -0300 Subject: [PATCH 01/25] Create Parking Lot Mutex in SharedDataBlock --- .../src/ecmascript/types/spec/data_block.rs | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/nova_vm/src/ecmascript/types/spec/data_block.rs b/nova_vm/src/ecmascript/types/spec/data_block.rs index 3d6738abc..8b59931db 100644 --- a/nova_vm/src/ecmascript/types/spec/data_block.rs +++ b/nova_vm/src/ecmascript/types/spec/data_block.rs @@ -8,6 +8,8 @@ use core::sync::atomic::{AtomicUsize, Ordering}; #[cfg(feature = "shared-array-buffer")] use std::hint::assert_unchecked; +#[cfg(feature = "shared-array-buffer")] +use std::sync::{Arc, Condvar, Mutex}; use core::{ f32, f64, @@ -419,6 +421,26 @@ impl SharedDataBlockMaxByteLength { } } +#[cfg(feature = "shared-array-buffer")] +pub(crate) struct WaiterRecord { + pub condvar: Arc, + pub result: WaitResult, +} + +#[cfg(feature = "shared-array-buffer")] +pub(crate) enum WaitResult { + Ok, + TimedOut, +} + +#[cfg(feature = "shared-array-buffer")] +pub(crate) struct WaiterList { + pub waiters: std::collections::VecDeque, +} + +#[cfg(feature = "shared-array-buffer")] +pub(crate) type SharedWaiterMap = Arc>>; + /// # [6.2.9 Data Blocks](https://tc39.es/ecma262/#sec-data-blocks) /// /// The Shared Data Block specification type is used to describe a distinct and @@ -471,13 +493,23 @@ impl SharedDataBlockMaxByteLength { /// [`SharedDataBlock`]: SharedDataBlock #[must_use] #[repr(C)] -#[derive(PartialEq, Eq)] #[cfg(feature = "shared-array-buffer")] pub struct SharedDataBlock { ptr: RacyPtr, max_byte_length: SharedDataBlockMaxByteLength, + waiters: Option, +} + +#[cfg(feature = "shared-array-buffer")] +impl PartialEq for SharedDataBlock { + fn eq(&self, other: &Self) -> bool { + self.ptr == other.ptr && self.max_byte_length == other.max_byte_length + } } +#[cfg(feature = "shared-array-buffer")] +impl Eq for SharedDataBlock {} + // SAFETY: Atomic RC. #[cfg(feature = "shared-array-buffer")] unsafe impl Send for SharedDataBlock {} @@ -504,6 +536,7 @@ impl Clone for SharedDataBlock { Self { ptr: self.ptr, max_byte_length: self.max_byte_length, + waiters: self.waiters.clone(), } } } @@ -584,11 +617,13 @@ impl SharedDataBlock { const DANGLING_STATIC_SHARED_DATA_BLOCK: Self = Self { ptr: RacyPtr::dangling(), max_byte_length: SharedDataBlockMaxByteLength(0), + waiters: None, }; const DANGLING_GROWABLE_SHARED_DATA_BLOCK: Self = Self { ptr: RacyPtr::dangling(), max_byte_length: SharedDataBlockMaxByteLength(1usize.rotate_right(1)), + waiters: None, }; /// Allocate a new SharedDataBlock. @@ -654,6 +689,7 @@ impl SharedDataBlock { Some(Self { ptr: ptr.as_slice().into_raw_parts().0, max_byte_length: SharedDataBlockMaxByteLength::new(size, growable), + waiters: Some(Arc::new(Mutex::new(std::collections::HashMap::new()))), }) } } @@ -731,6 +767,14 @@ impl SharedDataBlock { self.max_byte_length.is_growable() } + /// Get the shared waiter map for this data block. + /// + /// Returns `None` for dangling (zero-sized) blocks. + #[inline(always)] + pub(crate) fn get_waiter_map(&self) -> Option<&SharedWaiterMap> { + self.waiters.as_ref() + } + /// Read a value at the given aligned offset and with the given ordering. /// /// Returns `None` if the offset is not correctly aligned or the index is From 1938191f5a42165ec1f625ea824a072434480d43 Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Sun, 22 Feb 2026 12:58:14 -0300 Subject: [PATCH 02/25] Updated allocation --- .../src/ecmascript/types/spec/data_block.rs | 145 +++++++++++++----- 1 file changed, 103 insertions(+), 42 deletions(-) diff --git a/nova_vm/src/ecmascript/types/spec/data_block.rs b/nova_vm/src/ecmascript/types/spec/data_block.rs index 8b59931db..b0f184336 100644 --- a/nova_vm/src/ecmascript/types/spec/data_block.rs +++ b/nova_vm/src/ecmascript/types/spec/data_block.rs @@ -4,6 +4,8 @@ //! ### [6.2.9 Data Blocks](https://tc39.es/ecma262/#sec-data-blocks) +#[cfg(feature = "shared-array-buffer")] +use core::sync::atomic::AtomicPtr; #[cfg(feature = "shared-array-buffer")] use core::sync::atomic::{AtomicUsize, Ordering}; #[cfg(feature = "shared-array-buffer")] @@ -439,7 +441,7 @@ pub(crate) struct WaiterList { } #[cfg(feature = "shared-array-buffer")] -pub(crate) type SharedWaiterMap = Arc>>; +pub(crate) type SharedWaiterMap = Mutex>; /// # [6.2.9 Data Blocks](https://tc39.es/ecma262/#sec-data-blocks) /// @@ -462,6 +464,7 @@ pub(crate) type SharedWaiterMap = Arc { /// rc: AtomicUsize, +/// waiters: AtomicPtr, /// bytes: [RacyU8; N], /// } /// ``` @@ -472,11 +475,17 @@ pub(crate) type SharedWaiterMap = Arc { /// byte_length: AtomicUsize, /// rc: AtomicUsize, +/// waiters: AtomicPtr, /// bytes: [RacyU8; N], /// } /// ``` /// -/// The `ptr` field points to the start of the `bytes` +/// The `ptr` field points to the start of the `bytes`. +/// +/// The `waiters` pointer is initially null. It is lazily initialized via +/// a compare-and-swap the first time any thread calls `Atomics.wait` or +/// `Atomics.waitAsync` on this block. Its lifetime is managed by the +/// buffer's existing reference count. /// /// Note that the "viewed" byte length of the buffer is defined inside the /// buffer when the SharedDataBlock is growable. @@ -493,23 +502,13 @@ pub(crate) type SharedWaiterMap = Arc, max_byte_length: SharedDataBlockMaxByteLength, - waiters: Option, -} - -#[cfg(feature = "shared-array-buffer")] -impl PartialEq for SharedDataBlock { - fn eq(&self, other: &Self) -> bool { - self.ptr == other.ptr && self.max_byte_length == other.max_byte_length - } } -#[cfg(feature = "shared-array-buffer")] -impl Eq for SharedDataBlock {} - // SAFETY: Atomic RC. #[cfg(feature = "shared-array-buffer")] unsafe impl Send for SharedDataBlock {} @@ -536,7 +535,6 @@ impl Clone for SharedDataBlock { Self { ptr: self.ptr, max_byte_length: self.max_byte_length, - waiters: self.waiters.clone(), } } } @@ -551,9 +549,9 @@ impl Drop for SharedDataBlock { return; } let growable = self.is_growable(); - // SAFETY: SharedDataBlock guarantees we have a AtomicUsize allocated - // before the bytes. - let rc_ptr = unsafe { self.ptr.as_ptr().cast::().sub(1) }; + // SAFETY: SharedDataBlock guarantees we have rc and waiters_ptr + // allocated before the bytes. rc is 2 slots before ptr. + let rc_ptr = unsafe { self.ptr.as_ptr().cast::().sub(2) }; { // SAFETY: the RC is definitely still allocated, as we haven't // subtracted ourselves from it yet. @@ -578,23 +576,35 @@ impl Drop for SharedDataBlock { return; } } + + // We are the last holder. Drop the waiter map if it was initialized. + // SAFETY: non-dangling, and we're the sole owner now. + let waiters_ptr = unsafe { self.get_waiters_ptr() }; + let waiters = waiters_ptr.load(Ordering::Acquire); + if !waiters.is_null() { + // SAFETY: the pointer was allocated via Box::into_raw in + // get_or_init_waiters, and we are the last holder. + let _ = unsafe { Box::from_raw(waiters) }; + } + let max_byte_length = self.max_byte_length(); - // SAFETY: if we're here then we're the last holder of the data block. let (size, base_ptr) = if growable { - // This is a growable SharedDataBlock that we're working with here. - // SAFETY: layout guaranteed by type unsafe { ( - max_byte_length - .unchecked_add(core::mem::size_of::<(AtomicUsize, AtomicUsize)>()), + max_byte_length.unchecked_add(core::mem::size_of::<( + AtomicUsize, + AtomicUsize, + AtomicUsize, + )>()), rc_ptr.sub(1), ) } } else { unsafe { ( - max_byte_length.unchecked_add(core::mem::size_of::()), + max_byte_length + .unchecked_add(core::mem::size_of::<(AtomicUsize, AtomicUsize)>()), rc_ptr, ) } @@ -603,8 +613,8 @@ impl Drop for SharedDataBlock { // SAFETY: As per the CAS loop on the reference count, we are the only // referrer to the racy memory. We can thus deallocate the ECMAScript // memory; this effectively grows our Rust memory from being just the - // RC and possible byte length value, into also containing the byte - // data. + // RC, waiters pointer, and possible byte length value, into also + // containing the byte data. let _ = unsafe { memory.exit() }; // SAFETY: layout guaranteed by type. let layout = unsafe { Layout::from_size_align(size, 8).unwrap_unchecked() }; @@ -617,13 +627,11 @@ impl SharedDataBlock { const DANGLING_STATIC_SHARED_DATA_BLOCK: Self = Self { ptr: RacyPtr::dangling(), max_byte_length: SharedDataBlockMaxByteLength(0), - waiters: None, }; const DANGLING_GROWABLE_SHARED_DATA_BLOCK: Self = Self { ptr: RacyPtr::dangling(), max_byte_length: SharedDataBlockMaxByteLength(1usize.rotate_right(1)), - waiters: None, }; /// Allocate a new SharedDataBlock. @@ -651,10 +659,10 @@ impl SharedDataBlock { use ecmascript_atomics::RacyMemory; let alloc_size = if growable { // Growable SharedArrayBuffer - size.checked_add(core::mem::size_of::<(AtomicUsize, AtomicUsize)>())? + size.checked_add(core::mem::size_of::<(AtomicUsize, AtomicUsize, AtomicUsize)>())? } else { // Static SharedArrayBuffer - size.checked_add(core::mem::size_of::())? + size.checked_add(core::mem::size_of::<(AtomicUsize, AtomicUsize)>())? }; let Ok(layout) = Layout::from_size_align(alloc_size, 8) else { return None; @@ -668,7 +676,7 @@ impl SharedDataBlock { // SAFETY: properly allocated, everything is fine. unsafe { base_ptr.write(byte_length) }; // SAFETY: allocation size is - // (AtomicUsize, AtomicUsize, [AtomicU8; max_byte_length]) + // (AtomicUsize, AtomicUsize, AtomicUsize, [AtomicU8; max_byte_length]) unsafe { base_ptr.add(1) } } else { base_ptr @@ -677,19 +685,19 @@ impl SharedDataBlock { // SAFETY: we're the only owner of this data. unsafe { rc_ptr.write(1) }; } - // SAFETY: the pointer is len + usize - let ptr = unsafe { rc_ptr.add(1) }; + + // SAFETY: ptr is past rc and waiters_ptr + let ptr = unsafe { rc_ptr.add(2) }; // SAFETY: ptr does point to size bytes of readable and writable // Rust memory. After this call, that memory is deallocated and we // receive a new RacyMemory in its stead. Reads and writes through // it are undefined behaviour. Note though that we still have the - // RC and possible length values before the pointer; those are in - // normal Rust memory. + // RC, waiters pointer, and possible length values before the + // pointer; those are in normal Rust memory. let ptr = unsafe { RacyMemory::::enter(ptr.cast(), size) }; Some(Self { ptr: ptr.as_slice().into_raw_parts().0, max_byte_length: SharedDataBlockMaxByteLength::new(size, growable), - waiters: Some(Arc::new(Mutex::new(std::collections::HashMap::new()))), }) } } @@ -710,7 +718,7 @@ impl SharedDataBlock { /// Must not be a dangling SharedDataBlock. unsafe fn get_rc(&self) -> &AtomicUsize { // SAFETY: type guarantees layout - unsafe { self.ptr.as_ptr().cast::().sub(1).as_ref() } + unsafe { self.ptr.as_ptr().cast::().sub(2).as_ref() } } /// Get a reference to the atomic byte length. @@ -720,7 +728,7 @@ impl SharedDataBlock { /// Must be a growable, non-dangling SharedDataBlock. unsafe fn get_byte_length(&self) -> &AtomicUsize { // SAFETY: caller guarantees growable; type guarantees layout. - unsafe { self.ptr.as_ptr().cast::().sub(2).as_ref() } + unsafe { self.ptr.as_ptr().cast::().sub(3).as_ref() } } /// Returns the byte length of the SharedArrayBuffer. @@ -767,12 +775,65 @@ impl SharedDataBlock { self.max_byte_length.is_growable() } - /// Get the shared waiter map for this data block. + /// Get a reference to the atomic waiters pointer. /// - /// Returns `None` for dangling (zero-sized) blocks. - #[inline(always)] - pub(crate) fn get_waiter_map(&self) -> Option<&SharedWaiterMap> { - self.waiters.as_ref() + /// ## Safety + /// + /// Must not be a dangling SharedDataBlock. + unsafe fn get_waiters_ptr(&self) -> &AtomicPtr { + // SAFETY: type guarantees layout; waiters_ptr is 1 slot before ptr. + unsafe { + self.ptr + .as_ptr() + .cast::>() + .sub(1) + .as_ref() + } + } + + /// Get or lazily initialize the shared waiter map for this data block. + /// + /// On first call, allocates a new `SharedWaiterMap` and attempts to + /// store it via compare-and-swap. If another thread wins the race, + /// the locally allocated map is dropped and the winner's map is used. + /// + /// ## Safety + /// + /// Must not be a dangling SharedDataBlock. + pub(crate) unsafe fn get_or_init_waiters(&self) -> &SharedWaiterMap { + // SAFETY: caller guarantees non-dangling. + let waiters_atomic = unsafe { self.get_waiters_ptr() }; + let current = waiters_atomic.load(Ordering::Acquire); + if !current.is_null() { + // SAFETY: non-null means it was previously initialized; the + // buffer RC keeps the allocation alive. + return unsafe { &*current }; + } + + let new_map = Box::into_raw(Box::new(SharedWaiterMap::new( + std::collections::HashMap::new(), + ))); + match waiters_atomic.compare_exchange( + core::ptr::null_mut(), + new_map, + Ordering::AcqRel, + Ordering::Acquire, + ) { + Ok(_) => { + // We won the race; our map is now the canonical one. + // SAFETY: we just stored it and the buffer RC keeps it alive. + unsafe { &*new_map } + } + Err(winner) => { + // Another thread already initialized the waiters pointer. + // Drop our allocation and use theirs. + // SAFETY: new_map was just allocated by us and never shared. + let _ = unsafe { Box::from_raw(new_map) }; + // SAFETY: winner is the non-null pointer stored by the + // winning thread; the buffer RC keeps it alive. + unsafe { &*winner } + } + } } /// Read a value at the given aligned offset and with the given ordering. From 387d9898665daed11cc9689c92d193cd249eb878 Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Mon, 23 Feb 2026 07:38:27 -0300 Subject: [PATCH 03/25] Updated accessor --- .../src/ecmascript/types/spec/data_block.rs | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/nova_vm/src/ecmascript/types/spec/data_block.rs b/nova_vm/src/ecmascript/types/spec/data_block.rs index b0f184336..74b13cefc 100644 --- a/nova_vm/src/ecmascript/types/spec/data_block.rs +++ b/nova_vm/src/ecmascript/types/spec/data_block.rs @@ -5,9 +5,7 @@ //! ### [6.2.9 Data Blocks](https://tc39.es/ecma262/#sec-data-blocks) #[cfg(feature = "shared-array-buffer")] -use core::sync::atomic::AtomicPtr; -#[cfg(feature = "shared-array-buffer")] -use core::sync::atomic::{AtomicUsize, Ordering}; +use core::sync::atomic::{AtomicBool, AtomicPtr, AtomicUsize, Ordering}; #[cfg(feature = "shared-array-buffer")] use std::hint::assert_unchecked; #[cfg(feature = "shared-array-buffer")] @@ -424,24 +422,27 @@ impl SharedDataBlockMaxByteLength { } #[cfg(feature = "shared-array-buffer")] -pub(crate) struct WaiterRecord { +pub struct WaiterRecord { pub condvar: Arc, - pub result: WaitResult, + pub notified: Arc, } +/// Result of an `Atomics.wait` or `Atomics.waitAsync` operation. +#[derive(Debug)] #[cfg(feature = "shared-array-buffer")] -pub(crate) enum WaitResult { +pub enum WaitResult { Ok, TimedOut, + NotEqual, } #[cfg(feature = "shared-array-buffer")] -pub(crate) struct WaiterList { +pub struct WaiterList { pub waiters: std::collections::VecDeque, } #[cfg(feature = "shared-array-buffer")] -pub(crate) type SharedWaiterMap = Mutex>; +pub type SharedWaiterMap = Mutex>; /// # [6.2.9 Data Blocks](https://tc39.es/ecma262/#sec-data-blocks) /// @@ -836,6 +837,27 @@ impl SharedDataBlock { } } + /// Get the shared waiter map if it has been initialized. + /// + /// Returns `None` if no thread has ever called `get_or_init_waiters` on + /// this block (i.e. no `Atomics.wait` / `Atomics.waitAsync` has occurred). + /// + /// ## Safety + /// + /// Must not be a dangling SharedDataBlock. + pub(crate) unsafe fn get_waiters(&self) -> Option<&SharedWaiterMap> { + // SAFETY: caller guarantees non-dangling. + let waiters_atomic = unsafe { self.get_waiters_ptr() }; + let current = waiters_atomic.load(Ordering::Acquire); + if current.is_null() { + None + } else { + // SAFETY: non-null means it was previously initialized; the + // buffer RC keeps the allocation alive. + Some(unsafe { &*current }) + } + } + /// Read a value at the given aligned offset and with the given ordering. /// /// Returns `None` if the offset is not correctly aligned or the index is From 8d805e81f5b227a0b39f7ddc544bc861f62c731b Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Mon, 23 Feb 2026 09:14:30 -0300 Subject: [PATCH 04/25] Replace old wait / notify by native rust code --- .../structured_data/atomics_object.rs | 216 +++++++++++------- 1 file changed, 130 insertions(+), 86 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs index 1736ec2d3..7162ceafe 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs @@ -3,15 +3,18 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use std::{ + collections::VecDeque, hint::assert_unchecked, ops::ControlFlow, - sync::{Arc, atomic::AtomicBool}, + sync::{ + Arc, Condvar, + atomic::{AtomicBool, Ordering as StdOrdering}, + }, thread::{self, JoinHandle}, - time::Duration, + time::{Duration, Instant}, }; use ecmascript_atomics::Ordering; -use ecmascript_futex::{ECMAScriptAtomicWait, FutexError}; use crate::{ ecmascript::{ @@ -27,6 +30,7 @@ use crate::{ to_integer_or_infinity, to_number, try_result_into_js, try_to_index, unwrap_try, validate_index, validate_typed_array, }, + ecmascript::{WaitResult, WaiterList, WaiterRecord}, engine::{Bindable, GcScope, Global, NoGcScope, Scopable}, heap::{ObjectEntry, WellKnownSymbols}, }; @@ -666,35 +670,23 @@ impl AtomicsObject { }; // 6. Let block be buffer.[[ArrayBufferData]]. // 8. Let WL be GetWaiterList(block, byteIndexInBuffer). - let is_big_int_64_array = matches!(typed_array, AnyTypedArray::SharedBigInt64Array(_)); - let slot = buffer.as_slice(agent).slice_from(byte_index_in_buffer); - let n = if is_big_int_64_array { - // SAFETY: offset was checked. - let slot = unsafe { slot.as_aligned::().unwrap_unchecked() }; - if c == usize::MAX { - // Force the notify count down into a reasonable range: the - // ecmascript_futex may return usize::MAX if the OS doesn't - // give us a count number. - slot.notify_all().min(i32::MAX as usize) - } else { - slot.notify_many(c) - } - } else { - // SAFETY: offset was checked. - let slot = unsafe { slot.as_aligned::().unwrap_unchecked() }; - if c == usize::MAX { - // Force the notify count down into a reasonable range: the - // ecmascript_futex may return usize::MAX if the OS doesn't - // give us a count number. - slot.notify_all().min(i32::MAX as usize) - } else { - slot.notify_many(c) - } - }; + let data_block = buffer.get_data_block(agent); // 9. Perform EnterCriticalSection(WL). - // 10. Let S be RemoveWaiters(WL, c). - // 11. For each element W of S, do - // a. Perform NotifyWaiter(WL, W). + // SAFETY: buffer is a valid SharedArrayBuffer, data block is non-dangling. + let mut n = 0; + if let Some(waiters) = unsafe { data_block.get_waiters() } { + let mut guard = waiters.lock().unwrap(); + // 10. Let S be RemoveWaiters(WL, c). + if let Some(list) = guard.get_mut(&byte_index_in_buffer) { + // 11. For each element W of S, do + // a. Perform NotifyWaiter(WL, W). + while let Some(waiter) = list.waiters.pop_front() { + waiter.notified.store(true, StdOrdering::Release); + waiter.condvar.notify_one(); + n += 1; + } + } + } // 12. Perform LeaveCriticalSection(WL). // 13. Let n be the number of elements in S. // 14. Return 𝔽(n). @@ -1486,37 +1478,65 @@ fn do_wait_critical<'gc, const IS_ASYNC: bool, const IS_I64: bool>( // 28. Perform AddWaiter(WL, waiterRecord). // 29. If mode is sync, then if !IS_ASYNC { - // a. Perform SuspendThisAgent(WL, waiterRecord). - let result = if IS_I64 { - let v = v as u64; - // SAFETY: buffer is still live and index was checked. + let data_block = buffer.get_data_block(agent); + // SAFETY: buffer is a valid SharedArrayBuffer, data block is non-dangling. + let waiters = unsafe { data_block.get_or_init_waiters() }; + let condvar = Arc::new(Condvar::new()); + let notified = Arc::new(AtomicBool::new(false)); + let mut guard = waiters.lock().unwrap(); + + // Re-read value under critical section to avoid TOCTOU race. + let slot = data_block.as_racy_slice().slice_from(byte_index_in_buffer); + let v_changed = if IS_I64 { let slot = unsafe { slot.as_aligned::().unwrap_unchecked() }; - if t == u64::MAX { - slot.wait(v) - } else { - slot.wait_timeout(v, Duration::from_millis(t)) - } + v as u64 != slot.load(Ordering::SeqCst) } else { - let v = v as u32; - // SAFETY: buffer is still live and index was checked. let slot = unsafe { slot.as_aligned::().unwrap_unchecked() }; - if t == u64::MAX { - slot.wait(v) - } else { - slot.wait_timeout(v, Duration::from_millis(t)) - } + v as i32 as u32 != slot.load(Ordering::SeqCst) }; - // 31. Perform LeaveCriticalSection(WL). - // 32. If mode is sync, return waiterRecord.[[Result]]. + if v_changed { + return BUILTIN_STRING_MEMORY.not_equal.into(); + } - match result { - Ok(_) => BUILTIN_STRING_MEMORY.ok.into(), - Err(err) => match err { - FutexError::Timeout => BUILTIN_STRING_MEMORY.timed_out.into(), - FutexError::NotEqual => BUILTIN_STRING_MEMORY.not_equal.into(), - FutexError::Unknown => panic!(), - }, + // a. Perform SuspendThisAgent(WL, waiterRecord). + guard + .entry(byte_index_in_buffer) + .or_insert_with(|| WaiterList { + waiters: VecDeque::new(), + }) + .waiters + .push_back(WaiterRecord { + condvar: condvar.clone(), + notified: notified.clone(), + }); + + if t == u64::MAX { + while !notified.load(StdOrdering::Acquire) { + guard = condvar.wait(guard).unwrap(); + } + } else { + let deadline = Instant::now() + Duration::from_millis(t); + loop { + if notified.load(StdOrdering::Acquire) { + break; + } + let remaining = deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() { + // Timed out — remove ourselves from the waiter list. + if let Some(list) = guard.get_mut(&byte_index_in_buffer) { + list.waiters.retain(|w| !Arc::ptr_eq(&w.condvar, &condvar)); + } + // 31. Perform LeaveCriticalSection(WL). + // 32. If mode is sync, return waiterRecord.[[Result]]. + return BUILTIN_STRING_MEMORY.timed_out.into(); + } + let (new_guard, _) = condvar.wait_timeout(guard, remaining).unwrap(); + guard = new_guard; + } } + // 31. Perform LeaveCriticalSection(WL). + // 32. If mode is sync, return waiterRecord.[[Result]]. + BUILTIN_STRING_MEMORY.ok.into() } else { let promise_capability = PromiseCapability::new(agent, gc); let promise = Global::new(agent, promise_capability.promise.unbind()); @@ -1623,7 +1643,7 @@ fn create_wait_result_object<'gc>( #[derive(Debug)] struct WaitAsyncJobInner { promise_to_resolve: Global>, - join_handle: JoinHandle>, + join_handle: JoinHandle, _has_timeout: bool, } @@ -1662,18 +1682,8 @@ impl WaitAsyncJob { // c. Perform LeaveCriticalSection(WL). let promise_capability = PromiseCapability::from_promise(promise, true); let result = match result { - Ok(_) => BUILTIN_STRING_MEMORY.ok.into(), - Err(FutexError::NotEqual) => BUILTIN_STRING_MEMORY.ok.into(), - Err(FutexError::Timeout) => BUILTIN_STRING_MEMORY.timed_out.into(), - Err(FutexError::Unknown) => { - let error = agent.throw_exception_with_static_message( - ExceptionType::Error, - "unknown error occurred", - gc, - ); - promise_capability.reject(agent, error.value(), gc); - return Ok(()); - } + WaitResult::Ok | WaitResult::NotEqual => BUILTIN_STRING_MEMORY.ok.into(), + WaitResult::TimedOut => BUILTIN_STRING_MEMORY.timed_out.into(), }; unwrap_try(promise_capability.try_resolve(agent, result, gc)); // d. Return unused. @@ -1701,26 +1711,60 @@ fn enqueue_atomics_wait_async_job( let signal = Arc::new(AtomicBool::new(false)); let s = signal.clone(); let handle = thread::spawn(move || { + // SAFETY: buffer is a cloned SharedDataBlock; non-dangling. + let waiters = unsafe { buffer.get_or_init_waiters() }; + let condvar = Arc::new(Condvar::new()); + let notified = Arc::new(AtomicBool::new(false)); + let mut guard = waiters.lock().unwrap(); + + // Re-check the value under the critical section. let slot = buffer.as_racy_slice().slice_from(byte_index_in_buffer); - if IS_I64 { - let v = v as u64; - // SAFETY: buffer is still live and index was checked. + let v_not_equal = if IS_I64 { let slot = unsafe { slot.as_aligned::().unwrap_unchecked() }; - s.store(true, std::sync::atomic::Ordering::Release); - if t == u64::MAX { - slot.wait(v) - } else { - slot.wait_timeout(v, Duration::from_millis(t)) - } + v as u64 != slot.load(Ordering::SeqCst) } else { - let v = v as i32 as u32; - // SAFETY: buffer is still live and index was checked. let slot = unsafe { slot.as_aligned::().unwrap_unchecked() }; - s.store(true, std::sync::atomic::Ordering::Release); - if t == u64::MAX { - slot.wait(v) - } else { - slot.wait_timeout(v, Duration::from_millis(t)) + v as i32 as u32 != slot.load(Ordering::SeqCst) + }; + + // Signal the main thread that we have the lock and are about to sleep. + s.store(true, StdOrdering::Release); + + if v_not_equal { + return WaitResult::NotEqual; + } + + guard + .entry(byte_index_in_buffer) + .or_insert_with(|| WaiterList { + waiters: VecDeque::new(), + }) + .waiters + .push_back(WaiterRecord { + condvar: condvar.clone(), + notified: notified.clone(), + }); + + if t == u64::MAX { + while !notified.load(StdOrdering::Acquire) { + guard = condvar.wait(guard).unwrap(); + } + WaitResult::Ok + } else { + let deadline = Instant::now() + Duration::from_millis(t); + loop { + if notified.load(StdOrdering::Acquire) { + return WaitResult::Ok; + } + let remaining = deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() { + if let Some(list) = guard.get_mut(&byte_index_in_buffer) { + list.waiters.retain(|w| !Arc::ptr_eq(&w.condvar, &condvar)); + } + return WaitResult::TimedOut; + } + let (new_guard, _) = condvar.wait_timeout(guard, remaining).unwrap(); + guard = new_guard; } } }); @@ -1732,7 +1776,7 @@ fn enqueue_atomics_wait_async_job( _has_timeout: t != u64::MAX, }))), }; - while !signal.load(std::sync::atomic::Ordering::Acquire) { + while !signal.load(StdOrdering::Acquire) { // Wait until the thread has started up and is about to go to sleep. } // 2. Let now be the time value (UTC) identifying the current time. From e077131e87b139d8544be4a25d91e601c1056c37 Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Tue, 24 Feb 2026 10:06:13 -0300 Subject: [PATCH 05/25] Fix nitpick --- .../builtins/structured_data/atomics_object.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs index 7162ceafe..8f077b453 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs @@ -677,14 +677,16 @@ impl AtomicsObject { if let Some(waiters) = unsafe { data_block.get_waiters() } { let mut guard = waiters.lock().unwrap(); // 10. Let S be RemoveWaiters(WL, c). - if let Some(list) = guard.get_mut(&byte_index_in_buffer) { - // 11. For each element W of S, do - // a. Perform NotifyWaiter(WL, W). - while let Some(waiter) = list.waiters.pop_front() { - waiter.notified.store(true, StdOrdering::Release); - waiter.condvar.notify_one(); - n += 1; - } + let Some(list) = guard.get_mut(&byte_index_in_buffer) else { + return Ok(0.into()); + }; + + // 11. For each element W of S, do + // a. Perform NotifyWaiter(WL, W). + while let Some(waiter) = list.waiters.pop_front() { + waiter.notified.store(true, StdOrdering::Release); + waiter.condvar.notify_one(); + n += 1; } } // 12. Perform LeaveCriticalSection(WL). From f08b646c30ec5804931693e435df00682ffd90d7 Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Tue, 24 Feb 2026 10:41:32 -0300 Subject: [PATCH 06/25] Updated comment --- .../src/ecmascript/builtins/structured_data/atomics_object.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs index 8f077b453..e8b2a36fc 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs @@ -672,7 +672,7 @@ impl AtomicsObject { // 8. Let WL be GetWaiterList(block, byteIndexInBuffer). let data_block = buffer.get_data_block(agent); // 9. Perform EnterCriticalSection(WL). - // SAFETY: buffer is a valid SharedArrayBuffer, data block is non-dangling. + // SAFETY: buffer is a valid SharedArrayBuffer it cannot be detached, so the data block is non-dangling. let mut n = 0; if let Some(waiters) = unsafe { data_block.get_waiters() } { let mut guard = waiters.lock().unwrap(); @@ -1481,7 +1481,7 @@ fn do_wait_critical<'gc, const IS_ASYNC: bool, const IS_I64: bool>( // 29. If mode is sync, then if !IS_ASYNC { let data_block = buffer.get_data_block(agent); - // SAFETY: buffer is a valid SharedArrayBuffer, data block is non-dangling. + // SAFETY: buffer is a valid SharedArrayBuffer it cannot be detached, so the data block is non-dangling. let waiters = unsafe { data_block.get_or_init_waiters() }; let condvar = Arc::new(Condvar::new()); let notified = Arc::new(AtomicBool::new(false)); From 892e20140da8c06a36160ab7c79f13048966e465 Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Tue, 24 Feb 2026 11:01:17 -0300 Subject: [PATCH 07/25] Fix notify --- .../ecmascript/builtins/structured_data/atomics_object.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs index e8b2a36fc..8e29e4e98 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs @@ -683,7 +683,10 @@ impl AtomicsObject { // 11. For each element W of S, do // a. Perform NotifyWaiter(WL, W). - while let Some(waiter) = list.waiters.pop_front() { + while n < c { + let Some(waiter) = list.waiters.pop_front() else { + break; + }; waiter.notified.store(true, StdOrdering::Release); waiter.condvar.notify_one(); n += 1; From 6011eb151a2bbbd07eccc866613aa2302335944b Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Tue, 24 Feb 2026 11:37:35 -0300 Subject: [PATCH 08/25] Use single ARC pointer --- .../structured_data/atomics_object.rs | 48 ++++++++++--------- .../src/ecmascript/types/spec/data_block.rs | 6 +-- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs index 8e29e4e98..7a82276c4 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs @@ -1486,8 +1486,10 @@ fn do_wait_critical<'gc, const IS_ASYNC: bool, const IS_I64: bool>( let data_block = buffer.get_data_block(agent); // SAFETY: buffer is a valid SharedArrayBuffer it cannot be detached, so the data block is non-dangling. let waiters = unsafe { data_block.get_or_init_waiters() }; - let condvar = Arc::new(Condvar::new()); - let notified = Arc::new(AtomicBool::new(false)); + let waiter_record = Arc::new(WaiterRecord { + condvar: Condvar::new(), + notified: AtomicBool::new(false), + }); let mut guard = waiters.lock().unwrap(); // Re-read value under critical section to avoid TOCTOU race. @@ -1510,32 +1512,29 @@ fn do_wait_critical<'gc, const IS_ASYNC: bool, const IS_I64: bool>( waiters: VecDeque::new(), }) .waiters - .push_back(WaiterRecord { - condvar: condvar.clone(), - notified: notified.clone(), - }); + .push_back(waiter_record.clone()); if t == u64::MAX { - while !notified.load(StdOrdering::Acquire) { - guard = condvar.wait(guard).unwrap(); + while !waiter_record.notified.load(StdOrdering::Acquire) { + guard = waiter_record.condvar.wait(guard).unwrap(); } } else { let deadline = Instant::now() + Duration::from_millis(t); loop { - if notified.load(StdOrdering::Acquire) { + if waiter_record.notified.load(StdOrdering::Acquire) { break; } let remaining = deadline.saturating_duration_since(Instant::now()); if remaining.is_zero() { - // Timed out — remove ourselves from the waiter list. if let Some(list) = guard.get_mut(&byte_index_in_buffer) { - list.waiters.retain(|w| !Arc::ptr_eq(&w.condvar, &condvar)); + list.waiters.retain(|w| !Arc::ptr_eq(w, &waiter_record)); } // 31. Perform LeaveCriticalSection(WL). // 32. If mode is sync, return waiterRecord.[[Result]]. return BUILTIN_STRING_MEMORY.timed_out.into(); } - let (new_guard, _) = condvar.wait_timeout(guard, remaining).unwrap(); + let (new_guard, _) = + waiter_record.condvar.wait_timeout(guard, remaining).unwrap(); guard = new_guard; } } @@ -1718,8 +1717,10 @@ fn enqueue_atomics_wait_async_job( let handle = thread::spawn(move || { // SAFETY: buffer is a cloned SharedDataBlock; non-dangling. let waiters = unsafe { buffer.get_or_init_waiters() }; - let condvar = Arc::new(Condvar::new()); - let notified = Arc::new(AtomicBool::new(false)); + let waiter_record = Arc::new(WaiterRecord { + condvar: Condvar::new(), + notified: AtomicBool::new(false), + }); let mut guard = waiters.lock().unwrap(); // Re-check the value under the critical section. @@ -1745,30 +1746,31 @@ fn enqueue_atomics_wait_async_job( waiters: VecDeque::new(), }) .waiters - .push_back(WaiterRecord { - condvar: condvar.clone(), - notified: notified.clone(), - }); + .push_back(waiter_record.clone()); if t == u64::MAX { - while !notified.load(StdOrdering::Acquire) { - guard = condvar.wait(guard).unwrap(); + while !waiter_record.notified.load(StdOrdering::Acquire) { + guard = waiter_record.condvar.wait(guard).unwrap(); } WaitResult::Ok } else { let deadline = Instant::now() + Duration::from_millis(t); loop { - if notified.load(StdOrdering::Acquire) { + if waiter_record.notified.load(StdOrdering::Acquire) { return WaitResult::Ok; } let remaining = deadline.saturating_duration_since(Instant::now()); if remaining.is_zero() { if let Some(list) = guard.get_mut(&byte_index_in_buffer) { - list.waiters.retain(|w| !Arc::ptr_eq(&w.condvar, &condvar)); + list.waiters + .retain(|w| !Arc::ptr_eq(w, &waiter_record)); } return WaitResult::TimedOut; } - let (new_guard, _) = condvar.wait_timeout(guard, remaining).unwrap(); + let (new_guard, _) = waiter_record + .condvar + .wait_timeout(guard, remaining) + .unwrap(); guard = new_guard; } } diff --git a/nova_vm/src/ecmascript/types/spec/data_block.rs b/nova_vm/src/ecmascript/types/spec/data_block.rs index 74b13cefc..0dd99a879 100644 --- a/nova_vm/src/ecmascript/types/spec/data_block.rs +++ b/nova_vm/src/ecmascript/types/spec/data_block.rs @@ -423,8 +423,8 @@ impl SharedDataBlockMaxByteLength { #[cfg(feature = "shared-array-buffer")] pub struct WaiterRecord { - pub condvar: Arc, - pub notified: Arc, + pub condvar: Condvar, + pub notified: AtomicBool, } /// Result of an `Atomics.wait` or `Atomics.waitAsync` operation. @@ -438,7 +438,7 @@ pub enum WaitResult { #[cfg(feature = "shared-array-buffer")] pub struct WaiterList { - pub waiters: std::collections::VecDeque, + pub waiters: std::collections::VecDeque>, } #[cfg(feature = "shared-array-buffer")] From dd656953f10f811dd390fe9c34679e9daf94e196 Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Tue, 24 Feb 2026 11:43:08 -0300 Subject: [PATCH 09/25] Fix formatting --- .../builtins/structured_data/atomics_object.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs index 7a82276c4..b6ca78180 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs @@ -1533,8 +1533,10 @@ fn do_wait_critical<'gc, const IS_ASYNC: bool, const IS_I64: bool>( // 32. If mode is sync, return waiterRecord.[[Result]]. return BUILTIN_STRING_MEMORY.timed_out.into(); } - let (new_guard, _) = - waiter_record.condvar.wait_timeout(guard, remaining).unwrap(); + let (new_guard, _) = waiter_record + .condvar + .wait_timeout(guard, remaining) + .unwrap(); guard = new_guard; } } @@ -1762,8 +1764,7 @@ fn enqueue_atomics_wait_async_job( let remaining = deadline.saturating_duration_since(Instant::now()); if remaining.is_zero() { if let Some(list) = guard.get_mut(&byte_index_in_buffer) { - list.waiters - .retain(|w| !Arc::ptr_eq(w, &waiter_record)); + list.waiters.retain(|w| !Arc::ptr_eq(w, &waiter_record)); } return WaitResult::TimedOut; } From afc3e1b52f8ec65ace0a00f66361fa531c35af60 Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Tue, 24 Feb 2026 11:54:23 -0300 Subject: [PATCH 10/25] Remove nested `if` statements --- .../structured_data/atomics_object.rs | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs index b6ca78180..56961715a 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs @@ -674,24 +674,28 @@ impl AtomicsObject { // 9. Perform EnterCriticalSection(WL). // SAFETY: buffer is a valid SharedArrayBuffer it cannot be detached, so the data block is non-dangling. let mut n = 0; - if let Some(waiters) = unsafe { data_block.get_waiters() } { - let mut guard = waiters.lock().unwrap(); - // 10. Let S be RemoveWaiters(WL, c). - let Some(list) = guard.get_mut(&byte_index_in_buffer) else { - return Ok(0.into()); - }; + let Some(waiters) = (unsafe { data_block.get_waiters() }) else { + return Ok(0.into()); + }; - // 11. For each element W of S, do - // a. Perform NotifyWaiter(WL, W). - while n < c { - let Some(waiter) = list.waiters.pop_front() else { - break; - }; - waiter.notified.store(true, StdOrdering::Release); - waiter.condvar.notify_one(); - n += 1; - } + let mut guard = waiters.lock().unwrap(); + // 10. Let S be RemoveWaiters(WL, c). + let Some(list) = guard.get_mut(&byte_index_in_buffer) else { + return Ok(0.into()); + }; + + // 11. For each element W of S, do + // a. Perform NotifyWaiter(WL, W). + while n < c { + let Some(waiter) = list.waiters.pop_front() else { + break; + }; + waiter.notified.store(true, StdOrdering::Release); + waiter.condvar.notify_one(); + n += 1; } + drop(guard); + // 12. Perform LeaveCriticalSection(WL). // 13. Let n be the number of elements in S. // 14. Return 𝔽(n). From 8c791b47247f90806ae2ba374c0203e387f469bc Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Tue, 24 Feb 2026 12:11:50 -0300 Subject: [PATCH 11/25] Fix data block ordering --- .../src/ecmascript/types/spec/data_block.rs | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/nova_vm/src/ecmascript/types/spec/data_block.rs b/nova_vm/src/ecmascript/types/spec/data_block.rs index 0dd99a879..aee2f1a50 100644 --- a/nova_vm/src/ecmascript/types/spec/data_block.rs +++ b/nova_vm/src/ecmascript/types/spec/data_block.rs @@ -464,8 +464,8 @@ pub type SharedWaiterMap = Mutex>; /// ```rust,ignore /// #[repr(C)] /// struct StaticSharedDataBuffer { -/// rc: AtomicUsize, /// waiters: AtomicPtr, +/// rc: AtomicUsize, /// bytes: [RacyU8; N], /// } /// ``` @@ -475,8 +475,8 @@ pub type SharedWaiterMap = Mutex>; /// #[repr(C)] /// struct GrowableSharedDataBuffer { /// byte_length: AtomicUsize, -/// rc: AtomicUsize, /// waiters: AtomicPtr, +/// rc: AtomicUsize, /// bytes: [RacyU8; N], /// } /// ``` @@ -550,9 +550,9 @@ impl Drop for SharedDataBlock { return; } let growable = self.is_growable(); - // SAFETY: SharedDataBlock guarantees we have rc and waiters_ptr - // allocated before the bytes. rc is 2 slots before ptr. - let rc_ptr = unsafe { self.ptr.as_ptr().cast::().sub(2) }; + // SAFETY: SharedDataBlock guarantees we have waiters_ptr and rc + // allocated before the bytes. rc is 1 slot before ptr. + let rc_ptr = unsafe { self.ptr.as_ptr().cast::().sub(1) }; { // SAFETY: the RC is definitely still allocated, as we haven't // subtracted ourselves from it yet. @@ -598,7 +598,7 @@ impl Drop for SharedDataBlock { AtomicUsize, AtomicUsize, )>()), - rc_ptr.sub(1), + rc_ptr.sub(2), ) } } else { @@ -606,7 +606,7 @@ impl Drop for SharedDataBlock { ( max_byte_length .unchecked_add(core::mem::size_of::<(AtomicUsize, AtomicUsize)>()), - rc_ptr, + rc_ptr.sub(1), ) } }; @@ -614,7 +614,7 @@ impl Drop for SharedDataBlock { // SAFETY: As per the CAS loop on the reference count, we are the only // referrer to the racy memory. We can thus deallocate the ECMAScript // memory; this effectively grows our Rust memory from being just the - // RC, waiters pointer, and possible byte length value, into also + // Waiters pointer, RC, and possible byte length value, into also // containing the byte data. let _ = unsafe { memory.exit() }; // SAFETY: layout guaranteed by type. @@ -678,22 +678,24 @@ impl SharedDataBlock { unsafe { base_ptr.write(byte_length) }; // SAFETY: allocation size is // (AtomicUsize, AtomicUsize, AtomicUsize, [AtomicU8; max_byte_length]) - unsafe { base_ptr.add(1) } + // Skip byte_length and waiters to reach rc. + unsafe { base_ptr.add(2) } } else { - base_ptr + // Skip waiters to reach rc. + unsafe { base_ptr.add(1) } }; { // SAFETY: we're the only owner of this data. unsafe { rc_ptr.write(1) }; } - // SAFETY: ptr is past rc and waiters_ptr - let ptr = unsafe { rc_ptr.add(2) }; + // SAFETY: ptr is past waiters_ptr and rc + let ptr = unsafe { rc_ptr.add(1) }; // SAFETY: ptr does point to size bytes of readable and writable // Rust memory. After this call, that memory is deallocated and we // receive a new RacyMemory in its stead. Reads and writes through // it are undefined behaviour. Note though that we still have the - // RC, waiters pointer, and possible length values before the + // Waiters pointer, RC, and possible length values before the // pointer; those are in normal Rust memory. let ptr = unsafe { RacyMemory::::enter(ptr.cast(), size) }; Some(Self { @@ -718,8 +720,8 @@ impl SharedDataBlock { /// /// Must not be a dangling SharedDataBlock. unsafe fn get_rc(&self) -> &AtomicUsize { - // SAFETY: type guarantees layout - unsafe { self.ptr.as_ptr().cast::().sub(2).as_ref() } + // SAFETY: type guarantees layout; rc is 1 slot before ptr. + unsafe { self.ptr.as_ptr().cast::().sub(1).as_ref() } } /// Get a reference to the atomic byte length. @@ -782,12 +784,12 @@ impl SharedDataBlock { /// /// Must not be a dangling SharedDataBlock. unsafe fn get_waiters_ptr(&self) -> &AtomicPtr { - // SAFETY: type guarantees layout; waiters_ptr is 1 slot before ptr. + // SAFETY: type guarantees layout; waiters_ptr is 2 slots before ptr. unsafe { self.ptr .as_ptr() .cast::>() - .sub(1) + .sub(2) .as_ref() } } From da511304c7c94b530290f125dc894a685efef844 Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Tue, 3 Mar 2026 08:13:41 -0300 Subject: [PATCH 12/25] Fix nitpicks --- .../structured_data/atomics_object.rs | 21 +++++++++---------- .../src/ecmascript/types/spec/data_block.rs | 1 + 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs index 56961715a..32fbd428c 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs @@ -3,7 +3,6 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use std::{ - collections::VecDeque, hint::assert_unchecked, ops::ControlFlow, sync::{ @@ -30,7 +29,7 @@ use crate::{ to_integer_or_infinity, to_number, try_result_into_js, try_to_index, unwrap_try, validate_index, validate_typed_array, }, - ecmascript::{WaitResult, WaiterList, WaiterRecord}, + ecmascript::{WaitResult, WaiterRecord}, engine::{Bindable, GcScope, Global, NoGcScope, Scopable}, heap::{ObjectEntry, WellKnownSymbols}, }; @@ -672,7 +671,8 @@ impl AtomicsObject { // 8. Let WL be GetWaiterList(block, byteIndexInBuffer). let data_block = buffer.get_data_block(agent); // 9. Perform EnterCriticalSection(WL). - // SAFETY: buffer is a valid SharedArrayBuffer it cannot be detached, so the data block is non-dangling. + // SAFETY: buffer is a valid SharedArrayBuffer and cannot be detached. A 0-sized SAB has a + // dangling data block with no backing allocation, but `get_waiters` returns `None` in that case. let mut n = 0; let Some(waiters) = (unsafe { data_block.get_waiters() }) else { return Ok(0.into()); @@ -694,9 +694,10 @@ impl AtomicsObject { waiter.condvar.notify_one(); n += 1; } - drop(guard); // 12. Perform LeaveCriticalSection(WL). + drop(guard); + // 13. Let n be the number of elements in S. // 14. Return 𝔽(n). Ok(Number::from_usize(agent, n, gc).into()) @@ -1488,7 +1489,9 @@ fn do_wait_critical<'gc, const IS_ASYNC: bool, const IS_I64: bool>( // 29. If mode is sync, then if !IS_ASYNC { let data_block = buffer.get_data_block(agent); - // SAFETY: buffer is a valid SharedArrayBuffer it cannot be detached, so the data block is non-dangling. + // SAFETY: buffer is a valid SharedArrayBuffer and cannot be detached. A 0-sized SAB would + // have a dangling data block, but Atomics.wait requires `byteIndex` to be within bounds, + // so a 0-sized SAB would have been rejected earlier with a RangeError. let waiters = unsafe { data_block.get_or_init_waiters() }; let waiter_record = Arc::new(WaiterRecord { condvar: Condvar::new(), @@ -1512,9 +1515,7 @@ fn do_wait_critical<'gc, const IS_ASYNC: bool, const IS_I64: bool>( // a. Perform SuspendThisAgent(WL, waiterRecord). guard .entry(byte_index_in_buffer) - .or_insert_with(|| WaiterList { - waiters: VecDeque::new(), - }) + .or_default() .waiters .push_back(waiter_record.clone()); @@ -1748,9 +1749,7 @@ fn enqueue_atomics_wait_async_job( guard .entry(byte_index_in_buffer) - .or_insert_with(|| WaiterList { - waiters: VecDeque::new(), - }) + .or_default() .waiters .push_back(waiter_record.clone()); diff --git a/nova_vm/src/ecmascript/types/spec/data_block.rs b/nova_vm/src/ecmascript/types/spec/data_block.rs index aee2f1a50..1f5de8ec9 100644 --- a/nova_vm/src/ecmascript/types/spec/data_block.rs +++ b/nova_vm/src/ecmascript/types/spec/data_block.rs @@ -437,6 +437,7 @@ pub enum WaitResult { } #[cfg(feature = "shared-array-buffer")] +#[derive(Default)] pub struct WaiterList { pub waiters: std::collections::VecDeque>, } From 8ecc65ff2f724b247085b365d738e94ff254cee7 Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Tue, 3 Mar 2026 09:02:08 -0300 Subject: [PATCH 13/25] Fix accessors --- nova_vm/src/ecmascript/types/spec/data_block.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nova_vm/src/ecmascript/types/spec/data_block.rs b/nova_vm/src/ecmascript/types/spec/data_block.rs index 1f5de8ec9..d6115fbf0 100644 --- a/nova_vm/src/ecmascript/types/spec/data_block.rs +++ b/nova_vm/src/ecmascript/types/spec/data_block.rs @@ -422,7 +422,7 @@ impl SharedDataBlockMaxByteLength { } #[cfg(feature = "shared-array-buffer")] -pub struct WaiterRecord { +pub(crate) struct WaiterRecord { pub condvar: Condvar, pub notified: AtomicBool, } @@ -430,7 +430,7 @@ pub struct WaiterRecord { /// Result of an `Atomics.wait` or `Atomics.waitAsync` operation. #[derive(Debug)] #[cfg(feature = "shared-array-buffer")] -pub enum WaitResult { +pub(crate) enum WaitResult { Ok, TimedOut, NotEqual, @@ -438,12 +438,12 @@ pub enum WaitResult { #[cfg(feature = "shared-array-buffer")] #[derive(Default)] -pub struct WaiterList { +pub(crate) struct WaiterList { pub waiters: std::collections::VecDeque>, } #[cfg(feature = "shared-array-buffer")] -pub type SharedWaiterMap = Mutex>; +type SharedWaiterMap = Mutex>; /// # [6.2.9 Data Blocks](https://tc39.es/ecma262/#sec-data-blocks) /// From 41d688773dfaba5c80a9a5821fea742bba31ea42 Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Tue, 3 Mar 2026 09:06:30 -0300 Subject: [PATCH 14/25] Removed ecmascript_futex package --- Cargo.toml | 1 - nova_vm/Cargo.toml | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3cc23aa71..376aa6ad5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,6 @@ cliclack = "0.4.0" console = "0.16.2" ctrlc = "3.5.0" ecmascript_atomics = { version = "0.2.3" } -ecmascript_futex = { version = "0.1.0" } fast-float = "0.2.0" hashbrown = "0.16.1" lexical = { version = "7.0.5", default-features = false, features = [ diff --git a/nova_vm/Cargo.toml b/nova_vm/Cargo.toml index e4842090a..9945aad9e 100644 --- a/nova_vm/Cargo.toml +++ b/nova_vm/Cargo.toml @@ -14,7 +14,6 @@ categories.workspace = true [dependencies] ahash = { workspace = true } ecmascript_atomics = { workspace = true, optional = true } -ecmascript_futex = { workspace = true, optional = true } fast-float = { workspace = true } hashbrown = { workspace = true } lexical = { workspace = true } @@ -61,7 +60,7 @@ atomics = [ "array-buffer", "shared-array-buffer", "dep:ecmascript_atomics", - "dep:ecmascript_futex", + ] date = [] json = ["dep:sonic-rs"] From 17bc6e56456bb1b9c8236b44e6a5c158388abfc8 Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Tue, 3 Mar 2026 09:36:11 -0300 Subject: [PATCH 15/25] Use `wait_timeout_while` --- .../structured_data/atomics_object.rs | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs index 32fbd428c..f15248c05 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs @@ -1524,25 +1524,29 @@ fn do_wait_critical<'gc, const IS_ASYNC: bool, const IS_I64: bool>( guard = waiter_record.condvar.wait(guard).unwrap(); } } else { - let deadline = Instant::now() + Duration::from_millis(t); - loop { - if waiter_record.notified.load(StdOrdering::Acquire) { - break; - } - let remaining = deadline.saturating_duration_since(Instant::now()); - if remaining.is_zero() { - if let Some(list) = guard.get_mut(&byte_index_in_buffer) { - list.waiters.retain(|w| !Arc::ptr_eq(w, &waiter_record)); + let lock_result = + waiter_record + .condvar + .wait_timeout_while(guard, Duration::from_millis(t), |_| { + !waiter_record.notified.load(StdOrdering::Acquire) + }); + + match lock_result { + Ok((new_guard, timeout)) => { + guard = new_guard; + if timeout.timed_out() { + if let Some(list) = guard.get_mut(&byte_index_in_buffer) { + list.waiters.retain(|w| !Arc::ptr_eq(w, &waiter_record)); + } + + // 31. Perform LeaveCriticalSection(WL). + // 32. If mode is sync, return waiterRecord.[[Result]]. + return BUILTIN_STRING_MEMORY.timed_out.into(); } - // 31. Perform LeaveCriticalSection(WL). - // 32. If mode is sync, return waiterRecord.[[Result]]. - return BUILTIN_STRING_MEMORY.timed_out.into(); } - let (new_guard, _) = waiter_record - .condvar - .wait_timeout(guard, remaining) - .unwrap(); - guard = new_guard; + Err(e) => panic!( + "Another thread panicked while holding the waiter list lock, poisoning it: {e:?}" + ), } } // 31. Perform LeaveCriticalSection(WL). From b6c58bd143a39b2a78281e084a60ea7788ff8756 Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Tue, 3 Mar 2026 09:39:28 -0300 Subject: [PATCH 16/25] Use wait_while --- .../builtins/structured_data/atomics_object.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs index f15248c05..1ab010d48 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs @@ -1520,8 +1520,14 @@ fn do_wait_critical<'gc, const IS_ASYNC: bool, const IS_I64: bool>( .push_back(waiter_record.clone()); if t == u64::MAX { - while !waiter_record.notified.load(StdOrdering::Acquire) { - guard = waiter_record.condvar.wait(guard).unwrap(); + let lock_result = waiter_record.condvar.wait_while(guard, |_| { + !waiter_record.notified.load(StdOrdering::Acquire) + }); + match lock_result { + Ok(new_guard) => guard = new_guard, + Err(e) => panic!( + "Another thread panicked while holding the waiter list lock, poisoning it: {e:?}" + ), } } else { let lock_result = From 61821e3e340dfad64e7c732cb9f61e9bab09ea2c Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Tue, 3 Mar 2026 09:40:45 -0300 Subject: [PATCH 17/25] Fix warn --- .../src/ecmascript/builtins/structured_data/atomics_object.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs index 1ab010d48..f53aed447 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs @@ -1524,7 +1524,7 @@ fn do_wait_critical<'gc, const IS_ASYNC: bool, const IS_I64: bool>( !waiter_record.notified.load(StdOrdering::Acquire) }); match lock_result { - Ok(new_guard) => guard = new_guard, + Ok(_) => (), Err(e) => panic!( "Another thread panicked while holding the waiter list lock, poisoning it: {e:?}" ), From 9b3bc573c38233d882a00d99e70cc5c6df10d69e Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Tue, 3 Mar 2026 09:43:33 -0300 Subject: [PATCH 18/25] Update wait --- .../structured_data/atomics_object.rs | 48 ++++++++++++------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs index f53aed447..60ebaaf08 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs @@ -1764,28 +1764,40 @@ fn enqueue_atomics_wait_async_job( .push_back(waiter_record.clone()); if t == u64::MAX { - while !waiter_record.notified.load(StdOrdering::Acquire) { - guard = waiter_record.condvar.wait(guard).unwrap(); + let lock_result = waiter_record.condvar.wait_while(guard, |_| { + !waiter_record.notified.load(StdOrdering::Acquire) + }); + match lock_result { + Ok(_) => return WaitResult::Ok, + Err(e) => panic!( + "Another thread panicked while holding the waiter list lock, poisoning it: {e:?}" + ), } - WaitResult::Ok } else { - let deadline = Instant::now() + Duration::from_millis(t); - loop { - if waiter_record.notified.load(StdOrdering::Acquire) { - return WaitResult::Ok; - } - let remaining = deadline.saturating_duration_since(Instant::now()); - if remaining.is_zero() { - if let Some(list) = guard.get_mut(&byte_index_in_buffer) { - list.waiters.retain(|w| !Arc::ptr_eq(w, &waiter_record)); + let lock_result = + waiter_record + .condvar + .wait_timeout_while(guard, Duration::from_millis(t), |_| { + !waiter_record.notified.load(StdOrdering::Acquire) + }); + + match lock_result { + Ok((new_guard, timeout)) => { + guard = new_guard; + if timeout.timed_out() { + if let Some(list) = guard.get_mut(&byte_index_in_buffer) { + list.waiters.retain(|w| !Arc::ptr_eq(w, &waiter_record)); + } + + // 31. Perform LeaveCriticalSection(WL). + // 32. If mode is sync, return waiterRecord.[[Result]]. + return WaitResult::TimedOut; } - return WaitResult::TimedOut; + return WaitResult::Ok; } - let (new_guard, _) = waiter_record - .condvar - .wait_timeout(guard, remaining) - .unwrap(); - guard = new_guard; + Err(e) => panic!( + "Another thread panicked while holding the waiter list lock, poisoning it: {e:?}" + ), } } }); From 0b76b9d5a519d91c0eb8066091880d9a32eb66e0 Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Tue, 3 Mar 2026 09:43:48 -0300 Subject: [PATCH 19/25] Fix imports --- .../src/ecmascript/builtins/structured_data/atomics_object.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs index 60ebaaf08..143b0fd63 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs @@ -10,7 +10,7 @@ use std::{ atomic::{AtomicBool, Ordering as StdOrdering}, }, thread::{self, JoinHandle}, - time::{Duration, Instant}, + time::Duration, }; use ecmascript_atomics::Ordering; From 0c606d6dee214fc9ce0363661d7606c39629121d Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Tue, 3 Mar 2026 10:52:06 -0300 Subject: [PATCH 20/25] Fix Lint --- .../src/ecmascript/builtins/structured_data/atomics_object.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs index 143b0fd63..4dc3a3c9b 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs @@ -1768,7 +1768,7 @@ fn enqueue_atomics_wait_async_job( !waiter_record.notified.load(StdOrdering::Acquire) }); match lock_result { - Ok(_) => return WaitResult::Ok, + Ok(_) => WaitResult::Ok, Err(e) => panic!( "Another thread panicked while holding the waiter list lock, poisoning it: {e:?}" ), @@ -1793,7 +1793,7 @@ fn enqueue_atomics_wait_async_job( // 32. If mode is sync, return waiterRecord.[[Result]]. return WaitResult::TimedOut; } - return WaitResult::Ok; + WaitResult::Ok } Err(e) => panic!( "Another thread panicked while holding the waiter list lock, poisoning it: {e:?}" From 8ea4506e240f0f4fc09212086d0b9d69b9606f7d Mon Sep 17 00:00:00 2001 From: Aapo Alasuutari Date: Thu, 5 Mar 2026 19:38:16 +0200 Subject: [PATCH 21/25] Some API wrapping --- .../structured_data/atomics_object.rs | 134 +++++------------- .../src/ecmascript/types/spec/data_block.rs | 131 ++++++++++++++--- 2 files changed, 152 insertions(+), 113 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs index 4dc3a3c9b..506817463 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/atomics_object.rs @@ -6,7 +6,7 @@ use std::{ hint::assert_unchecked, ops::ControlFlow, sync::{ - Arc, Condvar, + Arc, atomic::{AtomicBool, Ordering as StdOrdering}, }, thread::{self, JoinHandle}, @@ -21,15 +21,14 @@ use crate::{ BigInt, Builtin, ExceptionType, InnerJob, Job, JsResult, Number, Numeric, OrdinaryObject, Promise, PromiseCapability, Realm, SharedArrayBuffer, SharedDataBlock, SharedTypedArray, String, TryError, TryResult, TypedArrayAbstractOperations, - TypedArrayWithBufferWitnessRecords, Value, builders::OrdinaryObjectBuilder, - compare_exchange_in_buffer, for_any_typed_array, get_modify_set_value_in_buffer, - get_value_from_buffer, make_typed_array_with_buffer_witness_record, - number_convert_to_integer_or_infinity, set_value_in_buffer, to_big_int, to_big_int64, - to_big_int64_big_int, to_index, to_int32, to_int32_number, to_integer_number_or_infinity, - to_integer_or_infinity, to_number, try_result_into_js, try_to_index, unwrap_try, - validate_index, validate_typed_array, + TypedArrayWithBufferWitnessRecords, Value, WaitResult, WaiterRecord, + builders::OrdinaryObjectBuilder, compare_exchange_in_buffer, for_any_typed_array, + get_modify_set_value_in_buffer, get_value_from_buffer, + make_typed_array_with_buffer_witness_record, number_convert_to_integer_or_infinity, + set_value_in_buffer, to_big_int, to_big_int64, to_big_int64_big_int, to_index, to_int32, + to_int32_number, to_integer_number_or_infinity, to_integer_or_infinity, to_number, + try_result_into_js, try_to_index, unwrap_try, validate_index, validate_typed_array, }, - ecmascript::{WaitResult, WaiterRecord}, engine::{Bindable, GcScope, Global, NoGcScope, Scopable}, heap::{ObjectEntry, WellKnownSymbols}, }; @@ -680,18 +679,17 @@ impl AtomicsObject { let mut guard = waiters.lock().unwrap(); // 10. Let S be RemoveWaiters(WL, c). - let Some(list) = guard.get_mut(&byte_index_in_buffer) else { + let Some(list) = guard.get_list_mut(byte_index_in_buffer) else { return Ok(0.into()); }; // 11. For each element W of S, do - // a. Perform NotifyWaiter(WL, W). while n < c { - let Some(waiter) = list.waiters.pop_front() else { + let Some(w) = list.pop() else { break; }; - waiter.notified.store(true, StdOrdering::Release); - waiter.condvar.notify_one(); + // a. Perform NotifyWaiter(WL, W). + w.notify_waiters(); n += 1; } @@ -1493,10 +1491,7 @@ fn do_wait_critical<'gc, const IS_ASYNC: bool, const IS_I64: bool>( // have a dangling data block, but Atomics.wait requires `byteIndex` to be within bounds, // so a 0-sized SAB would have been rejected earlier with a RangeError. let waiters = unsafe { data_block.get_or_init_waiters() }; - let waiter_record = Arc::new(WaiterRecord { - condvar: Condvar::new(), - notified: AtomicBool::new(false), - }); + let waiter_record = WaiterRecord::new_shared(); let mut guard = waiters.lock().unwrap(); // Re-read value under critical section to avoid TOCTOU race. @@ -1513,46 +1508,20 @@ fn do_wait_critical<'gc, const IS_ASYNC: bool, const IS_I64: bool>( } // a. Perform SuspendThisAgent(WL, waiterRecord). - guard - .entry(byte_index_in_buffer) - .or_default() - .waiters - .push_back(waiter_record.clone()); + guard.push_to_list(byte_index_in_buffer, waiter_record.clone()); if t == u64::MAX { - let lock_result = waiter_record.condvar.wait_while(guard, |_| { - !waiter_record.notified.load(StdOrdering::Acquire) - }); - match lock_result { - Ok(_) => (), - Err(e) => panic!( - "Another thread panicked while holding the waiter list lock, poisoning it: {e:?}" - ), - } + waiter_record.wait(guard); } else { - let lock_result = - waiter_record - .condvar - .wait_timeout_while(guard, Duration::from_millis(t), |_| { - !waiter_record.notified.load(StdOrdering::Acquire) - }); - - match lock_result { - Ok((new_guard, timeout)) => { - guard = new_guard; - if timeout.timed_out() { - if let Some(list) = guard.get_mut(&byte_index_in_buffer) { - list.waiters.retain(|w| !Arc::ptr_eq(w, &waiter_record)); - } - - // 31. Perform LeaveCriticalSection(WL). - // 32. If mode is sync, return waiterRecord.[[Result]]. - return BUILTIN_STRING_MEMORY.timed_out.into(); - } - } - Err(e) => panic!( - "Another thread panicked while holding the waiter list lock, poisoning it: {e:?}" - ), + let dur = Duration::from_millis(t); + let (new_guard, timeout) = waiter_record.wait_timeout(guard, dur); + guard = new_guard; + if timeout.timed_out() { + guard.remove_from_list(byte_index_in_buffer, waiter_record); + + // 31. Perform LeaveCriticalSection(WL). + // 32. If mode is sync, return waiterRecord.[[Result]]. + return BUILTIN_STRING_MEMORY.timed_out.into(); } } // 31. Perform LeaveCriticalSection(WL). @@ -1734,10 +1703,7 @@ fn enqueue_atomics_wait_async_job( let handle = thread::spawn(move || { // SAFETY: buffer is a cloned SharedDataBlock; non-dangling. let waiters = unsafe { buffer.get_or_init_waiters() }; - let waiter_record = Arc::new(WaiterRecord { - condvar: Condvar::new(), - notified: AtomicBool::new(false), - }); + let waiter_record = WaiterRecord::new_shared(); let mut guard = waiters.lock().unwrap(); // Re-check the value under the critical section. @@ -1757,49 +1723,23 @@ fn enqueue_atomics_wait_async_job( return WaitResult::NotEqual; } - guard - .entry(byte_index_in_buffer) - .or_default() - .waiters - .push_back(waiter_record.clone()); + guard.push_to_list(byte_index_in_buffer, waiter_record.clone()); if t == u64::MAX { - let lock_result = waiter_record.condvar.wait_while(guard, |_| { - !waiter_record.notified.load(StdOrdering::Acquire) - }); - match lock_result { - Ok(_) => WaitResult::Ok, - Err(e) => panic!( - "Another thread panicked while holding the waiter list lock, poisoning it: {e:?}" - ), - } + waiter_record.wait(guard); } else { - let lock_result = - waiter_record - .condvar - .wait_timeout_while(guard, Duration::from_millis(t), |_| { - !waiter_record.notified.load(StdOrdering::Acquire) - }); - - match lock_result { - Ok((new_guard, timeout)) => { - guard = new_guard; - if timeout.timed_out() { - if let Some(list) = guard.get_mut(&byte_index_in_buffer) { - list.waiters.retain(|w| !Arc::ptr_eq(w, &waiter_record)); - } - - // 31. Perform LeaveCriticalSection(WL). - // 32. If mode is sync, return waiterRecord.[[Result]]. - return WaitResult::TimedOut; - } - WaitResult::Ok - } - Err(e) => panic!( - "Another thread panicked while holding the waiter list lock, poisoning it: {e:?}" - ), + let dur = Duration::from_millis(t); + let (new_guard, timeout) = waiter_record.wait_timeout(guard, dur); + guard = new_guard; + if timeout.timed_out() { + guard.remove_from_list(byte_index_in_buffer, waiter_record); + + // 31. Perform LeaveCriticalSection(WL). + // 32. If mode is sync, return waiterRecord.[[Result]]. + return WaitResult::TimedOut; } } + WaitResult::Ok }); let wait_async_job = Job { realm: Some(Global::new(agent, agent.current_realm(gc).unbind())), diff --git a/nova_vm/src/ecmascript/types/spec/data_block.rs b/nova_vm/src/ecmascript/types/spec/data_block.rs index d6115fbf0..972ae1bc3 100644 --- a/nova_vm/src/ecmascript/types/spec/data_block.rs +++ b/nova_vm/src/ecmascript/types/spec/data_block.rs @@ -5,11 +5,15 @@ //! ### [6.2.9 Data Blocks](https://tc39.es/ecma262/#sec-data-blocks) #[cfg(feature = "shared-array-buffer")] -use core::sync::atomic::{AtomicBool, AtomicPtr, AtomicUsize, Ordering}; -#[cfg(feature = "shared-array-buffer")] -use std::hint::assert_unchecked; -#[cfg(feature = "shared-array-buffer")] -use std::sync::{Arc, Condvar, Mutex}; +use std::{ + collections::hash_map::Entry, + hint::assert_unchecked, + sync::{ + Arc, Condvar, Mutex, MutexGuard, WaitTimeoutResult, + atomic::{AtomicBool, AtomicPtr, AtomicUsize, Ordering}, + }, + time::Duration, +}; use core::{ f32, f64, @@ -422,9 +426,52 @@ impl SharedDataBlockMaxByteLength { } #[cfg(feature = "shared-array-buffer")] +#[derive(Default)] pub(crate) struct WaiterRecord { - pub condvar: Condvar, - pub notified: AtomicBool, + condvar: Condvar, + notified: AtomicBool, +} + +#[cfg(feature = "shared-array-buffer")] +impl WaiterRecord { + pub(crate) fn new_shared() -> Arc { + Arc::new(Self::default()) + } + + pub(crate) fn notify_waiters(self: Arc) { + self.notified + .store(true, std::sync::atomic::Ordering::Relaxed); + self.condvar.notify_all(); + } + + pub(crate) fn wait<'a, T>(self: &Arc, guard: MutexGuard<'a, T>) { + let lock_result = self + .condvar + .wait_while(guard, |_| !self.notified.load(Ordering::Relaxed)); + match lock_result { + Ok(_) => (), + Err(e) => panic!( + "Another thread panicked while holding the waiter list lock, poisoning it: {e:?}" + ), + } + } + + pub(crate) fn wait_timeout<'a, T>( + self: &Arc, + guard: MutexGuard<'a, T>, + dur: Duration, + ) -> (MutexGuard<'a, T>, WaitTimeoutResult) { + let lock_result = self + .condvar + .wait_timeout_while(guard, dur, |_| !self.notified.load(Ordering::Relaxed)); + + match lock_result { + Ok(result) => result, + Err(e) => panic!( + "Another thread panicked while holding the waiter list lock, poisoning it: {e:?}" + ), + } + } } /// Result of an `Atomics.wait` or `Atomics.waitAsync` operation. @@ -438,12 +485,66 @@ pub(crate) enum WaitResult { #[cfg(feature = "shared-array-buffer")] #[derive(Default)] +#[repr(transparent)] pub(crate) struct WaiterList { - pub waiters: std::collections::VecDeque>, + waiters: std::collections::VecDeque>, +} + +impl WaiterList { + pub(crate) fn is_empty(&self) -> bool { + self.waiters.is_empty() + } + + pub(crate) fn pop(&mut self) -> Option> { + self.waiters.pop_front() + } + + pub(crate) fn push(&mut self, w: Arc) { + self.waiters.push_back(w); + } + + pub(crate) fn remove(&mut self, w: Arc) -> bool { + let Some(index) = self + .waiters + .iter() + .enumerate() + .find(|(_, e)| Arc::ptr_eq(e, &w)) + .map(|(i, _)| i) + else { + return false; + }; + self.waiters.remove(index); + true + } } #[cfg(feature = "shared-array-buffer")] -type SharedWaiterMap = Mutex>; +#[repr(transparent)] +#[derive(Default)] +pub(crate) struct WaiterLists { + map: std::collections::HashMap, +} + +impl WaiterLists { + pub(crate) fn get_list_mut(&mut self, index: usize) -> Option<&mut WaiterList> { + self.map.get_mut(&index) + } + + pub(crate) fn push_to_list(&mut self, index: usize, w: Arc) { + self.map.entry(index).or_default().push(w); + } + + pub(crate) fn remove_from_list(&mut self, index: usize, w: Arc) { + match self.map.entry(index) { + Entry::Occupied(mut entry) => { + if entry.get_mut().remove(w) && entry.get().is_empty() { + entry.remove(); + } + } + Entry::Vacant(_) => {} + } + } +} /// # [6.2.9 Data Blocks](https://tc39.es/ecma262/#sec-data-blocks) /// @@ -784,12 +885,12 @@ impl SharedDataBlock { /// ## Safety /// /// Must not be a dangling SharedDataBlock. - unsafe fn get_waiters_ptr(&self) -> &AtomicPtr { + unsafe fn get_waiters_ptr(&self) -> &AtomicPtr> { // SAFETY: type guarantees layout; waiters_ptr is 2 slots before ptr. unsafe { self.ptr .as_ptr() - .cast::>() + .cast::>>() .sub(2) .as_ref() } @@ -804,7 +905,7 @@ impl SharedDataBlock { /// ## Safety /// /// Must not be a dangling SharedDataBlock. - pub(crate) unsafe fn get_or_init_waiters(&self) -> &SharedWaiterMap { + pub(crate) unsafe fn get_or_init_waiters(&self) -> &Mutex { // SAFETY: caller guarantees non-dangling. let waiters_atomic = unsafe { self.get_waiters_ptr() }; let current = waiters_atomic.load(Ordering::Acquire); @@ -814,9 +915,7 @@ impl SharedDataBlock { return unsafe { &*current }; } - let new_map = Box::into_raw(Box::new(SharedWaiterMap::new( - std::collections::HashMap::new(), - ))); + let new_map = Box::into_raw(Box::new(Default::default())); match waiters_atomic.compare_exchange( core::ptr::null_mut(), new_map, @@ -848,7 +947,7 @@ impl SharedDataBlock { /// ## Safety /// /// Must not be a dangling SharedDataBlock. - pub(crate) unsafe fn get_waiters(&self) -> Option<&SharedWaiterMap> { + pub(crate) unsafe fn get_waiters(&self) -> Option<&Mutex> { // SAFETY: caller guarantees non-dangling. let waiters_atomic = unsafe { self.get_waiters_ptr() }; let current = waiters_atomic.load(Ordering::Acquire); From 9df73fde139ec94a84423fb22bf1a3e51cc08568 Mon Sep 17 00:00:00 2001 From: Aapo Alasuutari Date: Thu, 5 Mar 2026 19:38:21 +0200 Subject: [PATCH 22/25] chore(test262): Update expectations --- tests/expectations.json | 4 +--- tests/metrics.json | 8 ++++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/expectations.json b/tests/expectations.json index 3c5e54df2..5816371fd 100644 --- a/tests/expectations.json +++ b/tests/expectations.json @@ -793,7 +793,7 @@ "built-ins/Number/prototype/toExponential/return-values.js": "FAIL", "built-ins/Number/prototype/toExponential/tointeger-fractiondigits.js": "FAIL", "built-ins/Number/prototype/toExponential/undefined-fractiondigits.js": "FAIL", - "built-ins/Object/defineProperty/15.2.3.6-4-116.js": "FAIL", + "built-ins/Object/defineProperty/15.2.3.6-4-116.js": "TIMEOUT", "built-ins/Object/defineProperty/15.2.3.6-4-292-1.js": "FAIL", "built-ins/Object/defineProperty/15.2.3.6-4-293-2.js": "FAIL", "built-ins/Object/defineProperty/15.2.3.6-4-293-3.js": "FAIL", @@ -6672,13 +6672,11 @@ "staging/sm/ArrayBuffer/slice-species.js": "FAIL", "staging/sm/AsyncGenerators/for-await-of-error.js": "CRASH", "staging/sm/BigInt/Number-conversion-rounding.js": "FAIL", - "staging/sm/Date/dst-offset-caching-1-of-8.js": "TIMEOUT", "staging/sm/Date/dst-offset-caching-2-of-8.js": "TIMEOUT", "staging/sm/Date/dst-offset-caching-3-of-8.js": "TIMEOUT", "staging/sm/Date/dst-offset-caching-4-of-8.js": "TIMEOUT", "staging/sm/Date/dst-offset-caching-5-of-8.js": "TIMEOUT", "staging/sm/Date/dst-offset-caching-6-of-8.js": "TIMEOUT", - "staging/sm/Date/dst-offset-caching-7-of-8.js": "TIMEOUT", "staging/sm/Date/dst-offset-caching-8-of-8.js": "TIMEOUT", "staging/sm/Date/non-iso.js": "FAIL", "staging/sm/Date/prototype-is-not-a-date.js": "FAIL", diff --git a/tests/metrics.json b/tests/metrics.json index 9cbff87df..4c4f613eb 100644 --- a/tests/metrics.json +++ b/tests/metrics.json @@ -1,11 +1,11 @@ { "results": { "crash": 52, - "fail": 6959, - "pass": 40341, + "fail": 6966, + "pass": 40335, "skip": 3326, - "timeout": 18, + "timeout": 17, "unresolved": 37 }, "total": 50733 -} \ No newline at end of file +} From 6ea74946301b10202d8e42f07587ea2e8a5349b0 Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Fri, 6 Mar 2026 09:24:53 -0300 Subject: [PATCH 23/25] Fix lint issue --- nova_vm/src/ecmascript/types/spec/data_block.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova_vm/src/ecmascript/types/spec/data_block.rs b/nova_vm/src/ecmascript/types/spec/data_block.rs index 972ae1bc3..77fa0214f 100644 --- a/nova_vm/src/ecmascript/types/spec/data_block.rs +++ b/nova_vm/src/ecmascript/types/spec/data_block.rs @@ -915,7 +915,7 @@ impl SharedDataBlock { return unsafe { &*current }; } - let new_map = Box::into_raw(Box::new(Default::default())); + let new_map = Box::into_raw(Box::default()); match waiters_atomic.compare_exchange( core::ptr::null_mut(), new_map, From 4fe6c8797f86e355225c02e4d9c7ca12f85c7b67 Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Fri, 6 Mar 2026 09:26:16 -0300 Subject: [PATCH 24/25] Updated test expectations --- tests/expectations.json | 6 ------ tests/metrics.json | 8 ++++---- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/tests/expectations.json b/tests/expectations.json index 5816371fd..9ebafc045 100644 --- a/tests/expectations.json +++ b/tests/expectations.json @@ -6672,12 +6672,6 @@ "staging/sm/ArrayBuffer/slice-species.js": "FAIL", "staging/sm/AsyncGenerators/for-await-of-error.js": "CRASH", "staging/sm/BigInt/Number-conversion-rounding.js": "FAIL", - "staging/sm/Date/dst-offset-caching-2-of-8.js": "TIMEOUT", - "staging/sm/Date/dst-offset-caching-3-of-8.js": "TIMEOUT", - "staging/sm/Date/dst-offset-caching-4-of-8.js": "TIMEOUT", - "staging/sm/Date/dst-offset-caching-5-of-8.js": "TIMEOUT", - "staging/sm/Date/dst-offset-caching-6-of-8.js": "TIMEOUT", - "staging/sm/Date/dst-offset-caching-8-of-8.js": "TIMEOUT", "staging/sm/Date/non-iso.js": "FAIL", "staging/sm/Date/prototype-is-not-a-date.js": "FAIL", "staging/sm/Date/setTime-argument-shortcircuiting.js": "FAIL", diff --git a/tests/metrics.json b/tests/metrics.json index 4c4f613eb..c092303a9 100644 --- a/tests/metrics.json +++ b/tests/metrics.json @@ -1,11 +1,11 @@ { "results": { "crash": 52, - "fail": 6966, - "pass": 40335, + "fail": 6958, + "pass": 40349, "skip": 3326, - "timeout": 17, + "timeout": 11, "unresolved": 37 }, "total": 50733 -} +} \ No newline at end of file From fad50c9eddb4291ea4f6d5649542fcf55f14d869 Mon Sep 17 00:00:00 2001 From: Felipe Armoni Date: Fri, 6 Mar 2026 09:36:25 -0300 Subject: [PATCH 25/25] Updated metrics --- tests/expectations.json | 12 ++++++++++-- tests/metrics.json | 6 +++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/tests/expectations.json b/tests/expectations.json index 9ebafc045..161c5d5d4 100644 --- a/tests/expectations.json +++ b/tests/expectations.json @@ -793,7 +793,7 @@ "built-ins/Number/prototype/toExponential/return-values.js": "FAIL", "built-ins/Number/prototype/toExponential/tointeger-fractiondigits.js": "FAIL", "built-ins/Number/prototype/toExponential/undefined-fractiondigits.js": "FAIL", - "built-ins/Object/defineProperty/15.2.3.6-4-116.js": "TIMEOUT", + "built-ins/Object/defineProperty/15.2.3.6-4-116.js": "FAIL", "built-ins/Object/defineProperty/15.2.3.6-4-292-1.js": "FAIL", "built-ins/Object/defineProperty/15.2.3.6-4-293-2.js": "FAIL", "built-ins/Object/defineProperty/15.2.3.6-4-293-3.js": "FAIL", @@ -6672,6 +6672,14 @@ "staging/sm/ArrayBuffer/slice-species.js": "FAIL", "staging/sm/AsyncGenerators/for-await-of-error.js": "CRASH", "staging/sm/BigInt/Number-conversion-rounding.js": "FAIL", + "staging/sm/Date/dst-offset-caching-1-of-8.js": "TIMEOUT", + "staging/sm/Date/dst-offset-caching-2-of-8.js": "TIMEOUT", + "staging/sm/Date/dst-offset-caching-3-of-8.js": "TIMEOUT", + "staging/sm/Date/dst-offset-caching-4-of-8.js": "TIMEOUT", + "staging/sm/Date/dst-offset-caching-5-of-8.js": "TIMEOUT", + "staging/sm/Date/dst-offset-caching-6-of-8.js": "TIMEOUT", + "staging/sm/Date/dst-offset-caching-7-of-8.js": "TIMEOUT", + "staging/sm/Date/dst-offset-caching-8-of-8.js": "TIMEOUT", "staging/sm/Date/non-iso.js": "FAIL", "staging/sm/Date/prototype-is-not-a-date.js": "FAIL", "staging/sm/Date/setTime-argument-shortcircuiting.js": "FAIL", @@ -7057,4 +7065,4 @@ "staging/sm/syntax/yield-as-identifier.js": "FAIL", "staging/source-phase-imports/import-source-source-text-module.js": "FAIL", "staging/top-level-await/tla-hang-entry.js": "FAIL" -} \ No newline at end of file +} diff --git a/tests/metrics.json b/tests/metrics.json index c092303a9..a98986a42 100644 --- a/tests/metrics.json +++ b/tests/metrics.json @@ -1,11 +1,11 @@ { "results": { "crash": 52, - "fail": 6958, + "fail": 6959, "pass": 40349, "skip": 3326, - "timeout": 11, + "timeout": 18, "unresolved": 37 }, "total": 50733 -} \ No newline at end of file +}