1 //! Implements the pooling instance allocator.
2 //!
3 //! The pooling instance allocator maps memory in advance and allocates
4 //! instances, memories, tables, and stacks from a pool of available resources.
5 //! Using the pooling instance allocator can speed up module instantiation when
6 //! modules can be constrained based on configurable limits
7 //! ([`InstanceLimits`]). Each new instance is stored in a "slot"; as instances
8 //! are allocated and freed, these slots are either filled or emptied:
9 //!
10 //! ```text
11 //! ┌──────┬──────┬──────┬──────┬──────┐
12 //! │Slot 0│Slot 1│Slot 2│Slot 3│......│
13 //! └──────┴──────┴──────┴──────┴──────┘
14 //! ```
15 //!
16 //! Each slot has a "slot ID"--an index into the pool. Slot IDs are handed out
17 //! by the [`index_allocator`] module. Note that each kind of pool-allocated
18 //! item is stored in its own separate pool: [`memory_pool`], [`table_pool`],
19 //! [`stack_pool`]. See those modules for more details.
20 
21 mod decommit_queue;
22 mod index_allocator;
23 mod memory_pool;
24 mod table_pool;
25 
26 #[cfg(feature = "gc")]
27 mod gc_heap_pool;
28 
29 #[cfg(all(feature = "async"))]
30 mod generic_stack_pool;
31 #[cfg(all(feature = "async", unix, not(miri)))]
32 mod unix_stack_pool;
33 
34 #[cfg(all(feature = "async"))]
35 cfg_if::cfg_if! {
36     if #[cfg(all(unix, not(miri), not(asan)))] {
37         use unix_stack_pool as stack_pool;
38     } else {
39         use generic_stack_pool as stack_pool;
40     }
41 }
42 
43 use self::decommit_queue::DecommitQueue;
44 use self::memory_pool::MemoryPool;
45 use self::table_pool::TablePool;
46 use super::{
47     InstanceAllocationRequest, InstanceAllocatorImpl, MemoryAllocationIndex, TableAllocationIndex,
48 };
49 use crate::MpkEnabled;
50 use crate::prelude::*;
51 use crate::runtime::vm::{
52     CompiledModuleId, Memory, Table,
53     instance::Instance,
54     mpk::{self, ProtectionKey, ProtectionMask},
55 };
56 use std::borrow::Cow;
57 use std::fmt::Display;
58 use std::sync::{Mutex, MutexGuard};
59 use std::{
60     mem,
61     sync::atomic::{AtomicU64, Ordering},
62 };
63 use wasmtime_environ::{
64     DefinedMemoryIndex, DefinedTableIndex, HostPtr, Module, Tunables, VMOffsets,
65 };
66 
67 #[cfg(feature = "gc")]
68 use super::GcHeapAllocationIndex;
69 #[cfg(feature = "gc")]
70 use crate::runtime::vm::{GcHeap, GcRuntime};
71 #[cfg(feature = "gc")]
72 use gc_heap_pool::GcHeapPool;
73 
74 #[cfg(feature = "async")]
75 use stack_pool::StackPool;
76 
77 #[cfg(feature = "component-model")]
78 use wasmtime_environ::{
79     StaticModuleIndex,
80     component::{Component, VMComponentOffsets},
81 };
82 
83 fn round_up_to_pow2(n: usize, to: usize) -> usize {
84     debug_assert!(to > 0);
85     debug_assert!(to.is_power_of_two());
86     (n + to - 1) & !(to - 1)
87 }
88 
89 /// Instance-related limit configuration for pooling.
90 ///
91 /// More docs on this can be found at `wasmtime::PoolingAllocationConfig`.
92 #[derive(Debug, Copy, Clone)]
93 pub struct InstanceLimits {
94     /// The maximum number of component instances that may be allocated
95     /// concurrently.
96     pub total_component_instances: u32,
97 
98     /// The maximum size of a component's `VMComponentContext`, not including
99     /// any of its inner core modules' `VMContext` sizes.
100     pub component_instance_size: usize,
101 
102     /// The maximum number of core module instances that may be allocated
103     /// concurrently.
104     pub total_core_instances: u32,
105 
106     /// The maximum number of core module instances that a single component may
107     /// transitively contain.
108     pub max_core_instances_per_component: u32,
109 
110     /// The maximum number of Wasm linear memories that a component may
111     /// transitively contain.
112     pub max_memories_per_component: u32,
113 
114     /// The maximum number of tables that a component may transitively contain.
115     pub max_tables_per_component: u32,
116 
117     /// The total number of linear memories in the pool, across all instances.
118     pub total_memories: u32,
119 
120     /// The total number of tables in the pool, across all instances.
121     pub total_tables: u32,
122 
123     /// The total number of async stacks in the pool, across all instances.
124     #[cfg(feature = "async")]
125     pub total_stacks: u32,
126 
127     /// Maximum size of a core instance's `VMContext`.
128     pub core_instance_size: usize,
129 
130     /// Maximum number of tables per instance.
131     pub max_tables_per_module: u32,
132 
133     /// Maximum number of word-size elements per table.
134     ///
135     /// Note that tables for element types such as continuations
136     /// that use more than one word of storage may store fewer
137     /// elements.
138     pub table_elements: usize,
139 
140     /// Maximum number of linear memories per instance.
141     pub max_memories_per_module: u32,
142 
143     /// Maximum byte size of a linear memory, must be smaller than
144     /// `memory_reservation` in `Tunables`.
145     pub max_memory_size: usize,
146 
147     /// The total number of GC heaps in the pool, across all instances.
148     #[cfg(feature = "gc")]
149     pub total_gc_heaps: u32,
150 }
151 
152 impl Default for InstanceLimits {
153     fn default() -> Self {
154         let total = if cfg!(target_pointer_width = "32") {
155             100
156         } else {
157             1000
158         };
159         // See doc comments for `wasmtime::PoolingAllocationConfig` for these
160         // default values
161         Self {
162             total_component_instances: total,
163             component_instance_size: 1 << 20, // 1 MiB
164             total_core_instances: total,
165             max_core_instances_per_component: u32::MAX,
166             max_memories_per_component: u32::MAX,
167             max_tables_per_component: u32::MAX,
168             total_memories: total,
169             total_tables: total,
170             #[cfg(feature = "async")]
171             total_stacks: total,
172             core_instance_size: 1 << 20, // 1 MiB
173             max_tables_per_module: 1,
174             // NB: in #8504 it was seen that a C# module in debug module can
175             // have 10k+ elements.
176             table_elements: 20_000,
177             max_memories_per_module: 1,
178             #[cfg(target_pointer_width = "64")]
179             max_memory_size: 1 << 32, // 4G,
180             #[cfg(target_pointer_width = "32")]
181             max_memory_size: 10 << 20, // 10 MiB
182             #[cfg(feature = "gc")]
183             total_gc_heaps: total,
184         }
185     }
186 }
187 
188 /// Configuration options for the pooling instance allocator supplied at
189 /// construction.
190 #[derive(Copy, Clone, Debug)]
191 pub struct PoolingInstanceAllocatorConfig {
192     /// See `PoolingAllocatorConfig::max_unused_warm_slots` in `wasmtime`
193     pub max_unused_warm_slots: u32,
194     /// The target number of decommits to do per batch. This is not precise, as
195     /// we can queue up decommits at times when we aren't prepared to
196     /// immediately flush them, and so we may go over this target size
197     /// occasionally.
198     pub decommit_batch_size: usize,
199     /// The size, in bytes, of async stacks to allocate (not including the guard
200     /// page).
201     pub stack_size: usize,
202     /// The limits to apply to instances allocated within this allocator.
203     pub limits: InstanceLimits,
204     /// Whether or not async stacks are zeroed after use.
205     pub async_stack_zeroing: bool,
206     /// If async stack zeroing is enabled and the host platform is Linux this is
207     /// how much memory to zero out with `memset`.
208     ///
209     /// The rest of memory will be zeroed out with `madvise`.
210     #[cfg(feature = "async")]
211     pub async_stack_keep_resident: usize,
212     /// How much linear memory, in bytes, to keep resident after resetting for
213     /// use with the next instance. This much memory will be `memset` to zero
214     /// when a linear memory is deallocated.
215     ///
216     /// Memory exceeding this amount in the wasm linear memory will be released
217     /// with `madvise` back to the kernel.
218     ///
219     /// Only applicable on Linux.
220     pub linear_memory_keep_resident: usize,
221     /// Same as `linear_memory_keep_resident` but for tables.
222     pub table_keep_resident: usize,
223     /// Whether to enable memory protection keys.
224     pub memory_protection_keys: MpkEnabled,
225     /// How many memory protection keys to allocate.
226     pub max_memory_protection_keys: usize,
227 }
228 
229 impl Default for PoolingInstanceAllocatorConfig {
230     fn default() -> PoolingInstanceAllocatorConfig {
231         PoolingInstanceAllocatorConfig {
232             max_unused_warm_slots: 100,
233             decommit_batch_size: 1,
234             stack_size: 2 << 20,
235             limits: InstanceLimits::default(),
236             async_stack_zeroing: false,
237             #[cfg(feature = "async")]
238             async_stack_keep_resident: 0,
239             linear_memory_keep_resident: 0,
240             table_keep_resident: 0,
241             memory_protection_keys: MpkEnabled::Disable,
242             max_memory_protection_keys: 16,
243         }
244     }
245 }
246 
247 /// An error returned when the pooling allocator cannot allocate a table,
248 /// memory, etc... because the maximum number of concurrent allocations for that
249 /// entity has been reached.
250 #[derive(Debug)]
251 pub struct PoolConcurrencyLimitError {
252     limit: usize,
253     kind: Cow<'static, str>,
254 }
255 
256 impl core::error::Error for PoolConcurrencyLimitError {}
257 
258 impl Display for PoolConcurrencyLimitError {
259     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
260         let limit = self.limit;
261         let kind = &self.kind;
262         write!(f, "maximum concurrent limit of {limit} for {kind} reached")
263     }
264 }
265 
266 impl PoolConcurrencyLimitError {
267     fn new(limit: usize, kind: impl Into<Cow<'static, str>>) -> Self {
268         Self {
269             limit,
270             kind: kind.into(),
271         }
272     }
273 }
274 
275 /// Implements the pooling instance allocator.
276 ///
277 /// This allocator internally maintains pools of instances, memories, tables,
278 /// and stacks.
279 ///
280 /// Note: the resource pools are manually dropped so that the fault handler
281 /// terminates correctly.
282 #[derive(Debug)]
283 pub struct PoolingInstanceAllocator {
284     decommit_batch_size: usize,
285     limits: InstanceLimits,
286 
287     // The number of live core module and component instances at any given
288     // time. Note that this can temporarily go over the configured limit. This
289     // doesn't mean we have actually overshot, but that we attempted to allocate
290     // a new instance and incremented the counter, we've seen (or are about to
291     // see) that the counter is beyond the configured threshold, and are going
292     // to decrement the counter and return an error but haven't done so yet. See
293     // the increment trait methods for more details.
294     live_core_instances: AtomicU64,
295     live_component_instances: AtomicU64,
296 
297     decommit_queue: Mutex<DecommitQueue>,
298     memories: MemoryPool,
299     tables: TablePool,
300 
301     #[cfg(feature = "gc")]
302     gc_heaps: GcHeapPool,
303 
304     #[cfg(feature = "async")]
305     stacks: StackPool,
306 }
307 
308 impl Drop for PoolingInstanceAllocator {
309     fn drop(&mut self) {
310         if !cfg!(debug_assertions) {
311             return;
312         }
313 
314         // NB: when cfg(not(debug_assertions)) it is okay that we don't flush
315         // the queue, as the sub-pools will unmap those ranges anyways, so
316         // there's no point in decommitting them. But we do need to flush the
317         // queue when debug assertions are enabled to make sure that all
318         // entities get returned to their associated sub-pools and we can
319         // differentiate between a leaking slot and an enqueued-for-decommit
320         // slot.
321         let queue = self.decommit_queue.lock().unwrap();
322         self.flush_decommit_queue(queue);
323 
324         debug_assert_eq!(self.live_component_instances.load(Ordering::Acquire), 0);
325         debug_assert_eq!(self.live_core_instances.load(Ordering::Acquire), 0);
326 
327         debug_assert!(self.memories.is_empty());
328         debug_assert!(self.tables.is_empty());
329 
330         #[cfg(feature = "gc")]
331         debug_assert!(self.gc_heaps.is_empty());
332 
333         #[cfg(feature = "async")]
334         debug_assert!(self.stacks.is_empty());
335     }
336 }
337 
338 impl PoolingInstanceAllocator {
339     /// Creates a new pooling instance allocator with the given strategy and limits.
340     pub fn new(config: &PoolingInstanceAllocatorConfig, tunables: &Tunables) -> Result<Self> {
341         Ok(Self {
342             decommit_batch_size: config.decommit_batch_size,
343             limits: config.limits,
344             live_component_instances: AtomicU64::new(0),
345             live_core_instances: AtomicU64::new(0),
346             decommit_queue: Mutex::new(DecommitQueue::default()),
347             memories: MemoryPool::new(config, tunables)?,
348             tables: TablePool::new(config)?,
349             #[cfg(feature = "gc")]
350             gc_heaps: GcHeapPool::new(config)?,
351             #[cfg(feature = "async")]
352             stacks: StackPool::new(config)?,
353         })
354     }
355 
356     fn core_instance_size(&self) -> usize {
357         round_up_to_pow2(self.limits.core_instance_size, mem::align_of::<Instance>())
358     }
359 
360     fn validate_table_plans(&self, module: &Module) -> Result<()> {
361         self.tables.validate(module)
362     }
363 
364     fn validate_memory_plans(&self, module: &Module) -> Result<()> {
365         self.memories.validate_memories(module)
366     }
367 
368     fn validate_core_instance_size(&self, offsets: &VMOffsets<HostPtr>) -> Result<()> {
369         let layout = Instance::alloc_layout(offsets);
370         if layout.size() <= self.core_instance_size() {
371             return Ok(());
372         }
373 
374         // If this `module` exceeds the allocation size allotted to it then an
375         // error will be reported here. The error of "required N bytes but
376         // cannot allocate that" is pretty opaque, however, because it's not
377         // clear what the breakdown of the N bytes are and what to optimize
378         // next. To help provide a better error message here some fancy-ish
379         // logic is done here to report the breakdown of the byte request into
380         // the largest portions and where it's coming from.
381         let mut message = format!(
382             "instance allocation for this module \
383              requires {} bytes which exceeds the configured maximum \
384              of {} bytes; breakdown of allocation requirement:\n\n",
385             layout.size(),
386             self.core_instance_size(),
387         );
388 
389         let mut remaining = layout.size();
390         let mut push = |name: &str, bytes: usize| {
391             assert!(remaining >= bytes);
392             remaining -= bytes;
393 
394             // If the `name` region is more than 5% of the allocation request
395             // then report it here, otherwise ignore it. We have less than 20
396             // fields so we're guaranteed that something should be reported, and
397             // otherwise it's not particularly interesting to learn about 5
398             // different fields that are all 8 or 0 bytes. Only try to report
399             // the "major" sources of bytes here.
400             if bytes > layout.size() / 20 {
401                 message.push_str(&format!(
402                     " * {:.02}% - {} bytes - {}\n",
403                     ((bytes as f32) / (layout.size() as f32)) * 100.0,
404                     bytes,
405                     name,
406                 ));
407             }
408         };
409 
410         // The `Instance` itself requires some size allocated to it.
411         push("instance state management", mem::size_of::<Instance>());
412 
413         // Afterwards the `VMContext`'s regions are why we're requesting bytes,
414         // so ask it for descriptions on each region's byte size.
415         for (desc, size) in offsets.region_sizes() {
416             push(desc, size as usize);
417         }
418 
419         // double-check we accounted for all the bytes
420         assert_eq!(remaining, 0);
421 
422         bail!("{}", message)
423     }
424 
425     #[cfg(feature = "component-model")]
426     fn validate_component_instance_size(
427         &self,
428         offsets: &VMComponentOffsets<HostPtr>,
429     ) -> Result<()> {
430         if usize::try_from(offsets.size_of_vmctx()).unwrap() <= self.limits.component_instance_size
431         {
432             return Ok(());
433         }
434 
435         // TODO: Add context with detailed accounting of what makes up all the
436         // `VMComponentContext`'s space like we do for module instances.
437         bail!(
438             "instance allocation for this component requires {} bytes of `VMComponentContext` \
439              space which exceeds the configured maximum of {} bytes",
440             offsets.size_of_vmctx(),
441             self.limits.component_instance_size
442         )
443     }
444 
445     fn flush_decommit_queue(&self, mut locked_queue: MutexGuard<'_, DecommitQueue>) -> bool {
446         // Take the queue out of the mutex and drop the lock, to minimize
447         // contention.
448         let queue = mem::take(&mut *locked_queue);
449         drop(locked_queue);
450         queue.flush(self)
451     }
452 
453     /// Execute `f` and if it returns `Err(PoolConcurrencyLimitError)`, then try
454     /// flushing the decommit queue. If flushing the queue freed up slots, then
455     /// try running `f` again.
456     fn with_flush_and_retry<T>(&self, mut f: impl FnMut() -> Result<T>) -> Result<T> {
457         f().or_else(|e| {
458             if e.is::<PoolConcurrencyLimitError>() {
459                 let queue = self.decommit_queue.lock().unwrap();
460                 if self.flush_decommit_queue(queue) {
461                     return f();
462                 }
463             }
464 
465             Err(e)
466         })
467     }
468 
469     fn merge_or_flush(&self, mut local_queue: DecommitQueue) {
470         match local_queue.raw_len() {
471             // If we didn't enqueue any regions for decommit, then we must have
472             // either memset the whole entity or eagerly remapped it to zero
473             // because we don't have linux's `madvise(DONTNEED)` semantics. In
474             // either case, the entity slot is ready for reuse immediately.
475             0 => {
476                 local_queue.flush(self);
477             }
478 
479             // We enqueued at least our batch size of regions for decommit, so
480             // flush the local queue immediately. Don't bother inspecting (or
481             // locking!) the shared queue.
482             n if n >= self.decommit_batch_size => {
483                 local_queue.flush(self);
484             }
485 
486             // If we enqueued some regions for decommit, but did not reach our
487             // batch size, so we don't want to flush it yet, then merge the
488             // local queue into the shared queue.
489             n => {
490                 debug_assert!(n < self.decommit_batch_size);
491                 let mut shared_queue = self.decommit_queue.lock().unwrap();
492                 shared_queue.append(&mut local_queue);
493                 // And if the shared queue now has at least as many regions
494                 // enqueued for decommit as our batch size, then we can flush
495                 // it.
496                 if shared_queue.raw_len() >= self.decommit_batch_size {
497                     self.flush_decommit_queue(shared_queue);
498                 }
499             }
500         }
501     }
502 }
503 
504 unsafe impl InstanceAllocatorImpl for PoolingInstanceAllocator {
505     #[cfg(feature = "component-model")]
506     fn validate_component_impl<'a>(
507         &self,
508         component: &Component,
509         offsets: &VMComponentOffsets<HostPtr>,
510         get_module: &'a dyn Fn(StaticModuleIndex) -> &'a Module,
511     ) -> Result<()> {
512         self.validate_component_instance_size(offsets)
513             .context("component instance size does not fit in pooling allocator requirements")?;
514 
515         let mut num_core_instances = 0;
516         let mut num_memories = 0;
517         let mut num_tables = 0;
518         for init in &component.initializers {
519             use wasmtime_environ::component::GlobalInitializer::*;
520             use wasmtime_environ::component::InstantiateModule;
521             match init {
522                 InstantiateModule(InstantiateModule::Import(_, _)) => {
523                     num_core_instances += 1;
524                     // Can't statically account for the total vmctx size, number
525                     // of memories, and number of tables in this component.
526                 }
527                 InstantiateModule(InstantiateModule::Static(static_module_index, _)) => {
528                     let module = get_module(*static_module_index);
529                     let offsets = VMOffsets::new(HostPtr, &module);
530                     self.validate_module_impl(module, &offsets)?;
531                     num_core_instances += 1;
532                     num_memories += module.num_defined_memories();
533                     num_tables += module.num_defined_tables();
534                 }
535                 LowerImport { .. }
536                 | ExtractMemory(_)
537                 | ExtractTable(_)
538                 | ExtractRealloc(_)
539                 | ExtractCallback(_)
540                 | ExtractPostReturn(_)
541                 | Resource(_) => {}
542             }
543         }
544 
545         if num_core_instances
546             > usize::try_from(self.limits.max_core_instances_per_component).unwrap()
547         {
548             bail!(
549                 "The component transitively contains {num_core_instances} core module instances, \
550                  which exceeds the configured maximum of {} in the pooling allocator",
551                 self.limits.max_core_instances_per_component
552             );
553         }
554 
555         if num_memories > usize::try_from(self.limits.max_memories_per_component).unwrap() {
556             bail!(
557                 "The component transitively contains {num_memories} Wasm linear memories, which \
558                  exceeds the configured maximum of {} in the pooling allocator",
559                 self.limits.max_memories_per_component
560             );
561         }
562 
563         if num_tables > usize::try_from(self.limits.max_tables_per_component).unwrap() {
564             bail!(
565                 "The component transitively contains {num_tables} tables, which exceeds the \
566                  configured maximum of {} in the pooling allocator",
567                 self.limits.max_tables_per_component
568             );
569         }
570 
571         Ok(())
572     }
573 
574     fn validate_module_impl(&self, module: &Module, offsets: &VMOffsets<HostPtr>) -> Result<()> {
575         self.validate_memory_plans(module)
576             .context("module memory does not fit in pooling allocator requirements")?;
577         self.validate_table_plans(module)
578             .context("module table does not fit in pooling allocator requirements")?;
579         self.validate_core_instance_size(offsets)
580             .context("module instance size does not fit in pooling allocator requirements")?;
581         Ok(())
582     }
583 
584     #[cfg(feature = "gc")]
585     fn validate_memory_impl(&self, memory: &wasmtime_environ::Memory) -> Result<()> {
586         self.memories.validate_memory(memory)
587     }
588 
589     #[cfg(feature = "component-model")]
590     fn increment_component_instance_count(&self) -> Result<()> {
591         let old_count = self.live_component_instances.fetch_add(1, Ordering::AcqRel);
592         if old_count >= u64::from(self.limits.total_component_instances) {
593             self.decrement_component_instance_count();
594             return Err(PoolConcurrencyLimitError::new(
595                 usize::try_from(self.limits.total_component_instances).unwrap(),
596                 "component instances",
597             )
598             .into());
599         }
600         Ok(())
601     }
602 
603     #[cfg(feature = "component-model")]
604     fn decrement_component_instance_count(&self) {
605         self.live_component_instances.fetch_sub(1, Ordering::AcqRel);
606     }
607 
608     fn increment_core_instance_count(&self) -> Result<()> {
609         let old_count = self.live_core_instances.fetch_add(1, Ordering::AcqRel);
610         if old_count >= u64::from(self.limits.total_core_instances) {
611             self.decrement_core_instance_count();
612             return Err(PoolConcurrencyLimitError::new(
613                 usize::try_from(self.limits.total_core_instances).unwrap(),
614                 "core instances",
615             )
616             .into());
617         }
618         Ok(())
619     }
620 
621     fn decrement_core_instance_count(&self) {
622         self.live_core_instances.fetch_sub(1, Ordering::AcqRel);
623     }
624 
625     unsafe fn allocate_memory(
626         &self,
627         request: &mut InstanceAllocationRequest,
628         ty: &wasmtime_environ::Memory,
629         tunables: &Tunables,
630         memory_index: Option<DefinedMemoryIndex>,
631     ) -> Result<(MemoryAllocationIndex, Memory)> {
632         self.with_flush_and_retry(|| self.memories.allocate(request, ty, tunables, memory_index))
633     }
634 
635     unsafe fn deallocate_memory(
636         &self,
637         _memory_index: Option<DefinedMemoryIndex>,
638         allocation_index: MemoryAllocationIndex,
639         memory: Memory,
640     ) {
641         // Reset the image slot. If there is any error clearing the
642         // image, just drop it here, and let the drop handler for the
643         // slot unmap in a way that retains the address space
644         // reservation.
645         let mut image = memory.unwrap_static_image();
646         let mut queue = DecommitQueue::default();
647         image
648             .clear_and_remain_ready(self.memories.keep_resident, |ptr, len| {
649                 // SAFETY: the memory in `image` won't be used until this
650                 // decommit queue is flushed, and by definition the memory is
651                 // not in use when calling this function.
652                 unsafe {
653                     queue.push_raw(ptr, len);
654                 }
655             })
656             .expect("failed to reset memory image");
657 
658         // SAFETY: this image is not in use and its memory regions were enqueued
659         // with `push_raw` above.
660         unsafe {
661             queue.push_memory(allocation_index, image);
662         }
663         self.merge_or_flush(queue);
664     }
665 
666     unsafe fn allocate_table(
667         &self,
668         request: &mut InstanceAllocationRequest,
669         ty: &wasmtime_environ::Table,
670         tunables: &Tunables,
671         _table_index: DefinedTableIndex,
672     ) -> Result<(super::TableAllocationIndex, Table)> {
673         self.with_flush_and_retry(|| self.tables.allocate(request, ty, tunables))
674     }
675 
676     unsafe fn deallocate_table(
677         &self,
678         _table_index: DefinedTableIndex,
679         allocation_index: TableAllocationIndex,
680         mut table: Table,
681     ) {
682         let mut queue = DecommitQueue::default();
683         // SAFETY: This table is no longer in use by the allocator when this
684         // method is called and additionally all image ranges are pushed with
685         // the understanding that the memory won't get used until the whole
686         // queue is flushed.
687         unsafe {
688             self.tables
689                 .reset_table_pages_to_zero(allocation_index, &mut table, |ptr, len| {
690                     queue.push_raw(ptr, len);
691                 });
692         }
693 
694         // SAFETY: the table has had all its memory regions enqueued above.
695         unsafe {
696             queue.push_table(allocation_index, table);
697         }
698         self.merge_or_flush(queue);
699     }
700 
701     #[cfg(feature = "async")]
702     fn allocate_fiber_stack(&self) -> Result<wasmtime_fiber::FiberStack> {
703         self.with_flush_and_retry(|| self.stacks.allocate())
704     }
705 
706     #[cfg(feature = "async")]
707     unsafe fn deallocate_fiber_stack(&self, mut stack: wasmtime_fiber::FiberStack) {
708         let mut queue = DecommitQueue::default();
709         // SAFETY: the stack is no longer in use by definition when this
710         // function is called and memory ranges pushed here are otherwise no
711         // longer in use.
712         unsafe {
713             self.stacks
714                 .zero_stack(&mut stack, |ptr, len| queue.push_raw(ptr, len));
715         }
716         // SAFETY: this stack's memory regions were enqueued above.
717         unsafe {
718             queue.push_stack(stack);
719         }
720         self.merge_or_flush(queue);
721     }
722 
723     fn purge_module(&self, module: CompiledModuleId) {
724         self.memories.purge_module(module);
725     }
726 
727     fn next_available_pkey(&self) -> Option<ProtectionKey> {
728         self.memories.next_available_pkey()
729     }
730 
731     fn restrict_to_pkey(&self, pkey: ProtectionKey) {
732         mpk::allow(ProtectionMask::zero().or(pkey));
733     }
734 
735     fn allow_all_pkeys(&self) {
736         mpk::allow(ProtectionMask::all());
737     }
738 
739     #[cfg(feature = "gc")]
740     fn allocate_gc_heap(
741         &self,
742         engine: &crate::Engine,
743         gc_runtime: &dyn GcRuntime,
744         memory_alloc_index: MemoryAllocationIndex,
745         memory: Memory,
746     ) -> Result<(GcHeapAllocationIndex, Box<dyn GcHeap>)> {
747         self.gc_heaps
748             .allocate(engine, gc_runtime, memory_alloc_index, memory)
749     }
750 
751     #[cfg(feature = "gc")]
752     fn deallocate_gc_heap(
753         &self,
754         allocation_index: GcHeapAllocationIndex,
755         gc_heap: Box<dyn GcHeap>,
756     ) -> (MemoryAllocationIndex, Memory) {
757         self.gc_heaps.deallocate(allocation_index, gc_heap)
758     }
759 }
760 
761 #[cfg(test)]
762 #[cfg(target_pointer_width = "64")]
763 mod test {
764     use super::*;
765 
766     #[test]
767     fn test_pooling_allocator_with_memory_pages_exceeded() {
768         let config = PoolingInstanceAllocatorConfig {
769             limits: InstanceLimits {
770                 total_memories: 1,
771                 max_memory_size: 0x100010000,
772                 ..Default::default()
773             },
774             ..PoolingInstanceAllocatorConfig::default()
775         };
776         assert_eq!(
777             PoolingInstanceAllocator::new(
778                 &config,
779                 &Tunables {
780                     memory_reservation: 0x10000,
781                     ..Tunables::default_host()
782                 },
783             )
784             .map_err(|e| e.to_string())
785             .expect_err("expected a failure constructing instance allocator"),
786             "maximum memory size of 0x100010000 bytes exceeds the configured \
787              memory reservation of 0x10000 bytes"
788         );
789     }
790 
791     #[cfg(all(
792         unix,
793         target_pointer_width = "64",
794         feature = "async",
795         not(miri),
796         not(asan)
797     ))]
798     #[test]
799     fn test_stack_zeroed() -> Result<()> {
800         let config = PoolingInstanceAllocatorConfig {
801             max_unused_warm_slots: 0,
802             limits: InstanceLimits {
803                 total_stacks: 1,
804                 total_memories: 0,
805                 total_tables: 0,
806                 ..Default::default()
807             },
808             stack_size: 128,
809             async_stack_zeroing: true,
810             ..PoolingInstanceAllocatorConfig::default()
811         };
812         let allocator = PoolingInstanceAllocator::new(&config, &Tunables::default_host())?;
813 
814         unsafe {
815             for _ in 0..255 {
816                 let stack = allocator.allocate_fiber_stack()?;
817 
818                 // The stack pointer is at the top, so decrement it first
819                 let addr = stack.top().unwrap().sub(1);
820 
821                 assert_eq!(*addr, 0);
822                 *addr = 1;
823 
824                 allocator.deallocate_fiber_stack(stack);
825             }
826         }
827 
828         Ok(())
829     }
830 
831     #[cfg(all(
832         unix,
833         target_pointer_width = "64",
834         feature = "async",
835         not(miri),
836         not(asan)
837     ))]
838     #[test]
839     fn test_stack_unzeroed() -> Result<()> {
840         let config = PoolingInstanceAllocatorConfig {
841             max_unused_warm_slots: 0,
842             limits: InstanceLimits {
843                 total_stacks: 1,
844                 total_memories: 0,
845                 total_tables: 0,
846                 ..Default::default()
847             },
848             stack_size: 128,
849             async_stack_zeroing: false,
850             ..PoolingInstanceAllocatorConfig::default()
851         };
852         let allocator = PoolingInstanceAllocator::new(&config, &Tunables::default_host())?;
853 
854         unsafe {
855             for i in 0..255 {
856                 let stack = allocator.allocate_fiber_stack()?;
857 
858                 // The stack pointer is at the top, so decrement it first
859                 let addr = stack.top().unwrap().sub(1);
860 
861                 assert_eq!(*addr, i);
862                 *addr = i + 1;
863 
864                 allocator.deallocate_fiber_stack(stack);
865             }
866         }
867 
868         Ok(())
869     }
870 }
871