wasmtime_runtime/instance/allocator/
pooling.rs

1//! Implements the pooling instance allocator.
2//!
3//! The pooling instance allocator maps memory in advance
4//! and allocates instances, memories, tables, and stacks from
5//! a pool of available resources.
6//!
7//! Using the pooling instance allocator can speed up module instantiation
8//! when modules can be constrained based on configurable limits.
9
10use super::{InstanceAllocationRequest, InstanceAllocator};
11use crate::{instance::Instance, Memory, Mmap, Table};
12use crate::{CompiledModuleId, MemoryImageSlot};
13use anyhow::{anyhow, bail, Context, Result};
14use libc::c_void;
15use std::convert::TryFrom;
16use std::mem;
17use std::sync::Mutex;
18use wasmtime_environ::{
19    DefinedMemoryIndex, DefinedTableIndex, HostPtr, MemoryStyle, Module, PrimaryMap, Tunables,
20    VMOffsets, WASM_PAGE_SIZE,
21};
22
23mod index_allocator;
24use index_allocator::{IndexAllocator, SlotId};
25
26cfg_if::cfg_if! {
27    if #[cfg(windows)] {
28        mod windows;
29        use windows as imp;
30    } else {
31        mod unix;
32        use unix as imp;
33    }
34}
35
36use imp::{commit_table_pages, decommit_table_pages};
37
38#[cfg(all(feature = "async", unix))]
39use imp::{commit_stack_pages, reset_stack_pages_to_zero};
40
41fn round_up_to_pow2(n: usize, to: usize) -> usize {
42    debug_assert!(to > 0);
43    debug_assert!(to.is_power_of_two());
44    (n + to - 1) & !(to - 1)
45}
46
47/// Instance-related limit configuration for pooling.
48///
49/// More docs on this can be found at `wasmtime::PoolingAllocationConfig`.
50#[derive(Debug, Copy, Clone)]
51pub struct InstanceLimits {
52    /// Maximum instances to support
53    pub count: u32,
54
55    /// Maximum size of instance VMContext
56    pub size: usize,
57
58    /// Maximum number of tables per instance
59    pub tables: u32,
60
61    /// Maximum number of table elements per table
62    pub table_elements: u32,
63
64    /// Maximum number of linear memories per instance
65    pub memories: u32,
66
67    /// Maximum number of wasm pages for each linear memory.
68    pub memory_pages: u64,
69}
70
71impl Default for InstanceLimits {
72    fn default() -> Self {
73        // See doc comments for `wasmtime::PoolingAllocationConfig` for these
74        // default values
75        Self {
76            count: 1000,
77            size: 1 << 20, // 1 MB
78            tables: 1,
79            table_elements: 10_000,
80            memories: 1,
81            memory_pages: 160,
82        }
83    }
84}
85
86/// Represents a pool of WebAssembly linear memories.
87///
88/// A linear memory is divided into accessible pages and guard pages.
89///
90/// Each instance index into the pool returns an iterator over the base
91/// addresses of the instance's linear memories.
92///
93/// A diagram for this struct's fields is:
94///
95/// ```ignore
96///                       memory_size
97///                           /
98///         max_accessible   /                    memory_and_guard_size
99///                 |       /                               |
100///              <--+--->  /                    <-----------+---------->
101///              <--------+->
102///
103/// +-----------+--------+---+-----------+     +--------+---+-----------+
104/// | PROT_NONE |            | PROT_NONE | ... |            | PROT_NONE |
105/// +-----------+--------+---+-----------+     +--------+---+-----------+
106/// |           |<------------------+---------------------------------->
107/// \           |                    \
108/// mapping     |     `max_instances * max_memories` memories
109///            /
110///    initial_memory_offset
111/// ```
112#[derive(Debug)]
113struct MemoryPool {
114    mapping: Mmap,
115    // If using a copy-on-write allocation scheme, the slot management. We
116    // dynamically transfer ownership of a slot to a Memory when in
117    // use.
118    image_slots: Vec<Mutex<Option<MemoryImageSlot>>>,
119    // The size, in bytes, of each linear memory's reservation, not including
120    // any guard region.
121    memory_size: usize,
122    // The size, in bytes, of each linear memory's reservation plus the trailing
123    // guard region allocated for it.
124    memory_and_guard_size: usize,
125    // The maximum size that can become accessible, in bytes, of each linear
126    // memory. Guaranteed to be a whole number of wasm pages.
127    max_accessible: usize,
128    // The size, in bytes, of the offset to the first linear memory in this
129    // pool. This is here to help account for the first region of guard pages,
130    // if desired, before the first linear memory.
131    initial_memory_offset: usize,
132    max_memories: usize,
133    max_instances: usize,
134}
135
136impl MemoryPool {
137    fn new(instance_limits: &InstanceLimits, tunables: &Tunables) -> Result<Self> {
138        // The maximum module memory page count cannot exceed 65536 pages
139        if instance_limits.memory_pages > 0x10000 {
140            bail!(
141                "module memory page limit of {} exceeds the maximum of 65536",
142                instance_limits.memory_pages
143            );
144        }
145
146        // Interpret the larger of the maximal size of memory or the static
147        // memory bound as the size of the virtual address space reservation for
148        // memory itself. Typically `static_memory_bound` is 4G which helps
149        // elide most bounds checks in wasm. If `memory_pages` is larger,
150        // though, then this is a non-moving pooling allocator so create larger
151        // reservations for account for that.
152        let memory_size = instance_limits
153            .memory_pages
154            .max(tunables.static_memory_bound)
155            * u64::from(WASM_PAGE_SIZE);
156
157        let memory_and_guard_size =
158            usize::try_from(memory_size + tunables.static_memory_offset_guard_size)
159                .map_err(|_| anyhow!("memory reservation size exceeds addressable memory"))?;
160
161        assert!(
162            memory_and_guard_size % crate::page_size() == 0,
163            "memory size {} is not a multiple of system page size",
164            memory_and_guard_size
165        );
166
167        let max_instances = instance_limits.count as usize;
168        let max_memories = instance_limits.memories as usize;
169        let initial_memory_offset = if tunables.guard_before_linear_memory {
170            usize::try_from(tunables.static_memory_offset_guard_size).unwrap()
171        } else {
172            0
173        };
174
175        // The entire allocation here is the size of each memory times the
176        // max memories per instance times the number of instances allowed in
177        // this pool, plus guard regions.
178        //
179        // Note, though, that guard regions are required to be after each linear
180        // memory. If the `guard_before_linear_memory` setting is specified,
181        // then due to the contiguous layout of linear memories the guard pages
182        // after one memory are also guard pages preceding the next linear
183        // memory. This means that we only need to handle pre-guard-page sizes
184        // specially for the first linear memory, hence the
185        // `initial_memory_offset` variable here. If guards aren't specified
186        // before linear memories this is set to `0`, otherwise it's set to
187        // the same size as guard regions for other memories.
188        let allocation_size = memory_and_guard_size
189            .checked_mul(max_memories)
190            .and_then(|c| c.checked_mul(max_instances))
191            .and_then(|c| c.checked_add(initial_memory_offset))
192            .ok_or_else(|| {
193                anyhow!("total size of memory reservation exceeds addressable memory")
194            })?;
195
196        // Create a completely inaccessible region to start
197        let mapping = Mmap::accessible_reserved(0, allocation_size)
198            .context("failed to create memory pool mapping")?;
199
200        let num_image_slots = max_instances * max_memories;
201        let image_slots: Vec<_> = std::iter::repeat_with(|| Mutex::new(None))
202            .take(num_image_slots)
203            .collect();
204
205        let pool = Self {
206            mapping,
207            image_slots,
208            memory_size: memory_size.try_into().unwrap(),
209            memory_and_guard_size,
210            initial_memory_offset,
211            max_memories,
212            max_instances,
213            max_accessible: (instance_limits.memory_pages as usize) * (WASM_PAGE_SIZE as usize),
214        };
215
216        Ok(pool)
217    }
218
219    fn get_base(&self, instance_index: usize, memory_index: DefinedMemoryIndex) -> *mut u8 {
220        assert!(instance_index < self.max_instances);
221        let memory_index = memory_index.as_u32() as usize;
222        assert!(memory_index < self.max_memories);
223        let idx = instance_index * self.max_memories + memory_index;
224        let offset = self.initial_memory_offset + idx * self.memory_and_guard_size;
225        unsafe { self.mapping.as_mut_ptr().offset(offset as isize) }
226    }
227
228    #[cfg(test)]
229    fn get<'a>(&'a self, instance_index: usize) -> impl Iterator<Item = *mut u8> + 'a {
230        (0..self.max_memories)
231            .map(move |i| self.get_base(instance_index, DefinedMemoryIndex::from_u32(i as u32)))
232    }
233
234    /// Take ownership of the given image slot. Must be returned via
235    /// `return_memory_image_slot` when the instance is done using it.
236    fn take_memory_image_slot(
237        &self,
238        instance_index: usize,
239        memory_index: DefinedMemoryIndex,
240    ) -> MemoryImageSlot {
241        let idx = instance_index * self.max_memories + (memory_index.as_u32() as usize);
242        let maybe_slot = self.image_slots[idx].lock().unwrap().take();
243
244        maybe_slot.unwrap_or_else(|| {
245            MemoryImageSlot::create(
246                self.get_base(instance_index, memory_index) as *mut c_void,
247                0,
248                self.max_accessible,
249            )
250        })
251    }
252
253    /// Return ownership of the given image slot.
254    fn return_memory_image_slot(
255        &self,
256        instance_index: usize,
257        memory_index: DefinedMemoryIndex,
258        slot: MemoryImageSlot,
259    ) {
260        assert!(!slot.is_dirty());
261        let idx = instance_index * self.max_memories + (memory_index.as_u32() as usize);
262        *self.image_slots[idx].lock().unwrap() = Some(slot);
263    }
264
265    /// Resets all the images for the instance index slot specified to clear out
266    /// any prior mappings.
267    ///
268    /// This is used when a `Module` is dropped at the `wasmtime` layer to clear
269    /// out any remaining mappings and ensure that its memfd backing, if any, is
270    /// removed from the address space to avoid lingering references to it.
271    fn clear_images(&self, instance_index: usize) {
272        for i in 0..self.max_memories {
273            let index = DefinedMemoryIndex::from_u32(i as u32);
274
275            // Clear the image from the slot and, if successful, return it back
276            // to our state. Note that on failure here the whole slot will get
277            // paved over with an anonymous mapping.
278            let mut slot = self.take_memory_image_slot(instance_index, index);
279            if slot.remove_image().is_ok() {
280                self.return_memory_image_slot(instance_index, index, slot);
281            }
282        }
283    }
284}
285
286impl Drop for MemoryPool {
287    fn drop(&mut self) {
288        // Clear the `clear_no_drop` flag (i.e., ask to *not* clear on
289        // drop) for all slots, and then drop them here. This is
290        // valid because the one `Mmap` that covers the whole region
291        // can just do its one munmap.
292        for mut slot in std::mem::take(&mut self.image_slots) {
293            if let Some(slot) = slot.get_mut().unwrap() {
294                slot.no_clear_on_drop();
295            }
296        }
297    }
298}
299
300/// Represents a pool of WebAssembly tables.
301///
302/// Each instance index into the pool returns an iterator over the base addresses
303/// of the instance's tables.
304#[derive(Debug)]
305struct TablePool {
306    mapping: Mmap,
307    table_size: usize,
308    max_tables: usize,
309    max_instances: usize,
310    page_size: usize,
311    max_elements: u32,
312}
313
314impl TablePool {
315    fn new(instance_limits: &InstanceLimits) -> Result<Self> {
316        let page_size = crate::page_size();
317
318        let table_size = round_up_to_pow2(
319            mem::size_of::<*mut u8>()
320                .checked_mul(instance_limits.table_elements as usize)
321                .ok_or_else(|| anyhow!("table size exceeds addressable memory"))?,
322            page_size,
323        );
324
325        let max_instances = instance_limits.count as usize;
326        let max_tables = instance_limits.tables as usize;
327
328        let allocation_size = table_size
329            .checked_mul(max_tables)
330            .and_then(|c| c.checked_mul(max_instances))
331            .ok_or_else(|| anyhow!("total size of instance tables exceeds addressable memory"))?;
332
333        let mapping = Mmap::accessible_reserved(allocation_size, allocation_size)
334            .context("failed to create table pool mapping")?;
335
336        Ok(Self {
337            mapping,
338            table_size,
339            max_tables,
340            max_instances,
341            page_size,
342            max_elements: instance_limits.table_elements,
343        })
344    }
345
346    fn get(&self, instance_index: usize) -> impl Iterator<Item = *mut u8> {
347        assert!(instance_index < self.max_instances);
348
349        let base: *mut u8 = unsafe {
350            self.mapping
351                .as_mut_ptr()
352                .add(instance_index * self.table_size * self.max_tables) as _
353        };
354
355        let size = self.table_size;
356        (0..self.max_tables).map(move |i| unsafe { base.add(i * size) })
357    }
358}
359
360/// Represents a pool of execution stacks (used for the async fiber implementation).
361///
362/// Each index into the pool represents a single execution stack. The maximum number of
363/// stacks is the same as the maximum number of instances.
364///
365/// As stacks grow downwards, each stack starts (lowest address) with a guard page
366/// that can be used to detect stack overflow.
367///
368/// The top of the stack (starting stack pointer) is returned when a stack is allocated
369/// from the pool.
370#[cfg(all(feature = "async", unix))]
371#[derive(Debug)]
372struct StackPool {
373    mapping: Mmap,
374    stack_size: usize,
375    max_instances: usize,
376    page_size: usize,
377    index_allocator: IndexAllocator,
378    async_stack_zeroing: bool,
379    async_stack_keep_resident: usize,
380}
381
382#[cfg(all(feature = "async", unix))]
383impl StackPool {
384    fn new(config: &PoolingInstanceAllocatorConfig) -> Result<Self> {
385        use rustix::mm::{mprotect, MprotectFlags};
386
387        let page_size = crate::page_size();
388
389        // Add a page to the stack size for the guard page when using fiber stacks
390        let stack_size = if config.stack_size == 0 {
391            0
392        } else {
393            round_up_to_pow2(config.stack_size, page_size)
394                .checked_add(page_size)
395                .ok_or_else(|| anyhow!("stack size exceeds addressable memory"))?
396        };
397
398        let max_instances = config.limits.count as usize;
399
400        let allocation_size = stack_size
401            .checked_mul(max_instances)
402            .ok_or_else(|| anyhow!("total size of execution stacks exceeds addressable memory"))?;
403
404        let mapping = Mmap::accessible_reserved(allocation_size, allocation_size)
405            .context("failed to create stack pool mapping")?;
406
407        // Set up the stack guard pages
408        if allocation_size > 0 {
409            unsafe {
410                for i in 0..max_instances {
411                    // Make the stack guard page inaccessible
412                    let bottom_of_stack = mapping.as_mut_ptr().add(i * stack_size);
413                    mprotect(bottom_of_stack.cast(), page_size, MprotectFlags::empty())
414                        .context("failed to protect stack guard page")?;
415                }
416            }
417        }
418
419        Ok(Self {
420            mapping,
421            stack_size,
422            max_instances,
423            page_size,
424            async_stack_zeroing: config.async_stack_zeroing,
425            async_stack_keep_resident: config.async_stack_keep_resident,
426            // Note that `max_unused_warm_slots` is set to zero since stacks
427            // have no affinity so there's no need to keep intentionally unused
428            // warm slots around.
429            index_allocator: IndexAllocator::new(config.limits.count, 0),
430        })
431    }
432
433    fn allocate(&self) -> Result<wasmtime_fiber::FiberStack> {
434        if self.stack_size == 0 {
435            bail!("pooling allocator not configured to enable fiber stack allocation");
436        }
437
438        let index = self
439            .index_allocator
440            .alloc(None)
441            .ok_or_else(|| {
442                anyhow!(
443                    "maximum concurrent fiber limit of {} reached",
444                    self.max_instances
445                )
446            })?
447            .index();
448
449        assert!(index < self.max_instances);
450
451        unsafe {
452            // Remove the guard page from the size
453            let size_without_guard = self.stack_size - self.page_size;
454
455            let bottom_of_stack = self
456                .mapping
457                .as_mut_ptr()
458                .add((index * self.stack_size) + self.page_size);
459
460            commit_stack_pages(bottom_of_stack, size_without_guard)?;
461
462            let stack =
463                wasmtime_fiber::FiberStack::from_top_ptr(bottom_of_stack.add(size_without_guard))?;
464            Ok(stack)
465        }
466    }
467
468    fn deallocate(&self, stack: &wasmtime_fiber::FiberStack) {
469        let top = stack
470            .top()
471            .expect("fiber stack not allocated from the pool") as usize;
472
473        let base = self.mapping.as_ptr() as usize;
474        let len = self.mapping.len();
475        assert!(
476            top > base && top <= (base + len),
477            "fiber stack top pointer not in range"
478        );
479
480        // Remove the guard page from the size
481        let stack_size = self.stack_size - self.page_size;
482        let bottom_of_stack = top - stack_size;
483        let start_of_stack = bottom_of_stack - self.page_size;
484        assert!(start_of_stack >= base && start_of_stack < (base + len));
485        assert!((start_of_stack - base) % self.stack_size == 0);
486
487        let index = (start_of_stack - base) / self.stack_size;
488        assert!(index < self.max_instances);
489
490        if self.async_stack_zeroing {
491            self.zero_stack(bottom_of_stack, stack_size);
492        }
493
494        self.index_allocator.free(SlotId(index as u32));
495    }
496
497    fn zero_stack(&self, bottom: usize, size: usize) {
498        // Manually zero the top of the stack to keep the pages resident in
499        // memory and avoid future page faults. Use the system to deallocate
500        // pages past this. This hopefully strikes a reasonable balance between:
501        //
502        // * memset for the whole range is probably expensive
503        // * madvise for the whole range incurs expensive future page faults
504        // * most threads probably don't use most of the stack anyway
505        let size_to_memset = size.min(self.async_stack_keep_resident);
506        unsafe {
507            std::ptr::write_bytes(
508                (bottom + size - size_to_memset) as *mut u8,
509                0,
510                size_to_memset,
511            );
512        }
513
514        // Use the system to reset remaining stack pages to zero.
515        reset_stack_pages_to_zero(bottom as _, size - size_to_memset).unwrap();
516    }
517}
518
519/// Configuration options for the pooling instance allocator supplied at
520/// construction.
521#[derive(Copy, Clone, Debug)]
522pub struct PoolingInstanceAllocatorConfig {
523    /// See `PoolingAllocatorConfig::max_unused_warm_slots` in `wasmtime`
524    pub max_unused_warm_slots: u32,
525    /// The size, in bytes, of async stacks to allocate (not including the guard
526    /// page).
527    pub stack_size: usize,
528    /// The limits to apply to instances allocated within this allocator.
529    pub limits: InstanceLimits,
530    /// Whether or not async stacks are zeroed after use.
531    pub async_stack_zeroing: bool,
532    /// If async stack zeroing is enabled and the host platform is Linux this is
533    /// how much memory to zero out with `memset`.
534    ///
535    /// The rest of memory will be zeroed out with `madvise`.
536    pub async_stack_keep_resident: usize,
537    /// How much linear memory, in bytes, to keep resident after resetting for
538    /// use with the next instance. This much memory will be `memset` to zero
539    /// when a linear memory is deallocated.
540    ///
541    /// Memory exceeding this amount in the wasm linear memory will be released
542    /// with `madvise` back to the kernel.
543    ///
544    /// Only applicable on Linux.
545    pub linear_memory_keep_resident: usize,
546    /// Same as `linear_memory_keep_resident` but for tables.
547    pub table_keep_resident: usize,
548}
549
550impl Default for PoolingInstanceAllocatorConfig {
551    fn default() -> PoolingInstanceAllocatorConfig {
552        PoolingInstanceAllocatorConfig {
553            max_unused_warm_slots: 100,
554            stack_size: 2 << 20,
555            limits: InstanceLimits::default(),
556            async_stack_zeroing: false,
557            async_stack_keep_resident: 0,
558            linear_memory_keep_resident: 0,
559            table_keep_resident: 0,
560        }
561    }
562}
563
564/// Implements the pooling instance allocator.
565///
566/// This allocator internally maintains pools of instances, memories, tables, and stacks.
567///
568/// Note: the resource pools are manually dropped so that the fault handler terminates correctly.
569#[derive(Debug)]
570pub struct PoolingInstanceAllocator {
571    instance_size: usize,
572    max_instances: usize,
573    index_allocator: IndexAllocator,
574    memories: MemoryPool,
575    tables: TablePool,
576    linear_memory_keep_resident: usize,
577    table_keep_resident: usize,
578
579    #[cfg(all(feature = "async", unix))]
580    stacks: StackPool,
581    #[cfg(all(feature = "async", windows))]
582    stack_size: usize,
583}
584
585impl PoolingInstanceAllocator {
586    /// Creates a new pooling instance allocator with the given strategy and limits.
587    pub fn new(config: &PoolingInstanceAllocatorConfig, tunables: &Tunables) -> Result<Self> {
588        if config.limits.count == 0 {
589            bail!("the instance count limit cannot be zero");
590        }
591
592        let max_instances = config.limits.count as usize;
593
594        Ok(Self {
595            instance_size: round_up_to_pow2(config.limits.size, mem::align_of::<Instance>()),
596            max_instances,
597            index_allocator: IndexAllocator::new(config.limits.count, config.max_unused_warm_slots),
598            memories: MemoryPool::new(&config.limits, tunables)?,
599            tables: TablePool::new(&config.limits)?,
600            linear_memory_keep_resident: config.linear_memory_keep_resident,
601            table_keep_resident: config.table_keep_resident,
602            #[cfg(all(feature = "async", unix))]
603            stacks: StackPool::new(config)?,
604            #[cfg(all(feature = "async", windows))]
605            stack_size: config.stack_size,
606        })
607    }
608
609    fn reset_table_pages_to_zero(&self, base: *mut u8, size: usize) -> Result<()> {
610        let size_to_memset = size.min(self.table_keep_resident);
611        unsafe {
612            std::ptr::write_bytes(base, 0, size_to_memset);
613            decommit_table_pages(base.add(size_to_memset), size - size_to_memset)?;
614        }
615        Ok(())
616    }
617
618    fn validate_table_plans(&self, module: &Module) -> Result<()> {
619        let tables = module.table_plans.len() - module.num_imported_tables;
620        if tables > self.tables.max_tables {
621            bail!(
622                "defined tables count of {} exceeds the limit of {}",
623                tables,
624                self.tables.max_tables,
625            );
626        }
627
628        for (i, plan) in module.table_plans.iter().skip(module.num_imported_tables) {
629            if plan.table.minimum > self.tables.max_elements {
630                bail!(
631                    "table index {} has a minimum element size of {} which exceeds the limit of {}",
632                    i.as_u32(),
633                    plan.table.minimum,
634                    self.tables.max_elements,
635                );
636            }
637        }
638        Ok(())
639    }
640
641    fn validate_memory_plans(&self, module: &Module) -> Result<()> {
642        let memories = module.memory_plans.len() - module.num_imported_memories;
643        if memories > self.memories.max_memories {
644            bail!(
645                "defined memories count of {} exceeds the limit of {}",
646                memories,
647                self.memories.max_memories,
648            );
649        }
650
651        for (i, plan) in module
652            .memory_plans
653            .iter()
654            .skip(module.num_imported_memories)
655        {
656            match plan.style {
657                MemoryStyle::Static { bound } => {
658                    if (self.memories.memory_size as u64) < bound {
659                        bail!(
660                            "memory size allocated per-memory is too small to \
661                             satisfy static bound of {bound:#x} pages"
662                        );
663                    }
664                }
665                MemoryStyle::Dynamic { .. } => {}
666            }
667            let max = self.memories.max_accessible / (WASM_PAGE_SIZE as usize);
668            if plan.memory.minimum > (max as u64) {
669                bail!(
670                    "memory index {} has a minimum page size of {} which exceeds the limit of {}",
671                    i.as_u32(),
672                    plan.memory.minimum,
673                    max,
674                );
675            }
676        }
677        Ok(())
678    }
679
680    fn validate_instance_size(&self, offsets: &VMOffsets<HostPtr>) -> Result<()> {
681        let layout = Instance::alloc_layout(offsets);
682        if layout.size() <= self.instance_size {
683            return Ok(());
684        }
685
686        // If this `module` exceeds the allocation size allotted to it then an
687        // error will be reported here. The error of "required N bytes but
688        // cannot allocate that" is pretty opaque, however, because it's not
689        // clear what the breakdown of the N bytes are and what to optimize
690        // next. To help provide a better error message here some fancy-ish
691        // logic is done here to report the breakdown of the byte request into
692        // the largest portions and where it's coming from.
693        let mut message = format!(
694            "instance allocation for this module \
695             requires {} bytes which exceeds the configured maximum \
696             of {} bytes; breakdown of allocation requirement:\n\n",
697            layout.size(),
698            self.instance_size,
699        );
700
701        let mut remaining = layout.size();
702        let mut push = |name: &str, bytes: usize| {
703            assert!(remaining >= bytes);
704            remaining -= bytes;
705
706            // If the `name` region is more than 5% of the allocation request
707            // then report it here, otherwise ignore it. We have less than 20
708            // fields so we're guaranteed that something should be reported, and
709            // otherwise it's not particularly interesting to learn about 5
710            // different fields that are all 8 or 0 bytes. Only try to report
711            // the "major" sources of bytes here.
712            if bytes > layout.size() / 20 {
713                message.push_str(&format!(
714                    " * {:.02}% - {} bytes - {}\n",
715                    ((bytes as f32) / (layout.size() as f32)) * 100.0,
716                    bytes,
717                    name,
718                ));
719            }
720        };
721
722        // The `Instance` itself requires some size allocated to it.
723        push("instance state management", mem::size_of::<Instance>());
724
725        // Afterwards the `VMContext`'s regions are why we're requesting bytes,
726        // so ask it for descriptions on each region's byte size.
727        for (desc, size) in offsets.region_sizes() {
728            push(desc, size as usize);
729        }
730
731        // double-check we accounted for all the bytes
732        assert_eq!(remaining, 0);
733
734        bail!("{}", message)
735    }
736}
737
738unsafe impl InstanceAllocator for PoolingInstanceAllocator {
739    fn validate(&self, module: &Module, offsets: &VMOffsets<HostPtr>) -> Result<()> {
740        self.validate_memory_plans(module)?;
741        self.validate_table_plans(module)?;
742        self.validate_instance_size(offsets)?;
743
744        Ok(())
745    }
746
747    fn allocate_index(&self, req: &InstanceAllocationRequest) -> Result<usize> {
748        self.index_allocator
749            .alloc(req.runtime_info.unique_id())
750            .map(|id| id.index())
751            .ok_or_else(|| {
752                anyhow!(
753                    "maximum concurrent instance limit of {} reached",
754                    self.max_instances
755                )
756            })
757    }
758
759    fn deallocate_index(&self, index: usize) {
760        self.index_allocator.free(SlotId(index as u32));
761    }
762
763    fn allocate_memories(
764        &self,
765        index: usize,
766        req: &mut InstanceAllocationRequest,
767        memories: &mut PrimaryMap<DefinedMemoryIndex, Memory>,
768    ) -> Result<()> {
769        let module = req.runtime_info.module();
770
771        self.validate_memory_plans(module)?;
772
773        for (memory_index, plan) in module
774            .memory_plans
775            .iter()
776            .skip(module.num_imported_memories)
777        {
778            let defined_index = module
779                .defined_memory_index(memory_index)
780                .expect("should be a defined memory since we skipped imported ones");
781
782            // Double-check that the runtime requirements of the memory are
783            // satisfied by the configuration of this pooling allocator. This
784            // should be returned as an error through `validate_memory_plans`
785            // but double-check here to be sure.
786            match plan.style {
787                MemoryStyle::Static { bound } => {
788                    let bound = bound * u64::from(WASM_PAGE_SIZE);
789                    assert!(bound <= (self.memories.memory_size as u64));
790                }
791                MemoryStyle::Dynamic { .. } => {}
792            }
793
794            let memory = unsafe {
795                std::slice::from_raw_parts_mut(
796                    self.memories.get_base(index, defined_index),
797                    self.memories.max_accessible,
798                )
799            };
800
801            let mut slot = self.memories.take_memory_image_slot(index, defined_index);
802            let image = req.runtime_info.memory_image(defined_index)?;
803            let initial_size = plan.memory.minimum * WASM_PAGE_SIZE as u64;
804
805            // If instantiation fails, we can propagate the error
806            // upward and drop the slot. This will cause the Drop
807            // handler to attempt to map the range with PROT_NONE
808            // memory, to reserve the space while releasing any
809            // stale mappings. The next use of this slot will then
810            // create a new slot that will try to map over
811            // this, returning errors as well if the mapping
812            // errors persist. The unmap-on-drop is best effort;
813            // if it fails, then we can still soundly continue
814            // using the rest of the pool and allowing the rest of
815            // the process to continue, because we never perform a
816            // mmap that would leave an open space for someone
817            // else to come in and map something.
818            slot.instantiate(initial_size as usize, image, &plan)?;
819
820            memories.push(Memory::new_static(
821                plan,
822                memory,
823                slot,
824                self.memories.memory_and_guard_size,
825                unsafe { &mut *req.store.get().unwrap() },
826            )?);
827        }
828
829        Ok(())
830    }
831
832    fn deallocate_memories(&self, index: usize, mems: &mut PrimaryMap<DefinedMemoryIndex, Memory>) {
833        // Decommit any linear memories that were used.
834        for (def_mem_idx, memory) in mem::take(mems) {
835            let mut image = memory.unwrap_static_image();
836            // Reset the image slot. If there is any error clearing the
837            // image, just drop it here, and let the drop handler for the
838            // slot unmap in a way that retains the address space
839            // reservation.
840            if image
841                .clear_and_remain_ready(self.linear_memory_keep_resident)
842                .is_ok()
843            {
844                self.memories
845                    .return_memory_image_slot(index, def_mem_idx, image);
846            }
847        }
848    }
849
850    fn allocate_tables(
851        &self,
852        index: usize,
853        req: &mut InstanceAllocationRequest,
854        tables: &mut PrimaryMap<DefinedTableIndex, Table>,
855    ) -> Result<()> {
856        let module = req.runtime_info.module();
857
858        self.validate_table_plans(module)?;
859
860        let mut bases = self.tables.get(index);
861        for (_, plan) in module.table_plans.iter().skip(module.num_imported_tables) {
862            let base = bases.next().unwrap() as _;
863
864            commit_table_pages(
865                base as *mut u8,
866                self.tables.max_elements as usize * mem::size_of::<*mut u8>(),
867            )?;
868
869            tables.push(Table::new_static(
870                plan,
871                unsafe { std::slice::from_raw_parts_mut(base, self.tables.max_elements as usize) },
872                unsafe { &mut *req.store.get().unwrap() },
873            )?);
874        }
875
876        Ok(())
877    }
878
879    fn deallocate_tables(&self, index: usize, tables: &mut PrimaryMap<DefinedTableIndex, Table>) {
880        // Decommit any tables that were used
881        for (table, base) in tables.values_mut().zip(self.tables.get(index)) {
882            let table = mem::take(table);
883            assert!(table.is_static());
884
885            let size = round_up_to_pow2(
886                table.size() as usize * mem::size_of::<*mut u8>(),
887                self.tables.page_size,
888            );
889
890            drop(table);
891            self.reset_table_pages_to_zero(base, size)
892                .expect("failed to decommit table pages");
893        }
894    }
895
896    #[cfg(all(feature = "async", unix))]
897    fn allocate_fiber_stack(&self) -> Result<wasmtime_fiber::FiberStack> {
898        self.stacks.allocate()
899    }
900
901    #[cfg(all(feature = "async", unix))]
902    unsafe fn deallocate_fiber_stack(&self, stack: &wasmtime_fiber::FiberStack) {
903        self.stacks.deallocate(stack);
904    }
905
906    #[cfg(all(feature = "async", windows))]
907    fn allocate_fiber_stack(&self) -> Result<wasmtime_fiber::FiberStack> {
908        if self.stack_size == 0 {
909            bail!("fiber stack allocation not supported")
910        }
911
912        // On windows, we don't use a stack pool as we use the native fiber implementation
913        let stack = wasmtime_fiber::FiberStack::new(self.stack_size)?;
914        Ok(stack)
915    }
916
917    #[cfg(all(feature = "async", windows))]
918    unsafe fn deallocate_fiber_stack(&self, _stack: &wasmtime_fiber::FiberStack) {
919        // A no-op as we don't own the fiber stack on Windows
920    }
921
922    fn purge_module(&self, module: CompiledModuleId) {
923        // Purging everything related to `module` primarily means clearing out
924        // all of its memory images present in the virtual address space. Go
925        // through the index allocator for slots affine to `module` and reset
926        // them, freeing up the index when we're done.
927        //
928        // Note that this is only called when the specified `module` won't be
929        // allocated further (the module is being dropped) so this shouldn't hit
930        // any sort of infinite loop since this should be the final operation
931        // working with `module`.
932        while let Some(index) = self.index_allocator.alloc_affine_and_clear_affinity(module) {
933            self.memories.clear_images(index.index());
934            self.index_allocator.free(index);
935        }
936    }
937}
938
939#[cfg(test)]
940mod test {
941    use super::*;
942    use crate::{
943        CompiledModuleId, Imports, MemoryImage, ModuleRuntimeInfo, StorePtr, VMFunctionBody,
944        VMSharedSignatureIndex,
945    };
946    use std::sync::Arc;
947    use wasmtime_environ::{DefinedFuncIndex, DefinedMemoryIndex};
948
949    pub(crate) fn empty_runtime_info(
950        module: Arc<wasmtime_environ::Module>,
951    ) -> Arc<dyn ModuleRuntimeInfo> {
952        struct RuntimeInfo(Arc<wasmtime_environ::Module>, VMOffsets<HostPtr>);
953
954        impl ModuleRuntimeInfo for RuntimeInfo {
955            fn module(&self) -> &Arc<wasmtime_environ::Module> {
956                &self.0
957            }
958            fn function(&self, _: DefinedFuncIndex) -> *mut VMFunctionBody {
959                unimplemented!()
960            }
961            fn memory_image(
962                &self,
963                _: DefinedMemoryIndex,
964            ) -> anyhow::Result<Option<&Arc<MemoryImage>>> {
965                Ok(None)
966            }
967
968            fn unique_id(&self) -> Option<CompiledModuleId> {
969                None
970            }
971            fn wasm_data(&self) -> &[u8] {
972                &[]
973            }
974            fn signature_ids(&self) -> &[VMSharedSignatureIndex] {
975                &[]
976            }
977            fn offsets(&self) -> &VMOffsets<HostPtr> {
978                &self.1
979            }
980        }
981
982        let offsets = VMOffsets::new(HostPtr, &module);
983        Arc::new(RuntimeInfo(module, offsets))
984    }
985
986    #[cfg(target_pointer_width = "64")]
987    #[test]
988    fn test_instance_pool() -> Result<()> {
989        let mut config = PoolingInstanceAllocatorConfig::default();
990        config.max_unused_warm_slots = 0;
991        config.limits = InstanceLimits {
992            count: 3,
993            tables: 1,
994            memories: 1,
995            table_elements: 10,
996            size: 1000,
997            memory_pages: 1,
998            ..Default::default()
999        };
1000
1001        let instances = PoolingInstanceAllocator::new(
1002            &config,
1003            &Tunables {
1004                static_memory_bound: 1,
1005                ..Tunables::default()
1006            },
1007        )?;
1008
1009        assert_eq!(instances.instance_size, 1008); // round 1000 up to alignment
1010        assert_eq!(instances.max_instances, 3);
1011
1012        assert_eq!(instances.index_allocator.testing_freelist(), []);
1013
1014        let mut handles = Vec::new();
1015        let module = Arc::new(Module::default());
1016
1017        for _ in (0..3).rev() {
1018            handles.push(
1019                instances
1020                    .allocate(InstanceAllocationRequest {
1021                        runtime_info: &empty_runtime_info(module.clone()),
1022                        imports: Imports {
1023                            functions: &[],
1024                            tables: &[],
1025                            memories: &[],
1026                            globals: &[],
1027                        },
1028                        host_state: Box::new(()),
1029                        store: StorePtr::empty(),
1030                    })
1031                    .expect("allocation should succeed"),
1032            );
1033        }
1034
1035        assert_eq!(instances.index_allocator.testing_freelist(), []);
1036
1037        match instances.allocate(InstanceAllocationRequest {
1038            runtime_info: &empty_runtime_info(module),
1039            imports: Imports {
1040                functions: &[],
1041                tables: &[],
1042                memories: &[],
1043                globals: &[],
1044            },
1045            host_state: Box::new(()),
1046            store: StorePtr::empty(),
1047        }) {
1048            Err(_) => {}
1049            _ => panic!("unexpected error"),
1050        };
1051
1052        for mut handle in handles.drain(..) {
1053            instances.deallocate(&mut handle);
1054        }
1055
1056        assert_eq!(
1057            instances.index_allocator.testing_freelist(),
1058            [SlotId(0), SlotId(1), SlotId(2)]
1059        );
1060
1061        Ok(())
1062    }
1063
1064    #[cfg(target_pointer_width = "64")]
1065    #[test]
1066    fn test_memory_pool() -> Result<()> {
1067        let pool = MemoryPool::new(
1068            &InstanceLimits {
1069                count: 5,
1070                tables: 0,
1071                memories: 3,
1072                table_elements: 0,
1073                memory_pages: 1,
1074                ..Default::default()
1075            },
1076            &Tunables {
1077                static_memory_bound: 1,
1078                static_memory_offset_guard_size: 0,
1079                ..Tunables::default()
1080            },
1081        )?;
1082
1083        assert_eq!(pool.memory_and_guard_size, WASM_PAGE_SIZE as usize);
1084        assert_eq!(pool.max_memories, 3);
1085        assert_eq!(pool.max_instances, 5);
1086        assert_eq!(pool.max_accessible, WASM_PAGE_SIZE as usize);
1087
1088        let base = pool.mapping.as_ptr() as usize;
1089
1090        for i in 0..5 {
1091            let mut iter = pool.get(i);
1092
1093            for j in 0..3 {
1094                assert_eq!(
1095                    iter.next().unwrap() as usize - base,
1096                    ((i * 3) + j) * pool.memory_and_guard_size
1097                );
1098            }
1099
1100            assert_eq!(iter.next(), None);
1101        }
1102
1103        Ok(())
1104    }
1105
1106    #[cfg(target_pointer_width = "64")]
1107    #[test]
1108    fn test_table_pool() -> Result<()> {
1109        let pool = TablePool::new(&InstanceLimits {
1110            count: 7,
1111            table_elements: 100,
1112            memory_pages: 0,
1113            tables: 4,
1114            memories: 0,
1115            ..Default::default()
1116        })?;
1117
1118        let host_page_size = crate::page_size();
1119
1120        assert_eq!(pool.table_size, host_page_size);
1121        assert_eq!(pool.max_tables, 4);
1122        assert_eq!(pool.max_instances, 7);
1123        assert_eq!(pool.page_size, host_page_size);
1124        assert_eq!(pool.max_elements, 100);
1125
1126        let base = pool.mapping.as_ptr() as usize;
1127
1128        for i in 0..7 {
1129            let mut iter = pool.get(i);
1130
1131            for j in 0..4 {
1132                assert_eq!(
1133                    iter.next().unwrap() as usize - base,
1134                    ((i * 4) + j) * pool.table_size
1135                );
1136            }
1137
1138            assert_eq!(iter.next(), None);
1139        }
1140
1141        Ok(())
1142    }
1143
1144    #[cfg(all(unix, target_pointer_width = "64", feature = "async"))]
1145    #[test]
1146    fn test_stack_pool() -> Result<()> {
1147        let config = PoolingInstanceAllocatorConfig {
1148            limits: InstanceLimits {
1149                count: 10,
1150                ..Default::default()
1151            },
1152            stack_size: 1,
1153            async_stack_zeroing: true,
1154            ..PoolingInstanceAllocatorConfig::default()
1155        };
1156        let pool = StackPool::new(&config)?;
1157
1158        let native_page_size = crate::page_size();
1159        assert_eq!(pool.stack_size, 2 * native_page_size);
1160        assert_eq!(pool.max_instances, 10);
1161        assert_eq!(pool.page_size, native_page_size);
1162
1163        assert_eq!(pool.index_allocator.testing_freelist(), []);
1164
1165        let base = pool.mapping.as_ptr() as usize;
1166
1167        let mut stacks = Vec::new();
1168        for i in 0..10 {
1169            let stack = pool.allocate().expect("allocation should succeed");
1170            assert_eq!(
1171                ((stack.top().unwrap() as usize - base) / pool.stack_size) - 1,
1172                i
1173            );
1174            stacks.push(stack);
1175        }
1176
1177        assert_eq!(pool.index_allocator.testing_freelist(), []);
1178
1179        pool.allocate().unwrap_err();
1180
1181        for stack in stacks {
1182            pool.deallocate(&stack);
1183        }
1184
1185        assert_eq!(
1186            pool.index_allocator.testing_freelist(),
1187            [
1188                SlotId(0),
1189                SlotId(1),
1190                SlotId(2),
1191                SlotId(3),
1192                SlotId(4),
1193                SlotId(5),
1194                SlotId(6),
1195                SlotId(7),
1196                SlotId(8),
1197                SlotId(9)
1198            ],
1199        );
1200
1201        Ok(())
1202    }
1203
1204    #[test]
1205    fn test_pooling_allocator_with_zero_instance_count() {
1206        let config = PoolingInstanceAllocatorConfig {
1207            limits: InstanceLimits {
1208                count: 0,
1209                ..Default::default()
1210            },
1211            ..PoolingInstanceAllocatorConfig::default()
1212        };
1213        assert_eq!(
1214            PoolingInstanceAllocator::new(&config, &Tunables::default(),)
1215                .map_err(|e| e.to_string())
1216                .expect_err("expected a failure constructing instance allocator"),
1217            "the instance count limit cannot be zero"
1218        );
1219    }
1220
1221    #[test]
1222    fn test_pooling_allocator_with_memory_pages_exceeded() {
1223        let config = PoolingInstanceAllocatorConfig {
1224            limits: InstanceLimits {
1225                count: 1,
1226                memory_pages: 0x10001,
1227                ..Default::default()
1228            },
1229            ..PoolingInstanceAllocatorConfig::default()
1230        };
1231        assert_eq!(
1232            PoolingInstanceAllocator::new(
1233                &config,
1234                &Tunables {
1235                    static_memory_bound: 1,
1236                    ..Tunables::default()
1237                },
1238            )
1239            .map_err(|e| e.to_string())
1240            .expect_err("expected a failure constructing instance allocator"),
1241            "module memory page limit of 65537 exceeds the maximum of 65536"
1242        );
1243    }
1244
1245    #[test]
1246    fn test_pooling_allocator_with_reservation_size_exceeded() {
1247        let config = PoolingInstanceAllocatorConfig {
1248            limits: InstanceLimits {
1249                count: 1,
1250                memory_pages: 2,
1251                ..Default::default()
1252            },
1253            ..PoolingInstanceAllocatorConfig::default()
1254        };
1255        let pool = PoolingInstanceAllocator::new(
1256            &config,
1257            &Tunables {
1258                static_memory_bound: 1,
1259                static_memory_offset_guard_size: 0,
1260                ..Tunables::default()
1261            },
1262        )
1263        .unwrap();
1264        assert_eq!(pool.memories.memory_size, 2 * 65536);
1265    }
1266
1267    #[cfg(all(unix, target_pointer_width = "64", feature = "async"))]
1268    #[test]
1269    fn test_stack_zeroed() -> Result<()> {
1270        let config = PoolingInstanceAllocatorConfig {
1271            max_unused_warm_slots: 0,
1272            limits: InstanceLimits {
1273                count: 1,
1274                table_elements: 0,
1275                memory_pages: 0,
1276                tables: 0,
1277                memories: 0,
1278                ..Default::default()
1279            },
1280            stack_size: 128,
1281            async_stack_zeroing: true,
1282            ..PoolingInstanceAllocatorConfig::default()
1283        };
1284        let allocator = PoolingInstanceAllocator::new(&config, &Tunables::default())?;
1285
1286        unsafe {
1287            for _ in 0..255 {
1288                let stack = allocator.allocate_fiber_stack()?;
1289
1290                // The stack pointer is at the top, so decrement it first
1291                let addr = stack.top().unwrap().sub(1);
1292
1293                assert_eq!(*addr, 0);
1294                *addr = 1;
1295
1296                allocator.deallocate_fiber_stack(&stack);
1297            }
1298        }
1299
1300        Ok(())
1301    }
1302
1303    #[cfg(all(unix, target_pointer_width = "64", feature = "async"))]
1304    #[test]
1305    fn test_stack_unzeroed() -> Result<()> {
1306        let config = PoolingInstanceAllocatorConfig {
1307            max_unused_warm_slots: 0,
1308            limits: InstanceLimits {
1309                count: 1,
1310                table_elements: 0,
1311                memory_pages: 0,
1312                tables: 0,
1313                memories: 0,
1314                ..Default::default()
1315            },
1316            stack_size: 128,
1317            async_stack_zeroing: false,
1318            ..PoolingInstanceAllocatorConfig::default()
1319        };
1320        let allocator = PoolingInstanceAllocator::new(&config, &Tunables::default())?;
1321
1322        unsafe {
1323            for i in 0..255 {
1324                let stack = allocator.allocate_fiber_stack()?;
1325
1326                // The stack pointer is at the top, so decrement it first
1327                let addr = stack.top().unwrap().sub(1);
1328
1329                assert_eq!(*addr, i);
1330                *addr = i + 1;
1331
1332                allocator.deallocate_fiber_stack(&stack);
1333            }
1334        }
1335
1336        Ok(())
1337    }
1338}