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