diff --git a/benches/instantiation.rs b/benches/instantiation.rs index d7cb53146b..2e919c4d87 100644 --- a/benches/instantiation.rs +++ b/benches/instantiation.rs @@ -209,13 +209,10 @@ fn strategies() -> impl Iterator { InstanceAllocationStrategy::OnDemand, InstanceAllocationStrategy::Pooling { strategy: Default::default(), - module_limits: ModuleLimits { - functions: 40_000, - memory_pages: 1_000, - types: 200, - ..ModuleLimits::default() + instance_limits: InstanceLimits { + memory_pages: 10_000, + ..Default::default() }, - instance_limits: InstanceLimits::default(), }, ]) } diff --git a/benches/thread_eager_init.rs b/benches/thread_eager_init.rs index 9a7971d16e..9bbfeb0414 100644 --- a/benches/thread_eager_init.rs +++ b/benches/thread_eager_init.rs @@ -94,11 +94,11 @@ fn test_setup() -> (Engine, Module) { let mut config = Config::new(); config.allocation_strategy(InstanceAllocationStrategy::Pooling { strategy: PoolingAllocationStrategy::NextAvailable, - module_limits: ModuleLimits { + instance_limits: InstanceLimits { + count: pool_count, memory_pages: 1, ..Default::default() }, - instance_limits: InstanceLimits { count: pool_count }, }); let engine = Engine::new(&config).unwrap(); diff --git a/crates/environ/src/vmoffsets.rs b/crates/environ/src/vmoffsets.rs index d30f6b9244..8092f8d705 100644 --- a/crates/environ/src/vmoffsets.rs +++ b/crates/environ/src/vmoffsets.rs @@ -154,6 +154,67 @@ impl VMOffsets

{ pub fn pointer_size(&self) -> u8 { self.ptr.size() } + + /// Returns an iterator which provides a human readable description and a + /// byte size. The iterator returned will iterate over the bytes allocated + /// to the entire `VMOffsets` structure to explain where each byte size is + /// coming from. + pub fn region_sizes(&self) -> impl Iterator { + macro_rules! calculate_sizes { + ($($name:ident: $desc:tt,)*) => {{ + let VMOffsets { + // These fields are metadata not talking about specific + // offsets of specific fields. + ptr: _, + num_imported_functions: _, + num_imported_tables: _, + num_imported_memories: _, + num_imported_globals: _, + num_defined_tables: _, + num_defined_globals: _, + num_defined_memories: _, + num_defined_functions: _, + + // used as the initial size below + size, + + // exhaustively match teh rest of the fields with input from + // the macro + $($name,)* + } = *self; + + // calculate the size of each field by relying on the inputs to + // the macro being in reverse order and determining the size of + // the field as the offset from the field to the last field. + let mut last = size; + $( + assert!($name <= last); + let tmp = $name; + let $name = last - $name; + last = tmp; + )* + assert_eq!(last, 0); + IntoIterator::into_iter([$(($desc, $name),)*]) + }}; + } + + calculate_sizes! { + defined_anyfuncs: "module functions", + defined_globals: "defined globals", + defined_memories: "defined memories", + defined_tables: "defined tables", + imported_globals: "imported globals", + imported_memories: "imported memories", + imported_tables: "imported tables", + imported_functions: "imported functions", + signature_ids: "module types", + builtin_functions: "jit builtin functions state", + store: "jit store state", + externref_activations_table: "jit host externref state", + epoch_ptr: "jit current epoch state", + interrupts: "jit interrupt state", + } + } } impl From> for VMOffsets

{ diff --git a/crates/fuzzing/src/generators.rs b/crates/fuzzing/src/generators.rs index b946235cf3..4d641a6b21 100644 --- a/crates/fuzzing/src/generators.rs +++ b/crates/fuzzing/src/generators.rs @@ -60,79 +60,28 @@ impl PoolingAllocationStrategy { } } } - -/// Configuration for `wasmtime::ModuleLimits`. -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub struct ModuleLimits { - imported_functions: u32, - imported_tables: u32, - imported_memories: u32, - imported_globals: u32, - types: u32, - functions: u32, - tables: u32, - memories: u32, - /// The maximum number of globals that can be defined in a module. - pub globals: u32, - table_elements: u32, - memory_pages: u64, -} - -impl ModuleLimits { - fn to_wasmtime(&self) -> wasmtime::ModuleLimits { - wasmtime::ModuleLimits { - imported_functions: self.imported_functions, - imported_tables: self.imported_tables, - imported_memories: self.imported_memories, - imported_globals: self.imported_globals, - types: self.types, - functions: self.functions, - tables: self.tables, - memories: self.memories, - globals: self.globals, - table_elements: self.table_elements, - memory_pages: self.memory_pages, - } - } -} - -impl<'a> Arbitrary<'a> for ModuleLimits { - fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { - const MAX_IMPORTS: u32 = 1000; - const MAX_TYPES: u32 = 1000; - const MAX_FUNCTIONS: u32 = 1000; - const MAX_TABLES: u32 = 10; - const MAX_MEMORIES: u32 = 10; - const MAX_GLOBALS: u32 = 1000; - const MAX_ELEMENTS: u32 = 1000; - const MAX_MEMORY_PAGES: u64 = 160; // 10 MiB - - Ok(Self { - imported_functions: u.int_in_range(0..=MAX_IMPORTS)?, - imported_tables: u.int_in_range(0..=MAX_IMPORTS)?, - imported_memories: u.int_in_range(0..=MAX_IMPORTS)?, - imported_globals: u.int_in_range(0..=MAX_IMPORTS)?, - types: u.int_in_range(0..=MAX_TYPES)?, - functions: u.int_in_range(0..=MAX_FUNCTIONS)?, - tables: u.int_in_range(0..=MAX_TABLES)?, - memories: u.int_in_range(0..=MAX_MEMORIES)?, - globals: u.int_in_range(0..=MAX_GLOBALS)?, - table_elements: u.int_in_range(0..=MAX_ELEMENTS)?, - memory_pages: u.int_in_range(0..=MAX_MEMORY_PAGES)?, - }) - } -} - /// Configuration for `wasmtime::PoolingAllocationStrategy`. #[derive(Debug, Clone, Eq, PartialEq, Hash)] +#[allow(missing_docs)] pub struct InstanceLimits { - /// The maximum number of instances that can be instantiated in the pool at a time. pub count: u32, + pub memories: u32, + pub tables: u32, + pub memory_pages: u64, + pub table_elements: u32, + pub size: usize, } impl InstanceLimits { fn to_wasmtime(&self) -> wasmtime::InstanceLimits { - wasmtime::InstanceLimits { count: self.count } + wasmtime::InstanceLimits { + count: self.count, + memories: self.memories, + tables: self.tables, + memory_pages: self.memory_pages, + table_elements: self.table_elements, + size: self.size, + } } } @@ -140,8 +89,19 @@ impl<'a> Arbitrary<'a> for InstanceLimits { fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { const MAX_COUNT: u32 = 100; + const MAX_TABLES: u32 = 10; + const MAX_MEMORIES: u32 = 10; + const MAX_ELEMENTS: u32 = 1000; + const MAX_MEMORY_PAGES: u64 = 160; // 10 MiB + const MAX_SIZE: usize = 1 << 20; // 1 MiB + Ok(Self { + tables: u.int_in_range(0..=MAX_TABLES)?, + memories: u.int_in_range(0..=MAX_MEMORIES)?, + table_elements: u.int_in_range(0..=MAX_ELEMENTS)?, + memory_pages: u.int_in_range(0..=MAX_MEMORY_PAGES)?, count: u.int_in_range(1..=MAX_COUNT)?, + size: u.int_in_range(0..=MAX_SIZE)?, }) } } @@ -155,8 +115,6 @@ pub enum InstanceAllocationStrategy { Pooling { /// The pooling strategy to use. strategy: PoolingAllocationStrategy, - /// The module limits. - module_limits: ModuleLimits, /// The instance limits. instance_limits: InstanceLimits, }, @@ -168,11 +126,9 @@ impl InstanceAllocationStrategy { InstanceAllocationStrategy::OnDemand => wasmtime::InstanceAllocationStrategy::OnDemand, InstanceAllocationStrategy::Pooling { strategy, - module_limits, instance_limits, } => wasmtime::InstanceAllocationStrategy::Pooling { strategy: strategy.to_wasmtime(), - module_limits: module_limits.to_wasmtime(), instance_limits: instance_limits.to_wasmtime(), }, } @@ -203,7 +159,7 @@ impl<'a> Arbitrary<'a> for Config { // If using the pooling allocator, constrain the memory and module configurations // to the module limits. if let InstanceAllocationStrategy::Pooling { - module_limits: limits, + instance_limits: limits, .. } = &config.wasmtime.strategy { @@ -223,14 +179,6 @@ impl<'a> Arbitrary<'a> for Config { }; let cfg = &mut config.module_config.config; - cfg.max_imports = limits.imported_functions.min( - limits - .imported_globals - .min(limits.imported_memories.min(limits.imported_tables)), - ) as usize; - cfg.max_types = limits.types as usize; - cfg.max_funcs = limits.functions as usize; - cfg.max_globals = limits.globals as usize; cfg.max_memories = limits.memories as usize; cfg.max_tables = limits.tables as usize; cfg.max_memory_pages = limits.memory_pages; @@ -343,21 +291,13 @@ impl Config { config.simd_enabled = false; config.memory64_enabled = false; - // If using the pooling allocator, update the module limits too + // If using the pooling allocator, update the instance limits too if let InstanceAllocationStrategy::Pooling { - module_limits: limits, + instance_limits: limits, .. } = &mut self.wasmtime.strategy { - // No imports - limits.imported_functions = 0; - limits.imported_tables = 0; - limits.imported_memories = 0; - limits.imported_globals = 0; - - // One type, one function, and one single-page memory - limits.types = 1; - limits.functions = 1; + // One single-page memory limits.memories = 1; limits.memory_pages = 1; @@ -385,13 +325,6 @@ impl Config { if let Some(default_fuel) = default_fuel { module.ensure_termination(default_fuel); - - // Bump the allowed global count by 1 - if let InstanceAllocationStrategy::Pooling { module_limits, .. } = - &mut self.wasmtime.strategy - { - module_limits.globals += 1; - } } Ok(module) @@ -408,7 +341,7 @@ impl Config { config.max_memories = 1; if let InstanceAllocationStrategy::Pooling { - module_limits: limits, + instance_limits: limits, .. } = &mut self.wasmtime.strategy { diff --git a/crates/runtime/src/instance/allocator.rs b/crates/runtime/src/instance/allocator.rs index 3428d76c8d..8a7a796c8c 100644 --- a/crates/runtime/src/instance/allocator.rs +++ b/crates/runtime/src/instance/allocator.rs @@ -23,9 +23,7 @@ use wasmtime_environ::{ mod pooling; #[cfg(feature = "pooling-allocator")] -pub use self::pooling::{ - InstanceLimits, ModuleLimits, PoolingAllocationStrategy, PoolingInstanceAllocator, -}; +pub use self::pooling::{InstanceLimits, PoolingAllocationStrategy, PoolingInstanceAllocator}; /// Represents a request for a new runtime instance. pub struct InstanceAllocationRequest<'a> { diff --git a/crates/runtime/src/instance/allocator/pooling.rs b/crates/runtime/src/instance/allocator/pooling.rs index ef9bb33455..f1e2a4c52e 100644 --- a/crates/runtime/src/instance/allocator/pooling.rs +++ b/crates/runtime/src/instance/allocator/pooling.rs @@ -19,8 +19,8 @@ use std::convert::TryFrom; use std::mem; use std::sync::Mutex; use wasmtime_environ::{ - DefinedMemoryIndex, DefinedTableIndex, HostPtr, MemoryStyle, Module, PrimaryMap, Tunables, - VMOffsets, VMOffsetsFields, WASM_PAGE_SIZE, + DefinedMemoryIndex, DefinedTableIndex, HostPtr, Module, PrimaryMap, Tunables, VMOffsets, + WASM_PAGE_SIZE, }; mod index_allocator; @@ -57,190 +57,122 @@ fn round_up_to_pow2(n: usize, to: usize) -> usize { (n + to - 1) & !(to - 1) } -/// Represents the limits placed on a module for compiling with the pooling instance allocator. -#[derive(Debug, Copy, Clone)] -pub struct ModuleLimits { - /// The maximum number of imported functions for a module. - pub imported_functions: u32, - - /// The maximum number of imported tables for a module. - pub imported_tables: u32, - - /// The maximum number of imported linear memories for a module. - pub imported_memories: u32, - - /// The maximum number of imported globals for a module. - pub imported_globals: u32, - - /// The maximum number of defined types for a module. - pub types: u32, - - /// The maximum number of defined functions for a module. - pub functions: u32, - - /// The maximum number of defined tables for a module. - pub tables: u32, - - /// The maximum number of defined linear memories for a module. - pub memories: u32, - - /// The maximum number of defined globals for a module. - pub globals: u32, - - /// The maximum table elements for any table defined in a module. - pub table_elements: u32, - - /// The maximum number of pages for any linear memory defined in a module. - pub memory_pages: u64, -} - -impl ModuleLimits { - fn validate(&self, module: &Module) -> Result<()> { - if module.num_imported_funcs > self.imported_functions as usize { - bail!( - "imported function count of {} exceeds the limit of {}", - module.num_imported_funcs, - self.imported_functions - ); - } - - if module.num_imported_tables > self.imported_tables as usize { - bail!( - "imported tables count of {} exceeds the limit of {}", - module.num_imported_tables, - self.imported_tables - ); - } - - if module.num_imported_memories > self.imported_memories as usize { - bail!( - "imported memories count of {} exceeds the limit of {}", - module.num_imported_memories, - self.imported_memories - ); - } - - if module.num_imported_globals > self.imported_globals as usize { - bail!( - "imported globals count of {} exceeds the limit of {}", - module.num_imported_globals, - self.imported_globals - ); - } - - if module.types.len() > self.types as usize { - bail!( - "defined types count of {} exceeds the limit of {}", - module.types.len(), - self.types - ); - } - - let functions = module.functions.len() - module.num_imported_funcs; - if functions > self.functions as usize { - bail!( - "defined functions count of {} exceeds the limit of {}", - functions, - self.functions - ); - } - - let tables = module.table_plans.len() - module.num_imported_tables; - if tables > self.tables as usize { - bail!( - "defined tables count of {} exceeds the limit of {}", - tables, - self.tables - ); - } - - let memories = module.memory_plans.len() - module.num_imported_memories; - if memories > self.memories as usize { - bail!( - "defined memories count of {} exceeds the limit of {}", - memories, - self.memories - ); - } - - let globals = module.globals.len() - module.num_imported_globals; - if globals > self.globals as usize { - bail!( - "defined globals count of {} exceeds the limit of {}", - globals, - self.globals - ); - } - - for (i, plan) in module.table_plans.values().as_slice()[module.num_imported_tables..] - .iter() - .enumerate() - { - if plan.table.minimum > self.table_elements { - bail!( - "table index {} has a minimum element size of {} which exceeds the limit of {}", - i, - plan.table.minimum, - self.table_elements - ); - } - } - - for (i, plan) in module.memory_plans.values().as_slice()[module.num_imported_memories..] - .iter() - .enumerate() - { - if plan.memory.minimum > self.memory_pages { - bail!( - "memory index {} has a minimum page size of {} which exceeds the limit of {}", - i, - plan.memory.minimum, - self.memory_pages - ); - } - - if let MemoryStyle::Dynamic { .. } = plan.style { - bail!( - "memory index {} has an unsupported dynamic memory plan style", - i, - ); - } - } - - Ok(()) - } -} - -impl Default for ModuleLimits { - fn default() -> Self { - // See doc comments for `wasmtime::ModuleLimits` for these default values - Self { - imported_functions: 1000, - imported_tables: 0, - imported_memories: 0, - imported_globals: 0, - types: 100, - functions: 10000, - tables: 1, - memories: 1, - globals: 10, - table_elements: 10000, - memory_pages: 160, - } - } -} - /// Represents the limits placed on instances by the pooling instance allocator. #[derive(Debug, Copy, Clone)] pub struct InstanceLimits { - /// The maximum number of concurrent instances supported. + /// The maximum number of concurrent instances supported (default is 1000). + /// + /// This value has a direct impact on the amount of memory allocated by the pooling + /// instance allocator. + /// + /// The pooling instance allocator allocates three memory pools with sizes depending on this value: + /// + /// * An instance pool, where each entry in the pool can store the runtime representation + /// of an instance, including a maximal `VMContext` structure. + /// + /// * A memory pool, where each entry in the pool contains the reserved address space for each + /// linear memory supported by an instance. + /// + /// * A table pool, where each entry in the pool contains the space needed for each WebAssembly table + /// supported by an instance (see `table_elements` to control the size of each table). + /// + /// Additionally, this value will also control the maximum number of execution stacks allowed for + /// asynchronous execution (one per instance), when enabled. + /// + /// The memory pool will reserve a large quantity of host process address space to elide the bounds + /// checks required for correct WebAssembly memory semantics. Even for 64-bit address spaces, the + /// address space is limited when dealing with a large number of supported instances. + /// + /// For example, on Linux x86_64, the userland address space limit is 128 TiB. That might seem like a lot, + /// but each linear memory will *reserve* 6 GiB of space by default. Multiply that by the number of linear + /// memories each instance supports and then by the number of supported instances and it becomes apparent + /// that address space can be exhausted depending on the number of supported instances. pub count: u32, + + /// The maximum size, in bytes, allocated for an instance and its + /// `VMContext`. + /// + /// This amount of space is pre-allocated for `count` number of instances + /// and is used to store the runtime `wasmtime_runtime::Instance` structure + /// along with its adjacent `VMContext` structure. The `Instance` type has a + /// static size but `VMContext` is dynamically sized depending on the module + /// being instantiated. This size limit loosely correlates to the size of + /// the wasm module, taking into account factors such as: + /// + /// * number of functions + /// * number of globals + /// * number of memories + /// * number of tables + /// * number of function types + /// + /// If the allocated size per instance is too small then instantiation of a + /// module will fail at runtime with an error indicating how many bytes were + /// needed. This amount of bytes are committed to memory per-instance when + /// a pooling allocator is created. + /// + /// The default value for this is 1MB. + pub size: usize, + + /// The maximum number of defined tables for a module (default is 1). + /// + /// This value controls the capacity of the `VMTableDefinition` table in each instance's + /// `VMContext` structure. + /// + /// The allocated size of the table will be `tables * sizeof(VMTableDefinition)` for each + /// instance regardless of how many tables are defined by an instance's module. + pub tables: u32, + + /// The maximum table elements for any table defined in a module (default is 10000). + /// + /// If a table's minimum element limit is greater than this value, the module will + /// fail to instantiate. + /// + /// If a table's maximum element limit is unbounded or greater than this value, + /// the maximum will be `table_elements` for the purpose of any `table.grow` instruction. + /// + /// This value is used to reserve the maximum space for each supported table; table elements + /// are pointer-sized in the Wasmtime runtime. Therefore, the space reserved for each instance + /// is `tables * table_elements * sizeof::<*const ()>`. + pub table_elements: u32, + + /// The maximum number of defined linear memories for a module (default is 1). + /// + /// This value controls the capacity of the `VMMemoryDefinition` table in each instance's + /// `VMContext` structure. + /// + /// The allocated size of the table will be `memories * sizeof(VMMemoryDefinition)` for each + /// instance regardless of how many memories are defined by an instance's module. + pub memories: u32, + + /// The maximum number of pages for any linear memory defined in a module (default is 160). + /// + /// The default of 160 means at most 10 MiB of host memory may be committed for each instance. + /// + /// If a memory's minimum page limit is greater than this value, the module will + /// fail to instantiate. + /// + /// If a memory's maximum page limit is unbounded or greater than this value, + /// the maximum will be `memory_pages` for the purpose of any `memory.grow` instruction. + /// + /// This value is used to control the maximum accessible space for each linear memory of an instance. + /// + /// The reservation size of each linear memory is controlled by the + /// `static_memory_maximum_size` setting and this value cannot + /// exceed the configured static memory maximum size. + pub memory_pages: u64, } impl Default for InstanceLimits { fn default() -> Self { // See doc comments for `wasmtime::InstanceLimits` for these default values - Self { count: 1000 } + Self { + count: 1000, + size: 1 << 20, // 1 MB + tables: 1, + table_elements: 10_000, + memories: 1, + memory_pages: 160, + } } } @@ -289,37 +221,21 @@ struct InstancePool { impl InstancePool { fn new( strategy: PoolingAllocationStrategy, - module_limits: &ModuleLimits, instance_limits: &InstanceLimits, tunables: &Tunables, ) -> Result { let page_size = region::page::size(); - // Calculate the maximum size of an Instance structure given the limits - let offsets = VMOffsets::from(VMOffsetsFields { - ptr: HostPtr, - num_imported_functions: module_limits.imported_functions, - num_imported_tables: module_limits.imported_tables, - num_imported_memories: module_limits.imported_memories, - num_imported_globals: module_limits.imported_globals, - num_defined_functions: module_limits.functions, - num_defined_tables: module_limits.tables, - num_defined_memories: module_limits.memories, - num_defined_globals: module_limits.globals, - }); - - let instance_size = round_up_to_pow2( - mem::size_of::() - .checked_add(offsets.size_of_vmctx() as usize) - .ok_or_else(|| anyhow!("instance size exceeds addressable memory"))?, - page_size, - ); + let instance_size = round_up_to_pow2(instance_limits.size, mem::align_of::()); let max_instances = instance_limits.count as usize; - let allocation_size = instance_size - .checked_mul(max_instances) - .ok_or_else(|| anyhow!("total size of instance data exceeds addressable memory"))?; + let allocation_size = round_up_to_pow2( + instance_size + .checked_mul(max_instances) + .ok_or_else(|| anyhow!("total size of instance data exceeds addressable memory"))?, + page_size, + ); let mapping = Mmap::accessible_reserved(allocation_size, allocation_size) .context("failed to create instance pool mapping")?; @@ -329,8 +245,8 @@ impl InstancePool { instance_size, max_instances, index_allocator: Mutex::new(PoolingAllocationState::new(strategy, max_instances)), - memories: MemoryPool::new(module_limits, instance_limits, tunables)?, - tables: TablePool::new(module_limits, instance_limits)?, + memories: MemoryPool::new(instance_limits, tunables)?, + tables: TablePool::new(instance_limits)?, }; Ok(pool) @@ -348,6 +264,15 @@ impl InstancePool { ) -> Result { let module = req.runtime_info.module(); + // Before doing anything else ensure that our instance slot is actually + // big enough to hold the `Instance` and `VMContext` for this instance. + // If this fails then it's a configuration error at the `Engine` level + // from when this pooling allocator was created and that needs updating + // if this is to succeed. + let offsets = self + .validate_instance_size(module) + .map_err(InstantiationError::Resource)?; + let mut memories = PrimaryMap::with_capacity(module.memory_plans.len() - module.num_imported_memories); let mut tables = @@ -372,7 +297,7 @@ impl InstancePool { Instance::new_at( instance_ptr, self.instance_size, - VMOffsets::new(HostPtr, module), + offsets, req, memories, tables, @@ -459,6 +384,9 @@ impl InstancePool { ) -> Result<(), InstantiationError> { let module = runtime_info.module(); + self.validate_memory_plans(module) + .map_err(InstantiationError::Resource)?; + for (memory_index, plan) in module .memory_plans .iter() @@ -471,7 +399,7 @@ impl InstancePool { let memory = unsafe { std::slice::from_raw_parts_mut( self.memories.get_base(instance_index, defined_index), - (self.memories.max_wasm_pages as usize) * (WASM_PAGE_SIZE as usize), + self.memories.max_memory_size, ) }; @@ -574,6 +502,10 @@ impl InstancePool { tables: &mut PrimaryMap, ) -> Result<(), InstantiationError> { let module = runtime_info.module(); + + self.validate_table_plans(module) + .map_err(InstantiationError::Resource)?; + let mut bases = self.tables.get(instance_index); for (_, plan) in module.table_plans.iter().skip(module.num_imported_tables) { let base = bases.next().unwrap() as _; @@ -618,6 +550,115 @@ impl InstancePool { decommit_table_pages(base, size).expect("failed to decommit table pages"); } } + + fn validate_table_plans(&self, module: &Module) -> Result<()> { + let tables = module.table_plans.len() - module.num_imported_tables; + if tables > self.tables.max_tables { + bail!( + "defined tables count of {} exceeds the limit of {}", + tables, + self.tables.max_tables, + ); + } + + for (i, plan) in module.table_plans.iter().skip(module.num_imported_tables) { + if plan.table.minimum > self.tables.max_elements { + bail!( + "table index {} has a minimum element size of {} which exceeds the limit of {}", + i.as_u32(), + plan.table.minimum, + self.tables.max_elements, + ); + } + } + Ok(()) + } + + fn validate_memory_plans(&self, module: &Module) -> Result<()> { + let memories = module.memory_plans.len() - module.num_imported_memories; + if memories > self.memories.max_memories { + bail!( + "defined memories count of {} exceeds the limit of {}", + memories, + self.memories.max_memories, + ); + } + + for (i, plan) in module + .memory_plans + .iter() + .skip(module.num_imported_memories) + { + let max = self.memories.max_memory_size / (WASM_PAGE_SIZE as usize); + if plan.memory.minimum > (max as u64) { + bail!( + "memory index {} has a minimum page size of {} which exceeds the limit of {}", + i.as_u32(), + plan.memory.minimum, + max, + ); + } + } + Ok(()) + } + + fn validate_instance_size(&self, module: &Module) -> Result> { + let offsets = VMOffsets::new(HostPtr, module); + let layout = Instance::alloc_layout(&offsets); + if layout.size() <= self.instance_size { + return Ok(offsets); + } + + // If this `module` exceeds the allocation size allotted to it then an + // error will be reported here. The error of "required N bytes but + // cannot allocate that" is pretty opaque, however, because it's not + // clear what the breakdown of the N bytes are and what to optimize + // next. To help provide a better error message here some fancy-ish + // logic is done here to report the breakdown of the byte request into + // the largest portions and where it's coming from. + let mut message = format!( + "instance allocation for this module \ + requires {} bytes which exceeds the configured maximum \ + of {} bytes; breakdown of allocation requirement:\n\n", + layout.size(), + self.instance_size, + ); + + let mut remaining = layout.size(); + let mut push = |name: &str, bytes: usize| { + assert!(remaining >= bytes); + remaining -= bytes; + + // If the `name` region is more than 5% of the allocation request + // then report it here, otherwise ignore it. We have less than 20 + // fields so we're guaranteed that something should be reported, and + // otherwise it's not particularly interesting to learn about 5 + // different fields that are all 8 or 0 bytes. Only try to report + // the "major" sources of bytes here. + if bytes > layout.size() / 20 { + message.push_str(&format!( + " * {:.02}% - {} bytes - {}\n", + ((bytes as f32) / (layout.size() as f32)) * 100.0, + bytes, + name, + )); + } + }; + + // The `Instance` itself requires some size allocated to it. + push("instance state management", mem::size_of::()); + + // Afterwards the `VMContext`'s regions are why we're requesting bytes, + // so ask it for descriptions on each region's byte size. + for (desc, size) in offsets.region_sizes() { + push(desc, size as usize); + } + + // double-check we accounted for all the bytes + assert_eq!(remaining, 0); + + bail!("{}", message) + } } /// Represents a pool of WebAssembly linear memories. @@ -638,40 +679,38 @@ struct MemoryPool { image_slots: Vec>>, // The size, in bytes, of each linear memory's reservation plus the guard // region allocated for it. - memory_size: usize, + memory_reservation_size: usize, + // The maximum size, in bytes, of each linear memory. Guaranteed to be a + // whole number of wasm pages. + max_memory_size: usize, // The size, in bytes, of the offset to the first linear memory in this // pool. This is here to help account for the first region of guard pages, // if desired, before the first linear memory. initial_memory_offset: usize, max_memories: usize, max_instances: usize, - max_wasm_pages: u64, } impl MemoryPool { - fn new( - module_limits: &ModuleLimits, - instance_limits: &InstanceLimits, - tunables: &Tunables, - ) -> Result { + fn new(instance_limits: &InstanceLimits, tunables: &Tunables) -> Result { // The maximum module memory page count cannot exceed 65536 pages - if module_limits.memory_pages > 0x10000 { + if instance_limits.memory_pages > 0x10000 { bail!( "module memory page limit of {} exceeds the maximum of 65536", - module_limits.memory_pages + instance_limits.memory_pages ); } // The maximum module memory page count cannot exceed the memory reservation size - if module_limits.memory_pages > tunables.static_memory_bound { + if u64::from(instance_limits.memory_pages) > tunables.static_memory_bound { bail!( "module memory page limit of {} pages exceeds maximum static memory limit of {} pages", - module_limits.memory_pages, + instance_limits.memory_pages, tunables.static_memory_bound, ); } - let memory_size = if module_limits.memory_pages > 0 { + let memory_size = if instance_limits.memory_pages > 0 { usize::try_from( u64::from(tunables.static_memory_bound) * u64::from(WASM_PAGE_SIZE) + tunables.static_memory_offset_guard_size, @@ -688,7 +727,7 @@ impl MemoryPool { ); let max_instances = instance_limits.count as usize; - let max_memories = module_limits.memories as usize; + let max_memories = instance_limits.memories as usize; let initial_memory_offset = if tunables.guard_before_linear_memory { usize::try_from(tunables.static_memory_offset_guard_size).unwrap() } else { @@ -732,11 +771,11 @@ impl MemoryPool { let pool = Self { mapping, image_slots, - memory_size, + memory_reservation_size: memory_size, initial_memory_offset, max_memories, max_instances, - max_wasm_pages: module_limits.memory_pages, + max_memory_size: (instance_limits.memory_pages as usize) * (WASM_PAGE_SIZE as usize), }; // uffd support requires some special setup for the memory pool @@ -751,7 +790,7 @@ impl MemoryPool { let memory_index = memory_index.as_u32() as usize; assert!(memory_index < self.max_memories); let idx = instance_index * self.max_memories + memory_index; - let offset = self.initial_memory_offset + idx * self.memory_size; + let offset = self.initial_memory_offset + idx * self.memory_reservation_size; unsafe { self.mapping.as_mut_ptr().offset(offset as isize) } } @@ -774,7 +813,7 @@ impl MemoryPool { MemoryImageSlot::create( self.get_base(instance_index, memory_index) as *mut c_void, 0, - self.memory_size, + self.max_memory_size, ) }) } @@ -821,22 +860,18 @@ struct TablePool { } impl TablePool { - fn new(module_limits: &ModuleLimits, instance_limits: &InstanceLimits) -> Result { + fn new(instance_limits: &InstanceLimits) -> Result { let page_size = region::page::size(); - let table_size = if module_limits.table_elements > 0 { - round_up_to_pow2( - mem::size_of::<*mut u8>() - .checked_mul(module_limits.table_elements as usize) - .ok_or_else(|| anyhow!("table size exceeds addressable memory"))?, - page_size, - ) - } else { - 0 - }; + let table_size = round_up_to_pow2( + mem::size_of::<*mut u8>() + .checked_mul(instance_limits.table_elements as usize) + .ok_or_else(|| anyhow!("table size exceeds addressable memory"))?, + page_size, + ); let max_instances = instance_limits.count as usize; - let max_tables = module_limits.tables as usize; + let max_tables = instance_limits.tables as usize; let allocation_size = table_size .checked_mul(max_tables) @@ -852,7 +887,7 @@ impl TablePool { max_tables, max_instances, page_size, - max_elements: module_limits.table_elements, + max_elements: instance_limits.table_elements, }) } @@ -1009,7 +1044,6 @@ impl StackPool { /// Note: the resource pools are manually dropped so that the fault handler terminates correctly. #[derive(Debug)] pub struct PoolingInstanceAllocator { - module_limits: ModuleLimits, // This is manually drop so that the pools unmap their memory before the page fault handler drops. instances: mem::ManuallyDrop, #[cfg(all(feature = "async", unix))] @@ -1024,7 +1058,6 @@ impl PoolingInstanceAllocator { /// Creates a new pooling instance allocator with the given strategy and limits. pub fn new( strategy: PoolingAllocationStrategy, - module_limits: ModuleLimits, instance_limits: InstanceLimits, stack_size: usize, tunables: &Tunables, @@ -1033,7 +1066,7 @@ impl PoolingInstanceAllocator { bail!("the instance count limit cannot be zero"); } - let instances = InstancePool::new(strategy, &module_limits, &instance_limits, tunables)?; + let instances = InstancePool::new(strategy, &instance_limits, tunables)?; #[cfg(all(feature = "uffd", target_os = "linux"))] let _fault_handler = imp::PageFaultHandler::new(&instances)?; @@ -1041,7 +1074,6 @@ impl PoolingInstanceAllocator { drop(stack_size); // suppress unused warnings w/o async feature Ok(Self { - module_limits, instances: mem::ManuallyDrop::new(instances), #[cfg(all(feature = "async", unix))] stacks: StackPool::new(&instance_limits, stack_size)?, @@ -1065,7 +1097,18 @@ impl Drop for PoolingInstanceAllocator { unsafe impl InstanceAllocator for PoolingInstanceAllocator { fn validate(&self, module: &Module) -> Result<()> { - self.module_limits.validate(module) + self.instances.validate_memory_plans(module)?; + self.instances.validate_table_plans(module)?; + + // Note that this check is not 100% accurate for cross-compiled systems + // where the pointer size may change since this check is often performed + // at compile time instead of runtime. Given that Wasmtime is almost + // always on a 64-bit platform though this is generally ok, and + // otherwise this check also happens during instantiation to + // double-check at that point. + self.instances.validate_instance_size(module)?; + + Ok(()) } fn adjust_tunables(&self, tunables: &mut Tunables) { @@ -1149,296 +1192,7 @@ mod test { use super::*; use crate::{CompiledModuleId, Imports, MemoryImage, StorePtr, VMSharedSignatureIndex}; use std::sync::Arc; - use wasmtime_environ::{ - DefinedFuncIndex, DefinedMemoryIndex, EntityRef, FunctionInfo, Global, GlobalInit, Memory, - MemoryPlan, ModuleType, SignatureIndex, Table, TablePlan, TableStyle, WasmType, - }; - - #[test] - fn test_module_imported_functions_limit() { - let limits = ModuleLimits { - imported_functions: 0, - ..Default::default() - }; - - let mut module = Module::default(); - - module.functions.push(SignatureIndex::new(0)); - assert!(limits.validate(&module).is_ok()); - - module.num_imported_funcs = 1; - assert_eq!( - limits.validate(&module).map_err(|e| e.to_string()), - Err("imported function count of 1 exceeds the limit of 0".into()) - ); - } - - #[test] - fn test_module_imported_tables_limit() { - let limits = ModuleLimits { - imported_tables: 0, - ..Default::default() - }; - - let mut module = Module::default(); - - module.table_plans.push(TablePlan { - style: TableStyle::CallerChecksSignature, - table: Table { - wasm_ty: WasmType::FuncRef, - minimum: 0, - maximum: None, - }, - }); - - assert!(limits.validate(&module).is_ok()); - - module.num_imported_tables = 1; - assert_eq!( - limits.validate(&module).map_err(|e| e.to_string()), - Err("imported tables count of 1 exceeds the limit of 0".into()) - ); - } - - #[test] - fn test_module_imported_memories_limit() { - let limits = ModuleLimits { - imported_memories: 0, - ..Default::default() - }; - - let mut module = Module::default(); - - module.memory_plans.push(MemoryPlan { - style: MemoryStyle::Static { bound: 0 }, - memory: Memory { - minimum: 0, - maximum: None, - shared: false, - memory64: false, - }, - pre_guard_size: 0, - offset_guard_size: 0, - }); - - assert!(limits.validate(&module).is_ok()); - - module.num_imported_memories = 1; - assert_eq!( - limits.validate(&module).map_err(|e| e.to_string()), - Err("imported memories count of 1 exceeds the limit of 0".into()) - ); - } - - #[test] - fn test_module_imported_globals_limit() { - let limits = ModuleLimits { - imported_globals: 0, - ..Default::default() - }; - - let mut module = Module::default(); - - module.globals.push(Global { - wasm_ty: WasmType::I32, - mutability: false, - initializer: GlobalInit::I32Const(0), - }); - - assert!(limits.validate(&module).is_ok()); - - module.num_imported_globals = 1; - assert_eq!( - limits.validate(&module).map_err(|e| e.to_string()), - Err("imported globals count of 1 exceeds the limit of 0".into()) - ); - } - - #[test] - fn test_module_defined_types_limit() { - let limits = ModuleLimits { - types: 0, - ..Default::default() - }; - - let mut module = Module::default(); - assert!(limits.validate(&module).is_ok()); - - module - .types - .push(ModuleType::Function(SignatureIndex::new(0))); - assert_eq!( - limits.validate(&module).map_err(|e| e.to_string()), - Err("defined types count of 1 exceeds the limit of 0".into()) - ); - } - - #[test] - fn test_module_defined_functions_limit() { - let limits = ModuleLimits { - functions: 0, - ..Default::default() - }; - - let mut module = Module::default(); - assert!(limits.validate(&module).is_ok()); - - module.functions.push(SignatureIndex::new(0)); - assert_eq!( - limits.validate(&module).map_err(|e| e.to_string()), - Err("defined functions count of 1 exceeds the limit of 0".into()) - ); - } - - #[test] - fn test_module_defined_tables_limit() { - let limits = ModuleLimits { - tables: 0, - ..Default::default() - }; - - let mut module = Module::default(); - assert!(limits.validate(&module).is_ok()); - - module.table_plans.push(TablePlan { - style: TableStyle::CallerChecksSignature, - table: Table { - wasm_ty: WasmType::FuncRef, - minimum: 0, - maximum: None, - }, - }); - assert_eq!( - limits.validate(&module).map_err(|e| e.to_string()), - Err("defined tables count of 1 exceeds the limit of 0".into()) - ); - } - - #[test] - fn test_module_defined_memories_limit() { - let limits = ModuleLimits { - memories: 0, - ..Default::default() - }; - - let mut module = Module::default(); - assert!(limits.validate(&module).is_ok()); - - module.memory_plans.push(MemoryPlan { - style: MemoryStyle::Static { bound: 0 }, - memory: Memory { - minimum: 0, - maximum: None, - shared: false, - memory64: false, - }, - pre_guard_size: 0, - offset_guard_size: 0, - }); - assert_eq!( - limits.validate(&module).map_err(|e| e.to_string()), - Err("defined memories count of 1 exceeds the limit of 0".into()) - ); - } - - #[test] - fn test_module_defined_globals_limit() { - let limits = ModuleLimits { - globals: 0, - ..Default::default() - }; - - let mut module = Module::default(); - assert!(limits.validate(&module).is_ok()); - - module.globals.push(Global { - wasm_ty: WasmType::I32, - mutability: false, - initializer: GlobalInit::I32Const(0), - }); - assert_eq!( - limits.validate(&module).map_err(|e| e.to_string()), - Err("defined globals count of 1 exceeds the limit of 0".into()) - ); - } - - #[test] - fn test_module_table_minimum_elements_limit() { - let limits = ModuleLimits { - tables: 1, - table_elements: 10, - ..Default::default() - }; - - let mut module = Module::default(); - module.table_plans.push(TablePlan { - style: TableStyle::CallerChecksSignature, - table: Table { - wasm_ty: WasmType::FuncRef, - minimum: 11, - maximum: None, - }, - }); - assert_eq!( - limits.validate(&module).map_err(|e| e.to_string()), - Err( - "table index 0 has a minimum element size of 11 which exceeds the limit of 10" - .into() - ) - ); - } - - #[test] - fn test_module_memory_minimum_size_limit() { - let limits = ModuleLimits { - memories: 1, - memory_pages: 5, - ..Default::default() - }; - - let mut module = Module::default(); - module.memory_plans.push(MemoryPlan { - style: MemoryStyle::Static { bound: 0 }, - memory: Memory { - minimum: 6, - maximum: None, - shared: false, - memory64: false, - }, - pre_guard_size: 0, - offset_guard_size: 0, - }); - assert_eq!( - limits.validate(&module).map_err(|e| e.to_string()), - Err("memory index 0 has a minimum page size of 6 which exceeds the limit of 5".into()) - ); - } - - #[test] - fn test_module_with_dynamic_memory_style() { - let limits = ModuleLimits { - memories: 1, - memory_pages: 5, - ..Default::default() - }; - - let mut module = Module::default(); - module.memory_plans.push(MemoryPlan { - style: MemoryStyle::Dynamic { reserve: 0 }, - memory: Memory { - minimum: 1, - maximum: None, - shared: false, - memory64: false, - }, - offset_guard_size: 0, - pre_guard_size: 0, - }); - assert_eq!( - limits.validate(&module).map_err(|e| e.to_string()), - Err("memory index 0 has an unsupported dynamic memory plan style".into()) - ); - } + use wasmtime_environ::{DefinedFuncIndex, DefinedMemoryIndex, FunctionInfo, SignatureIndex}; pub(crate) fn empty_runtime_info( module: Arc, @@ -1482,24 +1236,18 @@ mod test { #[cfg(target_pointer_width = "64")] #[test] fn test_instance_pool() -> Result<()> { - let module_limits = ModuleLimits { - imported_functions: 0, - imported_tables: 0, - imported_memories: 0, - imported_globals: 0, - types: 0, - functions: 0, + let instance_limits = InstanceLimits { + count: 3, tables: 1, memories: 1, - globals: 0, table_elements: 10, + size: 1000, memory_pages: 1, + ..Default::default() }; - let instance_limits = InstanceLimits { count: 3 }; let instances = InstancePool::new( PoolingAllocationStrategy::NextAvailable, - &module_limits, &instance_limits, &Tunables { static_memory_bound: 1, @@ -1507,9 +1255,7 @@ mod test { }, )?; - // As of April 2021, the instance struct's size is largely below the size of a single page, - // so it's safe to assume it's been rounded to the size of a single memory page here. - assert_eq!(instances.instance_size, region::page::size()); + assert_eq!(instances.instance_size, 1008); // round 1000 up to alignment assert_eq!(instances.max_instances, 3); assert_eq!( @@ -1574,20 +1320,14 @@ mod test { #[test] fn test_memory_pool() -> Result<()> { let pool = MemoryPool::new( - &ModuleLimits { - imported_functions: 0, - imported_tables: 0, - imported_memories: 0, - imported_globals: 0, - types: 0, - functions: 0, + &InstanceLimits { + count: 5, tables: 0, memories: 3, - globals: 0, table_elements: 0, memory_pages: 1, + ..Default::default() }, - &InstanceLimits { count: 5 }, &Tunables { static_memory_bound: 1, static_memory_offset_guard_size: 0, @@ -1595,10 +1335,10 @@ mod test { }, )?; - assert_eq!(pool.memory_size, WASM_PAGE_SIZE as usize); + assert_eq!(pool.memory_reservation_size, WASM_PAGE_SIZE as usize); assert_eq!(pool.max_memories, 3); assert_eq!(pool.max_instances, 5); - assert_eq!(pool.max_wasm_pages, 1); + assert_eq!(pool.max_memory_size, WASM_PAGE_SIZE as usize); let base = pool.mapping.as_ptr() as usize; @@ -1608,7 +1348,7 @@ mod test { for j in 0..3 { assert_eq!( iter.next().unwrap() as usize - base, - ((i * 3) + j) * pool.memory_size + ((i * 3) + j) * pool.memory_reservation_size ); } @@ -1621,22 +1361,14 @@ mod test { #[cfg(target_pointer_width = "64")] #[test] fn test_table_pool() -> Result<()> { - let pool = TablePool::new( - &ModuleLimits { - imported_functions: 0, - imported_tables: 0, - imported_memories: 0, - imported_globals: 0, - types: 0, - functions: 0, - tables: 4, - memories: 0, - globals: 0, - table_elements: 100, - memory_pages: 0, - }, - &InstanceLimits { count: 7 }, - )?; + let pool = TablePool::new(&InstanceLimits { + count: 7, + table_elements: 100, + memory_pages: 0, + tables: 4, + memories: 0, + ..Default::default() + })?; let host_page_size = region::page::size(); @@ -1667,7 +1399,13 @@ mod test { #[cfg(all(unix, target_pointer_width = "64", feature = "async"))] #[test] fn test_stack_pool() -> Result<()> { - let pool = StackPool::new(&InstanceLimits { count: 10 }, 1)?; + let pool = StackPool::new( + &InstanceLimits { + count: 10, + ..Default::default() + }, + 1, + )?; let native_page_size = region::page::size(); assert_eq!(pool.stack_size, 2 * native_page_size); @@ -1737,7 +1475,6 @@ mod test { assert_eq!( PoolingInstanceAllocator::new( PoolingAllocationStrategy::Random, - ModuleLimits::default(), InstanceLimits { count: 0, ..Default::default() @@ -1756,11 +1493,11 @@ mod test { assert_eq!( PoolingInstanceAllocator::new( PoolingAllocationStrategy::Random, - ModuleLimits { + InstanceLimits { + count: 1, memory_pages: 0x10001, ..Default::default() }, - InstanceLimits { count: 1 }, 4096, &Tunables { static_memory_bound: 1, @@ -1778,11 +1515,11 @@ mod test { assert_eq!( PoolingInstanceAllocator::new( PoolingAllocationStrategy::Random, - ModuleLimits { + InstanceLimits { + count: 1, memory_pages: 2, ..Default::default() }, - InstanceLimits { count: 1 }, 4096, &Tunables { static_memory_bound: 1, @@ -1801,18 +1538,14 @@ mod test { fn test_stack_zeroed() -> Result<()> { let allocator = PoolingInstanceAllocator::new( PoolingAllocationStrategy::NextAvailable, - ModuleLimits { - imported_functions: 0, - types: 0, - functions: 0, - tables: 0, - memories: 0, - globals: 0, + InstanceLimits { + count: 1, table_elements: 0, memory_pages: 0, + tables: 0, + memories: 0, ..Default::default() }, - InstanceLimits { count: 1 }, 4096, &Tunables::default(), )?; diff --git a/crates/runtime/src/instance/allocator/pooling/uffd.rs b/crates/runtime/src/instance/allocator/pooling/uffd.rs index 737445dc5b..21b52c6298 100644 --- a/crates/runtime/src/instance/allocator/pooling/uffd.rs +++ b/crates/runtime/src/instance/allocator/pooling/uffd.rs @@ -96,7 +96,7 @@ pub fn decommit_stack_pages(addr: *mut u8, len: usize) -> Result<()> { /// the page fault handler will detect an out of bounds access and treat the page, temporarily, /// as a guard page. pub(super) fn initialize_memory_pool(pool: &MemoryPool) -> Result<()> { - if pool.memory_size == 0 || pool.max_wasm_pages == 0 { + if pool.memory_reservation_size == 0 || pool.max_memory_size == 0 { return Ok(()); } @@ -105,7 +105,7 @@ pub(super) fn initialize_memory_pool(pool: &MemoryPool) -> Result<()> { unsafe { region::protect( base as _, - pool.max_wasm_pages as usize * WASM_PAGE_SIZE, + pool.max_memory_size, region::Protection::READ_WRITE, ) .context("failed to initialize memory pool for uffd")?; @@ -177,7 +177,7 @@ impl FaultLocator { max_instances: instances.max_instances, memories_start, memories_end, - memory_size: instances.memories.memory_size, + memory_size: instances.memories.memory_reservation_size, max_memories: instances.memories.max_memories, } } @@ -438,8 +438,8 @@ impl Drop for PageFaultHandler { mod test { use super::*; use crate::{ - Imports, InstanceAllocationRequest, InstanceLimits, ModuleLimits, - PoolingAllocationStrategy, Store, StorePtr, + Imports, InstanceAllocationRequest, InstanceLimits, PoolingAllocationStrategy, Store, + StorePtr, }; use std::sync::atomic::AtomicU64; use std::sync::Arc; @@ -448,20 +448,15 @@ mod test { #[cfg(target_pointer_width = "64")] #[test] fn test_address_locator() { - let module_limits = ModuleLimits { - imported_functions: 0, - imported_tables: 0, - imported_memories: 0, - imported_globals: 0, - types: 0, - functions: 0, + let instance_limits = InstanceLimits { + count: 3, tables: 0, memories: 2, - globals: 0, table_elements: 0, memory_pages: 2, + size: 1000, + ..Default::default() }; - let instance_limits = InstanceLimits { count: 3 }; let tunables = Tunables { static_memory_bound: 10, static_memory_offset_guard_size: 0, @@ -471,7 +466,6 @@ mod test { let instances = InstancePool::new( PoolingAllocationStrategy::Random, - &module_limits, &instance_limits, &tunables, ) @@ -480,7 +474,7 @@ mod test { let locator = FaultLocator::new(&instances); assert_eq!(locator.instances_start, instances.mapping.as_ptr() as usize); - assert_eq!(locator.instance_size, 4096); + assert_eq!(locator.instance_size, 1008); assert_eq!(locator.max_instances, 3); assert_eq!( locator.memories_start, @@ -499,7 +493,7 @@ mod test { let mut module = Module::new(); - for _ in 0..module_limits.memories { + for _ in 0..instance_limits.memories { module.memory_plans.push(MemoryPlan { memory: Memory { minimum: 2, @@ -513,8 +507,6 @@ mod test { }); } - module_limits.validate(&module).expect("should validate"); - // An InstanceAllocationRequest with a module must also have // a non-null StorePtr. Here we mock just enough of a store // to satisfy this test. diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs index e279d91d8c..ea5b8093f8 100644 --- a/crates/runtime/src/lib.rs +++ b/crates/runtime/src/lib.rs @@ -54,9 +54,7 @@ pub use crate::instance::{ OnDemandInstanceAllocator, StorePtr, }; #[cfg(feature = "pooling-allocator")] -pub use crate::instance::{ - InstanceLimits, ModuleLimits, PoolingAllocationStrategy, PoolingInstanceAllocator, -}; +pub use crate::instance::{InstanceLimits, PoolingAllocationStrategy, PoolingInstanceAllocator}; pub use crate::memory::{DefaultMemoryCreator, Memory, RuntimeLinearMemory, RuntimeMemoryCreator}; pub use crate::mmap::Mmap; pub use crate::mmap_vec::MmapVec; diff --git a/crates/runtime/src/memory.rs b/crates/runtime/src/memory.rs index b310ad8a86..6ee2628a17 100644 --- a/crates/runtime/src/memory.rs +++ b/crates/runtime/src/memory.rs @@ -315,6 +315,15 @@ impl Memory { ) -> Result { let (minimum, maximum) = Self::limit_new(plan, store)?; + if base.len() < minimum { + bail!( + "initial memory size of {} exceeds the pooling allocator's \ + configured maximum memory size of {} bytes", + minimum, + base.len(), + ); + } + let base = match maximum { Some(max) if max < base.len() => &mut base[..max], _ => base, diff --git a/crates/runtime/src/table.rs b/crates/runtime/src/table.rs index e5edff489d..63b4e440d7 100644 --- a/crates/runtime/src/table.rs +++ b/crates/runtime/src/table.rs @@ -195,6 +195,14 @@ impl Table { Self::limit_new(plan, store)?; let size = plan.table.minimum; let ty = wasm_to_table_type(plan.table.wasm_ty)?; + if data.len() < (plan.table.minimum as usize) { + bail!( + "initial table size of {} exceeds the pooling allocator's \ + configured maximum table size of {} elements", + plan.table.minimum, + data.len(), + ); + } let data = match plan.table.maximum { Some(max) if (max as usize) < data.len() => &mut data[..max as usize], _ => data, diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 00689904e9..29edbbbeac 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -15,10 +15,7 @@ use wasmtime_jit::{JitDumpAgent, NullProfilerAgent, ProfilingAgent, VTuneAgent}; use wasmtime_runtime::{InstanceAllocator, OnDemandInstanceAllocator, RuntimeMemoryCreator}; #[cfg(feature = "pooling-allocator")] -mod pooling; - -#[cfg(feature = "pooling-allocator")] -pub use self::pooling::*; +pub use wasmtime_runtime::{InstanceLimits, PoolingAllocationStrategy}; /// Represents the module instance allocation strategy to use. #[derive(Clone)] @@ -39,8 +36,6 @@ pub enum InstanceAllocationStrategy { Pooling { /// The allocation strategy to use. strategy: PoolingAllocationStrategy, - /// The module limits to use. - module_limits: ModuleLimits, /// The instance limits to use. instance_limits: InstanceLimits, }, @@ -52,7 +47,6 @@ impl InstanceAllocationStrategy { pub fn pooling() -> Self { Self::Pooling { strategy: PoolingAllocationStrategy::default(), - module_limits: ModuleLimits::default(), instance_limits: InstanceLimits::default(), } } @@ -1281,12 +1275,10 @@ impl Config { #[cfg(feature = "pooling-allocator")] InstanceAllocationStrategy::Pooling { strategy, - module_limits, instance_limits, } => Ok(Box::new(wasmtime_runtime::PoolingInstanceAllocator::new( - strategy.into(), - module_limits.into(), - instance_limits.into(), + strategy, + instance_limits, stack_size, &self.tunables, )?)), diff --git a/crates/wasmtime/src/config/pooling.rs b/crates/wasmtime/src/config/pooling.rs deleted file mode 100644 index 6f8f9ae62b..0000000000 --- a/crates/wasmtime/src/config/pooling.rs +++ /dev/null @@ -1,279 +0,0 @@ -//! This module contains types exposed via `Config` relating to the pooling allocator feature. - -/// Represents the limits placed on a module for compiling with the pooling instance allocation strategy. -#[derive(Debug, Copy, Clone)] -pub struct ModuleLimits { - /// The maximum number of imported functions for a module (default is 1000). - /// - /// This value controls the capacity of the `VMFunctionImport` table and the - /// `VMCallerCheckedAnyfunc` table in each instance's `VMContext` structure. - /// - /// The allocated size of the `VMFunctionImport` table will be `imported_functions * sizeof(VMFunctionImport)` - /// for each instance regardless of how many functions an instance imports. - /// - /// The allocated size of the `VMCallerCheckedAnyfunc` table will be - /// `imported_functions * functions * sizeof(VMCallerCheckedAnyfunc)` for each instance regardless of - /// how many functions are imported and defined by an instance. - pub imported_functions: u32, - - /// The maximum number of imported tables for a module (default is 0). - /// - /// This value controls the capacity of the `VMTableImport` table in each instance's - /// `VMContext` structure. - /// - /// The allocated size of the table will be `imported_tables * sizeof(VMTableImport)` for each - /// instance regardless of how many tables an instance imports. - pub imported_tables: u32, - - /// The maximum number of imported linear memories for a module (default is 0). - /// - /// This value controls the capacity of the `VMMemoryImport` table in each instance's - /// `VMContext` structure. - /// - /// The allocated size of the table will be `imported_memories * sizeof(VMMemoryImport)` for each - /// instance regardless of how many memories an instance imports. - pub imported_memories: u32, - - /// The maximum number of imported globals for a module (default is 0). - /// - /// This value controls the capacity of the `VMGlobalImport` table in each instance's - /// `VMContext` structure. - /// - /// The allocated size of the table will be `imported_globals * sizeof(VMGlobalImport)` for each - /// instance regardless of how many globals an instance imports. - pub imported_globals: u32, - - /// The maximum number of defined types for a module (default is 100). - /// - /// This value controls the capacity of the `VMSharedSignatureIndex` table in each instance's - /// `VMContext` structure. - /// - /// The allocated size of the table will be `types * sizeof(VMSharedSignatureIndex)` for each - /// instance regardless of how many types are defined by an instance's module. - pub types: u32, - - /// The maximum number of defined functions for a module (default is 10000). - /// - /// This value controls the capacity of the `VMCallerCheckedAnyfunc` table in each instance's - /// `VMContext` structure. - /// - /// The allocated size of the `VMCallerCheckedAnyfunc` table will be - /// `imported_functions * functions * sizeof(VMCallerCheckedAnyfunc)` for each instance - /// regardless of how many functions are imported and defined by an instance. - pub functions: u32, - - /// The maximum number of defined tables for a module (default is 1). - /// - /// This value controls the capacity of the `VMTableDefinition` table in each instance's - /// `VMContext` structure. - /// - /// The allocated size of the table will be `tables * sizeof(VMTableDefinition)` for each - /// instance regardless of how many tables are defined by an instance's module. - pub tables: u32, - - /// The maximum number of defined linear memories for a module (default is 1). - /// - /// This value controls the capacity of the `VMMemoryDefinition` table in each instance's - /// `VMContext` structure. - /// - /// The allocated size of the table will be `memories * sizeof(VMMemoryDefinition)` for each - /// instance regardless of how many memories are defined by an instance's module. - pub memories: u32, - - /// The maximum number of defined globals for a module (default is 10). - /// - /// This value controls the capacity of the `VMGlobalDefinition` table in each instance's - /// `VMContext` structure. - /// - /// The allocated size of the table will be `globals * sizeof(VMGlobalDefinition)` for each - /// instance regardless of how many globals are defined by an instance's module. - pub globals: u32, - - /// The maximum table elements for any table defined in a module (default is 10000). - /// - /// If a table's minimum element limit is greater than this value, the module will - /// fail to compile. - /// - /// If a table's maximum element limit is unbounded or greater than this value, - /// the maximum will be `table_elements` for the purpose of any `table.grow` instruction. - /// - /// This value is used to reserve the maximum space for each supported table; table elements - /// are pointer-sized in the Wasmtime runtime. Therefore, the space reserved for each instance - /// is `tables * table_elements * sizeof::<*const ()>`. - pub table_elements: u32, - - /// The maximum number of pages for any linear memory defined in a module (default is 160). - /// - /// The default of 160 means at most 10 MiB of host memory may be committed for each instance. - /// - /// If a memory's minimum page limit is greater than this value, the module will - /// fail to compile. - /// - /// If a memory's maximum page limit is unbounded or greater than this value, - /// the maximum will be `memory_pages` for the purpose of any `memory.grow` instruction. - /// - /// This value is used to control the maximum accessible space for each linear memory of an instance. - /// - /// The reservation size of each linear memory is controlled by the - /// [`static_memory_maximum_size`](super::Config::static_memory_maximum_size) setting and this value cannot - /// exceed the configured static memory maximum size. - pub memory_pages: u64, -} - -impl Default for ModuleLimits { - fn default() -> Self { - // Use the defaults from the runtime - let wasmtime_runtime::ModuleLimits { - imported_functions, - imported_tables, - imported_memories, - imported_globals, - types, - functions, - tables, - memories, - globals, - table_elements, - memory_pages, - } = wasmtime_runtime::ModuleLimits::default(); - - Self { - imported_functions, - imported_tables, - imported_memories, - imported_globals, - types, - functions, - tables, - memories, - globals, - table_elements, - memory_pages, - } - } -} - -// This exists so we can convert between the public Wasmtime API and the runtime representation -// without having to export runtime types from the Wasmtime API. -#[doc(hidden)] -impl Into for ModuleLimits { - fn into(self) -> wasmtime_runtime::ModuleLimits { - let Self { - imported_functions, - imported_tables, - imported_memories, - imported_globals, - types, - functions, - tables, - memories, - globals, - table_elements, - memory_pages, - } = self; - - wasmtime_runtime::ModuleLimits { - imported_functions, - imported_tables, - imported_memories, - imported_globals, - types, - functions, - tables, - memories, - globals, - table_elements, - memory_pages, - } - } -} - -/// Represents the limits placed on instances by the pooling instance allocation strategy. -#[derive(Debug, Copy, Clone)] -pub struct InstanceLimits { - /// The maximum number of concurrent instances supported (default is 1000). - /// - /// This value has a direct impact on the amount of memory allocated by the pooling - /// instance allocator. - /// - /// The pooling instance allocator allocates three memory pools with sizes depending on this value: - /// - /// * An instance pool, where each entry in the pool can store the runtime representation - /// of an instance, including a maximal `VMContext` structure (see [`ModuleLimits`](ModuleLimits) - /// for the various settings that control the size of each instance's `VMContext` structure). - /// - /// * A memory pool, where each entry in the pool contains the reserved address space for each - /// linear memory supported by an instance. - /// - /// * A table pool, where each entry in the pool contains the space needed for each WebAssembly table - /// supported by an instance (see `[ModuleLimits::table_elements`] to control the size of each table). - /// - /// Additionally, this value will also control the maximum number of execution stacks allowed for - /// asynchronous execution (one per instance), when enabled. - /// - /// The memory pool will reserve a large quantity of host process address space to elide the bounds - /// checks required for correct WebAssembly memory semantics. Even for 64-bit address spaces, the - /// address space is limited when dealing with a large number of supported instances. - /// - /// For example, on Linux x86_64, the userland address space limit is 128 TiB. That might seem like a lot, - /// but each linear memory will *reserve* 6 GiB of space by default. Multiply that by the number of linear - /// memories each instance supports and then by the number of supported instances and it becomes apparent - /// that address space can be exhausted depending on the number of supported instances. - pub count: u32, -} - -impl Default for InstanceLimits { - fn default() -> Self { - let wasmtime_runtime::InstanceLimits { count } = - wasmtime_runtime::InstanceLimits::default(); - - Self { count } - } -} - -// This exists so we can convert between the public Wasmtime API and the runtime representation -// without having to export runtime types from the Wasmtime API. -#[doc(hidden)] -impl Into for InstanceLimits { - fn into(self) -> wasmtime_runtime::InstanceLimits { - let Self { count } = self; - - wasmtime_runtime::InstanceLimits { count } - } -} - -/// The allocation strategy to use for the pooling instance allocation strategy. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PoolingAllocationStrategy { - /// Allocate from the next available instance. - NextAvailable, - /// Allocate from a random available instance. - Random, - /// Try to allocate an instance slot that was previously used for - /// the same module, potentially enabling faster instantiation by - /// reusing e.g. memory mappings. - ReuseAffinity, -} - -impl Default for PoolingAllocationStrategy { - fn default() -> Self { - match wasmtime_runtime::PoolingAllocationStrategy::default() { - wasmtime_runtime::PoolingAllocationStrategy::NextAvailable => Self::NextAvailable, - wasmtime_runtime::PoolingAllocationStrategy::Random => Self::Random, - wasmtime_runtime::PoolingAllocationStrategy::ReuseAffinity => Self::ReuseAffinity, - } - } -} - -// This exists so we can convert between the public Wasmtime API and the runtime representation -// without having to export runtime types from the Wasmtime API. -#[doc(hidden)] -impl Into for PoolingAllocationStrategy { - fn into(self) -> wasmtime_runtime::PoolingAllocationStrategy { - match self { - Self::NextAvailable => wasmtime_runtime::PoolingAllocationStrategy::NextAvailable, - Self::Random => wasmtime_runtime::PoolingAllocationStrategy::Random, - Self::ReuseAffinity => wasmtime_runtime::PoolingAllocationStrategy::ReuseAffinity, - } - } -} diff --git a/crates/wasmtime/src/module.rs b/crates/wasmtime/src/module.rs index e71d3e28d2..d31c3a4714 100644 --- a/crates/wasmtime/src/module.rs +++ b/crates/wasmtime/src/module.rs @@ -538,8 +538,10 @@ impl Module { types: Arc, module_upvars: &[serialization::SerializedModuleUpvar], ) -> Result { - // Validate the module can be used with the current allocator - engine.allocator().validate(modules[main_module].module())?; + // Validate all modules can be used with the current allocator + for module in modules.iter() { + engine.allocator().validate(module.module())?; + } let signatures = Arc::new(SignatureCollection::new_for_module( engine.signatures(), @@ -564,7 +566,7 @@ impl Module { &signatures, ) }) - .collect::>>()?; + .collect(); return Ok(Self { inner: Arc::new(ModuleInner { @@ -586,9 +588,9 @@ impl Module { artifact_upvars: &[usize], module_upvars: &[serialization::SerializedModuleUpvar], signatures: &Arc, - ) -> Result { + ) -> Module { let module = artifacts[module_index].clone(); - Ok(Module { + Module { inner: Arc::new(ModuleInner { engine: engine.clone(), types: types.clone(), @@ -611,10 +613,10 @@ impl Module { signatures, ) }) - .collect::>>()?, + .collect(), signatures: signatures.clone(), }), - }) + } } } diff --git a/src/lib.rs b/src/lib.rs index fc40d9d0d4..b94d6862dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -101,7 +101,7 @@ use std::path::PathBuf; use structopt::StructOpt; use wasmtime::{Config, ProfilingStrategy}; #[cfg(feature = "pooling-allocator")] -use wasmtime::{InstanceLimits, ModuleLimits, PoolingAllocationStrategy}; +use wasmtime::{InstanceLimits, PoolingAllocationStrategy}; fn pick_profiling_strategy(jitdump: bool, vtune: bool) -> Result { Ok(match (jitdump, vtune) { @@ -347,15 +347,9 @@ impl CommonOptions { #[cfg(feature = "pooling-allocator")] { if self.pooling_allocator { - let mut module_limits = ModuleLimits::default(); - module_limits.functions = 50000; - module_limits.types = 10000; - module_limits.globals = 1000; - module_limits.memory_pages = 2048; let instance_limits = InstanceLimits::default(); config.allocation_strategy(wasmtime::InstanceAllocationStrategy::Pooling { strategy: PoolingAllocationStrategy::NextAvailable, - module_limits, instance_limits, }); } diff --git a/tests/all/async_functions.rs b/tests/all/async_functions.rs index 584501ded3..e4aaf9c6aa 100644 --- a/tests/all/async_functions.rs +++ b/tests/all/async_functions.rs @@ -425,12 +425,12 @@ fn async_with_pooling_stacks() { config.async_support(true); config.allocation_strategy(InstanceAllocationStrategy::Pooling { strategy: PoolingAllocationStrategy::NextAvailable, - module_limits: ModuleLimits { + instance_limits: InstanceLimits { + count: 1, memory_pages: 1, table_elements: 0, ..Default::default() }, - instance_limits: InstanceLimits { count: 1 }, }); config.dynamic_memory_guard_size(0); config.static_memory_guard_size(0); @@ -454,12 +454,12 @@ fn async_host_func_with_pooling_stacks() -> Result<()> { config.async_support(true); config.allocation_strategy(InstanceAllocationStrategy::Pooling { strategy: PoolingAllocationStrategy::NextAvailable, - module_limits: ModuleLimits { + instance_limits: InstanceLimits { + count: 1, memory_pages: 1, table_elements: 0, ..Default::default() }, - instance_limits: InstanceLimits { count: 1 }, }); config.dynamic_memory_guard_size(0); config.static_memory_guard_size(0); diff --git a/tests/all/instance.rs b/tests/all/instance.rs index 32a2b706a2..43cf53cd13 100644 --- a/tests/all/instance.rs +++ b/tests/all/instance.rs @@ -66,11 +66,10 @@ fn linear_memory_limits() -> Result<()> { test(&Engine::new(Config::new().allocation_strategy( InstanceAllocationStrategy::Pooling { strategy: PoolingAllocationStrategy::NextAvailable, - module_limits: ModuleLimits { + instance_limits: InstanceLimits { memory_pages: 65536, - ..ModuleLimits::default() + ..Default::default() }, - instance_limits: InstanceLimits::default(), }, ))?)?; return Ok(()); diff --git a/tests/all/limits.rs b/tests/all/limits.rs index f1ee6d7c18..49224ef857 100644 --- a/tests/all/limits.rs +++ b/tests/all/limits.rs @@ -354,12 +354,9 @@ fn test_pooling_allocator_initial_limits_exceeded() -> Result<()> { config.wasm_multi_memory(true); config.allocation_strategy(InstanceAllocationStrategy::Pooling { strategy: PoolingAllocationStrategy::NextAvailable, - module_limits: ModuleLimits { - memories: 2, - ..Default::default() - }, instance_limits: InstanceLimits { count: 1, + memories: 2, ..Default::default() }, }); @@ -727,14 +724,11 @@ fn custom_limiter_detect_grow_failure() -> Result<()> { let mut config = Config::new(); config.allocation_strategy(InstanceAllocationStrategy::Pooling { strategy: PoolingAllocationStrategy::NextAvailable, - module_limits: ModuleLimits { + instance_limits: InstanceLimits { memory_pages: 10, table_elements: 10, ..Default::default() }, - instance_limits: InstanceLimits { - ..Default::default() - }, }); let engine = Engine::new(&config).unwrap(); let linker = Linker::new(&engine); @@ -839,14 +833,11 @@ async fn custom_limiter_async_detect_grow_failure() -> Result<()> { config.async_support(true); config.allocation_strategy(InstanceAllocationStrategy::Pooling { strategy: PoolingAllocationStrategy::NextAvailable, - module_limits: ModuleLimits { + instance_limits: InstanceLimits { memory_pages: 10, table_elements: 10, ..Default::default() }, - instance_limits: InstanceLimits { - ..Default::default() - }, }); let engine = Engine::new(&config).unwrap(); let linker = Linker::new(&engine); diff --git a/tests/all/memory.rs b/tests/all/memory.rs index a43f823bf2..5a2b01716a 100644 --- a/tests/all/memory.rs +++ b/tests/all/memory.rs @@ -193,11 +193,11 @@ fn guards_present_pooling() -> Result<()> { config.guard_before_linear_memory(true); config.allocation_strategy(InstanceAllocationStrategy::Pooling { strategy: PoolingAllocationStrategy::default(), - module_limits: ModuleLimits { + instance_limits: InstanceLimits { + count: 2, memory_pages: 10, - ..ModuleLimits::default() + ..Default::default() }, - instance_limits: InstanceLimits { count: 2 }, }); let engine = Engine::new(&config)?; diff --git a/tests/all/pooling_allocator.rs b/tests/all/pooling_allocator.rs index d17b65af82..fa867aceb5 100644 --- a/tests/all/pooling_allocator.rs +++ b/tests/all/pooling_allocator.rs @@ -7,12 +7,12 @@ fn successful_instantiation() -> Result<()> { let mut config = Config::new(); config.allocation_strategy(InstanceAllocationStrategy::Pooling { strategy: PoolingAllocationStrategy::NextAvailable, - module_limits: ModuleLimits { + instance_limits: InstanceLimits { + count: 1, memory_pages: 1, table_elements: 10, ..Default::default() }, - instance_limits: InstanceLimits { count: 1 }, }); config.dynamic_memory_guard_size(0); config.static_memory_guard_size(0); @@ -33,25 +33,36 @@ fn memory_limit() -> Result<()> { let mut config = Config::new(); config.allocation_strategy(InstanceAllocationStrategy::Pooling { strategy: PoolingAllocationStrategy::NextAvailable, - module_limits: ModuleLimits { + instance_limits: InstanceLimits { + count: 1, memory_pages: 3, table_elements: 10, ..Default::default() }, - instance_limits: InstanceLimits { count: 1 }, }); config.dynamic_memory_guard_size(0); config.static_memory_guard_size(65536); config.static_memory_maximum_size(3 * 65536); + config.wasm_multi_memory(true); let engine = Engine::new(&config)?; - // Module should fail to validate because the minimum is greater than the configured limit - match Module::new(&engine, r#"(module (memory 4))"#) { - Ok(_) => panic!("module compilation should fail"), + // Module should fail to instantiate because it has too many memories + match Module::new(&engine, r#"(module (memory 1) (memory 1))"#) { + Ok(_) => panic!("module instantiation should fail"), Err(e) => assert_eq!( e.to_string(), - "memory index 0 has a minimum page size of 4 which exceeds the limit of 3" + "defined memories count of 2 exceeds the limit of 1", + ), + } + + // Module should fail to instantiate because the minimum is greater than + // the configured limit + match Module::new(&engine, r#"(module (memory 4))"#) { + Ok(_) => panic!("module instantiation should fail"), + Err(e) => assert_eq!( + e.to_string(), + "memory index 0 has a minimum page size of 4 which exceeds the limit of 3", ), } @@ -101,13 +112,10 @@ fn memory_init() -> Result<()> { let mut config = Config::new(); config.allocation_strategy(InstanceAllocationStrategy::Pooling { strategy: PoolingAllocationStrategy::NextAvailable, - module_limits: ModuleLimits { - memory_pages: 2, - table_elements: 0, - ..Default::default() - }, instance_limits: InstanceLimits { count: 1, + memory_pages: 2, + table_elements: 0, ..Default::default() }, }); @@ -137,13 +145,10 @@ fn memory_guard_page_trap() -> Result<()> { let mut config = Config::new(); config.allocation_strategy(InstanceAllocationStrategy::Pooling { strategy: PoolingAllocationStrategy::NextAvailable, - module_limits: ModuleLimits { - memory_pages: 2, - table_elements: 0, - ..Default::default() - }, instance_limits: InstanceLimits { count: 1, + memory_pages: 2, + table_elements: 0, ..Default::default() }, }); @@ -196,12 +201,12 @@ fn memory_zeroed() -> Result<()> { let mut config = Config::new(); config.allocation_strategy(InstanceAllocationStrategy::Pooling { strategy: PoolingAllocationStrategy::NextAvailable, - module_limits: ModuleLimits { + instance_limits: InstanceLimits { + count: 1, memory_pages: 1, table_elements: 0, ..Default::default() }, - instance_limits: InstanceLimits { count: 1 }, }); config.dynamic_memory_guard_size(0); config.static_memory_guard_size(0); @@ -239,12 +244,12 @@ fn table_limit() -> Result<()> { let mut config = Config::new(); config.allocation_strategy(InstanceAllocationStrategy::Pooling { strategy: PoolingAllocationStrategy::NextAvailable, - module_limits: ModuleLimits { + instance_limits: InstanceLimits { + count: 1, memory_pages: 1, table_elements: TABLE_ELEMENTS, ..Default::default() }, - instance_limits: InstanceLimits { count: 1 }, }); config.dynamic_memory_guard_size(0); config.static_memory_guard_size(0); @@ -252,12 +257,22 @@ fn table_limit() -> Result<()> { let engine = Engine::new(&config)?; - // Module should fail to validate because the minimum is greater than the configured limit + // Module should fail to instantiate because it has too many tables + match Module::new(&engine, r#"(module (table 1 funcref) (table 1 funcref))"#) { + Ok(_) => panic!("module compilation should fail"), + Err(e) => assert_eq!( + e.to_string(), + "defined tables count of 2 exceeds the limit of 1", + ), + } + + // Module should fail to instantiate because the minimum is greater than + // the configured limit match Module::new(&engine, r#"(module (table 31 funcref))"#) { Ok(_) => panic!("module compilation should fail"), Err(e) => assert_eq!( e.to_string(), - "table index 0 has a minimum element size of 31 which exceeds the limit of 10" + "table index 0 has a minimum element size of 31 which exceeds the limit of 10", ), } @@ -316,13 +331,10 @@ fn table_init() -> Result<()> { let mut config = Config::new(); config.allocation_strategy(InstanceAllocationStrategy::Pooling { strategy: PoolingAllocationStrategy::NextAvailable, - module_limits: ModuleLimits { - memory_pages: 0, - table_elements: 6, - ..Default::default() - }, instance_limits: InstanceLimits { count: 1, + memory_pages: 0, + table_elements: 6, ..Default::default() }, }); @@ -369,12 +381,12 @@ fn table_zeroed() -> Result<()> { let mut config = Config::new(); config.allocation_strategy(InstanceAllocationStrategy::Pooling { strategy: PoolingAllocationStrategy::NextAvailable, - module_limits: ModuleLimits { + instance_limits: InstanceLimits { + count: 1, memory_pages: 1, table_elements: 10, ..Default::default() }, - instance_limits: InstanceLimits { count: 1 }, }); config.dynamic_memory_guard_size(0); config.static_memory_guard_size(0); @@ -413,14 +425,12 @@ fn instantiation_limit() -> Result<()> { let mut config = Config::new(); config.allocation_strategy(InstanceAllocationStrategy::Pooling { strategy: PoolingAllocationStrategy::NextAvailable, - module_limits: ModuleLimits { + instance_limits: InstanceLimits { + count: INSTANCE_LIMIT, memory_pages: 1, table_elements: 10, ..Default::default() }, - instance_limits: InstanceLimits { - count: INSTANCE_LIMIT, - }, }); config.dynamic_memory_guard_size(0); config.static_memory_guard_size(0); @@ -465,12 +475,12 @@ fn preserve_data_segments() -> Result<()> { let mut config = Config::new(); config.allocation_strategy(InstanceAllocationStrategy::Pooling { strategy: PoolingAllocationStrategy::NextAvailable, - module_limits: ModuleLimits { + instance_limits: InstanceLimits { + count: 2, memory_pages: 1, table_elements: 10, ..Default::default() }, - instance_limits: InstanceLimits { count: 2 }, }); let engine = Engine::new(&config)?; let m = Module::new( @@ -520,13 +530,12 @@ fn multi_memory_with_imported_memories() -> Result<()> { let mut config = Config::new(); config.allocation_strategy(InstanceAllocationStrategy::Pooling { strategy: PoolingAllocationStrategy::NextAvailable, - module_limits: ModuleLimits { + instance_limits: InstanceLimits { + count: 1, + memories: 2, memory_pages: 1, - imported_memories: 1, - memories: 1, ..Default::default() }, - instance_limits: InstanceLimits { count: 1 }, }); config.wasm_multi_memory(true); @@ -567,8 +576,10 @@ fn drop_externref_global_during_module_init() -> Result<()> { config.wasm_reference_types(true); config.allocation_strategy(InstanceAllocationStrategy::Pooling { strategy: PoolingAllocationStrategy::NextAvailable, - module_limits: Default::default(), - instance_limits: InstanceLimits { count: 1 }, + instance_limits: InstanceLimits { + count: 1, + ..Default::default() + }, }); let engine = Engine::new(&config)?; @@ -606,3 +617,50 @@ fn drop_externref_global_during_module_init() -> Result<()> { Ok(()) } + +#[test] +#[cfg(target_pointer_width = "64")] +fn instance_too_large() -> Result<()> { + let mut config = Config::new(); + config.allocation_strategy(InstanceAllocationStrategy::Pooling { + strategy: PoolingAllocationStrategy::NextAvailable, + instance_limits: InstanceLimits { + size: 16, + count: 1, + ..Default::default() + }, + }); + + let engine = Engine::new(&config)?; + let expected = "\ +instance allocation for this module requires 304 bytes which exceeds the \ +configured maximum of 16 bytes; breakdown of allocation requirement: + + * 78.95% - 240 bytes - instance state management + * 5.26% - 16 bytes - jit store state +"; + match Module::new(&engine, "(module)") { + Ok(_) => panic!("should have failed to compile"), + Err(e) => assert_eq!(e.to_string(), expected), + } + + let mut lots_of_globals = format!("(module"); + for _ in 0..100 { + lots_of_globals.push_str("(global i32 i32.const 0)\n"); + } + lots_of_globals.push_str(")"); + + let expected = "\ +instance allocation for this module requires 1904 bytes which exceeds the \ +configured maximum of 16 bytes; breakdown of allocation requirement: + + * 12.61% - 240 bytes - instance state management + * 84.03% - 1600 bytes - defined globals +"; + match Module::new(&engine, &lots_of_globals) { + Ok(_) => panic!("should have failed to compile"), + Err(e) => assert_eq!(e.to_string(), expected), + } + + Ok(()) +} diff --git a/tests/all/wast.rs b/tests/all/wast.rs index 1d0bf0ef96..588400d0bb 100644 --- a/tests/all/wast.rs +++ b/tests/all/wast.rs @@ -1,8 +1,8 @@ use std::path::Path; use std::sync::{Condvar, Mutex}; use wasmtime::{ - Config, Engine, InstanceAllocationStrategy, InstanceLimits, ModuleLimits, - PoolingAllocationStrategy, Store, Strategy, + Config, Engine, InstanceAllocationStrategy, InstanceLimits, PoolingAllocationStrategy, Store, + Strategy, }; use wasmtime_wast::WastContext; @@ -77,18 +77,11 @@ fn run_wast(wast: &str, strategy: Strategy, pooling: bool) -> anyhow::Result<()> // fails to grow, the values here will need to be adjusted. cfg.allocation_strategy(InstanceAllocationStrategy::Pooling { strategy: PoolingAllocationStrategy::NextAvailable, - module_limits: ModuleLimits { - imported_memories: 2, - imported_tables: 2, - imported_globals: 11, - memories: 2, - tables: 4, - globals: 13, - memory_pages: 805, - ..Default::default() - }, instance_limits: InstanceLimits { count: 450, + memories: 2, + tables: 4, + memory_pages: 805, ..Default::default() }, });