From cd0a3d37ccb93698b7ce80f79cd8ea1e02d6bcf6 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Wed, 4 Jun 2025 11:53:13 -0400 Subject: [PATCH 01/62] Initial CLI argument parsing for RR --- src/common.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/common.rs b/src/common.rs index 385ee1c4e48b..36e02b3ec20f 100644 --- a/src/common.rs +++ b/src/common.rs @@ -74,6 +74,23 @@ pub struct RunCommon { )] pub profile: Option, + /// Record the module execution + /// + /// Enabling this option will produce a Trace on module execution in the provided + /// endpoint. This trace can then subsequently be passed to the `--replay` generate + /// a equivalent run of the program. + /// + /// Note that determinism will be enforced during recording by default (NaN canonicalization) + #[arg(long, value_name = "TRACE_PATH")] + pub record: Option, + + /// Run a replay of the module according to a Trace file + /// + /// Replay executions will always be deterministic, and will mock all invoked + /// host calls made by the module with the respective trace results. + #[arg(long, value_name = "TRACE_PATH")] + pub replay: Option, + /// Grant access of a host directory to a guest. /// /// If specified as just `HOST_DIR` then the same directory name on the From 016f6b48cfc8ca7fa8eaeb202a7a8fc863a5f8d3 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Thu, 5 Jun 2025 10:05:50 -0400 Subject: [PATCH 02/62] Validate RR args with clap --- crates/cli-flags/src/lib.rs | 45 ++++++++++++++++++++++++++++++++++++- src/common.rs | 22 ++++-------------- 2 files changed, 48 insertions(+), 19 deletions(-) diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs index 5a407ef8e56f..00dc62f1899f 100644 --- a/crates/cli-flags/src/lib.rs +++ b/crates/cli-flags/src/lib.rs @@ -1,7 +1,7 @@ //! Contains the common Wasmtime command line interface (CLI) flags. use anyhow::{Context, Result}; -use clap::Parser; +use clap::{Args, Parser}; use serde::Deserialize; use std::{ fmt, fs, @@ -1249,3 +1249,46 @@ impl fmt::Display for CommonOptions { Ok(()) } } + +#[derive(Args)] +#[group(multiple = false)] +pub struct RROptions { + /// Record the module execution + /// + /// Enabling this option will produce a Trace on module execution in the provided + /// endpoint. This trace can then subsequently be passed to the `--replay` generate + /// a equivalent run of the program. + /// + /// Note that determinism will be enforced during recording by default (NaN canonicalization) + #[arg(long, value_name = "TRACE_PATH")] + pub record: Option, + + /// Run a replay of the module according to a Trace file + /// + /// Replay executions will always be deterministic, and will mock all invoked + /// host calls made by the module with the respective trace results. + #[arg(long, value_name = "TRACE_PATH")] + pub replay: Option, +} + +impl RROptions { + pub fn record_enabled(&self) -> bool { + self.record.is_some() + } + pub fn unwrap_record_path(&self) -> &String { + match &self.record { + None => panic!("cannot unwrap path to recording trace, specify `--record`"), + Some(path) => &path, + } + } + + pub fn replay_enabled(&self) -> bool { + self.replay.is_some() + } + pub fn unwrap_replay_path(&self) -> &String { + match &self.record { + None => panic!("cannot unwrap path to replay trace, specify `--replay`"), + Some(path) => path, + } + } +} diff --git a/src/common.rs b/src/common.rs index 36e02b3ec20f..3ffa2c1f85d0 100644 --- a/src/common.rs +++ b/src/common.rs @@ -5,7 +5,7 @@ use clap::Parser; use std::net::TcpListener; use std::{fs::File, path::Path, time::Duration}; use wasmtime::{Engine, Module, Precompiled, StoreLimits, StoreLimitsBuilder}; -use wasmtime_cli_flags::{CommonOptions, opt::WasmtimeOptionValue}; +use wasmtime_cli_flags::{CommonOptions, RROptions, opt::WasmtimeOptionValue}; use wasmtime_wasi::p2::WasiCtxBuilder; use wasmtime_wasi::p2::bindings::LinkOptions; @@ -43,6 +43,9 @@ pub struct RunCommon { #[command(flatten)] pub common: CommonOptions, + #[command(flatten)] + pub rr: RROptions, + /// Allow executing precompiled WebAssembly modules as `*.cwasm` files. /// /// Note that this option is not safe to pass if the module being passed in @@ -74,23 +77,6 @@ pub struct RunCommon { )] pub profile: Option, - /// Record the module execution - /// - /// Enabling this option will produce a Trace on module execution in the provided - /// endpoint. This trace can then subsequently be passed to the `--replay` generate - /// a equivalent run of the program. - /// - /// Note that determinism will be enforced during recording by default (NaN canonicalization) - #[arg(long, value_name = "TRACE_PATH")] - pub record: Option, - - /// Run a replay of the module according to a Trace file - /// - /// Replay executions will always be deterministic, and will mock all invoked - /// host calls made by the module with the respective trace results. - #[arg(long, value_name = "TRACE_PATH")] - pub replay: Option, - /// Grant access of a host directory to a guest. /// /// If specified as just `HOST_DIR` then the same directory name on the From bd6859fb7cde4a699132e6f3c058b8cc85e9f4a2 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Thu, 5 Jun 2025 13:35:54 -0400 Subject: [PATCH 03/62] Setup `RRConfig` for runtime access --- crates/cli-flags/src/lib.rs | 22 ------------ crates/wasmtime/src/config.rs | 64 +++++++++++++++++++++++++++++++++++ src/commands/run.rs | 2 ++ 3 files changed, 66 insertions(+), 22 deletions(-) diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs index 00dc62f1899f..4ec1155c9edc 100644 --- a/crates/cli-flags/src/lib.rs +++ b/crates/cli-flags/src/lib.rs @@ -1270,25 +1270,3 @@ pub struct RROptions { #[arg(long, value_name = "TRACE_PATH")] pub replay: Option, } - -impl RROptions { - pub fn record_enabled(&self) -> bool { - self.record.is_some() - } - pub fn unwrap_record_path(&self) -> &String { - match &self.record { - None => panic!("cannot unwrap path to recording trace, specify `--record`"), - Some(path) => &path, - } - } - - pub fn replay_enabled(&self) -> bool { - self.replay.is_some() - } - pub fn unwrap_replay_path(&self) -> &String { - match &self.record { - None => panic!("cannot unwrap path to replay trace, specify `--replay`"), - Some(path) => path, - } - } -} diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 6e59ac7da865..76d2127c4ea7 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -163,6 +163,7 @@ pub struct Config { pub(crate) coredump_on_trap: bool, pub(crate) macos_use_mach_ports: bool, pub(crate) detect_host_feature: Option Option>, + pub(crate) rr: Option, } /// User-provided configuration for the compiler. @@ -219,6 +220,52 @@ impl Default for CompilerConfig { } } +/// Configuration for record/replay targets +#[derive(Debug, Clone)] +pub enum RRConfig { + /// Recording trace filepath + Record(String), + /// Replay trace filepath + Replay(String), +} + +impl RRConfig { + /// Test if execution recording is enabled ([`Self::Record`]) + pub fn record_enabled(&self) -> bool { + if let Self::Record(_) = self { + true + } else { + false + } + } + /// Extract the record path. Panics if not an [`Self::Record`] + pub fn unwrap_record(&self) -> &String { + if let Self::Record(path) = self { + path + } else { + panic!("missing path to recording trace (specify `--record` option)") + } + } + + /// Test if execution replay is enabled ([`Self::Replay`]) + pub fn replay_enabled(&self) -> bool { + if let Self::Replay(_) = self { + true + } else { + false + } + } + + /// Extract the replay path. Panics if not a [`Self::Replay`] + pub fn unwrap_replay(&self) -> &String { + if let Self::Replay(path) = self { + path + } else { + panic!("missing path to replay trace (specify `--replay` option)") + } + } +} + impl Config { /// Creates a new configuration object with the default configuration /// specified. @@ -271,6 +318,7 @@ impl Config { detect_host_feature: Some(detect_host_feature), #[cfg(not(feature = "std"))] detect_host_feature: None, + rr: None, }; #[cfg(any(feature = "cranelift", feature = "winch"))] { @@ -2625,6 +2673,22 @@ impl Config { self.tunables.signals_based_traps = Some(enable); self } + + /// Configure the record/replay options for use by the runtime + /// + /// These options are derived from CLI configuration [`RROptions`], which + /// enforce that both record and replay cannot both be set simultaneously. + pub fn rr(&mut self, record_path: &Option, replay_path: &Option) { + if record_path.is_some() && replay_path.is_some() { + // Should be unreachable + panic!("Cannot set both record and replay simultaneously for execution"); + } + if let Some(rpath) = record_path { + self.rr = Some(RRConfig::Record(rpath.into())); + } else if let Some(rpath) = replay_path { + self.rr = Some(RRConfig::Replay(rpath.into())); + } + } } impl Default for Config { diff --git a/src/commands/run.rs b/src/commands/run.rs index d71a685b72a2..ab814fd792fd 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -108,6 +108,8 @@ impl RunCommand { } None => {} } + let rr = &self.run.rr; + config.rr(&rr.record, &rr.replay); let engine = Engine::new(&config)?; From 2533f7e55231e522008d8cd9386a80ddf54cbb2b Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Thu, 5 Jun 2025 19:00:35 -0400 Subject: [PATCH 04/62] Add rr buffers to `Store` --- crates/wasmtime/src/config.rs | 63 +++++++++++++--------------- crates/wasmtime/src/engine.rs | 8 ++++ crates/wasmtime/src/runtime/store.rs | 11 +++++ 3 files changed, 47 insertions(+), 35 deletions(-) diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 76d2127c4ea7..81659b2947fc 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -230,39 +230,32 @@ pub enum RRConfig { } impl RRConfig { - /// Test if execution recording is enabled ([`Self::Record`]) - pub fn record_enabled(&self) -> bool { - if let Self::Record(_) = self { - true - } else { - false + /// Test if execution recording is enabled (i.e, variant [`Self::Record`]), and wrap + /// the corresponding path + pub fn record(&self) -> Option<&String> { + match self { + Self::Record(p) => Some(p), + _ => None, } } - /// Extract the record path. Panics if not an [`Self::Record`] - pub fn unwrap_record(&self) -> &String { - if let Self::Record(path) = self { - path - } else { - panic!("missing path to recording trace (specify `--record` option)") - } + /// Extract the record path. Panics if not a [`Self::Record`] + pub fn record_unwrap(&self) -> &String { + self.record() + .expect("missing path to recording trace (specify `--record` option)") } - /// Test if execution replay is enabled ([`Self::Replay`]) - pub fn replay_enabled(&self) -> bool { - if let Self::Replay(_) = self { - true - } else { - false + /// Test if execution replay is enabled (i.e. variant [`Self::Replay`]), and wrap + /// the corresponding path + pub fn replay(&self) -> Option<&String> { + match self { + Self::Replay(p) => Some(p), + _ => None, } } - /// Extract the replay path. Panics if not a [`Self::Replay`] - pub fn unwrap_replay(&self) -> &String { - if let Self::Replay(path) = self { - path - } else { - panic!("missing path to replay trace (specify `--replay` option)") - } + pub fn replay_unwrap(&self) -> &String { + self.replay() + .expect("missing path to recording trace (specify `--record` option)") } } @@ -2676,17 +2669,17 @@ impl Config { /// Configure the record/replay options for use by the runtime /// - /// These options are derived from CLI configuration [`RROptions`], which - /// enforce that both record and replay cannot both be set simultaneously. + /// These options are derived from CLI configuration, which + /// enforces that both record and replay cannot both be set simultaneously. pub fn rr(&mut self, record_path: &Option, replay_path: &Option) { - if record_path.is_some() && replay_path.is_some() { + self.rr = match (record_path, replay_path) { // Should be unreachable - panic!("Cannot set both record and replay simultaneously for execution"); - } - if let Some(rpath) = record_path { - self.rr = Some(RRConfig::Record(rpath.into())); - } else if let Some(rpath) = replay_path { - self.rr = Some(RRConfig::Replay(rpath.into())); + (Some(_), Some(_)) => { + panic!("Cannot set both record and replay simultaneously for execution") + } + (Some(p), None) => Some(RRConfig::Record(p.into())), + (None, Some(p)) => Some(RRConfig::Replay(p.into())), + _ => None, } } } diff --git a/crates/wasmtime/src/engine.rs b/crates/wasmtime/src/engine.rs index 14a2f8569ac4..b452ad12f89b 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; @@ -220,6 +221,13 @@ impl Engine { self.config().async_support } + /// Returns an immutable reference to the record/replay configuration settings + /// used by the engine + #[inline] + pub fn rr(&self) -> Option<&RRConfig> { + self.config().rr.as_ref() + } + /// Detects whether the bytes provided are a precompiled object produced by /// Wasmtime. /// diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index c2f016fe2f0a..9730105829c9 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -394,6 +394,15 @@ pub struct StoreOpaque { /// For example if Pulley is enabled and configured then this will store a /// Pulley interpreter. executor: Executor, + + /// Storage for recording execution + /// + /// `None` implies recording is disabled for this store + record_buffer: Option>, + /// Storage for replaying execution + /// + /// `None` implies replay is disabled for this store + replay_buffer: Option>, } /// Executor state within `StoreOpaque`. @@ -578,6 +587,8 @@ impl Store { debug_assert!(engine.target().is_pulley()); Executor::Interpreter(Interpreter::new(engine)) }, + record_buffer: engine.rr().and_then(|x| x.record().and(Some(Vec::new()))), + replay_buffer: engine.rr().and_then(|x| x.replay().and(Some(Vec::new()))), }; let mut inner = Box::new(StoreInner { inner, From 21c9e9808db833c83305bd51bd1d404970ed8c54 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 10 Jun 2025 12:52:47 -0400 Subject: [PATCH 05/62] Determinism config enforcement during RR --- crates/wasmtime/src/config.rs | 50 +++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 81659b2947fc..77eb310d8e08 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -230,7 +230,7 @@ pub enum RRConfig { } impl RRConfig { - /// Test if execution recording is enabled (i.e, variant [`Self::Record`]), and wrap + /// Test if execution recording is enabled (i.e, variant [`RRConfig::Record`]), and wrap /// the corresponding path pub fn record(&self) -> Option<&String> { match self { @@ -238,13 +238,13 @@ impl RRConfig { _ => None, } } - /// Extract the record path. Panics if not a [`Self::Record`] + /// Extract the record path. Panics if not a [`RRConfig::Record`] pub fn record_unwrap(&self) -> &String { self.record() .expect("missing path to recording trace (specify `--record` option)") } - /// Test if execution replay is enabled (i.e. variant [`Self::Replay`]), and wrap + /// Test if execution replay is enabled (i.e. variant [`RRConfig::Replay`]), and wrap /// the corresponding path pub fn replay(&self) -> Option<&String> { match self { @@ -252,7 +252,7 @@ impl RRConfig { _ => None, } } - /// Extract the replay path. Panics if not a [`Self::Replay`] + /// Extract the replay path. Panics if not a [`RRConfig::Replay`] pub fn replay_unwrap(&self) -> &String { self.replay() .expect("missing path to recording trace (specify `--record` option)") @@ -1042,6 +1042,10 @@ impl Config { /// /// [proposal]: https://github.com/webassembly/relaxed-simd pub fn relaxed_simd_deterministic(&mut self, enable: bool) -> &mut Self { + assert!( + enable || !self.check_determinism(), + "Deterministic relaxed SIMD cannot be disabled when record/replay is enabled" + ); self.tunables.relaxed_simd_deterministic = Some(enable); self } @@ -1344,6 +1348,10 @@ impl Config { /// The default value for this is `false` #[cfg(any(feature = "cranelift", feature = "winch"))] pub fn cranelift_nan_canonicalization(&mut self, enable: bool) -> &mut Self { + assert!( + enable || !self.check_determinism(), + "NaN canonicalization cannot be disabled when record/replay is enabled" + ); let val = if enable { "true" } else { "false" }; self.compiler_config .settings @@ -2667,11 +2675,32 @@ impl Config { self } + /// Enforce deterministic execution configurations + /// + /// Required for faithful record/replay execution. Currently, this does the following: + /// * Enables NaN canonicalization with [`Config::cranelift_nan_canonicalization`] + /// * Enables deterministic relaxed SIMD with [`Config::relaxed_simd_deterministic`] + #[inline] + pub fn enforce_determinism(&mut self) -> &mut Self { + self.cranelift_nan_canonicalization(true) + .relaxed_simd_deterministic(true); + self + } + + /// Evaluates to true if current configuration must respect + /// deterministic execution in its configuration + /// + /// Required for faithful record/replay execution + #[inline] + pub fn check_determinism(&mut self) -> bool { + self.rr.is_some() + } + /// Configure the record/replay options for use by the runtime /// - /// These options are derived from CLI configuration, which - /// enforces that both record and replay cannot both be set simultaneously. - pub fn rr(&mut self, record_path: &Option, replay_path: &Option) { + /// This method implicitly enforces determinism (see [`Config::enforce_determinism`] + /// for details). Panics if both record and replay are set simultaneously + pub fn rr(&mut self, record_path: &Option, replay_path: &Option) -> &mut Self { self.rr = match (record_path, replay_path) { // Should be unreachable (Some(_), Some(_)) => { @@ -2680,7 +2709,12 @@ impl Config { (Some(p), None) => Some(RRConfig::Record(p.into())), (None, Some(p)) => Some(RRConfig::Replay(p.into())), _ => None, - } + }; + // Set appropriate configurations for determinstic execution + if self.rr.is_some() { + self.enforce_determinism(); + }; + self } } From 1dbaa034f5e0e062772a8f43337e3672469c6c8f Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Fri, 13 Jun 2025 14:43:31 -0400 Subject: [PATCH 06/62] Added RR event buffers --- crates/wasmtime/src/runtime.rs | 1 + crates/wasmtime/src/runtime/func.rs | 24 ++++++- crates/wasmtime/src/runtime/rr.rs | 95 ++++++++++++++++++++++++++++ crates/wasmtime/src/runtime/store.rs | 23 +++++-- 4 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 crates/wasmtime/src/runtime/rr.rs diff --git a/crates/wasmtime/src/runtime.rs b/crates/wasmtime/src/runtime.rs index 230178ce11b9..a5ee73ab448c 100644 --- a/crates/wasmtime/src/runtime.rs +++ b/crates/wasmtime/src/runtime.rs @@ -42,6 +42,7 @@ pub(crate) mod linker; pub(crate) mod memory; pub(crate) mod module; pub(crate) mod resources; +pub(crate) mod rr; pub(crate) mod store; pub(crate) mod trampoline; pub(crate) mod trap; diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 95a0cd785294..7257ad2dd116 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -1,5 +1,6 @@ use crate::prelude::*; use crate::runtime::Uninhabited; +use crate::runtime::rr::{RRBuffer, RREvent}; use crate::runtime::vm::{ ExportFunction, InterpreterRef, SendSyncPtr, StoreBox, VMArrayCallHostFuncContext, VMCommonStackInformation, VMContext, VMFuncRef, VMFunctionImport, VMOpaqueContext, @@ -2324,7 +2325,6 @@ impl HostContext { NonNull::slice_from_raw_parts(args.cast::>(), args_len); let vmctx = VMArrayCallHostFuncContext::from_opaque(callee_vmctx); let state = vmctx.as_ref().host_state(); - // Double-check ourselves in debug mode, but we control // the `Any` here so an unsafe downcast should also // work. @@ -2337,12 +2337,29 @@ impl HostContext { break 'ret R::fallible_from_error(trap); } + // Record interception + { + let record_buffer = caller.store.0.record_buffer_mut(); + if let Some(buf) = record_buffer { + let call_event = RREvent::extern_call_from_valraw_slice(args.as_ref()); + println!("Record | {:?}", &call_event); + buf.append(call_event); + } + // Replay interception + let replay_buffer = caller.store.0.replay_buffer_mut(); + if let Some(buf) = replay_buffer { + let call_event = buf.pop_front(); + println!("Replay | {:?}", &call_event); + } + } + let mut store = if P::may_gc() { AutoAssertNoGc::new(caller.store.0) } else { unsafe { AutoAssertNoGc::disabled(caller.store.0) } }; let params = P::load(&mut store, args.as_mut()); + let _ = &mut store; drop(store); @@ -2362,6 +2379,11 @@ impl HostContext { unsafe { AutoAssertNoGc::disabled(caller.store.0) } }; let ret = ret.store(&mut store, args.as_mut())?; + { + // Record the values + let x = RREvent::extern_return_from_valraw_slice(args.as_ref()); + println!("{:?}", x); + } Ok(ret) } }; diff --git a/crates/wasmtime/src/runtime/rr.rs b/crates/wasmtime/src/runtime/rr.rs new file mode 100644 index 000000000000..71f235fee829 --- /dev/null +++ b/crates/wasmtime/src/runtime/rr.rs @@ -0,0 +1,95 @@ +//! Wasmtime's Record and Replay support + +use crate::ValRaw; +use crate::prelude::*; +use core::fmt; +use core::mem::{self, MaybeUninit}; +use serde::{Deserialize, Serialize}; +use std::collections::VecDeque; + +const VAL_RAW_SIZE: usize = mem::size_of::(); + +// Since unions cannot be serialized, we encode them as transmutable byte arrays +//type ValRawSer = [u8; VAL_RAW_SIZE]; +#[derive(Serialize, Deserialize)] +struct ValRawSer([u8; VAL_RAW_SIZE]); + +impl From for ValRawSer { + fn from(value: ValRaw) -> Self { + unsafe { Self(mem::transmute(value)) } + } +} + +impl fmt::Debug for ValRawSer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let hex_digits_per_byte = 2; + let _ = write!(f, "0x.."); + for b in self.0.iter().rev() { + let _ = write!(f, "{:0width$x}", b, width = hex_digits_per_byte); + } + Ok(()) + } +} + +/// A single recording/replay event +#[derive(Debug, Serialize, Deserialize)] +pub enum RREvent { + ExternCall(Vec), + ExternReturn(Vec), +} + +impl RREvent { + fn raw_to_vec(args: &[MaybeUninit]) -> Vec { + args.iter() + .map(|x| unsafe { ValRawSer::from(x.assume_init()) }) + .collect::>() + } + + pub fn extern_call_from_valraw_slice(args: &[MaybeUninit]) -> Self { + Self::ExternCall(Self::raw_to_vec(args)) + } + pub fn extern_return_from_valraw_slice(args: &[MaybeUninit]) -> Self { + Self::ExternReturn(Self::raw_to_vec(args)) + } +} + +/// Buffer to read/write record/replay data respectively +pub struct RRBuffer { + inner: VecDeque, +} + +impl RRBuffer { + /// Constructs an new (empty) buffer + pub fn new() -> Self { + RRBuffer { + inner: VecDeque::new(), + } + } + + /// Appends a new [`RREvent`] to the buffer + pub fn append(&mut self, event: RREvent) { + self.inner.push_back(event) + } + + pub fn pop_front(&mut self) -> RREvent { + self.inner + .pop_front() + .expect("Replay event buffer is empty") + } + + /// Flush all the contents of the entire buffer to a writer + /// + /// Buffer is emptied during this process + fn to_file(writer: W) + where + W: std::io::Write, + { + } + + /// Read the + fn from_file(reader: R) + where + R: std::io::Read, + { + } +} diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index 9730105829c9..75fd7054072d 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -79,6 +79,7 @@ use crate::RootSet; use crate::module::RegisteredModuleId; use crate::prelude::*; +use crate::runtime::rr::{RRBuffer, RREvent}; #[cfg(feature = "gc")] use crate::runtime::vm::GcRootsList; #[cfg(feature = "stack-switching")] @@ -398,11 +399,11 @@ pub struct StoreOpaque { /// Storage for recording execution /// /// `None` implies recording is disabled for this store - record_buffer: Option>, + record_buffer: Option, /// Storage for replaying execution /// /// `None` implies replay is disabled for this store - replay_buffer: Option>, + replay_buffer: Option, } /// Executor state within `StoreOpaque`. @@ -587,8 +588,12 @@ impl Store { debug_assert!(engine.target().is_pulley()); Executor::Interpreter(Interpreter::new(engine)) }, - record_buffer: engine.rr().and_then(|x| x.record().and(Some(Vec::new()))), - replay_buffer: engine.rr().and_then(|x| x.replay().and(Some(Vec::new()))), + record_buffer: engine + .rr() + .and_then(|x| x.record().and(Some(RRBuffer::new()))), + replay_buffer: engine + .rr() + .and_then(|x| x.replay().and(Some(RRBuffer::new()))), }; let mut inner = Box::new(StoreInner { inner, @@ -1345,6 +1350,16 @@ impl StoreOpaque { &self.vm_store_context } + #[inline] + pub(crate) fn record_buffer_mut(&mut self) -> Option<&mut RRBuffer> { + self.record_buffer.as_mut() + } + + #[inline] + pub(crate) fn replay_buffer_mut(&mut self) -> Option<&mut RRBuffer> { + self.replay_buffer.as_mut() + } + #[inline(never)] pub(crate) fn allocate_gc_heap(&mut self) -> Result<()> { log::trace!("allocating GC heap for store {:?}", self.id()); From 63f96cf82d0bb19ef16f115b3f2a9394fa83f105 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Mon, 16 Jun 2025 19:57:40 -0400 Subject: [PATCH 07/62] Initial RR serde support --- crates/wasmtime/Cargo.toml | 2 +- crates/wasmtime/src/config.rs | 6 +-- crates/wasmtime/src/runtime/func.rs | 49 ++++++++++++++--------- crates/wasmtime/src/runtime/rr.rs | 60 +++++++++++++++++++--------- crates/wasmtime/src/runtime/store.rs | 28 +++++++++---- 5 files changed, 97 insertions(+), 48 deletions(-) diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml index 02188fa8fd91..2c5bf9c30582 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -45,7 +45,7 @@ wat = { workspace = true, optional = true } serde = { workspace = true } serde_derive = { workspace = true } serde_json = { workspace = true, optional = true } -postcard = { workspace = true } +postcard = { workspace = true, features = ["use-std"] } indexmap = { workspace = true } once_cell = { version = "1.12.0", optional = true } rayon = { version = "1.0", optional = true } diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 77eb310d8e08..3ec4823ba2c7 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -2700,14 +2700,14 @@ impl Config { /// /// This method implicitly enforces determinism (see [`Config::enforce_determinism`] /// for details). Panics if both record and replay are set simultaneously - pub fn rr(&mut self, record_path: &Option, replay_path: &Option) -> &mut Self { + pub fn rr(&mut self, record_path: Option, replay_path: Option) -> &mut Self { self.rr = match (record_path, replay_path) { // Should be unreachable (Some(_), Some(_)) => { panic!("Cannot set both record and replay simultaneously for execution") } - (Some(p), None) => Some(RRConfig::Record(p.into())), - (None, Some(p)) => Some(RRConfig::Replay(p.into())), + (Some(p), None) => Some(RRConfig::Record(p)), + (None, Some(p)) => Some(RRConfig::Replay(p)), _ => None, }; // Set appropriate configurations for determinstic execution diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 7257ad2dd116..a78b285e283d 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -1,6 +1,6 @@ use crate::prelude::*; use crate::runtime::Uninhabited; -use crate::runtime::rr::{RRBuffer, RREvent}; +use crate::runtime::rr::RREvent; use crate::runtime::vm::{ ExportFunction, InterpreterRef, SendSyncPtr, StoreBox, VMArrayCallHostFuncContext, VMCommonStackInformation, VMContext, VMFuncRef, VMFunctionImport, VMOpaqueContext, @@ -2314,6 +2314,23 @@ impl HostContext { R: WasmRet, T: 'static, { + let record_intercept = |caller: &mut Caller<'_, T>, event| { + let record_buffer = caller.store.0.record_buffer_mut(); + if let Some(buf) = record_buffer { + println!("Record | {:?}", &event); + buf.append(event); + } + }; + let replay_intercept = |caller: &mut Caller<'_, T>| -> Option { + let replay_buffer = caller.store.0.replay_buffer_mut(); + if let Some(buf) = replay_buffer { + let call_event = buf.pop_front(); + println!("Replay | {:?}", &call_event); + Some(call_event) + } else { + None + } + }; // Note that this function is intentionally scoped into a // separate closure. Handling traps and panics will involve // longjmp-ing from this function which means we won't run @@ -2336,21 +2353,13 @@ impl HostContext { if let Err(trap) = caller.store.0.call_hook(CallHook::CallingHost) { break 'ret R::fallible_from_error(trap); } - - // Record interception + // RR recording/replay interception on call { - let record_buffer = caller.store.0.record_buffer_mut(); - if let Some(buf) = record_buffer { - let call_event = RREvent::extern_call_from_valraw_slice(args.as_ref()); - println!("Record | {:?}", &call_event); - buf.append(call_event); - } - // Replay interception - let replay_buffer = caller.store.0.replay_buffer_mut(); - if let Some(buf) = replay_buffer { - let call_event = buf.pop_front(); - println!("Replay | {:?}", &call_event); - } + record_intercept( + &mut caller, + RREvent::extern_call_from_valraw_slice(args.as_ref()), + ); + let _event = replay_intercept(&mut caller); } let mut store = if P::may_gc() { @@ -2379,10 +2388,14 @@ impl HostContext { unsafe { AutoAssertNoGc::disabled(caller.store.0) } }; let ret = ret.store(&mut store, args.as_mut())?; + drop(store); + // RR recording/replay interception on return { - // Record the values - let x = RREvent::extern_return_from_valraw_slice(args.as_ref()); - println!("{:?}", x); + record_intercept( + &mut caller, + RREvent::extern_return_from_valraw_slice(args.as_ref()), + ); + let _event = replay_intercept(&mut caller); } Ok(ret) } diff --git a/crates/wasmtime/src/runtime/rr.rs b/crates/wasmtime/src/runtime/rr.rs index 71f235fee829..981b133ff957 100644 --- a/crates/wasmtime/src/runtime/rr.rs +++ b/crates/wasmtime/src/runtime/rr.rs @@ -4,15 +4,17 @@ use crate::ValRaw; use crate::prelude::*; use core::fmt; use core::mem::{self, MaybeUninit}; +use postcard; use serde::{Deserialize, Serialize}; use std::collections::VecDeque; +use std::fs::File; +use std::io::{BufWriter, Seek, Write}; const VAL_RAW_SIZE: usize = mem::size_of::(); -// Since unions cannot be serialized, we encode them as transmutable byte arrays -//type ValRawSer = [u8; VAL_RAW_SIZE]; +/// Transmutable byte arrays necessary to serialize unions #[derive(Serialize, Deserialize)] -struct ValRawSer([u8; VAL_RAW_SIZE]); +pub struct ValRawSer([u8; VAL_RAW_SIZE]); impl From for ValRawSer { fn from(value: ValRaw) -> Self { @@ -54,42 +56,62 @@ impl RREvent { } /// Buffer to read/write record/replay data respectively +#[derive(Debug)] pub struct RRBuffer { inner: VecDeque, + rw: File, } impl RRBuffer { - /// Constructs an new (empty) buffer - pub fn new() -> Self { - RRBuffer { + /// Constructs a writer on new, filesystem-backed buffer (record) + pub fn write_fs(path: String) -> Result { + Ok(RRBuffer { inner: VecDeque::new(), + rw: File::create(path)?, + }) + } + + /// Constructs a reader on filesystem-backed buffer (replay) + pub fn read_fs(path: String) -> Result { + let mut file = File::open(path)?; + let mut events = VecDeque::::new(); + while file.stream_position()? != file.metadata()?.len() { + let (event, _): (RREvent, _) = postcard::from_io((&mut file, &mut [0; 0]))?; + events.push_back(event); } + // Check that file is at EOF + //assert_eq!(file.stream_position()?, file.metadata()?.len()); + println!("Read from file: {:?}", events); + Ok(RRBuffer { + inner: events, + rw: file, + }) } - /// Appends a new [`RREvent`] to the buffer + /// Appends a new [`RREvent`] to the buffer (record) pub fn append(&mut self, event: RREvent) { self.inner.push_back(event) } + /// Retrieve the head of the buffer (replay) pub fn pop_front(&mut self) -> RREvent { self.inner .pop_front() - .expect("Replay event buffer is empty") + .expect("Incomplete replay trace. Event buffer is empty prior to completion") } /// Flush all the contents of the entire buffer to a writer /// /// Buffer is emptied during this process - fn to_file(writer: W) - where - W: std::io::Write, - { - } - - /// Read the - fn from_file(reader: R) - where - R: std::io::Read, - { + pub fn flush_to_file(&mut self) -> Result<()> { + println!("Flushing to file: {:?}", self.inner); + // Seralizing each event independently prevents checking for vector sizes + // during deserialization + for v in &self.inner { + postcard::to_io(&v, &mut self.rw)?; + } + self.rw.flush()?; + self.inner.clear(); + Ok(()) } } diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index 75fd7054072d..84fd58403433 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -79,7 +79,7 @@ use crate::RootSet; use crate::module::RegisteredModuleId; use crate::prelude::*; -use crate::runtime::rr::{RRBuffer, RREvent}; +use crate::runtime::rr::RRBuffer; #[cfg(feature = "gc")] use crate::runtime::vm::GcRootsList; #[cfg(feature = "stack-switching")] @@ -588,12 +588,14 @@ impl Store { debug_assert!(engine.target().is_pulley()); Executor::Interpreter(Interpreter::new(engine)) }, - record_buffer: engine - .rr() - .and_then(|x| x.record().and(Some(RRBuffer::new()))), - replay_buffer: engine - .rr() - .and_then(|x| x.replay().and(Some(RRBuffer::new()))), + record_buffer: engine.rr().and_then(|rr| { + rr.record() + .and_then(|x| Some(RRBuffer::write_fs(x.into()).unwrap())) + }), + replay_buffer: engine.rr().and_then(|rr| { + rr.replay() + .and_then(|x| Some(RRBuffer::read_fs(x.into()).unwrap())) + }), }; let mut inner = Box::new(StoreInner { inner, @@ -2042,6 +2044,16 @@ at https://bytecodealliance.org/security. let instance_id = vm::Instance::from_vmctx(vmctx, |i| i.id()); StoreInstanceId::new(self.id(), instance_id) } + + /// Flush the record buffer to the disk-backed storage + /// + /// This operation empties the buffer + pub(crate) fn flush_record_buffer(&mut self) -> Result<()> { + if let Some(buf) = self.record_buffer_mut() { + return Ok(buf.flush_to_file()?); + } + Ok(()) + } } /// Helper parameter to [`StoreOpaque::allocate_instance`]. @@ -2371,6 +2383,8 @@ impl Drop for StoreOpaque { } } } + + let _ = self.flush_record_buffer(); } } From b832a44e065358d73e16572a19bd82bfb17869d3 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 17 Jun 2025 17:05:46 -0400 Subject: [PATCH 08/62] Support types for `RREvent` --- crates/cli-flags/src/lib.rs | 2 +- crates/wasmtime/src/runtime/func.rs | 24 ++++++---- crates/wasmtime/src/runtime/rr.rs | 71 +++++++++++++++++++++-------- src/commands/run.rs | 4 +- 4 files changed, 72 insertions(+), 29 deletions(-) diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs index 4ec1155c9edc..3d08ffdbea25 100644 --- a/crates/cli-flags/src/lib.rs +++ b/crates/cli-flags/src/lib.rs @@ -1250,7 +1250,7 @@ impl fmt::Display for CommonOptions { } } -#[derive(Args)] +#[derive(Args, Clone)] #[group(multiple = false)] pub struct RROptions { /// Record the module execution diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index a78b285e283d..8e3292e3ad54 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -2323,13 +2323,11 @@ impl HostContext { }; let replay_intercept = |caller: &mut Caller<'_, T>| -> Option { let replay_buffer = caller.store.0.replay_buffer_mut(); - if let Some(buf) = replay_buffer { + replay_buffer.and_then(|buf| { let call_event = buf.pop_front(); println!("Replay | {:?}", &call_event); Some(call_event) - } else { - None - } + }) }; // Note that this function is intentionally scoped into a // separate closure. Handling traps and panics will involve @@ -2340,8 +2338,8 @@ impl HostContext { let run = move |mut caller: Caller<'_, T>| { let mut args = NonNull::slice_from_raw_parts(args.cast::>(), args_len); - let vmctx = VMArrayCallHostFuncContext::from_opaque(callee_vmctx); - let state = vmctx.as_ref().host_state(); + let vmctx = VMArrayCallHostFuncContext::from_opaque(callee_vmctx).as_ref(); + let state = vmctx.host_state(); // Double-check ourselves in debug mode, but we control // the `Any` here so an unsafe downcast should also // work. @@ -2349,6 +2347,16 @@ impl HostContext { let state = &*(state as *const _ as *const HostFuncState); let func = &state.func; + let type_index = vmctx.func_ref().type_index; + let wasm_func_type_arc = caller + .store + .0 + .engine() + .signatures() + .borrow(type_index) + .unwrap(); + let wasm_func_type = wasm_func_type_arc.unwrap_func(); + let ret = 'ret: { if let Err(trap) = caller.store.0.call_hook(CallHook::CallingHost) { break 'ret R::fallible_from_error(trap); @@ -2357,7 +2365,7 @@ impl HostContext { { record_intercept( &mut caller, - RREvent::extern_call_from_valraw_slice(args.as_ref()), + RREvent::host_func_entry(args.as_ref(), Some(wasm_func_type.clone())), ); let _event = replay_intercept(&mut caller); } @@ -2393,7 +2401,7 @@ impl HostContext { { record_intercept( &mut caller, - RREvent::extern_return_from_valraw_slice(args.as_ref()), + RREvent::host_func_return(args.as_ref(), Some(wasm_func_type.clone())), ); let _event = replay_intercept(&mut caller); } diff --git a/crates/wasmtime/src/runtime/rr.rs b/crates/wasmtime/src/runtime/rr.rs index 981b133ff957..59691b501c34 100644 --- a/crates/wasmtime/src/runtime/rr.rs +++ b/crates/wasmtime/src/runtime/rr.rs @@ -1,7 +1,16 @@ //! Wasmtime's Record and Replay support +//! +//! This feature is currently experimental and hence not optimized. +//! In particular, the following opportunities are immediately identifiable: +//! * Switch [RRFuncArgTypes] to use [Vec] +//! +//! Flexibility can also be improved with: +//! * Support for generic writers beyond [File] (will require a generic on [Store]) use crate::ValRaw; use crate::prelude::*; +#[allow(unused_imports)] +use crate::runtime::Store; use core::fmt; use core::mem::{self, MaybeUninit}; use postcard; @@ -9,10 +18,20 @@ use serde::{Deserialize, Serialize}; use std::collections::VecDeque; use std::fs::File; use std::io::{BufWriter, Seek, Write}; +use wasmtime_environ::{WasmFuncType, WasmValType}; const VAL_RAW_SIZE: usize = mem::size_of::(); -/// Transmutable byte arrays necessary to serialize unions +type RRFuncArgVals = Vec; +type RRFuncArgTypes = WasmFuncType; + +fn raw_to_func_argvals(args: &[MaybeUninit]) -> RRFuncArgVals { + args.iter() + .map(|x| unsafe { ValRawSer::from(x.assume_init()) }) + .collect::>() +} + +/// Transmutable byte array used to serialize [`ValRaw`] union #[derive(Serialize, Deserialize)] pub struct ValRawSer([u8; VAL_RAW_SIZE]); @@ -33,25 +52,44 @@ impl fmt::Debug for ValRawSer { } } -/// A single recording/replay event +/// Arguments for function call/return events +#[derive(Debug, Serialize, Deserialize)] +pub struct RRFuncArgs { + /// Raw values passed across the call/return boundary + args: RRFuncArgVals, + /// Optional param/return types (required to support replay validation) + types: Option, +} + +/// A single, low-level recording/replay event +/// +/// A high-level event (e.g. import calls consisting of lifts and lowers +/// of parameter/return types) may consist of multiple of these lower-level +/// [`RREvent`]s #[derive(Debug, Serialize, Deserialize)] pub enum RREvent { - ExternCall(Vec), - ExternReturn(Vec), + /// A function call from Wasm to Host + HostFuncEntry(RRFuncArgs), + /// A function return from Host to Wasm. + /// + /// Matches 1:1 with a prior [`RREvent::HostFuncEntry`] event + HostFuncReturn(RRFuncArgs), } impl RREvent { - fn raw_to_vec(args: &[MaybeUninit]) -> Vec { - args.iter() - .map(|x| unsafe { ValRawSer::from(x.assume_init()) }) - .collect::>() - } - - pub fn extern_call_from_valraw_slice(args: &[MaybeUninit]) -> Self { - Self::ExternCall(Self::raw_to_vec(args)) + /// Construct a [`RREvent::HostFuncEntry`] event from raw slice + pub fn host_func_entry(args: &[MaybeUninit], types: Option) -> Self { + Self::HostFuncEntry(RRFuncArgs { + args: raw_to_func_argvals(args), + types: types, + }) } - pub fn extern_return_from_valraw_slice(args: &[MaybeUninit]) -> Self { - Self::ExternReturn(Self::raw_to_vec(args)) + /// Construct a [`RREvent::HostFuncReturn`] event from raw slice + pub fn host_func_return(args: &[MaybeUninit], types: Option) -> Self { + Self::HostFuncReturn(RRFuncArgs { + args: raw_to_func_argvals(args), + types: types, + }) } } @@ -75,13 +113,11 @@ impl RRBuffer { pub fn read_fs(path: String) -> Result { let mut file = File::open(path)?; let mut events = VecDeque::::new(); + // Read till EOF while file.stream_position()? != file.metadata()?.len() { let (event, _): (RREvent, _) = postcard::from_io((&mut file, &mut [0; 0]))?; events.push_back(event); } - // Check that file is at EOF - //assert_eq!(file.stream_position()?, file.metadata()?.len()); - println!("Read from file: {:?}", events); Ok(RRBuffer { inner: events, rw: file, @@ -104,7 +140,6 @@ impl RRBuffer { /// /// Buffer is emptied during this process pub fn flush_to_file(&mut self) -> Result<()> { - println!("Flushing to file: {:?}", self.inner); // Seralizing each event independently prevents checking for vector sizes // during deserialization for v in &self.inner { diff --git a/src/commands/run.rs b/src/commands/run.rs index ab814fd792fd..315e94eab067 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -108,8 +108,8 @@ impl RunCommand { } None => {} } - let rr = &self.run.rr; - config.rr(&rr.record, &rr.replay); + let rr = self.run.rr.clone(); + config.rr(rr.record, rr.replay); let engine = Engine::new(&config)?; From 94364b04fd986c4e3b250f87bd2b1dcd8fe60103 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Wed, 18 Jun 2025 18:39:36 -0400 Subject: [PATCH 09/62] Refactor RR infrastructure * Integrated `Recorder` and `Replayer` traits * Supported validation in configs and CLI --- crates/cli-flags/src/lib.rs | 139 ++++++++++++++++++++---- crates/wasmtime/src/config.rs | 79 +++++++++----- crates/wasmtime/src/runtime/func.rs | 5 +- crates/wasmtime/src/runtime/rr.rs | 152 +++++++++++++++++++-------- crates/wasmtime/src/runtime/store.rs | 14 +-- src/commands/run.rs | 2 - src/common.rs | 5 +- 7 files changed, 288 insertions(+), 108 deletions(-) diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs index 3d08ffdbea25..a501413c4fb2 100644 --- a/crates/cli-flags/src/lib.rs +++ b/crates/cli-flags/src/lib.rs @@ -1,14 +1,14 @@ //! Contains the common Wasmtime command line interface (CLI) flags. use anyhow::{Context, Result}; -use clap::{Args, Parser}; +use clap::Parser; use serde::Deserialize; use std::{ fmt, fs, path::{Path, PathBuf}, time::Duration, }; -use wasmtime::Config; +use wasmtime::{Config, RRConfig, RecordConfig, ReplayConfig}; pub mod opt; @@ -478,6 +478,38 @@ wasmtime_option_group! { } } +wasmtime_option_group! { + #[derive(PartialEq, Clone, Deserialize)] + #[serde(rename_all = "kebab-case", deny_unknown_fields)] + pub struct RecordOptions { + /// Filesystem endpoint to store the recorded execution trace + pub path: Option, + /// Include (optional) signatures to facilitate validation checks during replay + /// (see `validate` in replay options). + pub validation_metadata: Option, + } + + enum Record { + ... + } +} + +wasmtime_option_group! { + #[derive(PartialEq, Clone, Deserialize)] + #[serde(rename_all = "kebab-case", deny_unknown_fields)] + pub struct ReplayOptions { + /// Filesystem endpoint to read the recorded execution trace from + pub path: Option, + /// Dynamic validation checks of record signatures to guarantee faithful replay. + /// Requires record traces to be generated with `validation_metadata` enabled. + pub validate: Option, + } + + enum Replay { + ... + } +} + #[derive(Debug, Clone, PartialEq)] pub struct WasiNnGraph { pub format: String, @@ -528,6 +560,29 @@ pub struct CommonOptions { #[serde(skip)] wasi_raw: Vec>, + /// Options to enable and configure execution recording, `-R help` to see all. + /// + /// Generates of 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-free manner (see the `--replay` option). + /// + /// Note: Minimal configs 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>, + + /// Options to enable and configure execution replay, `-P help` to see all. + /// + /// Run a determinstic, embedding-free replay execution of the Wasm module + /// according to a prior recorded execution trace (see the `--record` option). + /// + /// Note: Minimal configs for deterministic Wasm semantics will be + /// enforced during replay by default (NaN canonicalization, deterministic relaxed SIMD) + #[arg(short = 'P', long = "replay", value_name = "KEY[=VAL[,..]]")] + #[serde(skip)] + replay_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)] @@ -554,6 +609,14 @@ pub struct CommonOptions { #[serde(rename = "wasi", default)] pub wasi: WasiOptions, + #[arg(skip)] + #[serde(rename = "record", default)] + pub record: RecordOptions, + + #[arg(skip)] + #[serde(rename = "replay", default)] + pub replay: ReplayOptions, + /// The target triple; default is the host triple #[arg(long, value_name = "TARGET")] #[serde(skip)] @@ -600,12 +663,16 @@ impl CommonOptions { debug_raw: Vec::new(), wasm_raw: Vec::new(), wasi_raw: Vec::new(), + record_raw: Vec::new(), + replay_raw: Vec::new(), configured: true, opts: Default::default(), codegen: Default::default(), debug: Default::default(), wasm: Default::default(), wasi: Default::default(), + record: Default::default(), + replay: Default::default(), target: None, config: None, } @@ -623,12 +690,16 @@ impl CommonOptions { self.debug = toml_options.debug; self.wasm = toml_options.wasm; self.wasi = toml_options.wasi; + self.record = toml_options.record; + self.replay = toml_options.replay; } 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); + self.replay.configure_with(&self.replay_raw); Ok(()) } @@ -970,6 +1041,23 @@ impl CommonOptions { true => err, } + let record = &self.record; + let replay = &self.replay; + let rr_cfg = if let Some(path) = &record.path { + Some(RRConfig::Record(RecordConfig { + path: path.clone(), + validation_metadata: record.validation_metadata.unwrap_or(true), + })) + } else if let Some(path) = &replay.path { + Some(RRConfig::Replay(ReplayConfig { + path: path.clone(), + validate: replay.validate.unwrap_or(true), + })) + } else { + None + }; + config.rr(rr_cfg); + Ok(config) } @@ -1074,6 +1162,8 @@ mod tests { [debug] [wasm] [wasi] + [record] + [replay] "#; let mut common_options: CommonOptions = toml::from_str(basic_toml).unwrap(); common_options.config(None).unwrap(); @@ -1195,6 +1285,10 @@ impl fmt::Display for CommonOptions { wasm, wasi_raw, wasi, + record_raw, + record, + replay_raw, + replay, configured, target, config, @@ -1211,6 +1305,8 @@ impl fmt::Display for CommonOptions { let wasi_flags; let wasm_flags; let debug_flags; + let record_flags; + let replay_flags; if *configured { codegen_flags = codegen.to_options(); @@ -1218,6 +1314,8 @@ 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(); + replay_flags = replay.to_options(); } else { codegen_flags = codegen_raw .iter() @@ -1228,6 +1326,16 @@ 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(); + replay_flags = replay_raw + .iter() + .flat_map(|t| t.0.iter()) + .cloned() + .collect(); } for flag in codegen_flags { @@ -1245,28 +1353,13 @@ impl fmt::Display for CommonOptions { for flag in debug_flags { write!(f, "-D{flag} ")?; } + for flag in record_flags { + write!(f, "-R{flag} ")?; + } + for flag in replay_flags { + write!(f, "-P{flag} ")?; + } Ok(()) } } - -#[derive(Args, Clone)] -#[group(multiple = false)] -pub struct RROptions { - /// Record the module execution - /// - /// Enabling this option will produce a Trace on module execution in the provided - /// endpoint. This trace can then subsequently be passed to the `--replay` generate - /// a equivalent run of the program. - /// - /// Note that determinism will be enforced during recording by default (NaN canonicalization) - #[arg(long, value_name = "TRACE_PATH")] - pub record: Option, - - /// Run a replay of the module according to a Trace file - /// - /// Replay executions will always be deterministic, and will mock all invoked - /// host calls made by the module with the respective trace results. - #[arg(long, value_name = "TRACE_PATH")] - pub replay: Option, -} diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 3ec4823ba2c7..543834108a50 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -220,42 +220,60 @@ impl Default for CompilerConfig { } } -/// Configuration for record/replay targets +/// Configuration for recording execution +#[derive(Debug, Clone)] +pub struct RecordConfig { + /// Filesystem path to write record trace + pub path: String, + /// Flag to include additional signatures for replay validation + pub validation_metadata: bool, +} + +/// Configuration for replay execution +#[derive(Debug, Clone)] +pub struct ReplayConfig { + /// Filesystem path to read trace from + pub path: String, + /// Flag for dynamic validation checks when replaying events + pub validate: bool, +} + +/// Configurations for record/replay (RR) executions #[derive(Debug, Clone)] pub enum RRConfig { - /// Recording trace filepath - Record(String), - /// Replay trace filepath - Replay(String), + /// Record configuration + Record(RecordConfig), + /// Replay configuration + Replay(ReplayConfig), } impl RRConfig { /// Test if execution recording is enabled (i.e, variant [`RRConfig::Record`]), and wrap - /// the corresponding path - pub fn record(&self) -> Option<&String> { + /// the corresponding config + pub fn record(&self) -> Option<&RecordConfig> { match self { - Self::Record(p) => Some(p), + Self::Record(r) => Some(r), _ => None, } } /// Extract the record path. Panics if not a [`RRConfig::Record`] - pub fn record_unwrap(&self) -> &String { + pub fn record_unwrap(&self) -> &RecordConfig { self.record() - .expect("missing path to recording trace (specify `--record` option)") + .expect("use of incorrectly initialized record configuration") } /// Test if execution replay is enabled (i.e. variant [`RRConfig::Replay`]), and wrap - /// the corresponding path - pub fn replay(&self) -> Option<&String> { + /// the corresponding config + pub fn replay(&self) -> Option<&ReplayConfig> { match self { - Self::Replay(p) => Some(p), + Self::Replay(r) => Some(r), _ => None, } } - /// Extract the replay path. Panics if not a [`RRConfig::Replay`] - pub fn replay_unwrap(&self) -> &String { + /// Extract the replay config. Panics if not a [`RRConfig::Replay`] + pub fn replay_unwrap(&self) -> &ReplayConfig { self.replay() - .expect("missing path to recording trace (specify `--record` option)") + .expect("use of incorrectly initialized replay configuration") } } @@ -2687,6 +2705,15 @@ impl Config { self } + /// Repeal determinstic execution configurations (opposite + /// effect of [`Config::enforce_determinism`]) + #[inline] + pub fn repeal_determinism(&mut self) -> &mut Self { + self.cranelift_nan_canonicalization(false) + .relaxed_simd_deterministic(false); + self + } + /// Evaluates to true if current configuration must respect /// deterministic execution in its configuration /// @@ -2699,21 +2726,15 @@ impl Config { /// Configure the record/replay options for use by the runtime /// /// This method implicitly enforces determinism (see [`Config::enforce_determinism`] - /// for details). Panics if both record and replay are set simultaneously - pub fn rr(&mut self, record_path: Option, replay_path: Option) -> &mut Self { - self.rr = match (record_path, replay_path) { - // Should be unreachable - (Some(_), Some(_)) => { - panic!("Cannot set both record and replay simultaneously for execution") - } - (Some(p), None) => Some(RRConfig::Record(p)), - (None, Some(p)) => Some(RRConfig::Replay(p)), - _ => None, - }; + /// for details). + pub fn rr(&mut self, rr: Option) -> &mut Self { // Set appropriate configurations for determinstic execution - if self.rr.is_some() { + if rr.is_some() { self.enforce_determinism(); - }; + } else if self.rr.is_some() { + self.repeal_determinism(); + } + self.rr = rr; self } } diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 8e3292e3ad54..b3984f146aa4 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -1,4 +1,5 @@ use crate::prelude::*; +use crate::rr::{Recorder, Replayer}; use crate::runtime::Uninhabited; use crate::runtime::rr::RREvent; use crate::runtime::vm::{ @@ -2318,13 +2319,13 @@ impl HostContext { let record_buffer = caller.store.0.record_buffer_mut(); if let Some(buf) = record_buffer { println!("Record | {:?}", &event); - buf.append(event); + buf.push_event(event); } }; let replay_intercept = |caller: &mut Caller<'_, T>| -> Option { let replay_buffer = caller.store.0.replay_buffer_mut(); replay_buffer.and_then(|buf| { - let call_event = buf.pop_front(); + let call_event = buf.pop_event().unwrap(); println!("Replay | {:?}", &call_event); Some(call_event) }) diff --git a/crates/wasmtime/src/runtime/rr.rs b/crates/wasmtime/src/runtime/rr.rs index 59691b501c34..e3faef548e5f 100644 --- a/crates/wasmtime/src/runtime/rr.rs +++ b/crates/wasmtime/src/runtime/rr.rs @@ -3,11 +3,9 @@ //! This feature is currently experimental and hence not optimized. //! In particular, the following opportunities are immediately identifiable: //! * Switch [RRFuncArgTypes] to use [Vec] -//! -//! Flexibility can also be improved with: -//! * Support for generic writers beyond [File] (will require a generic on [Store]) use crate::ValRaw; +use crate::config::{RecordConfig, ReplayConfig}; use crate::prelude::*; #[allow(unused_imports)] use crate::runtime::Store; @@ -31,6 +29,51 @@ fn raw_to_func_argvals(args: &[MaybeUninit]) -> RRFuncArgVals { .collect::>() } +#[derive(Debug)] +pub enum ReplayError { + EmptyBuffer, + FailedValidation, +} + +impl fmt::Display for ReplayError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::EmptyBuffer => { + write!(f, "replay buffer is empty!") + } + Self::FailedValidation => { + write!(f, "replay event validation check failed") + } + } + } +} + +impl std::error::Error for ReplayError {} + +pub trait Recorder: Sized { + /// Constructs a writer on new buffer + fn new_recorder(cfg: RecordConfig) -> Result; + + /// Push a newly record event [`RREvent`] to the buffer + fn push_event(&mut self, event: RREvent) -> (); + + /// Flush memory contents to underlying persistent storage + /// + /// Buffer should be emptied during this process + fn flush_to_file(&mut self) -> Result<()>; +} + +pub trait Replayer: Sized { + type ReplayError; + + /// Constructs a reader on buffer + fn new_replayer(cfg: ReplayConfig) -> Result; + + /// Pop the next [`RREvent`] from the buffer + /// Events should be FIFO + fn pop_event(&mut self) -> Result; +} + /// Transmutable byte array used to serialize [`ValRaw`] union #[derive(Serialize, Deserialize)] pub struct ValRawSer([u8; VAL_RAW_SIZE]); @@ -93,60 +136,87 @@ impl RREvent { } } -/// Buffer to read/write record/replay data respectively +/// The underlying serialized/deserialized type +type RRBufferData = VecDeque; + +/// Common data for recorders and replayers +/// +/// Flexibility of this struct can also be improved with: +/// * Support for generic writers beyond [File] (will require a generic on [Store]) #[derive(Debug)] -pub struct RRBuffer { - inner: VecDeque, +pub struct RRDataCommon { + /// Ordered list of record/replay events + buf: RRBufferData, + /// Persistent storage-backed handle rw: File, } -impl RRBuffer { - /// Constructs a writer on new, filesystem-backed buffer (record) - pub fn write_fs(path: String) -> Result { - Ok(RRBuffer { - inner: VecDeque::new(), - rw: File::create(path)?, +#[derive(Debug)] +/// Buffer to write recording data +pub struct RecordBuffer { + data: RRDataCommon, + validation_metadata: bool, +} + +impl Recorder for RecordBuffer { + fn new_recorder(cfg: RecordConfig) -> Result { + Ok(RecordBuffer { + data: RRDataCommon { + buf: VecDeque::new(), + rw: File::create(cfg.path)?, + }, + validation_metadata: cfg.validation_metadata, }) } - /// Constructs a reader on filesystem-backed buffer (replay) - pub fn read_fs(path: String) -> Result { - let mut file = File::open(path)?; + fn push_event(&mut self, event: RREvent) { + self.data.buf.push_back(event) + } + + fn flush_to_file(&mut self) -> Result<()> { + // Seralizing each event independently prevents checking for vector sizes + // during deserialization + let data = &mut self.data; + for v in &data.buf { + postcard::to_io(&v, &mut data.rw)?; + } + data.rw.flush()?; + data.buf.clear(); + Ok(()) + } +} + +#[derive(Debug)] +/// Buffer to read replay data +pub struct ReplayBuffer { + data: RRDataCommon, + validate: bool, +} + +impl Replayer for ReplayBuffer { + type ReplayError = ReplayError; + + fn new_replayer(cfg: ReplayConfig) -> Result { + let mut file = File::open(cfg.path)?; let mut events = VecDeque::::new(); // Read till EOF while file.stream_position()? != file.metadata()?.len() { let (event, _): (RREvent, _) = postcard::from_io((&mut file, &mut [0; 0]))?; events.push_back(event); } - Ok(RRBuffer { - inner: events, - rw: file, + Ok(ReplayBuffer { + data: RRDataCommon { + buf: events, + rw: file, + }, + validate: cfg.validate, }) } - /// Appends a new [`RREvent`] to the buffer (record) - pub fn append(&mut self, event: RREvent) { - self.inner.push_back(event) - } - - /// Retrieve the head of the buffer (replay) - pub fn pop_front(&mut self) -> RREvent { - self.inner + fn pop_event(&mut self) -> Result { + self.data + .buf .pop_front() - .expect("Incomplete replay trace. Event buffer is empty prior to completion") - } - - /// Flush all the contents of the entire buffer to a writer - /// - /// Buffer is emptied during this process - pub fn flush_to_file(&mut self) -> Result<()> { - // Seralizing each event independently prevents checking for vector sizes - // during deserialization - for v in &self.inner { - postcard::to_io(&v, &mut self.rw)?; - } - self.rw.flush()?; - self.inner.clear(); - Ok(()) + .ok_or(Self::ReplayError::EmptyBuffer.into()) } } diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index 84fd58403433..d5f173d2445f 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -79,7 +79,7 @@ use crate::RootSet; use crate::module::RegisteredModuleId; use crate::prelude::*; -use crate::runtime::rr::RRBuffer; +use crate::runtime::rr::{RecordBuffer, Recorder, ReplayBuffer, Replayer}; #[cfg(feature = "gc")] use crate::runtime::vm::GcRootsList; #[cfg(feature = "stack-switching")] @@ -399,11 +399,11 @@ pub struct StoreOpaque { /// Storage for recording execution /// /// `None` implies recording is disabled for this store - record_buffer: Option, + record_buffer: Option, /// Storage for replaying execution /// /// `None` implies replay is disabled for this store - replay_buffer: Option, + replay_buffer: Option, } /// Executor state within `StoreOpaque`. @@ -590,11 +590,11 @@ impl Store { }, record_buffer: engine.rr().and_then(|rr| { rr.record() - .and_then(|x| Some(RRBuffer::write_fs(x.into()).unwrap())) + .and_then(|record| Some(RecordBuffer::new_recorder(record.clone()).unwrap())) }), replay_buffer: engine.rr().and_then(|rr| { rr.replay() - .and_then(|x| Some(RRBuffer::read_fs(x.into()).unwrap())) + .and_then(|replay| Some(ReplayBuffer::new_replayer(replay.clone()).unwrap())) }), }; let mut inner = Box::new(StoreInner { @@ -1353,12 +1353,12 @@ impl StoreOpaque { } #[inline] - pub(crate) fn record_buffer_mut(&mut self) -> Option<&mut RRBuffer> { + pub(crate) fn record_buffer_mut(&mut self) -> Option<&mut RecordBuffer> { self.record_buffer.as_mut() } #[inline] - pub(crate) fn replay_buffer_mut(&mut self) -> Option<&mut RRBuffer> { + pub(crate) fn replay_buffer_mut(&mut self) -> Option<&mut ReplayBuffer> { self.replay_buffer.as_mut() } diff --git a/src/commands/run.rs b/src/commands/run.rs index 315e94eab067..d71a685b72a2 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -108,8 +108,6 @@ impl RunCommand { } None => {} } - let rr = self.run.rr.clone(); - config.rr(rr.record, rr.replay); let engine = Engine::new(&config)?; diff --git a/src/common.rs b/src/common.rs index 3ffa2c1f85d0..385ee1c4e48b 100644 --- a/src/common.rs +++ b/src/common.rs @@ -5,7 +5,7 @@ use clap::Parser; use std::net::TcpListener; use std::{fs::File, path::Path, time::Duration}; use wasmtime::{Engine, Module, Precompiled, StoreLimits, StoreLimitsBuilder}; -use wasmtime_cli_flags::{CommonOptions, RROptions, opt::WasmtimeOptionValue}; +use wasmtime_cli_flags::{CommonOptions, opt::WasmtimeOptionValue}; use wasmtime_wasi::p2::WasiCtxBuilder; use wasmtime_wasi::p2::bindings::LinkOptions; @@ -43,9 +43,6 @@ pub struct RunCommon { #[command(flatten)] pub common: CommonOptions, - #[command(flatten)] - pub rr: RROptions, - /// Allow executing precompiled WebAssembly modules as `*.cwasm` files. /// /// Note that this option is not safe to pass if the module being passed in From 34e78a1d3fb292a0f11fcdef0eb57d62c38c31e7 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Fri, 20 Jun 2025 14:58:04 -0400 Subject: [PATCH 10/62] Add compressed validation/no-validation for Record --- crates/cli-flags/src/lib.rs | 22 +++++----- crates/wasmtime/src/config.rs | 60 ++++++++++++++++++++++++---- crates/wasmtime/src/runtime/func.rs | 49 ++++++++++------------- crates/wasmtime/src/runtime/rr.rs | 30 +++++++++----- crates/wasmtime/src/runtime/store.rs | 15 ++++++- 5 files changed, 119 insertions(+), 57 deletions(-) diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs index a501413c4fb2..d197b57c32cb 100644 --- a/crates/cli-flags/src/lib.rs +++ b/crates/cli-flags/src/lib.rs @@ -8,7 +8,7 @@ use std::{ path::{Path, PathBuf}, time::Duration, }; -use wasmtime::{Config, RRConfig, RecordConfig, ReplayConfig}; +use wasmtime::{Config, RRConfig, RecordMetadata, ReplayMetadata}; pub mod opt; @@ -1044,15 +1044,19 @@ impl CommonOptions { let record = &self.record; let replay = &self.replay; let rr_cfg = if let Some(path) = &record.path { - Some(RRConfig::Record(RecordConfig { - path: path.clone(), - validation_metadata: record.validation_metadata.unwrap_or(true), - })) + Some(RRConfig::record_cfg( + path.clone(), + Some(RecordMetadata { + add_validation: record.validation_metadata.unwrap_or(true), + }), + )) } else if let Some(path) = &replay.path { - Some(RRConfig::Replay(ReplayConfig { - path: path.clone(), - validate: replay.validate.unwrap_or(true), - })) + Some(RRConfig::replay_cfg( + path.clone(), + Some(ReplayMetadata { + validate: replay.validate.unwrap_or(true), + }), + )) } else { None }; diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 543834108a50..98728248e2b1 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -220,13 +220,41 @@ impl Default for CompilerConfig { } } +/// Metadata for specifying recording strategy +#[derive(Debug, Clone)] +pub struct RecordMetadata { + /// Flag to include additional signatures for replay validation + pub add_validation: bool, +} + +impl Default for RecordMetadata { + fn default() -> Self { + Self { + add_validation: true, + } + } +} + /// Configuration for recording execution #[derive(Debug, Clone)] pub struct RecordConfig { /// Filesystem path to write record trace - pub path: String, + pub(crate) path: String, + /// Associated metadata for configuring the recording strategy + pub(crate) metadata: RecordMetadata, +} + +/// Metadata for specifying replay strategy +#[derive(Debug, Clone)] +pub struct ReplayMetadata { /// Flag to include additional signatures for replay validation - pub validation_metadata: bool, + pub validate: bool, +} + +impl Default for ReplayMetadata { + fn default() -> Self { + Self { validate: true } + } } /// Configuration for replay execution @@ -235,7 +263,7 @@ pub struct ReplayConfig { /// Filesystem path to read trace from pub path: String, /// Flag for dynamic validation checks when replaying events - pub validate: bool, + pub metadata: ReplayMetadata, } /// Configurations for record/replay (RR) executions @@ -248,22 +276,38 @@ pub enum RRConfig { } impl RRConfig { - /// Test if execution recording is enabled (i.e, variant [`RRConfig::Record`]), and wrap - /// the corresponding config + /// Construct a record ([`RRConfig::Record`]) configuration. + /// If `metadata` is None, uses [`RecordMetadata::default()`] + pub fn record_cfg(path: String, metadata: Option) -> Self { + Self::Record(RecordConfig { + path, + metadata: metadata.unwrap_or(RecordMetadata::default()), + }) + } + + /// Construct a replay ([`RRConfig::Replay`]) configuration. + /// If `metadata` is None, uses [`ReplayMetadata::default()`] + pub fn replay_cfg(path: String, metadata: Option) -> Self { + Self::Replay(ReplayConfig { + path, + metadata: metadata.unwrap_or(ReplayMetadata::default()), + }) + } + + /// Wrap the record config in [`RRConfig::Record`] pub fn record(&self) -> Option<&RecordConfig> { match self { Self::Record(r) => Some(r), _ => None, } } - /// Extract the record path. Panics if not a [`RRConfig::Record`] + /// Extract the record config. Panics if not a [`RRConfig::Record`] pub fn record_unwrap(&self) -> &RecordConfig { self.record() .expect("use of incorrectly initialized record configuration") } - /// Test if execution replay is enabled (i.e. variant [`RRConfig::Replay`]), and wrap - /// the corresponding config + /// Wrap the replay config in [`RRConfig::Replay`] pub fn replay(&self) -> Option<&ReplayConfig> { match self { Self::Replay(r) => Some(r), diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index b3984f146aa4..73a78357697b 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -1,5 +1,5 @@ use crate::prelude::*; -use crate::rr::{Recorder, Replayer}; +use crate::rr::Replayer; use crate::runtime::Uninhabited; use crate::runtime::rr::RREvent; use crate::runtime::vm::{ @@ -2315,13 +2315,6 @@ impl HostContext { R: WasmRet, T: 'static, { - let record_intercept = |caller: &mut Caller<'_, T>, event| { - let record_buffer = caller.store.0.record_buffer_mut(); - if let Some(buf) = record_buffer { - println!("Record | {:?}", &event); - buf.push_event(event); - } - }; let replay_intercept = |caller: &mut Caller<'_, T>| -> Option { let replay_buffer = caller.store.0.replay_buffer_mut(); replay_buffer.and_then(|buf| { @@ -2349,13 +2342,7 @@ impl HostContext { let func = &state.func; let type_index = vmctx.func_ref().type_index; - let wasm_func_type_arc = caller - .store - .0 - .engine() - .signatures() - .borrow(type_index) - .unwrap(); + let wasm_func_type_arc = caller.engine().signatures().borrow(type_index).unwrap(); let wasm_func_type = wasm_func_type_arc.unwrap_func(); let ret = 'ret: { @@ -2363,13 +2350,14 @@ impl HostContext { break 'ret R::fallible_from_error(trap); } // RR recording/replay interception on call - { - record_intercept( - &mut caller, - RREvent::host_func_entry(args.as_ref(), Some(wasm_func_type.clone())), - ); - let _event = replay_intercept(&mut caller); - } + caller.as_context_mut().0.record_event(|r| { + let num_params = wasm_func_type.params().len(); + RREvent::host_func_entry( + unsafe { &args.as_ref()[..num_params] }, + r.add_validation.then_some(wasm_func_type.clone()), + ) + }); + let _event = replay_intercept(&mut caller); let mut store = if P::may_gc() { AutoAssertNoGc::new(caller.store.0) @@ -2398,14 +2386,17 @@ impl HostContext { }; let ret = ret.store(&mut store, args.as_mut())?; drop(store); + // RR recording/replay interception on return - { - record_intercept( - &mut caller, - RREvent::host_func_return(args.as_ref(), Some(wasm_func_type.clone())), - ); - let _event = replay_intercept(&mut caller); - } + caller.as_context_mut().0.record_event(|r| { + let num_results = wasm_func_type.params().len(); + RREvent::host_func_return( + unsafe { &args.as_ref()[..num_results] }, + r.add_validation.then_some(wasm_func_type.clone()), + ) + }); + let _event = replay_intercept(&mut caller); + Ok(ret) } }; diff --git a/crates/wasmtime/src/runtime/rr.rs b/crates/wasmtime/src/runtime/rr.rs index e3faef548e5f..f402e8ef778e 100644 --- a/crates/wasmtime/src/runtime/rr.rs +++ b/crates/wasmtime/src/runtime/rr.rs @@ -5,7 +5,7 @@ //! * Switch [RRFuncArgTypes] to use [Vec] use crate::ValRaw; -use crate::config::{RecordConfig, ReplayConfig}; +use crate::config::{RecordConfig, RecordMetadata, ReplayConfig, ReplayMetadata}; use crate::prelude::*; #[allow(unused_imports)] use crate::runtime::Store; @@ -50,9 +50,11 @@ impl fmt::Display for ReplayError { impl std::error::Error for ReplayError {} -pub trait Recorder: Sized { +pub trait Recorder { /// Constructs a writer on new buffer - fn new_recorder(cfg: RecordConfig) -> Result; + fn new_recorder(cfg: RecordConfig) -> Result + where + Self: Sized; /// Push a newly record event [`RREvent`] to the buffer fn push_event(&mut self, event: RREvent) -> (); @@ -61,13 +63,18 @@ pub trait Recorder: Sized { /// /// Buffer should be emptied during this process fn flush_to_file(&mut self) -> Result<()>; + + /// Get metadata associated with the recording process + fn metadata(&self) -> &RecordMetadata; } -pub trait Replayer: Sized { +pub trait Replayer { type ReplayError; /// Constructs a reader on buffer - fn new_replayer(cfg: ReplayConfig) -> Result; + fn new_replayer(cfg: ReplayConfig) -> Result + where + Self: Sized; /// Pop the next [`RREvent`] from the buffer /// Events should be FIFO @@ -155,7 +162,7 @@ pub struct RRDataCommon { /// Buffer to write recording data pub struct RecordBuffer { data: RRDataCommon, - validation_metadata: bool, + metadata: RecordMetadata, } impl Recorder for RecordBuffer { @@ -165,7 +172,7 @@ impl Recorder for RecordBuffer { buf: VecDeque::new(), rw: File::create(cfg.path)?, }, - validation_metadata: cfg.validation_metadata, + metadata: cfg.metadata, }) } @@ -182,15 +189,20 @@ impl Recorder for RecordBuffer { } data.rw.flush()?; data.buf.clear(); + println!("Record flush: {:?} bytes", data.rw.metadata()?.len()); Ok(()) } + + fn metadata(&self) -> &RecordMetadata { + &self.metadata + } } #[derive(Debug)] /// Buffer to read replay data pub struct ReplayBuffer { data: RRDataCommon, - validate: bool, + metadata: ReplayMetadata, } impl Replayer for ReplayBuffer { @@ -209,7 +221,7 @@ impl Replayer for ReplayBuffer { buf: events, rw: file, }, - validate: cfg.validate, + metadata: cfg.metadata, }) } diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index d5f173d2445f..b7356fec4459 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -76,10 +76,9 @@ //! contents of `StoreOpaque`. This is an invariant that we, as the authors of //! `wasmtime`, must uphold for the public interface to be safe. -use crate::RootSet; use crate::module::RegisteredModuleId; use crate::prelude::*; -use crate::runtime::rr::{RecordBuffer, Recorder, ReplayBuffer, Replayer}; +use crate::runtime::rr::{RREvent, RecordBuffer, Recorder, ReplayBuffer, Replayer}; #[cfg(feature = "gc")] use crate::runtime::vm::GcRootsList; #[cfg(feature = "stack-switching")] @@ -93,6 +92,7 @@ use crate::runtime::vm::{ use crate::trampoline::VMHostGlobalContext; use crate::{Engine, Module, Trap, Val, ValRaw, module::ModuleRegistry}; use crate::{Global, Instance, Memory, Table, Uninhabited}; +use crate::{RecordMetadata, RootSet}; use alloc::sync::Arc; use core::fmt; use core::marker; @@ -1357,6 +1357,17 @@ impl StoreOpaque { self.record_buffer.as_mut() } + pub(crate) fn record_event(&mut self, f: F) + where + F: FnOnce(&RecordMetadata) -> RREvent, + { + if let Some(buf) = self.record_buffer_mut() { + let event = f(buf.metadata()); + println!("Record | {:?}", &event); + buf.push_event(event); + } + } + #[inline] pub(crate) fn replay_buffer_mut(&mut self) -> Option<&mut ReplayBuffer> { self.replay_buffer.as_mut() From a0012dfd87f3a74fbe68c39722db7f1e9afcbbe8 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Mon, 23 Jun 2025 11:44:38 -0700 Subject: [PATCH 11/62] Added event callback closures --- crates/wasmtime/src/runtime/func.rs | 84 +++++++++++------ crates/wasmtime/src/runtime/rr.rs | 132 ++++++++++++++++++++++----- crates/wasmtime/src/runtime/store.rs | 55 +++++++++-- 3 files changed, 210 insertions(+), 61 deletions(-) diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 73a78357697b..5e041b8dab33 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -1,5 +1,4 @@ use crate::prelude::*; -use crate::rr::Replayer; use crate::runtime::Uninhabited; use crate::runtime::rr::RREvent; use crate::runtime::vm::{ @@ -2315,14 +2314,6 @@ impl HostContext { R: WasmRet, T: 'static, { - let replay_intercept = |caller: &mut Caller<'_, T>| -> Option { - let replay_buffer = caller.store.0.replay_buffer_mut(); - replay_buffer.and_then(|buf| { - let call_event = buf.pop_event().unwrap(); - println!("Replay | {:?}", &call_event); - Some(call_event) - }) - }; // Note that this function is intentionally scoped into a // separate closure. Handling traps and panics will involve // longjmp-ing from this function which means we won't run @@ -2343,21 +2334,43 @@ impl HostContext { let type_index = vmctx.func_ref().type_index; let wasm_func_type_arc = caller.engine().signatures().borrow(type_index).unwrap(); - let wasm_func_type = wasm_func_type_arc.unwrap_func(); + // A common copy of function type for typechecking avoids + // cloning for every replay interception + let wasm_func_type = caller + .store + .0 + .rr_enabled() + .then(|| wasm_func_type_arc.unwrap_func()); let ret = 'ret: { if let Err(trap) = caller.store.0.call_hook(CallHook::CallingHost) { break 'ret R::fallible_from_error(trap); } - // RR recording/replay interception on call - caller.as_context_mut().0.record_event(|r| { - let num_params = wasm_func_type.params().len(); - RREvent::host_func_entry( - unsafe { &args.as_ref()[..num_params] }, - r.add_validation.then_some(wasm_func_type.clone()), + + let store = caller.as_context_mut().0; + // Record/replay interceptions of function parameters only + // when validation is enabled. + // Function type unwraps should never panic since they are + // lazily evaluated + store.record_event( + |r| r.add_validation, + |_| { + let wasm_func_type = wasm_func_type.unwrap(); + let num_params = wasm_func_type.params().len(); + RREvent::host_func_entry( + unsafe { &args.as_ref()[..num_params] }, + // Don't need to check validation here since it is + // covered by the push predicate in this case + Some(wasm_func_type.clone()), + ) + }, + ); + store + .replay_event( + |r| r.validate, + |event, _| event.func_typecheck(wasm_func_type.unwrap()), ) - }); - let _event = replay_intercept(&mut caller); + .unwrap(); let mut store = if P::may_gc() { AutoAssertNoGc::new(caller.store.0) @@ -2379,6 +2392,31 @@ impl HostContext { if !ret.compatible_with_store(caller.store.0) { bail!("host function attempted to return cross-`Store` value to Wasm") } else { + let store = caller.as_context_mut().0; + // Always intercept return for record/replay + store.record_event( + |_| true, + |rmeta| { + let wasm_func_type = wasm_func_type.unwrap(); + let num_results = wasm_func_type.params().len(); + RREvent::host_func_return( + unsafe { &args.as_ref()[..num_results] }, + rmeta.add_validation.then_some(wasm_func_type.clone()), + ) + }, + ); + store + .replay_event( + |_| true, + |event, rmeta| { + event.move_into_slice( + args.as_mut(), + rmeta.validate.then_some(wasm_func_type.unwrap()), + ) + }, + ) + .unwrap(); + let mut store = if R::may_gc() { AutoAssertNoGc::new(caller.store.0) } else { @@ -2387,16 +2425,6 @@ impl HostContext { let ret = ret.store(&mut store, args.as_mut())?; drop(store); - // RR recording/replay interception on return - caller.as_context_mut().0.record_event(|r| { - let num_results = wasm_func_type.params().len(); - RREvent::host_func_return( - unsafe { &args.as_ref()[..num_results] }, - r.add_validation.then_some(wasm_func_type.clone()), - ) - }); - let _event = replay_intercept(&mut caller); - Ok(ret) } }; diff --git a/crates/wasmtime/src/runtime/rr.rs b/crates/wasmtime/src/runtime/rr.rs index f402e8ef778e..8301366ea08f 100644 --- a/crates/wasmtime/src/runtime/rr.rs +++ b/crates/wasmtime/src/runtime/rr.rs @@ -23,16 +23,54 @@ const VAL_RAW_SIZE: usize = mem::size_of::(); type RRFuncArgVals = Vec; type RRFuncArgTypes = WasmFuncType; +/// Transmutable byte array used to serialize [`ValRaw`] union +/// +/// Maintaining the exact layout is crucial for zero-copy transmutations +/// between [`ValRawSer`] and [`ValRaw`] +#[derive(Serialize, Deserialize)] +#[repr(C)] +pub struct ValRawSer([u8; VAL_RAW_SIZE]); + +impl From for ValRawSer { + fn from(value: ValRaw) -> Self { + unsafe { Self(mem::transmute(value)) } + } +} + +impl From for ValRaw { + fn from(value: ValRawSer) -> Self { + unsafe { mem::transmute(value.0) } + } +} + +impl fmt::Debug for ValRawSer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let hex_digits_per_byte = 2; + let _ = write!(f, "0x.."); + for b in self.0.iter().rev() { + let _ = write!(f, "{:0width$x}", b, width = hex_digits_per_byte); + } + Ok(()) + } +} + fn raw_to_func_argvals(args: &[MaybeUninit]) -> RRFuncArgVals { args.iter() .map(|x| unsafe { ValRawSer::from(x.assume_init()) }) .collect::>() } +fn func_argvals_into_raw(rr_args: RRFuncArgVals, raw_args: &mut [MaybeUninit]) { + for (src, dst) in rr_args.into_iter().zip(raw_args.iter_mut()) { + *dst = MaybeUninit::new(src.into()); + } +} + #[derive(Debug)] pub enum ReplayError { EmptyBuffer, - FailedValidation, + FailedFuncValidation, + IncorrectEventVariant, } impl fmt::Display for ReplayError { @@ -41,8 +79,11 @@ impl fmt::Display for ReplayError { Self::EmptyBuffer => { write!(f, "replay buffer is empty!") } - Self::FailedValidation => { - write!(f, "replay event validation check failed") + Self::FailedFuncValidation => { + write!(f, "func replay event typecheck validation failed") + } + Self::IncorrectEventVariant => { + write!(f, "event methods invoked on incorrect variant") } } } @@ -79,27 +120,9 @@ pub trait Replayer { /// Pop the next [`RREvent`] from the buffer /// Events should be FIFO fn pop_event(&mut self) -> Result; -} - -/// Transmutable byte array used to serialize [`ValRaw`] union -#[derive(Serialize, Deserialize)] -pub struct ValRawSer([u8; VAL_RAW_SIZE]); -impl From for ValRawSer { - fn from(value: ValRaw) -> Self { - unsafe { Self(mem::transmute(value)) } - } -} - -impl fmt::Debug for ValRawSer { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let hex_digits_per_byte = 2; - let _ = write!(f, "0x.."); - for b in self.0.iter().rev() { - let _ = write!(f, "{:0width$x}", b, width = hex_digits_per_byte); - } - Ok(()) - } + /// Get metadata associated with the replay process + fn metadata(&self) -> &ReplayMetadata; } /// Arguments for function call/return events @@ -111,6 +134,27 @@ pub struct RRFuncArgs { types: Option, } +impl RRFuncArgs { + /// Typecheck the types field, if it exists + /// + /// Errors with a [`ReplayError::FailedFuncValidation`] if typechecking fails + pub fn typecheck(&self, expect_types: &WasmFuncType) -> Result<(), ReplayError> { + if let Some(types) = &self.types { + if types == expect_types { + Ok(()) + } else { + Err(ReplayError::FailedFuncValidation) + } + } else { + println!( + "Warning: Replay typechecking cannot be performed + since recorded trace is missing validation data" + ); + Ok(()) + } + } +} + /// A single, low-level recording/replay event /// /// A high-level event (e.g. import calls consisting of lifts and lowers @@ -141,6 +185,37 @@ impl RREvent { types: types, }) } + + /// Typecheck the function signature for validation + /// + /// Errors with a [`ReplayError::IncorrectEventVariant`] if not + /// a func variant or a [`ReplayError::FailedFuncValidation`] if typechecking fails + pub fn func_typecheck(&self, expect_types: &WasmFuncType) -> Result<(), ReplayError> { + match self { + Self::HostFuncEntry(func_args) | Self::HostFuncReturn(func_args) => { + func_args.typecheck(expect_types) + } + _ => Err(ReplayError::IncorrectEventVariant), + } + } + + /// Consume the caller event and encode it back into the slice with an optional + /// typechecking validation of the event. + pub fn move_into_slice( + self, + args: &mut [MaybeUninit], + expect_types: Option<&WasmFuncType>, + ) -> Result<(), ReplayError> { + match self { + Self::HostFuncEntry(func_args) | Self::HostFuncReturn(func_args) => { + if let Some(e) = expect_types { + func_args.typecheck(e)?; + } + func_argvals_into_raw(func_args.args, args); + } + }; + Ok(()) + } } /// The underlying serialized/deserialized type @@ -189,10 +264,14 @@ impl Recorder for RecordBuffer { } data.rw.flush()?; data.buf.clear(); - println!("Record flush: {:?} bytes", data.rw.metadata()?.len()); + println!( + "Record flush | File size: {:?} bytes", + data.rw.metadata()?.len() + ); Ok(()) } + #[inline] fn metadata(&self) -> &RecordMetadata { &self.metadata } @@ -231,4 +310,9 @@ impl Replayer for ReplayBuffer { .pop_front() .ok_or(Self::ReplayError::EmptyBuffer.into()) } + + #[inline] + fn metadata(&self) -> &ReplayMetadata { + &self.metadata + } } diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index b7356fec4459..0e850ca68acf 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -76,9 +76,10 @@ //! contents of `StoreOpaque`. This is an invariant that we, as the authors of //! `wasmtime`, must uphold for the public interface to be safe. +use crate::RootSet; use crate::module::RegisteredModuleId; use crate::prelude::*; -use crate::runtime::rr::{RREvent, RecordBuffer, Recorder, ReplayBuffer, Replayer}; +use crate::runtime::rr::{RREvent, RecordBuffer, Recorder, ReplayBuffer, ReplayError, Replayer}; #[cfg(feature = "gc")] use crate::runtime::vm::GcRootsList; #[cfg(feature = "stack-switching")] @@ -92,7 +93,7 @@ use crate::runtime::vm::{ use crate::trampoline::VMHostGlobalContext; use crate::{Engine, Module, Trap, Val, ValRaw, module::ModuleRegistry}; use crate::{Global, Instance, Memory, Table, Uninhabited}; -use crate::{RecordMetadata, RootSet}; +use crate::{RecordMetadata, ReplayMetadata}; use alloc::sync::Arc; use core::fmt; use core::marker; @@ -1353,24 +1354,60 @@ impl StoreOpaque { } #[inline] - pub(crate) fn record_buffer_mut(&mut self) -> Option<&mut RecordBuffer> { + fn record_buffer_mut(&mut self) -> Option<&mut RecordBuffer> { self.record_buffer.as_mut() } - pub(crate) fn record_event(&mut self, f: F) + #[inline] + fn replay_buffer_mut(&mut self) -> Option<&mut ReplayBuffer> { + self.replay_buffer.as_mut() + } + + /// Record the event generated by `F` if `P` holds true + pub(crate) fn record_event(&mut self, push_predicate: P, f: F) where + P: FnOnce(&RecordMetadata) -> bool, F: FnOnce(&RecordMetadata) -> RREvent, { if let Some(buf) = self.record_buffer_mut() { - let event = f(buf.metadata()); - println!("Record | {:?}", &event); - buf.push_event(event); + let metadata = buf.metadata(); + if push_predicate(metadata) { + let event = f(buf.metadata()); + println!("Record | {:?}", &event); + buf.push_event(event); + } } } + /// Get the next replay event if `P` holds true and process it with `F` + pub(crate) fn replay_event(&mut self, pop_predicate: P, f: F) -> Result<()> + where + P: FnOnce(&ReplayMetadata) -> bool, + F: FnOnce(RREvent, &ReplayMetadata) -> Result<(), ReplayError>, + { + if let Some(buf) = self.replay_buffer_mut() { + if pop_predicate(buf.metadata()) { + let call_event = buf.pop_event()?; + println!("Replay | {:?}", &call_event); + Ok(f(call_event, buf.metadata())?) + } else { + Ok(()) + } + } else { + Ok(()) + } + } + + /// Check if recording or replaying is tied to the store #[inline] - pub(crate) fn replay_buffer_mut(&mut self) -> Option<&mut ReplayBuffer> { - self.replay_buffer.as_mut() + pub fn rr_enabled(&self) -> bool { + self.record_buffer.is_some() || self.replay_buffer.is_some() + } + + /// Check if replay is enabled for the Store + #[inline] + pub fn replay_enabled(&self) -> bool { + self.replay_buffer.is_some() } #[inline(never)] From aeb3c0b4b3acd7632a88790daef7b98a1ea6c28f Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Mon, 23 Jun 2025 16:53:21 -0700 Subject: [PATCH 12/62] Add replay injection with function call stubbing on trampoline --- crates/wasmtime/src/runtime/func.rs | 121 ++++++++++++++-------------- 1 file changed, 61 insertions(+), 60 deletions(-) diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 5e041b8dab33..eaa5311a04d0 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -2342,16 +2342,22 @@ impl HostContext { .rr_enabled() .then(|| wasm_func_type_arc.unwrap_func()); - let ret = 'ret: { - if let Err(trap) = caller.store.0.call_hook(CallHook::CallingHost) { - break 'ret R::fallible_from_error(trap); - } - - let store = caller.as_context_mut().0; - // Record/replay interceptions of function parameters only - // when validation is enabled. + // Setup call parameters + let params = { + let mut store = if P::may_gc() { + AutoAssertNoGc::new(caller.store.0) + } else { + unsafe { AutoAssertNoGc::disabled(caller.store.0) } + }; + + // Record/replay interceptions of raw parameters args + // (only when validation is enabled). // Function type unwraps should never panic since they are // lazily evaluated + store.replay_event( + |r| r.validate, + |event, _| event.func_typecheck(wasm_func_type.unwrap()), + )?; store.record_event( |r| r.add_validation, |_| { @@ -2365,68 +2371,63 @@ impl HostContext { ) }, ); - store - .replay_event( - |r| r.validate, - |event, _| event.func_typecheck(wasm_func_type.unwrap()), - ) - .unwrap(); - let mut store = if P::may_gc() { - AutoAssertNoGc::new(caller.store.0) - } else { - unsafe { AutoAssertNoGc::disabled(caller.store.0) } - }; - let params = P::load(&mut store, args.as_mut()); - - let _ = &mut store; - drop(store); + P::load(&mut store, args.as_mut()) + // Drop on store is necessary here; scope closure makes this implicit + }; - let r = func(caller.sub_caller(), params); - if let Err(trap) = caller.store.0.call_hook(CallHook::ReturningFromHost) { - break 'ret R::fallible_from_error(trap); - } - r.into_fallible() + let returns = if caller.store.0.replay_enabled() { + None + } else { + Some('ret: { + if let Err(trap) = caller.store.0.call_hook(CallHook::CallingHost) { + break 'ret R::fallible_from_error(trap); + } + let r = func(caller.sub_caller(), params); + if let Err(trap) = caller.store.0.call_hook(CallHook::ReturningFromHost) { + break 'ret R::fallible_from_error(trap); + } + let fallible = r.into_fallible(); + if !fallible.compatible_with_store(caller.store.0) { + bail!("host function attempted to return cross-`Store` value to Wasm") + } + fallible + }) }; - if !ret.compatible_with_store(caller.store.0) { - bail!("host function attempted to return cross-`Store` value to Wasm") + let mut store = if R::may_gc() { + AutoAssertNoGc::new(caller.store.0) } else { - let store = caller.as_context_mut().0; - // Always intercept return for record/replay - store.record_event( + unsafe { AutoAssertNoGc::disabled(caller.store.0) } + }; + + // Record/replay interceptions of raw return args + let ret = if store.replay_enabled() { + store.replay_event( |_| true, - |rmeta| { - let wasm_func_type = wasm_func_type.unwrap(); - let num_results = wasm_func_type.params().len(); - RREvent::host_func_return( - unsafe { &args.as_ref()[..num_results] }, - rmeta.add_validation.then_some(wasm_func_type.clone()), + |event, rmeta| { + event.move_into_slice( + args.as_mut(), + rmeta.validate.then_some(wasm_func_type.unwrap()), ) }, - ); - store - .replay_event( - |_| true, - |event, rmeta| { - event.move_into_slice( - args.as_mut(), - rmeta.validate.then_some(wasm_func_type.unwrap()), - ) - }, + ) + } else { + returns.unwrap().store(&mut store, args.as_mut()) + }?; + store.record_event( + |_| true, + |rmeta| { + let wasm_func_type = wasm_func_type.unwrap(); + let num_results = wasm_func_type.params().len(); + RREvent::host_func_return( + unsafe { &args.as_ref()[..num_results] }, + rmeta.add_validation.then_some(wasm_func_type.clone()), ) - .unwrap(); - - let mut store = if R::may_gc() { - AutoAssertNoGc::new(caller.store.0) - } else { - unsafe { AutoAssertNoGc::disabled(caller.store.0) } - }; - let ret = ret.store(&mut store, args.as_mut())?; - drop(store); + }, + ); - Ok(ret) - } + Ok(ret) }; // With nothing else on the stack move `run` into this From 89dec1e3d4c8dea9f6e622e9e6e81a252c6442fa Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 24 Jun 2025 11:14:20 -0700 Subject: [PATCH 13/62] Added RR buffer test --- crates/wasmtime/src/runtime/rr.rs | 66 ++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 5 deletions(-) diff --git a/crates/wasmtime/src/runtime/rr.rs b/crates/wasmtime/src/runtime/rr.rs index 8301366ea08f..584de527da55 100644 --- a/crates/wasmtime/src/runtime/rr.rs +++ b/crates/wasmtime/src/runtime/rr.rs @@ -27,7 +27,7 @@ type RRFuncArgTypes = WasmFuncType; /// /// Maintaining the exact layout is crucial for zero-copy transmutations /// between [`ValRawSer`] and [`ValRaw`] -#[derive(Serialize, Deserialize)] +#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)] #[repr(C)] pub struct ValRawSer([u8; VAL_RAW_SIZE]); @@ -119,14 +119,14 @@ pub trait Replayer { /// Pop the next [`RREvent`] from the buffer /// Events should be FIFO - fn pop_event(&mut self) -> Result; + fn pop_event(&mut self) -> Result; /// Get metadata associated with the replay process fn metadata(&self) -> &ReplayMetadata; } /// Arguments for function call/return events -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RRFuncArgs { /// Raw values passed across the call/return boundary args: RRFuncArgVals, @@ -160,7 +160,7 @@ impl RRFuncArgs { /// A high-level event (e.g. import calls consisting of lifts and lowers /// of parameter/return types) may consist of multiple of these lower-level /// [`RREvent`]s -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum RREvent { /// A function call from Wasm to Host HostFuncEntry(RRFuncArgs), @@ -304,7 +304,7 @@ impl Replayer for ReplayBuffer { }) } - fn pop_event(&mut self) -> Result { + fn pop_event(&mut self) -> Result { self.data .buf .pop_front() @@ -316,3 +316,59 @@ impl Replayer for ReplayBuffer { &self.metadata } } + +#[cfg(test)] +mod tests { + use super::*; + use std::path::Path; + use tempfile::{NamedTempFile, TempPath}; + + #[test] + fn rr_buffers() -> Result<()> { + let tmp = NamedTempFile::new()?; + + let tmppath = tmp.path().to_str().expect("Filename should be UTF-8"); + let record_cfg = RecordConfig { + path: String::from(tmppath), + metadata: RecordMetadata { + add_validation: true, + }, + }; + + let values = vec![ValRaw::i32(1), ValRaw::f32(2), ValRaw::i64(3)] + .into_iter() + .map(|x| ValRawSer::from(x)) + .collect::>(); + + let event = RREvent::HostFuncEntry(RRFuncArgs { + args: values, + types: None, + }); + + // Record values + let mut recorder = RecordBuffer::new_recorder(record_cfg)?; + recorder.push_event(event.clone()); + recorder.flush_to_file()?; + + let tmp = tmp.into_temp_path(); + let tmppath = >::as_ref(&tmp) + .to_str() + .expect("Filename should be UTF-8"); + + // Assert that replayed values are identical + let replay_cfg = ReplayConfig { + path: String::from(tmppath), + metadata: ReplayMetadata { validate: true }, + }; + let mut replayer = ReplayBuffer::new_replayer(replay_cfg)?; + let event_pop = replayer.pop_event()?; + // Replay matches record + assert!(event == event_pop); + + // Queue is empty + let event = replayer.pop_event(); + assert!(event.is_err() && matches!(event.unwrap_err(), ReplayError::EmptyBuffer)); + + Ok(()) + } +} From 0ed8b80c1b3ac0bdfbdc39869fa7fcce9a6bf136 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 24 Jun 2025 11:37:34 -0700 Subject: [PATCH 14/62] Clarify docs for RR cli --- crates/cli-flags/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs index d197b57c32cb..c1b9f001772b 100644 --- a/crates/cli-flags/src/lib.rs +++ b/crates/cli-flags/src/lib.rs @@ -564,7 +564,7 @@ pub struct CommonOptions { /// /// Generates of 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-free manner (see the `--replay` option). + /// re-executed in a determinstic, embedding-agnostic manner (see the `--replay` option). /// /// Note: Minimal configs for deterministic Wasm semantics will be /// enforced during recording by default (NaN canonicalization, deterministic relaxed SIMD) @@ -574,7 +574,7 @@ pub struct CommonOptions { /// Options to enable and configure execution replay, `-P help` to see all. /// - /// Run a determinstic, embedding-free replay execution of the Wasm module + /// Run a determinstic, embedding-agnostic replay execution of the Wasm module /// according to a prior recorded execution trace (see the `--record` option). /// /// Note: Minimal configs for deterministic Wasm semantics will be From e2591d92426869a91381b79493e804c818f0970b Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Mon, 30 Jun 2025 18:09:41 -0700 Subject: [PATCH 15/62] Refactor event interface from enum to typed event structs --- crates/wasmtime/src/runtime/func.rs | 12 +- crates/wasmtime/src/runtime/rr.rs | 178 ++++++++++++++++----------- crates/wasmtime/src/runtime/store.rs | 15 ++- 3 files changed, 123 insertions(+), 82 deletions(-) diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index eaa5311a04d0..58c211a70041 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -1,6 +1,6 @@ use crate::prelude::*; +use crate::rr::{CoreHostFuncEntryEvent, CoreHostFuncReturnEvent}; use crate::runtime::Uninhabited; -use crate::runtime::rr::RREvent; use crate::runtime::vm::{ ExportFunction, InterpreterRef, SendSyncPtr, StoreBox, VMArrayCallHostFuncContext, VMCommonStackInformation, VMContext, VMFuncRef, VMFunctionImport, VMOpaqueContext, @@ -2356,15 +2356,15 @@ impl HostContext { // lazily evaluated store.replay_event( |r| r.validate, - |event, _| event.func_typecheck(wasm_func_type.unwrap()), + |event: CoreHostFuncEntryEvent, _| event.validate(wasm_func_type.unwrap()), )?; store.record_event( |r| r.add_validation, |_| { let wasm_func_type = wasm_func_type.unwrap(); let num_params = wasm_func_type.params().len(); - RREvent::host_func_entry( - unsafe { &args.as_ref()[..num_params] }, + CoreHostFuncEntryEvent::new( + &args.as_ref()[..num_params], // Don't need to check validation here since it is // covered by the push predicate in this case Some(wasm_func_type.clone()), @@ -2405,7 +2405,7 @@ impl HostContext { let ret = if store.replay_enabled() { store.replay_event( |_| true, - |event, rmeta| { + |event: CoreHostFuncReturnEvent, rmeta| { event.move_into_slice( args.as_mut(), rmeta.validate.then_some(wasm_func_type.unwrap()), @@ -2420,7 +2420,7 @@ impl HostContext { |rmeta| { let wasm_func_type = wasm_func_type.unwrap(); let num_results = wasm_func_type.params().len(); - RREvent::host_func_return( + CoreHostFuncReturnEvent::new( unsafe { &args.as_ref()[..num_results] }, rmeta.add_validation.then_some(wasm_func_type.clone()), ) diff --git a/crates/wasmtime/src/runtime/rr.rs b/crates/wasmtime/src/runtime/rr.rs index 584de527da55..070c6552a4f8 100644 --- a/crates/wasmtime/src/runtime/rr.rs +++ b/crates/wasmtime/src/runtime/rr.rs @@ -16,6 +16,7 @@ use serde::{Deserialize, Serialize}; use std::collections::VecDeque; use std::fs::File; use std::io::{BufWriter, Seek, Write}; +use wasmtime_environ::component::InterfaceType; use wasmtime_environ::{WasmFuncType, WasmValType}; const VAL_RAW_SIZE: usize = mem::size_of::(); @@ -54,18 +55,42 @@ impl fmt::Debug for ValRawSer { } } -fn raw_to_func_argvals(args: &[MaybeUninit]) -> RRFuncArgVals { +/// Construct [`RRFuncArgVals`] from raw value buffer +fn func_argvals_from_raw_slice(args: &[MaybeUninit]) -> RRFuncArgVals { args.iter() .map(|x| unsafe { ValRawSer::from(x.assume_init()) }) .collect::>() } -fn func_argvals_into_raw(rr_args: RRFuncArgVals, raw_args: &mut [MaybeUninit]) { +/// Encode [`RRFuncArgVals`] back into raw value buffer +fn func_argvals_into_raw_slice(rr_args: RRFuncArgVals, raw_args: &mut [MaybeUninit]) { for (src, dst) in rr_args.into_iter().zip(raw_args.iter_mut()) { *dst = MaybeUninit::new(src.into()); } } +/// Typechecking validation for replay, if `src_types` exist +/// +/// Returns [`ReplayError::FailedFuncValidation`] if typechecking fails +fn replay_args_typecheck(src_types: Option<&T>, expect_types: &T) -> Result<(), ReplayError> +where + T: PartialEq, +{ + if let Some(types) = src_types { + if types == expect_types { + Ok(()) + } else { + Err(ReplayError::FailedFuncValidation) + } + } else { + println!( + "Warning: Replay typechecking cannot be performed + since recorded trace is missing validation data" + ); + Ok(()) + } +} + #[derive(Debug)] pub enum ReplayError { EmptyBuffer, @@ -125,80 +150,54 @@ pub trait Replayer { fn metadata(&self) -> &ReplayMetadata; } -/// Arguments for function call/return events +pub struct ComponentRRFuncArgs { + args: RRFuncArgVals, + types: Option, +} + +/// A call event from a Core Wasm module into the host #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RRFuncArgs { +pub struct CoreHostFuncEntryEvent { /// Raw values passed across the call/return boundary args: RRFuncArgVals, /// Optional param/return types (required to support replay validation) types: Option, } -impl RRFuncArgs { - /// Typecheck the types field, if it exists - /// - /// Errors with a [`ReplayError::FailedFuncValidation`] if typechecking fails - pub fn typecheck(&self, expect_types: &WasmFuncType) -> Result<(), ReplayError> { - if let Some(types) = &self.types { - if types == expect_types { - Ok(()) - } else { - Err(ReplayError::FailedFuncValidation) - } - } else { - println!( - "Warning: Replay typechecking cannot be performed - since recorded trace is missing validation data" - ); - Ok(()) +impl CoreHostFuncEntryEvent { + // Record + pub fn new(args: &[MaybeUninit], types: Option) -> Self { + Self { + args: func_argvals_from_raw_slice(args), + types: types, } } + // Replay + pub fn validate(&self, expect_types: &RRFuncArgTypes) -> Result<(), ReplayError> { + replay_args_typecheck(self.types.as_ref(), expect_types) + } } -/// A single, low-level recording/replay event +/// A return event after a host call for a Core Wasm /// -/// A high-level event (e.g. import calls consisting of lifts and lowers -/// of parameter/return types) may consist of multiple of these lower-level -/// [`RREvent`]s +/// Matches 1:1 with [`CoreHostFuncEntryEvent`] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum RREvent { - /// A function call from Wasm to Host - HostFuncEntry(RRFuncArgs), - /// A function return from Host to Wasm. - /// - /// Matches 1:1 with a prior [`RREvent::HostFuncEntry`] event - HostFuncReturn(RRFuncArgs), +pub struct CoreHostFuncReturnEvent { + /// Raw values passed across the call/return boundary + args: RRFuncArgVals, + /// Optional param/return types (required to support replay validation) + types: Option, } -impl RREvent { - /// Construct a [`RREvent::HostFuncEntry`] event from raw slice - pub fn host_func_entry(args: &[MaybeUninit], types: Option) -> Self { - Self::HostFuncEntry(RRFuncArgs { - args: raw_to_func_argvals(args), +impl CoreHostFuncReturnEvent { + // Record + pub fn new(args: &[MaybeUninit], types: Option) -> Self { + Self { + args: func_argvals_from_raw_slice(args), types: types, - }) - } - /// Construct a [`RREvent::HostFuncReturn`] event from raw slice - pub fn host_func_return(args: &[MaybeUninit], types: Option) -> Self { - Self::HostFuncReturn(RRFuncArgs { - args: raw_to_func_argvals(args), - types: types, - }) - } - - /// Typecheck the function signature for validation - /// - /// Errors with a [`ReplayError::IncorrectEventVariant`] if not - /// a func variant or a [`ReplayError::FailedFuncValidation`] if typechecking fails - pub fn func_typecheck(&self, expect_types: &WasmFuncType) -> Result<(), ReplayError> { - match self { - Self::HostFuncEntry(func_args) | Self::HostFuncReturn(func_args) => { - func_args.typecheck(expect_types) - } - _ => Err(ReplayError::IncorrectEventVariant), } } - + // Replay /// Consume the caller event and encode it back into the slice with an optional /// typechecking validation of the event. pub fn move_into_slice( @@ -206,18 +205,57 @@ impl RREvent { args: &mut [MaybeUninit], expect_types: Option<&WasmFuncType>, ) -> Result<(), ReplayError> { - match self { - Self::HostFuncEntry(func_args) | Self::HostFuncReturn(func_args) => { - if let Some(e) = expect_types { - func_args.typecheck(e)?; - } - func_argvals_into_raw(func_args.args, args); - } - }; + if let Some(e) = expect_types { + replay_args_typecheck(self.types.as_ref(), e)?; + } + func_argvals_into_raw_slice(self.args, args); Ok(()) } } +/// A single, unified, low-level recording/replay event +/// +/// This type is the narrow waist for serialization/deserialization. +/// Higher-level events (e.g. import calls consisting of lifts and lowers +/// of parameter/return types) may drop down to one or more [`RREvent`]s +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum RREvent { + CoreHostFuncEntry(CoreHostFuncEntryEvent), + CoreHostFuncReturn(CoreHostFuncReturnEvent), +} + +impl From for RREvent { + fn from(value: CoreHostFuncEntryEvent) -> Self { + RREvent::CoreHostFuncEntry(value) + } +} +impl TryFrom for CoreHostFuncEntryEvent { + type Error = ReplayError; + fn try_from(value: RREvent) -> Result { + if let RREvent::CoreHostFuncEntry(x) = value { + Ok(x) + } else { + Err(ReplayError::IncorrectEventVariant) + } + } +} + +impl From for RREvent { + fn from(value: CoreHostFuncReturnEvent) -> Self { + RREvent::CoreHostFuncReturn(value) + } +} +impl TryFrom for CoreHostFuncReturnEvent { + type Error = ReplayError; + fn try_from(value: RREvent) -> Result { + if let RREvent::CoreHostFuncReturn(x) = value { + Ok(x) + } else { + Err(ReplayError::IncorrectEventVariant) + } + } +} + /// The underlying serialized/deserialized type type RRBufferData = VecDeque; @@ -340,14 +378,14 @@ mod tests { .map(|x| ValRawSer::from(x)) .collect::>(); - let event = RREvent::HostFuncEntry(RRFuncArgs { + let event = CoreHostFuncEntryEvent { args: values, types: None, - }); + }; // Record values let mut recorder = RecordBuffer::new_recorder(record_cfg)?; - recorder.push_event(event.clone()); + recorder.push_event(event.clone().into()); recorder.flush_to_file()?; let tmp = tmp.into_temp_path(); @@ -361,7 +399,7 @@ mod tests { metadata: ReplayMetadata { validate: true }, }; let mut replayer = ReplayBuffer::new_replayer(replay_cfg)?; - let event_pop = replayer.pop_event()?; + let event_pop = CoreHostFuncEntryEvent::try_from(replayer.pop_event()?)?; // Replay matches record assert!(event == event_pop); diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index 0e850ca68acf..6b12517a6806 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -1364,30 +1364,33 @@ impl StoreOpaque { } /// Record the event generated by `F` if `P` holds true - pub(crate) fn record_event(&mut self, push_predicate: P, f: F) + pub(crate) fn record_event(&mut self, push_predicate: P, f: F) where + T: Into + fmt::Debug, P: FnOnce(&RecordMetadata) -> bool, - F: FnOnce(&RecordMetadata) -> RREvent, + F: FnOnce(&RecordMetadata) -> T, { if let Some(buf) = self.record_buffer_mut() { let metadata = buf.metadata(); if push_predicate(metadata) { let event = f(buf.metadata()); println!("Record | {:?}", &event); - buf.push_event(event); + buf.push_event(event.into()); } } } /// Get the next replay event if `P` holds true and process it with `F` - pub(crate) fn replay_event(&mut self, pop_predicate: P, f: F) -> Result<()> + pub(crate) fn replay_event(&mut self, pop_predicate: P, f: F) -> Result<()> where + T: TryFrom + fmt::Debug, + >::Error: std::error::Error + Send + Sync + 'static, P: FnOnce(&ReplayMetadata) -> bool, - F: FnOnce(RREvent, &ReplayMetadata) -> Result<(), ReplayError>, + F: FnOnce(T, &ReplayMetadata) -> Result<(), ReplayError>, { if let Some(buf) = self.replay_buffer_mut() { if pop_predicate(buf.metadata()) { - let call_event = buf.pop_event()?; + let call_event = T::try_from(buf.pop_event()?)?; println!("Replay | {:?}", &call_event); Ok(f(call_event, buf.metadata())?) } else { From b6866f7bcb7c009af331908fbfc11a4582a78091 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 1 Jul 2025 12:07:38 -0700 Subject: [PATCH 16/62] Refactor events to indepedent module --- crates/wasmtime/src/runtime/rr.rs | 412 ----------------------- crates/wasmtime/src/runtime/rr/events.rs | 171 ++++++++++ crates/wasmtime/src/runtime/rr/mod.rs | 244 ++++++++++++++ 3 files changed, 415 insertions(+), 412 deletions(-) delete mode 100644 crates/wasmtime/src/runtime/rr.rs create mode 100644 crates/wasmtime/src/runtime/rr/events.rs create mode 100644 crates/wasmtime/src/runtime/rr/mod.rs diff --git a/crates/wasmtime/src/runtime/rr.rs b/crates/wasmtime/src/runtime/rr.rs deleted file mode 100644 index 070c6552a4f8..000000000000 --- a/crates/wasmtime/src/runtime/rr.rs +++ /dev/null @@ -1,412 +0,0 @@ -//! Wasmtime's Record and Replay support -//! -//! This feature is currently experimental and hence not optimized. -//! In particular, the following opportunities are immediately identifiable: -//! * Switch [RRFuncArgTypes] to use [Vec] - -use crate::ValRaw; -use crate::config::{RecordConfig, RecordMetadata, ReplayConfig, ReplayMetadata}; -use crate::prelude::*; -#[allow(unused_imports)] -use crate::runtime::Store; -use core::fmt; -use core::mem::{self, MaybeUninit}; -use postcard; -use serde::{Deserialize, Serialize}; -use std::collections::VecDeque; -use std::fs::File; -use std::io::{BufWriter, Seek, Write}; -use wasmtime_environ::component::InterfaceType; -use wasmtime_environ::{WasmFuncType, WasmValType}; - -const VAL_RAW_SIZE: usize = mem::size_of::(); - -type RRFuncArgVals = Vec; -type RRFuncArgTypes = WasmFuncType; - -/// Transmutable byte array used to serialize [`ValRaw`] union -/// -/// Maintaining the exact layout is crucial for zero-copy transmutations -/// between [`ValRawSer`] and [`ValRaw`] -#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)] -#[repr(C)] -pub struct ValRawSer([u8; VAL_RAW_SIZE]); - -impl From for ValRawSer { - fn from(value: ValRaw) -> Self { - unsafe { Self(mem::transmute(value)) } - } -} - -impl From for ValRaw { - fn from(value: ValRawSer) -> Self { - unsafe { mem::transmute(value.0) } - } -} - -impl fmt::Debug for ValRawSer { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let hex_digits_per_byte = 2; - let _ = write!(f, "0x.."); - for b in self.0.iter().rev() { - let _ = write!(f, "{:0width$x}", b, width = hex_digits_per_byte); - } - Ok(()) - } -} - -/// Construct [`RRFuncArgVals`] from raw value buffer -fn func_argvals_from_raw_slice(args: &[MaybeUninit]) -> RRFuncArgVals { - args.iter() - .map(|x| unsafe { ValRawSer::from(x.assume_init()) }) - .collect::>() -} - -/// Encode [`RRFuncArgVals`] back into raw value buffer -fn func_argvals_into_raw_slice(rr_args: RRFuncArgVals, raw_args: &mut [MaybeUninit]) { - for (src, dst) in rr_args.into_iter().zip(raw_args.iter_mut()) { - *dst = MaybeUninit::new(src.into()); - } -} - -/// Typechecking validation for replay, if `src_types` exist -/// -/// Returns [`ReplayError::FailedFuncValidation`] if typechecking fails -fn replay_args_typecheck(src_types: Option<&T>, expect_types: &T) -> Result<(), ReplayError> -where - T: PartialEq, -{ - if let Some(types) = src_types { - if types == expect_types { - Ok(()) - } else { - Err(ReplayError::FailedFuncValidation) - } - } else { - println!( - "Warning: Replay typechecking cannot be performed - since recorded trace is missing validation data" - ); - Ok(()) - } -} - -#[derive(Debug)] -pub enum ReplayError { - EmptyBuffer, - FailedFuncValidation, - IncorrectEventVariant, -} - -impl fmt::Display for ReplayError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::EmptyBuffer => { - write!(f, "replay buffer is empty!") - } - Self::FailedFuncValidation => { - write!(f, "func replay event typecheck validation failed") - } - Self::IncorrectEventVariant => { - write!(f, "event methods invoked on incorrect variant") - } - } - } -} - -impl std::error::Error for ReplayError {} - -pub trait Recorder { - /// Constructs a writer on new buffer - fn new_recorder(cfg: RecordConfig) -> Result - where - Self: Sized; - - /// Push a newly record event [`RREvent`] to the buffer - fn push_event(&mut self, event: RREvent) -> (); - - /// Flush memory contents to underlying persistent storage - /// - /// Buffer should be emptied during this process - fn flush_to_file(&mut self) -> Result<()>; - - /// Get metadata associated with the recording process - fn metadata(&self) -> &RecordMetadata; -} - -pub trait Replayer { - type ReplayError; - - /// Constructs a reader on buffer - fn new_replayer(cfg: ReplayConfig) -> Result - where - Self: Sized; - - /// Pop the next [`RREvent`] from the buffer - /// Events should be FIFO - fn pop_event(&mut self) -> Result; - - /// Get metadata associated with the replay process - fn metadata(&self) -> &ReplayMetadata; -} - -pub struct ComponentRRFuncArgs { - args: RRFuncArgVals, - types: Option, -} - -/// A call event from a Core Wasm module into the host -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CoreHostFuncEntryEvent { - /// Raw values passed across the call/return boundary - args: RRFuncArgVals, - /// Optional param/return types (required to support replay validation) - types: Option, -} - -impl CoreHostFuncEntryEvent { - // Record - pub fn new(args: &[MaybeUninit], types: Option) -> Self { - Self { - args: func_argvals_from_raw_slice(args), - types: types, - } - } - // Replay - pub fn validate(&self, expect_types: &RRFuncArgTypes) -> Result<(), ReplayError> { - replay_args_typecheck(self.types.as_ref(), expect_types) - } -} - -/// A return event after a host call for a Core Wasm -/// -/// Matches 1:1 with [`CoreHostFuncEntryEvent`] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CoreHostFuncReturnEvent { - /// Raw values passed across the call/return boundary - args: RRFuncArgVals, - /// Optional param/return types (required to support replay validation) - types: Option, -} - -impl CoreHostFuncReturnEvent { - // Record - pub fn new(args: &[MaybeUninit], types: Option) -> Self { - Self { - args: func_argvals_from_raw_slice(args), - types: types, - } - } - // Replay - /// Consume the caller event and encode it back into the slice with an optional - /// typechecking validation of the event. - pub fn move_into_slice( - self, - args: &mut [MaybeUninit], - expect_types: Option<&WasmFuncType>, - ) -> Result<(), ReplayError> { - if let Some(e) = expect_types { - replay_args_typecheck(self.types.as_ref(), e)?; - } - func_argvals_into_raw_slice(self.args, args); - Ok(()) - } -} - -/// A single, unified, low-level recording/replay event -/// -/// This type is the narrow waist for serialization/deserialization. -/// Higher-level events (e.g. import calls consisting of lifts and lowers -/// of parameter/return types) may drop down to one or more [`RREvent`]s -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum RREvent { - CoreHostFuncEntry(CoreHostFuncEntryEvent), - CoreHostFuncReturn(CoreHostFuncReturnEvent), -} - -impl From for RREvent { - fn from(value: CoreHostFuncEntryEvent) -> Self { - RREvent::CoreHostFuncEntry(value) - } -} -impl TryFrom for CoreHostFuncEntryEvent { - type Error = ReplayError; - fn try_from(value: RREvent) -> Result { - if let RREvent::CoreHostFuncEntry(x) = value { - Ok(x) - } else { - Err(ReplayError::IncorrectEventVariant) - } - } -} - -impl From for RREvent { - fn from(value: CoreHostFuncReturnEvent) -> Self { - RREvent::CoreHostFuncReturn(value) - } -} -impl TryFrom for CoreHostFuncReturnEvent { - type Error = ReplayError; - fn try_from(value: RREvent) -> Result { - if let RREvent::CoreHostFuncReturn(x) = value { - Ok(x) - } else { - Err(ReplayError::IncorrectEventVariant) - } - } -} - -/// The underlying serialized/deserialized type -type RRBufferData = VecDeque; - -/// Common data for recorders and replayers -/// -/// Flexibility of this struct can also be improved with: -/// * Support for generic writers beyond [File] (will require a generic on [Store]) -#[derive(Debug)] -pub struct RRDataCommon { - /// Ordered list of record/replay events - buf: RRBufferData, - /// Persistent storage-backed handle - rw: File, -} - -#[derive(Debug)] -/// Buffer to write recording data -pub struct RecordBuffer { - data: RRDataCommon, - metadata: RecordMetadata, -} - -impl Recorder for RecordBuffer { - fn new_recorder(cfg: RecordConfig) -> Result { - Ok(RecordBuffer { - data: RRDataCommon { - buf: VecDeque::new(), - rw: File::create(cfg.path)?, - }, - metadata: cfg.metadata, - }) - } - - fn push_event(&mut self, event: RREvent) { - self.data.buf.push_back(event) - } - - fn flush_to_file(&mut self) -> Result<()> { - // Seralizing each event independently prevents checking for vector sizes - // during deserialization - let data = &mut self.data; - for v in &data.buf { - postcard::to_io(&v, &mut data.rw)?; - } - data.rw.flush()?; - data.buf.clear(); - println!( - "Record flush | File size: {:?} bytes", - data.rw.metadata()?.len() - ); - Ok(()) - } - - #[inline] - fn metadata(&self) -> &RecordMetadata { - &self.metadata - } -} - -#[derive(Debug)] -/// Buffer to read replay data -pub struct ReplayBuffer { - data: RRDataCommon, - metadata: ReplayMetadata, -} - -impl Replayer for ReplayBuffer { - type ReplayError = ReplayError; - - fn new_replayer(cfg: ReplayConfig) -> Result { - let mut file = File::open(cfg.path)?; - let mut events = VecDeque::::new(); - // Read till EOF - while file.stream_position()? != file.metadata()?.len() { - let (event, _): (RREvent, _) = postcard::from_io((&mut file, &mut [0; 0]))?; - events.push_back(event); - } - Ok(ReplayBuffer { - data: RRDataCommon { - buf: events, - rw: file, - }, - metadata: cfg.metadata, - }) - } - - fn pop_event(&mut self) -> Result { - self.data - .buf - .pop_front() - .ok_or(Self::ReplayError::EmptyBuffer.into()) - } - - #[inline] - fn metadata(&self) -> &ReplayMetadata { - &self.metadata - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::path::Path; - use tempfile::{NamedTempFile, TempPath}; - - #[test] - fn rr_buffers() -> Result<()> { - let tmp = NamedTempFile::new()?; - - let tmppath = tmp.path().to_str().expect("Filename should be UTF-8"); - let record_cfg = RecordConfig { - path: String::from(tmppath), - metadata: RecordMetadata { - add_validation: true, - }, - }; - - let values = vec![ValRaw::i32(1), ValRaw::f32(2), ValRaw::i64(3)] - .into_iter() - .map(|x| ValRawSer::from(x)) - .collect::>(); - - let event = CoreHostFuncEntryEvent { - args: values, - types: None, - }; - - // Record values - let mut recorder = RecordBuffer::new_recorder(record_cfg)?; - recorder.push_event(event.clone().into()); - recorder.flush_to_file()?; - - let tmp = tmp.into_temp_path(); - let tmppath = >::as_ref(&tmp) - .to_str() - .expect("Filename should be UTF-8"); - - // Assert that replayed values are identical - let replay_cfg = ReplayConfig { - path: String::from(tmppath), - metadata: ReplayMetadata { validate: true }, - }; - let mut replayer = ReplayBuffer::new_replayer(replay_cfg)?; - let event_pop = CoreHostFuncEntryEvent::try_from(replayer.pop_event()?)?; - // Replay matches record - assert!(event == event_pop); - - // Queue is empty - let event = replayer.pop_event(); - assert!(event.is_err() && matches!(event.unwrap_err(), ReplayError::EmptyBuffer)); - - Ok(()) - } -} diff --git a/crates/wasmtime/src/runtime/rr/events.rs b/crates/wasmtime/src/runtime/rr/events.rs new file mode 100644 index 000000000000..4579fd71fac1 --- /dev/null +++ b/crates/wasmtime/src/runtime/rr/events.rs @@ -0,0 +1,171 @@ +use crate::ValRaw; +use crate::prelude::*; +use core::fmt; +use core::mem::{self, MaybeUninit}; +use serde::{Deserialize, Serialize}; +use wasmtime_environ::component::InterfaceType; +use wasmtime_environ::{WasmFuncType, WasmValType}; + +const VAL_RAW_SIZE: usize = mem::size_of::(); + +#[derive(Debug)] +pub enum ReplayError { + EmptyBuffer, + FailedFuncValidation, + IncorrectEventVariant, +} + +impl fmt::Display for ReplayError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::EmptyBuffer => { + write!(f, "replay buffer is empty!") + } + Self::FailedFuncValidation => { + write!(f, "func replay event typecheck validation failed") + } + Self::IncorrectEventVariant => { + write!(f, "event methods invoked on incorrect variant") + } + } + } +} + +impl std::error::Error for ReplayError {} + +/// Transmutable byte array used to serialize [`ValRaw`] union +/// +/// Maintaining the exact layout is crucial for zero-copy transmutations +/// between [`ValRawSer`] and [`ValRaw`] +#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)] +#[repr(C)] +pub(super) struct ValRawSer([u8; VAL_RAW_SIZE]); + +impl From for ValRawSer { + fn from(value: ValRaw) -> Self { + unsafe { Self(mem::transmute(value)) } + } +} + +impl From for ValRaw { + fn from(value: ValRawSer) -> Self { + unsafe { mem::transmute(value.0) } + } +} + +impl fmt::Debug for ValRawSer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let hex_digits_per_byte = 2; + let _ = write!(f, "0x.."); + for b in self.0.iter().rev() { + let _ = write!(f, "{:0width$x}", b, width = hex_digits_per_byte); + } + Ok(()) + } +} + +type RRFuncArgVals = Vec; + +/// Note: Switch [`RRFuncArgTypes`] to use [`Vec`] for better efficiency +type RRFuncArgTypes = WasmFuncType; + +/// Construct [`RRFuncArgVals`] from raw value buffer +fn func_argvals_from_raw_slice(args: &[MaybeUninit]) -> RRFuncArgVals { + args.iter() + .map(|x| unsafe { ValRawSer::from(x.assume_init()) }) + .collect::>() +} + +/// Encode [`RRFuncArgVals`] back into raw value buffer +fn func_argvals_into_raw_slice(rr_args: RRFuncArgVals, raw_args: &mut [MaybeUninit]) { + for (src, dst) in rr_args.into_iter().zip(raw_args.iter_mut()) { + *dst = MaybeUninit::new(src.into()); + } +} + +/// Typechecking validation for replay, if `src_types` exist +/// +/// Returns [`ReplayError::FailedFuncValidation`] if typechecking fails +fn replay_args_typecheck(src_types: Option<&T>, expect_types: &T) -> Result<(), ReplayError> +where + T: PartialEq, +{ + if let Some(types) = src_types { + if types == expect_types { + Ok(()) + } else { + Err(ReplayError::FailedFuncValidation) + } + } else { + println!( + "Warning: Replay typechecking cannot be performed + since recorded trace is missing validation data" + ); + Ok(()) + } +} + +struct ComponentHostFuncEntryEvent { + /// Raw values passed across the call/return boundary + args: RRFuncArgVals, + /// Optional param/return types (required to support replay validation) + types: Option, +} + +/// A call event from a Core Wasm module into the host +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CoreHostFuncEntryEvent { + /// Raw values passed across the call/return boundary + args: RRFuncArgVals, + /// Optional param/return types (required to support replay validation) + types: Option, +} + +impl CoreHostFuncEntryEvent { + // Record + pub fn new(args: &[MaybeUninit], types: Option) -> Self { + Self { + args: func_argvals_from_raw_slice(args), + types: types, + } + } + // Replay + pub fn validate(&self, expect_types: &RRFuncArgTypes) -> Result<(), ReplayError> { + replay_args_typecheck(self.types.as_ref(), expect_types) + } +} + +/// A return event after a host call for a Core Wasm +/// +/// Matches 1:1 with [`CoreHostFuncEntryEvent`] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CoreHostFuncReturnEvent { + /// Raw values passed across the call/return boundary + args: RRFuncArgVals, + /// Optional param/return types (required to support replay validation) + types: Option, +} + +impl CoreHostFuncReturnEvent { + // Record + pub fn new(args: &[MaybeUninit], types: Option) -> Self { + Self { + args: func_argvals_from_raw_slice(args), + types: types, + } + } + // Replay + /// Consume the caller event and encode it back into the slice with an optional + /// typechecking validation of the event. + pub fn move_into_slice( + self, + args: &mut [MaybeUninit], + expect_types: Option<&WasmFuncType>, + ) -> Result<(), ReplayError> { + if let Some(e) = expect_types { + replay_args_typecheck(self.types.as_ref(), e)?; + } + func_argvals_into_raw_slice(self.args, args); + Ok(()) + } +} diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs new file mode 100644 index 000000000000..f73334d54c8b --- /dev/null +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -0,0 +1,244 @@ +//! Wasmtime's Record and Replay support +//! +//! This feature is currently experimental and hence not optimized. + +use crate::config::{RecordConfig, RecordMetadata, ReplayConfig, ReplayMetadata}; +use crate::prelude::*; +#[allow(unused_imports)] +use crate::runtime::Store; +use postcard; +use serde::{Deserialize, Serialize}; +use std::collections::VecDeque; +use std::fs::File; +use std::io::{BufWriter, Seek, Write}; + +/// Encapsulation of event types comprising an [`RREvent`] sum type +mod events; +pub use events::*; + +pub trait Recorder { + /// Constructs a writer on new buffer + fn new_recorder(cfg: RecordConfig) -> Result + where + Self: Sized; + + /// Push a newly record event [`RREvent`] to the buffer + fn push_event(&mut self, event: RREvent) -> (); + + /// Flush memory contents to underlying persistent storage + /// + /// Buffer should be emptied during this process + fn flush_to_file(&mut self) -> Result<()>; + + /// Get metadata associated with the recording process + fn metadata(&self) -> &RecordMetadata; +} + +pub trait Replayer { + type ReplayError; + + /// Constructs a reader on buffer + fn new_replayer(cfg: ReplayConfig) -> Result + where + Self: Sized; + + /// Pop the next [`RREvent`] from the buffer + /// Events should be FIFO + fn pop_event(&mut self) -> Result; + + /// Get metadata associated with the replay process + fn metadata(&self) -> &ReplayMetadata; +} + +/// Macro template for [`RREvent`] and its conversion to/from specific +/// event types +macro_rules! rr_event { + ( $( $variant:ident($event:ty) ),* ) => ( + /// A single, unified, low-level recording/replay event + /// + /// This type is the narrow waist for serialization/deserialization. + /// Higher-level events (e.g. import calls consisting of lifts and lowers + /// of parameter/return types) may drop down to one or more [`RREvent`]s + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub enum RREvent { + $($variant($event),)* + } + $( + impl From<$event> for RREvent { + fn from(value: $event) -> Self { + RREvent::$variant(value) + } + } + impl TryFrom for $event { + type Error = ReplayError; + fn try_from(value: RREvent) -> Result { + if let RREvent::$variant(x) = value { + Ok(x) + } else { + Err(ReplayError::IncorrectEventVariant) + } + } + } + )* + ); +} + +// Set of supported events +rr_event! { + CoreHostFuncEntry(CoreHostFuncEntryEvent), + CoreHostFuncReturn(CoreHostFuncReturnEvent) +} + +/// The underlying serialized/deserialized type +type RRBufferData = VecDeque; + +/// Common data for recorders and replayers +/// +/// Flexibility of this struct can also be improved with: +/// * Support for generic writers beyond [File] (will require a generic on [Store]) +#[derive(Debug)] +pub struct RRDataCommon { + /// Ordered list of record/replay events + buf: RRBufferData, + /// Persistent storage-backed handle + rw: File, +} + +#[derive(Debug)] +/// Buffer to write recording data +pub struct RecordBuffer { + data: RRDataCommon, + metadata: RecordMetadata, +} + +impl Recorder for RecordBuffer { + fn new_recorder(cfg: RecordConfig) -> Result { + Ok(RecordBuffer { + data: RRDataCommon { + buf: VecDeque::new(), + rw: File::create(cfg.path)?, + }, + metadata: cfg.metadata, + }) + } + + fn push_event(&mut self, event: RREvent) { + self.data.buf.push_back(event) + } + + fn flush_to_file(&mut self) -> Result<()> { + // Seralizing each event independently prevents checking for vector sizes + // during deserialization + let data = &mut self.data; + for v in &data.buf { + postcard::to_io(&v, &mut data.rw)?; + } + data.rw.flush()?; + data.buf.clear(); + println!( + "Record flush | File size: {:?} bytes", + data.rw.metadata()?.len() + ); + Ok(()) + } + + #[inline] + fn metadata(&self) -> &RecordMetadata { + &self.metadata + } +} + +#[derive(Debug)] +/// Buffer to read replay data +pub struct ReplayBuffer { + data: RRDataCommon, + metadata: ReplayMetadata, +} + +impl Replayer for ReplayBuffer { + type ReplayError = ReplayError; + + fn new_replayer(cfg: ReplayConfig) -> Result { + let mut file = File::open(cfg.path)?; + let mut events = VecDeque::::new(); + // Read till EOF + while file.stream_position()? != file.metadata()?.len() { + let (event, _): (RREvent, _) = postcard::from_io((&mut file, &mut [0; 0]))?; + events.push_back(event); + } + Ok(ReplayBuffer { + data: RRDataCommon { + buf: events, + rw: file, + }, + metadata: cfg.metadata, + }) + } + + fn pop_event(&mut self) -> Result { + self.data + .buf + .pop_front() + .ok_or(Self::ReplayError::EmptyBuffer.into()) + } + + #[inline] + fn metadata(&self) -> &ReplayMetadata { + &self.metadata + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ValRaw; + use core::mem::MaybeUninit; + use std::path::Path; + use tempfile::{NamedTempFile, TempPath}; + + #[test] + fn rr_buffers() -> Result<()> { + let tmp = NamedTempFile::new()?; + + let tmppath = tmp.path().to_str().expect("Filename should be UTF-8"); + let record_cfg = RecordConfig { + path: String::from(tmppath), + metadata: RecordMetadata { + add_validation: true, + }, + }; + + let values = vec![ValRaw::i32(1), ValRaw::f32(2), ValRaw::i64(3)] + .into_iter() + .map(|x| MaybeUninit::new(x)) + .collect::>(); + + let event = CoreHostFuncEntryEvent::new(values.as_slice(), None); + + // Record values + let mut recorder = RecordBuffer::new_recorder(record_cfg)?; + recorder.push_event(event.clone().into()); + recorder.flush_to_file()?; + + let tmp = tmp.into_temp_path(); + let tmppath = >::as_ref(&tmp) + .to_str() + .expect("Filename should be UTF-8"); + + // Assert that replayed values are identical + let replay_cfg = ReplayConfig { + path: String::from(tmppath), + metadata: ReplayMetadata { validate: true }, + }; + let mut replayer = ReplayBuffer::new_replayer(replay_cfg)?; + let event_pop = CoreHostFuncEntryEvent::try_from(replayer.pop_event()?)?; + // Replay matches record + assert!(event == event_pop); + + // Queue is empty + let event = replayer.pop_event(); + assert!(event.is_err() && matches!(event.unwrap_err(), ReplayError::EmptyBuffer)); + + Ok(()) + } +} From c5d6b6250b0a5646714142992b44f6179d835abd Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Wed, 2 Jul 2025 11:38:22 -0700 Subject: [PATCH 17/62] Added component host function entry/exit event recording --- .../src/runtime/component/func/host.rs | 62 ++++++++++--- crates/wasmtime/src/runtime/rr/events.rs | 87 ++++++++++++++++--- crates/wasmtime/src/runtime/rr/mod.rs | 72 +++++++-------- 3 files changed, 162 insertions(+), 59 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 2bab07251034..7d860b1ceaa1 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -1,8 +1,9 @@ use crate::component::func::{LiftContext, LowerContext, Options}; use crate::component::matching::InstanceType; -use crate::component::storage::slice_to_storage_mut; +use crate::component::storage::{slice_to_storage_mut, storage_as_slice}; use crate::component::{ComponentNamedList, ComponentType, Lift, Lower, Val}; use crate::prelude::*; +use crate::runtime::rr::{ComponentHostFuncEntryEvent, ComponentHostFuncReturnEvent}; use crate::runtime::vm::component::{ ComponentInstance, InstanceFlags, VMComponentContext, VMLowering, VMLoweringCallee, }; @@ -12,6 +13,7 @@ use alloc::sync::Arc; use core::any::Any; use core::mem::{self, MaybeUninit}; use core::ptr::NonNull; +use core::slice; use wasmtime_environ::component::{ CanonicalAbiInfo, ComponentTypes, InterfaceType, MAX_FLAT_PARAMS, MAX_FLAT_RESULTS, StringEncoding, TypeFuncIndex, @@ -203,6 +205,18 @@ where let param_tys = InterfaceType::Tuple(ty.params); let result_tys = InterfaceType::Tuple(ty.results); + cx.0.record_event( + |r| r.add_validation, + |_| { + ComponentHostFuncEntryEvent::new( + storage, + // Don't need to check validation here since it is + // covered by the push predicate in this case + Some(param_tys), + ) + }, + ); + // There's a 2x2 matrix of whether parameters and results are stored on the // stack or on the heap. Each of the 4 branches here have a different // representation of the storage of arguments/returns. @@ -227,6 +241,7 @@ where }; let mut lift = LiftContext::new(cx.0, &options, types, instance); lift.enter_call(); + let params = storage.lift_params(&mut lift, param_tys)?; let ret = closure(cx.as_context_mut(), params)?; @@ -272,19 +287,44 @@ where ty: InterfaceType, ret: R, ) -> Result<()> { + let direct_results_lower = + |cx: &mut LowerContext<'_, T>, + dst: &mut MaybeUninit<::Lower>| { + let res = ret.lower(cx, ty, dst); + cx.store.0.record_event( + |_| true, + |rmeta| { + ComponentHostFuncReturnEvent::new( + storage_as_slice(dst), + rmeta.add_validation.then_some(ty), + ) + }, + ); + res + }; + let indirect_results_lower = |cx: &mut LowerContext<'_, T>, dst: &ValRaw| { + let ptr = validate_inbounds::(cx.as_slice_mut(), dst)?; + let res = ret.store(cx, ty, ptr); + cx.store.0.record_event( + |_| true, + |rmeta| { + ComponentHostFuncReturnEvent::new( + slice::from_ref(dst), + rmeta.add_validation.then_some(ty), + ) + }, + ); + res + }; match self { - Storage::Direct(storage) => ret.lower(cx, ty, map_maybe_uninit!(storage.ret)), - Storage::ParamsIndirect(storage) => { - ret.lower(cx, ty, map_maybe_uninit!(storage.ret)) - } - Storage::ResultsIndirect(storage) => { - let ptr = validate_inbounds::(cx.as_slice_mut(), &storage.retptr)?; - ret.store(cx, ty, ptr) + Storage::Direct(storage) => { + direct_results_lower(cx, map_maybe_uninit!(storage.ret)) } - Storage::Indirect(storage) => { - let ptr = validate_inbounds::(cx.as_slice_mut(), &storage.retptr)?; - ret.store(cx, ty, ptr) + Storage::ParamsIndirect(storage) => { + direct_results_lower(cx, map_maybe_uninit!(storage.ret)) } + Storage::ResultsIndirect(storage) => indirect_results_lower(cx, &storage.retptr), + Storage::Indirect(storage) => indirect_results_lower(cx, &storage.retptr), } } } diff --git a/crates/wasmtime/src/runtime/rr/events.rs b/crates/wasmtime/src/runtime/rr/events.rs index 4579fd71fac1..bcee12ed0db5 100644 --- a/crates/wasmtime/src/runtime/rr/events.rs +++ b/crates/wasmtime/src/runtime/rr/events.rs @@ -53,6 +53,13 @@ impl From for ValRaw { } } +impl From> for ValRawSer { + /// Uninitialized data is assumed, and serialized + fn from(value: MaybeUninit) -> Self { + unsafe { Self::from(value.assume_init()) } + } +} + impl fmt::Debug for ValRawSer { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let hex_digits_per_byte = 2; @@ -66,14 +73,16 @@ impl fmt::Debug for ValRawSer { type RRFuncArgVals = Vec; -/// Note: Switch [`RRFuncArgTypes`] to use [`Vec`] for better efficiency -type RRFuncArgTypes = WasmFuncType; +/// Note: Switch [`CoreFuncArgTypes`] to use [`Vec`] for better efficiency +type CoreFuncArgTypes = WasmFuncType; /// Construct [`RRFuncArgVals`] from raw value buffer -fn func_argvals_from_raw_slice(args: &[MaybeUninit]) -> RRFuncArgVals { - args.iter() - .map(|x| unsafe { ValRawSer::from(x.assume_init()) }) - .collect::>() +fn func_argvals_from_raw_slice(args: &[T]) -> RRFuncArgVals +where + ValRawSer: From, + T: Copy, +{ + args.iter().map(|x| ValRawSer::from(*x)).collect::>() } /// Encode [`RRFuncArgVals`] back into raw value buffer @@ -105,12 +114,66 @@ where } } -struct ComponentHostFuncEntryEvent { - /// Raw values passed across the call/return boundary +/// A call event from a Wasm component into the host +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ComponentHostFuncEntryEvent { + /// Raw values passed across the call entry boundary + args: RRFuncArgVals, + + /// Optional param/return types (required to support replay validation). + /// + /// Note: This relies on the invariant that [InterfaceType] will always be + /// deterministic. Currently, the type indices into various [ComponentTypes] + /// do maintain this allowing for quick type-checking. + types: Option, +} +impl ComponentHostFuncEntryEvent { + // Record + pub fn new(args: &[MaybeUninit], types: Option) -> Self { + Self { + args: func_argvals_from_raw_slice(args), + types: types, + } + } + // Replay + pub fn validate(&self, expect_types: &InterfaceType) -> Result<(), ReplayError> { + replay_args_typecheck(self.types.as_ref(), expect_types) + } +} + +/// A return event after a host call for a Wasm component +/// +/// Matches 1:1 with [`ComponentHostFuncEntryEvent`] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ComponentHostFuncReturnEvent { + /// Lowered values passed across the call return boundary args: RRFuncArgVals, /// Optional param/return types (required to support replay validation) types: Option, } +impl ComponentHostFuncReturnEvent { + // Record + pub fn new(args: &[ValRaw], types: Option) -> Self { + Self { + args: func_argvals_from_raw_slice(args), + types: types, + } + } + // Replay + /// Consume the caller event and encode it back into the slice with an optional + /// typechecking validation of the event. + pub fn move_into_slice( + self, + args: &mut [MaybeUninit], + expect_types: Option<&InterfaceType>, + ) -> Result<(), ReplayError> { + if let Some(e) = expect_types { + replay_args_typecheck(self.types.as_ref(), e)?; + } + func_argvals_into_raw_slice(self.args, args); + Ok(()) + } +} /// A call event from a Core Wasm module into the host #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -118,9 +181,8 @@ pub struct CoreHostFuncEntryEvent { /// Raw values passed across the call/return boundary args: RRFuncArgVals, /// Optional param/return types (required to support replay validation) - types: Option, + types: Option, } - impl CoreHostFuncEntryEvent { // Record pub fn new(args: &[MaybeUninit], types: Option) -> Self { @@ -130,7 +192,7 @@ impl CoreHostFuncEntryEvent { } } // Replay - pub fn validate(&self, expect_types: &RRFuncArgTypes) -> Result<(), ReplayError> { + pub fn validate(&self, expect_types: &CoreFuncArgTypes) -> Result<(), ReplayError> { replay_args_typecheck(self.types.as_ref(), expect_types) } } @@ -143,9 +205,8 @@ pub struct CoreHostFuncReturnEvent { /// Raw values passed across the call/return boundary args: RRFuncArgVals, /// Optional param/return types (required to support replay validation) - types: Option, + types: Option, } - impl CoreHostFuncReturnEvent { // Record pub fn new(args: &[MaybeUninit], types: Option) -> Self { diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index f73334d54c8b..9ecd6a2d97a8 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -16,40 +16,6 @@ use std::io::{BufWriter, Seek, Write}; mod events; pub use events::*; -pub trait Recorder { - /// Constructs a writer on new buffer - fn new_recorder(cfg: RecordConfig) -> Result - where - Self: Sized; - - /// Push a newly record event [`RREvent`] to the buffer - fn push_event(&mut self, event: RREvent) -> (); - - /// Flush memory contents to underlying persistent storage - /// - /// Buffer should be emptied during this process - fn flush_to_file(&mut self) -> Result<()>; - - /// Get metadata associated with the recording process - fn metadata(&self) -> &RecordMetadata; -} - -pub trait Replayer { - type ReplayError; - - /// Constructs a reader on buffer - fn new_replayer(cfg: ReplayConfig) -> Result - where - Self: Sized; - - /// Pop the next [`RREvent`] from the buffer - /// Events should be FIFO - fn pop_event(&mut self) -> Result; - - /// Get metadata associated with the replay process - fn metadata(&self) -> &ReplayMetadata; -} - /// Macro template for [`RREvent`] and its conversion to/from specific /// event types macro_rules! rr_event { @@ -86,7 +52,43 @@ macro_rules! rr_event { // Set of supported events rr_event! { CoreHostFuncEntry(CoreHostFuncEntryEvent), - CoreHostFuncReturn(CoreHostFuncReturnEvent) + CoreHostFuncReturn(CoreHostFuncReturnEvent), + ComponentHostFuncEntry(ComponentHostFuncEntryEvent), + ComponentHostFuncReturn(ComponentHostFuncReturnEvent) +} + +pub trait Recorder { + /// Constructs a writer on new buffer + fn new_recorder(cfg: RecordConfig) -> Result + where + Self: Sized; + + /// Push a newly record event [`RREvent`] to the buffer + fn push_event(&mut self, event: RREvent) -> (); + + /// Flush memory contents to underlying persistent storage + /// + /// Buffer should be emptied during this process + fn flush_to_file(&mut self) -> Result<()>; + + /// Get metadata associated with the recording process + fn metadata(&self) -> &RecordMetadata; +} + +pub trait Replayer { + type ReplayError; + + /// Constructs a reader on buffer + fn new_replayer(cfg: ReplayConfig) -> Result + where + Self: Sized; + + /// Pop the next [`RREvent`] from the buffer + /// Events should be FIFO + fn pop_event(&mut self) -> Result; + + /// Get metadata associated with the replay process + fn metadata(&self) -> &ReplayMetadata; } /// The underlying serialized/deserialized type From 586ba3a039968ea6efe1df7495b1a1ba96a1f29d Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Wed, 2 Jul 2025 13:25:44 -0700 Subject: [PATCH 18/62] Added component host function entry/exit (shallow) replay support --- .../src/runtime/component/func/host.rs | 43 +++++++++++++++---- crates/wasmtime/src/runtime/rr/events.rs | 20 ++++++--- 2 files changed, 50 insertions(+), 13 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 7d860b1ceaa1..9059616f1e2c 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -1,6 +1,6 @@ use crate::component::func::{LiftContext, LowerContext, Options}; use crate::component::matching::InstanceType; -use crate::component::storage::{slice_to_storage_mut, storage_as_slice}; +use crate::component::storage::{slice_to_storage_mut, storage_as_slice, storage_as_slice_mut}; use crate::component::{ComponentNamedList, ComponentType, Lift, Lower, Val}; use crate::prelude::*; use crate::runtime::rr::{ComponentHostFuncEntryEvent, ComponentHostFuncReturnEvent}; @@ -216,6 +216,10 @@ where ) }, ); + cx.0.replay_event( + |r| r.validate, + |event: ComponentHostFuncEntryEvent, _| event.validate(¶m_tys), + )?; // There's a 2x2 matrix of whether parameters and results are stored on the // stack or on the heap. Each of the 4 branches here have a different @@ -239,18 +243,28 @@ where Storage::Indirect(slice_to_storage_mut(storage).assume_init_ref()) } }; - let mut lift = LiftContext::new(cx.0, &options, types, instance); - lift.enter_call(); - let params = storage.lift_params(&mut lift, param_tys)?; + let replay_enabled = cx.0.replay_enabled(); + let ret = if replay_enabled { + None + } else { + Some({ + let mut lift = LiftContext::new(cx.0, &options, types, instance); + lift.enter_call(); + + let params = storage.lift_params(&mut lift, param_tys)?; + closure(cx.as_context_mut(), params)? + }) + }; - let ret = closure(cx.as_context_mut(), params)?; flags.set_may_leave(false); let mut lower = LowerContext::new(cx, &options, types, instance); storage.lower_results(&mut lower, result_tys, ret)?; flags.set_may_leave(true); - lower.exit_call()?; + if !replay_enabled { + lower.exit_call()?; + } return Ok(()); @@ -285,12 +299,25 @@ where &mut self, cx: &mut LowerContext<'_, T>, ty: InterfaceType, - ret: R, + ret: Option, ) -> Result<()> { let direct_results_lower = |cx: &mut LowerContext<'_, T>, dst: &mut MaybeUninit<::Lower>| { - let res = ret.lower(cx, ty, dst); + let res = if let Some(retval) = &ret { + retval.lower(cx, ty, dst) + } else { + // `None` return value implies replay stubbing is required + cx.store.0.replay_event( + |_| true, + |event: ComponentHostFuncReturnEvent, rmeta| { + event.move_into_slice( + storage_as_slice_mut(dst), + rmeta.validate.then_some(&ty), + ) + }, + ) + }; cx.store.0.record_event( |_| true, |rmeta| { diff --git a/crates/wasmtime/src/runtime/rr/events.rs b/crates/wasmtime/src/runtime/rr/events.rs index bcee12ed0db5..3796051cc66f 100644 --- a/crates/wasmtime/src/runtime/rr/events.rs +++ b/crates/wasmtime/src/runtime/rr/events.rs @@ -22,7 +22,7 @@ impl fmt::Display for ReplayError { write!(f, "replay buffer is empty!") } Self::FailedFuncValidation => { - write!(f, "func replay event typecheck validation failed") + write!(f, "func replay event validation failed") } Self::IncorrectEventVariant => { write!(f, "event methods invoked on incorrect variant") @@ -36,7 +36,8 @@ impl std::error::Error for ReplayError {} /// Transmutable byte array used to serialize [`ValRaw`] union /// /// Maintaining the exact layout is crucial for zero-copy transmutations -/// between [`ValRawSer`] and [`ValRaw`] +/// between [`ValRawSer`] and [`ValRaw`] as currently assumed. However, +/// in the future, this type could be represented with LEB128s #[derive(PartialEq, Eq, Clone, Serialize, Deserialize)] #[repr(C)] pub(super) struct ValRawSer([u8; VAL_RAW_SIZE]); @@ -60,6 +61,12 @@ impl From> for ValRawSer { } } +impl From for MaybeUninit { + fn from(value: ValRawSer) -> Self { + MaybeUninit::new(value.into()) + } +} + impl fmt::Debug for ValRawSer { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let hex_digits_per_byte = 2; @@ -86,9 +93,12 @@ where } /// Encode [`RRFuncArgVals`] back into raw value buffer -fn func_argvals_into_raw_slice(rr_args: RRFuncArgVals, raw_args: &mut [MaybeUninit]) { +fn func_argvals_into_raw_slice(rr_args: RRFuncArgVals, raw_args: &mut [T]) +where + ValRawSer: Into, +{ for (src, dst) in rr_args.into_iter().zip(raw_args.iter_mut()) { - *dst = MaybeUninit::new(src.into()); + *dst = src.into(); } } @@ -164,7 +174,7 @@ impl ComponentHostFuncReturnEvent { /// typechecking validation of the event. pub fn move_into_slice( self, - args: &mut [MaybeUninit], + args: &mut [ValRaw], expect_types: Option<&InterfaceType>, ) -> Result<(), ReplayError> { if let Some(e) = expect_types { From 89ea4846342684cd31d0061d01e8559de1d8851c Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Wed, 2 Jul 2025 18:08:01 -0700 Subject: [PATCH 19/62] fixup! Added component host function entry/exit (shallow) replay support --- .../src/runtime/component/func/host.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 9059616f1e2c..e581574fad9b 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -331,16 +331,15 @@ where }; let indirect_results_lower = |cx: &mut LowerContext<'_, T>, dst: &ValRaw| { let ptr = validate_inbounds::(cx.as_slice_mut(), dst)?; - let res = ret.store(cx, ty, ptr); - cx.store.0.record_event( - |_| true, - |rmeta| { - ComponentHostFuncReturnEvent::new( - slice::from_ref(dst), - rmeta.add_validation.then_some(ty), - ) - }, - ); + let res = if let Some(retval) = &ret { + retval.store(cx, ty, ptr) + } else { + // `dst` is a Wasm i32 pointer to indirect results. This itself will remain + // deterministic, and thus replay will not change this, but will have to + // account for all of the `store` values. + // Replay will however have to overwrite all of the stored values (deep copy) + Ok(()) + }; res }; match self { From f21361baf7a9f223bfb64b3518ca1b3e0b194f0e Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Mon, 7 Jul 2025 16:14:28 -0700 Subject: [PATCH 20/62] Support dynamic entrypoints and defining linker imports as trap during replay --- .../src/runtime/component/func/host.rs | 203 +++++++++++++----- crates/wasmtime/src/runtime/rr/events.rs | 34 +-- 2 files changed, 170 insertions(+), 67 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index e581574fad9b..0488c69de82b 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -13,10 +13,9 @@ use alloc::sync::Arc; use core::any::Any; use core::mem::{self, MaybeUninit}; use core::ptr::NonNull; -use core::slice; use wasmtime_environ::component::{ CanonicalAbiInfo, ComponentTypes, InterfaceType, MAX_FLAT_PARAMS, MAX_FLAT_RESULTS, - StringEncoding, TypeFuncIndex, + StringEncoding, TypeFuncIndex, TypeTuple, }; pub struct HostFunc { @@ -212,13 +211,13 @@ where storage, // Don't need to check validation here since it is // covered by the push predicate in this case - Some(param_tys), + Some(&types[ty.params]), ) }, ); cx.0.replay_event( |r| r.validate, - |event: ComponentHostFuncEntryEvent, _| event.validate(¶m_tys), + |event: ComponentHostFuncEntryEvent, _| event.validate(&types[ty.params]), )?; // There's a 2x2 matrix of whether parameters and results are stored on the @@ -259,7 +258,7 @@ where flags.set_may_leave(false); let mut lower = LowerContext::new(cx, &options, types, instance); - storage.lower_results(&mut lower, result_tys, ret)?; + storage.lower_results(&mut lower, result_tys, &types[ty.results], ret)?; flags.set_may_leave(true); if !replay_enabled { @@ -299,6 +298,7 @@ where &mut self, cx: &mut LowerContext<'_, T>, ty: InterfaceType, + rr_tys: &TypeTuple, ret: Option, ) -> Result<()> { let direct_results_lower = @@ -313,7 +313,7 @@ where |event: ComponentHostFuncReturnEvent, rmeta| { event.move_into_slice( storage_as_slice_mut(dst), - rmeta.validate.then_some(&ty), + rmeta.validate.then_some(rr_tys), ) }, ) @@ -323,7 +323,7 @@ where |rmeta| { ComponentHostFuncReturnEvent::new( storage_as_slice(dst), - rmeta.add_validation.then_some(ty), + rmeta.add_validation.then_some(rr_tys), ) }, ); @@ -334,12 +334,30 @@ where let res = if let Some(retval) = &ret { retval.store(cx, ty, ptr) } else { - // `dst` is a Wasm i32 pointer to indirect results. This itself will remain - // deterministic, and thus replay will not change this, but will have to - // account for all of the `store` values. - // Replay will however have to overwrite all of the stored values (deep copy) - Ok(()) + // `dst` is a Wasm pointer to indirect results. This pointer itself will remain + // deterministic, and thus replay will not need to change this. However, + // replay will have to overwrite any nested stored lowerings (deep copy) + cx.store.0.replay_event( + |_| true, + |event: ComponentHostFuncReturnEvent, rmeta| { + if rmeta.validate { + event.validate(rr_tys) + } else { + Ok(()) + } + }, + ) }; + // Recording here is just for marking the return event + cx.store.0.record_event( + |_| true, + |rmeta| { + ComponentHostFuncReturnEvent::new( + &[], + rmeta.add_validation.then_some(rr_tys), + ) + }, + ); res }; match self { @@ -438,61 +456,138 @@ where let func_ty = &types[ty]; let param_tys = &types[func_ty.params]; let result_tys = &types[func_ty.results]; - let mut cx = LiftContext::new(store.0, &options, types, instance); - cx.enter_call(); - if let Some(param_count) = param_tys.abi.flat_count(MAX_FLAT_PARAMS) { - // NB: can use `MaybeUninit::slice_assume_init_ref` when that's stable - let mut iter = - mem::transmute::<&[MaybeUninit], &[ValRaw]>(&storage[..param_count]).iter(); - args = param_tys - .types - .iter() - .map(|ty| Val::lift(&mut cx, *ty, &mut iter)) - .collect::>>()?; - ret_index = param_count; - assert!(iter.next().is_none()); + + let replay_enabled = store.0.replay_enabled(); + + store.0.record_event( + |r| r.add_validation, + |_| { + ComponentHostFuncEntryEvent::new( + storage, + // Don't need to check validation here since it is + // covered by the push predicate in this case + Some(&types[func_ty.params]), + ) + }, + ); + store.0.replay_event( + |r| r.validate, + |event: ComponentHostFuncEntryEvent, _| event.validate(&types[func_ty.params]), + )?; + + let results = if replay_enabled { + None } else { - let mut offset = - validate_inbounds_dynamic(¶m_tys.abi, cx.memory(), storage[0].assume_init_ref())?; - args = param_tys - .types - .iter() - .map(|ty| { - let abi = types.canonical_abi(ty); - let size = usize::try_from(abi.size32).unwrap(); - let memory = &cx.memory()[abi.next_field32_size(&mut offset)..][..size]; - Val::load(&mut cx, *ty, memory) - }) - .collect::>>()?; - ret_index = 1; - }; + Some({ + let mut cx = LiftContext::new(store.0, &options, types, instance); + cx.enter_call(); + if let Some(param_count) = param_tys.abi.flat_count(MAX_FLAT_PARAMS) { + // NB: can use `MaybeUninit::slice_assume_init_ref` when that's stable + let mut iter = + mem::transmute::<&[MaybeUninit], &[ValRaw]>(&storage[..param_count]) + .iter(); + args = param_tys + .types + .iter() + .map(|ty| Val::lift(&mut cx, *ty, &mut iter)) + .collect::>>()?; + ret_index = param_count; + assert!(iter.next().is_none()); + } else { + let mut offset = validate_inbounds_dynamic( + ¶m_tys.abi, + cx.memory(), + storage[0].assume_init_ref(), + )?; + args = param_tys + .types + .iter() + .map(|ty| { + let abi = types.canonical_abi(ty); + let size = usize::try_from(abi.size32).unwrap(); + let memory = &cx.memory()[abi.next_field32_size(&mut offset)..][..size]; + Val::load(&mut cx, *ty, memory) + }) + .collect::>>()?; + ret_index = 1; + }; - let mut result_vals = Vec::with_capacity(result_tys.types.len()); - for _ in result_tys.types.iter() { - result_vals.push(Val::Bool(false)); - } - closure(store.as_context_mut(), &args, &mut result_vals)?; + let mut result_vals = Vec::with_capacity(result_tys.types.len()); + for _ in result_tys.types.iter() { + result_vals.push(Val::Bool(false)); + } + closure(store.as_context_mut(), &args, &mut result_vals)?; + (result_vals, ret_index) + }) + }; flags.set_may_leave(false); let mut cx = LowerContext::new(store, &options, types, instance); if let Some(cnt) = result_tys.abi.flat_count(MAX_FLAT_RESULTS) { - let mut dst = storage[..cnt].iter_mut(); - for (val, ty) in result_vals.iter().zip(result_tys.types.iter()) { - val.lower(&mut cx, *ty, &mut dst)?; + if let Some((result_vals, _)) = results { + let mut dst = storage[..cnt].iter_mut(); + for (val, ty) in result_vals.iter().zip(result_tys.types.iter()) { + val.lower(&mut cx, *ty, &mut dst)?; + } + assert!(dst.next().is_none()); + } else { + // Replay stubbing required + cx.store.0.replay_event( + |_| true, + |event: ComponentHostFuncReturnEvent, rmeta| { + event.move_into_slice( + mem::transmute::<&mut [MaybeUninit], &mut [ValRaw]>(storage), + rmeta.validate.then_some(result_tys), + ) + }, + )?; } - assert!(dst.next().is_none()); + cx.store.0.record_event( + |_| true, + |rmeta| { + ComponentHostFuncReturnEvent::new( + mem::transmute::<&[MaybeUninit], &[ValRaw]>(storage), + rmeta.add_validation.then_some(result_tys), + ) + }, + ); } else { - let ret_ptr = storage[ret_index].assume_init_ref(); - let mut ptr = validate_inbounds_dynamic(&result_tys.abi, cx.as_slice_mut(), ret_ptr)?; - for (val, ty) in result_vals.iter().zip(result_tys.types.iter()) { - let offset = types.canonical_abi(ty).next_field32_size(&mut ptr); - val.store(&mut cx, *ty, offset)?; + if let Some((result_vals, ret_index)) = results { + let ret_ptr = storage[ret_index].assume_init_ref(); + let mut ptr = validate_inbounds_dynamic(&result_tys.abi, cx.as_slice_mut(), ret_ptr)?; + for (val, ty) in result_vals.iter().zip(result_tys.types.iter()) { + let offset = types.canonical_abi(ty).next_field32_size(&mut ptr); + val.store(&mut cx, *ty, offset)?; + } + } else { + // The indirect `ret_ptr` itself will remain deterministic, and thus replay will not + // need to change this. However, replay will have to overwrite any nested stored + // lowerings (deep copy) + cx.store.0.replay_event( + |_| true, + |event: ComponentHostFuncReturnEvent, rmeta| { + if rmeta.validate { + event.validate(result_tys) + } else { + Ok(()) + } + }, + )?; } + // Recording here is just for marking the return event + cx.store.0.record_event( + |_| true, + |rmeta| { + ComponentHostFuncReturnEvent::new(&[], rmeta.add_validation.then_some(result_tys)) + }, + ); } flags.set_may_leave(true); - cx.exit_call()?; + if !replay_enabled { + cx.exit_call()?; + } return Ok(()); } diff --git a/crates/wasmtime/src/runtime/rr/events.rs b/crates/wasmtime/src/runtime/rr/events.rs index 3796051cc66f..1fa587ca81f6 100644 --- a/crates/wasmtime/src/runtime/rr/events.rs +++ b/crates/wasmtime/src/runtime/rr/events.rs @@ -3,7 +3,7 @@ use crate::prelude::*; use core::fmt; use core::mem::{self, MaybeUninit}; use serde::{Deserialize, Serialize}; -use wasmtime_environ::component::InterfaceType; +use wasmtime_environ::component::TypeTuple; use wasmtime_environ::{WasmFuncType, WasmValType}; const VAL_RAW_SIZE: usize = mem::size_of::(); @@ -105,7 +105,7 @@ where /// Typechecking validation for replay, if `src_types` exist /// /// Returns [`ReplayError::FailedFuncValidation`] if typechecking fails -fn replay_args_typecheck(src_types: Option<&T>, expect_types: &T) -> Result<(), ReplayError> +fn replay_args_typecheck(src_types: Option, expect_types: T) -> Result<(), ReplayError> where T: PartialEq, { @@ -134,19 +134,19 @@ pub struct ComponentHostFuncEntryEvent { /// /// Note: This relies on the invariant that [InterfaceType] will always be /// deterministic. Currently, the type indices into various [ComponentTypes] - /// do maintain this allowing for quick type-checking. - types: Option, + /// maintain this, allowing for quick type-checking. + types: Option, } impl ComponentHostFuncEntryEvent { // Record - pub fn new(args: &[MaybeUninit], types: Option) -> Self { + pub fn new(args: &[MaybeUninit], types: Option<&TypeTuple>) -> Self { Self { args: func_argvals_from_raw_slice(args), - types: types, + types: types.cloned(), } } // Replay - pub fn validate(&self, expect_types: &InterfaceType) -> Result<(), ReplayError> { + pub fn validate(&self, expect_types: &TypeTuple) -> Result<(), ReplayError> { replay_args_typecheck(self.types.as_ref(), expect_types) } } @@ -158,27 +158,35 @@ impl ComponentHostFuncEntryEvent { pub struct ComponentHostFuncReturnEvent { /// Lowered values passed across the call return boundary args: RRFuncArgVals, - /// Optional param/return types (required to support replay validation) - types: Option, + /// Optional param/return types (required to support replay validation). + /// + /// Note: This relies on the invariant that [InterfaceType] will always be + /// deterministic. Currently, the type indices into various [ComponentTypes] + /// maintain this, allowing for quick type-checking. + types: Option, } impl ComponentHostFuncReturnEvent { // Record - pub fn new(args: &[ValRaw], types: Option) -> Self { + pub fn new(args: &[ValRaw], types: Option<&TypeTuple>) -> Self { Self { args: func_argvals_from_raw_slice(args), - types: types, + types: types.cloned(), } } // Replay + pub fn validate(&self, expect_types: &TypeTuple) -> Result<(), ReplayError> { + replay_args_typecheck(self.types.as_ref(), expect_types) + } + /// Consume the caller event and encode it back into the slice with an optional /// typechecking validation of the event. pub fn move_into_slice( self, args: &mut [ValRaw], - expect_types: Option<&InterfaceType>, + expect_types: Option<&TypeTuple>, ) -> Result<(), ReplayError> { if let Some(e) = expect_types { - replay_args_typecheck(self.types.as_ref(), e)?; + self.validate(e)?; } func_argvals_into_raw_slice(self.args, args); Ok(()) From 3f187d7dac29ce0806b262552dda5a60a57707e2 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 8 Jul 2025 14:25:08 -0700 Subject: [PATCH 21/62] Ensure replay completion and restructure events directory --- crates/wasmtime/src/runtime/rr/events.rs | 250 ------------------ .../src/runtime/rr/events/component_wasm.rs | 72 +++++ .../src/runtime/rr/events/core_wasm.rs | 62 +++++ crates/wasmtime/src/runtime/rr/events/mod.rs | 126 +++++++++ crates/wasmtime/src/runtime/rr/mod.rs | 30 ++- crates/wasmtime/src/runtime/store.rs | 11 +- 6 files changed, 293 insertions(+), 258 deletions(-) delete mode 100644 crates/wasmtime/src/runtime/rr/events.rs create mode 100644 crates/wasmtime/src/runtime/rr/events/component_wasm.rs create mode 100644 crates/wasmtime/src/runtime/rr/events/core_wasm.rs create mode 100644 crates/wasmtime/src/runtime/rr/events/mod.rs diff --git a/crates/wasmtime/src/runtime/rr/events.rs b/crates/wasmtime/src/runtime/rr/events.rs deleted file mode 100644 index 1fa587ca81f6..000000000000 --- a/crates/wasmtime/src/runtime/rr/events.rs +++ /dev/null @@ -1,250 +0,0 @@ -use crate::ValRaw; -use crate::prelude::*; -use core::fmt; -use core::mem::{self, MaybeUninit}; -use serde::{Deserialize, Serialize}; -use wasmtime_environ::component::TypeTuple; -use wasmtime_environ::{WasmFuncType, WasmValType}; - -const VAL_RAW_SIZE: usize = mem::size_of::(); - -#[derive(Debug)] -pub enum ReplayError { - EmptyBuffer, - FailedFuncValidation, - IncorrectEventVariant, -} - -impl fmt::Display for ReplayError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::EmptyBuffer => { - write!(f, "replay buffer is empty!") - } - Self::FailedFuncValidation => { - write!(f, "func replay event validation failed") - } - Self::IncorrectEventVariant => { - write!(f, "event methods invoked on incorrect variant") - } - } - } -} - -impl std::error::Error for ReplayError {} - -/// Transmutable byte array used to serialize [`ValRaw`] union -/// -/// Maintaining the exact layout is crucial for zero-copy transmutations -/// between [`ValRawSer`] and [`ValRaw`] as currently assumed. However, -/// in the future, this type could be represented with LEB128s -#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)] -#[repr(C)] -pub(super) struct ValRawSer([u8; VAL_RAW_SIZE]); - -impl From for ValRawSer { - fn from(value: ValRaw) -> Self { - unsafe { Self(mem::transmute(value)) } - } -} - -impl From for ValRaw { - fn from(value: ValRawSer) -> Self { - unsafe { mem::transmute(value.0) } - } -} - -impl From> for ValRawSer { - /// Uninitialized data is assumed, and serialized - fn from(value: MaybeUninit) -> Self { - unsafe { Self::from(value.assume_init()) } - } -} - -impl From for MaybeUninit { - fn from(value: ValRawSer) -> Self { - MaybeUninit::new(value.into()) - } -} - -impl fmt::Debug for ValRawSer { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let hex_digits_per_byte = 2; - let _ = write!(f, "0x.."); - for b in self.0.iter().rev() { - let _ = write!(f, "{:0width$x}", b, width = hex_digits_per_byte); - } - Ok(()) - } -} - -type RRFuncArgVals = Vec; - -/// Note: Switch [`CoreFuncArgTypes`] to use [`Vec`] for better efficiency -type CoreFuncArgTypes = WasmFuncType; - -/// Construct [`RRFuncArgVals`] from raw value buffer -fn func_argvals_from_raw_slice(args: &[T]) -> RRFuncArgVals -where - ValRawSer: From, - T: Copy, -{ - args.iter().map(|x| ValRawSer::from(*x)).collect::>() -} - -/// Encode [`RRFuncArgVals`] back into raw value buffer -fn func_argvals_into_raw_slice(rr_args: RRFuncArgVals, raw_args: &mut [T]) -where - ValRawSer: Into, -{ - for (src, dst) in rr_args.into_iter().zip(raw_args.iter_mut()) { - *dst = src.into(); - } -} - -/// Typechecking validation for replay, if `src_types` exist -/// -/// Returns [`ReplayError::FailedFuncValidation`] if typechecking fails -fn replay_args_typecheck(src_types: Option, expect_types: T) -> Result<(), ReplayError> -where - T: PartialEq, -{ - if let Some(types) = src_types { - if types == expect_types { - Ok(()) - } else { - Err(ReplayError::FailedFuncValidation) - } - } else { - println!( - "Warning: Replay typechecking cannot be performed - since recorded trace is missing validation data" - ); - Ok(()) - } -} - -/// A call event from a Wasm component into the host -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ComponentHostFuncEntryEvent { - /// Raw values passed across the call entry boundary - args: RRFuncArgVals, - - /// Optional param/return types (required to support replay validation). - /// - /// Note: This relies on the invariant that [InterfaceType] will always be - /// deterministic. Currently, the type indices into various [ComponentTypes] - /// maintain this, allowing for quick type-checking. - types: Option, -} -impl ComponentHostFuncEntryEvent { - // Record - pub fn new(args: &[MaybeUninit], types: Option<&TypeTuple>) -> Self { - Self { - args: func_argvals_from_raw_slice(args), - types: types.cloned(), - } - } - // Replay - pub fn validate(&self, expect_types: &TypeTuple) -> Result<(), ReplayError> { - replay_args_typecheck(self.types.as_ref(), expect_types) - } -} - -/// A return event after a host call for a Wasm component -/// -/// Matches 1:1 with [`ComponentHostFuncEntryEvent`] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ComponentHostFuncReturnEvent { - /// Lowered values passed across the call return boundary - args: RRFuncArgVals, - /// Optional param/return types (required to support replay validation). - /// - /// Note: This relies on the invariant that [InterfaceType] will always be - /// deterministic. Currently, the type indices into various [ComponentTypes] - /// maintain this, allowing for quick type-checking. - types: Option, -} -impl ComponentHostFuncReturnEvent { - // Record - pub fn new(args: &[ValRaw], types: Option<&TypeTuple>) -> Self { - Self { - args: func_argvals_from_raw_slice(args), - types: types.cloned(), - } - } - // Replay - pub fn validate(&self, expect_types: &TypeTuple) -> Result<(), ReplayError> { - replay_args_typecheck(self.types.as_ref(), expect_types) - } - - /// Consume the caller event and encode it back into the slice with an optional - /// typechecking validation of the event. - pub fn move_into_slice( - self, - args: &mut [ValRaw], - expect_types: Option<&TypeTuple>, - ) -> Result<(), ReplayError> { - if let Some(e) = expect_types { - self.validate(e)?; - } - func_argvals_into_raw_slice(self.args, args); - Ok(()) - } -} - -/// A call event from a Core Wasm module into the host -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CoreHostFuncEntryEvent { - /// Raw values passed across the call/return boundary - args: RRFuncArgVals, - /// Optional param/return types (required to support replay validation) - types: Option, -} -impl CoreHostFuncEntryEvent { - // Record - pub fn new(args: &[MaybeUninit], types: Option) -> Self { - Self { - args: func_argvals_from_raw_slice(args), - types: types, - } - } - // Replay - pub fn validate(&self, expect_types: &CoreFuncArgTypes) -> Result<(), ReplayError> { - replay_args_typecheck(self.types.as_ref(), expect_types) - } -} - -/// A return event after a host call for a Core Wasm -/// -/// Matches 1:1 with [`CoreHostFuncEntryEvent`] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CoreHostFuncReturnEvent { - /// Raw values passed across the call/return boundary - args: RRFuncArgVals, - /// Optional param/return types (required to support replay validation) - types: Option, -} -impl CoreHostFuncReturnEvent { - // Record - pub fn new(args: &[MaybeUninit], types: Option) -> Self { - Self { - args: func_argvals_from_raw_slice(args), - types: types, - } - } - // Replay - /// Consume the caller event and encode it back into the slice with an optional - /// typechecking validation of the event. - pub fn move_into_slice( - self, - args: &mut [MaybeUninit], - expect_types: Option<&WasmFuncType>, - ) -> Result<(), ReplayError> { - if let Some(e) = expect_types { - replay_args_typecheck(self.types.as_ref(), e)?; - } - func_argvals_into_raw_slice(self.args, args); - Ok(()) - } -} diff --git a/crates/wasmtime/src/runtime/rr/events/component_wasm.rs b/crates/wasmtime/src/runtime/rr/events/component_wasm.rs new file mode 100644 index 000000000000..504942d5945b --- /dev/null +++ b/crates/wasmtime/src/runtime/rr/events/component_wasm.rs @@ -0,0 +1,72 @@ +//! Module comprising of component model wasm events +use super::*; +use wasmtime_environ::component::TypeTuple; + +/// A call event from a Wasm component into the host +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ComponentHostFuncEntryEvent { + /// Raw values passed across the call entry boundary + args: RRFuncArgVals, + + /// Optional param/return types (required to support replay validation). + /// + /// Note: This relies on the invariant that [InterfaceType] will always be + /// deterministic. Currently, the type indices into various [ComponentTypes] + /// maintain this, allowing for quick type-checking. + types: Option, +} +impl ComponentHostFuncEntryEvent { + // Record + pub fn new(args: &[MaybeUninit], types: Option<&TypeTuple>) -> Self { + Self { + args: func_argvals_from_raw_slice(args), + types: types.cloned(), + } + } + // Replay + pub fn validate(&self, expect_types: &TypeTuple) -> Result<(), ReplayError> { + replay_args_typecheck(self.types.as_ref(), expect_types) + } +} + +/// A return event after a host call for a Wasm component +/// +/// Matches 1:1 with [`ComponentHostFuncEntryEvent`] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ComponentHostFuncReturnEvent { + /// Lowered values passed across the call return boundary + args: RRFuncArgVals, + /// Optional param/return types (required to support replay validation). + /// + /// Note: This relies on the invariant that [InterfaceType] will always be + /// deterministic. Currently, the type indices into various [ComponentTypes] + /// maintain this, allowing for quick type-checking. + types: Option, +} +impl ComponentHostFuncReturnEvent { + // Record + pub fn new(args: &[ValRaw], types: Option<&TypeTuple>) -> Self { + Self { + args: func_argvals_from_raw_slice(args), + types: types.cloned(), + } + } + // Replay + pub fn validate(&self, expect_types: &TypeTuple) -> Result<(), ReplayError> { + replay_args_typecheck(self.types.as_ref(), expect_types) + } + + /// Consume the caller event and encode it back into the slice with an optional + /// typechecking validation of the event. + pub fn move_into_slice( + self, + args: &mut [ValRaw], + expect_types: Option<&TypeTuple>, + ) -> Result<(), ReplayError> { + if let Some(e) = expect_types { + self.validate(e)?; + } + func_argvals_into_raw_slice(self.args, args); + Ok(()) + } +} diff --git a/crates/wasmtime/src/runtime/rr/events/core_wasm.rs b/crates/wasmtime/src/runtime/rr/events/core_wasm.rs new file mode 100644 index 000000000000..4563a7c157d8 --- /dev/null +++ b/crates/wasmtime/src/runtime/rr/events/core_wasm.rs @@ -0,0 +1,62 @@ +//! Module comprising of core wasm events +use super::*; +use wasmtime_environ::{WasmFuncType, WasmValType}; + +/// Note: Switch [`CoreFuncArgTypes`] to use [`Vec`] for better efficiency +type CoreFuncArgTypes = WasmFuncType; + +/// A call event from a Core Wasm module into the host +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CoreHostFuncEntryEvent { + /// Raw values passed across the call/return boundary + args: RRFuncArgVals, + /// Optional param/return types (required to support replay validation) + types: Option, +} +impl CoreHostFuncEntryEvent { + // Record + pub fn new(args: &[MaybeUninit], types: Option) -> Self { + Self { + args: func_argvals_from_raw_slice(args), + types: types, + } + } + // Replay + pub fn validate(&self, expect_types: &CoreFuncArgTypes) -> Result<(), ReplayError> { + replay_args_typecheck(self.types.as_ref(), expect_types) + } +} + +/// A return event after a host call for a Core Wasm +/// +/// Matches 1:1 with [`CoreHostFuncEntryEvent`] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CoreHostFuncReturnEvent { + /// Raw values passed across the call/return boundary + args: RRFuncArgVals, + /// Optional param/return types (required to support replay validation) + types: Option, +} +impl CoreHostFuncReturnEvent { + // Record + pub fn new(args: &[MaybeUninit], types: Option) -> Self { + Self { + args: func_argvals_from_raw_slice(args), + types: types, + } + } + // Replay + /// Consume the caller event and encode it back into the slice with an optional + /// typechecking validation of the event. + pub fn move_into_slice( + self, + args: &mut [MaybeUninit], + expect_types: Option<&WasmFuncType>, + ) -> Result<(), ReplayError> { + if let Some(e) = expect_types { + replay_args_typecheck(self.types.as_ref(), e)?; + } + func_argvals_into_raw_slice(self.args, args); + Ok(()) + } +} diff --git a/crates/wasmtime/src/runtime/rr/events/mod.rs b/crates/wasmtime/src/runtime/rr/events/mod.rs new file mode 100644 index 000000000000..86135cd99039 --- /dev/null +++ b/crates/wasmtime/src/runtime/rr/events/mod.rs @@ -0,0 +1,126 @@ +use crate::ValRaw; +use crate::prelude::*; +use core::fmt; +use core::mem::{self, MaybeUninit}; +use serde::{Deserialize, Serialize}; + +const VAL_RAW_SIZE: usize = mem::size_of::(); + +#[derive(Debug)] +pub enum ReplayError { + EmptyBuffer, + FailedFuncValidation, + IncorrectEventVariant, +} + +impl fmt::Display for ReplayError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::EmptyBuffer => { + write!(f, "replay buffer is empty!") + } + Self::FailedFuncValidation => { + write!(f, "func replay event validation failed") + } + Self::IncorrectEventVariant => { + write!(f, "event methods invoked on incorrect variant") + } + } + } +} + +impl std::error::Error for ReplayError {} + +/// Transmutable byte array used to serialize [`ValRaw`] union +/// +/// Maintaining the exact layout is crucial for zero-copy transmutations +/// between [`ValRawSer`] and [`ValRaw`] as currently assumed. However, +/// in the future, this type could be represented with LEB128s +#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)] +#[repr(C)] +pub(super) struct ValRawSer([u8; VAL_RAW_SIZE]); + +impl From for ValRawSer { + fn from(value: ValRaw) -> Self { + unsafe { Self(mem::transmute(value)) } + } +} + +impl From for ValRaw { + fn from(value: ValRawSer) -> Self { + unsafe { mem::transmute(value.0) } + } +} + +impl From> for ValRawSer { + /// Uninitialized data is assumed, and serialized + fn from(value: MaybeUninit) -> Self { + unsafe { Self::from(value.assume_init()) } + } +} + +impl From for MaybeUninit { + fn from(value: ValRawSer) -> Self { + MaybeUninit::new(value.into()) + } +} + +impl fmt::Debug for ValRawSer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let hex_digits_per_byte = 2; + let _ = write!(f, "0x.."); + for b in self.0.iter().rev() { + let _ = write!(f, "{:0width$x}", b, width = hex_digits_per_byte); + } + Ok(()) + } +} + +type RRFuncArgVals = Vec; + +/// Construct [`RRFuncArgVals`] from raw value buffer +fn func_argvals_from_raw_slice(args: &[T]) -> RRFuncArgVals +where + ValRawSer: From, + T: Copy, +{ + args.iter().map(|x| ValRawSer::from(*x)).collect::>() +} + +/// Encode [`RRFuncArgVals`] back into raw value buffer +fn func_argvals_into_raw_slice(rr_args: RRFuncArgVals, raw_args: &mut [T]) +where + ValRawSer: Into, +{ + for (src, dst) in rr_args.into_iter().zip(raw_args.iter_mut()) { + *dst = src.into(); + } +} + +/// Typechecking validation for replay, if `src_types` exist +/// +/// Returns [`ReplayError::FailedFuncValidation`] if typechecking fails +fn replay_args_typecheck(src_types: Option, expect_types: T) -> Result<(), ReplayError> +where + T: PartialEq, +{ + if let Some(types) = src_types { + if types == expect_types { + Ok(()) + } else { + Err(ReplayError::FailedFuncValidation) + } + } else { + println!( + "Warning: Replay typechecking cannot be performed + since recorded trace is missing validation data" + ); + Ok(()) + } +} + +mod core_wasm; +pub use core_wasm::*; + +mod component_wasm; +pub use component_wasm::*; diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index 9ecd6a2d97a8..f0f4bdcbd181 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -19,7 +19,12 @@ pub use events::*; /// Macro template for [`RREvent`] and its conversion to/from specific /// event types macro_rules! rr_event { - ( $( $variant:ident($event:ty) ),* ) => ( + ( + $( + $(#[doc = $doc:literal])* + $variant:ident => $event:ty + ),* + ) => ( /// A single, unified, low-level recording/replay event /// /// This type is the narrow waist for serialization/deserialization. @@ -27,7 +32,10 @@ macro_rules! rr_event { /// of parameter/return types) may drop down to one or more [`RREvent`]s #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum RREvent { - $($variant($event),)* + $( + $(#[doc = $doc])* + $variant($event), + )* } $( impl From<$event> for RREvent { @@ -49,12 +57,20 @@ macro_rules! rr_event { ); } -// Set of supported events +// Set of supported record/replay events rr_event! { - CoreHostFuncEntry(CoreHostFuncEntryEvent), - CoreHostFuncReturn(CoreHostFuncReturnEvent), - ComponentHostFuncEntry(ComponentHostFuncEntryEvent), - ComponentHostFuncReturn(ComponentHostFuncReturnEvent) + /// Call into host function from Core Wasm + CoreHostFuncEntry => CoreHostFuncEntryEvent, + /// Return from host function to Core Wasm + CoreHostFuncReturn => CoreHostFuncReturnEvent, + /// Call into host function from component + ComponentHostFuncEntry => ComponentHostFuncEntryEvent, + /// Return from host function to component + ComponentHostFuncReturn => ComponentHostFuncReturnEvent + ///// Component ABI Realloc of linear wasm memory + //ComponentRealloc => ComponentReallocEvent, + ///// A store into linear wasm memory during component type lowering operations + //ComponentLowerStore => ComponentLowerStore } pub trait Recorder { diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index 6b12517a6806..2677c015fee5 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -2105,6 +2105,14 @@ at https://bytecodealliance.org/security. } Ok(()) } + + /// Panics if the replay buffer in the store is non-empty + pub(crate) fn ensure_empty_replay_buffer(&mut self) { + if let Some(buf) = self.replay_buffer_mut() { + let event = buf.pop_event(); + assert!(event.is_err_and(|e| matches!(e, ReplayError::EmptyBuffer))); + } + } } /// Helper parameter to [`StoreOpaque::allocate_instance`]. @@ -2435,7 +2443,8 @@ impl Drop for StoreOpaque { } } - let _ = self.flush_record_buffer(); + let _ = self.flush_record_buffer().unwrap(); + self.ensure_empty_replay_buffer(); } } From 73d2a1c18e811d139cd0addd8d442ff9c10ffc38 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Wed, 9 Jul 2025 15:12:20 -0700 Subject: [PATCH 22/62] Add recording for lowering, lowering-stores, and reallocations No replay support is included yet, and the recording still doesn't cover all cases surrounding overriden lowering implementations --- .../src/runtime/component/func/host.rs | 47 ++++---- .../src/runtime/component/func/options.rs | 24 +++- crates/wasmtime/src/runtime/func.rs | 10 +- .../src/runtime/rr/events/component_wasm.rs | 111 +++++++++++++++++- .../src/runtime/rr/events/core_wasm.rs | 11 +- crates/wasmtime/src/runtime/rr/events/mod.rs | 37 +++--- crates/wasmtime/src/runtime/rr/mod.rs | 69 +++++++++-- crates/wasmtime/src/runtime/store.rs | 2 + 8 files changed, 238 insertions(+), 73 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 0488c69de82b..d232ab0917c2 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -3,7 +3,9 @@ use crate::component::matching::InstanceType; use crate::component::storage::{slice_to_storage_mut, storage_as_slice, storage_as_slice_mut}; use crate::component::{ComponentNamedList, ComponentType, Lift, Lower, Val}; use crate::prelude::*; -use crate::runtime::rr::{ComponentHostFuncEntryEvent, ComponentHostFuncReturnEvent}; +use crate::runtime::rr::events::component_wasm::{ + HostFuncEntryEvent, HostFuncReturnEvent, LowerReturnEvent, LowerStoreReturnEvent, +}; use crate::runtime::vm::component::{ ComponentInstance, InstanceFlags, VMComponentContext, VMLowering, VMLoweringCallee, }; @@ -207,7 +209,7 @@ where cx.0.record_event( |r| r.add_validation, |_| { - ComponentHostFuncEntryEvent::new( + HostFuncEntryEvent::new( storage, // Don't need to check validation here since it is // covered by the push predicate in this case @@ -217,7 +219,7 @@ where ); cx.0.replay_event( |r| r.validate, - |event: ComponentHostFuncEntryEvent, _| event.validate(&types[ty.params]), + |event: HostFuncEntryEvent, _| event.validate(&types[ty.params]), )?; // There's a 2x2 matrix of whether parameters and results are stored on the @@ -305,12 +307,16 @@ where |cx: &mut LowerContext<'_, T>, dst: &mut MaybeUninit<::Lower>| { let res = if let Some(retval) = &ret { - retval.lower(cx, ty, dst) + let r = retval.lower(cx, ty, dst); + cx.store + .0 + .record_event(|_| true, |_| LowerReturnEvent::new(&r)); + r } else { // `None` return value implies replay stubbing is required cx.store.0.replay_event( |_| true, - |event: ComponentHostFuncReturnEvent, rmeta| { + |event: HostFuncReturnEvent, rmeta| { event.move_into_slice( storage_as_slice_mut(dst), rmeta.validate.then_some(rr_tys), @@ -321,7 +327,7 @@ where cx.store.0.record_event( |_| true, |rmeta| { - ComponentHostFuncReturnEvent::new( + HostFuncReturnEvent::new( storage_as_slice(dst), rmeta.add_validation.then_some(rr_tys), ) @@ -332,14 +338,18 @@ where let indirect_results_lower = |cx: &mut LowerContext<'_, T>, dst: &ValRaw| { let ptr = validate_inbounds::(cx.as_slice_mut(), dst)?; let res = if let Some(retval) = &ret { - retval.store(cx, ty, ptr) + let r = retval.store(cx, ty, ptr); + cx.store + .0 + .record_event(|_| true, |_| LowerStoreReturnEvent::new(&r)); + r } else { // `dst` is a Wasm pointer to indirect results. This pointer itself will remain // deterministic, and thus replay will not need to change this. However, // replay will have to overwrite any nested stored lowerings (deep copy) cx.store.0.replay_event( |_| true, - |event: ComponentHostFuncReturnEvent, rmeta| { + |event: HostFuncReturnEvent, rmeta| { if rmeta.validate { event.validate(rr_tys) } else { @@ -351,12 +361,7 @@ where // Recording here is just for marking the return event cx.store.0.record_event( |_| true, - |rmeta| { - ComponentHostFuncReturnEvent::new( - &[], - rmeta.add_validation.then_some(rr_tys), - ) - }, + |rmeta| HostFuncReturnEvent::new(&[], rmeta.add_validation.then_some(rr_tys)), ); res }; @@ -462,7 +467,7 @@ where store.0.record_event( |r| r.add_validation, |_| { - ComponentHostFuncEntryEvent::new( + HostFuncEntryEvent::new( storage, // Don't need to check validation here since it is // covered by the push predicate in this case @@ -472,7 +477,7 @@ where ); store.0.replay_event( |r| r.validate, - |event: ComponentHostFuncEntryEvent, _| event.validate(&types[func_ty.params]), + |event: HostFuncEntryEvent, _| event.validate(&types[func_ty.params]), )?; let results = if replay_enabled { @@ -534,7 +539,7 @@ where // Replay stubbing required cx.store.0.replay_event( |_| true, - |event: ComponentHostFuncReturnEvent, rmeta| { + |event: HostFuncReturnEvent, rmeta| { event.move_into_slice( mem::transmute::<&mut [MaybeUninit], &mut [ValRaw]>(storage), rmeta.validate.then_some(result_tys), @@ -545,7 +550,7 @@ where cx.store.0.record_event( |_| true, |rmeta| { - ComponentHostFuncReturnEvent::new( + HostFuncReturnEvent::new( mem::transmute::<&[MaybeUninit], &[ValRaw]>(storage), rmeta.add_validation.then_some(result_tys), ) @@ -565,7 +570,7 @@ where // lowerings (deep copy) cx.store.0.replay_event( |_| true, - |event: ComponentHostFuncReturnEvent, rmeta| { + |event: HostFuncReturnEvent, rmeta| { if rmeta.validate { event.validate(result_tys) } else { @@ -577,9 +582,7 @@ where // Recording here is just for marking the return event cx.store.0.record_event( |_| true, - |rmeta| { - ComponentHostFuncReturnEvent::new(&[], rmeta.add_validation.then_some(result_tys)) - }, + |rmeta| HostFuncReturnEvent::new(&[], rmeta.add_validation.then_some(result_tys)), ); } diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index 88be8a499af0..aaa29d30b6b9 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -2,6 +2,9 @@ use crate::component::ResourceType; use crate::component::matching::InstanceType; use crate::component::resources::{HostResourceData, HostResourceIndex, HostResourceTables}; use crate::prelude::*; +use crate::rr::events::component_wasm::{ + MemorySliceBorrowEvent, ReallocEntryEvent, ReallocReturnEvent, +}; use crate::runtime::vm::component::{ CallContexts, ComponentInstance, InstanceFlags, ResourceTable, ResourceTables, }; @@ -248,7 +251,13 @@ impl<'a, T: 'static> LowerContext<'a, T> { new_size: usize, ) -> Result { let realloc_func_ty = Arc::clone(unsafe { (*self.instance).component().realloc_func_ty() }); - self.options + + self.store.0.record_event( + |_| true, + |_| ReallocEntryEvent::new(old, old_size, old_align, new_size), + ); + let result = self + .options .realloc( &mut self.store, &realloc_func_ty, @@ -257,7 +266,11 @@ impl<'a, T: 'static> LowerContext<'a, T> { old_align, new_size, ) - .map(|(_, ptr)| ptr) + .map(|(_, ptr)| ptr); + self.store + .0 + .record_event(|r| r.add_validation, |_| ReallocReturnEvent::new(&result)); + result } /// Returns a fixed mutable slice of memory `N` bytes large starting at @@ -281,6 +294,13 @@ impl<'a, T: 'static> LowerContext<'a, T> { // For now I figure we can leave in this bounds check and if it becomes // an issue we can optimize further later, probably with judicious use // of `unsafe`. + + // Accessing the store for event recording after getting the slice is + // tricky to resolve by the borrow checker. Instead, we just record before + // since this operation panics anyway, and the replay should still be faithful + self.store + .0 + .record_event(|_| true, |_| MemorySliceBorrowEvent::new(offset, N)); self.as_slice_mut()[offset..].first_chunk_mut().unwrap() } diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 58c211a70041..ccb54d8042d0 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -1,5 +1,5 @@ use crate::prelude::*; -use crate::rr::{CoreHostFuncEntryEvent, CoreHostFuncReturnEvent}; +use crate::rr::events::core_wasm::{HostFuncEntryEvent, HostFuncReturnEvent}; use crate::runtime::Uninhabited; use crate::runtime::vm::{ ExportFunction, InterpreterRef, SendSyncPtr, StoreBox, VMArrayCallHostFuncContext, @@ -2356,14 +2356,14 @@ impl HostContext { // lazily evaluated store.replay_event( |r| r.validate, - |event: CoreHostFuncEntryEvent, _| event.validate(wasm_func_type.unwrap()), + |event: HostFuncEntryEvent, _| event.validate(wasm_func_type.unwrap()), )?; store.record_event( |r| r.add_validation, |_| { let wasm_func_type = wasm_func_type.unwrap(); let num_params = wasm_func_type.params().len(); - CoreHostFuncEntryEvent::new( + HostFuncEntryEvent::new( &args.as_ref()[..num_params], // Don't need to check validation here since it is // covered by the push predicate in this case @@ -2405,7 +2405,7 @@ impl HostContext { let ret = if store.replay_enabled() { store.replay_event( |_| true, - |event: CoreHostFuncReturnEvent, rmeta| { + |event: HostFuncReturnEvent, rmeta| { event.move_into_slice( args.as_mut(), rmeta.validate.then_some(wasm_func_type.unwrap()), @@ -2420,7 +2420,7 @@ impl HostContext { |rmeta| { let wasm_func_type = wasm_func_type.unwrap(); let num_results = wasm_func_type.params().len(); - CoreHostFuncReturnEvent::new( + HostFuncReturnEvent::new( unsafe { &args.as_ref()[..num_results] }, rmeta.add_validation.then_some(wasm_func_type.clone()), ) diff --git a/crates/wasmtime/src/runtime/rr/events/component_wasm.rs b/crates/wasmtime/src/runtime/rr/events/component_wasm.rs index 504942d5945b..75acd4a72f4e 100644 --- a/crates/wasmtime/src/runtime/rr/events/component_wasm.rs +++ b/crates/wasmtime/src/runtime/rr/events/component_wasm.rs @@ -1,10 +1,12 @@ //! Module comprising of component model wasm events use super::*; -use wasmtime_environ::component::TypeTuple; +#[allow(unused_imports)] +use crate::component::ComponentType; +use wasmtime_environ::component::{InterfaceType, TypeTuple}; /// A call event from a Wasm component into the host #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ComponentHostFuncEntryEvent { +pub struct HostFuncEntryEvent { /// Raw values passed across the call entry boundary args: RRFuncArgVals, @@ -15,7 +17,7 @@ pub struct ComponentHostFuncEntryEvent { /// maintain this, allowing for quick type-checking. types: Option, } -impl ComponentHostFuncEntryEvent { +impl HostFuncEntryEvent { // Record pub fn new(args: &[MaybeUninit], types: Option<&TypeTuple>) -> Self { Self { @@ -31,9 +33,9 @@ impl ComponentHostFuncEntryEvent { /// A return event after a host call for a Wasm component /// -/// Matches 1:1 with [`ComponentHostFuncEntryEvent`] +/// Matches 1:1 with [`HostFuncEntryEvent`] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ComponentHostFuncReturnEvent { +pub struct HostFuncReturnEvent { /// Lowered values passed across the call return boundary args: RRFuncArgVals, /// Optional param/return types (required to support replay validation). @@ -43,7 +45,7 @@ pub struct ComponentHostFuncReturnEvent { /// maintain this, allowing for quick type-checking. types: Option, } -impl ComponentHostFuncReturnEvent { +impl HostFuncReturnEvent { // Record pub fn new(args: &[ValRaw], types: Option<&TypeTuple>) -> Self { Self { @@ -70,3 +72,100 @@ impl ComponentHostFuncReturnEvent { Ok(()) } } + +macro_rules! generic_new_result_events { + ( + $( + $(#[doc = $doc:literal])* + $event:ident => ($ok_ty:ty,$err_variant:path) + ),* + ) => ( + $( + $(#[doc= $doc])* + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct $event { + ret: Result<$ok_ty, EventError>, + } + impl $event { + pub fn new(ret: &Result<$ok_ty>) -> Self { + Self { + ret: ret.as_ref().map(|t| *t).map_err(|e| $err_variant(e.to_string())) + } + } + } + )* + ); +} + +macro_rules! generic_new_events { + ( + $( + $(#[doc = $doc:literal])* + $struct:ident { + $( + $field:ident : $field_ty:ty + ),* + } + ),* + ) => ( + $( + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct $struct { + $( + $field: $field_ty, + )* + } + )* + $( + impl $struct { + #[allow(dead_code)] + pub fn new($($field: $field_ty),*) -> Self { + Self { + $($field),* + } + } + } + )* + ); +} + +generic_new_result_events! { + /// Return from a reallocation call (needed only for validation) + ReallocReturnEvent => (usize, EventError::ReallocError), + /// Return from a type lowering invocation + LowerReturnEvent => ((), EventError::LowerError), + /// Return from store invocations during type lowering + LowerStoreReturnEvent => ((), EventError::LowerStoreError) +} + +generic_new_events! { + /// A reallocation call event in the Component Model canonical ABI + /// + /// Usually performed during lowering of complex [`ComponentType`]s to Wasm + ReallocEntryEvent { + old_addr: usize, + old_size: usize, + old_align: u32, + new_size: usize + }, + + LowerEntryEvent { + ty: InterfaceType + }, + + LowerStoreEntryEvent { + ty: InterfaceType, + offset: usize + }, + + /// A mutable borrow of a slice of Wasm linear memory by the host + /// + /// This is the fundamental interface used during lowering of a [`ComponentType`]. + /// Note that this currently signifies a single mutable operation at the smallest granularity + /// on a given linear memory slice. These can be optimized and coalesced into + /// larger granularity operations in the future at either the recording or the replay level. + MemorySliceBorrowEvent { + offset: usize, + size: usize + } +} diff --git a/crates/wasmtime/src/runtime/rr/events/core_wasm.rs b/crates/wasmtime/src/runtime/rr/events/core_wasm.rs index 4563a7c157d8..e57736431cd1 100644 --- a/crates/wasmtime/src/runtime/rr/events/core_wasm.rs +++ b/crates/wasmtime/src/runtime/rr/events/core_wasm.rs @@ -1,5 +1,6 @@ //! Module comprising of core wasm events use super::*; +#[allow(unused_imports)] use wasmtime_environ::{WasmFuncType, WasmValType}; /// Note: Switch [`CoreFuncArgTypes`] to use [`Vec`] for better efficiency @@ -7,13 +8,13 @@ type CoreFuncArgTypes = WasmFuncType; /// A call event from a Core Wasm module into the host #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CoreHostFuncEntryEvent { +pub struct HostFuncEntryEvent { /// Raw values passed across the call/return boundary args: RRFuncArgVals, /// Optional param/return types (required to support replay validation) types: Option, } -impl CoreHostFuncEntryEvent { +impl HostFuncEntryEvent { // Record pub fn new(args: &[MaybeUninit], types: Option) -> Self { Self { @@ -29,15 +30,15 @@ impl CoreHostFuncEntryEvent { /// A return event after a host call for a Core Wasm /// -/// Matches 1:1 with [`CoreHostFuncEntryEvent`] +/// Matches 1:1 with [`HostFuncEntryEvent`] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CoreHostFuncReturnEvent { +pub struct HostFuncReturnEvent { /// Raw values passed across the call/return boundary args: RRFuncArgVals, /// Optional param/return types (required to support replay validation) types: Option, } -impl CoreHostFuncReturnEvent { +impl HostFuncReturnEvent { // Record pub fn new(args: &[MaybeUninit], types: Option) -> Self { Self { diff --git a/crates/wasmtime/src/runtime/rr/events/mod.rs b/crates/wasmtime/src/runtime/rr/events/mod.rs index 86135cd99039..3554dfc1982f 100644 --- a/crates/wasmtime/src/runtime/rr/events/mod.rs +++ b/crates/wasmtime/src/runtime/rr/events/mod.rs @@ -1,3 +1,4 @@ +use super::ReplayError; use crate::ValRaw; use crate::prelude::*; use core::fmt; @@ -6,30 +7,29 @@ use serde::{Deserialize, Serialize}; const VAL_RAW_SIZE: usize = mem::size_of::(); -#[derive(Debug)] -pub enum ReplayError { - EmptyBuffer, - FailedFuncValidation, - IncorrectEventVariant, +/// A serde compatible representation of errors produced by specific events +/// +/// We need this since the [anyhow::Error] trait object cannot be used. This +/// type just encapsulates the corresponding display messages during recording +/// so that it can be validated and/or re-thrown during replay +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum EventError { + ReallocError(String), + LowerError(String), + LowerStoreError(String), } -impl fmt::Display for ReplayError { +impl fmt::Display for EventError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::EmptyBuffer => { - write!(f, "replay buffer is empty!") - } - Self::FailedFuncValidation => { - write!(f, "func replay event validation failed") - } - Self::IncorrectEventVariant => { - write!(f, "event methods invoked on incorrect variant") + Self::ReallocError(s) | Self::LowerError(s) | Self::LowerStoreError(s) => { + write!(f, "{}", s) } } } } -impl std::error::Error for ReplayError {} +impl std::error::Error for EventError {} /// Transmutable byte array used to serialize [`ValRaw`] union /// @@ -119,8 +119,5 @@ where } } -mod core_wasm; -pub use core_wasm::*; - -mod component_wasm; -pub use component_wasm::*; +pub mod component_wasm; +pub mod core_wasm; diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index f0f4bdcbd181..02fa2a42725a 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -6,15 +6,17 @@ use crate::config::{RecordConfig, RecordMetadata, ReplayConfig, ReplayMetadata}; use crate::prelude::*; #[allow(unused_imports)] use crate::runtime::Store; +use core::fmt; use postcard; use serde::{Deserialize, Serialize}; use std::collections::VecDeque; use std::fs::File; +#[allow(unused_imports)] use std::io::{BufWriter, Seek, Write}; /// Encapsulation of event types comprising an [`RREvent`] sum type -mod events; -pub use events::*; +pub mod events; +use events::*; /// Macro template for [`RREvent`] and its conversion to/from specific /// event types @@ -60,19 +62,60 @@ macro_rules! rr_event { // Set of supported record/replay events rr_event! { /// Call into host function from Core Wasm - CoreHostFuncEntry => CoreHostFuncEntryEvent, + CoreHostFuncEntry => core_wasm::HostFuncEntryEvent, /// Return from host function to Core Wasm - CoreHostFuncReturn => CoreHostFuncReturnEvent, - /// Call into host function from component - ComponentHostFuncEntry => ComponentHostFuncEntryEvent, + CoreHostFuncReturn => core_wasm::HostFuncReturnEvent, + + // REQUIRED events for replay + // /// Return from host function to component - ComponentHostFuncReturn => ComponentHostFuncReturnEvent - ///// Component ABI Realloc of linear wasm memory - //ComponentRealloc => ComponentReallocEvent, - ///// A store into linear wasm memory during component type lowering operations - //ComponentLowerStore => ComponentLowerStore + ComponentHostFuncReturn => component_wasm::HostFuncReturnEvent, + /// Component ABI realloc call in linear wasm memory + ComponentReallocEntry => component_wasm::ReallocEntryEvent, + /// Return from a type lowering operation + ComponentLowerReturn => component_wasm::LowerReturnEvent, + /// Return from a store during a type lowering operation + ComponentLowerStoreReturn => component_wasm::LowerStoreReturnEvent, + /// An attempt to obtain a mutable slice into Wasm linear memory + ComponentMemorySliceBorrow => component_wasm::MemorySliceBorrowEvent, + + // OPTIONAL events for replay validation + // + /// Call into host function from component + ComponentHostFuncEntry => component_wasm::HostFuncEntryEvent, + /// Call into [Lower::lower] for type lowering + ComponentLowerEntry => component_wasm::LowerEntryEvent, + /// Call into [Lower::store] during type lowering + ComponentLowerStoreEntry => component_wasm::LowerStoreEntryEvent, + /// Return from Component ABI realloc call + ComponentReallocReturn => component_wasm::ReallocReturnEvent } +#[derive(Debug)] +pub enum ReplayError { + EmptyBuffer, + FailedFuncValidation, + IncorrectEventVariant, +} + +impl fmt::Display for ReplayError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::EmptyBuffer => { + write!(f, "replay buffer is empty!") + } + Self::FailedFuncValidation => { + write!(f, "func replay event validation failed") + } + Self::IncorrectEventVariant => { + write!(f, "event method invoked on incorrect variant") + } + } + } +} + +impl std::error::Error for ReplayError {} + pub trait Recorder { /// Constructs a writer on new buffer fn new_recorder(cfg: RecordConfig) -> Result @@ -231,7 +274,7 @@ mod tests { .map(|x| MaybeUninit::new(x)) .collect::>(); - let event = CoreHostFuncEntryEvent::new(values.as_slice(), None); + let event = core_wasm::CoreHostFuncEntryEvent::new(values.as_slice(), None); // Record values let mut recorder = RecordBuffer::new_recorder(record_cfg)?; @@ -249,7 +292,7 @@ mod tests { metadata: ReplayMetadata { validate: true }, }; let mut replayer = ReplayBuffer::new_replayer(replay_cfg)?; - let event_pop = CoreHostFuncEntryEvent::try_from(replayer.pop_event()?)?; + let event_pop = core_wasm::CoreHostFuncEntryEvent::try_from(replayer.pop_event()?)?; // Replay matches record assert!(event == event_pop); diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index 2677c015fee5..bbbf4b0c8811 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -1364,6 +1364,7 @@ impl StoreOpaque { } /// Record the event generated by `F` if `P` holds true + #[inline(always)] pub(crate) fn record_event(&mut self, push_predicate: P, f: F) where T: Into + fmt::Debug, @@ -1381,6 +1382,7 @@ impl StoreOpaque { } /// Get the next replay event if `P` holds true and process it with `F` + #[inline(always)] pub(crate) fn replay_event(&mut self, pop_predicate: P, f: F) -> Result<()> where T: TryFrom + fmt::Debug, From 82267748642a00ec0167273c8b99aa9158c3b78f Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Wed, 9 Jul 2025 15:54:35 -0700 Subject: [PATCH 23/62] Tighten mutability references acquision points for lowering contexts --- .../src/runtime/component/func/host.rs | 4 +-- .../src/runtime/component/func/options.rs | 34 +++++++++++++++---- .../src/runtime/component/func/typed.rs | 13 +++---- 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index d232ab0917c2..58fc5f510d9a 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -336,7 +336,7 @@ where res }; let indirect_results_lower = |cx: &mut LowerContext<'_, T>, dst: &ValRaw| { - let ptr = validate_inbounds::(cx.as_slice_mut(), dst)?; + let ptr = validate_inbounds::(cx.as_slice(), dst)?; let res = if let Some(retval) = &ret { let r = retval.store(cx, ty, ptr); cx.store @@ -559,7 +559,7 @@ where } else { if let Some((result_vals, ret_index)) = results { let ret_ptr = storage[ret_index].assume_init_ref(); - let mut ptr = validate_inbounds_dynamic(&result_tys.abi, cx.as_slice_mut(), ret_ptr)?; + let mut ptr = validate_inbounds_dynamic(&result_tys.abi, cx.as_slice(), ret_ptr)?; for (val, ty) in result_vals.iter().zip(result_tys.types.iter()) { let offset = types.canonical_abi(ty).next_field32_size(&mut ptr); val.store(&mut cx, *ty, offset)?; diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index aaa29d30b6b9..464d7864b2ef 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -232,10 +232,20 @@ impl<'a, T: 'static> LowerContext<'a, T> { /// /// This will panic if memory has not been configured for this lowering /// (e.g. it wasn't present during the specification of canonical options). - pub fn as_slice_mut(&mut self) -> &mut [u8] { + fn as_slice_mut(&mut self) -> &mut [u8] { self.options.memory_mut(self.store.0) } + /// Returns a view into memory as an immutable slice of bytes. + /// + /// # Panics + /// + /// This will panic if memory has not been configured for this lowering + /// (e.g. it wasn't present during the specification of canonical options). + pub fn as_slice(&mut self) -> &[u8] { + self.options.memory(self.store.0) + } + /// Invokes the memory allocation function (which is style after `realloc`) /// with the specified parameters. /// @@ -284,6 +294,12 @@ impl<'a, T: 'static> LowerContext<'a, T> { /// This will panic if memory has not been configured for this lowering /// (e.g. it wasn't present during the specification of canonical options). pub fn get(&mut self, offset: usize) -> &mut [u8; N] { + // Accessing the store for event recording after getting the slice is + // tricky to resolve by the borrow checker. Instead, we just record before + // since this operation panics anyway, and the replay should still be faithful + self.store + .0 + .record_event(|_| true, |_| MemorySliceBorrowEvent::new(offset, N)); // FIXME: this bounds check shouldn't actually be necessary, all // callers of `ComponentType::store` have already performed a bounds // check so we're guaranteed that `offset..offset+N` is in-bounds. That @@ -294,14 +310,20 @@ impl<'a, T: 'static> LowerContext<'a, T> { // For now I figure we can leave in this bounds check and if it becomes // an issue we can optimize further later, probably with judicious use // of `unsafe`. + self.as_slice_mut()[offset..].first_chunk_mut().unwrap() + } - // Accessing the store for event recording after getting the slice is - // tricky to resolve by the borrow checker. Instead, we just record before - // since this operation panics anyway, and the replay should still be faithful + /// The non-const version of [`get`](Self::get). If size of slice required is + /// statically known, prefer the const version for optimal efficiency + /// + /// # Panics + /// + /// Refer to [`get`](Self::get). + pub fn get_dyn(&mut self, offset: usize, size: usize) -> &mut [u8] { self.store .0 - .record_event(|_| true, |_| MemorySliceBorrowEvent::new(offset, N)); - self.as_slice_mut()[offset..].first_chunk_mut().unwrap() + .record_event(|_| true, |_| MemorySliceBorrowEvent::new(offset, size)); + &mut self.as_slice_mut()[offset..][..size] } /// Lowers an `own` resource into the guest, converting the `rep` specified diff --git a/crates/wasmtime/src/runtime/component/func/typed.rs b/crates/wasmtime/src/runtime/component/func/typed.rs index 186798defb11..33f691037b3a 100644 --- a/crates/wasmtime/src/runtime/component/func/typed.rs +++ b/crates/wasmtime/src/runtime/component/func/typed.rs @@ -847,7 +847,7 @@ macro_rules! integers { // `align_to_mut` which is not safe in general but is safe in // our specific case as all `u8` patterns are valid `Self` // patterns since `Self` is an integral type. - let dst = &mut cx.as_slice_mut()[offset..][..items.len() * Self::SIZE32]; + let dst = cx.get_dyn(offset, items.len() * Self::SIZE32); let (before, middle, end) = unsafe { dst.align_to_mut::() }; assert!(before.is_empty() && end.is_empty()); assert_eq!(middle.len(), items.len()); @@ -960,7 +960,7 @@ macro_rules! floats { // This should all have already been verified in terms of // alignment and sizing meaning that these assertions here are // not truly necessary but are instead double-checks. - let dst = &mut cx.as_slice_mut()[offset..][..items.len() * Self::SIZE32]; + let dst = cx.get_dyn(offset, items.len() * Self::SIZE32); assert!(dst.as_ptr().cast::().is_aligned()); // And with all that out of the way perform the copying loop. @@ -1214,7 +1214,8 @@ fn lower_string(cx: &mut LowerContext<'_, T>, string: &str) -> Result<(usize, ); } let ptr = cx.realloc(0, 0, 1, string.len())?; - cx.as_slice_mut()[ptr..][..string.len()].copy_from_slice(string.as_bytes()); + cx.get_dyn(ptr, string.len()) + .copy_from_slice(string.as_bytes()); Ok((ptr, string.len())) } @@ -1231,7 +1232,7 @@ fn lower_string(cx: &mut LowerContext<'_, T>, string: &str) -> Result<(usize, } let mut ptr = cx.realloc(0, 0, 2, size)?; let mut copied = 0; - let bytes = &mut cx.as_slice_mut()[ptr..][..size]; + let bytes = cx.get_dyn(ptr, size); for (u, bytes) in string.encode_utf16().zip(bytes.chunks_mut(2)) { let u_bytes = u.to_le_bytes(); bytes[0] = u_bytes[0]; @@ -1249,7 +1250,7 @@ fn lower_string(cx: &mut LowerContext<'_, T>, string: &str) -> Result<(usize, let bytes = string.as_bytes(); let mut iter = string.char_indices(); let mut ptr = cx.realloc(0, 0, 2, bytes.len())?; - let mut dst = &mut cx.as_slice_mut()[ptr..][..bytes.len()]; + let mut dst = cx.get_dyn(ptr, bytes.len()); let mut result = 0; while let Some((i, ch)) = iter.next() { // Test if this `char` fits into the latin1 encoding. @@ -1269,7 +1270,7 @@ fn lower_string(cx: &mut LowerContext<'_, T>, string: &str) -> Result<(usize, bail!("byte length too large"); } ptr = cx.realloc(ptr, bytes.len(), 2, worst_case)?; - dst = &mut cx.as_slice_mut()[ptr..][..worst_case]; + dst = cx.get_dyn(ptr, worst_case); // Previously encoded latin1 bytes are inflated to their 16-bit // size for utf16 From 478d4ce42043350f3343a44121ea32fa391462de Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Thu, 10 Jul 2025 18:07:20 -0700 Subject: [PATCH 24/62] Support event action and iterator on replayer --- .../src/runtime/rr/events/component_wasm.rs | 17 +++++--- crates/wasmtime/src/runtime/rr/events/mod.rs | 9 +++-- crates/wasmtime/src/runtime/rr/mod.rs | 40 +++++++++++-------- 3 files changed, 40 insertions(+), 26 deletions(-) diff --git a/crates/wasmtime/src/runtime/rr/events/component_wasm.rs b/crates/wasmtime/src/runtime/rr/events/component_wasm.rs index 75acd4a72f4e..f63ec925b4b6 100644 --- a/crates/wasmtime/src/runtime/rr/events/component_wasm.rs +++ b/crates/wasmtime/src/runtime/rr/events/component_wasm.rs @@ -2,6 +2,7 @@ use super::*; #[allow(unused_imports)] use crate::component::ComponentType; +use std::vec::Vec; use wasmtime_environ::component::{InterfaceType, TypeTuple}; /// A call event from a Wasm component into the host @@ -84,14 +85,18 @@ macro_rules! generic_new_result_events { $(#[doc= $doc])* #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct $event { - ret: Result<$ok_ty, EventError>, + ret: Result<$ok_ty, EventActionError>, } + impl $event { pub fn new(ret: &Result<$ok_ty>) -> Self { Self { ret: ret.as_ref().map(|t| *t).map_err(|e| $err_variant(e.to_string())) } } + #[inline] + #[allow(dead_code)] + pub fn ret(self) -> Result<$ok_ty, EventActionError> { self.ret } } )* ); @@ -112,7 +117,7 @@ macro_rules! generic_new_events { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct $struct { $( - $field: $field_ty, + pub $field: $field_ty, )* } )* @@ -131,11 +136,11 @@ macro_rules! generic_new_events { generic_new_result_events! { /// Return from a reallocation call (needed only for validation) - ReallocReturnEvent => (usize, EventError::ReallocError), + ReallocReturnEvent => (usize, EventActionError::ReallocError), /// Return from a type lowering invocation - LowerReturnEvent => ((), EventError::LowerError), + LowerReturnEvent => ((), EventActionError::LowerError), /// Return from store invocations during type lowering - LowerStoreReturnEvent => ((), EventError::LowerStoreError) + LowerStoreReturnEvent => ((), EventActionError::LowerStoreError) } generic_new_events! { @@ -166,6 +171,6 @@ generic_new_events! { /// larger granularity operations in the future at either the recording or the replay level. MemorySliceBorrowEvent { offset: usize, - size: usize + bytes: Vec } } diff --git a/crates/wasmtime/src/runtime/rr/events/mod.rs b/crates/wasmtime/src/runtime/rr/events/mod.rs index 3554dfc1982f..d0a57b4fb635 100644 --- a/crates/wasmtime/src/runtime/rr/events/mod.rs +++ b/crates/wasmtime/src/runtime/rr/events/mod.rs @@ -7,19 +7,20 @@ use serde::{Deserialize, Serialize}; const VAL_RAW_SIZE: usize = mem::size_of::(); -/// A serde compatible representation of errors produced by specific events +/// A serde compatible representation of errors produced by actions during +/// initial recording for specific events /// /// We need this since the [anyhow::Error] trait object cannot be used. This /// type just encapsulates the corresponding display messages during recording /// so that it can be validated and/or re-thrown during replay #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum EventError { +pub enum EventActionError { ReallocError(String), LowerError(String), LowerStoreError(String), } -impl fmt::Display for EventError { +impl fmt::Display for EventActionError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::ReallocError(s) | Self::LowerError(s) | Self::LowerStoreError(s) => { @@ -29,7 +30,7 @@ impl fmt::Display for EventError { } } -impl std::error::Error for EventError {} +impl std::error::Error for EventActionError {} /// Transmutable byte array used to serialize [`ValRaw`] union /// diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index 02fa2a42725a..046b086d67d8 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -96,6 +96,7 @@ pub enum ReplayError { EmptyBuffer, FailedFuncValidation, IncorrectEventVariant, + EventActionError(EventActionError), } impl fmt::Display for ReplayError { @@ -110,12 +111,21 @@ impl fmt::Display for ReplayError { Self::IncorrectEventVariant => { write!(f, "event method invoked on incorrect variant") } + Self::EventActionError(e) => { + write!(f, "{:?}", e) + } } } } impl std::error::Error for ReplayError {} +impl From for ReplayError { + fn from(value: EventActionError) -> Self { + Self::EventActionError(value) + } +} + pub trait Recorder { /// Constructs a writer on new buffer fn new_recorder(cfg: RecordConfig) -> Result @@ -134,7 +144,7 @@ pub trait Recorder { fn metadata(&self) -> &RecordMetadata; } -pub trait Replayer { +pub trait Replayer: Iterator { type ReplayError; /// Constructs a reader on buffer @@ -142,10 +152,6 @@ pub trait Replayer { where Self: Sized; - /// Pop the next [`RREvent`] from the buffer - /// Events should be FIFO - fn pop_event(&mut self) -> Result; - /// Get metadata associated with the replay process fn metadata(&self) -> &ReplayMetadata; } @@ -216,6 +222,14 @@ pub struct ReplayBuffer { metadata: ReplayMetadata, } +impl Iterator for ReplayBuffer { + type Item = RREvent; + + fn next(&mut self) -> Option { + self.data.buf.pop_front() + } +} + impl Replayer for ReplayBuffer { type ReplayError = ReplayError; @@ -236,13 +250,6 @@ impl Replayer for ReplayBuffer { }) } - fn pop_event(&mut self) -> Result { - self.data - .buf - .pop_front() - .ok_or(Self::ReplayError::EmptyBuffer.into()) - } - #[inline] fn metadata(&self) -> &ReplayMetadata { &self.metadata @@ -274,7 +281,7 @@ mod tests { .map(|x| MaybeUninit::new(x)) .collect::>(); - let event = core_wasm::CoreHostFuncEntryEvent::new(values.as_slice(), None); + let event = core_wasm::HostFuncEntryEvent::new(values.as_slice(), None); // Record values let mut recorder = RecordBuffer::new_recorder(record_cfg)?; @@ -292,13 +299,14 @@ mod tests { metadata: ReplayMetadata { validate: true }, }; let mut replayer = ReplayBuffer::new_replayer(replay_cfg)?; - let event_pop = core_wasm::CoreHostFuncEntryEvent::try_from(replayer.pop_event()?)?; + let event_pop = core_wasm::HostFuncEntryEvent::try_from( + replayer.next().ok_or(ReplayError::EmptyBuffer)?, + )?; // Replay matches record assert!(event == event_pop); // Queue is empty - let event = replayer.pop_event(); - assert!(event.is_err() && matches!(event.unwrap_err(), ReplayError::EmptyBuffer)); + assert!(replayer.next().is_none()); Ok(()) } From 8a1347fc4ac8d3d9ccc2cba756a4e79933262ccb Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Thu, 10 Jul 2025 18:09:09 -0700 Subject: [PATCH 25/62] fixup! Support event action and iterator on replayer --- crates/wasmtime/src/runtime/store.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index bbbf4b0c8811..e93e603abee7 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -1392,7 +1392,7 @@ impl StoreOpaque { { if let Some(buf) = self.replay_buffer_mut() { if pop_predicate(buf.metadata()) { - let call_event = T::try_from(buf.pop_event()?)?; + let call_event = T::try_from(buf.next().ok_or(ReplayError::EmptyBuffer)?)?; println!("Replay | {:?}", &call_event); Ok(f(call_event, buf.metadata())?) } else { @@ -2111,8 +2111,7 @@ at https://bytecodealliance.org/security. /// Panics if the replay buffer in the store is non-empty pub(crate) fn ensure_empty_replay_buffer(&mut self) { if let Some(buf) = self.replay_buffer_mut() { - let event = buf.pop_event(); - assert!(event.is_err_and(|e| matches!(e, ReplayError::EmptyBuffer))); + assert!(buf.next().is_none()); } } } From 7169109297cf37f200eca007953ef15a7058d800 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Fri, 11 Jul 2025 14:54:50 -0700 Subject: [PATCH 26/62] Support recording of memory slice bytes with `MemorySliceCell` --- .../src/runtime/component/func/host.rs | 22 +- .../src/runtime/component/func/options.rs | 202 +++++++++++++++--- .../src/runtime/component/func/typed.rs | 12 +- crates/wasmtime/src/runtime/func.rs | 4 +- .../src/runtime/rr/events/component_wasm.rs | 8 +- crates/wasmtime/src/runtime/rr/mod.rs | 2 +- crates/wasmtime/src/runtime/store.rs | 8 +- 7 files changed, 197 insertions(+), 61 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 58fc5f510d9a..23102c700ade 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -3,6 +3,7 @@ use crate::component::matching::InstanceType; use crate::component::storage::{slice_to_storage_mut, storage_as_slice, storage_as_slice_mut}; use crate::component::{ComponentNamedList, ComponentType, Lift, Lower, Val}; use crate::prelude::*; +use crate::runtime::rr::RREvent; use crate::runtime::rr::events::component_wasm::{ HostFuncEntryEvent, HostFuncReturnEvent, LowerReturnEvent, LowerStoreReturnEvent, }; @@ -217,7 +218,7 @@ where ) }, ); - cx.0.replay_event( + cx.0.replay_event_typed( |r| r.validate, |event: HostFuncEntryEvent, _| event.validate(&types[ty.params]), )?; @@ -314,7 +315,7 @@ where r } else { // `None` return value implies replay stubbing is required - cx.store.0.replay_event( + cx.store.0.replay_event_typed( |_| true, |event: HostFuncReturnEvent, rmeta| { event.move_into_slice( @@ -347,7 +348,7 @@ where // `dst` is a Wasm pointer to indirect results. This pointer itself will remain // deterministic, and thus replay will not need to change this. However, // replay will have to overwrite any nested stored lowerings (deep copy) - cx.store.0.replay_event( + cx.store.0.replay_event_typed( |_| true, |event: HostFuncReturnEvent, rmeta| { if rmeta.validate { @@ -475,7 +476,7 @@ where ) }, ); - store.0.replay_event( + store.0.replay_event_typed( |r| r.validate, |event: HostFuncEntryEvent, _| event.validate(&types[func_ty.params]), )?; @@ -537,7 +538,7 @@ where assert!(dst.next().is_none()); } else { // Replay stubbing required - cx.store.0.replay_event( + cx.store.0.replay_event_typed( |_| true, |event: HostFuncReturnEvent, rmeta| { event.move_into_slice( @@ -568,16 +569,7 @@ where // The indirect `ret_ptr` itself will remain deterministic, and thus replay will not // need to change this. However, replay will have to overwrite any nested stored // lowerings (deep copy) - cx.store.0.replay_event( - |_| true, - |event: HostFuncReturnEvent, rmeta| { - if rmeta.validate { - event.validate(result_tys) - } else { - Ok(()) - } - }, - )?; + cx.replay_lowering()?; } // Recording here is just for marking the return event cx.store.0.record_event( diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index 464d7864b2ef..c05be07212c8 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -2,9 +2,10 @@ use crate::component::ResourceType; use crate::component::matching::InstanceType; use crate::component::resources::{HostResourceData, HostResourceIndex, HostResourceTables}; use crate::prelude::*; -use crate::rr::events::component_wasm::{ - MemorySliceBorrowEvent, ReallocEntryEvent, ReallocReturnEvent, +use crate::runtime::rr::events::component_wasm::{ + MemorySliceWriteEvent, ReallocEntryEvent, ReallocReturnEvent, }; +use crate::runtime::rr::{RREvent, RecordBuffer, Recorder, ReplayError}; use crate::runtime::vm::component::{ CallContexts, ComponentInstance, InstanceFlags, ResourceTable, ResourceTables, }; @@ -12,9 +13,99 @@ use crate::runtime::vm::{VMFuncRef, VMMemoryDefinition}; use crate::store::{StoreId, StoreOpaque}; use crate::{FuncType, StoreContextMut}; use alloc::sync::Arc; +use core::ops::{Deref, DerefMut}; use core::ptr::NonNull; use wasmtime_environ::component::{ComponentTypes, StringEncoding, TypeResourceTableIndex}; +/// Same as [`ConstMemorySliceCell`] except allows for dynamically sized slices. +/// +/// Prefer the above for efficiency if slice size is known statically. +/// +/// **Note**: The correct operation of this type relies of several invariants. +/// See [`ConstMemorySliceCell`] for detailed description on the role +/// of these types. +pub struct MemorySliceCell<'a> { + offset: usize, + bytes: &'a mut [u8], + recorder: Option<&'a mut RecordBuffer>, +} +impl<'a> Deref for MemorySliceCell<'a> { + type Target = [u8]; + fn deref(&self) -> &Self::Target { + self.bytes + } +} +impl DerefMut for MemorySliceCell<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.bytes + } +} +impl Drop for MemorySliceCell<'_> { + /// Drop serves as a recording hook for stores to the memory slice + fn drop(&mut self) { + if let Some(buf) = &mut self.recorder { + buf.push_event(RREvent::ComponentMemorySliceWrite( + MemorySliceWriteEvent::new(self.offset, self.bytes.to_vec()), + )); + } + } +} + +/// Zero-cost encapsulation type for a statically sized slice of mutable memory +/// +/// # Purpose and Usage (Read Carefully!) +/// +/// This type (and its dynamic counterpart [`MemorySliceCell`]) are critical to +/// record/replay (RR) support in Wasmtime. In practice, all lowering operations utilize +/// a [`LowerContext`], which provides a capability to modify guest Wasm module state in +/// the following ways: +/// +/// 1. Write to slices of memory with [`get`](LowerContext::get)/[`get_dyn`](LowerContext::get_dyn) +/// 2. Movement of memory with [`realloc`](LowerContext::realloc) +/// +/// The above are intended to be the narrow waists for recording changes to guest state, and +/// should be the **only** interfaces used during lowerng. In particular, +/// [`get`](LowerContext::get)/[`get_dyn`](LowerContext::get_dyn) return +/// ([`ConstMemorySliceCell`]/[`MemorySliceCell`]), which implement [`Drop`] +/// allowing us a hook to just capture the final aggregate changes made to guest memory by the host. +/// +/// ## Critical Invariants +/// +/// Typically recording would need to know both when the slice was borrowed AND when it was +/// dropped, since memory movement with [`realloc`](LowerContext::realloc) can be interleave between +/// borrows and drops, and replays would have to be aware of this. **However**, with this abstraction, +/// we can be more efficient and get away with **only** recording drops, because of the implicit interaction between +/// [`realloc`](LowerContext::realloc) and [`get`](LowerContext::get)/[`get_dyn`](LowerContext::get_dyn), +/// which both take a `&mut self`. Since the latter implements [`Drop`], which also takes a `&mut self`, +/// the compiler will automatically enforce that drops of this type need to be triggered before a +/// [`realloc`](LowerContext::realloc), preventing any interleavings in between the borrow and drop of the slice. +pub struct ConstMemorySliceCell<'a, const N: usize> { + offset: usize, + bytes: &'a mut [u8; N], + recorder: Option<&'a mut RecordBuffer>, +} +impl<'a, const N: usize> Deref for ConstMemorySliceCell<'a, N> { + type Target = [u8; N]; + fn deref(&self) -> &Self::Target { + self.bytes + } +} +impl<'a, const N: usize> DerefMut for ConstMemorySliceCell<'a, N> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.bytes + } +} +impl<'a, const N: usize> Drop for ConstMemorySliceCell<'a, N> { + /// Drops serves as a recording hook for stores to the memory slice + fn drop(&mut self) { + if let Some(buf) = &mut self.recorder { + buf.push_event(RREvent::ComponentMemorySliceWrite( + MemorySliceWriteEvent::new(self.offset, self.bytes.to_vec()), + )); + } + } +} + /// Runtime representation of canonical ABI options in the component model. /// /// This structure packages up the runtime representation of each option from @@ -157,6 +248,21 @@ impl Options { } } + /// Same as above, but also obtain the record buffer from the encapsulating store + fn memory_mut_with_recorder<'a>( + &self, + store: &'a mut StoreOpaque, + ) -> (&'a mut [u8], Option<&'a mut RecordBuffer>) { + self.store_id.assert_belongs_to(store.id()); + + // See comments in `memory` about the unsafety + let memslice = unsafe { + let memory = self.memory.unwrap().as_ref(); + core::slice::from_raw_parts_mut(memory.base.as_ptr(), memory.current_length()) + }; + (memslice, store.record_buffer_mut()) + } + /// Returns the underlying encoding used for strings in this /// lifting/lowering. pub fn string_encoding(&self) -> StringEncoding { @@ -226,14 +332,15 @@ impl<'a, T: 'static> LowerContext<'a, T> { } } - /// Returns a view into memory as a mutable slice of bytes. + /// Returns a view into memory as a mutable slice of bytes along with the + /// record buffer to record state. /// /// # Panics /// /// This will panic if memory has not been configured for this lowering /// (e.g. it wasn't present during the specification of canonical options). - fn as_slice_mut(&mut self) -> &mut [u8] { - self.options.memory_mut(self.store.0) + fn as_slice_mut_with_recorder(&mut self) -> (&mut [u8], Option<&mut RecordBuffer>) { + self.options.memory_mut_with_recorder(self.store.0) } /// Returns a view into memory as an immutable slice of bytes. @@ -246,6 +353,26 @@ impl<'a, T: 'static> LowerContext<'a, T> { self.options.memory(self.store.0) } + fn realloc_inner( + &mut self, + old: usize, + old_size: usize, + old_align: u32, + new_size: usize, + ) -> Result { + let realloc_func_ty = Arc::clone(unsafe { (*self.instance).component().realloc_func_ty() }); + self.options + .realloc( + &mut self.store, + &realloc_func_ty, + old, + old_size, + old_align, + new_size, + ) + .map(|(_, ptr)| ptr) + } + /// Invokes the memory allocation function (which is style after `realloc`) /// with the specified parameters. /// @@ -260,23 +387,11 @@ impl<'a, T: 'static> LowerContext<'a, T> { old_align: u32, new_size: usize, ) -> Result { - let realloc_func_ty = Arc::clone(unsafe { (*self.instance).component().realloc_func_ty() }); - self.store.0.record_event( |_| true, |_| ReallocEntryEvent::new(old, old_size, old_align, new_size), ); - let result = self - .options - .realloc( - &mut self.store, - &realloc_func_ty, - old, - old_size, - old_align, - new_size, - ) - .map(|(_, ptr)| ptr); + let result = self.realloc_inner(old, old_size, old_align, new_size); self.store .0 .record_event(|r| r.add_validation, |_| ReallocReturnEvent::new(&result)); @@ -293,13 +408,9 @@ impl<'a, T: 'static> LowerContext<'a, T> { /// /// This will panic if memory has not been configured for this lowering /// (e.g. it wasn't present during the specification of canonical options). - pub fn get(&mut self, offset: usize) -> &mut [u8; N] { - // Accessing the store for event recording after getting the slice is - // tricky to resolve by the borrow checker. Instead, we just record before - // since this operation panics anyway, and the replay should still be faithful - self.store - .0 - .record_event(|_| true, |_| MemorySliceBorrowEvent::new(offset, N)); + #[inline] + pub fn get(&mut self, offset: usize) -> ConstMemorySliceCell { + let (slice_mut, recorder) = self.as_slice_mut_with_recorder(); // FIXME: this bounds check shouldn't actually be necessary, all // callers of `ComponentType::store` have already performed a bounds // check so we're guaranteed that `offset..offset+N` is in-bounds. That @@ -310,7 +421,11 @@ impl<'a, T: 'static> LowerContext<'a, T> { // For now I figure we can leave in this bounds check and if it becomes // an issue we can optimize further later, probably with judicious use // of `unsafe`. - self.as_slice_mut()[offset..].first_chunk_mut().unwrap() + ConstMemorySliceCell { + offset: offset, + bytes: slice_mut[offset..].first_chunk_mut().unwrap(), + recorder: recorder, + } } /// The non-const version of [`get`](Self::get). If size of slice required is @@ -319,11 +434,13 @@ impl<'a, T: 'static> LowerContext<'a, T> { /// # Panics /// /// Refer to [`get`](Self::get). - pub fn get_dyn(&mut self, offset: usize, size: usize) -> &mut [u8] { - self.store - .0 - .record_event(|_| true, |_| MemorySliceBorrowEvent::new(offset, size)); - &mut self.as_slice_mut()[offset..][..size] + pub fn get_dyn(&mut self, offset: usize, size: usize) -> MemorySliceCell { + let (slice_mut, recorder) = self.as_slice_mut_with_recorder(); + MemorySliceCell { + offset: offset, + bytes: &mut slice_mut[offset..][..size], + recorder: recorder, + } } /// Lowers an `own` resource into the guest, converting the `rep` specified @@ -416,6 +533,29 @@ impl<'a, T: 'static> LowerContext<'a, T> { ) } + /// Perform a replay of all the type lowering-associated events for this context + /// + /// These typically include all `Lower*` and `Realloc*` event, along with relevant + /// `HostFunctionReturnEvent` if it exists + pub fn replay_lowering(&mut self) -> Result<()> { + ///// Get and execute `F` on the events from the replay buffer iteratively + ///// + ///// Iteration continues as long as `F` returns `Ok(true)` or buffer ends + //pub(crate) fn replay_events_while(&mut self, mut f: F) -> Result<()> + //where + // F: FnMut(RREvent, &ReplayMetadata) -> Result, + //{ + // if let Some(buf) = self.replay_buffer_mut() { + // while f(buf.next().ok_or(ReplayError::EmptyBuffer)?, buf.metadata())? {} + // } else { + // } + // Ok(()) + //} + + todo!(); + Ok(()) + } + /// See [`HostResourceTables::enter_call`]. #[inline] pub fn enter_call(&mut self) { diff --git a/crates/wasmtime/src/runtime/component/func/typed.rs b/crates/wasmtime/src/runtime/component/func/typed.rs index 33f691037b3a..59bf6c469c46 100644 --- a/crates/wasmtime/src/runtime/component/func/typed.rs +++ b/crates/wasmtime/src/runtime/component/func/typed.rs @@ -847,7 +847,7 @@ macro_rules! integers { // `align_to_mut` which is not safe in general but is safe in // our specific case as all `u8` patterns are valid `Self` // patterns since `Self` is an integral type. - let dst = cx.get_dyn(offset, items.len() * Self::SIZE32); + let mut dst = cx.get_dyn(offset, items.len() * Self::SIZE32); let (before, middle, end) = unsafe { dst.align_to_mut::() }; assert!(before.is_empty() && end.is_empty()); assert_eq!(middle.len(), items.len()); @@ -938,7 +938,7 @@ macro_rules! floats { ) -> Result<()> { debug_assert!(matches!(ty, InterfaceType::$ty)); debug_assert!(offset % Self::SIZE32 == 0); - let ptr = cx.get(offset); + let mut ptr = cx.get(offset); *ptr = self.to_bits().to_le_bytes(); Ok(()) } @@ -960,7 +960,7 @@ macro_rules! floats { // This should all have already been verified in terms of // alignment and sizing meaning that these assertions here are // not truly necessary but are instead double-checks. - let dst = cx.get_dyn(offset, items.len() * Self::SIZE32); + let mut dst = cx.get_dyn(offset, items.len() * Self::SIZE32); assert!(dst.as_ptr().cast::().is_aligned()); // And with all that out of the way perform the copying loop. @@ -1232,13 +1232,14 @@ fn lower_string(cx: &mut LowerContext<'_, T>, string: &str) -> Result<(usize, } let mut ptr = cx.realloc(0, 0, 2, size)?; let mut copied = 0; - let bytes = cx.get_dyn(ptr, size); + let mut bytes = cx.get_dyn(ptr, size); for (u, bytes) in string.encode_utf16().zip(bytes.chunks_mut(2)) { let u_bytes = u.to_le_bytes(); bytes[0] = u_bytes[0]; bytes[1] = u_bytes[1]; copied += 1; } + drop(bytes); if (copied * 2) < size { ptr = cx.realloc(ptr, size, 2, copied * 2)?; } @@ -1269,6 +1270,7 @@ fn lower_string(cx: &mut LowerContext<'_, T>, string: &str) -> Result<(usize, if worst_case > MAX_STRING_BYTE_LENGTH { bail!("byte length too large"); } + drop(dst); ptr = cx.realloc(ptr, bytes.len(), 2, worst_case)?; dst = cx.get_dyn(ptr, worst_case); @@ -1289,11 +1291,13 @@ fn lower_string(cx: &mut LowerContext<'_, T>, string: &str) -> Result<(usize, bytes[1] = u_bytes[1]; result += 1; } + drop(dst); if worst_case > 2 * result { ptr = cx.realloc(ptr, worst_case, 2, 2 * result)?; } return Ok((ptr, result | UTF16_TAG)); } + drop(dst); if result < bytes.len() { ptr = cx.realloc(ptr, bytes.len(), 2, result)?; } diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index ccb54d8042d0..2efd7891cefa 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -2354,7 +2354,7 @@ impl HostContext { // (only when validation is enabled). // Function type unwraps should never panic since they are // lazily evaluated - store.replay_event( + store.replay_event_typed( |r| r.validate, |event: HostFuncEntryEvent, _| event.validate(wasm_func_type.unwrap()), )?; @@ -2403,7 +2403,7 @@ impl HostContext { // Record/replay interceptions of raw return args let ret = if store.replay_enabled() { - store.replay_event( + store.replay_event_typed( |_| true, |event: HostFuncReturnEvent, rmeta| { event.move_into_slice( diff --git a/crates/wasmtime/src/runtime/rr/events/component_wasm.rs b/crates/wasmtime/src/runtime/rr/events/component_wasm.rs index f63ec925b4b6..69be9a41c6b3 100644 --- a/crates/wasmtime/src/runtime/rr/events/component_wasm.rs +++ b/crates/wasmtime/src/runtime/rr/events/component_wasm.rs @@ -163,13 +163,13 @@ generic_new_events! { offset: usize }, - /// A mutable borrow of a slice of Wasm linear memory by the host - /// - /// This is the fundamental interface used during lowering of a [`ComponentType`]. + /// A write to a mutable slice of Wasm linear memory by the host. This is the + /// fundamental representation of host-written data to Wasm and is usually + /// performed during lowering of a [`ComponentType`]. /// Note that this currently signifies a single mutable operation at the smallest granularity /// on a given linear memory slice. These can be optimized and coalesced into /// larger granularity operations in the future at either the recording or the replay level. - MemorySliceBorrowEvent { + MemorySliceWriteEvent { offset: usize, bytes: Vec } diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index 046b086d67d8..f70fb8e47f6d 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -77,7 +77,7 @@ rr_event! { /// Return from a store during a type lowering operation ComponentLowerStoreReturn => component_wasm::LowerStoreReturnEvent, /// An attempt to obtain a mutable slice into Wasm linear memory - ComponentMemorySliceBorrow => component_wasm::MemorySliceBorrowEvent, + ComponentMemorySliceWrite => component_wasm::MemorySliceWriteEvent, // OPTIONAL events for replay validation // diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index e93e603abee7..de7f2ffb1649 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -1354,12 +1354,12 @@ impl StoreOpaque { } #[inline] - fn record_buffer_mut(&mut self) -> Option<&mut RecordBuffer> { + pub fn record_buffer_mut(&mut self) -> Option<&mut RecordBuffer> { self.record_buffer.as_mut() } #[inline] - fn replay_buffer_mut(&mut self) -> Option<&mut ReplayBuffer> { + pub fn replay_buffer_mut(&mut self) -> Option<&mut ReplayBuffer> { self.replay_buffer.as_mut() } @@ -1383,7 +1383,7 @@ impl StoreOpaque { /// Get the next replay event if `P` holds true and process it with `F` #[inline(always)] - pub(crate) fn replay_event(&mut self, pop_predicate: P, f: F) -> Result<()> + pub(crate) fn replay_event_typed(&mut self, pop_predicate: P, f: F) -> Result<()> where T: TryFrom + fmt::Debug, >::Error: std::error::Error + Send + Sync + 'static, @@ -1403,7 +1403,7 @@ impl StoreOpaque { } } - /// Check if recording or replaying is tied to the store + /// Check if recording or replaying is enabled for the Store #[inline] pub fn rr_enabled(&self) -> bool { self.record_buffer.is_some() || self.replay_buffer.is_some() From 3a93791b63963405b005c428cfad742e87cd5acd Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Mon, 14 Jul 2025 17:15:07 -0700 Subject: [PATCH 27/62] MVP for RR component model complete Todos: * Fix some interfaces for Recorder/Replayer and Stores * Include feature gating --- .../src/runtime/component/func/host.rs | 117 ++++++-------- .../src/runtime/component/func/options.rs | 115 +++++++++----- crates/wasmtime/src/runtime/func.rs | 32 ++-- crates/wasmtime/src/runtime/rr/mod.rs | 143 +++++++++++++++--- crates/wasmtime/src/runtime/store.rs | 64 +++++--- 5 files changed, 311 insertions(+), 160 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 23102c700ade..760d328b17c6 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -3,7 +3,6 @@ use crate::component::matching::InstanceType; use crate::component::storage::{slice_to_storage_mut, storage_as_slice, storage_as_slice_mut}; use crate::component::{ComponentNamedList, ComponentType, Lift, Lower, Val}; use crate::prelude::*; -use crate::runtime::rr::RREvent; use crate::runtime::rr::events::component_wasm::{ HostFuncEntryEvent, HostFuncReturnEvent, LowerReturnEvent, LowerStoreReturnEvent, }; @@ -207,7 +206,7 @@ where let param_tys = InterfaceType::Tuple(ty.params); let result_tys = InterfaceType::Tuple(ty.results); - cx.0.record_event( + cx.0.record_event_when( |r| r.add_validation, |_| { HostFuncEntryEvent::new( @@ -218,7 +217,7 @@ where ) }, ); - cx.0.replay_event_typed( + cx.0.replay_event_when( |r| r.validate, |event: HostFuncEntryEvent, _| event.validate(&types[ty.params]), )?; @@ -308,62 +307,46 @@ where |cx: &mut LowerContext<'_, T>, dst: &mut MaybeUninit<::Lower>| { let res = if let Some(retval) = &ret { - let r = retval.lower(cx, ty, dst); + // Normal execution path + let lower_result = retval.lower(cx, ty, dst); cx.store .0 - .record_event(|_| true, |_| LowerReturnEvent::new(&r)); - r + .record_event(|_| LowerReturnEvent::new(&lower_result)); + lower_result } else { - // `None` return value implies replay stubbing is required - cx.store.0.replay_event_typed( - |_| true, - |event: HostFuncReturnEvent, rmeta| { - event.move_into_slice( - storage_as_slice_mut(dst), - rmeta.validate.then_some(rr_tys), - ) - }, - ) + // Replay execution path + // This path also stores the final return values in resulting storage + cx.replay_lowering(rr_tys, Some(storage_as_slice_mut(dst))) }; - cx.store.0.record_event( - |_| true, - |rmeta| { - HostFuncReturnEvent::new( - storage_as_slice(dst), - rmeta.add_validation.then_some(rr_tys), - ) - }, - ); + cx.store.0.record_event(|rmeta| { + HostFuncReturnEvent::new( + storage_as_slice(dst), + rmeta.add_validation.then_some(rr_tys), + ) + }); res }; let indirect_results_lower = |cx: &mut LowerContext<'_, T>, dst: &ValRaw| { let ptr = validate_inbounds::(cx.as_slice(), dst)?; let res = if let Some(retval) = &ret { - let r = retval.store(cx, ty, ptr); + // Normal execution path + let store_result = retval.store(cx, ty, ptr); cx.store .0 - .record_event(|_| true, |_| LowerStoreReturnEvent::new(&r)); - r + .record_event(|_| LowerStoreReturnEvent::new(&store_result)); + store_result } else { + // Replay execution path + // // `dst` is a Wasm pointer to indirect results. This pointer itself will remain // deterministic, and thus replay will not need to change this. However, // replay will have to overwrite any nested stored lowerings (deep copy) - cx.store.0.replay_event_typed( - |_| true, - |event: HostFuncReturnEvent, rmeta| { - if rmeta.validate { - event.validate(rr_tys) - } else { - Ok(()) - } - }, - ) + cx.replay_lowering(rr_tys, None) }; // Recording here is just for marking the return event - cx.store.0.record_event( - |_| true, - |rmeta| HostFuncReturnEvent::new(&[], rmeta.add_validation.then_some(rr_tys)), - ); + cx.store.0.record_event(|rmeta| { + HostFuncReturnEvent::new(&[], rmeta.add_validation.then_some(rr_tys)) + }); res }; match self { @@ -465,7 +448,7 @@ where let replay_enabled = store.0.replay_enabled(); - store.0.record_event( + store.0.record_event_when( |r| r.add_validation, |_| { HostFuncEntryEvent::new( @@ -476,7 +459,7 @@ where ) }, ); - store.0.replay_event_typed( + store.0.replay_event_when( |r| r.validate, |event: HostFuncEntryEvent, _| event.validate(&types[func_ty.params]), )?; @@ -531,34 +514,31 @@ where let mut cx = LowerContext::new(store, &options, types, instance); if let Some(cnt) = result_tys.abi.flat_count(MAX_FLAT_RESULTS) { if let Some((result_vals, _)) = results { + // Normal execution path let mut dst = storage[..cnt].iter_mut(); for (val, ty) in result_vals.iter().zip(result_tys.types.iter()) { val.lower(&mut cx, *ty, &mut dst)?; } assert!(dst.next().is_none()); } else { - // Replay stubbing required - cx.store.0.replay_event_typed( - |_| true, - |event: HostFuncReturnEvent, rmeta| { - event.move_into_slice( - mem::transmute::<&mut [MaybeUninit], &mut [ValRaw]>(storage), - rmeta.validate.then_some(result_tys), - ) - }, + // Replay execution path + // This path also stores the final return values in resulting storage + cx.replay_lowering( + result_tys, + Some(mem::transmute::<&mut [MaybeUninit], &mut [ValRaw]>( + storage, + )), )?; } - cx.store.0.record_event( - |_| true, - |rmeta| { - HostFuncReturnEvent::new( - mem::transmute::<&[MaybeUninit], &[ValRaw]>(storage), - rmeta.add_validation.then_some(result_tys), - ) - }, - ); + cx.store.0.record_event(|rmeta| { + HostFuncReturnEvent::new( + mem::transmute::<&[MaybeUninit], &[ValRaw]>(storage), + rmeta.add_validation.then_some(result_tys), + ) + }); } else { if let Some((result_vals, ret_index)) = results { + // Normal execution path let ret_ptr = storage[ret_index].assume_init_ref(); let mut ptr = validate_inbounds_dynamic(&result_tys.abi, cx.as_slice(), ret_ptr)?; for (val, ty) in result_vals.iter().zip(result_tys.types.iter()) { @@ -566,16 +546,17 @@ where val.store(&mut cx, *ty, offset)?; } } else { + // Replay execution path + // // The indirect `ret_ptr` itself will remain deterministic, and thus replay will not - // need to change this. However, replay will have to overwrite any nested stored + // need to change the return storage. However, replay will have to overwrite any nested stored // lowerings (deep copy) - cx.replay_lowering()?; + cx.replay_lowering(result_tys, None)?; } // Recording here is just for marking the return event - cx.store.0.record_event( - |_| true, - |rmeta| HostFuncReturnEvent::new(&[], rmeta.add_validation.then_some(result_tys)), - ); + cx.store.0.record_event(|rmeta| { + HostFuncReturnEvent::new(&[], rmeta.add_validation.then_some(result_tys)) + }); } flags.set_may_leave(true); diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index c05be07212c8..05ab178b3d59 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -5,17 +5,19 @@ use crate::prelude::*; use crate::runtime::rr::events::component_wasm::{ MemorySliceWriteEvent, ReallocEntryEvent, ReallocReturnEvent, }; -use crate::runtime::rr::{RREvent, RecordBuffer, Recorder, ReplayError}; +use crate::runtime::rr::{RREvent, RecordBuffer, Recorder, Replayer}; use crate::runtime::vm::component::{ CallContexts, ComponentInstance, InstanceFlags, ResourceTable, ResourceTables, }; use crate::runtime::vm::{VMFuncRef, VMMemoryDefinition}; use crate::store::{StoreId, StoreOpaque}; -use crate::{FuncType, StoreContextMut}; +use crate::{FuncType, StoreContextMut, ValRaw}; use alloc::sync::Arc; use core::ops::{Deref, DerefMut}; use core::ptr::NonNull; -use wasmtime_environ::component::{ComponentTypes, StringEncoding, TypeResourceTableIndex}; +use wasmtime_environ::component::{ + ComponentTypes, StringEncoding, TypeResourceTableIndex, TypeTuple, +}; /// Same as [`ConstMemorySliceCell`] except allows for dynamically sized slices. /// @@ -44,9 +46,7 @@ impl Drop for MemorySliceCell<'_> { /// Drop serves as a recording hook for stores to the memory slice fn drop(&mut self) { if let Some(buf) = &mut self.recorder { - buf.push_event(RREvent::ComponentMemorySliceWrite( - MemorySliceWriteEvent::new(self.offset, self.bytes.to_vec()), - )); + buf.record_event(|_| MemorySliceWriteEvent::new(self.offset, self.bytes.to_vec())); } } } @@ -99,9 +99,10 @@ impl<'a, const N: usize> Drop for ConstMemorySliceCell<'a, N> { /// Drops serves as a recording hook for stores to the memory slice fn drop(&mut self) { if let Some(buf) = &mut self.recorder { - buf.push_event(RREvent::ComponentMemorySliceWrite( - MemorySliceWriteEvent::new(self.offset, self.bytes.to_vec()), - )); + buf.record_event_when( + |_| true, + |_| MemorySliceWriteEvent::new(self.offset, self.bytes.to_vec()), + ); } } } @@ -237,7 +238,7 @@ impl Options { } } - /// Same as above, just `_mut` + /// Same as [`memory`](Self::memory), just `_mut` pub fn memory_mut<'a>(&self, store: &'a mut StoreOpaque) -> &'a mut [u8] { self.store_id.assert_belongs_to(store.id()); @@ -248,7 +249,7 @@ impl Options { } } - /// Same as above, but also obtain the record buffer from the encapsulating store + /// Same as [`memory_mut`](Self::memory_mut), but with the record buffer from the encapsulating store fn memory_mut_with_recorder<'a>( &self, store: &'a mut StoreOpaque, @@ -332,17 +333,26 @@ impl<'a, T: 'static> LowerContext<'a, T> { } } - /// Returns a view into memory as a mutable slice of bytes along with the + /// Returns a view into memory as a mutable slice of bytes + the /// record buffer to record state. /// /// # Panics /// - /// This will panic if memory has not been configured for this lowering - /// (e.g. it wasn't present during the specification of canonical options). + /// See [`as_slice`](Self::as_slice) fn as_slice_mut_with_recorder(&mut self) -> (&mut [u8], Option<&mut RecordBuffer>) { self.options.memory_mut_with_recorder(self.store.0) } + /// Returns a view into memory as a mutable slice of bytes + /// + /// # Panics + /// + /// See [`as_slice`](Self::as_slice) + #[inline] + fn as_slice_mut(&mut self) -> &mut [u8] { + self.options.memory_mut(self.store.0) + } + /// Returns a view into memory as an immutable slice of bytes. /// /// # Panics @@ -353,6 +363,12 @@ impl<'a, T: 'static> LowerContext<'a, T> { self.options.memory(self.store.0) } + /// Inner invocation of realloc, without record/replay scaffolding + /// + /// # Panics + /// + /// This will panic if realloc hasn't been configured for this lowering via + /// its canonical options. fn realloc_inner( &mut self, old: usize, @@ -387,14 +403,13 @@ impl<'a, T: 'static> LowerContext<'a, T> { old_align: u32, new_size: usize, ) -> Result { - self.store.0.record_event( - |_| true, - |_| ReallocEntryEvent::new(old, old_size, old_align, new_size), - ); + self.store + .0 + .record_event(|_| ReallocEntryEvent::new(old, old_size, old_align, new_size)); let result = self.realloc_inner(old, old_size, old_align, new_size); self.store .0 - .record_event(|r| r.add_validation, |_| ReallocReturnEvent::new(&result)); + .record_event_when(|r| r.add_validation, |_| ReallocReturnEvent::new(&result)); result } @@ -536,23 +551,51 @@ impl<'a, T: 'static> LowerContext<'a, T> { /// Perform a replay of all the type lowering-associated events for this context /// /// These typically include all `Lower*` and `Realloc*` event, along with relevant - /// `HostFunctionReturnEvent` if it exists - pub fn replay_lowering(&mut self) -> Result<()> { - ///// Get and execute `F` on the events from the replay buffer iteratively - ///// - ///// Iteration continues as long as `F` returns `Ok(true)` or buffer ends - //pub(crate) fn replay_events_while(&mut self, mut f: F) -> Result<()> - //where - // F: FnMut(RREvent, &ReplayMetadata) -> Result, - //{ - // if let Some(buf) = self.replay_buffer_mut() { - // while f(buf.next().ok_or(ReplayError::EmptyBuffer)?, buf.metadata())? {} - // } else { - // } - // Ok(()) - //} - - todo!(); + /// `HostFunctionReturnEvent`. + /// + /// ## Important Notes + /// + /// * It is assumed that this is only invoked at the root lower/store calls + pub fn replay_lowering( + &mut self, + result_tys: &TypeTuple, + mut result_storage: Option<&mut [ValRaw]>, + ) -> Result<()> { + if self.store.0.replay_buffer_mut().is_none() { + return Ok(()); + } + let mut complete = false; + while !complete { + let event = self.store.0.replay_buffer_mut().unwrap().next_event()?; + let _ = match event { + RREvent::ComponentHostFuncReturn(e) => { + // End of lowering process + if let Some(storage) = result_storage.as_deref_mut() { + e.move_into_slice(storage, Some(&result_tys))?; + } else { + e.validate(result_tys)?; + } + complete = true; + } + RREvent::ComponentLowerReturn(e) => e.ret()?, + RREvent::ComponentLowerStoreReturn(e) => e.ret()?, + RREvent::ComponentReallocEntry(e) => { + let _ = self.realloc_inner(e.old_addr, e.old_size, e.old_align, e.new_size); + } + RREvent::ComponentMemorySliceWrite(e) => { + // The bounds check is performed here is required here (in the absence of + // trace validation) to protect against malicious out-of-bounds slice writes + self.as_slice_mut()[e.offset..e.offset + e.bytes.len()] + .copy_from_slice(e.bytes.as_slice()); + } + // Realloc or any lowering methods cannot call back to the host. Hence, you cannot + // have host calls entries during this method + RREvent::ComponentHostFuncEntry(_) => { + bail!("Cannot call back into host during lowering") + } + _ => {} + }; + } Ok(()) } diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 2efd7891cefa..9e8dba7a1c85 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -2354,11 +2354,11 @@ impl HostContext { // (only when validation is enabled). // Function type unwraps should never panic since they are // lazily evaluated - store.replay_event_typed( + store.replay_event_when( |r| r.validate, |event: HostFuncEntryEvent, _| event.validate(wasm_func_type.unwrap()), )?; - store.record_event( + store.record_event_when( |r| r.add_validation, |_| { let wasm_func_type = wasm_func_type.unwrap(); @@ -2403,29 +2403,25 @@ impl HostContext { // Record/replay interceptions of raw return args let ret = if store.replay_enabled() { - store.replay_event_typed( - |_| true, - |event: HostFuncReturnEvent, rmeta| { + store + .replay_event(|event: HostFuncReturnEvent, rmeta| { event.move_into_slice( args.as_mut(), rmeta.validate.then_some(wasm_func_type.unwrap()), ) - }, - ) + }) + .map_err(Into::into) } else { returns.unwrap().store(&mut store, args.as_mut()) }?; - store.record_event( - |_| true, - |rmeta| { - let wasm_func_type = wasm_func_type.unwrap(); - let num_results = wasm_func_type.params().len(); - HostFuncReturnEvent::new( - unsafe { &args.as_ref()[..num_results] }, - rmeta.add_validation.then_some(wasm_func_type.clone()), - ) - }, - ); + store.record_event(|rmeta| { + let wasm_func_type = wasm_func_type.unwrap(); + let num_results = wasm_func_type.params().len(); + HostFuncReturnEvent::new( + unsafe { &args.as_ref()[..num_results] }, + rmeta.add_validation.then_some(wasm_func_type.clone()), + ) + }); Ok(ret) }; diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index f70fb8e47f6d..1a5c5aa476a3 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -39,6 +39,17 @@ macro_rules! rr_event { $variant($event), )* } + + impl fmt::Display for RREvent { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + $( + Self::$variant(e) => write!(f, "{:?}", e), + )* + } + } + } + $( impl From<$event> for RREvent { fn from(value: $event) -> Self { @@ -91,11 +102,13 @@ rr_event! { ComponentReallocReturn => component_wasm::ReallocReturnEvent } +/// Error type signalling failures during a replay run #[derive(Debug)] pub enum ReplayError { EmptyBuffer, FailedFuncValidation, IncorrectEventVariant, + EventActionError(EventActionError), } @@ -126,14 +139,18 @@ impl From for ReplayError { } } +/// This trait provides the interface for a FIFO recorder pub trait Recorder { /// Constructs a writer on new buffer fn new_recorder(cfg: RecordConfig) -> Result where Self: Sized; - /// Push a newly record event [`RREvent`] to the buffer - fn push_event(&mut self, event: RREvent) -> (); + /// Record the event generated by `f` + fn record_event(&mut self, f: F) + where + T: Into + fmt::Debug, + F: FnOnce(&RecordMetadata) -> T; /// Flush memory contents to underlying persistent storage /// @@ -142,11 +159,25 @@ pub trait Recorder { /// Get metadata associated with the recording process fn metadata(&self) -> &RecordMetadata; -} -pub trait Replayer: Iterator { - type ReplayError; + // Provided methods + /// Conditionally [`record_event`](Self::record_event) when `pred` is true + fn record_event_when(&mut self, pred: P, f: F) + where + T: Into + fmt::Debug, + P: FnOnce(&RecordMetadata) -> bool, + F: FnOnce(&RecordMetadata) -> T, + { + if pred(self.metadata()) { + self.record_event(f); + } + } +} + +/// This trait provides the interface for a FIFO replayer that +/// essentially operates as an iterator over the recorded events +pub trait Replayer: Iterator { /// Constructs a reader on buffer fn new_replayer(cfg: ReplayConfig) -> Result where @@ -154,6 +185,71 @@ pub trait Replayer: Iterator { /// Get metadata associated with the replay process fn metadata(&self) -> &ReplayMetadata; + + // Provided Methods + + /// Pop the next replay event + /// + /// ## Errors + /// + /// Returns a `ReplayError::EmptyBuffer` if the buffer is empty + #[inline] + fn next_event(&mut self) -> Result { + let event = self.next().ok_or(ReplayError::EmptyBuffer); + if let Ok(e) = &event { + println!("Replay | {}", e); + } + event + } + + /// Pop the next replay event with an attemped type conversion to expected + /// event type + /// + /// ## Errors + /// + /// Returns a `ReplayError::EmptyBuffer` if the buffer is empty or a + /// `ReplayError::IncorrectEventVariant` if it failed to convert type safely + #[inline] + fn next_event_typed(&mut self) -> Result + where + T: TryFrom + fmt::Debug, + ReplayError: From<>::Error>, + { + T::try_from(self.next_event()?).map_err(|e| e.into()) + } + + /// Pop the next replay event and calls `f` with a desired type conversion + /// + /// ## Errors + /// + /// Returns a `ReplayError::EmptyBuffer` if the buffer is empty or a + /// `ReplayError::IncorrectEventVariant` if it failed to convert type safely + #[inline] + fn replay_event(&mut self, f: F) -> Result<(), ReplayError> + where + T: TryFrom + fmt::Debug, + ReplayError: From<>::Error>, + F: FnOnce(T, &ReplayMetadata) -> Result<(), ReplayError>, + { + let call_event = self.next_event_typed()?; + Ok(f(call_event, self.metadata())?) + } + + /// Conditionally [`replay_event`](Self::replay_event) when `pred` is true + #[inline] + fn replay_event_when(&mut self, pred: P, f: F) -> Result<(), ReplayError> + where + T: TryFrom + fmt::Debug, + ReplayError: From<>::Error>, + P: FnOnce(&ReplayMetadata) -> bool, + F: FnOnce(T, &ReplayMetadata) -> Result<(), ReplayError>, + { + if pred(self.metadata()) { + self.replay_event(f) + } else { + Ok(()) + } + } } /// The underlying serialized/deserialized type @@ -178,6 +274,13 @@ pub struct RecordBuffer { metadata: RecordMetadata, } +impl RecordBuffer { + /// Push a newly record event [`RREvent`] to the buffer + fn push_event(&mut self, event: RREvent) -> () { + self.data.buf.push_back(event) + } +} + impl Recorder for RecordBuffer { fn new_recorder(cfg: RecordConfig) -> Result { Ok(RecordBuffer { @@ -189,8 +292,15 @@ impl Recorder for RecordBuffer { }) } - fn push_event(&mut self, event: RREvent) { - self.data.buf.push_back(event) + #[inline] + fn record_event(&mut self, f: F) + where + T: Into + fmt::Debug, + F: FnOnce(&RecordMetadata) -> T, + { + let event = f(self.metadata()); + println!("Record | {:?}", &event); + self.push_event(event.into()); } fn flush_to_file(&mut self) -> Result<()> { @@ -231,8 +341,6 @@ impl Iterator for ReplayBuffer { } impl Replayer for ReplayBuffer { - type ReplayError = ReplayError; - fn new_replayer(cfg: ReplayConfig) -> Result { let mut file = File::open(cfg.path)?; let mut events = VecDeque::::new(); @@ -281,11 +389,10 @@ mod tests { .map(|x| MaybeUninit::new(x)) .collect::>(); - let event = core_wasm::HostFuncEntryEvent::new(values.as_slice(), None); - // Record values let mut recorder = RecordBuffer::new_recorder(record_cfg)?; - recorder.push_event(event.clone().into()); + let event = core_wasm::HostFuncEntryEvent::new(values.as_slice(), None); + recorder.record_event(|_| event.clone()); recorder.flush_to_file()?; let tmp = tmp.into_temp_path(); @@ -299,13 +406,13 @@ mod tests { metadata: ReplayMetadata { validate: true }, }; let mut replayer = ReplayBuffer::new_replayer(replay_cfg)?; - let event_pop = core_wasm::HostFuncEntryEvent::try_from( - replayer.next().ok_or(ReplayError::EmptyBuffer)?, - )?; - // Replay matches record - assert!(event == event_pop); + replayer.replay_event(|store_event: core_wasm::HostFuncEntryEvent, _| { + // Check replay matches record + assert!(store_event == event); + Ok(()) + })?; - // Queue is empty + // Check queue is empty assert!(replayer.next().is_none()); Ok(()) diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index de7f2ffb1649..fffaeff43906 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -1363,41 +1363,65 @@ impl StoreOpaque { self.replay_buffer.as_mut() } - /// Record the event generated by `F` if `P` holds true - #[inline(always)] - pub(crate) fn record_event(&mut self, push_predicate: P, f: F) + /// Record the given event into the store's record buffer + /// + /// Convenience wrapper around [`Recorder::record_event`] + #[inline] + pub(crate) fn record_event(&mut self, f: F) + where + T: Into + fmt::Debug, + F: FnOnce(&RecordMetadata) -> T, + { + if let Some(buf) = self.record_buffer_mut() { + buf.record_event(f); + } + } + + /// Conditionally record the given event into the store's record buffer + /// + /// Convenience wrapper around [`Recorder::record_event_when`] + #[inline] + pub(crate) fn record_event_when(&mut self, pred: P, f: F) where T: Into + fmt::Debug, P: FnOnce(&RecordMetadata) -> bool, F: FnOnce(&RecordMetadata) -> T, { if let Some(buf) = self.record_buffer_mut() { - let metadata = buf.metadata(); - if push_predicate(metadata) { - let event = f(buf.metadata()); - println!("Record | {:?}", &event); - buf.push_event(event.into()); - } + buf.record_event_when(pred, f); + } + } + + /// Process the next replay event from the store's replay buffer + /// + /// Convenience wrapper around [`Replayer::replay_event`] + #[inline] + pub(crate) fn replay_event(&mut self, f: F) -> Result<(), ReplayError> + where + T: TryFrom + fmt::Debug, + ReplayError: From<>::Error>, + F: FnOnce(T, &ReplayMetadata) -> Result<(), ReplayError>, + { + if let Some(buf) = self.replay_buffer_mut() { + buf.replay_event(f) + } else { + Ok(()) } } - /// Get the next replay event if `P` holds true and process it with `F` - #[inline(always)] - pub(crate) fn replay_event_typed(&mut self, pop_predicate: P, f: F) -> Result<()> + /// Conditionally process the next replay event from the store's replay buffer + /// + /// Convenience wrapper around [`Replayer::replay_event_when`] + #[inline] + pub(crate) fn replay_event_when(&mut self, pred: P, f: F) -> Result<(), ReplayError> where T: TryFrom + fmt::Debug, - >::Error: std::error::Error + Send + Sync + 'static, + ReplayError: From<>::Error>, P: FnOnce(&ReplayMetadata) -> bool, F: FnOnce(T, &ReplayMetadata) -> Result<(), ReplayError>, { if let Some(buf) = self.replay_buffer_mut() { - if pop_predicate(buf.metadata()) { - let call_event = T::try_from(buf.next().ok_or(ReplayError::EmptyBuffer)?)?; - println!("Replay | {:?}", &call_event); - Ok(f(call_event, buf.metadata())?) - } else { - Ok(()) - } + buf.replay_event_when(pred, f) } else { Ok(()) } From 7a1f754413dd8c39a6f702d9ac56ebe8c860e8ef Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Mon, 14 Jul 2025 18:24:22 -0700 Subject: [PATCH 28/62] Change interface names for buffers and store --- .../src/runtime/component/func/host.rs | 12 ++++---- .../src/runtime/component/func/options.rs | 4 +-- crates/wasmtime/src/runtime/func.rs | 6 ++-- crates/wasmtime/src/runtime/rr/events/mod.rs | 3 +- crates/wasmtime/src/runtime/rr/mod.rs | 28 +++++++++---------- crates/wasmtime/src/runtime/store.rs | 26 ++++++++--------- 6 files changed, 39 insertions(+), 40 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 760d328b17c6..b80c682d4178 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -206,18 +206,18 @@ where let param_tys = InterfaceType::Tuple(ty.params); let result_tys = InterfaceType::Tuple(ty.results); - cx.0.record_event_when( + cx.0.record_event_if( |r| r.add_validation, |_| { HostFuncEntryEvent::new( storage, // Don't need to check validation here since it is - // covered by the push predicate in this case + // covered by the predicate in this case Some(&types[ty.params]), ) }, ); - cx.0.replay_event_when( + cx.0.next_replay_event_if( |r| r.validate, |event: HostFuncEntryEvent, _| event.validate(&types[ty.params]), )?; @@ -448,18 +448,18 @@ where let replay_enabled = store.0.replay_enabled(); - store.0.record_event_when( + store.0.record_event_if( |r| r.add_validation, |_| { HostFuncEntryEvent::new( storage, // Don't need to check validation here since it is - // covered by the push predicate in this case + // covered by the predicate in this case Some(&types[func_ty.params]), ) }, ); - store.0.replay_event_when( + store.0.next_replay_event_if( |r| r.validate, |event: HostFuncEntryEvent, _| event.validate(&types[func_ty.params]), )?; diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index 05ab178b3d59..a170de93429c 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -99,7 +99,7 @@ impl<'a, const N: usize> Drop for ConstMemorySliceCell<'a, N> { /// Drops serves as a recording hook for stores to the memory slice fn drop(&mut self) { if let Some(buf) = &mut self.recorder { - buf.record_event_when( + buf.record_event_if( |_| true, |_| MemorySliceWriteEvent::new(self.offset, self.bytes.to_vec()), ); @@ -409,7 +409,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { let result = self.realloc_inner(old, old_size, old_align, new_size); self.store .0 - .record_event_when(|r| r.add_validation, |_| ReallocReturnEvent::new(&result)); + .record_event_if(|r| r.add_validation, |_| ReallocReturnEvent::new(&result)); result } diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 9e8dba7a1c85..09cfe4892d66 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -2354,11 +2354,11 @@ impl HostContext { // (only when validation is enabled). // Function type unwraps should never panic since they are // lazily evaluated - store.replay_event_when( + store.next_replay_event_if( |r| r.validate, |event: HostFuncEntryEvent, _| event.validate(wasm_func_type.unwrap()), )?; - store.record_event_when( + store.record_event_if( |r| r.add_validation, |_| { let wasm_func_type = wasm_func_type.unwrap(); @@ -2404,7 +2404,7 @@ impl HostContext { // Record/replay interceptions of raw return args let ret = if store.replay_enabled() { store - .replay_event(|event: HostFuncReturnEvent, rmeta| { + .next_replay_event_and(|event: HostFuncReturnEvent, rmeta| { event.move_into_slice( args.as_mut(), rmeta.validate.then_some(wasm_func_type.unwrap()), diff --git a/crates/wasmtime/src/runtime/rr/events/mod.rs b/crates/wasmtime/src/runtime/rr/events/mod.rs index d0a57b4fb635..7465c8f8bead 100644 --- a/crates/wasmtime/src/runtime/rr/events/mod.rs +++ b/crates/wasmtime/src/runtime/rr/events/mod.rs @@ -113,8 +113,7 @@ where } } else { println!( - "Warning: Replay typechecking cannot be performed - since recorded trace is missing validation data" + "Warning: Replay typechecking cannot be performed since recorded trace is missing validation data" ); Ok(()) } diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index 1a5c5aa476a3..49f87416fb59 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -149,7 +149,7 @@ pub trait Recorder { /// Record the event generated by `f` fn record_event(&mut self, f: F) where - T: Into + fmt::Debug, + T: Into, F: FnOnce(&RecordMetadata) -> T; /// Flush memory contents to underlying persistent storage @@ -163,9 +163,9 @@ pub trait Recorder { // Provided methods /// Conditionally [`record_event`](Self::record_event) when `pred` is true - fn record_event_when(&mut self, pred: P, f: F) + fn record_event_if(&mut self, pred: P, f: F) where - T: Into + fmt::Debug, + T: Into, P: FnOnce(&RecordMetadata) -> bool, F: FnOnce(&RecordMetadata) -> T, { @@ -212,7 +212,7 @@ pub trait Replayer: Iterator { #[inline] fn next_event_typed(&mut self) -> Result where - T: TryFrom + fmt::Debug, + T: TryFrom, ReplayError: From<>::Error>, { T::try_from(self.next_event()?).map_err(|e| e.into()) @@ -225,9 +225,9 @@ pub trait Replayer: Iterator { /// Returns a `ReplayError::EmptyBuffer` if the buffer is empty or a /// `ReplayError::IncorrectEventVariant` if it failed to convert type safely #[inline] - fn replay_event(&mut self, f: F) -> Result<(), ReplayError> + fn next_event_and(&mut self, f: F) -> Result<(), ReplayError> where - T: TryFrom + fmt::Debug, + T: TryFrom, ReplayError: From<>::Error>, F: FnOnce(T, &ReplayMetadata) -> Result<(), ReplayError>, { @@ -237,15 +237,15 @@ pub trait Replayer: Iterator { /// Conditionally [`replay_event`](Self::replay_event) when `pred` is true #[inline] - fn replay_event_when(&mut self, pred: P, f: F) -> Result<(), ReplayError> + fn next_event_if(&mut self, pred: P, f: F) -> Result<(), ReplayError> where - T: TryFrom + fmt::Debug, + T: TryFrom, ReplayError: From<>::Error>, P: FnOnce(&ReplayMetadata) -> bool, F: FnOnce(T, &ReplayMetadata) -> Result<(), ReplayError>, { if pred(self.metadata()) { - self.replay_event(f) + self.next_event_and(f) } else { Ok(()) } @@ -295,12 +295,12 @@ impl Recorder for RecordBuffer { #[inline] fn record_event(&mut self, f: F) where - T: Into + fmt::Debug, + T: Into, F: FnOnce(&RecordMetadata) -> T, { - let event = f(self.metadata()); - println!("Record | {:?}", &event); - self.push_event(event.into()); + let event = f(self.metadata()).into(); + println!("Record | {}", &event); + self.push_event(event); } fn flush_to_file(&mut self) -> Result<()> { @@ -406,7 +406,7 @@ mod tests { metadata: ReplayMetadata { validate: true }, }; let mut replayer = ReplayBuffer::new_replayer(replay_cfg)?; - replayer.replay_event(|store_event: core_wasm::HostFuncEntryEvent, _| { + replayer.next_event_and(|store_event: core_wasm::HostFuncEntryEvent, _| { // Check replay matches record assert!(store_event == event); Ok(()) diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index fffaeff43906..083b6d1a2b7e 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -1369,7 +1369,7 @@ impl StoreOpaque { #[inline] pub(crate) fn record_event(&mut self, f: F) where - T: Into + fmt::Debug, + T: Into, F: FnOnce(&RecordMetadata) -> T, { if let Some(buf) = self.record_buffer_mut() { @@ -1379,31 +1379,31 @@ impl StoreOpaque { /// Conditionally record the given event into the store's record buffer /// - /// Convenience wrapper around [`Recorder::record_event_when`] + /// Convenience wrapper around [`Recorder::record_event_if`] #[inline] - pub(crate) fn record_event_when(&mut self, pred: P, f: F) + pub(crate) fn record_event_if(&mut self, pred: P, f: F) where - T: Into + fmt::Debug, + T: Into, P: FnOnce(&RecordMetadata) -> bool, F: FnOnce(&RecordMetadata) -> T, { if let Some(buf) = self.record_buffer_mut() { - buf.record_event_when(pred, f); + buf.record_event_if(pred, f); } } /// Process the next replay event from the store's replay buffer /// - /// Convenience wrapper around [`Replayer::replay_event`] + /// Convenience wrapper around [`Replayer::next_event_and`] #[inline] - pub(crate) fn replay_event(&mut self, f: F) -> Result<(), ReplayError> + pub(crate) fn next_replay_event_and(&mut self, f: F) -> Result<(), ReplayError> where - T: TryFrom + fmt::Debug, + T: TryFrom, ReplayError: From<>::Error>, F: FnOnce(T, &ReplayMetadata) -> Result<(), ReplayError>, { if let Some(buf) = self.replay_buffer_mut() { - buf.replay_event(f) + buf.next_event_and(f) } else { Ok(()) } @@ -1411,17 +1411,17 @@ impl StoreOpaque { /// Conditionally process the next replay event from the store's replay buffer /// - /// Convenience wrapper around [`Replayer::replay_event_when`] + /// Convenience wrapper around [`Replayer::next_event_if`] #[inline] - pub(crate) fn replay_event_when(&mut self, pred: P, f: F) -> Result<(), ReplayError> + pub(crate) fn next_replay_event_if(&mut self, pred: P, f: F) -> Result<(), ReplayError> where - T: TryFrom + fmt::Debug, + T: TryFrom, ReplayError: From<>::Error>, P: FnOnce(&ReplayMetadata) -> bool, F: FnOnce(T, &ReplayMetadata) -> Result<(), ReplayError>, { if let Some(buf) = self.replay_buffer_mut() { - buf.replay_event_when(pred, f) + buf.next_event_if(pred, f) } else { Ok(()) } From 2f6716b3bb2dbc68aab5be5062f7f02705aab9c6 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 15 Jul 2025 11:40:07 -0700 Subject: [PATCH 29/62] Added macro wrappers for record/replay stubs --- .../src/runtime/component/func/host.rs | 133 ++++++++++-------- .../src/runtime/component/func/options.rs | 4 +- crates/wasmtime/src/runtime/rr/mod.rs | 2 +- crates/wasmtime/src/runtime/store.rs | 8 +- 4 files changed, 81 insertions(+), 66 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index b80c682d4178..05bd0ed46d03 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -20,6 +20,57 @@ use wasmtime_environ::component::{ StringEncoding, TypeFuncIndex, TypeTuple, }; +/// Record/replay stubs for host function entry events +macro_rules! rr_host_func_entry_event { + { $args:expr, $param_types:expr => $store:expr } => {{ + $store.record_event_if( + |r| r.add_validation, + |_| { + HostFuncEntryEvent::new( + $args, + // Don't need to check validation here since it is + // covered by the predicate in this case + Some($param_types), + ) + }, + ); + $store.next_replay_event_if( + |r| r.validate, + |event: HostFuncEntryEvent, _| event.validate($param_types), + )?; + }}; +} + +/// Record stubs for host function return events +macro_rules! record_host_func_return_event { + { $args:expr, $return_types:expr => $store:expr } => {{ + $store.record_event(|r| { + HostFuncReturnEvent::new( + $args, + r.add_validation.then_some($return_types), + ) + }); + }}; +} + +/// Record stubs for store events of component types +macro_rules! record_lower_store_event_wrapper { + { $lower_store:expr => $store:expr } => {{ + let store_result = $lower_store; + $store.record_event(|_| LowerStoreReturnEvent::new(&store_result)); + store_result + }}; +} + +/// Record stubs for lower events of component types +macro_rules! record_lower_event_wrapper { + { $lower:expr => $store:expr } => {{ + let lower_result = $lower; + $store.record_event(|_| LowerReturnEvent::new(&lower_result)); + lower_result + }}; +} + pub struct HostFunc { entrypoint: VMLoweringCallee, typecheck: Box) -> Result<()>) + Send + Sync>, @@ -206,21 +257,7 @@ where let param_tys = InterfaceType::Tuple(ty.params); let result_tys = InterfaceType::Tuple(ty.results); - cx.0.record_event_if( - |r| r.add_validation, - |_| { - HostFuncEntryEvent::new( - storage, - // Don't need to check validation here since it is - // covered by the predicate in this case - Some(&types[ty.params]), - ) - }, - ); - cx.0.next_replay_event_if( - |r| r.validate, - |event: HostFuncEntryEvent, _| event.validate(&types[ty.params]), - )?; + rr_host_func_entry_event! { storage, &types[ty.params] => cx.0 }; // There's a 2x2 matrix of whether parameters and results are stored on the // stack or on the heap. Each of the 4 branches here have a different @@ -308,33 +345,22 @@ where dst: &mut MaybeUninit<::Lower>| { let res = if let Some(retval) = &ret { // Normal execution path - let lower_result = retval.lower(cx, ty, dst); - cx.store - .0 - .record_event(|_| LowerReturnEvent::new(&lower_result)); - lower_result + record_lower_event_wrapper! { retval.lower(cx, ty, dst) => cx.store.0 } } else { // Replay execution path // This path also stores the final return values in resulting storage cx.replay_lowering(rr_tys, Some(storage_as_slice_mut(dst))) }; - cx.store.0.record_event(|rmeta| { - HostFuncReturnEvent::new( - storage_as_slice(dst), - rmeta.add_validation.then_some(rr_tys), - ) - }); + record_host_func_return_event! { + storage_as_slice(dst), rr_tys => cx.store.0 + }; res }; let indirect_results_lower = |cx: &mut LowerContext<'_, T>, dst: &ValRaw| { let ptr = validate_inbounds::(cx.as_slice(), dst)?; let res = if let Some(retval) = &ret { // Normal execution path - let store_result = retval.store(cx, ty, ptr); - cx.store - .0 - .record_event(|_| LowerStoreReturnEvent::new(&store_result)); - store_result + record_lower_store_event_wrapper! { retval.store(cx, ty, ptr) => cx.store.0 } } else { // Replay execution path // @@ -344,9 +370,9 @@ where cx.replay_lowering(rr_tys, None) }; // Recording here is just for marking the return event - cx.store.0.record_event(|rmeta| { - HostFuncReturnEvent::new(&[], rmeta.add_validation.then_some(rr_tys)) - }); + record_host_func_return_event! { + &[], rr_tys => cx.store.0 + }; res }; match self { @@ -448,21 +474,7 @@ where let replay_enabled = store.0.replay_enabled(); - store.0.record_event_if( - |r| r.add_validation, - |_| { - HostFuncEntryEvent::new( - storage, - // Don't need to check validation here since it is - // covered by the predicate in this case - Some(&types[func_ty.params]), - ) - }, - ); - store.0.next_replay_event_if( - |r| r.validate, - |event: HostFuncEntryEvent, _| event.validate(&types[func_ty.params]), - )?; + rr_host_func_entry_event! { storage, &types[func_ty.params] => store.0 }; let results = if replay_enabled { None @@ -517,7 +529,9 @@ where // Normal execution path let mut dst = storage[..cnt].iter_mut(); for (val, ty) in result_vals.iter().zip(result_tys.types.iter()) { - val.lower(&mut cx, *ty, &mut dst)?; + record_lower_event_wrapper! { + val.lower(&mut cx, *ty, &mut dst) => cx.store.0 + }?; } assert!(dst.next().is_none()); } else { @@ -530,12 +544,9 @@ where )), )?; } - cx.store.0.record_event(|rmeta| { - HostFuncReturnEvent::new( - mem::transmute::<&[MaybeUninit], &[ValRaw]>(storage), - rmeta.add_validation.then_some(result_tys), - ) - }); + record_host_func_return_event! { + mem::transmute::<&[MaybeUninit], &[ValRaw]>(storage), result_tys => cx.store.0 + }; } else { if let Some((result_vals, ret_index)) = results { // Normal execution path @@ -543,7 +554,9 @@ where let mut ptr = validate_inbounds_dynamic(&result_tys.abi, cx.as_slice(), ret_ptr)?; for (val, ty) in result_vals.iter().zip(result_tys.types.iter()) { let offset = types.canonical_abi(ty).next_field32_size(&mut ptr); - val.store(&mut cx, *ty, offset)?; + record_lower_store_event_wrapper! { + val.store(&mut cx, *ty, offset) => cx.store.0 + }?; } } else { // Replay execution path @@ -554,9 +567,9 @@ where cx.replay_lowering(result_tys, None)?; } // Recording here is just for marking the return event - cx.store.0.record_event(|rmeta| { - HostFuncReturnEvent::new(&[], rmeta.add_validation.then_some(result_tys)) - }); + record_host_func_return_event! { + &[], result_tys => cx.store.0 + }; } flags.set_may_leave(true); diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index a170de93429c..4bd11881b1ba 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -577,11 +577,11 @@ impl<'a, T: 'static> LowerContext<'a, T> { } complete = true; } - RREvent::ComponentLowerReturn(e) => e.ret()?, - RREvent::ComponentLowerStoreReturn(e) => e.ret()?, RREvent::ComponentReallocEntry(e) => { let _ = self.realloc_inner(e.old_addr, e.old_size, e.old_align, e.new_size); } + RREvent::ComponentLowerReturn(e) => e.ret()?, + RREvent::ComponentLowerStoreReturn(e) => e.ret()?, RREvent::ComponentMemorySliceWrite(e) => { // The bounds check is performed here is required here (in the absence of // trace validation) to protect against malicious out-of-bounds slice writes diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index 49f87416fb59..a979ac502a6b 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -103,7 +103,7 @@ rr_event! { } /// Error type signalling failures during a replay run -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub enum ReplayError { EmptyBuffer, FailedFuncValidation, diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index 083b6d1a2b7e..2c8c6668da67 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -2133,9 +2133,11 @@ at https://bytecodealliance.org/security. } /// Panics if the replay buffer in the store is non-empty - pub(crate) fn ensure_empty_replay_buffer(&mut self) { + pub(crate) fn check_empty_replay_buffer(&mut self) { if let Some(buf) = self.replay_buffer_mut() { - assert!(buf.next().is_none()); + if buf.next().is_some() { + println!("Warning: Replay buffer is not emptied!"); + } } } } @@ -2469,7 +2471,7 @@ impl Drop for StoreOpaque { } let _ = self.flush_record_buffer().unwrap(); - self.ensure_empty_replay_buffer(); + self.check_empty_replay_buffer(); } } From 836ab80cf3710054bb0e010c11b6f245e3093e75 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 15 Jul 2025 12:41:14 -0700 Subject: [PATCH 30/62] Add `RecordMetadata` to the trace to support optional replay validation --- crates/wasmtime/src/config.rs | 3 +- .../src/runtime/component/func/host.rs | 13 +++++--- .../src/runtime/component/func/options.rs | 11 ++++--- crates/wasmtime/src/runtime/func.rs | 15 +++++++--- .../src/runtime/rr/events/component_wasm.rs | 10 +------ crates/wasmtime/src/runtime/rr/mod.rs | 30 +++++++++++++++---- crates/wasmtime/src/runtime/store.rs | 2 +- 7 files changed, 55 insertions(+), 29 deletions(-) diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 98728248e2b1..931ea4a82423 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -3,6 +3,7 @@ use alloc::sync::Arc; use bitflags::Flags; use core::fmt; use core::str::FromStr; +use serde::{Deserialize, Serialize}; #[cfg(any(feature = "cache", feature = "cranelift", feature = "winch"))] use std::path::Path; use wasmparser::WasmFeatures; @@ -221,7 +222,7 @@ impl Default for CompilerConfig { } /// Metadata for specifying recording strategy -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct RecordMetadata { /// Flag to include additional signatures for replay validation pub add_validation: bool, diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 05bd0ed46d03..4c4281254cbd 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -2,6 +2,7 @@ use crate::component::func::{LiftContext, LowerContext, Options}; use crate::component::matching::InstanceType; use crate::component::storage::{slice_to_storage_mut, storage_as_slice, storage_as_slice_mut}; use crate::component::{ComponentNamedList, ComponentType, Lift, Lower, Val}; +use crate::config::ReplayMetadata; use crate::prelude::*; use crate::runtime::rr::events::component_wasm::{ HostFuncEntryEvent, HostFuncReturnEvent, LowerReturnEvent, LowerStoreReturnEvent, @@ -28,15 +29,19 @@ macro_rules! rr_host_func_entry_event { |_| { HostFuncEntryEvent::new( $args, - // Don't need to check validation here since it is - // covered by the predicate in this case Some($param_types), ) }, ); $store.next_replay_event_if( - |r| r.validate, - |event: HostFuncEntryEvent, _| event.validate($param_types), + |_, r| r.add_validation, + |event: HostFuncEntryEvent, r: &ReplayMetadata| { + if r.validate { + event.validate($param_types) + } else { + Ok(()) + } + }, )?; }}; } diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index 4bd11881b1ba..df86c65c8b3d 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -566,14 +566,17 @@ impl<'a, T: 'static> LowerContext<'a, T> { } let mut complete = false; while !complete { - let event = self.store.0.replay_buffer_mut().unwrap().next_event()?; + let buf = self.store.0.replay_buffer_mut().unwrap(); + let event = buf.next_event()?; + let replay_metadata = buf.metadata(); let _ = match event { RREvent::ComponentHostFuncReturn(e) => { // End of lowering process + if replay_metadata.validate { + e.validate(result_tys)? + } if let Some(storage) = result_storage.as_deref_mut() { - e.move_into_slice(storage, Some(&result_tys))?; - } else { - e.validate(result_tys)?; + e.move_into_slice(storage); } complete = true; } diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 09cfe4892d66..383812984fb9 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -1,3 +1,4 @@ +use crate::config::ReplayMetadata; use crate::prelude::*; use crate::rr::events::core_wasm::{HostFuncEntryEvent, HostFuncReturnEvent}; use crate::runtime::Uninhabited; @@ -2354,10 +2355,6 @@ impl HostContext { // (only when validation is enabled). // Function type unwraps should never panic since they are // lazily evaluated - store.next_replay_event_if( - |r| r.validate, - |event: HostFuncEntryEvent, _| event.validate(wasm_func_type.unwrap()), - )?; store.record_event_if( |r| r.add_validation, |_| { @@ -2371,6 +2368,16 @@ impl HostContext { ) }, ); + store.next_replay_event_if( + |_, r| r.add_validation, + |event: HostFuncEntryEvent, r: &ReplayMetadata| { + if r.validate { + event.validate(wasm_func_type.unwrap()) + } else { + Ok(()) + } + }, + )?; P::load(&mut store, args.as_mut()) // Drop on store is necessary here; scope closure makes this implicit diff --git a/crates/wasmtime/src/runtime/rr/events/component_wasm.rs b/crates/wasmtime/src/runtime/rr/events/component_wasm.rs index 69be9a41c6b3..9011c15ad443 100644 --- a/crates/wasmtime/src/runtime/rr/events/component_wasm.rs +++ b/crates/wasmtime/src/runtime/rr/events/component_wasm.rs @@ -61,16 +61,8 @@ impl HostFuncReturnEvent { /// Consume the caller event and encode it back into the slice with an optional /// typechecking validation of the event. - pub fn move_into_slice( - self, - args: &mut [ValRaw], - expect_types: Option<&TypeTuple>, - ) -> Result<(), ReplayError> { - if let Some(e) = expect_types { - self.validate(e)?; - } + pub fn move_into_slice(self, args: &mut [ValRaw]) { func_argvals_into_raw_slice(self.args, args); - Ok(()) } } diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index a979ac502a6b..d295c5b0133d 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -186,6 +186,9 @@ pub trait Replayer: Iterator { /// Get metadata associated with the replay process fn metadata(&self) -> &ReplayMetadata; + /// Get the metadata embedded within the trace during recording + fn trace_metadata(&self) -> &RecordMetadata; + // Provided Methods /// Pop the next replay event @@ -222,8 +225,8 @@ pub trait Replayer: Iterator { /// /// ## Errors /// - /// Returns a `ReplayError::EmptyBuffer` if the buffer is empty or a - /// `ReplayError::IncorrectEventVariant` if it failed to convert type safely + /// Returns a [`ReplayError::EmptyBuffer`] if the buffer is empty or a + /// [`ReplayError::IncorrectEventVariant`] if it failed to convert type safely #[inline] fn next_event_and(&mut self, f: F) -> Result<(), ReplayError> where @@ -235,16 +238,16 @@ pub trait Replayer: Iterator { Ok(f(call_event, self.metadata())?) } - /// Conditionally [`replay_event`](Self::replay_event) when `pred` is true + /// Conditionally execute [`next_event_and`](Self::next_event_and) when `pred` is true #[inline] fn next_event_if(&mut self, pred: P, f: F) -> Result<(), ReplayError> where T: TryFrom, ReplayError: From<>::Error>, - P: FnOnce(&ReplayMetadata) -> bool, + P: FnOnce(&ReplayMetadata, &RecordMetadata) -> bool, F: FnOnce(T, &ReplayMetadata) -> Result<(), ReplayError>, { - if pred(self.metadata()) { + if pred(self.metadata(), self.trace_metadata()) { self.next_event_and(f) } else { Ok(()) @@ -279,6 +282,11 @@ impl RecordBuffer { fn push_event(&mut self, event: RREvent) -> () { self.data.buf.push_back(event) } + + /// Generate indepedent references to data and metadata + fn split(&mut self) -> (&mut RRDataCommon, &RecordMetadata) { + (&mut self.data, &self.metadata) + } } impl Recorder for RecordBuffer { @@ -304,9 +312,11 @@ impl Recorder for RecordBuffer { } fn flush_to_file(&mut self) -> Result<()> { + let (data, metadata) = self.split(); + // Replay requires the RecordMetadata configuration + postcard::to_io(metadata, &mut data.rw)?; // Seralizing each event independently prevents checking for vector sizes // during deserialization - let data = &mut self.data; for v in &data.buf { postcard::to_io(&v, &mut data.rw)?; } @@ -330,6 +340,7 @@ impl Recorder for RecordBuffer { pub struct ReplayBuffer { data: RRDataCommon, metadata: ReplayMetadata, + trace_metadata: RecordMetadata, } impl Iterator for ReplayBuffer { @@ -345,6 +356,7 @@ impl Replayer for ReplayBuffer { let mut file = File::open(cfg.path)?; let mut events = VecDeque::::new(); // Read till EOF + let (trace_metadata, _) = postcard::from_io((&mut file, &mut [0; 0]))?; while file.stream_position()? != file.metadata()?.len() { let (event, _): (RREvent, _) = postcard::from_io((&mut file, &mut [0; 0]))?; events.push_back(event); @@ -355,6 +367,7 @@ impl Replayer for ReplayBuffer { rw: file, }, metadata: cfg.metadata, + trace_metadata: trace_metadata, }) } @@ -362,6 +375,11 @@ impl Replayer for ReplayBuffer { fn metadata(&self) -> &ReplayMetadata { &self.metadata } + + #[inline] + fn trace_metadata(&self) -> &RecordMetadata { + &self.trace_metadata + } } #[cfg(test)] diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index 2c8c6668da67..14956d469f3c 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -1417,7 +1417,7 @@ impl StoreOpaque { where T: TryFrom, ReplayError: From<>::Error>, - P: FnOnce(&ReplayMetadata) -> bool, + P: FnOnce(&ReplayMetadata, &RecordMetadata) -> bool, F: FnOnce(T, &ReplayMetadata) -> Result<(), ReplayError>, { if let Some(buf) = self.replay_buffer_mut() { From d2df9f476ea3c7b77b94919c5018a8fab27256cc Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 15 Jul 2025 16:42:58 -0700 Subject: [PATCH 31/62] Move unsafe for valraw transmute out of rr module --- crates/wasmtime/src/runtime/rr/events/mod.rs | 34 +++++++++----------- crates/wasmtime/src/runtime/vm/vmcontext.rs | 12 +++++++ 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/crates/wasmtime/src/runtime/rr/events/mod.rs b/crates/wasmtime/src/runtime/rr/events/mod.rs index 7465c8f8bead..3867945a486a 100644 --- a/crates/wasmtime/src/runtime/rr/events/mod.rs +++ b/crates/wasmtime/src/runtime/rr/events/mod.rs @@ -5,8 +5,6 @@ use core::fmt; use core::mem::{self, MaybeUninit}; use serde::{Deserialize, Serialize}; -const VAL_RAW_SIZE: usize = mem::size_of::(); - /// A serde compatible representation of errors produced by actions during /// initial recording for specific events /// @@ -35,38 +33,38 @@ impl std::error::Error for EventActionError {} /// Transmutable byte array used to serialize [`ValRaw`] union /// /// Maintaining the exact layout is crucial for zero-copy transmutations -/// between [`ValRawSer`] and [`ValRaw`] as currently assumed. However, +/// between [`ValRawBytes`] and [`ValRaw`] as currently assumed. However, /// in the future, this type could be represented with LEB128s #[derive(PartialEq, Eq, Clone, Serialize, Deserialize)] #[repr(C)] -pub(super) struct ValRawSer([u8; VAL_RAW_SIZE]); +pub(super) struct ValRawBytes([u8; mem::size_of::()]); -impl From for ValRawSer { +impl From for ValRawBytes { fn from(value: ValRaw) -> Self { - unsafe { Self(mem::transmute(value)) } + Self(value.as_bytes()) } } -impl From for ValRaw { - fn from(value: ValRawSer) -> Self { - unsafe { mem::transmute(value.0) } +impl From for ValRaw { + fn from(value: ValRawBytes) -> Self { + ValRaw::from_bytes(value.0) } } -impl From> for ValRawSer { +impl From> for ValRawBytes { /// Uninitialized data is assumed, and serialized fn from(value: MaybeUninit) -> Self { - unsafe { Self::from(value.assume_init()) } + Self::from(unsafe { value.assume_init() }) } } -impl From for MaybeUninit { - fn from(value: ValRawSer) -> Self { +impl From for MaybeUninit { + fn from(value: ValRawBytes) -> Self { MaybeUninit::new(value.into()) } } -impl fmt::Debug for ValRawSer { +impl fmt::Debug for ValRawBytes { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let hex_digits_per_byte = 2; let _ = write!(f, "0x.."); @@ -77,21 +75,21 @@ impl fmt::Debug for ValRawSer { } } -type RRFuncArgVals = Vec; +type RRFuncArgVals = Vec; /// Construct [`RRFuncArgVals`] from raw value buffer fn func_argvals_from_raw_slice(args: &[T]) -> RRFuncArgVals where - ValRawSer: From, + ValRawBytes: From, T: Copy, { - args.iter().map(|x| ValRawSer::from(*x)).collect::>() + args.iter().map(|x| ValRawBytes::from(*x)).collect() } /// Encode [`RRFuncArgVals`] back into raw value buffer fn func_argvals_into_raw_slice(rr_args: RRFuncArgVals, raw_args: &mut [T]) where - ValRawSer: Into, + ValRawBytes: Into, { for (src, dst) in rr_args.into_iter().zip(raw_args.iter_mut()) { *dst = src.into(); diff --git a/crates/wasmtime/src/runtime/vm/vmcontext.rs b/crates/wasmtime/src/runtime/vm/vmcontext.rs index 0a0f07a90b7d..47d9802b84f3 100644 --- a/crates/wasmtime/src/runtime/vm/vmcontext.rs +++ b/crates/wasmtime/src/runtime/vm/vmcontext.rs @@ -1533,6 +1533,18 @@ impl ValRaw { assert!(cfg!(feature = "gc") || anyref == 0); anyref } + + /// Get the raw bits of the union + #[inline] + pub fn as_bytes(&self) -> [u8; mem::size_of::()] { + unsafe { mem::transmute(*self) } + } + + /// Construct ValRaw from raw bits + #[inline] + pub fn from_bytes(value: [u8; mem::size_of::()]) -> Self { + unsafe { mem::transmute(value) } + } } /// An "opaque" version of `VMContext` which must be explicitly casted to a From 2c803f9861cc5db89588123ba06e878216456842 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 15 Jul 2025 17:28:19 -0700 Subject: [PATCH 32/62] Added enum in host call rr stubs for readability --- .../src/runtime/component/func/host.rs | 254 ++++++++++-------- 1 file changed, 135 insertions(+), 119 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 4c4281254cbd..1d4b8ba3f70a 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -288,16 +288,16 @@ where }; let replay_enabled = cx.0.replay_enabled(); - let ret = if replay_enabled { - None - } else { - Some({ + let ret = if !replay_enabled { + ReturnMode::Standard({ let mut lift = LiftContext::new(cx.0, &options, types, instance); lift.enter_call(); let params = storage.lift_params(&mut lift, param_tys)?; closure(cx.as_context_mut(), params)? }) + } else { + ReturnMode::Replay }; flags.set_may_leave(false); @@ -311,6 +311,11 @@ where return Ok(()); + enum ReturnMode { + Standard(Return), + Replay, + } + enum Storage<'a, P: ComponentType, R: ComponentType> { Direct(&'a mut MaybeUninit>), ParamsIndirect(&'a mut MaybeUninit>), @@ -343,52 +348,56 @@ where cx: &mut LowerContext<'_, T>, ty: InterfaceType, rr_tys: &TypeTuple, - ret: Option, + ret: ReturnMode, ) -> Result<()> { - let direct_results_lower = - |cx: &mut LowerContext<'_, T>, - dst: &mut MaybeUninit<::Lower>| { - let res = if let Some(retval) = &ret { - // Normal execution path + let direct_results_lower = |cx: &mut LowerContext<'_, T>, + dst: &mut MaybeUninit<::Lower>, + ret: ReturnMode| { + let res = match ret { + ReturnMode::Standard(retval) => { record_lower_event_wrapper! { retval.lower(cx, ty, dst) => cx.store.0 } - } else { - // Replay execution path + } + ReturnMode::Replay => { // This path also stores the final return values in resulting storage cx.replay_lowering(rr_tys, Some(storage_as_slice_mut(dst))) - }; - record_host_func_return_event! { - storage_as_slice(dst), rr_tys => cx.store.0 - }; - res - }; - let indirect_results_lower = |cx: &mut LowerContext<'_, T>, dst: &ValRaw| { - let ptr = validate_inbounds::(cx.as_slice(), dst)?; - let res = if let Some(retval) = &ret { - // Normal execution path - record_lower_store_event_wrapper! { retval.store(cx, ty, ptr) => cx.store.0 } - } else { - // Replay execution path - // - // `dst` is a Wasm pointer to indirect results. This pointer itself will remain - // deterministic, and thus replay will not need to change this. However, - // replay will have to overwrite any nested stored lowerings (deep copy) - cx.replay_lowering(rr_tys, None) + } }; - // Recording here is just for marking the return event record_host_func_return_event! { - &[], rr_tys => cx.store.0 + storage_as_slice(dst), rr_tys => cx.store.0 }; res }; + let indirect_results_lower = + |cx: &mut LowerContext<'_, T>, dst: &ValRaw, ret: ReturnMode| { + let ptr = validate_inbounds::(cx.as_slice(), dst)?; + let res = match ret { + ReturnMode::Standard(retval) => { + record_lower_store_event_wrapper! { retval.store(cx, ty, ptr) => cx.store.0 } + } + ReturnMode::Replay => { + // `dst` is a Wasm pointer to indirect results. This pointer itself will remain + // deterministic, and thus replay will not need to change this. However, + // replay will have to overwrite any nested stored lowerings (deep copy) + cx.replay_lowering(rr_tys, None) + } + }; + // Recording here is just for marking the return event + record_host_func_return_event! { + &[], rr_tys => cx.store.0 + }; + res + }; match self { Storage::Direct(storage) => { - direct_results_lower(cx, map_maybe_uninit!(storage.ret)) + direct_results_lower(cx, map_maybe_uninit!(storage.ret), ret) } Storage::ParamsIndirect(storage) => { - direct_results_lower(cx, map_maybe_uninit!(storage.ret)) + direct_results_lower(cx, map_maybe_uninit!(storage.ret), ret) + } + Storage::ResultsIndirect(storage) => { + indirect_results_lower(cx, &storage.retptr, ret) } - Storage::ResultsIndirect(storage) => indirect_results_lower(cx, &storage.retptr), - Storage::Indirect(storage) => indirect_results_lower(cx, &storage.retptr), + Storage::Indirect(storage) => indirect_results_lower(cx, &storage.retptr, ret), } } } @@ -452,6 +461,11 @@ where F: FnOnce(StoreContextMut<'_, T>, &[Val], &mut [Val]) -> Result<()>, T: 'static, { + enum ReturnMode { + Standard { vals: Vec, index: usize }, + Replay, + } + if async_ { todo!() } @@ -481,100 +495,102 @@ where rr_host_func_entry_event! { storage, &types[func_ty.params] => store.0 }; - let results = if replay_enabled { - None - } else { - Some({ - let mut cx = LiftContext::new(store.0, &options, types, instance); - cx.enter_call(); - if let Some(param_count) = param_tys.abi.flat_count(MAX_FLAT_PARAMS) { - // NB: can use `MaybeUninit::slice_assume_init_ref` when that's stable - let mut iter = - mem::transmute::<&[MaybeUninit], &[ValRaw]>(&storage[..param_count]) - .iter(); - args = param_tys - .types - .iter() - .map(|ty| Val::lift(&mut cx, *ty, &mut iter)) - .collect::>>()?; - ret_index = param_count; - assert!(iter.next().is_none()); - } else { - let mut offset = validate_inbounds_dynamic( - ¶m_tys.abi, - cx.memory(), - storage[0].assume_init_ref(), - )?; - args = param_tys - .types - .iter() - .map(|ty| { - let abi = types.canonical_abi(ty); - let size = usize::try_from(abi.size32).unwrap(); - let memory = &cx.memory()[abi.next_field32_size(&mut offset)..][..size]; - Val::load(&mut cx, *ty, memory) - }) - .collect::>>()?; - ret_index = 1; - }; + let results = if !replay_enabled { + let mut cx = LiftContext::new(store.0, &options, types, instance); + cx.enter_call(); + if let Some(param_count) = param_tys.abi.flat_count(MAX_FLAT_PARAMS) { + // NB: can use `MaybeUninit::slice_assume_init_ref` when that's stable + let mut iter = + mem::transmute::<&[MaybeUninit], &[ValRaw]>(&storage[..param_count]).iter(); + args = param_tys + .types + .iter() + .map(|ty| Val::lift(&mut cx, *ty, &mut iter)) + .collect::>>()?; + ret_index = param_count; + assert!(iter.next().is_none()); + } else { + let mut offset = validate_inbounds_dynamic( + ¶m_tys.abi, + cx.memory(), + storage[0].assume_init_ref(), + )?; + args = param_tys + .types + .iter() + .map(|ty| { + let abi = types.canonical_abi(ty); + let size = usize::try_from(abi.size32).unwrap(); + let memory = &cx.memory()[abi.next_field32_size(&mut offset)..][..size]; + Val::load(&mut cx, *ty, memory) + }) + .collect::>>()?; + ret_index = 1; + }; - let mut result_vals = Vec::with_capacity(result_tys.types.len()); - for _ in result_tys.types.iter() { - result_vals.push(Val::Bool(false)); - } - closure(store.as_context_mut(), &args, &mut result_vals)?; - (result_vals, ret_index) - }) + let mut result_vals = Vec::with_capacity(result_tys.types.len()); + for _ in result_tys.types.iter() { + result_vals.push(Val::Bool(false)); + } + closure(store.as_context_mut(), &args, &mut result_vals)?; + ReturnMode::Standard { + vals: result_vals, + index: ret_index, + } + } else { + ReturnMode::Replay }; + flags.set_may_leave(false); let mut cx = LowerContext::new(store, &options, types, instance); if let Some(cnt) = result_tys.abi.flat_count(MAX_FLAT_RESULTS) { - if let Some((result_vals, _)) = results { - // Normal execution path - let mut dst = storage[..cnt].iter_mut(); - for (val, ty) in result_vals.iter().zip(result_tys.types.iter()) { - record_lower_event_wrapper! { - val.lower(&mut cx, *ty, &mut dst) => cx.store.0 - }?; + match results { + ReturnMode::Standard { vals, index: _ } => { + let mut dst = storage[..cnt].iter_mut(); + for (val, ty) in vals.iter().zip(result_tys.types.iter()) { + record_lower_event_wrapper! { + val.lower(&mut cx, *ty, &mut dst) => cx.store.0 + }?; + } + assert!(dst.next().is_none()); + record_host_func_return_event! { + mem::transmute::<&[MaybeUninit], &[ValRaw]>(storage), result_tys => cx.store.0 + }; + } + ReturnMode::Replay => { + // This path also stores the final return values in resulting storage + cx.replay_lowering( + result_tys, + Some(mem::transmute::<&mut [MaybeUninit], &mut [ValRaw]>( + storage, + )), + )?; } - assert!(dst.next().is_none()); - } else { - // Replay execution path - // This path also stores the final return values in resulting storage - cx.replay_lowering( - result_tys, - Some(mem::transmute::<&mut [MaybeUninit], &mut [ValRaw]>( - storage, - )), - )?; - } - record_host_func_return_event! { - mem::transmute::<&[MaybeUninit], &[ValRaw]>(storage), result_tys => cx.store.0 }; } else { - if let Some((result_vals, ret_index)) = results { - // Normal execution path - let ret_ptr = storage[ret_index].assume_init_ref(); - let mut ptr = validate_inbounds_dynamic(&result_tys.abi, cx.as_slice(), ret_ptr)?; - for (val, ty) in result_vals.iter().zip(result_tys.types.iter()) { - let offset = types.canonical_abi(ty).next_field32_size(&mut ptr); - record_lower_store_event_wrapper! { - val.store(&mut cx, *ty, offset) => cx.store.0 - }?; + match results { + ReturnMode::Standard { vals, index } => { + let ret_ptr = storage[index].assume_init_ref(); + let mut ptr = validate_inbounds_dynamic(&result_tys.abi, cx.as_slice(), ret_ptr)?; + for (val, ty) in vals.iter().zip(result_tys.types.iter()) { + let offset = types.canonical_abi(ty).next_field32_size(&mut ptr); + record_lower_store_event_wrapper! { + val.store(&mut cx, *ty, offset) => cx.store.0 + }?; + } + // Recording here is just for marking the return event + record_host_func_return_event! { + &[], result_tys => cx.store.0 + }; + } + ReturnMode::Replay => { + // The indirect `ret_ptr` itself will remain deterministic, and thus replay will not + // need to change the return storage. However, replay will have to overwrite any nested stored + // lowerings (deep copy) + cx.replay_lowering(result_tys, None)?; } - } else { - // Replay execution path - // - // The indirect `ret_ptr` itself will remain deterministic, and thus replay will not - // need to change the return storage. However, replay will have to overwrite any nested stored - // lowerings (deep copy) - cx.replay_lowering(result_tys, None)?; } - // Recording here is just for marking the return event - record_host_func_return_event! { - &[], result_tys => cx.store.0 - }; } flags.set_may_leave(true); From 423b19ec18a20d4133a7b6f9edfa27517b8b341a Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 15 Jul 2025 17:30:26 -0700 Subject: [PATCH 33/62] fixup! Added enum in host call rr stubs for readability --- crates/wasmtime/src/runtime/component/func/host.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 1d4b8ba3f70a..e1888256f767 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -559,13 +559,10 @@ where }; } ReturnMode::Replay => { + let result_storage = + mem::transmute::<&mut [MaybeUninit], &mut [ValRaw]>(storage); // This path also stores the final return values in resulting storage - cx.replay_lowering( - result_tys, - Some(mem::transmute::<&mut [MaybeUninit], &mut [ValRaw]>( - storage, - )), - )?; + cx.replay_lowering(result_tys, Some(result_storage))?; } }; } else { From 0cb5ce17d4f16fee7ad099b60e5c938eb284fa68 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 15 Jul 2025 17:40:07 -0700 Subject: [PATCH 34/62] Switch `rr_event` macro syntax --- crates/wasmtime/src/runtime/rr/mod.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index d295c5b0133d..9b25c5e7ae6b 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -24,7 +24,7 @@ macro_rules! rr_event { ( $( $(#[doc = $doc:literal])* - $variant:ident => $event:ty + $variant:ident($event:ty) ),* ) => ( /// A single, unified, low-level recording/replay event @@ -73,33 +73,33 @@ macro_rules! rr_event { // Set of supported record/replay events rr_event! { /// Call into host function from Core Wasm - CoreHostFuncEntry => core_wasm::HostFuncEntryEvent, + CoreHostFuncEntry(core_wasm::HostFuncEntryEvent), /// Return from host function to Core Wasm - CoreHostFuncReturn => core_wasm::HostFuncReturnEvent, + CoreHostFuncReturn(core_wasm::HostFuncReturnEvent), // REQUIRED events for replay // /// Return from host function to component - ComponentHostFuncReturn => component_wasm::HostFuncReturnEvent, + ComponentHostFuncReturn(component_wasm::HostFuncReturnEvent), /// Component ABI realloc call in linear wasm memory - ComponentReallocEntry => component_wasm::ReallocEntryEvent, + ComponentReallocEntry(component_wasm::ReallocEntryEvent), /// Return from a type lowering operation - ComponentLowerReturn => component_wasm::LowerReturnEvent, + ComponentLowerReturn(component_wasm::LowerReturnEvent), /// Return from a store during a type lowering operation - ComponentLowerStoreReturn => component_wasm::LowerStoreReturnEvent, + ComponentLowerStoreReturn(component_wasm::LowerStoreReturnEvent), /// An attempt to obtain a mutable slice into Wasm linear memory - ComponentMemorySliceWrite => component_wasm::MemorySliceWriteEvent, + ComponentMemorySliceWrite(component_wasm::MemorySliceWriteEvent), // OPTIONAL events for replay validation // /// Call into host function from component - ComponentHostFuncEntry => component_wasm::HostFuncEntryEvent, + ComponentHostFuncEntry(component_wasm::HostFuncEntryEvent), /// Call into [Lower::lower] for type lowering - ComponentLowerEntry => component_wasm::LowerEntryEvent, + ComponentLowerEntry(component_wasm::LowerEntryEvent), /// Call into [Lower::store] during type lowering - ComponentLowerStoreEntry => component_wasm::LowerStoreEntryEvent, + ComponentLowerStoreEntry(component_wasm::LowerStoreEntryEvent), /// Return from Component ABI realloc call - ComponentReallocReturn => component_wasm::ReallocReturnEvent + ComponentReallocReturn(component_wasm::ReallocReturnEvent) } /// Error type signalling failures during a replay run From 42101ea53df91fd9076bb50eba2623e3530ace4d Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Wed, 16 Jul 2025 11:38:56 -0700 Subject: [PATCH 35/62] Initial feature gating of RR for type info; fix doc inclusion for component events --- crates/wasmtime/Cargo.toml | 7 ++ .../src/runtime/component/func/host.rs | 92 ++++++++++++------- .../src/runtime/component/func/options.rs | 13 +-- .../src/runtime/rr/events/component_wasm.rs | 21 ++++- 4 files changed, 93 insertions(+), 40 deletions(-) diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml index 2c5bf9c30582..fd270a2fc90e 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -393,3 +393,10 @@ component-model-async = [ "wasmtime-component-macro?/component-model-async", "dep:futures" ] + +# Enables support for record/replay +rr = ["rr-component", "rr-core"] +rr-component = ["component-model", "std"] +rr-core = ["std"] +rr-type-validation = [] +rr-args-validation = [] diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index e1888256f767..406cc6a28f06 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -2,10 +2,9 @@ use crate::component::func::{LiftContext, LowerContext, Options}; use crate::component::matching::InstanceType; use crate::component::storage::{slice_to_storage_mut, storage_as_slice, storage_as_slice_mut}; use crate::component::{ComponentNamedList, ComponentType, Lift, Lower, Val}; -use crate::config::ReplayMetadata; use crate::prelude::*; use crate::runtime::rr::events::component_wasm::{ - HostFuncEntryEvent, HostFuncReturnEvent, LowerReturnEvent, LowerStoreReturnEvent, + HostFuncReturnEvent, LowerReturnEvent, LowerStoreReturnEvent, }; use crate::runtime::vm::component::{ ComponentInstance, InstanceFlags, VMComponentContext, VMLowering, VMLoweringCallee, @@ -16,43 +15,52 @@ use alloc::sync::Arc; use core::any::Any; use core::mem::{self, MaybeUninit}; use core::ptr::NonNull; +#[cfg(feature = "rr-type-validation")] +use wasmtime_environ::component::TypeTuple; use wasmtime_environ::component::{ CanonicalAbiInfo, ComponentTypes, InterfaceType, MAX_FLAT_PARAMS, MAX_FLAT_RESULTS, - StringEncoding, TypeFuncIndex, TypeTuple, + StringEncoding, TypeFuncIndex, }; /// Record/replay stubs for host function entry events macro_rules! rr_host_func_entry_event { - { $args:expr, $param_types:expr => $store:expr } => {{ - $store.record_event_if( - |r| r.add_validation, - |_| { - HostFuncEntryEvent::new( - $args, - Some($param_types), - ) - }, - ); - $store.next_replay_event_if( - |_, r| r.add_validation, - |event: HostFuncEntryEvent, r: &ReplayMetadata| { - if r.validate { - event.validate($param_types) - } else { + { $args:expr, $param_types:expr => $store:expr } => { + #[cfg(any(feature = "rr-type-validation", feature = "rr-args-validation"))] + { + use crate::config::ReplayMetadata; + use crate::runtime::rr::events::component_wasm::HostFuncEntryEvent; + $store.record_event_if( + |r| r.add_validation, + |_| { + HostFuncEntryEvent::new( + $args, + #[cfg(feature = "rr-type-validation")] + Some($param_types), + ) + }, + ); + $store.next_replay_event_if( + |_, r| r.add_validation, + |_event: HostFuncEntryEvent, _r: &ReplayMetadata| { + #[cfg(feature = "rr-type-validation")] + if _r.validate { + _event.validate($param_types) + } Ok(()) - } - }, - )?; - }}; + }, + )?; + } + }; } /// Record stubs for host function return events macro_rules! record_host_func_return_event { { $args:expr, $return_types:expr => $store:expr } => {{ - $store.record_event(|r| { + $store.record_event(|_r| { HostFuncReturnEvent::new( $args, - r.add_validation.then_some($return_types), + #[cfg(feature = "rr-type-validation")] + _r.add_validation.then_some($return_types), ) }); }}; @@ -302,7 +310,13 @@ where flags.set_may_leave(false); let mut lower = LowerContext::new(cx, &options, types, instance); - storage.lower_results(&mut lower, result_tys, &types[ty.results], ret)?; + storage.lower_results( + &mut lower, + result_tys, + ret, + #[cfg(feature = "rr-type-validation")] + &types[ty.results], + )?; flags.set_may_leave(true); if !replay_enabled { @@ -347,8 +361,8 @@ where &mut self, cx: &mut LowerContext<'_, T>, ty: InterfaceType, - rr_tys: &TypeTuple, ret: ReturnMode, + #[cfg(feature = "rr-type-validation")] rr_tys: &TypeTuple, ) -> Result<()> { let direct_results_lower = |cx: &mut LowerContext<'_, T>, dst: &mut MaybeUninit<::Lower>, @@ -359,7 +373,11 @@ where } ReturnMode::Replay => { // This path also stores the final return values in resulting storage - cx.replay_lowering(rr_tys, Some(storage_as_slice_mut(dst))) + cx.replay_lowering( + Some(storage_as_slice_mut(dst)), + #[cfg(feature = "rr-type-validation")] + rr_tys, + ) } }; record_host_func_return_event! { @@ -378,7 +396,11 @@ where // `dst` is a Wasm pointer to indirect results. This pointer itself will remain // deterministic, and thus replay will not need to change this. However, // replay will have to overwrite any nested stored lowerings (deep copy) - cx.replay_lowering(rr_tys, None) + cx.replay_lowering( + None, + #[cfg(feature = "rr-type-validation")] + rr_tys, + ) } }; // Recording here is just for marking the return event @@ -562,7 +584,11 @@ where let result_storage = mem::transmute::<&mut [MaybeUninit], &mut [ValRaw]>(storage); // This path also stores the final return values in resulting storage - cx.replay_lowering(result_tys, Some(result_storage))?; + cx.replay_lowering( + Some(result_storage), + #[cfg(feature = "rr-type-validation")] + result_tys, + )?; } }; } else { @@ -585,7 +611,11 @@ where // The indirect `ret_ptr` itself will remain deterministic, and thus replay will not // need to change the return storage. However, replay will have to overwrite any nested stored // lowerings (deep copy) - cx.replay_lowering(result_tys, None)?; + cx.replay_lowering( + None, + #[cfg(feature = "rr-type-validation")] + result_tys, + )?; } } } diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index df86c65c8b3d..d64ae6f7f318 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -15,9 +15,9 @@ use crate::{FuncType, StoreContextMut, ValRaw}; use alloc::sync::Arc; use core::ops::{Deref, DerefMut}; use core::ptr::NonNull; -use wasmtime_environ::component::{ - ComponentTypes, StringEncoding, TypeResourceTableIndex, TypeTuple, -}; +#[cfg(feature = "rr-type-validation")] +use wasmtime_environ::component::TypeTuple; +use wasmtime_environ::component::{ComponentTypes, StringEncoding, TypeResourceTableIndex}; /// Same as [`ConstMemorySliceCell`] except allows for dynamically sized slices. /// @@ -558,8 +558,8 @@ impl<'a, T: 'static> LowerContext<'a, T> { /// * It is assumed that this is only invoked at the root lower/store calls pub fn replay_lowering( &mut self, - result_tys: &TypeTuple, mut result_storage: Option<&mut [ValRaw]>, + #[cfg(feature = "rr-type-validation")] result_tys: &TypeTuple, ) -> Result<()> { if self.store.0.replay_buffer_mut().is_none() { return Ok(()); @@ -567,12 +567,13 @@ impl<'a, T: 'static> LowerContext<'a, T> { let mut complete = false; while !complete { let buf = self.store.0.replay_buffer_mut().unwrap(); + let _replay_metadata = buf.metadata(); let event = buf.next_event()?; - let replay_metadata = buf.metadata(); let _ = match event { RREvent::ComponentHostFuncReturn(e) => { // End of lowering process - if replay_metadata.validate { + #[cfg(feature = "rr-type-validation")] + if _replay_metadata.validate { e.validate(result_tys)? } if let Some(storage) = result_storage.as_deref_mut() { diff --git a/crates/wasmtime/src/runtime/rr/events/component_wasm.rs b/crates/wasmtime/src/runtime/rr/events/component_wasm.rs index 9011c15ad443..f295e31fb049 100644 --- a/crates/wasmtime/src/runtime/rr/events/component_wasm.rs +++ b/crates/wasmtime/src/runtime/rr/events/component_wasm.rs @@ -3,7 +3,9 @@ use super::*; #[allow(unused_imports)] use crate::component::ComponentType; use std::vec::Vec; -use wasmtime_environ::component::{InterfaceType, TypeTuple}; +use wasmtime_environ::component::InterfaceType; +#[cfg(feature = "rr-type-validation")] +use wasmtime_environ::component::TypeTuple; /// A call event from a Wasm component into the host #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -16,17 +18,23 @@ pub struct HostFuncEntryEvent { /// Note: This relies on the invariant that [InterfaceType] will always be /// deterministic. Currently, the type indices into various [ComponentTypes] /// maintain this, allowing for quick type-checking. + #[cfg(feature = "rr-type-validation")] types: Option, } impl HostFuncEntryEvent { // Record - pub fn new(args: &[MaybeUninit], types: Option<&TypeTuple>) -> Self { + pub fn new( + args: &[MaybeUninit], + #[cfg(feature = "rr-type-validation")] types: Option<&TypeTuple>, + ) -> Self { Self { args: func_argvals_from_raw_slice(args), + #[cfg(feature = "rr-type-validation")] types: types.cloned(), } } // Replay + #[cfg(feature = "rr-type-validation")] pub fn validate(&self, expect_types: &TypeTuple) -> Result<(), ReplayError> { replay_args_typecheck(self.types.as_ref(), expect_types) } @@ -44,17 +52,23 @@ pub struct HostFuncReturnEvent { /// Note: This relies on the invariant that [InterfaceType] will always be /// deterministic. Currently, the type indices into various [ComponentTypes] /// maintain this, allowing for quick type-checking. + #[cfg(feature = "rr-type-validation")] types: Option, } impl HostFuncReturnEvent { // Record - pub fn new(args: &[ValRaw], types: Option<&TypeTuple>) -> Self { + pub fn new( + args: &[ValRaw], + #[cfg(feature = "rr-type-validation")] types: Option<&TypeTuple>, + ) -> Self { Self { args: func_argvals_from_raw_slice(args), + #[cfg(feature = "rr-type-validation")] types: types.cloned(), } } // Replay + #[cfg(feature = "rr-type-validation")] pub fn validate(&self, expect_types: &TypeTuple) -> Result<(), ReplayError> { replay_args_typecheck(self.types.as_ref(), expect_types) } @@ -107,6 +121,7 @@ macro_rules! generic_new_events { ) => ( $( #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + $(#[doc = $doc])* pub struct $struct { $( pub $field: $field_ty, From 7c7a60eeb78f63dfd7ee56c24b5a25a406902eb4 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Wed, 16 Jul 2025 12:43:36 -0700 Subject: [PATCH 36/62] Enum and rr type gating for core wasm; bugfix in component lowering type --- crates/wasmtime/Cargo.toml | 10 +++ .../src/runtime/component/func/host.rs | 2 +- .../src/runtime/component/func/options.rs | 2 +- crates/wasmtime/src/runtime/func.rs | 84 +++++++++++-------- .../src/runtime/rr/events/core_wasm.rs | 19 ++++- crates/wasmtime/src/runtime/rr/events/mod.rs | 1 + crates/wasmtime/src/runtime/rr/mod.rs | 14 ++-- crates/wasmtime/src/runtime/store.rs | 1 + 8 files changed, 86 insertions(+), 47 deletions(-) diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml index fd270a2fc90e..70bc0ea076a1 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -396,7 +396,17 @@ component-model-async = [ # Enables support for record/replay rr = ["rr-component", "rr-core"] + +# Component model RR support rr-component = ["component-model", "std"] + +# Core wasm RR support rr-core = ["std"] + +# Support for type information of recorded events for replay validation rr-type-validation = [] + +# Support for input values to recorded events for replay validation. +# +# This can be used to check whether the entire module is truly deterministic rr-args-validation = [] diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 406cc6a28f06..31c18e104c86 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -44,7 +44,7 @@ macro_rules! rr_host_func_entry_event { |_event: HostFuncEntryEvent, _r: &ReplayMetadata| { #[cfg(feature = "rr-type-validation")] if _r.validate { - _event.validate($param_types) + _event.validate($param_types)?; } Ok(()) }, diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index d64ae6f7f318..bdb436edea2b 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -567,8 +567,8 @@ impl<'a, T: 'static> LowerContext<'a, T> { let mut complete = false; while !complete { let buf = self.store.0.replay_buffer_mut().unwrap(); - let _replay_metadata = buf.metadata(); let event = buf.next_event()?; + let _replay_metadata = buf.metadata(); let _ = match event { RREvent::ComponentHostFuncReturn(e) => { // End of lowering process diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 383812984fb9..7efb99199b2e 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -1,6 +1,5 @@ -use crate::config::ReplayMetadata; use crate::prelude::*; -use crate::rr::events::core_wasm::{HostFuncEntryEvent, HostFuncReturnEvent}; +use crate::rr::events::core_wasm::HostFuncReturnEvent; use crate::runtime::Uninhabited; use crate::runtime::vm::{ ExportFunction, InterpreterRef, SendSyncPtr, StoreBox, VMArrayCallHostFuncContext, @@ -2322,6 +2321,11 @@ impl HostContext { // should be part of this closure, and the long-jmp-ing // happens after the closure in handling the result. let run = move |mut caller: Caller<'_, T>| { + enum ReturnMode { + Standard(R::Fallible), + Replay, + } + let mut args = NonNull::slice_from_raw_parts(args.cast::>(), args_len); let vmctx = VMArrayCallHostFuncContext::from_opaque(callee_vmctx).as_ref(); @@ -2355,38 +2359,44 @@ impl HostContext { // (only when validation is enabled). // Function type unwraps should never panic since they are // lazily evaluated - store.record_event_if( - |r| r.add_validation, - |_| { - let wasm_func_type = wasm_func_type.unwrap(); - let num_params = wasm_func_type.params().len(); - HostFuncEntryEvent::new( - &args.as_ref()[..num_params], - // Don't need to check validation here since it is - // covered by the push predicate in this case - Some(wasm_func_type.clone()), - ) - }, - ); - store.next_replay_event_if( - |_, r| r.add_validation, - |event: HostFuncEntryEvent, r: &ReplayMetadata| { - if r.validate { - event.validate(wasm_func_type.unwrap()) - } else { + #[cfg(any(feature = "rr-type-validation", feature = "rr-args-validation"))] + { + use crate::config::ReplayMetadata; + use crate::rr::events::core_wasm::HostFuncEntryEvent; + store.record_event_if( + |r| r.add_validation, + |_| { + let wasm_func_type = wasm_func_type.unwrap(); + let num_params = wasm_func_type.params().len(); + HostFuncEntryEvent::new( + &args.as_ref()[..num_params], + // Don't need to check validation here since it is + // covered by the push predicate in this case + #[cfg(feature = "rr-type-validation")] + Some(wasm_func_type.clone()), + ) + }, + ); + store.next_replay_event_if( + |_, r| r.add_validation, + |_event: HostFuncEntryEvent, _r: &ReplayMetadata| { + #[cfg(feature = "rr-type-validation")] + if _r.validate { + _event.validate(wasm_func_type.unwrap())?; + } Ok(()) - } - }, - )?; + }, + )?; + } P::load(&mut store, args.as_mut()) // Drop on store is necessary here; scope closure makes this implicit }; let returns = if caller.store.0.replay_enabled() { - None + ReturnMode::::Replay } else { - Some('ret: { + ReturnMode::Standard('ret: { if let Err(trap) = caller.store.0.call_hook(CallHook::CallingHost) { break 'ret R::fallible_from_error(trap); } @@ -2409,24 +2419,28 @@ impl HostContext { }; // Record/replay interceptions of raw return args - let ret = if store.replay_enabled() { - store - .next_replay_event_and(|event: HostFuncReturnEvent, rmeta| { + let ret = match returns { + ReturnMode::Replay => store + .next_replay_event_and(|event: HostFuncReturnEvent, _rmeta| { event.move_into_slice( args.as_mut(), - rmeta.validate.then_some(wasm_func_type.unwrap()), + #[cfg(feature = "rr-type-validation")] + _rmeta.validate.then_some(wasm_func_type.unwrap()), ) }) - .map_err(Into::into) - } else { - returns.unwrap().store(&mut store, args.as_mut()) + .map_err(Into::into), + ReturnMode::Standard(fallible) => { + let fallible: ::Fallible = fallible; + fallible.store(&mut store, args.as_mut()) + } }?; - store.record_event(|rmeta| { + store.record_event(|_rmeta| { let wasm_func_type = wasm_func_type.unwrap(); let num_results = wasm_func_type.params().len(); HostFuncReturnEvent::new( unsafe { &args.as_ref()[..num_results] }, - rmeta.add_validation.then_some(wasm_func_type.clone()), + #[cfg(feature = "rr-type-validation")] + _rmeta.add_validation.then_some(wasm_func_type.clone()), ) }); diff --git a/crates/wasmtime/src/runtime/rr/events/core_wasm.rs b/crates/wasmtime/src/runtime/rr/events/core_wasm.rs index e57736431cd1..603945b28e6b 100644 --- a/crates/wasmtime/src/runtime/rr/events/core_wasm.rs +++ b/crates/wasmtime/src/runtime/rr/events/core_wasm.rs @@ -4,6 +4,7 @@ use super::*; use wasmtime_environ::{WasmFuncType, WasmValType}; /// Note: Switch [`CoreFuncArgTypes`] to use [`Vec`] for better efficiency +#[cfg(feature = "rr-type-validation")] type CoreFuncArgTypes = WasmFuncType; /// A call event from a Core Wasm module into the host @@ -12,17 +13,23 @@ pub struct HostFuncEntryEvent { /// Raw values passed across the call/return boundary args: RRFuncArgVals, /// Optional param/return types (required to support replay validation) + #[cfg(feature = "rr-type-validation")] types: Option, } impl HostFuncEntryEvent { // Record - pub fn new(args: &[MaybeUninit], types: Option) -> Self { + pub fn new( + args: &[MaybeUninit], + #[cfg(feature = "rr-type-validation")] types: Option, + ) -> Self { Self { args: func_argvals_from_raw_slice(args), + #[cfg(feature = "rr-type-validation")] types: types, } } // Replay + #[cfg(feature = "rr-type-validation")] pub fn validate(&self, expect_types: &CoreFuncArgTypes) -> Result<(), ReplayError> { replay_args_typecheck(self.types.as_ref(), expect_types) } @@ -36,13 +43,18 @@ pub struct HostFuncReturnEvent { /// Raw values passed across the call/return boundary args: RRFuncArgVals, /// Optional param/return types (required to support replay validation) + #[cfg(feature = "rr-type-validation")] types: Option, } impl HostFuncReturnEvent { // Record - pub fn new(args: &[MaybeUninit], types: Option) -> Self { + pub fn new( + args: &[MaybeUninit], + #[cfg(feature = "rr-type-validation")] types: Option, + ) -> Self { Self { args: func_argvals_from_raw_slice(args), + #[cfg(feature = "rr-type-validation")] types: types, } } @@ -52,8 +64,9 @@ impl HostFuncReturnEvent { pub fn move_into_slice( self, args: &mut [MaybeUninit], - expect_types: Option<&WasmFuncType>, + #[cfg(feature = "rr-type-validation")] expect_types: Option<&WasmFuncType>, ) -> Result<(), ReplayError> { + #[cfg(feature = "rr-type-validation")] if let Some(e) = expect_types { replay_args_typecheck(self.types.as_ref(), e)?; } diff --git a/crates/wasmtime/src/runtime/rr/events/mod.rs b/crates/wasmtime/src/runtime/rr/events/mod.rs index 3867945a486a..c6f3a7d09eca 100644 --- a/crates/wasmtime/src/runtime/rr/events/mod.rs +++ b/crates/wasmtime/src/runtime/rr/events/mod.rs @@ -99,6 +99,7 @@ where /// Typechecking validation for replay, if `src_types` exist /// /// Returns [`ReplayError::FailedFuncValidation`] if typechecking fails +#[cfg(feature = "rr-type-validation")] fn replay_args_typecheck(src_types: Option, expect_types: T) -> Result<(), ReplayError> where T: PartialEq, diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index 9b25c5e7ae6b..7ed0dfd00e9b 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -386,7 +386,6 @@ impl Replayer for ReplayBuffer { mod tests { use super::*; use crate::ValRaw; - use core::mem::MaybeUninit; use std::path::Path; use tempfile::{NamedTempFile, TempPath}; @@ -402,14 +401,15 @@ mod tests { }, }; - let values = vec![ValRaw::i32(1), ValRaw::f32(2), ValRaw::i64(3)] - .into_iter() - .map(|x| MaybeUninit::new(x)) - .collect::>(); + let values = vec![ValRaw::i32(1), ValRaw::f32(2), ValRaw::i64(3)]; // Record values let mut recorder = RecordBuffer::new_recorder(record_cfg)?; - let event = core_wasm::HostFuncEntryEvent::new(values.as_slice(), None); + let event = component_wasm::HostFuncReturnEvent::new( + values.as_slice(), + #[cfg(feature = "rr-type-validation")] + None, + ); recorder.record_event(|_| event.clone()); recorder.flush_to_file()?; @@ -424,7 +424,7 @@ mod tests { metadata: ReplayMetadata { validate: true }, }; let mut replayer = ReplayBuffer::new_replayer(replay_cfg)?; - replayer.next_event_and(|store_event: core_wasm::HostFuncEntryEvent, _| { + replayer.next_event_and(|store_event: component_wasm::HostFuncReturnEvent, _| { // Check replay matches record assert!(store_event == event); Ok(()) diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index 14956d469f3c..0b0cd58ffbd1 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -1413,6 +1413,7 @@ impl StoreOpaque { /// /// Convenience wrapper around [`Replayer::next_event_if`] #[inline] + #[allow(dead_code)] pub(crate) fn next_replay_event_if(&mut self, pred: P, f: F) -> Result<(), ReplayError> where T: TryFrom, From 0cac3b8d794f8f84351a24fb7e58bb09629b7613 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Fri, 18 Jul 2025 11:04:41 -0700 Subject: [PATCH 37/62] Change all prints to log interface --- crates/wasmtime/src/runtime/rr/mod.rs | 14 ++++++++------ crates/wasmtime/src/runtime/store.rs | 4 +++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index 7ed0dfd00e9b..56493b19f18f 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -200,7 +200,7 @@ pub trait Replayer: Iterator { fn next_event(&mut self) -> Result { let event = self.next().ok_or(ReplayError::EmptyBuffer); if let Ok(e) = &event { - println!("Replay | {}", e); + log::debug!("Replay Event => {}", e); } event } @@ -291,13 +291,15 @@ impl RecordBuffer { impl Recorder for RecordBuffer { fn new_recorder(cfg: RecordConfig) -> Result { - Ok(RecordBuffer { + let mut buf = RecordBuffer { data: RRDataCommon { buf: VecDeque::new(), rw: File::create(cfg.path)?, }, metadata: cfg.metadata, - }) + }; + + Ok(buf) } #[inline] @@ -307,7 +309,7 @@ impl Recorder for RecordBuffer { F: FnOnce(&RecordMetadata) -> T, { let event = f(self.metadata()).into(); - println!("Record | {}", &event); + log::debug!("Recording event => {}", &event); self.push_event(event); } @@ -322,8 +324,8 @@ impl Recorder for RecordBuffer { } data.rw.flush()?; data.buf.clear(); - println!( - "Record flush | File size: {:?} bytes", + log::debug!( + "Flushing record buffer | File size: {:?} bytes", data.rw.metadata()?.len() ); Ok(()) diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index 0b0cd58ffbd1..fea1d8925e83 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -2137,7 +2137,9 @@ at https://bytecodealliance.org/security. pub(crate) fn check_empty_replay_buffer(&mut self) { if let Some(buf) = self.replay_buffer_mut() { if buf.next().is_some() { - println!("Warning: Replay buffer is not emptied!"); + log::warn!( + "Replay buffer is expected to be empty (possibly incorrect execution encountered)" + ); } } } From d0346fbb1c3a92b7d338fb4d08908de3b4e3935d Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Fri, 18 Jul 2025 11:59:06 -0700 Subject: [PATCH 38/62] Remove `ValRawSer` struct abstraction --- crates/wasmtime/src/runtime/rr/events/mod.rs | 69 +++++++++----------- crates/wasmtime/src/runtime/rr/mod.rs | 3 +- 2 files changed, 31 insertions(+), 41 deletions(-) diff --git a/crates/wasmtime/src/runtime/rr/events/mod.rs b/crates/wasmtime/src/runtime/rr/events/mod.rs index c6f3a7d09eca..843635f3254b 100644 --- a/crates/wasmtime/src/runtime/rr/events/mod.rs +++ b/crates/wasmtime/src/runtime/rr/events/mod.rs @@ -30,48 +30,40 @@ impl fmt::Display for EventActionError { impl std::error::Error for EventActionError {} -/// Transmutable byte array used to serialize [`ValRaw`] union -/// -/// Maintaining the exact layout is crucial for zero-copy transmutations -/// between [`ValRawBytes`] and [`ValRaw`] as currently assumed. However, -/// in the future, this type could be represented with LEB128s -#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)] -#[repr(C)] -pub(super) struct ValRawBytes([u8; mem::size_of::()]); +type ValRawBytes = [u8; mem::size_of::()]; -impl From for ValRawBytes { - fn from(value: ValRaw) -> Self { - Self(value.as_bytes()) - } +/// Types that can be converted zero-copy to [`ValRawBytes`] for +/// serialization/deserialization in record/replay (since +/// unions are non serializable by `serde`) +/// +/// Essentially [`From`] and [`Into`] but local to the crate +/// to bypass orphan rule for externally defined types +trait ValRawBytesConvertable { + fn to_valraw_bytes(self) -> ValRawBytes; + fn from_valraw_bytes(value: ValRawBytes) -> Self; } -impl From for ValRaw { - fn from(value: ValRawBytes) -> Self { - ValRaw::from_bytes(value.0) +impl ValRawBytesConvertable for ValRaw { + #[inline] + fn to_valraw_bytes(self) -> ValRawBytes { + self.as_bytes() } -} - -impl From> for ValRawBytes { - /// Uninitialized data is assumed, and serialized - fn from(value: MaybeUninit) -> Self { - Self::from(unsafe { value.assume_init() }) + #[inline] + fn from_valraw_bytes(value: ValRawBytes) -> Self { + ValRaw::from_bytes(value) } } -impl From for MaybeUninit { - fn from(value: ValRawBytes) -> Self { - MaybeUninit::new(value.into()) +impl ValRawBytesConvertable for MaybeUninit { + #[inline] + fn to_valraw_bytes(self) -> ValRawBytes { + // Uninitialized data is assumed and serialized, so hence + // may contain some undefined values + unsafe { self.assume_init() }.to_valraw_bytes() } -} - -impl fmt::Debug for ValRawBytes { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let hex_digits_per_byte = 2; - let _ = write!(f, "0x.."); - for b in self.0.iter().rev() { - let _ = write!(f, "{:0width$x}", b, width = hex_digits_per_byte); - } - Ok(()) + #[inline] + fn from_valraw_bytes(value: ValRawBytes) -> Self { + MaybeUninit::new(ValRaw::from_valraw_bytes(value)) } } @@ -80,19 +72,18 @@ type RRFuncArgVals = Vec; /// Construct [`RRFuncArgVals`] from raw value buffer fn func_argvals_from_raw_slice(args: &[T]) -> RRFuncArgVals where - ValRawBytes: From, - T: Copy, + T: ValRawBytesConvertable + Copy, { - args.iter().map(|x| ValRawBytes::from(*x)).collect() + args.iter().map(|x| x.to_valraw_bytes()).collect() } /// Encode [`RRFuncArgVals`] back into raw value buffer fn func_argvals_into_raw_slice(rr_args: RRFuncArgVals, raw_args: &mut [T]) where - ValRawBytes: Into, + T: ValRawBytesConvertable, { for (src, dst) in rr_args.into_iter().zip(raw_args.iter_mut()) { - *dst = src.into(); + *dst = T::from_valraw_bytes(src); } } diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index 56493b19f18f..61fba9730afe 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -291,14 +291,13 @@ impl RecordBuffer { impl Recorder for RecordBuffer { fn new_recorder(cfg: RecordConfig) -> Result { - let mut buf = RecordBuffer { + let buf = RecordBuffer { data: RRDataCommon { buf: VecDeque::new(), rw: File::create(cfg.path)?, }, metadata: cfg.metadata, }; - Ok(buf) } From 81f8bae1833d18dccd8c36e08802fde60098bc92 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Fri, 18 Jul 2025 15:14:50 -0700 Subject: [PATCH 39/62] Add wasmtime version to recorded trace --- crates/wasmtime/src/config.rs | 11 +++++++ crates/wasmtime/src/engine/serialization.rs | 6 +--- crates/wasmtime/src/runtime/rr/mod.rs | 32 ++++++++++++--------- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 931ea4a82423..0e65cfd56f1a 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -100,6 +100,17 @@ 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"), + Self::Custom(c) => c, + Self::None => "", + } + } +} + /// Global configuration options used to create an [`Engine`](crate::Engine) /// and customize its behavior. /// diff --git a/crates/wasmtime/src/engine/serialization.rs b/crates/wasmtime/src/engine/serialization.rs index 741e03a5cc85..1a5d7c21f174 100644 --- a/crates/wasmtime/src/engine/serialization.rs +++ b/crates/wasmtime/src/engine/serialization.rs @@ -127,11 +127,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"), - 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, diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index 61fba9730afe..c4c1b536236d 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -2,7 +2,9 @@ //! //! This feature is currently experimental and hence not optimized. -use crate::config::{RecordConfig, RecordMetadata, ReplayConfig, ReplayMetadata}; +use crate::config::{ + ModuleVersionStrategy, RecordConfig, RecordMetadata, ReplayConfig, ReplayMetadata, +}; use crate::prelude::*; #[allow(unused_imports)] use crate::runtime::Store; @@ -282,23 +284,21 @@ impl RecordBuffer { fn push_event(&mut self, event: RREvent) -> () { self.data.buf.push_back(event) } - - /// Generate indepedent references to data and metadata - fn split(&mut self) -> (&mut RRDataCommon, &RecordMetadata) { - (&mut self.data, &self.metadata) - } } impl Recorder for RecordBuffer { fn new_recorder(cfg: RecordConfig) -> Result { - let buf = RecordBuffer { + let mut file = File::create(cfg.path)?; + // Replay requires the Module version and RecordMetadata configuration + postcard::to_io(ModuleVersionStrategy::WasmtimeVersion.as_str(), &mut file)?; + postcard::to_io(&cfg.metadata, &mut file)?; + Ok(RecordBuffer { data: RRDataCommon { buf: VecDeque::new(), - rw: File::create(cfg.path)?, + rw: file, }, metadata: cfg.metadata, - }; - Ok(buf) + }) } #[inline] @@ -313,9 +313,7 @@ impl Recorder for RecordBuffer { } fn flush_to_file(&mut self) -> Result<()> { - let (data, metadata) = self.split(); - // Replay requires the RecordMetadata configuration - postcard::to_io(metadata, &mut data.rw)?; + let data = &mut self.data; // Seralizing each event independently prevents checking for vector sizes // during deserialization for v in &data.buf { @@ -356,6 +354,14 @@ impl Replayer for ReplayBuffer { fn new_replayer(cfg: ReplayConfig) -> Result { let mut file = File::open(cfg.path)?; let mut events = VecDeque::::new(); + // Ensure module versions match + let mut scratch = [0u8; 12]; + let (version, _) = postcard::from_io::<&str, _>((&mut file, &mut scratch))?; + assert_eq!( + version, + ModuleVersionStrategy::WasmtimeVersion.as_str(), + "Wasmtime version mismatch between engine used for record and replay" + ); // Read till EOF let (trace_metadata, _) = postcard::from_io((&mut file, &mut [0; 0]))?; while file.stream_position()? != file.metadata()?.len() { From fdd8b45a1e753ce81c8d17075bb647c8c941bea7 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Thu, 24 Jul 2025 11:09:01 -0700 Subject: [PATCH 40/62] Initial support for generic RR readers/writers --- crates/cli-flags/src/lib.rs | 27 ++--- crates/wasmtime/src/config.rs | 72 +++++++------- crates/wasmtime/src/runtime/rr/mod.rs | 137 ++++++++++++-------------- crates/wasmtime/src/runtime/store.rs | 30 ++++-- 4 files changed, 135 insertions(+), 131 deletions(-) diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs index c1b9f001772b..3362ff78efa9 100644 --- a/crates/cli-flags/src/lib.rs +++ b/crates/cli-flags/src/lib.rs @@ -6,9 +6,10 @@ use serde::Deserialize; use std::{ fmt, fs, path::{Path, PathBuf}, + sync::Arc, time::Duration, }; -use wasmtime::{Config, RRConfig, RecordMetadata, ReplayMetadata}; +use wasmtime::{Config, RRConfig, RecordConfig, RecordMetadata, ReplayConfig, ReplayMetadata}; pub mod opt; @@ -1043,20 +1044,20 @@ impl CommonOptions { let record = &self.record; let replay = &self.replay; - let rr_cfg = if let Some(path) = &record.path { - Some(RRConfig::record_cfg( - path.clone(), - Some(RecordMetadata { + let rr_cfg = if let Some(path) = record.path.clone() { + Some(RRConfig::from(RecordConfig { + writer_initializer: Arc::new(move || Box::new(fs::File::create(&path).unwrap())), + metadata: RecordMetadata { add_validation: record.validation_metadata.unwrap_or(true), - }), - )) - } else if let Some(path) = &replay.path { - Some(RRConfig::replay_cfg( - path.clone(), - Some(ReplayMetadata { + }, + })) + } else if let Some(path) = replay.path.clone() { + Some(RRConfig::from(ReplayConfig { + reader_initializer: Arc::new(move || Box::new(fs::File::open(&path).unwrap())), + metadata: ReplayMetadata { validate: replay.validate.unwrap_or(true), - }), - )) + }, + })) } else { None }; diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 0e65cfd56f1a..e36ee08453b3 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -4,6 +4,7 @@ use bitflags::Flags; use core::fmt; use core::str::FromStr; use serde::{Deserialize, Serialize}; +use std::io::{Read, Write}; #[cfg(any(feature = "cache", feature = "cranelift", feature = "winch"))] use std::path::Path; use wasmparser::WasmFeatures; @@ -247,13 +248,19 @@ impl Default for RecordMetadata { } } +/// A [`Write`] usable for recording in RR +/// +/// Only constraint is that writer must be Send + Sync +pub trait RecordWriter: Write + Send + Sync {} +impl RecordWriter for T {} + /// Configuration for recording execution -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct RecordConfig { - /// Filesystem path to write record trace - pub(crate) path: String, + /// Closure that generates a writer for recording execution traces + pub writer_initializer: Arc Box + Send + Sync>, /// Associated metadata for configuring the recording strategy - pub(crate) metadata: RecordMetadata, + pub metadata: RecordMetadata, } /// Metadata for specifying replay strategy @@ -269,17 +276,23 @@ impl Default for ReplayMetadata { } } +/// A [`Read`] usable for replaying in RR +/// +/// Only constraint is that reader must be Send + Sync +pub trait ReplayReader: Read + Send + Sync {} +impl ReplayReader for T {} + /// Configuration for replay execution -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct ReplayConfig { - /// Filesystem path to read trace from - pub path: String, + /// Closure that generates a reader for replaying execution traces + pub reader_initializer: Arc Box + Send + Sync>, /// Flag for dynamic validation checks when replaying events pub metadata: ReplayMetadata, } /// Configurations for record/replay (RR) executions -#[derive(Debug, Clone)] +#[derive(Clone)] pub enum RRConfig { /// Record configuration Record(RecordConfig), @@ -287,50 +300,37 @@ pub enum RRConfig { Replay(ReplayConfig), } -impl RRConfig { - /// Construct a record ([`RRConfig::Record`]) configuration. - /// If `metadata` is None, uses [`RecordMetadata::default()`] - pub fn record_cfg(path: String, metadata: Option) -> Self { - Self::Record(RecordConfig { - path, - metadata: metadata.unwrap_or(RecordMetadata::default()), - }) +impl From for RRConfig { + fn from(value: RecordConfig) -> Self { + Self::Record(value) } +} - /// Construct a replay ([`RRConfig::Replay`]) configuration. - /// If `metadata` is None, uses [`ReplayMetadata::default()`] - pub fn replay_cfg(path: String, metadata: Option) -> Self { - Self::Replay(ReplayConfig { - path, - metadata: metadata.unwrap_or(ReplayMetadata::default()), - }) +impl From for RRConfig { + fn from(value: ReplayConfig) -> Self { + Self::Replay(value) } +} - /// Wrap the record config in [`RRConfig::Record`] +impl RRConfig { + /// Obtain the record configuration + /// + /// Return [`None`] if it is not configured pub fn record(&self) -> Option<&RecordConfig> { match self { Self::Record(r) => Some(r), _ => None, } } - /// Extract the record config. Panics if not a [`RRConfig::Record`] - pub fn record_unwrap(&self) -> &RecordConfig { - self.record() - .expect("use of incorrectly initialized record configuration") - } - - /// Wrap the replay config in [`RRConfig::Replay`] + /// Obtain the replay configuration + /// + /// Return [`None`] if it is not configured pub fn replay(&self) -> Option<&ReplayConfig> { match self { Self::Replay(r) => Some(r), _ => None, } } - /// Extract the replay config. Panics if not a [`RRConfig::Replay`] - pub fn replay_unwrap(&self) -> &ReplayConfig { - self.replay() - .expect("use of incorrectly initialized replay configuration") - } } impl Config { diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index c4c1b536236d..2cc2691caa77 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -1,20 +1,17 @@ //! Wasmtime's Record and Replay support //! -//! This feature is currently experimental and hence not optimized. +//! This feature is currently not optimized use crate::config::{ - ModuleVersionStrategy, RecordConfig, RecordMetadata, ReplayConfig, ReplayMetadata, + ModuleVersionStrategy, RecordMetadata, RecordWriter, ReplayMetadata, ReplayReader, }; use crate::prelude::*; -#[allow(unused_imports)] -use crate::runtime::Store; use core::fmt; use postcard; use serde::{Deserialize, Serialize}; use std::collections::VecDeque; -use std::fs::File; -#[allow(unused_imports)] -use std::io::{BufWriter, Seek, Write}; +#[allow(unused_imports, reason = "may be used in the future")] +use std::io::{BufWriter, Read, Seek, Write}; /// Encapsulation of event types comprising an [`RREvent`] sum type pub mod events; @@ -36,6 +33,8 @@ macro_rules! rr_event { /// of parameter/return types) may drop down to one or more [`RREvent`]s #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum RREvent { + /// TOOD: Used temporarily only for eof detection. This is never generated, so remove in future + Eof, $( $(#[doc = $doc])* $variant($event), @@ -45,6 +44,7 @@ macro_rules! rr_event { impl fmt::Display for RREvent { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Self::Eof => write!(f, "Eof event"), $( Self::$variant(e) => write!(f, "{:?}", e), )* @@ -143,8 +143,8 @@ impl From for ReplayError { /// This trait provides the interface for a FIFO recorder pub trait Recorder { - /// Constructs a writer on new buffer - fn new_recorder(cfg: RecordConfig) -> Result + /// Construct a recorder with the writer backend + fn new_recorder(writer: Box, metadata: RecordMetadata) -> Result where Self: Sized; @@ -154,10 +154,10 @@ pub trait Recorder { T: Into, F: FnOnce(&RecordMetadata) -> T; - /// Flush memory contents to underlying persistent storage + /// Flush memory contents to underlying persistent storage writer /// /// Buffer should be emptied during this process - fn flush_to_file(&mut self) -> Result<()>; + fn flush(&mut self) -> Result<()>; /// Get metadata associated with the recording process fn metadata(&self) -> &RecordMetadata; @@ -181,7 +181,7 @@ pub trait Recorder { /// essentially operates as an iterator over the recorded events pub trait Replayer: Iterator { /// Constructs a reader on buffer - fn new_replayer(cfg: ReplayConfig) -> Result + fn new_replayer(reader: Box, metadata: ReplayMetadata) -> Result where Self: Sized; @@ -260,44 +260,32 @@ pub trait Replayer: Iterator { /// The underlying serialized/deserialized type type RRBufferData = VecDeque; -/// Common data for recorders and replayers -/// -/// Flexibility of this struct can also be improved with: -/// * Support for generic writers beyond [File] (will require a generic on [Store]) -#[derive(Debug)] -pub struct RRDataCommon { - /// Ordered list of record/replay events - buf: RRBufferData, - /// Persistent storage-backed handle - rw: File, -} - -#[derive(Debug)] /// Buffer to write recording data pub struct RecordBuffer { - data: RRDataCommon, + /// In-memory buffer with ordered list of record/replay events (fastest) + buf: RRBufferData, + /// Writer to persistent storage backing (typically slower) + writer: Box, + /// Metadata for record configuration metadata: RecordMetadata, } impl RecordBuffer { /// Push a newly record event [`RREvent`] to the buffer fn push_event(&mut self, event: RREvent) -> () { - self.data.buf.push_back(event) + self.buf.push_back(event) } } impl Recorder for RecordBuffer { - fn new_recorder(cfg: RecordConfig) -> Result { - let mut file = File::create(cfg.path)?; + fn new_recorder(mut writer: Box, metadata: RecordMetadata) -> Result { // Replay requires the Module version and RecordMetadata configuration - postcard::to_io(ModuleVersionStrategy::WasmtimeVersion.as_str(), &mut file)?; - postcard::to_io(&cfg.metadata, &mut file)?; + postcard::to_io(ModuleVersionStrategy::WasmtimeVersion.as_str(), &mut writer)?; + postcard::to_io(&metadata, &mut writer)?; Ok(RecordBuffer { - data: RRDataCommon { - buf: VecDeque::new(), - rw: file, - }, - metadata: cfg.metadata, + buf: VecDeque::new(), + writer: writer, + metadata: metadata, }) } @@ -312,19 +300,16 @@ impl Recorder for RecordBuffer { self.push_event(event); } - fn flush_to_file(&mut self) -> Result<()> { - let data = &mut self.data; + fn flush(&mut self) -> Result<()> { + log::debug!("Flushing record buffer..."); // Seralizing each event independently prevents checking for vector sizes // during deserialization - for v in &data.buf { - postcard::to_io(&v, &mut data.rw)?; + for v in &self.buf { + postcard::to_io(v, &mut self.writer)?; } - data.rw.flush()?; - data.buf.clear(); - log::debug!( - "Flushing record buffer | File size: {:?} bytes", - data.rw.metadata()?.len() - ); + self.writer.flush()?; + self.buf.clear(); + log::debug!("Record buffer flush complete"); Ok(()) } @@ -334,11 +319,16 @@ impl Recorder for RecordBuffer { } } -#[derive(Debug)] /// Buffer to read replay data pub struct ReplayBuffer { - data: RRDataCommon, + /// In-memory buffer with ordered list of record/replay events (fastest) + buf: RRBufferData, + /// Reader from persistent storage backing (typically slower) + #[allow(dead_code, reason = "expected to be used in the future")] + reader: Box, + /// Metadata for replay configuration metadata: ReplayMetadata, + /// Metadata for record configuration (encoded in the trace) trace_metadata: RecordMetadata, } @@ -346,34 +336,35 @@ impl Iterator for ReplayBuffer { type Item = RREvent; fn next(&mut self) -> Option { - self.data.buf.pop_front() + self.buf.pop_front() } } impl Replayer for ReplayBuffer { - fn new_replayer(cfg: ReplayConfig) -> Result { - let mut file = File::open(cfg.path)?; - let mut events = VecDeque::::new(); + fn new_replayer(mut reader: Box, metadata: ReplayMetadata) -> Result { // Ensure module versions match let mut scratch = [0u8; 12]; - let (version, _) = postcard::from_io::<&str, _>((&mut file, &mut scratch))?; + let (version, _) = postcard::from_io::<&str, _>((&mut reader, &mut scratch))?; assert_eq!( version, ModuleVersionStrategy::WasmtimeVersion.as_str(), "Wasmtime version mismatch between engine used for record and replay" ); + let (trace_metadata, _) = postcard::from_io((&mut reader, &mut [0; 0]))?; + let mut events = VecDeque::::new(); + // Read till EOF - let (trace_metadata, _) = postcard::from_io((&mut file, &mut [0; 0]))?; - while file.stream_position()? != file.metadata()?.len() { - let (event, _): (RREvent, _) = postcard::from_io((&mut file, &mut [0; 0]))?; + 'event_loop: loop { + let (event, _) = postcard::from_io((&mut reader, &mut [0; 0]))?; + if let RREvent::Eof = event { + break 'event_loop; + } events.push_back(event); } Ok(ReplayBuffer { - data: RRDataCommon { - buf: events, - rw: file, - }, - metadata: cfg.metadata, + buf: events, + reader: reader, + metadata: metadata, trace_metadata: trace_metadata, }) } @@ -393,44 +384,40 @@ impl Replayer for ReplayBuffer { mod tests { use super::*; use crate::ValRaw; + use std::fs::File; use std::path::Path; use tempfile::{NamedTempFile, TempPath}; #[test] fn rr_buffers() -> Result<()> { + let record_metadata = RecordMetadata { + add_validation: true, + }; let tmp = NamedTempFile::new()?; - let tmppath = tmp.path().to_str().expect("Filename should be UTF-8"); - let record_cfg = RecordConfig { - path: String::from(tmppath), - metadata: RecordMetadata { - add_validation: true, - }, - }; let values = vec![ValRaw::i32(1), ValRaw::f32(2), ValRaw::i64(3)]; // Record values - let mut recorder = RecordBuffer::new_recorder(record_cfg)?; + let mut recorder = + RecordBuffer::new_recorder(Box::new(File::create(tmppath)?), record_metadata)?; let event = component_wasm::HostFuncReturnEvent::new( values.as_slice(), #[cfg(feature = "rr-type-validation")] None, ); recorder.record_event(|_| event.clone()); - recorder.flush_to_file()?; + recorder.flush()?; let tmp = tmp.into_temp_path(); let tmppath = >::as_ref(&tmp) .to_str() .expect("Filename should be UTF-8"); + let replay_metadata = ReplayMetadata { validate: true }; // Assert that replayed values are identical - let replay_cfg = ReplayConfig { - path: String::from(tmppath), - metadata: ReplayMetadata { validate: true }, - }; - let mut replayer = ReplayBuffer::new_replayer(replay_cfg)?; + let mut replayer = + ReplayBuffer::new_replayer(Box::new(File::open(tmppath)?), replay_metadata)?; replayer.next_event_and(|store_event: component_wasm::HostFuncReturnEvent, _| { // Check replay matches record assert!(store_event == event); diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index fea1d8925e83..d1c6f20556bb 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -537,6 +537,8 @@ impl Store { let pkey = engine.allocator().next_available_pkey(); + let rr = engine.rr(); + let inner = StoreOpaque { _marker: marker::PhantomPinned, engine: engine.clone(), @@ -589,13 +591,27 @@ impl Store { debug_assert!(engine.target().is_pulley()); Executor::Interpreter(Interpreter::new(engine)) }, - record_buffer: engine.rr().and_then(|rr| { - rr.record() - .and_then(|record| Some(RecordBuffer::new_recorder(record.clone()).unwrap())) + record_buffer: rr.and_then(|v| { + v.record().and_then(|record| { + Some( + RecordBuffer::new_recorder( + (record.writer_initializer)(), + record.metadata.clone(), + ) + .unwrap(), + ) + }) }), - replay_buffer: engine.rr().and_then(|rr| { - rr.replay() - .and_then(|replay| Some(ReplayBuffer::new_replayer(replay.clone()).unwrap())) + replay_buffer: rr.and_then(|v| { + v.replay().and_then(|replay| { + Some( + ReplayBuffer::new_replayer( + (replay.reader_initializer)(), + replay.metadata.clone(), + ) + .unwrap(), + ) + }) }), }; let mut inner = Box::new(StoreInner { @@ -2128,7 +2144,7 @@ at https://bytecodealliance.org/security. /// This operation empties the buffer pub(crate) fn flush_record_buffer(&mut self) -> Result<()> { if let Some(buf) = self.record_buffer_mut() { - return Ok(buf.flush_to_file()?); + return Ok(buf.flush()?); } Ok(()) } From 65118cf2b0fe34539acd07290fcc148285ca0186 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Thu, 24 Jul 2025 11:39:12 -0700 Subject: [PATCH 41/62] Move RR buffer sanity checks into `Drop` implementations --- crates/wasmtime/src/runtime/rr/mod.rs | 19 +++++++++++++++++++ crates/wasmtime/src/runtime/store.rs | 24 ------------------------ 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index 2cc2691caa77..c6cac78b8f76 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -277,6 +277,15 @@ impl RecordBuffer { } } +impl Drop for RecordBuffer { + /// Flush all the data to backing writer on drop + fn drop(&mut self) { + if let Err(e) = self.flush() { + log::error!("Cannot flush record buffer: {}", e); + } + } +} + impl Recorder for RecordBuffer { fn new_recorder(mut writer: Box, metadata: RecordMetadata) -> Result { // Replay requires the Module version and RecordMetadata configuration @@ -340,6 +349,16 @@ impl Iterator for ReplayBuffer { } } +impl Drop for ReplayBuffer { + fn drop(&mut self) { + if self.next().is_some() { + log::warn!( + "Replay buffer is dropped without being consumed completely... this may be an incorrect execution" + ); + } + } +} + impl Replayer for ReplayBuffer { fn new_replayer(mut reader: Box, metadata: ReplayMetadata) -> Result { // Ensure module versions match diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index d1c6f20556bb..c15d213fc179 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -2138,27 +2138,6 @@ at https://bytecodealliance.org/security. let instance_id = vm::Instance::from_vmctx(vmctx, |i| i.id()); StoreInstanceId::new(self.id(), instance_id) } - - /// Flush the record buffer to the disk-backed storage - /// - /// This operation empties the buffer - pub(crate) fn flush_record_buffer(&mut self) -> Result<()> { - if let Some(buf) = self.record_buffer_mut() { - return Ok(buf.flush()?); - } - Ok(()) - } - - /// Panics if the replay buffer in the store is non-empty - pub(crate) fn check_empty_replay_buffer(&mut self) { - if let Some(buf) = self.replay_buffer_mut() { - if buf.next().is_some() { - log::warn!( - "Replay buffer is expected to be empty (possibly incorrect execution encountered)" - ); - } - } - } } /// Helper parameter to [`StoreOpaque::allocate_instance`]. @@ -2488,9 +2467,6 @@ impl Drop for StoreOpaque { } } } - - let _ = self.flush_record_buffer().unwrap(); - self.check_empty_replay_buffer(); } } From ceadca3334eecde6e36aa204760f0b75790c0d40 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Thu, 24 Jul 2025 16:18:02 -0700 Subject: [PATCH 42/62] Added `InstantiationEvent` for checksumming components; also fix replay lowering spurious event handling --- Cargo.lock | 1 + crates/environ/src/component/artifacts.rs | 2 ++ crates/wasmtime/Cargo.toml | 1 + crates/wasmtime/src/compile.rs | 2 ++ crates/wasmtime/src/config.rs | 6 +++++ .../src/runtime/component/component.rs | 9 +++++++ .../src/runtime/component/func/options.rs | 4 ++- .../src/runtime/component/instance.rs | 11 ++++++++ .../src/runtime/rr/events/component_wasm.rs | 26 ++++++++++++++++++- crates/wasmtime/src/runtime/rr/mod.rs | 9 +++++-- 10 files changed, 67 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bb416f937c2a..64d294005e93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4174,6 +4174,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "sha2", "smallvec", "target-lexicon", "tempfile", diff --git a/crates/environ/src/component/artifacts.rs b/crates/environ/src/component/artifacts.rs index 096ea839bea0..f32cf95e5861 100644 --- a/crates/environ/src/component/artifacts.rs +++ b/crates/environ/src/component/artifacts.rs @@ -18,6 +18,8 @@ pub struct ComponentArtifacts { pub types: ComponentTypes, /// Serialized metadata about all included core wasm modules. pub static_modules: PrimaryMap, + /// A SHA-256 checksum of the source Wasm binary from which the component was compiled + pub checksum: [u8; 32], } /// Runtime state that a component retains to support its operation. diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml index 70bc0ea076a1..580f25963eb3 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -62,6 +62,7 @@ smallvec = { workspace = true, optional = true } hashbrown = { workspace = true, features = ["default-hasher"] } bitflags = { workspace = true } futures = { workspace = true, features = ["alloc"], optional = true } +sha2 = "0.10.2" [target.'cfg(target_os = "windows")'.dependencies.windows-sys] workspace = true diff --git a/crates/wasmtime/src/compile.rs b/crates/wasmtime/src/compile.rs index 5c35ad3caec1..ed020e1b8c40 100644 --- a/crates/wasmtime/src/compile.rs +++ b/crates/wasmtime/src/compile.rs @@ -26,6 +26,7 @@ use crate::Engine; use crate::hash_map::HashMap; use crate::hash_set::HashSet; use crate::prelude::*; +use sha2::{Digest, Sha256}; use std::{ any::Any, borrow::Cow, @@ -198,6 +199,7 @@ pub(crate) fn build_component_artifacts( ty, types, static_modules: compilation_artifacts.modules, + checksum: Sha256::digest(binary).into(), }; object.serialize_info(&artifacts); diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index e36ee08453b3..2890801ad2f4 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -255,6 +255,9 @@ pub trait RecordWriter: Write + Send + Sync {} impl RecordWriter for T {} /// Configuration for recording execution +/// +/// ## Notes +/// The writers are buffered internally as needed, so avoid using buffered writers as intializers #[derive(Clone)] pub struct RecordConfig { /// Closure that generates a writer for recording execution traces @@ -283,6 +286,9 @@ pub trait ReplayReader: Read + Send + Sync {} impl ReplayReader for T {} /// Configuration for replay execution +/// +/// ## Notes +/// The readers are buffered internally as needed, so avoid using buffered readers as intializers #[derive(Clone)] pub struct ReplayConfig { /// Closure that generates a reader for replaying execution traces diff --git a/crates/wasmtime/src/runtime/component/component.rs b/crates/wasmtime/src/runtime/component/component.rs index 9c44a03ad603..66cc1a47c5f2 100644 --- a/crates/wasmtime/src/runtime/component/component.rs +++ b/crates/wasmtime/src/runtime/component/component.rs @@ -92,6 +92,9 @@ struct ComponentInner { /// `realloc`, to avoid the need to look up types in the registry and take /// locks when calling `realloc` via `TypedFunc::call_raw`. realloc_func_type: Arc, + + /// The SHA-256 checksum of the source binary + checksum: [u8; 32], } pub(crate) struct AllCallFuncPointers { @@ -402,6 +405,7 @@ impl Component { info, mut types, mut static_modules, + checksum, } = match artifacts { Some(artifacts) => artifacts, None => postcard::from_bytes(code_memory.wasmtime_info())?, @@ -452,6 +456,7 @@ impl Component { code, info, realloc_func_type, + checksum, }), }) } @@ -828,6 +833,10 @@ impl Component { &self.inner.realloc_func_type } + pub(crate) fn checksum(&self) -> &[u8; 32] { + &self.inner.checksum + } + /// Returns the `Export::LiftedFunction` metadata associated with `export`. /// /// # Panics diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index bdb436edea2b..d6bf1cac101d 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -597,7 +597,9 @@ impl<'a, T: 'static> LowerContext<'a, T> { RREvent::ComponentHostFuncEntry(_) => { bail!("Cannot call back into host during lowering") } - _ => {} + _ => { + bail!("Invalid event \'{:?}\' encountered during lowering", event); + } }; } Ok(()) diff --git a/crates/wasmtime/src/runtime/component/instance.rs b/crates/wasmtime/src/runtime/component/instance.rs index 878ece823490..83bc2c2931df 100644 --- a/crates/wasmtime/src/runtime/component/instance.rs +++ b/crates/wasmtime/src/runtime/component/instance.rs @@ -8,6 +8,7 @@ use crate::component::{ use crate::instance::OwnedImports; use crate::linker::DefinitionType; use crate::prelude::*; +use crate::rr::events::component_wasm::InstantiationEvent; use crate::runtime::vm::VMFuncRef; use crate::runtime::vm::component::{ComponentInstance, OwnedComponentInstance}; use crate::store::StoreOpaque; @@ -845,6 +846,16 @@ impl InstancePre { fn instantiate_impl(&self, mut store: impl AsContextMut) -> Result { let mut store = store.as_context_mut(); + { + store + .0 + .record_event(|_| InstantiationEvent::from_component(&self.component)); + store + .0 + .next_replay_event_and(|event: InstantiationEvent, _| { + event.validate(&self.component) + })?; + } store .engine() .allocator() diff --git a/crates/wasmtime/src/runtime/rr/events/component_wasm.rs b/crates/wasmtime/src/runtime/rr/events/component_wasm.rs index f295e31fb049..d25ddd55b1b3 100644 --- a/crates/wasmtime/src/runtime/rr/events/component_wasm.rs +++ b/crates/wasmtime/src/runtime/rr/events/component_wasm.rs @@ -1,12 +1,36 @@ //! Module comprising of component model wasm events use super::*; #[allow(unused_imports)] -use crate::component::ComponentType; +use crate::component::{Component, ComponentType}; use std::vec::Vec; use wasmtime_environ::component::InterfaceType; #[cfg(feature = "rr-type-validation")] use wasmtime_environ::component::TypeTuple; +/// A [`Component`] instantiatation event +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct InstantiationEvent { + /// A checksum of the component bytecode + checksum: [u8; 32], +} + +impl InstantiationEvent { + pub fn from_component(component: &Component) -> Self { + Self { + checksum: *component.checksum(), + } + } + + /// Validate that checksums match + pub fn validate(self, component: &Component) -> Result<(), ReplayError> { + if self.checksum != *component.checksum() { + Err(ReplayError::FailedModuleValidation) + } else { + Ok(()) + } + } +} + /// A call event from a Wasm component into the host #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct HostFuncEntryEvent { diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index c6cac78b8f76..86d76a496e99 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -11,7 +11,7 @@ use postcard; use serde::{Deserialize, Serialize}; use std::collections::VecDeque; #[allow(unused_imports, reason = "may be used in the future")] -use std::io::{BufWriter, Read, Seek, Write}; +use std::io::{BufReader, BufWriter, Read, Seek, Write}; /// Encapsulation of event types comprising an [`RREvent`] sum type pub mod events; @@ -81,6 +81,8 @@ rr_event! { // REQUIRED events for replay // + /// Instantiation of a component + ComponentInstantiation(component_wasm::InstantiationEvent), /// Return from host function to component ComponentHostFuncReturn(component_wasm::HostFuncReturnEvent), /// Component ABI realloc call in linear wasm memory @@ -109,8 +111,8 @@ rr_event! { pub enum ReplayError { EmptyBuffer, FailedFuncValidation, + FailedModuleValidation, IncorrectEventVariant, - EventActionError(EventActionError), } @@ -123,6 +125,9 @@ impl fmt::Display for ReplayError { Self::FailedFuncValidation => { write!(f, "func replay event validation failed") } + Self::FailedModuleValidation => { + write!(f, "module load replay event validation failed") + } Self::IncorrectEventVariant => { write!(f, "event method invoked on incorrect variant") } From 6cc4147e1b3a92b44f444e1cb5b404f951e0b473 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Thu, 24 Jul 2025 17:48:02 -0700 Subject: [PATCH 43/62] Added buffered reader/writers for RR --- .../src/runtime/component/func/host.rs | 8 +- .../src/runtime/component/func/options.rs | 10 +- .../src/runtime/component/instance.rs | 2 +- crates/wasmtime/src/runtime/func.rs | 4 +- crates/wasmtime/src/runtime/rr/mod.rs | 111 +++++++----------- crates/wasmtime/src/runtime/store.rs | 12 +- 6 files changed, 65 insertions(+), 82 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 31c18e104c86..a6df9dd90930 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -38,7 +38,7 @@ macro_rules! rr_host_func_entry_event { Some($param_types), ) }, - ); + )?; $store.next_replay_event_if( |_, r| r.add_validation, |_event: HostFuncEntryEvent, _r: &ReplayMetadata| { @@ -62,7 +62,7 @@ macro_rules! record_host_func_return_event { #[cfg(feature = "rr-type-validation")] _r.add_validation.then_some($return_types), ) - }); + })?; }}; } @@ -70,7 +70,7 @@ macro_rules! record_host_func_return_event { macro_rules! record_lower_store_event_wrapper { { $lower_store:expr => $store:expr } => {{ let store_result = $lower_store; - $store.record_event(|_| LowerStoreReturnEvent::new(&store_result)); + $store.record_event(|_| LowerStoreReturnEvent::new(&store_result))?; store_result }}; } @@ -79,7 +79,7 @@ macro_rules! record_lower_store_event_wrapper { macro_rules! record_lower_event_wrapper { { $lower:expr => $store:expr } => {{ let lower_result = $lower; - $store.record_event(|_| LowerReturnEvent::new(&lower_result)); + $store.record_event(|_| LowerReturnEvent::new(&lower_result))?; lower_result }}; } diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index d6bf1cac101d..3ec6245337d0 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -46,7 +46,8 @@ impl Drop for MemorySliceCell<'_> { /// Drop serves as a recording hook for stores to the memory slice fn drop(&mut self) { if let Some(buf) = &mut self.recorder { - buf.record_event(|_| MemorySliceWriteEvent::new(self.offset, self.bytes.to_vec())); + buf.record_event(|_| MemorySliceWriteEvent::new(self.offset, self.bytes.to_vec())) + .unwrap(); } } } @@ -102,7 +103,8 @@ impl<'a, const N: usize> Drop for ConstMemorySliceCell<'a, N> { buf.record_event_if( |_| true, |_| MemorySliceWriteEvent::new(self.offset, self.bytes.to_vec()), - ); + ) + .unwrap(); } } } @@ -405,11 +407,11 @@ impl<'a, T: 'static> LowerContext<'a, T> { ) -> Result { self.store .0 - .record_event(|_| ReallocEntryEvent::new(old, old_size, old_align, new_size)); + .record_event(|_| ReallocEntryEvent::new(old, old_size, old_align, new_size))?; let result = self.realloc_inner(old, old_size, old_align, new_size); self.store .0 - .record_event_if(|r| r.add_validation, |_| ReallocReturnEvent::new(&result)); + .record_event_if(|r| r.add_validation, |_| ReallocReturnEvent::new(&result))?; result } diff --git a/crates/wasmtime/src/runtime/component/instance.rs b/crates/wasmtime/src/runtime/component/instance.rs index 83bc2c2931df..f6d2ab48de80 100644 --- a/crates/wasmtime/src/runtime/component/instance.rs +++ b/crates/wasmtime/src/runtime/component/instance.rs @@ -849,7 +849,7 @@ impl InstancePre { { store .0 - .record_event(|_| InstantiationEvent::from_component(&self.component)); + .record_event(|_| InstantiationEvent::from_component(&self.component))?; store .0 .next_replay_event_and(|event: InstantiationEvent, _| { diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 7efb99199b2e..b04f74ec20d1 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -2376,7 +2376,7 @@ impl HostContext { Some(wasm_func_type.clone()), ) }, - ); + )?; store.next_replay_event_if( |_, r| r.add_validation, |_event: HostFuncEntryEvent, _r: &ReplayMetadata| { @@ -2442,7 +2442,7 @@ impl HostContext { #[cfg(feature = "rr-type-validation")] _rmeta.add_validation.then_some(wasm_func_type.clone()), ) - }); + })?; Ok(ret) }; diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index 86d76a496e99..5a33e4f579d0 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -9,9 +9,7 @@ use crate::prelude::*; use core::fmt; use postcard; use serde::{Deserialize, Serialize}; -use std::collections::VecDeque; -#[allow(unused_imports, reason = "may be used in the future")] -use std::io::{BufReader, BufWriter, Read, Seek, Write}; +use std::io::{BufRead, BufReader, BufWriter, Write}; /// Encapsulation of event types comprising an [`RREvent`] sum type pub mod events; @@ -154,14 +152,19 @@ pub trait Recorder { Self: Sized; /// Record the event generated by `f` - fn record_event(&mut self, f: F) + /// + /// ## Error + /// + /// Propogates from underlying writer + fn record_event(&mut self, f: F) -> Result<()> where T: Into, F: FnOnce(&RecordMetadata) -> T; - /// Flush memory contents to underlying persistent storage writer + /// Trigger an explicit flush of buffer to (persistent) storage /// /// Buffer should be emptied during this process + #[allow(dead_code)] fn flush(&mut self) -> Result<()>; /// Get metadata associated with the recording process @@ -170,15 +173,16 @@ pub trait Recorder { // Provided methods /// Conditionally [`record_event`](Self::record_event) when `pred` is true - fn record_event_if(&mut self, pred: P, f: F) + fn record_event_if(&mut self, pred: P, f: F) -> Result<()> where T: Into, P: FnOnce(&RecordMetadata) -> bool, F: FnOnce(&RecordMetadata) -> T, { if pred(self.metadata()) { - self.record_event(f); + self.record_event(f)?; } + Ok(()) } } @@ -262,69 +266,46 @@ pub trait Replayer: Iterator { } } -/// The underlying serialized/deserialized type -type RRBufferData = VecDeque; - /// Buffer to write recording data pub struct RecordBuffer { - /// In-memory buffer with ordered list of record/replay events (fastest) - buf: RRBufferData, - /// Writer to persistent storage backing (typically slower) - writer: Box, + /// Buffered writer over recording trace writer + buf: BufWriter>, /// Metadata for record configuration metadata: RecordMetadata, } impl RecordBuffer { /// Push a newly record event [`RREvent`] to the buffer - fn push_event(&mut self, event: RREvent) -> () { - self.buf.push_back(event) - } -} - -impl Drop for RecordBuffer { - /// Flush all the data to backing writer on drop - fn drop(&mut self) { - if let Err(e) = self.flush() { - log::error!("Cannot flush record buffer: {}", e); - } + fn push_event(&mut self, event: RREvent) -> Result<()> { + let _ = postcard::to_io(&event, &mut self.buf)?; + Ok(()) } } impl Recorder for RecordBuffer { - fn new_recorder(mut writer: Box, metadata: RecordMetadata) -> Result { + fn new_recorder(writer: Box, metadata: RecordMetadata) -> Result { + let mut buf = BufWriter::new(writer); // Replay requires the Module version and RecordMetadata configuration - postcard::to_io(ModuleVersionStrategy::WasmtimeVersion.as_str(), &mut writer)?; - postcard::to_io(&metadata, &mut writer)?; - Ok(RecordBuffer { - buf: VecDeque::new(), - writer: writer, - metadata: metadata, - }) + postcard::to_io(ModuleVersionStrategy::WasmtimeVersion.as_str(), &mut buf)?; + postcard::to_io(&metadata, &mut buf)?; + Ok(RecordBuffer { buf, metadata }) } #[inline] - fn record_event(&mut self, f: F) + fn record_event(&mut self, f: F) -> Result<()> where T: Into, F: FnOnce(&RecordMetadata) -> T, { let event = f(self.metadata()).into(); log::debug!("Recording event => {}", &event); - self.push_event(event); + self.push_event(event) } + #[allow(dead_code)] fn flush(&mut self) -> Result<()> { - log::debug!("Flushing record buffer..."); - // Seralizing each event independently prevents checking for vector sizes - // during deserialization - for v in &self.buf { - postcard::to_io(v, &mut self.writer)?; - } - self.writer.flush()?; - self.buf.clear(); - log::debug!("Record buffer flush complete"); - Ok(()) + log::debug!("Explicit flushing of record buffer..."); + Ok(self.buf.flush()?) } #[inline] @@ -335,11 +316,8 @@ impl Recorder for RecordBuffer { /// Buffer to read replay data pub struct ReplayBuffer { - /// In-memory buffer with ordered list of record/replay events (fastest) - buf: RRBufferData, - /// Reader from persistent storage backing (typically slower) - #[allow(dead_code, reason = "expected to be used in the future")] - reader: Box, + /// Buffered reader over replay trace reader + buf: BufReader>, /// Metadata for replay configuration metadata: ReplayMetadata, /// Metadata for record configuration (encoded in the trace) @@ -350,7 +328,12 @@ impl Iterator for ReplayBuffer { type Item = RREvent; fn next(&mut self) -> Option { - self.buf.pop_front() + // Check for EoF + if self.buf.fill_buf().unwrap().is_empty() { + None + } else { + Some(postcard::from_io((&mut self.buf, &mut [0; 0])).unwrap().0) + } } } @@ -365,29 +348,23 @@ impl Drop for ReplayBuffer { } impl Replayer for ReplayBuffer { - fn new_replayer(mut reader: Box, metadata: ReplayMetadata) -> Result { + fn new_replayer(reader: Box, metadata: ReplayMetadata) -> Result { + let mut buf = BufReader::new(reader); + // Ensure module versions match let mut scratch = [0u8; 12]; - let (version, _) = postcard::from_io::<&str, _>((&mut reader, &mut scratch))?; + let (version, _) = postcard::from_io::<&str, _>((&mut buf, &mut scratch))?; assert_eq!( version, ModuleVersionStrategy::WasmtimeVersion.as_str(), "Wasmtime version mismatch between engine used for record and replay" ); - let (trace_metadata, _) = postcard::from_io((&mut reader, &mut [0; 0]))?; - let mut events = VecDeque::::new(); - - // Read till EOF - 'event_loop: loop { - let (event, _) = postcard::from_io((&mut reader, &mut [0; 0]))?; - if let RREvent::Eof = event { - break 'event_loop; - } - events.push_back(event); - } + + // Read the recording metadata + let (trace_metadata, _) = postcard::from_io((&mut buf, &mut [0; 0]))?; + Ok(ReplayBuffer { - buf: events, - reader: reader, + buf: buf, metadata: metadata, trace_metadata: trace_metadata, }) @@ -430,7 +407,7 @@ mod tests { #[cfg(feature = "rr-type-validation")] None, ); - recorder.record_event(|_| event.clone()); + recorder.record_event(|_| event.clone())?; recorder.flush()?; let tmp = tmp.into_temp_path(); diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index c15d213fc179..0b2e134d19b3 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -1383,13 +1383,15 @@ impl StoreOpaque { /// /// Convenience wrapper around [`Recorder::record_event`] #[inline] - pub(crate) fn record_event(&mut self, f: F) + pub(crate) fn record_event(&mut self, f: F) -> Result<()> where T: Into, F: FnOnce(&RecordMetadata) -> T, { if let Some(buf) = self.record_buffer_mut() { - buf.record_event(f); + buf.record_event(f) + } else { + Ok(()) } } @@ -1397,14 +1399,16 @@ impl StoreOpaque { /// /// Convenience wrapper around [`Recorder::record_event_if`] #[inline] - pub(crate) fn record_event_if(&mut self, pred: P, f: F) + pub(crate) fn record_event_if(&mut self, pred: P, f: F) -> Result<()> where T: Into, P: FnOnce(&RecordMetadata) -> bool, F: FnOnce(&RecordMetadata) -> T, { if let Some(buf) = self.record_buffer_mut() { - buf.record_event_if(pred, f); + buf.record_event_if(pred, f) + } else { + Ok(()) } } From c15be6e39be73e532ac4d32466c026229fd6b38e Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Fri, 25 Jul 2025 11:08:36 -0700 Subject: [PATCH 44/62] Fix some defaults and docs --- crates/cli-flags/src/lib.rs | 4 ++-- crates/wasmtime/src/runtime/component/func/options.rs | 1 + crates/wasmtime/src/runtime/rr/mod.rs | 8 ++++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs index 3362ff78efa9..7b5107618821 100644 --- a/crates/cli-flags/src/lib.rs +++ b/crates/cli-flags/src/lib.rs @@ -1048,14 +1048,14 @@ impl CommonOptions { Some(RRConfig::from(RecordConfig { writer_initializer: Arc::new(move || Box::new(fs::File::create(&path).unwrap())), metadata: RecordMetadata { - add_validation: record.validation_metadata.unwrap_or(true), + add_validation: record.validation_metadata.unwrap_or(false), }, })) } else if let Some(path) = replay.path.clone() { Some(RRConfig::from(ReplayConfig { reader_initializer: Arc::new(move || Box::new(fs::File::open(&path).unwrap())), metadata: ReplayMetadata { - validate: replay.validate.unwrap_or(true), + validate: replay.validate.unwrap_or(false), }, })) } else { diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index 3ec6245337d0..4a99d33673cf 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -451,6 +451,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { /// # Panics /// /// Refer to [`get`](Self::get). + #[inline] pub fn get_dyn(&mut self, offset: usize, size: usize) -> MemorySliceCell { let (slice_mut, recorder) = self.as_slice_mut_with_recorder(); MemorySliceCell { diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index 5a33e4f579d0..71ec38587844 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -1,6 +1,10 @@ -//! Wasmtime's Record and Replay support +//! Wasmtime's Record and Replay support. //! -//! This feature is currently not optimized +//! This feature is currently not optimized and under development +//! +//! ## Notes +//! +//! This module does NOT support RR for component builtins yet. use crate::config::{ ModuleVersionStrategy, RecordMetadata, RecordWriter, ReplayMetadata, ReplayReader, From d819287edd52dae3209643f94986735c20c14f5e Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Mon, 28 Jul 2025 13:04:14 -0700 Subject: [PATCH 45/62] Added internal RR event buffering with configurable window size --- crates/cli-flags/src/lib.rs | 23 ++++-- crates/wasmtime/src/config.rs | 5 +- crates/wasmtime/src/runtime/rr/mod.rs | 100 ++++++++++++++++---------- 3 files changed, 87 insertions(+), 41 deletions(-) diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs index 7b5107618821..e10422972853 100644 --- a/crates/cli-flags/src/lib.rs +++ b/crates/cli-flags/src/lib.rs @@ -3,6 +3,7 @@ use anyhow::{Context, Result}; use clap::Parser; use serde::Deserialize; +use std::io::{BufReader, BufWriter}; use std::{ fmt, fs, path::{Path, PathBuf}, @@ -488,6 +489,9 @@ wasmtime_option_group! { /// Include (optional) signatures to facilitate validation checks during replay /// (see `validate` in replay options). 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 { @@ -1045,17 +1049,28 @@ impl CommonOptions { let record = &self.record; let replay = &self.replay; let rr_cfg = if let Some(path) = record.path.clone() { + let default_settings = RecordMetadata::default(); Some(RRConfig::from(RecordConfig { - writer_initializer: Arc::new(move || Box::new(fs::File::create(&path).unwrap())), + writer_initializer: Arc::new(move || { + Box::new(BufWriter::new(fs::File::create(&path).unwrap())) + }), metadata: RecordMetadata { - add_validation: record.validation_metadata.unwrap_or(false), + add_validation: record + .validation_metadata + .unwrap_or(default_settings.add_validation), + event_window_size: record + .event_window_size + .unwrap_or(default_settings.event_window_size), }, })) } else if let Some(path) = replay.path.clone() { + let default_settings = ReplayMetadata::default(); Some(RRConfig::from(ReplayConfig { - reader_initializer: Arc::new(move || Box::new(fs::File::open(&path).unwrap())), + reader_initializer: Arc::new(move || { + Box::new(BufReader::new(fs::File::open(&path).unwrap())) + }), metadata: ReplayMetadata { - validate: replay.validate.unwrap_or(false), + validate: replay.validate.unwrap_or(default_settings.validate), }, })) } else { diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 2890801ad2f4..4d5bf95daa54 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -238,12 +238,15 @@ impl Default for CompilerConfig { pub struct RecordMetadata { /// Flag to include additional signatures for replay validation pub add_validation: bool, + /// Maximum window size of internal event buffer + pub event_window_size: usize, } impl Default for RecordMetadata { fn default() -> Self { Self { - add_validation: true, + add_validation: false, + event_window_size: 16, } } } diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index 71ec38587844..b0bb0917d48c 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -13,7 +13,6 @@ use crate::prelude::*; use core::fmt; use postcard; use serde::{Deserialize, Serialize}; -use std::io::{BufRead, BufReader, BufWriter, Write}; /// Encapsulation of event types comprising an [`RREvent`] sum type pub mod events; @@ -35,7 +34,7 @@ macro_rules! rr_event { /// of parameter/return types) may drop down to one or more [`RREvent`]s #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum RREvent { - /// TOOD: Used temporarily only for eof detection. This is never generated, so remove in future + /// Event signalling the end of a trace Eof, $( $(#[doc = $doc])* @@ -225,8 +224,7 @@ pub trait Replayer: Iterator { /// /// ## Errors /// - /// Returns a `ReplayError::EmptyBuffer` if the buffer is empty or a - /// `ReplayError::IncorrectEventVariant` if it failed to convert type safely + /// See [`next_event_and`](Self::next_event_and) #[inline] fn next_event_typed(&mut self) -> Result where @@ -270,29 +268,46 @@ pub trait Replayer: Iterator { } } -/// Buffer to write recording data +/// Buffer to write recording data. +/// +/// This type can be optimized for [`RREvent`] data configurations. pub struct RecordBuffer { - /// Buffered writer over recording trace writer - buf: BufWriter>, + /// In-memory event buffer to enable windows for coalescing + buf: Vec, + /// Writer to store data into + writer: Box, /// Metadata for record configuration metadata: RecordMetadata, } impl RecordBuffer { - /// Push a newly record event [`RREvent`] to the buffer + /// Push a new record event [`RREvent`] to the buffer fn push_event(&mut self, event: RREvent) -> Result<()> { - let _ = postcard::to_io(&event, &mut self.buf)?; + self.buf.push(event); + if self.buf.len() >= self.metadata().event_window_size { + self.flush()?; + } Ok(()) } } +impl Drop for RecordBuffer { + fn drop(&mut self) { + self.push_event(RREvent::Eof).unwrap(); + self.flush().unwrap(); + } +} + impl Recorder for RecordBuffer { - fn new_recorder(writer: Box, metadata: RecordMetadata) -> Result { - let mut buf = BufWriter::new(writer); + fn new_recorder(mut writer: Box, metadata: RecordMetadata) -> Result { // Replay requires the Module version and RecordMetadata configuration - postcard::to_io(ModuleVersionStrategy::WasmtimeVersion.as_str(), &mut buf)?; - postcard::to_io(&metadata, &mut buf)?; - Ok(RecordBuffer { buf, metadata }) + postcard::to_io(ModuleVersionStrategy::WasmtimeVersion.as_str(), &mut writer)?; + postcard::to_io(&metadata, &mut writer)?; + Ok(RecordBuffer { + buf: Vec::new(), + writer: writer, + metadata: metadata, + }) } #[inline] @@ -306,10 +321,12 @@ impl Recorder for RecordBuffer { self.push_event(event) } - #[allow(dead_code)] fn flush(&mut self) -> Result<()> { - log::debug!("Explicit flushing of record buffer..."); - Ok(self.buf.flush()?) + log::debug!("Flushing record buffer..."); + for e in self.buf.drain(..) { + postcard::to_io(&e, &mut self.writer)?; + } + return Ok(()); } #[inline] @@ -320,8 +337,8 @@ impl Recorder for RecordBuffer { /// Buffer to read replay data pub struct ReplayBuffer { - /// Buffered reader over replay trace reader - buf: BufReader>, + /// Reader to read replay trace from + reader: Box, /// Metadata for replay configuration metadata: ReplayMetadata, /// Metadata for record configuration (encoded in the trace) @@ -333,31 +350,44 @@ impl Iterator for ReplayBuffer { fn next(&mut self) -> Option { // Check for EoF - if self.buf.fill_buf().unwrap().is_empty() { - None - } else { - Some(postcard::from_io((&mut self.buf, &mut [0; 0])).unwrap().0) + let result = postcard::from_io((&mut self.reader, &mut [0; 0])); + match result { + Err(e) => { + log::error!("Erroneous replay read: {}", e); + None + } + Ok((event, _)) => { + if let RREvent::Eof = event { + None + } else { + Some(event) + } + } } } } impl Drop for ReplayBuffer { fn drop(&mut self) { - if self.next().is_some() { - log::warn!( - "Replay buffer is dropped without being consumed completely... this may be an incorrect execution" - ); + if let Some(event) = self.next() { + if let RREvent::Eof = event { + } else { + log::warn!( + "Replay buffer is dropped without being consumed completely... this may be an incorrect execution" + ); + while let Some(e) = self.next() { + log::warn!("Event remaining => {}", e); + } + } } } } impl Replayer for ReplayBuffer { - fn new_replayer(reader: Box, metadata: ReplayMetadata) -> Result { - let mut buf = BufReader::new(reader); - + fn new_replayer(mut reader: Box, metadata: ReplayMetadata) -> Result { // Ensure module versions match let mut scratch = [0u8; 12]; - let (version, _) = postcard::from_io::<&str, _>((&mut buf, &mut scratch))?; + let (version, _) = postcard::from_io::<&str, _>((&mut reader, &mut scratch))?; assert_eq!( version, ModuleVersionStrategy::WasmtimeVersion.as_str(), @@ -365,10 +395,10 @@ impl Replayer for ReplayBuffer { ); // Read the recording metadata - let (trace_metadata, _) = postcard::from_io((&mut buf, &mut [0; 0]))?; + let (trace_metadata, _) = postcard::from_io((&mut reader, &mut [0; 0]))?; Ok(ReplayBuffer { - buf: buf, + reader: reader, metadata: metadata, trace_metadata: trace_metadata, }) @@ -395,9 +425,7 @@ mod tests { #[test] fn rr_buffers() -> Result<()> { - let record_metadata = RecordMetadata { - add_validation: true, - }; + let record_metadata = RecordMetadata::default(); let tmp = NamedTempFile::new()?; let tmppath = tmp.path().to_str().expect("Filename should be UTF-8"); From 4e8e2f843febf70334d1b692dd447c40adaa8e58 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Mon, 28 Jul 2025 13:57:37 -0700 Subject: [PATCH 46/62] Prevent memory export during RR for core wasm --- crates/wasmtime/src/runtime/instance.rs | 16 ++++++++++++++-- crates/wasmtime/src/runtime/rr/mod.rs | 6 ++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/crates/wasmtime/src/runtime/instance.rs b/crates/wasmtime/src/runtime/instance.rs index 4c3346c21d0e..f63432883321 100644 --- a/crates/wasmtime/src/runtime/instance.rs +++ b/crates/wasmtime/src/runtime/instance.rs @@ -7,8 +7,8 @@ use crate::runtime::vm::{ use crate::store::{AllocateInstanceKind, InstanceId, StoreInstanceId, StoreOpaque}; use crate::types::matching; use crate::{ - AsContextMut, Engine, Export, Extern, Func, Global, Memory, Module, ModuleExport, SharedMemory, - StoreContext, StoreContextMut, Table, Tag, TypedFunc, + AsContextMut, Engine, Export, Extern, ExternType, Func, Global, Memory, Module, ModuleExport, + SharedMemory, StoreContext, StoreContextMut, Table, Tag, TypedFunc, }; use alloc::sync::Arc; use core::ptr::NonNull; @@ -923,6 +923,18 @@ fn pre_instantiate_raw( imports.push(&item, store); } + if module.engine().rr().is_some() + && module.exports().any(|export| { + if let ExternType::Memory(_) = export.ty() { + true + } else { + false + } + }) + { + bail!("Cannot enable record/replay for core wasm modules when a memory is exported"); + } + Ok(imports) } diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index b0bb0917d48c..da05d262d860 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -293,6 +293,7 @@ impl RecordBuffer { impl Drop for RecordBuffer { fn drop(&mut self) { + // Insert End of trace delimiter self.push_event(RREvent::Eof).unwrap(); self.flush().unwrap(); } @@ -375,8 +376,9 @@ impl Drop for ReplayBuffer { log::warn!( "Replay buffer is dropped without being consumed completely... this may be an incorrect execution" ); - while let Some(e) = self.next() { - log::warn!("Event remaining => {}", e); + log::warn!("Event remaining => {}", event); + while let Some(rem_event) = self.next() { + log::warn!("Event remaining => {}", rem_event); } } } From 9a056c6ddf7822efd066fb799a3ea00277744b16 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Mon, 28 Jul 2025 16:37:18 -0700 Subject: [PATCH 47/62] Remove dead code attributes --- crates/wasmtime/src/runtime/rr/events/component_wasm.rs | 6 ++---- crates/wasmtime/src/runtime/rr/events/core_wasm.rs | 2 +- crates/wasmtime/src/runtime/rr/mod.rs | 3 +-- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/crates/wasmtime/src/runtime/rr/events/component_wasm.rs b/crates/wasmtime/src/runtime/rr/events/component_wasm.rs index d25ddd55b1b3..d3cb0f58c8cd 100644 --- a/crates/wasmtime/src/runtime/rr/events/component_wasm.rs +++ b/crates/wasmtime/src/runtime/rr/events/component_wasm.rs @@ -1,6 +1,6 @@ //! Module comprising of component model wasm events use super::*; -#[allow(unused_imports)] +#[expect(unused_imports, reason = "used for doc-links")] use crate::component::{Component, ComponentType}; use std::vec::Vec; use wasmtime_environ::component::InterfaceType; @@ -112,7 +112,7 @@ macro_rules! generic_new_result_events { ),* ) => ( $( - $(#[doc= $doc])* + $(#[doc = $doc])* #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct $event { ret: Result<$ok_ty, EventActionError>, @@ -125,7 +125,6 @@ macro_rules! generic_new_result_events { } } #[inline] - #[allow(dead_code)] pub fn ret(self) -> Result<$ok_ty, EventActionError> { self.ret } } )* @@ -154,7 +153,6 @@ macro_rules! generic_new_events { )* $( impl $struct { - #[allow(dead_code)] pub fn new($($field: $field_ty),*) -> Self { Self { $($field),* diff --git a/crates/wasmtime/src/runtime/rr/events/core_wasm.rs b/crates/wasmtime/src/runtime/rr/events/core_wasm.rs index 603945b28e6b..3f5c6ba4e9a3 100644 --- a/crates/wasmtime/src/runtime/rr/events/core_wasm.rs +++ b/crates/wasmtime/src/runtime/rr/events/core_wasm.rs @@ -1,6 +1,6 @@ //! Module comprising of core wasm events use super::*; -#[allow(unused_imports)] +#[expect(unused_imports, reason = "used for doc-links")] use wasmtime_environ::{WasmFuncType, WasmValType}; /// Note: Switch [`CoreFuncArgTypes`] to use [`Vec`] for better efficiency diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index da05d262d860..0ea9a8d2f033 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -164,10 +164,9 @@ pub trait Recorder { T: Into, F: FnOnce(&RecordMetadata) -> T; - /// Trigger an explicit flush of buffer to (persistent) storage + /// Trigger an explicit flush of any buffered data to the writer /// /// Buffer should be emptied during this process - #[allow(dead_code)] fn flush(&mut self) -> Result<()>; /// Get metadata associated with the recording process From 217ddd2795e68ac9884be1123704e4389504d29e Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Mon, 28 Jul 2025 19:59:50 -0700 Subject: [PATCH 48/62] Added `replay` command to CLI; refactor config api --- crates/cli-flags/src/lib.rs | 17 +++--- crates/wasmtime/Cargo.toml | 13 ++--- crates/wasmtime/src/config.rs | 43 ++++++++++----- .../src/runtime/component/func/host.rs | 2 +- crates/wasmtime/src/runtime/func.rs | 2 +- crates/wasmtime/src/runtime/rr/mod.rs | 7 +-- src/bin/wasmtime.rs | 14 ++++- src/commands.rs | 3 ++ src/commands/replay.rs | 54 +++++++++++++++++++ src/commands/run.rs | 8 ++- 10 files changed, 119 insertions(+), 44 deletions(-) create mode 100644 src/commands/replay.rs diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs index e10422972853..bf85ae328b88 100644 --- a/crates/cli-flags/src/lib.rs +++ b/crates/cli-flags/src/lib.rs @@ -10,7 +10,7 @@ use std::{ sync::Arc, time::Duration, }; -use wasmtime::{Config, RRConfig, RecordConfig, RecordMetadata, ReplayConfig, ReplayMetadata}; +use wasmtime::{Config, RecordConfig, RecordMetadata, ReplayConfig, ReplayMetadata}; pub mod opt; @@ -1048,9 +1048,9 @@ impl CommonOptions { let record = &self.record; let replay = &self.replay; - let rr_cfg = if let Some(path) = record.path.clone() { + if let Some(path) = record.path.clone() { let default_settings = RecordMetadata::default(); - Some(RRConfig::from(RecordConfig { + config.enable_record(RecordConfig { writer_initializer: Arc::new(move || { Box::new(BufWriter::new(fs::File::create(&path).unwrap())) }), @@ -1062,21 +1062,18 @@ impl CommonOptions { .event_window_size .unwrap_or(default_settings.event_window_size), }, - })) + }); } else if let Some(path) = replay.path.clone() { let default_settings = ReplayMetadata::default(); - Some(RRConfig::from(ReplayConfig { + config.enable_replay(ReplayConfig { reader_initializer: Arc::new(move || { Box::new(BufReader::new(fs::File::open(&path).unwrap())) }), metadata: ReplayMetadata { validate: replay.validate.unwrap_or(default_settings.validate), }, - })) - } else { - None - }; - config.rr(rr_cfg); + }); + } Ok(config) } diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml index 580f25963eb3..f967c7250318 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -397,17 +397,10 @@ component-model-async = [ # Enables support for record/replay rr = ["rr-component", "rr-core"] - -# Component model RR support +# RR for components rr-component = ["component-model", "std"] - -# Core wasm RR support +# RR for core wasm rr-core = ["std"] - -# Support for type information of recorded events for replay validation +# Support for validation signatures/checks during record/replay respectively rr-type-validation = [] -# Support for input values to recorded events for replay validation. -# -# This can be used to check whether the entire module is truly deterministic -rr-args-validation = [] diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 4d5bf95daa54..411b1e1ac4b3 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -1126,7 +1126,7 @@ impl Config { /// [proposal]: https://github.com/webassembly/relaxed-simd pub fn relaxed_simd_deterministic(&mut self, enable: bool) -> &mut Self { assert!( - enable || !self.check_determinism(), + !(self.check_determinism() && !enable), "Deterministic relaxed SIMD cannot be disabled when record/replay is enabled" ); self.tunables.relaxed_simd_deterministic = Some(enable); @@ -1432,7 +1432,7 @@ impl Config { #[cfg(any(feature = "cranelift", feature = "winch"))] pub fn cranelift_nan_canonicalization(&mut self, enable: bool) -> &mut Self { assert!( - enable || !self.check_determinism(), + !(self.check_determinism() && !enable), "NaN canonicalization cannot be disabled when record/replay is enabled" ); let val = if enable { "true" } else { "false" }; @@ -2770,10 +2770,10 @@ impl Config { self } - /// Repeal determinstic execution configurations (opposite - /// effect of [`Config::enforce_determinism`]) + /// Remove determinstic execution enforcements (if any) applied + /// by [`Config::enforce_determinism`] #[inline] - pub fn repeal_determinism(&mut self) -> &mut Self { + pub fn remove_determinism_enforcement(&mut self) -> &mut Self { self.cranelift_nan_canonicalization(false) .relaxed_simd_deterministic(false); self @@ -2788,18 +2788,33 @@ impl Config { self.rr.is_some() } - /// Configure the record/replay options for use by the runtime + /// Enable execution trace recording with the provided configuration /// /// This method implicitly enforces determinism (see [`Config::enforce_determinism`] /// for details). - pub fn rr(&mut self, rr: Option) -> &mut Self { - // Set appropriate configurations for determinstic execution - if rr.is_some() { - self.enforce_determinism(); - } else if self.rr.is_some() { - self.repeal_determinism(); - } - self.rr = rr; + pub fn enable_record(&mut self, record: RecordConfig) -> &mut Self { + self.enforce_determinism(); + self.rr = Some(RRConfig::from(record)); + self + } + + /// Enable replay execution based on the provided configuration + /// + /// This method implicitly enforces determinism (see [`Config::enforce_determinism`] + /// for details). + pub fn enable_replay(&mut self, replay: ReplayConfig) -> &mut Self { + self.enforce_determinism(); + self.rr = Some(RRConfig::from(replay)); + self + } + + /// Disable the currently active record/replay configuration + /// + /// Note: A common option is used for both record/replay here + /// since record and replay can never be set simultaneously + pub fn disable_record_replay(&mut self) -> &mut Self { + self.remove_determinism_enforcement(); + self.rr = None; self } } diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index a6df9dd90930..03b0bd7a4fa6 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -25,7 +25,7 @@ use wasmtime_environ::component::{ /// Record/replay stubs for host function entry events macro_rules! rr_host_func_entry_event { { $args:expr, $param_types:expr => $store:expr } => { - #[cfg(any(feature = "rr-type-validation", feature = "rr-args-validation"))] + #[cfg(feature = "rr-type-validation")] { use crate::config::ReplayMetadata; use crate::runtime::rr::events::component_wasm::HostFuncEntryEvent; diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index b04f74ec20d1..6ea33420b455 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -2359,7 +2359,7 @@ impl HostContext { // (only when validation is enabled). // Function type unwraps should never panic since they are // lazily evaluated - #[cfg(any(feature = "rr-type-validation", feature = "rr-args-validation"))] + #[cfg(any(feature = "rr-type-validation"))] { use crate::config::ReplayMetadata; use crate::rr::events::core_wasm::HostFuncEntryEvent; diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index 0ea9a8d2f033..fcab631e42f0 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -373,12 +373,9 @@ impl Drop for ReplayBuffer { if let RREvent::Eof = event { } else { log::warn!( - "Replay buffer is dropped without being consumed completely... this may be an incorrect execution" + "Replay buffer is dropped with {} remaining events, and is likely an invalid execution", + self.count() - 1 ); - log::warn!("Event remaining => {}", event); - while let Some(rem_event) = self.next() { - log::warn!("Event remaining => {}", rem_event); - } } } } diff --git a/src/bin/wasmtime.rs b/src/bin/wasmtime.rs index ecafff3aa51c..d91650ac4549 100644 --- a/src/bin/wasmtime.rs +++ b/src/bin/wasmtime.rs @@ -89,6 +89,16 @@ enum Subcommand { /// Inspect `*.cwasm` files output from Wasmtime #[cfg(feature = "objdump")] Objdump(wasmtime_cli::commands::ObjdumpCommand), + + /// Replay execution of a recorded execution trace + /// + /// The options below are the superset of the `run` command. The notable options + /// added for replay are: + /// + /// * `--trace` to specify the recorded trace to replay + /// + /// * Settings for replay operation (e.g. `--validate`) + Replay(wasmtime_cli::commands::ReplayCommand), } impl Wasmtime { @@ -101,7 +111,7 @@ impl Wasmtime { match subcommand { #[cfg(feature = "run")] - Subcommand::Run(c) => c.execute(), + Subcommand::Run(c) => c.execute(None), #[cfg(feature = "cache")] Subcommand::Config(c) => c.execute(), @@ -126,6 +136,8 @@ impl Wasmtime { #[cfg(feature = "objdump")] Subcommand::Objdump(c) => c.execute(), + + Subcommand::Replay(c) => c.execute(), } } } diff --git a/src/commands.rs b/src/commands.rs index 04fd0286ba2c..187c99ef405f 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -39,3 +39,6 @@ pub use self::settings::*; mod objdump; #[cfg(feature = "objdump")] pub use self::objdump::*; + +mod replay; +pub use self::replay::*; diff --git a/src/commands/replay.rs b/src/commands/replay.rs new file mode 100644 index 000000000000..6b6c4a817e29 --- /dev/null +++ b/src/commands/replay.rs @@ -0,0 +1,54 @@ +//! Implementation of the `wasmtime replay` command + +use crate::commands::run::RunCommand; +use anyhow::Result; +use clap::Parser; +use std::{fs, io::BufReader, path::PathBuf, sync::Arc}; +use wasmtime::{ReplayConfig, ReplayMetadata}; + +#[derive(Parser)] +/// Replay-specific options for CLI +pub struct ReplayOptions { + /// The path of the recorded trace + /// + /// Execution traces can be obtained for most modes of Wasmtime execution with -R. + /// See `wasmtime run -R help` for relevant information on recording execution + /// + /// Note: The module used for replay must exactly match that used during recording + #[arg(short, long, required = true, value_name = "RECORDED TRACE")] + trace: PathBuf, + + /// Dynamic checks of record signatures to validate replay consistency. + /// + /// Requires record traces to be generated with `validation_metadata` enabled. + #[arg(short, long, default_value_t = false)] + validate: bool, +} + +/// Execute a deterministic, embedding-agnostic replay of a Wasm modules given its associated recorded trace +#[derive(Parser)] +pub struct ReplayCommand { + #[command(flatten)] + replay_opts: ReplayOptions, + + #[command(flatten)] + run_cmd: RunCommand, +} + +impl ReplayCommand { + /// Executes the command. + pub fn execute(self) -> Result<()> { + let replay_cfg = ReplayConfig { + reader_initializer: Arc::new(move || { + Box::new(BufReader::new( + fs::File::open(&self.replay_opts.trace).unwrap(), + )) + }), + metadata: ReplayMetadata { + validate: self.replay_opts.validate, + }, + }; + // Replay uses the `run` command harness + self.run_cmd.execute(Some(replay_cfg)) + } +} diff --git a/src/commands/run.rs b/src/commands/run.rs index d71a685b72a2..76754ac2440a 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -14,7 +14,7 @@ use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::thread; use wasi_common::sync::{Dir, TcpListener, WasiCtxBuilder, ambient_authority}; -use wasmtime::{Engine, Func, Module, Store, StoreLimits, Val, ValType}; +use wasmtime::{Engine, Func, Module, ReplayConfig, Store, StoreLimits, Val, ValType}; use wasmtime_wasi::p2::{IoView, WasiView}; #[cfg(feature = "wasi-nn")] @@ -89,7 +89,7 @@ enum CliLinker { impl RunCommand { /// Executes the command. - pub fn execute(mut self) -> Result<()> { + pub fn execute(mut self, replay_cfg: Option) -> Result<()> { self.run.common.init_logging()?; let mut config = self.run.common.config(None)?; @@ -109,6 +109,10 @@ impl RunCommand { None => {} } + if let Some(cfg) = replay_cfg { + config.enable_replay(cfg); + } + let engine = Engine::new(&config)?; // Read the wasm module binary either as `*.wat` or a raw binary. From 8d3d374a3ca224ea60276f3f19ac8e2e1a2afeda Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Mon, 28 Jul 2025 20:25:08 -0700 Subject: [PATCH 49/62] fixup! Added `replay` command to CLI; refactor config api --- crates/cli-flags/src/lib.rs | 69 +++-------------------------- crates/wasmtime/src/config.rs | 28 +++++++++--- crates/wasmtime/src/runtime/func.rs | 2 +- src/bin/wasmtime.rs | 12 ++--- src/commands/run.rs | 2 +- 5 files changed, 37 insertions(+), 76 deletions(-) diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs index bf85ae328b88..c8148920a72a 100644 --- a/crates/cli-flags/src/lib.rs +++ b/crates/cli-flags/src/lib.rs @@ -3,14 +3,14 @@ use anyhow::{Context, Result}; use clap::Parser; use serde::Deserialize; -use std::io::{BufReader, BufWriter}; +use std::io::BufWriter; use std::{ fmt, fs, path::{Path, PathBuf}, sync::Arc, time::Duration, }; -use wasmtime::{Config, RecordConfig, RecordMetadata, ReplayConfig, ReplayMetadata}; +use wasmtime::{Config, RecordConfig, RecordMetadata}; pub mod opt; @@ -487,7 +487,7 @@ wasmtime_option_group! { /// Filesystem endpoint to store the recorded execution trace pub path: Option, /// Include (optional) signatures to facilitate validation checks during replay - /// (see `validate` in replay options). + /// (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). @@ -499,22 +499,6 @@ wasmtime_option_group! { } } -wasmtime_option_group! { - #[derive(PartialEq, Clone, Deserialize)] - #[serde(rename_all = "kebab-case", deny_unknown_fields)] - pub struct ReplayOptions { - /// Filesystem endpoint to read the recorded execution trace from - pub path: Option, - /// Dynamic validation checks of record signatures to guarantee faithful replay. - /// Requires record traces to be generated with `validation_metadata` enabled. - pub validate: Option, - } - - enum Replay { - ... - } -} - #[derive(Debug, Clone, PartialEq)] pub struct WasiNnGraph { pub format: String, @@ -569,7 +553,7 @@ pub struct CommonOptions { /// /// Generates of 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 `--replay` option). + /// re-executed in a determinstic, embedding-agnostic manner (see the `wasmtime replay` command). /// /// Note: Minimal configs for deterministic Wasm semantics will be /// enforced during recording by default (NaN canonicalization, deterministic relaxed SIMD) @@ -577,17 +561,6 @@ pub struct CommonOptions { #[serde(skip)] record_raw: Vec>, - /// Options to enable and configure execution replay, `-P help` to see all. - /// - /// Run a determinstic, embedding-agnostic replay execution of the Wasm module - /// according to a prior recorded execution trace (see the `--record` option). - /// - /// Note: Minimal configs for deterministic Wasm semantics will be - /// enforced during replay by default (NaN canonicalization, deterministic relaxed SIMD) - #[arg(short = 'P', long = "replay", value_name = "KEY[=VAL[,..]]")] - #[serde(skip)] - replay_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)] @@ -618,10 +591,6 @@ pub struct CommonOptions { #[serde(rename = "record", default)] pub record: RecordOptions, - #[arg(skip)] - #[serde(rename = "replay", default)] - pub replay: ReplayOptions, - /// The target triple; default is the host triple #[arg(long, value_name = "TARGET")] #[serde(skip)] @@ -669,7 +638,6 @@ impl CommonOptions { wasm_raw: Vec::new(), wasi_raw: Vec::new(), record_raw: Vec::new(), - replay_raw: Vec::new(), configured: true, opts: Default::default(), codegen: Default::default(), @@ -677,7 +645,6 @@ impl CommonOptions { wasm: Default::default(), wasi: Default::default(), record: Default::default(), - replay: Default::default(), target: None, config: None, } @@ -696,7 +663,6 @@ impl CommonOptions { self.wasm = toml_options.wasm; self.wasi = toml_options.wasi; self.record = toml_options.record; - self.replay = toml_options.replay; } self.opts.configure_with(&self.opts_raw); self.codegen.configure_with(&self.codegen_raw); @@ -704,7 +670,6 @@ impl CommonOptions { self.wasm.configure_with(&self.wasm_raw); self.wasi.configure_with(&self.wasi_raw); self.record.configure_with(&self.record_raw); - self.replay.configure_with(&self.replay_raw); Ok(()) } @@ -1047,7 +1012,6 @@ impl CommonOptions { } let record = &self.record; - let replay = &self.replay; if let Some(path) = record.path.clone() { let default_settings = RecordMetadata::default(); config.enable_record(RecordConfig { @@ -1062,17 +1026,7 @@ impl CommonOptions { .event_window_size .unwrap_or(default_settings.event_window_size), }, - }); - } else if let Some(path) = replay.path.clone() { - let default_settings = ReplayMetadata::default(); - config.enable_replay(ReplayConfig { - reader_initializer: Arc::new(move || { - Box::new(BufReader::new(fs::File::open(&path).unwrap())) - }), - metadata: ReplayMetadata { - validate: replay.validate.unwrap_or(default_settings.validate), - }, - }); + })?; } Ok(config) @@ -1180,7 +1134,6 @@ mod tests { [wasm] [wasi] [record] - [replay] "#; let mut common_options: CommonOptions = toml::from_str(basic_toml).unwrap(); common_options.config(None).unwrap(); @@ -1304,8 +1257,6 @@ impl fmt::Display for CommonOptions { wasi, record_raw, record, - replay_raw, - replay, configured, target, config, @@ -1323,7 +1274,6 @@ impl fmt::Display for CommonOptions { let wasm_flags; let debug_flags; let record_flags; - let replay_flags; if *configured { codegen_flags = codegen.to_options(); @@ -1332,7 +1282,6 @@ impl fmt::Display for CommonOptions { wasm_flags = wasm.to_options(); opts_flags = opts.to_options(); record_flags = record.to_options(); - replay_flags = replay.to_options(); } else { codegen_flags = codegen_raw .iter() @@ -1348,11 +1297,6 @@ impl fmt::Display for CommonOptions { .flat_map(|t| t.0.iter()) .cloned() .collect(); - replay_flags = replay_raw - .iter() - .flat_map(|t| t.0.iter()) - .cloned() - .collect(); } for flag in codegen_flags { @@ -1373,9 +1317,6 @@ impl fmt::Display for CommonOptions { for flag in record_flags { write!(f, "-R{flag} ")?; } - for flag in replay_flags { - write!(f, "-P{flag} ")?; - } Ok(()) } diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 411b1e1ac4b3..93f51d3c9a19 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -2792,25 +2792,43 @@ impl Config { /// /// This method implicitly enforces determinism (see [`Config::enforce_determinism`] /// for details). - pub fn enable_record(&mut self, record: RecordConfig) -> &mut Self { + /// + /// ## Errors + /// + /// Errors if record/replay are simultaneously enabled + pub fn enable_record(&mut self, record: RecordConfig) -> Result<&mut Self> { self.enforce_determinism(); + if let Some(cfg) = &self.rr { + if let RRConfig::Replay(_) = cfg { + bail!("Cannot enable recording when replay is already enabled"); + } + } self.rr = Some(RRConfig::from(record)); - self + Ok(self) } /// Enable replay execution based on the provided configuration /// /// This method implicitly enforces determinism (see [`Config::enforce_determinism`] /// for details). - pub fn enable_replay(&mut self, replay: ReplayConfig) -> &mut Self { + /// + /// ## Errors + /// + /// Errors if record/replay are simultaneously enabled + pub fn enable_replay(&mut self, replay: ReplayConfig) -> Result<&mut Self> { self.enforce_determinism(); + if let Some(cfg) = &self.rr { + if let RRConfig::Record(_) = cfg { + bail!("Cannot enable replay when recording is already enabled"); + } + } self.rr = Some(RRConfig::from(replay)); - self + Ok(self) } /// Disable the currently active record/replay configuration /// - /// Note: A common option is used for both record/replay here + /// A common option is used for both record/replay here /// since record and replay can never be set simultaneously pub fn disable_record_replay(&mut self) -> &mut Self { self.remove_determinism_enforcement(); diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 6ea33420b455..d2336b5fd430 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -2359,7 +2359,7 @@ impl HostContext { // (only when validation is enabled). // Function type unwraps should never panic since they are // lazily evaluated - #[cfg(any(feature = "rr-type-validation"))] + #[cfg(feature = "rr-type-validation")] { use crate::config::ReplayMetadata; use crate::rr::events::core_wasm::HostFuncEntryEvent; diff --git a/src/bin/wasmtime.rs b/src/bin/wasmtime.rs index d91650ac4549..cb6d0d5302f9 100644 --- a/src/bin/wasmtime.rs +++ b/src/bin/wasmtime.rs @@ -90,14 +90,16 @@ enum Subcommand { #[cfg(feature = "objdump")] Objdump(wasmtime_cli::commands::ObjdumpCommand), - /// Replay execution of a recorded execution trace + /// Run a determinstic, embedding-agnostic replay execution of the Wasm module + /// according to a prior recorded execution trace (e.g. generated with the + /// `--record` option under `wasmtime run`). /// /// The options below are the superset of the `run` command. The notable options - /// added for replay are: + /// added for replay are `--trace` (to specify the recorded traces) and + /// corresponding settings (e.g. `--validate`) /// - /// * `--trace` to specify the recorded trace to replay - /// - /// * Settings for replay operation (e.g. `--validate`) + /// Note: Minimal configs for deterministic Wasm semantics will be + /// enforced during replay by default (NaN canonicalization, deterministic relaxed SIMD) Replay(wasmtime_cli::commands::ReplayCommand), } diff --git a/src/commands/run.rs b/src/commands/run.rs index 76754ac2440a..d42be6686559 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -110,7 +110,7 @@ impl RunCommand { } if let Some(cfg) = replay_cfg { - config.enable_replay(cfg); + config.enable_replay(cfg)?; } let engine = Engine::new(&config)?; From 03421d6def2b12ad2777265e7dfdb23608f0d8f8 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 29 Jul 2025 16:22:39 -0700 Subject: [PATCH 50/62] Added `no_std` support for RR --- Cargo.lock | 9 ++- crates/wasmtime/Cargo.toml | 8 ++- crates/wasmtime/src/config.rs | 35 +++------- .../src/runtime/component/func/options.rs | 4 +- crates/wasmtime/src/runtime/func.rs | 14 +--- .../src/runtime/rr/events/component_wasm.rs | 1 - crates/wasmtime/src/runtime/rr/events/mod.rs | 2 +- crates/wasmtime/src/runtime/rr/io.rs | 70 +++++++++++++++++++ crates/wasmtime/src/runtime/rr/mod.rs | 25 +++---- 9 files changed, 111 insertions(+), 57 deletions(-) create mode 100644 crates/wasmtime/src/runtime/rr/io.rs diff --git a/Cargo.lock b/Cargo.lock index 64d294005e93..2cbf0f49faa2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1179,6 +1179,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "embedding" version = "35.0.0" @@ -2575,7 +2581,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a55c51ee6c0db07e68448e336cf8ea4131a620edefebf9893e759b2d793420f8" dependencies = [ "cobs", - "embedded-io", + "embedded-io 0.4.0", "serde", ] @@ -4149,6 +4155,7 @@ dependencies = [ "cc", "cfg-if", "cranelift-native", + "embedded-io 0.6.1", "encoding_rs", "env_logger 0.11.5", "futures", diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml index f967c7250318..eaf7bd7e47af 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -45,7 +45,7 @@ wat = { workspace = true, optional = true } serde = { workspace = true } serde_derive = { workspace = true } serde_json = { workspace = true, optional = true } -postcard = { workspace = true, features = ["use-std"] } +postcard = { workspace = true, optional = true } indexmap = { workspace = true } once_cell = { version = "1.12.0", optional = true } rayon = { version = "1.0", optional = true } @@ -62,6 +62,7 @@ smallvec = { workspace = true, optional = true } hashbrown = { workspace = true, features = ["default-hasher"] } bitflags = { workspace = true } futures = { workspace = true, features = ["alloc"], optional = true } +embedded-io = { version = "0.6.1", features = ["alloc"], optional = true } sha2 = "0.10.2" [target.'cfg(target_os = "windows")'.dependencies.windows-sys] @@ -326,6 +327,7 @@ addr2line = ["dep:addr2line", "dep:gimli", "std"] # Many features of the Wasmtime crate implicitly require this `std` feature. # This will be automatically enabled if necessary. std = [ + 'dep:postcard', 'postcard/use-std', 'wasmtime-environ/std', 'object/std', @@ -398,9 +400,9 @@ component-model-async = [ # Enables support for record/replay rr = ["rr-component", "rr-core"] # RR for components -rr-component = ["component-model", "std"] +rr-component = ["component-model", "dep:embedded-io", "dep:postcard"] # RR for core wasm -rr-core = ["std"] +rr-core = ["dep:embedded-io", "dep:postcard"] # Support for validation signatures/checks during record/replay respectively rr-type-validation = [] diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 93f51d3c9a19..62a27efc8c1f 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -4,7 +4,6 @@ use bitflags::Flags; use core::fmt; use core::str::FromStr; use serde::{Deserialize, Serialize}; -use std::io::{Read, Write}; #[cfg(any(feature = "cache", feature = "cranelift", feature = "winch"))] use std::path::Path; use wasmparser::WasmFeatures; @@ -33,6 +32,8 @@ pub use wasmtime_cache::{Cache, CacheConfig}; #[cfg(all(feature = "incremental-cache", feature = "cranelift"))] pub use wasmtime_environ::CacheStore; +use crate::rr::{RecordWriter, ReplayReader}; + /// Represents the module instance allocation strategy to use. #[derive(Clone)] #[non_exhaustive] @@ -251,16 +252,7 @@ impl Default for RecordMetadata { } } -/// A [`Write`] usable for recording in RR -/// -/// Only constraint is that writer must be Send + Sync -pub trait RecordWriter: Write + Send + Sync {} -impl RecordWriter for T {} - /// Configuration for recording execution -/// -/// ## Notes -/// The writers are buffered internally as needed, so avoid using buffered writers as intializers #[derive(Clone)] pub struct RecordConfig { /// Closure that generates a writer for recording execution traces @@ -282,16 +274,7 @@ impl Default for ReplayMetadata { } } -/// A [`Read`] usable for replaying in RR -/// -/// Only constraint is that reader must be Send + Sync -pub trait ReplayReader: Read + Send + Sync {} -impl ReplayReader for T {} - /// Configuration for replay execution -/// -/// ## Notes -/// The readers are buffered internally as needed, so avoid using buffered readers as intializers #[derive(Clone)] pub struct ReplayConfig { /// Closure that generates a reader for replaying execution traces @@ -1126,7 +1109,7 @@ impl Config { /// [proposal]: https://github.com/webassembly/relaxed-simd pub fn relaxed_simd_deterministic(&mut self, enable: bool) -> &mut Self { assert!( - !(self.check_determinism() && !enable), + !(self.is_determinism_enforced() && !enable), "Deterministic relaxed SIMD cannot be disabled when record/replay is enabled" ); self.tunables.relaxed_simd_deterministic = Some(enable); @@ -1432,7 +1415,7 @@ impl Config { #[cfg(any(feature = "cranelift", feature = "winch"))] pub fn cranelift_nan_canonicalization(&mut self, enable: bool) -> &mut Self { assert!( - !(self.check_determinism() && !enable), + !(self.is_determinism_enforced() && !enable), "NaN canonicalization cannot be disabled when record/replay is enabled" ); let val = if enable { "true" } else { "false" }; @@ -2758,11 +2741,11 @@ impl Config { self } - /// Enforce deterministic execution configurations + /// Enforce deterministic execution configurations. Currently, means the following: + /// * Enabling NaN canonicalization with [`Config::cranelift_nan_canonicalization`] + /// * Enabling deterministic relaxed SIMD with [`Config::relaxed_simd_deterministic`] /// - /// Required for faithful record/replay execution. Currently, this does the following: - /// * Enables NaN canonicalization with [`Config::cranelift_nan_canonicalization`] - /// * Enables deterministic relaxed SIMD with [`Config::relaxed_simd_deterministic`] + /// Required for faithful record/replay execution. #[inline] pub fn enforce_determinism(&mut self) -> &mut Self { self.cranelift_nan_canonicalization(true) @@ -2784,7 +2767,7 @@ impl Config { /// /// Required for faithful record/replay execution #[inline] - pub fn check_determinism(&mut self) -> bool { + pub fn is_determinism_enforced(&mut self) -> bool { self.rr.is_some() } diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index 4a99d33673cf..489db727b6f1 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -73,7 +73,7 @@ impl Drop for MemorySliceCell<'_> { /// ## Critical Invariants /// /// Typically recording would need to know both when the slice was borrowed AND when it was -/// dropped, since memory movement with [`realloc`](LowerContext::realloc) can be interleave between +/// dropped, since memory movement with [`realloc`](LowerContext::realloc) can be interleaved between /// borrows and drops, and replays would have to be aware of this. **However**, with this abstraction, /// we can be more efficient and get away with **only** recording drops, because of the implicit interaction between /// [`realloc`](LowerContext::realloc) and [`get`](LowerContext::get)/[`get_dyn`](LowerContext::get_dyn), @@ -572,7 +572,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { let buf = self.store.0.replay_buffer_mut().unwrap(); let event = buf.next_event()?; let _replay_metadata = buf.metadata(); - let _ = match event { + match event { RREvent::ComponentHostFuncReturn(e) => { // End of lowering process #[cfg(feature = "rr-type-validation")] diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index d2336b5fd430..5f4211bf7e92 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -2339,13 +2339,7 @@ impl HostContext { let type_index = vmctx.func_ref().type_index; let wasm_func_type_arc = caller.engine().signatures().borrow(type_index).unwrap(); - // A common copy of function type for typechecking avoids - // cloning for every replay interception - let wasm_func_type = caller - .store - .0 - .rr_enabled() - .then(|| wasm_func_type_arc.unwrap_func()); + let wasm_func_type = wasm_func_type_arc.unwrap_func(); // Setup call parameters let params = { @@ -2366,7 +2360,6 @@ impl HostContext { store.record_event_if( |r| r.add_validation, |_| { - let wasm_func_type = wasm_func_type.unwrap(); let num_params = wasm_func_type.params().len(); HostFuncEntryEvent::new( &args.as_ref()[..num_params], @@ -2382,7 +2375,7 @@ impl HostContext { |_event: HostFuncEntryEvent, _r: &ReplayMetadata| { #[cfg(feature = "rr-type-validation")] if _r.validate { - _event.validate(wasm_func_type.unwrap())?; + _event.validate(wasm_func_type)?; } Ok(()) }, @@ -2425,7 +2418,7 @@ impl HostContext { event.move_into_slice( args.as_mut(), #[cfg(feature = "rr-type-validation")] - _rmeta.validate.then_some(wasm_func_type.unwrap()), + _rmeta.validate.then_some(wasm_func_type), ) }) .map_err(Into::into), @@ -2435,7 +2428,6 @@ impl HostContext { } }?; store.record_event(|_rmeta| { - let wasm_func_type = wasm_func_type.unwrap(); let num_results = wasm_func_type.params().len(); HostFuncReturnEvent::new( unsafe { &args.as_ref()[..num_results] }, diff --git a/crates/wasmtime/src/runtime/rr/events/component_wasm.rs b/crates/wasmtime/src/runtime/rr/events/component_wasm.rs index d3cb0f58c8cd..6cb4ada728d3 100644 --- a/crates/wasmtime/src/runtime/rr/events/component_wasm.rs +++ b/crates/wasmtime/src/runtime/rr/events/component_wasm.rs @@ -2,7 +2,6 @@ use super::*; #[expect(unused_imports, reason = "used for doc-links")] use crate::component::{Component, ComponentType}; -use std::vec::Vec; use wasmtime_environ::component::InterfaceType; #[cfg(feature = "rr-type-validation")] use wasmtime_environ::component::TypeTuple; diff --git a/crates/wasmtime/src/runtime/rr/events/mod.rs b/crates/wasmtime/src/runtime/rr/events/mod.rs index 843635f3254b..dc2de7e3f7cd 100644 --- a/crates/wasmtime/src/runtime/rr/events/mod.rs +++ b/crates/wasmtime/src/runtime/rr/events/mod.rs @@ -28,7 +28,7 @@ impl fmt::Display for EventActionError { } } -impl std::error::Error for EventActionError {} +impl core::error::Error for EventActionError {} type ValRawBytes = [u8; mem::size_of::()]; diff --git a/crates/wasmtime/src/runtime/rr/io.rs b/crates/wasmtime/src/runtime/rr/io.rs new file mode 100644 index 000000000000..c59e54a64dd3 --- /dev/null +++ b/crates/wasmtime/src/runtime/rr/io.rs @@ -0,0 +1,70 @@ +use crate::prelude::*; +use postcard; +use serde::{Deserialize, Serialize}; + +cfg_if::cfg_if! { + if #[cfg(feature = "std")] { + use std::io::{Write, Read}; + /// An [`Write`] usable for recording in RR + /// + /// This supports `no_std`, but must be [Send] and [Sync] + pub trait RecordWriter: Write + Send + Sync {} + impl RecordWriter for T {} + + /// An [`Read`] usable for replaying in RR + pub trait ReplayReader: Read + Send + Sync {} + impl ReplayReader for T {} + + } else { + // `no_std` configuration + use embedded_io::{Read, Write}; + + /// An [`Write`] usable for recording in RR + /// + /// This supports `no_std`, but must be [Send] and [Sync] + pub trait RecordWriter: Write + Send + Sync {} + impl RecordWriter for T {} + + /// An [`Read`] usable for replaying in RR + /// + /// This supports `no_std`, but must be [Send] and [Sync] + pub trait ReplayReader: Read + Send + Sync {} + impl ReplayReader for T {} + } +} + +/// Serialize and write `value` to a `RecordWriter` +/// +/// Currently uses `postcard` serializer +pub fn to_record_writer(value: &T, writer: W) -> Result<()> +where + T: Serialize + ?Sized, + W: RecordWriter, +{ + cfg_if::cfg_if! { + if #[cfg(feature = "std")] { + postcard::to_io(value, writer)?; + } else { + postcard::to_eio(value, writer)?; + } + } + Ok(()) +} + +/// Read and deserialize a `value` from a `ReplayReader`. +/// +/// Currently uses `postcard` deserializer, with optional scratch +/// buffer to deserialize into +pub fn from_replay_reader<'a, T, R>(reader: R, scratch: &'a mut [u8]) -> Result +where + T: Deserialize<'a>, + R: ReplayReader + 'a, +{ + cfg_if::cfg_if! { + if #[cfg(feature = "std")] { + Ok(postcard::from_io((reader, scratch))?.0) + } else { + Ok(postcard::from_eio((reader, scratch))?.0) + } + } +} diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index fcab631e42f0..c47c669f7937 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -6,18 +6,19 @@ //! //! This module does NOT support RR for component builtins yet. -use crate::config::{ - ModuleVersionStrategy, RecordMetadata, RecordWriter, ReplayMetadata, ReplayReader, -}; +use crate::config::{ModuleVersionStrategy, RecordMetadata, ReplayMetadata}; use crate::prelude::*; use core::fmt; -use postcard; use serde::{Deserialize, Serialize}; /// Encapsulation of event types comprising an [`RREvent`] sum type pub mod events; use events::*; +/// I/O support for reading and writing traces +mod io; +pub use io::{RecordWriter, ReplayReader}; + /// Macro template for [`RREvent`] and its conversion to/from specific /// event types macro_rules! rr_event { @@ -139,7 +140,7 @@ impl fmt::Display for ReplayError { } } -impl std::error::Error for ReplayError {} +impl core::error::Error for ReplayError {} impl From for ReplayError { fn from(value: EventActionError) -> Self { @@ -301,8 +302,8 @@ impl Drop for RecordBuffer { impl Recorder for RecordBuffer { fn new_recorder(mut writer: Box, metadata: RecordMetadata) -> Result { // Replay requires the Module version and RecordMetadata configuration - postcard::to_io(ModuleVersionStrategy::WasmtimeVersion.as_str(), &mut writer)?; - postcard::to_io(&metadata, &mut writer)?; + io::to_record_writer(ModuleVersionStrategy::WasmtimeVersion.as_str(), &mut writer)?; + io::to_record_writer(&metadata, &mut writer)?; Ok(RecordBuffer { buf: Vec::new(), writer: writer, @@ -324,7 +325,7 @@ impl Recorder for RecordBuffer { fn flush(&mut self) -> Result<()> { log::debug!("Flushing record buffer..."); for e in self.buf.drain(..) { - postcard::to_io(&e, &mut self.writer)?; + io::to_record_writer(&e, &mut self.writer)?; } return Ok(()); } @@ -350,13 +351,13 @@ impl Iterator for ReplayBuffer { fn next(&mut self) -> Option { // Check for EoF - let result = postcard::from_io((&mut self.reader, &mut [0; 0])); + let result = io::from_replay_reader(&mut self.reader, &mut [0; 0]); match result { Err(e) => { log::error!("Erroneous replay read: {}", e); None } - Ok((event, _)) => { + Ok(event) => { if let RREvent::Eof = event { None } else { @@ -385,7 +386,7 @@ impl Replayer for ReplayBuffer { fn new_replayer(mut reader: Box, metadata: ReplayMetadata) -> Result { // Ensure module versions match let mut scratch = [0u8; 12]; - let (version, _) = postcard::from_io::<&str, _>((&mut reader, &mut scratch))?; + let version = io::from_replay_reader::<&str, _>(&mut reader, &mut scratch)?; assert_eq!( version, ModuleVersionStrategy::WasmtimeVersion.as_str(), @@ -393,7 +394,7 @@ impl Replayer for ReplayBuffer { ); // Read the recording metadata - let (trace_metadata, _) = postcard::from_io((&mut reader, &mut [0; 0]))?; + let trace_metadata = io::from_replay_reader(&mut reader, &mut [0; 0])?; Ok(ReplayBuffer { reader: reader, From 2918403b9c3571af9b39134328017bfd57e9ed48 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Wed, 30 Jul 2025 09:54:22 -0700 Subject: [PATCH 51/62] Added RR feature gating and style nits --- Cargo.toml | 3 + crates/cli-flags/Cargo.toml | 1 + crates/cli-flags/src/lib.rs | 37 ++- crates/wasmtime/Cargo.toml | 15 +- crates/wasmtime/src/config.rs | 92 +++--- crates/wasmtime/src/engine.rs | 2 + .../src/runtime/component/func/host.rs | 278 +++++++++--------- .../src/runtime/component/func/options.rs | 39 ++- .../src/runtime/component/instance.rs | 4 +- crates/wasmtime/src/runtime/func.rs | 164 +++++------ crates/wasmtime/src/runtime/instance.rs | 8 +- ...{component_wasm.rs => component_events.rs} | 1 + .../events/{core_wasm.rs => core_events.rs} | 0 crates/wasmtime/src/runtime/rr/events/mod.rs | 4 +- crates/wasmtime/src/runtime/rr/mod.rs | 136 ++++----- crates/wasmtime/src/runtime/store.rs | 55 ++-- src/bin/wasmtime.rs | 7 +- src/commands.rs | 2 + src/commands/replay.rs | 4 +- src/commands/run.rs | 10 +- 20 files changed, 469 insertions(+), 393 deletions(-) rename crates/wasmtime/src/runtime/rr/events/{component_wasm.rs => component_events.rs} (99%) rename crates/wasmtime/src/runtime/rr/events/{core_wasm.rs => core_events.rs} (100%) diff --git a/Cargo.toml b/Cargo.toml index 683f79141a4a..af055f1506d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -492,6 +492,9 @@ gc-drc = ["gc", "wasmtime/gc-drc", "wasmtime-cli-flags/gc-drc"] gc-null = ["gc", "wasmtime/gc-null", "wasmtime-cli-flags/gc-null"] pulley = ["wasmtime-cli-flags/pulley"] stack-switching = ["wasmtime/stack-switching", "wasmtime-cli-flags/stack-switching"] +rr = ["wasmtime-cli-flags/rr"] +rr-component = ["wasmtime/rr-component", "rr"] +rr-core = ["wasmtime/rr-core", "rr"] # CLI subcommands for the `wasmtime` executable. See `wasmtime $cmd --help` # for more information on each subcommand. diff --git a/crates/cli-flags/Cargo.toml b/crates/cli-flags/Cargo.toml index 1469c9535b15..2874fb092e20 100644 --- a/crates/cli-flags/Cargo.toml +++ b/crates/cli-flags/Cargo.toml @@ -40,3 +40,4 @@ threads = ["wasmtime/threads"] memory-protection-keys = ["wasmtime/memory-protection-keys"] pulley = ["wasmtime/pulley"] stack-switching = ["wasmtime/stack-switching"] +rr = ["wasmtime/rr"] diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs index c8148920a72a..9fdf1f28cfbe 100644 --- a/crates/cli-flags/src/lib.rs +++ b/crates/cli-flags/src/lib.rs @@ -10,7 +10,7 @@ use std::{ sync::Arc, time::Duration, }; -use wasmtime::{Config, RecordConfig, RecordMetadata}; +use wasmtime::Config; pub mod opt; @@ -1012,21 +1012,26 @@ impl CommonOptions { } let record = &self.record; - if let Some(path) = record.path.clone() { - let default_settings = RecordMetadata::default(); - config.enable_record(RecordConfig { - writer_initializer: Arc::new(move || { - Box::new(BufWriter::new(fs::File::create(&path).unwrap())) - }), - metadata: RecordMetadata { - add_validation: record - .validation_metadata - .unwrap_or(default_settings.add_validation), - event_window_size: record - .event_window_size - .unwrap_or(default_settings.event_window_size), - }, - })?; + match_feature! { + ["rr" : record.path.clone()] + path => { + use wasmtime::{RecordConfig, RecordSettings}; + let default_settings = RecordSettings::default(); + config.enable_record(RecordConfig { + writer_initializer: Arc::new(move || { + Box::new(BufWriter::new(fs::File::create(&path).unwrap())) + }), + settings: RecordSettings { + add_validation: record + .validation_metadata + .unwrap_or(default_settings.add_validation), + event_window_size: record + .event_window_size + .unwrap_or(default_settings.event_window_size), + }, + })? + }, + _ => err, } Ok(config) diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml index eaf7bd7e47af..c44cc3453f3b 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -397,12 +397,15 @@ component-model-async = [ "dep:futures" ] -# Enables support for record/replay -rr = ["rr-component", "rr-core"] # RR for components -rr-component = ["component-model", "dep:embedded-io", "dep:postcard"] +rr-component = ["component-model", "rr"] + # RR for core wasm -rr-core = ["dep:embedded-io", "dep:postcard"] -# Support for validation signatures/checks during record/replay respectively -rr-type-validation = [] +rr-core = ["rr"] + +# Support for validation signatures/checks during record/replay respectively. +# This feature only makes sense if 'rr-component' or 'rr-core' is enabled +rr-type-validation = ["rr"] +# Base primtives for any rr capability +rr = ["dep:embedded-io", "dep:postcard"] diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 62a27efc8c1f..07c9d69095c5 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -3,6 +3,7 @@ use alloc::sync::Arc; use bitflags::Flags; use core::fmt; use core::str::FromStr; +#[cfg(feature = "rr")] use serde::{Deserialize, Serialize}; #[cfg(any(feature = "cache", feature = "cranelift", feature = "winch"))] use std::path::Path; @@ -25,6 +26,8 @@ use crate::stack::{StackCreator, StackCreatorProxy}; #[cfg(feature = "async")] use wasmtime_fiber::RuntimeFiberStackCreator; +#[cfg(feature = "rr")] +use crate::rr::{RecordWriter, ReplayReader}; #[cfg(feature = "runtime")] pub use crate::runtime::code_memory::CustomCodeMemory; #[cfg(feature = "cache")] @@ -32,8 +35,6 @@ pub use wasmtime_cache::{Cache, CacheConfig}; #[cfg(all(feature = "incremental-cache", feature = "cranelift"))] pub use wasmtime_environ::CacheStore; -use crate::rr::{RecordWriter, ReplayReader}; - /// Represents the module instance allocation strategy to use. #[derive(Clone)] #[non_exhaustive] @@ -103,7 +104,7 @@ impl core::hash::Hash for ModuleVersionStrategy { } impl ModuleVersionStrategy { - /// Get the string-encoding version of the module + /// Get the string-encoding version of the module. pub fn as_str(&self) -> &str { match &self { Self::WasmtimeVersion => env!("CARGO_PKG_VERSION"), @@ -177,6 +178,7 @@ pub struct Config { pub(crate) coredump_on_trap: bool, pub(crate) macos_use_mach_ports: bool, pub(crate) detect_host_feature: Option Option>, + #[cfg(feature = "rr")] pub(crate) rr: Option, } @@ -234,16 +236,18 @@ impl Default for CompilerConfig { } } -/// Metadata for specifying recording strategy +/// Settings for execution recording. +#[cfg(feature = "rr")] #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RecordMetadata { - /// Flag to include additional signatures for replay validation +pub struct RecordSettings { + /// Flag to include additional signatures for replay validation. pub add_validation: bool, - /// Maximum window size of internal event buffer + /// Maximum window size of internal event buffer. pub event_window_size: usize, } -impl Default for RecordMetadata { +#[cfg(feature = "rr")] +impl Default for RecordSettings { fn default() -> Self { Self { add_validation: false, @@ -252,71 +256,79 @@ impl Default for RecordMetadata { } } -/// Configuration for recording execution +/// Configuration for recording execution. +#[cfg(feature = "rr")] #[derive(Clone)] pub struct RecordConfig { - /// Closure that generates a writer for recording execution traces + /// Closure that generates a writer for recording execution traces. pub writer_initializer: Arc Box + Send + Sync>, - /// Associated metadata for configuring the recording strategy - pub metadata: RecordMetadata, + /// Associated metadata for configuring the recording strategy. + pub settings: RecordSettings, } -/// Metadata for specifying replay strategy +/// Settings for execution replay. +#[cfg(feature = "rr")] #[derive(Debug, Clone)] -pub struct ReplayMetadata { - /// Flag to include additional signatures for replay validation +pub struct ReplaySettings { + /// Flag to include additional signatures for replay validation. pub validate: bool, } -impl Default for ReplayMetadata { +#[cfg(feature = "rr")] +impl Default for ReplaySettings { fn default() -> Self { Self { validate: true } } } -/// Configuration for replay execution +/// Configuration for replay execution. +#[cfg(feature = "rr")] #[derive(Clone)] pub struct ReplayConfig { - /// Closure that generates a reader for replaying execution traces + /// Closure that generates a reader for replaying execution traces. pub reader_initializer: Arc Box + Send + Sync>, - /// Flag for dynamic validation checks when replaying events - pub metadata: ReplayMetadata, + /// Flag for dynamic validation checks when replaying events. + pub settings: ReplaySettings, } -/// Configurations for record/replay (RR) executions +/// Configurations for record/replay (RR) executions. +#[cfg(feature = "rr")] #[derive(Clone)] pub enum RRConfig { - /// Record configuration + /// Record configuration. Record(RecordConfig), - /// Replay configuration + /// Replay configuration. Replay(ReplayConfig), } +#[cfg(feature = "rr")] impl From for RRConfig { fn from(value: RecordConfig) -> Self { Self::Record(value) } } +#[cfg(feature = "rr")] impl From for RRConfig { fn from(value: ReplayConfig) -> Self { Self::Replay(value) } } +#[cfg(feature = "rr")] impl RRConfig { - /// Obtain the record configuration + /// Obtain the record configuration. /// - /// Return [`None`] if it is not configured + /// Return [`None`] if it is not configured. pub fn record(&self) -> Option<&RecordConfig> { match self { Self::Record(r) => Some(r), _ => None, } } - /// Obtain the replay configuration + /// Obtain the replay configuration. /// - /// Return [`None`] if it is not configured + /// Return [`None`] if it is not configured. pub fn replay(&self) -> Option<&ReplayConfig> { match self { Self::Replay(r) => Some(r), @@ -377,6 +389,7 @@ impl Config { detect_host_feature: Some(detect_host_feature), #[cfg(not(feature = "std"))] detect_host_feature: None, + #[cfg(feature = "rr")] rr: None, }; #[cfg(any(feature = "cranelift", feature = "winch"))] @@ -1108,6 +1121,7 @@ impl Config { /// /// [proposal]: https://github.com/webassembly/relaxed-simd pub fn relaxed_simd_deterministic(&mut self, enable: bool) -> &mut Self { + #[cfg(feature = "rr")] assert!( !(self.is_determinism_enforced() && !enable), "Deterministic relaxed SIMD cannot be disabled when record/replay is enabled" @@ -1414,6 +1428,7 @@ impl Config { /// The default value for this is `false` #[cfg(any(feature = "cranelift", feature = "winch"))] pub fn cranelift_nan_canonicalization(&mut self, enable: bool) -> &mut Self { + #[cfg(feature = "rr")] assert!( !(self.is_determinism_enforced() && !enable), "NaN canonicalization cannot be disabled when record/replay is enabled" @@ -2754,7 +2769,7 @@ impl Config { } /// Remove determinstic execution enforcements (if any) applied - /// by [`Config::enforce_determinism`] + /// by [`Config::enforce_determinism`]. #[inline] pub fn remove_determinism_enforcement(&mut self) -> &mut Self { self.cranelift_nan_canonicalization(false) @@ -2763,22 +2778,24 @@ impl Config { } /// Evaluates to true if current configuration must respect - /// deterministic execution in its configuration + /// deterministic execution in its configuration. /// - /// Required for faithful record/replay execution + /// Required for faithful record/replay execution. + #[cfg(feature = "rr")] #[inline] pub fn is_determinism_enforced(&mut self) -> bool { self.rr.is_some() } - /// Enable execution trace recording with the provided configuration + /// Enable execution trace recording with the provided configuration. /// /// This method implicitly enforces determinism (see [`Config::enforce_determinism`] /// for details). /// /// ## Errors /// - /// Errors if record/replay are simultaneously enabled + /// Errors if record/replay are simultaneously enabled. + #[cfg(feature = "rr")] pub fn enable_record(&mut self, record: RecordConfig) -> Result<&mut Self> { self.enforce_determinism(); if let Some(cfg) = &self.rr { @@ -2790,14 +2807,15 @@ impl Config { Ok(self) } - /// Enable replay execution based on the provided configuration + /// Enable replay execution based on the provided configuration. /// /// This method implicitly enforces determinism (see [`Config::enforce_determinism`] /// for details). /// /// ## Errors /// - /// Errors if record/replay are simultaneously enabled + /// Errors if record/replay are simultaneously enabled. + #[cfg(feature = "rr")] pub fn enable_replay(&mut self, replay: ReplayConfig) -> Result<&mut Self> { self.enforce_determinism(); if let Some(cfg) = &self.rr { @@ -2809,10 +2827,12 @@ impl Config { Ok(self) } - /// Disable the currently active record/replay configuration + /// Disable the currently active record/replay configuration, and remove + /// any determinism enforcement it introduced as side-effects. /// /// A common option is used for both record/replay here - /// since record and replay can never be set simultaneously + /// since record and replay can never be set simultaneously/ + #[cfg(feature = "rr")] pub fn disable_record_replay(&mut self) -> &mut Self { self.remove_determinism_enforcement(); self.rr = None; diff --git a/crates/wasmtime/src/engine.rs b/crates/wasmtime/src/engine.rs index b452ad12f89b..0abe503fce86 100644 --- a/crates/wasmtime/src/engine.rs +++ b/crates/wasmtime/src/engine.rs @@ -1,4 +1,5 @@ use crate::Config; +#[cfg(feature = "rr")] use crate::RRConfig; use crate::prelude::*; #[cfg(feature = "runtime")] @@ -223,6 +224,7 @@ impl Engine { /// Returns an immutable reference to the record/replay configuration settings /// used by the engine + #[cfg(feature = "rr")] #[inline] pub fn rr(&self) -> Option<&RRConfig> { self.config().rr.as_ref() diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 03b0bd7a4fa6..2bc85ff208da 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -3,7 +3,8 @@ use crate::component::matching::InstanceType; use crate::component::storage::{slice_to_storage_mut, storage_as_slice, storage_as_slice_mut}; use crate::component::{ComponentNamedList, ComponentType, Lift, Lower, Val}; use crate::prelude::*; -use crate::runtime::rr::events::component_wasm::{ +#[cfg(feature = "rr-component")] +use crate::runtime::rr::component_events::{ HostFuncReturnEvent, LowerReturnEvent, LowerStoreReturnEvent, }; use crate::runtime::vm::component::{ @@ -15,7 +16,6 @@ use alloc::sync::Arc; use core::any::Any; use core::mem::{self, MaybeUninit}; use core::ptr::NonNull; -#[cfg(feature = "rr-type-validation")] use wasmtime_environ::component::TypeTuple; use wasmtime_environ::component::{ CanonicalAbiInfo, ComponentTypes, InterfaceType, MAX_FLAT_PARAMS, MAX_FLAT_RESULTS, @@ -25,10 +25,10 @@ use wasmtime_environ::component::{ /// Record/replay stubs for host function entry events macro_rules! rr_host_func_entry_event { { $args:expr, $param_types:expr => $store:expr } => { - #[cfg(feature = "rr-type-validation")] + #[cfg(all(feature = "rr-component", feature = "rr-type-validation"))] { - use crate::config::ReplayMetadata; - use crate::runtime::rr::events::component_wasm::HostFuncEntryEvent; + use crate::config::ReplaySettings; + use crate::runtime::rr::component_events::HostFuncEntryEvent; $store.record_event_if( |r| r.add_validation, |_| { @@ -41,7 +41,7 @@ macro_rules! rr_host_func_entry_event { )?; $store.next_replay_event_if( |_, r| r.add_validation, - |_event: HostFuncEntryEvent, _r: &ReplayMetadata| { + |_event: HostFuncEntryEvent, _r: &ReplaySettings| { #[cfg(feature = "rr-type-validation")] if _r.validate { _event.validate($param_types)?; @@ -55,21 +55,25 @@ macro_rules! rr_host_func_entry_event { /// Record stubs for host function return events macro_rules! record_host_func_return_event { - { $args:expr, $return_types:expr => $store:expr } => {{ - $store.record_event(|_r| { - HostFuncReturnEvent::new( - $args, - #[cfg(feature = "rr-type-validation")] - _r.add_validation.then_some($return_types), - ) - })?; - }}; + { $args:expr, $return_types:expr => $store:expr } => { + #[cfg(feature = "rr-component")] + { + $store.record_event(|_r| { + HostFuncReturnEvent::new( + $args, + #[cfg(feature = "rr-type-validation")] + _r.add_validation.then_some($return_types), + ) + })?; + } + }; } /// Record stubs for store events of component types macro_rules! record_lower_store_event_wrapper { { $lower_store:expr => $store:expr } => {{ let store_result = $lower_store; + #[cfg(feature = "rr-component")] $store.record_event(|_| LowerStoreReturnEvent::new(&store_result))?; store_result }}; @@ -79,6 +83,7 @@ macro_rules! record_lower_store_event_wrapper { macro_rules! record_lower_event_wrapper { { $lower:expr => $store:expr } => {{ let lower_result = $lower; + #[cfg(feature = "rr-component")] $store.record_event(|_| LowerReturnEvent::new(&lower_result))?; lower_result }}; @@ -295,41 +300,31 @@ where } }; - let replay_enabled = cx.0.replay_enabled(); - let ret = if !replay_enabled { - ReturnMode::Standard({ - let mut lift = LiftContext::new(cx.0, &options, types, instance); - lift.enter_call(); + if !cx.0.replay_enabled() { + let mut lift = LiftContext::new(cx.0, &options, types, instance); + lift.enter_call(); + let params = storage.lift_params(&mut lift, param_tys)?; - let params = storage.lift_params(&mut lift, param_tys)?; - closure(cx.as_context_mut(), params)? - }) - } else { - ReturnMode::Replay - }; + let ret = closure(cx.as_context_mut(), params)?; + + flags.set_may_leave(false); + let mut lower = LowerContext::new(cx, &options, types, instance); + storage.lower_results(&mut lower, result_tys, &types[ty.results], ret)?; + flags.set_may_leave(true); - flags.set_may_leave(false); - let mut lower = LowerContext::new(cx, &options, types, instance); - storage.lower_results( - &mut lower, - result_tys, - ret, - #[cfg(feature = "rr-type-validation")] - &types[ty.results], - )?; - flags.set_may_leave(true); - - if !replay_enabled { lower.exit_call()?; + } else { + #[cfg(feature = "rr-component")] + { + flags.set_may_leave(false); + let mut lower = LowerContext::new(cx, &options, types, instance); + storage.replay_lower_results(&mut lower, &types[ty.results])?; + flags.set_may_leave(true); + } } return Ok(()); - enum ReturnMode { - Standard(Return), - Replay, - } - enum Storage<'a, P: ComponentType, R: ComponentType> { Direct(&'a mut MaybeUninit>), ParamsIndirect(&'a mut MaybeUninit>), @@ -361,54 +356,28 @@ where &mut self, cx: &mut LowerContext<'_, T>, ty: InterfaceType, - ret: ReturnMode, - #[cfg(feature = "rr-type-validation")] rr_tys: &TypeTuple, + record_tys: &TypeTuple, + ret: R, ) -> Result<()> { let direct_results_lower = |cx: &mut LowerContext<'_, T>, dst: &mut MaybeUninit<::Lower>, - ret: ReturnMode| { - let res = match ret { - ReturnMode::Standard(retval) => { - record_lower_event_wrapper! { retval.lower(cx, ty, dst) => cx.store.0 } - } - ReturnMode::Replay => { - // This path also stores the final return values in resulting storage - cx.replay_lowering( - Some(storage_as_slice_mut(dst)), - #[cfg(feature = "rr-type-validation")] - rr_tys, - ) - } - }; + ret: R| { + let res = record_lower_event_wrapper! { ret.lower(cx, ty, dst) => cx.store.0 }; record_host_func_return_event! { - storage_as_slice(dst), rr_tys => cx.store.0 + storage_as_slice(dst), record_tys => cx.store.0 }; res }; - let indirect_results_lower = - |cx: &mut LowerContext<'_, T>, dst: &ValRaw, ret: ReturnMode| { - let ptr = validate_inbounds::(cx.as_slice(), dst)?; - let res = match ret { - ReturnMode::Standard(retval) => { - record_lower_store_event_wrapper! { retval.store(cx, ty, ptr) => cx.store.0 } - } - ReturnMode::Replay => { - // `dst` is a Wasm pointer to indirect results. This pointer itself will remain - // deterministic, and thus replay will not need to change this. However, - // replay will have to overwrite any nested stored lowerings (deep copy) - cx.replay_lowering( - None, - #[cfg(feature = "rr-type-validation")] - rr_tys, - ) - } - }; - // Recording here is just for marking the return event - record_host_func_return_event! { - &[], rr_tys => cx.store.0 - }; - res + let indirect_results_lower = |cx: &mut LowerContext<'_, T>, dst: &ValRaw, ret: R| { + let ptr = validate_inbounds::(cx.as_slice(), dst)?; + let res = + record_lower_store_event_wrapper! { ret.store(cx, ty, ptr) => cx.store.0 }; + // Recording here is just for marking the return event + record_host_func_return_event! { + &[], record_tys => cx.store.0 }; + res + }; match self { Storage::Direct(storage) => { direct_results_lower(cx, map_maybe_uninit!(storage.ret), ret) @@ -422,6 +391,44 @@ where Storage::Indirect(storage) => indirect_results_lower(cx, &storage.retptr, ret), } } + + #[cfg(feature = "rr-component")] + unsafe fn replay_lower_results( + &mut self, + cx: &mut LowerContext<'_, T>, + expect_tys: &TypeTuple, + ) -> Result<()> { + let direct_results_lower = + |cx: &mut LowerContext<'_, T>, + dst: &mut MaybeUninit<::Lower>| { + // This path also stores the final return values in resulting storage + cx.replay_lowering( + Some(storage_as_slice_mut(dst)), + #[cfg(feature = "rr-type-validation")] + expect_tys, + ) + }; + let indirect_results_lower = |cx: &mut LowerContext<'_, T>, _dst: &ValRaw| { + // `_dst` is a Wasm pointer to indirect results. This pointer itself will remain + // deterministic, and thus replay will not need to change this. However, + // replay will have to overwrite any nested stored lowerings (deep copy) + cx.replay_lowering( + None, + #[cfg(feature = "rr-type-validation")] + expect_tys, + ) + }; + match self { + Storage::Direct(storage) => { + direct_results_lower(cx, map_maybe_uninit!(storage.ret)) + } + Storage::ParamsIndirect(storage) => { + direct_results_lower(cx, map_maybe_uninit!(storage.ret)) + } + Storage::ResultsIndirect(storage) => indirect_results_lower(cx, &storage.retptr), + Storage::Indirect(storage) => indirect_results_lower(cx, &storage.retptr), + } + } } } @@ -483,11 +490,6 @@ where F: FnOnce(StoreContextMut<'_, T>, &[Val], &mut [Val]) -> Result<()>, T: 'static, { - enum ReturnMode { - Standard { vals: Vec, index: usize }, - Replay, - } - if async_ { todo!() } @@ -506,20 +508,19 @@ where bail!("cannot leave component instance"); } - let args; - let ret_index; - let func_ty = &types[ty]; let param_tys = &types[func_ty.params]; let result_tys = &types[func_ty.results]; - let replay_enabled = store.0.replay_enabled(); - rr_host_func_entry_event! { storage, &types[func_ty.params] => store.0 }; - let results = if !replay_enabled { + if !store.0.replay_enabled() { + let args; + let ret_index; + let mut cx = LiftContext::new(store.0, &options, types, instance); cx.enter_call(); + if let Some(param_count) = param_tys.abi.flat_count(MAX_FLAT_PARAMS) { // NB: can use `MaybeUninit::slice_assume_init_ref` when that's stable let mut iter = @@ -555,32 +556,44 @@ where result_vals.push(Val::Bool(false)); } closure(store.as_context_mut(), &args, &mut result_vals)?; - ReturnMode::Standard { - vals: result_vals, - index: ret_index, - } - } else { - ReturnMode::Replay - }; - flags.set_may_leave(false); - - let mut cx = LowerContext::new(store, &options, types, instance); - if let Some(cnt) = result_tys.abi.flat_count(MAX_FLAT_RESULTS) { - match results { - ReturnMode::Standard { vals, index: _ } => { - let mut dst = storage[..cnt].iter_mut(); - for (val, ty) in vals.iter().zip(result_tys.types.iter()) { - record_lower_event_wrapper! { - val.lower(&mut cx, *ty, &mut dst) => cx.store.0 - }?; - } - assert!(dst.next().is_none()); - record_host_func_return_event! { - mem::transmute::<&[MaybeUninit], &[ValRaw]>(storage), result_tys => cx.store.0 - }; + flags.set_may_leave(false); + let mut cx = LowerContext::new(store, &options, types, instance); + if let Some(cnt) = result_tys.abi.flat_count(MAX_FLAT_RESULTS) { + let mut dst = storage[..cnt].iter_mut(); + for (val, ty) in result_vals.iter().zip(result_tys.types.iter()) { + record_lower_event_wrapper! { + val.lower(&mut cx, *ty, &mut dst) => cx.store.0 + }?; } - ReturnMode::Replay => { + assert!(dst.next().is_none()); + record_host_func_return_event! { + mem::transmute::<&[MaybeUninit], &[ValRaw]>(storage), result_tys => cx.store.0 + }; + } else { + let ret_ptr = storage[ret_index].assume_init_ref(); + let mut ptr = validate_inbounds_dynamic(&result_tys.abi, cx.as_slice(), ret_ptr)?; + for (val, ty) in result_vals.iter().zip(result_tys.types.iter()) { + let offset = types.canonical_abi(ty).next_field32_size(&mut ptr); + record_lower_store_event_wrapper! { + val.store(&mut cx, *ty, offset) => cx.store.0 + }?; + } + // Recording here is just for marking the return event + record_host_func_return_event! { + &[], result_tys => cx.store.0 + }; + } + flags.set_may_leave(true); + + cx.exit_call()?; + } else { + #[cfg(feature = "rr-component")] + { + flags.set_may_leave(false); + let mut cx = LowerContext::new(store, &options, types, instance); + if let Some(_cnt) = result_tys.abi.flat_count(MAX_FLAT_RESULTS) { + // Copy the entire contiguous storage slice (instead of looping values one-by-one) let result_storage = mem::transmute::<&mut [MaybeUninit], &mut [ValRaw]>(storage); // This path also stores the final return values in resulting storage @@ -589,25 +602,7 @@ where #[cfg(feature = "rr-type-validation")] result_tys, )?; - } - }; - } else { - match results { - ReturnMode::Standard { vals, index } => { - let ret_ptr = storage[index].assume_init_ref(); - let mut ptr = validate_inbounds_dynamic(&result_tys.abi, cx.as_slice(), ret_ptr)?; - for (val, ty) in vals.iter().zip(result_tys.types.iter()) { - let offset = types.canonical_abi(ty).next_field32_size(&mut ptr); - record_lower_store_event_wrapper! { - val.store(&mut cx, *ty, offset) => cx.store.0 - }?; - } - // Recording here is just for marking the return event - record_host_func_return_event! { - &[], result_tys => cx.store.0 - }; - } - ReturnMode::Replay => { + } else { // The indirect `ret_ptr` itself will remain deterministic, and thus replay will not // need to change the return storage. However, replay will have to overwrite any nested stored // lowerings (deep copy) @@ -617,16 +612,11 @@ where result_tys, )?; } + flags.set_may_leave(true); } } - flags.set_may_leave(true); - - if !replay_enabled { - cx.exit_call()?; - } - - return Ok(()); + Ok(()) } fn validate_inbounds_dynamic(abi: &CanonicalAbiInfo, memory: &[u8], ptr: &ValRaw) -> Result { diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index 489db727b6f1..711089f65abd 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -1,17 +1,21 @@ +#[cfg(feature = "rr-component")] +use crate::ValRaw; use crate::component::ResourceType; use crate::component::matching::InstanceType; use crate::component::resources::{HostResourceData, HostResourceIndex, HostResourceTables}; use crate::prelude::*; -use crate::runtime::rr::events::component_wasm::{ +#[cfg(feature = "rr-component")] +use crate::runtime::rr::component_events::{ MemorySliceWriteEvent, ReallocEntryEvent, ReallocReturnEvent, }; +#[cfg(feature = "rr-component")] use crate::runtime::rr::{RREvent, RecordBuffer, Recorder, Replayer}; use crate::runtime::vm::component::{ CallContexts, ComponentInstance, InstanceFlags, ResourceTable, ResourceTables, }; use crate::runtime::vm::{VMFuncRef, VMMemoryDefinition}; use crate::store::{StoreId, StoreOpaque}; -use crate::{FuncType, StoreContextMut, ValRaw}; +use crate::{FuncType, StoreContextMut}; use alloc::sync::Arc; use core::ops::{Deref, DerefMut}; use core::ptr::NonNull; @@ -29,6 +33,7 @@ use wasmtime_environ::component::{ComponentTypes, StringEncoding, TypeResourceTa pub struct MemorySliceCell<'a> { offset: usize, bytes: &'a mut [u8], + #[cfg(feature = "rr-component")] recorder: Option<&'a mut RecordBuffer>, } impl<'a> Deref for MemorySliceCell<'a> { @@ -45,6 +50,7 @@ impl DerefMut for MemorySliceCell<'_> { impl Drop for MemorySliceCell<'_> { /// Drop serves as a recording hook for stores to the memory slice fn drop(&mut self) { + #[cfg(feature = "rr-component")] if let Some(buf) = &mut self.recorder { buf.record_event(|_| MemorySliceWriteEvent::new(self.offset, self.bytes.to_vec())) .unwrap(); @@ -83,6 +89,7 @@ impl Drop for MemorySliceCell<'_> { pub struct ConstMemorySliceCell<'a, const N: usize> { offset: usize, bytes: &'a mut [u8; N], + #[cfg(feature = "rr-component")] recorder: Option<&'a mut RecordBuffer>, } impl<'a, const N: usize> Deref for ConstMemorySliceCell<'a, N> { @@ -99,6 +106,7 @@ impl<'a, const N: usize> DerefMut for ConstMemorySliceCell<'a, N> { impl<'a, const N: usize> Drop for ConstMemorySliceCell<'a, N> { /// Drops serves as a recording hook for stores to the memory slice fn drop(&mut self) { + #[cfg(feature = "rr-component")] if let Some(buf) = &mut self.recorder { buf.record_event_if( |_| true, @@ -252,6 +260,7 @@ impl Options { } /// Same as [`memory_mut`](Self::memory_mut), but with the record buffer from the encapsulating store + #[cfg(feature = "rr-component")] fn memory_mut_with_recorder<'a>( &self, store: &'a mut StoreOpaque, @@ -341,6 +350,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { /// # Panics /// /// See [`as_slice`](Self::as_slice) + #[cfg(feature = "rr-component")] fn as_slice_mut_with_recorder(&mut self) -> (&mut [u8], Option<&mut RecordBuffer>) { self.options.memory_mut_with_recorder(self.store.0) } @@ -405,10 +415,12 @@ impl<'a, T: 'static> LowerContext<'a, T> { old_align: u32, new_size: usize, ) -> Result { + #[cfg(feature = "rr-component")] self.store .0 .record_event(|_| ReallocEntryEvent::new(old, old_size, old_align, new_size))?; let result = self.realloc_inner(old, old_size, old_align, new_size); + #[cfg(feature = "rr-component")] self.store .0 .record_event_if(|r| r.add_validation, |_| ReallocReturnEvent::new(&result))?; @@ -427,7 +439,13 @@ impl<'a, T: 'static> LowerContext<'a, T> { /// (e.g. it wasn't present during the specification of canonical options). #[inline] pub fn get(&mut self, offset: usize) -> ConstMemorySliceCell { - let (slice_mut, recorder) = self.as_slice_mut_with_recorder(); + cfg_if::cfg_if! { + if #[cfg(feature = "rr-component")] { + let (slice_mut, recorder) = self.as_slice_mut_with_recorder(); + } else { + let slice_mut = self.as_slice_mut(); + } + } // FIXME: this bounds check shouldn't actually be necessary, all // callers of `ComponentType::store` have already performed a bounds // check so we're guaranteed that `offset..offset+N` is in-bounds. That @@ -441,6 +459,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { ConstMemorySliceCell { offset: offset, bytes: slice_mut[offset..].first_chunk_mut().unwrap(), + #[cfg(feature = "rr-component")] recorder: recorder, } } @@ -453,10 +472,17 @@ impl<'a, T: 'static> LowerContext<'a, T> { /// Refer to [`get`](Self::get). #[inline] pub fn get_dyn(&mut self, offset: usize, size: usize) -> MemorySliceCell { - let (slice_mut, recorder) = self.as_slice_mut_with_recorder(); + cfg_if::cfg_if! { + if #[cfg(feature = "rr-component")] { + let (slice_mut, recorder) = self.as_slice_mut_with_recorder(); + } else { + let slice_mut = self.as_slice_mut(); + } + } MemorySliceCell { offset: offset, bytes: &mut slice_mut[offset..][..size], + #[cfg(feature = "rr-component")] recorder: recorder, } } @@ -559,6 +585,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { /// ## Important Notes /// /// * It is assumed that this is only invoked at the root lower/store calls + #[cfg(feature = "rr-component")] pub fn replay_lowering( &mut self, mut result_storage: Option<&mut [ValRaw]>, @@ -571,12 +598,12 @@ impl<'a, T: 'static> LowerContext<'a, T> { while !complete { let buf = self.store.0.replay_buffer_mut().unwrap(); let event = buf.next_event()?; - let _replay_metadata = buf.metadata(); + let _replay_settings = buf.settings(); match event { RREvent::ComponentHostFuncReturn(e) => { // End of lowering process #[cfg(feature = "rr-type-validation")] - if _replay_metadata.validate { + if _replay_settings.validate { e.validate(result_tys)? } if let Some(storage) = result_storage.as_deref_mut() { diff --git a/crates/wasmtime/src/runtime/component/instance.rs b/crates/wasmtime/src/runtime/component/instance.rs index f6d2ab48de80..1ac09989a841 100644 --- a/crates/wasmtime/src/runtime/component/instance.rs +++ b/crates/wasmtime/src/runtime/component/instance.rs @@ -8,7 +8,8 @@ use crate::component::{ use crate::instance::OwnedImports; use crate::linker::DefinitionType; use crate::prelude::*; -use crate::rr::events::component_wasm::InstantiationEvent; +#[cfg(feature = "rr-component")] +use crate::rr::component_events::InstantiationEvent; use crate::runtime::vm::VMFuncRef; use crate::runtime::vm::component::{ComponentInstance, OwnedComponentInstance}; use crate::store::StoreOpaque; @@ -846,6 +847,7 @@ impl InstancePre { fn instantiate_impl(&self, mut store: impl AsContextMut) -> Result { let mut store = store.as_context_mut(); + #[cfg(feature = "rr-component")] { store .0 diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 5f4211bf7e92..f4ffbb67e79c 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -1,5 +1,6 @@ use crate::prelude::*; -use crate::rr::events::core_wasm::HostFuncReturnEvent; +#[cfg(feature = "rr-core")] +use crate::rr::core_events::HostFuncReturnEvent; use crate::runtime::Uninhabited; use crate::runtime::vm::{ ExportFunction, InterpreterRef, SendSyncPtr, StoreBox, VMArrayCallHostFuncContext, @@ -2321,11 +2322,6 @@ impl HostContext { // should be part of this closure, and the long-jmp-ing // happens after the closure in handling the result. let run = move |mut caller: Caller<'_, T>| { - enum ReturnMode { - Standard(R::Fallible), - Replay, - } - let mut args = NonNull::slice_from_raw_parts(args.cast::>(), args_len); let vmctx = VMArrayCallHostFuncContext::from_opaque(callee_vmctx).as_ref(); @@ -2337,106 +2333,102 @@ impl HostContext { let state = &*(state as *const _ as *const HostFuncState); let func = &state.func; - let type_index = vmctx.func_ref().type_index; - let wasm_func_type_arc = caller.engine().signatures().borrow(type_index).unwrap(); + #[cfg(feature = "rr-core")] + let wasm_func_type_arc = { + let type_index = vmctx.func_ref().type_index; + caller.engine().signatures().borrow(type_index).unwrap() + }; + #[cfg(feature = "rr-core")] let wasm_func_type = wasm_func_type_arc.unwrap_func(); - // Setup call parameters - let params = { - let mut store = if P::may_gc() { - AutoAssertNoGc::new(caller.store.0) - } else { - unsafe { AutoAssertNoGc::disabled(caller.store.0) } - }; - + #[cfg(all(feature = "rr-core", feature = "rr-type-validation"))] + { // Record/replay interceptions of raw parameters args - // (only when validation is enabled). - // Function type unwraps should never panic since they are - // lazily evaluated - #[cfg(feature = "rr-type-validation")] - { - use crate::config::ReplayMetadata; - use crate::rr::events::core_wasm::HostFuncEntryEvent; - store.record_event_if( - |r| r.add_validation, - |_| { - let num_params = wasm_func_type.params().len(); - HostFuncEntryEvent::new( - &args.as_ref()[..num_params], - // Don't need to check validation here since it is - // covered by the push predicate in this case - #[cfg(feature = "rr-type-validation")] - Some(wasm_func_type.clone()), - ) - }, - )?; - store.next_replay_event_if( - |_, r| r.add_validation, - |_event: HostFuncEntryEvent, _r: &ReplayMetadata| { + use crate::config::ReplaySettings; + use crate::rr::core_events::HostFuncEntryEvent; + caller.store.0.record_event_if( + |r| r.add_validation, + |_| { + let num_params = wasm_func_type.params().len(); + HostFuncEntryEvent::new( + &args.as_ref()[..num_params], + // Don't need to check validation here since it is + // covered by the push predicate in this case #[cfg(feature = "rr-type-validation")] - if _r.validate { - _event.validate(wasm_func_type)?; - } - Ok(()) - }, - )?; - } - - P::load(&mut store, args.as_mut()) - // Drop on store is necessary here; scope closure makes this implicit - }; + Some(wasm_func_type.clone()), + ) + }, + )?; + // Don't need to auto-assert GC here since we aren't using P + caller.store.0.next_replay_event_if( + |_, r| r.add_validation, + |_event: HostFuncEntryEvent, _r: &ReplaySettings| { + #[cfg(feature = "rr-type-validation")] + if _r.validate { + _event.validate(wasm_func_type)?; + } + Ok(()) + }, + )?; + } - let returns = if caller.store.0.replay_enabled() { - ReturnMode::::Replay - } else { - ReturnMode::Standard('ret: { + if !caller.store.0.replay_enabled() { + let ret = 'ret: { if let Err(trap) = caller.store.0.call_hook(CallHook::CallingHost) { break 'ret R::fallible_from_error(trap); } + // Setup call parameters + let params = { + let mut store = if P::may_gc() { + AutoAssertNoGc::new(caller.store.0) + } else { + unsafe { AutoAssertNoGc::disabled(caller.store.0) } + }; + P::load(&mut store, args.as_mut()) + // Drop on store is necessary here; scope closure makes this implicit + }; let r = func(caller.sub_caller(), params); if let Err(trap) = caller.store.0.call_hook(CallHook::ReturningFromHost) { break 'ret R::fallible_from_error(trap); } - let fallible = r.into_fallible(); - if !fallible.compatible_with_store(caller.store.0) { - bail!("host function attempted to return cross-`Store` value to Wasm") - } - fallible - }) - }; - - let mut store = if R::may_gc() { - AutoAssertNoGc::new(caller.store.0) + r.into_fallible() + }; + if !ret.compatible_with_store(caller.store.0) { + bail!("host function attempted to return cross-`Store` value to Wasm") + } else { + let mut store = if R::may_gc() { + AutoAssertNoGc::new(caller.store.0) + } else { + unsafe { AutoAssertNoGc::disabled(caller.store.0) } + }; + ret.store(&mut store, args.as_mut())?; + } + #[cfg(feature = "rr-core")] + // Record the return value of store + caller.store.0.record_event(|_rmeta| { + let num_results = wasm_func_type.params().len(); + HostFuncReturnEvent::new( + unsafe { &args.as_ref()[..num_results] }, + #[cfg(feature = "rr-type-validation")] + _rmeta.add_validation.then_some(wasm_func_type.clone()), + ) + })?; } else { - unsafe { AutoAssertNoGc::disabled(caller.store.0) } - }; - - // Record/replay interceptions of raw return args - let ret = match returns { - ReturnMode::Replay => store + // Replay the return value of the store + #[cfg(feature = "rr-core")] + caller + .store + .0 .next_replay_event_and(|event: HostFuncReturnEvent, _rmeta| { event.move_into_slice( args.as_mut(), #[cfg(feature = "rr-type-validation")] _rmeta.validate.then_some(wasm_func_type), ) - }) - .map_err(Into::into), - ReturnMode::Standard(fallible) => { - let fallible: ::Fallible = fallible; - fallible.store(&mut store, args.as_mut()) - } - }?; - store.record_event(|_rmeta| { - let num_results = wasm_func_type.params().len(); - HostFuncReturnEvent::new( - unsafe { &args.as_ref()[..num_results] }, - #[cfg(feature = "rr-type-validation")] - _rmeta.add_validation.then_some(wasm_func_type.clone()), - ) - })?; + })?; + } - Ok(ret) + Ok(()) }; // With nothing else on the stack move `run` into this diff --git a/crates/wasmtime/src/runtime/instance.rs b/crates/wasmtime/src/runtime/instance.rs index f63432883321..7a2b652d5387 100644 --- a/crates/wasmtime/src/runtime/instance.rs +++ b/crates/wasmtime/src/runtime/instance.rs @@ -7,8 +7,8 @@ use crate::runtime::vm::{ use crate::store::{AllocateInstanceKind, InstanceId, StoreInstanceId, StoreOpaque}; use crate::types::matching; use crate::{ - AsContextMut, Engine, Export, Extern, ExternType, Func, Global, Memory, Module, ModuleExport, - SharedMemory, StoreContext, StoreContextMut, Table, Tag, TypedFunc, + AsContextMut, Engine, Export, Extern, Func, Global, Memory, Module, ModuleExport, SharedMemory, + StoreContext, StoreContextMut, Table, Tag, TypedFunc, }; use alloc::sync::Arc; use core::ptr::NonNull; @@ -923,8 +923,10 @@ fn pre_instantiate_raw( imports.push(&item, store); } + #[cfg(feature = "rr")] if module.engine().rr().is_some() && module.exports().any(|export| { + use crate::ExternType; if let ExternType::Memory(_) = export.ty() { true } else { @@ -932,7 +934,7 @@ fn pre_instantiate_raw( } }) { - bail!("Cannot enable record/replay for core wasm modules when a memory is exported"); + bail!("Cannot support record/replay for core wasm modules when a memory is exported"); } Ok(imports) diff --git a/crates/wasmtime/src/runtime/rr/events/component_wasm.rs b/crates/wasmtime/src/runtime/rr/events/component_events.rs similarity index 99% rename from crates/wasmtime/src/runtime/rr/events/component_wasm.rs rename to crates/wasmtime/src/runtime/rr/events/component_events.rs index 6cb4ada728d3..7adaf39e70e6 100644 --- a/crates/wasmtime/src/runtime/rr/events/component_wasm.rs +++ b/crates/wasmtime/src/runtime/rr/events/component_events.rs @@ -1,4 +1,5 @@ //! Module comprising of component model wasm events + use super::*; #[expect(unused_imports, reason = "used for doc-links")] use crate::component::{Component, ComponentType}; diff --git a/crates/wasmtime/src/runtime/rr/events/core_wasm.rs b/crates/wasmtime/src/runtime/rr/events/core_events.rs similarity index 100% rename from crates/wasmtime/src/runtime/rr/events/core_wasm.rs rename to crates/wasmtime/src/runtime/rr/events/core_events.rs diff --git a/crates/wasmtime/src/runtime/rr/events/mod.rs b/crates/wasmtime/src/runtime/rr/events/mod.rs index dc2de7e3f7cd..68e24171c98d 100644 --- a/crates/wasmtime/src/runtime/rr/events/mod.rs +++ b/crates/wasmtime/src/runtime/rr/events/mod.rs @@ -109,5 +109,5 @@ where } } -pub mod component_wasm; -pub mod core_wasm; +pub mod component_events; +pub mod core_events; diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index c47c669f7937..21bc48ecc17f 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -1,3 +1,4 @@ +#![cfg(feature = "rr")] //! Wasmtime's Record and Replay support. //! //! This feature is currently not optimized and under development @@ -6,14 +7,17 @@ //! //! This module does NOT support RR for component builtins yet. -use crate::config::{ModuleVersionStrategy, RecordMetadata, ReplayMetadata}; +use crate::config::{ModuleVersionStrategy, RecordSettings, ReplaySettings}; use crate::prelude::*; use core::fmt; use serde::{Deserialize, Serialize}; /// Encapsulation of event types comprising an [`RREvent`] sum type -pub mod events; -use events::*; +mod events; +use events::EventActionError; + +pub use events::component_events; +pub use events::core_events; /// I/O support for reading and writing traces mod io; @@ -77,35 +81,35 @@ macro_rules! rr_event { // Set of supported record/replay events rr_event! { /// Call into host function from Core Wasm - CoreHostFuncEntry(core_wasm::HostFuncEntryEvent), + CoreHostFuncEntry(core_events::HostFuncEntryEvent), /// Return from host function to Core Wasm - CoreHostFuncReturn(core_wasm::HostFuncReturnEvent), + CoreHostFuncReturn(core_events::HostFuncReturnEvent), // REQUIRED events for replay // /// Instantiation of a component - ComponentInstantiation(component_wasm::InstantiationEvent), + ComponentInstantiation(component_events::InstantiationEvent), /// Return from host function to component - ComponentHostFuncReturn(component_wasm::HostFuncReturnEvent), + ComponentHostFuncReturn(component_events::HostFuncReturnEvent), /// Component ABI realloc call in linear wasm memory - ComponentReallocEntry(component_wasm::ReallocEntryEvent), + ComponentReallocEntry(component_events::ReallocEntryEvent), /// Return from a type lowering operation - ComponentLowerReturn(component_wasm::LowerReturnEvent), + ComponentLowerReturn(component_events::LowerReturnEvent), /// Return from a store during a type lowering operation - ComponentLowerStoreReturn(component_wasm::LowerStoreReturnEvent), + ComponentLowerStoreReturn(component_events::LowerStoreReturnEvent), /// An attempt to obtain a mutable slice into Wasm linear memory - ComponentMemorySliceWrite(component_wasm::MemorySliceWriteEvent), + ComponentMemorySliceWrite(component_events::MemorySliceWriteEvent), // OPTIONAL events for replay validation // /// Call into host function from component - ComponentHostFuncEntry(component_wasm::HostFuncEntryEvent), + ComponentHostFuncEntry(component_events::HostFuncEntryEvent), /// Call into [Lower::lower] for type lowering - ComponentLowerEntry(component_wasm::LowerEntryEvent), + ComponentLowerEntry(component_events::LowerEntryEvent), /// Call into [Lower::store] during type lowering - ComponentLowerStoreEntry(component_wasm::LowerStoreEntryEvent), + ComponentLowerStoreEntry(component_events::LowerStoreEntryEvent), /// Return from Component ABI realloc call - ComponentReallocReturn(component_wasm::ReallocReturnEvent) + ComponentReallocReturn(component_events::ReallocReturnEvent) } /// Error type signalling failures during a replay run @@ -151,7 +155,7 @@ impl From for ReplayError { /// This trait provides the interface for a FIFO recorder pub trait Recorder { /// Construct a recorder with the writer backend - fn new_recorder(writer: Box, metadata: RecordMetadata) -> Result + fn new_recorder(writer: Box, settings: RecordSettings) -> Result where Self: Sized; @@ -163,26 +167,26 @@ pub trait Recorder { fn record_event(&mut self, f: F) -> Result<()> where T: Into, - F: FnOnce(&RecordMetadata) -> T; + F: FnOnce(&RecordSettings) -> T; /// Trigger an explicit flush of any buffered data to the writer /// /// Buffer should be emptied during this process fn flush(&mut self) -> Result<()>; - /// Get metadata associated with the recording process - fn metadata(&self) -> &RecordMetadata; + /// Get settings associated with the recording process + fn settings(&self) -> &RecordSettings; // Provided methods - /// Conditionally [`record_event`](Self::record_event) when `pred` is true + /// Conditionally [`record_event`](Recorder::record_event) when `pred` is true fn record_event_if(&mut self, pred: P, f: F) -> Result<()> where T: Into, - P: FnOnce(&RecordMetadata) -> bool, - F: FnOnce(&RecordMetadata) -> T, + P: FnOnce(&RecordSettings) -> bool, + F: FnOnce(&RecordSettings) -> T, { - if pred(self.metadata()) { + if pred(self.settings()) { self.record_event(f)?; } Ok(()) @@ -193,15 +197,15 @@ pub trait Recorder { /// essentially operates as an iterator over the recorded events pub trait Replayer: Iterator { /// Constructs a reader on buffer - fn new_replayer(reader: Box, metadata: ReplayMetadata) -> Result + fn new_replayer(reader: Box, settings: ReplaySettings) -> Result where Self: Sized; - /// Get metadata associated with the replay process - fn metadata(&self) -> &ReplayMetadata; + /// Get settings associated with the replay process + fn settings(&self) -> &ReplaySettings; - /// Get the metadata embedded within the trace during recording - fn trace_metadata(&self) -> &RecordMetadata; + /// Get the settings (embedded within the trace) during recording + fn trace_settings(&self) -> &RecordSettings; // Provided Methods @@ -209,7 +213,7 @@ pub trait Replayer: Iterator { /// /// ## Errors /// - /// Returns a `ReplayError::EmptyBuffer` if the buffer is empty + /// Returns a [`ReplayError::EmptyBuffer`] if the buffer is empty #[inline] fn next_event(&mut self) -> Result { let event = self.next().ok_or(ReplayError::EmptyBuffer); @@ -224,7 +228,7 @@ pub trait Replayer: Iterator { /// /// ## Errors /// - /// See [`next_event_and`](Self::next_event_and) + /// See [`next_event_and`](Replayer::next_event_and) #[inline] fn next_event_typed(&mut self) -> Result where @@ -245,22 +249,22 @@ pub trait Replayer: Iterator { where T: TryFrom, ReplayError: From<>::Error>, - F: FnOnce(T, &ReplayMetadata) -> Result<(), ReplayError>, + F: FnOnce(T, &ReplaySettings) -> Result<(), ReplayError>, { let call_event = self.next_event_typed()?; - Ok(f(call_event, self.metadata())?) + Ok(f(call_event, self.settings())?) } - /// Conditionally execute [`next_event_and`](Self::next_event_and) when `pred` is true + /// Conditionally execute [`next_event_and`](Replayer::next_event_and) when `pred` is true #[inline] fn next_event_if(&mut self, pred: P, f: F) -> Result<(), ReplayError> where T: TryFrom, ReplayError: From<>::Error>, - P: FnOnce(&ReplayMetadata, &RecordMetadata) -> bool, - F: FnOnce(T, &ReplayMetadata) -> Result<(), ReplayError>, + P: FnOnce(&ReplaySettings, &RecordSettings) -> bool, + F: FnOnce(T, &ReplaySettings) -> Result<(), ReplayError>, { - if pred(self.metadata(), self.trace_metadata()) { + if pred(self.settings(), self.trace_settings()) { self.next_event_and(f) } else { Ok(()) @@ -276,15 +280,15 @@ pub struct RecordBuffer { buf: Vec, /// Writer to store data into writer: Box, - /// Metadata for record configuration - metadata: RecordMetadata, + /// Settings in record configuration + settings: RecordSettings, } impl RecordBuffer { /// Push a new record event [`RREvent`] to the buffer fn push_event(&mut self, event: RREvent) -> Result<()> { self.buf.push(event); - if self.buf.len() >= self.metadata().event_window_size { + if self.buf.len() >= self.settings().event_window_size { self.flush()?; } Ok(()) @@ -300,14 +304,14 @@ impl Drop for RecordBuffer { } impl Recorder for RecordBuffer { - fn new_recorder(mut writer: Box, metadata: RecordMetadata) -> Result { - // Replay requires the Module version and RecordMetadata configuration + fn new_recorder(mut writer: Box, settings: RecordSettings) -> Result { + // Replay requires the Module version and record settings io::to_record_writer(ModuleVersionStrategy::WasmtimeVersion.as_str(), &mut writer)?; - io::to_record_writer(&metadata, &mut writer)?; + io::to_record_writer(&settings, &mut writer)?; Ok(RecordBuffer { buf: Vec::new(), writer: writer, - metadata: metadata, + settings: settings, }) } @@ -315,9 +319,9 @@ impl Recorder for RecordBuffer { fn record_event(&mut self, f: F) -> Result<()> where T: Into, - F: FnOnce(&RecordMetadata) -> T, + F: FnOnce(&RecordSettings) -> T, { - let event = f(self.metadata()).into(); + let event = f(self.settings()).into(); log::debug!("Recording event => {}", &event); self.push_event(event) } @@ -331,8 +335,8 @@ impl Recorder for RecordBuffer { } #[inline] - fn metadata(&self) -> &RecordMetadata { - &self.metadata + fn settings(&self) -> &RecordSettings { + &self.settings } } @@ -340,10 +344,10 @@ impl Recorder for RecordBuffer { pub struct ReplayBuffer { /// Reader to read replay trace from reader: Box, - /// Metadata for replay configuration - metadata: ReplayMetadata, - /// Metadata for record configuration (encoded in the trace) - trace_metadata: RecordMetadata, + /// Settings in replay configuration + settings: ReplaySettings, + /// Settings for record configuration (encoded in the trace) + trace_settings: RecordSettings, } impl Iterator for ReplayBuffer { @@ -375,7 +379,7 @@ impl Drop for ReplayBuffer { } else { log::warn!( "Replay buffer is dropped with {} remaining events, and is likely an invalid execution", - self.count() - 1 + self.count() ); } } @@ -383,7 +387,7 @@ impl Drop for ReplayBuffer { } impl Replayer for ReplayBuffer { - fn new_replayer(mut reader: Box, metadata: ReplayMetadata) -> Result { + fn new_replayer(mut reader: Box, settings: ReplaySettings) -> Result { // Ensure module versions match let mut scratch = [0u8; 12]; let version = io::from_replay_reader::<&str, _>(&mut reader, &mut scratch)?; @@ -393,24 +397,24 @@ impl Replayer for ReplayBuffer { "Wasmtime version mismatch between engine used for record and replay" ); - // Read the recording metadata - let trace_metadata = io::from_replay_reader(&mut reader, &mut [0; 0])?; + // Read the recording settings + let trace_settings = io::from_replay_reader(&mut reader, &mut [0; 0])?; Ok(ReplayBuffer { - reader: reader, - metadata: metadata, - trace_metadata: trace_metadata, + reader, + settings, + trace_settings, }) } #[inline] - fn metadata(&self) -> &ReplayMetadata { - &self.metadata + fn settings(&self) -> &ReplaySettings { + &self.settings } #[inline] - fn trace_metadata(&self) -> &RecordMetadata { - &self.trace_metadata + fn trace_settings(&self) -> &RecordSettings { + &self.trace_settings } } @@ -424,7 +428,7 @@ mod tests { #[test] fn rr_buffers() -> Result<()> { - let record_metadata = RecordMetadata::default(); + let record_settings = RecordSettings::default(); let tmp = NamedTempFile::new()?; let tmppath = tmp.path().to_str().expect("Filename should be UTF-8"); @@ -432,7 +436,7 @@ mod tests { // Record values let mut recorder = - RecordBuffer::new_recorder(Box::new(File::create(tmppath)?), record_metadata)?; + RecordBuffer::new_recorder(Box::new(File::create(tmppath)?), record_settings)?; let event = component_wasm::HostFuncReturnEvent::new( values.as_slice(), #[cfg(feature = "rr-type-validation")] @@ -445,11 +449,11 @@ mod tests { let tmppath = >::as_ref(&tmp) .to_str() .expect("Filename should be UTF-8"); - let replay_metadata = ReplayMetadata { validate: true }; + let replay_settings = ReplaySettings { validate: true }; // Assert that replayed values are identical let mut replayer = - ReplayBuffer::new_replayer(Box::new(File::open(tmppath)?), replay_metadata)?; + ReplayBuffer::new_replayer(Box::new(File::open(tmppath)?), replay_settings)?; replayer.next_event_and(|store_event: component_wasm::HostFuncReturnEvent, _| { // Check replay matches record assert!(store_event == event); diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index 0b2e134d19b3..63efb2b8d8f1 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -79,6 +79,7 @@ use crate::RootSet; use crate::module::RegisteredModuleId; use crate::prelude::*; +#[cfg(feature = "rr")] use crate::runtime::rr::{RREvent, RecordBuffer, Recorder, ReplayBuffer, ReplayError, Replayer}; #[cfg(feature = "gc")] use crate::runtime::vm::GcRootsList; @@ -93,7 +94,8 @@ use crate::runtime::vm::{ use crate::trampoline::VMHostGlobalContext; use crate::{Engine, Module, Trap, Val, ValRaw, module::ModuleRegistry}; use crate::{Global, Instance, Memory, Table, Uninhabited}; -use crate::{RecordMetadata, ReplayMetadata}; +#[cfg(feature = "rr")] +use crate::{RecordSettings, ReplaySettings}; use alloc::sync::Arc; use core::fmt; use core::marker; @@ -400,10 +402,12 @@ pub struct StoreOpaque { /// Storage for recording execution /// /// `None` implies recording is disabled for this store + #[cfg(feature = "rr")] record_buffer: Option, /// Storage for replaying execution /// /// `None` implies replay is disabled for this store + #[cfg(feature = "rr")] replay_buffer: Option, } @@ -537,8 +541,6 @@ impl Store { let pkey = engine.allocator().next_available_pkey(); - let rr = engine.rr(); - let inner = StoreOpaque { _marker: marker::PhantomPinned, engine: engine.clone(), @@ -591,23 +593,25 @@ impl Store { debug_assert!(engine.target().is_pulley()); Executor::Interpreter(Interpreter::new(engine)) }, - record_buffer: rr.and_then(|v| { + #[cfg(feature = "rr")] + record_buffer: engine.rr().and_then(|v| { v.record().and_then(|record| { Some( RecordBuffer::new_recorder( (record.writer_initializer)(), - record.metadata.clone(), + record.settings.clone(), ) .unwrap(), ) }) }), - replay_buffer: rr.and_then(|v| { + #[cfg(feature = "rr")] + replay_buffer: engine.rr().and_then(|v| { v.replay().and_then(|replay| { Some( ReplayBuffer::new_replayer( (replay.reader_initializer)(), - replay.metadata.clone(), + replay.settings.clone(), ) .unwrap(), ) @@ -1369,11 +1373,13 @@ impl StoreOpaque { &self.vm_store_context } + #[cfg(feature = "rr")] #[inline] pub fn record_buffer_mut(&mut self) -> Option<&mut RecordBuffer> { self.record_buffer.as_mut() } + #[cfg(feature = "rr")] #[inline] pub fn replay_buffer_mut(&mut self) -> Option<&mut ReplayBuffer> { self.replay_buffer.as_mut() @@ -1382,11 +1388,12 @@ impl StoreOpaque { /// Record the given event into the store's record buffer /// /// Convenience wrapper around [`Recorder::record_event`] + #[cfg(feature = "rr")] #[inline] pub(crate) fn record_event(&mut self, f: F) -> Result<()> where T: Into, - F: FnOnce(&RecordMetadata) -> T, + F: FnOnce(&RecordSettings) -> T, { if let Some(buf) = self.record_buffer_mut() { buf.record_event(f) @@ -1398,12 +1405,13 @@ impl StoreOpaque { /// Conditionally record the given event into the store's record buffer /// /// Convenience wrapper around [`Recorder::record_event_if`] + #[cfg(feature = "rr")] #[inline] pub(crate) fn record_event_if(&mut self, pred: P, f: F) -> Result<()> where T: Into, - P: FnOnce(&RecordMetadata) -> bool, - F: FnOnce(&RecordMetadata) -> T, + P: FnOnce(&RecordSettings) -> bool, + F: FnOnce(&RecordSettings) -> T, { if let Some(buf) = self.record_buffer_mut() { buf.record_event_if(pred, f) @@ -1415,12 +1423,13 @@ impl StoreOpaque { /// Process the next replay event from the store's replay buffer /// /// Convenience wrapper around [`Replayer::next_event_and`] + #[cfg(feature = "rr")] #[inline] pub(crate) fn next_replay_event_and(&mut self, f: F) -> Result<(), ReplayError> where T: TryFrom, ReplayError: From<>::Error>, - F: FnOnce(T, &ReplayMetadata) -> Result<(), ReplayError>, + F: FnOnce(T, &ReplaySettings) -> Result<(), ReplayError>, { if let Some(buf) = self.replay_buffer_mut() { buf.next_event_and(f) @@ -1432,14 +1441,14 @@ impl StoreOpaque { /// Conditionally process the next replay event from the store's replay buffer /// /// Convenience wrapper around [`Replayer::next_event_if`] + #[cfg(feature = "rr")] #[inline] - #[allow(dead_code)] pub(crate) fn next_replay_event_if(&mut self, pred: P, f: F) -> Result<(), ReplayError> where T: TryFrom, ReplayError: From<>::Error>, - P: FnOnce(&ReplayMetadata, &RecordMetadata) -> bool, - F: FnOnce(T, &ReplayMetadata) -> Result<(), ReplayError>, + P: FnOnce(&ReplaySettings, &RecordSettings) -> bool, + F: FnOnce(T, &ReplaySettings) -> Result<(), ReplayError>, { if let Some(buf) = self.replay_buffer_mut() { buf.next_event_if(pred, f) @@ -1448,16 +1457,18 @@ impl StoreOpaque { } } - /// Check if recording or replaying is enabled for the Store - #[inline] - pub fn rr_enabled(&self) -> bool { - self.record_buffer.is_some() || self.replay_buffer.is_some() - } - /// Check if replay is enabled for the Store - #[inline] + /// + /// Note: Defaults to false when `rr` feature is disabled + #[inline(always)] pub fn replay_enabled(&self) -> bool { - self.replay_buffer.is_some() + cfg_if::cfg_if! { + if #[cfg(feature = "rr")] { + self.replay_buffer.is_some() + } else { + false + } + } } #[inline(never)] diff --git a/src/bin/wasmtime.rs b/src/bin/wasmtime.rs index cb6d0d5302f9..54e84969b95d 100644 --- a/src/bin/wasmtime.rs +++ b/src/bin/wasmtime.rs @@ -100,6 +100,7 @@ enum Subcommand { /// /// Note: Minimal configs for deterministic Wasm semantics will be /// enforced during replay by default (NaN canonicalization, deterministic relaxed SIMD) + #[cfg(feature = "rr")] Replay(wasmtime_cli::commands::ReplayCommand), } @@ -113,7 +114,10 @@ impl Wasmtime { match subcommand { #[cfg(feature = "run")] - Subcommand::Run(c) => c.execute(None), + Subcommand::Run(c) => c.execute( + #[cfg(feature = "rr")] + None, + ), #[cfg(feature = "cache")] Subcommand::Config(c) => c.execute(), @@ -139,6 +143,7 @@ impl Wasmtime { #[cfg(feature = "objdump")] Subcommand::Objdump(c) => c.execute(), + #[cfg(feature = "rr")] Subcommand::Replay(c) => c.execute(), } } diff --git a/src/commands.rs b/src/commands.rs index 187c99ef405f..eda254fb97ca 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -40,5 +40,7 @@ mod objdump; #[cfg(feature = "objdump")] pub use self::objdump::*; +#[cfg(feature = "rr")] mod replay; +#[cfg(feature = "rr")] pub use self::replay::*; diff --git a/src/commands/replay.rs b/src/commands/replay.rs index 6b6c4a817e29..a0acdc55837d 100644 --- a/src/commands/replay.rs +++ b/src/commands/replay.rs @@ -4,7 +4,7 @@ use crate::commands::run::RunCommand; use anyhow::Result; use clap::Parser; use std::{fs, io::BufReader, path::PathBuf, sync::Arc}; -use wasmtime::{ReplayConfig, ReplayMetadata}; +use wasmtime::{ReplayConfig, ReplaySettings}; #[derive(Parser)] /// Replay-specific options for CLI @@ -44,7 +44,7 @@ impl ReplayCommand { fs::File::open(&self.replay_opts.trace).unwrap(), )) }), - metadata: ReplayMetadata { + settings: ReplaySettings { validate: self.replay_opts.validate, }, }; diff --git a/src/commands/run.rs b/src/commands/run.rs index d42be6686559..1b92739d346e 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -14,7 +14,9 @@ use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::thread; use wasi_common::sync::{Dir, TcpListener, WasiCtxBuilder, ambient_authority}; -use wasmtime::{Engine, Func, Module, ReplayConfig, Store, StoreLimits, Val, ValType}; +#[cfg(feature = "rr")] +use wasmtime::ReplayConfig; +use wasmtime::{Engine, Func, Module, Store, StoreLimits, Val, ValType}; use wasmtime_wasi::p2::{IoView, WasiView}; #[cfg(feature = "wasi-nn")] @@ -89,7 +91,10 @@ enum CliLinker { impl RunCommand { /// Executes the command. - pub fn execute(mut self, replay_cfg: Option) -> Result<()> { + pub fn execute( + mut self, + #[cfg(feature = "rr")] replay_cfg: Option, + ) -> Result<()> { self.run.common.init_logging()?; let mut config = self.run.common.config(None)?; @@ -109,6 +114,7 @@ impl RunCommand { None => {} } + #[cfg(feature = "rr")] if let Some(cfg) = replay_cfg { config.enable_replay(cfg)?; } From 8ae8f84d58049bbbd8e158aa0cbfaf2437c61048 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Wed, 30 Jul 2025 18:57:22 -0700 Subject: [PATCH 52/62] Add notes about `rr` feature configurability --- Cargo.toml | 3 +++ crates/wasmtime/Cargo.toml | 8 +++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index af055f1506d7..078bfa0021a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -492,6 +492,9 @@ gc-drc = ["gc", "wasmtime/gc-drc", "wasmtime-cli-flags/gc-drc"] gc-null = ["gc", "wasmtime/gc-null", "wasmtime-cli-flags/gc-null"] pulley = ["wasmtime-cli-flags/pulley"] stack-switching = ["wasmtime/stack-switching", "wasmtime-cli-flags/stack-switching"] +# `rr` only configures the base infrastructure and is not practically useful by itself +# Use `rr-component` or `rr-core` for generating record/replay events for components/core wasm +# respectively rr = ["wasmtime-cli-flags/rr"] rr-component = ["wasmtime/rr-component", "rr"] rr-core = ["wasmtime/rr-core", "rr"] diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml index c44cc3453f3b..6eab80f06d97 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -397,9 +397,13 @@ component-model-async = [ "dep:futures" ] + +# `rr` only configures the base infrastructure and is not practically useful by itself +# Use `rr-component` or `rr-core` for generating record/replay events for components/core wasm +# respectively +rr = ["dep:embedded-io", "dep:postcard"] # RR for components rr-component = ["component-model", "rr"] - # RR for core wasm rr-core = ["rr"] @@ -407,5 +411,3 @@ rr-core = ["rr"] # This feature only makes sense if 'rr-component' or 'rr-core' is enabled rr-type-validation = ["rr"] -# Base primtives for any rr capability -rr = ["dep:embedded-io", "dep:postcard"] From fdc6887a1b6cd86dd3ee0483df2e13d1f7da9d03 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Thu, 31 Jul 2025 17:58:41 -0700 Subject: [PATCH 53/62] Refactor type validation across the RR stack --- .../src/runtime/component/func/host.rs | 43 +++------ .../src/runtime/component/func/options.rs | 94 +++++++++++++++---- .../src/runtime/component/instance.rs | 2 +- crates/wasmtime/src/runtime/func.rs | 32 +++---- .../src/runtime/rr/events/component_events.rs | 71 ++++++++------ .../src/runtime/rr/events/core_events.rs | 31 +++--- crates/wasmtime/src/runtime/rr/events/mod.rs | 56 +++++++++-- crates/wasmtime/src/runtime/rr/mod.rs | 52 +++++----- crates/wasmtime/src/runtime/store.rs | 10 +- 9 files changed, 239 insertions(+), 152 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 2bc85ff208da..1071abb7c0bd 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -4,9 +4,7 @@ use crate::component::storage::{slice_to_storage_mut, storage_as_slice, storage_ use crate::component::{ComponentNamedList, ComponentType, Lift, Lower, Val}; use crate::prelude::*; #[cfg(feature = "rr-component")] -use crate::runtime::rr::component_events::{ - HostFuncReturnEvent, LowerReturnEvent, LowerStoreReturnEvent, -}; +use crate::rr::component_events::{HostFuncReturnEvent, LowerReturnEvent, LowerStoreReturnEvent}; use crate::runtime::vm::component::{ ComponentInstance, InstanceFlags, VMComponentContext, VMLowering, VMLoweringCallee, }; @@ -28,23 +26,21 @@ macro_rules! rr_host_func_entry_event { #[cfg(all(feature = "rr-component", feature = "rr-type-validation"))] { use crate::config::ReplaySettings; - use crate::runtime::rr::component_events::HostFuncEntryEvent; + use crate::rr::{Validate, component_events::HostFuncEntryEvent}; $store.record_event_if( |r| r.add_validation, |_| { HostFuncEntryEvent::new( $args, - #[cfg(feature = "rr-type-validation")] Some($param_types), ) }, )?; $store.next_replay_event_if( |_, r| r.add_validation, - |_event: HostFuncEntryEvent, _r: &ReplaySettings| { - #[cfg(feature = "rr-type-validation")] - if _r.validate { - _event.validate($param_types)?; + |event: HostFuncEntryEvent, r: &ReplaySettings| { + if r.validate { + event.validate($param_types)?; } Ok(()) }, @@ -58,11 +54,10 @@ macro_rules! record_host_func_return_event { { $args:expr, $return_types:expr => $store:expr } => { #[cfg(feature = "rr-component")] { - $store.record_event(|_r| { + $store.record_event(|r| { HostFuncReturnEvent::new( $args, - #[cfg(feature = "rr-type-validation")] - _r.add_validation.then_some($return_types), + r.add_validation.then_some($return_types), ) })?; } @@ -402,21 +397,13 @@ where |cx: &mut LowerContext<'_, T>, dst: &mut MaybeUninit<::Lower>| { // This path also stores the final return values in resulting storage - cx.replay_lowering( - Some(storage_as_slice_mut(dst)), - #[cfg(feature = "rr-type-validation")] - expect_tys, - ) + cx.replay_lowering(Some(storage_as_slice_mut(dst)), expect_tys) }; let indirect_results_lower = |cx: &mut LowerContext<'_, T>, _dst: &ValRaw| { // `_dst` is a Wasm pointer to indirect results. This pointer itself will remain // deterministic, and thus replay will not need to change this. However, // replay will have to overwrite any nested stored lowerings (deep copy) - cx.replay_lowering( - None, - #[cfg(feature = "rr-type-validation")] - expect_tys, - ) + cx.replay_lowering(None, expect_tys) }; match self { Storage::Direct(storage) => { @@ -597,20 +584,12 @@ where let result_storage = mem::transmute::<&mut [MaybeUninit], &mut [ValRaw]>(storage); // This path also stores the final return values in resulting storage - cx.replay_lowering( - Some(result_storage), - #[cfg(feature = "rr-type-validation")] - result_tys, - )?; + cx.replay_lowering(Some(result_storage), result_tys)?; } else { // The indirect `ret_ptr` itself will remain deterministic, and thus replay will not // need to change the return storage. However, replay will have to overwrite any nested stored // lowerings (deep copy) - cx.replay_lowering( - None, - #[cfg(feature = "rr-type-validation")] - result_tys, - )?; + cx.replay_lowering(None, result_tys)?; } flags.set_may_leave(true); } diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index 711089f65abd..efac9483c5f6 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -5,11 +5,9 @@ use crate::component::matching::InstanceType; use crate::component::resources::{HostResourceData, HostResourceIndex, HostResourceTables}; use crate::prelude::*; #[cfg(feature = "rr-component")] -use crate::runtime::rr::component_events::{ - MemorySliceWriteEvent, ReallocEntryEvent, ReallocReturnEvent, -}; +use crate::rr::component_events::{MemorySliceWriteEvent, ReallocEntryEvent, ReallocReturnEvent}; #[cfg(feature = "rr-component")] -use crate::runtime::rr::{RREvent, RecordBuffer, Recorder, Replayer}; +use crate::rr::{RREvent, RecordBuffer, Recorder, ReplayError, Replayer, Validate}; use crate::runtime::vm::component::{ CallContexts, ComponentInstance, InstanceFlags, ResourceTable, ResourceTables, }; @@ -589,47 +587,111 @@ impl<'a, T: 'static> LowerContext<'a, T> { pub fn replay_lowering( &mut self, mut result_storage: Option<&mut [ValRaw]>, - #[cfg(feature = "rr-type-validation")] result_tys: &TypeTuple, + result_tys: &TypeTuple, ) -> Result<()> { if self.store.0.replay_buffer_mut().is_none() { return Ok(()); } let mut complete = false; + let mut lowering_error: Option = None; + // No nested expected; these depths should only be 1 + let mut realloc_stack = Vec::>::new(); + let mut lower_stack = Vec::>::new(); + let mut lower_store_stack = Vec::>::new(); while !complete { let buf = self.store.0.replay_buffer_mut().unwrap(); let event = buf.next_event()?; - let _replay_settings = buf.settings(); + let replay_validate = buf.settings().validate; + let trace_has_validation_events = buf.trace_settings().add_validation; match event { RREvent::ComponentHostFuncReturn(e) => { - // End of lowering process - #[cfg(feature = "rr-type-validation")] - if _replay_settings.validate { - e.validate(result_tys)? + if let Some(e) = lowering_error { + return Err(e.into()); } + // End of the lowering process if let Some(storage) = result_storage.as_deref_mut() { - e.move_into_slice(storage); + e.move_into_slice(storage, replay_validate.then_some(result_tys))?; } complete = true; } RREvent::ComponentReallocEntry(e) => { - let _ = self.realloc_inner(e.old_addr, e.old_size, e.old_align, e.new_size); + let _result = + self.realloc_inner(e.old_addr, e.old_size, e.old_align, e.new_size); + #[cfg(feature = "rr-type-validation")] + if trace_has_validation_events && replay_validate { + realloc_stack.push(_result); + } + } + // No return value to validate for lower/lower-store; store error and just check that entry happened before + RREvent::ComponentLowerReturn(e) => { + #[cfg(feature = "rr-type-validation")] + //// TODO: We don't have insertion points for these entry yet! + //if trace_has_validation_events && replay_validate { + // lowering_error = e.validate(&lower_stack.pop().unwrap()).err(); + //} else { + // lowering_error = e.ret().map_err(Into::into).err(); + //} + { + lowering_error = e.ret().map_err(Into::into).err(); + } + #[cfg(not(feature = "rr-type-validation"))] + { + lowering_error = e.ret().map_err(Into::into).err(); + } + } + RREvent::ComponentLowerStoreReturn(e) => { + #[cfg(feature = "rr-type-validation")] + //// TODO: We don't have insertion ponts for these entry yet! + //if trace_has_validation_events && replay_validate { + // lowering_error = e.validate(&lower_store_stack.pop().unwrap()).err(); + //} else { + // lowering_error = e.ret().map_err(Into::into).err(); + //} + { + lowering_error = e.ret().map_err(Into::into).err(); + } + #[cfg(not(feature = "rr-type-validation"))] + { + lowering_error = e.ret().map_err(Into::into).err(); + } } - RREvent::ComponentLowerReturn(e) => e.ret()?, - RREvent::ComponentLowerStoreReturn(e) => e.ret()?, RREvent::ComponentMemorySliceWrite(e) => { // The bounds check is performed here is required here (in the absence of // trace validation) to protect against malicious out-of-bounds slice writes self.as_slice_mut()[e.offset..e.offset + e.bytes.len()] .copy_from_slice(e.bytes.as_slice()); } + // Optional events + // // Realloc or any lowering methods cannot call back to the host. Hence, you cannot // have host calls entries during this method RREvent::ComponentHostFuncEntry(_) => { bail!("Cannot call back into host during lowering") } - _ => { - bail!("Invalid event \'{:?}\' encountered during lowering", event); + // Unwrapping should never occur on valid executions since *Entry should be before *Return in trace + RREvent::ComponentReallocReturn(e) => { + #[cfg(feature = "rr-type-validation")] + if trace_has_validation_events && replay_validate { + lowering_error = e.validate(&realloc_stack.pop().unwrap()).err() + } else { + // Error is subsumed by the LowerReturn or the LowerStoreReturn + lowering_error = None + } + } + RREvent::ComponentLowerEntry(_) => { + #[cfg(feature = "rr-type-validation")] + if trace_has_validation_events && replay_validate { + lower_stack.push(Ok(())) + } + } + RREvent::ComponentLowerStoreEntry(_) => { + #[cfg(feature = "rr-type-validation")] + if trace_has_validation_events && replay_validate { + lower_store_stack.push(Ok(())) + } } + + _ => bail!("Invalid event \'{:?}\' encountered during lowering", event), }; } Ok(()) diff --git a/crates/wasmtime/src/runtime/component/instance.rs b/crates/wasmtime/src/runtime/component/instance.rs index 1ac09989a841..12e2daf09732 100644 --- a/crates/wasmtime/src/runtime/component/instance.rs +++ b/crates/wasmtime/src/runtime/component/instance.rs @@ -9,7 +9,7 @@ use crate::instance::OwnedImports; use crate::linker::DefinitionType; use crate::prelude::*; #[cfg(feature = "rr-component")] -use crate::rr::component_events::InstantiationEvent; +use crate::rr::{Validate, component_events::InstantiationEvent}; use crate::runtime::vm::VMFuncRef; use crate::runtime::vm::component::{ComponentInstance, OwnedComponentInstance}; use crate::store::StoreOpaque; diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index f4ffbb67e79c..dbf1b1be7c81 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -2334,38 +2334,35 @@ impl HostContext { let func = &state.func; #[cfg(feature = "rr-core")] - let wasm_func_type_arc = { + let wasm_func_type = { let type_index = vmctx.func_ref().type_index; caller.engine().signatures().borrow(type_index).unwrap() }; - #[cfg(feature = "rr-core")] - let wasm_func_type = wasm_func_type_arc.unwrap_func(); #[cfg(all(feature = "rr-core", feature = "rr-type-validation"))] { // Record/replay interceptions of raw parameters args use crate::config::ReplaySettings; - use crate::rr::core_events::HostFuncEntryEvent; + use crate::rr::{Validate, core_events::HostFuncEntryEvent}; caller.store.0.record_event_if( |r| r.add_validation, |_| { - let num_params = wasm_func_type.params().len(); + let func_type = wasm_func_type.unwrap_func(); + let num_params = func_type.params().len(); HostFuncEntryEvent::new( &args.as_ref()[..num_params], // Don't need to check validation here since it is // covered by the push predicate in this case - #[cfg(feature = "rr-type-validation")] - Some(wasm_func_type.clone()), + Some(func_type.clone()), ) }, )?; // Don't need to auto-assert GC here since we aren't using P caller.store.0.next_replay_event_if( |_, r| r.add_validation, - |_event: HostFuncEntryEvent, _r: &ReplaySettings| { - #[cfg(feature = "rr-type-validation")] - if _r.validate { - _event.validate(wasm_func_type)?; + |event: HostFuncEntryEvent, r: &ReplaySettings| { + if r.validate { + event.validate(wasm_func_type.unwrap_func())?; } Ok(()) }, @@ -2405,12 +2402,12 @@ impl HostContext { } #[cfg(feature = "rr-core")] // Record the return value of store - caller.store.0.record_event(|_rmeta| { - let num_results = wasm_func_type.params().len(); + caller.store.0.record_event(|rmeta| { + let func_type = wasm_func_type.unwrap_func(); + let num_results = func_type.params().len(); HostFuncReturnEvent::new( unsafe { &args.as_ref()[..num_results] }, - #[cfg(feature = "rr-type-validation")] - _rmeta.add_validation.then_some(wasm_func_type.clone()), + rmeta.add_validation.then_some(func_type.clone()), ) })?; } else { @@ -2419,11 +2416,10 @@ impl HostContext { caller .store .0 - .next_replay_event_and(|event: HostFuncReturnEvent, _rmeta| { + .next_replay_event_and(|event: HostFuncReturnEvent, rmeta| { event.move_into_slice( args.as_mut(), - #[cfg(feature = "rr-type-validation")] - _rmeta.validate.then_some(wasm_func_type), + rmeta.validate.then_some(wasm_func_type.unwrap_func()), ) })?; } diff --git a/crates/wasmtime/src/runtime/rr/events/component_events.rs b/crates/wasmtime/src/runtime/rr/events/component_events.rs index 7adaf39e70e6..bbe5281858eb 100644 --- a/crates/wasmtime/src/runtime/rr/events/component_events.rs +++ b/crates/wasmtime/src/runtime/rr/events/component_events.rs @@ -4,7 +4,6 @@ use super::*; #[expect(unused_imports, reason = "used for doc-links")] use crate::component::{Component, ComponentType}; use wasmtime_environ::component::InterfaceType; -#[cfg(feature = "rr-type-validation")] use wasmtime_environ::component::TypeTuple; /// A [`Component`] instantiatation event @@ -20,10 +19,11 @@ impl InstantiationEvent { checksum: *component.checksum(), } } - +} +impl Validate for InstantiationEvent { /// Validate that checksums match - pub fn validate(self, component: &Component) -> Result<(), ReplayError> { - if self.checksum != *component.checksum() { + fn validate(&self, expect_component: &Component) -> Result<(), ReplayError> { + if self.checksum != *expect_component.checksum() { Err(ReplayError::FailedModuleValidation) } else { Ok(()) @@ -42,24 +42,19 @@ pub struct HostFuncEntryEvent { /// Note: This relies on the invariant that [InterfaceType] will always be /// deterministic. Currently, the type indices into various [ComponentTypes] /// maintain this, allowing for quick type-checking. - #[cfg(feature = "rr-type-validation")] types: Option, } impl HostFuncEntryEvent { // Record - pub fn new( - args: &[MaybeUninit], - #[cfg(feature = "rr-type-validation")] types: Option<&TypeTuple>, - ) -> Self { + pub fn new(args: &[MaybeUninit], types: Option<&TypeTuple>) -> Self { Self { args: func_argvals_from_raw_slice(args), - #[cfg(feature = "rr-type-validation")] types: types.cloned(), } } - // Replay - #[cfg(feature = "rr-type-validation")] - pub fn validate(&self, expect_types: &TypeTuple) -> Result<(), ReplayError> { +} +impl Validate for HostFuncEntryEvent { + fn validate(&self, expect_types: &TypeTuple) -> Result<(), ReplayError> { replay_args_typecheck(self.types.as_ref(), expect_types) } } @@ -76,31 +71,32 @@ pub struct HostFuncReturnEvent { /// Note: This relies on the invariant that [InterfaceType] will always be /// deterministic. Currently, the type indices into various [ComponentTypes] /// maintain this, allowing for quick type-checking. - #[cfg(feature = "rr-type-validation")] types: Option, } impl HostFuncReturnEvent { - // Record - pub fn new( - args: &[ValRaw], - #[cfg(feature = "rr-type-validation")] types: Option<&TypeTuple>, - ) -> Self { + pub fn new(args: &[ValRaw], types: Option<&TypeTuple>) -> Self { Self { args: func_argvals_from_raw_slice(args), - #[cfg(feature = "rr-type-validation")] types: types.cloned(), } } - // Replay - #[cfg(feature = "rr-type-validation")] - pub fn validate(&self, expect_types: &TypeTuple) -> Result<(), ReplayError> { - replay_args_typecheck(self.types.as_ref(), expect_types) - } - /// Consume the caller event and encode it back into the slice with an optional - /// typechecking validation of the event. - pub fn move_into_slice(self, args: &mut [ValRaw]) { + /// Consume the caller event and encode it back into the slice + pub fn move_into_slice( + self, + args: &mut [ValRaw], + expect_types: Option<&TypeTuple>, + ) -> Result<(), ReplayError> { + if let Some(e) = expect_types { + self.validate(e)?; + } func_argvals_into_raw_slice(self.args, args); + Ok(()) + } +} +impl Validate for HostFuncReturnEvent { + fn validate(&self, expect_types: &TypeTuple) -> Result<(), ReplayError> { + replay_args_typecheck(self.types.as_ref(), expect_types) } } @@ -124,9 +120,26 @@ macro_rules! generic_new_result_events { ret: ret.as_ref().map(|t| *t).map_err(|e| $err_variant(e.to_string())) } } - #[inline] pub fn ret(self) -> Result<$ok_ty, EventActionError> { self.ret } } + + impl Validate> for $event { + fn validate(&self, expect_ret: &Result<$ok_ty>) -> Result<(), ReplayError> { + // Cannot just use eq since anyhow::Error and EventActionError cannot be compared + match (self.ret.as_ref(), expect_ret.as_ref()) { + (Ok(r), Ok(s)) => replay_args_valcheck(r, s), + // Return the recorded error + (Err(e), Err(f)) => Err(ReplayError::from($err_variant(format!( + "Replayed Error: {} \nRecorded Error: {}", + e, f + )))), + // Diverging errors.. Report as a failed validation + (Ok(_), Err(_)) => Err(ReplayError::FailedFuncValidation), + (Err(_), Ok(_)) => Err(ReplayError::FailedFuncValidation), + } + } + } + )* ); } diff --git a/crates/wasmtime/src/runtime/rr/events/core_events.rs b/crates/wasmtime/src/runtime/rr/events/core_events.rs index 3f5c6ba4e9a3..28d167f1c8bc 100644 --- a/crates/wasmtime/src/runtime/rr/events/core_events.rs +++ b/crates/wasmtime/src/runtime/rr/events/core_events.rs @@ -4,7 +4,6 @@ use super::*; use wasmtime_environ::{WasmFuncType, WasmValType}; /// Note: Switch [`CoreFuncArgTypes`] to use [`Vec`] for better efficiency -#[cfg(feature = "rr-type-validation")] type CoreFuncArgTypes = WasmFuncType; /// A call event from a Core Wasm module into the host @@ -13,24 +12,19 @@ pub struct HostFuncEntryEvent { /// Raw values passed across the call/return boundary args: RRFuncArgVals, /// Optional param/return types (required to support replay validation) - #[cfg(feature = "rr-type-validation")] types: Option, } impl HostFuncEntryEvent { // Record - pub fn new( - args: &[MaybeUninit], - #[cfg(feature = "rr-type-validation")] types: Option, - ) -> Self { + pub fn new(args: &[MaybeUninit], types: Option) -> Self { Self { args: func_argvals_from_raw_slice(args), - #[cfg(feature = "rr-type-validation")] types: types, } } - // Replay - #[cfg(feature = "rr-type-validation")] - pub fn validate(&self, expect_types: &CoreFuncArgTypes) -> Result<(), ReplayError> { +} +impl Validate for HostFuncEntryEvent { + fn validate(&self, expect_types: &CoreFuncArgTypes) -> Result<(), ReplayError> { replay_args_typecheck(self.types.as_ref(), expect_types) } } @@ -43,18 +37,13 @@ pub struct HostFuncReturnEvent { /// Raw values passed across the call/return boundary args: RRFuncArgVals, /// Optional param/return types (required to support replay validation) - #[cfg(feature = "rr-type-validation")] types: Option, } impl HostFuncReturnEvent { // Record - pub fn new( - args: &[MaybeUninit], - #[cfg(feature = "rr-type-validation")] types: Option, - ) -> Self { + pub fn new(args: &[MaybeUninit], types: Option) -> Self { Self { args: func_argvals_from_raw_slice(args), - #[cfg(feature = "rr-type-validation")] types: types, } } @@ -64,13 +53,17 @@ impl HostFuncReturnEvent { pub fn move_into_slice( self, args: &mut [MaybeUninit], - #[cfg(feature = "rr-type-validation")] expect_types: Option<&WasmFuncType>, + expect_types: Option<&WasmFuncType>, ) -> Result<(), ReplayError> { - #[cfg(feature = "rr-type-validation")] if let Some(e) = expect_types { - replay_args_typecheck(self.types.as_ref(), e)?; + self.validate(e)?; } func_argvals_into_raw_slice(self.args, args); Ok(()) } } +impl Validate for HostFuncReturnEvent { + fn validate(&self, expect_types: &CoreFuncArgTypes) -> Result<(), ReplayError> { + replay_args_typecheck(self.types.as_ref(), expect_types) + } +} diff --git a/crates/wasmtime/src/runtime/rr/events/mod.rs b/crates/wasmtime/src/runtime/rr/events/mod.rs index 68e24171c98d..480146f26b79 100644 --- a/crates/wasmtime/src/runtime/rr/events/mod.rs +++ b/crates/wasmtime/src/runtime/rr/events/mod.rs @@ -10,7 +10,11 @@ use serde::{Deserialize, Serialize}; /// /// We need this since the [anyhow::Error] trait object cannot be used. This /// type just encapsulates the corresponding display messages during recording -/// so that it can be validated and/or re-thrown during replay +/// so that it can be re-thrown during replay +/// +/// Unforunately since we cannot serialize [anyhow::Error], there's no good +/// way to equate errors across record/replay boundary without creating a +/// common error format. Perhaps this is future work #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum EventActionError { ReallocError(String), @@ -90,21 +94,57 @@ where /// Typechecking validation for replay, if `src_types` exist /// /// Returns [`ReplayError::FailedFuncValidation`] if typechecking fails -#[cfg(feature = "rr-type-validation")] +#[inline(always)] fn replay_args_typecheck(src_types: Option, expect_types: T) -> Result<(), ReplayError> where T: PartialEq, { - if let Some(types) = src_types { - if types == expect_types { + #[cfg(feature = "rr-type-validation")] + { + if let Some(types) = src_types { + if types == expect_types { + Ok(()) + } else { + Err(ReplayError::FailedFuncValidation) + } + } else { + println!( + "Warning: Replay typechecking cannot be performed since recorded trace is missing validation data" + ); + Ok(()) + } + } + #[cfg(not(feature = "rr-type-validation"))] + Ok(()) +} + +/// Validation of values +#[inline(always)] +fn replay_args_valcheck(src_val: T, expect_val: T) -> Result<(), ReplayError> +where + T: PartialEq, +{ + #[cfg(feature = "rr-type-validation")] + { + if src_val == expect_val { Ok(()) } else { Err(ReplayError::FailedFuncValidation) } - } else { - println!( - "Warning: Replay typechecking cannot be performed since recorded trace is missing validation data" - ); + } + #[cfg(not(feature = "rr-type-validation"))] + Ok(()) +} + +/// Trait signifying types that can be validated on replay +/// +/// Default nop behavior when `rr-validate` is disabled. +/// Note howeverthat some [`Validate`] overriden implementations are present even +/// when feature `rr-validate` is disabled, when validation is needed +/// for a faithful replay. +pub trait Validate { + /// Perform a validation of the event to ensure replay consistency + fn validate(&self, _expect_t: &T) -> Result<(), ReplayError> { Ok(()) } } diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index 21bc48ecc17f..9a81e5d80411 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -10,18 +10,24 @@ use crate::config::{ModuleVersionStrategy, RecordSettings, ReplaySettings}; use crate::prelude::*; use core::fmt; -use serde::{Deserialize, Serialize}; - -/// Encapsulation of event types comprising an [`RREvent`] sum type -mod events; use events::EventActionError; - +use serde::{Deserialize, Serialize}; +// Use component/core events internally even without feature flags enabled +// so that [`RREvent`] has a well-defined serialization format, but export +// it for other modules only when enabled +pub use events::Validate; +use events::component_events as __component_events; +#[cfg(feature = "rr-component")] pub use events::component_events; +use events::core_events as __core_events; +#[cfg(feature = "rr-core")] pub use events::core_events; +pub use io::{RecordWriter, ReplayReader}; +/// Encapsulation of event types comprising an [`RREvent`] sum type +mod events; /// I/O support for reading and writing traces mod io; -pub use io::{RecordWriter, ReplayReader}; /// Macro template for [`RREvent`] and its conversion to/from specific /// event types @@ -81,35 +87,37 @@ macro_rules! rr_event { // Set of supported record/replay events rr_event! { /// Call into host function from Core Wasm - CoreHostFuncEntry(core_events::HostFuncEntryEvent), + CoreHostFuncEntry(__core_events::HostFuncEntryEvent), /// Return from host function to Core Wasm - CoreHostFuncReturn(core_events::HostFuncReturnEvent), + CoreHostFuncReturn(__core_events::HostFuncReturnEvent), // REQUIRED events for replay // /// Instantiation of a component - ComponentInstantiation(component_events::InstantiationEvent), + ComponentInstantiation(__component_events::InstantiationEvent), /// Return from host function to component - ComponentHostFuncReturn(component_events::HostFuncReturnEvent), + ComponentHostFuncReturn(__component_events::HostFuncReturnEvent), /// Component ABI realloc call in linear wasm memory - ComponentReallocEntry(component_events::ReallocEntryEvent), + ComponentReallocEntry(__component_events::ReallocEntryEvent), /// Return from a type lowering operation - ComponentLowerReturn(component_events::LowerReturnEvent), + ComponentLowerReturn(__component_events::LowerReturnEvent), /// Return from a store during a type lowering operation - ComponentLowerStoreReturn(component_events::LowerStoreReturnEvent), + ComponentLowerStoreReturn(__component_events::LowerStoreReturnEvent), /// An attempt to obtain a mutable slice into Wasm linear memory - ComponentMemorySliceWrite(component_events::MemorySliceWriteEvent), + ComponentMemorySliceWrite(__component_events::MemorySliceWriteEvent), // OPTIONAL events for replay validation // + // ReallocReturn is optional because we can assume the realloc is deterministic + // and the error message is subsumed by the containing LowerReturn/LowerStoreReturn + /// Return from Component ABI realloc call + ComponentReallocReturn(__component_events::ReallocReturnEvent), /// Call into host function from component - ComponentHostFuncEntry(component_events::HostFuncEntryEvent), + ComponentHostFuncEntry(__component_events::HostFuncEntryEvent), /// Call into [Lower::lower] for type lowering - ComponentLowerEntry(component_events::LowerEntryEvent), + ComponentLowerEntry(__component_events::LowerEntryEvent), /// Call into [Lower::store] during type lowering - ComponentLowerStoreEntry(component_events::LowerStoreEntryEvent), - /// Return from Component ABI realloc call - ComponentReallocReturn(component_events::ReallocReturnEvent) + ComponentLowerStoreEntry(__component_events::LowerStoreEntryEvent) } /// Error type signalling failures during a replay run @@ -437,11 +445,7 @@ mod tests { // Record values let mut recorder = RecordBuffer::new_recorder(Box::new(File::create(tmppath)?), record_settings)?; - let event = component_wasm::HostFuncReturnEvent::new( - values.as_slice(), - #[cfg(feature = "rr-type-validation")] - None, - ); + let event = component_wasm::HostFuncReturnEvent::new(values.as_slice(), None); recorder.record_event(|_| event.clone())?; recorder.flush()?; diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index 63efb2b8d8f1..977e0ce1b22d 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -80,7 +80,7 @@ use crate::RootSet; use crate::module::RegisteredModuleId; use crate::prelude::*; #[cfg(feature = "rr")] -use crate::runtime::rr::{RREvent, RecordBuffer, Recorder, ReplayBuffer, ReplayError, Replayer}; +use crate::rr::{RREvent, RecordBuffer, Recorder, ReplayBuffer, ReplayError, Replayer}; #[cfg(feature = "gc")] use crate::runtime::vm::GcRootsList; #[cfg(feature = "stack-switching")] @@ -1374,13 +1374,13 @@ impl StoreOpaque { } #[cfg(feature = "rr")] - #[inline] + #[inline(always)] pub fn record_buffer_mut(&mut self) -> Option<&mut RecordBuffer> { self.record_buffer.as_mut() } #[cfg(feature = "rr")] - #[inline] + #[inline(always)] pub fn replay_buffer_mut(&mut self) -> Option<&mut ReplayBuffer> { self.replay_buffer.as_mut() } @@ -1389,7 +1389,7 @@ impl StoreOpaque { /// /// Convenience wrapper around [`Recorder::record_event`] #[cfg(feature = "rr")] - #[inline] + #[inline(always)] pub(crate) fn record_event(&mut self, f: F) -> Result<()> where T: Into, @@ -1406,7 +1406,7 @@ impl StoreOpaque { /// /// Convenience wrapper around [`Recorder::record_event_if`] #[cfg(feature = "rr")] - #[inline] + #[inline(always)] pub(crate) fn record_event_if(&mut self, pred: P, f: F) -> Result<()> where T: Into, From b500f074c0dc3a9486c0931d33ec8c3e25336689 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Thu, 31 Jul 2025 19:09:31 -0700 Subject: [PATCH 54/62] Transition away from macros in component RR --- .../src/runtime/component/func/host.rs | 160 +++++++++--------- 1 file changed, 81 insertions(+), 79 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 1071abb7c0bd..51ab2bd129ef 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -9,6 +9,7 @@ use crate::runtime::vm::component::{ ComponentInstance, InstanceFlags, VMComponentContext, VMLowering, VMLoweringCallee, }; use crate::runtime::vm::{VMFuncRef, VMGlobalDefinition, VMMemoryDefinition, VMOpaqueContext}; +use crate::store::StoreOpaque; use crate::{AsContextMut, CallHook, StoreContextMut, ValRaw}; use alloc::sync::Arc; use core::any::Any; @@ -20,68 +21,78 @@ use wasmtime_environ::component::{ StringEncoding, TypeFuncIndex, }; -/// Record/replay stubs for host function entry events -macro_rules! rr_host_func_entry_event { - { $args:expr, $param_types:expr => $store:expr } => { - #[cfg(all(feature = "rr-component", feature = "rr-type-validation"))] - { - use crate::config::ReplaySettings; - use crate::rr::{Validate, component_events::HostFuncEntryEvent}; - $store.record_event_if( - |r| r.add_validation, - |_| { - HostFuncEntryEvent::new( - $args, - Some($param_types), - ) - }, - )?; - $store.next_replay_event_if( - |_, r| r.add_validation, - |event: HostFuncEntryEvent, r: &ReplaySettings| { - if r.validate { - event.validate($param_types)?; - } - Ok(()) - }, - )?; - } - }; +/// Record/replay hook operation for host function entry events +#[inline] +fn record_replay_hook_host_func_entry( + args: &mut [MaybeUninit], + param_types: &TypeTuple, + store: &mut StoreOpaque, +) -> Result<()> { + #[cfg(all(feature = "rr-component", feature = "rr-type-validation"))] + { + use crate::config::ReplaySettings; + use crate::rr::{Validate, component_events::HostFuncEntryEvent}; + store.record_event_if( + |r| r.add_validation, + |_| HostFuncEntryEvent::new(args, Some(param_types)), + )?; + store.next_replay_event_if( + |_, r| r.add_validation, + |event: HostFuncEntryEvent, r: &ReplaySettings| { + if r.validate { + event.validate(param_types)?; + } + Ok(()) + }, + )?; + } + let _ = (args, param_types, store); + Ok(()) } -/// Record stubs for host function return events -macro_rules! record_host_func_return_event { - { $args:expr, $return_types:expr => $store:expr } => { - #[cfg(feature = "rr-component")] - { - $store.record_event(|r| { - HostFuncReturnEvent::new( - $args, - r.add_validation.then_some($return_types), - ) - })?; - } - }; +/// Record hook operation for host function return events +#[inline] +fn record_hook_host_func_return( + args: &[ValRaw], + result_types: &TypeTuple, + store: &mut StoreOpaque, +) -> Result<()> { + #[cfg(feature = "rr-component")] + { + store.record_event(|r| { + HostFuncReturnEvent::new(args, r.add_validation.then_some(result_types)) + })?; + } + let _ = (args, result_types, store); + Ok(()) } -/// Record stubs for store events of component types -macro_rules! record_lower_store_event_wrapper { - { $lower_store:expr => $store:expr } => {{ - let store_result = $lower_store; - #[cfg(feature = "rr-component")] - $store.record_event(|_| LowerStoreReturnEvent::new(&store_result))?; - store_result - }}; +/// Record hook wrapping a lowering `store` call of component types +#[inline] +fn record_hook_wrapper_lower_store(lower_store: F, cx: &mut LowerContext<'_, T>) -> Result<()> +where + F: FnOnce(&mut LowerContext<'_, T>) -> Result<()>, +{ + let store_result = lower_store(cx); + #[cfg(feature = "rr-component")] + cx.store + .0 + .record_event(|_| LowerStoreReturnEvent::new(&store_result))?; + store_result } -/// Record stubs for lower events of component types -macro_rules! record_lower_event_wrapper { - { $lower:expr => $store:expr } => {{ - let lower_result = $lower; - #[cfg(feature = "rr-component")] - $store.record_event(|_| LowerReturnEvent::new(&lower_result))?; - lower_result - }}; +/// Record hook wrapping a lowering `lower` call of component types +#[inline] +fn record_hook_wrapper_lower(lower: F, cx: &mut LowerContext<'_, T>) -> Result<()> +where + F: FnOnce(&mut LowerContext<'_, T>) -> Result<()>, +{ + let lower_result = lower(cx); + #[cfg(feature = "rr-component")] + cx.store + .0 + .record_event(|_| LowerReturnEvent::new(&lower_result))?; + lower_result } pub struct HostFunc { @@ -270,7 +281,7 @@ where let param_tys = InterfaceType::Tuple(ty.params); let result_tys = InterfaceType::Tuple(ty.results); - rr_host_func_entry_event! { storage, &types[ty.params] => cx.0 }; + record_replay_hook_host_func_entry(storage, &types[ty.params], cx.0)?; // There's a 2x2 matrix of whether parameters and results are stored on the // stack or on the heap. Each of the 4 branches here have a different @@ -357,20 +368,15 @@ where let direct_results_lower = |cx: &mut LowerContext<'_, T>, dst: &mut MaybeUninit<::Lower>, ret: R| { - let res = record_lower_event_wrapper! { ret.lower(cx, ty, dst) => cx.store.0 }; - record_host_func_return_event! { - storage_as_slice(dst), record_tys => cx.store.0 - }; + let res = record_hook_wrapper_lower(|cx| ret.lower(cx, ty, dst), cx); + record_hook_host_func_return(storage_as_slice(dst), record_tys, cx.store.0)?; res }; let indirect_results_lower = |cx: &mut LowerContext<'_, T>, dst: &ValRaw, ret: R| { let ptr = validate_inbounds::(cx.as_slice(), dst)?; - let res = - record_lower_store_event_wrapper! { ret.store(cx, ty, ptr) => cx.store.0 }; + let res = record_hook_wrapper_lower_store(|cx| ret.store(cx, ty, ptr), cx); // Recording here is just for marking the return event - record_host_func_return_event! { - &[], record_tys => cx.store.0 - }; + record_hook_host_func_return(&[], record_tys, cx.store.0)?; res }; match self { @@ -499,7 +505,7 @@ where let param_tys = &types[func_ty.params]; let result_tys = &types[func_ty.results]; - rr_host_func_entry_event! { storage, &types[func_ty.params] => store.0 }; + record_replay_hook_host_func_entry(storage, &types[func_ty.params], store.0)?; if !store.0.replay_enabled() { let args; @@ -549,27 +555,23 @@ where if let Some(cnt) = result_tys.abi.flat_count(MAX_FLAT_RESULTS) { let mut dst = storage[..cnt].iter_mut(); for (val, ty) in result_vals.iter().zip(result_tys.types.iter()) { - record_lower_event_wrapper! { - val.lower(&mut cx, *ty, &mut dst) => cx.store.0 - }?; + record_hook_wrapper_lower(|cx| val.lower(cx, *ty, &mut dst), &mut cx)?; } assert!(dst.next().is_none()); - record_host_func_return_event! { - mem::transmute::<&[MaybeUninit], &[ValRaw]>(storage), result_tys => cx.store.0 - }; + record_hook_host_func_return( + mem::transmute::<&[MaybeUninit], &[ValRaw]>(storage), + result_tys, + cx.store.0, + )?; } else { let ret_ptr = storage[ret_index].assume_init_ref(); let mut ptr = validate_inbounds_dynamic(&result_tys.abi, cx.as_slice(), ret_ptr)?; for (val, ty) in result_vals.iter().zip(result_tys.types.iter()) { let offset = types.canonical_abi(ty).next_field32_size(&mut ptr); - record_lower_store_event_wrapper! { - val.store(&mut cx, *ty, offset) => cx.store.0 - }?; + record_hook_wrapper_lower_store(|cx| val.store(cx, *ty, offset), &mut cx)?; } // Recording here is just for marking the return event - record_host_func_return_event! { - &[], result_tys => cx.store.0 - }; + record_hook_host_func_return(&[], result_tys, cx.store.0)?; } flags.set_may_leave(true); From a468a11a4fb8bc09945603d899b8af5005334da8 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 5 Aug 2025 11:34:39 -0700 Subject: [PATCH 55/62] Cleanup core RR funcs and validation flows --- .../src/runtime/component/func/host.rs | 190 ++++++++++-------- .../src/runtime/component/func/options.rs | 75 +++---- crates/wasmtime/src/runtime/func.rs | 149 +++++++++----- .../src/runtime/rr/events/component_events.rs | 41 ++-- .../src/runtime/rr/events/core_events.rs | 2 + crates/wasmtime/src/runtime/rr/events/mod.rs | 9 +- crates/wasmtime/src/runtime/rr/mod.rs | 4 + 7 files changed, 281 insertions(+), 189 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 51ab2bd129ef..a68295ec9991 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -1,10 +1,8 @@ use crate::component::func::{LiftContext, LowerContext, Options}; use crate::component::matching::InstanceType; -use crate::component::storage::{slice_to_storage_mut, storage_as_slice, storage_as_slice_mut}; +use crate::component::storage::{slice_to_storage_mut, storage_as_slice}; use crate::component::{ComponentNamedList, ComponentType, Lift, Lower, Val}; use crate::prelude::*; -#[cfg(feature = "rr-component")] -use crate::rr::component_events::{HostFuncReturnEvent, LowerReturnEvent, LowerStoreReturnEvent}; use crate::runtime::vm::component::{ ComponentInstance, InstanceFlags, VMComponentContext, VMLowering, VMLoweringCallee, }; @@ -21,78 +19,102 @@ use wasmtime_environ::component::{ StringEncoding, TypeFuncIndex, }; -/// Record/replay hook operation for host function entry events -#[inline] -fn record_replay_hook_host_func_entry( - args: &mut [MaybeUninit], - param_types: &TypeTuple, - store: &mut StoreOpaque, -) -> Result<()> { - #[cfg(all(feature = "rr-component", feature = "rr-type-validation"))] - { - use crate::config::ReplaySettings; - use crate::rr::{Validate, component_events::HostFuncEntryEvent}; - store.record_event_if( - |r| r.add_validation, - |_| HostFuncEntryEvent::new(args, Some(param_types)), - )?; - store.next_replay_event_if( - |_, r| r.add_validation, - |event: HostFuncEntryEvent, r: &ReplaySettings| { - if r.validate { - event.validate(param_types)?; - } - Ok(()) - }, - )?; +/// Convenience methods to inject record + replay logic +mod rr_hooks { + use super::*; + #[cfg(feature = "rr-component")] + use crate::rr::component_events::{ + HostFuncReturnEvent, LowerEntryEvent, LowerReturnEvent, LowerStoreEntryEvent, + LowerStoreReturnEvent, + }; + /// Record/replay hook operation for host function entry events + #[inline] + pub fn record_replay_host_func_entry( + args: &mut [MaybeUninit], + param_types: &TypeTuple, + store: &mut StoreOpaque, + ) -> Result<()> { + #[cfg(all(feature = "rr-component", feature = "rr-type-validation"))] + { + use crate::config::ReplaySettings; + use crate::rr::{Validate, component_events::HostFuncEntryEvent}; + store.record_event_if( + |r| r.add_validation, + |_| HostFuncEntryEvent::new(args, Some(param_types)), + )?; + store.next_replay_event_if( + |_, r| r.add_validation, + |event: HostFuncEntryEvent, r: &ReplaySettings| { + if r.validate { + event.validate(param_types)?; + } + Ok(()) + }, + )?; + } + let _ = (args, param_types, store); + Ok(()) } - let _ = (args, param_types, store); - Ok(()) -} -/// Record hook operation for host function return events -#[inline] -fn record_hook_host_func_return( - args: &[ValRaw], - result_types: &TypeTuple, - store: &mut StoreOpaque, -) -> Result<()> { - #[cfg(feature = "rr-component")] - { - store.record_event(|r| { - HostFuncReturnEvent::new(args, r.add_validation.then_some(result_types)) - })?; + /// Record hook operation for host function return events + #[inline] + pub fn record_host_func_return( + args: &[ValRaw], + result_types: &TypeTuple, + store: &mut StoreOpaque, + ) -> Result<()> { + #[cfg(feature = "rr-component")] + { + store.record_event(|r| { + HostFuncReturnEvent::new(args, r.add_validation.then_some(result_types)) + })?; + } + let _ = (args, result_types, store); + Ok(()) } - let _ = (args, result_types, store); - Ok(()) -} -/// Record hook wrapping a lowering `store` call of component types -#[inline] -fn record_hook_wrapper_lower_store(lower_store: F, cx: &mut LowerContext<'_, T>) -> Result<()> -where - F: FnOnce(&mut LowerContext<'_, T>) -> Result<()>, -{ - let store_result = lower_store(cx); - #[cfg(feature = "rr-component")] - cx.store - .0 - .record_event(|_| LowerStoreReturnEvent::new(&store_result))?; - store_result -} + /// Record hook wrapping a lowering `store` call of component types + #[inline] + pub fn record_lower_store( + lower_store: F, + cx: &mut LowerContext<'_, T>, + ty: InterfaceType, + offset: usize, + ) -> Result<()> + where + F: FnOnce(&mut LowerContext<'_, T>, InterfaceType, usize) -> Result<()>, + { + #[cfg(all(feature = "rr-component", feature = "rr-type-validation"))] + cx.store + .0 + .record_event(|_| LowerStoreEntryEvent::new(ty, offset))?; + let store_result = lower_store(cx, ty, offset); + #[cfg(feature = "rr-component")] + cx.store + .0 + .record_event(|_| LowerStoreReturnEvent::new(&store_result))?; + store_result + } -/// Record hook wrapping a lowering `lower` call of component types -#[inline] -fn record_hook_wrapper_lower(lower: F, cx: &mut LowerContext<'_, T>) -> Result<()> -where - F: FnOnce(&mut LowerContext<'_, T>) -> Result<()>, -{ - let lower_result = lower(cx); - #[cfg(feature = "rr-component")] - cx.store - .0 - .record_event(|_| LowerReturnEvent::new(&lower_result))?; - lower_result + /// Record hook wrapping a lowering `lower` call of component types + #[inline] + pub fn record_lower( + lower: F, + cx: &mut LowerContext<'_, T>, + ty: InterfaceType, + ) -> Result<()> + where + F: FnOnce(&mut LowerContext<'_, T>, InterfaceType) -> Result<()>, + { + #[cfg(all(feature = "rr-component", feature = "rr-type-validation"))] + cx.store.0.record_event(|_| LowerEntryEvent::new(ty))?; + let lower_result = lower(cx, ty); + #[cfg(feature = "rr-component")] + cx.store + .0 + .record_event(|_| LowerReturnEvent::new(&lower_result))?; + lower_result + } } pub struct HostFunc { @@ -281,7 +303,7 @@ where let param_tys = InterfaceType::Tuple(ty.params); let result_tys = InterfaceType::Tuple(ty.results); - record_replay_hook_host_func_entry(storage, &types[ty.params], cx.0)?; + rr_hooks::record_replay_host_func_entry(storage, &types[ty.params], cx.0)?; // There's a 2x2 matrix of whether parameters and results are stored on the // stack or on the heap. Each of the 4 branches here have a different @@ -368,15 +390,16 @@ where let direct_results_lower = |cx: &mut LowerContext<'_, T>, dst: &mut MaybeUninit<::Lower>, ret: R| { - let res = record_hook_wrapper_lower(|cx| ret.lower(cx, ty, dst), cx); - record_hook_host_func_return(storage_as_slice(dst), record_tys, cx.store.0)?; + let res = rr_hooks::record_lower(|cx, ty| ret.lower(cx, ty, dst), cx, ty); + rr_hooks::record_host_func_return(storage_as_slice(dst), record_tys, cx.store.0)?; res }; let indirect_results_lower = |cx: &mut LowerContext<'_, T>, dst: &ValRaw, ret: R| { let ptr = validate_inbounds::(cx.as_slice(), dst)?; - let res = record_hook_wrapper_lower_store(|cx| ret.store(cx, ty, ptr), cx); + let res = + rr_hooks::record_lower_store(|cx, ty, ptr| ret.store(cx, ty, ptr), cx, ty, ptr); // Recording here is just for marking the return event - record_hook_host_func_return(&[], record_tys, cx.store.0)?; + rr_hooks::record_host_func_return(&[], record_tys, cx.store.0)?; res }; match self { @@ -399,6 +422,8 @@ where cx: &mut LowerContext<'_, T>, expect_tys: &TypeTuple, ) -> Result<()> { + use crate::component::storage::storage_as_slice_mut; + let direct_results_lower = |cx: &mut LowerContext<'_, T>, dst: &mut MaybeUninit<::Lower>| { @@ -505,7 +530,7 @@ where let param_tys = &types[func_ty.params]; let result_tys = &types[func_ty.results]; - record_replay_hook_host_func_entry(storage, &types[func_ty.params], store.0)?; + rr_hooks::record_replay_host_func_entry(storage, &types[func_ty.params], store.0)?; if !store.0.replay_enabled() { let args; @@ -555,10 +580,10 @@ where if let Some(cnt) = result_tys.abi.flat_count(MAX_FLAT_RESULTS) { let mut dst = storage[..cnt].iter_mut(); for (val, ty) in result_vals.iter().zip(result_tys.types.iter()) { - record_hook_wrapper_lower(|cx| val.lower(cx, *ty, &mut dst), &mut cx)?; + rr_hooks::record_lower(|cx, ty| val.lower(cx, ty, &mut dst), &mut cx, *ty)?; } assert!(dst.next().is_none()); - record_hook_host_func_return( + rr_hooks::record_host_func_return( mem::transmute::<&[MaybeUninit], &[ValRaw]>(storage), result_tys, cx.store.0, @@ -568,10 +593,15 @@ where let mut ptr = validate_inbounds_dynamic(&result_tys.abi, cx.as_slice(), ret_ptr)?; for (val, ty) in result_vals.iter().zip(result_tys.types.iter()) { let offset = types.canonical_abi(ty).next_field32_size(&mut ptr); - record_hook_wrapper_lower_store(|cx| val.store(cx, *ty, offset), &mut cx)?; + rr_hooks::record_lower_store( + |cx, ty, offset| val.store(cx, ty, offset), + &mut cx, + *ty, + offset, + )?; } // Recording here is just for marking the return event - record_hook_host_func_return(&[], result_tys, cx.store.0)?; + rr_hooks::record_host_func_return(&[], result_tys, cx.store.0)?; } flags.set_may_leave(true); diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index efac9483c5f6..b4a31279d937 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -29,9 +29,10 @@ use wasmtime_environ::component::{ComponentTypes, StringEncoding, TypeResourceTa /// See [`ConstMemorySliceCell`] for detailed description on the role /// of these types. pub struct MemorySliceCell<'a> { - offset: usize, bytes: &'a mut [u8], #[cfg(feature = "rr-component")] + offset: usize, + #[cfg(feature = "rr-component")] recorder: Option<&'a mut RecordBuffer>, } impl<'a> Deref for MemorySliceCell<'a> { @@ -85,9 +86,10 @@ impl Drop for MemorySliceCell<'_> { /// the compiler will automatically enforce that drops of this type need to be triggered before a /// [`realloc`](LowerContext::realloc), preventing any interleavings in between the borrow and drop of the slice. pub struct ConstMemorySliceCell<'a, const N: usize> { - offset: usize, bytes: &'a mut [u8; N], #[cfg(feature = "rr-component")] + offset: usize, + #[cfg(feature = "rr-component")] recorder: Option<&'a mut RecordBuffer>, } impl<'a, const N: usize> Deref for ConstMemorySliceCell<'a, N> { @@ -455,9 +457,10 @@ impl<'a, T: 'static> LowerContext<'a, T> { // an issue we can optimize further later, probably with judicious use // of `unsafe`. ConstMemorySliceCell { - offset: offset, bytes: slice_mut[offset..].first_chunk_mut().unwrap(), #[cfg(feature = "rr-component")] + offset: offset, + #[cfg(feature = "rr-component")] recorder: recorder, } } @@ -478,9 +481,10 @@ impl<'a, T: 'static> LowerContext<'a, T> { } } MemorySliceCell { - offset: offset, bytes: &mut slice_mut[offset..][..size], #[cfg(feature = "rr-component")] + offset: offset, + #[cfg(feature = "rr-component")] recorder: recorder, } } @@ -583,12 +587,16 @@ impl<'a, T: 'static> LowerContext<'a, T> { /// ## Important Notes /// /// * It is assumed that this is only invoked at the root lower/store calls + /// #[cfg(feature = "rr-component")] pub fn replay_lowering( &mut self, mut result_storage: Option<&mut [ValRaw]>, result_tys: &TypeTuple, ) -> Result<()> { + // There is a lot of `rr-validate` feature gating here for optimal replay performance + // and memory overhead in a non-validating scenario. If this proves to not produce a huge + // overhead in practice, gating can be removed in the future in favor of readability if self.store.0.replay_buffer_mut().is_none() { return Ok(()); } @@ -596,19 +604,26 @@ impl<'a, T: 'static> LowerContext<'a, T> { let mut lowering_error: Option = None; // No nested expected; these depths should only be 1 let mut realloc_stack = Vec::>::new(); - let mut lower_stack = Vec::>::new(); - let mut lower_store_stack = Vec::>::new(); + // Lowering tracks is only for ordering entry/exit events + let mut lower_stack = Vec::<()>::new(); + let mut lower_store_stack = Vec::<()>::new(); while !complete { let buf = self.store.0.replay_buffer_mut().unwrap(); let event = buf.next_event()?; let replay_validate = buf.settings().validate; let trace_has_validation_events = buf.trace_settings().add_validation; + if replay_validate && !trace_has_validation_events { + log::warn!( + "Validation on replay has been enabled, but the recorded trace has no validation metadata... omitting validation" + ); + } + let run_validate = replay_validate && trace_has_validation_events; match event { RREvent::ComponentHostFuncReturn(e) => { + // End of the lowering process if let Some(e) = lowering_error { return Err(e.into()); } - // End of the lowering process if let Some(storage) = result_storage.as_deref_mut() { e.move_into_slice(storage, replay_validate.then_some(result_tys))?; } @@ -618,42 +633,26 @@ impl<'a, T: 'static> LowerContext<'a, T> { let _result = self.realloc_inner(e.old_addr, e.old_size, e.old_align, e.new_size); #[cfg(feature = "rr-type-validation")] - if trace_has_validation_events && replay_validate { + if run_validate { realloc_stack.push(_result); } } // No return value to validate for lower/lower-store; store error and just check that entry happened before RREvent::ComponentLowerReturn(e) => { #[cfg(feature = "rr-type-validation")] - //// TODO: We don't have insertion points for these entry yet! - //if trace_has_validation_events && replay_validate { - // lowering_error = e.validate(&lower_stack.pop().unwrap()).err(); - //} else { - // lowering_error = e.ret().map_err(Into::into).err(); - //} - { - lowering_error = e.ret().map_err(Into::into).err(); - } - #[cfg(not(feature = "rr-type-validation"))] - { - lowering_error = e.ret().map_err(Into::into).err(); + if run_validate { + lower_stack.pop().ok_or(ReplayError::InvalidOrdering)?; } + lowering_error = e.ret().map_err(Into::into).err(); } RREvent::ComponentLowerStoreReturn(e) => { #[cfg(feature = "rr-type-validation")] - //// TODO: We don't have insertion ponts for these entry yet! - //if trace_has_validation_events && replay_validate { - // lowering_error = e.validate(&lower_store_stack.pop().unwrap()).err(); - //} else { - // lowering_error = e.ret().map_err(Into::into).err(); - //} - { - lowering_error = e.ret().map_err(Into::into).err(); - } - #[cfg(not(feature = "rr-type-validation"))] - { - lowering_error = e.ret().map_err(Into::into).err(); + if run_validate { + lower_store_stack + .pop() + .ok_or(ReplayError::InvalidOrdering)?; } + lowering_error = e.ret().map_err(Into::into).err(); } RREvent::ComponentMemorySliceWrite(e) => { // The bounds check is performed here is required here (in the absence of @@ -671,7 +670,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { // Unwrapping should never occur on valid executions since *Entry should be before *Return in trace RREvent::ComponentReallocReturn(e) => { #[cfg(feature = "rr-type-validation")] - if trace_has_validation_events && replay_validate { + if run_validate { lowering_error = e.validate(&realloc_stack.pop().unwrap()).err() } else { // Error is subsumed by the LowerReturn or the LowerStoreReturn @@ -679,15 +678,17 @@ impl<'a, T: 'static> LowerContext<'a, T> { } } RREvent::ComponentLowerEntry(_) => { + // Validation here is just ensuring Entry occurs before Return #[cfg(feature = "rr-type-validation")] - if trace_has_validation_events && replay_validate { - lower_stack.push(Ok(())) + if run_validate { + lower_stack.push(()) } } RREvent::ComponentLowerStoreEntry(_) => { + // Validation here is just ensuring Entry occurs before Return #[cfg(feature = "rr-type-validation")] - if trace_has_validation_events && replay_validate { - lower_store_stack.push(Ok(())) + if run_validate { + lower_store_stack.push(()) } } diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index dbf1b1be7c81..52a7a832fb7d 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -1,6 +1,4 @@ use crate::prelude::*; -#[cfg(feature = "rr-core")] -use crate::rr::core_events::HostFuncReturnEvent; use crate::runtime::Uninhabited; use crate::runtime::vm::{ ExportFunction, InterpreterRef, SendSyncPtr, StoreBox, VMArrayCallHostFuncContext, @@ -1508,6 +1506,92 @@ impl Func { } } +/// Convenience methods to inject record + replay logic +mod rr_hooks { + use super::*; + #[cfg(feature = "rr-core")] + use crate::rr::core_events::HostFuncReturnEvent; + use wasmtime_environ::WasmFuncType; + + #[inline] + /// Record and replay hook operation for host function entry events + pub fn record_replay_host_func_entry( + args: &[MaybeUninit], + wasm_func_type: &WasmFuncType, + store: &mut StoreOpaque, + ) -> Result<()> { + #[cfg(all(feature = "rr-core", feature = "rr-type-validation"))] + { + // Record/replay the raw parameter args + use crate::config::ReplaySettings; + use crate::rr::{Validate, core_events::HostFuncEntryEvent}; + store.record_event_if( + |r| r.add_validation, + |_| { + let num_params = wasm_func_type.params().len(); + HostFuncEntryEvent::new( + &args[..num_params], + // Don't need to check validation here since it is + // covered by the push predicate in this case + Some(wasm_func_type.clone()), + ) + }, + )?; + store.next_replay_event_if( + |_, r| r.add_validation, + |event: HostFuncEntryEvent, r: &ReplaySettings| { + if r.validate { + event.validate(wasm_func_type)?; + } + Ok(()) + }, + )?; + } + let _ = (args, wasm_func_type, store); + Ok(()) + } + + #[inline] + /// Record hook operation for host function return events + pub fn record_host_func_return( + args: &[MaybeUninit], + wasm_func_type: &WasmFuncType, + store: &mut StoreOpaque, + ) -> Result<()> { + // Record the return values + #[cfg(feature = "rr-core")] + { + store.record_event(|rmeta| { + let func_type = wasm_func_type; + let num_results = func_type.params().len(); + HostFuncReturnEvent::new( + &args[..num_results], + rmeta.add_validation.then_some(func_type.clone()), + ) + })?; + } + let _ = (args, wasm_func_type, store); + Ok(()) + } + + #[inline] + /// Replay hook operation for host function return events + pub fn replay_host_func_return( + args: &mut [MaybeUninit], + wasm_func_type: &WasmFuncType, + store: &mut StoreOpaque, + ) -> Result<()> { + #[cfg(feature = "rr-core")] + { + store.next_replay_event_and(|event: HostFuncReturnEvent, rmeta| { + event.move_into_slice(args, rmeta.validate.then_some(wasm_func_type)) + })?; + } + let _ = (args, wasm_func_type, store); + Ok(()) + } +} + /// Prepares for entrance into WebAssembly. /// /// This function will set up context such that `closure` is allowed to call a @@ -2333,41 +2417,15 @@ impl HostContext { let state = &*(state as *const _ as *const HostFuncState); let func = &state.func; - #[cfg(feature = "rr-core")] - let wasm_func_type = { + let wasm_func_subtype = { let type_index = vmctx.func_ref().type_index; caller.engine().signatures().borrow(type_index).unwrap() }; + let wasm_func_type = wasm_func_subtype.unwrap_func(); - #[cfg(all(feature = "rr-core", feature = "rr-type-validation"))] - { - // Record/replay interceptions of raw parameters args - use crate::config::ReplaySettings; - use crate::rr::{Validate, core_events::HostFuncEntryEvent}; - caller.store.0.record_event_if( - |r| r.add_validation, - |_| { - let func_type = wasm_func_type.unwrap_func(); - let num_params = func_type.params().len(); - HostFuncEntryEvent::new( - &args.as_ref()[..num_params], - // Don't need to check validation here since it is - // covered by the push predicate in this case - Some(func_type.clone()), - ) - }, - )?; - // Don't need to auto-assert GC here since we aren't using P - caller.store.0.next_replay_event_if( - |_, r| r.add_validation, - |event: HostFuncEntryEvent, r: &ReplaySettings| { - if r.validate { - event.validate(wasm_func_type.unwrap_func())?; - } - Ok(()) - }, - )?; - } + // Record/replay(validation) of the raw parameter arguments + // Don't need auto-assert GC store here since we aren't using P, just raw args + rr_hooks::record_replay_host_func_entry(args.as_ref(), wasm_func_type, caller.store.0)?; if !caller.store.0.replay_enabled() { let ret = 'ret: { @@ -2400,28 +2458,11 @@ impl HostContext { }; ret.store(&mut store, args.as_mut())?; } - #[cfg(feature = "rr-core")] - // Record the return value of store - caller.store.0.record_event(|rmeta| { - let func_type = wasm_func_type.unwrap_func(); - let num_results = func_type.params().len(); - HostFuncReturnEvent::new( - unsafe { &args.as_ref()[..num_results] }, - rmeta.add_validation.then_some(func_type.clone()), - ) - })?; + // Record the return values + rr_hooks::record_host_func_return(args.as_ref(), wasm_func_type, caller.store.0)?; } else { - // Replay the return value of the store - #[cfg(feature = "rr-core")] - caller - .store - .0 - .next_replay_event_and(|event: HostFuncReturnEvent, rmeta| { - event.move_into_slice( - args.as_mut(), - rmeta.validate.then_some(wasm_func_type.unwrap_func()), - ) - })?; + // Replay the return values + rr_hooks::replay_host_func_return(args.as_mut(), wasm_func_type, caller.store.0)?; } Ok(()) diff --git a/crates/wasmtime/src/runtime/rr/events/component_events.rs b/crates/wasmtime/src/runtime/rr/events/component_events.rs index bbe5281858eb..43b8ae3b9947 100644 --- a/crates/wasmtime/src/runtime/rr/events/component_events.rs +++ b/crates/wasmtime/src/runtime/rr/events/component_events.rs @@ -23,6 +23,7 @@ impl InstantiationEvent { impl Validate for InstantiationEvent { /// Validate that checksums match fn validate(&self, expect_component: &Component) -> Result<(), ReplayError> { + self.log(); if self.checksum != *expect_component.checksum() { Err(ReplayError::FailedModuleValidation) } else { @@ -55,6 +56,7 @@ impl HostFuncEntryEvent { } impl Validate for HostFuncEntryEvent { fn validate(&self, expect_types: &TypeTuple) -> Result<(), ReplayError> { + self.log(); replay_args_typecheck(self.types.as_ref(), expect_types) } } @@ -96,6 +98,7 @@ impl HostFuncReturnEvent { } impl Validate for HostFuncReturnEvent { fn validate(&self, expect_types: &TypeTuple) -> Result<(), ReplayError> { + self.log(); replay_args_typecheck(self.types.as_ref(), expect_types) } } @@ -123,23 +126,6 @@ macro_rules! generic_new_result_events { pub fn ret(self) -> Result<$ok_ty, EventActionError> { self.ret } } - impl Validate> for $event { - fn validate(&self, expect_ret: &Result<$ok_ty>) -> Result<(), ReplayError> { - // Cannot just use eq since anyhow::Error and EventActionError cannot be compared - match (self.ret.as_ref(), expect_ret.as_ref()) { - (Ok(r), Ok(s)) => replay_args_valcheck(r, s), - // Return the recorded error - (Err(e), Err(f)) => Err(ReplayError::from($err_variant(format!( - "Replayed Error: {} \nRecorded Error: {}", - e, f - )))), - // Diverging errors.. Report as a failed validation - (Ok(_), Err(_)) => Err(ReplayError::FailedFuncValidation), - (Err(_), Ok(_)) => Err(ReplayError::FailedFuncValidation), - } - } - } - )* ); } @@ -185,6 +171,25 @@ generic_new_result_events! { LowerStoreReturnEvent => ((), EventActionError::LowerStoreError) } +impl Validate> for ReallocReturnEvent { + /// We can check that realloc is deterministic (as expected by the engine) + fn validate(&self, expect_ret: &Result) -> Result<(), ReplayError> { + self.log(); + // Cannot just use eq since anyhow::Error and EventActionError cannot be compared + match (self.ret.as_ref(), expect_ret.as_ref()) { + (Ok(r), Ok(s)) => replay_args_valcheck(r, s), + // Return the recorded error + (Err(e), Err(f)) => Err(ReplayError::from(EventActionError::ReallocError(format!( + "Replayed Realloc Error: {} \nRecorded Realloc Error: {}", + e, f + )))), + // Diverging errors.. Report as a failed validation + (Ok(_), Err(_)) => Err(ReplayError::FailedFuncValidation), + (Err(_), Ok(_)) => Err(ReplayError::FailedFuncValidation), + } + } +} + generic_new_events! { /// A reallocation call event in the Component Model canonical ABI /// @@ -196,10 +201,12 @@ generic_new_events! { new_size: usize }, + /// Entry to a type lowering invocation LowerEntryEvent { ty: InterfaceType }, + /// Entry to store invocations during type lowering LowerStoreEntryEvent { ty: InterfaceType, offset: usize diff --git a/crates/wasmtime/src/runtime/rr/events/core_events.rs b/crates/wasmtime/src/runtime/rr/events/core_events.rs index 28d167f1c8bc..86f78572e013 100644 --- a/crates/wasmtime/src/runtime/rr/events/core_events.rs +++ b/crates/wasmtime/src/runtime/rr/events/core_events.rs @@ -25,6 +25,7 @@ impl HostFuncEntryEvent { } impl Validate for HostFuncEntryEvent { fn validate(&self, expect_types: &CoreFuncArgTypes) -> Result<(), ReplayError> { + self.log(); replay_args_typecheck(self.types.as_ref(), expect_types) } } @@ -64,6 +65,7 @@ impl HostFuncReturnEvent { } impl Validate for HostFuncReturnEvent { fn validate(&self, expect_types: &CoreFuncArgTypes) -> Result<(), ReplayError> { + self.log(); replay_args_typecheck(self.types.as_ref(), expect_types) } } diff --git a/crates/wasmtime/src/runtime/rr/events/mod.rs b/crates/wasmtime/src/runtime/rr/events/mod.rs index 480146f26b79..1d5e4c0c1496 100644 --- a/crates/wasmtime/src/runtime/rr/events/mod.rs +++ b/crates/wasmtime/src/runtime/rr/events/mod.rs @@ -139,7 +139,7 @@ where /// Trait signifying types that can be validated on replay /// /// Default nop behavior when `rr-validate` is disabled. -/// Note howeverthat some [`Validate`] overriden implementations are present even +/// Note however that some [`Validate`] overriden implementations are present even /// when feature `rr-validate` is disabled, when validation is needed /// for a faithful replay. pub trait Validate { @@ -147,6 +147,13 @@ pub trait Validate { fn validate(&self, _expect_t: &T) -> Result<(), ReplayError> { Ok(()) } + /// Write a log message + fn log(&self) + where + Self: fmt::Debug, + { + log::debug!("Validating => {:?}", self); + } } pub mod component_events; diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index 9a81e5d80411..a14b90a53ddb 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -127,6 +127,7 @@ pub enum ReplayError { FailedFuncValidation, FailedModuleValidation, IncorrectEventVariant, + InvalidOrdering, EventActionError(EventActionError), } @@ -148,6 +149,9 @@ impl fmt::Display for ReplayError { Self::EventActionError(e) => { write!(f, "{:?}", e) } + Self::InvalidOrdering => { + write!(f, "event occured at an invalid position in the trace") + } } } } From f55bbff9fe8d1eb84e906db93a7c5a37b154cb7f Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 5 Aug 2025 15:02:55 -0700 Subject: [PATCH 56/62] Refactor validation API --- .../src/runtime/component/func/host.rs | 50 +++++--------- .../src/runtime/component/func/options.rs | 36 +++------- .../src/runtime/component/instance.rs | 12 ++-- crates/wasmtime/src/runtime/func.rs | 53 +++++---------- .../src/runtime/rr/events/component_events.rs | 48 +++----------- .../src/runtime/rr/events/core_events.rs | 37 ++++------- crates/wasmtime/src/runtime/rr/events/mod.rs | 32 ++++++--- crates/wasmtime/src/runtime/rr/mod.rs | 66 +++++++++++-------- crates/wasmtime/src/runtime/store.rs | 34 +++++----- 9 files changed, 146 insertions(+), 222 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index a68295ec9991..8e256fd0f0bd 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -36,21 +36,9 @@ mod rr_hooks { ) -> Result<()> { #[cfg(all(feature = "rr-component", feature = "rr-type-validation"))] { - use crate::config::ReplaySettings; - use crate::rr::{Validate, component_events::HostFuncEntryEvent}; - store.record_event_if( - |r| r.add_validation, - |_| HostFuncEntryEvent::new(args, Some(param_types)), - )?; - store.next_replay_event_if( - |_, r| r.add_validation, - |event: HostFuncEntryEvent, r: &ReplaySettings| { - if r.validate { - event.validate(param_types)?; - } - Ok(()) - }, - )?; + use crate::rr::component_events::HostFuncEntryEvent; + store.record_event_validation(|| HostFuncEntryEvent::new(args, Some(param_types)))?; + store.next_replay_event_validation::(param_types)?; } let _ = (args, param_types, store); Ok(()) @@ -64,11 +52,7 @@ mod rr_hooks { store: &mut StoreOpaque, ) -> Result<()> { #[cfg(feature = "rr-component")] - { - store.record_event(|r| { - HostFuncReturnEvent::new(args, r.add_validation.then_some(result_types)) - })?; - } + store.record_event(|| HostFuncReturnEvent::new(args))?; let _ = (args, result_types, store); Ok(()) } @@ -87,12 +71,12 @@ mod rr_hooks { #[cfg(all(feature = "rr-component", feature = "rr-type-validation"))] cx.store .0 - .record_event(|_| LowerStoreEntryEvent::new(ty, offset))?; + .record_event_validation(|| LowerStoreEntryEvent::new(ty, offset))?; let store_result = lower_store(cx, ty, offset); #[cfg(feature = "rr-component")] cx.store .0 - .record_event(|_| LowerStoreReturnEvent::new(&store_result))?; + .record_event(|| LowerStoreReturnEvent::new(&store_result))?; store_result } @@ -107,12 +91,14 @@ mod rr_hooks { F: FnOnce(&mut LowerContext<'_, T>, InterfaceType) -> Result<()>, { #[cfg(all(feature = "rr-component", feature = "rr-type-validation"))] - cx.store.0.record_event(|_| LowerEntryEvent::new(ty))?; + cx.store + .0 + .record_event_validation(|| LowerEntryEvent::new(ty))?; let lower_result = lower(cx, ty); #[cfg(feature = "rr-component")] cx.store .0 - .record_event(|_| LowerReturnEvent::new(&lower_result))?; + .record_event(|| LowerReturnEvent::new(&lower_result))?; lower_result } } @@ -346,7 +332,7 @@ where { flags.set_may_leave(false); let mut lower = LowerContext::new(cx, &options, types, instance); - storage.replay_lower_results(&mut lower, &types[ty.results])?; + storage.replay_lower_results(&mut lower)?; flags.set_may_leave(true); } } @@ -417,24 +403,20 @@ where } #[cfg(feature = "rr-component")] - unsafe fn replay_lower_results( - &mut self, - cx: &mut LowerContext<'_, T>, - expect_tys: &TypeTuple, - ) -> Result<()> { + unsafe fn replay_lower_results(&mut self, cx: &mut LowerContext<'_, T>) -> Result<()> { use crate::component::storage::storage_as_slice_mut; let direct_results_lower = |cx: &mut LowerContext<'_, T>, dst: &mut MaybeUninit<::Lower>| { // This path also stores the final return values in resulting storage - cx.replay_lowering(Some(storage_as_slice_mut(dst)), expect_tys) + cx.replay_lowering(Some(storage_as_slice_mut(dst))) }; let indirect_results_lower = |cx: &mut LowerContext<'_, T>, _dst: &ValRaw| { // `_dst` is a Wasm pointer to indirect results. This pointer itself will remain // deterministic, and thus replay will not need to change this. However, // replay will have to overwrite any nested stored lowerings (deep copy) - cx.replay_lowering(None, expect_tys) + cx.replay_lowering(None) }; match self { Storage::Direct(storage) => { @@ -616,12 +598,12 @@ where let result_storage = mem::transmute::<&mut [MaybeUninit], &mut [ValRaw]>(storage); // This path also stores the final return values in resulting storage - cx.replay_lowering(Some(result_storage), result_tys)?; + cx.replay_lowering(Some(result_storage))?; } else { // The indirect `ret_ptr` itself will remain deterministic, and thus replay will not // need to change the return storage. However, replay will have to overwrite any nested stored // lowerings (deep copy) - cx.replay_lowering(None, result_tys)?; + cx.replay_lowering(None)?; } flags.set_may_leave(true); } diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index b4a31279d937..84b752ab80ff 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -17,8 +17,6 @@ use crate::{FuncType, StoreContextMut}; use alloc::sync::Arc; use core::ops::{Deref, DerefMut}; use core::ptr::NonNull; -#[cfg(feature = "rr-type-validation")] -use wasmtime_environ::component::TypeTuple; use wasmtime_environ::component::{ComponentTypes, StringEncoding, TypeResourceTableIndex}; /// Same as [`ConstMemorySliceCell`] except allows for dynamically sized slices. @@ -51,7 +49,7 @@ impl Drop for MemorySliceCell<'_> { fn drop(&mut self) { #[cfg(feature = "rr-component")] if let Some(buf) = &mut self.recorder { - buf.record_event(|_| MemorySliceWriteEvent::new(self.offset, self.bytes.to_vec())) + buf.record_event(|| MemorySliceWriteEvent::new(self.offset, self.bytes.to_vec())) .unwrap(); } } @@ -108,11 +106,8 @@ impl<'a, const N: usize> Drop for ConstMemorySliceCell<'a, N> { fn drop(&mut self) { #[cfg(feature = "rr-component")] if let Some(buf) = &mut self.recorder { - buf.record_event_if( - |_| true, - |_| MemorySliceWriteEvent::new(self.offset, self.bytes.to_vec()), - ) - .unwrap(); + buf.record_event(|| MemorySliceWriteEvent::new(self.offset, self.bytes.to_vec())) + .unwrap(); } } } @@ -418,12 +413,12 @@ impl<'a, T: 'static> LowerContext<'a, T> { #[cfg(feature = "rr-component")] self.store .0 - .record_event(|_| ReallocEntryEvent::new(old, old_size, old_align, new_size))?; + .record_event(|| ReallocEntryEvent::new(old, old_size, old_align, new_size))?; let result = self.realloc_inner(old, old_size, old_align, new_size); #[cfg(feature = "rr-component")] self.store .0 - .record_event_if(|r| r.add_validation, |_| ReallocReturnEvent::new(&result))?; + .record_event_validation(|| ReallocReturnEvent::new(&result))?; result } @@ -589,11 +584,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { /// * It is assumed that this is only invoked at the root lower/store calls /// #[cfg(feature = "rr-component")] - pub fn replay_lowering( - &mut self, - mut result_storage: Option<&mut [ValRaw]>, - result_tys: &TypeTuple, - ) -> Result<()> { + pub fn replay_lowering(&mut self, mut result_storage: Option<&mut [ValRaw]>) -> Result<()> { // There is a lot of `rr-validate` feature gating here for optimal replay performance // and memory overhead in a non-validating scenario. If this proves to not produce a huge // overhead in practice, gating can be removed in the future in favor of readability @@ -610,14 +601,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { while !complete { let buf = self.store.0.replay_buffer_mut().unwrap(); let event = buf.next_event()?; - let replay_validate = buf.settings().validate; - let trace_has_validation_events = buf.trace_settings().add_validation; - if replay_validate && !trace_has_validation_events { - log::warn!( - "Validation on replay has been enabled, but the recorded trace has no validation metadata... omitting validation" - ); - } - let run_validate = replay_validate && trace_has_validation_events; + let run_validate = buf.settings().validate && buf.trace_settings().add_validation; match event { RREvent::ComponentHostFuncReturn(e) => { // End of the lowering process @@ -625,7 +609,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { return Err(e.into()); } if let Some(storage) = result_storage.as_deref_mut() { - e.move_into_slice(storage, replay_validate.then_some(result_tys))?; + e.move_into_slice(storage); } complete = true; } @@ -678,14 +662,14 @@ impl<'a, T: 'static> LowerContext<'a, T> { } } RREvent::ComponentLowerEntry(_) => { - // Validation here is just ensuring Entry occurs before Return + // All we want here is ensuring Entry occurs before Return #[cfg(feature = "rr-type-validation")] if run_validate { lower_stack.push(()) } } RREvent::ComponentLowerStoreEntry(_) => { - // Validation here is just ensuring Entry occurs before Return + // All we want here is ensuring Entry occurs before Return #[cfg(feature = "rr-type-validation")] if run_validate { lower_store_stack.push(()) diff --git a/crates/wasmtime/src/runtime/component/instance.rs b/crates/wasmtime/src/runtime/component/instance.rs index 12e2daf09732..6d3799e81e89 100644 --- a/crates/wasmtime/src/runtime/component/instance.rs +++ b/crates/wasmtime/src/runtime/component/instance.rs @@ -851,12 +851,12 @@ impl InstancePre { { store .0 - .record_event(|_| InstantiationEvent::from_component(&self.component))?; - store - .0 - .next_replay_event_and(|event: InstantiationEvent, _| { - event.validate(&self.component) - })?; + .record_event(|| InstantiationEvent::from_component(&self.component))?; + // This is a required validation check for functional correctness, so don't use + // [`StoreOpaque::next_replay_event_validation`] + store.0.next_replay_event_and(|event: InstantiationEvent| { + event.validate(&InstantiationEvent::from_component(&self.component)) + })?; } store .engine() diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 52a7a832fb7d..fcaa191e2fe6 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -1523,29 +1523,12 @@ mod rr_hooks { #[cfg(all(feature = "rr-core", feature = "rr-type-validation"))] { // Record/replay the raw parameter args - use crate::config::ReplaySettings; - use crate::rr::{Validate, core_events::HostFuncEntryEvent}; - store.record_event_if( - |r| r.add_validation, - |_| { - let num_params = wasm_func_type.params().len(); - HostFuncEntryEvent::new( - &args[..num_params], - // Don't need to check validation here since it is - // covered by the push predicate in this case - Some(wasm_func_type.clone()), - ) - }, - )?; - store.next_replay_event_if( - |_, r| r.add_validation, - |event: HostFuncEntryEvent, r: &ReplaySettings| { - if r.validate { - event.validate(wasm_func_type)?; - } - Ok(()) - }, - )?; + use crate::rr::core_events::HostFuncEntryEvent; + store.record_event_validation(|| { + let num_params = wasm_func_type.params().len(); + HostFuncEntryEvent::new(&args[..num_params], wasm_func_type.clone()) + })?; + store.next_replay_event_validation::(wasm_func_type)?; } let _ = (args, wasm_func_type, store); Ok(()) @@ -1560,16 +1543,11 @@ mod rr_hooks { ) -> Result<()> { // Record the return values #[cfg(feature = "rr-core")] - { - store.record_event(|rmeta| { - let func_type = wasm_func_type; - let num_results = func_type.params().len(); - HostFuncReturnEvent::new( - &args[..num_results], - rmeta.add_validation.then_some(func_type.clone()), - ) - })?; - } + store.record_event(|| { + let func_type = wasm_func_type; + let num_results = func_type.params().len(); + HostFuncReturnEvent::new(&args[..num_results]) + })?; let _ = (args, wasm_func_type, store); Ok(()) } @@ -1582,11 +1560,10 @@ mod rr_hooks { store: &mut StoreOpaque, ) -> Result<()> { #[cfg(feature = "rr-core")] - { - store.next_replay_event_and(|event: HostFuncReturnEvent, rmeta| { - event.move_into_slice(args, rmeta.validate.then_some(wasm_func_type)) - })?; - } + store.next_replay_event_and(|event: HostFuncReturnEvent| { + event.move_into_slice(args); + Ok(()) + })?; let _ = (args, wasm_func_type, store); Ok(()) } diff --git a/crates/wasmtime/src/runtime/rr/events/component_events.rs b/crates/wasmtime/src/runtime/rr/events/component_events.rs index 43b8ae3b9947..862f7a4e508f 100644 --- a/crates/wasmtime/src/runtime/rr/events/component_events.rs +++ b/crates/wasmtime/src/runtime/rr/events/component_events.rs @@ -20,20 +20,9 @@ impl InstantiationEvent { } } } -impl Validate for InstantiationEvent { - /// Validate that checksums match - fn validate(&self, expect_component: &Component) -> Result<(), ReplayError> { - self.log(); - if self.checksum != *expect_component.checksum() { - Err(ReplayError::FailedModuleValidation) - } else { - Ok(()) - } - } -} /// A call event from a Wasm component into the host -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct HostFuncEntryEvent { /// Raw values passed across the call entry boundary args: RRFuncArgVals, @@ -64,42 +53,21 @@ impl Validate for HostFuncEntryEvent { /// A return event after a host call for a Wasm component /// /// Matches 1:1 with [`HostFuncEntryEvent`] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct HostFuncReturnEvent { /// Lowered values passed across the call return boundary args: RRFuncArgVals, - /// Optional param/return types (required to support replay validation). - /// - /// Note: This relies on the invariant that [InterfaceType] will always be - /// deterministic. Currently, the type indices into various [ComponentTypes] - /// maintain this, allowing for quick type-checking. - types: Option, } impl HostFuncReturnEvent { - pub fn new(args: &[ValRaw], types: Option<&TypeTuple>) -> Self { + pub fn new(args: &[ValRaw]) -> Self { Self { args: func_argvals_from_raw_slice(args), - types: types.cloned(), } } /// Consume the caller event and encode it back into the slice - pub fn move_into_slice( - self, - args: &mut [ValRaw], - expect_types: Option<&TypeTuple>, - ) -> Result<(), ReplayError> { - if let Some(e) = expect_types { - self.validate(e)?; - } + pub fn move_into_slice(self, args: &mut [ValRaw]) { func_argvals_into_raw_slice(self.args, args); - Ok(()) - } -} -impl Validate for HostFuncReturnEvent { - fn validate(&self, expect_types: &TypeTuple) -> Result<(), ReplayError> { - self.log(); - replay_args_typecheck(self.types.as_ref(), expect_types) } } @@ -112,7 +80,7 @@ macro_rules! generic_new_result_events { ) => ( $( $(#[doc = $doc])* - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct $event { ret: Result<$ok_ty, EventActionError>, } @@ -142,7 +110,7 @@ macro_rules! generic_new_events { ),* ) => ( $( - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + #[derive(Debug, Clone, Serialize, Deserialize)] $(#[doc = $doc])* pub struct $struct { $( @@ -184,8 +152,8 @@ impl Validate> for ReallocReturnEvent { e, f )))), // Diverging errors.. Report as a failed validation - (Ok(_), Err(_)) => Err(ReplayError::FailedFuncValidation), - (Err(_), Ok(_)) => Err(ReplayError::FailedFuncValidation), + (Ok(_), Err(_)) => Err(ReplayError::FailedValidation), + (Err(_), Ok(_)) => Err(ReplayError::FailedValidation), } } } diff --git a/crates/wasmtime/src/runtime/rr/events/core_events.rs b/crates/wasmtime/src/runtime/rr/events/core_events.rs index 86f78572e013..f3aff8f75c46 100644 --- a/crates/wasmtime/src/runtime/rr/events/core_events.rs +++ b/crates/wasmtime/src/runtime/rr/events/core_events.rs @@ -7,16 +7,16 @@ use wasmtime_environ::{WasmFuncType, WasmValType}; type CoreFuncArgTypes = WasmFuncType; /// A call event from a Core Wasm module into the host -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct HostFuncEntryEvent { /// Raw values passed across the call/return boundary args: RRFuncArgVals, - /// Optional param/return types (required to support replay validation) - types: Option, + /// Param/return types (required to support replay validation) + types: CoreFuncArgTypes, } impl HostFuncEntryEvent { // Record - pub fn new(args: &[MaybeUninit], types: Option) -> Self { + pub fn new(args: &[MaybeUninit], types: WasmFuncType) -> Self { Self { args: func_argvals_from_raw_slice(args), types: types, @@ -26,46 +26,33 @@ impl HostFuncEntryEvent { impl Validate for HostFuncEntryEvent { fn validate(&self, expect_types: &CoreFuncArgTypes) -> Result<(), ReplayError> { self.log(); - replay_args_typecheck(self.types.as_ref(), expect_types) + if &self.types == expect_types { + Ok(()) + } else { + Err(ReplayError::FailedValidation) + } } } /// A return event after a host call for a Core Wasm /// /// Matches 1:1 with [`HostFuncEntryEvent`] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct HostFuncReturnEvent { /// Raw values passed across the call/return boundary args: RRFuncArgVals, - /// Optional param/return types (required to support replay validation) - types: Option, } impl HostFuncReturnEvent { // Record - pub fn new(args: &[MaybeUninit], types: Option) -> Self { + pub fn new(args: &[MaybeUninit]) -> Self { Self { args: func_argvals_from_raw_slice(args), - types: types, } } // Replay /// Consume the caller event and encode it back into the slice with an optional /// typechecking validation of the event. - pub fn move_into_slice( - self, - args: &mut [MaybeUninit], - expect_types: Option<&WasmFuncType>, - ) -> Result<(), ReplayError> { - if let Some(e) = expect_types { - self.validate(e)?; - } + pub fn move_into_slice(self, args: &mut [MaybeUninit]) { func_argvals_into_raw_slice(self.args, args); - Ok(()) - } -} -impl Validate for HostFuncReturnEvent { - fn validate(&self, expect_types: &CoreFuncArgTypes) -> Result<(), ReplayError> { - self.log(); - replay_args_typecheck(self.types.as_ref(), expect_types) } } diff --git a/crates/wasmtime/src/runtime/rr/events/mod.rs b/crates/wasmtime/src/runtime/rr/events/mod.rs index 1d5e4c0c1496..3a3f40cf3ac5 100644 --- a/crates/wasmtime/src/runtime/rr/events/mod.rs +++ b/crates/wasmtime/src/runtime/rr/events/mod.rs @@ -93,7 +93,7 @@ where /// Typechecking validation for replay, if `src_types` exist /// -/// Returns [`ReplayError::FailedFuncValidation`] if typechecking fails +/// Returns [`ReplayError::FailedValidation`] if typechecking fails #[inline(always)] fn replay_args_typecheck(src_types: Option, expect_types: T) -> Result<(), ReplayError> where @@ -105,7 +105,7 @@ where if types == expect_types { Ok(()) } else { - Err(ReplayError::FailedFuncValidation) + Err(ReplayError::FailedValidation) } } else { println!( @@ -129,7 +129,7 @@ where if src_val == expect_val { Ok(()) } else { - Err(ReplayError::FailedFuncValidation) + Err(ReplayError::FailedValidation) } } #[cfg(not(feature = "rr-type-validation"))] @@ -138,15 +138,14 @@ where /// Trait signifying types that can be validated on replay /// -/// Default nop behavior when `rr-validate` is disabled. -/// Note however that some [`Validate`] overriden implementations are present even +/// All `PartialEq` and `Eq` types are directly validatable with themselves. +/// Note however that some [`Validate`] implementations are present even /// when feature `rr-validate` is disabled, when validation is needed -/// for a faithful replay. +/// for a faithful replay (e.g. [`component_events::InstantiationEvent`]). pub trait Validate { /// Perform a validation of the event to ensure replay consistency - fn validate(&self, _expect_t: &T) -> Result<(), ReplayError> { - Ok(()) - } + fn validate(&self, expect: &T) -> Result<(), ReplayError>; + /// Write a log message fn log(&self) where @@ -156,5 +155,20 @@ pub trait Validate { } } +impl Validate for T +where + T: PartialEq + fmt::Debug, +{ + /// All types that are [`PartialEq`] are directly validatable with themselves + fn validate(&self, expect: &T) -> Result<(), ReplayError> { + self.log(); + if self == expect { + Ok(()) + } else { + Err(ReplayError::FailedValidation) + } + } +} + pub mod component_events; pub mod core_events; diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index a14b90a53ddb..b9c7fe83512e 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -43,7 +43,7 @@ macro_rules! rr_event { /// This type is the narrow waist for serialization/deserialization. /// Higher-level events (e.g. import calls consisting of lifts and lowers /// of parameter/return types) may drop down to one or more [`RREvent`]s - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + #[derive(Debug, Clone, Serialize, Deserialize)] pub enum RREvent { /// Event signalling the end of a trace Eof, @@ -124,8 +124,7 @@ rr_event! { #[derive(Debug, PartialEq, Eq)] pub enum ReplayError { EmptyBuffer, - FailedFuncValidation, - FailedModuleValidation, + FailedValidation, IncorrectEventVariant, InvalidOrdering, EventActionError(EventActionError), @@ -137,11 +136,8 @@ impl fmt::Display for ReplayError { Self::EmptyBuffer => { write!(f, "replay buffer is empty!") } - Self::FailedFuncValidation => { - write!(f, "func replay event validation failed") - } - Self::FailedModuleValidation => { - write!(f, "module load replay event validation failed") + Self::FailedValidation => { + write!(f, "replay event validation failed") } Self::IncorrectEventVariant => { write!(f, "event method invoked on incorrect variant") @@ -179,7 +175,7 @@ pub trait Recorder { fn record_event(&mut self, f: F) -> Result<()> where T: Into, - F: FnOnce(&RecordSettings) -> T; + F: FnOnce() -> T; /// Trigger an explicit flush of any buffered data to the writer /// @@ -191,14 +187,15 @@ pub trait Recorder { // Provided methods - /// Conditionally [`record_event`](Recorder::record_event) when `pred` is true - fn record_event_if(&mut self, pred: P, f: F) -> Result<()> + /// Record a event only when validation is requested + #[inline] + fn record_event_validation(&mut self, f: F) -> Result<()> where T: Into, - P: FnOnce(&RecordSettings) -> bool, - F: FnOnce(&RecordSettings) -> T, + F: FnOnce() -> T, { - if pred(self.settings()) { + let settings = self.settings(); + if settings.add_validation { self.record_event(f)?; } Ok(()) @@ -261,23 +258,32 @@ pub trait Replayer: Iterator { where T: TryFrom, ReplayError: From<>::Error>, - F: FnOnce(T, &ReplaySettings) -> Result<(), ReplayError>, + F: FnOnce(T) -> Result<(), ReplayError>, { let call_event = self.next_event_typed()?; - Ok(f(call_event, self.settings())?) + Ok(f(call_event)?) } - /// Conditionally execute [`next_event_and`](Replayer::next_event_and) when `pred` is true + /// Conditionally process the next validation recorded event and if + /// replay validation is enabled, run the validation check + /// + /// ## Errors + /// + /// In addition to errors in [`next_event_typed`](Replayer::next_event_typed), + /// validation errors can be thrown #[inline] - fn next_event_if(&mut self, pred: P, f: F) -> Result<(), ReplayError> + fn next_event_validation(&mut self, expect: &Y) -> Result<(), ReplayError> where - T: TryFrom, + T: TryFrom + Validate, ReplayError: From<>::Error>, - P: FnOnce(&ReplaySettings, &RecordSettings) -> bool, - F: FnOnce(T, &ReplaySettings) -> Result<(), ReplayError>, { - if pred(self.settings(), self.trace_settings()) { - self.next_event_and(f) + if self.trace_settings().add_validation { + let event = self.next_event_typed::()?; + if self.settings().validate { + event.validate(expect) + } else { + Ok(()) + } } else { Ok(()) } @@ -331,9 +337,9 @@ impl Recorder for RecordBuffer { fn record_event(&mut self, f: F) -> Result<()> where T: Into, - F: FnOnce(&RecordSettings) -> T, + F: FnOnce() -> T, { - let event = f(self.settings()).into(); + let event = f().into(); log::debug!("Recording event => {}", &event); self.push_event(event) } @@ -410,7 +416,13 @@ impl Replayer for ReplayBuffer { ); // Read the recording settings - let trace_settings = io::from_replay_reader(&mut reader, &mut [0; 0])?; + let trace_settings: RecordSettings = io::from_replay_reader(&mut reader, &mut [0; 0])?; + + if settings.validate && !trace_settings.add_validation { + log::warn!( + "Replay validation will be omitted since the recorded trace has no validation metadata..." + ); + } Ok(ReplayBuffer { reader, @@ -450,7 +462,7 @@ mod tests { let mut recorder = RecordBuffer::new_recorder(Box::new(File::create(tmppath)?), record_settings)?; let event = component_wasm::HostFuncReturnEvent::new(values.as_slice(), None); - recorder.record_event(|_| event.clone())?; + recorder.record_event(event.clone())?; recorder.flush()?; let tmp = tmp.into_temp_path(); diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index 977e0ce1b22d..bd0b696fcaea 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -80,7 +80,7 @@ use crate::RootSet; use crate::module::RegisteredModuleId; use crate::prelude::*; #[cfg(feature = "rr")] -use crate::rr::{RREvent, RecordBuffer, Recorder, ReplayBuffer, ReplayError, Replayer}; +use crate::rr::{RREvent, RecordBuffer, Recorder, ReplayBuffer, ReplayError, Replayer, Validate}; #[cfg(feature = "gc")] use crate::runtime::vm::GcRootsList; #[cfg(feature = "stack-switching")] @@ -94,8 +94,6 @@ use crate::runtime::vm::{ use crate::trampoline::VMHostGlobalContext; use crate::{Engine, Module, Trap, Val, ValRaw, module::ModuleRegistry}; use crate::{Global, Instance, Memory, Table, Uninhabited}; -#[cfg(feature = "rr")] -use crate::{RecordSettings, ReplaySettings}; use alloc::sync::Arc; use core::fmt; use core::marker; @@ -1393,7 +1391,7 @@ impl StoreOpaque { pub(crate) fn record_event(&mut self, f: F) -> Result<()> where T: Into, - F: FnOnce(&RecordSettings) -> T, + F: FnOnce() -> T, { if let Some(buf) = self.record_buffer_mut() { buf.record_event(f) @@ -1403,18 +1401,18 @@ impl StoreOpaque { } /// Conditionally record the given event into the store's record buffer + /// if validation is enabled for recording /// - /// Convenience wrapper around [`Recorder::record_event_if`] + /// Convenience wrapper around [`Recorder::record_event_validation`] #[cfg(feature = "rr")] #[inline(always)] - pub(crate) fn record_event_if(&mut self, pred: P, f: F) -> Result<()> + pub(crate) fn record_event_validation(&mut self, f: F) -> Result<()> where T: Into, - P: FnOnce(&RecordSettings) -> bool, - F: FnOnce(&RecordSettings) -> T, + F: FnOnce() -> T, { if let Some(buf) = self.record_buffer_mut() { - buf.record_event_if(pred, f) + buf.record_event_validation(f) } else { Ok(()) } @@ -1429,7 +1427,7 @@ impl StoreOpaque { where T: TryFrom, ReplayError: From<>::Error>, - F: FnOnce(T, &ReplaySettings) -> Result<(), ReplayError>, + F: FnOnce(T) -> Result<(), ReplayError>, { if let Some(buf) = self.replay_buffer_mut() { buf.next_event_and(f) @@ -1438,20 +1436,22 @@ impl StoreOpaque { } } - /// Conditionally process the next replay event from the store's replay buffer + /// Process the next replay event as a validation event from the store's replay buffer + /// and if validation is enabled on replay, and run the validation check /// - /// Convenience wrapper around [`Replayer::next_event_if`] + /// Convenience wrapper around [`Replayer::next_event_validation`] #[cfg(feature = "rr")] #[inline] - pub(crate) fn next_replay_event_if(&mut self, pred: P, f: F) -> Result<(), ReplayError> + pub(crate) fn next_replay_event_validation( + &mut self, + expect: &Y, + ) -> Result<(), ReplayError> where - T: TryFrom, + T: TryFrom + Validate, ReplayError: From<>::Error>, - P: FnOnce(&ReplaySettings, &RecordSettings) -> bool, - F: FnOnce(T, &ReplaySettings) -> Result<(), ReplayError>, { if let Some(buf) = self.replay_buffer_mut() { - buf.next_event_if(pred, f) + buf.next_event_validation::(expect) } else { Ok(()) } From c852b1297c8bee167f38b5dce44895e7183f14bb Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 5 Aug 2025 15:30:58 -0700 Subject: [PATCH 57/62] Move to typefunc for host function entry validation --- .../src/runtime/component/component.rs | 1 + .../src/runtime/component/func/host.rs | 32 ++++++------- .../src/runtime/component/func/options.rs | 6 +-- .../src/runtime/rr/events/component_events.rs | 28 ++++++++---- crates/wasmtime/src/runtime/rr/events/mod.rs | 45 ------------------- 5 files changed, 35 insertions(+), 77 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/component.rs b/crates/wasmtime/src/runtime/component/component.rs index 66cc1a47c5f2..3aaa0ddbc5e9 100644 --- a/crates/wasmtime/src/runtime/component/component.rs +++ b/crates/wasmtime/src/runtime/component/component.rs @@ -833,6 +833,7 @@ impl Component { &self.inner.realloc_func_type } + #[allow(unused)] pub(crate) fn checksum(&self) -> &[u8; 32] { &self.inner.checksum } diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 8e256fd0f0bd..cf974698add8 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -13,7 +13,7 @@ use alloc::sync::Arc; use core::any::Any; use core::mem::{self, MaybeUninit}; use core::ptr::NonNull; -use wasmtime_environ::component::TypeTuple; +use wasmtime_environ::component::TypeFunc; use wasmtime_environ::component::{ CanonicalAbiInfo, ComponentTypes, InterfaceType, MAX_FLAT_PARAMS, MAX_FLAT_RESULTS, StringEncoding, TypeFuncIndex, @@ -31,29 +31,25 @@ mod rr_hooks { #[inline] pub fn record_replay_host_func_entry( args: &mut [MaybeUninit], - param_types: &TypeTuple, + func_type: &TypeFunc, store: &mut StoreOpaque, ) -> Result<()> { #[cfg(all(feature = "rr-component", feature = "rr-type-validation"))] { use crate::rr::component_events::HostFuncEntryEvent; - store.record_event_validation(|| HostFuncEntryEvent::new(args, Some(param_types)))?; - store.next_replay_event_validation::(param_types)?; + store.record_event_validation(|| HostFuncEntryEvent::new(args, func_type.clone()))?; + store.next_replay_event_validation::(func_type)?; } - let _ = (args, param_types, store); + let _ = (args, func_type, store); Ok(()) } /// Record hook operation for host function return events #[inline] - pub fn record_host_func_return( - args: &[ValRaw], - result_types: &TypeTuple, - store: &mut StoreOpaque, - ) -> Result<()> { + pub fn record_host_func_return(args: &[ValRaw], store: &mut StoreOpaque) -> Result<()> { #[cfg(feature = "rr-component")] store.record_event(|| HostFuncReturnEvent::new(args))?; - let _ = (args, result_types, store); + let _ = (args, store); Ok(()) } @@ -289,7 +285,7 @@ where let param_tys = InterfaceType::Tuple(ty.params); let result_tys = InterfaceType::Tuple(ty.results); - rr_hooks::record_replay_host_func_entry(storage, &types[ty.params], cx.0)?; + rr_hooks::record_replay_host_func_entry(storage, &ty, cx.0)?; // There's a 2x2 matrix of whether parameters and results are stored on the // stack or on the heap. Each of the 4 branches here have a different @@ -323,7 +319,7 @@ where flags.set_may_leave(false); let mut lower = LowerContext::new(cx, &options, types, instance); - storage.lower_results(&mut lower, result_tys, &types[ty.results], ret)?; + storage.lower_results(&mut lower, result_tys, ret)?; flags.set_may_leave(true); lower.exit_call()?; @@ -370,14 +366,13 @@ where &mut self, cx: &mut LowerContext<'_, T>, ty: InterfaceType, - record_tys: &TypeTuple, ret: R, ) -> Result<()> { let direct_results_lower = |cx: &mut LowerContext<'_, T>, dst: &mut MaybeUninit<::Lower>, ret: R| { let res = rr_hooks::record_lower(|cx, ty| ret.lower(cx, ty, dst), cx, ty); - rr_hooks::record_host_func_return(storage_as_slice(dst), record_tys, cx.store.0)?; + rr_hooks::record_host_func_return(storage_as_slice(dst), cx.store.0)?; res }; let indirect_results_lower = |cx: &mut LowerContext<'_, T>, dst: &ValRaw, ret: R| { @@ -385,7 +380,7 @@ where let res = rr_hooks::record_lower_store(|cx, ty, ptr| ret.store(cx, ty, ptr), cx, ty, ptr); // Recording here is just for marking the return event - rr_hooks::record_host_func_return(&[], record_tys, cx.store.0)?; + rr_hooks::record_host_func_return(&[], cx.store.0)?; res }; match self { @@ -512,7 +507,7 @@ where let param_tys = &types[func_ty.params]; let result_tys = &types[func_ty.results]; - rr_hooks::record_replay_host_func_entry(storage, &types[func_ty.params], store.0)?; + rr_hooks::record_replay_host_func_entry(storage, &types[ty], store.0)?; if !store.0.replay_enabled() { let args; @@ -567,7 +562,6 @@ where assert!(dst.next().is_none()); rr_hooks::record_host_func_return( mem::transmute::<&[MaybeUninit], &[ValRaw]>(storage), - result_tys, cx.store.0, )?; } else { @@ -583,7 +577,7 @@ where )?; } // Recording here is just for marking the return event - rr_hooks::record_host_func_return(&[], result_tys, cx.store.0)?; + rr_hooks::record_host_func_return(&[], cx.store.0)?; } flags.set_may_leave(true); diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index 84b752ab80ff..c9666cb97112 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -652,13 +652,11 @@ impl<'a, T: 'static> LowerContext<'a, T> { bail!("Cannot call back into host during lowering") } // Unwrapping should never occur on valid executions since *Entry should be before *Return in trace - RREvent::ComponentReallocReturn(e) => { + RREvent::ComponentReallocReturn(e) => + { #[cfg(feature = "rr-type-validation")] if run_validate { lowering_error = e.validate(&realloc_stack.pop().unwrap()).err() - } else { - // Error is subsumed by the LowerReturn or the LowerStoreReturn - lowering_error = None } } RREvent::ComponentLowerEntry(_) => { diff --git a/crates/wasmtime/src/runtime/rr/events/component_events.rs b/crates/wasmtime/src/runtime/rr/events/component_events.rs index 862f7a4e508f..afdab3d94387 100644 --- a/crates/wasmtime/src/runtime/rr/events/component_events.rs +++ b/crates/wasmtime/src/runtime/rr/events/component_events.rs @@ -4,7 +4,7 @@ use super::*; #[expect(unused_imports, reason = "used for doc-links")] use crate::component::{Component, ComponentType}; use wasmtime_environ::component::InterfaceType; -use wasmtime_environ::component::TypeTuple; +use wasmtime_environ::component::TypeFunc; /// A [`Component`] instantiatation event #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -27,26 +27,30 @@ pub struct HostFuncEntryEvent { /// Raw values passed across the call entry boundary args: RRFuncArgVals, - /// Optional param/return types (required to support replay validation). + /// Param/return types (required to support replay validation). /// /// Note: This relies on the invariant that [InterfaceType] will always be /// deterministic. Currently, the type indices into various [ComponentTypes] /// maintain this, allowing for quick type-checking. - types: Option, + types: TypeFunc, } impl HostFuncEntryEvent { // Record - pub fn new(args: &[MaybeUninit], types: Option<&TypeTuple>) -> Self { + pub fn new(args: &[MaybeUninit], types: TypeFunc) -> Self { Self { args: func_argvals_from_raw_slice(args), - types: types.cloned(), + types: types, } } } -impl Validate for HostFuncEntryEvent { - fn validate(&self, expect_types: &TypeTuple) -> Result<(), ReplayError> { +impl Validate for HostFuncEntryEvent { + fn validate(&self, expect_types: &TypeFunc) -> Result<(), ReplayError> { self.log(); - replay_args_typecheck(self.types.as_ref(), expect_types) + if &self.types == expect_types { + Ok(()) + } else { + Err(ReplayError::FailedValidation) + } } } @@ -145,7 +149,13 @@ impl Validate> for ReallocReturnEvent { self.log(); // Cannot just use eq since anyhow::Error and EventActionError cannot be compared match (self.ret.as_ref(), expect_ret.as_ref()) { - (Ok(r), Ok(s)) => replay_args_valcheck(r, s), + (Ok(r), Ok(s)) => { + if r == s { + Ok(()) + } else { + Err(ReplayError::FailedValidation) + } + } // Return the recorded error (Err(e), Err(f)) => Err(ReplayError::from(EventActionError::ReallocError(format!( "Replayed Realloc Error: {} \nRecorded Realloc Error: {}", diff --git a/crates/wasmtime/src/runtime/rr/events/mod.rs b/crates/wasmtime/src/runtime/rr/events/mod.rs index 3a3f40cf3ac5..13cb173dc57d 100644 --- a/crates/wasmtime/src/runtime/rr/events/mod.rs +++ b/crates/wasmtime/src/runtime/rr/events/mod.rs @@ -91,51 +91,6 @@ where } } -/// Typechecking validation for replay, if `src_types` exist -/// -/// Returns [`ReplayError::FailedValidation`] if typechecking fails -#[inline(always)] -fn replay_args_typecheck(src_types: Option, expect_types: T) -> Result<(), ReplayError> -where - T: PartialEq, -{ - #[cfg(feature = "rr-type-validation")] - { - if let Some(types) = src_types { - if types == expect_types { - Ok(()) - } else { - Err(ReplayError::FailedValidation) - } - } else { - println!( - "Warning: Replay typechecking cannot be performed since recorded trace is missing validation data" - ); - Ok(()) - } - } - #[cfg(not(feature = "rr-type-validation"))] - Ok(()) -} - -/// Validation of values -#[inline(always)] -fn replay_args_valcheck(src_val: T, expect_val: T) -> Result<(), ReplayError> -where - T: PartialEq, -{ - #[cfg(feature = "rr-type-validation")] - { - if src_val == expect_val { - Ok(()) - } else { - Err(ReplayError::FailedValidation) - } - } - #[cfg(not(feature = "rr-type-validation"))] - Ok(()) -} - /// Trait signifying types that can be validated on replay /// /// All `PartialEq` and `Eq` types are directly validatable with themselves. From b0dfb0ff90ec4474ddfe68bad926f5b62a15bdff Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 5 Aug 2025 16:58:05 -0700 Subject: [PATCH 58/62] Add validation tests and gating across whole project --- Cargo.toml | 2 + crates/cli-flags/Cargo.toml | 1 + crates/cli-flags/src/lib.rs | 8 ++- crates/wasmtime/Cargo.toml | 2 +- .../src/runtime/component/func/host.rs | 27 ++++++---- .../src/runtime/component/func/options.rs | 50 ++++++++++--------- crates/wasmtime/src/runtime/func.rs | 2 +- crates/wasmtime/src/runtime/rr/mod.rs | 3 +- crates/wasmtime/src/runtime/store.rs | 6 ++- src/commands/replay.rs | 4 ++ 10 files changed, 63 insertions(+), 42 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 078bfa0021a0..a4b3b1da98cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -495,9 +495,11 @@ stack-switching = ["wasmtime/stack-switching", "wasmtime-cli-flags/stack-switchi # `rr` only configures the base infrastructure and is not practically useful by itself # Use `rr-component` or `rr-core` for generating record/replay events for components/core wasm # respectively +# Use `rr-validate` for additional validation checks over the above two features rr = ["wasmtime-cli-flags/rr"] rr-component = ["wasmtime/rr-component", "rr"] rr-core = ["wasmtime/rr-core", "rr"] +rr-validate = ["wasmtime/rr-validate", "wasmtime-cli-flags/rr-validate"] # CLI subcommands for the `wasmtime` executable. See `wasmtime $cmd --help` # for more information on each subcommand. diff --git a/crates/cli-flags/Cargo.toml b/crates/cli-flags/Cargo.toml index 2874fb092e20..062dd85b14b2 100644 --- a/crates/cli-flags/Cargo.toml +++ b/crates/cli-flags/Cargo.toml @@ -41,3 +41,4 @@ memory-protection-keys = ["wasmtime/memory-protection-keys"] pulley = ["wasmtime/pulley"] stack-switching = ["wasmtime/stack-switching"] rr = ["wasmtime/rr"] +rr-validate = ["wasmtime/rr-validate"] diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs index 9fdf1f28cfbe..1f89493e9547 100644 --- a/crates/cli-flags/src/lib.rs +++ b/crates/cli-flags/src/lib.rs @@ -3,11 +3,9 @@ use anyhow::{Context, Result}; use clap::Parser; use serde::Deserialize; -use std::io::BufWriter; use std::{ fmt, fs, path::{Path, PathBuf}, - sync::Arc, time::Duration, }; use wasmtime::Config; @@ -1015,8 +1013,14 @@ impl CommonOptions { match_feature! { ["rr" : record.path.clone()] path => { + use std::{io::BufWriter, sync::Arc}; use wasmtime::{RecordConfig, RecordSettings}; let default_settings = RecordSettings::default(); + match_feature! { + ["rr-validate": record.validation_metadata] + _v => (), + _ => err, + } config.enable_record(RecordConfig { writer_initializer: Arc::new(move || { Box::new(BufWriter::new(fs::File::create(&path).unwrap())) diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml index 6eab80f06d97..02dba1f54f63 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -409,5 +409,5 @@ rr-core = ["rr"] # Support for validation signatures/checks during record/replay respectively. # This feature only makes sense if 'rr-component' or 'rr-core' is enabled -rr-type-validation = ["rr"] +rr-validate = ["rr"] diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index cf974698add8..ca553367313d 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -24,8 +24,7 @@ mod rr_hooks { use super::*; #[cfg(feature = "rr-component")] use crate::rr::component_events::{ - HostFuncReturnEvent, LowerEntryEvent, LowerReturnEvent, LowerStoreEntryEvent, - LowerStoreReturnEvent, + HostFuncReturnEvent, LowerReturnEvent, LowerStoreReturnEvent, }; /// Record/replay hook operation for host function entry events #[inline] @@ -34,7 +33,7 @@ mod rr_hooks { func_type: &TypeFunc, store: &mut StoreOpaque, ) -> Result<()> { - #[cfg(all(feature = "rr-component", feature = "rr-type-validation"))] + #[cfg(all(feature = "rr-component", feature = "rr-validate"))] { use crate::rr::component_events::HostFuncEntryEvent; store.record_event_validation(|| HostFuncEntryEvent::new(args, func_type.clone()))?; @@ -64,10 +63,13 @@ mod rr_hooks { where F: FnOnce(&mut LowerContext<'_, T>, InterfaceType, usize) -> Result<()>, { - #[cfg(all(feature = "rr-component", feature = "rr-type-validation"))] - cx.store - .0 - .record_event_validation(|| LowerStoreEntryEvent::new(ty, offset))?; + #[cfg(all(feature = "rr-component", feature = "rr-validate"))] + { + use crate::rr::component_events::LowerStoreEntryEvent; + cx.store + .0 + .record_event_validation(|| LowerStoreEntryEvent::new(ty, offset))?; + } let store_result = lower_store(cx, ty, offset); #[cfg(feature = "rr-component")] cx.store @@ -86,10 +88,13 @@ mod rr_hooks { where F: FnOnce(&mut LowerContext<'_, T>, InterfaceType) -> Result<()>, { - #[cfg(all(feature = "rr-component", feature = "rr-type-validation"))] - cx.store - .0 - .record_event_validation(|| LowerEntryEvent::new(ty))?; + #[cfg(all(feature = "rr-component", feature = "rr-validate"))] + { + use crate::rr::component_events::LowerEntryEvent; + cx.store + .0 + .record_event_validation(|| LowerEntryEvent::new(ty))?; + } let lower_result = lower(cx, ty); #[cfg(feature = "rr-component")] cx.store diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index c9666cb97112..3a31e921a986 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -4,10 +4,12 @@ use crate::component::ResourceType; use crate::component::matching::InstanceType; use crate::component::resources::{HostResourceData, HostResourceIndex, HostResourceTables}; use crate::prelude::*; +#[cfg(feature = "rr-validate")] +use crate::rr::Validate; #[cfg(feature = "rr-component")] use crate::rr::component_events::{MemorySliceWriteEvent, ReallocEntryEvent, ReallocReturnEvent}; #[cfg(feature = "rr-component")] -use crate::rr::{RREvent, RecordBuffer, Recorder, ReplayError, Replayer, Validate}; +use crate::rr::{RREvent, RecordBuffer, Recorder, ReplayError, Replayer}; use crate::runtime::vm::component::{ CallContexts, ComponentInstance, InstanceFlags, ResourceTable, ResourceTables, }; @@ -594,14 +596,14 @@ impl<'a, T: 'static> LowerContext<'a, T> { let mut complete = false; let mut lowering_error: Option = None; // No nested expected; these depths should only be 1 - let mut realloc_stack = Vec::>::new(); + let mut _realloc_stack = Vec::>::new(); // Lowering tracks is only for ordering entry/exit events - let mut lower_stack = Vec::<()>::new(); - let mut lower_store_stack = Vec::<()>::new(); + let mut _lower_stack = Vec::<()>::new(); + let mut _lower_store_stack = Vec::<()>::new(); while !complete { let buf = self.store.0.replay_buffer_mut().unwrap(); let event = buf.next_event()?; - let run_validate = buf.settings().validate && buf.trace_settings().add_validation; + let _run_validate = buf.settings().validate && buf.trace_settings().add_validation; match event { RREvent::ComponentHostFuncReturn(e) => { // End of the lowering process @@ -616,23 +618,23 @@ impl<'a, T: 'static> LowerContext<'a, T> { RREvent::ComponentReallocEntry(e) => { let _result = self.realloc_inner(e.old_addr, e.old_size, e.old_align, e.new_size); - #[cfg(feature = "rr-type-validation")] - if run_validate { - realloc_stack.push(_result); + #[cfg(feature = "rr-validate")] + if _run_validate { + _realloc_stack.push(_result); } } // No return value to validate for lower/lower-store; store error and just check that entry happened before RREvent::ComponentLowerReturn(e) => { - #[cfg(feature = "rr-type-validation")] - if run_validate { - lower_stack.pop().ok_or(ReplayError::InvalidOrdering)?; + #[cfg(feature = "rr-validate")] + if _run_validate { + _lower_stack.pop().ok_or(ReplayError::InvalidOrdering)?; } lowering_error = e.ret().map_err(Into::into).err(); } RREvent::ComponentLowerStoreReturn(e) => { - #[cfg(feature = "rr-type-validation")] - if run_validate { - lower_store_stack + #[cfg(feature = "rr-validate")] + if _run_validate { + _lower_store_stack .pop() .ok_or(ReplayError::InvalidOrdering)?; } @@ -652,25 +654,25 @@ impl<'a, T: 'static> LowerContext<'a, T> { bail!("Cannot call back into host during lowering") } // Unwrapping should never occur on valid executions since *Entry should be before *Return in trace - RREvent::ComponentReallocReturn(e) => + RREvent::ComponentReallocReturn(_e) => { - #[cfg(feature = "rr-type-validation")] - if run_validate { - lowering_error = e.validate(&realloc_stack.pop().unwrap()).err() + #[cfg(feature = "rr-validate")] + if _run_validate { + lowering_error = _e.validate(&_realloc_stack.pop().unwrap()).err() } } RREvent::ComponentLowerEntry(_) => { // All we want here is ensuring Entry occurs before Return - #[cfg(feature = "rr-type-validation")] - if run_validate { - lower_stack.push(()) + #[cfg(feature = "rr-validate")] + if _run_validate { + _lower_stack.push(()) } } RREvent::ComponentLowerStoreEntry(_) => { // All we want here is ensuring Entry occurs before Return - #[cfg(feature = "rr-type-validation")] - if run_validate { - lower_store_stack.push(()) + #[cfg(feature = "rr-validate")] + if _run_validate { + _lower_store_stack.push(()) } } diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index fcaa191e2fe6..e0e222d70b41 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -1520,7 +1520,7 @@ mod rr_hooks { wasm_func_type: &WasmFuncType, store: &mut StoreOpaque, ) -> Result<()> { - #[cfg(all(feature = "rr-core", feature = "rr-type-validation"))] + #[cfg(all(feature = "rr-core", feature = "rr-validate"))] { // Record/replay the raw parameter args use crate::rr::core_events::HostFuncEntryEvent; diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index b9c7fe83512e..692e22faed26 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -272,6 +272,7 @@ pub trait Replayer: Iterator { /// In addition to errors in [`next_event_typed`](Replayer::next_event_typed), /// validation errors can be thrown #[inline] + #[cfg(feature = "rr-validate")] fn next_event_validation(&mut self, expect: &Y) -> Result<(), ReplayError> where T: TryFrom + Validate, @@ -416,7 +417,7 @@ impl Replayer for ReplayBuffer { ); // Read the recording settings - let trace_settings: RecordSettings = io::from_replay_reader(&mut reader, &mut [0; 0])?; + let trace_settings: RecordSettings = io::from_replay_reader(&mut reader, &mut scratch)?; if settings.validate && !trace_settings.add_validation { log::warn!( diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index bd0b696fcaea..617a74a727b3 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -79,8 +79,10 @@ use crate::RootSet; use crate::module::RegisteredModuleId; use crate::prelude::*; +#[cfg(feature = "rr-validate")] +use crate::rr::Validate; #[cfg(feature = "rr")] -use crate::rr::{RREvent, RecordBuffer, Recorder, ReplayBuffer, ReplayError, Replayer, Validate}; +use crate::rr::{RREvent, RecordBuffer, Recorder, ReplayBuffer, ReplayError, Replayer}; #[cfg(feature = "gc")] use crate::runtime::vm::GcRootsList; #[cfg(feature = "stack-switching")] @@ -1440,7 +1442,7 @@ impl StoreOpaque { /// and if validation is enabled on replay, and run the validation check /// /// Convenience wrapper around [`Replayer::next_event_validation`] - #[cfg(feature = "rr")] + #[cfg(all(feature = "rr", feature = "rr-validate"))] #[inline] pub(crate) fn next_replay_event_validation( &mut self, diff --git a/src/commands/replay.rs b/src/commands/replay.rs index a0acdc55837d..49e5a93e794a 100644 --- a/src/commands/replay.rs +++ b/src/commands/replay.rs @@ -38,6 +38,10 @@ pub struct ReplayCommand { impl ReplayCommand { /// Executes the command. pub fn execute(self) -> Result<()> { + #[cfg(not(feature = "rr-validate"))] + if self.replay_opts.validate { + anyhow::bail!("Cannot use `validate` when `rr-validate` feature is disabled"); + } let replay_cfg = ReplayConfig { reader_initializer: Arc::new(move || { Box::new(BufReader::new( From 205f436c5d23f4c3951150a55b60072ed2360268 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 5 Aug 2025 17:39:44 -0700 Subject: [PATCH 59/62] Added configuration flag for deserialization buffer --- crates/wasmtime/src/config.rs | 7 ++++++- crates/wasmtime/src/runtime/rr/mod.rs | 16 ++++++++++++---- src/commands/replay.rs | 8 ++++++++ 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 07c9d69095c5..54a127cdcd1e 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -272,12 +272,17 @@ pub struct RecordConfig { pub struct ReplaySettings { /// Flag to include additional signatures for replay validation. pub validate: bool, + /// Static buffer size for deserialization of variable-length types (like [String]) + pub deser_buffer_size: usize, } #[cfg(feature = "rr")] impl Default for ReplaySettings { fn default() -> Self { - Self { validate: true } + Self { + validate: false, + deser_buffer_size: 64, + } } } diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index 692e22faed26..033a361b525e 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -134,7 +134,10 @@ impl fmt::Display for ReplayError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::EmptyBuffer => { - write!(f, "replay buffer is empty!") + write!( + f, + "replay buffer is empty (or unexpected read-failure encountered). Ensure sufficient `deserialization-buffer-size` in replay settings if you included `validation-metadata` during recording" + ) } Self::FailedValidation => { write!(f, "replay event validation failed") @@ -367,6 +370,8 @@ pub struct ReplayBuffer { settings: ReplaySettings, /// Settings for record configuration (encoded in the trace) trace_settings: RecordSettings, + /// Intermediate static buffer for deserialization + deser_buffer: Vec, } impl Iterator for ReplayBuffer { @@ -374,7 +379,7 @@ impl Iterator for ReplayBuffer { fn next(&mut self) -> Option { // Check for EoF - let result = io::from_replay_reader(&mut self.reader, &mut [0; 0]); + let result = io::from_replay_reader(&mut self.reader, &mut self.deser_buffer); match result { Err(e) => { log::error!("Erroneous replay read: {}", e); @@ -407,8 +412,8 @@ impl Drop for ReplayBuffer { impl Replayer for ReplayBuffer { fn new_replayer(mut reader: Box, settings: ReplaySettings) -> Result { - // Ensure module versions match let mut scratch = [0u8; 12]; + // Ensure module versions match let version = io::from_replay_reader::<&str, _>(&mut reader, &mut scratch)?; assert_eq!( version, @@ -425,10 +430,13 @@ impl Replayer for ReplayBuffer { ); } + let deser_buffer = vec![0; settings.deser_buffer_size]; + Ok(ReplayBuffer { reader, settings, trace_settings, + deser_buffer, }) } @@ -470,7 +478,7 @@ mod tests { let tmppath = >::as_ref(&tmp) .to_str() .expect("Filename should be UTF-8"); - let replay_settings = ReplaySettings { validate: true }; + let replay_settings = ReplaySettings::default(); // Assert that replayed values are identical let mut replayer = diff --git a/src/commands/replay.rs b/src/commands/replay.rs index 49e5a93e794a..9bd9aeb83f3d 100644 --- a/src/commands/replay.rs +++ b/src/commands/replay.rs @@ -23,6 +23,12 @@ pub struct ReplayOptions { /// Requires record traces to be generated with `validation_metadata` enabled. #[arg(short, long, default_value_t = false)] validate: bool, + + /// Size of static buffer needed to deserialized variable-length types like String. This is not + /// not relevant for basic functional recording/replaying, but may be required to replay traces where + /// `validation-metadata` was enabled for recording + #[arg(short, long, default_value_t = 64)] + deser_buffer_size: usize, } /// Execute a deterministic, embedding-agnostic replay of a Wasm modules given its associated recorded trace @@ -50,6 +56,8 @@ impl ReplayCommand { }), settings: ReplaySettings { validate: self.replay_opts.validate, + deser_buffer_size: self.replay_opts.deser_buffer_size, + ..Default::default() }, }; // Replay uses the `run` command harness From a2beaef14da33497da365b45b50893353d6deac6 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Wed, 6 Aug 2025 09:49:35 -0700 Subject: [PATCH 60/62] Doc comment style fix --- crates/wasmtime/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 54a127cdcd1e..cc0506cdc5ed 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -272,7 +272,7 @@ pub struct RecordConfig { pub struct ReplaySettings { /// Flag to include additional signatures for replay validation. pub validate: bool, - /// Static buffer size for deserialization of variable-length types (like [String]) + /// Static buffer size for deserialization of variable-length types (like [String]). pub deser_buffer_size: usize, } From 6afb9c1cf6cdb0d727304ef7f91e9dabbda62f7e Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Wed, 6 Aug 2025 14:17:06 -0700 Subject: [PATCH 61/62] Added panic stubs for rr in libcalls --- crates/wasmtime/src/runtime/rr/events/mod.rs | 22 ++++++++ crates/wasmtime/src/runtime/rr/mod.rs | 54 +++++++++++++------ .../src/runtime/vm/component/libcalls.rs | 44 +++++++++++++-- 3 files changed, 101 insertions(+), 19 deletions(-) diff --git a/crates/wasmtime/src/runtime/rr/events/mod.rs b/crates/wasmtime/src/runtime/rr/events/mod.rs index 13cb173dc57d..b5d062a39adb 100644 --- a/crates/wasmtime/src/runtime/rr/events/mod.rs +++ b/crates/wasmtime/src/runtime/rr/events/mod.rs @@ -125,5 +125,27 @@ where } } +/// Events used as markers for debugging/testing in traces +/// +/// Marker events should be injectable at any point in a record +/// trace without impacting functional correctness of replay +pub mod marker_events { + use crate::prelude::*; + use serde::{Deserialize, Serialize}; + + /// A Nop event + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct NopEvent; + + /// An event for custom String messages + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct CustomMessageEvent(pub String); + impl From<&str> for CustomMessageEvent { + fn from(v: &str) -> Self { + Self(v.into()) + } + } +} + pub mod component_events; pub mod core_events; diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index 033a361b525e..f070716134af 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -7,6 +7,9 @@ //! //! This module does NOT support RR for component builtins yet. +#[cfg(feature = "component-model-async")] +compile_error!("Support for `rr` not available with `component-model-async`"); + use crate::config::{ModuleVersionStrategy, RecordSettings, ReplaySettings}; use crate::prelude::*; use core::fmt; @@ -15,13 +18,13 @@ use serde::{Deserialize, Serialize}; // Use component/core events internally even without feature flags enabled // so that [`RREvent`] has a well-defined serialization format, but export // it for other modules only when enabled -pub use events::Validate; use events::component_events as __component_events; #[cfg(feature = "rr-component")] pub use events::component_events; use events::core_events as __core_events; #[cfg(feature = "rr-core")] pub use events::core_events; +pub use events::{Validate, marker_events}; pub use io::{RecordWriter, ReplayReader}; /// Encapsulation of event types comprising an [`RREvent`] sum type @@ -86,6 +89,12 @@ macro_rules! rr_event { // Set of supported record/replay events rr_event! { + // Marker events + /// Nop Event + Nop(marker_events::NopEvent), + /// A custom message + CustomMessage(marker_events::CustomMessageEvent), + /// Call into host function from Core Wasm CoreHostFuncEntry(__core_events::HostFuncEntryEvent), /// Return from host function to Core Wasm @@ -120,6 +129,17 @@ rr_event! { ComponentLowerStoreEntry(__component_events::LowerStoreEntryEvent) } +impl RREvent { + /// Indicates whether current event is a marker event + #[inline] + fn is_marker(&self) -> bool { + match self { + Self::Nop(_) | Self::CustomMessage(_) => true, + _ => false, + } + } +} + /// Error type signalling failures during a replay run #[derive(Debug, PartialEq, Eq)] pub enum ReplayError { @@ -221,7 +241,7 @@ pub trait Replayer: Iterator { // Provided Methods - /// Pop the next replay event + /// Get the next functional replay event (skips past all non-marker events) /// /// ## Errors /// @@ -378,21 +398,25 @@ impl Iterator for ReplayBuffer { type Item = RREvent; fn next(&mut self) -> Option { - // Check for EoF - let result = io::from_replay_reader(&mut self.reader, &mut self.deser_buffer); - match result { - Err(e) => { - log::error!("Erroneous replay read: {}", e); - None - } - Ok(event) => { - if let RREvent::Eof = event { - None - } else { - Some(event) + let ret = 'event_loop: loop { + let result = io::from_replay_reader(&mut self.reader, &mut self.deser_buffer); + match result { + Err(e) => { + log::error!("Erroneous replay read: {}", e); + break 'event_loop None; + } + Ok(event) => { + if let RREvent::Eof = &event { + break 'event_loop None; + } else if event.is_marker() { + continue 'event_loop; + } else { + break 'event_loop Some(event); + } } } - } + }; + ret } } diff --git a/crates/wasmtime/src/runtime/vm/component/libcalls.rs b/crates/wasmtime/src/runtime/vm/component/libcalls.rs index 47c475af6924..3dbef8058e07 100644 --- a/crates/wasmtime/src/runtime/vm/component/libcalls.rs +++ b/crates/wasmtime/src/runtime/vm/component/libcalls.rs @@ -499,13 +499,37 @@ fn inflate_latin1_bytes(dst: &mut [u16], latin1_bytes_so_far: usize) -> &mut [u1 return rest; } +/// Hook for record/replay of libcalls. Currently stubbed for record and panics on replay +/// +/// TODO: Implement libcall hooks +#[inline] +unsafe fn rr_hook(instance: &mut ComponentInstance, libcall: &str) -> Result<()> { + #[cfg(feature = "rr-component")] + { + let store = instance.store(); + if (*store).replay_enabled() { + return Err(anyhow!( + "Replay support for libcall {libcall:?} not yet supported!" + )); + } else { + use crate::rr::marker_events::CustomMessageEvent; + (*store).record_event(|| CustomMessageEvent::from(libcall))?; + } + } + let _ = (instance, libcall); + Ok(()) +} + unsafe fn resource_new32( vmctx: NonNull, resource: u32, rep: u32, ) -> Result { let resource = TypeResourceTableIndex::from_u32(resource); - ComponentInstance::from_vmctx(vmctx, |instance| instance.resource_new32(resource, rep)) + ComponentInstance::from_vmctx(vmctx, |instance| { + rr_hook(instance, "resource_new32")?; + instance.resource_new32(resource, rep) + }) } unsafe fn resource_rep32( @@ -514,7 +538,10 @@ unsafe fn resource_rep32( idx: u32, ) -> Result { let resource = TypeResourceTableIndex::from_u32(resource); - ComponentInstance::from_vmctx(vmctx, |instance| instance.resource_rep32(resource, idx)) + ComponentInstance::from_vmctx(vmctx, |instance| { + rr_hook(instance, "resource_rep32")?; + instance.resource_rep32(resource, idx) + }) } unsafe fn resource_drop( @@ -524,6 +551,7 @@ unsafe fn resource_drop( ) -> Result { let resource = TypeResourceTableIndex::from_u32(resource); ComponentInstance::from_vmctx(vmctx, |instance| { + rr_hook(instance, "resource_drop")?; Ok(ResourceDropRet(instance.resource_drop(resource, idx)?)) }) } @@ -550,6 +578,7 @@ unsafe fn resource_transfer_own( let src_table = TypeResourceTableIndex::from_u32(src_table); let dst_table = TypeResourceTableIndex::from_u32(dst_table); ComponentInstance::from_vmctx(vmctx, |instance| { + rr_hook(instance, "resource_transfer_own")?; instance.resource_transfer_own(src_idx, src_table, dst_table) }) } @@ -563,16 +592,23 @@ unsafe fn resource_transfer_borrow( let src_table = TypeResourceTableIndex::from_u32(src_table); let dst_table = TypeResourceTableIndex::from_u32(dst_table); ComponentInstance::from_vmctx(vmctx, |instance| { + rr_hook(instance, "resource_transfer_borrow")?; instance.resource_transfer_borrow(src_idx, src_table, dst_table) }) } unsafe fn resource_enter_call(vmctx: NonNull) { - ComponentInstance::from_vmctx(vmctx, |instance| instance.resource_enter_call()) + ComponentInstance::from_vmctx(vmctx, |instance| { + rr_hook(instance, "resource_enter_call").unwrap(); + instance.resource_enter_call() + }) } unsafe fn resource_exit_call(vmctx: NonNull) -> Result<()> { - ComponentInstance::from_vmctx(vmctx, |instance| instance.resource_exit_call()) + ComponentInstance::from_vmctx(vmctx, |instance| { + rr_hook(instance, "resource_exit_call")?; + instance.resource_exit_call() + }) } unsafe fn trap(_vmctx: NonNull, code: u8) -> Result { From 0025593db08cb3e0b1361865c9bc961b21c8e876 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Wed, 6 Aug 2025 14:18:17 -0700 Subject: [PATCH 62/62] fixup! Added panic stubs for rr in libcalls --- crates/wasmtime/src/runtime/vm/component/libcalls.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/wasmtime/src/runtime/vm/component/libcalls.rs b/crates/wasmtime/src/runtime/vm/component/libcalls.rs index 3dbef8058e07..84c71af76df1 100644 --- a/crates/wasmtime/src/runtime/vm/component/libcalls.rs +++ b/crates/wasmtime/src/runtime/vm/component/libcalls.rs @@ -508,9 +508,7 @@ unsafe fn rr_hook(instance: &mut ComponentInstance, libcall: &str) -> Result<()> { let store = instance.store(); if (*store).replay_enabled() { - return Err(anyhow!( - "Replay support for libcall {libcall:?} not yet supported!" - )); + bail!("Replay support for libcall {libcall:?} not yet supported!"); } else { use crate::rr::marker_events::CustomMessageEvent; (*store).record_event(|| CustomMessageEvent::from(libcall))?;