From 4e5af2f3d3078d39c8cf0c1abc34c0df241f59d5 Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 1 Nov 2024 17:44:41 +0000 Subject: [PATCH 1/4] Pausing execution of interpreter. --- crates/interpreter/Cargo.toml | 2 + crates/interpreter/examples/hello.rs | 27 ++- crates/interpreter/src/exec.rs | 74 ++++++- crates/interpreter/test.sh | 6 +- crates/interpreter/tests/spec.rs | 194 ++++++++++++++---- .../pause/test/core/infinite_loop.wast | 27 +++ 6 files changed, 279 insertions(+), 51 deletions(-) create mode 100644 third_party/WebAssembly/pause/test/core/infinite_loop.wast diff --git a/crates/interpreter/Cargo.toml b/crates/interpreter/Cargo.toml index 1e42ac6c0..894104165 100644 --- a/crates/interpreter/Cargo.toml +++ b/crates/interpreter/Cargo.toml @@ -33,6 +33,8 @@ float-types = ["dep:libm"] vector-types = [] # Enable caching for execution. cache = ["dep:lru"] +# Enable pausing / preemption. +pause = [] [lints] clippy.unit-arg = "allow" diff --git a/crates/interpreter/examples/hello.rs b/crates/interpreter/examples/hello.rs index 224e7c7b4..3ce55b2a3 100644 --- a/crates/interpreter/examples/hello.rs +++ b/crates/interpreter/examples/hello.rs @@ -13,7 +13,8 @@ // limitations under the License. #![allow(unused_crate_dependencies)] - +#[cfg(feature = "pause")] +use portable_atomic::AtomicBool; use wasefire_interpreter::*; fn main() { @@ -52,11 +53,29 @@ fn main() { // the host does neither have enough memory nor virtual memory. let mut memory = [0; 5]; + #[cfg(feature = "pause")] + let interrupt = AtomicBool::new(false); + // Instantiate the module in the store. - let inst = store.instantiate(module, &mut memory).unwrap(); + let inst = store + .instantiate( + module, + &mut memory, + #[cfg(feature = "pause")] + Some(&interrupt), + ) + .unwrap(); // Call the "main" function exported by the instance. - let mut result = store.invoke(inst, "main", vec![]).unwrap(); + let mut result = store + .invoke( + inst, + "main", + vec![], + #[cfg(feature = "pause")] + &interrupt, + ) + .unwrap(); // Process calls from the module to the host until "main" terminates. loop { @@ -67,6 +86,8 @@ fn main() { assert!(results.is_empty()); break; } + #[cfg(feature = "pause")] + RunResult::Interrupt() => unreachable!(), }; // We only linked one function, which has thus index zero. diff --git a/crates/interpreter/src/exec.rs b/crates/interpreter/src/exec.rs index e0c0a4fec..710cdd3de 100644 --- a/crates/interpreter/src/exec.rs +++ b/crates/interpreter/src/exec.rs @@ -16,6 +16,9 @@ use alloc::vec; use alloc::vec::Vec; +#[cfg(feature = "pause")] +use portable_atomic::AtomicBool; + use crate::error::*; use crate::module::*; use crate::syntax::*; @@ -116,6 +119,7 @@ impl<'m> Store<'m> { /// access part of the memory that does not exist. pub fn instantiate( &mut self, module: Module<'m>, memory: &'m mut [u8], + #[cfg(feature = "pause")] interrupt: Option<&'m AtomicBool>, ) -> Result { let inst_id = self.insts.len(); self.insts.push(Instance::default()); @@ -195,8 +199,17 @@ impl<'m> Store<'m> { let mut parser = self.insts[inst_id].module.func(ptr.index()); let mut locals = Vec::new(); append_locals(&mut parser, &mut locals); - let thread = Thread::new(parser, Frame::new(inst_id, 0, &[], locals)); + let thread = Thread::new( + parser, + Frame::new(inst_id, 0, &[], locals), + #[cfg(feature = "pause")] + interrupt, + ); let result = thread.run(self)?; + #[cfg(feature = "pause")] + if matches!(result, RunResult::Interrupt()) { + return Err(Error::Trap); + } assert!(matches!(result, RunResult::Done(x) if x.is_empty())); } Ok(InstId { store_id: self.id, inst_id }) @@ -209,6 +222,7 @@ impl<'m> Store<'m> { /// may be corrupted. pub fn invoke<'a>( &'a mut self, inst: InstId, name: &str, args: Vec, + #[cfg(feature = "pause")] interrupt: &'m AtomicBool, ) -> Result, Error> { let inst_id = self.inst_id(inst)?; let inst = &self.insts[inst_id]; @@ -225,7 +239,13 @@ impl<'m> Store<'m> { let mut locals = args; append_locals(&mut parser, &mut locals); let frame = Frame::new(inst_id, t.results.len(), &[], locals); - Thread::new(parser, frame).run(self) + Thread::new( + parser, + frame, + #[cfg(feature = "pause")] + Some(interrupt), + ) + .run(self) } /// Returns the value of a global of an instance. @@ -460,6 +480,8 @@ struct Instance<'m> { struct Thread<'m> { parser: Parser<'m>, frames: Vec>, + #[cfg(feature = "pause")] + interrupt: Option<&'m AtomicBool>, } /// Runtime result. @@ -470,6 +492,10 @@ pub enum RunResult<'a, 'm> { /// Execution is calling into the host. Host(Call<'a, 'm>), + + #[cfg(feature = "pause")] + // Execution pre-empted / interrupted. + Interrupt(), } /// Runtime result without host call information. @@ -484,6 +510,8 @@ impl RunResult<'_, '_> { match self { RunResult::Done(result) => RunAnswer::Done(result), RunResult::Host(_) => RunAnswer::Host, + #[cfg(feature = "pause")] + RunResult::Interrupt() => RunAnswer::Host, } } } @@ -724,21 +752,38 @@ enum ThreadResult<'m> { Continue(Thread<'m>), Done(Vec), Host, + #[cfg(feature = "pause")] + Interrupt, } impl<'m> Thread<'m> { - fn new(parser: Parser<'m>, frame: Frame<'m>) -> Thread<'m> { - Thread { parser, frames: vec![frame] } + fn new( + parser: Parser<'m>, frame: Frame<'m>, + #[cfg(feature = "pause")] interrupt: Option<&'m AtomicBool>, + ) -> Thread<'m> { + Thread { + parser, + frames: vec![frame], + #[cfg(feature = "pause")] + interrupt, + } } fn const_expr(store: &mut Store<'m>, inst_id: usize, mut_parser: &mut Parser<'m>) -> Val { let parser = mut_parser.clone(); - let mut thread = Thread::new(parser, Frame::new(inst_id, 1, &[], Vec::new())); + let mut thread = Thread::new( + parser, + Frame::new(inst_id, 1, &[], Vec::new()), + #[cfg(feature = "pause")] + None, + ); let (parser, results) = loop { let p = thread.parser.save(); match thread.step(store).unwrap() { ThreadResult::Continue(x) => thread = x, ThreadResult::Done(x) => break (p, x), + #[cfg(feature = "pause")] + ThreadResult::Interrupt => unreachable!(), ThreadResult::Host => unreachable!(), } }; @@ -757,6 +802,8 @@ impl<'m> Thread<'m> { ThreadResult::Continue(x) => self = x, ThreadResult::Done(x) => return Ok(RunResult::Done(x)), ThreadResult::Host => return Ok(RunResult::Host(Call { store })), + #[cfg(feature = "pause")] + ThreadResult::Interrupt => return Ok(RunResult::Interrupt()), } } } @@ -1034,6 +1081,17 @@ impl<'m> Thread<'m> { let label = Label { arity, kind, values }; self.labels().push(label); } + #[cfg(feature = "pause")] + fn check_interrupt_or_continue(self) -> ThreadResult<'m> { + if self + .interrupt + .is_some_and(|interrupt| interrupt.load(core::sync::atomic::Ordering::Relaxed)) + { + return ThreadResult::Interrupt; + } + + ThreadResult::Continue(self) + } fn pop_label(mut self, inst: &mut Instance<'m>, l: LabelIdx) -> ThreadResult<'m> { let i = self.labels().len() - l as usize - 1; @@ -1048,6 +1106,9 @@ impl<'m> Thread<'m> { LabelKind::Loop(pos) => unsafe { self.parser.restore(pos) }, LabelKind::Block | LabelKind::If => self.skip_to_end(inst, l), } + #[cfg(feature = "pause")] + return self.check_interrupt_or_continue(); + #[cfg(not(feature = "pause"))] ThreadResult::Continue(self) } @@ -1366,6 +1427,9 @@ impl<'m> Thread<'m> { let ret = self.parser.save(); self.parser = parser; self.frames.push(Frame::new(inst_id, t.results.len(), ret, locals)); + #[cfg(feature = "pause")] + return Ok(self.check_interrupt_or_continue()); + #[cfg(not(feature = "pause"))] Ok(ThreadResult::Continue(self)) } } diff --git a/crates/interpreter/test.sh b/crates/interpreter/test.sh index de8007957..ca256d4c3 100755 --- a/crates/interpreter/test.sh +++ b/crates/interpreter/test.sh @@ -22,9 +22,11 @@ ensure_submodule third_party/WebAssembly/spec list_files() { find ../../third_party/WebAssembly/spec/test/core \ -maxdepth 1 -name '*.wast' -execdir basename -s .wast {} \; + find ../../third_party/WebAssembly/pause/test/core \ + -maxdepth 1 -name '*.wast' -execdir basename -s .wast {} \; } list_tests() { - sed -n 's/^test!(.*, "\([^"]*\)".*);$/\1/p;s/^test!(\([^,]*\).*);$/\1/p' tests/spec.rs + sed -n 's/^test!(.*, "\([^"]*\)".*);$/\1/p;s/^test!(\([^,]*\).*);$/\1/p;s/^test!("[^"]*",[^,]+,"\([^"]*\)");$/\1/p' tests/spec.rs | sort } diff_sorted tests/spec.rs "$(list_files | sort)" $(list_tests) @@ -39,4 +41,4 @@ RUSTFLAGS=--cfg=portable_atomic_unsafe_assume_single_core \ cargo check --lib --target=riscv32imc-unknown-none-elf cargo check --example=hello # Run with `-- --test-threads=1 --nocapture` to see unsupported tests. -cargo test --test=spec --features=debug,toctou,float-types,vector-types +cargo test --test=spec --features=debug,toctou,float-types,vector-types,pause diff --git a/crates/interpreter/tests/spec.rs b/crates/interpreter/tests/spec.rs index b5ba42510..547f7e10b 100644 --- a/crates/interpreter/tests/spec.rs +++ b/crates/interpreter/tests/spec.rs @@ -18,6 +18,8 @@ use std::collections::HashMap; use lazy_static::lazy_static; +#[cfg(feature = "pause")] +use portable_atomic::AtomicBool; use wasefire_interpreter::*; use wast::core::{AbstractHeapType, WastArgCore, WastRetCore}; use wast::lexer::Lexer; @@ -33,32 +35,68 @@ fn test(repo: &str, name: &str, skip: usize) { let wast: Wast = parser::parse(&buffer).unwrap(); let layout = std::alloc::Layout::from_size_align(pool_size(name), MEMORY_ALIGN).unwrap(); let pool = unsafe { std::slice::from_raw_parts_mut(std::alloc::alloc(layout), layout.size()) }; - let mut env = Env::new(pool); - env.instantiate("spectest", &SPECTEST); - env.register_name("spectest", None); - assert!(matches!(env.inst, Sup::Yes(_))); - for directive in wast.directives { - eprintln!("{name}:{}", directive.span().offset()); - match directive { - WastDirective::Module(QuoteWat::Wat(Wat::Module(mut m))) => { - env.instantiate(name, &m.encode().unwrap()); - env.register_id(m.id, env.inst); + #[cfg(feature = "pause")] + let interrupt = AtomicBool::new(false); + std::thread::scope(|s| { + #[cfg(feature = "pause")] + s.spawn(|| { + if name == "infinite_loop" { + interrupt.store(true, std::sync::atomic::Ordering::SeqCst); } - WastDirective::Module(mut wat) => env.instantiate(name, &wat.encode().unwrap()), - WastDirective::AssertMalformed { module, .. } => assert_malformed(&mut env, module), - WastDirective::AssertInvalid { module, .. } => assert_invalid(&mut env, module), - WastDirective::AssertReturn { exec, results, .. } => { - assert_return(&mut env, exec, results) + }); + let mut env = Env::new( + pool, + #[cfg(feature = "pause")] + &interrupt, + ); + env.instantiate( + "spectest", + &SPECTEST, + #[cfg(feature = "pause")] + Some(&interrupt), + ); + env.register_name("spectest", None); + assert!(matches!(env.inst, Sup::Yes(_))); + for directive in wast.directives { + eprintln!("{name}:{}", directive.span().offset()); + match directive { + WastDirective::Module(QuoteWat::Wat(Wat::Module(mut m))) => { + env.instantiate( + name, + &m.encode().unwrap(), + #[cfg(feature = "pause")] + Some(&interrupt), + ); + env.register_id(m.id, env.inst); + } + WastDirective::Module(mut wat) => env.instantiate( + name, + &wat.encode().unwrap(), + #[cfg(feature = "pause")] + Some(&interrupt), + ), + WastDirective::AssertMalformed { module, .. } => assert_malformed(&mut env, module), + WastDirective::AssertInvalid { module, .. } => assert_invalid(&mut env, module), + WastDirective::AssertReturn { exec, results, .. } => { + assert_return(&mut env, exec, results) + } + WastDirective::AssertTrap { exec, .. } => assert_trap( + &mut env, + exec, + #[cfg(feature = "pause")] + Some(&interrupt), + ), + WastDirective::Invoke(invoke) => assert_invoke(&mut env, invoke), + WastDirective::AssertExhaustion { call, .. } => assert_exhaustion(&mut env, call), + WastDirective::Register { name, module, .. } => env.register_name(name, module), + WastDirective::AssertUnlinkable { module, .. } => { + assert_unlinkable(&mut env, module) + } + _ => unimplemented!("{:?}", directive), } - WastDirective::AssertTrap { exec, .. } => assert_trap(&mut env, exec), - WastDirective::Invoke(invoke) => assert_invoke(&mut env, invoke), - WastDirective::AssertExhaustion { call, .. } => assert_exhaustion(&mut env, call), - WastDirective::Register { name, module, .. } => env.register_name(name, module), - WastDirective::AssertUnlinkable { module, .. } => assert_unlinkable(&mut env, module), - _ => unimplemented!("{:?}", directive), } - } - assert_eq!(env.skip, skip); + assert_eq!(env.skip, skip); + }); } fn pool_size(name: &str) -> usize { @@ -144,12 +182,22 @@ struct Env<'m> { store: Store<'m>, inst: Sup, map: HashMap, Sup>, + #[cfg(feature = "pause")] + interrupt: &'m AtomicBool, skip: usize, } impl<'m> Env<'m> { - fn new(pool: &'m mut [u8]) -> Self { - Env { pool, store: Store::default(), inst: Sup::Uninit, map: HashMap::new(), skip: 0 } + fn new(pool: &'m mut [u8], #[cfg(feature = "pause")] interrupt: &'m AtomicBool) -> Self { + Env { + pool, + store: Store::default(), + inst: Sup::Uninit, + map: HashMap::new(), + #[cfg(feature = "pause")] + interrupt, + skip: 0, + } } fn alloc(&mut self, size: usize) -> &'m mut [u8] { @@ -163,7 +211,10 @@ impl<'m> Env<'m> { &mut result[.. size] } - fn maybe_instantiate(&mut self, name: &str, wasm: &[u8]) -> Result { + fn maybe_instantiate( + &mut self, name: &str, wasm: &[u8], + #[cfg(feature = "pause")] interrupt: Option<&'m AtomicBool>, + ) -> Result { let module = self.alloc(wasm.len()); module.copy_from_slice(wasm); let module = match Module::new(module) { @@ -171,19 +222,40 @@ impl<'m> Env<'m> { Err(e) => return Err(e), }; let memory = self.alloc(mem_size(name)); - self.store.instantiate(module, memory) + self.store.instantiate( + module, + memory, + #[cfg(feature = "pause")] + interrupt, + ) } - fn instantiate(&mut self, name: &str, wasm: &[u8]) { - let inst = self.maybe_instantiate(name, wasm); + fn instantiate( + &mut self, name: &str, wasm: &[u8], + #[cfg(feature = "pause")] interrupt: Option<&'m AtomicBool>, + ) { + let inst = self.maybe_instantiate( + name, + wasm, + #[cfg(feature = "pause")] + interrupt, + ); self.inst = Sup::conv(inst).unwrap(); } fn invoke(&mut self, inst_id: InstId, name: &str, args: Vec) -> Result, Error> { - Ok(match self.store.invoke(inst_id, name, args)? { - RunResult::Done(x) => x, + match self.store.invoke( + inst_id, + name, + args, + #[cfg(feature = "pause")] + self.interrupt, + )? { + RunResult::Done(x) => Ok(x), + #[cfg(feature = "pause")] + RunResult::Interrupt() => Ok(vec![Val::I64(1111)]), RunResult::Host { .. } => unreachable!(), - }) + } } fn register_name(&mut self, name: &'m str, module: Option>) { @@ -293,7 +365,16 @@ fn spectest() -> Vec { } fn assert_return(env: &mut Env, exec: WastExecute, expected: Vec) { - let actual = only_sup!(env, wast_execute(env, exec)).unwrap(); + let actual = only_sup!( + env, + wast_execute( + env, + exec, + #[cfg(feature = "pause")] + None + ) + ) + .unwrap(); assert_eq!(actual.len(), expected.len()); for (actual, expected) in actual.into_iter().zip(expected.into_iter()) { use wast::core::HeapType; @@ -330,8 +411,22 @@ fn assert_return(env: &mut Env, exec: WastExecute, expected: Vec) { } } -fn assert_trap(env: &mut Env, exec: WastExecute) { - assert_eq!(only_sup!(env, wast_execute(env, exec)), Err(Error::Trap)); +fn assert_trap<'m>( + env: &mut Env<'m>, exec: WastExecute, + #[cfg(feature = "pause")] interrupt: Option<&'m AtomicBool>, +) { + assert_eq!( + only_sup!( + env, + wast_execute( + env, + exec, + #[cfg(feature = "pause")] + interrupt + ) + ), + Err(Error::Trap) + ); } fn assert_invoke(env: &mut Env, invoke: WastInvoke) { @@ -354,16 +449,32 @@ fn assert_exhaustion(env: &mut Env, call: WastInvoke) { } fn assert_unlinkable(env: &mut Env, mut wat: Wat) { - let inst = only_sup!(env, env.maybe_instantiate("", &wat.encode().unwrap())); + let inst = only_sup!( + env, + env.maybe_instantiate( + "", + &wat.encode().unwrap(), + #[cfg(feature = "pause")] + None + ) + ); assert_eq!(inst.err(), Some(Error::NotFound)); } -fn wast_execute(env: &mut Env, exec: WastExecute) -> Result, Error> { +fn wast_execute<'m>( + env: &mut Env<'m>, exec: WastExecute, + #[cfg(feature = "pause")] interrupt: Option<&'m AtomicBool>, +) -> Result, Error> { match exec { WastExecute::Invoke(invoke) => wast_invoke(env, invoke), - WastExecute::Wat(mut wat) => { - env.maybe_instantiate("", &wat.encode().unwrap()).map(|_| Vec::new()) - } + WastExecute::Wat(mut wat) => env + .maybe_instantiate( + "", + &wat.encode().unwrap(), + #[cfg(feature = "pause")] + interrupt, + ) + .map(|_| Vec::new()), WastExecute::Get { module, global, .. } => { let inst_id = env.inst_id(module).res()?; env.store.get_global(inst_id, global).map(|x| vec![x]) @@ -436,7 +547,6 @@ macro_rules! test { (=5 $name:ident) => { stringify!($name) }; (=5 $file:literal) => { $file }; } - test!(address); test!(align); test!(binary); @@ -527,3 +637,5 @@ test!(utf8_custom_section_id, "utf8-custom-section-id"); test!(utf8_import_field, "utf8-import-field"); test!(utf8_import_module, "utf8-import-module"); test!(utf8_invalid_encoding, "utf8-invalid-encoding"); +#[cfg(feature = "pause")] +test!("pause", pause, "infinite_loop"); diff --git a/third_party/WebAssembly/pause/test/core/infinite_loop.wast b/third_party/WebAssembly/pause/test/core/infinite_loop.wast new file mode 100644 index 000000000..8f746c5ed --- /dev/null +++ b/third_party/WebAssembly/pause/test/core/infinite_loop.wast @@ -0,0 +1,27 @@ +(module + (func (export "loopforever") + (loop + (br 0) + ) + ) + + (func $recurseforever + call $recurseforever + ) + (export "recurseforever" (func $recurseforever)) +) + +(assert_return (invoke "recurseforever") (i64.const 1111)) +(assert_return (invoke "loopforever") (i64.const 1111)) + +(assert_trap + (module + (func $loopforever + (loop + (br 0) + ) + ) + + (start $loopforever) + ) +"interrupt") \ No newline at end of file From 3eb6b3d7af3800aa3fe0fd36d08b99147e6c5909 Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 29 Nov 2024 15:11:10 +0000 Subject: [PATCH 2/4] Dedicated tests and interrupt bool on store instead of argument. --- crates/interpreter/Cargo.toml | 4 +- crates/interpreter/examples/hello.rs | 27 +-- crates/interpreter/src/exec.rs | 141 ++++++++----- crates/interpreter/test.sh | 6 +- crates/interpreter/tests/infinite_loop.wasm | Bin 0 -> 111 bytes crates/interpreter/tests/infinite_loop.wat | 21 ++ crates/interpreter/tests/interrupt.rs | 70 +++++++ crates/interpreter/tests/spec.rs | 196 ++++-------------- .../pause/test/core/infinite_loop.wast | 27 --- 9 files changed, 235 insertions(+), 257 deletions(-) create mode 100644 crates/interpreter/tests/infinite_loop.wasm create mode 100644 crates/interpreter/tests/infinite_loop.wat create mode 100644 crates/interpreter/tests/interrupt.rs delete mode 100644 third_party/WebAssembly/pause/test/core/infinite_loop.wast diff --git a/crates/interpreter/Cargo.toml b/crates/interpreter/Cargo.toml index 1e0778027..07e4874ad 100644 --- a/crates/interpreter/Cargo.toml +++ b/crates/interpreter/Cargo.toml @@ -33,8 +33,8 @@ float-types = ["dep:libm"] vector-types = [] # Enable caching for execution. cache = ["dep:lru"] -# Enable pausing / preemption. -pause = [] +# Enable interrupting execution. +interrupt = [] [lints] clippy.unit-arg = "allow" diff --git a/crates/interpreter/examples/hello.rs b/crates/interpreter/examples/hello.rs index 3ce55b2a3..224e7c7b4 100644 --- a/crates/interpreter/examples/hello.rs +++ b/crates/interpreter/examples/hello.rs @@ -13,8 +13,7 @@ // limitations under the License. #![allow(unused_crate_dependencies)] -#[cfg(feature = "pause")] -use portable_atomic::AtomicBool; + use wasefire_interpreter::*; fn main() { @@ -53,29 +52,11 @@ fn main() { // the host does neither have enough memory nor virtual memory. let mut memory = [0; 5]; - #[cfg(feature = "pause")] - let interrupt = AtomicBool::new(false); - // Instantiate the module in the store. - let inst = store - .instantiate( - module, - &mut memory, - #[cfg(feature = "pause")] - Some(&interrupt), - ) - .unwrap(); + let inst = store.instantiate(module, &mut memory).unwrap(); // Call the "main" function exported by the instance. - let mut result = store - .invoke( - inst, - "main", - vec![], - #[cfg(feature = "pause")] - &interrupt, - ) - .unwrap(); + let mut result = store.invoke(inst, "main", vec![]).unwrap(); // Process calls from the module to the host until "main" terminates. loop { @@ -86,8 +67,6 @@ fn main() { assert!(results.is_empty()); break; } - #[cfg(feature = "pause")] - RunResult::Interrupt() => unreachable!(), }; // We only linked one function, which has thus index zero. diff --git a/crates/interpreter/src/exec.rs b/crates/interpreter/src/exec.rs index 710cdd3de..c4ecc5c1e 100644 --- a/crates/interpreter/src/exec.rs +++ b/crates/interpreter/src/exec.rs @@ -15,8 +15,10 @@ // TODO: Some toctou could be used instead of panic. use alloc::vec; use alloc::vec::Vec; +#[cfg(feature = "interrupt")] +use core::sync::atomic::Ordering::Relaxed; -#[cfg(feature = "pause")] +#[cfg(feature = "interrupt")] use portable_atomic::AtomicBool; use crate::error::*; @@ -61,6 +63,9 @@ pub struct Store<'m> { // functions in `funcs` is stored to limit normal linking to that part. func_default: Option<(&'m str, usize)>, threads: Vec>, + + #[cfg(feature = "interrupt")] + interrupt: Option<&'m AtomicBool>, } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] @@ -102,6 +107,8 @@ impl Default for Store<'_> { funcs: vec![], func_default: None, threads: vec![], + #[cfg(feature = "interrupt")] + interrupt: None, } } } @@ -119,7 +126,6 @@ impl<'m> Store<'m> { /// access part of the memory that does not exist. pub fn instantiate( &mut self, module: Module<'m>, memory: &'m mut [u8], - #[cfg(feature = "pause")] interrupt: Option<&'m AtomicBool>, ) -> Result { let inst_id = self.insts.len(); self.insts.push(Instance::default()); @@ -202,15 +208,21 @@ impl<'m> Store<'m> { let thread = Thread::new( parser, Frame::new(inst_id, 0, &[], locals), - #[cfg(feature = "pause")] - interrupt, + #[cfg(feature = "interrupt")] + self.interrupt, ); + + // Disable interrupts for the start section. + #[cfg(feature = "interrupt")] + let interrupt = self.interrupt; + #[cfg(feature = "interrupt")] + self.set_interrupt(None); + let result = thread.run(self)?; - #[cfg(feature = "pause")] - if matches!(result, RunResult::Interrupt()) { - return Err(Error::Trap); - } assert!(matches!(result, RunResult::Done(x) if x.is_empty())); + + #[cfg(feature = "interrupt")] + self.set_interrupt(interrupt); } Ok(InstId { store_id: self.id, inst_id }) } @@ -222,7 +234,6 @@ impl<'m> Store<'m> { /// may be corrupted. pub fn invoke<'a>( &'a mut self, inst: InstId, name: &str, args: Vec, - #[cfg(feature = "pause")] interrupt: &'m AtomicBool, ) -> Result, Error> { let inst_id = self.inst_id(inst)?; let inst = &self.insts[inst_id]; @@ -242,8 +253,8 @@ impl<'m> Store<'m> { Thread::new( parser, frame, - #[cfg(feature = "pause")] - Some(interrupt), + #[cfg(feature = "interrupt")] + self.interrupt, ) .run(self) } @@ -323,6 +334,11 @@ impl<'m> Store<'m> { Some(Call { store: self }) } } + + #[cfg(feature = "interrupt")] + pub fn set_interrupt(&mut self, interrupt: Option<&'m AtomicBool>) { + self.interrupt = interrupt; + } } impl<'a, 'm> Call<'a, 'm> { @@ -359,6 +375,12 @@ impl<'a, 'm> Call<'a, 'm> { thread.run(self.store) } + // Returns if this call is due to an interrupt. + #[cfg(feature = "interrupt")] + pub fn is_interrupt(&self) -> bool { + self.cont().interrupted + } + fn cont(&self) -> &Continuation { self.store.threads.last().unwrap() } @@ -480,7 +502,7 @@ struct Instance<'m> { struct Thread<'m> { parser: Parser<'m>, frames: Vec>, - #[cfg(feature = "pause")] + #[cfg(feature = "interrupt")] interrupt: Option<&'m AtomicBool>, } @@ -493,9 +515,9 @@ pub enum RunResult<'a, 'm> { /// Execution is calling into the host. Host(Call<'a, 'm>), - #[cfg(feature = "pause")] - // Execution pre-empted / interrupted. - Interrupt(), + // Execution was interrupted by the host. + #[cfg(feature = "interrupt")] + Interrupt(Call<'a, 'm>), } /// Runtime result without host call information. @@ -510,8 +532,8 @@ impl RunResult<'_, '_> { match self { RunResult::Done(result) => RunAnswer::Done(result), RunResult::Host(_) => RunAnswer::Host, - #[cfg(feature = "pause")] - RunResult::Interrupt() => RunAnswer::Host, + #[cfg(feature = "interrupt")] + RunResult::Interrupt(_) => RunAnswer::Host, } } } @@ -522,6 +544,8 @@ struct Continuation<'m> { index: usize, args: Vec, arity: usize, + #[cfg(feature = "interrupt")] + interrupted: bool, } impl<'m> Store<'m> { @@ -752,19 +776,20 @@ enum ThreadResult<'m> { Continue(Thread<'m>), Done(Vec), Host, - #[cfg(feature = "pause")] + #[cfg(feature = "interrupt")] Interrupt, } impl<'m> Thread<'m> { fn new( parser: Parser<'m>, frame: Frame<'m>, - #[cfg(feature = "pause")] interrupt: Option<&'m AtomicBool>, + #[cfg(feature = "interrupt")] interrupt: Option<&'m AtomicBool>, ) -> Thread<'m> { Thread { parser, frames: vec![frame], - #[cfg(feature = "pause")] + + #[cfg(feature = "interrupt")] interrupt, } } @@ -774,15 +799,15 @@ impl<'m> Thread<'m> { let mut thread = Thread::new( parser, Frame::new(inst_id, 1, &[], Vec::new()), - #[cfg(feature = "pause")] - None, + #[cfg(feature = "interrupt")] + store.interrupt, ); let (parser, results) = loop { let p = thread.parser.save(); match thread.step(store).unwrap() { ThreadResult::Continue(x) => thread = x, ThreadResult::Done(x) => break (p, x), - #[cfg(feature = "pause")] + #[cfg(feature = "interrupt")] ThreadResult::Interrupt => unreachable!(), ThreadResult::Host => unreachable!(), } @@ -802,8 +827,8 @@ impl<'m> Thread<'m> { ThreadResult::Continue(x) => self = x, ThreadResult::Done(x) => return Ok(RunResult::Done(x)), ThreadResult::Host => return Ok(RunResult::Host(Call { store })), - #[cfg(feature = "pause")] - ThreadResult::Interrupt => return Ok(RunResult::Interrupt()), + #[cfg(feature = "interrupt")] + ThreadResult::Interrupt => return Ok(RunResult::Interrupt(Call { store })), } } } @@ -812,7 +837,7 @@ impl<'m> Thread<'m> { use Instr::*; let saved = self.parser.save(); let inst_id = self.frame().inst_id; - let inst = &mut store.insts[inst_id]; + let inst: &mut Instance<'m> = &mut store.insts[inst_id]; match self.parser.parse_instr().into_ok() { Unreachable => return Err(trap()), Nop => (), @@ -830,15 +855,15 @@ impl<'m> Thread<'m> { return Ok(self.exit_label()); } End => return Ok(self.exit_label()), - Br(l) => return Ok(self.pop_label(inst, l)), + Br(l) => return self.pop_label(inst, l, &mut store.threads), BrIf(l) => { if self.pop_value().unwrap_i32() != 0 { - return Ok(self.pop_label(inst, l)); + return self.pop_label(inst, l, &mut store.threads); } } BrTable(ls, ln) => { let i = self.pop_value().unwrap_i32() as usize; - return Ok(self.pop_label(inst, ls.get(i).cloned().unwrap_or(ln))); + return self.pop_label(inst, ls.get(i).cloned().unwrap_or(ln), &mut store.threads); } Return => return Ok(self.exit_frame()), Call(x) => return self.invoke(store, store.func_ptr(inst_id, x)), @@ -1081,35 +1106,51 @@ impl<'m> Thread<'m> { let label = Label { arity, kind, values }; self.labels().push(label); } - #[cfg(feature = "pause")] - fn check_interrupt_or_continue(self) -> ThreadResult<'m> { - if self - .interrupt - .is_some_and(|interrupt| interrupt.load(core::sync::atomic::Ordering::Relaxed)) - { + + #[allow(clippy::ptr_arg)] + fn check_interrupt_or_continue(self, _threads: &mut Vec>) -> ThreadResult<'m> { + #[cfg(feature = "interrupt")] + if self.interrupt.is_some_and(|interrupt| { + interrupt.compare_exchange_weak(true, false, Relaxed, Relaxed).is_ok() + }) { + _threads.push(Continuation { + thread: self, + index: 0, + args: vec![], + arity: 0, + #[cfg(feature = "interrupt")] + interrupted: true, + }); return ThreadResult::Interrupt; } ThreadResult::Continue(self) } - fn pop_label(mut self, inst: &mut Instance<'m>, l: LabelIdx) -> ThreadResult<'m> { + fn pop_label( + mut self, inst: &mut Instance<'m>, l: LabelIdx, threads: &mut Vec>, + ) -> Result, Error> { let i = self.labels().len() - l as usize - 1; if i == 0 { - return self.exit_frame(); + return Ok(self.exit_frame()); } let values = core::mem::take(self.values()); let frame = self.frame(); let Label { arity, kind, .. } = frame.labels.drain(i ..).next().unwrap(); self.values().extend_from_slice(&values[values.len() - arity ..]); + match kind { - LabelKind::Loop(pos) => unsafe { self.parser.restore(pos) }, - LabelKind::Block | LabelKind::If => self.skip_to_end(inst, l), + LabelKind::Loop(pos) => { + unsafe { + self.parser.restore(pos); + } + Ok(self.check_interrupt_or_continue(threads)) + } + LabelKind::Block | LabelKind::If => { + self.skip_to_end(inst, l); + Ok(ThreadResult::Continue(self)) + } } - #[cfg(feature = "pause")] - return self.check_interrupt_or_continue(); - #[cfg(not(feature = "pause"))] - ThreadResult::Continue(self) } fn exit_label(mut self) -> ThreadResult<'m> { @@ -1416,7 +1457,14 @@ impl<'m> Thread<'m> { let t = store.funcs[index].1; let arity = t.results.len(); let args = self.pop_values(t.params.len()); - store.threads.push(Continuation { thread: self, arity, index, args }); + store.threads.push(Continuation { + thread: self, + arity, + index, + args, + #[cfg(feature = "interrupt")] + interrupted: false, + }); return Ok(ThreadResult::Host); } Side::Wasm(x) => x, @@ -1427,10 +1475,7 @@ impl<'m> Thread<'m> { let ret = self.parser.save(); self.parser = parser; self.frames.push(Frame::new(inst_id, t.results.len(), ret, locals)); - #[cfg(feature = "pause")] - return Ok(self.check_interrupt_or_continue()); - #[cfg(not(feature = "pause"))] - Ok(ThreadResult::Continue(self)) + Ok(self.check_interrupt_or_continue(&mut store.threads)) } } diff --git a/crates/interpreter/test.sh b/crates/interpreter/test.sh index ca256d4c3..6c75ee1ef 100755 --- a/crates/interpreter/test.sh +++ b/crates/interpreter/test.sh @@ -22,8 +22,6 @@ ensure_submodule third_party/WebAssembly/spec list_files() { find ../../third_party/WebAssembly/spec/test/core \ -maxdepth 1 -name '*.wast' -execdir basename -s .wast {} \; - find ../../third_party/WebAssembly/pause/test/core \ - -maxdepth 1 -name '*.wast' -execdir basename -s .wast {} \; } list_tests() { sed -n 's/^test!(.*, "\([^"]*\)".*);$/\1/p;s/^test!(\([^,]*\).*);$/\1/p;s/^test!("[^"]*",[^,]+,"\([^"]*\)");$/\1/p' tests/spec.rs | sort @@ -41,4 +39,6 @@ RUSTFLAGS=--cfg=portable_atomic_unsafe_assume_single_core \ cargo check --lib --target=riscv32imc-unknown-none-elf cargo check --example=hello # Run with `-- --test-threads=1 --nocapture` to see unsupported tests. -cargo test --test=spec --features=debug,toctou,float-types,vector-types,pause +cargo test --test=spec --features=debug,toctou,float-types,vector-types +cargo test --test=spec --features=debug,toctou,float-types,vector-types,interrupt +cargo test --test=interrupt --all-features \ No newline at end of file diff --git a/crates/interpreter/tests/infinite_loop.wasm b/crates/interpreter/tests/infinite_loop.wasm new file mode 100644 index 0000000000000000000000000000000000000000..e8f9e8439e02229a4194fa8c689026e349060c7b GIT binary patch literal 111 zcmWl|!3u&v6a~+@y-OmlZ@a#0@6~` zx>Zm6t{*^!tdwBWOPy1;@)Nkl>@`_QTPooB#w%sl*dbtGphS(H9`}}>1LwdCoclij DIieA< literal 0 HcmV?d00001 diff --git a/crates/interpreter/tests/infinite_loop.wat b/crates/interpreter/tests/infinite_loop.wat new file mode 100644 index 000000000..9f446f42e --- /dev/null +++ b/crates/interpreter/tests/infinite_loop.wat @@ -0,0 +1,21 @@ +;; Use `wat2wasm infinite_loop.wat` to regenerate `.wasm`. +(module + (import "env" "count" (func $count (result i32))) + + (memory (export "memory") 1) + (func (export "loopforever") + (local i32 i32) + (loop + (local.set 0 (call $count)) + (local.set 1 (i32.const 1)) + (block + (loop + (br_if 1 (i32.gt_u (local.get 1) (local.get 0))) + (local.set 1 (i32.add (local.get 1) (i32.const 1))) + (br 0) + ) + ) + (br 0) + ) + ) +) \ No newline at end of file diff --git a/crates/interpreter/tests/interrupt.rs b/crates/interpreter/tests/interrupt.rs new file mode 100644 index 000000000..533bd2c0f --- /dev/null +++ b/crates/interpreter/tests/interrupt.rs @@ -0,0 +1,70 @@ +#![allow(unused_crate_dependencies)] +use core::time; +use std::sync::atomic::Ordering::SeqCst; +use std::thread; + +use portable_atomic::{AtomicBool, AtomicI32}; +use wasefire_interpreter::*; + +#[test] +fn test_interrupt() { + let n_interrupts = AtomicI32::new(0); + let n_loops = AtomicI32::new(0); + let interrupt = AtomicBool::new(false); + + std::thread::scope(|s: &std::thread::Scope<'_, '_>| { + s.spawn(|| { + // Create an empty store. + let mut store = Store::default(); + + store.link_func("env", "count", 0, 1).unwrap(); + + const WASM: &[u8] = include_bytes!("infinite_loop.wasm"); + let module = Module::new(WASM).unwrap(); + + // Allocate memory for the module. The module needs one 64kB page, but we know it + // doesn't use more than 5 bytes. The interpreter supports smaller memory + // and traps the module if it accesses outside the actual memory size. This + // behavior is not compliant but necessary when the host does neither have + // enough memory nor virtual memory. + let mut memory = [0; 16]; + + // Instantiate the module in the store. + let inst = store.instantiate(module, &mut memory).unwrap(); + + store.set_interrupt(Some(&interrupt)); + let mut result = store.invoke(inst, "loopforever", vec![]).unwrap(); + + // Let the outer infinite loop do 10 iterations. + while n_loops.load(SeqCst) <= 10 { + let call = match result { + RunResult::Host(call) => call, + RunResult::Interrupt(call) => call, + RunResult::Done(_) => unreachable!(), + }; + + if n_loops.load(SeqCst) > 10 { + break; + } + + if call.is_interrupt() { + n_interrupts.add(1, SeqCst); + result = call.resume(&[]).unwrap(); + } else { + // This is the count() function called in the loop header. + assert!(call.index() == 0); + n_loops.add(1, SeqCst); + // Interrupt. + s.spawn(|| { + thread::sleep(time::Duration::from_millis(1)); + interrupt.store(true, SeqCst); + }); + result = call.resume(&[Val::I32(1000)]).unwrap(); + } + } + }); + + thread::sleep(time::Duration::from_millis(100)); + assert!(n_loops.load(SeqCst) > 9 && n_interrupts.load(SeqCst) > 9); + }); +} diff --git a/crates/interpreter/tests/spec.rs b/crates/interpreter/tests/spec.rs index 547f7e10b..2dd26fde6 100644 --- a/crates/interpreter/tests/spec.rs +++ b/crates/interpreter/tests/spec.rs @@ -18,8 +18,6 @@ use std::collections::HashMap; use lazy_static::lazy_static; -#[cfg(feature = "pause")] -use portable_atomic::AtomicBool; use wasefire_interpreter::*; use wast::core::{AbstractHeapType, WastArgCore, WastRetCore}; use wast::lexer::Lexer; @@ -35,68 +33,32 @@ fn test(repo: &str, name: &str, skip: usize) { let wast: Wast = parser::parse(&buffer).unwrap(); let layout = std::alloc::Layout::from_size_align(pool_size(name), MEMORY_ALIGN).unwrap(); let pool = unsafe { std::slice::from_raw_parts_mut(std::alloc::alloc(layout), layout.size()) }; - #[cfg(feature = "pause")] - let interrupt = AtomicBool::new(false); - std::thread::scope(|s| { - #[cfg(feature = "pause")] - s.spawn(|| { - if name == "infinite_loop" { - interrupt.store(true, std::sync::atomic::Ordering::SeqCst); + let mut env = Env::new(pool); + env.instantiate("spectest", &SPECTEST); + env.register_name("spectest", None); + assert!(matches!(env.inst, Sup::Yes(_))); + for directive in wast.directives { + eprintln!("{name}:{}", directive.span().offset()); + match directive { + WastDirective::Module(QuoteWat::Wat(Wat::Module(mut m))) => { + env.instantiate(name, &m.encode().unwrap()); + env.register_id(m.id, env.inst); } - }); - let mut env = Env::new( - pool, - #[cfg(feature = "pause")] - &interrupt, - ); - env.instantiate( - "spectest", - &SPECTEST, - #[cfg(feature = "pause")] - Some(&interrupt), - ); - env.register_name("spectest", None); - assert!(matches!(env.inst, Sup::Yes(_))); - for directive in wast.directives { - eprintln!("{name}:{}", directive.span().offset()); - match directive { - WastDirective::Module(QuoteWat::Wat(Wat::Module(mut m))) => { - env.instantiate( - name, - &m.encode().unwrap(), - #[cfg(feature = "pause")] - Some(&interrupt), - ); - env.register_id(m.id, env.inst); - } - WastDirective::Module(mut wat) => env.instantiate( - name, - &wat.encode().unwrap(), - #[cfg(feature = "pause")] - Some(&interrupt), - ), - WastDirective::AssertMalformed { module, .. } => assert_malformed(&mut env, module), - WastDirective::AssertInvalid { module, .. } => assert_invalid(&mut env, module), - WastDirective::AssertReturn { exec, results, .. } => { - assert_return(&mut env, exec, results) - } - WastDirective::AssertTrap { exec, .. } => assert_trap( - &mut env, - exec, - #[cfg(feature = "pause")] - Some(&interrupt), - ), - WastDirective::Invoke(invoke) => assert_invoke(&mut env, invoke), - WastDirective::AssertExhaustion { call, .. } => assert_exhaustion(&mut env, call), - WastDirective::Register { name, module, .. } => env.register_name(name, module), - WastDirective::AssertUnlinkable { module, .. } => { - assert_unlinkable(&mut env, module) - } - _ => unimplemented!("{:?}", directive), + WastDirective::Module(mut wat) => env.instantiate(name, &wat.encode().unwrap()), + WastDirective::AssertMalformed { module, .. } => assert_malformed(&mut env, module), + WastDirective::AssertInvalid { module, .. } => assert_invalid(&mut env, module), + WastDirective::AssertReturn { exec, results, .. } => { + assert_return(&mut env, exec, results) } + WastDirective::AssertTrap { exec, .. } => assert_trap(&mut env, exec), + WastDirective::Invoke(invoke) => assert_invoke(&mut env, invoke), + WastDirective::AssertExhaustion { call, .. } => assert_exhaustion(&mut env, call), + WastDirective::Register { name, module, .. } => env.register_name(name, module), + WastDirective::AssertUnlinkable { module, .. } => assert_unlinkable(&mut env, module), + _ => unimplemented!("{:?}", directive), } - assert_eq!(env.skip, skip); - }); + } + assert_eq!(env.skip, skip); } fn pool_size(name: &str) -> usize { @@ -182,22 +144,12 @@ struct Env<'m> { store: Store<'m>, inst: Sup, map: HashMap, Sup>, - #[cfg(feature = "pause")] - interrupt: &'m AtomicBool, skip: usize, } impl<'m> Env<'m> { - fn new(pool: &'m mut [u8], #[cfg(feature = "pause")] interrupt: &'m AtomicBool) -> Self { - Env { - pool, - store: Store::default(), - inst: Sup::Uninit, - map: HashMap::new(), - #[cfg(feature = "pause")] - interrupt, - skip: 0, - } + fn new(pool: &'m mut [u8]) -> Self { + Env { pool, store: Store::default(), inst: Sup::Uninit, map: HashMap::new(), skip: 0 } } fn alloc(&mut self, size: usize) -> &'m mut [u8] { @@ -211,10 +163,7 @@ impl<'m> Env<'m> { &mut result[.. size] } - fn maybe_instantiate( - &mut self, name: &str, wasm: &[u8], - #[cfg(feature = "pause")] interrupt: Option<&'m AtomicBool>, - ) -> Result { + fn maybe_instantiate(&mut self, name: &str, wasm: &[u8]) -> Result { let module = self.alloc(wasm.len()); module.copy_from_slice(wasm); let module = match Module::new(module) { @@ -222,40 +171,21 @@ impl<'m> Env<'m> { Err(e) => return Err(e), }; let memory = self.alloc(mem_size(name)); - self.store.instantiate( - module, - memory, - #[cfg(feature = "pause")] - interrupt, - ) + self.store.instantiate(module, memory) } - fn instantiate( - &mut self, name: &str, wasm: &[u8], - #[cfg(feature = "pause")] interrupt: Option<&'m AtomicBool>, - ) { - let inst = self.maybe_instantiate( - name, - wasm, - #[cfg(feature = "pause")] - interrupt, - ); + fn instantiate(&mut self, name: &str, wasm: &[u8]) { + let inst = self.maybe_instantiate(name, wasm); self.inst = Sup::conv(inst).unwrap(); } fn invoke(&mut self, inst_id: InstId, name: &str, args: Vec) -> Result, Error> { - match self.store.invoke( - inst_id, - name, - args, - #[cfg(feature = "pause")] - self.interrupt, - )? { - RunResult::Done(x) => Ok(x), - #[cfg(feature = "pause")] - RunResult::Interrupt() => Ok(vec![Val::I64(1111)]), + Ok(match self.store.invoke(inst_id, name, args)? { + RunResult::Done(x) => x, RunResult::Host { .. } => unreachable!(), - } + #[cfg(feature = "interrupt")] + RunResult::Interrupt { .. } => unreachable!(), + }) } fn register_name(&mut self, name: &'m str, module: Option>) { @@ -365,16 +295,7 @@ fn spectest() -> Vec { } fn assert_return(env: &mut Env, exec: WastExecute, expected: Vec) { - let actual = only_sup!( - env, - wast_execute( - env, - exec, - #[cfg(feature = "pause")] - None - ) - ) - .unwrap(); + let actual = only_sup!(env, wast_execute(env, exec)).unwrap(); assert_eq!(actual.len(), expected.len()); for (actual, expected) in actual.into_iter().zip(expected.into_iter()) { use wast::core::HeapType; @@ -411,22 +332,8 @@ fn assert_return(env: &mut Env, exec: WastExecute, expected: Vec) { } } -fn assert_trap<'m>( - env: &mut Env<'m>, exec: WastExecute, - #[cfg(feature = "pause")] interrupt: Option<&'m AtomicBool>, -) { - assert_eq!( - only_sup!( - env, - wast_execute( - env, - exec, - #[cfg(feature = "pause")] - interrupt - ) - ), - Err(Error::Trap) - ); +fn assert_trap(env: &mut Env, exec: WastExecute) { + assert_eq!(only_sup!(env, wast_execute(env, exec)), Err(Error::Trap)); } fn assert_invoke(env: &mut Env, invoke: WastInvoke) { @@ -449,32 +356,16 @@ fn assert_exhaustion(env: &mut Env, call: WastInvoke) { } fn assert_unlinkable(env: &mut Env, mut wat: Wat) { - let inst = only_sup!( - env, - env.maybe_instantiate( - "", - &wat.encode().unwrap(), - #[cfg(feature = "pause")] - None - ) - ); + let inst = only_sup!(env, env.maybe_instantiate("", &wat.encode().unwrap())); assert_eq!(inst.err(), Some(Error::NotFound)); } -fn wast_execute<'m>( - env: &mut Env<'m>, exec: WastExecute, - #[cfg(feature = "pause")] interrupt: Option<&'m AtomicBool>, -) -> Result, Error> { +fn wast_execute(env: &mut Env, exec: WastExecute) -> Result, Error> { match exec { WastExecute::Invoke(invoke) => wast_invoke(env, invoke), - WastExecute::Wat(mut wat) => env - .maybe_instantiate( - "", - &wat.encode().unwrap(), - #[cfg(feature = "pause")] - interrupt, - ) - .map(|_| Vec::new()), + WastExecute::Wat(mut wat) => { + env.maybe_instantiate("", &wat.encode().unwrap()).map(|_| Vec::new()) + } WastExecute::Get { module, global, .. } => { let inst_id = env.inst_id(module).res()?; env.store.get_global(inst_id, global).map(|x| vec![x]) @@ -547,6 +438,7 @@ macro_rules! test { (=5 $name:ident) => { stringify!($name) }; (=5 $file:literal) => { $file }; } + test!(address); test!(align); test!(binary); @@ -637,5 +529,3 @@ test!(utf8_custom_section_id, "utf8-custom-section-id"); test!(utf8_import_field, "utf8-import-field"); test!(utf8_import_module, "utf8-import-module"); test!(utf8_invalid_encoding, "utf8-invalid-encoding"); -#[cfg(feature = "pause")] -test!("pause", pause, "infinite_loop"); diff --git a/third_party/WebAssembly/pause/test/core/infinite_loop.wast b/third_party/WebAssembly/pause/test/core/infinite_loop.wast deleted file mode 100644 index 8f746c5ed..000000000 --- a/third_party/WebAssembly/pause/test/core/infinite_loop.wast +++ /dev/null @@ -1,27 +0,0 @@ -(module - (func (export "loopforever") - (loop - (br 0) - ) - ) - - (func $recurseforever - call $recurseforever - ) - (export "recurseforever" (func $recurseforever)) -) - -(assert_return (invoke "recurseforever") (i64.const 1111)) -(assert_return (invoke "loopforever") (i64.const 1111)) - -(assert_trap - (module - (func $loopforever - (loop - (br 0) - ) - ) - - (start $loopforever) - ) -"interrupt") \ No newline at end of file From 08a9d411611b80c61a4a95d967cee0d742014d4c Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 29 Nov 2024 15:22:46 +0000 Subject: [PATCH 3/4] Remove obsolete comment. --- crates/interpreter/tests/interrupt.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/crates/interpreter/tests/interrupt.rs b/crates/interpreter/tests/interrupt.rs index 533bd2c0f..b87215569 100644 --- a/crates/interpreter/tests/interrupt.rs +++ b/crates/interpreter/tests/interrupt.rs @@ -21,12 +21,6 @@ fn test_interrupt() { const WASM: &[u8] = include_bytes!("infinite_loop.wasm"); let module = Module::new(WASM).unwrap(); - - // Allocate memory for the module. The module needs one 64kB page, but we know it - // doesn't use more than 5 bytes. The interpreter supports smaller memory - // and traps the module if it accesses outside the actual memory size. This - // behavior is not compliant but necessary when the host does neither have - // enough memory nor virtual memory. let mut memory = [0; 16]; // Instantiate the module in the store. From f986ed4b6095e4e8c03861a31a0ed2038899185e Mon Sep 17 00:00:00 2001 From: Dan Date: Mon, 2 Dec 2024 20:07:45 +0000 Subject: [PATCH 4/4] Reworked interrupt.rs --- crates/interpreter/src/exec.rs | 30 ++---- crates/interpreter/test.sh | 2 +- crates/interpreter/tests/infinite_loop.wasm | Bin 111 -> 0 bytes crates/interpreter/tests/infinite_loop.wat | 21 ---- crates/interpreter/tests/interrupt.rs | 109 ++++++++++++-------- scripts/log.sh | 2 + 6 files changed, 78 insertions(+), 86 deletions(-) delete mode 100644 crates/interpreter/tests/infinite_loop.wasm delete mode 100644 crates/interpreter/tests/infinite_loop.wat diff --git a/crates/interpreter/src/exec.rs b/crates/interpreter/src/exec.rs index c4ecc5c1e..00e6177ac 100644 --- a/crates/interpreter/src/exec.rs +++ b/crates/interpreter/src/exec.rs @@ -63,7 +63,6 @@ pub struct Store<'m> { // functions in `funcs` is stored to limit normal linking to that part. func_default: Option<(&'m str, usize)>, threads: Vec>, - #[cfg(feature = "interrupt")] interrupt: Option<&'m AtomicBool>, } @@ -209,24 +208,14 @@ impl<'m> Store<'m> { parser, Frame::new(inst_id, 0, &[], locals), #[cfg(feature = "interrupt")] - self.interrupt, + None, ); - // Disable interrupts for the start section. - #[cfg(feature = "interrupt")] - let interrupt = self.interrupt; - #[cfg(feature = "interrupt")] - self.set_interrupt(None); - let result = thread.run(self)?; - assert!(matches!(result, RunResult::Done(x) if x.is_empty())); - - #[cfg(feature = "interrupt")] - self.set_interrupt(interrupt); + assert!(matches!(result, RunResult::Done(x) if x.is_empty())) } Ok(InstId { store_id: self.id, inst_id }) } - /// Invokes a function in an instance provided its name. /// /// If a function was already running, it will resume once the function being called terminates. @@ -515,7 +504,7 @@ pub enum RunResult<'a, 'm> { /// Execution is calling into the host. Host(Call<'a, 'm>), - // Execution was interrupted by the host. + /// Execution was interrupted by the host. #[cfg(feature = "interrupt")] Interrupt(Call<'a, 'm>), } @@ -800,8 +789,9 @@ impl<'m> Thread<'m> { parser, Frame::new(inst_id, 1, &[], Vec::new()), #[cfg(feature = "interrupt")] - store.interrupt, + None, ); + let (parser, results) = loop { let p = thread.parser.save(); match thread.step(store).unwrap() { @@ -1108,11 +1098,9 @@ impl<'m> Thread<'m> { } #[allow(clippy::ptr_arg)] - fn check_interrupt_or_continue(self, _threads: &mut Vec>) -> ThreadResult<'m> { + fn unbounded_continue(self, _threads: &mut Vec>) -> ThreadResult<'m> { #[cfg(feature = "interrupt")] - if self.interrupt.is_some_and(|interrupt| { - interrupt.compare_exchange_weak(true, false, Relaxed, Relaxed).is_ok() - }) { + if self.interrupt.is_some_and(|interrupt| interrupt.swap(false, Relaxed)) { _threads.push(Continuation { thread: self, index: 0, @@ -1144,7 +1132,7 @@ impl<'m> Thread<'m> { unsafe { self.parser.restore(pos); } - Ok(self.check_interrupt_or_continue(threads)) + Ok(self.unbounded_continue(threads)) } LabelKind::Block | LabelKind::If => { self.skip_to_end(inst, l); @@ -1475,7 +1463,7 @@ impl<'m> Thread<'m> { let ret = self.parser.save(); self.parser = parser; self.frames.push(Frame::new(inst_id, t.results.len(), ret, locals)); - Ok(self.check_interrupt_or_continue(&mut store.threads)) + Ok(self.unbounded_continue(&mut store.threads)) } } diff --git a/crates/interpreter/test.sh b/crates/interpreter/test.sh index 6c75ee1ef..17da2a0ac 100755 --- a/crates/interpreter/test.sh +++ b/crates/interpreter/test.sh @@ -24,7 +24,7 @@ list_files() { -maxdepth 1 -name '*.wast' -execdir basename -s .wast {} \; } list_tests() { - sed -n 's/^test!(.*, "\([^"]*\)".*);$/\1/p;s/^test!(\([^,]*\).*);$/\1/p;s/^test!("[^"]*",[^,]+,"\([^"]*\)");$/\1/p' tests/spec.rs | sort + sed -n 's/^test!(.*, "\([^"]*\)".*);$/\1/p;s/^test!(\([^,]*\).*);$/\1/p' tests/spec.rs } diff_sorted tests/spec.rs "$(list_files | sort)" $(list_tests) diff --git a/crates/interpreter/tests/infinite_loop.wasm b/crates/interpreter/tests/infinite_loop.wasm deleted file mode 100644 index e8f9e8439e02229a4194fa8c689026e349060c7b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 111 zcmWl|!3u&v6a~+@y-OmlZ@a#0@6~` zx>Zm6t{*^!tdwBWOPy1;@)Nkl>@`_QTPooB#w%sl*dbtGphS(H9`}}>1LwdCoclij DIieA< diff --git a/crates/interpreter/tests/infinite_loop.wat b/crates/interpreter/tests/infinite_loop.wat deleted file mode 100644 index 9f446f42e..000000000 --- a/crates/interpreter/tests/infinite_loop.wat +++ /dev/null @@ -1,21 +0,0 @@ -;; Use `wat2wasm infinite_loop.wat` to regenerate `.wasm`. -(module - (import "env" "count" (func $count (result i32))) - - (memory (export "memory") 1) - (func (export "loopforever") - (local i32 i32) - (loop - (local.set 0 (call $count)) - (local.set 1 (i32.const 1)) - (block - (loop - (br_if 1 (i32.gt_u (local.get 1) (local.get 0))) - (local.set 1 (i32.add (local.get 1) (i32.const 1))) - (br 0) - ) - ) - (br 0) - ) - ) -) \ No newline at end of file diff --git a/crates/interpreter/tests/interrupt.rs b/crates/interpreter/tests/interrupt.rs index b87215569..89e33c7c0 100644 --- a/crates/interpreter/tests/interrupt.rs +++ b/crates/interpreter/tests/interrupt.rs @@ -1,64 +1,87 @@ #![allow(unused_crate_dependencies)] use core::time; -use std::sync::atomic::Ordering::SeqCst; +use std::sync::atomic::Ordering::Relaxed; use std::thread; -use portable_atomic::{AtomicBool, AtomicI32}; +use portable_atomic::AtomicBool; use wasefire_interpreter::*; #[test] fn test_interrupt() { - let n_interrupts = AtomicI32::new(0); - let n_loops = AtomicI32::new(0); + let mut n_interrupts = 0; + let mut n_loops = 0; let interrupt = AtomicBool::new(false); std::thread::scope(|s: &std::thread::Scope<'_, '_>| { - s.spawn(|| { - // Create an empty store. - let mut store = Store::default(); + // Create an empty store. + let mut store = Store::default(); - store.link_func("env", "count", 0, 1).unwrap(); + store.link_func("env", "count", 0, 1).unwrap(); - const WASM: &[u8] = include_bytes!("infinite_loop.wasm"); - let module = Module::new(WASM).unwrap(); - let mut memory = [0; 16]; + // ;; Use `wat2wasm infinite_loop.wat` to regenerate `.wasm`. + // (module + // (import "env" "count" (func $count (result i32))) - // Instantiate the module in the store. - let inst = store.instantiate(module, &mut memory).unwrap(); + // (memory (export "memory") 1) + // (func (export "loopforever") + // (local i32 i32) + // (loop + // (local.set 0 (call $count)) + // (local.set 1 (i32.const 1)) + // (block + // (loop + // (br_if 1 (i32.gt_u (local.get 1) (local.get 0))) + // (local.set 1 (i32.add (local.get 1) (i32.const 1))) + // (br 0) + // ) + // ) + // (br 0) + // ) + // ) + // ) - store.set_interrupt(Some(&interrupt)); - let mut result = store.invoke(inst, "loopforever", vec![]).unwrap(); + const WASM: &[u8] = &[ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x60, 0x00, 0x01, + 0x7f, 0x60, 0x00, 0x00, 0x02, 0x0d, 0x01, 0x03, 0x65, 0x6e, 0x76, 0x05, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x00, 0x00, 0x03, 0x02, 0x01, 0x01, 0x05, 0x03, 0x01, 0x00, 0x01, + 0x07, 0x18, 0x02, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00, 0x0b, 0x6c, + 0x6f, 0x6f, 0x70, 0x66, 0x6f, 0x72, 0x65, 0x76, 0x65, 0x72, 0x00, 0x01, 0x0a, 0x29, + 0x01, 0x27, 0x01, 0x02, 0x7f, 0x03, 0x40, 0x10, 0x00, 0x21, 0x00, 0x41, 0x01, 0x21, + 0x01, 0x02, 0x40, 0x03, 0x40, 0x20, 0x01, 0x20, 0x00, 0x4b, 0x0d, 0x01, 0x20, 0x01, + 0x41, 0x01, 0x6a, 0x21, 0x01, 0x0c, 0x00, 0x0b, 0x0b, 0x0c, 0x00, 0x0b, 0x0b, + ]; + let module = Module::new(WASM).unwrap(); + let mut memory = [0; 16]; - // Let the outer infinite loop do 10 iterations. - while n_loops.load(SeqCst) <= 10 { - let call = match result { - RunResult::Host(call) => call, - RunResult::Interrupt(call) => call, - RunResult::Done(_) => unreachable!(), - }; + // Instantiate the module in the store. + let inst = store.instantiate(module, &mut memory).unwrap(); - if n_loops.load(SeqCst) > 10 { - break; - } + store.set_interrupt(Some(&interrupt)); + let mut result = store.invoke(inst, "loopforever", vec![]).unwrap(); - if call.is_interrupt() { - n_interrupts.add(1, SeqCst); - result = call.resume(&[]).unwrap(); - } else { - // This is the count() function called in the loop header. - assert!(call.index() == 0); - n_loops.add(1, SeqCst); - // Interrupt. - s.spawn(|| { - thread::sleep(time::Duration::from_millis(1)); - interrupt.store(true, SeqCst); - }); - result = call.resume(&[Val::I32(1000)]).unwrap(); - } - } - }); + // Let the outer infinite loop do 10 iterations. + while n_loops <= 10 { + let call = match result { + RunResult::Host(call) => call, + RunResult::Interrupt(call) => call, + RunResult::Done(_) => unreachable!(), + }; - thread::sleep(time::Duration::from_millis(100)); - assert!(n_loops.load(SeqCst) > 9 && n_interrupts.load(SeqCst) > 9); + if call.is_interrupt() { + n_interrupts += 1; + result = call.resume(&[]).unwrap(); + } else { + // This is the count() function called in the loop header. + assert!(call.index() == 0); + n_loops += 1; + // Interrupt. + s.spawn(|| { + thread::sleep(time::Duration::from_millis(1)); + interrupt.store(true, Relaxed); + }); + result = call.resume(&[Val::I32(1000)]).unwrap(); + } + } }); + assert!(n_interrupts > 9); } diff --git a/scripts/log.sh b/scripts/log.sh index 25c5f7ab4..d2376669f 100644 --- a/scripts/log.sh +++ b/scripts/log.sh @@ -20,6 +20,8 @@ t() { _log '1;33' Todo "$*"; } d() { _log '1;32' Done "$*"; exit 0; } e() { _log '1;31' Error "$*"; exit 1; } +export LC_COLLATE=C + # We put the escape character in a variable because bash doesn't interpret escaped characters and # some scripts use bash instead of sh. _LOG=$(printf '\e')