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