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 index_allocator;
22 mod memory_pool;
23 mod table_pool;
24 
25 #[cfg(feature = "gc")]
26 mod gc_heap_pool;
27 
28 #[cfg(all(feature = "async"))]
29 mod generic_stack_pool;
30 #[cfg(all(feature = "async", unix, not(miri)))]
31 mod unix_stack_pool;
32 
33 #[cfg(all(feature = "async"))]
34 cfg_if::cfg_if! {
35     if #[cfg(all(unix, not(miri), not(asan)))] {
36         use unix_stack_pool as stack_pool;
37     } else {
38         use generic_stack_pool as stack_pool;
39     }
40 }
41 
42 use super::{
43     InstanceAllocationRequest, InstanceAllocatorImpl, MemoryAllocationIndex, TableAllocationIndex,
44 };
45 use crate::runtime::vm::{
46     instance::Instance,
47     mpk::{self, MpkEnabled, ProtectionKey, ProtectionMask},
48     CompiledModuleId, Memory, Table,
49 };
50 use anyhow::{bail, Result};
51 use memory_pool::MemoryPool;
52 use std::{
53     mem,
54     sync::atomic::{AtomicU64, Ordering},
55 };
56 use table_pool::TablePool;
57 use wasmtime_environ::{
58     DefinedMemoryIndex, DefinedTableIndex, HostPtr, MemoryPlan, Module, TablePlan, Tunables,
59     VMOffsets,
60 };
61 
62 #[cfg(feature = "gc")]
63 use super::GcHeapAllocationIndex;
64 #[cfg(feature = "gc")]
65 use crate::runtime::vm::{GcHeap, GcRuntime};
66 #[cfg(feature = "gc")]
67 use gc_heap_pool::GcHeapPool;
68 
69 #[cfg(feature = "async")]
70 use stack_pool::StackPool;
71 
72 #[cfg(feature = "component-model")]
73 use wasmtime_environ::{
74     component::{Component, VMComponentOffsets},
75     StaticModuleIndex,
76 };
77 
78 fn round_up_to_pow2(n: usize, to: usize) -> usize {
79     debug_assert!(to > 0);
80     debug_assert!(to.is_power_of_two());
81     (n + to - 1) & !(to - 1)
82 }
83 
84 /// Instance-related limit configuration for pooling.
85 ///
86 /// More docs on this can be found at `wasmtime::PoolingAllocationConfig`.
87 #[derive(Debug, Copy, Clone)]
88 pub struct InstanceLimits {
89     /// The maximum number of component instances that may be allocated
90     /// concurrently.
91     pub total_component_instances: u32,
92 
93     /// The maximum size of a component's `VMComponentContext`, not including
94     /// any of its inner core modules' `VMContext` sizes.
95     pub component_instance_size: usize,
96 
97     /// The maximum number of core module instances that may be allocated
98     /// concurrently.
99     pub total_core_instances: u32,
100 
101     /// The maximum number of core module instances that a single component may
102     /// transitively contain.
103     pub max_core_instances_per_component: u32,
104 
105     /// The maximum number of Wasm linear memories that a component may
106     /// transitively contain.
107     pub max_memories_per_component: u32,
108 
109     /// The maximum number of tables that a component may transitively contain.
110     pub max_tables_per_component: u32,
111 
112     /// The total number of linear memories in the pool, across all instances.
113     pub total_memories: u32,
114 
115     /// The total number of tables in the pool, across all instances.
116     pub total_tables: u32,
117 
118     /// The total number of async stacks in the pool, across all instances.
119     #[cfg(feature = "async")]
120     pub total_stacks: u32,
121 
122     /// Maximum size of a core instance's `VMContext`.
123     pub core_instance_size: usize,
124 
125     /// Maximum number of tables per instance.
126     pub max_tables_per_module: u32,
127 
128     /// Maximum number of table elements per table.
129     pub table_elements: u32,
130 
131     /// Maximum number of linear memories per instance.
132     pub max_memories_per_module: u32,
133 
134     /// Maximum number of Wasm pages for each linear memory.
135     pub memory_pages: u64,
136 
137     /// The total number of GC heaps in the pool, across all instances.
138     #[cfg(feature = "gc")]
139     pub total_gc_heaps: u32,
140 }
141 
142 impl Default for InstanceLimits {
143     fn default() -> Self {
144         // See doc comments for `wasmtime::PoolingAllocationConfig` for these
145         // default values
146         Self {
147             total_component_instances: 1000,
148             component_instance_size: 1 << 20, // 1 MiB
149             total_core_instances: 1000,
150             max_core_instances_per_component: 20,
151             max_memories_per_component: 20,
152             max_tables_per_component: 20,
153             total_memories: 1000,
154             total_tables: 1000,
155             #[cfg(feature = "async")]
156             total_stacks: 1000,
157             core_instance_size: 1 << 20, // 1 MiB
158             max_tables_per_module: 1,
159             table_elements: 10_000,
160             max_memories_per_module: 1,
161             memory_pages: 160,
162             #[cfg(feature = "gc")]
163             total_gc_heaps: 1000,
164         }
165     }
166 }
167 
168 /// Configuration options for the pooling instance allocator supplied at
169 /// construction.
170 #[derive(Copy, Clone, Debug)]
171 pub struct PoolingInstanceAllocatorConfig {
172     /// See `PoolingAllocatorConfig::max_unused_warm_slots` in `wasmtime`
173     pub max_unused_warm_slots: u32,
174     /// The size, in bytes, of async stacks to allocate (not including the guard
175     /// page).
176     pub stack_size: usize,
177     /// The limits to apply to instances allocated within this allocator.
178     pub limits: InstanceLimits,
179     /// Whether or not async stacks are zeroed after use.
180     pub async_stack_zeroing: bool,
181     /// If async stack zeroing is enabled and the host platform is Linux this is
182     /// how much memory to zero out with `memset`.
183     ///
184     /// The rest of memory will be zeroed out with `madvise`.
185     pub async_stack_keep_resident: usize,
186     /// How much linear memory, in bytes, to keep resident after resetting for
187     /// use with the next instance. This much memory will be `memset` to zero
188     /// when a linear memory is deallocated.
189     ///
190     /// Memory exceeding this amount in the wasm linear memory will be released
191     /// with `madvise` back to the kernel.
192     ///
193     /// Only applicable on Linux.
194     pub linear_memory_keep_resident: usize,
195     /// Same as `linear_memory_keep_resident` but for tables.
196     pub table_keep_resident: usize,
197     /// Whether to enable memory protection keys.
198     pub memory_protection_keys: MpkEnabled,
199     /// How many memory protection keys to allocate.
200     pub max_memory_protection_keys: usize,
201 }
202 
203 impl Default for PoolingInstanceAllocatorConfig {
204     fn default() -> PoolingInstanceAllocatorConfig {
205         PoolingInstanceAllocatorConfig {
206             max_unused_warm_slots: 100,
207             stack_size: 2 << 20,
208             limits: InstanceLimits::default(),
209             async_stack_zeroing: false,
210             async_stack_keep_resident: 0,
211             linear_memory_keep_resident: 0,
212             table_keep_resident: 0,
213             memory_protection_keys: MpkEnabled::Disable,
214             max_memory_protection_keys: 16,
215         }
216     }
217 }
218 
219 /// Implements the pooling instance allocator.
220 ///
221 /// This allocator internally maintains pools of instances, memories, tables,
222 /// and stacks.
223 ///
224 /// Note: the resource pools are manually dropped so that the fault handler
225 /// terminates correctly.
226 #[derive(Debug)]
227 pub struct PoolingInstanceAllocator {
228     limits: InstanceLimits,
229 
230     // The number of live core module and component instances at any given
231     // time. Note that this can temporarily go over the configured limit. This
232     // doesn't mean we have actually overshot, but that we attempted to allocate
233     // a new instance and incremented the counter, we've seen (or are about to
234     // see) that the counter is beyond the configured threshold, and are going
235     // to decrement the counter and return an error but haven't done so yet. See
236     // the increment trait methods for more details.
237     live_core_instances: AtomicU64,
238     live_component_instances: AtomicU64,
239 
240     memories: MemoryPool,
241     tables: TablePool,
242 
243     #[cfg(feature = "gc")]
244     gc_heaps: GcHeapPool,
245 
246     #[cfg(feature = "async")]
247     stacks: StackPool,
248 }
249 
250 impl Drop for PoolingInstanceAllocator {
251     fn drop(&mut self) {
252         debug_assert_eq!(self.live_component_instances.load(Ordering::Acquire), 0);
253         debug_assert_eq!(self.live_core_instances.load(Ordering::Acquire), 0);
254 
255         debug_assert!(self.memories.is_empty());
256         debug_assert!(self.tables.is_empty());
257 
258         #[cfg(feature = "gc")]
259         debug_assert!(self.gc_heaps.is_empty());
260 
261         #[cfg(feature = "async")]
262         debug_assert!(self.stacks.is_empty());
263     }
264 }
265 
266 impl PoolingInstanceAllocator {
267     /// Creates a new pooling instance allocator with the given strategy and limits.
268     pub fn new(config: &PoolingInstanceAllocatorConfig, tunables: &Tunables) -> Result<Self> {
269         Ok(Self {
270             limits: config.limits,
271             live_component_instances: AtomicU64::new(0),
272             live_core_instances: AtomicU64::new(0),
273             memories: MemoryPool::new(config, tunables)?,
274             tables: TablePool::new(config)?,
275             #[cfg(feature = "gc")]
276             gc_heaps: GcHeapPool::new(config)?,
277             #[cfg(feature = "async")]
278             stacks: StackPool::new(config)?,
279         })
280     }
281 
282     fn core_instance_size(&self) -> usize {
283         round_up_to_pow2(self.limits.core_instance_size, mem::align_of::<Instance>())
284     }
285 
286     fn validate_table_plans(&self, module: &Module) -> Result<()> {
287         self.tables.validate(module)
288     }
289 
290     fn validate_memory_plans(&self, module: &Module) -> Result<()> {
291         self.memories.validate(module)
292     }
293 
294     fn validate_core_instance_size(&self, offsets: &VMOffsets<HostPtr>) -> Result<()> {
295         let layout = Instance::alloc_layout(offsets);
296         if layout.size() <= self.core_instance_size() {
297             return Ok(());
298         }
299 
300         // If this `module` exceeds the allocation size allotted to it then an
301         // error will be reported here. The error of "required N bytes but
302         // cannot allocate that" is pretty opaque, however, because it's not
303         // clear what the breakdown of the N bytes are and what to optimize
304         // next. To help provide a better error message here some fancy-ish
305         // logic is done here to report the breakdown of the byte request into
306         // the largest portions and where it's coming from.
307         let mut message = format!(
308             "instance allocation for this module \
309              requires {} bytes which exceeds the configured maximum \
310              of {} bytes; breakdown of allocation requirement:\n\n",
311             layout.size(),
312             self.core_instance_size(),
313         );
314 
315         let mut remaining = layout.size();
316         let mut push = |name: &str, bytes: usize| {
317             assert!(remaining >= bytes);
318             remaining -= bytes;
319 
320             // If the `name` region is more than 5% of the allocation request
321             // then report it here, otherwise ignore it. We have less than 20
322             // fields so we're guaranteed that something should be reported, and
323             // otherwise it's not particularly interesting to learn about 5
324             // different fields that are all 8 or 0 bytes. Only try to report
325             // the "major" sources of bytes here.
326             if bytes > layout.size() / 20 {
327                 message.push_str(&format!(
328                     " * {:.02}% - {} bytes - {}\n",
329                     ((bytes as f32) / (layout.size() as f32)) * 100.0,
330                     bytes,
331                     name,
332                 ));
333             }
334         };
335 
336         // The `Instance` itself requires some size allocated to it.
337         push("instance state management", mem::size_of::<Instance>());
338 
339         // Afterwards the `VMContext`'s regions are why we're requesting bytes,
340         // so ask it for descriptions on each region's byte size.
341         for (desc, size) in offsets.region_sizes() {
342             push(desc, size as usize);
343         }
344 
345         // double-check we accounted for all the bytes
346         assert_eq!(remaining, 0);
347 
348         bail!("{}", message)
349     }
350 
351     #[cfg(feature = "component-model")]
352     fn validate_component_instance_size(
353         &self,
354         offsets: &VMComponentOffsets<HostPtr>,
355     ) -> Result<()> {
356         if usize::try_from(offsets.size_of_vmctx()).unwrap() <= self.limits.component_instance_size
357         {
358             return Ok(());
359         }
360 
361         // TODO: Add context with detailed accounting of what makes up all the
362         // `VMComponentContext`'s space like we do for module instances.
363         bail!(
364             "instance allocation for this component requires {} bytes of `VMComponentContext` \
365              space which exceeds the configured maximum of {} bytes",
366             offsets.size_of_vmctx(),
367             self.limits.component_instance_size
368         )
369     }
370 }
371 
372 unsafe impl InstanceAllocatorImpl for PoolingInstanceAllocator {
373     #[cfg(feature = "component-model")]
374     fn validate_component_impl<'a>(
375         &self,
376         component: &Component,
377         offsets: &VMComponentOffsets<HostPtr>,
378         get_module: &'a dyn Fn(StaticModuleIndex) -> &'a Module,
379     ) -> Result<()> {
380         self.validate_component_instance_size(offsets)?;
381 
382         let mut num_core_instances = 0;
383         let mut num_memories = 0;
384         let mut num_tables = 0;
385         for init in &component.initializers {
386             use wasmtime_environ::component::GlobalInitializer::*;
387             use wasmtime_environ::component::InstantiateModule;
388             match init {
389                 InstantiateModule(InstantiateModule::Import(_, _)) => {
390                     num_core_instances += 1;
391                     // Can't statically account for the total vmctx size, number
392                     // of memories, and number of tables in this component.
393                 }
394                 InstantiateModule(InstantiateModule::Static(static_module_index, _)) => {
395                     let module = get_module(*static_module_index);
396                     let offsets = VMOffsets::new(HostPtr, &module);
397                     self.validate_module_impl(module, &offsets)?;
398                     num_core_instances += 1;
399                     num_memories += module.memory_plans.len() - module.num_imported_memories;
400                     num_tables += module.table_plans.len() - module.num_imported_tables;
401                 }
402                 LowerImport { .. }
403                 | ExtractMemory(_)
404                 | ExtractRealloc(_)
405                 | ExtractPostReturn(_)
406                 | Resource(_) => {}
407             }
408         }
409 
410         if num_core_instances
411             > usize::try_from(self.limits.max_core_instances_per_component).unwrap()
412         {
413             bail!(
414                 "The component transitively contains {num_core_instances} core module instances, \
415                  which exceeds the configured maximum of {}",
416                 self.limits.max_core_instances_per_component
417             );
418         }
419 
420         if num_memories > usize::try_from(self.limits.max_memories_per_component).unwrap() {
421             bail!(
422                 "The component transitively contains {num_memories} Wasm linear memories, which \
423                  exceeds the configured maximum of {}",
424                 self.limits.max_memories_per_component
425             );
426         }
427 
428         if num_tables > usize::try_from(self.limits.max_tables_per_component).unwrap() {
429             bail!(
430                 "The component transitively contains {num_tables} tables, which exceeds the \
431                  configured maximum of {}",
432                 self.limits.max_tables_per_component
433             );
434         }
435 
436         Ok(())
437     }
438 
439     fn validate_module_impl(&self, module: &Module, offsets: &VMOffsets<HostPtr>) -> Result<()> {
440         self.validate_memory_plans(module)?;
441         self.validate_table_plans(module)?;
442         self.validate_core_instance_size(offsets)?;
443         Ok(())
444     }
445 
446     fn increment_component_instance_count(&self) -> Result<()> {
447         let old_count = self.live_component_instances.fetch_add(1, Ordering::AcqRel);
448         if old_count >= u64::from(self.limits.total_component_instances) {
449             self.decrement_component_instance_count();
450             bail!(
451                 "maximum concurrent component instance limit of {} reached",
452                 self.limits.total_component_instances
453             );
454         }
455         Ok(())
456     }
457 
458     fn decrement_component_instance_count(&self) {
459         self.live_component_instances.fetch_sub(1, Ordering::AcqRel);
460     }
461 
462     fn increment_core_instance_count(&self) -> Result<()> {
463         let old_count = self.live_core_instances.fetch_add(1, Ordering::AcqRel);
464         if old_count >= u64::from(self.limits.total_core_instances) {
465             self.decrement_core_instance_count();
466             bail!(
467                 "maximum concurrent core instance limit of {} reached",
468                 self.limits.total_core_instances
469             );
470         }
471         Ok(())
472     }
473 
474     fn decrement_core_instance_count(&self) {
475         self.live_core_instances.fetch_sub(1, Ordering::AcqRel);
476     }
477 
478     unsafe fn allocate_memory(
479         &self,
480         request: &mut InstanceAllocationRequest,
481         memory_plan: &MemoryPlan,
482         memory_index: DefinedMemoryIndex,
483     ) -> Result<(MemoryAllocationIndex, Memory)> {
484         self.memories.allocate(request, memory_plan, memory_index)
485     }
486 
487     unsafe fn deallocate_memory(
488         &self,
489         _memory_index: DefinedMemoryIndex,
490         allocation_index: MemoryAllocationIndex,
491         memory: Memory,
492     ) {
493         self.memories.deallocate(allocation_index, memory);
494     }
495 
496     unsafe fn allocate_table(
497         &self,
498         request: &mut InstanceAllocationRequest,
499         table_plan: &TablePlan,
500         _table_index: DefinedTableIndex,
501     ) -> Result<(super::TableAllocationIndex, Table)> {
502         self.tables.allocate(request, table_plan)
503     }
504 
505     unsafe fn deallocate_table(
506         &self,
507         _table_index: DefinedTableIndex,
508         allocation_index: TableAllocationIndex,
509         table: Table,
510     ) {
511         self.tables.deallocate(allocation_index, table);
512     }
513 
514     #[cfg(feature = "async")]
515     fn allocate_fiber_stack(&self) -> Result<wasmtime_fiber::FiberStack> {
516         self.stacks.allocate()
517     }
518 
519     #[cfg(feature = "async")]
520     unsafe fn deallocate_fiber_stack(&self, stack: &wasmtime_fiber::FiberStack) {
521         self.stacks.deallocate(stack);
522     }
523 
524     fn purge_module(&self, module: CompiledModuleId) {
525         self.memories.purge_module(module);
526     }
527 
528     fn next_available_pkey(&self) -> Option<ProtectionKey> {
529         self.memories.next_available_pkey()
530     }
531 
532     fn restrict_to_pkey(&self, pkey: ProtectionKey) {
533         mpk::allow(ProtectionMask::zero().or(pkey));
534     }
535 
536     fn allow_all_pkeys(&self) {
537         mpk::allow(ProtectionMask::all());
538     }
539 
540     #[cfg(feature = "gc")]
541     fn allocate_gc_heap(
542         &self,
543         gc_runtime: &dyn GcRuntime,
544     ) -> Result<(GcHeapAllocationIndex, Box<dyn GcHeap>)> {
545         self.gc_heaps.allocate(gc_runtime)
546     }
547 
548     #[cfg(feature = "gc")]
549     fn deallocate_gc_heap(
550         &self,
551         allocation_index: GcHeapAllocationIndex,
552         gc_heap: Box<dyn GcHeap>,
553     ) {
554         self.gc_heaps.deallocate(allocation_index, gc_heap);
555     }
556 }
557 
558 #[cfg(test)]
559 mod test {
560     use super::*;
561 
562     #[test]
563     fn test_pooling_allocator_with_memory_pages_exceeded() {
564         let config = PoolingInstanceAllocatorConfig {
565             limits: InstanceLimits {
566                 total_memories: 1,
567                 memory_pages: 0x10001,
568                 ..Default::default()
569             },
570             ..PoolingInstanceAllocatorConfig::default()
571         };
572         assert_eq!(
573             PoolingInstanceAllocator::new(
574                 &config,
575                 &Tunables {
576                     static_memory_bound: 1,
577                     ..Tunables::default_host()
578                 },
579             )
580             .map_err(|e| e.to_string())
581             .expect_err("expected a failure constructing instance allocator"),
582             "module memory page limit of 65537 exceeds the maximum of 65536"
583         );
584     }
585 
586     #[cfg(all(unix, target_pointer_width = "64", feature = "async", not(miri)))]
587     #[test]
588     fn test_stack_zeroed() -> Result<()> {
589         let config = PoolingInstanceAllocatorConfig {
590             max_unused_warm_slots: 0,
591             limits: InstanceLimits {
592                 total_stacks: 1,
593                 total_memories: 0,
594                 total_tables: 0,
595                 ..Default::default()
596             },
597             stack_size: 128,
598             async_stack_zeroing: true,
599             ..PoolingInstanceAllocatorConfig::default()
600         };
601         let allocator = PoolingInstanceAllocator::new(&config, &Tunables::default_host())?;
602 
603         unsafe {
604             for _ in 0..255 {
605                 let stack = allocator.allocate_fiber_stack()?;
606 
607                 // The stack pointer is at the top, so decrement it first
608                 let addr = stack.top().unwrap().sub(1);
609 
610                 assert_eq!(*addr, 0);
611                 *addr = 1;
612 
613                 allocator.deallocate_fiber_stack(&stack);
614             }
615         }
616 
617         Ok(())
618     }
619 
620     #[cfg(all(unix, target_pointer_width = "64", feature = "async", not(miri)))]
621     #[test]
622     fn test_stack_unzeroed() -> Result<()> {
623         let config = PoolingInstanceAllocatorConfig {
624             max_unused_warm_slots: 0,
625             limits: InstanceLimits {
626                 total_stacks: 1,
627                 total_memories: 0,
628                 total_tables: 0,
629                 ..Default::default()
630             },
631             stack_size: 128,
632             async_stack_zeroing: false,
633             ..PoolingInstanceAllocatorConfig::default()
634         };
635         let allocator = PoolingInstanceAllocator::new(&config, &Tunables::default_host())?;
636 
637         unsafe {
638             for i in 0..255 {
639                 let stack = allocator.allocate_fiber_stack()?;
640 
641                 // The stack pointer is at the top, so decrement it first
642                 let addr = stack.top().unwrap().sub(1);
643 
644                 assert_eq!(*addr, i);
645                 *addr = i + 1;
646 
647                 allocator.deallocate_fiber_stack(&stack);
648             }
649         }
650 
651         Ok(())
652     }
653 }
654