From 3e88be558ee6d2e03b57a4aac1d782ef3ace90fa Mon Sep 17 00:00:00 2001 From: Kaur Matas <33095685+kmatasfp@users.noreply.github.com> Date: Tue, 4 Aug 2026 15:30:58 -0700 Subject: [PATCH 01/11] Expose linear memory tracking APIs --- crates/wasmtime/src/runtime/memory.rs | 162 ++++++++++++++++++ crates/wasmtime/src/runtime/store.rs | 18 +- crates/wasmtime/src/runtime/vm.rs | 1 + crates/wasmtime/src/runtime/vm/memory.rs | 2 + .../src/runtime/vm/memory/shared_memory.rs | 35 +++- .../vm/memory/shared_memory_disabled.rs | 7 +- 6 files changed, 220 insertions(+), 5 deletions(-) diff --git a/crates/wasmtime/src/runtime/memory.rs b/crates/wasmtime/src/runtime/memory.rs index ab418253d4ea..acfee05ddced 100644 --- a/crates/wasmtime/src/runtime/memory.rs +++ b/crates/wasmtime/src/runtime/memory.rs @@ -6,6 +6,7 @@ use crate::trampoline::generate_memory_export; #[cfg(feature = "async")] use crate::vm::VMStore; use crate::{AsContext, AsContextMut, Engine, MemoryType, StoreContext, StoreContextMut}; +use alloc::sync::Arc; use core::cell::UnsafeCell; use core::fmt; use core::slice; @@ -14,6 +15,19 @@ use wasmtime_environ::DefinedMemoryIndex; pub use crate::runtime::vm::WaitResult; +/// A unique linear-memory backing allocated in a [`Store`](crate::Store). +/// +/// Values returned by [`Store::linear_memories`](crate::Store::linear_memories) +/// include memories that are not exported. Imports and multiple exports that +/// refer to the same backing do not produce duplicate values. +#[derive(Clone, Debug)] +pub enum StoreMemory { + /// An unshared linear memory owned by the store. + Unshared(Memory), + /// A shared linear memory visible to the store. + Shared(SharedMemory), +} + /// Error for out of bounds [`Memory`] access. #[derive(Debug)] #[non_exhaustive] @@ -861,6 +875,21 @@ pub struct SharedMemory { engine: Engine, } +/// Keeps a shared-memory growth observer registered. +/// +/// Dropping this value unregisters the observer. The observer is called after a +/// growth has successfully committed and receives the old and new byte sizes. +pub struct SharedMemoryGrowthSubscription { + _observer: Arc, +} + +impl fmt::Debug for SharedMemoryGrowthSubscription { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SharedMemoryGrowthSubscription") + .finish_non_exhaustive() + } +} + impl SharedMemory { /// Construct a [`SharedMemory`] by providing both the `minimum` and /// `maximum` number of 64K-sized pages. This call allocates the necessary @@ -978,6 +1007,22 @@ impl SharedMemory { } } + /// Registers an observer for successful growth of this shared memory. + /// + /// The observer runs synchronously after the new size is committed. It must + /// not block or attempt to grow this memory. Dropping the returned + /// subscription unregisters the observer. + pub fn subscribe_to_growth( + &self, + observer: impl Fn(usize, usize) + Send + Sync + 'static, + ) -> SharedMemoryGrowthSubscription { + let observer: Arc = Arc::new(observer); + self.vm.subscribe_to_growth(&observer); + SharedMemoryGrowthSubscription { + _observer: observer, + } + } + /// Equivalent of the WebAssembly `memory.atomic.notify` instruction for /// this shared memory. /// @@ -1098,6 +1143,123 @@ impl fmt::Debug for SharedMemory { #[cfg(test)] mod tests { use crate::*; + use alloc::sync::Arc; + use alloc::vec::Vec; + use core::sync::atomic::{AtomicUsize, Ordering}; + + #[test] + fn linear_memories_include_unique_non_exported_backings() -> Result<()> { + let engine = Engine::default(); + let module = Module::new( + &engine, + r#"(module + (memory $aliased 2 3) + (export "a" (memory $aliased)) + (export "b" (memory $aliased)) + (memory 4 5) + )"#, + )?; + let mut store = Store::new(&engine, ()); + Instance::new(&mut store, &module, &[])?; + + let mut sizes = store + .linear_memories() + .into_iter() + .map(|memory| match memory { + StoreMemory::Unshared(memory) => memory.data_size(&store), + StoreMemory::Shared(_) => unreachable!(), + }) + .collect::>(); + sizes.sort_unstable(); + + assert_eq!(sizes, [2 * 65536, 4 * 65536]); + Ok(()) + } + + #[cfg(feature = "threads")] + #[test] + fn linear_memories_include_shared_and_imported_backings_once() -> Result<()> { + let mut config = Config::new(); + config.wasm_threads(true).shared_memory(true); + let engine = Engine::new(&config)?; + let module = Module::new( + &engine, + r#"(module + (import "env" "imported" (memory $imported 5 10 shared)) + (memory $owned 2 3) + (export "a" (memory $owned)) + (export "b" (memory $owned)) + (memory 1 2 shared) + )"#, + )?; + let mut store = Store::new(&engine, ()); + let imported = SharedMemory::new(&engine, MemoryType::shared(5, 10))?; + Instance::new(&mut store, &module, &[imported.into()])?; + + let memories = store.linear_memories(); + assert_eq!(memories.len(), 3); + assert_eq!( + memories + .iter() + .filter(|memory| matches!(memory, StoreMemory::Unshared(_))) + .count(), + 1 + ); + let mut shared_sizes = memories + .iter() + .filter_map(|memory| match memory { + StoreMemory::Shared(memory) => Some(memory.data_size()), + StoreMemory::Unshared(_) => None, + }) + .collect::>(); + shared_sizes.sort_unstable(); + assert_eq!(shared_sizes, [65536, 5 * 65536]); + Ok(()) + } + + #[cfg(feature = "threads")] + #[test] + fn shared_memory_growth_subscriptions_observe_successful_growth() -> Result<()> { + let mut config = Config::new(); + config.wasm_threads(true).shared_memory(true); + let engine = Engine::new(&config)?; + let memory = SharedMemory::new(&engine, MemoryType::shared(1, 2))?; + let module = Module::new( + &engine, + r#"(module + (import "env" "memory" (memory 1 2 shared)) + (func (export "grow") (result i32) (memory.grow (i32.const 1))) + )"#, + )?; + let mut store = Store::new(&engine, ()); + let instance = Instance::new(&mut store, &module, &[memory.clone().into()])?; + let grow = instance.get_typed_func::<(), i32>(&mut store, "grow")?; + let old_size = Arc::new(AtomicUsize::new(0)); + let new_size = Arc::new(AtomicUsize::new(0)); + let calls = Arc::new(AtomicUsize::new(0)); + let subscription = memory.subscribe_to_growth({ + let old_size = old_size.clone(); + let new_size = new_size.clone(); + let calls = calls.clone(); + move |old, new| { + old_size.store(old, Ordering::SeqCst); + new_size.store(new, Ordering::SeqCst); + calls.fetch_add(1, Ordering::SeqCst); + } + }); + + assert_eq!(grow.call(&mut store, ())?, 1); + assert!(memory.grow(1).is_err()); + + assert_eq!(calls.load(Ordering::SeqCst), 1); + assert_eq!(old_size.load(Ordering::SeqCst), 65536); + assert_eq!(new_size.load(Ordering::SeqCst), 2 * 65536); + + drop(subscription); + assert!(memory.grow(0).is_ok()); + assert_eq!(calls.load(Ordering::SeqCst), 1); + Ok(()) + } // Assert that creating a memory via `Memory::new` respects the limits/tunables // in `Config`. diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index c099fc2d3781..95d8962d880a 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -97,7 +97,7 @@ use crate::trampoline::VMHostGlobalContext; #[cfg(feature = "debug")] use crate::{BreakpointState, DebugHandler, FrameDataCache}; use crate::{Engine, Module, Val, ValRaw, module::ModuleRegistry}; -use crate::{Global, Instance, Table}; +use crate::{Global, Instance, SharedMemory, StoreMemory, Table}; use core::convert::Infallible; use core::fmt; #[cfg(any(feature = "async", feature = "gc"))] @@ -1000,6 +1000,22 @@ impl Store { self.inner.engine() } + /// Returns every unique linear-memory backing allocated in this store. + /// + /// This includes non-exported memories and host-created memories. Imported + /// aliases and multiple exports of one backing are returned only once. + pub fn linear_memories(&self) -> Vec { + self.inner + .all_memories() + .map(|memory| match memory { + ExportMemory::Unshared(memory) => StoreMemory::Unshared(memory), + ExportMemory::Shared(memory, _) => { + StoreMemory::Shared(SharedMemory::from_raw(memory, self.engine().clone())) + } + }) + .collect() + } + /// Returns the amount fuel in this [`Store`]. When fuel is enabled, it must /// be configured via [`Store::set_fuel`]. /// diff --git a/crates/wasmtime/src/runtime/vm.rs b/crates/wasmtime/src/runtime/vm.rs index 7b9bac6be456..41beb496765c 100644 --- a/crates/wasmtime/src/runtime/vm.rs +++ b/crates/wasmtime/src/runtime/vm.rs @@ -110,6 +110,7 @@ pub use crate::runtime::vm::instance::{ PoolingInstanceAllocatorConfig, }; pub use crate::runtime::vm::interpreter::*; +pub(crate) use crate::runtime::vm::memory::SharedMemoryGrowthObserver; pub use crate::runtime::vm::memory::{ Memory, MemoryBase, RuntimeLinearMemory, RuntimeMemoryCreator, SharedMemory, }; diff --git a/crates/wasmtime/src/runtime/vm/memory.rs b/crates/wasmtime/src/runtime/vm/memory.rs index fce3c4afa03f..4665278233f2 100644 --- a/crates/wasmtime/src/runtime/vm/memory.rs +++ b/crates/wasmtime/src/runtime/vm/memory.rs @@ -85,6 +85,8 @@ use alloc::sync::Arc; use core::{ops::Range, ptr::NonNull}; use wasmtime_environ::{MemoryKind, MemoryTunables}; +pub(crate) type SharedMemoryGrowthObserver = dyn Fn(usize, usize) + Send + Sync + 'static; + #[cfg(feature = "threads")] use wasmtime_environ::Trap; diff --git a/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs b/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs index 679984c25063..3177fb751abe 100644 --- a/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs +++ b/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs @@ -1,13 +1,15 @@ use crate::Engine; use crate::prelude::*; -use crate::runtime::vm::memory::{LocalMemory, MmapMemory, validate_atomic_addr}; +use crate::runtime::vm::memory::{ + LocalMemory, MmapMemory, SharedMemoryGrowthObserver, validate_atomic_addr, +}; use crate::runtime::vm::parking_spot::{ParkingSpot, Waiter}; use crate::runtime::vm::{self, Memory, VMMemoryDefinition, WaitResult}; use std::cell::RefCell; use std::ops::Range; use std::ptr::NonNull; use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; -use std::sync::{Arc, RwLock}; +use std::sync::{Arc, RwLock, Weak}; use std::time::{Duration, Instant}; use wasmtime_environ::Trap; @@ -24,6 +26,7 @@ pub struct SharedMemory(Arc); struct SharedMemoryInner { memory: RwLock, + growth_observers: RwLock>>, spot: ParkingSpot, ty: wasmtime_environ::Memory, def: LongTermVMMemoryDefinition, @@ -69,6 +72,7 @@ impl SharedMemory { spot: ParkingSpot::default(), def: LongTermVMMemoryDefinition(memory.vmmemory()), memory: RwLock::new(memory), + growth_observers: RwLock::new(Vec::new()), })?)) } @@ -93,7 +97,7 @@ impl SharedMemory { // Without a limiter being passed in this shouldn't have an await point, // so it should be safe to assert that it's ready. let result = vm::assert_ready(memory.grow(delta_pages, None))?; - if let Some((_old_size_in_bytes, new_size_in_bytes)) = result { + if let Some((old_size_in_bytes, new_size_in_bytes)) = result { // Store the new size to the `VMMemoryDefinition` for JIT-generated // code (and runtime functions) to access. No other code can be // growing this memory due to the write lock, but code in other @@ -118,10 +122,35 @@ impl SharedMemory { .0 .current_length .store(new_size_in_bytes, Ordering::SeqCst); + + let observers = { + let mut registered = self.0.growth_observers.write().unwrap(); + let mut observers = Vec::with_capacity(registered.len()); + registered.retain(|observer| { + if let Some(observer) = observer.upgrade() { + observers.push(observer); + true + } else { + false + } + }); + observers + }; + for observer in observers { + observer(old_size_in_bytes, new_size_in_bytes); + } } Ok(result) } + pub fn subscribe_to_growth(&self, observer: &Arc) { + self.0 + .growth_observers + .write() + .unwrap() + .push(Arc::downgrade(observer)); + } + /// Implementation of `memory.atomic.notify` for this shared memory. pub fn atomic_notify(&self, addr_index: u64, count: u32) -> Result { let ptr = validate_atomic_addr(&self.0.def.0, addr_index, 4, 4)?; diff --git a/crates/wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs b/crates/wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs index e9d470acbf61..4ebbdd5b8949 100644 --- a/crates/wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs +++ b/crates/wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs @@ -1,7 +1,8 @@ use crate::Engine; use crate::prelude::*; -use crate::runtime::vm::memory::LocalMemory; +use crate::runtime::vm::memory::{LocalMemory, SharedMemoryGrowthObserver}; use crate::runtime::vm::{VMMemoryDefinition, WaitResult}; +use alloc::sync::Arc; use core::ops::Range; use core::ptr::NonNull; use core::time::Duration; @@ -31,6 +32,10 @@ impl SharedMemory { match *self {} } + pub fn subscribe_to_growth(&self, _observer: &Arc) { + match *self {} + } + pub fn atomic_notify(&self, _addr_index: u64, _count: u32) -> Result { match *self {} } From 6f5d494a8309a743d43c6947da7a6ee6bb7af99c Mon Sep 17 00:00:00 2001 From: Kaur Matas <33095685+kmatasfp@users.noreply.github.com> Date: Tue, 4 Aug 2026 19:59:32 -0700 Subject: [PATCH 02/11] Atomically subscribe to shared memory growth --- crates/wasmtime/src/runtime/memory.rs | 24 +++++++++++++++---- .../src/runtime/vm/memory/shared_memory.rs | 4 +++- .../vm/memory/shared_memory_disabled.rs | 2 +- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/crates/wasmtime/src/runtime/memory.rs b/crates/wasmtime/src/runtime/memory.rs index acfee05ddced..08f7e480fce5 100644 --- a/crates/wasmtime/src/runtime/memory.rs +++ b/crates/wasmtime/src/runtime/memory.rs @@ -1016,11 +1016,24 @@ impl SharedMemory { &self, observer: impl Fn(usize, usize) + Send + Sync + 'static, ) -> SharedMemoryGrowthSubscription { + self.subscribe_to_growth_with_current_size(observer).0 + } + + /// Registers a growth observer and atomically samples the current byte size. + /// + /// No growth can commit between registration and the returned size sample. + pub fn subscribe_to_growth_with_current_size( + &self, + observer: impl Fn(usize, usize) + Send + Sync + 'static, + ) -> (SharedMemoryGrowthSubscription, usize) { let observer: Arc = Arc::new(observer); - self.vm.subscribe_to_growth(&observer); - SharedMemoryGrowthSubscription { - _observer: observer, - } + let current_size = self.vm.subscribe_to_growth(&observer); + ( + SharedMemoryGrowthSubscription { + _observer: observer, + }, + current_size, + ) } /// Equivalent of the WebAssembly `memory.atomic.notify` instruction for @@ -1237,7 +1250,7 @@ mod tests { let old_size = Arc::new(AtomicUsize::new(0)); let new_size = Arc::new(AtomicUsize::new(0)); let calls = Arc::new(AtomicUsize::new(0)); - let subscription = memory.subscribe_to_growth({ + let (subscription, initial_size) = memory.subscribe_to_growth_with_current_size({ let old_size = old_size.clone(); let new_size = new_size.clone(); let calls = calls.clone(); @@ -1247,6 +1260,7 @@ mod tests { calls.fetch_add(1, Ordering::SeqCst); } }); + assert_eq!(initial_size, 65536); assert_eq!(grow.call(&mut store, ())?, 1); assert!(memory.grow(1).is_err()); diff --git a/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs b/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs index 3177fb751abe..7a5e0ca69ec4 100644 --- a/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs +++ b/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs @@ -143,12 +143,14 @@ impl SharedMemory { Ok(result) } - pub fn subscribe_to_growth(&self, observer: &Arc) { + pub fn subscribe_to_growth(&self, observer: &Arc) -> usize { + let memory = self.0.memory.read().unwrap(); self.0 .growth_observers .write() .unwrap() .push(Arc::downgrade(observer)); + memory.byte_size() } /// Implementation of `memory.atomic.notify` for this shared memory. diff --git a/crates/wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs b/crates/wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs index 4ebbdd5b8949..9387ea7d95d0 100644 --- a/crates/wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs +++ b/crates/wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs @@ -32,7 +32,7 @@ impl SharedMemory { match *self {} } - pub fn subscribe_to_growth(&self, _observer: &Arc) { + pub fn subscribe_to_growth(&self, _observer: &Arc) -> usize { match *self {} } From 3d1f41bf51d014788bc5ad6c100685020a551137 Mon Sep 17 00:00:00 2001 From: Kaur Matas <33095685+kmatasfp@users.noreply.github.com> Date: Tue, 4 Aug 2026 21:10:53 -0700 Subject: [PATCH 03/11] Report committed linear memory growth --- crates/wasmtime/src/runtime/limits.rs | 15 ++ crates/wasmtime/src/runtime/memory.rs | 138 ++++++------------ crates/wasmtime/src/runtime/store.rs | 8 + crates/wasmtime/src/runtime/vm.rs | 1 - crates/wasmtime/src/runtime/vm/memory.rs | 6 +- .../src/runtime/vm/memory/shared_memory.rs | 37 +---- .../vm/memory/shared_memory_disabled.rs | 7 +- tests/all/limits.rs | 12 ++ 8 files changed, 89 insertions(+), 135 deletions(-) diff --git a/crates/wasmtime/src/runtime/limits.rs b/crates/wasmtime/src/runtime/limits.rs index 41a40b6529c8..39dd59ac8b92 100644 --- a/crates/wasmtime/src/runtime/limits.rs +++ b/crates/wasmtime/src/runtime/limits.rs @@ -90,6 +90,16 @@ pub trait ResourceLimiter: Send { Ok(()) } + /// Notifies the resource limiter that a linear-memory growth permitted by + /// `memory_growing` has successfully committed. + /// + /// This is not called for a memory's initial allocation or for shared + /// memories. `current` and `desired` are the memory's old and new sizes in + /// bytes. + fn memory_grown(&mut self, _current: usize, _desired: usize) -> Result<()> { + Ok(()) + } + /// Notifies the resource limiter that an instance's table has been /// requested to grow. /// @@ -185,6 +195,11 @@ pub trait ResourceLimiterAsync: Send { Ok(()) } + /// Identical to [`ResourceLimiter::memory_grown`]. + fn memory_grown(&mut self, _current: usize, _desired: usize) -> Result<()> { + Ok(()) + } + /// Asynchronous version of [`ResourceLimiter::table_growing`] async fn table_growing( &mut self, diff --git a/crates/wasmtime/src/runtime/memory.rs b/crates/wasmtime/src/runtime/memory.rs index 08f7e480fce5..3f6dda7af56b 100644 --- a/crates/wasmtime/src/runtime/memory.rs +++ b/crates/wasmtime/src/runtime/memory.rs @@ -6,7 +6,6 @@ use crate::trampoline::generate_memory_export; #[cfg(feature = "async")] use crate::vm::VMStore; use crate::{AsContext, AsContextMut, Engine, MemoryType, StoreContext, StoreContextMut}; -use alloc::sync::Arc; use core::cell::UnsafeCell; use core::fmt; use core::slice; @@ -875,21 +874,6 @@ pub struct SharedMemory { engine: Engine, } -/// Keeps a shared-memory growth observer registered. -/// -/// Dropping this value unregisters the observer. The observer is called after a -/// growth has successfully committed and receives the old and new byte sizes. -pub struct SharedMemoryGrowthSubscription { - _observer: Arc, -} - -impl fmt::Debug for SharedMemoryGrowthSubscription { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("SharedMemoryGrowthSubscription") - .finish_non_exhaustive() - } -} - impl SharedMemory { /// Construct a [`SharedMemory`] by providing both the `minimum` and /// `maximum` number of 64K-sized pages. This call allocates the necessary @@ -1007,35 +991,6 @@ impl SharedMemory { } } - /// Registers an observer for successful growth of this shared memory. - /// - /// The observer runs synchronously after the new size is committed. It must - /// not block or attempt to grow this memory. Dropping the returned - /// subscription unregisters the observer. - pub fn subscribe_to_growth( - &self, - observer: impl Fn(usize, usize) + Send + Sync + 'static, - ) -> SharedMemoryGrowthSubscription { - self.subscribe_to_growth_with_current_size(observer).0 - } - - /// Registers a growth observer and atomically samples the current byte size. - /// - /// No growth can commit between registration and the returned size sample. - pub fn subscribe_to_growth_with_current_size( - &self, - observer: impl Fn(usize, usize) + Send + Sync + 'static, - ) -> (SharedMemoryGrowthSubscription, usize) { - let observer: Arc = Arc::new(observer); - let current_size = self.vm.subscribe_to_growth(&observer); - ( - SharedMemoryGrowthSubscription { - _observer: observer, - }, - current_size, - ) - } - /// Equivalent of the WebAssembly `memory.atomic.notify` instruction for /// this shared memory. /// @@ -1156,9 +1111,53 @@ impl fmt::Debug for SharedMemory { #[cfg(test)] mod tests { use crate::*; - use alloc::sync::Arc; use alloc::vec::Vec; - use core::sync::atomic::{AtomicUsize, Ordering}; + + #[derive(Default)] + struct SuccessfulGrowths(Vec<(usize, usize)>); + + impl ResourceLimiter for SuccessfulGrowths { + fn memory_growing( + &mut self, + _current: usize, + _desired: usize, + _maximum: Option, + ) -> Result { + Ok(true) + } + + fn memory_grown(&mut self, current: usize, desired: usize) -> Result<()> { + self.0.push((current, desired)); + Ok(()) + } + + fn table_growing( + &mut self, + _current: usize, + _desired: usize, + _maximum: Option, + ) -> Result { + Ok(true) + } + } + + #[test] + fn limiter_observes_only_successful_post_instantiation_growth() -> Result<()> { + let engine = Engine::default(); + let module = Module::new(&engine, r#"(module (memory (export "m") 1 2))"#)?; + let mut store = Store::new(&engine, SuccessfulGrowths::default()); + store.limiter(|state| state); + let instance = Instance::new(&mut store, &module, &[])?; + assert!(store.data().0.is_empty()); + + let memory = instance.get_memory(&mut store, "m").unwrap(); + assert_eq!(memory.grow(&mut store, 1)?, 1); + assert_eq!(store.data().0, [(65536, 2 * 65536)]); + + assert!(memory.grow(&mut store, 1).is_err()); + assert_eq!(store.data().0, [(65536, 2 * 65536)]); + Ok(()) + } #[test] fn linear_memories_include_unique_non_exported_backings() -> Result<()> { @@ -1230,51 +1229,6 @@ mod tests { Ok(()) } - #[cfg(feature = "threads")] - #[test] - fn shared_memory_growth_subscriptions_observe_successful_growth() -> Result<()> { - let mut config = Config::new(); - config.wasm_threads(true).shared_memory(true); - let engine = Engine::new(&config)?; - let memory = SharedMemory::new(&engine, MemoryType::shared(1, 2))?; - let module = Module::new( - &engine, - r#"(module - (import "env" "memory" (memory 1 2 shared)) - (func (export "grow") (result i32) (memory.grow (i32.const 1))) - )"#, - )?; - let mut store = Store::new(&engine, ()); - let instance = Instance::new(&mut store, &module, &[memory.clone().into()])?; - let grow = instance.get_typed_func::<(), i32>(&mut store, "grow")?; - let old_size = Arc::new(AtomicUsize::new(0)); - let new_size = Arc::new(AtomicUsize::new(0)); - let calls = Arc::new(AtomicUsize::new(0)); - let (subscription, initial_size) = memory.subscribe_to_growth_with_current_size({ - let old_size = old_size.clone(); - let new_size = new_size.clone(); - let calls = calls.clone(); - move |old, new| { - old_size.store(old, Ordering::SeqCst); - new_size.store(new, Ordering::SeqCst); - calls.fetch_add(1, Ordering::SeqCst); - } - }); - assert_eq!(initial_size, 65536); - - assert_eq!(grow.call(&mut store, ())?, 1); - assert!(memory.grow(1).is_err()); - - assert_eq!(calls.load(Ordering::SeqCst), 1); - assert_eq!(old_size.load(Ordering::SeqCst), 65536); - assert_eq!(new_size.load(Ordering::SeqCst), 2 * 65536); - - drop(subscription); - assert!(memory.grow(0).is_ok()); - assert_eq!(calls.load(Ordering::SeqCst), 1); - Ok(()) - } - // Assert that creating a memory via `Memory::new` respects the limits/tunables // in `Config`. #[test] diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index 95d8962d880a..3c43668decd5 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -346,6 +346,14 @@ impl StoreResourceLimiter<'_> { } } + pub(crate) fn memory_grown(&mut self, current: usize, desired: usize) -> Result<()> { + match self { + Self::Sync(s) => s.memory_grown(current, desired), + #[cfg(feature = "async")] + Self::Async(s) => s.memory_grown(current, desired), + } + } + pub(crate) async fn table_growing( &mut self, current: usize, diff --git a/crates/wasmtime/src/runtime/vm.rs b/crates/wasmtime/src/runtime/vm.rs index 41beb496765c..7b9bac6be456 100644 --- a/crates/wasmtime/src/runtime/vm.rs +++ b/crates/wasmtime/src/runtime/vm.rs @@ -110,7 +110,6 @@ pub use crate::runtime::vm::instance::{ PoolingInstanceAllocatorConfig, }; pub use crate::runtime::vm::interpreter::*; -pub(crate) use crate::runtime::vm::memory::SharedMemoryGrowthObserver; pub use crate::runtime::vm::memory::{ Memory, MemoryBase, RuntimeLinearMemory, RuntimeMemoryCreator, SharedMemory, }; diff --git a/crates/wasmtime/src/runtime/vm/memory.rs b/crates/wasmtime/src/runtime/vm/memory.rs index 4665278233f2..c4fc586fa6f1 100644 --- a/crates/wasmtime/src/runtime/vm/memory.rs +++ b/crates/wasmtime/src/runtime/vm/memory.rs @@ -85,8 +85,6 @@ use alloc::sync::Arc; use core::{ops::Range, ptr::NonNull}; use wasmtime_environ::{MemoryKind, MemoryTunables}; -pub(crate) type SharedMemoryGrowthObserver = dyn Fn(usize, usize) + Send + Sync + 'static; - #[cfg(feature = "threads")] use wasmtime_environ::Trap; @@ -712,6 +710,10 @@ impl LocalMemory { assert_eq!(base_ptr_before, self.alloc.base().as_mut_ptr()); } + if let Some(limiter) = limiter { + limiter.memory_grown(old_byte_size, new_byte_size)?; + } + Ok(Some((old_byte_size, new_byte_size))) } Err(e) => { diff --git a/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs b/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs index 7a5e0ca69ec4..679984c25063 100644 --- a/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs +++ b/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs @@ -1,15 +1,13 @@ use crate::Engine; use crate::prelude::*; -use crate::runtime::vm::memory::{ - LocalMemory, MmapMemory, SharedMemoryGrowthObserver, validate_atomic_addr, -}; +use crate::runtime::vm::memory::{LocalMemory, MmapMemory, validate_atomic_addr}; use crate::runtime::vm::parking_spot::{ParkingSpot, Waiter}; use crate::runtime::vm::{self, Memory, VMMemoryDefinition, WaitResult}; use std::cell::RefCell; use std::ops::Range; use std::ptr::NonNull; use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; -use std::sync::{Arc, RwLock, Weak}; +use std::sync::{Arc, RwLock}; use std::time::{Duration, Instant}; use wasmtime_environ::Trap; @@ -26,7 +24,6 @@ pub struct SharedMemory(Arc); struct SharedMemoryInner { memory: RwLock, - growth_observers: RwLock>>, spot: ParkingSpot, ty: wasmtime_environ::Memory, def: LongTermVMMemoryDefinition, @@ -72,7 +69,6 @@ impl SharedMemory { spot: ParkingSpot::default(), def: LongTermVMMemoryDefinition(memory.vmmemory()), memory: RwLock::new(memory), - growth_observers: RwLock::new(Vec::new()), })?)) } @@ -97,7 +93,7 @@ impl SharedMemory { // Without a limiter being passed in this shouldn't have an await point, // so it should be safe to assert that it's ready. let result = vm::assert_ready(memory.grow(delta_pages, None))?; - if let Some((old_size_in_bytes, new_size_in_bytes)) = result { + if let Some((_old_size_in_bytes, new_size_in_bytes)) = result { // Store the new size to the `VMMemoryDefinition` for JIT-generated // code (and runtime functions) to access. No other code can be // growing this memory due to the write lock, but code in other @@ -122,37 +118,10 @@ impl SharedMemory { .0 .current_length .store(new_size_in_bytes, Ordering::SeqCst); - - let observers = { - let mut registered = self.0.growth_observers.write().unwrap(); - let mut observers = Vec::with_capacity(registered.len()); - registered.retain(|observer| { - if let Some(observer) = observer.upgrade() { - observers.push(observer); - true - } else { - false - } - }); - observers - }; - for observer in observers { - observer(old_size_in_bytes, new_size_in_bytes); - } } Ok(result) } - pub fn subscribe_to_growth(&self, observer: &Arc) -> usize { - let memory = self.0.memory.read().unwrap(); - self.0 - .growth_observers - .write() - .unwrap() - .push(Arc::downgrade(observer)); - memory.byte_size() - } - /// Implementation of `memory.atomic.notify` for this shared memory. pub fn atomic_notify(&self, addr_index: u64, count: u32) -> Result { let ptr = validate_atomic_addr(&self.0.def.0, addr_index, 4, 4)?; diff --git a/crates/wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs b/crates/wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs index 9387ea7d95d0..e9d470acbf61 100644 --- a/crates/wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs +++ b/crates/wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs @@ -1,8 +1,7 @@ use crate::Engine; use crate::prelude::*; -use crate::runtime::vm::memory::{LocalMemory, SharedMemoryGrowthObserver}; +use crate::runtime::vm::memory::LocalMemory; use crate::runtime::vm::{VMMemoryDefinition, WaitResult}; -use alloc::sync::Arc; use core::ops::Range; use core::ptr::NonNull; use core::time::Duration; @@ -32,10 +31,6 @@ impl SharedMemory { match *self {} } - pub fn subscribe_to_growth(&self, _observer: &Arc) -> usize { - match *self {} - } - pub fn atomic_notify(&self, _addr_index: u64, _count: u32) -> Result { match *self {} } diff --git a/tests/all/limits.rs b/tests/all/limits.rs index 1e75a38d1257..81e380eec6cd 100644 --- a/tests/all/limits.rs +++ b/tests/all/limits.rs @@ -702,6 +702,8 @@ struct FailureDetector { /// Arguments of most recent call to memory_growing memory_current: usize, memory_desired: usize, + /// Arguments of most recent call to memory_grown + memory_grown: Option<(usize, usize)>, /// Display impl of most recent call to memory_grow_failed memory_error: Option, /// Arguments of most recent call to table_growing @@ -726,6 +728,10 @@ impl ResourceLimiter for FailureDetector { self.memory_error = Some(err.to_string()); Ok(()) } + fn memory_grown(&mut self, current: usize, desired: usize) -> Result<()> { + self.memory_grown = Some((current, desired)); + Ok(()) + } fn table_growing( &mut self, current: usize, @@ -772,6 +778,7 @@ fn custom_limiter_detect_grow_failure() -> Result<()> { assert!(store.data().memory_error.is_none()); assert_eq!(store.data().memory_current, 0); assert_eq!(store.data().memory_desired, 10 * 64 * 1024); + assert_eq!(store.data().memory_grown, Some((0, 10 * 64 * 1024))); // Grow past the static limit set by ModuleLimits. // The ResourceLimiter will permit this, but the grow will fail. @@ -786,6 +793,7 @@ fn custom_limiter_detect_grow_failure() -> Result<()> { store.data().memory_error.as_ref().unwrap(), "Memory maximum size exceeded" ); + assert_eq!(store.data().memory_grown, Some((0, 10 * 64 * 1024))); let table = instance.get_table(&mut store, "t").unwrap(); // Grow the table 10 elements @@ -835,6 +843,10 @@ impl ResourceLimiterAsync for FailureDetector { self.memory_error = Some(err.to_string()); Ok(()) } + fn memory_grown(&mut self, current: usize, desired: usize) -> Result<()> { + self.memory_grown = Some((current, desired)); + Ok(()) + } async fn table_growing( &mut self, From cd382650e06e5cbb959b02deda0a59a57c62f7db Mon Sep 17 00:00:00 2001 From: Kaur Matas <33095685+kmatasfp@users.noreply.github.com> Date: Tue, 4 Aug 2026 21:44:52 -0700 Subject: [PATCH 04/11] Restrict committed growth notifications --- crates/wasmtime/src/runtime/limits.rs | 8 ++----- crates/wasmtime/src/runtime/memory.rs | 14 +++++++---- crates/wasmtime/src/runtime/store.rs | 23 ++++++++++++------- crates/wasmtime/src/runtime/vm/memory.rs | 19 +++++++++++---- .../src/runtime/vm/memory/shared_memory.rs | 12 +++++++++- .../vm/memory/shared_memory_disabled.rs | 4 ++++ tests/all/limits.rs | 6 ++--- 7 files changed, 58 insertions(+), 28 deletions(-) diff --git a/crates/wasmtime/src/runtime/limits.rs b/crates/wasmtime/src/runtime/limits.rs index 39dd59ac8b92..f92eb4b4500d 100644 --- a/crates/wasmtime/src/runtime/limits.rs +++ b/crates/wasmtime/src/runtime/limits.rs @@ -96,9 +96,7 @@ pub trait ResourceLimiter: Send { /// This is not called for a memory's initial allocation or for shared /// memories. `current` and `desired` are the memory's old and new sizes in /// bytes. - fn memory_grown(&mut self, _current: usize, _desired: usize) -> Result<()> { - Ok(()) - } + fn memory_grown(&mut self, _current: usize, _desired: usize) {} /// Notifies the resource limiter that an instance's table has been /// requested to grow. @@ -196,9 +194,7 @@ pub trait ResourceLimiterAsync: Send { } /// Identical to [`ResourceLimiter::memory_grown`]. - fn memory_grown(&mut self, _current: usize, _desired: usize) -> Result<()> { - Ok(()) - } + fn memory_grown(&mut self, _current: usize, _desired: usize) {} /// Asynchronous version of [`ResourceLimiter::table_growing`] async fn table_growing( diff --git a/crates/wasmtime/src/runtime/memory.rs b/crates/wasmtime/src/runtime/memory.rs index 3f6dda7af56b..8f0155d8d342 100644 --- a/crates/wasmtime/src/runtime/memory.rs +++ b/crates/wasmtime/src/runtime/memory.rs @@ -875,6 +875,10 @@ pub struct SharedMemory { } impl SharedMemory { + pub(crate) fn same_backing(&self, other: &Self) -> bool { + self.vm.same_backing(&other.vm) + } + /// Construct a [`SharedMemory`] by providing both the `minimum` and /// `maximum` number of 64K-sized pages. This call allocates the necessary /// pages on the system. @@ -1126,9 +1130,8 @@ mod tests { Ok(true) } - fn memory_grown(&mut self, current: usize, desired: usize) -> Result<()> { + fn memory_grown(&mut self, current: usize, desired: usize) { self.0.push((current, desired)); - Ok(()) } fn table_growing( @@ -1206,16 +1209,17 @@ mod tests { )?; let mut store = Store::new(&engine, ()); let imported = SharedMemory::new(&engine, MemoryType::shared(5, 10))?; + Instance::new(&mut store, &module, &[imported.clone().into()])?; Instance::new(&mut store, &module, &[imported.into()])?; let memories = store.linear_memories(); - assert_eq!(memories.len(), 3); + assert_eq!(memories.len(), 5); assert_eq!( memories .iter() .filter(|memory| matches!(memory, StoreMemory::Unshared(_))) .count(), - 1 + 2 ); let mut shared_sizes = memories .iter() @@ -1225,7 +1229,7 @@ mod tests { }) .collect::>(); shared_sizes.sort_unstable(); - assert_eq!(shared_sizes, [65536, 5 * 65536]); + assert_eq!(shared_sizes, [65536, 65536, 5 * 65536]); Ok(()) } diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index 3c43668decd5..a7b2a3a41c7f 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -346,7 +346,7 @@ impl StoreResourceLimiter<'_> { } } - pub(crate) fn memory_grown(&mut self, current: usize, desired: usize) -> Result<()> { + pub(crate) fn memory_grown(&mut self, current: usize, desired: usize) { match self { Self::Sync(s) => s.memory_grown(current, desired), #[cfg(feature = "async")] @@ -1013,15 +1013,22 @@ impl Store { /// This includes non-exported memories and host-created memories. Imported /// aliases and multiple exports of one backing are returned only once. pub fn linear_memories(&self) -> Vec { - self.inner - .all_memories() - .map(|memory| match memory { - ExportMemory::Unshared(memory) => StoreMemory::Unshared(memory), + let mut memories = Vec::new(); + for memory in self.inner.all_memories() { + match memory { + ExportMemory::Unshared(memory) => memories.push(StoreMemory::Unshared(memory)), ExportMemory::Shared(memory, _) => { - StoreMemory::Shared(SharedMemory::from_raw(memory, self.engine().clone())) + let memory = SharedMemory::from_raw(memory, self.engine().clone()); + if !memories.iter().any(|existing| match existing { + StoreMemory::Shared(existing) => existing.same_backing(&memory), + StoreMemory::Unshared(_) => false, + }) { + memories.push(StoreMemory::Shared(memory)); + } } - }) - .collect() + } + } + memories } /// Returns the amount fuel in this [`Store`]. When fuel is enabled, it must diff --git a/crates/wasmtime/src/runtime/vm/memory.rs b/crates/wasmtime/src/runtime/vm/memory.rs index c4fc586fa6f1..e82565d25253 100644 --- a/crates/wasmtime/src/runtime/vm/memory.rs +++ b/crates/wasmtime/src/runtime/vm/memory.rs @@ -251,7 +251,7 @@ impl Memory { let memory_tunables = MemoryTunables::new(tunables, kind); let allocation = creator.new_memory(ty, &memory_tunables, minimum, maximum)?; - let memory = LocalMemory::new(ty, &memory_tunables, allocation, memory_image)?; + let memory = LocalMemory::new(ty, &memory_tunables, allocation, memory_image, kind)?; Ok(if ty.shared { Memory::Shared(SharedMemory::wrap(engine, ty, memory)?) } else { @@ -277,7 +277,13 @@ impl Memory { // `LocalMemory` structure created, notably we already have // `memory_image` and regardless of configuration settings this memory // can't move its base pointer since it's a fixed allocation. - let mut memory = LocalMemory::new(ty, memory_tunables, allocation, None)?; + let mut memory = LocalMemory::new( + ty, + memory_tunables, + allocation, + None, + MemoryKind::LinearMemory, + )?; assert!(memory.memory_image.is_none()); memory.memory_image = Some(memory_image); memory.memory_may_move = false; @@ -536,6 +542,7 @@ impl Memory { pub struct LocalMemory { alloc: Box, ty: wasmtime_environ::Memory, + kind: MemoryKind, memory_may_move: bool, memory_guard_size: usize, memory_reservation: usize, @@ -551,6 +558,7 @@ impl LocalMemory { memory_tunables: &MemoryTunables<'_>, alloc: Box, memory_image: Option<&Arc>, + kind: MemoryKind, ) -> Result { // If a memory image was specified, try to create the MemoryImageSlot on // top of our mmap. @@ -584,6 +592,7 @@ impl LocalMemory { }; Ok(LocalMemory { ty: *ty, + kind, alloc, memory_may_move: ty.memory_may_move(memory_tunables), memory_image, @@ -710,8 +719,10 @@ impl LocalMemory { assert_eq!(base_ptr_before, self.alloc.base().as_mut_ptr()); } - if let Some(limiter) = limiter { - limiter.memory_grown(old_byte_size, new_byte_size)?; + if matches!(self.kind, MemoryKind::LinearMemory) + && let Some(limiter) = limiter + { + limiter.memory_grown(old_byte_size, new_byte_size); } Ok(Some((old_byte_size, new_byte_size))) diff --git a/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs b/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs index 679984c25063..1bff2586167c 100644 --- a/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs +++ b/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs @@ -30,6 +30,10 @@ struct SharedMemoryInner { } impl SharedMemory { + pub(crate) fn same_backing(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } + /// Construct a new [`SharedMemory`]. pub fn new(engine: &Engine, ty: &wasmtime_environ::Memory) -> Result { let tunables = engine.tunables(); @@ -46,7 +50,13 @@ impl SharedMemory { Self::wrap( engine, ty, - LocalMemory::new(ty, &memory_tunables, boxed, None)?, + LocalMemory::new( + ty, + &memory_tunables, + boxed, + None, + wasmtime_environ::MemoryKind::LinearMemory, + )?, ) } diff --git a/crates/wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs b/crates/wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs index e9d470acbf61..e90f045733bc 100644 --- a/crates/wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs +++ b/crates/wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs @@ -11,6 +11,10 @@ use wasmtime_environ::Trap; pub enum SharedMemory {} impl SharedMemory { + pub(crate) fn same_backing(&self, other: &Self) -> bool { + match (*self, *other) {} + } + pub fn wrap(_: &Engine, _ty: &wasmtime_environ::Memory, _memory: LocalMemory) -> Result { bail!("support for shared memories was disabled at compile time"); } diff --git a/tests/all/limits.rs b/tests/all/limits.rs index 81e380eec6cd..01da0d1d4e20 100644 --- a/tests/all/limits.rs +++ b/tests/all/limits.rs @@ -728,9 +728,8 @@ impl ResourceLimiter for FailureDetector { self.memory_error = Some(err.to_string()); Ok(()) } - fn memory_grown(&mut self, current: usize, desired: usize) -> Result<()> { + fn memory_grown(&mut self, current: usize, desired: usize) { self.memory_grown = Some((current, desired)); - Ok(()) } fn table_growing( &mut self, @@ -843,9 +842,8 @@ impl ResourceLimiterAsync for FailureDetector { self.memory_error = Some(err.to_string()); Ok(()) } - fn memory_grown(&mut self, current: usize, desired: usize) -> Result<()> { + fn memory_grown(&mut self, current: usize, desired: usize) { self.memory_grown = Some((current, desired)); - Ok(()) } async fn table_growing( From 9ab8d5cf2a34aa91c162d409e89e83437020ad7e Mon Sep 17 00:00:00 2001 From: Kaur Matas <33095685+kmatasfp@users.noreply.github.com> Date: Tue, 4 Aug 2026 22:08:11 -0700 Subject: [PATCH 05/11] Preserve pooled memory kinds --- .../src/runtime/vm/instance/allocator/pooling.rs | 13 ++++++++++--- .../vm/instance/allocator/pooling/memory_pool.rs | 4 +++- crates/wasmtime/src/runtime/vm/memory.rs | 9 ++------- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/crates/wasmtime/src/runtime/vm/instance/allocator/pooling.rs b/crates/wasmtime/src/runtime/vm/instance/allocator/pooling.rs index 630cbcc4df82..f89dc3322024 100644 --- a/crates/wasmtime/src/runtime/vm/instance/allocator/pooling.rs +++ b/crates/wasmtime/src/runtime/vm/instance/allocator/pooling.rs @@ -688,7 +688,7 @@ unsafe impl InstanceAllocator for PoolingInstanceAllocator { request: &'a mut InstanceAllocationRequest<'b, 'c>, ty: &'a wasmtime_environ::Memory, memory_index: Option, - _memory_kind: MemoryKind, + memory_kind: MemoryKind, ) -> Pin> + Send + 'a>> { crate::runtime::box_future(async move { async { @@ -696,7 +696,11 @@ unsafe impl InstanceAllocator for PoolingInstanceAllocator { // `with_flush_and_retry` but adapted for async closures instead of only // sync closures. Right now that won't compile though so this is the // manually expanded version of the method. - let e = match self.memories.allocate(request, ty, memory_index).await { + let e = match self + .memories + .allocate(request, ty, memory_index, memory_kind) + .await + { Ok(result) => return Ok(result), Err(e) => e, }; @@ -704,7 +708,10 @@ unsafe impl InstanceAllocator for PoolingInstanceAllocator { if e.is::() { let queue = self.decommit_queue.lock().unwrap(); if self.flush_decommit_queue(queue) { - return self.memories.allocate(request, ty, memory_index).await; + return self + .memories + .allocate(request, ty, memory_index, memory_kind) + .await; } } diff --git a/crates/wasmtime/src/runtime/vm/instance/allocator/pooling/memory_pool.rs b/crates/wasmtime/src/runtime/vm/instance/allocator/pooling/memory_pool.rs index aaa26ef5f52c..4554ae455fbc 100644 --- a/crates/wasmtime/src/runtime/vm/instance/allocator/pooling/memory_pool.rs +++ b/crates/wasmtime/src/runtime/vm/instance/allocator/pooling/memory_pool.rs @@ -352,9 +352,10 @@ impl MemoryPool { request: &mut InstanceAllocationRequest<'_, '_>, ty: &wasmtime_environ::Memory, memory_index: Option, + memory_kind: MemoryKind, ) -> Result<(MemoryAllocationIndex, Memory)> { let tunables = request.store.engine().tunables(); - let memory_tunables = MemoryTunables::new(tunables, MemoryKind::LinearMemory); + let memory_tunables = MemoryTunables::new(tunables, memory_kind); let stripe_index = if let Some(pkey) = request.store.get_pkey() { pkey.as_stripe() } else { @@ -427,6 +428,7 @@ impl MemoryPool { let memory = Memory::new_static( ty, &memory_tunables, + memory_kind, MemoryBase::Mmap(base), base_capacity.byte_count(), slot, diff --git a/crates/wasmtime/src/runtime/vm/memory.rs b/crates/wasmtime/src/runtime/vm/memory.rs index e82565d25253..5077182c4298 100644 --- a/crates/wasmtime/src/runtime/vm/memory.rs +++ b/crates/wasmtime/src/runtime/vm/memory.rs @@ -264,6 +264,7 @@ impl Memory { pub async fn new_static( ty: &wasmtime_environ::Memory, memory_tunables: &MemoryTunables<'_>, + kind: MemoryKind, base: MemoryBase, base_capacity: usize, memory_image: MemoryImageSlot, @@ -277,13 +278,7 @@ impl Memory { // `LocalMemory` structure created, notably we already have // `memory_image` and regardless of configuration settings this memory // can't move its base pointer since it's a fixed allocation. - let mut memory = LocalMemory::new( - ty, - memory_tunables, - allocation, - None, - MemoryKind::LinearMemory, - )?; + let mut memory = LocalMemory::new(ty, memory_tunables, allocation, None, kind)?; assert!(memory.memory_image.is_none()); memory.memory_image = Some(memory_image); memory.memory_may_move = false; From ff9eb0643d2e423eb13723a2b96bbb82bc1b62f0 Mon Sep 17 00:00:00 2001 From: Kaur Matas <33095685+kmatasfp@users.noreply.github.com> Date: Tue, 4 Aug 2026 22:40:22 -0700 Subject: [PATCH 06/11] Fix disabled shared memory build --- .../wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs b/crates/wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs index e90f045733bc..079e1616cd22 100644 --- a/crates/wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs +++ b/crates/wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs @@ -11,8 +11,8 @@ use wasmtime_environ::Trap; pub enum SharedMemory {} impl SharedMemory { - pub(crate) fn same_backing(&self, other: &Self) -> bool { - match (*self, *other) {} + pub(crate) fn same_backing(&self, _other: &Self) -> bool { + unreachable!() } pub fn wrap(_: &Engine, _ty: &wasmtime_environ::Memory, _memory: LocalMemory) -> Result { From 2874832f41e3fc48d1334e221073002d6a621630 Mon Sep 17 00:00:00 2001 From: Kaur Matas <33095685+kmatasfp@users.noreply.github.com> Date: Wed, 5 Aug 2026 09:15:35 -0700 Subject: [PATCH 07/11] Track committed shared memory growth --- crates/wasmtime/src/runtime/memory.rs | 81 +++++++++++++++++++ crates/wasmtime/src/runtime/vm.rs | 1 + crates/wasmtime/src/runtime/vm/memory.rs | 2 + .../src/runtime/vm/memory/shared_memory.rs | 29 ++++++- .../vm/memory/shared_memory_disabled.rs | 7 +- 5 files changed, 116 insertions(+), 4 deletions(-) diff --git a/crates/wasmtime/src/runtime/memory.rs b/crates/wasmtime/src/runtime/memory.rs index 8f0155d8d342..3001f8b042b6 100644 --- a/crates/wasmtime/src/runtime/memory.rs +++ b/crates/wasmtime/src/runtime/memory.rs @@ -6,6 +6,7 @@ use crate::trampoline::generate_memory_export; #[cfg(feature = "async")] use crate::vm::VMStore; use crate::{AsContext, AsContextMut, Engine, MemoryType, StoreContext, StoreContextMut}; +use alloc::sync::Arc; use core::cell::UnsafeCell; use core::fmt; use core::slice; @@ -874,6 +875,21 @@ pub struct SharedMemory { engine: Engine, } +/// Keeps a shared-memory growth observer registered. +/// +/// Dropping this value unregisters the observer. The observer is called after a +/// growth has successfully committed and receives the old and new byte sizes. +pub struct SharedMemoryGrowthSubscription { + _observer: Arc, +} + +impl fmt::Debug for SharedMemoryGrowthSubscription { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SharedMemoryGrowthSubscription") + .finish_non_exhaustive() + } +} + impl SharedMemory { pub(crate) fn same_backing(&self, other: &Self) -> bool { self.vm.same_backing(&other.vm) @@ -995,6 +1011,35 @@ impl SharedMemory { } } + /// Registers an observer for successful growth of this shared memory. + /// + /// The observer runs synchronously after the new size is committed. It must + /// not block or attempt to grow this memory. Dropping the returned + /// subscription unregisters the observer. + pub fn subscribe_to_growth( + &self, + observer: impl Fn(usize, usize) + Send + Sync + 'static, + ) -> SharedMemoryGrowthSubscription { + self.subscribe_to_growth_with_current_size(observer).0 + } + + /// Registers a growth observer and atomically samples the current byte size. + /// + /// No growth can commit between registration and the returned size sample. + pub fn subscribe_to_growth_with_current_size( + &self, + observer: impl Fn(usize, usize) + Send + Sync + 'static, + ) -> (SharedMemoryGrowthSubscription, usize) { + let observer: Arc = Arc::new(observer); + let current_size = self.vm.subscribe_to_growth(&observer); + ( + SharedMemoryGrowthSubscription { + _observer: observer, + }, + current_size, + ) + } + /// Equivalent of the WebAssembly `memory.atomic.notify` instruction for /// this shared memory. /// @@ -1115,7 +1160,9 @@ impl fmt::Debug for SharedMemory { #[cfg(test)] mod tests { use crate::*; + use alloc::sync::Arc; use alloc::vec::Vec; + use core::sync::atomic::{AtomicUsize, Ordering}; #[derive(Default)] struct SuccessfulGrowths(Vec<(usize, usize)>); @@ -1233,6 +1280,40 @@ mod tests { Ok(()) } + #[cfg(feature = "threads")] + #[test] + fn shared_memory_growth_subscriptions_observe_successful_growth() -> Result<()> { + let mut config = Config::new(); + config.wasm_threads(true).shared_memory(true); + let engine = Engine::new(&config)?; + let memory = SharedMemory::new(&engine, MemoryType::shared(1, 3))?; + let old_size = Arc::new(AtomicUsize::new(0)); + let new_size = Arc::new(AtomicUsize::new(0)); + let calls = Arc::new(AtomicUsize::new(0)); + let (subscription, initial_size) = memory.subscribe_to_growth_with_current_size({ + let old_size = old_size.clone(); + let new_size = new_size.clone(); + let calls = calls.clone(); + move |old, new| { + old_size.store(old, Ordering::SeqCst); + new_size.store(new, Ordering::SeqCst); + calls.fetch_add(1, Ordering::SeqCst); + } + }); + assert_eq!(initial_size, 65536); + + assert_eq!(memory.grow(1)?, 1); + assert!(memory.grow(3).is_err()); + assert_eq!(calls.load(Ordering::SeqCst), 1); + assert_eq!(old_size.load(Ordering::SeqCst), 65536); + assert_eq!(new_size.load(Ordering::SeqCst), 2 * 65536); + + drop(subscription); + assert_eq!(memory.grow(1)?, 2); + assert_eq!(calls.load(Ordering::SeqCst), 1); + Ok(()) + } + // Assert that creating a memory via `Memory::new` respects the limits/tunables // in `Config`. #[test] diff --git a/crates/wasmtime/src/runtime/vm.rs b/crates/wasmtime/src/runtime/vm.rs index 7b9bac6be456..41beb496765c 100644 --- a/crates/wasmtime/src/runtime/vm.rs +++ b/crates/wasmtime/src/runtime/vm.rs @@ -110,6 +110,7 @@ pub use crate::runtime::vm::instance::{ PoolingInstanceAllocatorConfig, }; pub use crate::runtime::vm::interpreter::*; +pub(crate) use crate::runtime::vm::memory::SharedMemoryGrowthObserver; pub use crate::runtime::vm::memory::{ Memory, MemoryBase, RuntimeLinearMemory, RuntimeMemoryCreator, SharedMemory, }; diff --git a/crates/wasmtime/src/runtime/vm/memory.rs b/crates/wasmtime/src/runtime/vm/memory.rs index 5077182c4298..3383555b6424 100644 --- a/crates/wasmtime/src/runtime/vm/memory.rs +++ b/crates/wasmtime/src/runtime/vm/memory.rs @@ -85,6 +85,8 @@ use alloc::sync::Arc; use core::{ops::Range, ptr::NonNull}; use wasmtime_environ::{MemoryKind, MemoryTunables}; +pub(crate) type SharedMemoryGrowthObserver = dyn Fn(usize, usize) + Send + Sync + 'static; + #[cfg(feature = "threads")] use wasmtime_environ::Trap; diff --git a/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs b/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs index 1bff2586167c..ca146a7b9bd0 100644 --- a/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs +++ b/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs @@ -1,13 +1,15 @@ use crate::Engine; use crate::prelude::*; -use crate::runtime::vm::memory::{LocalMemory, MmapMemory, validate_atomic_addr}; +use crate::runtime::vm::memory::{ + LocalMemory, MmapMemory, SharedMemoryGrowthObserver, validate_atomic_addr, +}; use crate::runtime::vm::parking_spot::{ParkingSpot, Waiter}; use crate::runtime::vm::{self, Memory, VMMemoryDefinition, WaitResult}; use std::cell::RefCell; use std::ops::Range; use std::ptr::NonNull; use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; -use std::sync::{Arc, RwLock}; +use std::sync::{Arc, RwLock, Weak}; use std::time::{Duration, Instant}; use wasmtime_environ::Trap; @@ -24,6 +26,7 @@ pub struct SharedMemory(Arc); struct SharedMemoryInner { memory: RwLock, + growth_observers: RwLock>>, spot: ParkingSpot, ty: wasmtime_environ::Memory, def: LongTermVMMemoryDefinition, @@ -79,6 +82,7 @@ impl SharedMemory { spot: ParkingSpot::default(), def: LongTermVMMemoryDefinition(memory.vmmemory()), memory: RwLock::new(memory), + growth_observers: RwLock::new(Vec::new()), })?)) } @@ -103,7 +107,7 @@ impl SharedMemory { // Without a limiter being passed in this shouldn't have an await point, // so it should be safe to assert that it's ready. let result = vm::assert_ready(memory.grow(delta_pages, None))?; - if let Some((_old_size_in_bytes, new_size_in_bytes)) = result { + if let Some((old_size_in_bytes, new_size_in_bytes)) = result { // Store the new size to the `VMMemoryDefinition` for JIT-generated // code (and runtime functions) to access. No other code can be // growing this memory due to the write lock, but code in other @@ -128,10 +132,29 @@ impl SharedMemory { .0 .current_length .store(new_size_in_bytes, Ordering::SeqCst); + + self.0.growth_observers.write().unwrap().retain(|observer| { + if let Some(observer) = observer.upgrade() { + observer(old_size_in_bytes, new_size_in_bytes); + true + } else { + false + } + }); } Ok(result) } + pub fn subscribe_to_growth(&self, observer: &Arc) -> usize { + let memory = self.0.memory.read().unwrap(); + self.0 + .growth_observers + .write() + .unwrap() + .push(Arc::downgrade(observer)); + memory.byte_size() + } + /// Implementation of `memory.atomic.notify` for this shared memory. pub fn atomic_notify(&self, addr_index: u64, count: u32) -> Result { let ptr = validate_atomic_addr(&self.0.def.0, addr_index, 4, 4)?; diff --git a/crates/wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs b/crates/wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs index 079e1616cd22..61a2635a26f3 100644 --- a/crates/wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs +++ b/crates/wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs @@ -1,7 +1,8 @@ use crate::Engine; use crate::prelude::*; -use crate::runtime::vm::memory::LocalMemory; +use crate::runtime::vm::memory::{LocalMemory, SharedMemoryGrowthObserver}; use crate::runtime::vm::{VMMemoryDefinition, WaitResult}; +use alloc::sync::Arc; use core::ops::Range; use core::ptr::NonNull; use core::time::Duration; @@ -35,6 +36,10 @@ impl SharedMemory { match *self {} } + pub fn subscribe_to_growth(&self, _observer: &Arc) -> usize { + match *self {} + } + pub fn atomic_notify(&self, _addr_index: u64, _count: u32) -> Result { match *self {} } From 409775c26404187e7a7411e36fbcbc3a9b16de29 Mon Sep 17 00:00:00 2001 From: Kaur Matas <33095685+kmatasfp@users.noreply.github.com> Date: Wed, 5 Aug 2026 09:58:39 -0700 Subject: [PATCH 08/11] Prune stale shared memory observers --- crates/wasmtime/src/runtime/vm/memory/shared_memory.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs b/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs index ca146a7b9bd0..5b0df42602df 100644 --- a/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs +++ b/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs @@ -147,11 +147,9 @@ impl SharedMemory { pub fn subscribe_to_growth(&self, observer: &Arc) -> usize { let memory = self.0.memory.read().unwrap(); - self.0 - .growth_observers - .write() - .unwrap() - .push(Arc::downgrade(observer)); + let mut observers = self.0.growth_observers.write().unwrap(); + observers.retain(|observer| observer.strong_count() > 0); + observers.push(Arc::downgrade(observer)); memory.byte_size() } From 1bbe6a6db7bc556554eeb8323a27794e2b5ac0f5 Mon Sep 17 00:00:00 2001 From: Kaur Matas <33095685+kmatasfp@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:17:04 -0700 Subject: [PATCH 09/11] Notify shared growth observers outside locks --- crates/wasmtime/src/runtime/memory.rs | 2 + .../src/runtime/vm/memory/shared_memory.rs | 72 +++++++++---------- 2 files changed, 36 insertions(+), 38 deletions(-) diff --git a/crates/wasmtime/src/runtime/memory.rs b/crates/wasmtime/src/runtime/memory.rs index 3001f8b042b6..7326dc13c66e 100644 --- a/crates/wasmtime/src/runtime/memory.rs +++ b/crates/wasmtime/src/runtime/memory.rs @@ -1291,10 +1291,12 @@ mod tests { let new_size = Arc::new(AtomicUsize::new(0)); let calls = Arc::new(AtomicUsize::new(0)); let (subscription, initial_size) = memory.subscribe_to_growth_with_current_size({ + let memory = memory.clone(); let old_size = old_size.clone(); let new_size = new_size.clone(); let calls = calls.clone(); move |old, new| { + assert_eq!(memory.data_size(), new); old_size.store(old, Ordering::SeqCst); new_size.store(new, Ordering::SeqCst); calls.fetch_add(1, Ordering::SeqCst); diff --git a/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs b/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs index 5b0df42602df..b3f79127beca 100644 --- a/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs +++ b/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs @@ -9,7 +9,7 @@ use std::cell::RefCell; use std::ops::Range; use std::ptr::NonNull; use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; -use std::sync::{Arc, RwLock, Weak}; +use std::sync::{Arc, Mutex, RwLock, Weak}; use std::time::{Duration, Instant}; use wasmtime_environ::Trap; @@ -27,6 +27,7 @@ pub struct SharedMemory(Arc); struct SharedMemoryInner { memory: RwLock, growth_observers: RwLock>>, + growth_notification: Mutex<()>, spot: ParkingSpot, ty: wasmtime_environ::Memory, def: LongTermVMMemoryDefinition, @@ -83,6 +84,7 @@ impl SharedMemory { def: LongTermVMMemoryDefinition(memory.vmmemory()), memory: RwLock::new(memory), growth_observers: RwLock::new(Vec::new()), + growth_notification: Mutex::new(()), })?)) } @@ -103,44 +105,38 @@ impl SharedMemory { /// Same as `RuntimeLinearMemory::grow`, except with `&self`. pub fn grow(&self, delta_pages: u64) -> Result, Error> { - let mut memory = self.0.memory.write().unwrap(); - // Without a limiter being passed in this shouldn't have an await point, - // so it should be safe to assert that it's ready. - let result = vm::assert_ready(memory.grow(delta_pages, None))?; + let _notification = self.0.growth_notification.lock().unwrap(); + let result = { + let mut memory = self.0.memory.write().unwrap(); + // Without a limiter being passed in this shouldn't have an await point, + // so it should be safe to assert that it's ready. + let result = vm::assert_ready(memory.grow(delta_pages, None))?; + if let Some((_old_size_in_bytes, new_size_in_bytes)) = result { + self.0 + .def + .0 + .current_length + .store(new_size_in_bytes, Ordering::SeqCst); + } + result + }; if let Some((old_size_in_bytes, new_size_in_bytes)) = result { - // Store the new size to the `VMMemoryDefinition` for JIT-generated - // code (and runtime functions) to access. No other code can be - // growing this memory due to the write lock, but code in other - // threads could have access to this shared memory and we want them - // to see the most consistent version of the `current_length`; a - // weaker consistency is possible if we accept them seeing an older, - // smaller memory size (assumption: memory only grows) but presently - // we are aiming for accuracy. - // - // Note that it could be possible to access a memory address that is - // now-valid due to changes to the page flags in `grow` above but - // beyond the `memory.size` that we are about to assign to. In these - // and similar cases, discussion in the thread proposal concluded - // that: "multiple accesses in one thread racing with another - // thread's `memory.grow` that are in-bounds only after the grow - // commits may independently succeed or trap" (see - // https://github.com/WebAssembly/threads/issues/26#issuecomment-433930711). - // In other words, some non-determinism is acceptable when using - // `memory.size` on work being done by `memory.grow`. - self.0 - .def - .0 - .current_length - .store(new_size_in_bytes, Ordering::SeqCst); - - self.0.growth_observers.write().unwrap().retain(|observer| { - if let Some(observer) = observer.upgrade() { - observer(old_size_in_bytes, new_size_in_bytes); - true - } else { - false - } - }); + let observers = { + let mut registered = self.0.growth_observers.write().unwrap(); + let mut observers = Vec::with_capacity(registered.len()); + registered.retain(|observer| { + if let Some(observer) = observer.upgrade() { + observers.push(observer); + true + } else { + false + } + }); + observers + }; + for observer in observers { + observer(old_size_in_bytes, new_size_in_bytes); + } } Ok(result) } From 81fae079f9c5cc7365c98e7feee9360554cbcb42 Mon Sep 17 00:00:00 2001 From: Kaur Matas <33095685+kmatasfp@users.noreply.github.com> Date: Wed, 5 Aug 2026 16:32:13 -0700 Subject: [PATCH 10/11] Remove unsupported shared memory tracking --- crates/wasmtime/src/runtime/memory.rs | 83 ------------------- crates/wasmtime/src/runtime/vm.rs | 1 - crates/wasmtime/src/runtime/vm/memory.rs | 2 - .../src/runtime/vm/memory/shared_memory.rs | 60 +++----------- .../vm/memory/shared_memory_disabled.rs | 7 +- 5 files changed, 13 insertions(+), 140 deletions(-) diff --git a/crates/wasmtime/src/runtime/memory.rs b/crates/wasmtime/src/runtime/memory.rs index 7326dc13c66e..8f0155d8d342 100644 --- a/crates/wasmtime/src/runtime/memory.rs +++ b/crates/wasmtime/src/runtime/memory.rs @@ -6,7 +6,6 @@ use crate::trampoline::generate_memory_export; #[cfg(feature = "async")] use crate::vm::VMStore; use crate::{AsContext, AsContextMut, Engine, MemoryType, StoreContext, StoreContextMut}; -use alloc::sync::Arc; use core::cell::UnsafeCell; use core::fmt; use core::slice; @@ -875,21 +874,6 @@ pub struct SharedMemory { engine: Engine, } -/// Keeps a shared-memory growth observer registered. -/// -/// Dropping this value unregisters the observer. The observer is called after a -/// growth has successfully committed and receives the old and new byte sizes. -pub struct SharedMemoryGrowthSubscription { - _observer: Arc, -} - -impl fmt::Debug for SharedMemoryGrowthSubscription { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("SharedMemoryGrowthSubscription") - .finish_non_exhaustive() - } -} - impl SharedMemory { pub(crate) fn same_backing(&self, other: &Self) -> bool { self.vm.same_backing(&other.vm) @@ -1011,35 +995,6 @@ impl SharedMemory { } } - /// Registers an observer for successful growth of this shared memory. - /// - /// The observer runs synchronously after the new size is committed. It must - /// not block or attempt to grow this memory. Dropping the returned - /// subscription unregisters the observer. - pub fn subscribe_to_growth( - &self, - observer: impl Fn(usize, usize) + Send + Sync + 'static, - ) -> SharedMemoryGrowthSubscription { - self.subscribe_to_growth_with_current_size(observer).0 - } - - /// Registers a growth observer and atomically samples the current byte size. - /// - /// No growth can commit between registration and the returned size sample. - pub fn subscribe_to_growth_with_current_size( - &self, - observer: impl Fn(usize, usize) + Send + Sync + 'static, - ) -> (SharedMemoryGrowthSubscription, usize) { - let observer: Arc = Arc::new(observer); - let current_size = self.vm.subscribe_to_growth(&observer); - ( - SharedMemoryGrowthSubscription { - _observer: observer, - }, - current_size, - ) - } - /// Equivalent of the WebAssembly `memory.atomic.notify` instruction for /// this shared memory. /// @@ -1160,9 +1115,7 @@ impl fmt::Debug for SharedMemory { #[cfg(test)] mod tests { use crate::*; - use alloc::sync::Arc; use alloc::vec::Vec; - use core::sync::atomic::{AtomicUsize, Ordering}; #[derive(Default)] struct SuccessfulGrowths(Vec<(usize, usize)>); @@ -1280,42 +1233,6 @@ mod tests { Ok(()) } - #[cfg(feature = "threads")] - #[test] - fn shared_memory_growth_subscriptions_observe_successful_growth() -> Result<()> { - let mut config = Config::new(); - config.wasm_threads(true).shared_memory(true); - let engine = Engine::new(&config)?; - let memory = SharedMemory::new(&engine, MemoryType::shared(1, 3))?; - let old_size = Arc::new(AtomicUsize::new(0)); - let new_size = Arc::new(AtomicUsize::new(0)); - let calls = Arc::new(AtomicUsize::new(0)); - let (subscription, initial_size) = memory.subscribe_to_growth_with_current_size({ - let memory = memory.clone(); - let old_size = old_size.clone(); - let new_size = new_size.clone(); - let calls = calls.clone(); - move |old, new| { - assert_eq!(memory.data_size(), new); - old_size.store(old, Ordering::SeqCst); - new_size.store(new, Ordering::SeqCst); - calls.fetch_add(1, Ordering::SeqCst); - } - }); - assert_eq!(initial_size, 65536); - - assert_eq!(memory.grow(1)?, 1); - assert!(memory.grow(3).is_err()); - assert_eq!(calls.load(Ordering::SeqCst), 1); - assert_eq!(old_size.load(Ordering::SeqCst), 65536); - assert_eq!(new_size.load(Ordering::SeqCst), 2 * 65536); - - drop(subscription); - assert_eq!(memory.grow(1)?, 2); - assert_eq!(calls.load(Ordering::SeqCst), 1); - Ok(()) - } - // Assert that creating a memory via `Memory::new` respects the limits/tunables // in `Config`. #[test] diff --git a/crates/wasmtime/src/runtime/vm.rs b/crates/wasmtime/src/runtime/vm.rs index 41beb496765c..7b9bac6be456 100644 --- a/crates/wasmtime/src/runtime/vm.rs +++ b/crates/wasmtime/src/runtime/vm.rs @@ -110,7 +110,6 @@ pub use crate::runtime::vm::instance::{ PoolingInstanceAllocatorConfig, }; pub use crate::runtime::vm::interpreter::*; -pub(crate) use crate::runtime::vm::memory::SharedMemoryGrowthObserver; pub use crate::runtime::vm::memory::{ Memory, MemoryBase, RuntimeLinearMemory, RuntimeMemoryCreator, SharedMemory, }; diff --git a/crates/wasmtime/src/runtime/vm/memory.rs b/crates/wasmtime/src/runtime/vm/memory.rs index 3383555b6424..5077182c4298 100644 --- a/crates/wasmtime/src/runtime/vm/memory.rs +++ b/crates/wasmtime/src/runtime/vm/memory.rs @@ -85,8 +85,6 @@ use alloc::sync::Arc; use core::{ops::Range, ptr::NonNull}; use wasmtime_environ::{MemoryKind, MemoryTunables}; -pub(crate) type SharedMemoryGrowthObserver = dyn Fn(usize, usize) + Send + Sync + 'static; - #[cfg(feature = "threads")] use wasmtime_environ::Trap; diff --git a/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs b/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs index b3f79127beca..569a1ddc16a0 100644 --- a/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs +++ b/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs @@ -1,15 +1,13 @@ use crate::Engine; use crate::prelude::*; -use crate::runtime::vm::memory::{ - LocalMemory, MmapMemory, SharedMemoryGrowthObserver, validate_atomic_addr, -}; +use crate::runtime::vm::memory::{LocalMemory, MmapMemory, validate_atomic_addr}; use crate::runtime::vm::parking_spot::{ParkingSpot, Waiter}; use crate::runtime::vm::{self, Memory, VMMemoryDefinition, WaitResult}; use std::cell::RefCell; use std::ops::Range; use std::ptr::NonNull; use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; -use std::sync::{Arc, Mutex, RwLock, Weak}; +use std::sync::{Arc, RwLock}; use std::time::{Duration, Instant}; use wasmtime_environ::Trap; @@ -26,8 +24,6 @@ pub struct SharedMemory(Arc); struct SharedMemoryInner { memory: RwLock, - growth_observers: RwLock>>, - growth_notification: Mutex<()>, spot: ParkingSpot, ty: wasmtime_environ::Memory, def: LongTermVMMemoryDefinition, @@ -83,8 +79,6 @@ impl SharedMemory { spot: ParkingSpot::default(), def: LongTermVMMemoryDefinition(memory.vmmemory()), memory: RwLock::new(memory), - growth_observers: RwLock::new(Vec::new()), - growth_notification: Mutex::new(()), })?)) } @@ -105,50 +99,20 @@ impl SharedMemory { /// Same as `RuntimeLinearMemory::grow`, except with `&self`. pub fn grow(&self, delta_pages: u64) -> Result, Error> { - let _notification = self.0.growth_notification.lock().unwrap(); - let result = { - let mut memory = self.0.memory.write().unwrap(); - // Without a limiter being passed in this shouldn't have an await point, - // so it should be safe to assert that it's ready. - let result = vm::assert_ready(memory.grow(delta_pages, None))?; - if let Some((_old_size_in_bytes, new_size_in_bytes)) = result { - self.0 - .def - .0 - .current_length - .store(new_size_in_bytes, Ordering::SeqCst); - } - result - }; - if let Some((old_size_in_bytes, new_size_in_bytes)) = result { - let observers = { - let mut registered = self.0.growth_observers.write().unwrap(); - let mut observers = Vec::with_capacity(registered.len()); - registered.retain(|observer| { - if let Some(observer) = observer.upgrade() { - observers.push(observer); - true - } else { - false - } - }); - observers - }; - for observer in observers { - observer(old_size_in_bytes, new_size_in_bytes); - } + let mut memory = self.0.memory.write().unwrap(); + // Without a limiter being passed in this shouldn't have an await point, + // so it should be safe to assert that it's ready. + let result = vm::assert_ready(memory.grow(delta_pages, None))?; + if let Some((_old_size_in_bytes, new_size_in_bytes)) = result { + self.0 + .def + .0 + .current_length + .store(new_size_in_bytes, Ordering::SeqCst); } Ok(result) } - pub fn subscribe_to_growth(&self, observer: &Arc) -> usize { - let memory = self.0.memory.read().unwrap(); - let mut observers = self.0.growth_observers.write().unwrap(); - observers.retain(|observer| observer.strong_count() > 0); - observers.push(Arc::downgrade(observer)); - memory.byte_size() - } - /// Implementation of `memory.atomic.notify` for this shared memory. pub fn atomic_notify(&self, addr_index: u64, count: u32) -> Result { let ptr = validate_atomic_addr(&self.0.def.0, addr_index, 4, 4)?; diff --git a/crates/wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs b/crates/wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs index 61a2635a26f3..079e1616cd22 100644 --- a/crates/wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs +++ b/crates/wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs @@ -1,8 +1,7 @@ use crate::Engine; use crate::prelude::*; -use crate::runtime::vm::memory::{LocalMemory, SharedMemoryGrowthObserver}; +use crate::runtime::vm::memory::LocalMemory; use crate::runtime::vm::{VMMemoryDefinition, WaitResult}; -use alloc::sync::Arc; use core::ops::Range; use core::ptr::NonNull; use core::time::Duration; @@ -36,10 +35,6 @@ impl SharedMemory { match *self {} } - pub fn subscribe_to_growth(&self, _observer: &Arc) -> usize { - match *self {} - } - pub fn atomic_notify(&self, _addr_index: u64, _count: u32) -> Result { match *self {} } From ea16f04fdbc932429360af1077697bdab6e07b74 Mon Sep 17 00:00:00 2001 From: Kaur Matas <33095685+kmatasfp@users.noreply.github.com> Date: Wed, 5 Aug 2026 19:46:34 -0700 Subject: [PATCH 11/11] Restore shared memory growth rationale --- .../src/runtime/vm/memory/shared_memory.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs b/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs index 569a1ddc16a0..1bff2586167c 100644 --- a/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs +++ b/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs @@ -104,6 +104,25 @@ impl SharedMemory { // so it should be safe to assert that it's ready. let result = vm::assert_ready(memory.grow(delta_pages, None))?; if let Some((_old_size_in_bytes, new_size_in_bytes)) = result { + // Store the new size to the `VMMemoryDefinition` for JIT-generated + // code (and runtime functions) to access. No other code can be + // growing this memory due to the write lock, but code in other + // threads could have access to this shared memory and we want them + // to see the most consistent version of the `current_length`; a + // weaker consistency is possible if we accept them seeing an older, + // smaller memory size (assumption: memory only grows) but presently + // we are aiming for accuracy. + // + // Note that it could be possible to access a memory address that is + // now-valid due to changes to the page flags in `grow` above but + // beyond the `memory.size` that we are about to assign to. In these + // and similar cases, discussion in the thread proposal concluded + // that: "multiple accesses in one thread racing with another + // thread's `memory.grow` that are in-bounds only after the grow + // commits may independently succeed or trap" (see + // https://github.com/WebAssembly/threads/issues/26#issuecomment-433930711). + // In other words, some non-determinism is acceptable when using + // `memory.size` on work being done by `memory.grow`. self.0 .def .0