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