diff --git a/crates/fuzzing/src/oracles.rs b/crates/fuzzing/src/oracles.rs index b656dc2eac41..6c10de3db605 100644 --- a/crates/fuzzing/src/oracles.rs +++ b/crates/fuzzing/src/oracles.rs @@ -97,13 +97,18 @@ impl StoreLimits { } impl ResourceLimiter for StoreLimits { - fn memory_growing(&mut self, current: usize, desired: usize, _maximum: Option) -> bool { - self.alloc(desired - current) + fn memory_growing( + &mut self, + current: usize, + desired: usize, + _maximum: Option, + ) -> Result { + Ok(self.alloc(desired - current)) } - fn table_growing(&mut self, current: u32, desired: u32, _maximum: Option) -> bool { + fn table_growing(&mut self, current: u32, desired: u32, _maximum: Option) -> Result { let delta = (desired - current) as usize * std::mem::size_of::(); - self.alloc(delta) + Ok(self.alloc(delta)) } } diff --git a/crates/wasmtime/src/limits.rs b/crates/wasmtime/src/limits.rs index afe679d13154..718048544b17 100644 --- a/crates/wasmtime/src/limits.rs +++ b/crates/wasmtime/src/limits.rs @@ -1,3 +1,5 @@ +use anyhow::{bail, Result}; + /// Value returned by [`ResourceLimiter::instances`] default method pub const DEFAULT_INSTANCE_LIMIT: usize = 10000; /// Value returned by [`ResourceLimiter::tables`] default method @@ -40,22 +42,36 @@ pub trait ResourceLimiter { /// The `current` and `desired` amounts are guaranteed to always be /// multiples of the WebAssembly page size, 64KiB. /// - /// This function should return `true` to indicate that the growing - /// operation is permitted or `false` if not permitted. Returning `true` - /// when a maximum has been exceeded will have no effect as the linear - /// memory will not grow. + /// This function is not invoked when the requested size doesn't fit in + /// `usize`. Additionally this function is not invoked for shared memories + /// at this time. Otherwise even when `desired` exceeds `maximum` this + /// function will still be called. + /// + /// ## Return Value /// - /// This function is not guaranteed to be invoked for all requests to - /// `memory.grow`. Requests where the allocation requested size doesn't fit - /// in `usize` or exceeds the memory's listed maximum size may not invoke - /// this method. + /// If `Ok(true)` is returned from this function then the growth operation + /// is allowed. This means that the wasm `memory.grow` instruction will + /// return with the `desired` size, in wasm pages. Note that even if + /// `Ok(true)` is returned, though, if `desired` exceeds `maximum` then the + /// growth operation will still fail. /// - /// Returning `false` from this method will cause the `memory.grow` + /// If `Ok(false)` is returned then this will cause the `memory.grow` /// instruction in a module to return -1 (failure), or in the case of an /// embedder API calling [`Memory::new`](crate::Memory::new) or /// [`Memory::grow`](crate::Memory::grow) an error will be returned from /// those methods. - fn memory_growing(&mut self, current: usize, desired: usize, maximum: Option) -> bool; + /// + /// If `Err(e)` is returned then the `memory.grow` function will behave + /// as if a trap has been raised. Note that this is not necessarily + /// compliant with the WebAssembly specification but it can be a handy and + /// useful tool to get a precise backtrace at "what requested so much memory + /// to cause a growth failure?". + fn memory_growing( + &mut self, + current: usize, + desired: usize, + maximum: Option, + ) -> Result; /// Notifies the resource limiter that growing a linear memory, permitted by /// the `memory_growing` method, has failed. @@ -73,17 +89,12 @@ pub trait ResourceLimiter { /// * `maximum` is either the table's maximum or a maximum from an instance /// allocator. A value of `None` indicates that the table is unbounded. /// - /// This function should return `true` to indicate that the growing - /// operation is permitted or `false` if not permitted. Returning `true` - /// when a maximum has been exceeded will have no effect as the table will - /// not grow. - /// /// Currently in Wasmtime each table element requires a pointer's worth of /// space (e.g. `mem::size_of::()`). /// - /// Like `memory_growing` returning `false` from this function will cause - /// `table.grow` to return -1 or embedder APIs will return an error. - fn table_growing(&mut self, current: u32, desired: u32, maximum: Option) -> bool; + /// See the details on the return values for `memory_growing` for what the + /// return value of this function indicates. + fn table_growing(&mut self, current: u32, desired: u32, maximum: Option) -> Result; /// Notifies the resource limiter that growing a linear memory, permitted by /// the `table_growing` method, has failed. @@ -146,13 +157,18 @@ pub trait ResourceLimiterAsync { current: usize, desired: usize, maximum: Option, - ) -> bool; + ) -> Result; /// Identical to [`ResourceLimiter::memory_grow_failed`] fn memory_grow_failed(&mut self, _error: &anyhow::Error) {} /// Asynchronous version of [`ResourceLimiter::table_growing`] - async fn table_growing(&mut self, current: u32, desired: u32, maximum: Option) -> bool; + async fn table_growing( + &mut self, + current: u32, + desired: u32, + maximum: Option, + ) -> Result; /// Identical to [`ResourceLimiter::table_grow_failed`] fn table_grow_failed(&mut self, _error: &anyhow::Error) {} @@ -187,7 +203,10 @@ impl StoreLimitsBuilder { /// The maximum number of bytes a linear memory can grow to. /// - /// Growing a linear memory beyond this limit will fail. + /// Growing a linear memory beyond this limit will fail. This limit is + /// applied to each linear memory individually, so if a wasm module has + /// multiple linear memories then they're all allowed to reach up to the + /// `limit` specified. /// /// By default, linear memory will not be limited. pub fn memory_size(mut self, limit: usize) -> Self { @@ -197,7 +216,9 @@ impl StoreLimitsBuilder { /// The maximum number of elements in a table. /// - /// Growing a table beyond this limit will fail. + /// Growing a table beyond this limit will fail. This limit is applied to + /// each table individually, so if a wasm module has multiple tables then + /// they're all allowed to reach up to the `limit` specified. /// /// By default, table elements will not be limited. pub fn table_elements(mut self, limit: u32) -> Self { @@ -235,6 +256,20 @@ impl StoreLimitsBuilder { self } + /// Indicates that a trap should be raised whenever a growth operation + /// would fail. + /// + /// This operation will force `memory.grow` and `table.grow` instructions + /// to raise a trap on failure instead of returning -1. This is not + /// necessarily spec-compliant, but it can be quite handy when debugging a + /// module that fails to allocate memory and might behave oddly as a result. + /// + /// This value defaults to `false`. + pub fn trap_on_grow_failure(mut self, trap: bool) -> Self { + self.0.trap_on_grow_failure = trap; + self + } + /// Consumes this builder and returns the [`StoreLimits`]. pub fn build(self) -> StoreLimits { self.0 @@ -249,12 +284,14 @@ impl StoreLimitsBuilder { /// This is a convenience type included to avoid needing to implement the /// [`ResourceLimiter`] trait if your use case fits in the static configuration /// that this [`StoreLimits`] provides. +#[derive(Clone, Debug)] pub struct StoreLimits { memory_size: Option, table_elements: Option, instances: usize, tables: usize, memories: usize, + trap_on_grow_failure: bool, } impl Default for StoreLimits { @@ -265,22 +302,44 @@ impl Default for StoreLimits { instances: DEFAULT_INSTANCE_LIMIT, tables: DEFAULT_TABLE_LIMIT, memories: DEFAULT_MEMORY_LIMIT, + trap_on_grow_failure: false, } } } impl ResourceLimiter for StoreLimits { - fn memory_growing(&mut self, _current: usize, desired: usize, _maximum: Option) -> bool { - match self.memory_size { + fn memory_growing( + &mut self, + _current: usize, + desired: usize, + maximum: Option, + ) -> Result { + let allow = match self.memory_size { Some(limit) if desired > limit => false, - _ => true, + _ => match maximum { + Some(max) if desired > max => false, + _ => true, + }, + }; + if !allow && self.trap_on_grow_failure { + bail!("forcing trap when growing memory to {desired} bytes") + } else { + Ok(allow) } } - fn table_growing(&mut self, _current: u32, desired: u32, _maximum: Option) -> bool { - match self.table_elements { + fn table_growing(&mut self, _current: u32, desired: u32, maximum: Option) -> Result { + let allow = match self.table_elements { Some(limit) if desired > limit => false, - _ => true, + _ => match maximum { + Some(max) if desired > max => false, + _ => true, + }, + }; + if !allow && self.trap_on_grow_failure { + bail!("forcing trap when growing table to {desired} elements") + } else { + Ok(allow) } } diff --git a/crates/wasmtime/src/store.rs b/crates/wasmtime/src/store.rs index 8b7f5f3e6175..71a9a54f7f89 100644 --- a/crates/wasmtime/src/store.rs +++ b/crates/wasmtime/src/store.rs @@ -1874,19 +1874,18 @@ unsafe impl wasmtime_runtime::Store for StoreInner { ) -> Result { match self.limiter { Some(ResourceLimiterInner::Sync(ref mut limiter)) => { - Ok(limiter(&mut self.data).memory_growing(current, desired, maximum)) + limiter(&mut self.data).memory_growing(current, desired, maximum) } #[cfg(feature = "async")] Some(ResourceLimiterInner::Async(ref mut limiter)) => unsafe { - Ok(self - .inner + self.inner .async_cx() .expect("ResourceLimiterAsync requires async Store") .block_on( limiter(&mut self.data) .memory_growing(current, desired, maximum) .as_mut(), - )?) + )? }, None => Ok(true), } @@ -1923,17 +1922,17 @@ unsafe impl wasmtime_runtime::Store for StoreInner { match self.limiter { Some(ResourceLimiterInner::Sync(ref mut limiter)) => { - Ok(limiter(&mut self.data).table_growing(current, desired, maximum)) + limiter(&mut self.data).table_growing(current, desired, maximum) } #[cfg(feature = "async")] Some(ResourceLimiterInner::Async(ref mut limiter)) => unsafe { - Ok(async_cx + async_cx .expect("ResourceLimiterAsync requires async Store") .block_on( limiter(&mut self.data) .table_growing(current, desired, maximum) .as_mut(), - )?) + )? }, None => Ok(true), } diff --git a/src/commands/run.rs b/src/commands/run.rs index 9fef95d04c8b..e8e8fd94f842 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -9,7 +9,9 @@ use std::io::Write; use std::path::{Component, Path, PathBuf}; use std::thread; use std::time::Duration; -use wasmtime::{Engine, Func, Linker, Module, Store, Val, ValType}; +use wasmtime::{ + Engine, Func, Linker, Module, Store, StoreLimits, StoreLimitsBuilder, Val, ValType, +}; use wasmtime_cli_flags::{CommonOptions, WasiModules}; use wasmtime_wasi::maybe_exit_on_error; use wasmtime_wasi::sync::{ambient_authority, Dir, TcpListener, WasiCtxBuilder}; @@ -166,6 +168,38 @@ pub struct RunCommand { /// The arguments to pass to the module #[clap(value_name = "ARGS")] module_args: Vec, + + /// Maximum size, in bytes, that a linear memory is allowed to reach. + /// + /// Growth beyond this limit will cause `memory.grow` instructions in + /// WebAssembly modules to return -1 and fail. + #[clap(long, value_name = "BYTES")] + max_memory_size: Option, + + /// Maximum size, in table elements, that a table is allowed to reach. + #[clap(long)] + max_table_elements: Option, + + /// Maximum number of WebAssembly instances allowed to be created. + #[clap(long)] + max_instances: Option, + + /// Maximum number of WebAssembly tables allowed to be created. + #[clap(long)] + max_tables: Option, + + /// Maximum number of WebAssembly linear memories allowed to be created. + #[clap(long)] + max_memories: Option, + + /// Force a trap to be raised on `memory.grow` and `table.grow` failure + /// instead of returning -1 from these instructions. + /// + /// This is not necessarily a spec-compliant option to enable but can be + /// useful for tracking down a backtrace of what is requesting so much + /// memory, for example. + #[clap(long)] + trap_on_grow_failure: bool, } impl RunCommand { @@ -212,6 +246,27 @@ impl RunCommand { preopen_sockets, )?; + let mut limits = StoreLimitsBuilder::new(); + if let Some(max) = self.max_memory_size { + limits = limits.memory_size(max); + } + if let Some(max) = self.max_table_elements { + limits = limits.table_elements(max); + } + if let Some(max) = self.max_instances { + limits = limits.instances(max); + } + if let Some(max) = self.max_tables { + limits = limits.tables(max); + } + if let Some(max) = self.max_memories { + limits = limits.memories(max); + } + store.data_mut().limits = limits + .trap_on_grow_failure(self.trap_on_grow_failure) + .build(); + store.limiter(|t| &mut t.limits); + // If fuel has been configured, we want to add the configured // fuel amount to this store. if let Some(fuel) = self.common.fuel { @@ -470,6 +525,7 @@ struct Host { wasi_nn: Option>, #[cfg(feature = "wasi-threads")] wasi_threads: Option>>, + limits: StoreLimits, } /// Populates the given `Linker` with WASI APIs. diff --git a/tests/all/limits.rs b/tests/all/limits.rs index 2d798e5d1d8a..02b356525b0a 100644 --- a/tests/all/limits.rs +++ b/tests/all/limits.rs @@ -111,16 +111,16 @@ async fn test_limits_async() -> Result<()> { _current: usize, desired: usize, _maximum: Option, - ) -> bool { - desired <= self.memory_size + ) -> Result { + Ok(desired <= self.memory_size) } async fn table_growing( &mut self, _current: u32, desired: u32, _maximum: Option, - ) -> bool { - desired <= self.table_elements + ) -> Result { + Ok(desired <= self.table_elements) } } @@ -394,7 +394,12 @@ struct MemoryContext { } impl ResourceLimiter for MemoryContext { - fn memory_growing(&mut self, current: usize, desired: usize, maximum: Option) -> bool { + fn memory_growing( + &mut self, + current: usize, + desired: usize, + maximum: Option, + ) -> Result { // Check if the desired exceeds a maximum (either from Wasm or from the host) assert!(desired < maximum.unwrap_or(usize::MAX)); @@ -403,14 +408,19 @@ impl ResourceLimiter for MemoryContext { if desired + self.host_memory_used > self.memory_limit { self.limit_exceeded = true; - return false; + return Ok(false); } self.wasm_memory_used = desired; - true + Ok(true) } - fn table_growing(&mut self, _current: u32, _desired: u32, _maximum: Option) -> bool { - true + fn table_growing( + &mut self, + _current: u32, + _desired: u32, + _maximum: Option, + ) -> Result { + Ok(true) } } @@ -501,7 +511,7 @@ impl ResourceLimiterAsync for MemoryContext { current: usize, desired: usize, maximum: Option, - ) -> bool { + ) -> Result { // Show we can await in this async context: tokio::time::sleep(std::time::Duration::from_millis(1)).await; // Check if the desired exceeds a maximum (either from Wasm or from the host) @@ -512,14 +522,19 @@ impl ResourceLimiterAsync for MemoryContext { if desired + self.host_memory_used > self.memory_limit { self.limit_exceeded = true; - return false; + return Ok(false); } self.wasm_memory_used = desired; - true + Ok(true) } - async fn table_growing(&mut self, _current: u32, _desired: u32, _maximum: Option) -> bool { - true + async fn table_growing( + &mut self, + _current: u32, + _desired: u32, + _maximum: Option, + ) -> Result { + Ok(true) } fn table_grow_failed(&mut self, _e: &anyhow::Error) {} } @@ -619,20 +634,20 @@ impl ResourceLimiter for TableContext { _current: usize, _desired: usize, _maximum: Option, - ) -> bool { - true + ) -> Result { + Ok(true) } - fn table_growing(&mut self, current: u32, desired: u32, maximum: Option) -> bool { + fn table_growing(&mut self, current: u32, desired: u32, maximum: Option) -> Result { // Check if the desired exceeds a maximum (either from Wasm or from the host) assert!(desired < maximum.unwrap_or(u32::MAX)); assert_eq!(current, self.elements_used); - if desired > self.element_limit { + Ok(if desired > self.element_limit { self.limit_exceeded = true; - return false; + false } else { self.elements_used = desired; true - } + }) } } @@ -693,18 +708,23 @@ struct FailureDetector { } impl ResourceLimiter for FailureDetector { - fn memory_growing(&mut self, current: usize, desired: usize, _maximum: Option) -> bool { + fn memory_growing( + &mut self, + current: usize, + desired: usize, + _maximum: Option, + ) -> Result { self.memory_current = current; self.memory_desired = desired; - true + Ok(true) } fn memory_grow_failed(&mut self, err: &anyhow::Error) { self.memory_error = Some(err.to_string()); } - fn table_growing(&mut self, current: u32, desired: u32, _maximum: Option) -> bool { + fn table_growing(&mut self, current: u32, desired: u32, _maximum: Option) -> Result { self.table_current = current; self.table_desired = desired; - true + Ok(true) } fn table_grow_failed(&mut self, err: &anyhow::Error) { self.table_error = Some(err.to_string()); @@ -793,21 +813,26 @@ impl ResourceLimiterAsync for FailureDetector { current: usize, desired: usize, _maximum: Option, - ) -> bool { + ) -> Result { // Show we can await in this async context: tokio::time::sleep(std::time::Duration::from_millis(1)).await; self.memory_current = current; self.memory_desired = desired; - true + Ok(true) } fn memory_grow_failed(&mut self, err: &anyhow::Error) { self.memory_error = Some(err.to_string()); } - async fn table_growing(&mut self, current: u32, desired: u32, _maximum: Option) -> bool { + async fn table_growing( + &mut self, + current: u32, + desired: u32, + _maximum: Option, + ) -> Result { self.table_current = current; self.table_desired = desired; - true + Ok(true) } fn table_grow_failed(&mut self, err: &anyhow::Error) { self.table_error = Some(err.to_string()); @@ -903,10 +928,15 @@ impl ResourceLimiter for Panic { _current: usize, _desired: usize, _maximum: Option, - ) -> bool { + ) -> Result { panic!("resource limiter memory growing"); } - fn table_growing(&mut self, _current: u32, _desired: u32, _maximum: Option) -> bool { + fn table_growing( + &mut self, + _current: u32, + _desired: u32, + _maximum: Option, + ) -> Result { panic!("resource limiter table growing"); } } @@ -917,10 +947,15 @@ impl ResourceLimiterAsync for Panic { _current: usize, _desired: usize, _maximum: Option, - ) -> bool { + ) -> Result { panic!("async resource limiter memory growing"); } - async fn table_growing(&mut self, _current: u32, _desired: u32, _maximum: Option) -> bool { + async fn table_growing( + &mut self, + _current: u32, + _desired: u32, + _maximum: Option, + ) -> Result { panic!("async resource limiter table growing"); } } @@ -1059,3 +1094,61 @@ async fn panic_in_async_table_limiter() { .await .unwrap(); } + +#[test] +fn growth_trap() -> Result<()> { + let engine = Engine::default(); + let module = Module::new( + &engine, + r#"(module + (memory $m (export "m") 0) + (table (export "t") 0 anyfunc) + (func (export "grow") (param i32) (result i32) + (memory.grow $m (local.get 0))) + )"#, + )?; + + let mut store = Store::new( + &engine, + StoreLimitsBuilder::new() + .memory_size(WASM_PAGE_SIZE) + .table_elements(1) + .trap_on_grow_failure(true) + .build(), + ); + store.limiter(|s| s as &mut dyn ResourceLimiter); + + let instance = Instance::new(&mut store, &module, &[])?; + + // Test instance exports and host objects hitting the limit + for memory in [ + instance.get_memory(&mut store, "m").unwrap(), + Memory::new(&mut store, MemoryType::new(0, None))?, + ] { + memory.grow(&mut store, 1)?; + assert!(memory.grow(&mut store, 1).is_err()); + } + + // Test instance exports and host objects hitting the limit + for table in [ + instance.get_table(&mut store, "t").unwrap(), + Table::new( + &mut store, + TableType::new(ValType::FuncRef, 0, None), + Val::FuncRef(None), + )?, + ] { + table.grow(&mut store, 1, Val::FuncRef(None))?; + assert!(table.grow(&mut store, 1, Val::FuncRef(None)).is_err()); + } + + let mut store = Store::new(&engine, store.data().clone()); + store.limiter(|s| s as &mut dyn ResourceLimiter); + let instance = Instance::new(&mut store, &module, &[])?; + let grow = instance.get_func(&mut store, "grow").unwrap(); + let grow = grow.typed::(&store).unwrap(); + grow.call(&mut store, 1)?; + assert!(grow.call(&mut store, 1).is_err()); + + Ok(()) +} diff --git a/tests/all/memory.rs b/tests/all/memory.rs index 2f7663db9342..a2eaa0e54acb 100644 --- a/tests/all/memory.rs +++ b/tests/all/memory.rs @@ -306,11 +306,16 @@ fn massive_64_bit_still_limited() -> Result<()> { _current: usize, _request: usize, _max: Option, - ) -> bool { + ) -> Result { self.hit = true; - true + Ok(true) } - fn table_growing(&mut self, _current: u32, _request: u32, _max: Option) -> bool { + fn table_growing( + &mut self, + _current: u32, + _request: u32, + _max: Option, + ) -> Result { unreachable!() } } diff --git a/tests/all/pooling_allocator.rs b/tests/all/pooling_allocator.rs index eb3ec2fe8f2b..8eaafbf63a64 100644 --- a/tests/all/pooling_allocator.rs +++ b/tests/all/pooling_allocator.rs @@ -528,12 +528,12 @@ fn drop_externref_global_during_module_init() -> Result<()> { struct Limiter; impl ResourceLimiter for Limiter { - fn memory_growing(&mut self, _: usize, _: usize, _: Option) -> bool { - false + fn memory_growing(&mut self, _: usize, _: usize, _: Option) -> Result { + Ok(false) } - fn table_growing(&mut self, _: u32, _: u32, _: Option) -> bool { - false + fn table_growing(&mut self, _: u32, _: u32, _: Option) -> Result { + Ok(false) } } diff --git a/tests/rlimited-memory.rs b/tests/rlimited-memory.rs index 0dd9d57a7a3e..810ea07d5d2c 100644 --- a/tests/rlimited-memory.rs +++ b/tests/rlimited-memory.rs @@ -13,16 +13,26 @@ struct MemoryGrowFailureDetector { } impl ResourceLimiter for MemoryGrowFailureDetector { - fn memory_growing(&mut self, current: usize, desired: usize, _maximum: Option) -> bool { + fn memory_growing( + &mut self, + current: usize, + desired: usize, + _maximum: Option, + ) -> Result { self.current = current; self.desired = desired; - true + Ok(true) } fn memory_grow_failed(&mut self, err: &anyhow::Error) { self.error = Some(err.to_string()); } - fn table_growing(&mut self, _current: u32, _desired: u32, _maximum: Option) -> bool { - true + fn table_growing( + &mut self, + _current: u32, + _desired: u32, + _maximum: Option, + ) -> Result { + Ok(true) } }