Add resource limiting to the Wasmtime API. (#2736)
* Add resource limiting to the Wasmtime API. This commit adds a `ResourceLimiter` trait to the Wasmtime API. When used in conjunction with `Store::new_with_limiter`, this can be used to monitor and prevent WebAssembly code from growing linear memories and tables. This is particularly useful when hosts need to take into account host resource usage to determine if WebAssembly code can consume more resources. A simple `StaticResourceLimiter` is also included with these changes that will simply limit the size of linear memories or tables for all instances created in the store based on static values. * Code review feedback. * Implemented `StoreLimits` and `StoreLimitsBuilder`. * Moved `max_instances`, `max_memories`, `max_tables` out of `Config` and into `StoreLimits`. * Moved storage of the limiter in the runtime into `Memory` and `Table`. * Made `InstanceAllocationRequest` use a reference to the limiter. * Updated docs. * Made `ResourceLimiterProxy` generic to remove a level of indirection. * Fixed the limiter not being used for `wasmtime::Memory` and `wasmtime::Table`. * Code review feedback and bug fix. * `Memory::new` now returns `Result<Self>` so that an error can be returned if the initial requested memory exceeds any limits placed on the store. * Changed an `Arc` to `Rc` as the `Arc` wasn't necessary. * Removed `Store` from the `ResourceLimiter` callbacks. Custom resource limiter implementations are free to capture any context they want, so no need to unnecessarily store a weak reference to `Store` from the proxy type. * Fixed a bug in the pooling instance allocator where an instance would be leaked from the pool. Previously, this would only have happened if the OS was unable to make the necessary linear memory available for the instance. With these changes, however, the instance might not be created due to limits placed on the store. We now properly deallocate the instance on error. * Added more tests, including one that covers the fix mentioned above. * Code review feedback. * Add another memory to `test_pooling_allocator_initial_limits_exceeded` to ensure a partially created instance is successfully deallocated. * Update some doc comments for better documentation of `Store` and `ResourceLimiter`.
This commit is contained in:
@@ -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>>;
|
||||
@@ -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());
|
||||
@@ -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(
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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(()));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
use super::{
|
||||
initialize_instance, initialize_vmcontext, InstanceAllocationRequest, InstanceAllocator,
|
||||
InstanceHandle, InstantiationError,
|
||||
InstanceHandle, InstantiationError, ResourceLimiter,
|
||||
};
|
||||
use crate::{instance::Instance, Memory, Mmap, Table, VMContext};
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
@@ -18,6 +18,7 @@ use std::cell::RefCell;
|
||||
use std::cmp::min;
|
||||
use std::convert::TryFrom;
|
||||
use std::mem;
|
||||
use std::rc::Rc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use wasmtime_environ::{
|
||||
entity::{EntitySet, PrimaryMap},
|
||||
@@ -376,10 +377,45 @@ impl InstancePool {
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn setup_instance(
|
||||
&self,
|
||||
index: usize,
|
||||
mut req: InstanceAllocationRequest,
|
||||
) -> Result<InstanceHandle, InstantiationError> {
|
||||
let instance = self.instance(index);
|
||||
|
||||
instance.module = req.module.clone();
|
||||
instance.offsets = VMOffsets::new(
|
||||
std::mem::size_of::<*const u8>() as u8,
|
||||
instance.module.as_ref(),
|
||||
);
|
||||
instance.host_state = std::mem::replace(&mut req.host_state, Box::new(()));
|
||||
|
||||
Self::set_instance_memories(
|
||||
instance,
|
||||
self.memories.get(index),
|
||||
self.memories.max_wasm_pages,
|
||||
req.limiter,
|
||||
)?;
|
||||
|
||||
Self::set_instance_tables(
|
||||
instance,
|
||||
self.tables.get(index),
|
||||
self.tables.max_elements,
|
||||
req.limiter,
|
||||
)?;
|
||||
|
||||
initialize_vmcontext(instance, req);
|
||||
|
||||
Ok(InstanceHandle {
|
||||
instance: instance as _,
|
||||
})
|
||||
}
|
||||
|
||||
fn allocate(
|
||||
&self,
|
||||
strategy: PoolingAllocationStrategy,
|
||||
mut req: InstanceAllocationRequest,
|
||||
req: InstanceAllocationRequest,
|
||||
) -> Result<InstanceHandle, InstantiationError> {
|
||||
let index = {
|
||||
let mut free_list = self.free_list.lock().unwrap();
|
||||
@@ -390,28 +426,15 @@ impl InstancePool {
|
||||
free_list.swap_remove(free_index)
|
||||
};
|
||||
|
||||
let host_state = std::mem::replace(&mut req.host_state, Box::new(()));
|
||||
|
||||
unsafe {
|
||||
let instance = self.instance(index);
|
||||
|
||||
instance.module = req.module.clone();
|
||||
instance.offsets = VMOffsets::new(
|
||||
std::mem::size_of::<*const u8>() as u8,
|
||||
instance.module.as_ref(),
|
||||
);
|
||||
instance.host_state = host_state;
|
||||
|
||||
Self::set_instance_memories(
|
||||
instance,
|
||||
self.memories.get(index),
|
||||
self.memories.max_wasm_pages,
|
||||
)?;
|
||||
Self::set_instance_tables(instance, self.tables.get(index), self.tables.max_elements)?;
|
||||
|
||||
initialize_vmcontext(instance, req);
|
||||
|
||||
Ok(InstanceHandle::new(instance as _))
|
||||
self.setup_instance(index, req).or_else(|e| {
|
||||
// Deallocate the allocated instance on error
|
||||
let instance = self.instance(index);
|
||||
self.deallocate(&InstanceHandle {
|
||||
instance: instance as _,
|
||||
});
|
||||
Err(e)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,6 +496,7 @@ impl InstancePool {
|
||||
instance: &mut Instance,
|
||||
mut memories: impl Iterator<Item = *mut u8>,
|
||||
max_pages: u32,
|
||||
limiter: Option<&Rc<dyn ResourceLimiter>>,
|
||||
) -> Result<(), InstantiationError> {
|
||||
let module = instance.module.as_ref();
|
||||
|
||||
@@ -487,6 +511,7 @@ impl InstancePool {
|
||||
memories.next().unwrap(),
|
||||
max_pages,
|
||||
commit_memory_pages,
|
||||
limiter,
|
||||
)
|
||||
.map_err(InstantiationError::Resource)?,
|
||||
);
|
||||
@@ -503,6 +528,7 @@ impl InstancePool {
|
||||
instance: &mut Instance,
|
||||
mut tables: impl Iterator<Item = *mut u8>,
|
||||
max_elements: u32,
|
||||
limiter: Option<&Rc<dyn ResourceLimiter>>,
|
||||
) -> Result<(), InstantiationError> {
|
||||
let module = instance.module.as_ref();
|
||||
|
||||
@@ -514,9 +540,10 @@ impl InstancePool {
|
||||
commit_table_pages(base, max_elements as usize * mem::size_of::<*mut u8>())
|
||||
.map_err(InstantiationError::Resource)?;
|
||||
|
||||
instance
|
||||
.tables
|
||||
.push(Table::new_static(plan, base as _, max_elements));
|
||||
instance.tables.push(
|
||||
Table::new_static(plan, base as _, max_elements, limiter)
|
||||
.map_err(InstantiationError::Resource)?,
|
||||
);
|
||||
}
|
||||
|
||||
let mut dropped_elements = instance.dropped_elements.borrow_mut();
|
||||
@@ -1371,6 +1398,7 @@ mod test {
|
||||
interrupts: std::ptr::null(),
|
||||
externref_activations_table: std::ptr::null_mut(),
|
||||
module_info_lookup: None,
|
||||
limiter: None,
|
||||
},
|
||||
)
|
||||
.expect("allocation should succeed"),
|
||||
@@ -1395,6 +1423,7 @@ mod test {
|
||||
interrupts: std::ptr::null(),
|
||||
externref_activations_table: std::ptr::null_mut(),
|
||||
module_info_lookup: None,
|
||||
limiter: None,
|
||||
},
|
||||
) {
|
||||
Err(InstantiationError::Limit(3)) => {}
|
||||
|
||||
@@ -524,6 +524,7 @@ mod test {
|
||||
interrupts: ptr::null(),
|
||||
externref_activations_table: ptr::null_mut(),
|
||||
module_info_lookup: None,
|
||||
limiter: None,
|
||||
},
|
||||
)
|
||||
.expect("instance should allocate"),
|
||||
|
||||
@@ -40,7 +40,7 @@ pub use crate::imports::Imports;
|
||||
pub use crate::instance::{
|
||||
InstanceAllocationRequest, InstanceAllocator, InstanceHandle, InstanceLimits,
|
||||
InstantiationError, LinkError, ModuleLimits, OnDemandInstanceAllocator,
|
||||
PoolingAllocationStrategy, PoolingInstanceAllocator, RuntimeInstance,
|
||||
PoolingAllocationStrategy, PoolingInstanceAllocator, ResourceLimiter, RuntimeInstance,
|
||||
};
|
||||
pub use crate::jit_int::GdbJitImageRegistration;
|
||||
pub use crate::memory::{Memory, RuntimeLinearMemory, RuntimeMemoryCreator};
|
||||
|
||||
@@ -4,12 +4,14 @@
|
||||
|
||||
use crate::mmap::Mmap;
|
||||
use crate::vmcontext::VMMemoryDefinition;
|
||||
use anyhow::Result;
|
||||
use crate::ResourceLimiter;
|
||||
use anyhow::{bail, Result};
|
||||
use more_asserts::{assert_ge, assert_le};
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::cmp::min;
|
||||
use std::convert::TryFrom;
|
||||
use std::ptr;
|
||||
use std::rc::Rc;
|
||||
use wasmtime_environ::{MemoryPlan, MemoryStyle, WASM_MAX_PAGES, WASM_PAGE_SIZE};
|
||||
|
||||
/// A memory allocator
|
||||
@@ -33,6 +35,10 @@ pub trait RuntimeLinearMemory {
|
||||
/// Returns the number of allocated wasm pages.
|
||||
fn size(&self) -> u32;
|
||||
|
||||
/// Returns the maximum number of pages the memory can grow to.
|
||||
/// Returns `None` if the memory is unbounded.
|
||||
fn maximum(&self) -> Option<u32>;
|
||||
|
||||
/// Grow memory by the specified amount of wasm pages.
|
||||
///
|
||||
/// Returns `None` if memory can't be grown by the specified amount
|
||||
@@ -105,6 +111,12 @@ impl RuntimeLinearMemory for MmapMemory {
|
||||
self.mmap.borrow().size
|
||||
}
|
||||
|
||||
/// Returns the maximum number of pages the memory can grow to.
|
||||
/// Returns `None` if the memory is unbounded.
|
||||
fn maximum(&self) -> Option<u32> {
|
||||
self.maximum
|
||||
}
|
||||
|
||||
/// Grow memory by the specified amount of wasm pages.
|
||||
///
|
||||
/// Returns `None` if memory can't be grown by the specified amount
|
||||
@@ -189,12 +201,23 @@ enum MemoryStorage {
|
||||
}
|
||||
|
||||
/// Represents an instantiation of a WebAssembly memory.
|
||||
pub struct Memory(MemoryStorage);
|
||||
pub struct Memory {
|
||||
storage: MemoryStorage,
|
||||
limiter: Option<Rc<dyn ResourceLimiter>>,
|
||||
}
|
||||
|
||||
impl Memory {
|
||||
/// Create a new dynamic (movable) memory instance for the specified plan.
|
||||
pub fn new_dynamic(plan: &MemoryPlan, creator: &dyn RuntimeMemoryCreator) -> Result<Self> {
|
||||
Ok(Self(MemoryStorage::Dynamic(creator.new_memory(plan)?)))
|
||||
pub fn new_dynamic(
|
||||
plan: &MemoryPlan,
|
||||
creator: &dyn RuntimeMemoryCreator,
|
||||
limiter: Option<&Rc<dyn ResourceLimiter>>,
|
||||
) -> Result<Self> {
|
||||
Self::new(
|
||||
plan,
|
||||
MemoryStorage::Dynamic(creator.new_memory(plan)?),
|
||||
limiter,
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a new static (immovable) memory instance for the specified plan.
|
||||
@@ -203,32 +226,78 @@ impl Memory {
|
||||
base: *mut u8,
|
||||
maximum: u32,
|
||||
make_accessible: fn(*mut u8, usize) -> Result<()>,
|
||||
limiter: Option<&Rc<dyn ResourceLimiter>>,
|
||||
) -> Result<Self> {
|
||||
if plan.memory.minimum > 0 {
|
||||
make_accessible(base, plan.memory.minimum as usize * WASM_PAGE_SIZE as usize)?;
|
||||
}
|
||||
|
||||
Ok(Self(MemoryStorage::Static {
|
||||
let storage = MemoryStorage::Static {
|
||||
base,
|
||||
size: Cell::new(plan.memory.minimum),
|
||||
maximum: min(plan.memory.maximum.unwrap_or(maximum), maximum),
|
||||
make_accessible,
|
||||
#[cfg(all(feature = "uffd", target_os = "linux"))]
|
||||
guard_page_faults: RefCell::new(Vec::new()),
|
||||
}))
|
||||
};
|
||||
|
||||
Self::new(plan, storage, limiter)
|
||||
}
|
||||
|
||||
fn new(
|
||||
plan: &MemoryPlan,
|
||||
storage: MemoryStorage,
|
||||
limiter: Option<&Rc<dyn ResourceLimiter>>,
|
||||
) -> Result<Self> {
|
||||
if let Some(limiter) = limiter {
|
||||
if !limiter.memory_growing(0, plan.memory.minimum, plan.memory.maximum) {
|
||||
bail!(
|
||||
"memory minimum size of {} pages exceeds memory limits",
|
||||
plan.memory.minimum
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let MemoryStorage::Static {
|
||||
base,
|
||||
make_accessible,
|
||||
..
|
||||
} = &storage
|
||||
{
|
||||
if plan.memory.minimum > 0 {
|
||||
make_accessible(
|
||||
*base,
|
||||
plan.memory.minimum as usize * WASM_PAGE_SIZE as usize,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
storage,
|
||||
limiter: limiter.cloned(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the number of allocated wasm pages.
|
||||
pub fn size(&self) -> u32 {
|
||||
match &self.0 {
|
||||
match &self.storage {
|
||||
MemoryStorage::Static { size, .. } => size.get(),
|
||||
MemoryStorage::Dynamic(mem) => mem.size(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the maximum number of pages the memory can grow to at runtime.
|
||||
///
|
||||
/// Returns `None` if the memory is unbounded.
|
||||
///
|
||||
/// The runtime maximum may not be equal to the maximum from the linear memory's
|
||||
/// Wasm type when it is being constrained by an instance allocator.
|
||||
pub fn maximum(&self) -> Option<u32> {
|
||||
match &self.storage {
|
||||
MemoryStorage::Static { maximum, .. } => Some(*maximum),
|
||||
MemoryStorage::Dynamic(mem) => mem.maximum(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether or not the underlying storage of the memory is "static".
|
||||
pub(crate) fn is_static(&self) -> bool {
|
||||
if let MemoryStorage::Static { .. } = &self.0 {
|
||||
if let MemoryStorage::Static { .. } = &self.storage {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
@@ -239,8 +308,30 @@ impl Memory {
|
||||
///
|
||||
/// Returns `None` if memory can't be grown by the specified amount
|
||||
/// of wasm pages.
|
||||
pub fn grow(&self, delta: u32) -> Option<u32> {
|
||||
match &self.0 {
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// Resizing the memory can reallocate the memory buffer for dynamic memories.
|
||||
/// An instance's `VMContext` may have pointers to the memory's base and will
|
||||
/// need to be fixed up after growing the memory.
|
||||
///
|
||||
/// Generally, prefer using `InstanceHandle::memory_grow`, which encapsulates
|
||||
/// this unsafety.
|
||||
pub unsafe fn grow(&self, delta: u32) -> Option<u32> {
|
||||
let old_size = self.size();
|
||||
if delta == 0 {
|
||||
return Some(old_size);
|
||||
}
|
||||
|
||||
let new_size = old_size.checked_add(delta)?;
|
||||
|
||||
if let Some(limiter) = &self.limiter {
|
||||
if !limiter.memory_growing(old_size, new_size, self.maximum()) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
match &self.storage {
|
||||
MemoryStorage::Static {
|
||||
base,
|
||||
size,
|
||||
@@ -252,13 +343,6 @@ impl Memory {
|
||||
#[cfg(all(feature = "uffd", target_os = "linux"))]
|
||||
self.reset_guard_pages().ok()?;
|
||||
|
||||
let old_size = size.get();
|
||||
if delta == 0 {
|
||||
return Some(old_size);
|
||||
}
|
||||
|
||||
let new_size = old_size.checked_add(delta)?;
|
||||
|
||||
if new_size > *maximum || new_size >= WASM_MAX_PAGES {
|
||||
return None;
|
||||
}
|
||||
@@ -266,7 +350,7 @@ impl Memory {
|
||||
let start = usize::try_from(old_size).unwrap() * WASM_PAGE_SIZE as usize;
|
||||
let len = usize::try_from(delta).unwrap() * WASM_PAGE_SIZE as usize;
|
||||
|
||||
make_accessible(unsafe { base.add(start) }, len).ok()?;
|
||||
make_accessible(base.add(start), len).ok()?;
|
||||
|
||||
size.set(new_size);
|
||||
|
||||
@@ -278,7 +362,7 @@ impl Memory {
|
||||
|
||||
/// Return a `VMMemoryDefinition` for exposing the memory to compiled wasm code.
|
||||
pub fn vmmemory(&self) -> VMMemoryDefinition {
|
||||
match &self.0 {
|
||||
match &self.storage {
|
||||
MemoryStorage::Static { base, size, .. } => VMMemoryDefinition {
|
||||
base: *base,
|
||||
current_length: size.get() as usize * WASM_PAGE_SIZE as usize,
|
||||
@@ -299,7 +383,7 @@ impl Memory {
|
||||
size: usize,
|
||||
reset: fn(*mut u8, usize) -> Result<()>,
|
||||
) {
|
||||
match &self.0 {
|
||||
match &self.storage {
|
||||
MemoryStorage::Static {
|
||||
guard_page_faults, ..
|
||||
} => {
|
||||
@@ -320,7 +404,7 @@ impl Memory {
|
||||
/// This function will panic if called on a dynamic memory.
|
||||
#[cfg(all(feature = "uffd", target_os = "linux"))]
|
||||
pub(crate) fn reset_guard_pages(&self) -> Result<()> {
|
||||
match &self.0 {
|
||||
match &self.storage {
|
||||
MemoryStorage::Static {
|
||||
guard_page_faults, ..
|
||||
} => {
|
||||
@@ -345,13 +429,16 @@ impl Default for Memory {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
Self(MemoryStorage::Static {
|
||||
base: ptr::null_mut(),
|
||||
size: Cell::new(0),
|
||||
maximum: 0,
|
||||
make_accessible,
|
||||
#[cfg(all(feature = "uffd", target_os = "linux"))]
|
||||
guard_page_faults: RefCell::new(Vec::new()),
|
||||
})
|
||||
Self {
|
||||
storage: MemoryStorage::Static {
|
||||
base: ptr::null_mut(),
|
||||
size: Cell::new(0),
|
||||
maximum: 0,
|
||||
make_accessible,
|
||||
#[cfg(all(feature = "uffd", target_os = "linux"))]
|
||||
guard_page_faults: RefCell::new(Vec::new()),
|
||||
},
|
||||
limiter: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,19 +3,21 @@
|
||||
//! `Table` is to WebAssembly tables what `LinearMemory` is to WebAssembly linear memories.
|
||||
|
||||
use crate::vmcontext::{VMCallerCheckedAnyfunc, VMTableDefinition};
|
||||
use crate::{Trap, VMExternRef};
|
||||
use crate::{ResourceLimiter, Trap, VMExternRef};
|
||||
use anyhow::{bail, Result};
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::cmp::min;
|
||||
use std::convert::TryInto;
|
||||
use std::ops::Range;
|
||||
use std::ptr;
|
||||
use std::rc::Rc;
|
||||
use wasmtime_environ::wasm::TableElementType;
|
||||
use wasmtime_environ::{ir, TablePlan};
|
||||
|
||||
/// An element going into or coming out of a table.
|
||||
///
|
||||
/// Table elements are stored as pointers and are default-initialized with `ptr::null_mut`.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone)]
|
||||
pub enum TableElement {
|
||||
/// A `funcref`.
|
||||
FuncRef(*mut VMCallerCheckedAnyfunc),
|
||||
@@ -92,7 +94,6 @@ impl From<VMExternRef> for TableElement {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum TableStorage {
|
||||
Static {
|
||||
data: *mut *mut u8,
|
||||
@@ -108,38 +109,74 @@ enum TableStorage {
|
||||
}
|
||||
|
||||
/// Represents an instance's table.
|
||||
#[derive(Debug)]
|
||||
pub struct Table(TableStorage);
|
||||
pub struct Table {
|
||||
storage: TableStorage,
|
||||
limiter: Option<Rc<dyn ResourceLimiter>>,
|
||||
}
|
||||
|
||||
impl Table {
|
||||
/// Create a new dynamic (movable) table instance for the specified table plan.
|
||||
pub fn new_dynamic(plan: &TablePlan) -> Self {
|
||||
pub fn new_dynamic(
|
||||
plan: &TablePlan,
|
||||
limiter: Option<&Rc<dyn ResourceLimiter>>,
|
||||
) -> Result<Self> {
|
||||
let elements = RefCell::new(vec![ptr::null_mut(); plan.table.minimum as usize]);
|
||||
let ty = plan.table.ty.clone();
|
||||
let maximum = plan.table.maximum;
|
||||
Self(TableStorage::Dynamic {
|
||||
|
||||
let storage = TableStorage::Dynamic {
|
||||
elements,
|
||||
ty,
|
||||
maximum,
|
||||
})
|
||||
};
|
||||
|
||||
Self::new(plan, storage, limiter)
|
||||
}
|
||||
|
||||
/// Create a new static (immovable) table instance for the specified table plan.
|
||||
pub fn new_static(plan: &TablePlan, data: *mut *mut u8, maximum: u32) -> Self {
|
||||
pub fn new_static(
|
||||
plan: &TablePlan,
|
||||
data: *mut *mut u8,
|
||||
maximum: u32,
|
||||
limiter: Option<&Rc<dyn ResourceLimiter>>,
|
||||
) -> Result<Self> {
|
||||
let size = Cell::new(plan.table.minimum);
|
||||
let ty = plan.table.ty.clone();
|
||||
let maximum = min(plan.table.maximum.unwrap_or(maximum), maximum);
|
||||
Self(TableStorage::Static {
|
||||
|
||||
let storage = TableStorage::Static {
|
||||
data,
|
||||
size,
|
||||
ty,
|
||||
maximum,
|
||||
};
|
||||
|
||||
Self::new(plan, storage, limiter)
|
||||
}
|
||||
|
||||
fn new(
|
||||
plan: &TablePlan,
|
||||
storage: TableStorage,
|
||||
limiter: Option<&Rc<dyn ResourceLimiter>>,
|
||||
) -> Result<Self> {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
storage,
|
||||
limiter: limiter.cloned(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the type of the elements in this table.
|
||||
pub fn element_type(&self) -> TableElementType {
|
||||
match &self.0 {
|
||||
match &self.storage {
|
||||
TableStorage::Static { ty, .. } => *ty,
|
||||
TableStorage::Dynamic { ty, .. } => *ty,
|
||||
}
|
||||
@@ -147,7 +184,7 @@ impl Table {
|
||||
|
||||
/// Returns whether or not the underlying storage of the table is "static".
|
||||
pub(crate) fn is_static(&self) -> bool {
|
||||
if let TableStorage::Static { .. } = &self.0 {
|
||||
if let TableStorage::Static { .. } = &self.storage {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
@@ -156,15 +193,20 @@ impl Table {
|
||||
|
||||
/// Returns the number of allocated elements.
|
||||
pub fn size(&self) -> u32 {
|
||||
match &self.0 {
|
||||
match &self.storage {
|
||||
TableStorage::Static { size, .. } => size.get(),
|
||||
TableStorage::Dynamic { elements, .. } => elements.borrow().len().try_into().unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the maximum number of elements.
|
||||
/// Returns the maximum number of elements at runtime.
|
||||
///
|
||||
/// Returns `None` if the table is unbounded.
|
||||
///
|
||||
/// The runtime maximum may not be equal to the maximum from the table's Wasm type
|
||||
/// when it is being constrained by an instance allocator.
|
||||
pub fn maximum(&self) -> Option<u32> {
|
||||
match &self.0 {
|
||||
match &self.storage {
|
||||
TableStorage::Static { maximum, .. } => Some(*maximum),
|
||||
TableStorage::Dynamic { maximum, .. } => maximum.clone(),
|
||||
}
|
||||
@@ -218,8 +260,14 @@ impl Table {
|
||||
/// this unsafety.
|
||||
pub unsafe fn grow(&self, delta: u32, init_value: TableElement) -> Option<u32> {
|
||||
let old_size = self.size();
|
||||
|
||||
let new_size = old_size.checked_add(delta)?;
|
||||
|
||||
if let Some(limiter) = &self.limiter {
|
||||
if !limiter.table_growing(old_size, new_size, self.maximum()) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(max) = self.maximum() {
|
||||
if new_size > max {
|
||||
return None;
|
||||
@@ -229,7 +277,7 @@ impl Table {
|
||||
debug_assert!(self.type_matches(&init_value));
|
||||
|
||||
// First resize the storage and then fill with the init value
|
||||
match &self.0 {
|
||||
match &self.storage {
|
||||
TableStorage::Static { size, .. } => {
|
||||
size.set(new_size);
|
||||
}
|
||||
@@ -319,7 +367,7 @@ impl Table {
|
||||
|
||||
/// Return a `VMTableDefinition` for exposing the table to compiled wasm code.
|
||||
pub fn vmtable(&self) -> VMTableDefinition {
|
||||
match &self.0 {
|
||||
match &self.storage {
|
||||
TableStorage::Static { data, size, .. } => VMTableDefinition {
|
||||
base: *data as _,
|
||||
current_elements: size.get(),
|
||||
@@ -346,7 +394,7 @@ impl Table {
|
||||
where
|
||||
F: FnOnce(&[*mut u8]) -> R,
|
||||
{
|
||||
match &self.0 {
|
||||
match &self.storage {
|
||||
TableStorage::Static { data, size, .. } => unsafe {
|
||||
f(std::slice::from_raw_parts(*data, size.get() as usize))
|
||||
},
|
||||
@@ -361,7 +409,7 @@ impl Table {
|
||||
where
|
||||
F: FnOnce(&mut [*mut u8]) -> R,
|
||||
{
|
||||
match &self.0 {
|
||||
match &self.storage {
|
||||
TableStorage::Static { data, size, .. } => unsafe {
|
||||
f(std::slice::from_raw_parts_mut(*data, size.get() as usize))
|
||||
},
|
||||
@@ -463,11 +511,14 @@ impl Drop for Table {
|
||||
// The default table representation is an empty funcref table that cannot grow.
|
||||
impl Default for Table {
|
||||
fn default() -> Self {
|
||||
Self(TableStorage::Static {
|
||||
data: std::ptr::null_mut(),
|
||||
size: Cell::new(0),
|
||||
ty: TableElementType::Func,
|
||||
maximum: 0,
|
||||
})
|
||||
Self {
|
||||
storage: TableStorage::Static {
|
||||
data: std::ptr::null_mut(),
|
||||
size: Cell::new(0),
|
||||
ty: TableElementType::Func,
|
||||
maximum: 0,
|
||||
},
|
||||
limiter: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user