diff --git a/Cargo.toml b/Cargo.toml index a23ec3d974e4..89cd8391d02b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -521,6 +521,7 @@ component-model-async = [ "wasmtime-wasi-http?/p3", "dep:futures", ] +rr = ["wasmtime/rr", "component-model", "wasmtime-cli-flags/rr", "run"] # This feature, when enabled, will statically compile out all logging statements # throughout Wasmtime and its dependencies. diff --git a/crates/cli-flags/Cargo.toml b/crates/cli-flags/Cargo.toml index 83823ad77276..4873875fea95 100644 --- a/crates/cli-flags/Cargo.toml +++ b/crates/cli-flags/Cargo.toml @@ -40,3 +40,4 @@ memory-protection-keys = ["wasmtime/memory-protection-keys"] pulley = ["wasmtime/pulley"] stack-switching = ["wasmtime/stack-switching"] debug = ["wasmtime/debug"] +rr = ["wasmtime/rr", "component-model"] diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs index aad803fe729c..49437ace7203 100644 --- a/crates/cli-flags/src/lib.rs +++ b/crates/cli-flags/src/lib.rs @@ -504,6 +504,25 @@ wasmtime_option_group! { } } +wasmtime_option_group! { + #[derive(PartialEq, Clone, Deserialize)] + #[serde(rename_all = "kebab-case", deny_unknown_fields)] + pub struct RecordOptions { + /// Filename for the recorded execution trace (or empty string to skip writing a file). + pub path: Option, + /// Include (optional) signatures to facilitate validation checks during replay + /// (see `wasmtime replay` for details). + pub validation_metadata: Option, + /// Window size of internal buffering for record events (large windows offer more opportunities + /// for coalescing events at the cost of memory usage). + pub event_window_size: Option, + } + + enum Record { + ... + } +} + #[derive(Debug, Clone, PartialEq)] pub struct WasiNnGraph { pub format: String, @@ -554,6 +573,18 @@ pub struct CommonOptions { #[serde(skip)] wasi_raw: Vec>, + /// Options to enable and configure execution recording, `-R help` to see all. + /// + /// Generates a serialized trace of the Wasm module execution that captures all + /// non-determinism observable by the module. This trace can subsequently be + /// re-executed in a determinstic, embedding-agnostic manner (see the `wasmtime replay` command). + /// + /// Note: Minimal configuration options for deterministic Wasm semantics will be + /// enforced during recording by default (NaN canonicalization, deterministic relaxed SIMD). + #[arg(short = 'R', long = "record", value_name = "KEY[=VAL[,..]]")] + #[serde(skip)] + record_raw: Vec>, + // These fields are filled in by the `configure` method below via the // options parsed from the CLI above. This is what the CLI should use. #[arg(skip)] @@ -580,6 +611,10 @@ pub struct CommonOptions { #[serde(rename = "wasi", default)] pub wasi: WasiOptions, + #[arg(skip)] + #[serde(rename = "record", default)] + pub record: RecordOptions, + /// The target triple; default is the host triple #[arg(long, value_name = "TARGET")] #[serde(skip)] @@ -626,12 +661,14 @@ impl CommonOptions { debug_raw: Vec::new(), wasm_raw: Vec::new(), wasi_raw: Vec::new(), + record_raw: Vec::new(), configured: true, opts: Default::default(), codegen: Default::default(), debug: Default::default(), wasm: Default::default(), wasi: Default::default(), + record: Default::default(), target: None, config: None, } @@ -649,12 +686,14 @@ impl CommonOptions { self.debug = toml_options.debug; self.wasm = toml_options.wasm; self.wasi = toml_options.wasi; + self.record = toml_options.record; } self.opts.configure_with(&self.opts_raw); self.codegen.configure_with(&self.codegen_raw); self.debug.configure_with(&self.debug_raw); self.wasm.configure_with(&self.wasm_raw); self.wasi.configure_with(&self.wasi_raw); + self.record.configure_with(&self.record_raw); Ok(()) } @@ -1017,6 +1056,15 @@ impl CommonOptions { config.shared_memory(enable); } + let record = &self.record; + match_feature! { + ["rr" : &record.path] + _path => { + bail!("recording configuration for `rr` feature is not supported yet"); + }, + _ => err, + } + Ok(config) } @@ -1127,6 +1175,7 @@ mod tests { [debug] [wasm] [wasi] + [record] "#; let mut common_options: CommonOptions = toml::from_str(basic_toml).unwrap(); common_options.config(None).unwrap(); @@ -1249,6 +1298,8 @@ impl fmt::Display for CommonOptions { wasm, wasi_raw, wasi, + record_raw, + record, configured, target, config, @@ -1265,6 +1316,7 @@ impl fmt::Display for CommonOptions { let wasi_flags; let wasm_flags; let debug_flags; + let record_flags; if *configured { codegen_flags = codegen.to_options(); @@ -1272,6 +1324,7 @@ impl fmt::Display for CommonOptions { wasi_flags = wasi.to_options(); wasm_flags = wasm.to_options(); opts_flags = opts.to_options(); + record_flags = record.to_options(); } else { codegen_flags = codegen_raw .iter() @@ -1282,6 +1335,11 @@ impl fmt::Display for CommonOptions { wasi_flags = wasi_raw.iter().flat_map(|t| t.0.iter()).cloned().collect(); wasm_flags = wasm_raw.iter().flat_map(|t| t.0.iter()).cloned().collect(); opts_flags = opts_raw.iter().flat_map(|t| t.0.iter()).cloned().collect(); + record_flags = record_raw + .iter() + .flat_map(|t| t.0.iter()) + .cloned() + .collect(); } for flag in codegen_flags { @@ -1299,6 +1357,9 @@ impl fmt::Display for CommonOptions { for flag in debug_flags { write!(f, "-D{flag} ")?; } + for flag in record_flags { + write!(f, "-R{flag} ")?; + } Ok(()) } diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml index c2bbb83b5b4a..f57ff712e06f 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -431,3 +431,6 @@ debug = [ # Enables support for defining compile-time builtins. compile-time-builtins = ['anyhow', 'dep:wasm-compose', 'dep:tempfile'] + +# Enable support for the common base infrastructure of record/replay +rr = ["component-model"] diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 1dbb3235bf5f..6c611d5405ab 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -99,6 +99,31 @@ impl core::hash::Hash for ModuleVersionStrategy { } } +impl ModuleVersionStrategy { + /// Get the string-encoding version of the module. + pub fn as_str(&self) -> &str { + match &self { + Self::WasmtimeVersion => env!("CARGO_PKG_VERSION_MAJOR"), + Self::Custom(c) => c, + Self::None => "", + } + } +} + +/// Configuration for record/replay +#[derive(Clone)] +#[non_exhaustive] +pub enum RRConfig { + #[cfg(feature = "rr")] + /// Recording on store is enabled + Recording, + #[cfg(feature = "rr")] + /// Replaying on store is enabled + Replaying, + /// No record/replay is enabled + None, +} + /// Global configuration options used to create an [`Engine`](crate::Engine) /// and customize its behavior. /// @@ -164,6 +189,7 @@ pub struct Config { pub(crate) detect_host_feature: Option Option>, pub(crate) x86_float_abi_ok: Option, pub(crate) shared_memory: bool, + pub(crate) rr_config: RRConfig, } /// User-provided configuration for the compiler. @@ -273,6 +299,7 @@ impl Config { detect_host_feature: None, x86_float_abi_ok: None, shared_memory: false, + rr_config: RRConfig::None, }; ret.wasm_backtrace_details(WasmBacktraceDetails::Environment); ret @@ -2356,6 +2383,14 @@ impl Config { bail!("exceptions support requires garbage collection (GC) to be enabled in the build"); } + match &self.rr_config { + #[cfg(feature = "rr")] + RRConfig::Recording | RRConfig::Replaying => { + self.validate_rr_determinism_conflicts()?; + } + RRConfig::None => {} + }; + let mut tunables = Tunables::default_for_target(&self.compiler_target())?; // By default this is enabled with the Cargo feature, and if the feature @@ -2977,6 +3012,44 @@ impl Config { self.tunables.concurrency_support = Some(enable); self } + + /// Validate if the current configuration has conflicting overrides that prevent + /// execution determinism. Returns an error if a conflict exists. + /// + /// Note: Keep this in sync with [`Config::enforce_determinism`]. + #[inline] + #[cfg(feature = "rr")] + pub(crate) fn validate_rr_determinism_conflicts(&self) -> Result<()> { + if let Some(v) = self.tunables.relaxed_simd_deterministic { + if v == false { + bail!("Relaxed deterministic SIMD cannot be disabled when determinism is enforced"); + } + } + #[cfg(any(feature = "cranelift", feature = "winch"))] + if let Some(v) = self + .compiler_config + .as_ref() + .and_then(|c| c.settings.get("enable_nan_canonicalization")) + { + if v != "true" { + bail!("NaN canonicalization cannot be disabled when determinism is enforced"); + } + } + Ok(()) + } + + /// Enable execution trace recording or replaying to the configuration. + /// + /// When either recording/replaying are enabled, validation fails if settings + /// that control determinism are not set appropriately. In particular, RR requires + /// doing the following: + /// * Enabling NaN canonicalization with [`Config::cranelift_nan_canonicalization`]. + /// * Enabling deterministic relaxed SIMD with [`Config::relaxed_simd_deterministic`]. + #[inline] + pub fn rr(&mut self, cfg: RRConfig) -> &mut Self { + self.rr_config = cfg; + self + } } impl Default for Config { diff --git a/crates/wasmtime/src/engine.rs b/crates/wasmtime/src/engine.rs index c55df6210c61..cfb8c4eba43d 100644 --- a/crates/wasmtime/src/engine.rs +++ b/crates/wasmtime/src/engine.rs @@ -1,4 +1,5 @@ use crate::Config; +use crate::RRConfig; use crate::prelude::*; #[cfg(feature = "runtime")] pub use crate::runtime::code_memory::CustomCodeMemory; @@ -264,6 +265,30 @@ impl Engine { Arc::ptr_eq(&a.inner, &b.inner) } + /// Returns whether the engine is configured to support execution recording + #[inline] + pub fn is_recording(&self) -> bool { + match self.config().rr_config { + #[cfg(feature = "rr")] + RRConfig::Recording => true, + #[cfg(feature = "rr")] + RRConfig::Replaying => false, + RRConfig::None => false, + } + } + + /// Returns whether the engine is configured to support execution replaying + #[inline] + pub fn is_replaying(&self) -> bool { + match self.config().rr_config { + #[cfg(feature = "rr")] + RRConfig::Replaying => true, + #[cfg(feature = "rr")] + RRConfig::Recording => false, + RRConfig::None => false, + } + } + /// Detects whether the bytes provided are a precompiled object produced by /// Wasmtime. /// diff --git a/crates/wasmtime/src/engine/serialization.rs b/crates/wasmtime/src/engine/serialization.rs index c014cb86116b..9b17cbb87ead 100644 --- a/crates/wasmtime/src/engine/serialization.rs +++ b/crates/wasmtime/src/engine/serialization.rs @@ -95,19 +95,13 @@ pub fn check_compatible(engine: &Engine, mmap: &[u8], expected: ObjectKind) -> R }; match &engine.config().module_version { - ModuleVersionStrategy::WasmtimeVersion => { - let version = core::str::from_utf8(version)?; - if version != env!("CARGO_PKG_VERSION_MAJOR") { - bail!("Module was compiled with incompatible Wasmtime version '{version}'"); - } - } - ModuleVersionStrategy::Custom(v) => { + ModuleVersionStrategy::None => { /* ignore the version info, accept all */ } + _ => { let version = core::str::from_utf8(&version)?; - if version != v { + if version != engine.config().module_version.as_str() { bail!("Module was compiled with incompatible version '{version}'"); } } - ModuleVersionStrategy::None => { /* ignore the version info, accept all */ } } postcard::from_bytes::>(data)?.check_compatible(engine) } @@ -121,11 +115,7 @@ pub fn append_compiler_info(engine: &Engine, obj: &mut Object<'_>, metadata: &Me ); let mut data = Vec::new(); data.push(VERSION); - let version = match &engine.config().module_version { - ModuleVersionStrategy::WasmtimeVersion => env!("CARGO_PKG_VERSION_MAJOR"), - ModuleVersionStrategy::Custom(c) => c, - ModuleVersionStrategy::None => "", - }; + let version = engine.config().module_version.as_str(); // This precondition is checked in Config::module_version: assert!( version.len() < 256,