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