From 667a819a7b8808d1f5f4a52e57d1f0d9fdb8c1a2 Mon Sep 17 00:00:00 2001 From: FRAU KOUJIRO Date: Mon, 8 Jun 2026 13:49:06 -0700 Subject: [PATCH 1/3] feat(runtime): Memory swapping Hot swap an existing Memory backing buffer with another. Requires the new memory to be shaped the same as the old. --- crates/wasmtime/src/runtime/memory.rs | 140 +++++++++++++++++++++ crates/wasmtime/src/runtime/store.rs | 41 +++++- crates/wasmtime/src/runtime/vm/instance.rs | 21 ++++ crates/wasmtime/src/runtime/vm/memory.rs | 16 +++ 4 files changed, 217 insertions(+), 1 deletion(-) diff --git a/crates/wasmtime/src/runtime/memory.rs b/crates/wasmtime/src/runtime/memory.rs index ab418253d4ea..4520fd329358 100644 --- a/crates/wasmtime/src/runtime/memory.rs +++ b/crates/wasmtime/src/runtime/memory.rs @@ -680,6 +680,72 @@ impl Memory { } } + /// Hot-swaps the backing storage of this memory with `other`'s, in O(1) and + /// without copying either memory's contents. + /// + /// After this returns, reads and writes through `self` observe what `other` + /// previously held and vice versa, and both memories' `VMContext`s are + /// updated so that running wasm sees the swap immediately. The two memories + /// exchange their entire backing allocations; their lengths and capacities + /// come along with them. + /// + /// # Errors + /// + /// Returns an error if the two memories have different [types](Memory::ty) or + /// different byte capacities, or if they are defined in the same instance + /// (swapping operates across instances). Swapping a memory with itself is a + /// no-op and returns `Ok`. + /// + /// # Panics + /// + /// Panics if either memory does not belong to `store`. + pub fn swap(&self, mut store: impl AsContextMut, other: &Memory) -> Result<()> { + let store = store.as_context_mut().0; + assert!( + self.comes_from_same_store(store), + "memory used with the wrong store" + ); + assert!( + other.comes_from_same_store(store), + "memory used with the wrong store" + ); + + let a = self.instance.instance(); + let b = other.instance.instance(); + if a == b { + // Same instance: only the no-op self-swap is supported; swapping two + // memories within one instance isn't (it has no use case and would + // need a different disjoint-borrow path). + if self.index == other.index { + return Ok(()); + } + bail!("cannot swap two memories defined in the same instance"); + } + + // Types (page size, limits, shared bit, index type) must match so the + // swapped-in memory is interchangeable. + if self.wasmtime_ty(store) != other.wasmtime_ty(store) { + bail!("cannot swap memories with different types"); + } + + // Capacities (reservations) must match so the swapped-in base satisfies + // the bounds checks baked into compiled code. Equal types under one + // engine already imply this, but check explicitly to keep the invariant + // local to the swap. + let a_cap = store[self.instance] + .get_defined_memory(self.index) + .byte_capacity(); + let b_cap = store[other.instance] + .get_defined_memory(other.index) + .byte_capacity(); + if a_cap != b_cap { + bail!("cannot swap memories with different byte capacities"); + } + + store.swap_defined_memories((a, self.index), (b, other.index)); + Ok(()) + } + /// Creates a new memory from its raw component parts. /// /// # Safety @@ -1156,4 +1222,78 @@ mod tests { Ok(()) } + + // Two host memories of the same type exchange their contents in place. + #[test] + fn swap_exchanges_contents() -> Result<()> { + let mut store = Store::<()>::default(); + let ty = MemoryType::new(1, None); + let a = Memory::new(&mut store, ty.clone())?; + let b = Memory::new(&mut store, ty)?; + + a.data_mut(&mut store)[0] = 0xAA; + b.data_mut(&mut store)[0] = 0xBB; + + a.swap(&mut store, &b)?; + assert_eq!(a.data(&store)[0], 0xBB); + assert_eq!(b.data(&store)[0], 0xAA); + + a.swap(&mut store, &b)?; + assert_eq!(a.data(&store)[0], 0xAA); + assert_eq!(b.data(&store)[0], 0xBB); + + Ok(()) + } + + // After swap, compiled wasm observes the new backing. + // i.e. the swap rewrites the in-`VMContext` `VMMemoryDefinition`. + #[test] + fn swap_is_visible_to_running_wasm() -> Result<()> { + let mut store = Store::<()>::default(); + let module = Module::new( + store.engine(), + r#" + (module + (memory (export "m") 1 1) + (func (export "load8") (param i32) (result i32) + local.get 0 + i32.load8_u)) + "#, + )?; + let instance = Instance::new(&mut store, &module, &[])?; + let m = instance.get_memory(&mut store, "m").unwrap(); + let load8 = instance.get_typed_func::(&mut store, "load8")?; + + // A detached buffer of the matching type holding different contents. + let buf_ty = m.ty(&store); + let buf = Memory::new(&mut store, buf_ty)?; + m.data_mut(&mut store)[0] = 11; + buf.data_mut(&mut store)[0] = 99; + + assert_eq!(load8.call(&mut store, 0)?, 11); + + m.swap(&mut store, &buf)?; + + // Compiled code now reads the swapped-in backing. + assert_eq!(load8.call(&mut store, 0)?, 99); + assert_eq!(m.data(&store)[0], 99); + assert_eq!(buf.data(&store)[0], 11); + + Ok(()) + } + + // Swapping a memory with itself is a no-op; mismatched types are rejected. + #[test] + fn swap_self_noop_and_type_mismatch() -> Result<()> { + let mut store = Store::<()>::default(); + let a = Memory::new(&mut store, MemoryType::new(1, None))?; + a.data_mut(&mut store)[0] = 7; + a.swap(&mut store, &a)?; + assert_eq!(a.data(&store)[0], 7); + + let big = Memory::new(&mut store, MemoryType::new(2, None))?; + assert!(a.swap(&mut store, &big).is_err()); + + Ok(()) + } } diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index c099fc2d3781..0376066a5a12 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -110,7 +110,9 @@ use core::pin::Pin; use core::ptr::NonNull; #[cfg(any(feature = "async", feature = "gc"))] use core::task::Poll; -use wasmtime_environ::{DefinedGlobalIndex, DefinedTableIndex, EntityRef, TripleExt}; +use wasmtime_environ::{ + DefinedGlobalIndex, DefinedMemoryIndex, DefinedTableIndex, EntityRef, TripleExt, +}; mod context; pub use self::context::*; @@ -1660,6 +1662,43 @@ impl StoreOpaque { self.instances[id].handle.get_mut() } + /// Hot-swap the backing storage of two defined memories living in two + /// different instances within this store without copying contents. + /// The `(allocation, Memory)` entries are swapped wholesale (so the + /// instance allocator's bookkeeping travels with each memory, keeping both + /// the on-demand and pooling allocators correct), and each instance's + /// `VMContext` is refreshed so compiled code observes the swap. + /// + /// # Panics + /// + /// Panics if `a.0 == b.0` (the two memories must be in different instances); + /// callers (`Memory::swap`) reject that case with an error beforehand. The + /// caller must also have validated that the two memories have matching types + /// and byte capacities. + pub(crate) fn swap_defined_memories( + &mut self, + a: (InstanceId, DefinedMemoryIndex), + b: (InstanceId, DefinedMemoryIndex), + ) { + assert_ne!( + a.0, b.0, + "swap_defined_memories requires different instances" + ); + // SAFETY: `a.0 != b.0` so the two instance handles are disjoint, and we + // only touch each instance's own defined memory (never traversing + // laterally between instances), satisfying the contract of + // `optional_gc_store_and_instances_mut`. + unsafe { + let (_gc, [mut ia, mut ib]) = self.optional_gc_store_and_instances_mut([a.0, b.0]); + core::mem::swap( + ia.as_mut().defined_memory_entry_mut(a.1), + ib.as_mut().defined_memory_entry_mut(b.1), + ); + ia.as_mut().refresh_defined_memory(a.1); + ib.as_mut().refresh_defined_memory(b.1); + } + } + /// Accessor from `InstanceId` to both `Pin<&mut vm::Instance>` /// and `&ModuleRegistry`. #[inline] diff --git a/crates/wasmtime/src/runtime/vm/instance.rs b/crates/wasmtime/src/runtime/vm/instance.rs index 06ba7e44551c..ec3b26a39a72 100644 --- a/crates/wasmtime/src/runtime/vm/instance.rs +++ b/crates/wasmtime/src/runtime/vm/instance.rs @@ -1034,6 +1034,27 @@ impl Instance { &self.memories[index].1 } + /// Mutable access to the `index`th defined memory's `(allocation, memory)` + /// entry. Used by the store to hot-swap a memory's backing storage between + /// two instances; after swapping the entry, call + /// [`Instance::refresh_defined_memory`] to update the `VMContext`. + pub(crate) fn defined_memory_entry_mut( + self: Pin<&mut Self>, + index: DefinedMemoryIndex, + ) -> &mut (MemoryAllocationIndex, Memory) { + &mut self.memories_mut()[index] + } + + /// Recompute the `index`th defined memory's `VMMemoryDefinition` (base + + /// length) from its current allocation and write it into the `VMContext`, so + /// compiled code observes the memory's current backing. This is the same + /// fix-up [`Instance::memory_grow`] performs after a grow; it must be called + /// after the entry is swapped via [`Instance::defined_memory_entry_mut`]. + pub(crate) fn refresh_defined_memory(mut self: Pin<&mut Self>, index: DefinedMemoryIndex) { + let vmmemory = self.as_mut().get_defined_memory_mut(index).vmmemory(); + self.set_memory(index, vmmemory); + } + pub fn get_defined_memory_vmimport(&self, index: DefinedMemoryIndex) -> VMMemoryImport { crate::runtime::vm::VMMemoryImport { from: self.memory_ptr(index).into(), diff --git a/crates/wasmtime/src/runtime/vm/memory.rs b/crates/wasmtime/src/runtime/vm/memory.rs index fce3c4afa03f..136fa9317f14 100644 --- a/crates/wasmtime/src/runtime/vm/memory.rs +++ b/crates/wasmtime/src/runtime/vm/memory.rs @@ -385,6 +385,18 @@ impl Memory { } } + /// Returns the number of bytes this memory's current allocation can address + /// without relocating its base pointer (i.e. its reservation). Two memories + /// can only be hot-swapped (see the embedder `Memory::swap`) if their + /// capacities match. + pub fn byte_capacity(&self) -> usize { + match self { + Memory::Local(mem) => mem.byte_capacity(), + // Shared memories are never hot-swapped; report the logical size. + Memory::Shared(mem) => mem.byte_size(), + } + } + /// Returns whether or not this memory needs initialization. It /// may not if it already has initial content thanks to a CoW /// mechanism. @@ -733,6 +745,10 @@ impl LocalMemory { self.alloc.byte_size() } + pub fn byte_capacity(&self) -> usize { + self.alloc.byte_capacity() + } + pub fn needs_init(&self) -> bool { match &self.memory_image { Some(image) => !image.has_image(), From ed8b798426b94b6631ea8a02d3f3b8c9f37d4e5f Mon Sep 17 00:00:00 2001 From: FRAU KOUJIRO Date: Tue, 9 Jun 2026 12:53:42 -0700 Subject: [PATCH 2/3] Instance::defined_memory_is_on_demand() predicate --- crates/wasmtime/src/runtime/vm/instance.rs | 6 ++++++ crates/wasmtime/src/runtime/vm/instance/allocator.rs | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/crates/wasmtime/src/runtime/vm/instance.rs b/crates/wasmtime/src/runtime/vm/instance.rs index ec3b26a39a72..9f07b626c55e 100644 --- a/crates/wasmtime/src/runtime/vm/instance.rs +++ b/crates/wasmtime/src/runtime/vm/instance.rs @@ -1029,6 +1029,12 @@ impl Instance { &mut self.memories_mut()[index].1 } + /// Returns whether the given locally-defined memory was allocated by the + /// on-demand allocator (as opposed to the pooling allocator). + pub(crate) fn defined_memory_is_on_demand(&self, index: DefinedMemoryIndex) -> bool { + self.memories[index].0.is_on_demand() + } + /// Get a locally-defined memory. pub fn get_defined_memory(&self, index: DefinedMemoryIndex) -> &Memory { &self.memories[index].1 diff --git a/crates/wasmtime/src/runtime/vm/instance/allocator.rs b/crates/wasmtime/src/runtime/vm/instance/allocator.rs index 2dbc1f122607..938f13284583 100644 --- a/crates/wasmtime/src/runtime/vm/instance/allocator.rs +++ b/crates/wasmtime/src/runtime/vm/instance/allocator.rs @@ -74,6 +74,12 @@ impl MemoryAllocationIndex { pub fn index(&self) -> usize { self.0 as usize } + + /// Whether this is the sentinel value used by the on-demand allocator + /// (meaning this memory was not allocated from a pool). + pub(super) fn is_on_demand(&self) -> bool { + self.0 == u32::MAX + } } /// The index of a table allocation within an `InstanceAllocator`. From a6dd055440060c13704be28d59a5e00672bb055d Mon Sep 17 00:00:00 2001 From: FRAU KOUJIRO Date: Tue, 9 Jun 2026 12:55:58 -0700 Subject: [PATCH 3/3] fix cross-allocator swap guard, add tests --- crates/wasmtime/src/runtime/memory.rs | 12 + tests/all/memory.rs | 367 ++++++++++++++++++++++++++ 2 files changed, 379 insertions(+) diff --git a/crates/wasmtime/src/runtime/memory.rs b/crates/wasmtime/src/runtime/memory.rs index 4520fd329358..523544200b78 100644 --- a/crates/wasmtime/src/runtime/memory.rs +++ b/crates/wasmtime/src/runtime/memory.rs @@ -742,6 +742,18 @@ impl Memory { bail!("cannot swap memories with different byte capacities"); } + // Both memories must come from the same kind of allocator. Swapping a + // pooling-allocator memory with an on-demand memory would cross-wire + // the allocation indices, corrupting the allocator's bookkeeping and + // causing panics on deallocation. + let a_is_on_demand = store[self.instance] + .defined_memory_is_on_demand(self.index); + let b_is_on_demand = store[other.instance] + .defined_memory_is_on_demand(other.index); + if a_is_on_demand != b_is_on_demand { + bail!("cannot swap memories allocated from different allocators"); + } + store.swap_defined_memories((a, self.index), (b, other.index)); Ok(()) } diff --git a/tests/all/memory.rs b/tests/all/memory.rs index f321f5d98c36..eb76d81d6596 100644 --- a/tests/all/memory.rs +++ b/tests/all/memory.rs @@ -6,6 +6,8 @@ use std::time::Duration; use wasmtime::*; use wasmtime_test_macros::wasmtime_test; +use crate::ErrorExt; + fn module(engine: &Engine) -> Result { let mut wat = format!("(module\n"); wat.push_str("(import \"\" \"\" (memory 0))\n"); @@ -876,3 +878,368 @@ fn atomic_wait_massive_timeout() -> Result<()> { Ok(()) } + +#[test] +fn swap_rejects_cross_allocator() -> Result<()> { + if crate::skip_pooling_allocator_tests() { + return Ok(()); + } + + let mut pool = crate::small_pool_config(); + pool.total_memories(2).max_memory_size(1 << 16); + let mut config = Config::new(); + config.allocation_strategy(InstanceAllocationStrategy::Pooling(pool)); + config.memory_guard_size(0); + config.memory_reservation(1 << 16); + let engine = Engine::new(&config)?; + let module = Module::new( + &engine, + r#"(module (memory (export "m") 1 1))"#, + )?; + let mut store = Store::new(&engine, ()); + let instance = Instance::new(&mut store, &module, &[])?; + let pooled = instance.get_memory(&mut store, "m").unwrap(); + let host = Memory::new(&mut store, MemoryType::new(1, Some(1)))?; + let err = pooled.swap(&mut store, &host).unwrap_err(); + err.assert_contains("different allocators"); + + Ok(()) +} + +#[test] +fn swap_pooling_allocator() -> Result<()> { + if crate::skip_pooling_allocator_tests() { + return Ok(()); + } + + let mut pool = crate::small_pool_config(); + pool.total_memories(2).max_memory_size(1 << 16); + let mut config = Config::new(); + config.allocation_strategy(InstanceAllocationStrategy::Pooling(pool)); + config.memory_guard_size(0); + config.memory_reservation(1 << 16); + let engine = Engine::new(&config)?; + + let module = Module::new( + &engine, + r#"(module (memory (export "m") 1 1))"#, + )?; + + let mut store = Store::new(&engine, ()); + let inst_a = Instance::new(&mut store, &module, &[])?; + let inst_b = Instance::new(&mut store, &module, &[])?; + + let mem_a = inst_a.get_memory(&mut store, "m").unwrap(); + let mem_b = inst_b.get_memory(&mut store, "m").unwrap(); + + // Write distinct data to each memory + mem_a.data_mut(&mut store)[0] = 0xAA; + mem_b.data_mut(&mut store)[0] = 0xBB; + assert_eq!(mem_a.data(&store)[0], 0xAA); + assert_eq!(mem_b.data(&store)[0], 0xBB); + + // Swap: data should travel with the memory + mem_a.swap(&mut store, &mem_b)?; + assert_eq!( + mem_a.data(&store)[0], + 0xBB, + "mem_a should now hold inst_b's data" + ); + assert_eq!( + mem_b.data(&store)[0], + 0xAA, + "mem_b should now hold inst_a's data" + ); + + // Swap back + mem_a.swap(&mut store, &mem_b)?; + assert_eq!(mem_a.data(&store)[0], 0xAA); + assert_eq!(mem_b.data(&store)[0], 0xBB); + + Ok(()) +} + +// Self-swap on pooling allocator is a no-op. +#[test] +fn swap_pooling_allocator_self_swap() -> Result<()> { + if crate::skip_pooling_allocator_tests() { + return Ok(()); + } + + let mut pool = crate::small_pool_config(); + pool.total_memories(1).max_memory_size(1 << 16); + let mut config = Config::new(); + config.allocation_strategy(InstanceAllocationStrategy::Pooling(pool)); + config.memory_guard_size(0); + config.memory_reservation(1 << 16); + let engine = Engine::new(&config)?; + + let module = Module::new( + &engine, + r#"(module (memory (export "m") 1 1))"#, + )?; + + let mut store = Store::new(&engine, ()); + let inst = Instance::new(&mut store, &module, &[])?; + let mem = inst.get_memory(&mut store, "m").unwrap(); + + mem.data_mut(&mut store)[0] = 0xAA; + mem.swap(&mut store, &mem)?; + assert_eq!(mem.data(&store)[0], 0xAA); + + Ok(()) +} + +#[test] +fn swap_pooling_allocator_type_mismatch() -> Result<()> { + if crate::skip_pooling_allocator_tests() { + return Ok(()); + } + + let mut pool = crate::small_pool_config(); + pool.total_memories(2).max_memory_size(2 << 16); + let mut config = Config::new(); + config.allocation_strategy(InstanceAllocationStrategy::Pooling(pool)); + config.memory_guard_size(0); + config.memory_reservation(2 << 16); + let engine = Engine::new(&config)?; + + let module_1page = Module::new( + &engine, + r#"(module (memory (export "m") 1 1))"#, + )?; + let module_2page = Module::new( + &engine, + r#"(module (memory (export "m") 2 2))"#, + )?; + + let mut store = Store::new(&engine, ()); + let inst_a = Instance::new(&mut store, &module_1page, &[])?; + let inst_b = Instance::new(&mut store, &module_2page, &[])?; + + let mem_a = inst_a.get_memory(&mut store, "m").unwrap(); + let mem_b = inst_b.get_memory(&mut store, "m").unwrap(); + + let err = mem_a.swap(&mut store, &mem_b).unwrap_err(); + err.assert_contains("different types"); + + Ok(()) +} + +#[test] +fn swap_ondemand_mmap() -> Result<()> { + let mut config = Config::new(); + config.allocation_strategy(InstanceAllocationStrategy::OnDemand); + // allocate a page to trigger mmap path + config.memory_reservation(1 << 16); + let engine = Engine::new(&config)?; + let mut store = Store::new(&engine, ()); + + let mem1 = Memory::new(&mut store, MemoryType::new(1, Some(2)))?; + let mem2 = Memory::new(&mut store, MemoryType::new(1, Some(2)))?; + + mem1.data_mut(&mut store)[0] = 0x11; + mem2.data_mut(&mut store)[0] = 0x22; + + mem1.swap(&mut store, &mem2)?; + assert_eq!(mem1.data(&store)[0], 0x22); + assert_eq!(mem2.data(&store)[0], 0x11); + + Ok(()) +} + +#[test] +fn swap_ondemand_mmap_self_swap() -> Result<()> { + let mut config = Config::new(); + config.allocation_strategy(InstanceAllocationStrategy::OnDemand); + config.memory_reservation(1 << 16); + let engine = Engine::new(&config)?; + let mut store = Store::new(&engine, ()); + + let mem = Memory::new(&mut store, MemoryType::new(1, Some(2)))?; + mem.data_mut(&mut store)[0] = 0x11; + + mem.swap(&mut store, &mem)?; + assert_eq!(mem.data(&store)[0], 0x11); + + Ok(()) +} + +#[test] +fn swap_ondemand_mmap_type_mismatch() -> Result<()> { + let mut config = Config::new(); + config.allocation_strategy(InstanceAllocationStrategy::OnDemand); + config.memory_reservation(1 << 16); + let engine = Engine::new(&config)?; + let mut store = Store::new(&engine, ()); + + let mem1 = Memory::new(&mut store, MemoryType::new(1, Some(2)))?; + let mem2 = Memory::new(&mut store, MemoryType::new(2, Some(2)))?; + + let err = mem1.swap(&mut store, &mem2).unwrap_err(); + err.assert_contains("different types"); + + Ok(()) +} + +#[test] +fn swap_ondemand_mmap_capacity_mismatch() -> Result<()> { + let mut config = Config::new(); + config.allocation_strategy(InstanceAllocationStrategy::OnDemand); + config.memory_reservation(1 << 16); + let engine = Engine::new(&config)?; + let mut store = Store::new(&engine, ()); + + let mem_a = Memory::new(&mut store, MemoryType::new(1, None))?; + let mem_b = Memory::new(&mut store, MemoryType::new(1, None))?; + mem_a.swap(&mut store, &mem_b)?; + mem_a.grow(&mut store, 1)?; // bump capacity + let err = mem_a.swap(&mut store, &mem_b).unwrap_err(); + err.assert_contains("different byte capacities"); + + Ok(()) +} + +#[test] +fn swap_ondemand_malloc() -> Result<()> { + let mut config = Config::new(); + config.allocation_strategy(InstanceAllocationStrategy::OnDemand); + config.memory_reservation(0); + config.memory_guard_size(0); + config.signals_based_traps(false); + let engine = Engine::new(&config)?; + let mut store = Store::new(&engine, ()); + + let mem1 = Memory::new(&mut store, MemoryType::new(1, Some(2)))?; + let mem2 = Memory::new(&mut store, MemoryType::new(1, Some(2)))?; + + mem1.data_mut(&mut store)[0] = 0x11; + mem2.data_mut(&mut store)[0] = 0x22; + + mem1.swap(&mut store, &mem2)?; + assert_eq!(mem1.data(&store)[0], 0x22); + assert_eq!(mem2.data(&store)[0], 0x11); + + Ok(()) +} + +#[test] +fn swap_ondemand_malloc_self_swap() -> Result<()> { + let mut config = Config::new(); + config.allocation_strategy(InstanceAllocationStrategy::OnDemand); + config.memory_reservation(0); + config.memory_guard_size(0); + config.signals_based_traps(false); + let engine = Engine::new(&config)?; + let mut store = Store::new(&engine, ()); + + let mem = Memory::new(&mut store, MemoryType::new(1, Some(2)))?; + mem.data_mut(&mut store)[0] = 0x11; + + mem.swap(&mut store, &mem)?; + assert_eq!(mem.data(&store)[0], 0x11); + + Ok(()) +} + +#[test] +fn swap_ondemand_malloc_type_mismatch() -> Result<()> { + let mut config = Config::new(); + config.allocation_strategy(InstanceAllocationStrategy::OnDemand); + config.memory_reservation(0); + config.memory_guard_size(0); + config.signals_based_traps(false); + let engine = Engine::new(&config)?; + let mut store = Store::new(&engine, ()); + + let mem1 = Memory::new(&mut store, MemoryType::new(1, Some(2)))?; + let mem2 = Memory::new(&mut store, MemoryType::new(2, Some(2)))?; + + let err = mem1.swap(&mut store, &mem2).unwrap_err(); + err.assert_contains("different types"); + + Ok(()) +} + +#[test] +fn swap_ondemand_malloc_capacity_mismatch() -> Result<()> { + // Must set `memory_reservation_for_growth(0)` so Vec capacity is tight + // and actually changes on grow (default is 2GiB headroom on 64-bit). + let mut config = Config::new(); + config.allocation_strategy(InstanceAllocationStrategy::OnDemand); + config.memory_reservation(0); + config.memory_guard_size(0); + config.memory_reservation_for_growth(0); + config.signals_based_traps(false); + let engine = Engine::new(&config)?; + let mut store = Store::new(&engine, ()); + + let mem_a = Memory::new(&mut store, MemoryType::new(1, None))?; + let mem_b = Memory::new(&mut store, MemoryType::new(1, None))?; + mem_a.swap(&mut store, &mem_b)?; + mem_a.grow(&mut store, 1)?; + let err = mem_a.swap(&mut store, &mem_b).unwrap_err(); + err.assert_contains("different byte capacities"); + + Ok(()) +} + +#[test] +fn swap_multi_memory_pooling() -> Result<()> { + if crate::skip_pooling_allocator_tests() { + return Ok(()); + } + + let mut pool = crate::small_pool_config(); + pool.total_memories(4) + .max_memories_per_module(2) + .max_memory_size(1 << 16) + .total_tables(4) + .table_elements(10); + let mut config = Config::new(); + config.allocation_strategy(InstanceAllocationStrategy::Pooling(pool)); + config.memory_guard_size(0); + config.memory_reservation(1 << 16); + config.wasm_multi_memory(true); + + let engine = Engine::new(&config)?; + let module = Module::new( + &engine, + r#" + (module + (memory (export "m0") 1 1) + (memory (export "m1") 1 1) + ) + "#, + )?; + + let mut store = Store::new(&engine, ()); + let inst_a = Instance::new(&mut store, &module, &[])?; + let inst_b = Instance::new(&mut store, &module, &[])?; + + let a_m0 = inst_a.get_memory(&mut store, "m0").unwrap(); + let a_m1 = inst_a.get_memory(&mut store, "m1").unwrap(); + let b_m0 = inst_b.get_memory(&mut store, "m0").unwrap(); + let b_m1 = inst_b.get_memory(&mut store, "m1").unwrap(); + + a_m0.data_mut(&mut store)[0] = b'A'; + a_m1.data_mut(&mut store)[0] = b'B'; + b_m0.data_mut(&mut store)[0] = b'C'; + b_m1.data_mut(&mut store)[0] = b'D'; + + a_m0.swap(&mut store, &b_m0)?; + assert_eq!(a_m0.data(&store)[0], b'C'); // got inst_b's m0 + assert_eq!(b_m0.data(&store)[0], b'A'); // got inst_a's m0 + + a_m1.swap(&mut store, &b_m1)?; + assert_eq!(a_m1.data(&store)[0], b'D'); // got inst_b's m1 + assert_eq!(b_m1.data(&store)[0], b'B'); // got inst_a's m1 + + // Now inst_a has (C, D) and inst_b has (A, B) + // Cross-swap: swap a_m0 with b_m1 + a_m0.swap(&mut store, &b_m1)?; + assert_eq!(a_m0.data(&store)[0], b'B'); // got inst_b's original m1 + assert_eq!(b_m1.data(&store)[0], b'C'); // got inst_b's original m0 + + Ok(()) +}