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