Files
wasmtime/tests/all/limits.rs
Alex Crichton 7a1b7cdf92 Implement RFC 11: Redesigning Wasmtime's APIs (#2897)
Implement Wasmtime's new API as designed by RFC 11. This is quite a large commit which has had lots of discussion externally, so for more information it's best to read the RFC thread and the PR thread.
2021-06-03 09:10:53 -05:00

392 lines
12 KiB
Rust

use anyhow::Result;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst};
use std::sync::Arc;
use wasmtime::*;
#[test]
fn test_limits() -> Result<()> {
let engine = Engine::default();
let module = Module::new(
&engine,
r#"(module (memory (export "m") 0) (table (export "t") 0 anyfunc))"#,
)?;
let mut store = Store::new(&engine, ());
store.limiter(
StoreLimitsBuilder::new()
.memory_pages(10)
.table_elements(5)
.build(),
);
let instance = Instance::new(&mut store, &module, &[])?;
// Test instance exports and host objects hitting the limit
for memory in std::array::IntoIter::new([
instance.get_memory(&mut store, "m").unwrap(),
Memory::new(&mut store, MemoryType::new(Limits::new(0, None)))?,
]) {
memory.grow(&mut store, 3)?;
memory.grow(&mut store, 5)?;
memory.grow(&mut store, 2)?;
assert_eq!(
memory
.grow(&mut store, 1)
.map_err(|e| e.to_string())
.unwrap_err(),
"failed to grow memory by `1`"
);
}
// Test instance exports and host objects hitting the limit
for table in std::array::IntoIter::new([
instance.get_table(&mut store, "t").unwrap(),
Table::new(
&mut store,
TableType::new(ValType::FuncRef, Limits::new(0, None)),
Val::FuncRef(None),
)?,
]) {
table.grow(&mut store, 2, Val::FuncRef(None))?;
table.grow(&mut store, 1, Val::FuncRef(None))?;
table.grow(&mut store, 2, Val::FuncRef(None))?;
assert_eq!(
table
.grow(&mut store, 1, Val::FuncRef(None))
.map_err(|e| e.to_string())
.unwrap_err(),
"failed to grow table by `1`"
);
}
Ok(())
}
#[test]
fn test_limits_memory_only() -> Result<()> {
let engine = Engine::default();
let module = Module::new(
&engine,
r#"(module (memory (export "m") 0) (table (export "t") 0 anyfunc))"#,
)?;
let mut store = Store::new(&engine, ());
store.limiter(StoreLimitsBuilder::new().memory_pages(10).build());
let instance = Instance::new(&mut store, &module, &[])?;
// Test instance exports and host objects hitting the limit
for memory in std::array::IntoIter::new([
instance.get_memory(&mut store, "m").unwrap(),
Memory::new(&mut store, MemoryType::new(Limits::new(0, None)))?,
]) {
memory.grow(&mut store, 3)?;
memory.grow(&mut store, 5)?;
memory.grow(&mut store, 2)?;
assert_eq!(
memory
.grow(&mut store, 1)
.map_err(|e| e.to_string())
.unwrap_err(),
"failed to grow memory by `1`"
);
}
// Test instance exports and host objects *not* hitting the limit
for table in std::array::IntoIter::new([
instance.get_table(&mut store, "t").unwrap(),
Table::new(
&mut store,
TableType::new(ValType::FuncRef, Limits::new(0, None)),
Val::FuncRef(None),
)?,
]) {
table.grow(&mut store, 2, Val::FuncRef(None))?;
table.grow(&mut store, 1, Val::FuncRef(None))?;
table.grow(&mut store, 2, Val::FuncRef(None))?;
table.grow(&mut store, 1, Val::FuncRef(None))?;
}
Ok(())
}
#[test]
fn test_initial_memory_limits_exceeded() -> Result<()> {
let engine = Engine::default();
let module = Module::new(&engine, r#"(module (memory (export "m") 11))"#)?;
let mut store = Store::new(&engine, ());
store.limiter(StoreLimitsBuilder::new().memory_pages(10).build());
match Instance::new(&mut store, &module, &[]) {
Ok(_) => unreachable!(),
Err(e) => assert_eq!(
e.to_string(),
"Insufficient resources: memory minimum size of 11 pages exceeds memory limits"
),
}
match Memory::new(&mut store, MemoryType::new(Limits::new(25, None))) {
Ok(_) => unreachable!(),
Err(e) => assert_eq!(
e.to_string(),
"Insufficient resources: memory minimum size of 25 pages exceeds memory limits"
),
}
Ok(())
}
#[test]
fn test_limits_table_only() -> Result<()> {
let engine = Engine::default();
let module = Module::new(
&engine,
r#"(module (memory (export "m") 0) (table (export "t") 0 anyfunc))"#,
)?;
let mut store = Store::new(&engine, ());
store.limiter(StoreLimitsBuilder::new().table_elements(5).build());
let instance = Instance::new(&mut store, &module, &[])?;
// Test instance exports and host objects *not* hitting the limit
for memory in std::array::IntoIter::new([
instance.get_memory(&mut store, "m").unwrap(),
Memory::new(&mut store, MemoryType::new(Limits::new(0, None)))?,
]) {
memory.grow(&mut store, 3)?;
memory.grow(&mut store, 5)?;
memory.grow(&mut store, 2)?;
memory.grow(&mut store, 1)?;
}
// Test instance exports and host objects hitting the limit
for table in std::array::IntoIter::new([
instance.get_table(&mut store, "t").unwrap(),
Table::new(
&mut store,
TableType::new(ValType::FuncRef, Limits::new(0, None)),
Val::FuncRef(None),
)?,
]) {
table.grow(&mut store, 2, Val::FuncRef(None))?;
table.grow(&mut store, 1, Val::FuncRef(None))?;
table.grow(&mut store, 2, Val::FuncRef(None))?;
assert_eq!(
table
.grow(&mut store, 1, Val::FuncRef(None))
.map_err(|e| e.to_string())
.unwrap_err(),
"failed to grow table by `1`"
);
}
Ok(())
}
#[test]
fn test_initial_table_limits_exceeded() -> Result<()> {
let engine = Engine::default();
let module = Module::new(&engine, r#"(module (table (export "t") 23 anyfunc))"#)?;
let mut store = Store::new(&engine, ());
store.limiter(StoreLimitsBuilder::new().table_elements(4).build());
match Instance::new(&mut store, &module, &[]) {
Ok(_) => unreachable!(),
Err(e) => assert_eq!(
e.to_string(),
"Insufficient resources: table minimum size of 23 elements exceeds table limits"
),
}
match Table::new(
&mut store,
TableType::new(ValType::FuncRef, Limits::new(99, None)),
Val::FuncRef(None),
) {
Ok(_) => unreachable!(),
Err(e) => assert_eq!(
e.to_string(),
"Insufficient resources: table minimum size of 99 elements exceeds table limits"
),
}
Ok(())
}
#[test]
fn test_pooling_allocator_initial_limits_exceeded() -> Result<()> {
let mut config = Config::new();
config.wasm_multi_memory(true);
config.allocation_strategy(InstanceAllocationStrategy::Pooling {
strategy: PoolingAllocationStrategy::NextAvailable,
module_limits: ModuleLimits {
memories: 2,
..Default::default()
},
instance_limits: InstanceLimits {
count: 1,
..Default::default()
},
});
let engine = Engine::new(&config)?;
let module = Module::new(
&engine,
r#"(module (memory (export "m1") 2) (memory (export "m2") 5))"#,
)?;
let mut store = Store::new(&engine, ());
store.limiter(StoreLimitsBuilder::new().memory_pages(3).build());
match Instance::new(&mut store, &module, &[]) {
Ok(_) => unreachable!(),
Err(e) => assert_eq!(
e.to_string(),
"Insufficient resources: memory minimum size of 5 pages exceeds memory limits"
),
}
// An instance should still be able to be created after the failure above
let module = Module::new(&engine, r#"(module (memory (export "m") 2))"#)?;
Instance::new(&mut store, &module, &[])?;
Ok(())
}
struct MemoryContext {
host_memory_used: AtomicUsize,
wasm_memory_used: AtomicUsize,
memory_limit: usize,
limit_exceeded: AtomicBool,
limiter_dropped: AtomicBool,
}
struct HostMemoryLimiter(Arc<MemoryContext>);
impl ResourceLimiter for HostMemoryLimiter {
fn memory_growing(&mut self, current: u32, desired: u32, maximum: Option<u32>) -> bool {
// Check if the desired exceeds a maximum (either from Wasm or from the host)
if desired > maximum.unwrap_or(u32::MAX) {
self.0.limit_exceeded.store(true, SeqCst);
return false;
}
assert_eq!(
current as usize * 0x10000,
self.0.wasm_memory_used.load(SeqCst)
);
let desired = desired as usize * 0x10000;
if desired + self.0.host_memory_used.load(SeqCst) > self.0.memory_limit {
self.0.limit_exceeded.store(true, SeqCst);
return false;
}
self.0.wasm_memory_used.store(desired, SeqCst);
true
}
fn table_growing(&mut self, _current: u32, _desired: u32, _maximum: Option<u32>) -> bool {
true
}
}
impl Drop for HostMemoryLimiter {
fn drop(&mut self) {
self.0.limiter_dropped.store(true, SeqCst);
}
}
#[test]
fn test_custom_limiter() -> Result<()> {
let engine = Engine::default();
let mut linker = Linker::new(&engine);
// This approximates a function that would "allocate" resources that the host tracks.
// Here this is a simple function that increments the current host memory "used".
linker.func_wrap(
"",
"alloc",
|caller: Caller<'_, Arc<MemoryContext>>, size: u32| -> u32 {
let ctx = caller.data();
let size = size as usize;
if size + ctx.host_memory_used.load(SeqCst) + ctx.wasm_memory_used.load(SeqCst)
<= ctx.memory_limit
{
ctx.host_memory_used.fetch_add(size, SeqCst);
return 1;
}
ctx.limit_exceeded.store(true, SeqCst);
0
},
)?;
let module = Module::new(
&engine,
r#"(module (import "" "alloc" (func $alloc (param i32) (result i32))) (memory (export "m") 0) (func (export "f") (param i32) (result i32) local.get 0 call $alloc))"#,
)?;
let context = Arc::new(MemoryContext {
host_memory_used: AtomicUsize::new(0),
wasm_memory_used: AtomicUsize::new(0),
memory_limit: 1 << 20, // 16 wasm pages is the limit for both wasm + host memory
limit_exceeded: AtomicBool::new(false),
limiter_dropped: AtomicBool::new(false),
});
let mut store = Store::new(&engine, context.clone());
store.limiter(HostMemoryLimiter(context.clone()));
let instance = linker.instantiate(&mut store, &module)?;
let memory = instance.get_memory(&mut store, "m").unwrap();
// Grow the memory by 640 KiB
memory.grow(&mut store, 3)?;
memory.grow(&mut store, 5)?;
memory.grow(&mut store, 2)?;
assert!(!context.limit_exceeded.load(SeqCst));
// Grow the host "memory" by 384 KiB
let f = instance.get_typed_func::<u32, u32, _>(&mut store, "f")?;
assert_eq!(f.call(&mut store, 1 * 0x10000)?, 1);
assert_eq!(f.call(&mut store, 3 * 0x10000)?, 1);
assert_eq!(f.call(&mut store, 2 * 0x10000)?, 1);
// Memory is at the maximum, but the limit hasn't been exceeded
assert!(!context.limit_exceeded.load(SeqCst));
// Try to grow the memory again
assert_eq!(
memory
.grow(&mut store, 1)
.map_err(|e| e.to_string())
.unwrap_err(),
"failed to grow memory by `1`"
);
assert!(context.limit_exceeded.load(SeqCst));
// Try to grow the host "memory" again
assert_eq!(f.call(&mut store, 1)?, 0);
assert!(context.limit_exceeded.load(SeqCst));
drop(store);
assert!(context.limiter_dropped.load(SeqCst));
Ok(())
}