give sychronous ResourceLimiter an async alternative

This commit is contained in:
Pat Hickey
2021-09-28 12:21:13 -07:00
parent e04357505e
commit 18a355e092
19 changed files with 373 additions and 236 deletions

View File

@@ -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"]

View File

@@ -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;

View File

@@ -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(),
})?;

View File

@@ -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<usize>) -> 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<u32>) -> 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<usize>,
) -> 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<u32>) -> 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<usize>) -> bool {
match self.memory_size {

View File

@@ -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;

View File

@@ -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<T> {
/// Generic metadata about the store that doesn't need access to `T`.
inner: StoreOpaque,
limiter: Option<Box<dyn FnMut(&mut T) -> &mut (dyn crate::ResourceLimiter) + Send + Sync>>,
limiter: Option<ResourceLimiterInner<T>>,
call_hook: Option<Box<dyn FnMut(&mut T, CallHook) -> Result<(), crate::Trap> + Send + Sync>>,
// for comments about `ManuallyDrop`, see `Store::into_data`
data: ManuallyDrop<T>,
}
enum ResourceLimiterInner<T> {
Sync(Box<dyn FnMut(&mut T) -> &mut (dyn crate::ResourceLimiter) + Send + Sync>),
#[cfg(feature = "async")]
Async(Box<dyn FnMut(&mut T) -> &mut (dyn crate::ResourceLimiterAsync) + Send + Sync>),
}
// Forward methods on `StoreOpaque` to also being on `StoreInner<T>`
impl<T> Deref for StoreInner<T> {
type Target = StoreOpaque;
@@ -402,7 +408,7 @@ impl<T> Store<T> {
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<T> Store<T> {
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<T> Store<T> {
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<T> StoreInner<T> {
&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<T> wasmtime_runtime::Store for StoreInner<T> {
(&mut inner.externref_activations_table, &inner.modules)
}
fn limiter(&mut self) -> Option<&mut dyn wasmtime_runtime::ResourceLimiter> {
<Self>::limiter(self)
fn limiter_memory_growing(
&mut self,
current: usize,
desired: usize,
maximum: Option<usize>,
) -> 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<u32>) -> 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<dyn Error + Send + Sync>> {

View File

@@ -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: &[],
},
)?;

View File

@@ -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<F> {
@@ -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: &[],
})?,
)