Skip to main content

wasmtime/runtime/vm/instance/allocator/pooling/
memory_pool.rs

1//! Implements a memory pool using a single allocated memory slab.
2//!
3//! The pooling instance allocator maps one large slab of memory in advance and
4//! allocates WebAssembly memories from this slab--a [`MemoryPool`]. Each
5//! WebAssembly memory is allocated in its own slot (see uses of `index` and
6//! [`SlotId`] in this module):
7//!
8//! ```text
9//! ┌──────┬──────┬──────┬──────┬──────┐
10//! │Slot 0│Slot 1│Slot 2│Slot 3│......│
11//! └──────┴──────┴──────┴──────┴──────┘
12//! ```
13//!
14//! Diving deeper, we note that a [`MemoryPool`] protects Wasmtime from
15//! out-of-bounds memory accesses by inserting inaccessible guard regions
16//! between memory slots. These guard regions are configured to raise a signal
17//! if they are accessed--a WebAssembly out-of-bounds (OOB) memory access. The
18//! [`MemoryPool`] documentation has a more detailed chart but one can think of
19//! memory slots being laid out like the following:
20//!
21//! ```text
22//! ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
23//! │Guard│Mem 0│Guard│Mem 1│Guard│Mem 2│.....│Guard│
24//! └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
25//! ```
26//!
27//! But we can be more efficient about guard regions: with memory protection
28//! keys (MPK) enabled, the interleaved guard regions can be smaller. If we
29//! surround a memory with memories from other instances and each instance is
30//! protected by different protection keys, the guard region can be smaller AND
31//! the pool will still raise a signal on an OOB access. This complicates how we
32//! lay out memory slots: we must store memories from the same instance in the
33//! same "stripe". Each stripe is protected by a different protection key.
34//!
35//! This concept, dubbed [ColorGuard] in the original paper, relies on careful
36//! calculation of the memory sizes to prevent any "overlapping access" (see
37//! [`calculate`]): there are limited protection keys available (15) so the next
38//! memory using the same key must be at least as far away as the guard region
39//! we would insert otherwise. This ends up looking like the following, where a
40//! store for instance 0 (`I0`) "stripes" two memories (`M0` and `M1`) with the
41//! same protection key 1 and far enough apart to signal an OOB access:
42//!
43//! ```text
44//! ┌─────┬─────┬─────┬─────┬────────────────┬─────┬─────┬─────┐
45//! │.....│I0:M1│.....│.....│.<enough slots>.│I0:M2│.....│.....│
46//! ├─────┼─────┼─────┼─────┼────────────────┼─────┼─────┼─────┤
47//! │.....│key 1│key 2│key 3│..<more keys>...│key 1│key 2│.....│
48//! └─────┴─────┴─────┴─────┴────────────────┴─────┴─────┴─────┘
49//! ```
50//!
51//! [ColorGuard]: https://plas2022.github.io/files/pdf/SegueColorGuard.pdf
52
53use super::{
54    MemoryAllocationIndex,
55    index_allocator::{MemoryInModule, ModuleAffinityIndexAllocator, SlotId},
56};
57use crate::config::InstanceLimits;
58use crate::config::PoolingAllocationConfig;
59use crate::prelude::*;
60use crate::runtime::vm::{
61    CompiledModuleId, InstanceAllocationRequest, Memory, MemoryBase, MemoryImageSlot, Mmap,
62    MmapOffset, mmap::AlignedLength,
63};
64use crate::{
65    Enabled,
66    runtime::vm::mpk::{self, ProtectionKey, ProtectionMask},
67    vm::HostAlignedByteCount,
68};
69use std::mem;
70use std::sync::atomic::{AtomicUsize, Ordering};
71use std::sync::{Arc, Mutex};
72use wasmtime_environ::{DefinedMemoryIndex, MemoryKind, MemoryTunables, Module, Tunables};
73
74/// A set of allocator slots.
75///
76/// The allocated slots can be split by striping them: e.g., with two stripe
77/// colors 0 and 1, we would allocate all even slots using stripe 0 and all odd
78/// slots using stripe 1.
79///
80/// This is helpful for the use of protection keys: (a) if a request comes to
81/// allocate multiple instances, we can allocate them all from the same stripe
82/// and (b) if a store wants to allocate more from the same stripe it can.
83#[derive(Debug)]
84struct Stripe {
85    allocator: ModuleAffinityIndexAllocator,
86    pkey: Option<ProtectionKey>,
87}
88
89/// Represents a pool of WebAssembly linear memories.
90///
91/// A linear memory is divided into accessible pages and guard pages. A memory
92/// pool contains linear memories: each memory occupies a slot in an
93/// allocated slab (i.e., `mapping`):
94///
95/// ```text
96///          layout.max_memory_bytes                 layout.slot_bytes
97///                    |                                   |
98///              ◄─────┴────►                  ◄───────────┴──────────►
99/// ┌───────────┬────────────┬───────────┐     ┌───────────┬───────────┬───────────┐
100/// | PROT_NONE |            | PROT_NONE | ... |           | PROT_NONE | PROT_NONE |
101/// └───────────┴────────────┴───────────┘     └───────────┴───────────┴───────────┘
102/// |           |◄──────────────────┬─────────────────────────────────► ◄────┬────►
103/// |           |                   |                                        |
104/// mapping     |            `layout.num_slots` memories         layout.post_slab_guard_size
105///             |
106///   layout.pre_slab_guard_size
107/// ```
108#[derive(Debug)]
109pub struct MemoryPool {
110    mapping: Arc<Mmap<AlignedLength>>,
111    /// This memory pool is stripe-aware. If using  memory protection keys, this
112    /// will contain one stripe per available key; otherwise, a single stripe
113    /// with an empty key.
114    stripes: Vec<Stripe>,
115
116    /// If using a copy-on-write allocation scheme, the slot management. We
117    /// dynamically transfer ownership of a slot to a Memory when in use.
118    image_slots: Vec<Mutex<ImageSlot>>,
119
120    /// A description of the various memory sizes used in allocating the
121    /// `mapping` slab.
122    layout: SlabLayout,
123
124    /// The maximum number of memories that a single core module instance may
125    /// use.
126    ///
127    /// NB: this is needed for validation but does not affect the pool's size.
128    memories_per_instance: usize,
129
130    /// How much linear memory, in bytes, to keep resident after resetting for
131    /// use with the next instance. This much memory will be `memset` to zero
132    /// when a linear memory is deallocated.
133    ///
134    /// Memory exceeding this amount in the wasm linear memory will be released
135    /// with `madvise` back to the kernel.
136    ///
137    /// Only applicable on Linux.
138    pub(super) keep_resident: HostAlignedByteCount,
139
140    /// Keep track of protection keys handed out to initialized stores; this
141    /// allows us to round-robin the assignment of stores to stripes.
142    next_available_pkey: AtomicUsize,
143}
144
145/// The state of memory for each slot in this pool.
146#[derive(Debug)]
147enum ImageSlot {
148    /// This slot is guaranteed to be entirely unmapped.
149    ///
150    /// This is the initial state of all slots.
151    Unmapped,
152
153    /// The state of this slot is unknown.
154    ///
155    /// This encompasses a number of situations such as:
156    ///
157    /// * The slot is currently in use.
158    /// * The slot was attempted to be in use, but allocation failed.
159    /// * The slot was used but not deallocated properly.
160    ///
161    /// All of these situations are lumped into this one variant indicating
162    /// that, at a base level, no knowledge is known about this slot. Using a
163    /// slot in this state first requires resetting all memory in this slot by
164    /// mapping anonymous memory on top of the entire slot.
165    Unknown,
166
167    /// This slot was previously used and `MemoryImageSlot` maintains the state
168    /// about what this slot was last configured as.
169    ///
170    /// Future use of this slot will use `MemoryImageSlot` to continue to
171    /// re-instantiate and reuse images and such. This state is entered after
172    /// and allocated slot is successfully deallocated.
173    PreviouslyUsed(MemoryImageSlot),
174}
175
176impl MemoryPool {
177    /// Create a new `MemoryPool`.
178    pub fn new(config: &PoolingAllocationConfig, tunables: &Tunables) -> Result<Self> {
179        if u64::try_from(config.limits.max_memory_size).unwrap() > tunables.memory_reservation {
180            bail!(
181                "maximum memory size of {:#x} bytes exceeds the configured \
182                 memory reservation of {:#x} bytes",
183                config.limits.max_memory_size,
184                tunables.memory_reservation
185            );
186        }
187        let pkeys = match config.memory_protection_keys {
188            Enabled::Auto => {
189                if mpk::is_supported() {
190                    mpk::keys(config.max_memory_protection_keys)
191                } else {
192                    &[]
193                }
194            }
195            Enabled::Yes => {
196                if mpk::is_supported() {
197                    mpk::keys(config.max_memory_protection_keys)
198                } else {
199                    bail!("mpk is disabled on this system")
200                }
201            }
202            Enabled::No => &[],
203        };
204
205        // This is a tricky bit of global state: when creating a memory pool
206        // that uses memory protection keys, we ensure here that any host code
207        // will have access to all keys (i.e., stripes). It's only when we enter
208        // the WebAssembly guest code (see `StoreInner::call_hook`) that we
209        // enforce which keys/stripes can be accessed. Be forewarned about the
210        // assumptions here:
211        // - we expect this "allow all" configuration to reset the default
212        //   process state (only allow key 0) _before_ any memories are accessed
213        // - and we expect no other code (e.g., host-side code) to modify this
214        //   global MPK configuration
215        if !pkeys.is_empty() {
216            mpk::allow(ProtectionMask::all());
217        }
218
219        // Create a slab layout and allocate it as a completely inaccessible
220        // region to start--`PROT_NONE`.
221        let constraints = SlabConstraints::new(&config.limits, tunables, pkeys.len())?;
222        let layout = calculate(&constraints)?;
223        log::debug!(
224            "creating memory pool: {constraints:?} -> {layout:?} (total: {})",
225            layout.total_slab_bytes()?
226        );
227        let mut mapping =
228            Mmap::accessible_reserved(HostAlignedByteCount::ZERO, layout.total_slab_bytes()?)
229                .context("failed to create memory pool mapping")?;
230
231        // Then, stripe the memory with the available protection keys. This is
232        // unnecessary if there is only one stripe color.
233        if layout.num_stripes >= 2 {
234            let mut cursor = layout.pre_slab_guard_bytes;
235            let pkeys = &pkeys[..layout.num_stripes];
236            for i in 0..constraints.num_slots {
237                let pkey = &pkeys[i % pkeys.len()];
238                let region = unsafe {
239                    mapping.slice_mut(
240                        cursor.byte_count()..cursor.byte_count() + layout.slot_bytes.byte_count(),
241                    )
242                };
243                pkey.protect(region)?;
244                cursor = cursor
245                    .checked_add(layout.slot_bytes)
246                    .context("cursor + slot_bytes overflows")?;
247            }
248            debug_assert_eq!(
249                cursor
250                    .checked_add(layout.post_slab_guard_bytes)
251                    .context("cursor + post_slab_guard_bytes overflows")?,
252                layout.total_slab_bytes()?
253            );
254        }
255
256        let image_slots: Vec<_> = std::iter::repeat_with(|| Mutex::new(ImageSlot::Unmapped))
257            .take(constraints.num_slots)
258            .collect();
259
260        let create_stripe = |i| {
261            let num_slots = constraints.num_slots / layout.num_stripes
262                + usize::from(constraints.num_slots % layout.num_stripes > i);
263            let allocator = ModuleAffinityIndexAllocator::new(
264                num_slots.try_into().unwrap(),
265                config.max_unused_warm_slots,
266            )?;
267            Ok(Stripe {
268                allocator,
269                pkey: pkeys.get(i).cloned(),
270            })
271        };
272
273        debug_assert!(layout.num_stripes > 0);
274        let stripes: Vec<_> = (0..layout.num_stripes)
275            .map(create_stripe)
276            .collect::<Result<_, OutOfMemory>>()?;
277
278        let pool = Self {
279            stripes,
280            mapping: Arc::new(mapping),
281            image_slots,
282            layout,
283            memories_per_instance: usize::try_from(config.limits.max_memories_per_module).unwrap(),
284            keep_resident: HostAlignedByteCount::new_rounded_up(
285                config.linear_memory_keep_resident,
286            )?,
287            next_available_pkey: AtomicUsize::new(0),
288        };
289
290        Ok(pool)
291    }
292
293    /// Return a protection key that stores can use for requesting new
294    pub fn next_available_pkey(&self) -> Option<ProtectionKey> {
295        let index = self.next_available_pkey.fetch_add(1, Ordering::SeqCst) % self.stripes.len();
296        debug_assert!(
297            self.stripes.len() < 2 || self.stripes[index].pkey.is_some(),
298            "if we are using stripes, we cannot have an empty protection key"
299        );
300        self.stripes[index].pkey
301    }
302
303    /// Validate whether this memory pool supports the given module.
304    pub fn validate_memories(&self, module: &Module) -> Result<()> {
305        let memories = module.num_defined_memories();
306        if memories > self.memories_per_instance {
307            bail!(
308                "defined memories count of {} exceeds the per-instance limit of {}",
309                memories,
310                self.memories_per_instance,
311            );
312        }
313
314        for (i, memory) in module.memories.iter().skip(module.num_imported_memories) {
315            self.validate_memory(memory).with_context(|| {
316                format!(
317                    "memory index {} is unsupported in this pooling allocator configuration",
318                    i.as_u32()
319                )
320            })?;
321        }
322        Ok(())
323    }
324
325    /// Validate one memory for this pool.
326    pub fn validate_memory(&self, memory: &wasmtime_environ::Memory) -> Result<()> {
327        let min = memory.minimum_byte_size().with_context(|| {
328            format!("memory has a minimum byte size that cannot be represented in a u64",)
329        })?;
330        if min > u64::try_from(self.layout.max_memory_bytes.byte_count()).unwrap() {
331            bail!(
332                "memory has a minimum byte size of {} which exceeds the limit of {} bytes",
333                min,
334                self.layout.max_memory_bytes,
335            );
336        }
337        if memory.shared {
338            // FIXME(#4244): since the pooling allocator owns the memory
339            // allocation (which is torn down with the instance), that
340            // can't be used with shared memory where threads or the host
341            // might persist the memory beyond the lifetime of the instance
342            // itself.
343            bail!("memory is shared which is not supported in the pooling allocator");
344        }
345        Ok(())
346    }
347
348    /// Are zero slots in use right now?
349    pub fn is_empty(&self) -> bool {
350        self.stripes.iter().all(|s| s.allocator.is_empty())
351    }
352
353    /// Allocate a single memory for the given instance allocation request.
354    pub async fn allocate(
355        &self,
356        request: &mut InstanceAllocationRequest<'_, '_>,
357        ty: &wasmtime_environ::Memory,
358        memory_index: Option<DefinedMemoryIndex>,
359    ) -> Result<(MemoryAllocationIndex, Memory)> {
360        let tunables = request.store.engine().tunables();
361        let memory_tunables = MemoryTunables::new(tunables, MemoryKind::LinearMemory);
362        let stripe_index = if let Some(pkey) = request.store.get_pkey() {
363            pkey.as_stripe()
364        } else {
365            debug_assert!(self.stripes.len() < 2);
366            0
367        };
368
369        let striped_allocation_index = self.stripes[stripe_index]
370            .allocator
371            .alloc(memory_index.and_then(|mem_idx| {
372                request
373                    .runtime_info
374                    .unique_id()
375                    .map(|id| MemoryInModule(id, mem_idx))
376            }))
377            .map(|slot| StripedAllocationIndex(u32::try_from(slot.index()).unwrap()))
378            .ok_or_else(|| {
379                super::PoolConcurrencyLimitError::new(
380                    self.stripes[stripe_index].allocator.len(),
381                    format!("memory stripe {stripe_index}"),
382                )
383            })?;
384        let mut guard = DeallocateIndexGuard {
385            pool: self,
386            stripe_index,
387            striped_allocation_index,
388            active: true,
389        };
390
391        let allocation_index =
392            striped_allocation_index.as_unstriped_slot_index(stripe_index, self.stripes.len());
393
394        // Double-check that the runtime requirements of the memory are
395        // satisfied by the configuration of this pooling allocator. This
396        // should be returned as an error through `validate_memory_plans`
397        // but double-check here to be sure.
398        assert!(
399            memory_tunables.reservation() + memory_tunables.guard_size()
400                <= u64::try_from(self.layout.bytes_to_next_stripe_slot().byte_count()).unwrap()
401        );
402
403        let base = self.get_base(allocation_index);
404        let base_capacity = self.layout.max_memory_bytes;
405
406        let mut slot = self.take_memory_image_slot(allocation_index)?;
407        let image = match memory_index {
408            Some(memory_index) => request.runtime_info.memory_image(memory_index)?,
409            None => None,
410        };
411        let initial_size = ty
412            .minimum_byte_size()
413            .expect("min size checked in validation");
414
415        // If instantiation fails, we can propagate the error
416        // upward and drop the slot. This will cause the Drop
417        // handler to attempt to map the range with PROT_NONE
418        // memory, to reserve the space while releasing any
419        // stale mappings. The next use of this slot will then
420        // create a new slot that will try to map over
421        // this, returning errors as well if the mapping
422        // errors persist. The unmap-on-drop is best effort;
423        // if it fails, then we can still soundly continue
424        // using the rest of the pool and allowing the rest of
425        // the process to continue, because we never perform a
426        // mmap that would leave an open space for someone
427        // else to come in and map something.
428        let initial_size = usize::try_from(initial_size).unwrap();
429        slot.instantiate(initial_size, image, ty, &memory_tunables)?;
430
431        let memory = Memory::new_static(
432            ty,
433            &memory_tunables,
434            MemoryBase::Mmap(base),
435            base_capacity.byte_count(),
436            slot,
437            request.limiter.as_deref_mut(),
438        )
439        .await?;
440        guard.active = false;
441        return Ok((allocation_index, memory));
442
443        struct DeallocateIndexGuard<'a> {
444            pool: &'a MemoryPool,
445            stripe_index: usize,
446            striped_allocation_index: StripedAllocationIndex,
447            active: bool,
448        }
449
450        impl Drop for DeallocateIndexGuard<'_> {
451            fn drop(&mut self) {
452                if !self.active {
453                    return;
454                }
455                self.pool.stripes[self.stripe_index]
456                    .allocator
457                    .free(SlotId(self.striped_allocation_index.0), 0);
458            }
459        }
460    }
461
462    /// Deallocate a previously-allocated memory.
463    ///
464    /// If `image` is `None` then the state of this memory's slot is left
465    /// unknown. Otherwise `image` is used to retain information about the state
466    /// of this slot.
467    ///
468    /// # Safety
469    ///
470    /// The memory must have been previously allocated from this pool and
471    /// assigned the given index, must currently be in an allocated state, and
472    /// must never be used again.
473    ///
474    /// The caller must have already called `clear_and_remain_ready` on the
475    /// memory's image and flushed any enqueued decommits for this memory. Note
476    /// that if `image` is `None` then this is not required.
477    pub unsafe fn deallocate(
478        &self,
479        allocation_index: MemoryAllocationIndex,
480        image: Option<MemoryImageSlot>,
481        bytes_resident: usize,
482    ) {
483        let (stripe_index, slot_id) = self.return_slot(allocation_index, image);
484        self.stripes[stripe_index]
485            .allocator
486            .free(slot_id, bytes_resident);
487    }
488
489    /// Same as [`Self::deallocate`], but for many memories at once, returning
490    /// slot indices to each stripe's index allocator under a single lock
491    /// acquisition per stripe.
492    ///
493    /// # Safety
494    ///
495    /// Same as [`Self::deallocate`].
496    pub unsafe fn deallocate_many(
497        &self,
498        items: impl Iterator<Item = (MemoryAllocationIndex, Option<MemoryImageSlot>, usize)>,
499    ) {
500        let mut per_stripe: smallvec::SmallVec<[smallvec::SmallVec<[(SlotId, usize); 8]>; 16]> = (0
501            ..self.stripes.len())
502            .map(|_| Default::default())
503            .collect();
504        for (allocation_index, image, bytes_resident) in items {
505            let (stripe_index, slot_id) = self.return_slot(allocation_index, image);
506            per_stripe[stripe_index].push((slot_id, bytes_resident));
507        }
508        for (stripe, items) in self.stripes.iter().zip(per_stripe) {
509            if !items.is_empty() {
510                stripe.allocator.free_many(items);
511            }
512        }
513    }
514
515    /// Return `allocation_index`'s image slot to the pool and translate the
516    /// index to its stripe and stripe-local slot id, on behalf of
517    /// [`Self::deallocate`] and [`Self::deallocate_many`].
518    fn return_slot(
519        &self,
520        allocation_index: MemoryAllocationIndex,
521        image: Option<MemoryImageSlot>,
522    ) -> (usize, SlotId) {
523        self.return_memory_image_slot(allocation_index, image);
524        let (stripe_index, striped_allocation_index) =
525            StripedAllocationIndex::from_unstriped_slot_index(allocation_index, self.stripes.len());
526        (stripe_index, SlotId(striped_allocation_index.0))
527    }
528
529    /// Purging everything related to `module`.
530    pub fn purge_module(&self, module: CompiledModuleId) {
531        // This primarily means clearing out all of its memory images present in
532        // the virtual address space. Go through the index allocator for slots
533        // affine to `module` and reset them, freeing up the index when we're
534        // done.
535        //
536        // Note that this is only called when the specified `module` won't be
537        // allocated further (the module is being dropped) so this shouldn't hit
538        // any sort of infinite loop since this should be the final operation
539        // working with `module`.
540        //
541        // TODO: We are given a module id, but key affinity by pair of module id
542        // and defined memory index. We are missing any defined memory index or
543        // count of how many memories the module defines here. Therefore, we
544        // probe up to the maximum number of memories per instance. This is fine
545        // because that maximum is generally relatively small. If this method
546        // somehow ever gets hot because of unnecessary probing, we should
547        // either pass in the actual number of defined memories for the given
548        // module to this method, or keep a side table of all slots that are
549        // associated with a module (not just module and memory). The latter
550        // would require care to make sure that its maintenance wouldn't be too
551        // expensive for normal allocation/free operations.
552        for (stripe_index, stripe) in self.stripes.iter().enumerate() {
553            for i in 0..self.memories_per_instance {
554                use wasmtime_environ::EntityRef;
555                let memory_index = DefinedMemoryIndex::new(i);
556                while let Some(id) = stripe
557                    .allocator
558                    .alloc_affine_and_clear_affinity(module, memory_index)
559                {
560                    // Attempt to acquire the `MemoryImageSlot` state for this
561                    // slot, and then if we have that try to remove the image,
562                    // and then if all that succeeds put the slot back in.
563                    //
564                    // If anything fails then the slot will be in an "unknown"
565                    // state which means that on next use it'll be remapped with
566                    // anonymous memory.
567                    let index = StripedAllocationIndex(id.0)
568                        .as_unstriped_slot_index(stripe_index, self.stripes.len());
569                    if let Ok(mut slot) = self.take_memory_image_slot(index) {
570                        if slot.remove_image().is_ok() {
571                            self.return_memory_image_slot(index, Some(slot));
572                        }
573                    }
574
575                    stripe.allocator.free(id, 0);
576                }
577            }
578        }
579    }
580
581    fn get_base(&self, allocation_index: MemoryAllocationIndex) -> MmapOffset {
582        assert!(allocation_index.index() < self.layout.num_slots);
583        let offset = self
584            .layout
585            .slot_bytes
586            .checked_mul(allocation_index.index())
587            .and_then(|c| c.checked_add(self.layout.pre_slab_guard_bytes))
588            .expect("slot_bytes * index + pre_slab_guard_bytes overflows");
589        self.mapping.offset(offset).expect("offset is in bounds")
590    }
591
592    /// Return the protection key that this slot's memory was striped with when
593    /// the pool was created, if any.
594    ///
595    /// This mirrors the striping performed in `new`: memory is only colored
596    /// when there are at least two stripes, and slot `i` is colored with the
597    /// `i % num_stripes`th key.
598    fn pkey_for_slot(&self, allocation_index: MemoryAllocationIndex) -> Option<ProtectionKey> {
599        if self.stripes.len() < 2 {
600            return None;
601        }
602        self.stripes[allocation_index.index() % self.stripes.len()].pkey
603    }
604
605    /// Take ownership of the given image slot.
606    ///
607    /// This method is used when a `MemoryAllocationIndex` has been allocated
608    /// and the state of the slot needs to be acquired. This will lazily
609    /// allocate a `MemoryImageSlot` which describes the current (and possibly
610    /// prior) state of the slot.
611    ///
612    /// During deallocation this structure is passed back to
613    /// `return_memory_image_slot`.
614    ///
615    /// Note that this is a fallible method because using a slot might require
616    /// resetting the memory that was previously there. This reset operation
617    /// is a fallible operation that may not succeed. If it fails then this
618    /// slot cannot be used at this time.
619    fn take_memory_image_slot(
620        &self,
621        allocation_index: MemoryAllocationIndex,
622    ) -> Result<MemoryImageSlot> {
623        let (maybe_slot, needs_reset) = {
624            let mut slot = self.image_slots[allocation_index.index()].lock().unwrap();
625            match mem::replace(&mut *slot, ImageSlot::Unknown) {
626                ImageSlot::Unmapped => (None, false),
627                ImageSlot::Unknown => (None, true),
628                ImageSlot::PreviouslyUsed(state) => (Some(state), false),
629            }
630        };
631        let mut slot = maybe_slot.unwrap_or_else(|| {
632            MemoryImageSlot::create(
633                self.get_base(allocation_index),
634                HostAlignedByteCount::ZERO,
635                self.layout.max_memory_bytes.byte_count(),
636                self.pkey_for_slot(allocation_index),
637            )
638        });
639
640        // For `Unknown` slots it means that `slot` is brand new and isn't
641        // actually tracking the state of the previous slot, so reset it
642        // entirely with anonymous memory to wipe the slate clean and start
643        // from zero. This should only happen if allocation of the previous
644        // slot failed, for example.
645        if needs_reset {
646            slot.reset_with_anon_memory()?;
647        }
648        Ok(slot)
649    }
650
651    /// Return ownership of the given image slot.
652    ///
653    /// If `slot` is not provided then it's reset with `Unknown` meaning a
654    /// future allocation will need to pave over it to use it.
655    fn return_memory_image_slot(
656        &self,
657        allocation_index: MemoryAllocationIndex,
658        slot: Option<MemoryImageSlot>,
659    ) {
660        let prev = mem::replace(
661            &mut *self.image_slots[allocation_index.index()].lock().unwrap(),
662            match slot {
663                Some(slot) => {
664                    assert!(!slot.is_dirty());
665                    ImageSlot::PreviouslyUsed(slot)
666                }
667                None => ImageSlot::Unknown,
668            },
669        );
670        assert!(matches!(prev, ImageSlot::Unknown));
671    }
672
673    pub fn unused_warm_slots(&self) -> u32 {
674        self.stripes
675            .iter()
676            .map(|i| i.allocator.unused_warm_slots())
677            .sum()
678    }
679
680    pub fn unused_bytes_resident(&self) -> usize {
681        self.stripes
682            .iter()
683            .map(|i| i.allocator.unused_bytes_resident())
684            .sum()
685    }
686}
687
688/// The index of a memory allocation within an `InstanceAllocator`.
689#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
690pub struct StripedAllocationIndex(u32);
691
692impl StripedAllocationIndex {
693    fn from_unstriped_slot_index(
694        index: MemoryAllocationIndex,
695        num_stripes: usize,
696    ) -> (usize, Self) {
697        let stripe_index = index.index() % num_stripes;
698        let num_stripes: u32 = num_stripes.try_into().unwrap();
699        let index_within_stripe = Self(index.0 / num_stripes);
700        (stripe_index, index_within_stripe)
701    }
702
703    fn as_unstriped_slot_index(self, stripe: usize, num_stripes: usize) -> MemoryAllocationIndex {
704        let num_stripes: u32 = num_stripes.try_into().unwrap();
705        let stripe: u32 = stripe.try_into().unwrap();
706        MemoryAllocationIndex(self.0 * num_stripes + stripe)
707    }
708}
709
710#[derive(Clone, Debug)]
711struct SlabConstraints {
712    /// Essentially, the `static_memory_bound`: this is an assumption that the
713    /// runtime and JIT compiler make about how much space will be guarded
714    /// between slots.
715    expected_slot_bytes: HostAlignedByteCount,
716    /// The maximum size of any memory in the pool. Always a non-zero multiple
717    /// of the page size.
718    max_memory_bytes: HostAlignedByteCount,
719    num_slots: usize,
720    num_pkeys_available: usize,
721    guard_bytes: HostAlignedByteCount,
722    guard_before_slots: bool,
723}
724
725impl SlabConstraints {
726    fn new(
727        limits: &InstanceLimits,
728        tunables: &Tunables,
729        num_pkeys_available: usize,
730    ) -> Result<Self> {
731        // `memory_reservation` is the configured number of bytes for a
732        // static memory slot (see `Config::memory_reservation`); even
733        // if the memory never grows to this size (e.g., it has a lower memory
734        // maximum), codegen will assume that this unused memory is mapped
735        // `PROT_NONE`. Typically `memory_reservation` is 4GiB which helps
736        // elide most bounds checks. `MemoryPool` must respect this bound,
737        // though not explicitly: if we can achieve the same effect via
738        // MPK-protected stripes, the slot size can be lower than the
739        // `memory_reservation`.
740        let expected_slot_bytes =
741            HostAlignedByteCount::new_rounded_up_u64(tunables.memory_reservation)
742                .context("memory reservation is too large")?;
743
744        // Page-align the maximum size of memory since that's the granularity that
745        // permissions are going to be controlled at.
746        let max_memory_bytes = HostAlignedByteCount::new_rounded_up(limits.max_memory_size)
747            .context("maximum size of memory is too large")?;
748
749        let guard_bytes = HostAlignedByteCount::new_rounded_up_u64(tunables.memory_guard_size)
750            .context("guard region is too large")?;
751
752        let num_slots = usize::try_from(limits.total_memories).context("too many memories")?;
753
754        let constraints = SlabConstraints {
755            max_memory_bytes,
756            num_slots,
757            expected_slot_bytes,
758            num_pkeys_available,
759            guard_bytes,
760            guard_before_slots: tunables.guard_before_linear_memory,
761        };
762        Ok(constraints)
763    }
764}
765
766#[derive(Debug)]
767struct SlabLayout {
768    /// The total number of slots available in the memory pool slab.
769    num_slots: usize,
770    /// The size of each slot in the memory pool; this contains the maximum
771    /// memory size (i.e., from WebAssembly or Wasmtime configuration) plus any
772    /// guard region after the memory to catch OOB access. On these guard
773    /// regions, note that:
774    /// - users can configure how aggressively (or not) to elide bounds checks
775    ///   via `Config::memory_guard_size` (see also:
776    ///   `memory_and_guard_size`)
777    /// - memory protection keys can compress the size of the guard region by
778    ///   placing slots from a different key (i.e., a stripe) in the guard
779    ///   region; this means the slot itself can be smaller and we can allocate
780    ///   more of them.
781    slot_bytes: HostAlignedByteCount,
782    /// The maximum size that can become accessible, in bytes, for each linear
783    /// memory. Guaranteed to be a whole number of Wasm pages.
784    max_memory_bytes: HostAlignedByteCount,
785    /// If necessary, the number of bytes to reserve as a guard region at the
786    /// beginning of the slab.
787    pre_slab_guard_bytes: HostAlignedByteCount,
788    /// Like `pre_slab_guard_bytes`, but at the end of the slab.
789    post_slab_guard_bytes: HostAlignedByteCount,
790    /// The number of stripes needed in the slab layout.
791    num_stripes: usize,
792}
793
794impl SlabLayout {
795    /// Return the total size of the slab, using the final layout (where `n =
796    /// num_slots`):
797    ///
798    /// ```text
799    /// ┌────────────────────┬──────┬──────┬───┬──────┬─────────────────────┐
800    /// │pre_slab_guard_bytes│slot 1│slot 2│...│slot n│post_slab_guard_bytes│
801    /// └────────────────────┴──────┴──────┴───┴──────┴─────────────────────┘
802    /// ```
803    fn total_slab_bytes(&self) -> Result<HostAlignedByteCount> {
804        self.slot_bytes
805            .checked_mul(self.num_slots)
806            .and_then(|c| c.checked_add(self.pre_slab_guard_bytes))
807            .and_then(|c| c.checked_add(self.post_slab_guard_bytes))
808            .context("total size of memory reservation exceeds addressable memory")
809    }
810
811    /// Returns the number of Wasm bytes from the beginning of one slot to the
812    /// next slot in the same stripe--this is the striped equivalent of
813    /// `static_memory_bound`. Recall that between slots of the same stripe we
814    /// will see a slot from every other stripe.
815    ///
816    /// For example, in a 3-stripe pool, this function measures the distance
817    /// from the beginning of slot 1 to slot 4, which are of the same stripe:
818    ///
819    /// ```text
820    ///  ◄────────────────────►
821    /// ┌────────┬──────┬──────┬────────┬───┐
822    /// │*slot 1*│slot 2│slot 3│*slot 4*│...|
823    /// └────────┴──────┴──────┴────────┴───┘
824    /// ```
825    fn bytes_to_next_stripe_slot(&self) -> HostAlignedByteCount {
826        self.slot_bytes
827            .checked_mul(self.num_stripes)
828            .expect("constructor checks that self.slot_bytes * self.num_stripes is in bounds")
829    }
830}
831
832fn calculate(constraints: &SlabConstraints) -> Result<SlabLayout> {
833    let SlabConstraints {
834        max_memory_bytes,
835        num_slots,
836        expected_slot_bytes,
837        num_pkeys_available,
838        guard_bytes,
839        guard_before_slots,
840    } = *constraints;
841
842    // If the user specifies a guard region, we always need to allocate a
843    // `PROT_NONE` region for it before any memory slots. Recall that we can
844    // avoid bounds checks for loads and stores with immediates up to
845    // `guard_bytes`, but we rely on Wasmtime to emit bounds checks for any
846    // accesses greater than this.
847    let pre_slab_guard_bytes = if guard_before_slots {
848        guard_bytes
849    } else {
850        HostAlignedByteCount::ZERO
851    };
852
853    // To calculate the slot size, we start with the default configured size and
854    // attempt to chip away at this via MPK protection. Note here how we begin
855    // to define a slot as "all of the memory and guard region."
856    let faulting_region_bytes = expected_slot_bytes
857        .max(max_memory_bytes)
858        .checked_add(guard_bytes)
859        .context("faulting region is too large")?;
860
861    let (num_stripes, slot_bytes) = if guard_bytes == 0 || max_memory_bytes == 0 || num_slots == 0 {
862        // In the uncommon case where the memory/guard regions are empty or we don't need any slots , we
863        // will not need any stripes: we just lay out the slots back-to-back
864        // using a single stripe.
865        (1, faulting_region_bytes.byte_count())
866    } else if num_pkeys_available < 2 {
867        // If we do not have enough protection keys to stripe the memory, we do
868        // the same. We can't elide any of the guard bytes because we aren't
869        // overlapping guard regions with other stripes...
870        (1, faulting_region_bytes.byte_count())
871    } else {
872        // ...but if we can create at least two stripes, we can use another
873        // stripe (i.e., a different pkey) as this slot's guard region--this
874        // reduces the guard bytes each slot has to allocate. We must make
875        // sure, though, that if the size of that other stripe(s) does not
876        // fully cover `guard_bytes`, we keep those around to prevent OOB
877        // access.
878
879        // We first calculate the number of stripes we need: we want to
880        // minimize this so that there is less chance of a single store
881        // running out of slots with its stripe--we need at least two,
882        // though. But this is not just an optimization; we need to handle
883        // the case when there are fewer slots than stripes. E.g., if our
884        // pool is configured with only three slots (`num_memory_slots =
885        // 3`), we will run into failures if we attempt to set up more than
886        // three stripes.
887        let needed_num_stripes = faulting_region_bytes
888            .checked_div(max_memory_bytes)
889            .expect("if condition above implies max_memory_bytes is non-zero")
890            + usize::from(
891                faulting_region_bytes
892                    .checked_rem(max_memory_bytes)
893                    .expect("if condition above implies max_memory_bytes is non-zero")
894                    != 0,
895            );
896        assert!(needed_num_stripes > 0);
897        let num_stripes = num_pkeys_available.min(needed_num_stripes).min(num_slots);
898
899        // Next, we try to reduce the slot size by "overlapping" the stripes: we
900        // can make slot `n` smaller since we know that slot `n+1` and following
901        // are in different stripes and will look just like `PROT_NONE` memory.
902        // Recall that codegen expects a guarantee that at least
903        // `faulting_region_bytes` will catch OOB accesses via segfaults.
904        let needed_slot_bytes = faulting_region_bytes
905            .byte_count()
906            .checked_div(num_stripes)
907            .unwrap_or(faulting_region_bytes.byte_count())
908            .max(max_memory_bytes.byte_count());
909        assert!(needed_slot_bytes >= max_memory_bytes.byte_count());
910
911        (num_stripes, needed_slot_bytes)
912    };
913
914    // The page-aligned slot size; equivalent to `memory_and_guard_size`.
915    let slot_bytes =
916        HostAlignedByteCount::new_rounded_up(slot_bytes).context("slot size is too large")?;
917
918    // We may need another guard region (like `pre_slab_guard_bytes`) at the end
919    // of our slab to maintain our `faulting_region_bytes` guarantee. We could
920    // be conservative and just create it as large as `faulting_region_bytes`,
921    // but because we know that the last slot's `slot_bytes` make up the first
922    // part of that region, we reduce the final guard region by that much.
923    let post_slab_guard_bytes = faulting_region_bytes.saturating_sub(slot_bytes);
924
925    // Check that we haven't exceeded the slab we can calculate given the limits
926    // of `usize`.
927    let layout = SlabLayout {
928        num_slots,
929        slot_bytes,
930        max_memory_bytes,
931        pre_slab_guard_bytes,
932        post_slab_guard_bytes,
933        num_stripes,
934    };
935    match layout.total_slab_bytes() {
936        Ok(_) => Ok(layout),
937        Err(e) => Err(e),
938    }
939}
940
941#[cfg(test)]
942mod tests {
943    use super::*;
944    use proptest::prelude::*;
945
946    const WASM_PAGE_SIZE: u32 = wasmtime_environ::Memory::DEFAULT_PAGE_SIZE;
947
948    #[cfg(target_pointer_width = "64")]
949    #[test]
950    fn test_memory_pool() -> Result<()> {
951        let pool = MemoryPool::new(
952            &PoolingAllocationConfig {
953                limits: InstanceLimits {
954                    total_memories: 5,
955                    max_tables_per_module: 0,
956                    max_memories_per_module: 3,
957                    table_elements: 0,
958                    max_memory_size: WASM_PAGE_SIZE as usize,
959                    ..Default::default()
960                },
961                ..Default::default()
962            },
963            &Tunables {
964                memory_reservation: WASM_PAGE_SIZE as u64,
965                memory_guard_size: 0,
966                ..Tunables::default_host()
967            },
968        )?;
969
970        assert_eq!(pool.layout.slot_bytes, WASM_PAGE_SIZE as usize);
971        assert_eq!(pool.layout.num_slots, 5);
972        assert_eq!(pool.layout.max_memory_bytes, WASM_PAGE_SIZE as usize);
973
974        let base = pool.mapping.as_ptr() as usize;
975
976        for i in 0..5 {
977            let index = MemoryAllocationIndex(i);
978            let ptr = pool.get_base(index).as_mut_ptr();
979            assert_eq!(
980                ptr as usize - base,
981                i as usize * pool.layout.slot_bytes.byte_count()
982            );
983        }
984
985        Ok(())
986    }
987
988    #[test]
989    #[cfg_attr(miri, ignore)]
990    fn test_pooling_allocator_striping() {
991        if !mpk::is_supported() {
992            println!("skipping `test_pooling_allocator_striping` test; mpk is not supported");
993            return;
994        }
995
996        // Force the use of MPK.
997        let config = PoolingAllocationConfig {
998            memory_protection_keys: Enabled::Yes,
999            ..PoolingAllocationConfig::default()
1000        };
1001        let pool = MemoryPool::new(&config, &Tunables::default_host()).unwrap();
1002        assert!(pool.stripes.len() >= 2);
1003
1004        let max_memory_slots = config.limits.total_memories;
1005        dbg!(pool.stripes[0].allocator.num_empty_slots());
1006        dbg!(pool.stripes[1].allocator.num_empty_slots());
1007        let available_memory_slots: usize = pool
1008            .stripes
1009            .iter()
1010            .map(|s| s.allocator.num_empty_slots())
1011            .sum();
1012        assert_eq!(
1013            max_memory_slots,
1014            u32::try_from(available_memory_slots).unwrap()
1015        );
1016    }
1017
1018    #[test]
1019    fn check_known_layout_calculations() {
1020        for num_pkeys_available in 0..16 {
1021            for num_memory_slots in [0, 1, 10, 64] {
1022                for expected_slot_bytes in [0, 1 << 30 /* 1GB */, 4 << 30 /* 4GB */] {
1023                    let expected_slot_bytes =
1024                        HostAlignedByteCount::new(expected_slot_bytes).unwrap();
1025                    for max_memory_bytes in
1026                        [0, 1 * WASM_PAGE_SIZE as usize, 10 * WASM_PAGE_SIZE as usize]
1027                    {
1028                        // Note new rather than new_rounded_up here -- for now,
1029                        // WASM_PAGE_SIZE is 64KiB, which is a multiple of the
1030                        // host page size on all platforms.
1031                        let max_memory_bytes = HostAlignedByteCount::new(max_memory_bytes).unwrap();
1032                        for guard_bytes in [0, 2 << 30 /* 2GB */] {
1033                            let guard_bytes = HostAlignedByteCount::new(guard_bytes).unwrap();
1034                            for guard_before_slots in [true, false] {
1035                                let constraints = SlabConstraints {
1036                                    max_memory_bytes,
1037                                    num_slots: num_memory_slots,
1038                                    expected_slot_bytes,
1039                                    num_pkeys_available,
1040                                    guard_bytes,
1041                                    guard_before_slots,
1042                                };
1043                                match calculate(&constraints) {
1044                                    Ok(layout) => {
1045                                        assert_slab_layout_invariants(constraints, layout)
1046                                    }
1047                                    Err(e) => {
1048                                        // Only allow failure on 32-bit
1049                                        // platforms where the calculation
1050                                        // exceeded the size of the address
1051                                        // space
1052                                        assert!(
1053                                            cfg!(target_pointer_width = "32")
1054                                                && e.to_string()
1055                                                    .contains("exceeds addressable memory"),
1056                                            "bad error: {e:?}"
1057                                        );
1058                                    }
1059                                }
1060                            }
1061                        }
1062                    }
1063                }
1064            }
1065        }
1066    }
1067
1068    proptest! {
1069        #[test]
1070        #[cfg_attr(miri, ignore)]
1071        fn check_random_layout_calculations(c in constraints()) {
1072            if let Ok(l) = calculate(&c) {
1073                assert_slab_layout_invariants(c, l);
1074            }
1075        }
1076    }
1077
1078    fn constraints() -> impl Strategy<Value = SlabConstraints> {
1079        (
1080            any::<HostAlignedByteCount>(),
1081            any::<usize>(),
1082            any::<HostAlignedByteCount>(),
1083            any::<usize>(),
1084            any::<HostAlignedByteCount>(),
1085            any::<bool>(),
1086        )
1087            .prop_map(
1088                |(
1089                    max_memory_bytes,
1090                    num_memory_slots,
1091                    expected_slot_bytes,
1092                    num_pkeys_available,
1093                    guard_bytes,
1094                    guard_before_slots,
1095                )| {
1096                    SlabConstraints {
1097                        max_memory_bytes,
1098                        num_slots: num_memory_slots,
1099                        expected_slot_bytes,
1100                        num_pkeys_available,
1101                        guard_bytes,
1102                        guard_before_slots,
1103                    }
1104                },
1105            )
1106    }
1107
1108    fn assert_slab_layout_invariants(c: SlabConstraints, s: SlabLayout) {
1109        // Check that all the sizes add up.
1110        assert_eq!(
1111            s.total_slab_bytes().unwrap(),
1112            s.pre_slab_guard_bytes
1113                .checked_add(s.slot_bytes.checked_mul(c.num_slots).unwrap())
1114                .and_then(|c| c.checked_add(s.post_slab_guard_bytes))
1115                .unwrap(),
1116            "the slab size does not add up: {c:?} => {s:?}"
1117        );
1118        assert!(
1119            s.slot_bytes >= s.max_memory_bytes,
1120            "slot is not big enough: {c:?} => {s:?}"
1121        );
1122
1123        // The HostAlignedByteCount newtype wrapper ensures that the various
1124        // byte values are page-aligned.
1125
1126        // Check that we use no more or less stripes than needed.
1127        assert!(s.num_stripes >= 1, "not enough stripes: {c:?} => {s:?}");
1128        if c.num_pkeys_available == 0 || c.num_slots == 0 {
1129            assert_eq!(
1130                s.num_stripes, 1,
1131                "expected at least one stripe: {c:?} => {s:?}"
1132            );
1133        } else {
1134            assert!(
1135                s.num_stripes <= c.num_pkeys_available,
1136                "layout has more stripes than available pkeys: {c:?} => {s:?}"
1137            );
1138            assert!(
1139                s.num_stripes <= c.num_slots,
1140                "layout has more stripes than memory slots: {c:?} => {s:?}"
1141            );
1142        }
1143
1144        // Check that we use the minimum number of stripes/protection keys.
1145        // - if the next MPK-protected slot is bigger or the same as the
1146        //   required guard region, we only need two stripes
1147        // - if the next slot is smaller than the guard region, we only need
1148        //   enough stripes to add up to at least that guard region size.
1149        if c.num_pkeys_available > 1 && !c.max_memory_bytes.is_zero() {
1150            assert!(
1151                s.num_stripes <= (c.guard_bytes.checked_div(c.max_memory_bytes).unwrap() + 2),
1152                "calculated more stripes than needed: {c:?} => {s:?}"
1153            );
1154        }
1155
1156        // Check that the memory-striping will not allow OOB access.
1157        // - we may have reduced the slot size from `expected_slot_bytes` to
1158        //   `slot_bytes` assuming MPK striping; we check that our guaranteed
1159        //   "faulting region" is respected
1160        // - the last slot won't have MPK striping after it; we check that the
1161        //   `post_slab_guard_bytes` accounts for this
1162        assert!(
1163            s.bytes_to_next_stripe_slot()
1164                >= c.expected_slot_bytes
1165                    .max(c.max_memory_bytes)
1166                    .checked_add(c.guard_bytes)
1167                    .unwrap(),
1168            "faulting region not large enough: {c:?} => {s:?}"
1169        );
1170        assert!(
1171            s.slot_bytes.checked_add(s.post_slab_guard_bytes).unwrap() >= c.expected_slot_bytes,
1172            "last slot may allow OOB access: {c:?} => {s:?}"
1173        );
1174    }
1175}