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