Skip to main content

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
150        // Note that `checked_add` is used such that when `timeout` is too large
151        // it'll cause there to be no timeout at all if we can't represent the
152        // deadline. That effectively maps to the requested timeout since if we
153        // can't represent the deadline we'll be here awhile.
154        let deadline = timeout.and_then(|d| Instant::now().checked_add(d));
155
156        WAITER.with(|waiter| {
157            let mut waiter = waiter.borrow_mut();
158            Ok(self.0.spot.wait32(atomic, expected, deadline, &mut waiter))
159        })
160    }
161
162    /// Implementation of `memory.atomic.wait64` for this shared memory.
163    pub fn atomic_wait64(
164        &self,
165        addr_index: u64,
166        expected: u64,
167        timeout: Option<Duration>,
168    ) -> Result<WaitResult, Trap> {
169        let addr = validate_atomic_addr(&self.0.def.0, addr_index, 8, 8)?;
170        log::trace!(
171            "memory.atomic.wait64(addr={addr_index:#x}, expected={expected}, timeout={timeout:?})"
172        );
173
174        // SAFETY: `addr_index` was validated by `validate_atomic_addr` above.
175        assert!(std::mem::size_of::<AtomicU64>() == 8);
176        assert!(std::mem::align_of::<AtomicU64>() <= 8);
177        let atomic = unsafe { AtomicU64::from_ptr(addr.cast()) };
178
179        // See `atomic_wait32` for why this is using `checked_add`.
180        let deadline = timeout.and_then(|d| Instant::now().checked_add(d));
181
182        WAITER.with(|waiter| {
183            let mut waiter = waiter.borrow_mut();
184            Ok(self.0.spot.wait64(atomic, expected, deadline, &mut waiter))
185        })
186    }
187
188    pub(crate) fn byte_size(&self) -> usize {
189        self.0.memory.read().unwrap().byte_size()
190    }
191
192    pub(crate) fn needs_init(&self) -> bool {
193        self.0.memory.read().unwrap().needs_init()
194    }
195
196    pub(crate) fn wasm_accessible(&self) -> Range<usize> {
197        self.0.memory.read().unwrap().wasm_accessible()
198    }
199}
200
201thread_local! {
202    /// Structure used in conjunction with `ParkingSpot` to block the current
203    /// thread if necessary. Note that this is lazily initialized.
204    static WAITER: RefCell<Waiter> = const { RefCell::new(Waiter::new()) };
205}
206
207/// Shared memory needs some representation of a `VMMemoryDefinition` for
208/// JIT-generated code to access. This structure owns the base pointer and
209/// length to the actual memory and we share this definition across threads by:
210/// - never changing the base pointer; according to the specification, shared
211///   memory must be created with a known maximum size so it can be allocated
212///   once and never moved
213/// - carefully changing the length, using atomic accesses in both the runtime
214///   and JIT-generated code.
215struct LongTermVMMemoryDefinition(VMMemoryDefinition);
216unsafe impl Send for LongTermVMMemoryDefinition {}
217unsafe impl Sync for LongTermVMMemoryDefinition {}