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