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