Add a limits and trap-on-OOM options to the CLI (#6149)

* Add a limits and trap-on-OOM options to the CLI

This commit adds new options to the `wasmtime` CLI to control the
`Store::limiter` behavior at runtime. This enables artificially
restriction the memory usage of the wasm instance, for example.
Additionally a new option is added to `StoreLimits` to force a trap on
growth failure. This is intended to help quickly debug modules with
backtraces if OOM is happening, or even diagnosing if OOM is happening
in the first place.

* Fix compile of fuzzing oracle
This commit is contained in:
Alex Crichton
2023-04-05 12:26:36 -05:00
committed by GitHub
parent 967543eb43
commit 52e90532e0
8 changed files with 310 additions and 83 deletions

View File

@@ -111,16 +111,16 @@ async fn test_limits_async() -> Result<()> {
_current: usize,
desired: usize,
_maximum: Option<usize>,
) -> bool {
desired <= self.memory_size
) -> Result<bool> {
Ok(desired <= self.memory_size)
}
async fn table_growing(
&mut self,
_current: u32,
desired: u32,
_maximum: Option<u32>,
) -> bool {
desired <= self.table_elements
) -> Result<bool> {
Ok(desired <= self.table_elements)
}
}
@@ -394,7 +394,12 @@ struct MemoryContext {
}
impl ResourceLimiter for MemoryContext {
fn memory_growing(&mut self, current: usize, desired: usize, maximum: Option<usize>) -> bool {
fn memory_growing(
&mut self,
current: usize,
desired: usize,
maximum: Option<usize>,
) -> Result<bool> {
// Check if the desired exceeds a maximum (either from Wasm or from the host)
assert!(desired < maximum.unwrap_or(usize::MAX));
@@ -403,14 +408,19 @@ impl ResourceLimiter for MemoryContext {
if desired + self.host_memory_used > self.memory_limit {
self.limit_exceeded = true;
return false;
return Ok(false);
}
self.wasm_memory_used = desired;
true
Ok(true)
}
fn table_growing(&mut self, _current: u32, _desired: u32, _maximum: Option<u32>) -> bool {
true
fn table_growing(
&mut self,
_current: u32,
_desired: u32,
_maximum: Option<u32>,
) -> Result<bool> {
Ok(true)
}
}
@@ -501,7 +511,7 @@ impl ResourceLimiterAsync for MemoryContext {
current: usize,
desired: usize,
maximum: Option<usize>,
) -> bool {
) -> Result<bool> {
// Show we can await in this async context:
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
// Check if the desired exceeds a maximum (either from Wasm or from the host)
@@ -512,14 +522,19 @@ impl ResourceLimiterAsync for MemoryContext {
if desired + self.host_memory_used > self.memory_limit {
self.limit_exceeded = true;
return false;
return Ok(false);
}
self.wasm_memory_used = desired;
true
Ok(true)
}
async fn table_growing(&mut self, _current: u32, _desired: u32, _maximum: Option<u32>) -> bool {
true
async fn table_growing(
&mut self,
_current: u32,
_desired: u32,
_maximum: Option<u32>,
) -> Result<bool> {
Ok(true)
}
fn table_grow_failed(&mut self, _e: &anyhow::Error) {}
}
@@ -619,20 +634,20 @@ impl ResourceLimiter for TableContext {
_current: usize,
_desired: usize,
_maximum: Option<usize>,
) -> bool {
true
) -> Result<bool> {
Ok(true)
}
fn table_growing(&mut self, current: u32, desired: u32, maximum: Option<u32>) -> bool {
fn table_growing(&mut self, current: u32, desired: u32, maximum: Option<u32>) -> Result<bool> {
// Check if the desired exceeds a maximum (either from Wasm or from the host)
assert!(desired < maximum.unwrap_or(u32::MAX));
assert_eq!(current, self.elements_used);
if desired > self.element_limit {
Ok(if desired > self.element_limit {
self.limit_exceeded = true;
return false;
false
} else {
self.elements_used = desired;
true
}
})
}
}
@@ -693,18 +708,23 @@ struct FailureDetector {
}
impl ResourceLimiter for FailureDetector {
fn memory_growing(&mut self, current: usize, desired: usize, _maximum: Option<usize>) -> bool {
fn memory_growing(
&mut self,
current: usize,
desired: usize,
_maximum: Option<usize>,
) -> Result<bool> {
self.memory_current = current;
self.memory_desired = desired;
true
Ok(true)
}
fn memory_grow_failed(&mut self, err: &anyhow::Error) {
self.memory_error = Some(err.to_string());
}
fn table_growing(&mut self, current: u32, desired: u32, _maximum: Option<u32>) -> bool {
fn table_growing(&mut self, current: u32, desired: u32, _maximum: Option<u32>) -> Result<bool> {
self.table_current = current;
self.table_desired = desired;
true
Ok(true)
}
fn table_grow_failed(&mut self, err: &anyhow::Error) {
self.table_error = Some(err.to_string());
@@ -793,21 +813,26 @@ impl ResourceLimiterAsync for FailureDetector {
current: usize,
desired: usize,
_maximum: Option<usize>,
) -> bool {
) -> Result<bool> {
// Show we can await in this async context:
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
self.memory_current = current;
self.memory_desired = desired;
true
Ok(true)
}
fn memory_grow_failed(&mut self, err: &anyhow::Error) {
self.memory_error = Some(err.to_string());
}
async fn table_growing(&mut self, current: u32, desired: u32, _maximum: Option<u32>) -> bool {
async fn table_growing(
&mut self,
current: u32,
desired: u32,
_maximum: Option<u32>,
) -> Result<bool> {
self.table_current = current;
self.table_desired = desired;
true
Ok(true)
}
fn table_grow_failed(&mut self, err: &anyhow::Error) {
self.table_error = Some(err.to_string());
@@ -903,10 +928,15 @@ impl ResourceLimiter for Panic {
_current: usize,
_desired: usize,
_maximum: Option<usize>,
) -> bool {
) -> Result<bool> {
panic!("resource limiter memory growing");
}
fn table_growing(&mut self, _current: u32, _desired: u32, _maximum: Option<u32>) -> bool {
fn table_growing(
&mut self,
_current: u32,
_desired: u32,
_maximum: Option<u32>,
) -> Result<bool> {
panic!("resource limiter table growing");
}
}
@@ -917,10 +947,15 @@ impl ResourceLimiterAsync for Panic {
_current: usize,
_desired: usize,
_maximum: Option<usize>,
) -> bool {
) -> Result<bool> {
panic!("async resource limiter memory growing");
}
async fn table_growing(&mut self, _current: u32, _desired: u32, _maximum: Option<u32>) -> bool {
async fn table_growing(
&mut self,
_current: u32,
_desired: u32,
_maximum: Option<u32>,
) -> Result<bool> {
panic!("async resource limiter table growing");
}
}
@@ -1059,3 +1094,61 @@ async fn panic_in_async_table_limiter() {
.await
.unwrap();
}
#[test]
fn growth_trap() -> Result<()> {
let engine = Engine::default();
let module = Module::new(
&engine,
r#"(module
(memory $m (export "m") 0)
(table (export "t") 0 anyfunc)
(func (export "grow") (param i32) (result i32)
(memory.grow $m (local.get 0)))
)"#,
)?;
let mut store = Store::new(
&engine,
StoreLimitsBuilder::new()
.memory_size(WASM_PAGE_SIZE)
.table_elements(1)
.trap_on_grow_failure(true)
.build(),
);
store.limiter(|s| s as &mut dyn ResourceLimiter);
let instance = Instance::new(&mut store, &module, &[])?;
// Test instance exports and host objects hitting the limit
for memory in [
instance.get_memory(&mut store, "m").unwrap(),
Memory::new(&mut store, MemoryType::new(0, None))?,
] {
memory.grow(&mut store, 1)?;
assert!(memory.grow(&mut store, 1).is_err());
}
// Test instance exports and host objects hitting the limit
for table in [
instance.get_table(&mut store, "t").unwrap(),
Table::new(
&mut store,
TableType::new(ValType::FuncRef, 0, None),
Val::FuncRef(None),
)?,
] {
table.grow(&mut store, 1, Val::FuncRef(None))?;
assert!(table.grow(&mut store, 1, Val::FuncRef(None)).is_err());
}
let mut store = Store::new(&engine, store.data().clone());
store.limiter(|s| s as &mut dyn ResourceLimiter);
let instance = Instance::new(&mut store, &module, &[])?;
let grow = instance.get_func(&mut store, "grow").unwrap();
let grow = grow.typed::<i32, i32>(&store).unwrap();
grow.call(&mut store, 1)?;
assert!(grow.call(&mut store, 1).is_err());
Ok(())
}

View File

@@ -306,11 +306,16 @@ fn massive_64_bit_still_limited() -> Result<()> {
_current: usize,
_request: usize,
_max: Option<usize>,
) -> bool {
) -> Result<bool> {
self.hit = true;
true
Ok(true)
}
fn table_growing(&mut self, _current: u32, _request: u32, _max: Option<u32>) -> bool {
fn table_growing(
&mut self,
_current: u32,
_request: u32,
_max: Option<u32>,
) -> Result<bool> {
unreachable!()
}
}

View File

@@ -528,12 +528,12 @@ fn drop_externref_global_during_module_init() -> Result<()> {
struct Limiter;
impl ResourceLimiter for Limiter {
fn memory_growing(&mut self, _: usize, _: usize, _: Option<usize>) -> bool {
false
fn memory_growing(&mut self, _: usize, _: usize, _: Option<usize>) -> Result<bool> {
Ok(false)
}
fn table_growing(&mut self, _: u32, _: u32, _: Option<u32>) -> bool {
false
fn table_growing(&mut self, _: u32, _: u32, _: Option<u32>) -> Result<bool> {
Ok(false)
}
}

View File

@@ -13,16 +13,26 @@ struct MemoryGrowFailureDetector {
}
impl ResourceLimiter for MemoryGrowFailureDetector {
fn memory_growing(&mut self, current: usize, desired: usize, _maximum: Option<usize>) -> bool {
fn memory_growing(
&mut self,
current: usize,
desired: usize,
_maximum: Option<usize>,
) -> Result<bool> {
self.current = current;
self.desired = desired;
true
Ok(true)
}
fn memory_grow_failed(&mut self, err: &anyhow::Error) {
self.error = Some(err.to_string());
}
fn table_growing(&mut self, _current: u32, _desired: u32, _maximum: Option<u32>) -> bool {
true
fn table_growing(
&mut self,
_current: u32,
_desired: u32,
_maximum: Option<u32>,
) -> Result<bool> {
Ok(true)
}
}