wasmtime/runtime/vm/memory/shared_memory.rs
1use crate::Engine;
2use crate::prelude::*;
3use crate::runtime::vm::memory::{LocalMemory, MmapMemory, validate_atomic_addr};
4use crate::runtime::vm::parking_spot::{ParkingSpot, Waiter};
5use crate::runtime::vm::{self, Memory, VMMemoryDefinition, WaitResult};
6use std::cell::RefCell;
7use std::ops::Range;
8use std::ptr::NonNull;
9use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
10use std::sync::{Arc, RwLock};
11use std::time::{Duration, Instant};
12use wasmtime_environ::Trap;
13
14/// For shared memory (and only for shared memory), this lock-version restricts
15/// access when growing the memory or checking its size. This is to conform with
16/// the [thread proposal]: "When `IsSharedArrayBuffer(...)` is true, the return
17/// value should be the result of an atomic read-modify-write of the new size to
18/// the internal `length` slot."
19///
20/// [thread proposal]:
21/// https://github.com/WebAssembly/threads/blob/master/proposals/threads/Overview.md#webassemblymemoryprototypegrow
22#[derive(Clone)]
23pub struct SharedMemory(Arc<SharedMemoryInner>);
24
25struct SharedMemoryInner {
26 memory: RwLock<LocalMemory>,
27 spot: ParkingSpot,
28 ty: wasmtime_environ::Memory,
29 def: LongTermVMMemoryDefinition,
30}
31
32impl SharedMemory {
33 /// Construct a new [`SharedMemory`].
34 pub fn new(engine: &Engine, ty: &wasmtime_environ::Memory) -> Result<Self> {
35 let tunables = engine.tunables();
36 let memory_tunables = wasmtime_environ::MemoryTunables::new(
37 tunables,
38 wasmtime_environ::MemoryKind::LinearMemory,
39 );
40 // Note that without a limiter being passed to `limit_new` this
41 // `assert_ready` should never panic.
42 let (minimum_bytes, maximum_bytes) = vm::assert_ready(Memory::limit_new(ty, None))?;
43 let mmap_memory = MmapMemory::new(ty, &memory_tunables, minimum_bytes, maximum_bytes)?;
44 let boxed: Box<dyn crate::runtime::vm::RuntimeLinearMemory> =
45 try_new::<Box<_>>(mmap_memory)?;
46 Self::wrap(
47 engine,
48 ty,
49 LocalMemory::new(ty, &memory_tunables, boxed, None)?,
50 )
51 }
52
53 /// Wrap an existing [Memory] with the locking provided by a [SharedMemory].
54 pub fn wrap(
55 engine: &Engine,
56 ty: &wasmtime_environ::Memory,
57 memory: LocalMemory,
58 ) -> Result<Self> {
59 if !engine.config().shared_memory {
60 bail!(
61 "shared memory support is disabled for this engine -- see `Config::shared_memory`"
62 );
63 }
64 if !ty.shared {
65 bail!("shared memory must have a `shared` memory type");
66 }
67 Ok(Self(try_new::<Arc<_>>(SharedMemoryInner {
68 ty: *ty,
69 spot: ParkingSpot::default(),
70 def: LongTermVMMemoryDefinition(memory.vmmemory()),
71 memory: RwLock::new(memory),
72 })?))
73 }
74
75 /// Return the memory type for this [`SharedMemory`].
76 pub fn ty(&self) -> &wasmtime_environ::Memory {
77 &self.0.ty
78 }
79
80 /// Convert this shared memory into a [`Memory`].
81 pub fn as_memory(self) -> Memory {
82 Memory::Shared(self)
83 }
84
85 /// Return a pointer to the shared memory's [VMMemoryDefinition].
86 pub fn vmmemory_ptr(&self) -> NonNull<VMMemoryDefinition> {
87 NonNull::from(&self.0.def.0)
88 }
89
90 /// Same as `RuntimeLinearMemory::grow`, except with `&self`.
91 pub fn grow(&self, delta_pages: u64) -> Result<Option<(usize, usize)>, Error> {
92 let mut memory = self.0.memory.write().unwrap();
93 // Without a limiter being passed in this shouldn't have an await point,
94 // so it should be safe to assert that it's ready.
95 let result = vm::assert_ready(memory.grow(delta_pages, None))?;
96 if let Some((_old_size_in_bytes, new_size_in_bytes)) = result {
97 // Store the new size to the `VMMemoryDefinition` for JIT-generated
98 // code (and runtime functions) to access. No other code can be
99 // growing this memory due to the write lock, but code in other
100 // threads could have access to this shared memory and we want them
101 // to see the most consistent version of the `current_length`; a
102 // weaker consistency is possible if we accept them seeing an older,
103 // smaller memory size (assumption: memory only grows) but presently
104 // we are aiming for accuracy.
105 //
106 // Note that it could be possible to access a memory address that is
107 // now-valid due to changes to the page flags in `grow` above but
108 // beyond the `memory.size` that we are about to assign to. In these
109 // and similar cases, discussion in the thread proposal concluded
110 // that: "multiple accesses in one thread racing with another
111 // thread's `memory.grow` that are in-bounds only after the grow
112 // commits may independently succeed or trap" (see
113 // https://github.com/WebAssembly/threads/issues/26#issuecomment-433930711).
114 // In other words, some non-determinism is acceptable when using
115 // `memory.size` on work being done by `memory.grow`.
116 self.0
117 .def
118 .0
119 .current_length
120 .store(new_size_in_bytes, Ordering::SeqCst);
121 }
122 Ok(result)
123 }
124
125 /// Implementation of `memory.atomic.notify` for this shared memory.
126 pub fn atomic_notify(&self, addr_index: u64, count: u32) -> Result<u32, Trap> {
127 let ptr = validate_atomic_addr(&self.0.def.0, addr_index, 4, 4)?;
128 log::trace!("memory.atomic.notify(addr={addr_index:#x}, count={count})");
129 let ptr = unsafe { &*ptr };
130 Ok(self.0.spot.notify(ptr, count))
131 }
132
133 /// Implementation of `memory.atomic.wait32` for this shared memory.
134 pub fn atomic_wait32(
135 &self,
136 addr_index: u64,
137 expected: u32,
138 timeout: Option<Duration>,
139 ) -> Result<WaitResult, Trap> {
140 let addr = validate_atomic_addr(&self.0.def.0, addr_index, 4, 4)?;
141 log::trace!(
142 "memory.atomic.wait32(addr={addr_index:#x}, expected={expected}, timeout={timeout:?})"
143 );
144
145 // SAFETY: `addr_index` was validated by `validate_atomic_addr` above.
146 assert!(std::mem::size_of::<AtomicU32>() == 4);
147 assert!(std::mem::align_of::<AtomicU32>() <= 4);
148 let atomic = unsafe { AtomicU32::from_ptr(addr.cast()) };
149 // Wasm linear memory is always little-endian, but `AtomicU32` uses the
150 // host's native endianness.
151 let expected = expected.to_le();
152
153 // Note that `checked_add` is used such that when `timeout` is too large
154 // it'll cause there to be no timeout at all if we can't represent the
155 // deadline. That effectively maps to the requested timeout since if we
156 // can't represent the deadline we'll be here awhile.
157 let deadline = timeout.and_then(|d| Instant::now().checked_add(d));
158
159 WAITER.with(|waiter| {
160 let mut waiter = waiter.borrow_mut();
161 Ok(self.0.spot.wait32(atomic, expected, deadline, &mut waiter))
162 })
163 }
164
165 /// Implementation of `memory.atomic.wait64` for this shared memory.
166 pub fn atomic_wait64(
167 &self,
168 addr_index: u64,
169 expected: u64,
170 timeout: Option<Duration>,
171 ) -> Result<WaitResult, Trap> {
172 let addr = validate_atomic_addr(&self.0.def.0, addr_index, 8, 8)?;
173 log::trace!(
174 "memory.atomic.wait64(addr={addr_index:#x}, expected={expected}, timeout={timeout:?})"
175 );
176
177 // SAFETY: `addr_index` was validated by `validate_atomic_addr` above.
178 assert!(std::mem::size_of::<AtomicU64>() == 8);
179 assert!(std::mem::align_of::<AtomicU64>() <= 8);
180 let atomic = unsafe { AtomicU64::from_ptr(addr.cast()) };
181 // Wasm linear memory is always little-endian, but `AtomicU64` uses the
182 // host's native endianness.
183 let expected = expected.to_le();
184
185 // See `atomic_wait32` for why this is using `checked_add`.
186 let deadline = timeout.and_then(|d| Instant::now().checked_add(d));
187
188 WAITER.with(|waiter| {
189 let mut waiter = waiter.borrow_mut();
190 Ok(self.0.spot.wait64(atomic, expected, deadline, &mut waiter))
191 })
192 }
193
194 pub(crate) fn byte_size(&self) -> usize {
195 self.0.memory.read().unwrap().byte_size()
196 }
197
198 pub(crate) fn needs_init(&self) -> bool {
199 self.0.memory.read().unwrap().needs_init()
200 }
201
202 pub(crate) fn wasm_accessible(&self) -> Range<usize> {
203 self.0.memory.read().unwrap().wasm_accessible()
204 }
205}
206
207thread_local! {
208 /// Structure used in conjunction with `ParkingSpot` to block the current
209 /// thread if necessary. Note that this is lazily initialized.
210 static WAITER: RefCell<Waiter> = const { RefCell::new(Waiter::new()) };
211}
212
213/// Shared memory needs some representation of a `VMMemoryDefinition` for
214/// JIT-generated code to access. This structure owns the base pointer and
215/// length to the actual memory and we share this definition across threads by:
216/// - never changing the base pointer; according to the specification, shared
217/// memory must be created with a known maximum size so it can be allocated
218/// once and never moved
219/// - carefully changing the length, using atomic accesses in both the runtime
220/// and JIT-generated code.
221struct LongTermVMMemoryDefinition(VMMemoryDefinition);
222unsafe impl Send for LongTermVMMemoryDefinition {}
223unsafe impl Sync for LongTermVMMemoryDefinition {}