From 15bb0c69031499df1ee682edb5570d43aaea10d0 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Fri, 25 Feb 2022 09:11:51 -0600 Subject: [PATCH] Remove the `ModuleLimits` pooling configuration structure (#3837) * Remove the `ModuleLimits` pooling configuration structure This commit is an attempt to improve the usability of the pooling allocator by removing the need to configure a `ModuleLimits` structure. Internally this structure has limits on all forms of wasm constructs but this largely bottoms out in the size of an allocation for an instance in the instance pooling allocator. Maintaining this list of limits can be cumbersome as modules may get tweaked over time and there's otherwise no real reason to limit the number of globals in a module since the main goal is to limit the memory consumption of a `VMContext` which can be done with a memory allocation limit rather than fine-tuned control over each maximum and minimum. The new approach taken in this commit is to remove `ModuleLimits`. Some fields, such as `tables`, `table_elements` , `memories`, and `memory_pages` are moved to `InstanceLimits` since they're still enforced at runtime. A new field `size` is added to `InstanceLimits` which indicates, in bytes, the maximum size of the `VMContext` allocation. If the size of a `VMContext` for a module exceeds this value then instantiation will fail. This involved adding a few more checks to `{Table, Memory}::new_static` to ensure that the minimum size is able to fit in the allocation, since previously modules were validated at compile time of the module that everything fit and that validation no longer happens (it happens at runtime). A consequence of this commit is that Wasmtime will have no built-in way to reject modules at compile time if they'll fail to be instantiated within a particular pooling allocator configuration. Instead a module must attempt instantiation see if a failure happens. * Fix benchmark compiles * Fix some doc links * Fix a panic by ensuring modules have limited tables/memories * Review comments * Add back validation at `Module` time instantiation is possible This allows for getting an early signal at compile time that a module will never be instantiable in an engine with matching settings. * Provide a better error message when sizes are exceeded Improve the error message when an instance size exceeds the maximum by providing a breakdown of where the bytes are all going and why the large size is being requested. * Try to fix test in qemu * Flag new test as 64-bit only Sizes are all specific to 64-bit right now --- benches/instantiation.rs | 9 +- benches/thread_eager_init.rs | 4 +- crates/environ/src/vmoffsets.rs | 61 ++ crates/fuzzing/src/generators.rs | 121 +-- crates/runtime/src/instance/allocator.rs | 4 +- .../runtime/src/instance/allocator/pooling.rs | 889 ++++++------------ .../src/instance/allocator/pooling/uffd.rs | 30 +- crates/runtime/src/lib.rs | 4 +- crates/runtime/src/memory.rs | 9 + crates/runtime/src/table.rs | 8 + crates/wasmtime/src/config.rs | 14 +- crates/wasmtime/src/config/pooling.rs | 279 ------ crates/wasmtime/src/module.rs | 16 +- src/lib.rs | 8 +- tests/all/async_functions.rs | 8 +- tests/all/instance.rs | 5 +- tests/all/limits.rs | 15 +- tests/all/memory.rs | 6 +- tests/all/pooling_allocator.rs | 142 ++- tests/all/wast.rs | 15 +- 20 files changed, 563 insertions(+), 1084 deletions(-) delete mode 100644 crates/wasmtime/src/config/pooling.rs diff --git a/benches/instantiation.rs b/benches/instantiation.rs index d7cb53146b35..2e919c4d8734 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 9a7971d16eb9..9bbfeb0414fa 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 d30f6b924481..8092f8d705ed 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 b946235cf32a..4d641a6b21c2 100644 --- a/crates/fuzzing/src/generators.rs +++ b/crates/fuzzing/src/generators.rs @@ -60,88 +60,48 @@ 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, +/// Configuration for `wasmtime::PoolingAllocationStrategy`. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +#[allow(missing_docs)] +pub struct InstanceLimits { + pub count: u32, + pub memories: u32, + pub tables: u32, + pub memory_pages: u64, + pub table_elements: u32, + pub size: usize, } -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, +impl InstanceLimits { + fn to_wasmtime(&self) -> wasmtime::InstanceLimits { + wasmtime::InstanceLimits { + count: self.count, memories: self.memories, - globals: self.globals, - table_elements: self.table_elements, + tables: self.tables, memory_pages: self.memory_pages, + table_elements: self.table_elements, + size: self.size, } } } -impl<'a> Arbitrary<'a> for ModuleLimits { +impl<'a> Arbitrary<'a> for InstanceLimits { 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_COUNT: u32 = 100; + 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 + const MAX_SIZE: usize = 1 << 20; // 1 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)] -pub struct InstanceLimits { - /// The maximum number of instances that can be instantiated in the pool at a time. - pub count: u32, -} - -impl InstanceLimits { - fn to_wasmtime(&self) -> wasmtime::InstanceLimits { - wasmtime::InstanceLimits { count: self.count } - } -} - -impl<'a> Arbitrary<'a> for InstanceLimits { - fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { - const MAX_COUNT: u32 = 100; - - Ok(Self { 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 3428d76c8d09..8a7a796c8c69 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 ef9bb33455ee..f1e2a4c52edd 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,193 +57,125 @@ 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. +/// Represents the limits placed on instances by 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, +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. + /// + /// * 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 number of defined tables for a module. + /// 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 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. + /// 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 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, - ); - } - } + /// 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, - Ok(()) - } + /// 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 ModuleLimits { +impl Default for InstanceLimits { fn default() -> Self { - // See doc comments for `wasmtime::ModuleLimits` for these default values + // See doc comments for `wasmtime::InstanceLimits` for these default values Self { - imported_functions: 1000, - imported_tables: 0, - imported_memories: 0, - imported_globals: 0, - types: 100, - functions: 10000, + count: 1000, + size: 1 << 20, // 1 MB tables: 1, + table_elements: 10_000, 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. - pub count: u32, -} - -impl Default for InstanceLimits { - fn default() -> Self { - // See doc comments for `wasmtime::InstanceLimits` for these default values - Self { count: 1000 } - } -} - /// The allocation strategy to use for the pooling instance allocator. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PoolingAllocationStrategy { @@ -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 737445dc5b50..21b52c6298bd 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 e279d91d8cf0..ea5b8093f888 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 b310ad8a86d9..6ee2628a1731 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 e5edff489dc8..63b4e440d7ce 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 00689904e920..29edbbbeac05 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 6f8f9ae62b5f..000000000000 --- 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 e71d3e28d2b7..d31c3a47147a 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 fc40d9d0d4b8..b94d6862dc1a 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 584501ded30f..e4aaf9c6aa62 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 32a2b706a2ea..43cf53cd130e 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 f1ee6d7c1882..49224ef857a3 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 a43f823bf27a..5a2b01716ae7 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 d17b65af8288..fa867aceb514 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 + // 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(), + "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 compilation should fail"), + 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" + "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 1d0bf0ef9683..588400d0bb48 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,20 +77,13 @@ 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, + instance_limits: InstanceLimits { + count: 450, memories: 2, tables: 4, - globals: 13, memory_pages: 805, ..Default::default() }, - instance_limits: InstanceLimits { - count: 450, - ..Default::default() - }, }); Some(lock_pooling()) } else {