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:
Pat Hickey
2021-06-08 07:37:00 -07:00
committed by GitHub
parent ffb92d9109
commit 8b4bdf92e2
17 changed files with 550 additions and 283 deletions

View File

@@ -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(())
}

View File

@@ -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;

View File

@@ -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
View 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")),
}
}
}