diff --git a/crates/fuzzing/src/oracles.rs b/crates/fuzzing/src/oracles.rs index d154904204b0..11910cc0bc97 100644 --- a/crates/fuzzing/src/oracles.rs +++ b/crates/fuzzing/src/oracles.rs @@ -23,9 +23,7 @@ use self::engine::{DiffEngine, DiffInstance}; use crate::generators::{self, DiffValue, DiffValueType}; use arbitrary::Arbitrary; pub use stacks::check_stacks; -use std::cell::Cell; -use std::rc::Rc; -use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}; use std::sync::{Arc, Condvar, Mutex}; use std::time::{Duration, Instant}; use wasmtime::*; @@ -62,35 +60,36 @@ pub fn log_wasm(wasm: &[u8]) { /// The `T` in `Store` for fuzzing stores, used to limit resource /// consumption during fuzzing. #[derive(Clone)] -pub struct StoreLimits(Rc); +pub struct StoreLimits(Arc); struct LimitsState { /// Remaining memory, in bytes, left to allocate - remaining_memory: Cell, + remaining_memory: AtomicUsize, /// Whether or not an allocation request has been denied - oom: Cell, + oom: AtomicBool, } impl StoreLimits { /// Creates the default set of limits for all fuzzing stores. pub fn new() -> StoreLimits { - StoreLimits(Rc::new(LimitsState { + StoreLimits(Arc::new(LimitsState { // Limits tables/memories within a store to at most 1gb for now to // exercise some larger address but not overflow various limits. - remaining_memory: Cell::new(1 << 30), - oom: Cell::new(false), + remaining_memory: AtomicUsize::new(1 << 30), + oom: AtomicBool::new(false), })) } fn alloc(&mut self, amt: usize) -> bool { log::trace!("alloc {amt:#x} bytes"); - match self.0.remaining_memory.get().checked_sub(amt) { - Some(mem) => { - self.0.remaining_memory.set(mem); - true - } - None => { - self.0.oom.set(true); + match self + .0 + .remaining_memory + .fetch_update(SeqCst, SeqCst, |remaining| remaining.checked_sub(amt)) + { + Ok(_) => true, + Err(_) => { + self.0.oom.store(true, SeqCst); log::debug!("OOM hit"); false } @@ -98,7 +97,7 @@ impl StoreLimits { } fn is_oom(&self) -> bool { - self.0.oom.get() + self.0.oom.load(SeqCst) } } diff --git a/crates/wast/src/wast.rs b/crates/wast/src/wast.rs index defb6dd08e11..559ac32da937 100644 --- a/crates/wast/src/wast.rs +++ b/crates/wast/src/wast.rs @@ -3,8 +3,10 @@ use crate::component; use crate::core; use crate::spectest::*; use anyhow::{anyhow, bail, Context as _, Error, Result}; +use std::collections::HashMap; use std::path::Path; use std::str; +use std::thread; use wasmtime::*; use wast::lexer::Lexer; use wast::parser::{self, ParseBuffer}; @@ -62,7 +64,10 @@ enum Export { Component(component::Func), } -impl WastContext { +impl WastContext +where + T: Clone + Send + 'static, +{ /// Construct a new instance of `WastContext`. pub fn new(store: Store) -> Self { // Spec tests will redefine the same module/name sometimes, so we need @@ -361,26 +366,54 @@ impl WastContext { let buf = ParseBuffer::new_with_lexer(lexer).map_err(adjust_wast)?; let ast = parser::parse::(&buf).map_err(adjust_wast)?; - for directive in ast.directives { - let sp = directive.span(); - if log::log_enabled!(log::Level::Debug) { - let (line, col) = sp.linecol_in(wast); - log::debug!("running directive on {}:{}:{}", filename, line + 1, col); - } - self.run_directive(directive) - .map_err(|e| match e.downcast() { - Ok(err) => adjust_wast(err).into(), - Err(e) => e, - }) - .with_context(|| { + self.run_directives(ast.directives, filename, wast) + } + + fn run_directives( + &mut self, + directives: Vec>, + filename: &str, + wast: &str, + ) -> Result<()> { + let adjust_wast = |mut err: wast::Error| { + err.set_path(filename.as_ref()); + err.set_text(wast); + err + }; + + thread::scope(|scope| { + let mut threads = HashMap::new(); + for directive in directives { + let sp = directive.span(); + if log::log_enabled!(log::Level::Debug) { let (line, col) = sp.linecol_in(wast); - format!("failed directive on {}:{}:{}", filename, line + 1, col) - })?; - } - Ok(()) + log::debug!("running directive on {}:{}:{}", filename, line + 1, col); + } + self.run_directive(directive, filename, wast, &scope, &mut threads) + .map_err(|e| match e.downcast() { + Ok(err) => adjust_wast(err).into(), + Err(e) => e, + }) + .with_context(|| { + let (line, col) = sp.linecol_in(wast); + format!("failed directive on {}:{}:{}", filename, line + 1, col) + })?; + } + Ok(()) + }) } - fn run_directive(&mut self, directive: WastDirective) -> Result<()> { + fn run_directive<'a>( + &mut self, + directive: WastDirective<'a>, + filename: &'a str, + wast: &'a str, + scope: &'a thread::Scope<'a, '_>, + threads: &mut HashMap<&'a str, thread::ScopedJoinHandle<'a, Result<()>>>, + ) -> Result<()> + where + T: 'a, + { use wast::WastDirective::*; match directive { @@ -466,9 +499,39 @@ impl WastContext { } AssertException { .. } => bail!("unimplemented assert_exception"), - Thread(_) => bail!("unimplemented `thread`"), + Thread(thread) => { + let mut core_linker = Linker::new(self.store.engine()); + if let Some(id) = thread.shared_module { + let items = self + .core_linker + .iter(&mut self.store) + .filter(|(module, _, _)| *module == id.name()) + .collect::>(); + for (module, name, item) in items { + core_linker.define(&mut self.store, module, name, item)?; + } + } + let mut child_cx = WastContext { + current: None, + core_linker, + #[cfg(feature = "component-model")] + component_linker: component::Linker::new(self.store.engine()), + store: Store::new(self.store.engine(), self.store.data().clone()), + }; + let name = thread.name.name(); + let child = + scope.spawn(move || child_cx.run_directives(thread.directives, filename, wast)); + threads.insert(name, child); + } - Wait { .. } => bail!("unimplemented `wait`"), + Wait { thread, .. } => { + let name = thread.name(); + threads + .remove(name) + .ok_or_else(|| anyhow!("no thread named `{name}`"))? + .join() + .unwrap()?; + } } Ok(()) diff --git a/tests/misc_testsuite/threads/LB.wast b/tests/misc_testsuite/threads/LB.wast new file mode 100644 index 000000000000..b92eb4488030 --- /dev/null +++ b/tests/misc_testsuite/threads/LB.wast @@ -0,0 +1,62 @@ +(module $Mem + (memory (export "shared") 1 1 shared) +) + +(thread $T1 (shared (module $Mem)) + (register "mem" $Mem) + (module + (memory (import "mem" "shared") 1 10 shared) + (func (export "run") + (local i32) + (i32.load (i32.const 4)) + (local.set 0) + (i32.store (i32.const 0) (i32.const 1)) + + ;; store results for checking + (i32.store (i32.const 24) (local.get 0)) + ) + ) + (invoke "run") +) + +(thread $T2 (shared (module $Mem)) + (register "mem" $Mem) + (module + (memory (import "mem" "shared") 1 1 shared) + (func (export "run") + (local i32) + (i32.load (i32.const 0)) + (local.set 0) + (i32.store (i32.const 4) (i32.const 1)) + + ;; store results for checking + (i32.store (i32.const 32) (local.get 0)) + ) + ) + + (invoke "run") +) + +(wait $T1) +(wait $T2) + +(module $Check + (memory (import "Mem" "shared") 1 1 shared) + + (func (export "check") (result i32) + (local i32 i32) + (i32.load (i32.const 24)) + (local.set 0) + (i32.load (i32.const 32)) + (local.set 1) + + ;; allowed results: (L_0 = 0 || L_0 = 1) && (L_1 = 0 || L_1 = 1) + + (i32.or (i32.eq (local.get 0) (i32.const 1)) (i32.eq (local.get 0) (i32.const 0))) + (i32.or (i32.eq (local.get 1) (i32.const 1)) (i32.eq (local.get 1) (i32.const 0))) + (i32.and) + (return) + ) +) + +(assert_return (invoke $Check "check") (i32.const 1)) diff --git a/tests/misc_testsuite/threads/LB_atomic.wast b/tests/misc_testsuite/threads/LB_atomic.wast new file mode 100644 index 000000000000..c13f948a12be --- /dev/null +++ b/tests/misc_testsuite/threads/LB_atomic.wast @@ -0,0 +1,64 @@ +(module $Mem + (memory (export "shared") 1 1 shared) +) + +(thread $T1 (shared (module $Mem)) + (register "mem" $Mem) + (module + (memory (import "mem" "shared") 1 10 shared) + (func (export "run") + (local i32) + (i32.atomic.load (i32.const 4)) + (local.set 0) + (i32.atomic.store (i32.const 0) (i32.const 1)) + + ;; store results for checking + (i32.store (i32.const 24) (local.get 0)) + ) + ) + (invoke "run") +) + +(thread $T2 (shared (module $Mem)) + (register "mem" $Mem) + (module + (memory (import "mem" "shared") 1 1 shared) + (func (export "run") + (local i32) + (i32.atomic.load (i32.const 0)) + (local.set 0) + (i32.atomic.store (i32.const 4) (i32.const 1)) + + ;; store results for checking + (i32.store (i32.const 32) (local.get 0)) + ) + ) + + (invoke "run") +) + +(wait $T1) +(wait $T2) + +(module $Check + (memory (import "Mem" "shared") 1 1 shared) + + (func (export "check") (result i32) + (local i32 i32) + (i32.load (i32.const 24)) + (local.set 0) + (i32.load (i32.const 32)) + (local.set 1) + + ;; allowed results: (L_0 = 0 && L_1 = 0) || (L_0 = 0 && L_1 = 1) || (L_0 = 1 && L_1 = 0) + + (i32.and (i32.eq (local.get 0) (i32.const 0)) (i32.eq (local.get 1) (i32.const 0))) + (i32.and (i32.eq (local.get 0) (i32.const 0)) (i32.eq (local.get 1) (i32.const 1))) + (i32.and (i32.eq (local.get 0) (i32.const 1)) (i32.eq (local.get 1) (i32.const 0))) + (i32.or) + (i32.or) + (return) + ) +) + +(assert_return (invoke $Check "check") (i32.const 1)) diff --git a/tests/misc_testsuite/threads/MP.wast b/tests/misc_testsuite/threads/MP.wast new file mode 100644 index 000000000000..8ae62a4c2597 --- /dev/null +++ b/tests/misc_testsuite/threads/MP.wast @@ -0,0 +1,59 @@ +(module $Mem + (memory (export "shared") 1 1 shared) +) + +(thread $T1 (shared (module $Mem)) + (register "mem" $Mem) + (module + (memory (import "mem" "shared") 1 10 shared) + (func (export "run") + (i32.store (i32.const 0) (i32.const 42)) + (i32.store (i32.const 4) (i32.const 1)) + ) + ) + (invoke "run") +) + +(thread $T2 (shared (module $Mem)) + (register "mem" $Mem) + (module + (memory (import "mem" "shared") 1 1 shared) + (func (export "run") + (local i32 i32) + (i32.load (i32.const 4)) + (local.set 0) + (i32.load (i32.const 0)) + (local.set 1) + + ;; store results for checking + (i32.store (i32.const 24) (local.get 0)) + (i32.store (i32.const 32) (local.get 1)) + ) + ) + + (invoke "run") +) + +(wait $T1) +(wait $T2) + +(module $Check + (memory (import "Mem" "shared") 1 1 shared) + + (func (export "check") (result i32) + (local i32 i32) + (i32.load (i32.const 24)) + (local.set 0) + (i32.load (i32.const 32)) + (local.set 1) + + ;; allowed results: (L_0 = 0 || L_0 = 1) && (L_1 = 0 || L_1 = 42) + + (i32.or (i32.eq (local.get 0) (i32.const 1)) (i32.eq (local.get 0) (i32.const 0))) + (i32.or (i32.eq (local.get 1) (i32.const 42)) (i32.eq (local.get 1) (i32.const 0))) + (i32.and) + (return) + ) +) + +(assert_return (invoke $Check "check") (i32.const 1)) diff --git a/tests/misc_testsuite/threads/MP_atomic.wast b/tests/misc_testsuite/threads/MP_atomic.wast new file mode 100644 index 000000000000..1af863441b91 --- /dev/null +++ b/tests/misc_testsuite/threads/MP_atomic.wast @@ -0,0 +1,61 @@ +(module $Mem + (memory (export "shared") 1 1 shared) +) + +(thread $T1 (shared (module $Mem)) + (register "mem" $Mem) + (module + (memory (import "mem" "shared") 1 10 shared) + (func (export "run") + (i32.atomic.store (i32.const 0) (i32.const 42)) + (i32.atomic.store (i32.const 4) (i32.const 1)) + ) + ) + (invoke "run") +) + +(thread $T2 (shared (module $Mem)) + (register "mem" $Mem) + (module + (memory (import "mem" "shared") 1 1 shared) + (func (export "run") + (local i32 i32) + (i32.atomic.load (i32.const 4)) + (local.set 0) + (i32.atomic.load (i32.const 0)) + (local.set 1) + + ;; store results for checking + (i32.store (i32.const 24) (local.get 0)) + (i32.store (i32.const 32) (local.get 1)) + ) + ) + + (invoke "run") +) + +(wait $T1) +(wait $T2) + +(module $Check + (memory (import "Mem" "shared") 1 1 shared) + + (func (export "check") (result i32) + (local i32 i32) + (i32.load (i32.const 24)) + (local.set 0) + (i32.load (i32.const 32)) + (local.set 1) + + ;; allowed results: (L_0 = 1 && L_1 = 42) || (L_0 = 0 && L_1 = 0) || (L_0 = 0 && L_1 = 42) + + (i32.and (i32.eq (local.get 0) (i32.const 1)) (i32.eq (local.get 1) (i32.const 42))) + (i32.and (i32.eq (local.get 0) (i32.const 0)) (i32.eq (local.get 1) (i32.const 0))) + (i32.and (i32.eq (local.get 0) (i32.const 0)) (i32.eq (local.get 1) (i32.const 42))) + (i32.or) + (i32.or) + (return) + ) +) + +(assert_return (invoke $Check "check") (i32.const 1)) diff --git a/tests/misc_testsuite/threads/MP_wait.wast b/tests/misc_testsuite/threads/MP_wait.wast new file mode 100644 index 000000000000..1af863441b91 --- /dev/null +++ b/tests/misc_testsuite/threads/MP_wait.wast @@ -0,0 +1,61 @@ +(module $Mem + (memory (export "shared") 1 1 shared) +) + +(thread $T1 (shared (module $Mem)) + (register "mem" $Mem) + (module + (memory (import "mem" "shared") 1 10 shared) + (func (export "run") + (i32.atomic.store (i32.const 0) (i32.const 42)) + (i32.atomic.store (i32.const 4) (i32.const 1)) + ) + ) + (invoke "run") +) + +(thread $T2 (shared (module $Mem)) + (register "mem" $Mem) + (module + (memory (import "mem" "shared") 1 1 shared) + (func (export "run") + (local i32 i32) + (i32.atomic.load (i32.const 4)) + (local.set 0) + (i32.atomic.load (i32.const 0)) + (local.set 1) + + ;; store results for checking + (i32.store (i32.const 24) (local.get 0)) + (i32.store (i32.const 32) (local.get 1)) + ) + ) + + (invoke "run") +) + +(wait $T1) +(wait $T2) + +(module $Check + (memory (import "Mem" "shared") 1 1 shared) + + (func (export "check") (result i32) + (local i32 i32) + (i32.load (i32.const 24)) + (local.set 0) + (i32.load (i32.const 32)) + (local.set 1) + + ;; allowed results: (L_0 = 1 && L_1 = 42) || (L_0 = 0 && L_1 = 0) || (L_0 = 0 && L_1 = 42) + + (i32.and (i32.eq (local.get 0) (i32.const 1)) (i32.eq (local.get 1) (i32.const 42))) + (i32.and (i32.eq (local.get 0) (i32.const 0)) (i32.eq (local.get 1) (i32.const 0))) + (i32.and (i32.eq (local.get 0) (i32.const 0)) (i32.eq (local.get 1) (i32.const 42))) + (i32.or) + (i32.or) + (return) + ) +) + +(assert_return (invoke $Check "check") (i32.const 1)) diff --git a/tests/misc_testsuite/threads/SB.wast b/tests/misc_testsuite/threads/SB.wast new file mode 100644 index 000000000000..f94abab035d8 --- /dev/null +++ b/tests/misc_testsuite/threads/SB.wast @@ -0,0 +1,62 @@ +(module $Mem + (memory (export "shared") 1 1 shared) +) + +(thread $T1 (shared (module $Mem)) + (register "mem" $Mem) + (module + (memory (import "mem" "shared") 1 10 shared) + (func (export "run") + (local i32) + (i32.store (i32.const 0) (i32.const 1)) + (i32.load (i32.const 4)) + (local.set 0) + + ;; store results for checking + (i32.store (i32.const 24) (local.get 0)) + ) + ) + (invoke "run") +) + +(thread $T2 (shared (module $Mem)) + (register "mem" $Mem) + (module + (memory (import "mem" "shared") 1 1 shared) + (func (export "run") + (local i32) + (i32.store (i32.const 4) (i32.const 1)) + (i32.load (i32.const 0)) + (local.set 0) + + ;; store results for checking + (i32.store (i32.const 32) (local.get 0)) + ) + ) + + (invoke "run") +) + +(wait $T1) +(wait $T2) + +(module $Check + (memory (import "Mem" "shared") 1 1 shared) + + (func (export "check") (result i32) + (local i32 i32) + (i32.load (i32.const 24)) + (local.set 0) + (i32.load (i32.const 32)) + (local.set 1) + + ;; allowed results: (L_0 = 0 || L_0 = 1) && (L_1 = 0 || L_1 = 1) + + (i32.or (i32.eq (local.get 0) (i32.const 1)) (i32.eq (local.get 0) (i32.const 0))) + (i32.or (i32.eq (local.get 1) (i32.const 1)) (i32.eq (local.get 1) (i32.const 0))) + (i32.and) + (return) + ) +) + +(assert_return (invoke $Check "check") (i32.const 1)) diff --git a/tests/misc_testsuite/threads/SB_atomic.wast b/tests/misc_testsuite/threads/SB_atomic.wast new file mode 100644 index 000000000000..3cedaed55db5 --- /dev/null +++ b/tests/misc_testsuite/threads/SB_atomic.wast @@ -0,0 +1,64 @@ +(module $Mem + (memory (export "shared") 1 1 shared) +) + +(thread $T1 (shared (module $Mem)) + (register "mem" $Mem) + (module + (memory (import "mem" "shared") 1 10 shared) + (func (export "run") + (local i32) + (i32.atomic.store (i32.const 0) (i32.const 1)) + (i32.atomic.load (i32.const 4)) + (local.set 0) + + ;; store results for checking + (i32.store (i32.const 24) (local.get 0)) + ) + ) + (invoke "run") +) + +(thread $T2 (shared (module $Mem)) + (register "mem" $Mem) + (module + (memory (import "mem" "shared") 1 1 shared) + (func (export "run") + (local i32) + (i32.atomic.store (i32.const 4) (i32.const 1)) + (i32.atomic.load (i32.const 0)) + (local.set 0) + + ;; store results for checking + (i32.store (i32.const 32) (local.get 0)) + ) + ) + + (invoke "run") +) + +(wait $T1) +(wait $T2) + +(module $Check + (memory (import "Mem" "shared") 1 1 shared) + + (func (export "check") (result i32) + (local i32 i32) + (i32.load (i32.const 24)) + (local.set 0) + (i32.load (i32.const 32)) + (local.set 1) + + ;; allowed results: (L_0 = 1 && L_1 = 1) || (L_0 = 0 && L_1 = 1) || (L_0 = 1 && L_1 = 0) + + (i32.and (i32.eq (local.get 0) (i32.const 1)) (i32.eq (local.get 1) (i32.const 1))) + (i32.and (i32.eq (local.get 0) (i32.const 0)) (i32.eq (local.get 1) (i32.const 1))) + (i32.and (i32.eq (local.get 0) (i32.const 1)) (i32.eq (local.get 1) (i32.const 0))) + (i32.or) + (i32.or) + (return) + ) +) + +(assert_return (invoke $Check "check") (i32.const 1)) diff --git a/tests/misc_testsuite/threads/wait_notify.wast b/tests/misc_testsuite/threads/wait_notify.wast new file mode 100644 index 000000000000..270509a92dae --- /dev/null +++ b/tests/misc_testsuite/threads/wait_notify.wast @@ -0,0 +1,41 @@ +;; test that looping notify eventually unblocks a parallel waiting thread +(module $Mem + (memory (export "shared") 1 1 shared) +) + +(thread $T1 (shared (module $Mem)) + (register "mem" $Mem) + (module + (memory (import "mem" "shared") 1 10 shared) + (func (export "run") (result i32) + (memory.atomic.wait32 (i32.const 0) (i32.const 0) (i64.const -1)) + ) + ) + ;; test that this thread eventually gets unblocked + (assert_return (invoke "run") (i32.const 0)) +) + +(thread $T2 (shared (module $Mem)) + (register "mem" $Mem) + (module + (memory (import "mem" "shared") 1 1 shared) + (func (export "notify-0") (result i32) + (memory.atomic.notify (i32.const 0) (i32.const 0)) + ) + (func (export "notify-1-while") + (loop + (i32.const 1) + (memory.atomic.notify (i32.const 0) (i32.const 1)) + (i32.ne) + (br_if 0) + ) + ) + ) + ;; notifying with a count of 0 will not unblock + (assert_return (invoke "notify-0") (i32.const 0)) + ;; loop until something is notified + (assert_return (invoke "notify-1-while")) +) + +(wait $T1) +(wait $T2)