Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add resource limiting to the Wasmtime API. #2736

Merged
merged 4 commits into from
Apr 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@

## Unreleased

### Added

* Added `Store::with_limits`, `StoreLimits`, and `ResourceLimiter` to the
Wasmtime API to help with enforcing resource limits at runtime. The
`ResourceLimiter` trait can be implemented by custom resource limiters to
decide if linear memories or tables can be grown.

### Changed

* Breaking: `Memory::new` has been changed to return `Result` as creating a
host memory object is now a fallible operation when the initial size of
the memory exceeds the store limits.

## 0.26.0

Released 2021-04-05.
Expand Down
5 changes: 0 additions & 5 deletions crates/c-api/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,3 @@ pub extern "C" fn wasmtime_config_static_memory_guard_size_set(c: &mut wasm_conf
pub extern "C" fn wasmtime_config_dynamic_memory_guard_size_set(c: &mut wasm_config_t, size: u64) {
c.config.dynamic_memory_guard_size(size);
}

#[no_mangle]
pub extern "C" fn wasmtime_config_max_instances_set(c: &mut wasm_config_t, limit: usize) {
c.config.max_instances(limit);
}
8 changes: 4 additions & 4 deletions crates/c-api/src/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ impl wasm_memory_t {
pub extern "C" fn wasm_memory_new(
store: &wasm_store_t,
mt: &wasm_memorytype_t,
) -> Box<wasm_memory_t> {
let memory = Memory::new(&store.store, mt.ty().ty.clone());
Box::new(wasm_memory_t {
) -> Option<Box<wasm_memory_t>> {
let memory = Memory::new(&store.store, mt.ty().ty.clone()).ok()?;
Some(Box::new(wasm_memory_t {
ext: wasm_extern_t {
which: memory.into(),
},
})
}))
}

#[no_mangle]
Expand Down
7 changes: 0 additions & 7 deletions crates/fuzzing/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,6 @@ pub fn fuzz_default_config(strategy: wasmtime::Strategy) -> anyhow::Result<wasmt
.wasm_bulk_memory(true)
.wasm_reference_types(true)
.wasm_module_linking(true)
// The limits here are chosen based on the default "maximum type size"
// configured in wasm-smith, which is 1000. This means that instances
// are allowed to, for example, export up to 1000 memories. We bump that
// a little bit here to give us some slop.
.max_instances(1100)
.max_tables(1100)
.max_memories(1100)
.strategy(strategy)?;
Ok(config)
}
27 changes: 21 additions & 6 deletions crates/fuzzing/src/oracles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,21 @@ fn log_wasm(wasm: &[u8]) {
}
}

fn create_store(engine: &Engine) -> Store {
Store::new_with_limits(
&engine,
StoreLimitsBuilder::new()
// The limits here are chosen based on the default "maximum type size"
// configured in wasm-smith, which is 1000. This means that instances
// are allowed to, for example, export up to 1000 memories. We bump that
// a little bit here to give us some slop.
.instances(1100)
.tables(1100)
.memories(1100)
.build(),
)
}

/// Methods of timing out execution of a WebAssembly module
#[derive(Debug)]
pub enum Timeout {
Expand Down Expand Up @@ -95,7 +110,7 @@ pub fn instantiate_with_config(
_ => false,
});
let engine = Engine::new(&config).unwrap();
let store = Store::new(&engine);
let store = create_store(&engine);

let mut timeout_state = SignalOnDrop::default();
match timeout {
Expand Down Expand Up @@ -203,7 +218,7 @@ pub fn differential_execution(
config.wasm_module_linking(false);

let engine = Engine::new(&config).unwrap();
let store = Store::new(&engine);
let store = create_store(&engine);

let module = Module::new(&engine, &wasm).unwrap();

Expand Down Expand Up @@ -348,7 +363,7 @@ pub fn make_api_calls(api: crate::generators::api::ApiCalls) {
ApiCall::StoreNew => {
log::trace!("creating store");
assert!(store.is_none());
store = Some(Store::new(engine.as_ref().unwrap()));
store = Some(create_store(engine.as_ref().unwrap()));
}

ApiCall::ModuleNew { id, wasm } => {
Expand Down Expand Up @@ -439,7 +454,7 @@ pub fn spectest(fuzz_config: crate::generators::Config, test: crate::generators:
config.wasm_reference_types(false);
config.wasm_bulk_memory(false);
config.wasm_module_linking(false);
let store = Store::new(&Engine::new(&config).unwrap());
let store = create_store(&Engine::new(&config).unwrap());
if fuzz_config.consume_fuel {
store.add_fuel(u64::max_value()).unwrap();
}
Expand All @@ -463,7 +478,7 @@ pub fn table_ops(
let mut config = fuzz_config.to_wasmtime();
config.wasm_reference_types(true);
let engine = Engine::new(&config).unwrap();
let store = Store::new(&engine);
let store = create_store(&engine);
if fuzz_config.consume_fuel {
store.add_fuel(u64::max_value()).unwrap();
}
Expand Down Expand Up @@ -578,7 +593,7 @@ pub fn differential_wasmi_execution(wasm: &[u8], config: &crate::generators::Con
let mut wasmtime_config = config.to_wasmtime();
wasmtime_config.cranelift_nan_canonicalization(true);
let wasmtime_engine = Engine::new(&wasmtime_config).unwrap();
let wasmtime_store = Store::new(&wasmtime_engine);
let wasmtime_store = create_store(&wasmtime_engine);
if config.consume_fuel {
wasmtime_store.add_fuel(u64::max_value()).unwrap();
}
Expand Down
2 changes: 1 addition & 1 deletion crates/fuzzing/src/oracles/dummy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ pub fn dummy_table(store: &Store, ty: TableType) -> Table {

/// Construct a dummy memory for the given memory type.
pub fn dummy_memory(store: &Store, ty: MemoryType) -> Memory {
Memory::new(store, ty)
Memory::new(store, ty).unwrap()
}

/// Construct a dummy instance for the given instance type.
Expand Down
78 changes: 60 additions & 18 deletions crates/runtime/src/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,52 @@ mod allocator;

pub use allocator::*;

/// 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 WebAssembly page units.
/// * `desired` is the desired size of the linear memory in WebAssembly page units.
/// * `maximum` is either the linear memory's maximum or a maximum from an instance allocator,
/// also in WebAssembly page units. 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.
fn memory_growing(&self, current: u32, desired: u32, maximum: Option<u32>) -> bool;

/// 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(&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.
fn instances(&self) -> usize;

/// The maximum number of tables that can be created for a `Store`.
///
/// Module instantiation will fail if this limit is exceeded.
fn tables(&self) -> usize;

/// The maximum number of tables that can be created for a `Store`.
///
/// Module instantiation will fail if this limit is exceeded.
fn memories(&self) -> usize;
}

/// Runtime representation of an instance value, which erases all `Instance`
/// information since instances are just a collection of values.
pub type RuntimeInstance = Rc<IndexMap<String, Export>>;
Expand Down Expand Up @@ -378,11 +424,12 @@ impl Instance {
/// Returns `None` if memory can't be grown by the specified amount
/// of pages.
pub(crate) fn memory_grow(&self, memory_index: DefinedMemoryIndex, delta: u32) -> Option<u32> {
let result = self
let memory = self
.memories
.get(memory_index)
.unwrap_or_else(|| panic!("no memory for index {}", memory_index.index()))
.grow(delta);
.unwrap_or_else(|| panic!("no memory for index {}", memory_index.index()));

let result = unsafe { memory.grow(delta) };

// Keep current the VMContext pointers used by compiled wasm code.
self.set_memory(memory_index, self.memories[memory_index].vmmemory());
Expand Down Expand Up @@ -460,19 +507,18 @@ impl Instance {
delta: u32,
init_value: TableElement,
) -> Option<u32> {
unsafe {
let orig_size = self
.tables
.get(table_index)
.unwrap_or_else(|| panic!("no table for index {}", table_index.index()))
.grow(delta, init_value)?;
let table = self
.tables
.get(table_index)
.unwrap_or_else(|| panic!("no table for index {}", table_index.index()));

// Keep the `VMContext` pointers used by compiled Wasm code up to
// date.
self.set_table(table_index, self.tables[table_index].vmtable());
let result = unsafe { table.grow(delta, init_value) };

Some(orig_size)
}
// Keep the `VMContext` pointers used by compiled Wasm code up to
// date.
self.set_table(table_index, self.tables[table_index].vmtable());

result
}

pub(crate) fn defined_table_fill(
Expand Down Expand Up @@ -818,10 +864,6 @@ pub struct InstanceHandle {
}

impl InstanceHandle {
pub(crate) unsafe fn new(instance: *mut Instance) -> Self {
Self { instance }
}

/// Create a new `InstanceHandle` pointing at the instance
/// pointed to by the given `VMContext` pointer.
///
Expand Down
30 changes: 21 additions & 9 deletions crates/runtime/src/instance/allocator.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::externref::{ModuleInfoLookup, VMExternRefActivationsTable, EMPTY_MODULE_LOOKUP};
use crate::imports::Imports;
use crate::instance::{Instance, InstanceHandle, RuntimeMemoryCreator};
use crate::instance::{Instance, InstanceHandle, ResourceLimiter, RuntimeMemoryCreator};
use crate::memory::{DefaultMemoryCreator, Memory};
use crate::table::{Table, TableElement};
use crate::traphandlers::Trap;
Expand All @@ -15,6 +15,7 @@ use std::any::Any;
use std::cell::RefCell;
use std::convert::TryFrom;
use std::ptr::{self, NonNull};
use std::rc::Rc;
use std::slice;
use std::sync::Arc;
use thiserror::Error;
Expand Down Expand Up @@ -59,6 +60,9 @@ pub struct InstanceAllocationRequest<'a> {

/// The pointer to the module info lookup to use for the instance.
pub module_info_lookup: Option<*const dyn ModuleInfoLookup>,

/// The resource limiter to use for the instance.
pub limiter: Option<&'a Rc<dyn ResourceLimiter>>,
}

/// An link error while instantiating a module.
Expand Down Expand Up @@ -590,19 +594,23 @@ impl OnDemandInstanceAllocator {
}
}

fn create_tables(module: &Module) -> PrimaryMap<DefinedTableIndex, Table> {
fn create_tables(
module: &Module,
limiter: Option<&Rc<dyn ResourceLimiter>>,
) -> Result<PrimaryMap<DefinedTableIndex, Table>, InstantiationError> {
let num_imports = module.num_imported_tables;
let mut tables: PrimaryMap<DefinedTableIndex, _> =
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));
tables.push(Table::new_dynamic(table, limiter).map_err(InstantiationError::Resource)?);
}
tables
Ok(tables)
}

fn create_memories(
&self,
module: &Module,
limiter: Option<&Rc<dyn ResourceLimiter>>,
) -> Result<PrimaryMap<DefinedMemoryIndex, Memory>, InstantiationError> {
let creator = self
.mem_creator
Expand All @@ -612,8 +620,10 @@ impl OnDemandInstanceAllocator {
let mut memories: PrimaryMap<DefinedMemoryIndex, _> =
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).map_err(InstantiationError::Resource)?);
memories.push(
Memory::new_dynamic(plan, creator, limiter)
.map_err(InstantiationError::Resource)?,
);
}
Ok(memories)
}
Expand All @@ -633,8 +643,8 @@ unsafe impl InstanceAllocator for OnDemandInstanceAllocator {
&self,
mut req: InstanceAllocationRequest,
) -> Result<InstanceHandle, InstantiationError> {
let memories = self.create_memories(&req.module)?;
let tables = Self::create_tables(&req.module);
let memories = self.create_memories(&req.module, req.limiter)?;
let tables = Self::create_tables(&req.module, req.limiter)?;

let host_state = std::mem::replace(&mut req.host_state, Box::new(()));

Expand All @@ -657,7 +667,9 @@ unsafe impl InstanceAllocator for OnDemandInstanceAllocator {
alloc::handle_alloc_error(layout);
}
ptr::write(instance_ptr, instance);
InstanceHandle::new(instance_ptr)
InstanceHandle {
instance: instance_ptr,
}
};

initialize_vmcontext(handle.instance(), req);
Expand Down
Loading