make ResourceLimiter operate on Store data; add hooks for entering and exiting native code (#2952)
* wasmtime_runtime: move ResourceLimiter defaults into this crate In preparation of changing wasmtime::ResourceLimiter to be a re-export of this definition, because translating between two traits was causing problems elsewhere. * wasmtime: make ResourceLimiter a re-export of wasmtime_runtime::ResourceLimiter * refactor Store internals to support ResourceLimiter as part of store's data * add hooks for entering and exiting native code to Store * wasmtime-wast, fuzz: changes to adapt ResourceLimiter API * fix tests * wrap calls into wasm with entering/exiting exit hooks as well * the most trivial test found a bug, lets write some more * store: mark some methods as #[inline] on Store, StoreInner, StoreInnerMost Co-authored-By: Alex Crichton <alex@alexcrichton.com> * improve tests for the entering/exiting native hooks Co-authored-by: Alex Crichton <alex@alexcrichton.com>
This commit is contained in:
@@ -1,6 +1,4 @@
|
||||
use anyhow::Result;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst};
|
||||
use std::sync::Arc;
|
||||
use wasmtime::*;
|
||||
|
||||
#[test]
|
||||
@@ -11,13 +9,14 @@ fn test_limits() -> Result<()> {
|
||||
r#"(module (memory (export "m") 0) (table (export "t") 0 anyfunc))"#,
|
||||
)?;
|
||||
|
||||
let mut store = Store::new(&engine, ());
|
||||
store.limiter(
|
||||
let mut store = Store::new(
|
||||
&engine,
|
||||
StoreLimitsBuilder::new()
|
||||
.memory_pages(10)
|
||||
.table_elements(5)
|
||||
.build(),
|
||||
);
|
||||
store.limiter(|s| s as &mut dyn ResourceLimiter);
|
||||
|
||||
let instance = Instance::new(&mut store, &module, &[])?;
|
||||
|
||||
@@ -72,8 +71,8 @@ fn test_limits_memory_only() -> Result<()> {
|
||||
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 mut store = Store::new(&engine, StoreLimitsBuilder::new().memory_pages(10).build());
|
||||
store.limiter(|s| s as &mut dyn ResourceLimiter);
|
||||
|
||||
let instance = Instance::new(&mut store, &module, &[])?;
|
||||
|
||||
@@ -118,8 +117,8 @@ 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());
|
||||
let mut store = Store::new(&engine, StoreLimitsBuilder::new().memory_pages(10).build());
|
||||
store.limiter(|s| s as &mut dyn ResourceLimiter);
|
||||
|
||||
match Instance::new(&mut store, &module, &[]) {
|
||||
Ok(_) => unreachable!(),
|
||||
@@ -148,8 +147,8 @@ fn test_limits_table_only() -> Result<()> {
|
||||
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 mut store = Store::new(&engine, StoreLimitsBuilder::new().table_elements(5).build());
|
||||
store.limiter(|s| s as &mut dyn ResourceLimiter);
|
||||
|
||||
let instance = Instance::new(&mut store, &module, &[])?;
|
||||
|
||||
@@ -194,8 +193,8 @@ 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());
|
||||
let mut store = Store::new(&engine, StoreLimitsBuilder::new().table_elements(4).build());
|
||||
store.limiter(|s| s as &mut dyn ResourceLimiter);
|
||||
|
||||
match Instance::new(&mut store, &module, &[]) {
|
||||
Ok(_) => unreachable!(),
|
||||
@@ -242,8 +241,8 @@ fn test_pooling_allocator_initial_limits_exceeded() -> Result<()> {
|
||||
r#"(module (memory (export "m1") 2) (memory (export "m2") 5))"#,
|
||||
)?;
|
||||
|
||||
let mut store = Store::new(&engine, ());
|
||||
store.limiter(StoreLimitsBuilder::new().memory_pages(3).build());
|
||||
let mut store = Store::new(&engine, StoreLimitsBuilder::new().memory_pages(3).build());
|
||||
store.limiter(|s| s as &mut dyn ResourceLimiter);
|
||||
|
||||
match Instance::new(&mut store, &module, &[]) {
|
||||
Ok(_) => unreachable!(),
|
||||
@@ -262,35 +261,29 @@ fn test_pooling_allocator_initial_limits_exceeded() -> Result<()> {
|
||||
}
|
||||
|
||||
struct MemoryContext {
|
||||
host_memory_used: AtomicUsize,
|
||||
wasm_memory_used: AtomicUsize,
|
||||
host_memory_used: usize,
|
||||
wasm_memory_used: usize,
|
||||
memory_limit: usize,
|
||||
limit_exceeded: AtomicBool,
|
||||
limiter_dropped: AtomicBool,
|
||||
limit_exceeded: bool,
|
||||
}
|
||||
|
||||
struct HostMemoryLimiter(Arc<MemoryContext>);
|
||||
|
||||
impl ResourceLimiter for HostMemoryLimiter {
|
||||
impl ResourceLimiter for MemoryContext {
|
||||
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);
|
||||
self.limit_exceeded = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
current as usize * 0x10000,
|
||||
self.0.wasm_memory_used.load(SeqCst)
|
||||
);
|
||||
assert_eq!(current as usize * 0x10000, self.wasm_memory_used,);
|
||||
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);
|
||||
if desired + self.host_memory_used > self.memory_limit {
|
||||
self.limit_exceeded = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
self.0.wasm_memory_used.store(desired, SeqCst);
|
||||
self.wasm_memory_used = desired;
|
||||
true
|
||||
}
|
||||
|
||||
@@ -299,12 +292,6 @@ impl ResourceLimiter for HostMemoryLimiter {
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -315,18 +302,16 @@ fn test_custom_limiter() -> Result<()> {
|
||||
linker.func_wrap(
|
||||
"",
|
||||
"alloc",
|
||||
|caller: Caller<'_, Arc<MemoryContext>>, size: u32| -> u32 {
|
||||
let ctx = caller.data();
|
||||
|mut caller: Caller<'_, MemoryContext>, size: u32| -> u32 {
|
||||
let mut ctx = caller.data_mut();
|
||||
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);
|
||||
if size + ctx.host_memory_used + ctx.wasm_memory_used <= ctx.memory_limit {
|
||||
ctx.host_memory_used += size;
|
||||
return 1;
|
||||
}
|
||||
|
||||
ctx.limit_exceeded.store(true, SeqCst);
|
||||
ctx.limit_exceeded = true;
|
||||
|
||||
0
|
||||
},
|
||||
@@ -337,16 +322,15 @@ fn test_custom_limiter() -> Result<()> {
|
||||
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),
|
||||
let context = MemoryContext {
|
||||
host_memory_used: 0,
|
||||
wasm_memory_used: 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),
|
||||
});
|
||||
limit_exceeded: false,
|
||||
};
|
||||
|
||||
let mut store = Store::new(&engine, context.clone());
|
||||
store.limiter(HostMemoryLimiter(context.clone()));
|
||||
let mut store = Store::new(&engine, context);
|
||||
store.limiter(|s| s as &mut dyn ResourceLimiter);
|
||||
let instance = linker.instantiate(&mut store, &module)?;
|
||||
let memory = instance.get_memory(&mut store, "m").unwrap();
|
||||
|
||||
@@ -355,7 +339,7 @@ fn test_custom_limiter() -> Result<()> {
|
||||
memory.grow(&mut store, 5)?;
|
||||
memory.grow(&mut store, 2)?;
|
||||
|
||||
assert!(!context.limit_exceeded.load(SeqCst));
|
||||
assert!(!store.data().limit_exceeded);
|
||||
|
||||
// Grow the host "memory" by 384 KiB
|
||||
let f = instance.get_typed_func::<u32, u32, _>(&mut store, "f")?;
|
||||
@@ -365,7 +349,7 @@ fn test_custom_limiter() -> Result<()> {
|
||||
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));
|
||||
assert!(!store.data().limit_exceeded);
|
||||
|
||||
// Try to grow the memory again
|
||||
assert_eq!(
|
||||
@@ -376,16 +360,14 @@ fn test_custom_limiter() -> Result<()> {
|
||||
"failed to grow memory by `1`"
|
||||
);
|
||||
|
||||
assert!(context.limit_exceeded.load(SeqCst));
|
||||
assert!(store.data().limit_exceeded);
|
||||
|
||||
// Try to grow the host "memory" again
|
||||
assert_eq!(f.call(&mut store, 1)?, 0);
|
||||
|
||||
assert!(context.limit_exceeded.load(SeqCst));
|
||||
assert!(store.data().limit_exceeded);
|
||||
|
||||
drop(store);
|
||||
|
||||
assert!(context.limiter_dropped.load(SeqCst));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ mod module;
|
||||
mod module_linking;
|
||||
mod module_serialize;
|
||||
mod name;
|
||||
mod native_hooks;
|
||||
mod pooling_allocator;
|
||||
mod stack_overflow;
|
||||
mod store;
|
||||
|
||||
@@ -217,8 +217,8 @@ fn limit_instances() -> Result<()> {
|
||||
)
|
||||
"#,
|
||||
)?;
|
||||
let mut store = Store::new(&engine, ());
|
||||
store.limiter(StoreLimitsBuilder::new().instances(10).build());
|
||||
let mut store = Store::new(&engine, StoreLimitsBuilder::new().instances(10).build());
|
||||
store.limiter(|s| s as &mut dyn ResourceLimiter);
|
||||
let err = Instance::new(&mut store, &module, &[]).err().unwrap();
|
||||
assert!(
|
||||
err.to_string().contains("resource limit exceeded"),
|
||||
@@ -253,8 +253,8 @@ fn limit_memories() -> Result<()> {
|
||||
)
|
||||
"#,
|
||||
)?;
|
||||
let mut store = Store::new(&engine, ());
|
||||
store.limiter(StoreLimitsBuilder::new().memories(10).build());
|
||||
let mut store = Store::new(&engine, StoreLimitsBuilder::new().memories(10).build());
|
||||
store.limiter(|s| s as &mut dyn ResourceLimiter);
|
||||
let err = Instance::new(&mut store, &module, &[]).err().unwrap();
|
||||
assert!(
|
||||
err.to_string().contains("resource limit exceeded"),
|
||||
@@ -288,8 +288,8 @@ fn limit_tables() -> Result<()> {
|
||||
)
|
||||
"#,
|
||||
)?;
|
||||
let mut store = Store::new(&engine, ());
|
||||
store.limiter(StoreLimitsBuilder::new().tables(10).build());
|
||||
let mut store = Store::new(&engine, StoreLimitsBuilder::new().tables(10).build());
|
||||
store.limiter(|s| s as &mut dyn ResourceLimiter);
|
||||
let err = Instance::new(&mut store, &module, &[]).err().unwrap();
|
||||
assert!(
|
||||
err.to_string().contains("resource limit exceeded"),
|
||||
|
||||
244
tests/all/native_hooks.rs
Normal file
244
tests/all/native_hooks.rs
Normal file
@@ -0,0 +1,244 @@
|
||||
use anyhow::Error;
|
||||
use wasmtime::*;
|
||||
|
||||
// Crate a synchronous Func, call it directly:
|
||||
#[test]
|
||||
fn call_wrapped_func() -> Result<(), Error> {
|
||||
let mut store = Store::<State>::default();
|
||||
store.entering_native_code_hook(State::entering_native);
|
||||
store.exiting_native_code_hook(State::exiting_native);
|
||||
let f = Func::wrap(
|
||||
&mut store,
|
||||
|caller: Caller<State>, a: i32, b: i64, c: f32, d: f64| {
|
||||
assert_eq!(
|
||||
caller.data().switches_into_native % 2,
|
||||
1,
|
||||
"odd number of switches into native while in a Func"
|
||||
);
|
||||
assert_eq!(a, 1);
|
||||
assert_eq!(b, 2);
|
||||
assert_eq!(c, 3.0);
|
||||
assert_eq!(d, 4.0);
|
||||
},
|
||||
);
|
||||
|
||||
f.call(
|
||||
&mut store,
|
||||
&[Val::I32(1), Val::I64(2), 3.0f32.into(), 4.0f64.into()],
|
||||
)?;
|
||||
|
||||
// One switch from vm to native to call f, another in return from f.
|
||||
assert_eq!(store.data().switches_into_native, 2);
|
||||
|
||||
f.typed::<(i32, i64, f32, f64), (), _>(&store)?
|
||||
.call(&mut store, (1, 2, 3.0, 4.0))?;
|
||||
|
||||
assert_eq!(store.data().switches_into_native, 4);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Create an async Func, call it directly:
|
||||
#[tokio::test]
|
||||
async fn call_wrapped_async_func() -> Result<(), Error> {
|
||||
let mut config = Config::new();
|
||||
config.async_support(true);
|
||||
let engine = Engine::new(&config)?;
|
||||
let mut store = Store::new(&engine, State::default());
|
||||
store.entering_native_code_hook(State::entering_native);
|
||||
store.exiting_native_code_hook(State::exiting_native);
|
||||
let f = Func::wrap4_async(
|
||||
&mut store,
|
||||
|caller: Caller<State>, a: i32, b: i64, c: f32, d: f64| {
|
||||
Box::new(async move {
|
||||
assert_eq!(
|
||||
caller.data().switches_into_native % 2,
|
||||
1,
|
||||
"odd number of switches into native while in a Func"
|
||||
);
|
||||
assert_eq!(a, 1);
|
||||
assert_eq!(b, 2);
|
||||
assert_eq!(c, 3.0);
|
||||
assert_eq!(d, 4.0);
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
f.call_async(
|
||||
&mut store,
|
||||
&[Val::I32(1), Val::I64(2), 3.0f32.into(), 4.0f64.into()],
|
||||
)
|
||||
.await?;
|
||||
|
||||
// One switch from vm to native to call f, another in return from f.
|
||||
assert_eq!(store.data().switches_into_native, 2);
|
||||
|
||||
f.typed::<(i32, i64, f32, f64), (), _>(&store)?
|
||||
.call_async(&mut store, (1, 2, 3.0, 4.0))
|
||||
.await?;
|
||||
|
||||
assert_eq!(store.data().switches_into_native, 4);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Use the Linker to define a sync func, call it through WebAssembly:
|
||||
#[test]
|
||||
fn call_linked_func() -> Result<(), Error> {
|
||||
let engine = Engine::default();
|
||||
let mut store = Store::new(&engine, State::default());
|
||||
store.entering_native_code_hook(State::entering_native);
|
||||
store.exiting_native_code_hook(State::exiting_native);
|
||||
let mut linker = Linker::new(&engine);
|
||||
|
||||
linker.func_wrap(
|
||||
"host",
|
||||
"f",
|
||||
|caller: Caller<State>, a: i32, b: i64, c: f32, d: f64| {
|
||||
assert_eq!(
|
||||
caller.data().switches_into_native % 2,
|
||||
1,
|
||||
"odd number of switches into native while in a Func"
|
||||
);
|
||||
assert_eq!(a, 1);
|
||||
assert_eq!(b, 2);
|
||||
assert_eq!(c, 3.0);
|
||||
assert_eq!(d, 4.0);
|
||||
},
|
||||
)?;
|
||||
|
||||
let wat = r#"
|
||||
(module
|
||||
(import "host" "f"
|
||||
(func $f (param i32) (param i64) (param f32) (param f64)))
|
||||
(func (export "export")
|
||||
(call $f (i32.const 1) (i64.const 2) (f32.const 3.0) (f64.const 4.0)))
|
||||
)
|
||||
"#;
|
||||
let module = Module::new(&engine, wat)?;
|
||||
|
||||
let inst = linker.instantiate(&mut store, &module)?;
|
||||
let export = inst
|
||||
.get_export(&mut store, "export")
|
||||
.expect("get export")
|
||||
.into_func()
|
||||
.expect("export is func");
|
||||
|
||||
export.call(&mut store, &[])?;
|
||||
|
||||
// One switch from vm to native to call f, another in return from f.
|
||||
assert_eq!(store.data().switches_into_native, 2);
|
||||
|
||||
export.typed::<(), (), _>(&store)?.call(&mut store, ())?;
|
||||
|
||||
assert_eq!(store.data().switches_into_native, 4);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Use the Linker to define an async func, call it through WebAssembly:
|
||||
#[tokio::test]
|
||||
async fn call_linked_func_async() -> Result<(), Error> {
|
||||
let mut config = Config::new();
|
||||
config.async_support(true);
|
||||
let engine = Engine::new(&config)?;
|
||||
let mut store = Store::new(&engine, State::default());
|
||||
store.entering_native_code_hook(State::entering_native);
|
||||
store.exiting_native_code_hook(State::exiting_native);
|
||||
|
||||
let f = Func::wrap4_async(
|
||||
&mut store,
|
||||
|caller: Caller<State>, a: i32, b: i64, c: f32, d: f64| {
|
||||
Box::new(async move {
|
||||
assert_eq!(
|
||||
caller.data().switches_into_native % 2,
|
||||
1,
|
||||
"odd number of switches into native while in a Func"
|
||||
);
|
||||
assert_eq!(a, 1);
|
||||
assert_eq!(b, 2);
|
||||
assert_eq!(c, 3.0);
|
||||
assert_eq!(d, 4.0);
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
let mut linker = Linker::new(&engine);
|
||||
|
||||
linker.define("host", "f", f)?;
|
||||
|
||||
let wat = r#"
|
||||
(module
|
||||
(import "host" "f"
|
||||
(func $f (param i32) (param i64) (param f32) (param f64)))
|
||||
(func (export "export")
|
||||
(call $f (i32.const 1) (i64.const 2) (f32.const 3.0) (f64.const 4.0)))
|
||||
)
|
||||
"#;
|
||||
let module = Module::new(&engine, wat)?;
|
||||
|
||||
let inst = linker.instantiate_async(&mut store, &module).await?;
|
||||
let export = inst
|
||||
.get_export(&mut store, "export")
|
||||
.expect("get export")
|
||||
.into_func()
|
||||
.expect("export is func");
|
||||
|
||||
export.call_async(&mut store, &[]).await?;
|
||||
|
||||
// One switch from vm to native to call f, another in return from export.
|
||||
assert_eq!(store.data().switches_into_native, 2);
|
||||
|
||||
export
|
||||
.typed::<(), (), _>(&store)?
|
||||
.call_async(&mut store, ())
|
||||
.await?;
|
||||
|
||||
// 2 more switches.
|
||||
assert_eq!(store.data().switches_into_native, 4);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
enum Context {
|
||||
Native,
|
||||
Vm,
|
||||
}
|
||||
|
||||
struct State {
|
||||
context: Context,
|
||||
switches_into_native: usize,
|
||||
}
|
||||
|
||||
impl Default for State {
|
||||
fn default() -> Self {
|
||||
State {
|
||||
context: Context::Native,
|
||||
switches_into_native: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn entering_native(&mut self) -> Result<(), Trap> {
|
||||
match self.context {
|
||||
Context::Vm => {
|
||||
println!("entering native");
|
||||
self.context = Context::Native;
|
||||
self.switches_into_native += 1;
|
||||
Ok(())
|
||||
}
|
||||
Context::Native => Err(Trap::new("illegal state: exiting vm when in native")),
|
||||
}
|
||||
}
|
||||
fn exiting_native(&mut self) -> Result<(), Trap> {
|
||||
match self.context {
|
||||
Context::Native => {
|
||||
println!("entering vm");
|
||||
self.context = Context::Vm;
|
||||
Ok(())
|
||||
}
|
||||
Context::Vm => Err(Trap::new("illegal state: exiting native when in vm")),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user