diff --git a/Cargo.lock b/Cargo.lock index 8c73ff9549..5631a2d6c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -108,9 +108,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.50" +version = "0.1.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b98e84bbb4cbcdd97da190ba0c58a1bb0de2c1fdf67d159e192ed766aeca722" +checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e" dependencies = [ "proc-macro2", "quote", @@ -3362,6 +3362,7 @@ name = "wasmtime" version = "0.30.0" dependencies = [ "anyhow", + "async-trait", "backtrace", "bincode", "cfg-if 1.0.0", @@ -3459,6 +3460,7 @@ name = "wasmtime-cli" version = "0.30.0" dependencies = [ "anyhow", + "async-trait", "criterion", "env_logger 0.8.3", "file-per-thread-logger", diff --git a/Cargo.toml b/Cargo.toml index 0fa869d330..a5d19c3c88 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,7 @@ criterion = "0.3.4" num_cpus = "1.13.0" winapi = { version = "0.3.9", features = ['memoryapi'] } memchr = "2.4" +async-trait = "0.1" [build-dependencies] anyhow = "1.0.19" diff --git a/crates/runtime/src/instance.rs b/crates/runtime/src/instance.rs index f69b17a8ae..3bff9803ea 100644 --- a/crates/runtime/src/instance.rs +++ b/crates/runtime/src/instance.rs @@ -12,7 +12,6 @@ use crate::vmcontext::{ VMInterrupts, VMMemoryDefinition, VMMemoryImport, VMTableDefinition, VMTableImport, }; use crate::{ExportFunction, ExportGlobal, ExportMemory, ExportTable, Store}; -use anyhow::Error; use memoffset::offset_of; use more_asserts::assert_lt; use std::alloc::Layout; @@ -33,86 +32,6 @@ mod allocator; pub use allocator::*; -/// Value returned by [`ResourceLimiter::instances`] default method -pub const DEFAULT_INSTANCE_LIMIT: usize = 10000; -/// Value returned by [`ResourceLimiter::tables`] default method -pub const DEFAULT_TABLE_LIMIT: usize = 10000; -/// Value returned by [`ResourceLimiter::memories`] default method -pub const DEFAULT_MEMORY_LIMIT: usize = 10000; - -/// Used by hosts to limit resource consumption of instances. -/// -/// An instance can be created with a resource limiter so that hosts can take into account -/// non-WebAssembly resource usage to determine if a linear memory or table should grow. -pub trait ResourceLimiter { - /// Notifies the resource limiter that an instance's linear memory has been - /// requested to grow. - /// - /// * `current` is the current size of the linear memory in bytes. - /// * `desired` is the desired size of the linear memory in bytes. - /// * `maximum` is either the linear memory's maximum or a maximum from an - /// instance allocator, also in bytes. A value of `None` - /// indicates that the linear memory 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 linear - /// memory will not grow. - /// - /// 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. - fn memory_growing(&mut self, current: usize, desired: usize, maximum: Option) -> bool; - - /// Notifies the resource limiter that growing a linear memory, permitted by - /// the `memory_growing` method, has failed. - /// - /// Reasons for failure include: the growth exceeds the `maximum` passed to - /// `memory_growing`, or the operating system failed to allocate additional - /// memory. In that case, `error` might be downcastable to a `std::io::Error`. - fn memory_grow_failed(&mut self, _error: &Error) {} - - /// Notifies the resource limiter that an instance's table has been requested to grow. - /// - /// * `current` is the current number of elements in the table. - /// * `desired` is the desired number of elements in the table. - /// * `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. - fn table_growing(&mut self, current: u32, desired: u32, maximum: Option) -> bool; - - /// The maximum number of instances that can be created for a `Store`. - /// - /// Module instantiation will fail if this limit is exceeded. - /// - /// This value defaults to 10,000. - fn instances(&self) -> usize { - DEFAULT_INSTANCE_LIMIT - } - - /// The maximum number of tables that can be created for a `Store`. - /// - /// Module instantiation will fail if this limit is exceeded. - /// - /// This value defaults to 10,000. - fn tables(&self) -> usize { - DEFAULT_TABLE_LIMIT - } - - /// The maximum number of linear memories that can be created for a `Store` - /// - /// Instantiation will fail with an error if this limit is exceeded. - /// - /// This value defaults to 10,000. - fn memories(&self) -> usize { - DEFAULT_MEMORY_LIMIT - } -} - /// A type that roughly corresponds to a WebAssembly instance, but is also used /// for host-defined objects. /// @@ -441,10 +360,10 @@ impl Instance { (foreign_memory_index, foreign_instance) } }; - let limiter = unsafe { (*instance.store()).limiter() }; + let store = unsafe { &mut *instance.store() }; let memory = &mut instance.memories[idx]; - let result = unsafe { memory.grow(delta, limiter) }; + let result = unsafe { memory.grow(delta, store) }; let vmmemory = memory.vmmemory(); // Update the state used by wasm code in case the base pointer and/or @@ -480,13 +399,13 @@ impl Instance { delta: u32, init_value: TableElement, ) -> Option { - let limiter = unsafe { (*self.store()).limiter() }; + let store = unsafe { &mut *self.store() }; let table = self .tables .get_mut(table_index) .unwrap_or_else(|| panic!("no table for index {}", table_index.index())); - let result = unsafe { table.grow(delta, init_value, limiter) }; + let result = unsafe { table.grow(delta, init_value, store) }; // Keep the `VMContext` pointers used by compiled Wasm code up to // date. diff --git a/crates/runtime/src/instance/allocator.rs b/crates/runtime/src/instance/allocator.rs index 0caa891c27..0826eecd8b 100644 --- a/crates/runtime/src/instance/allocator.rs +++ b/crates/runtime/src/instance/allocator.rs @@ -1,5 +1,5 @@ use crate::imports::Imports; -use crate::instance::{Instance, InstanceHandle, ResourceLimiter, RuntimeMemoryCreator}; +use crate::instance::{Instance, InstanceHandle, RuntimeMemoryCreator}; use crate::memory::{DefaultMemoryCreator, Memory}; use crate::table::Table; use crate::traphandlers::Trap; @@ -58,13 +58,13 @@ pub struct InstanceAllocationRequest<'a> { /// are a bit of a lie. This is done purely so a store can learn about /// itself when it gets called as a host function, and additionally so this /// runtime can access internals as necessary (such as the - /// VMExternRefActivationsTable or the ResourceLimiter). + /// VMExternRefActivationsTable or the resource limiter methods). /// /// Note that this ends up being a self-pointer to the instance when stored. /// The reason is that the instance itself is then stored within the store. /// We use a number of `PhantomPinned` declarations to indicate this to the /// compiler. More info on this in `wasmtime/src/store.rs` - pub store: Option<*mut dyn Store>, + pub store: StorePtr, /// A list of all wasm data that can be referenced by the module that /// will be allocated. The `Module` given here has active/passive data @@ -77,6 +77,37 @@ pub struct InstanceAllocationRequest<'a> { pub wasm_data: *const [u8], } +/// A pointer to a Store. This Option<*mut dyn Store> is wrapped in a struct +/// so that the function to create a &mut dyn Store is a method on a member of +/// InstanceAllocationRequest, rather than on a &mut InstanceAllocationRequest +/// itself, because several use-sites require a split mut borrow on the +/// InstanceAllocationRequest. +pub struct StorePtr(Option<*mut dyn Store>); +impl StorePtr { + /// A pointer to no Store. + pub fn empty() -> Self { + Self(None) + } + /// A pointer to a Store. + pub fn new(ptr: *mut dyn Store) -> Self { + Self(Some(ptr)) + } + /* + /// Update an empty StorePtr to point to a Store. + pub fn set(&mut self, ptr: *mut dyn Store) { + self.0 = Some(ptr) + } + */ + /// Use the StorePtr as a mut ref to the Store. + // XXX should this be an unsafe fn? is it always safe at a use site? + pub(crate) fn get(&mut self) -> Option<&mut dyn Store> { + match self.0 { + Some(ptr) => Some(unsafe { &mut *ptr }), + None => None, + } + } +} + /// An link error while instantiating a module. #[derive(Error, Debug)] #[error("Link error: {0}")] @@ -430,7 +461,7 @@ fn initialize_instance( } unsafe fn initialize_vmcontext(instance: &mut Instance, req: InstanceAllocationRequest) { - if let Some(store) = req.store { + if let Some(store) = req.store.0 { *instance.interrupts() = (*store).vminterrupts(); *instance.externref_activations_table() = (*store).externref_activations_table().0; instance.set_store(store); @@ -581,17 +612,6 @@ pub struct OnDemandInstanceAllocator { stack_size: usize, } -// rustc is quite strict with the lifetimes when dealing with mutable borrows, -// so this is a little helper to get a shorter lifetime on `Option<&mut T>` -fn borrow_limiter<'a>( - limiter: &'a mut Option<&mut dyn ResourceLimiter>, -) -> Option<&'a mut dyn ResourceLimiter> { - match limiter { - Some(limiter) => Some(&mut **limiter), - None => None, - } -} - impl OnDemandInstanceAllocator { /// Creates a new on-demand instance allocator. pub fn new(mem_creator: Option>, stack_size: usize) -> Self { @@ -605,15 +625,20 @@ impl OnDemandInstanceAllocator { fn create_tables( module: &Module, - mut limiter: Option<&mut dyn ResourceLimiter>, + store: &mut StorePtr, ) -> Result, InstantiationError> { let num_imports = module.num_imported_tables; let mut tables: PrimaryMap = PrimaryMap::with_capacity(module.table_plans.len() - num_imports); for table in &module.table_plans.values().as_slice()[num_imports..] { tables.push( - Table::new_dynamic(table, borrow_limiter(&mut limiter)) - .map_err(InstantiationError::Resource)?, + Table::new_dynamic( + table, + store + .get() + .expect("if module has table plans, store is not empty"), + ) + .map_err(InstantiationError::Resource)?, ); } Ok(tables) @@ -622,7 +647,7 @@ impl OnDemandInstanceAllocator { fn create_memories( &self, module: &Module, - mut limiter: Option<&mut dyn ResourceLimiter>, + store: &mut StorePtr, ) -> Result, InstantiationError> { let creator = self .mem_creator @@ -633,8 +658,14 @@ impl OnDemandInstanceAllocator { PrimaryMap::with_capacity(module.memory_plans.len() - num_imports); for plan in &module.memory_plans.values().as_slice()[num_imports..] { memories.push( - Memory::new_dynamic(plan, creator, borrow_limiter(&mut limiter)) - .map_err(InstantiationError::Resource)?, + Memory::new_dynamic( + plan, + creator, + store + .get() + .expect("if module has memory plans, store is not empty"), + ) + .map_err(InstantiationError::Resource)?, ); } Ok(memories) @@ -656,9 +687,8 @@ unsafe impl InstanceAllocator for OnDemandInstanceAllocator { &self, mut req: InstanceAllocationRequest, ) -> Result { - let mut limiter = req.store.and_then(|s| (*s).limiter()); - let memories = self.create_memories(&req.module, borrow_limiter(&mut limiter))?; - let tables = Self::create_tables(&req.module, borrow_limiter(&mut limiter))?; + let memories = self.create_memories(&req.module, &mut req.store)?; + let tables = Self::create_tables(&req.module, &mut req.store)?; let host_state = std::mem::replace(&mut req.host_state, Box::new(())); diff --git a/crates/runtime/src/instance/allocator/pooling.rs b/crates/runtime/src/instance/allocator/pooling.rs index 2d074aa17c..b99986a498 100644 --- a/crates/runtime/src/instance/allocator/pooling.rs +++ b/crates/runtime/src/instance/allocator/pooling.rs @@ -7,10 +7,9 @@ //! Using the pooling instance allocator can speed up module instantiation //! when modules can be constrained based on configurable limits. -use super::borrow_limiter; use super::{ initialize_instance, initialize_vmcontext, InstanceAllocationRequest, InstanceAllocator, - InstanceHandle, InstantiationError, ResourceLimiter, + InstanceHandle, InstantiationError, }; use crate::{instance::Instance, Memory, Mmap, Table, VMContext}; use anyhow::{anyhow, bail, Context, Result}; @@ -385,19 +384,16 @@ impl InstancePool { instance.host_state = std::mem::replace(&mut req.host_state, Box::new(())); instance.wasm_data = &*req.wasm_data; - let mut limiter = req.store.and_then(|s| (*s).limiter()); Self::set_instance_memories( instance, self.memories.get(index), self.memories.max_wasm_pages, - borrow_limiter(&mut limiter), )?; Self::set_instance_tables( instance, self.tables.get(index).map(|x| x as *mut usize), self.tables.max_elements, - borrow_limiter(&mut limiter), )?; initialize_vmcontext(instance, req); @@ -503,7 +499,6 @@ impl InstancePool { instance: &mut Instance, mut memories: impl Iterator, max_pages: u64, - mut limiter: Option<&mut dyn ResourceLimiter>, ) -> Result<(), InstantiationError> { let module = instance.module.as_ref(); @@ -519,12 +514,9 @@ impl InstancePool { ) }; instance.memories.push( - Memory::new_static( - plan, - memory, - commit_memory_pages, - borrow_limiter(&mut limiter), - ) + Memory::new_static(plan, memory, commit_memory_pages, unsafe { + &mut *instance.store() + }) .map_err(InstantiationError::Resource)?, ); } @@ -538,7 +530,6 @@ impl InstancePool { instance: &mut Instance, mut tables: impl Iterator, max_elements: u32, - mut limiter: Option<&mut dyn ResourceLimiter>, ) -> Result<(), InstantiationError> { let module = instance.module.as_ref(); @@ -555,7 +546,7 @@ impl InstancePool { let table = unsafe { std::slice::from_raw_parts_mut(base, max_elements as usize) }; instance.tables.push( - Table::new_static(plan, table, borrow_limiter(&mut limiter)) + Table::new_static(plan, table, unsafe { &mut *instance.store() }) .map_err(InstantiationError::Resource)?, ); } @@ -1052,7 +1043,7 @@ unsafe impl InstanceAllocator for PoolingInstanceAllocator { #[cfg(test)] mod test { use super::*; - use crate::{Imports, VMSharedSignatureIndex}; + use crate::{Imports, StorePtr, VMSharedSignatureIndex}; use wasmtime_environ::{ EntityRef, Global, GlobalInit, Memory, MemoryPlan, ModuleType, SignatureIndex, Table, TablePlan, TableStyle, WasmType, @@ -1414,7 +1405,7 @@ mod test { }, shared_signatures: VMSharedSignatureIndex::default().into(), host_state: Box::new(()), - store: None, + store: StorePtr::empty(), wasm_data: &[], }, ) @@ -1438,7 +1429,7 @@ mod test { }, shared_signatures: VMSharedSignatureIndex::default().into(), host_state: Box::new(()), - store: None, + store: StorePtr::empty(), wasm_data: &[], }, ) { diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs index 4a2ca4d3a2..f9afeed984 100644 --- a/crates/runtime/src/lib.rs +++ b/crates/runtime/src/lib.rs @@ -42,8 +42,7 @@ pub use crate::imports::Imports; pub use crate::instance::{ InstanceAllocationRequest, InstanceAllocator, InstanceHandle, InstanceLimits, InstantiationError, LinkError, ModuleLimits, OnDemandInstanceAllocator, - PoolingAllocationStrategy, PoolingInstanceAllocator, ResourceLimiter, DEFAULT_INSTANCE_LIMIT, - DEFAULT_MEMORY_LIMIT, DEFAULT_TABLE_LIMIT, + PoolingAllocationStrategy, PoolingInstanceAllocator, StorePtr, }; pub use crate::jit_int::GdbJitImageRegistration; pub use crate::memory::{Memory, RuntimeLinearMemory, RuntimeMemoryCreator}; @@ -91,8 +90,18 @@ pub unsafe trait Store { &mut self, ) -> (&mut VMExternRefActivationsTable, &dyn ModuleInfoLookup); - /// Returns a reference to the store's limiter for limiting resources, if any. - fn limiter(&mut self) -> Option<&mut dyn ResourceLimiter>; + /// Callback invoked to allow the store's resource limiter to reject a memory grow operation. + fn limiter_memory_growing( + &mut self, + current: usize, + desired: usize, + maximum: Option, + ) -> bool; + /// Callback invoked to notify the store's resource limiter that a memory grow operation has + /// failed. + fn limiter_memory_grow_failed(&mut self, error: &anyhow::Error); + /// Callback invoked to allow the store's resource limiter to reject a table grow operation. + fn limiter_table_growing(&mut self, current: u32, desired: u32, maximum: Option) -> bool; /// Callback invoked whenever fuel runs out by a wasm instance. If an error /// is returned that's raised as a trap. Otherwise wasm execution will diff --git a/crates/runtime/src/memory.rs b/crates/runtime/src/memory.rs index 0f25c7a1d8..e9dde6689f 100644 --- a/crates/runtime/src/memory.rs +++ b/crates/runtime/src/memory.rs @@ -4,8 +4,8 @@ use crate::mmap::Mmap; use crate::vmcontext::VMMemoryDefinition; -use crate::ResourceLimiter; -use anyhow::{bail, format_err, Error, Result}; +use crate::Store; +use anyhow::{bail, format_err, Result}; use more_asserts::{assert_ge, assert_le}; use std::convert::TryFrom; use wasmtime_environ::{MemoryPlan, MemoryStyle, WASM32_MAX_PAGES, WASM64_MAX_PAGES}; @@ -212,33 +212,14 @@ pub enum Memory { Dynamic(Box), } -fn memory_growing( - limiter: &mut Option<&mut dyn ResourceLimiter>, - current: usize, - desired: usize, - maximum: Option, -) -> bool { - match limiter { - Some(ref mut l) => l.memory_growing(current, desired, maximum), - None => true, - } -} - -fn memory_grow_failed(limiter: &mut Option<&mut dyn ResourceLimiter>, error: &Error) { - match limiter { - Some(l) => l.memory_grow_failed(error), - None => {} - } -} - impl Memory { /// Create a new dynamic (movable) memory instance for the specified plan. pub fn new_dynamic( plan: &MemoryPlan, creator: &dyn RuntimeMemoryCreator, - limiter: Option<&mut dyn ResourceLimiter>, + store: &mut dyn Store, ) -> Result { - let (minimum, maximum) = Self::limit_new(plan, limiter)?; + let (minimum, maximum) = Self::limit_new(plan, store)?; Ok(Memory::Dynamic(creator.new_memory(plan, minimum, maximum)?)) } @@ -247,9 +228,9 @@ impl Memory { plan: &MemoryPlan, base: &'static mut [u8], make_accessible: fn(*mut u8, usize) -> Result<()>, - limiter: Option<&mut dyn ResourceLimiter>, + store: &mut dyn Store, ) -> Result { - let (minimum, maximum) = Self::limit_new(plan, limiter)?; + let (minimum, maximum) = Self::limit_new(plan, store)?; let base = match maximum { Some(max) if max < base.len() => &mut base[..max], @@ -269,15 +250,11 @@ impl Memory { }) } - /// Calls the `limiter`, if specified, to optionally prevent a memory from - /// being allocated. + /// Calls the `store`'s limiter to optionally prevent a memory from being allocated. /// /// Returns the minimum size and optional maximum size of the memory, in /// bytes. - fn limit_new( - plan: &MemoryPlan, - mut limiter: Option<&mut dyn ResourceLimiter>, - ) -> Result<(usize, Option)> { + fn limit_new(plan: &MemoryPlan, store: &mut dyn Store) -> Result<(usize, Option)> { // Sanity-check what should already be true from wasm module validation. let absolute_max = if plan.memory.memory64 { WASM64_MAX_PAGES @@ -291,7 +268,7 @@ impl Memory { // allocate, which is our entire address space minus a wasm page. That // shouldn't ever actually work in terms of an allocation because // presumably the kernel wants *something* for itself, but this is used - // to pass to the `limiter` specified, if present, for a requested size + // to pass to the `store`'s limiter for a requested size // to approximate the scale of the request that the wasm module is // making. This is necessary because the limiter works on `usize` bytes // whereas we're working with possibly-overflowing `u64` calculations @@ -302,7 +279,7 @@ impl Memory { // If the minimum memory size overflows the size of our own address // space, then we can't satisfy this request, but defer the error to - // later so the `limiter` can be informed that an effective oom is + // later so the `store` can be informed that an effective oom is // happening. let minimum = plan .memory @@ -332,13 +309,13 @@ impl Memory { maximum = usize::try_from(1u64 << 32).ok(); } - // Inform the limiter what's about to happen. This will let the limiter + // Inform the store's limiter what's about to happen. This will let the limiter // reject anything if necessary, and this also guarantees that we should // call the limiter for all requested memories, even if our `minimum` // calculation overflowed. This means that the `minimum` we're informing // the limiter is lossy and may not be 100% accurate, but for now the - // expected uses of `limiter` means that's ok. - if !memory_growing(&mut limiter, 0, minimum.unwrap_or(absolute_max), maximum) { + // expected uses of limiter means that's ok. + if !store.limiter_memory_growing(0, minimum.unwrap_or(absolute_max), maximum) { bail!( "memory minimum size of {} pages exceeds memory limits", plan.memory.minimum @@ -400,11 +377,7 @@ impl Memory { /// /// Generally, prefer using `InstanceHandle::memory_grow`, which encapsulates /// this unsafety. - pub unsafe fn grow( - &mut self, - delta_pages: u64, - mut limiter: Option<&mut dyn ResourceLimiter>, - ) -> Option { + pub unsafe fn grow(&mut self, delta_pages: u64, store: &mut dyn Store) -> Option { let old_byte_size = self.byte_size(); // Wasm spec: when growing by 0 pages, always return the current size. if delta_pages == 0 { @@ -428,15 +401,15 @@ impl Memory { }; let maximum = self.maximum_byte_size(); - // Limiter gets first chance to reject memory_growing. - if !memory_growing(&mut limiter, old_byte_size, new_byte_size, maximum) { + // Store limiter gets first chance to reject memory_growing. + if !store.limiter_memory_growing(old_byte_size, new_byte_size, maximum) { return None; } // Never exceed maximum, even if limiter permitted it. if let Some(max) = maximum { if new_byte_size > max { - memory_grow_failed(&mut limiter, &format_err!("Memory maximum size exceeded")); + store.limiter_memory_grow_failed(&format_err!("Memory maximum size exceeded")); return None; } } @@ -458,7 +431,7 @@ impl Memory { } => { // Never exceed static memory size if new_byte_size > base.len() { - memory_grow_failed(&mut limiter, &format_err!("static memory size exceeded")); + store.limiter_memory_grow_failed(&format_err!("static memory size exceeded")); return None; } @@ -467,13 +440,13 @@ impl Memory { base.as_mut_ptr().add(old_byte_size), new_byte_size - old_byte_size, ); - r.map_err(|e| memory_grow_failed(&mut limiter, &e)).ok()?; + r.map_err(|e| store.limiter_memory_grow_failed(&e)).ok()?; *size = new_byte_size; } Memory::Dynamic(mem) => { let r = mem.grow_to(new_byte_size); - r.map_err(|e| memory_grow_failed(&mut limiter, &e)).ok()?; + r.map_err(|e| store.limiter_memory_grow_failed(&e)).ok()?; } } Some(old_byte_size) diff --git a/crates/runtime/src/table.rs b/crates/runtime/src/table.rs index c6e977d308..690a1ed82a 100644 --- a/crates/runtime/src/table.rs +++ b/crates/runtime/src/table.rs @@ -3,7 +3,7 @@ //! `Table` is to WebAssembly tables what `LinearMemory` is to WebAssembly linear memories. use crate::vmcontext::{VMCallerCheckedAnyfunc, VMTableDefinition}; -use crate::{ResourceLimiter, Trap, VMExternRef}; +use crate::{Store, Trap, VMExternRef}; use anyhow::{bail, Result}; use std::convert::{TryFrom, TryInto}; use std::ops::Range; @@ -137,11 +137,8 @@ fn wasm_to_table_type(ty: WasmType) -> Result { impl Table { /// Create a new dynamic (movable) table instance for the specified table plan. - pub fn new_dynamic( - plan: &TablePlan, - limiter: Option<&mut dyn ResourceLimiter>, - ) -> Result { - Self::limit_new(plan, limiter)?; + pub fn new_dynamic(plan: &TablePlan, store: &mut dyn Store) -> Result { + Self::limit_new(plan, store)?; let elements = vec![0; plan.table.minimum as usize]; let ty = wasm_to_table_type(plan.table.wasm_ty)?; let maximum = plan.table.maximum; @@ -157,9 +154,9 @@ impl Table { pub fn new_static( plan: &TablePlan, data: &'static mut [usize], - limiter: Option<&mut dyn ResourceLimiter>, + store: &mut dyn Store, ) -> Result { - Self::limit_new(plan, limiter)?; + Self::limit_new(plan, store)?; let size = plan.table.minimum; let ty = wasm_to_table_type(plan.table.wasm_ty)?; let data = match plan.table.maximum { @@ -170,14 +167,12 @@ impl Table { Ok(Table::Static { data, size, ty }) } - fn limit_new(plan: &TablePlan, limiter: Option<&mut dyn ResourceLimiter>) -> Result<()> { - if let Some(limiter) = limiter { - if !limiter.table_growing(0, plan.table.minimum, plan.table.maximum) { - bail!( - "table minimum size of {} elements exceeds table limits", - plan.table.minimum - ); - } + fn limit_new(plan: &TablePlan, store: &mut dyn Store) -> Result<()> { + if !store.limiter_table_growing(0, plan.table.minimum, plan.table.maximum) { + bail!( + "table minimum size of {} elements exceeds table limits", + plan.table.minimum + ); } Ok(()) } @@ -292,15 +287,13 @@ impl Table { &mut self, delta: u32, init_value: TableElement, - limiter: Option<&mut dyn ResourceLimiter>, + store: &mut dyn Store, ) -> Option { let old_size = self.size(); let new_size = old_size.checked_add(delta)?; - if let Some(limiter) = limiter { - if !limiter.table_growing(old_size, new_size, self.maximum()) { - return None; - } + if !store.limiter_table_growing(old_size, new_size, self.maximum()) { + return None; } if let Some(max) = self.maximum() { diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml index 188920241f..daa9edadaf 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -38,6 +38,7 @@ psm = "0.1.11" lazy_static = "1.4" rayon = { version = "1.0", optional = true } object = { version = "0.27", default-features = false, features = ['read_core', 'elf'] } +async-trait = { version = "0.1.51", optional = true } [target.'cfg(target_os = "windows")'.dependencies] winapi = "0.3.7" @@ -73,7 +74,7 @@ cache = ["wasmtime-cache"] # Enables support for "async stores" as well as defining host functions as # `async fn` and calling functions asynchronously. -async = ["wasmtime-fiber", "wasmtime-runtime/async"] +async = ["wasmtime-fiber", "wasmtime-runtime/async", "async-trait"] # Enables userfaultfd support in the runtime's pooling allocator when building on Linux uffd = ["wasmtime-runtime/uffd"] diff --git a/crates/wasmtime/src/externals.rs b/crates/wasmtime/src/externals.rs index 6324bac13e..8f3d7e1645 100644 --- a/crates/wasmtime/src/externals.rs +++ b/crates/wasmtime/src/externals.rs @@ -551,7 +551,7 @@ impl Table { let init = init.into_table_element(store, ty)?; let table = self.wasmtime_table(store); unsafe { - match (*table).grow(delta, init, store.limiter()) { + match (*table).grow(delta, init, store) { Some(size) => { let vm = (*table).vmtable(); *store[self.0].definition = vm; diff --git a/crates/wasmtime/src/instance.rs b/crates/wasmtime/src/instance.rs index bd7afcfba8..e11971739a 100644 --- a/crates/wasmtime/src/instance.rs +++ b/crates/wasmtime/src/instance.rs @@ -15,7 +15,7 @@ use wasmtime_environ::{ }; use wasmtime_jit::TypeTables; use wasmtime_runtime::{ - Imports, InstanceAllocationRequest, InstantiationError, VMContext, VMFunctionBody, + Imports, InstanceAllocationRequest, InstantiationError, StorePtr, VMContext, VMFunctionBody, VMFunctionImport, VMGlobalImport, VMMemoryImport, VMTableImport, }; @@ -734,7 +734,7 @@ impl<'a> Instantiator<'a> { imports: self.cur.build(), shared_signatures: self.cur.module.signatures().as_module_map().into(), host_state: Box::new(Instance(instance_to_be)), - store: Some(store.traitobj()), + store: StorePtr::new(store.traitobj()), wasm_data: compiled_module.wasm_data(), })?; diff --git a/crates/wasmtime/src/limits.rs b/crates/wasmtime/src/limits.rs index 6f9ca1e23e..acca7dad4e 100644 --- a/crates/wasmtime/src/limits.rs +++ b/crates/wasmtime/src/limits.rs @@ -1,4 +1,118 @@ -pub use wasmtime_runtime::ResourceLimiter; +/// Value returned by [`ResourceLimiter::instances`] default method +pub const DEFAULT_INSTANCE_LIMIT: usize = 10000; +/// Value returned by [`ResourceLimiter::tables`] default method +pub const DEFAULT_TABLE_LIMIT: usize = 10000; +/// Value returned by [`ResourceLimiter::memories`] default method +pub const DEFAULT_MEMORY_LIMIT: usize = 10000; + +/// Used by hosts to limit resource consumption of instances. +/// +/// An instance can be created with a resource limiter so that hosts can take into account +/// non-WebAssembly resource usage to determine if a linear memory or table should grow. +pub trait ResourceLimiter { + /// Notifies the resource limiter that an instance's linear memory has been + /// requested to grow. + /// + /// * `current` is the current size of the linear memory in bytes. + /// * `desired` is the desired size of the linear memory in bytes. + /// * `maximum` is either the linear memory's maximum or a maximum from an + /// instance allocator, also in bytes. A value of `None` + /// indicates that the linear memory 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 linear + /// memory will not grow. + /// + /// 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. + fn memory_growing(&mut self, current: usize, desired: usize, maximum: Option) -> bool; + + /// Notifies the resource limiter that growing a linear memory, permitted by + /// the `memory_growing` method, has failed. + /// + /// Reasons for failure include: the growth exceeds the `maximum` passed to + /// `memory_growing`, or the operating system failed to allocate additional + /// memory. In that case, `error` might be downcastable to a `std::io::Error`. + fn memory_grow_failed(&mut self, _error: &anyhow::Error) {} + + /// Notifies the resource limiter that an instance's table has been requested to grow. + /// + /// * `current` is the current number of elements in the table. + /// * `desired` is the desired number of elements in the table. + /// * `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. + fn table_growing(&mut self, current: u32, desired: u32, maximum: Option) -> bool; + + /// The maximum number of instances that can be created for a `Store`. + /// + /// Module instantiation will fail if this limit is exceeded. + /// + /// This value defaults to 10,000. + fn instances(&self) -> usize { + DEFAULT_INSTANCE_LIMIT + } + + /// The maximum number of tables that can be created for a `Store`. + /// + /// Module instantiation will fail if this limit is exceeded. + /// + /// This value defaults to 10,000. + fn tables(&self) -> usize { + DEFAULT_TABLE_LIMIT + } + + /// The maximum number of linear memories that can be created for a `Store` + /// + /// Instantiation will fail with an error if this limit is exceeded. + /// + /// This value defaults to 10,000. + fn memories(&self) -> usize { + DEFAULT_MEMORY_LIMIT + } +} + +#[cfg(feature = "async")] +/// Used by hosts to limit resource consumption of instances. +/// Identical to [`ResourceLimiter`], except that the `memory_growing` and `table_growing` +/// functions are async. Must be used with an async [`Store`]. +#[async_trait::async_trait] +pub trait ResourceLimiterAsync { + /// Async version of [`ResourceLimiter::memory_growing`] + async fn memory_growing( + &mut self, + current: usize, + desired: usize, + maximum: Option, + ) -> bool; + + /// 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; + + /// Identical to [`ResourceLimiter::instances`]` + fn instances(&self) -> usize { + DEFAULT_INSTANCE_LIMIT + } + + /// Identical to [`ResourceLimiter::tables`]` + fn tables(&self) -> usize { + DEFAULT_TABLE_LIMIT + } + + /// Identical to [`ResourceLimiter::memories`]` + fn memories(&self) -> usize { + DEFAULT_MEMORY_LIMIT + } +} /// Used to build [`StoreLimits`]. pub struct StoreLimitsBuilder(StoreLimits); @@ -79,13 +193,14 @@ impl Default for StoreLimits { Self { memory_size: None, table_elements: None, - instances: wasmtime_runtime::DEFAULT_INSTANCE_LIMIT, - tables: wasmtime_runtime::DEFAULT_TABLE_LIMIT, - memories: wasmtime_runtime::DEFAULT_MEMORY_LIMIT, + instances: DEFAULT_INSTANCE_LIMIT, + tables: DEFAULT_TABLE_LIMIT, + memories: DEFAULT_MEMORY_LIMIT, } } } +#[cfg_attr(feature = "async", async_trait::async_trait)] impl ResourceLimiter for StoreLimits { fn memory_growing(&mut self, _current: usize, desired: usize, _maximum: Option) -> bool { match self.memory_size { diff --git a/crates/wasmtime/src/memory.rs b/crates/wasmtime/src/memory.rs index a1b305118b..1d04a6edad 100644 --- a/crates/wasmtime/src/memory.rs +++ b/crates/wasmtime/src/memory.rs @@ -461,7 +461,7 @@ impl Memory { let store = store.as_context_mut().0; let mem = self.wasmtime_memory(store); unsafe { - match (*mem).grow(delta, store.limiter()) { + match (*mem).grow(delta, store) { Some(size) => { let vm = (*mem).vmmemory(); *store[self.0].definition = vm; diff --git a/crates/wasmtime/src/store.rs b/crates/wasmtime/src/store.rs index 365574bd11..4a72c18959 100644 --- a/crates/wasmtime/src/store.rs +++ b/crates/wasmtime/src/store.rs @@ -93,8 +93,8 @@ use std::sync::Arc; use std::task::{Context, Poll}; use wasmtime_runtime::{ InstanceAllocationRequest, InstanceAllocator, InstanceHandle, ModuleInfo, - OnDemandInstanceAllocator, SignalHandler, VMCallerCheckedAnyfunc, VMContext, VMExternRef, - VMExternRefActivationsTable, VMInterrupts, VMSharedSignatureIndex, VMTrampoline, + OnDemandInstanceAllocator, SignalHandler, StorePtr, VMCallerCheckedAnyfunc, VMContext, + VMExternRef, VMExternRefActivationsTable, VMInterrupts, VMSharedSignatureIndex, VMTrampoline, }; mod context; @@ -197,12 +197,18 @@ pub struct StoreInner { /// Generic metadata about the store that doesn't need access to `T`. inner: StoreOpaque, - limiter: Option &mut (dyn crate::ResourceLimiter) + Send + Sync>>, + limiter: Option>, call_hook: Option Result<(), crate::Trap> + Send + Sync>>, // for comments about `ManuallyDrop`, see `Store::into_data` data: ManuallyDrop, } +enum ResourceLimiterInner { + Sync(Box &mut (dyn crate::ResourceLimiter) + Send + Sync>), + #[cfg(feature = "async")] + Async(Box &mut (dyn crate::ResourceLimiterAsync) + Send + Sync>), +} + // Forward methods on `StoreOpaque` to also being on `StoreInner` impl Deref for StoreInner { type Target = StoreOpaque; @@ -402,7 +408,7 @@ impl Store { shared_signatures: None.into(), imports: Default::default(), module: Arc::new(wasmtime_environ::Module::default()), - store: None, + store: StorePtr::empty(), wasm_data: &[], }) .expect("failed to allocate default callee") @@ -418,11 +424,11 @@ impl Store { modules: ModuleRegistry::default(), host_trampolines: HashMap::default(), instance_count: 0, - instance_limit: wasmtime_runtime::DEFAULT_INSTANCE_LIMIT, + instance_limit: crate::DEFAULT_INSTANCE_LIMIT, memory_count: 0, - memory_limit: wasmtime_runtime::DEFAULT_MEMORY_LIMIT, + memory_limit: crate::DEFAULT_MEMORY_LIMIT, table_count: 0, - table_limit: wasmtime_runtime::DEFAULT_TABLE_LIMIT, + table_limit: crate::DEFAULT_TABLE_LIMIT, fuel_adj: 0, #[cfg(feature = "async")] async_state: AsyncState { @@ -525,7 +531,36 @@ impl Store { innermost.memory_limit = memory_limit; // Save the limiter accessor function: - inner.limiter = Some(Box::new(limiter)); + inner.limiter = Some(ResourceLimiterInner::Sync(Box::new(limiter))); + } + + /// Configures the [`ResourceLimiterAsync`](crate::ResourceLimiterAsync) used to limit + /// resource creation within this [`Store`]. Must be used with an async `Store`!. + /// + /// Note that this limiter is only used to limit the creation/growth of + /// resources in the future, this does not retroactively attempt to apply + /// limits to the [`Store`]. + pub fn limiter_async( + &mut self, + mut limiter: impl FnMut(&mut T) -> &mut (dyn crate::ResourceLimiterAsync) + + Send + + Sync + + 'static, + ) { + debug_assert!(self.inner.async_support()); + // Apply the limits on instances, tables, and memory given by the limiter: + let inner = &mut self.inner; + let (instance_limit, table_limit, memory_limit) = { + let l = limiter(&mut inner.data); + (l.instances(), l.tables(), l.memories()) + }; + let innermost = &mut inner.inner; + innermost.instance_limit = instance_limit; + innermost.table_limit = table_limit; + innermost.memory_limit = memory_limit; + + // Save the limiter accessor function: + inner.limiter = Some(ResourceLimiterInner::Async(Box::new(limiter))); } /// Configure a function that runs on calls and returns between WebAssembly @@ -872,11 +907,6 @@ impl StoreInner { &mut self.data } - pub fn limiter(&mut self) -> Option<&mut dyn crate::limits::ResourceLimiter> { - let accessor = self.limiter.as_mut()?; - Some(accessor(&mut self.data)) - } - pub fn call_hook(&mut self, s: CallHook) -> Result<(), Trap> { if let Some(hook) = &mut self.call_hook { hook(&mut self.data, s) @@ -1496,8 +1526,81 @@ unsafe impl wasmtime_runtime::Store for StoreInner { (&mut inner.externref_activations_table, &inner.modules) } - fn limiter(&mut self) -> Option<&mut dyn wasmtime_runtime::ResourceLimiter> { - ::limiter(self) + fn limiter_memory_growing( + &mut self, + current: usize, + desired: usize, + maximum: Option, + ) -> bool { + // Need to borrow async_cx before the mut borrow of the limiter. + // self.async_cx() panicks when used with a non-async store, so + // wrap this in an option. + #[cfg(feature = "async")] + let async_cx = if self.async_support() { + Some(self.async_cx()) + } else { + None + }; + match self.limiter { + Some(ResourceLimiterInner::Sync(ref mut limiter)) => { + limiter(&mut self.data).memory_growing(current, desired, maximum) + } + #[cfg(feature = "async")] + Some(ResourceLimiterInner::Async(ref mut limiter)) => unsafe { + async_cx + .expect("ResourceLimiterAsync requires async Store") + .block_on( + limiter(&mut self.data) + .memory_growing(current, desired, maximum) + .as_mut(), + ) + .expect("FIXME idk how to deal with a trap here!") + }, + None => true, + } + } + + fn limiter_memory_grow_failed(&mut self, error: &anyhow::Error) { + match self.limiter { + Some(ResourceLimiterInner::Sync(ref mut limiter)) => { + limiter(&mut self.data).memory_grow_failed(error) + } + #[cfg(feature = "async")] + Some(ResourceLimiterInner::Async(ref mut limiter)) => { + limiter(&mut self.data).memory_grow_failed(error) + } + None => {} + } + } + + fn limiter_table_growing(&mut self, current: u32, desired: u32, maximum: Option) -> bool { + // Need to borrow async_cx before the mut borrow of the limiter. + // self.async_cx() panicks when used with a non-async store, so + // wrap this in an option. + #[cfg(feature = "async")] + let async_cx = if self.async_support() { + Some(self.async_cx()) + } else { + None + }; + + match self.limiter { + Some(ResourceLimiterInner::Sync(ref mut limiter)) => { + limiter(&mut self.data).table_growing(current, desired, maximum) + } + #[cfg(feature = "async")] + Some(ResourceLimiterInner::Async(ref mut limiter)) => unsafe { + async_cx + .expect("ResourceLimiterAsync requires async Store") + .block_on( + limiter(&mut self.data) + .table_growing(current, desired, maximum) + .as_mut(), + ) + .expect("FIXME idk how to deal with a trap here!") + }, + None => true, + } } fn out_of_gas(&mut self) -> Result<(), Box> { diff --git a/crates/wasmtime/src/trampoline.rs b/crates/wasmtime/src/trampoline.rs index 4452d7b42a..c1f8038a5a 100644 --- a/crates/wasmtime/src/trampoline.rs +++ b/crates/wasmtime/src/trampoline.rs @@ -18,7 +18,7 @@ use std::any::Any; use std::sync::Arc; use wasmtime_environ::{EntityIndex, GlobalIndex, MemoryIndex, Module, TableIndex}; use wasmtime_runtime::{ - Imports, InstanceAllocationRequest, InstanceAllocator, OnDemandInstanceAllocator, + Imports, InstanceAllocationRequest, InstanceAllocator, OnDemandInstanceAllocator, StorePtr, VMFunctionImport, VMSharedSignatureIndex, }; @@ -46,7 +46,7 @@ fn create_handle( imports, shared_signatures: shared_signature_id.into(), host_state, - store: Some(store.traitobj()), + store: StorePtr::new(store.traitobj()), wasm_data: &[], }, )?; diff --git a/crates/wasmtime/src/trampoline/func.rs b/crates/wasmtime/src/trampoline/func.rs index 5c2e2a1b84..e37803763f 100644 --- a/crates/wasmtime/src/trampoline/func.rs +++ b/crates/wasmtime/src/trampoline/func.rs @@ -9,7 +9,8 @@ use wasmtime_environ::{EntityIndex, Module, ModuleType, PrimaryMap, SignatureInd use wasmtime_jit::{CodeMemory, MmapVec}; use wasmtime_runtime::{ Imports, InstanceAllocationRequest, InstanceAllocator, InstanceHandle, - OnDemandInstanceAllocator, VMContext, VMFunctionBody, VMSharedSignatureIndex, VMTrampoline, + OnDemandInstanceAllocator, StorePtr, VMContext, VMFunctionBody, VMSharedSignatureIndex, + VMTrampoline, }; struct TrampolineState { @@ -131,7 +132,7 @@ pub unsafe fn create_raw_function( imports: Imports::default(), shared_signatures: sig.into(), host_state, - store: None, + store: StorePtr::empty(), wasm_data: &[], })?, ) diff --git a/tests/all/limits.rs b/tests/all/limits.rs index c6490c05a8..7e1b303906 100644 --- a/tests/all/limits.rs +++ b/tests/all/limits.rs @@ -300,7 +300,6 @@ impl ResourceLimiter for MemoryContext { self.wasm_memory_used = desired; true } - fn table_growing(&mut self, _current: u32, _desired: u32, _maximum: Option) -> bool { true } @@ -401,11 +400,9 @@ impl ResourceLimiter for MemoryGrowFailureDetector { self.desired = desired; 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 } diff --git a/tests/all/memory.rs b/tests/all/memory.rs index e2d823508b..c212f50a5a 100644 --- a/tests/all/memory.rs +++ b/tests/all/memory.rs @@ -311,12 +311,16 @@ fn massive_64_bit_still_limited() -> Result<()> { } impl ResourceLimiter for MyLimiter { - fn memory_growing(&mut self, _request: usize, _min: usize, _max: Option) -> bool { + fn memory_growing( + &mut self, + _current: usize, + _request: usize, + _max: Option, + ) -> bool { self.hit = true; true } - - fn table_growing(&mut self, _request: u32, _min: u32, _max: Option) -> bool { + fn table_growing(&mut self, _current: u32, _request: u32, _max: Option) -> bool { unreachable!() } } diff --git a/tests/rlimited-memory.rs b/tests/rlimited-memory.rs index 2d64a6edef..5acc2f5d8f 100644 --- a/tests/rlimited-memory.rs +++ b/tests/rlimited-memory.rs @@ -18,11 +18,9 @@ impl ResourceLimiter for MemoryGrowFailureDetector { self.desired = desired; 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 }