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