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