From afd436996e747c73465e08f7208ae97a5210e7fb Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Wed, 6 Aug 2025 13:55:17 -0700 Subject: [PATCH 01/73] Publish dev artifacts from CI Helps get some binaries to test with --- .github/workflows/publish-artifacts.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-artifacts.yml b/.github/workflows/publish-artifacts.yml index 3f6565e2f2..70fcab53b7 100644 --- a/.github/workflows/publish-artifacts.yml +++ b/.github/workflows/publish-artifacts.yml @@ -13,7 +13,7 @@ jobs: publish: name: Publish artifacts of build runs-on: ubuntu-latest - if: github.repository == 'bytecodealliance/wasmtime' + if: github.repository == 'bytecodealliance/wasmtime' || github.repository == 'bytecodealliance/wasmtime-rr-prototyping' steps: - uses: actions/checkout@v4 - uses: ./.github/actions/fetch-run-id From c106978fbef3cbd813c87de67813edcd6729ebc1 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Wed, 6 Aug 2025 13:57:36 -0700 Subject: [PATCH 02/73] Test out the merge queue --- .github/workflows/publish-artifacts.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish-artifacts.yml b/.github/workflows/publish-artifacts.yml index 70fcab53b7..1030cc973a 100644 --- a/.github/workflows/publish-artifacts.yml +++ b/.github/workflows/publish-artifacts.yml @@ -14,6 +14,7 @@ jobs: name: Publish artifacts of build runs-on: ubuntu-latest if: github.repository == 'bytecodealliance/wasmtime' || github.repository == 'bytecodealliance/wasmtime-rr-prototyping' + steps: - uses: actions/checkout@v4 - uses: ./.github/actions/fetch-run-id From 71291fe17445c2bd7ec7c0c707a88c1428ac6f36 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Wed, 4 Jun 2025 11:53:13 -0400 Subject: [PATCH 03/73] Squashed RR rebase changes commit with CI support --- Cargo.lock | 10 +- Cargo.toml | 3 + crates/cli-flags/Cargo.toml | 2 + crates/cli-flags/src/lib.rs | 81 +++ crates/environ/src/component/artifacts.rs | 2 + crates/wasmtime/Cargo.toml | 12 + crates/wasmtime/src/compile.rs | 3 + crates/wasmtime/src/config.rs | 220 +++++++ crates/wasmtime/src/engine.rs | 10 + crates/wasmtime/src/engine/serialization.rs | 6 +- crates/wasmtime/src/runtime.rs | 1 + .../src/runtime/component/component.rs | 14 + .../concurrent/futures_and_streams.rs | 16 +- .../src/runtime/component/func/host.rs | 572 ++++++++++++------ .../src/runtime/component/func/options.rs | 333 +++++++++- .../src/runtime/component/func/typed.rs | 19 +- .../src/runtime/component/instance.rs | 12 + crates/wasmtime/src/runtime/func.rs | 158 ++++- crates/wasmtime/src/runtime/instance.rs | 14 + .../src/runtime/rr/events/component_events.rs | 205 +++++++ .../src/runtime/rr/events/core_events.rs | 59 ++ crates/wasmtime/src/runtime/rr/events/mod.rs | 154 +++++ crates/wasmtime/src/runtime/rr/io.rs | 70 +++ crates/wasmtime/src/runtime/rr/mod.rs | 540 +++++++++++++++++ crates/wasmtime/src/runtime/store.rs | 139 +++++ .../src/runtime/vm/component/libcalls.rs | 25 + crates/wasmtime/src/runtime/vm/vmcontext.rs | 12 + src/bin/wasmtime.rs | 21 +- src/commands.rs | 5 + src/commands/replay.rs | 66 ++ src/commands/run.rs | 12 +- 31 files changed, 2540 insertions(+), 256 deletions(-) create mode 100644 crates/wasmtime/src/runtime/rr/events/component_events.rs create mode 100644 crates/wasmtime/src/runtime/rr/events/core_events.rs create mode 100644 crates/wasmtime/src/runtime/rr/events/mod.rs create mode 100644 crates/wasmtime/src/runtime/rr/io.rs create mode 100644 crates/wasmtime/src/runtime/rr/mod.rs create mode 100644 src/commands/replay.rs diff --git a/Cargo.lock b/Cargo.lock index 543b56dec6..07469f5cf1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1219,6 +1219,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 = "37.0.0" @@ -2759,7 +2765,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a55c51ee6c0db07e68448e336cf8ea4131a620edefebf9893e759b2d793420f8" dependencies = [ "cobs", - "embedded-io", + "embedded-io 0.4.0", "serde", ] @@ -4455,6 +4461,7 @@ dependencies = [ "cc", "cfg-if", "cranelift-native", + "embedded-io 0.6.1", "encoding_rs", "env_logger 0.11.5", "futures", @@ -4480,6 +4487,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "sha2", "smallvec", "target-lexicon", "tempfile", diff --git a/Cargo.toml b/Cargo.toml index 5fb9e36bd0..55d7c92da1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -523,6 +523,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/rr", "wasmtime-cli-flags/rr"] +rr-component = ["rr", "wasmtime/rr-component"] +rr-validate = ["rr", "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 1469c9535b..062dd85b14 100644 --- a/crates/cli-flags/Cargo.toml +++ b/crates/cli-flags/Cargo.toml @@ -40,3 +40,5 @@ threads = ["wasmtime/threads"] 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 64f109f349..49a220c15d 100644 --- a/crates/cli-flags/src/lib.rs +++ b/crates/cli-flags/src/lib.rs @@ -484,6 +484,25 @@ 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 `wasmtime replay` for details). + pub validation_metadata: Option, + /// Window size of internal buffering for record events (large windows offer more opportunities + /// for coalescing events at the cost of memory usage). + pub event_window_size: Option, + } + + enum Record { + ... + } +} + #[derive(Debug, Clone, PartialEq)] pub struct WasiNnGraph { pub format: String, @@ -534,6 +553,18 @@ 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-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) + #[arg(short = 'R', long = "record", value_name = "KEY[=VAL[,..]]")] + #[serde(skip)] + record_raw: Vec>, + // These fields are filled in by the `configure` method below via the // options parsed from the CLI above. This is what the CLI should use. #[arg(skip)] @@ -560,6 +591,10 @@ pub struct CommonOptions { #[serde(rename = "wasi", default)] pub wasi: WasiOptions, + #[arg(skip)] + #[serde(rename = "record", default)] + pub record: RecordOptions, + /// The target triple; default is the host triple #[arg(long, value_name = "TARGET")] #[serde(skip)] @@ -606,12 +641,14 @@ impl CommonOptions { debug_raw: Vec::new(), wasm_raw: Vec::new(), wasi_raw: Vec::new(), + record_raw: Vec::new(), configured: true, opts: Default::default(), codegen: Default::default(), debug: Default::default(), wasm: Default::default(), wasi: Default::default(), + record: Default::default(), target: None, config: None, } @@ -629,12 +666,14 @@ impl CommonOptions { self.debug = toml_options.debug; self.wasm = toml_options.wasm; self.wasi = toml_options.wasi; + self.record = toml_options.record; } self.opts.configure_with(&self.opts_raw); self.codegen.configure_with(&self.codegen_raw); self.debug.configure_with(&self.debug_raw); self.wasm.configure_with(&self.wasm_raw); self.wasi.configure_with(&self.wasi_raw); + self.record.configure_with(&self.record_raw); Ok(()) } @@ -979,6 +1018,35 @@ impl CommonOptions { true => err, } + let record = &self.record; + 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())) + }), + 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) } @@ -1093,6 +1161,7 @@ mod tests { [debug] [wasm] [wasi] + [record] "#; let mut common_options: CommonOptions = toml::from_str(basic_toml).unwrap(); common_options.config(None).unwrap(); @@ -1214,6 +1283,8 @@ impl fmt::Display for CommonOptions { wasm, wasi_raw, wasi, + record_raw, + record, configured, target, config, @@ -1230,6 +1301,7 @@ impl fmt::Display for CommonOptions { let wasi_flags; let wasm_flags; let debug_flags; + let record_flags; if *configured { codegen_flags = codegen.to_options(); @@ -1237,6 +1309,7 @@ impl fmt::Display for CommonOptions { wasi_flags = wasi.to_options(); wasm_flags = wasm.to_options(); opts_flags = opts.to_options(); + record_flags = record.to_options(); } else { codegen_flags = codegen_raw .iter() @@ -1247,6 +1320,11 @@ impl fmt::Display for CommonOptions { wasi_flags = wasi_raw.iter().flat_map(|t| t.0.iter()).cloned().collect(); wasm_flags = wasm_raw.iter().flat_map(|t| t.0.iter()).cloned().collect(); opts_flags = opts_raw.iter().flat_map(|t| t.0.iter()).cloned().collect(); + record_flags = record_raw + .iter() + .flat_map(|t| t.0.iter()) + .cloned() + .collect(); } for flag in codegen_flags { @@ -1264,6 +1342,9 @@ impl fmt::Display for CommonOptions { for flag in debug_flags { write!(f, "-D{flag} ")?; } + for flag in record_flags { + write!(f, "-R{flag} ")?; + } Ok(()) } diff --git a/crates/environ/src/component/artifacts.rs b/crates/environ/src/component/artifacts.rs index 096ea839be..f32cf95e58 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 f8f67e81aa..b08ceee230 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -62,6 +62,8 @@ hashbrown = { workspace = true, features = ["default-hasher"] } bitflags = { workspace = true } futures = { workspace = true, features = ["alloc"], optional = true } bytes = { workspace = true, optional = true } +embedded-io = { version = "0.6.1", features = ["alloc"], optional = true } +sha2 = { version = "0.10.2", default-features = false } [target.'cfg(target_os = "windows")'.dependencies.windows-sys] workspace = true @@ -402,3 +404,13 @@ component-model-async-bytes = [ "component-model-async", "dep:bytes", ] + +# Enables support for record/replay for components +rr-component = ["component-model", "rr"] +# Enable support for the common base infrastructure of record/replay +# By default, this supports core wasm `rr`. For components, enable `rr-component` +rr = ["dep:embedded-io"] + +# Enables record/replay with support for additional validation signatures/checks. +rr-validate = ["rr"] + diff --git a/crates/wasmtime/src/compile.rs b/crates/wasmtime/src/compile.rs index 9db99d0f9d..68efb82125 100644 --- a/crates/wasmtime/src/compile.rs +++ b/crates/wasmtime/src/compile.rs @@ -26,6 +26,8 @@ use crate::Engine; use crate::hash_map::HashMap; use crate::hash_set::HashSet; use crate::prelude::*; +#[cfg(feature = "component-model")] +use sha2::{Digest, Sha256}; use std::{ any::Any, borrow::Cow, @@ -208,6 +210,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 61b3e6bf3c..a842375292 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -3,6 +3,8 @@ 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; use wasmparser::WasmFeatures; @@ -24,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")] @@ -99,6 +103,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. /// @@ -163,6 +178,8 @@ 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, } /// User-provided configuration for the compiler. @@ -219,6 +236,112 @@ impl Default for CompilerConfig { } } +/// Settings for execution recording. +#[cfg(feature = "rr")] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RecordSettings { + /// Flag to include additional signatures for replay validation. + pub add_validation: bool, + /// Maximum window size of internal event buffer. + pub event_window_size: usize, +} + +#[cfg(feature = "rr")] +impl Default for RecordSettings { + fn default() -> Self { + Self { + add_validation: false, + event_window_size: 16, + } + } +} + +/// Configuration for recording execution. +#[cfg(feature = "rr")] +#[derive(Clone)] +pub struct RecordConfig { + /// Closure that generates a writer for recording execution traces. + pub writer_initializer: Arc Box + Send + Sync>, + /// Associated metadata for configuring the recording strategy. + pub settings: RecordSettings, +} + +/// Settings for execution replay. +#[cfg(feature = "rr")] +#[derive(Debug, Clone)] +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: false, + deser_buffer_size: 64, + } + } +} + +/// Configuration for replay execution. +#[cfg(feature = "rr")] +#[derive(Clone)] +pub struct ReplayConfig { + /// 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 settings: ReplaySettings, +} + +/// Configurations for record/replay (RR) executions. +#[cfg(feature = "rr")] +#[derive(Clone)] +pub enum RRConfig { + /// Record configuration. + Record(RecordConfig), + /// 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. + /// + /// 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. + /// + /// Return [`None`] if it is not configured. + pub fn replay(&self) -> Option<&ReplayConfig> { + match self { + Self::Replay(r) => Some(r), + _ => None, + } + } +} + impl Config { /// Creates a new configuration object with the default configuration /// specified. @@ -271,6 +394,8 @@ 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"))] { @@ -1001,6 +1126,11 @@ 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" + ); self.tunables.relaxed_simd_deterministic = Some(enable); self } @@ -1318,6 +1448,11 @@ 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" + ); let val = if enable { "true" } else { "false" }; self.compiler_config .settings @@ -2675,6 +2810,91 @@ impl Config { self.tunables.signals_based_traps = Some(enable); self } + + /// 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. + #[inline] + pub fn enforce_determinism(&mut self) -> &mut Self { + #[cfg(any(feature = "cranelift", feature = "winch"))] + self.cranelift_nan_canonicalization(true); + self.relaxed_simd_deterministic(true); + self + } + + /// Remove determinstic execution enforcements (if any) applied + /// by [`Config::enforce_determinism`]. + #[inline] + pub fn remove_determinism_enforcement(&mut self) -> &mut Self { + #[cfg(any(feature = "cranelift", feature = "winch"))] + self.cranelift_nan_canonicalization(false); + self.relaxed_simd_deterministic(false); + self + } + + /// Evaluates to true if current configuration must respect + /// deterministic execution in its configuration. + /// + /// 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. + /// + /// This method implicitly enforces determinism (see [`Config::enforce_determinism`] + /// for details). + /// + /// ## Errors + /// + /// 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 { + if let RRConfig::Replay(_) = cfg { + bail!("Cannot enable recording when replay is already enabled"); + } + } + self.rr = Some(RRConfig::from(record)); + Ok(self) + } + + /// 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. + #[cfg(feature = "rr")] + 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)); + Ok(self) + } + + /// 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/ + #[cfg(feature = "rr")] + pub fn disable_record_replay(&mut self) -> &mut Self { + self.remove_determinism_enforcement(); + self.rr = None; + self + } } impl Default for Config { diff --git a/crates/wasmtime/src/engine.rs b/crates/wasmtime/src/engine.rs index 5c49b99857..a5f9c024fa 100644 --- a/crates/wasmtime/src/engine.rs +++ b/crates/wasmtime/src/engine.rs @@ -1,4 +1,6 @@ use crate::Config; +#[cfg(feature = "rr")] +use crate::RRConfig; use crate::prelude::*; #[cfg(feature = "runtime")] pub use crate::runtime::code_memory::CustomCodeMemory; @@ -254,6 +256,14 @@ impl Engine { self.config().async_support } + /// 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() + } + /// Detects whether the bytes provided are a precompiled object produced by /// Wasmtime. /// diff --git a/crates/wasmtime/src/engine/serialization.rs b/crates/wasmtime/src/engine/serialization.rs index feccd9f5dc..84accb2ee7 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.rs b/crates/wasmtime/src/runtime.rs index 43c6eee333..e715f91acd 100644 --- a/crates/wasmtime/src/runtime.rs +++ b/crates/wasmtime/src/runtime.rs @@ -48,6 +48,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/component/component.rs b/crates/wasmtime/src/runtime/component/component.rs index 00f7acf72d..b418589684 100644 --- a/crates/wasmtime/src/runtime/component/component.rs +++ b/crates/wasmtime/src/runtime/component/component.rs @@ -90,6 +90,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,15 @@ impl Component { &self.inner.realloc_func_type } + #[allow( + unused, + reason = "used only for verification with wasmtime `rr` feature \ + and requires a lot of unnecessary gating across crates" + )] + 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/concurrent/futures_and_streams.rs b/crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs index 0433bbb2fa..6e80141b4b 100644 --- a/crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs +++ b/crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs @@ -204,9 +204,9 @@ fn accept_reader, U: 'static> bail!("read pointer not aligned"); } lower - .as_slice_mut() - .get_mut(address..) - .and_then(|b| b.get_mut(..T::SIZE32 * count)) + .as_slice() + .get(address..) + .and_then(|b| b.get(..T::SIZE32 * count)) .ok_or_else(|| anyhow::anyhow!("read pointer out of bounds of memory"))?; if let Some(ty) = payload(ty, &types) { @@ -2468,7 +2468,7 @@ impl Instance { let ty = types[types[read_ty].ty].payload.unwrap(); let ptr = func::validate_inbounds_dynamic( types.canonical_abi(&ty), - lower.as_slice_mut(), + lower.as_slice(), &ValRaw::u32(read_address.try_into().unwrap()), )?; val.store(lower, ty, ptr)?; @@ -2541,9 +2541,9 @@ impl Instance { } let size = usize::try_from(abi.size32).unwrap(); lower - .as_slice_mut() - .get_mut(read_address..) - .and_then(|b| b.get_mut(..size * count)) + .as_slice() + .get(read_address..) + .and_then(|b| b.get(..size * count)) .ok_or_else(|| anyhow::anyhow!("read pointer out of bounds of memory"))?; let mut ptr = read_address; for value in values { @@ -3096,7 +3096,7 @@ impl Instance { let debug_msg_address = usize::try_from(debug_msg_address)?; // Lower the string into the component's memory let offset = lower_cx - .as_slice_mut() + .as_slice() .get(debug_msg_address..) .and_then(|b| b.get(..debug_msg.bytes().len())) .map(|_| debug_msg_address) diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index d858a71c13..d2d2cb7266 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -2,13 +2,14 @@ use crate::component::concurrent::{Accessor, Status}; 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_mut}; use crate::component::{ComponentNamedList, ComponentType, Instance, Lift, Lower, Val}; use crate::prelude::*; use crate::runtime::vm::component::{ ComponentInstance, VMComponentContext, VMLowering, VMLoweringCallee, }; use crate::runtime::vm::{SendSyncPtr, VMOpaqueContext, VMStore}; +use crate::store::StoreOpaque; use crate::{AsContextMut, CallHook, StoreContextMut, ValRaw}; use alloc::sync::Arc; use core::any::Any; @@ -16,11 +17,100 @@ use core::future::Future; use core::mem::{self, MaybeUninit}; use core::pin::Pin; use core::ptr::NonNull; +use wasmtime_environ::component::TypeFunc; use wasmtime_environ::component::{ CanonicalAbiInfo, ComponentTypes, InterfaceType, MAX_FLAT_ASYNC_PARAMS, MAX_FLAT_PARAMS, MAX_FLAT_RESULTS, OptionsIndex, TypeFuncIndex, TypeTuple, }; +/// Convenience methods to inject record + replay logic +mod rr_hooks { + use super::*; + #[cfg(feature = "rr-component")] + use crate::rr::component_events::{ + HostFuncReturnEvent, LowerReturnEvent, LowerStoreReturnEvent, + }; + /// Record/replay hook operation for host function entry events + #[inline] + pub fn record_replay_host_func_entry( + args: &mut [MaybeUninit], + func_type: &TypeFunc, + store: &mut StoreOpaque, + ) -> Result<()> { + #[cfg(all(feature = "rr-component", feature = "rr-validate"))] + { + use crate::rr::component_events::HostFuncEntryEvent; + store.record_event_validation(|| HostFuncEntryEvent::new(args, func_type.clone()))?; + store.next_replay_event_validation::(func_type)?; + } + let _ = (args, func_type, store); + Ok(()) + } + + /// Record hook operation for host function return events + #[inline] + pub fn record_host_func_return( + args: &[MaybeUninit], + store: &mut StoreOpaque, + ) -> Result<()> { + #[cfg(feature = "rr-component")] + store.record_event(|| HostFuncReturnEvent::new(args))?; + let _ = (args, store); + Ok(()) + } + + /// 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-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 + .0 + .record_event(|| LowerStoreReturnEvent::new(&store_result))?; + store_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-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 + .0 + .record_event(|| LowerReturnEvent::new(&lower_result))?; + lower_result + } +} + pub struct HostFunc { entrypoint: VMLoweringCallee, typecheck: Box) -> Result<()>) + Send + Sync>, @@ -257,109 +347,147 @@ where let param_tys = InterfaceType::Tuple(ty.params); let result_tys = InterfaceType::Tuple(ty.results); - if async_ { + rr_hooks::record_replay_host_func_entry(storage, &ty, store.0)?; + + let storage_type = if async_ { #[cfg(feature = "component-model-async")] { - let mut storage = unsafe { Storage::<'_, Params, u32>::new_async::(storage) }; + StorageType::Async(unsafe { Storage::<'_, Params, u32>::new_async::(storage) }) + } + #[cfg(not(feature = "component-model-async"))] + unreachable!( + "async-lowered imports should have failed validation \ + when `component-model-async` feature disabled" + ); + } else { + StorageType::Sync(unsafe { Storage::<'_, Params, Return>::new_sync(storage) }) + }; - // Lift the parameters, either from flat storage or from linear - // memory. - let lift = &mut LiftContext::new(store.0.store_opaque_mut(), &options, instance); - lift.enter_call(); - let params = storage.lift_params(lift, param_tys)?; + if !store.0.replay_enabled() { + match storage_type { + #[cfg(feature = "component-model-async")] + StorageType::Async(mut storage) => { + // Lift the parameters, either from flat storage or from linear + // memory. + let lift = &mut LiftContext::new(store.0.store_opaque_mut(), &options, instance); + lift.enter_call(); + let params = storage.lift_params(lift, param_tys)?; + + // Load the return pointer, if present. + let retptr = match storage.async_retptr() { + Some(ptr) => { + let mut lower = + LowerContext::new(store.as_context_mut(), &options, &types, instance); + validate_inbounds::(lower.as_slice(), ptr)? + } + // If there's no return pointer then `Return` should have an + // empty flat representation. In this situation pretend the + // return pointer was 0 so we have something to shepherd along + // into the closure below. + None => { + assert_eq!(Return::flatten_count(), 0); + 0 + } + }; + + let host_result = closure(store.as_context_mut(), instance, params); + + let mut lower_result = { + let types = types.clone(); + move |store: StoreContextMut, instance: Instance, ret: Return| { + unsafe { + flags.set_may_leave(false); + } + let mut lower = LowerContext::new(store, &options, &types, instance); + ret.linear_lower_to_memory(&mut lower, result_tys, retptr)?; + unsafe { + flags.set_may_leave(true); + } + lower.exit_call()?; + Ok(()) + } + }; + let task = match host_result { + HostResult::Done(result) => { + lower_result(store.as_context_mut(), instance, result?)?; + None + } + #[cfg(feature = "component-model-async")] + HostResult::Future(future) => instance.first_poll( + store.as_context_mut(), + future, + caller_instance, + lower_result, + )?, + }; + + let status = if let Some(task) = task { + Status::Started.pack(Some(task)) + } else { + Status::Returned.pack(None) + }; - // Load the return pointer, if present. - let retptr = match storage.async_retptr() { - Some(ptr) => { - let mut lower = - LowerContext::new(store.as_context_mut(), &options, &types, instance); - validate_inbounds::(lower.as_slice_mut(), ptr)? + let mut lower = LowerContext::new(store, &options, &types, instance); + storage.lower_results(&mut lower, InterfaceType::U32, status)?; + } + StorageType::Sync(mut storage) => { + let mut lift = LiftContext::new(store.0.store_opaque_mut(), &options, instance); + lift.enter_call(); + let params = storage.lift_params(&mut lift, param_tys)?; + + let ret = match closure(store.as_context_mut(), instance, params) { + HostResult::Done(result) => result?, + #[cfg(feature = "component-model-async")] + HostResult::Future(future) => { + instance.poll_and_block(store.0.traitobj_mut(), future, caller_instance)? + } + }; + + unsafe { + flags.set_may_leave(false); } - // If there's no return pointer then `Return` should have an - // empty flat representation. In this situation pretend the - // return pointer was 0 so we have something to shepherd along - // into the closure below. - None => { - assert_eq!(Return::flatten_count(), 0); - 0 + let mut lower = LowerContext::new(store, &options, &types, instance); + storage.lower_results(&mut lower, result_tys, ret)?; + unsafe { + flags.set_may_leave(true); } - }; - - let host_result = closure(store.as_context_mut(), instance, params); - - let mut lower_result = { - let types = types.clone(); - move |store: StoreContextMut, instance: Instance, ret: Return| { + lower.exit_call()?; + } + } + } else { + #[cfg(feature = "rr-component")] + { + match storage_type { + #[cfg(feature = "component-model-async")] + StorageType::Async(_) => unreachable!("`rr` should not be configurable with async"), + StorageType::Sync(mut storage) => { unsafe { flags.set_may_leave(false); } let mut lower = LowerContext::new(store, &options, &types, instance); - ret.linear_lower_to_memory(&mut lower, result_tys, retptr)?; + storage.replay_lower_results(&mut lower)?; unsafe { flags.set_may_leave(true); } - lower.exit_call()?; - Ok(()) } - }; - let task = match host_result { - HostResult::Done(result) => { - lower_result(store.as_context_mut(), instance, result?)?; - None - } - #[cfg(feature = "component-model-async")] - HostResult::Future(future) => instance.first_poll( - store.as_context_mut(), - future, - caller_instance, - lower_result, - )?, - }; - - let status = if let Some(task) = task { - Status::Started.pack(Some(task)) - } else { - Status::Returned.pack(None) - }; - - let mut lower = LowerContext::new(store, &options, &types, instance); - storage.lower_results(&mut lower, InterfaceType::U32, status)?; - } - #[cfg(not(feature = "component-model-async"))] - { - let _ = caller_instance; - unreachable!( - "async-lowered imports should have failed validation \ - when `component-model-async` feature disabled" - ); - } - } else { - let mut storage = unsafe { Storage::<'_, Params, Return>::new_sync(storage) }; - let mut lift = LiftContext::new(store.0.store_opaque_mut(), &options, instance); - lift.enter_call(); - let params = storage.lift_params(&mut lift, param_tys)?; - - let ret = match closure(store.as_context_mut(), instance, params) { - HostResult::Done(result) => result?, - #[cfg(feature = "component-model-async")] - HostResult::Future(future) => { - instance.poll_and_block(store.0.traitobj_mut(), future, caller_instance)? } - }; - - unsafe { - flags.set_may_leave(false); } - let mut lower = LowerContext::new(store, &options, &types, instance); - storage.lower_results(&mut lower, result_tys, ret)?; - unsafe { - flags.set_may_leave(true); - } - lower.exit_call()?; } return Ok(()); + /// Sum storage type across async/sync storage formats + enum StorageType< + 'a, + P: ComponentType, + ReturnSync: ComponentType, + #[cfg(feature = "component-model-async")] ReturnAsync: ComponentType, + > { + #[cfg(feature = "component-model-async")] + Async(Storage<'a, P, ReturnAsync>), + Sync(Storage<'a, P, ReturnSync>), + } + /// Type-level representation of the matrix of possibilities of how /// WebAssembly parameters and results are handled in the canonical ABI. /// @@ -578,10 +706,45 @@ where ret: R, ) -> Result<()> { match self.lower_dst() { - Dst::Direct(storage) => ret.linear_lower_to_flat(cx, ty, storage), + Dst::Direct(storage) => { + let result = rr_hooks::record_lower( + |cx, ty| ret.linear_lower_to_flat(cx, ty, storage), + cx, + ty, + ); + rr_hooks::record_host_func_return( + unsafe { storage_as_slice_mut(storage) }, + cx.store.0, + )?; + result + } Dst::Indirect(ptr) => { - let ptr = validate_inbounds::(cx.as_slice_mut(), ptr)?; - ret.linear_lower_to_memory(cx, ty, ptr) + let ptr = validate_inbounds::(cx.as_slice(), ptr)?; + let result = rr_hooks::record_lower_store( + |cx, ty, ptr| ret.linear_lower_to_memory(cx, ty, ptr), + cx, + ty, + ptr, + ); + // Recording here is just for marking the return event + rr_hooks::record_host_func_return(&[], cx.store.0)?; + result + } + } + } + + #[cfg(feature = "rr-component")] + fn replay_lower_results(&mut self, cx: &mut LowerContext<'_, T>) -> Result<()> { + use crate::component::storage::storage_as_slice_mut; + match self.lower_dst() { + Dst::Direct(storage) => { + // This path also stores the final return values in resulting storage + cx.replay_lowering(Some(unsafe { storage_as_slice_mut(storage) })) + } + Dst::Indirect(_ptr) => { + // While replay will not have to change '_ptr' for indirect results, + // it will have to overwrite any nested stored lowerings (deep copy) + cx.replay_lowering(None) } } } @@ -734,129 +897,156 @@ where let param_tys = &types[func_ty.params]; let result_tys = &types[func_ty.results]; - let mut params_and_results = Vec::new(); - let mut lift = &mut LiftContext::new(store.0.store_opaque_mut(), &options, instance); - lift.enter_call(); - let max_flat = if async_ { - MAX_FLAT_ASYNC_PARAMS - } else { - MAX_FLAT_PARAMS - }; + rr_hooks::record_replay_host_func_entry(storage, &types[ty], store.0)?; - let ret_index = unsafe { - dynamic_params_load( - &mut lift, - &types, - storage, - param_tys, - &mut params_and_results, - max_flat, - )? - }; - let result_start = params_and_results.len(); - for _ in 0..result_tys.types.len() { - params_and_results.push(Val::Bool(false)); - } + if !store.0.replay_enabled() { + let mut params_and_results = Vec::new(); + let mut lift = &mut LiftContext::new(store.0.store_opaque_mut(), &options, instance); + lift.enter_call(); + let max_flat = if async_ { + MAX_FLAT_ASYNC_PARAMS + } else { + MAX_FLAT_PARAMS + }; - if async_ { - #[cfg(feature = "component-model-async")] - { - let retptr = if result_tys.types.len() == 0 { - 0 - } else { - let retptr = unsafe { storage[ret_index].assume_init() }; - let mut lower = - LowerContext::new(store.as_context_mut(), &options, &types, instance); - validate_inbounds_dynamic(&result_tys.abi, lower.as_slice_mut(), &retptr)? - }; + let ret_index = unsafe { + dynamic_params_load( + &mut lift, + &types, + storage, + param_tys, + &mut params_and_results, + max_flat, + )? + }; + let result_start = params_and_results.len(); + for _ in 0..result_tys.types.len() { + params_and_results.push(Val::Bool(false)); + } + + if async_ { + #[cfg(feature = "component-model-async")] + { + let retptr = if result_tys.types.len() == 0 { + 0 + } else { + let retptr = unsafe { storage[ret_index].assume_init() }; + let mut lower = + LowerContext::new(store.as_context_mut(), &options, &types, instance); + validate_inbounds_dynamic(&result_tys.abi, lower.as_slice(), &retptr)? + }; + + let future = closure( + store.as_context_mut(), + instance, + params_and_results, + result_start, + ); + + let task = instance.first_poll(store, future, caller_instance, { + let types = types.clone(); + let result_tys = func_ty.results; + move |store: StoreContextMut, instance: Instance, result_vals: Vec| { + let result_tys = &types[result_tys]; + let result_vals = &result_vals[result_start..]; + assert_eq!(result_vals.len(), result_tys.types.len()); + + unsafe { + flags.set_may_leave(false); + } + + let mut lower = LowerContext::new(store, &options, &types, instance); + let mut ptr = retptr; + 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 lower, *ty, offset)?; + } + + unsafe { + flags.set_may_leave(true); + } + + lower.exit_call()?; + + Ok(()) + } + })?; + + let status = if let Some(task) = task { + Status::Started.pack(Some(task)) + } else { + Status::Returned.pack(None) + }; + storage[0] = MaybeUninit::new(ValRaw::i32(status as i32)); + } + #[cfg(not(feature = "component-model-async"))] + { + unreachable!( + "async-lowered imports should have failed validation \ + when `component-model-async` feature disabled" + ); + } + } else { let future = closure( store.as_context_mut(), instance, params_and_results, result_start, ); + let result_vals = + instance.poll_and_block(store.0.traitobj_mut(), future, caller_instance)?; + let result_vals = &result_vals[result_start..]; - let task = instance.first_poll(store, future, caller_instance, { - let types = types.clone(); - let result_tys = func_ty.results; - move |store: StoreContextMut, instance: Instance, result_vals: Vec| { - let result_tys = &types[result_tys]; - let result_vals = &result_vals[result_start..]; - assert_eq!(result_vals.len(), result_tys.types.len()); - - unsafe { - flags.set_may_leave(false); - } - - let mut lower = LowerContext::new(store, &options, &types, instance); - let mut ptr = retptr; - 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 lower, *ty, offset)?; - } - - unsafe { - flags.set_may_leave(true); - } - - lower.exit_call()?; + unsafe { + flags.set_may_leave(false); + } - Ok(()) + 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()) { + rr_hooks::record_lower(|cx, ty| val.lower(cx, ty, &mut dst), &mut cx, *ty)?; } - })?; - - let status = if let Some(task) = task { - Status::Started.pack(Some(task)) + assert!(dst.next().is_none()); + rr_hooks::record_host_func_return(storage, cx.store.0)?; } else { - Status::Returned.pack(None) - }; + let ret_ptr = unsafe { 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); + val.store(&mut cx, *ty, offset)?; + } + } - storage[0] = MaybeUninit::new(ValRaw::i32(status as i32)); - } - #[cfg(not(feature = "component-model-async"))] - { - unreachable!( - "async-lowered imports should have failed validation \ - when `component-model-async` feature disabled" - ); - } - } else { - let future = closure( - store.as_context_mut(), - instance, - params_and_results, - result_start, - ); - let result_vals = - instance.poll_and_block(store.0.traitobj_mut(), future, caller_instance)?; - let result_vals = &result_vals[result_start..]; + unsafe { + flags.set_may_leave(true); + } - unsafe { - flags.set_may_leave(false); + cx.exit_call()?; } - - 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)?; - } - assert!(dst.next().is_none()); + } else { + #[cfg(feature = "rr-component")] + if async_ { + unreachable!("`rr` should not be configurable with async"); } else { - let ret_ptr = unsafe { 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)?; + unsafe { + 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) + // This path also stores the final return values in resulting storage + cx.replay_lowering(Some(storage))?; + } else { + // The indirect `ret_ptr` will not change during replay, but it will + // have to overwrite any nested stored lowerings (deep copy) + cx.replay_lowering(None)?; + } + unsafe { + flags.set_may_leave(true); } } - - unsafe { - flags.set_may_leave(true); - } - - cx.exit_call()?; } Ok(()) diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index eaa6b27cd7..8ccdc4a715 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -1,7 +1,16 @@ +#[cfg(feature = "rr-component")] +use crate::ValRaw; use crate::component::matching::InstanceType; use crate::component::resources::{HostResourceData, HostResourceIndex, HostResourceTables}; use crate::component::{Instance, ResourceType}; use crate::prelude::*; +#[cfg(feature = "rr-component")] +use crate::rr::{ + RREvent, RecordBuffer, Recorder, ReplayError, Replayer, + component_events::MemorySliceWriteEvent, component_events::ReallocEntryEvent, +}; +#[cfg(all(feature = "rr-component", feature = "rr-validate"))] +use crate::rr::{Validate, component_events::ReallocReturnEvent}; use crate::runtime::vm::component::{ CallContexts, ComponentInstance, InstanceFlags, ResourceTable, ResourceTables, }; @@ -9,6 +18,9 @@ use crate::runtime::vm::{VMFuncRef, VMMemoryDefinition}; use crate::store::{StoreId, StoreOpaque}; use crate::{FuncType, StoreContextMut}; use alloc::sync::Arc; +#[cfg(feature = "rr-component")] +use core::mem::MaybeUninit; +use core::ops::{Deref, DerefMut}; use core::pin::Pin; use core::ptr::NonNull; use wasmtime_environ::component::{ @@ -16,6 +28,99 @@ use wasmtime_environ::component::{ 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> { + 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> { + 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) { + #[cfg(feature = "rr-component")] + if let Some(buf) = &mut self.recorder { + buf.record_event(|| MemorySliceWriteEvent::new(self.offset, self.bytes.to_vec())) + .unwrap(); + } + } +} + +/// 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 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), +/// 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> { + 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> { + 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) { + #[cfg(feature = "rr-component")] + if let Some(buf) = &mut self.recorder { + buf.record_event(|| MemorySliceWriteEvent::new(self.offset, self.bytes.to_vec())) + .unwrap(); + } + } +} + /// Runtime representation of canonical ABI options in the component model. /// /// This structure packages up the runtime representation of each option from @@ -163,7 +268,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()); @@ -174,6 +279,22 @@ 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, + ) -> (&'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 { @@ -293,24 +414,44 @@ impl<'a, T: 'static> LowerContext<'a, T> { self.instance.id().get_mut(self.store.0) } - /// Returns a view into memory as a mutable slice of bytes. + /// Returns a view into memory as a mutable slice of bytes + the + /// record buffer to record state. + /// + /// # 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) + } + + /// 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 /// /// 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] { - self.options.memory_mut(self.store.0) + 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. + /// 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. - pub fn realloc( + fn realloc_inner( &mut self, old: usize, old_size: usize, @@ -332,6 +473,32 @@ impl<'a, T: 'static> LowerContext<'a, T> { .map(|(_, ptr)| ptr) } + /// Invokes the memory allocation function (which is style after `realloc`) + /// with the specified parameters. + /// + /// # Panics + /// + /// This will panic if realloc hasn't been configured for this lowering via + /// its canonical options. + pub fn realloc( + &mut self, + old: usize, + old_size: usize, + 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(all(feature = "rr-component", feature = "rr-validate"))] + self.store + .0 + .record_event_validation(|| ReallocReturnEvent::new(&result))?; + result + } + /// Returns a fixed mutable slice of memory `N` bytes large starting at /// offset `N`, panicking on out-of-bounds. /// @@ -342,7 +509,15 @@ 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] { + #[inline] + pub fn get(&mut self, offset: usize) -> ConstMemorySliceCell<'_, N> { + 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 @@ -353,7 +528,37 @@ 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 { + bytes: slice_mut[offset..].first_chunk_mut().unwrap(), + #[cfg(feature = "rr-component")] + offset: offset, + #[cfg(feature = "rr-component")] + recorder: recorder, + } + } + + /// The dynamically-sized 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). + #[inline] + pub fn get_dyn(&mut self, offset: usize, size: usize) -> MemorySliceCell<'_> { + 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 { + bytes: &mut slice_mut[offset..][..size], + #[cfg(feature = "rr-component")] + offset: offset, + #[cfg(feature = "rr-component")] + recorder: recorder, + } } /// Lowers an `own` resource into the guest, converting the `rep` specified @@ -442,6 +647,116 @@ 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`. + /// + /// ## 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 [MaybeUninit]>, + ) -> 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(()); + } + 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(); + // 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()?; + #[cfg(feature = "rr-validate")] + let run_validate = buf.settings().validate && buf.trace_settings().add_validation; + match event { + RREvent::ComponentHostFuncReturn(e) => { + // End of the lowering process + if let Some(e) = lowering_error { + return Err(e.into()); + } + if let Some(storage) = result_storage.as_deref_mut() { + e.move_into_slice(storage); + } + complete = true; + } + RREvent::ComponentReallocEntry(e) => { + let _result = + self.realloc_inner(e.old_addr, e.old_size, e.old_align, e.new_size); + #[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-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-validate")] + 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 + // 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") + } + // Unwrapping should never occur on valid executions since *Entry should be before *Return in trace + RREvent::ComponentReallocReturn(_e) => + { + #[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-validate")] + if run_validate { + _lower_stack.push(()) + } + } + RREvent::ComponentLowerStoreEntry(_) => { + // All we want here is ensuring Entry occurs before Return + #[cfg(feature = "rr-validate")] + if run_validate { + _lower_store_stack.push(()) + } + } + + _ => bail!("Invalid event \'{:?}\' encountered during lowering", event), + }; + } + 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 3e669ab278..ac636cce58 100644 --- a/crates/wasmtime/src/runtime/component/func/typed.rs +++ b/crates/wasmtime/src/runtime/component/func/typed.rs @@ -1085,7 +1085,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 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()); @@ -1185,7 +1185,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(()) } @@ -1207,7 +1207,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 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. @@ -1477,7 +1477,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())) } @@ -1494,13 +1495,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 = &mut cx.as_slice_mut()[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)?; } @@ -1512,7 +1514,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. @@ -1531,8 +1533,9 @@ 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 = &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 @@ -1551,11 +1554,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/component/instance.rs b/crates/wasmtime/src/runtime/component/instance.rs index b60d43eab2..28402a3511 100644 --- a/crates/wasmtime/src/runtime/component/instance.rs +++ b/crates/wasmtime/src/runtime/component/instance.rs @@ -1016,6 +1016,18 @@ impl InstancePre { fn instantiate_impl(&self, mut store: impl AsContextMut) -> Result { let mut store = store.as_context_mut(); + #[cfg(feature = "rr-component")] + { + use crate::rr::{Validate, component_events::InstantiationEvent}; + store + .0 + .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() .allocator() diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 101a94cdef..be8725d6e2 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -1482,6 +1482,69 @@ impl Func { } } +/// Convenience methods to inject record + replay logic +mod rr_hooks { + use super::*; + #[cfg(feature = "rr")] + 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", feature = "rr-validate"))] + { + // Record/replay the raw parameter args + 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(()) + } + + #[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")] + 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(()) + } + + #[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")] + store.next_replay_event_and(|event: HostFuncReturnEvent| { + event.move_into_slice(args); + Ok(()) + })?; + 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 @@ -2364,45 +2427,74 @@ impl HostContext { }; let func = &state.func; - let ret = 'ret: { - if let Err(trap) = caller.store.0.call_hook(CallHook::CallingHost) { - break 'ret R::fallible_from_error(trap); - } + let wasm_func_subtype = { + let type_index = state._ty.index(); + caller.engine().signatures().borrow(type_index).unwrap() + }; + let wasm_func_type = wasm_func_subtype.unwrap_func(); + + // 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( + unsafe { args.as_ref() }, + wasm_func_type, + caller.store.0, + )?; - let mut store = if P::may_gc() { - AutoAssertNoGc::new(caller.store.0) - } else { - unsafe { AutoAssertNoGc::disabled(caller.store.0) } + 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) } + }; + // SAFETY: this function requires `args` to be valid and the + // `WasmTyList` trait means that everything should be correctly + // ascribed/typed, making this valid to load from. + unsafe { 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() }; - // SAFETY: this function requires `args` to be valid and the - // `WasmTyList` trait means that everything should be correctly - // ascribed/typed, making this valid to load from. - let params = unsafe { P::load(&mut store, args.as_mut()) }; - let _ = &mut store; - drop(store); - - 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); + 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) } + }; + // SAFETY: this function requires that `args` is safe for this + // type signature, and the guarantees of `WasmRet` means that + // everything should be typed appropriately. + unsafe { ret.store(&mut store, args.as_mut())? }; } - r.into_fallible() - }; - - if !ret.compatible_with_store(caller.store.0) { - bail!("host function attempted to return cross-`Store` value to Wasm") + // Record the return values + rr_hooks::record_host_func_return( + unsafe { args.as_ref() }, + wasm_func_type, + caller.store.0, + )?; } else { - let mut store = if R::may_gc() { - AutoAssertNoGc::new(caller.store.0) - } else { - unsafe { AutoAssertNoGc::disabled(caller.store.0) } - }; - // SAFETY: this function requires that `args` is safe for this - // type signature, and the guarantees of `WasmRet` means that - // everything should be typed appropriately. - let ret = unsafe { ret.store(&mut store, args.as_mut())? }; - Ok(ret) + // Replay the return values + rr_hooks::replay_host_func_return( + unsafe { args.as_mut() }, + wasm_func_type, + caller.store.0, + )?; } + + 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 fef680a35a..d852912268 100644 --- a/crates/wasmtime/src/runtime/instance.rs +++ b/crates/wasmtime/src/runtime/instance.rs @@ -932,6 +932,20 @@ 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 { + false + } + }) + { + 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_events.rs b/crates/wasmtime/src/runtime/rr/events/component_events.rs new file mode 100644 index 0000000000..e103b11823 --- /dev/null +++ b/crates/wasmtime/src/runtime/rr/events/component_events.rs @@ -0,0 +1,205 @@ +//! Module comprising of component model wasm events + +use super::*; +#[expect(unused_imports, reason = "used for doc-links")] +use crate::component::{Component, ComponentType}; +use wasmtime_environ::component::InterfaceType; +use wasmtime_environ::component::TypeFunc; + +/// 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(), + } + } +} + +/// A call event from a Wasm component into the host +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HostFuncEntryEvent { + /// Raw values passed across the call entry boundary + args: RRFuncArgVals, + + /// 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: TypeFunc, +} +impl HostFuncEntryEvent { + // Record + pub fn new(args: &[MaybeUninit], types: TypeFunc) -> Self { + Self { + args: func_argvals_from_raw_slice(args), + types: types, + } + } +} +#[cfg(feature = "rr-validate")] +impl Validate for HostFuncEntryEvent { + fn validate(&self, expect_types: &TypeFunc) -> Result<(), ReplayError> { + self.log(); + if &self.types == expect_types { + Ok(()) + } else { + Err(ReplayError::FailedValidation) + } + } +} + +/// A return event after a host call for a Wasm component +/// +/// Matches 1:1 with [`HostFuncEntryEvent`] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HostFuncReturnEvent { + /// Lowered values passed across the call return boundary + args: RRFuncArgVals, +} +impl HostFuncReturnEvent { + pub fn new(args: &[MaybeUninit]) -> Self { + Self { + args: func_argvals_from_raw_slice(args), + } + } + + /// Consume the caller event and encode it back into the slice + pub fn move_into_slice(self, args: &mut [MaybeUninit]) { + func_argvals_into_raw_slice(self.args, args); + } +} + +macro_rules! generic_new_result_events { + ( + $( + $(#[doc = $doc:literal])* + $event:ident => ($ok_ty:ty,$err_variant:path) + ),* + ) => ( + $( + $(#[doc = $doc])* + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct $event { + 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())) + } + } + pub fn ret(self) -> Result<$ok_ty, EventActionError> { self.ret } + } + + )* + ); +} + +macro_rules! generic_new_events { + ( + $( + $(#[doc = $doc:literal])* + $struct:ident { + $( + $field:ident : $field_ty:ty + ),* + } + ),* + ) => ( + $( + #[derive(Debug, Clone, Serialize, Deserialize)] + $(#[doc = $doc])* + pub struct $struct { + $( + pub $field: $field_ty, + )* + } + )* + $( + impl $struct { + pub fn new($($field: $field_ty),*) -> Self { + Self { + $($field),* + } + } + } + )* + ); +} + +generic_new_result_events! { + /// Return from a reallocation call (needed only for validation) + ReallocReturnEvent => (usize, EventActionError::ReallocError), + /// Return from a type lowering invocation + LowerReturnEvent => ((), EventActionError::LowerError), + /// Return from store invocations during type lowering + LowerStoreReturnEvent => ((), EventActionError::LowerStoreError) +} + +#[cfg(feature = "rr-validate")] +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)) => { + 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: {}", + e, f + )))), + // Diverging errors.. Report as a failed validation + (Ok(_), Err(_)) => Err(ReplayError::FailedValidation), + (Err(_), Ok(_)) => Err(ReplayError::FailedValidation), + } + } +} + +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 + }, + + /// Entry to a type lowering invocation + LowerEntryEvent { + ty: InterfaceType + }, + + /// Entry to store invocations during type lowering + LowerStoreEntryEvent { + ty: InterfaceType, + offset: usize + }, + + /// 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. + MemorySliceWriteEvent { + offset: usize, + bytes: Vec + } +} diff --git a/crates/wasmtime/src/runtime/rr/events/core_events.rs b/crates/wasmtime/src/runtime/rr/events/core_events.rs new file mode 100644 index 0000000000..885de03fa6 --- /dev/null +++ b/crates/wasmtime/src/runtime/rr/events/core_events.rs @@ -0,0 +1,59 @@ +//! Module comprising of core wasm events +use super::*; +#[expect(unused_imports, reason = "used for doc-links")] +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, Serialize, Deserialize)] +pub struct HostFuncEntryEvent { + /// Raw values passed across the call/return boundary + args: RRFuncArgVals, + /// Param/return types (required to support replay validation) + types: CoreFuncArgTypes, +} +impl HostFuncEntryEvent { + // Record + pub fn new(args: &[MaybeUninit], types: WasmFuncType) -> Self { + Self { + args: func_argvals_from_raw_slice(args), + types: types, + } + } +} +#[cfg(feature = "rr-validate")] +impl Validate for HostFuncEntryEvent { + fn validate(&self, expect_types: &CoreFuncArgTypes) -> Result<(), ReplayError> { + self.log(); + 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, Serialize, Deserialize)] +pub struct HostFuncReturnEvent { + /// Raw values passed across the call/return boundary + args: RRFuncArgVals, +} +impl HostFuncReturnEvent { + // Record + pub fn new(args: &[MaybeUninit]) -> Self { + Self { + args: func_argvals_from_raw_slice(args), + } + } + // 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]) { + func_argvals_into_raw_slice(self.args, args); + } +} 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 0000000000..8b0e7dde4d --- /dev/null +++ b/crates/wasmtime/src/runtime/rr/events/mod.rs @@ -0,0 +1,154 @@ +#[cfg(any(feature = "rr-component", feature = "rr-validate"))] +use super::ReplayError; +use crate::ValRaw; +use crate::prelude::*; +use core::fmt; +use core::mem::{self, MaybeUninit}; +use serde::{Deserialize, Serialize}; + +/// 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 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), + LowerError(String), + LowerStoreError(String), +} + +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) => { + write!(f, "{}", s) + } + } + } +} + +impl core::error::Error for EventActionError {} + +type ValRawBytes = [u8; mem::size_of::()]; + +/// 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 ValRawBytesConvertable for ValRaw { + #[inline] + fn to_valraw_bytes(self) -> ValRawBytes { + self.as_bytes() + } + #[inline] + fn from_valraw_bytes(value: ValRawBytes) -> Self { + ValRaw::from_bytes(value) + } +} + +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() + } + #[inline] + fn from_valraw_bytes(value: ValRawBytes) -> Self { + MaybeUninit::new(ValRaw::from_valraw_bytes(value)) + } +} + +type RRFuncArgVals = Vec; + +/// Construct [`RRFuncArgVals`] from raw value buffer +fn func_argvals_from_raw_slice(args: &[T]) -> RRFuncArgVals +where + T: ValRawBytesConvertable + Copy, +{ + 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 + T: ValRawBytesConvertable, +{ + for (src, dst) in rr_args.into_iter().zip(raw_args.iter_mut()) { + *dst = T::from_valraw_bytes(src); + } +} + +/// Trait signifying types that can be validated on replay +/// +/// 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 (e.g. [`component_events::InstantiationEvent`]). +#[cfg(any(feature = "rr-component", feature = "rr-validate"))] +pub trait Validate { + /// Perform a validation of the event to ensure replay consistency + fn validate(&self, expect: &T) -> Result<(), ReplayError>; + + /// Write a log message + fn log(&self) + where + Self: fmt::Debug, + { + log::debug!("Validating => {:?}", self); + } +} + +#[cfg(any(feature = "rr-component", feature = "rr-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) + } + } +} + +/// 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/io.rs b/crates/wasmtime/src/runtime/rr/io.rs new file mode 100644 index 0000000000..c59e54a64d --- /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 new file mode 100644 index 0000000000..9fec674b48 --- /dev/null +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -0,0 +1,540 @@ +#![cfg(feature = "rr")] +//! Wasmtime's Record and Replay support. +//! +//! This feature is currently not optimized and under development +//! +//! ## Notes +//! +//! This module does NOT support RR for component builtins yet. + +use crate::config::{ModuleVersionStrategy, RecordSettings, ReplaySettings}; +use crate::prelude::*; +use core::fmt; +use events::EventActionError; +use serde::{Deserialize, Serialize}; +// Use component 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 +#[cfg(any(feature = "rr-validate", feature = "rr-component"))] +pub use events::Validate; +use events::component_events as __component_events; +#[cfg(feature = "rr-component")] +pub use events::component_events; +pub use events::{core_events, marker_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; + +/// Macro template for [`RREvent`] and its conversion to/from specific +/// event types +macro_rules! rr_event { + ( + $( + $(#[doc = $doc:literal])* + $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, Serialize, Deserialize)] + pub enum RREvent { + /// Event signalling the end of a trace + Eof, + $( + $(#[doc = $doc])* + $variant($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), + )* + } + } + } + + $( + 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 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 + CoreHostFuncReturn(core_events::HostFuncReturnEvent), + + // REQUIRED events for replay + // + /// Instantiation of a component + ComponentInstantiation(__component_events::InstantiationEvent), + /// Return from host function to component + ComponentHostFuncReturn(__component_events::HostFuncReturnEvent), + /// Component ABI realloc call in linear wasm memory + ComponentReallocEntry(__component_events::ReallocEntryEvent), + /// Return from a type lowering operation + ComponentLowerReturn(__component_events::LowerReturnEvent), + /// Return from a store during a type lowering operation + ComponentLowerStoreReturn(__component_events::LowerStoreReturnEvent), + /// An attempt to obtain a mutable slice into Wasm linear memory + 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), + /// Call into [Lower::lower] for type lowering + ComponentLowerEntry(__component_events::LowerEntryEvent), + /// Call into [Lower::store] during type lowering + 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 { + EmptyBuffer, + FailedValidation, + IncorrectEventVariant, + InvalidOrdering, + EventActionError(EventActionError), +} + +impl fmt::Display for ReplayError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::EmptyBuffer => { + 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") + } + Self::IncorrectEventVariant => { + write!(f, "event method invoked on incorrect variant") + } + Self::EventActionError(e) => { + write!(f, "{:?}", e) + } + Self::InvalidOrdering => { + write!(f, "event occured at an invalid position in the trace") + } + } + } +} + +impl core::error::Error for ReplayError {} + +impl From for ReplayError { + fn from(value: EventActionError) -> Self { + Self::EventActionError(value) + } +} + +/// This trait provides the interface for a FIFO recorder +pub trait Recorder { + /// Construct a recorder with the writer backend + fn new_recorder(writer: Box, settings: RecordSettings) -> Result + where + Self: Sized; + + /// Record the event generated by `f` + /// + /// ## Error + /// + /// Propogates from underlying writer + fn record_event(&mut self, f: F) -> Result<()> + where + T: Into, + F: FnOnce() -> 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 settings associated with the recording process + fn settings(&self) -> &RecordSettings; + + // Provided methods + + /// Record a event only when validation is requested + #[inline] + #[cfg(feature = "rr-validate")] + fn record_event_validation(&mut self, f: F) -> Result<()> + where + T: Into, + F: FnOnce() -> T, + { + let settings = self.settings(); + if settings.add_validation { + self.record_event(f)?; + } + Ok(()) + } +} + +/// 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(reader: Box, settings: ReplaySettings) -> Result + where + Self: Sized; + + /// Get settings associated with the replay process + #[allow( + unused, + reason = "currently used only for validation resulting in \ + many unnecessary feature gates. will expand in the future to more features" + )] + fn settings(&self) -> &ReplaySettings; + + /// Get the settings (embedded within the trace) during recording + #[allow( + unused, + reason = "currently used only for validation resulting in \ + many unnecessary feature gates. will expand in the future to more features" + )] + fn trace_settings(&self) -> &RecordSettings; + + // Provided Methods + + /// Get the next functional replay event (skips past all non-marker events) + /// + /// ## 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 { + log::debug!("Replay Event => {}", e); + } + event + } + + /// Pop the next replay event with an attemped type conversion to expected + /// event type + /// + /// ## Errors + /// + /// See [`next_event_and`](Replayer::next_event_and) + #[inline] + fn next_event_typed(&mut self) -> Result + where + T: TryFrom, + 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 next_event_and(&mut self, f: F) -> Result<(), ReplayError> + where + T: TryFrom, + ReplayError: From<>::Error>, + F: FnOnce(T) -> Result<(), ReplayError>, + { + let call_event = self.next_event_typed()?; + Ok(f(call_event)?) + } + + /// 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] + #[cfg(feature = "rr-validate")] + fn next_event_validation(&mut self, expect: &Y) -> Result<(), ReplayError> + where + T: TryFrom + Validate, + ReplayError: From<>::Error>, + { + if self.trace_settings().add_validation { + let event = self.next_event_typed::()?; + if self.settings().validate { + event.validate(expect) + } else { + Ok(()) + } + } else { + Ok(()) + } + } +} + +/// Buffer to write recording data. +/// +/// This type can be optimized for [`RREvent`] data configurations. +pub struct RecordBuffer { + /// In-memory event buffer to enable windows for coalescing + buf: Vec, + /// Writer to store data into + writer: Box, + /// 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.settings().event_window_size { + self.flush()?; + } + Ok(()) + } +} + +impl Drop for RecordBuffer { + fn drop(&mut self) { + // Insert End of trace delimiter + self.push_event(RREvent::Eof).unwrap(); + self.flush().unwrap(); + } +} + +impl Recorder for RecordBuffer { + 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(&settings, &mut writer)?; + Ok(RecordBuffer { + buf: Vec::new(), + writer: writer, + settings: settings, + }) + } + + #[inline] + fn record_event(&mut self, f: F) -> Result<()> + where + T: Into, + F: FnOnce() -> T, + { + let event = f().into(); + log::debug!("Recording event => {}", &event); + self.push_event(event) + } + + fn flush(&mut self) -> Result<()> { + log::debug!("Flushing record buffer..."); + for e in self.buf.drain(..) { + io::to_record_writer(&e, &mut self.writer)?; + } + return Ok(()); + } + + #[inline] + fn settings(&self) -> &RecordSettings { + &self.settings + } +} + +/// Buffer to read replay data +pub struct ReplayBuffer { + /// Reader to read replay trace from + reader: Box, + /// Settings in replay configuration + 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 { + type Item = RREvent; + + fn next(&mut self) -> Option { + 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 + } +} + +impl Drop for ReplayBuffer { + fn drop(&mut self) { + if let Some(event) = self.next() { + if let RREvent::Eof = event { + } else { + log::warn!( + "Replay buffer is dropped with {} remaining events, and is likely an invalid execution", + self.count() + ); + } + } + } +} + +impl Replayer for ReplayBuffer { + fn new_replayer(mut reader: Box, settings: ReplaySettings) -> Result { + let mut scratch = [0u8; 12]; + // Ensure module versions match + let version = io::from_replay_reader::<&str, _>(&mut reader, &mut scratch)?; + assert_eq!( + version, + ModuleVersionStrategy::WasmtimeVersion.as_str(), + "Wasmtime version mismatch between engine used for record and replay" + ); + + // Read the recording settings + let trace_settings: RecordSettings = io::from_replay_reader(&mut reader, &mut scratch)?; + + if settings.validate && !trace_settings.add_validation { + log::warn!( + "Replay validation will be omitted since the recorded trace has no validation metadata..." + ); + } + + let deser_buffer = vec![0; settings.deser_buffer_size]; + + Ok(ReplayBuffer { + reader, + settings, + trace_settings, + deser_buffer, + }) + } + + #[inline] + fn settings(&self) -> &ReplaySettings { + &self.settings + } + + #[inline] + fn trace_settings(&self) -> &RecordSettings { + &self.trace_settings + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ValRaw; + use core::mem::MaybeUninit; + use std::fs::File; + use std::path::Path; + use tempfile::{NamedTempFile, TempPath}; + + #[test] + #[cfg(all(feature = "rr", feature = "rr-component"))] + fn rr_buffers() -> Result<()> { + let record_settings = RecordSettings::default(); + let tmp = NamedTempFile::new()?; + let tmppath = tmp.path().to_str().expect("Filename should be UTF-8"); + + let values = vec![ValRaw::i32(1), ValRaw::f32(2), ValRaw::i64(3)] + .into_iter() + .map(|x| MaybeUninit::new(x)) + .collect::>(); + + // Record values + let mut recorder = + RecordBuffer::new_recorder(Box::new(File::create(tmppath)?), record_settings)?; + recorder + .record_event(|| __component_events::HostFuncReturnEvent::new(values.as_slice()))?; + recorder.flush()?; + + let tmp = tmp.into_temp_path(); + let tmppath = >::as_ref(&tmp) + .to_str() + .expect("Filename should be UTF-8"); + let replay_settings = ReplaySettings::default(); + + // Assert that replayed values are identical + let mut replayer = + ReplayBuffer::new_replayer(Box::new(File::open(tmppath)?), replay_settings)?; + let mut result_values = values.clone(); + replayer.next_event_and(|event: __component_events::HostFuncReturnEvent| { + event.move_into_slice(result_values.as_mut_slice()); + + // Check replay `values` matches record `values` + for (a, b) in values.iter().zip(result_values.iter()) { + unsafe { + assert!(a.assume_init().as_bytes() == b.assume_init().as_bytes()); + } + } + Ok(()) + })?; + + // 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 33e4b6dae5..18d486951e 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -85,6 +85,10 @@ use crate::component::concurrent; use crate::fiber; 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}; #[cfg(feature = "gc")] use crate::runtime::vm::GcRootsList; #[cfg(feature = "stack-switching")] @@ -409,6 +413,17 @@ 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 + #[cfg(feature = "rr")] + record_buffer: Option, + /// Storage for replaying execution + /// + /// `None` implies replay is disabled for this store + #[cfg(feature = "rr")] + replay_buffer: Option, } /// Executor state within `StoreOpaque`. @@ -601,6 +616,30 @@ impl Store { executor: Executor::new(engine), #[cfg(feature = "component-model-async")] concurrent_async_state: Default::default(), + #[cfg(feature = "rr")] + record_buffer: engine.rr().and_then(|v| { + v.record().and_then(|record| { + Some( + RecordBuffer::new_recorder( + (record.writer_initializer)(), + record.settings.clone(), + ) + .unwrap(), + ) + }) + }), + #[cfg(feature = "rr")] + replay_buffer: engine.rr().and_then(|v| { + v.replay().and_then(|replay| { + Some( + ReplayBuffer::new_replayer( + (replay.reader_initializer)(), + replay.settings.clone(), + ) + .unwrap(), + ) + }) + }), }; let mut inner = Box::new(StoreInner { inner, @@ -1419,6 +1458,106 @@ impl StoreOpaque { &mut self.vm_store_context } + #[cfg(feature = "rr")] + #[inline(always)] + pub fn record_buffer_mut(&mut self) -> Option<&mut RecordBuffer> { + self.record_buffer.as_mut() + } + + #[cfg(feature = "rr")] + #[inline(always)] + pub fn replay_buffer_mut(&mut self) -> Option<&mut ReplayBuffer> { + self.replay_buffer.as_mut() + } + + /// Record the given event into the store's record buffer + /// + /// Convenience wrapper around [`Recorder::record_event`] + #[cfg(feature = "rr")] + #[inline(always)] + pub(crate) fn record_event(&mut self, f: F) -> Result<()> + where + T: Into, + F: FnOnce() -> T, + { + if let Some(buf) = self.record_buffer_mut() { + buf.record_event(f) + } else { + Ok(()) + } + } + + /// Conditionally record the given event into the store's record buffer + /// if validation is enabled for recording + /// + /// Convenience wrapper around [`Recorder::record_event_validation`] + #[cfg(feature = "rr-validate")] + #[inline(always)] + pub(crate) fn record_event_validation(&mut self, f: F) -> Result<()> + where + T: Into, + F: FnOnce() -> T, + { + if let Some(buf) = self.record_buffer_mut() { + buf.record_event_validation(f) + } else { + Ok(()) + } + } + + /// 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) -> Result<(), ReplayError>, + { + if let Some(buf) = self.replay_buffer_mut() { + buf.next_event_and(f) + } else { + Ok(()) + } + } + + /// 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_validation`] + #[cfg(feature = "rr-validate")] + #[inline] + pub(crate) fn next_replay_event_validation( + &mut self, + expect: &Y, + ) -> Result<(), ReplayError> + where + T: TryFrom + Validate, + ReplayError: From<>::Error>, + { + if let Some(buf) = self.replay_buffer_mut() { + buf.next_event_validation::(expect) + } else { + Ok(()) + } + } + + /// Check if replay is enabled for the Store + /// + /// Note: Defaults to false when `rr` feature is disabled + #[inline(always)] + pub fn replay_enabled(&self) -> bool { + cfg_if::cfg_if! { + if #[cfg(feature = "rr")] { + self.replay_buffer.is_some() + } else { + false + } + } + } + #[inline(never)] pub(crate) fn allocate_gc_heap(&mut self) -> Result<()> { log::trace!("allocating GC heap for store {:?}", self.id()); diff --git a/crates/wasmtime/src/runtime/vm/component/libcalls.rs b/crates/wasmtime/src/runtime/vm/component/libcalls.rs index be65a8820a..21e9df7020 100644 --- a/crates/wasmtime/src/runtime/vm/component/libcalls.rs +++ b/crates/wasmtime/src/runtime/vm/component/libcalls.rs @@ -576,12 +576,31 @@ 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] +fn rr_hook(store: &mut dyn VMStore, libcall: &str) -> Result<()> { + #[cfg(feature = "rr-component")] + { + if (*store).replay_enabled() { + bail!("Replay support for libcall {libcall:?} not yet supported!"); + } else { + use crate::rr::marker_events::CustomMessageEvent; + (*store).record_event(|| CustomMessageEvent::from(libcall))?; + } + } + let _ = (store, libcall); + Ok(()) +} + fn resource_new32( store: &mut dyn VMStore, instance: Instance, resource: u32, rep: u32, ) -> Result { + rr_hook(store, "resource_new32")?; let resource = TypeResourceTableIndex::from_u32(resource); instance.resource_new32(store, resource, rep) } @@ -592,6 +611,7 @@ fn resource_rep32( resource: u32, idx: u32, ) -> Result { + rr_hook(store, "resource_rep32")?; let resource = TypeResourceTableIndex::from_u32(resource); instance.resource_rep32(store, resource, idx) } @@ -602,6 +622,7 @@ fn resource_drop( resource: u32, idx: u32, ) -> Result { + rr_hook(store, "resource_drop")?; let resource = TypeResourceTableIndex::from_u32(resource); Ok(ResourceDropRet( instance.resource_drop(store, resource, idx)?, @@ -628,6 +649,7 @@ fn resource_transfer_own( src_table: u32, dst_table: u32, ) -> Result { + rr_hook(store, "resource_transfer_own")?; let src_table = TypeResourceTableIndex::from_u32(src_table); let dst_table = TypeResourceTableIndex::from_u32(dst_table); instance.resource_transfer_own(store, src_idx, src_table, dst_table) @@ -640,16 +662,19 @@ fn resource_transfer_borrow( src_table: u32, dst_table: u32, ) -> Result { + rr_hook(store, "resource_transfer_borrow")?; let src_table = TypeResourceTableIndex::from_u32(src_table); let dst_table = TypeResourceTableIndex::from_u32(dst_table); instance.resource_transfer_borrow(store, src_idx, src_table, dst_table) } fn resource_enter_call(store: &mut dyn VMStore, instance: Instance) { + rr_hook(store, "resource_enter_call").unwrap(); instance.resource_enter_call(store) } fn resource_exit_call(store: &mut dyn VMStore, instance: Instance) -> Result<()> { + rr_hook(store, "resource_exit_call")?; instance.resource_exit_call(store) } diff --git a/crates/wasmtime/src/runtime/vm/vmcontext.rs b/crates/wasmtime/src/runtime/vm/vmcontext.rs index ff2ba0141a..59d95f7dfa 100644 --- a/crates/wasmtime/src/runtime/vm/vmcontext.rs +++ b/crates/wasmtime/src/runtime/vm/vmcontext.rs @@ -1605,6 +1605,18 @@ impl ValRaw { assert!(cfg!(feature = "gc") || exnref == 0); exnref } + + /// 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 diff --git a/src/bin/wasmtime.rs b/src/bin/wasmtime.rs index ecafff3aa5..54e84969b9 100644 --- a/src/bin/wasmtime.rs +++ b/src/bin/wasmtime.rs @@ -89,6 +89,19 @@ enum Subcommand { /// Inspect `*.cwasm` files output from Wasmtime #[cfg(feature = "objdump")] Objdump(wasmtime_cli::commands::ObjdumpCommand), + + /// 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 `--trace` (to specify the recorded traces) and + /// corresponding settings (e.g. `--validate`) + /// + /// 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), } impl Wasmtime { @@ -101,7 +114,10 @@ impl Wasmtime { match subcommand { #[cfg(feature = "run")] - Subcommand::Run(c) => c.execute(), + Subcommand::Run(c) => c.execute( + #[cfg(feature = "rr")] + None, + ), #[cfg(feature = "cache")] Subcommand::Config(c) => c.execute(), @@ -126,6 +142,9 @@ 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 04fd0286ba..eda254fb97 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -39,3 +39,8 @@ pub use self::settings::*; 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 new file mode 100644 index 0000000000..9bd9aeb83f --- /dev/null +++ b/src/commands/replay.rs @@ -0,0 +1,66 @@ +//! 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, ReplaySettings}; + +#[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, + + /// 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 +#[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<()> { + #[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( + fs::File::open(&self.replay_opts.trace).unwrap(), + )) + }), + settings: ReplaySettings { + validate: self.replay_opts.validate, + deser_buffer_size: self.replay_opts.deser_buffer_size, + ..Default::default() + }, + }; + // 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 17f6fbbef0..0188e19936 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -13,6 +13,8 @@ use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::thread; use wasi_common::sync::{Dir, TcpListener, WasiCtxBuilder, ambient_authority}; +#[cfg(feature = "rr")] +use wasmtime::ReplayConfig; use wasmtime::{Engine, Func, Module, Store, StoreLimits, Val, ValType}; use wasmtime_wasi::{WasiCtxView, WasiView}; @@ -85,7 +87,10 @@ enum CliLinker { impl RunCommand { /// Executes the command. - pub fn execute(mut self) -> 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)?; @@ -105,6 +110,11 @@ impl RunCommand { None => {} } + #[cfg(feature = "rr")] + 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 808d221e841a21ecc52b6c19ac371b063fb2f294 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 12 Aug 2025 14:03:36 -0400 Subject: [PATCH 04/73] Add a sink support in path for recording benchmarking --- crates/cli-flags/src/lib.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs index 49a220c15d..42c1072876 100644 --- a/crates/cli-flags/src/lib.rs +++ b/crates/cli-flags/src/lib.rs @@ -488,7 +488,7 @@ 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 + /// Filesystem endpoint to store the recorded execution trace (empty string "" for void endpoint) pub path: Option, /// Include (optional) signatures to facilitate validation checks during replay /// (see `wasmtime replay` for details). @@ -1022,7 +1022,7 @@ impl CommonOptions { match_feature! { ["rr" : record.path.clone()] path => { - use std::{io::BufWriter, sync::Arc}; + use std::{io, sync::Arc}; use wasmtime::{RecordConfig, RecordSettings}; let default_settings = RecordSettings::default(); match_feature! { @@ -1032,7 +1032,11 @@ impl CommonOptions { } config.enable_record(RecordConfig { writer_initializer: Arc::new(move || { - Box::new(BufWriter::new(fs::File::create(&path).unwrap())) + if path.trim().is_empty() { + Box::new(io::sink()) + } else { + Box::new(io::BufWriter::new(fs::File::create(&path).unwrap())) + } }), settings: RecordSettings { add_validation: record From d53de4d6e63e2a795539a7bc22cf5eb80b999aae Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Mon, 18 Aug 2025 10:04:02 -0400 Subject: [PATCH 05/73] Add builtin record/replay events --- crates/environ/src/component.rs | 46 ++++ .../src/runtime/component/func/host.rs | 8 +- .../src/runtime/component/func/options.rs | 25 +- .../src/runtime/rr/events/component_events.rs | 215 ++++++++++++------ crates/wasmtime/src/runtime/rr/events/mod.rs | 13 +- crates/wasmtime/src/runtime/rr/mod.rs | 20 +- crates/wasmtime/src/runtime/vm/component.rs | 2 +- .../src/runtime/vm/component/libcalls.rs | 50 +++- crates/wasmtime/src/runtime/vm/interpreter.rs | 1 + 9 files changed, 279 insertions(+), 101 deletions(-) diff --git a/crates/environ/src/component.rs b/crates/environ/src/component.rs index 10589c2eff..256f010e7e 100644 --- a/crates/environ/src/component.rs +++ b/crates/environ/src/component.rs @@ -83,18 +83,64 @@ pub use self::types_builder::*; macro_rules! foreach_builtin_component_function { ($mac:ident) => { $mac! { + #[rr_builtin( + entry = ResourceNew32EntryEvent, + exit = ResourceNew32ReturnEvent, + variant = ResourceNew32, + success_ty = u32 + )] resource_new32(vmctx: vmctx, resource: u32, rep: u32) -> u64; + + #[rr_builtin( + entry = ResourceRep32EntryEvent, + exit = ResourceRep32ReturnEvent, + variant = ResourceRep32, + success_ty = u32 + )] resource_rep32(vmctx: vmctx, resource: u32, idx: u32) -> u64; // Returns an `Option` where `None` is "no destructor needed" // and `Some(val)` is "run the destructor on this rep". The option // is encoded as a 64-bit integer where the low bit is Some/None // and bits 1-33 are the payload. + #[rr_builtin( + entry = ResourceDropEntryEvent, + exit = ResourceDropReturnEvent, + variant = ResourceDrop, + success_ty = ResourceDropRet + )] resource_drop(vmctx: vmctx, resource: u32, idx: u32) -> u64; + #[rr_builtin( + entry = ResourceTransferOwnEntryEvent, + exit = ResourceTransferOwnReturnEvent, + variant = ResourceTransferOwn, + success_ty = u32 + )] resource_transfer_own(vmctx: vmctx, src_idx: u32, src_table: u32, dst_table: u32) -> u64; + + #[rr_builtin( + entry = ResourceTransferBorrowEntryEvent, + exit = ResourceTransferBorrowReturnEvent, + variant = ResourceTransferBorrow, + success_ty = u32 + )] resource_transfer_borrow(vmctx: vmctx, src_idx: u32, src_table: u32, dst_table: u32) -> u64; + + //#[rr_builtin( + // entry = ResourceEnterCallEntryEvent, + // exit = ResourceEnterCallReturnEvent, + // variant = ResourceEnterCall, + // success_ty = () + //)] resource_enter_call(vmctx: vmctx); + + #[rr_builtin( + entry = ResourceExitCallEntryEvent, + exit = ResourceExitCallReturnEvent, + variant = ResourceExitCall, + success_ty = () + )] resource_exit_call(vmctx: vmctx) -> bool; #[cfg(feature = "component-model-async")] diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index d2d2cb7266..76f83e533b 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -75,13 +75,13 @@ mod rr_hooks { use crate::rr::component_events::LowerStoreEntryEvent; cx.store .0 - .record_event_validation(|| LowerStoreEntryEvent::new(ty, offset))?; + .record_event_validation(|| LowerStoreEntryEvent { 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::from_anyhow_result(&store_result))?; store_result } @@ -100,13 +100,13 @@ mod rr_hooks { use crate::rr::component_events::LowerEntryEvent; cx.store .0 - .record_event_validation(|| LowerEntryEvent::new(ty))?; + .record_event_validation(|| LowerEntryEvent { ty })?; } let lower_result = lower(cx, ty); #[cfg(feature = "rr-component")] cx.store .0 - .record_event(|| LowerReturnEvent::new(&lower_result))?; + .record_event(|| LowerReturnEvent::from_anyhow_result(&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 8ccdc4a715..d458c4fad2 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -58,8 +58,11 @@ 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())) - .unwrap(); + buf.record_event(|| MemorySliceWriteEvent { + offset: self.offset, + bytes: self.bytes.to_vec(), + }) + .unwrap(); } } } @@ -115,8 +118,11 @@ 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(|| MemorySliceWriteEvent::new(self.offset, self.bytes.to_vec())) - .unwrap(); + buf.record_event(|| MemorySliceWriteEvent { + offset: self.offset, + bytes: self.bytes.to_vec(), + }) + .unwrap(); } } } @@ -488,14 +494,17 @@ impl<'a, T: 'static> LowerContext<'a, T> { new_size: usize, ) -> Result { #[cfg(feature = "rr-component")] - self.store - .0 - .record_event(|| ReallocEntryEvent::new(old, old_size, old_align, new_size))?; + self.store.0.record_event(|| ReallocEntryEvent { + old_addr: old, + old_size, + old_align, + new_size, + })?; let result = self.realloc_inner(old, old_size, old_align, new_size); #[cfg(all(feature = "rr-component", feature = "rr-validate"))] self.store .0 - .record_event_validation(|| ReallocReturnEvent::new(&result))?; + .record_event_validation(|| ReallocReturnEvent::from_anyhow_result(&result))?; result } diff --git a/crates/wasmtime/src/runtime/rr/events/component_events.rs b/crates/wasmtime/src/runtime/rr/events/component_events.rs index e103b11823..80614afba0 100644 --- a/crates/wasmtime/src/runtime/rr/events/component_events.rs +++ b/crates/wasmtime/src/runtime/rr/events/component_events.rs @@ -1,10 +1,9 @@ //! Module comprising of component model wasm events use super::*; -#[expect(unused_imports, reason = "used for doc-links")] -use crate::component::{Component, ComponentType}; -use wasmtime_environ::component::InterfaceType; -use wasmtime_environ::component::TypeFunc; +use crate::component::Component; +use crate::vm::component::libcalls::ResourceDropRet; +use wasmtime_environ::{self, component::InterfaceType, component::TypeFunc}; /// A [`Component`] instantiatation event #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -76,81 +75,189 @@ impl HostFuncReturnEvent { } } +/// A reallocation call event in the Component Model canonical ABI +/// +/// Usually performed during lowering of complex [`ComponentType`]s to Wasm +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ReallocEntryEvent { + pub old_addr: usize, + pub old_size: usize, + pub old_align: u32, + pub new_size: usize, +} + +/// Entry to a type lowering invocation +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LowerEntryEvent { + pub ty: InterfaceType, +} + +/// Entry to store invocations during type lowering +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LowerStoreEntryEvent { + pub ty: InterfaceType, + pub offset: usize, +} + +/// 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. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MemorySliceWriteEvent { + pub offset: usize, + pub bytes: Vec, +} + macro_rules! generic_new_result_events { ( $( $(#[doc = $doc:literal])* - $event:ident => ($ok_ty:ty,$err_variant:path) + $event:ident -> ($ok_ty:ty,$err_variant:path) ),* ) => ( $( $(#[doc = $doc])* #[derive(Debug, Clone, Serialize, Deserialize)] - pub struct $event { - ret: Result<$ok_ty, EventActionError>, - } + pub struct $event(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())) - } + pub fn from_anyhow_result(ret: &Result<$ok_ty>) -> Self { + Self(ret.as_ref().map(|t| *t).map_err(|e| $err_variant(e.to_string()))) } - pub fn ret(self) -> Result<$ok_ty, EventActionError> { self.ret } + pub fn ret(self) -> Result<$ok_ty, EventActionError> { self.0 } } )* ); } -macro_rules! generic_new_events { +// Macro to generate RR events from the builtin descriptions +macro_rules! builtin_events { + // Main rule matching component function definitions ( $( - $(#[doc = $doc:literal])* - $struct:ident { - $( - $field:ident : $field_ty:ty - ),* - } - ),* + $( #[cfg($attr:meta)] )? + $( #[rr_builtin(entry = $rr_entry:ident, exit = $rr_return:ident, variant = $rr_var:ident $(, success_ty = $rr_succ:tt)?)] )? + $name:ident( vmctx: vmctx $(, $pname:ident: $param:ident )* ) $( -> $result:ident )?; + )* ) => ( + builtin_events!(@gen_return_enum $($($rr_var $rr_return)?)*); + builtin_events!(@gen_entry_enum $($($rr_var $rr_entry)?)*); + // Prioitize ret_succ if provided $( - #[derive(Debug, Clone, Serialize, Deserialize)] - $(#[doc = $doc])* - pub struct $struct { - $( - pub $field: $field_ty, - )* - } + builtin_events!(@gen_events + $($rr_entry $rr_return)? + $($pname, $param)* + -> $($($rr_succ)?)? $($result)? + ); )* + ); + + // All things related to BuiltinReturnEvent enum + (@gen_return_enum $($rr_var:ident $event:ident)*) => { + #[derive(Debug, Clone, Serialize, Deserialize)] + pub enum BuiltinReturnEvent { + $($rr_var($event),)* + } + builtin_events!(@from_impls BuiltinReturnEvent $($rr_var $event)*); + }; + + // All things related to BuiltinEntryEvent enum + (@gen_entry_enum $($rr_var:ident $event:ident)*) => { + // PartialEq gives all these events `Validate` + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub enum BuiltinEntryEvent { + $($rr_var($event),)* + } + builtin_events!(@from_impls BuiltinEntryEvent $($rr_var $event)*); + }; + + + // Generate entry/exit events if rr_builtin provided + (@gen_events $rr_entry:ident $rr_return:ident $($pname:ident, $param:ident)* -> $($result_opts:tt)*) => { + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct $rr_entry { + $(pub $pname: $param),* + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct $rr_return(Result); + + impl $rr_return { + pub fn from_anyhow_result(ret: &Result) -> Self { + Self( + ret.as_ref() + .map(|t| t.clone()) + .map_err(|e| EventActionError::BuiltinError(e.to_string())), + ) + } + pub fn ret(self) -> Result { + self.0.map_err(|e| e.into()) + } + } + + }; + + + // Conversion to/from specific return `$event` and `BuiltinEntryEvent` + (@from_impls $enum:ident $($rr_var:ident $event:ident)*) => { $( - impl $struct { - pub fn new($($field: $field_ty),*) -> Self { - Self { - $($field),* + impl From<$event> for $enum { + fn from(value: $event) -> Self { + Self::$rr_var(value) + } + } + + impl TryFrom<$enum> for $event { + type Error = ReplayError; + + fn try_from(value: $enum) -> Result { + #[allow(irrefutable_let_patterns)] + if let $enum::$rr_var(x) = value { + Ok(x) + } else { + Err(ReplayError::IncorrectEventVariant) } } } )* - ); + }; + + // Return first value if it exists + (@ret_first $first:tt $($rest:tt)*) => ($first); + (@ret_first ) => (); + + + // Stubbed if `rr_builtin` not provided + (@gen_events $($pname:ident, $param:ident)* -> $($result_opts:ident)*) => {}; } +// Return events with anyhow error conversion to EventActionError generic_new_result_events! { /// Return from a reallocation call (needed only for validation) - ReallocReturnEvent => (usize, EventActionError::ReallocError), + ReallocReturnEvent -> (usize, EventActionError::ReallocError), /// Return from a type lowering invocation - LowerReturnEvent => ((), EventActionError::LowerError), + LowerReturnEvent -> ((), EventActionError::LowerError), /// Return from store invocations during type lowering - LowerStoreReturnEvent => ((), EventActionError::LowerStoreError) + LowerStoreReturnEvent -> ((), EventActionError::LowerStoreError) } +// Entry/return events for each builtin function +wasmtime_environ::foreach_builtin_component_function!(builtin_events); + +// === Special Validation === +// `realloc` needs to actually check for divergence +// between recorded and replayed realloc effects #[cfg(feature = "rr-validate")] 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()) { + match (self.0.as_ref(), expect_ret.as_ref()) { (Ok(r), Ok(s)) => { if r == s { Ok(()) @@ -169,37 +276,3 @@ impl Validate> for ReallocReturnEvent { } } } - -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 - }, - - /// Entry to a type lowering invocation - LowerEntryEvent { - ty: InterfaceType - }, - - /// Entry to store invocations during type lowering - LowerStoreEntryEvent { - ty: InterfaceType, - offset: usize - }, - - /// 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. - MemorySliceWriteEvent { - offset: usize, - bytes: Vec - } -} diff --git a/crates/wasmtime/src/runtime/rr/events/mod.rs b/crates/wasmtime/src/runtime/rr/events/mod.rs index 8b0e7dde4d..8ef2823b21 100644 --- a/crates/wasmtime/src/runtime/rr/events/mod.rs +++ b/crates/wasmtime/src/runtime/rr/events/mod.rs @@ -21,12 +21,16 @@ pub enum EventActionError { ReallocError(String), LowerError(String), LowerStoreError(String), + BuiltinError(String), } 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) => { + Self::ReallocError(s) + | Self::LowerError(s) + | Self::LowerStoreError(s) + | Self::BuiltinError(s) => { write!(f, "{}", s) } } @@ -143,8 +147,11 @@ pub mod marker_events { /// 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 { + impl From for CustomMessageEvent + where + T: Into, + { + fn from(v: T) -> Self { Self(v.into()) } } diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index 9fec674b48..b2a4d1d9af 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -110,6 +110,8 @@ rr_event! { ComponentLowerStoreReturn(__component_events::LowerStoreReturnEvent), /// An attempt to obtain a mutable slice into Wasm linear memory ComponentMemorySliceWrite(__component_events::MemorySliceWriteEvent), + /// Return from a component builtin + ComponentBuiltinReturn(__component_events::BuiltinReturnEvent), // OPTIONAL events for replay validation // @@ -122,7 +124,9 @@ rr_event! { /// Call into [Lower::lower] for type lowering ComponentLowerEntry(__component_events::LowerEntryEvent), /// Call into [Lower::store] during type lowering - ComponentLowerStoreEntry(__component_events::LowerStoreEntryEvent) + ComponentLowerStoreEntry(__component_events::LowerStoreEntryEvent), + /// Call into a component builtin + ComponentBuiltinEntry(__component_events::BuiltinEntryEvent) } impl RREvent { @@ -234,7 +238,7 @@ pub trait Replayer: Iterator { #[allow( unused, reason = "currently used only for validation resulting in \ - many unnecessary feature gates. will expand in the future to more features" + many unnecessary feature gates. will expand in the future to more features and this attribute can be removed" )] fn settings(&self) -> &ReplaySettings; @@ -242,7 +246,7 @@ pub trait Replayer: Iterator { #[allow( unused, reason = "currently used only for validation resulting in \ - many unnecessary feature gates. will expand in the future to more features" + many unnecessary feature gates. will expand in the future to more features and this attribute can be removed" )] fn trace_settings(&self) -> &RecordSettings; @@ -394,8 +398,18 @@ pub struct ReplayBuffer { /// Reader to read replay trace from reader: Box, /// Settings in replay configuration + #[allow( + unused, + reason = "currently used only for validation resulting in \ + many unnecessary feature gates. will expand in the future to more features and this attribute can be removed" + )] settings: ReplaySettings, /// Settings for record configuration (encoded in the trace) + #[allow( + unused, + reason = "currently used only for validation resulting in \ + many unnecessary feature gates. will expand in the future to more features and this attribute can be removed" + )] trace_settings: RecordSettings, /// Intermediate static buffer for deserialization deser_buffer: Vec, diff --git a/crates/wasmtime/src/runtime/vm/component.rs b/crates/wasmtime/src/runtime/vm/component.rs index b0d2e163aa..be4f3da546 100644 --- a/crates/wasmtime/src/runtime/vm/component.rs +++ b/crates/wasmtime/src/runtime/vm/component.rs @@ -31,7 +31,7 @@ use wasmtime_environ::{HostPtr, PrimaryMap, VMSharedTypeIndex}; )] const INVALID_PTR: usize = 0xdead_dead_beef_beef_u64 as usize; -mod libcalls; +pub(crate) mod libcalls; mod resources; #[cfg(feature = "component-model-async")] diff --git a/crates/wasmtime/src/runtime/vm/component/libcalls.rs b/crates/wasmtime/src/runtime/vm/component/libcalls.rs index 21e9df7020..f783639016 100644 --- a/crates/wasmtime/src/runtime/vm/component/libcalls.rs +++ b/crates/wasmtime/src/runtime/vm/component/libcalls.rs @@ -9,6 +9,7 @@ use crate::runtime::vm::{HostResultHasUnwindSentinel, VMStore, VmSafe}; use core::cell::Cell; use core::ptr::NonNull; use core::slice; +use serde::{Deserialize, Serialize}; use wasmtime_environ::component::*; const UTF16_TAG: usize = 1 << 31; @@ -81,12 +82,15 @@ wasmtime_environ::foreach_builtin_component_function!(define_builtins); /// implementation following this submodule. mod trampolines { use super::{ComponentInstance, VMComponentContext}; + #[cfg(feature = "rr-component")] + use crate::rr::{Replayer, component_events::*}; use core::ptr::NonNull; macro_rules! shims { ( $( $( #[cfg($attr:meta)] )? + $( #[rr_builtin(entry = $rr_entry:ident, exit = $rr_return:ident, variant = $rr_var:ident $(, success_ty = $rr_succ:tt)? )] )? $name:ident( vmctx: vmctx $(, $pname:ident: $param:ident )* ) $( -> $result:ident )?; )* ) => ( @@ -101,7 +105,7 @@ mod trampolines { let ret = crate::runtime::vm::traphandlers::catch_unwind_and_record_trap(|| unsafe { ComponentInstance::from_vmctx(vmctx, |store, instance| { - shims!(@invoke $name(store, instance,) $($pname)*) + shims!(@invoke $([$rr_entry, $rr_return])? $name(store, instance,) $($pname)*) }) }); shims!(@convert_ret ret $($pname: $param)*) @@ -152,6 +156,33 @@ mod trampolines { (@invoke $m:ident ($($args:tt)*) $param:ident $($rest:tt)*) => ( shims!(@invoke $m ($($args)* $param,) $($rest)*) ); + + // main invoke rule with a record/replay hook wrapper around the above invoke rules + // when `rr_builtin`` is provided + (@invoke [$rr_entry:ident, $rr_exit:ident] $name:ident($store:ident, $instance:ident,) $($pname:ident)*) => ({ + #[cfg(not(feature = "rr-component"))] + { + shims!(@invoke $name($store, $instance,) $($pname)*) + } + #[cfg(feature = "rr-component")] + { + if let Some(buf) = (*$store).replay_buffer_mut() { + #[cfg(feature = "rr-validate")] + buf.next_event_validation::(&$rr_entry{ $($pname),* }.into())?; + // Replay the return value + let builtin_ret_event = buf.next_event_typed::()?; + $rr_exit::try_from(builtin_ret_event)?.ret() + } else { + // Recording entry/return + #[cfg(feature = "rr-validate")] + (*$store).record_event_validation::(|| $rr_entry{ $($pname),* }.into())?; + let retval = shims!(@invoke $name($store, $instance,) $($pname)*); + (*$store).record_event::(|| $rr_exit::from_anyhow_result(&retval).into())?; + retval + } + } + }); + } wasmtime_environ::foreach_builtin_component_function!(shims); @@ -580,12 +611,14 @@ fn inflate_latin1_bytes(dst: &mut [u16], latin1_bytes_so_far: usize) -> &mut [u1 /// /// TODO: Implement libcall hooks #[inline] -fn rr_hook(store: &mut dyn VMStore, libcall: &str) -> Result<()> { +fn rr_unsupported_hook(store: &mut dyn VMStore, libcall: &str) -> Result<()> { #[cfg(feature = "rr-component")] { if (*store).replay_enabled() { bail!("Replay support for libcall {libcall:?} not yet supported!"); - } else { + } + #[cfg(feature = "rr-validate")] + { use crate::rr::marker_events::CustomMessageEvent; (*store).record_event(|| CustomMessageEvent::from(libcall))?; } @@ -600,7 +633,6 @@ fn resource_new32( resource: u32, rep: u32, ) -> Result { - rr_hook(store, "resource_new32")?; let resource = TypeResourceTableIndex::from_u32(resource); instance.resource_new32(store, resource, rep) } @@ -611,7 +643,6 @@ fn resource_rep32( resource: u32, idx: u32, ) -> Result { - rr_hook(store, "resource_rep32")?; let resource = TypeResourceTableIndex::from_u32(resource); instance.resource_rep32(store, resource, idx) } @@ -622,14 +653,14 @@ fn resource_drop( resource: u32, idx: u32, ) -> Result { - rr_hook(store, "resource_drop")?; let resource = TypeResourceTableIndex::from_u32(resource); Ok(ResourceDropRet( instance.resource_drop(store, resource, idx)?, )) } -struct ResourceDropRet(Option); +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ResourceDropRet(Option); unsafe impl HostResultHasUnwindSentinel for ResourceDropRet { type Abi = u64; @@ -649,7 +680,6 @@ fn resource_transfer_own( src_table: u32, dst_table: u32, ) -> Result { - rr_hook(store, "resource_transfer_own")?; let src_table = TypeResourceTableIndex::from_u32(src_table); let dst_table = TypeResourceTableIndex::from_u32(dst_table); instance.resource_transfer_own(store, src_idx, src_table, dst_table) @@ -662,19 +692,17 @@ fn resource_transfer_borrow( src_table: u32, dst_table: u32, ) -> Result { - rr_hook(store, "resource_transfer_borrow")?; let src_table = TypeResourceTableIndex::from_u32(src_table); let dst_table = TypeResourceTableIndex::from_u32(dst_table); instance.resource_transfer_borrow(store, src_idx, src_table, dst_table) } fn resource_enter_call(store: &mut dyn VMStore, instance: Instance) { - rr_hook(store, "resource_enter_call").unwrap(); + rr_unsupported_hook(store, "resource_enter_call").unwrap(); instance.resource_enter_call(store) } fn resource_exit_call(store: &mut dyn VMStore, instance: Instance) -> Result<()> { - rr_hook(store, "resource_exit_call")?; instance.resource_exit_call(store) } diff --git a/crates/wasmtime/src/runtime/vm/interpreter.rs b/crates/wasmtime/src/runtime/vm/interpreter.rs index 3e8e3023fc..da2dba3e8d 100644 --- a/crates/wasmtime/src/runtime/vm/interpreter.rs +++ b/crates/wasmtime/src/runtime/vm/interpreter.rs @@ -430,6 +430,7 @@ impl InterpreterRef<'_> { ( $( $( #[cfg($attr:meta)] )? + $( #[rr_builtin($($rr:tt)*)] )? $name:ident($($pname:ident: $param:ident ),* ) $(-> $result:ident)?; )* ) => { From 1e9f7253d7d07008cea128158b021ab8219806cc Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Mon, 18 Aug 2025 12:22:24 -0400 Subject: [PATCH 06/73] Add support for no-return builtins --- crates/environ/src/component.rs | 22 +++++++------- crates/misc/component-async-tests/src/lib.rs | 2 +- .../src/runtime/rr/events/component_events.rs | 29 ++++++++----------- .../src/runtime/vm/component/libcalls.rs | 27 +++++++++++++++-- examples/wasip2/main.rs | 2 +- 5 files changed, 49 insertions(+), 33 deletions(-) diff --git a/crates/environ/src/component.rs b/crates/environ/src/component.rs index 256f010e7e..721b345dd1 100644 --- a/crates/environ/src/component.rs +++ b/crates/environ/src/component.rs @@ -84,17 +84,17 @@ macro_rules! foreach_builtin_component_function { ($mac:ident) => { $mac! { #[rr_builtin( + variant = ResourceNew32, entry = ResourceNew32EntryEvent, exit = ResourceNew32ReturnEvent, - variant = ResourceNew32, success_ty = u32 )] resource_new32(vmctx: vmctx, resource: u32, rep: u32) -> u64; #[rr_builtin( + variant = ResourceRep32, entry = ResourceRep32EntryEvent, exit = ResourceRep32ReturnEvent, - variant = ResourceRep32, success_ty = u32 )] resource_rep32(vmctx: vmctx, resource: u32, idx: u32) -> u64; @@ -104,41 +104,39 @@ macro_rules! foreach_builtin_component_function { // is encoded as a 64-bit integer where the low bit is Some/None // and bits 1-33 are the payload. #[rr_builtin( + variant = ResourceDrop, entry = ResourceDropEntryEvent, exit = ResourceDropReturnEvent, - variant = ResourceDrop, success_ty = ResourceDropRet )] resource_drop(vmctx: vmctx, resource: u32, idx: u32) -> u64; #[rr_builtin( + variant = ResourceTransferOwn, entry = ResourceTransferOwnEntryEvent, exit = ResourceTransferOwnReturnEvent, - variant = ResourceTransferOwn, success_ty = u32 )] resource_transfer_own(vmctx: vmctx, src_idx: u32, src_table: u32, dst_table: u32) -> u64; #[rr_builtin( + variant = ResourceTransferBorrow, entry = ResourceTransferBorrowEntryEvent, exit = ResourceTransferBorrowReturnEvent, - variant = ResourceTransferBorrow, success_ty = u32 )] resource_transfer_borrow(vmctx: vmctx, src_idx: u32, src_table: u32, dst_table: u32) -> u64; - //#[rr_builtin( - // entry = ResourceEnterCallEntryEvent, - // exit = ResourceEnterCallReturnEvent, - // variant = ResourceEnterCall, - // success_ty = () - //)] + #[rr_builtin( + variant = ResourceEnterCall, + entry = ResourceEnterCallEntryEvent + )] resource_enter_call(vmctx: vmctx); #[rr_builtin( + variant = ResourceExitCall, entry = ResourceExitCallEntryEvent, exit = ResourceExitCallReturnEvent, - variant = ResourceExitCall, success_ty = () )] resource_exit_call(vmctx: vmctx) -> bool; diff --git a/crates/misc/component-async-tests/src/lib.rs b/crates/misc/component-async-tests/src/lib.rs index 689b01f12a..24925d9153 100644 --- a/crates/misc/component-async-tests/src/lib.rs +++ b/crates/misc/component-async-tests/src/lib.rs @@ -26,7 +26,7 @@ pub struct Ctx { } impl WasiView for Ctx { - fn ctx(&mut self) -> WasiCtxView { + fn ctx(&mut self) -> WasiCtxView<'_> { WasiCtxView { ctx: &mut self.wasi, table: &mut self.table, diff --git a/crates/wasmtime/src/runtime/rr/events/component_events.rs b/crates/wasmtime/src/runtime/rr/events/component_events.rs index 80614afba0..cb313f30c2 100644 --- a/crates/wasmtime/src/runtime/rr/events/component_events.rs +++ b/crates/wasmtime/src/runtime/rr/events/component_events.rs @@ -140,19 +140,16 @@ macro_rules! builtin_events { ( $( $( #[cfg($attr:meta)] )? - $( #[rr_builtin(entry = $rr_entry:ident, exit = $rr_return:ident, variant = $rr_var:ident $(, success_ty = $rr_succ:tt)?)] )? + $( #[rr_builtin(variant = $rr_var:ident, entry = $rr_entry:ident $(, exit = $rr_return:ident)? $(, success_ty = $rr_succ:tt)?)] )? $name:ident( vmctx: vmctx $(, $pname:ident: $param:ident )* ) $( -> $result:ident )?; )* ) => ( - builtin_events!(@gen_return_enum $($($rr_var $rr_return)?)*); + builtin_events!(@gen_return_enum $($($($rr_var $rr_return)?)?)*); builtin_events!(@gen_entry_enum $($($rr_var $rr_entry)?)*); // Prioitize ret_succ if provided $( - builtin_events!(@gen_events - $($rr_entry $rr_return)? - $($pname, $param)* - -> $($($rr_succ)?)? $($result)? - ); + builtin_events!(@gen_entry_events $($rr_entry)? $($pname, $param)*); + builtin_events!(@gen_return_events $($($rr_return)?)? -> $($($rr_succ)?)? $($result)?); )* ); @@ -176,13 +173,16 @@ macro_rules! builtin_events { }; - // Generate entry/exit events if rr_builtin provided - (@gen_events $rr_entry:ident $rr_return:ident $($pname:ident, $param:ident)* -> $($result_opts:tt)*) => { + (@gen_entry_events $rr_entry:ident $($pname:ident, $param:ident)*) => { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct $rr_entry { $(pub $pname: $param),* } + }; + // Stubbed if `rr_builtin` not provided + (@gen_entry_events $($pname:ident, $param:ident)*) => {}; + (@gen_return_events $rr_return:ident -> $($result_opts:tt)*) => { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct $rr_return(Result); @@ -198,9 +198,9 @@ macro_rules! builtin_events { self.0.map_err(|e| e.into()) } } - }; - + // Stubbed if `rr_builtin` not provided + (@gen_return_events -> $($result_opts:tt)*) => {}; // Conversion to/from specific return `$event` and `BuiltinEntryEvent` (@from_impls $enum:ident $($rr_var:ident $event:ident)*) => { @@ -226,13 +226,8 @@ macro_rules! builtin_events { )* }; - // Return first value if it exists + // Return first value (@ret_first $first:tt $($rest:tt)*) => ($first); - (@ret_first ) => (); - - - // Stubbed if `rr_builtin` not provided - (@gen_events $($pname:ident, $param:ident)* -> $($result_opts:ident)*) => {}; } // Return events with anyhow error conversion to EventActionError diff --git a/crates/wasmtime/src/runtime/vm/component/libcalls.rs b/crates/wasmtime/src/runtime/vm/component/libcalls.rs index f783639016..9339a2a2c6 100644 --- a/crates/wasmtime/src/runtime/vm/component/libcalls.rs +++ b/crates/wasmtime/src/runtime/vm/component/libcalls.rs @@ -90,7 +90,7 @@ mod trampolines { ( $( $( #[cfg($attr:meta)] )? - $( #[rr_builtin(entry = $rr_entry:ident, exit = $rr_return:ident, variant = $rr_var:ident $(, success_ty = $rr_succ:tt)? )] )? + $( #[rr_builtin( variant = $rr_var:ident, entry = $rr_entry:ident $(, exit = $rr_return:ident)? $(, success_ty = $rr_succ:tt)? )] )? $name:ident( vmctx: vmctx $(, $pname:ident: $param:ident )* ) $( -> $result:ident )?; )* ) => ( @@ -105,7 +105,7 @@ mod trampolines { let ret = crate::runtime::vm::traphandlers::catch_unwind_and_record_trap(|| unsafe { ComponentInstance::from_vmctx(vmctx, |store, instance| { - shims!(@invoke $([$rr_entry, $rr_return])? $name(store, instance,) $($pname)*) + shims!(@invoke $([$rr_entry $(, $rr_return)?])? $name(store, instance,) $($pname)*) }) }); shims!(@convert_ret ret $($pname: $param)*) @@ -183,6 +183,29 @@ mod trampolines { } }); + // same as above rule for builtins *without* a return value + (@invoke [$rr_entry:ident] $name:ident($store:ident, $instance:ident,) $($pname:ident)*) => ({ + #[cfg(not(feature = "rr-component"))] + { + shims!(@invoke $name($store, $instance,) $($pname)*) + } + #[cfg(feature = "rr-component")] + { + if let Some(_buf) = (*$store).replay_buffer_mut() { + // Just perform replay validation, if required + #[cfg(feature = "rr-validate")] + _buf.next_event_validation::(&$rr_entry{ $($pname),* }.into()).unwrap(); + } else { + // Record entry only; return is not present + #[cfg(feature = "rr-validate")] + (*$store).record_event_validation::(|| $rr_entry{ $($pname),* }.into()).unwrap(); + shims!(@invoke $name($store, $instance,) $($pname)*) + } + } + }); + + + } wasmtime_environ::foreach_builtin_component_function!(shims); diff --git a/examples/wasip2/main.rs b/examples/wasip2/main.rs index d395f31511..06739b0103 100644 --- a/examples/wasip2/main.rs +++ b/examples/wasip2/main.rs @@ -21,7 +21,7 @@ pub struct ComponentRunStates { } impl WasiView for ComponentRunStates { - fn ctx(&mut self) -> WasiCtxView { + fn ctx(&mut self) -> WasiCtxView<'_> { WasiCtxView { ctx: &mut self.wasi_ctx, table: &mut self.resource_table, From 78a95534c914ea4734eef8ac85fd2c01c344d90f Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 2 Sep 2025 18:01:02 -0400 Subject: [PATCH 07/73] [FIX] Missing lower_store record for call host dynamic + Decouple rr hooks into a separate module --- crates/wasmtime/src/runtime.rs | 1 + .../src/runtime/component/func/host.rs | 120 +++--------------- crates/wasmtime/src/runtime/component/mod.rs | 2 +- crates/wasmtime/src/runtime/func.rs | 70 +--------- .../src/runtime/rr_hooks/component.rs | 87 +++++++++++++ crates/wasmtime/src/runtime/rr_hooks/core.rs | 62 +++++++++ crates/wasmtime/src/runtime/rr_hooks/mod.rs | 6 + 7 files changed, 182 insertions(+), 166 deletions(-) create mode 100644 crates/wasmtime/src/runtime/rr_hooks/component.rs create mode 100644 crates/wasmtime/src/runtime/rr_hooks/core.rs create mode 100644 crates/wasmtime/src/runtime/rr_hooks/mod.rs diff --git a/crates/wasmtime/src/runtime.rs b/crates/wasmtime/src/runtime.rs index e715f91acd..b7ef589ec0 100644 --- a/crates/wasmtime/src/runtime.rs +++ b/crates/wasmtime/src/runtime.rs @@ -49,6 +49,7 @@ pub(crate) mod memory; pub(crate) mod module; pub(crate) mod resources; pub(crate) mod rr; +pub(crate) mod rr_hooks; pub(crate) mod store; pub(crate) mod trampoline; pub(crate) mod trap; diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 76f83e533b..286c277a4a 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -5,11 +5,11 @@ use crate::component::matching::InstanceType; use crate::component::storage::{slice_to_storage_mut, storage_as_slice_mut}; use crate::component::{ComponentNamedList, ComponentType, Instance, Lift, Lower, Val}; use crate::prelude::*; +use crate::rr_hooks; use crate::runtime::vm::component::{ ComponentInstance, VMComponentContext, VMLowering, VMLoweringCallee, }; use crate::runtime::vm::{SendSyncPtr, VMOpaqueContext, VMStore}; -use crate::store::StoreOpaque; use crate::{AsContextMut, CallHook, StoreContextMut, ValRaw}; use alloc::sync::Arc; use core::any::Any; @@ -17,100 +17,11 @@ use core::future::Future; use core::mem::{self, MaybeUninit}; use core::pin::Pin; use core::ptr::NonNull; -use wasmtime_environ::component::TypeFunc; use wasmtime_environ::component::{ CanonicalAbiInfo, ComponentTypes, InterfaceType, MAX_FLAT_ASYNC_PARAMS, MAX_FLAT_PARAMS, MAX_FLAT_RESULTS, OptionsIndex, TypeFuncIndex, TypeTuple, }; -/// Convenience methods to inject record + replay logic -mod rr_hooks { - use super::*; - #[cfg(feature = "rr-component")] - use crate::rr::component_events::{ - HostFuncReturnEvent, LowerReturnEvent, LowerStoreReturnEvent, - }; - /// Record/replay hook operation for host function entry events - #[inline] - pub fn record_replay_host_func_entry( - args: &mut [MaybeUninit], - func_type: &TypeFunc, - store: &mut StoreOpaque, - ) -> Result<()> { - #[cfg(all(feature = "rr-component", feature = "rr-validate"))] - { - use crate::rr::component_events::HostFuncEntryEvent; - store.record_event_validation(|| HostFuncEntryEvent::new(args, func_type.clone()))?; - store.next_replay_event_validation::(func_type)?; - } - let _ = (args, func_type, store); - Ok(()) - } - - /// Record hook operation for host function return events - #[inline] - pub fn record_host_func_return( - args: &[MaybeUninit], - store: &mut StoreOpaque, - ) -> Result<()> { - #[cfg(feature = "rr-component")] - store.record_event(|| HostFuncReturnEvent::new(args))?; - let _ = (args, store); - Ok(()) - } - - /// 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-validate"))] - { - use crate::rr::component_events::LowerStoreEntryEvent; - cx.store - .0 - .record_event_validation(|| LowerStoreEntryEvent { ty, offset })?; - } - let store_result = lower_store(cx, ty, offset); - #[cfg(feature = "rr-component")] - cx.store - .0 - .record_event(|| LowerStoreReturnEvent::from_anyhow_result(&store_result))?; - store_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-validate"))] - { - use crate::rr::component_events::LowerEntryEvent; - cx.store - .0 - .record_event_validation(|| LowerEntryEvent { ty })?; - } - let lower_result = lower(cx, ty); - #[cfg(feature = "rr-component")] - cx.store - .0 - .record_event(|| LowerReturnEvent::from_anyhow_result(&lower_result))?; - lower_result - } -} - pub struct HostFunc { entrypoint: VMLoweringCallee, typecheck: Box) -> Result<()>) + Send + Sync>, @@ -347,7 +258,7 @@ where let param_tys = InterfaceType::Tuple(ty.params); let result_tys = InterfaceType::Tuple(ty.results); - rr_hooks::record_replay_host_func_entry(storage, &ty, store.0)?; + rr_hooks::component::record_replay_host_func_entry(storage, &ty, store.0)?; let storage_type = if async_ { #[cfg(feature = "component-model-async")] @@ -707,12 +618,12 @@ where ) -> Result<()> { match self.lower_dst() { Dst::Direct(storage) => { - let result = rr_hooks::record_lower( + let result = rr_hooks::component::record_lower( |cx, ty| ret.linear_lower_to_flat(cx, ty, storage), cx, ty, ); - rr_hooks::record_host_func_return( + rr_hooks::component::record_host_func_return( unsafe { storage_as_slice_mut(storage) }, cx.store.0, )?; @@ -720,14 +631,14 @@ where } Dst::Indirect(ptr) => { let ptr = validate_inbounds::(cx.as_slice(), ptr)?; - let result = rr_hooks::record_lower_store( + let result = rr_hooks::component::record_lower_store( |cx, ty, ptr| ret.linear_lower_to_memory(cx, ty, ptr), cx, ty, ptr, ); // Recording here is just for marking the return event - rr_hooks::record_host_func_return(&[], cx.store.0)?; + rr_hooks::component::record_host_func_return(&[], cx.store.0)?; result } } @@ -897,7 +808,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[ty], store.0)?; + rr_hooks::component::record_replay_host_func_entry(storage, &types[ty], store.0)?; if !store.0.replay_enabled() { let mut params_and_results = Vec::new(); @@ -1006,16 +917,27 @@ 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()) { - rr_hooks::record_lower(|cx, ty| val.lower(cx, ty, &mut dst), &mut cx, *ty)?; + rr_hooks::component::record_lower( + |cx, ty| val.lower(cx, ty, &mut dst), + &mut cx, + *ty, + )?; } assert!(dst.next().is_none()); - rr_hooks::record_host_func_return(storage, cx.store.0)?; + rr_hooks::component::record_host_func_return(storage, cx.store.0)?; } else { let ret_ptr = unsafe { 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); - val.store(&mut cx, *ty, offset)?; + rr_hooks::component::record_lower_store( + |cx, ty, ptr| val.store(cx, ty, ptr), + &mut cx, + *ty, + offset, + )?; + // Recording here is just for marking the return event + rr_hooks::component::record_host_func_return(&[], cx.store.0)?; } } diff --git a/crates/wasmtime/src/runtime/component/mod.rs b/crates/wasmtime/src/runtime/component/mod.rs index 9219fd8513..437ba2c24d 100644 --- a/crates/wasmtime/src/runtime/component/mod.rs +++ b/crates/wasmtime/src/runtime/component/mod.rs @@ -105,7 +105,7 @@ mod component; #[cfg(feature = "component-model-async")] pub(crate) mod concurrent; -mod func; +pub(crate) mod func; mod has_data; mod instance; mod linker; diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index be8725d6e2..aec5802a99 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_hooks; use crate::runtime::Uninhabited; use crate::runtime::vm::{ InterpreterRef, SendSyncPtr, StoreBox, VMArrayCallHostFuncContext, VMCommonStackInformation, @@ -1482,69 +1483,6 @@ impl Func { } } -/// Convenience methods to inject record + replay logic -mod rr_hooks { - use super::*; - #[cfg(feature = "rr")] - 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", feature = "rr-validate"))] - { - // Record/replay the raw parameter args - 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(()) - } - - #[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")] - 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(()) - } - - #[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")] - store.next_replay_event_and(|event: HostFuncReturnEvent| { - event.move_into_slice(args); - Ok(()) - })?; - 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 @@ -2435,7 +2373,7 @@ impl HostContext { // 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( + rr_hooks::core::record_replay_host_func_entry( unsafe { args.as_ref() }, wasm_func_type, caller.store.0, @@ -2480,14 +2418,14 @@ impl HostContext { unsafe { ret.store(&mut store, args.as_mut())? }; } // Record the return values - rr_hooks::record_host_func_return( + rr_hooks::core::record_host_func_return( unsafe { args.as_ref() }, wasm_func_type, caller.store.0, )?; } else { // Replay the return values - rr_hooks::replay_host_func_return( + rr_hooks::core::replay_host_func_return( unsafe { args.as_mut() }, wasm_func_type, caller.store.0, diff --git a/crates/wasmtime/src/runtime/rr_hooks/component.rs b/crates/wasmtime/src/runtime/rr_hooks/component.rs new file mode 100644 index 0000000000..bf5c15d5e4 --- /dev/null +++ b/crates/wasmtime/src/runtime/rr_hooks/component.rs @@ -0,0 +1,87 @@ +use crate::ValRaw; +#[cfg(feature = "component-model")] +use crate::component::func::LowerContext; +use crate::prelude::*; +use crate::store::StoreOpaque; +use core::mem::MaybeUninit; +#[cfg(feature = "component-model")] +use wasmtime_environ::component::{InterfaceType, TypeFunc}; + +#[cfg(feature = "rr-component")] +use crate::rr::component_events::{HostFuncReturnEvent, LowerReturnEvent, LowerStoreReturnEvent}; + +/// Record/replay hook operation for host function entry events +#[inline] +pub fn record_replay_host_func_entry( + args: &mut [MaybeUninit], + func_type: &TypeFunc, + store: &mut StoreOpaque, +) -> Result<()> { + #[cfg(all(feature = "rr-component", feature = "rr-validate"))] + { + use crate::rr::component_events::HostFuncEntryEvent; + store.record_event_validation(|| HostFuncEntryEvent::new(args, func_type.clone()))?; + store.next_replay_event_validation::(func_type)?; + } + let _ = (args, func_type, store); + Ok(()) +} + +/// Record hook operation for host function return events +#[inline] +pub fn record_host_func_return( + args: &[MaybeUninit], + store: &mut StoreOpaque, +) -> Result<()> { + #[cfg(feature = "rr-component")] + store.record_event(|| HostFuncReturnEvent::new(args))?; + let _ = (args, store); + Ok(()) +} + +/// 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-validate"))] + { + use crate::rr::component_events::LowerStoreEntryEvent; + cx.store + .0 + .record_event_validation(|| LowerStoreEntryEvent { ty, offset })?; + } + let store_result = lower_store(cx, ty, offset); + #[cfg(feature = "rr-component")] + cx.store + .0 + .record_event(|| LowerStoreReturnEvent::from_anyhow_result(&store_result))?; + store_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-validate"))] + { + use crate::rr::component_events::LowerEntryEvent; + cx.store + .0 + .record_event_validation(|| LowerEntryEvent { ty })?; + } + let lower_result = lower(cx, ty); + #[cfg(feature = "rr-component")] + cx.store + .0 + .record_event(|| LowerReturnEvent::from_anyhow_result(&lower_result))?; + lower_result +} diff --git a/crates/wasmtime/src/runtime/rr_hooks/core.rs b/crates/wasmtime/src/runtime/rr_hooks/core.rs new file mode 100644 index 0000000000..53cfa945dc --- /dev/null +++ b/crates/wasmtime/src/runtime/rr_hooks/core.rs @@ -0,0 +1,62 @@ +use crate::ValRaw; +use crate::prelude::*; +#[cfg(feature = "rr")] +use crate::rr::core_events::HostFuncReturnEvent; +use crate::store::StoreOpaque; +use core::mem::MaybeUninit; +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", feature = "rr-validate"))] + { + // Record/replay the raw parameter args + 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(()) +} + +#[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")] + 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(()) +} + +#[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")] + 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_hooks/mod.rs b/crates/wasmtime/src/runtime/rr_hooks/mod.rs new file mode 100644 index 0000000000..f5ef54e1ed --- /dev/null +++ b/crates/wasmtime/src/runtime/rr_hooks/mod.rs @@ -0,0 +1,6 @@ +//! Convenience methods for hooking in RR event recording/replaying to the rest of the engine + +/// Component RR hooks +pub mod component; +/// Core RR hooks +pub mod core; From 5d4940e6b55f2ed89e2e578b007fc4ea30ecde10 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Wed, 3 Sep 2025 17:08:55 -0400 Subject: [PATCH 08/73] Factor out common events between core and component wasm --- .../src/runtime/component/func/options.rs | 2 +- .../src/runtime/rr/events/common_events.rs | 55 +++++++++++++++++++ .../src/runtime/rr/events/component_events.rs | 23 +------- .../src/runtime/rr/events/core_events.rs | 25 +-------- crates/wasmtime/src/runtime/rr/events/mod.rs | 1 + crates/wasmtime/src/runtime/rr/mod.rs | 13 +++-- 6 files changed, 68 insertions(+), 51 deletions(-) create mode 100644 crates/wasmtime/src/runtime/rr/events/common_events.rs diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index d458c4fad2..674d086010 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -689,7 +689,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { #[cfg(feature = "rr-validate")] let run_validate = buf.settings().validate && buf.trace_settings().add_validation; match event { - RREvent::ComponentHostFuncReturn(e) => { + RREvent::HostFuncReturn(e) => { // End of the lowering process if let Some(e) = lowering_error { return Err(e.into()); diff --git a/crates/wasmtime/src/runtime/rr/events/common_events.rs b/crates/wasmtime/src/runtime/rr/events/common_events.rs new file mode 100644 index 0000000000..b2a540ccf9 --- /dev/null +++ b/crates/wasmtime/src/runtime/rr/events/common_events.rs @@ -0,0 +1,55 @@ +//! Module comprising of event descriptions common to both core wasm and components +//! +//! When using these events, prefer using the re-exported links in [`component_events`] +//! or [`core_events`] + +use super::*; +use serde::{Deserialize, Serialize}; + +/// A return event after a host call for a core OR component Wasm +/// +/// Matches with either [`component_events::HostFuncEntryEvent`] or +/// [`core_events::HostFuncEntryEvent`] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HostFuncReturnEvent { + /// Raw values passed across the call/return boundary + args: RRFuncArgVals, +} +impl HostFuncReturnEvent { + // Record + pub fn new(args: &[MaybeUninit]) -> Self { + Self { + args: func_argvals_from_raw_slice(args), + } + } + // 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]) { + func_argvals_into_raw_slice(self.args, args); + } +} + +//type WasmFuncArgVals = Vec; +///// A call event from Host into a Wasm component function +//#[derive(Debug, Clone, Serialize, Deserialize)] +//pub struct WasmFuncEntryEvent { +// /// Wasm component values passed as parameters to the function +// args: WasmFuncArgVals, +//} +// +///// A return event from a Wasm component function to Host +///// +///// Matches 1:1 with [`WasmFuncEntryEvent`]. +///// +///// Note: Could potential merge with [`HostFuncReturnEvent`]? +//#[derive(Debug, Clone, Serialize, Deserialize)] +//pub struct WasmFuncReturnEvent { +// /// Lowered values passed across the call return boundary +// args: RRFuncArgVals, +//} +//impl WasmFuncReturnEvent { +// pub fn new(args: &[Val]) { +// Self { args } +// } +//} diff --git a/crates/wasmtime/src/runtime/rr/events/component_events.rs b/crates/wasmtime/src/runtime/rr/events/component_events.rs index cb313f30c2..7891bf800f 100644 --- a/crates/wasmtime/src/runtime/rr/events/component_events.rs +++ b/crates/wasmtime/src/runtime/rr/events/component_events.rs @@ -3,6 +3,8 @@ use super::*; use crate::component::Component; use crate::vm::component::libcalls::ResourceDropRet; +// Re-export common events from this module +pub use common_events::*; use wasmtime_environ::{self, component::InterfaceType, component::TypeFunc}; /// A [`Component`] instantiatation event @@ -54,27 +56,6 @@ impl Validate for HostFuncEntryEvent { } } -/// A return event after a host call for a Wasm component -/// -/// Matches 1:1 with [`HostFuncEntryEvent`] -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct HostFuncReturnEvent { - /// Lowered values passed across the call return boundary - args: RRFuncArgVals, -} -impl HostFuncReturnEvent { - pub fn new(args: &[MaybeUninit]) -> Self { - Self { - args: func_argvals_from_raw_slice(args), - } - } - - /// Consume the caller event and encode it back into the slice - pub fn move_into_slice(self, args: &mut [MaybeUninit]) { - func_argvals_into_raw_slice(self.args, args); - } -} - /// A reallocation call event in the Component Model canonical ABI /// /// Usually performed during lowering of complex [`ComponentType`]s to Wasm diff --git a/crates/wasmtime/src/runtime/rr/events/core_events.rs b/crates/wasmtime/src/runtime/rr/events/core_events.rs index 885de03fa6..465ed12225 100644 --- a/crates/wasmtime/src/runtime/rr/events/core_events.rs +++ b/crates/wasmtime/src/runtime/rr/events/core_events.rs @@ -2,6 +2,8 @@ use super::*; #[expect(unused_imports, reason = "used for doc-links")] use wasmtime_environ::{WasmFuncType, WasmValType}; +// Re-export common events from this module +pub use common_events::*; /// Note: Switch [`CoreFuncArgTypes`] to use [`Vec`] for better efficiency type CoreFuncArgTypes = WasmFuncType; @@ -34,26 +36,3 @@ impl Validate for HostFuncEntryEvent { } } } - -/// A return event after a host call for a Core Wasm -/// -/// Matches 1:1 with [`HostFuncEntryEvent`] -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct HostFuncReturnEvent { - /// Raw values passed across the call/return boundary - args: RRFuncArgVals, -} -impl HostFuncReturnEvent { - // Record - pub fn new(args: &[MaybeUninit]) -> Self { - Self { - args: func_argvals_from_raw_slice(args), - } - } - // 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]) { - func_argvals_into_raw_slice(self.args, args); - } -} diff --git a/crates/wasmtime/src/runtime/rr/events/mod.rs b/crates/wasmtime/src/runtime/rr/events/mod.rs index 8ef2823b21..3062d3b499 100644 --- a/crates/wasmtime/src/runtime/rr/events/mod.rs +++ b/crates/wasmtime/src/runtime/rr/events/mod.rs @@ -157,5 +157,6 @@ pub mod marker_events { } } +pub mod common_events; 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 b2a4d1d9af..4e76fa4fbf 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -17,9 +17,9 @@ use serde::{Deserialize, Serialize}; // it for other modules only when enabled #[cfg(any(feature = "rr-validate", feature = "rr-component"))] pub use events::Validate; -use events::component_events as __component_events; #[cfg(feature = "rr-component")] pub use events::component_events; +use events::{common_events, component_events as __component_events}; pub use events::{core_events, marker_events}; pub use io::{RecordWriter, ReplayReader}; @@ -91,17 +91,17 @@ rr_event! { /// A custom message CustomMessage(marker_events::CustomMessageEvent), + // Common events for both core or component wasm + /// Return from host function to either Core Wasm or component + HostFuncReturn(common_events::HostFuncReturnEvent), + /// Call into host function from Core Wasm CoreHostFuncEntry(core_events::HostFuncEntryEvent), - /// Return from host function to Core Wasm - CoreHostFuncReturn(core_events::HostFuncReturnEvent), // REQUIRED events for replay - // + /// Instantiation of a component ComponentInstantiation(__component_events::InstantiationEvent), - /// Return from host function to component - ComponentHostFuncReturn(__component_events::HostFuncReturnEvent), /// Component ABI realloc call in linear wasm memory ComponentReallocEntry(__component_events::ReallocEntryEvent), /// Return from a type lowering operation @@ -117,6 +117,7 @@ rr_event! { // // 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 From 0f4d0b430d339fa45e3533b58f6f81eb724793ac Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 23 Sep 2025 17:20:58 -0400 Subject: [PATCH 09/73] Switch validation of host functions to use type index --- .../src/runtime/component/func/host.rs | 10 ++++--- crates/wasmtime/src/runtime/func.rs | 23 +++++++++------- .../src/runtime/rr/events/component_events.rs | 16 +++++------ .../src/runtime/rr/events/core_events.rs | 14 ++++------ .../src/runtime/rr_hooks/component.rs | 10 +++---- crates/wasmtime/src/runtime/rr_hooks/core.rs | 27 +++++++------------ 6 files changed, 48 insertions(+), 52 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 286c277a4a..82fbaf9c21 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -254,12 +254,13 @@ where } let types = vminstance.component().types().clone(); + + rr_hooks::component::record_replay_host_func_entry(storage, &ty, store.0)?; + let ty = &types[ty]; let param_tys = InterfaceType::Tuple(ty.params); let result_tys = InterfaceType::Tuple(ty.results); - rr_hooks::component::record_replay_host_func_entry(storage, &ty, store.0)?; - let storage_type = if async_ { #[cfg(feature = "component-model-async")] { @@ -804,12 +805,13 @@ where } let types = instance.id().get(store.0).component().types().clone(); + + rr_hooks::component::record_replay_host_func_entry(storage, &ty, store.0)?; + let func_ty = &types[ty]; let param_tys = &types[func_ty.params]; let result_tys = &types[func_ty.results]; - rr_hooks::component::record_replay_host_func_entry(storage, &types[ty], store.0)?; - if !store.0.replay_enabled() { let mut params_and_results = Vec::new(); let mut lift = &mut LiftContext::new(store.0.store_opaque_mut(), &options, instance); diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index aec5802a99..1957960d58 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -2365,17 +2365,22 @@ impl HostContext { }; let func = &state.func; - let wasm_func_subtype = { + let func_type_index = state._ty.index(); + let (num_params, num_results) = { let type_index = state._ty.index(); - caller.engine().signatures().borrow(type_index).unwrap() + let wasm_func_subtype = caller.engine().signatures().borrow(type_index).unwrap(); + let wasm_func_type = wasm_func_subtype.unwrap_func(); + ( + wasm_func_type.params().len(), + wasm_func_type.returns().len(), + ) }; - let wasm_func_type = wasm_func_subtype.unwrap_func(); // 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::core::record_replay_host_func_entry( - unsafe { args.as_ref() }, - wasm_func_type, + unsafe { &args.as_ref()[..num_params] }, + &func_type_index, caller.store.0, )?; @@ -2419,15 +2424,15 @@ impl HostContext { } // Record the return values rr_hooks::core::record_host_func_return( - unsafe { args.as_ref() }, - wasm_func_type, + unsafe { &args.as_ref()[..num_results] }, + &func_type_index, caller.store.0, )?; } else { // Replay the return values rr_hooks::core::replay_host_func_return( - unsafe { args.as_mut() }, - wasm_func_type, + unsafe { &mut args.as_mut()[..num_results] }, + &func_type_index, caller.store.0, )?; } diff --git a/crates/wasmtime/src/runtime/rr/events/component_events.rs b/crates/wasmtime/src/runtime/rr/events/component_events.rs index 7891bf800f..62144f5185 100644 --- a/crates/wasmtime/src/runtime/rr/events/component_events.rs +++ b/crates/wasmtime/src/runtime/rr/events/component_events.rs @@ -5,7 +5,7 @@ use crate::component::Component; use crate::vm::component::libcalls::ResourceDropRet; // Re-export common events from this module pub use common_events::*; -use wasmtime_environ::{self, component::InterfaceType, component::TypeFunc}; +use wasmtime_environ::{self, component::InterfaceType, component::TypeFuncIndex}; /// A [`Component`] instantiatation event #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -28,27 +28,27 @@ pub struct HostFuncEntryEvent { /// Raw values passed across the call entry boundary args: RRFuncArgVals, - /// Param/return types (required to support replay validation). + /// Function index (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: TypeFunc, + ty: TypeFuncIndex, } impl HostFuncEntryEvent { // Record - pub fn new(args: &[MaybeUninit], types: TypeFunc) -> Self { + pub fn new(args: &[MaybeUninit], ty: TypeFuncIndex) -> Self { Self { args: func_argvals_from_raw_slice(args), - types: types, + ty: ty, } } } #[cfg(feature = "rr-validate")] -impl Validate for HostFuncEntryEvent { - fn validate(&self, expect_types: &TypeFunc) -> Result<(), ReplayError> { +impl Validate for HostFuncEntryEvent { + fn validate(&self, expect_ty: &TypeFuncIndex) -> Result<(), ReplayError> { self.log(); - if &self.types == expect_types { + if &self.ty == expect_ty { Ok(()) } else { 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 465ed12225..d51e4eefee 100644 --- a/crates/wasmtime/src/runtime/rr/events/core_events.rs +++ b/crates/wasmtime/src/runtime/rr/events/core_events.rs @@ -1,24 +1,20 @@ //! Module comprising of core wasm events use super::*; -#[expect(unused_imports, reason = "used for doc-links")] -use wasmtime_environ::{WasmFuncType, WasmValType}; +use wasmtime_environ::VMSharedTypeIndex; // Re-export common events from this module pub use common_events::*; -/// 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, Serialize, Deserialize)] pub struct HostFuncEntryEvent { /// Raw values passed across the call/return boundary args: RRFuncArgVals, /// Param/return types (required to support replay validation) - types: CoreFuncArgTypes, + types: VMSharedTypeIndex, } impl HostFuncEntryEvent { // Record - pub fn new(args: &[MaybeUninit], types: WasmFuncType) -> Self { + pub fn new(args: &[MaybeUninit], types: VMSharedTypeIndex) -> Self { Self { args: func_argvals_from_raw_slice(args), types: types, @@ -26,8 +22,8 @@ impl HostFuncEntryEvent { } } #[cfg(feature = "rr-validate")] -impl Validate for HostFuncEntryEvent { - fn validate(&self, expect_types: &CoreFuncArgTypes) -> Result<(), ReplayError> { +impl Validate for HostFuncEntryEvent { + fn validate(&self, expect_types: &VMSharedTypeIndex) -> Result<(), ReplayError> { self.log(); if &self.types == expect_types { Ok(()) diff --git a/crates/wasmtime/src/runtime/rr_hooks/component.rs b/crates/wasmtime/src/runtime/rr_hooks/component.rs index bf5c15d5e4..82467ca44a 100644 --- a/crates/wasmtime/src/runtime/rr_hooks/component.rs +++ b/crates/wasmtime/src/runtime/rr_hooks/component.rs @@ -5,7 +5,7 @@ use crate::prelude::*; use crate::store::StoreOpaque; use core::mem::MaybeUninit; #[cfg(feature = "component-model")] -use wasmtime_environ::component::{InterfaceType, TypeFunc}; +use wasmtime_environ::component::{InterfaceType, TypeFuncIndex}; #[cfg(feature = "rr-component")] use crate::rr::component_events::{HostFuncReturnEvent, LowerReturnEvent, LowerStoreReturnEvent}; @@ -14,16 +14,16 @@ use crate::rr::component_events::{HostFuncReturnEvent, LowerReturnEvent, LowerSt #[inline] pub fn record_replay_host_func_entry( args: &mut [MaybeUninit], - func_type: &TypeFunc, + func_idx: &TypeFuncIndex, store: &mut StoreOpaque, ) -> Result<()> { #[cfg(all(feature = "rr-component", feature = "rr-validate"))] { use crate::rr::component_events::HostFuncEntryEvent; - store.record_event_validation(|| HostFuncEntryEvent::new(args, func_type.clone()))?; - store.next_replay_event_validation::(func_type)?; + store.record_event_validation(|| HostFuncEntryEvent::new(args, func_idx.clone()))?; + store.next_replay_event_validation::(func_idx)?; } - let _ = (args, func_type, store); + let _ = (args, func_idx, store); Ok(()) } diff --git a/crates/wasmtime/src/runtime/rr_hooks/core.rs b/crates/wasmtime/src/runtime/rr_hooks/core.rs index 53cfa945dc..871a8b311f 100644 --- a/crates/wasmtime/src/runtime/rr_hooks/core.rs +++ b/crates/wasmtime/src/runtime/rr_hooks/core.rs @@ -4,26 +4,23 @@ use crate::prelude::*; use crate::rr::core_events::HostFuncReturnEvent; use crate::store::StoreOpaque; use core::mem::MaybeUninit; -use wasmtime_environ::WasmFuncType; +use wasmtime_environ::VMSharedTypeIndex; #[inline] /// Record and replay hook operation for host function entry events pub fn record_replay_host_func_entry( args: &[MaybeUninit], - wasm_func_type: &WasmFuncType, + ty: &VMSharedTypeIndex, store: &mut StoreOpaque, ) -> Result<()> { #[cfg(all(feature = "rr", feature = "rr-validate"))] { // Record/replay the raw parameter args 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)?; + store.record_event_validation(|| HostFuncEntryEvent::new(&args, ty.clone()))?; + store.next_replay_event_validation::(ty)?; } - let _ = (args, wasm_func_type, store); + let _ = (args, ty, store); Ok(()) } @@ -31,17 +28,13 @@ pub fn record_replay_host_func_entry( /// Record hook operation for host function return events pub fn record_host_func_return( args: &[MaybeUninit], - wasm_func_type: &WasmFuncType, + ty: &VMSharedTypeIndex, store: &mut StoreOpaque, ) -> Result<()> { // Record the return values #[cfg(feature = "rr")] - 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); + store.record_event(|| HostFuncReturnEvent::new(&args))?; + let _ = (args, ty, store); Ok(()) } @@ -49,7 +42,7 @@ pub fn record_host_func_return( /// Replay hook operation for host function return events pub fn replay_host_func_return( args: &mut [MaybeUninit], - wasm_func_type: &WasmFuncType, + ty: &VMSharedTypeIndex, store: &mut StoreOpaque, ) -> Result<()> { #[cfg(feature = "rr")] @@ -57,6 +50,6 @@ pub fn replay_host_func_return( event.move_into_slice(args); Ok(()) })?; - let _ = (args, wasm_func_type, store); + let _ = (args, ty, store); Ok(()) } From 997406dc806f9ab55722603ecccfa51f6554116d Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Fri, 26 Sep 2025 15:41:20 -0400 Subject: [PATCH 10/73] Support wasm function call/return for RR (component; typed) --- crates/wasmtime/src/runtime/component/func.rs | 18 ++- .../src/runtime/component/func/host.rs | 33 ++-- .../src/runtime/component/func/options.rs | 143 ++++-------------- .../src/runtime/component/func/typed.rs | 28 +++- crates/wasmtime/src/runtime/func.rs | 6 +- .../src/runtime/rr/events/common_events.rs | 27 +--- .../src/runtime/rr/events/component_events.rs | 112 +++++++++----- crates/wasmtime/src/runtime/rr/events/mod.rs | 23 +-- crates/wasmtime/src/runtime/rr/mod.rs | 18 ++- .../src/runtime/rr_hooks/component.rs | 87 ----------- .../src/runtime/rr_hooks/component_hooks.rs | 138 +++++++++++++++++ .../rr_hooks/{core.rs => core_hooks.rs} | 0 crates/wasmtime/src/runtime/rr_hooks/mod.rs | 109 ++++++++++++- 13 files changed, 438 insertions(+), 304 deletions(-) delete mode 100644 crates/wasmtime/src/runtime/rr_hooks/component.rs create mode 100644 crates/wasmtime/src/runtime/rr_hooks/component_hooks.rs rename crates/wasmtime/src/runtime/rr_hooks/{core.rs => core_hooks.rs} (100%) diff --git a/crates/wasmtime/src/runtime/component/func.rs b/crates/wasmtime/src/runtime/component/func.rs index ad97995acf..43a7b0e38a 100644 --- a/crates/wasmtime/src/runtime/component/func.rs +++ b/crates/wasmtime/src/runtime/component/func.rs @@ -4,6 +4,7 @@ use crate::component::storage::storage_as_slice; use crate::component::types::Type; use crate::component::values::Val; use crate::prelude::*; +use crate::rr_hooks::component_hooks; use crate::runtime::vm::component::{ComponentInstance, InstanceFlags, ResourceTables}; use crate::runtime::vm::{Export, VMFuncRef}; use crate::store::StoreOpaque; @@ -585,14 +586,17 @@ impl Func { // and `ComponentType` implementations, hence `ComponentType` being an // `unsafe` trait. unsafe { - crate::Func::call_unchecked_raw( + let params_and_returns = NonNull::new(core::ptr::slice_from_raw_parts_mut( + space.as_mut_ptr().cast(), + mem::size_of_val(space) / mem::size_of::(), + )) + .unwrap(); + + component_hooks::record_replay_wasm_func( + |store| crate::Func::call_unchecked_raw(store, export, params_and_returns), + params_and_returns.as_ref(), + self.index, &mut store, - export, - NonNull::new(core::ptr::slice_from_raw_parts_mut( - space.as_mut_ptr().cast(), - mem::size_of_val(space) / mem::size_of::(), - )) - .unwrap(), )?; } diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 82fbaf9c21..0ffc311197 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -6,6 +6,8 @@ use crate::component::storage::{slice_to_storage_mut, storage_as_slice_mut}; use crate::component::{ComponentNamedList, ComponentType, Instance, Lift, Lower, Val}; use crate::prelude::*; use crate::rr_hooks; +#[cfg(feature = "rr-component")] +use crate::rr_hooks::component_hooks::ReplayLoweringPhase; use crate::runtime::vm::component::{ ComponentInstance, VMComponentContext, VMLowering, VMLoweringCallee, }; @@ -255,7 +257,7 @@ where let types = vminstance.component().types().clone(); - rr_hooks::component::record_replay_host_func_entry(storage, &ty, store.0)?; + rr_hooks::component_hooks::record_replay_host_func_entry(storage, &ty, store.0)?; let ty = &types[ty]; let param_tys = InterfaceType::Tuple(ty.params); @@ -619,12 +621,12 @@ where ) -> Result<()> { match self.lower_dst() { Dst::Direct(storage) => { - let result = rr_hooks::component::record_lower( + let result = rr_hooks::component_hooks::record_lower_flat( |cx, ty| ret.linear_lower_to_flat(cx, ty, storage), cx, ty, ); - rr_hooks::component::record_host_func_return( + rr_hooks::component_hooks::record_host_func_return( unsafe { storage_as_slice_mut(storage) }, cx.store.0, )?; @@ -632,14 +634,14 @@ where } Dst::Indirect(ptr) => { let ptr = validate_inbounds::(cx.as_slice(), ptr)?; - let result = rr_hooks::component::record_lower_store( + let result = rr_hooks::component_hooks::record_lower_memory( |cx, ty, ptr| ret.linear_lower_to_memory(cx, ty, ptr), cx, ty, ptr, ); // Recording here is just for marking the return event - rr_hooks::component::record_host_func_return(&[], cx.store.0)?; + rr_hooks::component_hooks::record_host_func_return(&[], cx.store.0)?; result } } @@ -651,12 +653,15 @@ where match self.lower_dst() { Dst::Direct(storage) => { // This path also stores the final return values in resulting storage - cx.replay_lowering(Some(unsafe { storage_as_slice_mut(storage) })) + cx.replay_lowering( + Some(unsafe { storage_as_slice_mut(storage) }), + ReplayLoweringPhase::HostFuncReturn, + ) } Dst::Indirect(_ptr) => { // While replay will not have to change '_ptr' for indirect results, // it will have to overwrite any nested stored lowerings (deep copy) - cx.replay_lowering(None) + cx.replay_lowering(None, ReplayLoweringPhase::HostFuncReturn) } } } @@ -806,7 +811,7 @@ where let types = instance.id().get(store.0).component().types().clone(); - rr_hooks::component::record_replay_host_func_entry(storage, &ty, store.0)?; + rr_hooks::component_hooks::record_replay_host_func_entry(storage, &ty, store.0)?; let func_ty = &types[ty]; let param_tys = &types[func_ty.params]; @@ -919,27 +924,27 @@ 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()) { - rr_hooks::component::record_lower( + rr_hooks::component_hooks::record_lower_flat( |cx, ty| val.lower(cx, ty, &mut dst), &mut cx, *ty, )?; } assert!(dst.next().is_none()); - rr_hooks::component::record_host_func_return(storage, cx.store.0)?; + rr_hooks::component_hooks::record_host_func_return(storage, cx.store.0)?; } else { let ret_ptr = unsafe { 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); - rr_hooks::component::record_lower_store( + rr_hooks::component_hooks::record_lower_memory( |cx, ty, ptr| val.store(cx, ty, ptr), &mut cx, *ty, offset, )?; // Recording here is just for marking the return event - rr_hooks::component::record_host_func_return(&[], cx.store.0)?; + rr_hooks::component_hooks::record_host_func_return(&[], cx.store.0)?; } } @@ -961,11 +966,11 @@ where 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) // This path also stores the final return values in resulting storage - cx.replay_lowering(Some(storage))?; + cx.replay_lowering(Some(storage), ReplayLoweringPhase::HostFuncReturn)?; } else { // The indirect `ret_ptr` will not change during replay, but it will // have to overwrite any nested stored lowerings (deep copy) - cx.replay_lowering(None)?; + cx.replay_lowering(None, ReplayLoweringPhase::HostFuncReturn)?; } unsafe { 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 674d086010..c39735ce2a 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -6,11 +6,13 @@ use crate::component::{Instance, ResourceType}; use crate::prelude::*; #[cfg(feature = "rr-component")] use crate::rr::{ - RREvent, RecordBuffer, Recorder, ReplayError, Replayer, - component_events::MemorySliceWriteEvent, component_events::ReallocEntryEvent, + RREvent, RecordBuffer, ReplayError, Replayer, component_events::ReallocEntryEvent, }; #[cfg(all(feature = "rr-component", feature = "rr-validate"))] use crate::rr::{Validate, component_events::ReallocReturnEvent}; +#[cfg(feature = "rr-component")] +use crate::rr_hooks::component_hooks::ReplayLoweringPhase; +use crate::rr_hooks::{ConstMemorySliceCell, MemorySliceCell}; use crate::runtime::vm::component::{ CallContexts, ComponentInstance, InstanceFlags, ResourceTable, ResourceTables, }; @@ -20,7 +22,6 @@ use crate::{FuncType, StoreContextMut}; use alloc::sync::Arc; #[cfg(feature = "rr-component")] use core::mem::MaybeUninit; -use core::ops::{Deref, DerefMut}; use core::pin::Pin; use core::ptr::NonNull; use wasmtime_environ::component::{ @@ -28,105 +29,6 @@ use wasmtime_environ::component::{ 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> { - 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> { - 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) { - #[cfg(feature = "rr-component")] - if let Some(buf) = &mut self.recorder { - buf.record_event(|| MemorySliceWriteEvent { - offset: self.offset, - bytes: self.bytes.to_vec(), - }) - .unwrap(); - } - } -} - -/// 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 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), -/// 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> { - 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> { - 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) { - #[cfg(feature = "rr-component")] - if let Some(buf) = &mut self.recorder { - buf.record_event(|| MemorySliceWriteEvent { - offset: self.offset, - bytes: self.bytes.to_vec(), - }) - .unwrap(); - } - } -} - /// Runtime representation of canonical ABI options in the component model. /// /// This structure packages up the runtime representation of each option from @@ -658,8 +560,10 @@ 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`. + /// These typically include all `Lower*` and `Realloc*` event, along with the putting + /// the resulting value into storage (if provided) on the following termination conditions: + /// - `HostFunctionReturnEvent` when phase == HostFuncReturn + /// - `WasmFunctionEntryEvent` when phase == WasmFuncEntry /// /// ## Important Notes /// @@ -669,6 +573,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { pub fn replay_lowering( &mut self, mut result_storage: Option<&mut [MaybeUninit]>, + phase: ReplayLoweringPhase, ) -> 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 @@ -690,7 +595,25 @@ impl<'a, T: 'static> LowerContext<'a, T> { let run_validate = buf.settings().validate && buf.trace_settings().add_validation; match event { RREvent::HostFuncReturn(e) => { - // End of the lowering process + match phase { + ReplayLoweringPhase::HostFuncReturn => {} + _ => bail!("HostFuncReturn encountered in invalid phase"), + } + // End of the lowering process (for host calls) + if let Some(e) = lowering_error { + return Err(e.into()); + } + if let Some(storage) = result_storage.as_deref_mut() { + e.move_into_slice(storage); + } + complete = true; + } + RREvent::ComponentWasmFuncEntry(e) => { + match phase { + ReplayLoweringPhase::WasmFuncEntry => {} + _ => bail!("WasmFuncEntry encountered in invalid phase"), + } + // End of the lowering process (for wasm calls) if let Some(e) = lowering_error { return Err(e.into()); } @@ -708,14 +631,14 @@ impl<'a, T: 'static> LowerContext<'a, T> { } } // No return value to validate for lower/lower-store; store error and just check that entry happened before - RREvent::ComponentLowerReturn(e) => { + RREvent::ComponentLowerFlatReturn(e) => { #[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) => { + RREvent::ComponentLowerMemoryReturn(e) => { #[cfg(feature = "rr-validate")] if run_validate { _lower_store_stack @@ -735,7 +658,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { // 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!("Cannot call into host during lowering") } // Unwrapping should never occur on valid executions since *Entry should be before *Return in trace RREvent::ComponentReallocReturn(_e) => @@ -745,14 +668,14 @@ impl<'a, T: 'static> LowerContext<'a, T> { lowering_error = _e.validate(&_realloc_stack.pop().unwrap()).err() } } - RREvent::ComponentLowerEntry(_) => { + RREvent::ComponentLowerFlatEntry(_) => { // All we want here is ensuring Entry occurs before Return #[cfg(feature = "rr-validate")] if run_validate { _lower_stack.push(()) } } - RREvent::ComponentLowerStoreEntry(_) => { + RREvent::ComponentLowerMemoryEntry(_) => { // All we want here is ensuring Entry occurs before Return #[cfg(feature = "rr-validate")] if run_validate { diff --git a/crates/wasmtime/src/runtime/component/func/typed.rs b/crates/wasmtime/src/runtime/component/func/typed.rs index ac636cce58..3e7b059cf5 100644 --- a/crates/wasmtime/src/runtime/component/func/typed.rs +++ b/crates/wasmtime/src/runtime/component/func/typed.rs @@ -3,6 +3,7 @@ use crate::component::func::{Func, LiftContext, LowerContext, Options}; use crate::component::matching::InstanceType; use crate::component::storage::{storage_as_slice, storage_as_slice_mut}; use crate::prelude::*; +use crate::rr_hooks::component_hooks; use crate::{AsContextMut, StoreContext, StoreContextMut, ValRaw}; use alloc::borrow::Cow; use core::fmt; @@ -454,7 +455,19 @@ where dst: &mut MaybeUninit, ) -> Result<()> { assert!(Params::flatten_count() <= MAX_FLAT_PARAMS); - params.linear_lower_to_flat(cx, ty, dst)?; + if cx.store.0.replay_enabled() { + #[cfg(feature = "rr-component")] + cx.replay_lowering( + Some(unsafe { storage_as_slice_mut(dst) }), + component_hooks::ReplayLoweringPhase::WasmFuncEntry, + )? + } else { + component_hooks::record_lower_flat( + |cx, ty| params.linear_lower_to_flat(cx, ty, dst), + cx, + ty, + )?; + } Ok(()) } @@ -478,7 +491,18 @@ where // Note that `realloc` will bake in a check that the returned pointer is // in-bounds. let ptr = cx.realloc(0, 0, Params::ALIGN32, Params::SIZE32)?; - params.linear_lower_to_memory(cx, ty, ptr)?; + + if cx.store.0.replay_enabled() { + #[cfg(feature = "rr-component")] + cx.replay_lowering(None, component_hooks::ReplayLoweringPhase::WasmFuncEntry)? + } else { + component_hooks::record_lower_memory( + |cx, ty, ptr| params.linear_lower_to_memory(cx, ty, ptr), + cx, + ty, + ptr, + )?; + } // Note that the pointer here is stored as a 64-bit integer. This allows // this to work with either 32 or 64-bit memories. For a 32-bit memory diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 1957960d58..e45c9f72b6 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -2378,7 +2378,7 @@ impl HostContext { // 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::core::record_replay_host_func_entry( + rr_hooks::core_hooks::record_replay_host_func_entry( unsafe { &args.as_ref()[..num_params] }, &func_type_index, caller.store.0, @@ -2423,14 +2423,14 @@ impl HostContext { unsafe { ret.store(&mut store, args.as_mut())? }; } // Record the return values - rr_hooks::core::record_host_func_return( + rr_hooks::core_hooks::record_host_func_return( unsafe { &args.as_ref()[..num_results] }, &func_type_index, caller.store.0, )?; } else { // Replay the return values - rr_hooks::core::replay_host_func_return( + rr_hooks::core_hooks::replay_host_func_return( unsafe { &mut args.as_mut()[..num_results] }, &func_type_index, caller.store.0, diff --git a/crates/wasmtime/src/runtime/rr/events/common_events.rs b/crates/wasmtime/src/runtime/rr/events/common_events.rs index b2a540ccf9..5c483ad6b1 100644 --- a/crates/wasmtime/src/runtime/rr/events/common_events.rs +++ b/crates/wasmtime/src/runtime/rr/events/common_events.rs @@ -23,33 +23,8 @@ impl HostFuncReturnEvent { } } // Replay - /// Consume the caller event and encode it back into the slice with an optional - /// typechecking validation of the event. + /// Consume the caller event and encode it back into the slice pub fn move_into_slice(self, args: &mut [MaybeUninit]) { func_argvals_into_raw_slice(self.args, args); } } - -//type WasmFuncArgVals = Vec; -///// A call event from Host into a Wasm component function -//#[derive(Debug, Clone, Serialize, Deserialize)] -//pub struct WasmFuncEntryEvent { -// /// Wasm component values passed as parameters to the function -// args: WasmFuncArgVals, -//} -// -///// A return event from a Wasm component function to Host -///// -///// Matches 1:1 with [`WasmFuncEntryEvent`]. -///// -///// Note: Could potential merge with [`HostFuncReturnEvent`]? -//#[derive(Debug, Clone, Serialize, Deserialize)] -//pub struct WasmFuncReturnEvent { -// /// Lowered values passed across the call return boundary -// args: RRFuncArgVals, -//} -//impl WasmFuncReturnEvent { -// pub fn new(args: &[Val]) { -// Self { args } -// } -//} diff --git a/crates/wasmtime/src/runtime/rr/events/component_events.rs b/crates/wasmtime/src/runtime/rr/events/component_events.rs index 62144f5185..76f59debde 100644 --- a/crates/wasmtime/src/runtime/rr/events/component_events.rs +++ b/crates/wasmtime/src/runtime/rr/events/component_events.rs @@ -5,7 +5,10 @@ use crate::component::Component; use crate::vm::component::libcalls::ResourceDropRet; // Re-export common events from this module pub use common_events::*; -use wasmtime_environ::{self, component::InterfaceType, component::TypeFuncIndex}; +use wasmtime_environ::{ + self, + component::{ExportIndex, InterfaceType, TypeFuncIndex}, +}; /// A [`Component`] instantiatation event #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -22,6 +25,31 @@ impl InstantiationEvent { } } +/// A call event from Host into a Wasm component function +/// +/// Note: Could potential merge with [`HostFuncReturnEvent`] as [`WasmToHostEvent`]? +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WasmFuncEntryEvent { + /// Raw values passed across call boundary + args: RRFuncArgVals, + func_idx: ExportIndex, +} +impl WasmFuncEntryEvent { + // Record + pub fn new(args: &[ValRaw], func_idx: ExportIndex) -> Self { + Self { + args: func_argvals_from_raw_slice(args), + func_idx, + } + } + + // Replay + /// Consume the caller event and encode it back into the slice + pub fn move_into_slice(self, args: &mut [MaybeUninit]) { + func_argvals_into_raw_slice(self.args, args); + } +} + /// A call event from a Wasm component into the host #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HostFuncEntryEvent { @@ -67,15 +95,15 @@ pub struct ReallocEntryEvent { pub new_size: usize, } -/// Entry to a type lowering invocation +/// Entry to a type lowering invocation to flat destination #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct LowerEntryEvent { +pub struct LowerFlatEntryEvent { pub ty: InterfaceType, } -/// Entry to store invocations during type lowering +/// Entry to type lowering invocation to destination in memory #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct LowerStoreEntryEvent { +pub struct LowerMemoryEntryEvent { pub ty: InterfaceType, pub offset: usize, } @@ -95,24 +123,53 @@ pub struct MemorySliceWriteEvent { macro_rules! generic_new_result_events { ( $( - $(#[doc = $doc:literal])* + $(#[$meta:meta])* $event:ident -> ($ok_ty:ty,$err_variant:path) ),* ) => ( $( - $(#[doc = $doc])* + $(#[$meta])* #[derive(Debug, Clone, Serialize, Deserialize)] pub struct $event(Result<$ok_ty, EventActionError>); impl $event { pub fn from_anyhow_result(ret: &Result<$ok_ty>) -> Self { - Self(ret.as_ref().map(|t| *t).map_err(|e| $err_variant(e.to_string()))) + Self(ret.as_ref().map(|t| (*t).clone()).map_err(|e| $err_variant(e.to_string()))) } pub fn ret(self) -> Result<$ok_ty, EventActionError> { self.0 } } + generic_new_result_events!(@validate_impl $event,$ok_ty,$err_variant); )* ); + + (@validate_impl $event:ident,$ok_ty:ty,$err_variant:path) => { + #[cfg(feature = "rr-validate")] + impl Validate> for $event { + /// We can check that realloc is deterministic (as expected by the engine) + fn validate(&self, expect_ret: &Result<$ok_ty>) -> Result<(), ReplayError> { + self.log(); + // Cannot just use eq since anyhow::Error and EventActionError cannot be compared + match (self.0.as_ref(), expect_ret.as_ref()) { + (Ok(r), Ok(s)) => { + if r == s { + Ok(()) + } else { + Err(ReplayError::FailedValidation) + } + } + // Return the recorded error + (Err(e), Err(f)) => Err(ReplayError::from($err_variant(format!( + "Replayed Error for {}: {} \nRecorded Error for {}: {}", + stringify!($event), e, stringify!($event), f + )))), + // Diverging errors.. Report as a failed validation + (Ok(_), Err(_)) => Err(ReplayError::FailedValidation), + (Err(_), Ok(_)) => Err(ReplayError::FailedValidation), + } + } + } + }; } // Macro to generate RR events from the builtin descriptions @@ -215,10 +272,16 @@ macro_rules! builtin_events { generic_new_result_events! { /// Return from a reallocation call (needed only for validation) ReallocReturnEvent -> (usize, EventActionError::ReallocError), - /// Return from a type lowering invocation - LowerReturnEvent -> ((), EventActionError::LowerError), - /// Return from store invocations during type lowering - LowerStoreReturnEvent -> ((), EventActionError::LowerStoreError) + /// Return from type lowering to flat destination + LowerFlatReturnEvent -> ((), EventActionError::LowerFlatError), + /// Return from type lowering to destination in memory + LowerMemoryReturnEvent -> ((), EventActionError::LowerMemoryError), + /// A return event from a Wasm component function to Host + /// + /// Matches 1:1 with [`WasmFuncEntryEvent`]. + /// + /// Note: Could potential merge with [`HostFuncEntryEvent`] as [`HostToWasmEvent`]? + WasmFuncReturnEvent -> (RRFuncArgVals, EventActionError::WasmFuncReturnError) } // Entry/return events for each builtin function @@ -227,28 +290,3 @@ wasmtime_environ::foreach_builtin_component_function!(builtin_events); // === Special Validation === // `realloc` needs to actually check for divergence // between recorded and replayed realloc effects -#[cfg(feature = "rr-validate")] -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.0.as_ref(), expect_ret.as_ref()) { - (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: {}", - e, f - )))), - // Diverging errors.. Report as a failed validation - (Ok(_), Err(_)) => Err(ReplayError::FailedValidation), - (Err(_), Ok(_)) => Err(ReplayError::FailedValidation), - } - } -} diff --git a/crates/wasmtime/src/runtime/rr/events/mod.rs b/crates/wasmtime/src/runtime/rr/events/mod.rs index 3062d3b499..3d376088c9 100644 --- a/crates/wasmtime/src/runtime/rr/events/mod.rs +++ b/crates/wasmtime/src/runtime/rr/events/mod.rs @@ -19,18 +19,20 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum EventActionError { ReallocError(String), - LowerError(String), - LowerStoreError(String), + LowerFlatError(String), + LowerMemoryError(String), BuiltinError(String), + WasmFuncReturnError(String), } 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) - | Self::BuiltinError(s) => { + | Self::LowerFlatError(s) + | Self::LowerMemoryError(s) + | Self::BuiltinError(s) + | Self::WasmFuncReturnError(s) => { write!(f, "{}", s) } } @@ -47,7 +49,7 @@ type ValRawBytes = [u8; mem::size_of::()]; /// /// Essentially [`From`] and [`Into`] but local to the crate /// to bypass orphan rule for externally defined types -trait ValRawBytesConvertable { +pub trait ValRawBytesConvertable { fn to_valraw_bytes(self) -> ValRawBytes; fn from_valraw_bytes(value: ValRawBytes) -> Self; } @@ -76,10 +78,10 @@ impl ValRawBytesConvertable for MaybeUninit { } } -type RRFuncArgVals = Vec; +pub type RRFuncArgVals = Vec; /// Construct [`RRFuncArgVals`] from raw value buffer -fn func_argvals_from_raw_slice(args: &[T]) -> RRFuncArgVals +pub fn func_argvals_from_raw_slice(args: &[T]) -> RRFuncArgVals where T: ValRawBytesConvertable + Copy, { @@ -98,10 +100,13 @@ where /// Trait signifying types that can be validated on replay /// -/// All `PartialEq` and `Eq` types are directly validatable with themselves. +/// All `PartialEq` 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 (e.g. [`component_events::InstantiationEvent`]). +/// +/// In terms of usage, an event that implements `Validate` can call +/// any RR validation methods on a `Store` #[cfg(any(feature = "rr-component", feature = "rr-validate"))] pub trait Validate { /// Perform a validation of the event to ensure replay consistency diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index 4e76fa4fbf..596e56d656 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -19,6 +19,8 @@ use serde::{Deserialize, Serialize}; pub use events::Validate; #[cfg(feature = "rr-component")] pub use events::component_events; +#[cfg(all(feature = "rr-validate", feature = "rr-component"))] +pub use events::{RRFuncArgVals, func_argvals_from_raw_slice}; use events::{common_events, component_events as __component_events}; pub use events::{core_events, marker_events}; pub use io::{RecordWriter, ReplayReader}; @@ -100,14 +102,16 @@ rr_event! { // REQUIRED events for replay + /// Call into a Wasm component function from host + ComponentWasmFuncEntry(__component_events::WasmFuncEntryEvent), /// Instantiation of a component ComponentInstantiation(__component_events::InstantiationEvent), /// Component ABI realloc call in linear wasm memory ComponentReallocEntry(__component_events::ReallocEntryEvent), /// Return from a type lowering operation - ComponentLowerReturn(__component_events::LowerReturnEvent), + ComponentLowerFlatReturn(__component_events::LowerFlatReturnEvent), /// Return from a store during a type lowering operation - ComponentLowerStoreReturn(__component_events::LowerStoreReturnEvent), + ComponentLowerMemoryReturn(__component_events::LowerMemoryReturnEvent), /// An attempt to obtain a mutable slice into Wasm linear memory ComponentMemorySliceWrite(__component_events::MemorySliceWriteEvent), /// Return from a component builtin @@ -118,14 +122,16 @@ rr_event! { // ReallocReturn is optional because we can assume the realloc is deterministic // and the error message is subsumed by the containing LowerReturn/LowerStoreReturn + /// Return from a Wasm component function back to host + ComponentWasmFuncReturn(__component_events::WasmFuncReturnEvent), /// Return from Component ABI realloc call ComponentReallocReturn(__component_events::ReallocReturnEvent), /// Call into host function from component ComponentHostFuncEntry(__component_events::HostFuncEntryEvent), - /// Call into [Lower::lower] for type lowering - ComponentLowerEntry(__component_events::LowerEntryEvent), - /// Call into [Lower::store] during type lowering - ComponentLowerStoreEntry(__component_events::LowerStoreEntryEvent), + /// Call into type lowering for flat destination + ComponentLowerFlatEntry(__component_events::LowerFlatEntryEvent), + /// Call into type lowering for memory destination + ComponentLowerMemoryEntry(__component_events::LowerMemoryEntryEvent), /// Call into a component builtin ComponentBuiltinEntry(__component_events::BuiltinEntryEvent) } diff --git a/crates/wasmtime/src/runtime/rr_hooks/component.rs b/crates/wasmtime/src/runtime/rr_hooks/component.rs deleted file mode 100644 index 82467ca44a..0000000000 --- a/crates/wasmtime/src/runtime/rr_hooks/component.rs +++ /dev/null @@ -1,87 +0,0 @@ -use crate::ValRaw; -#[cfg(feature = "component-model")] -use crate::component::func::LowerContext; -use crate::prelude::*; -use crate::store::StoreOpaque; -use core::mem::MaybeUninit; -#[cfg(feature = "component-model")] -use wasmtime_environ::component::{InterfaceType, TypeFuncIndex}; - -#[cfg(feature = "rr-component")] -use crate::rr::component_events::{HostFuncReturnEvent, LowerReturnEvent, LowerStoreReturnEvent}; - -/// Record/replay hook operation for host function entry events -#[inline] -pub fn record_replay_host_func_entry( - args: &mut [MaybeUninit], - func_idx: &TypeFuncIndex, - store: &mut StoreOpaque, -) -> Result<()> { - #[cfg(all(feature = "rr-component", feature = "rr-validate"))] - { - use crate::rr::component_events::HostFuncEntryEvent; - store.record_event_validation(|| HostFuncEntryEvent::new(args, func_idx.clone()))?; - store.next_replay_event_validation::(func_idx)?; - } - let _ = (args, func_idx, store); - Ok(()) -} - -/// Record hook operation for host function return events -#[inline] -pub fn record_host_func_return( - args: &[MaybeUninit], - store: &mut StoreOpaque, -) -> Result<()> { - #[cfg(feature = "rr-component")] - store.record_event(|| HostFuncReturnEvent::new(args))?; - let _ = (args, store); - Ok(()) -} - -/// 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-validate"))] - { - use crate::rr::component_events::LowerStoreEntryEvent; - cx.store - .0 - .record_event_validation(|| LowerStoreEntryEvent { ty, offset })?; - } - let store_result = lower_store(cx, ty, offset); - #[cfg(feature = "rr-component")] - cx.store - .0 - .record_event(|| LowerStoreReturnEvent::from_anyhow_result(&store_result))?; - store_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-validate"))] - { - use crate::rr::component_events::LowerEntryEvent; - cx.store - .0 - .record_event_validation(|| LowerEntryEvent { ty })?; - } - let lower_result = lower(cx, ty); - #[cfg(feature = "rr-component")] - cx.store - .0 - .record_event(|| LowerReturnEvent::from_anyhow_result(&lower_result))?; - lower_result -} diff --git a/crates/wasmtime/src/runtime/rr_hooks/component_hooks.rs b/crates/wasmtime/src/runtime/rr_hooks/component_hooks.rs new file mode 100644 index 0000000000..b2c81fb569 --- /dev/null +++ b/crates/wasmtime/src/runtime/rr_hooks/component_hooks.rs @@ -0,0 +1,138 @@ +use crate::ValRaw; +#[cfg(feature = "component-model")] +use crate::component::func::LowerContext; +#[cfg(all(feature = "rr-component", feature = "rr-validate"))] +use crate::rr::{ + RRFuncArgVals, component_events::WasmFuncReturnEvent, func_argvals_from_raw_slice, +}; +use crate::store::StoreOpaque; +use crate::{StoreContextMut, prelude::*}; +use core::mem::MaybeUninit; +#[cfg(feature = "component-model")] +use wasmtime_environ::component::{ExportIndex, InterfaceType, TypeFuncIndex}; + +#[cfg(feature = "rr-component")] +use crate::rr::component_events::{ + HostFuncReturnEvent, LowerFlatReturnEvent, LowerMemoryReturnEvent, WasmFuncEntryEvent, +}; + +/// Indicator type signalling the context during lowering +#[cfg(feature = "rr-component")] +pub enum ReplayLoweringPhase { + WasmFuncEntry, + HostFuncReturn, +} + +/// Record hook wrapping a wasm component export function invocation and replay +/// validation of return value +#[inline] +pub fn record_replay_wasm_func( + wasm_call: F, + args: &[ValRaw], + func_idx: ExportIndex, + store: &mut StoreContextMut<'_, T>, +) -> Result<()> +where + F: FnOnce(&mut StoreContextMut<'_, T>) -> Result<()>, +{ + let _ = (args, func_idx); + #[cfg(feature = "rr-component")] + store + .0 + .record_event(|| WasmFuncEntryEvent::new(args, func_idx))?; + let result = wasm_call(store); + #[cfg(all(feature = "rr-component", feature = "rr-validate"))] + { + let result = result.map(|_| func_argvals_from_raw_slice(args)); + store + .0 + .record_event_validation(|| WasmFuncReturnEvent::from_anyhow_result(&result))?; + store + .0 + .next_replay_event_validation::>(&result)?; + result?; + return Ok(()); + } + #[cfg(not(all(feature = "rr-component", feature = "rr-validate")))] + return result; +} + +/// Record/replay hook operation for host function entry events +#[inline] +pub fn record_replay_host_func_entry( + args: &mut [MaybeUninit], + func_idx: &TypeFuncIndex, + store: &mut StoreOpaque, +) -> Result<()> { + #[cfg(all(feature = "rr-component", feature = "rr-validate"))] + { + use crate::rr::component_events::HostFuncEntryEvent; + store.record_event_validation(|| HostFuncEntryEvent::new(args, func_idx.clone()))?; + store.next_replay_event_validation::(func_idx)?; + } + let _ = (args, func_idx, store); + Ok(()) +} + +/// Record hook operation for host function return events +#[inline] +pub fn record_host_func_return( + args: &[MaybeUninit], + store: &mut StoreOpaque, +) -> Result<()> { + #[cfg(feature = "rr-component")] + store.record_event(|| HostFuncReturnEvent::new(args))?; + let _ = (args, store); + Ok(()) +} + +/// Record hook wrapping a memory lowering call of component types +#[inline] +pub fn record_lower_memory( + 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-validate"))] + { + use crate::rr::component_events::LowerMemoryEntryEvent; + cx.store + .0 + .record_event_validation(|| LowerMemoryEntryEvent { ty, offset })?; + } + let store_result = lower_store(cx, ty, offset); + #[cfg(feature = "rr-component")] + cx.store + .0 + .record_event(|| LowerMemoryReturnEvent::from_anyhow_result(&store_result))?; + store_result +} + +/// Record hook wrapping a flat lowering call of component types +#[inline] +pub fn record_lower_flat( + lower: F, + cx: &mut LowerContext<'_, T>, + ty: InterfaceType, +) -> Result<()> +where + F: FnOnce(&mut LowerContext<'_, T>, InterfaceType) -> Result<()>, +{ + #[cfg(all(feature = "rr-component", feature = "rr-validate"))] + { + use crate::rr::component_events::LowerFlatEntryEvent; + cx.store + .0 + .record_event_validation(|| LowerFlatEntryEvent { ty })?; + } + let lower_result = lower(cx, ty); + #[cfg(feature = "rr-component")] + cx.store + .0 + .record_event(|| LowerFlatReturnEvent::from_anyhow_result(&lower_result))?; + lower_result +} diff --git a/crates/wasmtime/src/runtime/rr_hooks/core.rs b/crates/wasmtime/src/runtime/rr_hooks/core_hooks.rs similarity index 100% rename from crates/wasmtime/src/runtime/rr_hooks/core.rs rename to crates/wasmtime/src/runtime/rr_hooks/core_hooks.rs diff --git a/crates/wasmtime/src/runtime/rr_hooks/mod.rs b/crates/wasmtime/src/runtime/rr_hooks/mod.rs index f5ef54e1ed..34cb26226a 100644 --- a/crates/wasmtime/src/runtime/rr_hooks/mod.rs +++ b/crates/wasmtime/src/runtime/rr_hooks/mod.rs @@ -1,6 +1,109 @@ //! Convenience methods for hooking in RR event recording/replaying to the rest of the engine +#[cfg(feature = "rr-component")] +use crate::rr::{RecordBuffer, Recorder, component_events::MemorySliceWriteEvent}; -/// Component RR hooks -pub mod component; +use core::ops::{Deref, DerefMut}; + +/// Component specific RR hooks that use `component-model` feature gating +pub mod component_hooks; /// Core RR hooks -pub mod core; +pub mod core_hooks; + +/// 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> { + pub bytes: &'a mut [u8], + #[cfg(feature = "rr-component")] + pub offset: usize, + #[cfg(feature = "rr-component")] + pub 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) { + #[cfg(feature = "rr-component")] + if let Some(buf) = &mut self.recorder { + buf.record_event(|| MemorySliceWriteEvent { + offset: self.offset, + bytes: self.bytes.to_vec(), + }) + .unwrap(); + } + } +} + +/// 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 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), +/// 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> { + pub bytes: &'a mut [u8; N], + #[cfg(feature = "rr-component")] + pub offset: usize, + #[cfg(feature = "rr-component")] + pub 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) { + #[cfg(feature = "rr-component")] + if let Some(buf) = &mut self.recorder { + buf.record_event(|| MemorySliceWriteEvent { + offset: self.offset, + bytes: self.bytes.to_vec(), + }) + .unwrap(); + } + } +} From 4a42d21a8dcd691017fae81ce94a288289a06751 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Fri, 3 Oct 2025 04:49:11 +0530 Subject: [PATCH 11/73] Added component signature for wasm func call recording --- crates/wasmtime/src/runtime/component/func.rs | 1 + .../src/runtime/rr/events/component_events.rs | 17 +++++++++-------- .../src/runtime/rr_hooks/component_hooks.rs | 14 +++++++------- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func.rs b/crates/wasmtime/src/runtime/component/func.rs index 43a7b0e38a..39b06f30f2 100644 --- a/crates/wasmtime/src/runtime/component/func.rs +++ b/crates/wasmtime/src/runtime/component/func.rs @@ -596,6 +596,7 @@ impl Func { |store| crate::Func::call_unchecked_raw(store, export, params_and_returns), params_and_returns.as_ref(), self.index, + *self.instance.id().get(store.0).component().checksum(), &mut store, )?; } diff --git a/crates/wasmtime/src/runtime/rr/events/component_events.rs b/crates/wasmtime/src/runtime/rr/events/component_events.rs index 76f59debde..e730556223 100644 --- a/crates/wasmtime/src/runtime/rr/events/component_events.rs +++ b/crates/wasmtime/src/runtime/rr/events/component_events.rs @@ -14,13 +14,13 @@ use wasmtime_environ::{ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct InstantiationEvent { /// A checksum of the component bytecode - checksum: [u8; 32], + component: [u8; 32], } impl InstantiationEvent { pub fn from_component(component: &Component) -> Self { Self { - checksum: *component.checksum(), + component: *component.checksum(), } } } @@ -32,13 +32,16 @@ impl InstantiationEvent { pub struct WasmFuncEntryEvent { /// Raw values passed across call boundary args: RRFuncArgVals, + /// Checksum of component containing function + component: [u8; 32], func_idx: ExportIndex, } impl WasmFuncEntryEvent { // Record - pub fn new(args: &[ValRaw], func_idx: ExportIndex) -> Self { + pub fn new(args: &[ValRaw], component: [u8; 32], func_idx: ExportIndex) -> Self { Self { args: func_argvals_from_raw_slice(args), + component, func_idx, } } @@ -145,8 +148,10 @@ macro_rules! generic_new_result_events { (@validate_impl $event:ident,$ok_ty:ty,$err_variant:path) => { #[cfg(feature = "rr-validate")] + /// Note: Anyhow result types cannot use blanket PartialEq implementations since + /// anyhow results are not serialized directly. They need to specifically check + /// for divergence between recorded and replayed effects with [EventActionError] impl Validate> for $event { - /// We can check that realloc is deterministic (as expected by the engine) fn validate(&self, expect_ret: &Result<$ok_ty>) -> Result<(), ReplayError> { self.log(); // Cannot just use eq since anyhow::Error and EventActionError cannot be compared @@ -286,7 +291,3 @@ generic_new_result_events! { // Entry/return events for each builtin function wasmtime_environ::foreach_builtin_component_function!(builtin_events); - -// === Special Validation === -// `realloc` needs to actually check for divergence -// between recorded and replayed realloc effects diff --git a/crates/wasmtime/src/runtime/rr_hooks/component_hooks.rs b/crates/wasmtime/src/runtime/rr_hooks/component_hooks.rs index b2c81fb569..c880490901 100644 --- a/crates/wasmtime/src/runtime/rr_hooks/component_hooks.rs +++ b/crates/wasmtime/src/runtime/rr_hooks/component_hooks.rs @@ -1,6 +1,10 @@ use crate::ValRaw; #[cfg(feature = "component-model")] use crate::component::func::LowerContext; +#[cfg(feature = "rr-component")] +use crate::rr::component_events::{ + HostFuncReturnEvent, LowerFlatReturnEvent, LowerMemoryReturnEvent, WasmFuncEntryEvent, +}; #[cfg(all(feature = "rr-component", feature = "rr-validate"))] use crate::rr::{ RRFuncArgVals, component_events::WasmFuncReturnEvent, func_argvals_from_raw_slice, @@ -11,11 +15,6 @@ use core::mem::MaybeUninit; #[cfg(feature = "component-model")] use wasmtime_environ::component::{ExportIndex, InterfaceType, TypeFuncIndex}; -#[cfg(feature = "rr-component")] -use crate::rr::component_events::{ - HostFuncReturnEvent, LowerFlatReturnEvent, LowerMemoryReturnEvent, WasmFuncEntryEvent, -}; - /// Indicator type signalling the context during lowering #[cfg(feature = "rr-component")] pub enum ReplayLoweringPhase { @@ -30,16 +29,17 @@ pub fn record_replay_wasm_func( wasm_call: F, args: &[ValRaw], func_idx: ExportIndex, + component: [u8; 32], store: &mut StoreContextMut<'_, T>, ) -> Result<()> where F: FnOnce(&mut StoreContextMut<'_, T>) -> Result<()>, { - let _ = (args, func_idx); + let _ = (args, component, func_idx); #[cfg(feature = "rr-component")] store .0 - .record_event(|| WasmFuncEntryEvent::new(args, func_idx))?; + .record_event(|| WasmFuncEntryEvent::new(args, component, func_idx))?; let result = wasm_call(store); #[cfg(all(feature = "rr-component", feature = "rr-validate"))] { From cb8e0fc83704a95dcaaf93cc2e553a695fc753a5 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Sat, 4 Oct 2025 04:56:52 +0530 Subject: [PATCH 12/73] Revamped public API for record; replay still broken --- crates/wasmtime/src/config.rs | 68 ++++++------------------- crates/wasmtime/src/engine.rs | 9 ++-- crates/wasmtime/src/runtime.rs | 2 + crates/wasmtime/src/runtime/instance.rs | 6 ++- crates/wasmtime/src/runtime/rr/io.rs | 4 +- crates/wasmtime/src/runtime/rr/mod.rs | 39 +++++--------- crates/wasmtime/src/runtime/store.rs | 62 +++++++++++++--------- 7 files changed, 78 insertions(+), 112 deletions(-) diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index a842375292..78e7d6f832 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -179,7 +179,7 @@ pub struct Config { pub(crate) macos_use_mach_ports: bool, pub(crate) detect_host_feature: Option Option>, #[cfg(feature = "rr")] - pub(crate) rr: Option, + pub(crate) record_support: bool, } /// User-provided configuration for the compiler. @@ -395,7 +395,7 @@ impl Config { #[cfg(not(feature = "std"))] detect_host_feature: None, #[cfg(feature = "rr")] - rr: None, + record_support: false, }; #[cfg(any(feature = "cranelift", feature = "winch"))] { @@ -2834,66 +2834,30 @@ impl Config { self } - /// Evaluates to true if current configuration must respect - /// deterministic execution in its configuration. - /// - /// 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. /// /// This method implicitly enforces determinism (see [`Config::enforce_determinism`] /// for details). - /// - /// ## Errors - /// - /// 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 { - if let RRConfig::Replay(_) = cfg { - bail!("Cannot enable recording when replay is already enabled"); - } - } - self.rr = Some(RRConfig::from(record)); - Ok(self) - } - - /// 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. - #[cfg(feature = "rr")] - 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"); - } + #[inline] + pub fn recording(&mut self, enable: bool) -> &mut Self { + if enable { + self.enforce_determinism(); + } else { + self.remove_determinism_enforcement(); } - self.rr = Some(RRConfig::from(replay)); - Ok(self) + self.record_support = enable; + self } - /// Disable the currently active record/replay configuration, and remove - /// any determinism enforcement it introduced as side-effects. + /// Evaluates to true if current configuration must respect + /// deterministic execution in its configuration. /// - /// A common option is used for both record/replay here - /// since record and replay can never be set simultaneously/ + /// Required for faithful record/replay execution. #[cfg(feature = "rr")] - pub fn disable_record_replay(&mut self) -> &mut Self { - self.remove_determinism_enforcement(); - self.rr = None; - self + #[inline] + pub fn is_determinism_enforced(&mut self) -> bool { + self.record_support } } diff --git a/crates/wasmtime/src/engine.rs b/crates/wasmtime/src/engine.rs index a5f9c024fa..97926d42a7 100644 --- a/crates/wasmtime/src/engine.rs +++ b/crates/wasmtime/src/engine.rs @@ -1,6 +1,4 @@ use crate::Config; -#[cfg(feature = "rr")] -use crate::RRConfig; use crate::prelude::*; #[cfg(feature = "runtime")] pub use crate::runtime::code_memory::CustomCodeMemory; @@ -256,12 +254,11 @@ impl Engine { self.config().async_support } - /// Returns an immutable reference to the record/replay configuration settings - /// used by the engine + /// Returns whether the engine is configured to support execution recording #[cfg(feature = "rr")] #[inline] - pub fn rr(&self) -> Option<&RRConfig> { - self.config().rr.as_ref() + pub fn is_recording(&self) -> bool { + self.config().record_support } /// Detects whether the bytes provided are a precompiled object produced by diff --git a/crates/wasmtime/src/runtime.rs b/crates/wasmtime/src/runtime.rs index b7ef589ec0..09d4df18fc 100644 --- a/crates/wasmtime/src/runtime.rs +++ b/crates/wasmtime/src/runtime.rs @@ -88,6 +88,8 @@ pub use linker::*; pub use memory::*; pub use module::{Module, ModuleExport}; pub use resources::*; +#[cfg(feature = "rr")] +pub use rr::{RecordWriter, ReplayReader}; #[cfg(all(feature = "async", feature = "call-hook"))] pub use store::CallHookHandler; pub use store::{ diff --git a/crates/wasmtime/src/runtime/instance.rs b/crates/wasmtime/src/runtime/instance.rs index d852912268..7b04d5b594 100644 --- a/crates/wasmtime/src/runtime/instance.rs +++ b/crates/wasmtime/src/runtime/instance.rs @@ -933,7 +933,7 @@ fn pre_instantiate_raw( } #[cfg(feature = "rr")] - if module.engine().rr().is_some() + if module.engine().is_recording() && module.exports().any(|export| { use crate::ExternType; if let ExternType::Memory(_) = export.ty() { @@ -943,7 +943,9 @@ fn pre_instantiate_raw( } }) { - bail!("Cannot support record/replay for core wasm modules when a memory is exported"); + bail!( + "Cannot support recording for core wasm modules when a memory is exported; consider using components" + ); } Ok(imports) diff --git a/crates/wasmtime/src/runtime/rr/io.rs b/crates/wasmtime/src/runtime/rr/io.rs index c59e54a64d..5f8e49e57f 100644 --- a/crates/wasmtime/src/runtime/rr/io.rs +++ b/crates/wasmtime/src/runtime/rr/io.rs @@ -36,7 +36,7 @@ cfg_if::cfg_if! { /// Serialize and write `value` to a `RecordWriter` /// /// Currently uses `postcard` serializer -pub fn to_record_writer(value: &T, writer: W) -> Result<()> +pub(super) fn to_record_writer(value: &T, writer: W) -> Result<()> where T: Serialize + ?Sized, W: RecordWriter, @@ -55,7 +55,7 @@ where /// /// 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 +pub(super) fn from_replay_reader<'a, T, R>(reader: R, scratch: &'a mut [u8]) -> Result where T: Deserialize<'a>, R: ReplayReader + 'a, diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index 596e56d656..f74a5d879e 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -118,13 +118,14 @@ rr_event! { ComponentBuiltinReturn(__component_events::BuiltinReturnEvent), // 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 a Wasm component function back to host ComponentWasmFuncReturn(__component_events::WasmFuncReturnEvent), /// Return from Component ABI realloc call + /// + /// Since realloc is deterministic, ReallocReturn is optional. + /// Any error is subsumed by the containing LowerReturn/LowerStoreReturn + /// that triggered realloc ComponentReallocReturn(__component_events::ReallocReturnEvent), /// Call into host function from component ComponentHostFuncEntry(__component_events::HostFuncEntryEvent), @@ -193,7 +194,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, settings: RecordSettings) -> Result + fn new_recorder(writer: impl RecordWriter + 'static, settings: RecordSettings) -> Result where Self: Sized; @@ -242,19 +243,9 @@ pub trait Replayer: Iterator { Self: Sized; /// Get settings associated with the replay process - #[allow( - unused, - reason = "currently used only for validation resulting in \ - many unnecessary feature gates. will expand in the future to more features and this attribute can be removed" - )] fn settings(&self) -> &ReplaySettings; /// Get the settings (embedded within the trace) during recording - #[allow( - unused, - reason = "currently used only for validation resulting in \ - many unnecessary feature gates. will expand in the future to more features and this attribute can be removed" - )] fn trace_settings(&self) -> &RecordSettings; // Provided Methods @@ -364,13 +355,16 @@ impl Drop for RecordBuffer { } impl Recorder for RecordBuffer { - fn new_recorder(mut writer: Box, settings: RecordSettings) -> Result { + fn new_recorder( + mut writer: impl RecordWriter + 'static, + 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(&settings, &mut writer)?; Ok(RecordBuffer { buf: Vec::new(), - writer: writer, + writer: Box::new(writer), settings: settings, }) } @@ -405,18 +399,8 @@ pub struct ReplayBuffer { /// Reader to read replay trace from reader: Box, /// Settings in replay configuration - #[allow( - unused, - reason = "currently used only for validation resulting in \ - many unnecessary feature gates. will expand in the future to more features and this attribute can be removed" - )] settings: ReplaySettings, /// Settings for record configuration (encoded in the trace) - #[allow( - unused, - reason = "currently used only for validation resulting in \ - many unnecessary feature gates. will expand in the future to more features and this attribute can be removed" - )] trace_settings: RecordSettings, /// Intermediate static buffer for deserialization deser_buffer: Vec, @@ -454,7 +438,8 @@ impl Drop for ReplayBuffer { if let RREvent::Eof = event { } else { log::warn!( - "Replay buffer is dropped with {} remaining events, and is likely an invalid execution", + "Replay buffer is dropped with {} remaining events, + and is likely an invalid/incomplete execution", self.count() ); } diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index 18d486951e..efdc9f27c5 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -76,6 +76,8 @@ //! contents of `StoreOpaque`. This is an invariant that we, as the authors of //! `wasmtime`, must uphold for the public interface to be safe. +#[cfg(feature = "rr")] +use crate::RecordSettings; use crate::RootSet; #[cfg(feature = "component-model-async")] use crate::component::ComponentStoreData; @@ -88,7 +90,9 @@ use crate::prelude::*; #[cfg(feature = "rr-validate")] use crate::rr::Validate; #[cfg(feature = "rr")] -use crate::rr::{RREvent, RecordBuffer, Recorder, ReplayBuffer, ReplayError, Replayer}; +use crate::rr::{ + RREvent, RecordBuffer, RecordWriter, Recorder, ReplayBuffer, ReplayError, Replayer, +}; #[cfg(feature = "gc")] use crate::runtime::vm::GcRootsList; #[cfg(feature = "stack-switching")] @@ -617,29 +621,9 @@ impl Store { #[cfg(feature = "component-model-async")] concurrent_async_state: Default::default(), #[cfg(feature = "rr")] - record_buffer: engine.rr().and_then(|v| { - v.record().and_then(|record| { - Some( - RecordBuffer::new_recorder( - (record.writer_initializer)(), - record.settings.clone(), - ) - .unwrap(), - ) - }) - }), + record_buffer: None, #[cfg(feature = "rr")] - replay_buffer: engine.rr().and_then(|v| { - v.replay().and_then(|replay| { - Some( - ReplayBuffer::new_replayer( - (replay.reader_initializer)(), - replay.settings.clone(), - ) - .unwrap(), - ) - }) - }), + replay_buffer: None, }; let mut inner = Box::new(StoreInner { inner, @@ -1028,6 +1012,20 @@ impl Store { ) { self.inner.epoch_deadline_callback(Box::new(callback)); } + + /// Configure a [`Store`] to enable execution recording + /// + /// This feature must be initialized before instantiating any module within + /// the Store. Recording of events is performed according to provided settings, and + /// written to the provided writer. + #[cfg(feature = "rr")] + pub fn init_recording( + &mut self, + recorder: impl RecordWriter + 'static, + settings: RecordSettings, + ) -> Result<()> { + self.inner.init_recording(recorder, settings) + } } impl<'a, T> StoreContext<'a, T> { @@ -1458,6 +1456,24 @@ impl StoreOpaque { &mut self.vm_store_context } + #[cfg(feature = "rr")] + pub fn init_recording( + &mut self, + recorder: impl RecordWriter + 'static, + settings: RecordSettings, + ) -> Result<()> { + ensure!( + self.instance_count == 0, + "store recording must be initialized before instantiating any modules or components" + ); + ensure!( + self.engine().is_recording(), + "store recording requires recording enabled on config" + ); + self.record_buffer = Some(RecordBuffer::new_recorder(recorder, settings)?); + Ok(()) + } + #[cfg(feature = "rr")] #[inline(always)] pub fn record_buffer_mut(&mut self) -> Option<&mut RecordBuffer> { From ebd1b380c5c9c6e152266a51a7dfd28c50c9282c Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Sat, 4 Oct 2025 06:44:10 +0530 Subject: [PATCH 13/73] Fix: host func return rr placement for call_host_dynamic --- crates/wasmtime/src/runtime/component/func/host.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 0ffc311197..6b59868877 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -943,9 +943,9 @@ where *ty, offset, )?; - // Recording here is just for marking the return event - rr_hooks::component_hooks::record_host_func_return(&[], cx.store.0)?; } + // Recording here is just for marking the return event + rr_hooks::component_hooks::record_host_func_return(&[], cx.store.0)?; } unsafe { From d6adc43c8856c62011188c3e50585377ce08432a Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Sun, 5 Oct 2025 07:30:43 +0530 Subject: [PATCH 14/73] Support new recording API on CLI --- crates/cli-flags/src/lib.rs | 25 ++----------- crates/wasmtime/src/config.rs | 53 +-------------------------- crates/wasmtime/src/runtime.rs | 2 +- crates/wasmtime/src/runtime/rr/mod.rs | 22 ++++++++++- crates/wasmtime/src/runtime/store.rs | 5 +-- src/commands/run.rs | 27 +++++++++++++- 6 files changed, 54 insertions(+), 80 deletions(-) diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs index 42c1072876..17319ff34e 100644 --- a/crates/cli-flags/src/lib.rs +++ b/crates/cli-flags/src/lib.rs @@ -1020,33 +1020,14 @@ impl CommonOptions { let record = &self.record; match_feature! { - ["rr" : record.path.clone()] - path => { - use std::{io, sync::Arc}; - use wasmtime::{RecordConfig, RecordSettings}; - let default_settings = RecordSettings::default(); + ["rr" : &record.path] + _path => { match_feature! { ["rr-validate": record.validation_metadata] _v => (), _ => err, } - config.enable_record(RecordConfig { - writer_initializer: Arc::new(move || { - if path.trim().is_empty() { - Box::new(io::sink()) - } else { - Box::new(io::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), - }, - })? + config.recording(true); }, _ => err, } diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 78e7d6f832..2ea09837a6 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -3,8 +3,6 @@ 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; use wasmparser::WasmFeatures; @@ -27,7 +25,7 @@ use crate::stack::{StackCreator, StackCreatorProxy}; use wasmtime_fiber::RuntimeFiberStackCreator; #[cfg(feature = "rr")] -use crate::rr::{RecordWriter, ReplayReader}; +use crate::rr::ReplayReader; #[cfg(feature = "runtime")] pub use crate::runtime::code_memory::CustomCodeMemory; #[cfg(feature = "cache")] @@ -236,36 +234,6 @@ impl Default for CompilerConfig { } } -/// Settings for execution recording. -#[cfg(feature = "rr")] -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RecordSettings { - /// Flag to include additional signatures for replay validation. - pub add_validation: bool, - /// Maximum window size of internal event buffer. - pub event_window_size: usize, -} - -#[cfg(feature = "rr")] -impl Default for RecordSettings { - fn default() -> Self { - Self { - add_validation: false, - event_window_size: 16, - } - } -} - -/// Configuration for recording execution. -#[cfg(feature = "rr")] -#[derive(Clone)] -pub struct RecordConfig { - /// Closure that generates a writer for recording execution traces. - pub writer_initializer: Arc Box + Send + Sync>, - /// Associated metadata for configuring the recording strategy. - pub settings: RecordSettings, -} - /// Settings for execution replay. #[cfg(feature = "rr")] #[derive(Debug, Clone)] @@ -300,19 +268,10 @@ pub struct ReplayConfig { #[cfg(feature = "rr")] #[derive(Clone)] pub enum RRConfig { - /// Record configuration. - Record(RecordConfig), /// 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 { @@ -322,22 +281,12 @@ impl From for RRConfig { #[cfg(feature = "rr")] 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, - } - } /// 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, } } } diff --git a/crates/wasmtime/src/runtime.rs b/crates/wasmtime/src/runtime.rs index 09d4df18fc..f8d25dec01 100644 --- a/crates/wasmtime/src/runtime.rs +++ b/crates/wasmtime/src/runtime.rs @@ -89,7 +89,7 @@ pub use memory::*; pub use module::{Module, ModuleExport}; pub use resources::*; #[cfg(feature = "rr")] -pub use rr::{RecordWriter, ReplayReader}; +pub use rr::{RecordSettings, RecordWriter, ReplayReader}; #[cfg(all(feature = "async", feature = "call-hook"))] pub use store::CallHookHandler; pub use store::{ diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/mod.rs index f74a5d879e..4f121b0093 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/mod.rs @@ -7,7 +7,7 @@ //! //! This module does NOT support RR for component builtins yet. -use crate::config::{ModuleVersionStrategy, RecordSettings, ReplaySettings}; +use crate::config::{ModuleVersionStrategy, ReplaySettings}; use crate::prelude::*; use core::fmt; use events::EventActionError; @@ -25,6 +25,26 @@ use events::{common_events, component_events as __component_events}; pub use events::{core_events, marker_events}; pub use io::{RecordWriter, ReplayReader}; +/// Settings for execution recording. +#[cfg(feature = "rr")] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RecordSettings { + /// Flag to include additional signatures for replay validation. + pub add_validation: bool, + /// Maximum window size of internal event buffer. + pub event_window_size: usize, +} + +#[cfg(feature = "rr")] +impl Default for RecordSettings { + fn default() -> Self { + Self { + add_validation: false, + event_window_size: 16, + } + } +} + /// Encapsulation of event types comprising an [`RREvent`] sum type mod events; /// I/O support for reading and writing traces diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index efdc9f27c5..fc05090f50 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -76,8 +76,6 @@ //! contents of `StoreOpaque`. This is an invariant that we, as the authors of //! `wasmtime`, must uphold for the public interface to be safe. -#[cfg(feature = "rr")] -use crate::RecordSettings; use crate::RootSet; #[cfg(feature = "component-model-async")] use crate::component::ComponentStoreData; @@ -91,7 +89,8 @@ use crate::prelude::*; use crate::rr::Validate; #[cfg(feature = "rr")] use crate::rr::{ - RREvent, RecordBuffer, RecordWriter, Recorder, ReplayBuffer, ReplayError, Replayer, + RREvent, RecordBuffer, RecordSettings, RecordWriter, Recorder, ReplayBuffer, ReplayError, + Replayer, }; #[cfg(feature = "gc")] use crate::runtime::vm::GcRootsList; diff --git a/src/commands/run.rs b/src/commands/run.rs index 0188e19936..7456fabc02 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -18,6 +18,10 @@ use wasmtime::ReplayConfig; use wasmtime::{Engine, Func, Module, Store, StoreLimits, Val, ValType}; use wasmtime_wasi::{WasiCtxView, WasiView}; +#[cfg(feature = "rr")] +use std::{fs, io}; +#[cfg(feature = "rr")] +use wasmtime::RecordSettings; #[cfg(feature = "wasi-config")] use wasmtime_wasi_config::{WasiConfig, WasiConfigVariables}; #[cfg(feature = "wasi-http")] @@ -112,7 +116,7 @@ impl RunCommand { #[cfg(feature = "rr")] if let Some(cfg) = replay_cfg { - config.enable_replay(cfg)?; + //config.enable_replay(cfg)?; } let engine = Engine::new(&config)?; @@ -172,6 +176,27 @@ impl RunCommand { store.set_fuel(fuel)?; } + #[cfg(feature = "rr")] + { + let record = &self.run.common.record; + if let Some(path) = &record.path { + let default_settings = RecordSettings::default(); + let 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), + }; + if path.trim().is_empty() { + store.init_recording(io::sink(), settings)?; + } else { + store.init_recording(fs::File::create(&path)?, settings)?; + } + } + } + // Always run the module asynchronously to ensure that the module can be // interrupted, even if it is blocking on I/O or a timeout or something. let runtime = tokio::runtime::Builder::new_multi_thread() From 3ba98494d4273e9926bd4dec50b4a2d92fa688fd Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Sun, 5 Oct 2025 12:11:02 +0530 Subject: [PATCH 15/73] Reorganize RR module with hooks --- crates/wasmtime/src/runtime.rs | 1 - crates/wasmtime/src/runtime/component/func.rs | 2 +- .../src/runtime/component/func/host.rs | 24 +++++++++---------- .../src/runtime/component/func/options.rs | 4 ++-- .../src/runtime/component/func/typed.rs | 2 +- crates/wasmtime/src/runtime/func.rs | 8 +++---- crates/wasmtime/src/runtime/rr.rs | 13 ++++++++++ .../src/runtime/rr/{mod.rs => core.rs} | 9 ------- .../rr/{events/mod.rs => core/events.rs} | 0 .../rr/{ => core}/events/common_events.rs | 0 .../rr/{ => core}/events/component_events.rs | 0 .../rr/{ => core}/events/core_events.rs | 0 .../wasmtime/src/runtime/rr/{ => core}/io.rs | 0 .../runtime/{rr_hooks/mod.rs => rr/hooks.rs} | 1 - .../{rr_hooks => rr/hooks}/component_hooks.rs | 0 .../{rr_hooks => rr/hooks}/core_hooks.rs | 0 16 files changed, 33 insertions(+), 31 deletions(-) create mode 100644 crates/wasmtime/src/runtime/rr.rs rename crates/wasmtime/src/runtime/rr/{mod.rs => core.rs} (98%) rename crates/wasmtime/src/runtime/rr/{events/mod.rs => core/events.rs} (100%) rename crates/wasmtime/src/runtime/rr/{ => core}/events/common_events.rs (100%) rename crates/wasmtime/src/runtime/rr/{ => core}/events/component_events.rs (100%) rename crates/wasmtime/src/runtime/rr/{ => core}/events/core_events.rs (100%) rename crates/wasmtime/src/runtime/rr/{ => core}/io.rs (100%) rename crates/wasmtime/src/runtime/{rr_hooks/mod.rs => rr/hooks.rs} (97%) rename crates/wasmtime/src/runtime/{rr_hooks => rr/hooks}/component_hooks.rs (100%) rename crates/wasmtime/src/runtime/{rr_hooks => rr/hooks}/core_hooks.rs (100%) diff --git a/crates/wasmtime/src/runtime.rs b/crates/wasmtime/src/runtime.rs index f8d25dec01..94fb504eb1 100644 --- a/crates/wasmtime/src/runtime.rs +++ b/crates/wasmtime/src/runtime.rs @@ -49,7 +49,6 @@ pub(crate) mod memory; pub(crate) mod module; pub(crate) mod resources; pub(crate) mod rr; -pub(crate) mod rr_hooks; pub(crate) mod store; pub(crate) mod trampoline; pub(crate) mod trap; diff --git a/crates/wasmtime/src/runtime/component/func.rs b/crates/wasmtime/src/runtime/component/func.rs index 39b06f30f2..794d0a9933 100644 --- a/crates/wasmtime/src/runtime/component/func.rs +++ b/crates/wasmtime/src/runtime/component/func.rs @@ -4,7 +4,7 @@ use crate::component::storage::storage_as_slice; use crate::component::types::Type; use crate::component::values::Val; use crate::prelude::*; -use crate::rr_hooks::component_hooks; +use crate::rr::component_hooks; use crate::runtime::vm::component::{ComponentInstance, InstanceFlags, ResourceTables}; use crate::runtime::vm::{Export, VMFuncRef}; use crate::store::StoreOpaque; diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 6b59868877..3442c93d32 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -5,9 +5,9 @@ use crate::component::matching::InstanceType; use crate::component::storage::{slice_to_storage_mut, storage_as_slice_mut}; use crate::component::{ComponentNamedList, ComponentType, Instance, Lift, Lower, Val}; use crate::prelude::*; -use crate::rr_hooks; +use crate::rr; #[cfg(feature = "rr-component")] -use crate::rr_hooks::component_hooks::ReplayLoweringPhase; +use crate::rr::component_hooks::ReplayLoweringPhase; use crate::runtime::vm::component::{ ComponentInstance, VMComponentContext, VMLowering, VMLoweringCallee, }; @@ -257,7 +257,7 @@ where let types = vminstance.component().types().clone(); - rr_hooks::component_hooks::record_replay_host_func_entry(storage, &ty, store.0)?; + rr::component_hooks::record_replay_host_func_entry(storage, &ty, store.0)?; let ty = &types[ty]; let param_tys = InterfaceType::Tuple(ty.params); @@ -621,12 +621,12 @@ where ) -> Result<()> { match self.lower_dst() { Dst::Direct(storage) => { - let result = rr_hooks::component_hooks::record_lower_flat( + let result = rr::component_hooks::record_lower_flat( |cx, ty| ret.linear_lower_to_flat(cx, ty, storage), cx, ty, ); - rr_hooks::component_hooks::record_host_func_return( + rr::component_hooks::record_host_func_return( unsafe { storage_as_slice_mut(storage) }, cx.store.0, )?; @@ -634,14 +634,14 @@ where } Dst::Indirect(ptr) => { let ptr = validate_inbounds::(cx.as_slice(), ptr)?; - let result = rr_hooks::component_hooks::record_lower_memory( + let result = rr::component_hooks::record_lower_memory( |cx, ty, ptr| ret.linear_lower_to_memory(cx, ty, ptr), cx, ty, ptr, ); // Recording here is just for marking the return event - rr_hooks::component_hooks::record_host_func_return(&[], cx.store.0)?; + rr::component_hooks::record_host_func_return(&[], cx.store.0)?; result } } @@ -811,7 +811,7 @@ where let types = instance.id().get(store.0).component().types().clone(); - rr_hooks::component_hooks::record_replay_host_func_entry(storage, &ty, store.0)?; + rr::component_hooks::record_replay_host_func_entry(storage, &ty, store.0)?; let func_ty = &types[ty]; let param_tys = &types[func_ty.params]; @@ -924,20 +924,20 @@ 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()) { - rr_hooks::component_hooks::record_lower_flat( + rr::component_hooks::record_lower_flat( |cx, ty| val.lower(cx, ty, &mut dst), &mut cx, *ty, )?; } assert!(dst.next().is_none()); - rr_hooks::component_hooks::record_host_func_return(storage, cx.store.0)?; + rr::component_hooks::record_host_func_return(storage, cx.store.0)?; } else { let ret_ptr = unsafe { 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); - rr_hooks::component_hooks::record_lower_memory( + rr::component_hooks::record_lower_memory( |cx, ty, ptr| val.store(cx, ty, ptr), &mut cx, *ty, @@ -945,7 +945,7 @@ where )?; } // Recording here is just for marking the return event - rr_hooks::component_hooks::record_host_func_return(&[], cx.store.0)?; + rr::component_hooks::record_host_func_return(&[], cx.store.0)?; } unsafe { diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index c39735ce2a..ce24d07f61 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -11,8 +11,8 @@ use crate::rr::{ #[cfg(all(feature = "rr-component", feature = "rr-validate"))] use crate::rr::{Validate, component_events::ReallocReturnEvent}; #[cfg(feature = "rr-component")] -use crate::rr_hooks::component_hooks::ReplayLoweringPhase; -use crate::rr_hooks::{ConstMemorySliceCell, MemorySliceCell}; +use crate::rr::component_hooks::ReplayLoweringPhase; +use crate::rr::{ConstMemorySliceCell, MemorySliceCell}; use crate::runtime::vm::component::{ CallContexts, ComponentInstance, InstanceFlags, ResourceTable, ResourceTables, }; diff --git a/crates/wasmtime/src/runtime/component/func/typed.rs b/crates/wasmtime/src/runtime/component/func/typed.rs index 3e7b059cf5..f8902f622c 100644 --- a/crates/wasmtime/src/runtime/component/func/typed.rs +++ b/crates/wasmtime/src/runtime/component/func/typed.rs @@ -3,7 +3,7 @@ use crate::component::func::{Func, LiftContext, LowerContext, Options}; use crate::component::matching::InstanceType; use crate::component::storage::{storage_as_slice, storage_as_slice_mut}; use crate::prelude::*; -use crate::rr_hooks::component_hooks; +use crate::rr::component_hooks; use crate::{AsContextMut, StoreContext, StoreContextMut, ValRaw}; use alloc::borrow::Cow; use core::fmt; diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index e45c9f72b6..149e7d401b 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_hooks; +use crate::rr; use crate::runtime::Uninhabited; use crate::runtime::vm::{ InterpreterRef, SendSyncPtr, StoreBox, VMArrayCallHostFuncContext, VMCommonStackInformation, @@ -2378,7 +2378,7 @@ impl HostContext { // 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::core_hooks::record_replay_host_func_entry( + rr::core_hooks::record_replay_host_func_entry( unsafe { &args.as_ref()[..num_params] }, &func_type_index, caller.store.0, @@ -2423,14 +2423,14 @@ impl HostContext { unsafe { ret.store(&mut store, args.as_mut())? }; } // Record the return values - rr_hooks::core_hooks::record_host_func_return( + rr::core_hooks::record_host_func_return( unsafe { &args.as_ref()[..num_results] }, &func_type_index, caller.store.0, )?; } else { // Replay the return values - rr_hooks::core_hooks::replay_host_func_return( + rr::core_hooks::replay_host_func_return( unsafe { &mut args.as_mut()[..num_results] }, &func_type_index, caller.store.0, diff --git a/crates/wasmtime/src/runtime/rr.rs b/crates/wasmtime/src/runtime/rr.rs new file mode 100644 index 0000000000..79f730ce36 --- /dev/null +++ b/crates/wasmtime/src/runtime/rr.rs @@ -0,0 +1,13 @@ +//! Wasmtime's Record and Replay support. +//! +//! This feature is currently not optimized and under development + +/// Convenience method hooks for injecting event recording/replaying in the rest of the engine +mod hooks; +pub use hooks::{ConstMemorySliceCell, MemorySliceCell, component_hooks, core_hooks}; + +#[cfg(feature = "rr")] +/// Core infrastructure for RR support +mod core; +#[cfg(feature = "rr")] +pub use core::*; diff --git a/crates/wasmtime/src/runtime/rr/mod.rs b/crates/wasmtime/src/runtime/rr/core.rs similarity index 98% rename from crates/wasmtime/src/runtime/rr/mod.rs rename to crates/wasmtime/src/runtime/rr/core.rs index 4f121b0093..39bbc8c44d 100644 --- a/crates/wasmtime/src/runtime/rr/mod.rs +++ b/crates/wasmtime/src/runtime/rr/core.rs @@ -1,12 +1,3 @@ -#![cfg(feature = "rr")] -//! Wasmtime's Record and Replay support. -//! -//! This feature is currently not optimized and under development -//! -//! ## Notes -//! -//! This module does NOT support RR for component builtins yet. - use crate::config::{ModuleVersionStrategy, ReplaySettings}; use crate::prelude::*; use core::fmt; diff --git a/crates/wasmtime/src/runtime/rr/events/mod.rs b/crates/wasmtime/src/runtime/rr/core/events.rs similarity index 100% rename from crates/wasmtime/src/runtime/rr/events/mod.rs rename to crates/wasmtime/src/runtime/rr/core/events.rs diff --git a/crates/wasmtime/src/runtime/rr/events/common_events.rs b/crates/wasmtime/src/runtime/rr/core/events/common_events.rs similarity index 100% rename from crates/wasmtime/src/runtime/rr/events/common_events.rs rename to crates/wasmtime/src/runtime/rr/core/events/common_events.rs diff --git a/crates/wasmtime/src/runtime/rr/events/component_events.rs b/crates/wasmtime/src/runtime/rr/core/events/component_events.rs similarity index 100% rename from crates/wasmtime/src/runtime/rr/events/component_events.rs rename to crates/wasmtime/src/runtime/rr/core/events/component_events.rs diff --git a/crates/wasmtime/src/runtime/rr/events/core_events.rs b/crates/wasmtime/src/runtime/rr/core/events/core_events.rs similarity index 100% rename from crates/wasmtime/src/runtime/rr/events/core_events.rs rename to crates/wasmtime/src/runtime/rr/core/events/core_events.rs diff --git a/crates/wasmtime/src/runtime/rr/io.rs b/crates/wasmtime/src/runtime/rr/core/io.rs similarity index 100% rename from crates/wasmtime/src/runtime/rr/io.rs rename to crates/wasmtime/src/runtime/rr/core/io.rs diff --git a/crates/wasmtime/src/runtime/rr_hooks/mod.rs b/crates/wasmtime/src/runtime/rr/hooks.rs similarity index 97% rename from crates/wasmtime/src/runtime/rr_hooks/mod.rs rename to crates/wasmtime/src/runtime/rr/hooks.rs index 34cb26226a..a63269d06a 100644 --- a/crates/wasmtime/src/runtime/rr_hooks/mod.rs +++ b/crates/wasmtime/src/runtime/rr/hooks.rs @@ -1,4 +1,3 @@ -//! Convenience methods for hooking in RR event recording/replaying to the rest of the engine #[cfg(feature = "rr-component")] use crate::rr::{RecordBuffer, Recorder, component_events::MemorySliceWriteEvent}; diff --git a/crates/wasmtime/src/runtime/rr_hooks/component_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs similarity index 100% rename from crates/wasmtime/src/runtime/rr_hooks/component_hooks.rs rename to crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs diff --git a/crates/wasmtime/src/runtime/rr_hooks/core_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs similarity index 100% rename from crates/wasmtime/src/runtime/rr_hooks/core_hooks.rs rename to crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs From bc16bc798fca2a9121e7b6dbf19c6b97f458a150 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 28 Oct 2025 13:36:37 +0100 Subject: [PATCH 16/73] Temporary patch: Replay API in-store; separate into independent driver in future --- crates/wasmtime/src/config.rs | 81 +++++-------------- crates/wasmtime/src/runtime.rs | 2 +- crates/wasmtime/src/runtime/rr/core.rs | 30 ++++++- crates/wasmtime/src/runtime/rr/core/events.rs | 1 + .../src/runtime/rr/hooks/component_hooks.rs | 8 +- crates/wasmtime/src/runtime/store.rs | 34 +++++++- src/commands/replay.rs | 23 ++---- src/commands/run.rs | 26 ++++-- 8 files changed, 113 insertions(+), 92 deletions(-) diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 2ea09837a6..e193acc33e 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -24,8 +24,6 @@ use crate::stack::{StackCreator, StackCreatorProxy}; #[cfg(feature = "async")] use wasmtime_fiber::RuntimeFiberStackCreator; -#[cfg(feature = "rr")] -use crate::rr::ReplayReader; #[cfg(feature = "runtime")] pub use crate::runtime::code_memory::CustomCodeMemory; #[cfg(feature = "cache")] @@ -178,6 +176,8 @@ pub struct Config { pub(crate) detect_host_feature: Option Option>, #[cfg(feature = "rr")] pub(crate) record_support: bool, + #[cfg(feature = "rr")] + pub(crate) replay_support: bool, } /// User-provided configuration for the compiler. @@ -234,63 +234,6 @@ impl Default for CompilerConfig { } } -/// Settings for execution replay. -#[cfg(feature = "rr")] -#[derive(Debug, Clone)] -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: false, - deser_buffer_size: 64, - } - } -} - -/// Configuration for replay execution. -#[cfg(feature = "rr")] -#[derive(Clone)] -pub struct ReplayConfig { - /// 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 settings: ReplaySettings, -} - -/// Configurations for record/replay (RR) executions. -#[cfg(feature = "rr")] -#[derive(Clone)] -pub enum RRConfig { - /// Replay configuration. - Replay(ReplayConfig), -} - -#[cfg(feature = "rr")] -impl From for RRConfig { - fn from(value: ReplayConfig) -> Self { - Self::Replay(value) - } -} - -#[cfg(feature = "rr")] -impl RRConfig { - /// Obtain the replay configuration. - /// - /// Return [`None`] if it is not configured. - pub fn replay(&self) -> Option<&ReplayConfig> { - match self { - Self::Replay(r) => Some(r), - } - } -} - impl Config { /// Creates a new configuration object with the default configuration /// specified. @@ -345,6 +288,8 @@ impl Config { detect_host_feature: None, #[cfg(feature = "rr")] record_support: false, + #[cfg(feature = "rr")] + replay_support: false, }; #[cfg(any(feature = "cranelift", feature = "winch"))] { @@ -2799,6 +2744,22 @@ impl Config { self } + /// Enable execution trace replaying with the provided configuration. + /// + /// This method implicitly enforces determinism (see [`Config::enforce_determinism`] + /// for details). + #[cfg(feature = "rr")] + #[inline] + pub fn replaying(&mut self, enable: bool) -> &mut Self { + if enable { + self.enforce_determinism(); + } else { + self.remove_determinism_enforcement(); + } + self.replay_support = enable; + self + } + /// Evaluates to true if current configuration must respect /// deterministic execution in its configuration. /// @@ -2806,7 +2767,7 @@ impl Config { #[cfg(feature = "rr")] #[inline] pub fn is_determinism_enforced(&mut self) -> bool { - self.record_support + self.record_support || self.replay_support } } diff --git a/crates/wasmtime/src/runtime.rs b/crates/wasmtime/src/runtime.rs index 94fb504eb1..0a6482687b 100644 --- a/crates/wasmtime/src/runtime.rs +++ b/crates/wasmtime/src/runtime.rs @@ -88,7 +88,7 @@ pub use memory::*; pub use module::{Module, ModuleExport}; pub use resources::*; #[cfg(feature = "rr")] -pub use rr::{RecordSettings, RecordWriter, ReplayReader}; +pub use rr::{RecordSettings, RecordWriter, ReplayReader, ReplaySettings}; #[cfg(all(feature = "async", feature = "call-hook"))] pub use store::CallHookHandler; pub use store::{ diff --git a/crates/wasmtime/src/runtime/rr/core.rs b/crates/wasmtime/src/runtime/rr/core.rs index 39bbc8c44d..3fb1efa813 100644 --- a/crates/wasmtime/src/runtime/rr/core.rs +++ b/crates/wasmtime/src/runtime/rr/core.rs @@ -1,4 +1,4 @@ -use crate::config::{ModuleVersionStrategy, ReplaySettings}; +use crate::config::ModuleVersionStrategy; use crate::prelude::*; use core::fmt; use events::EventActionError; @@ -36,6 +36,26 @@ impl Default for RecordSettings { } } +/// Settings for execution replay. +#[cfg(feature = "rr")] +#[derive(Debug, Clone, Serialize, Deserialize)] +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: false, + deser_buffer_size: 64, + } + } +} + /// Encapsulation of event types comprising an [`RREvent`] sum type mod events; /// I/O support for reading and writing traces @@ -249,7 +269,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(reader: Box, settings: ReplaySettings) -> Result + fn new_replayer(reader: impl ReplayReader + 'static, settings: ReplaySettings) -> Result where Self: Sized; @@ -459,7 +479,10 @@ impl Drop for ReplayBuffer { } impl Replayer for ReplayBuffer { - fn new_replayer(mut reader: Box, settings: ReplaySettings) -> Result { + fn new_replayer( + mut reader: impl ReplayReader + 'static, + settings: ReplaySettings, + ) -> Result { let mut scratch = [0u8; 12]; // Ensure module versions match let version = io::from_replay_reader::<&str, _>(&mut reader, &mut scratch)?; @@ -479,6 +502,7 @@ impl Replayer for ReplayBuffer { } let deser_buffer = vec![0; settings.deser_buffer_size]; + let reader = Box::new(reader); Ok(ReplayBuffer { reader, diff --git a/crates/wasmtime/src/runtime/rr/core/events.rs b/crates/wasmtime/src/runtime/rr/core/events.rs index 3d376088c9..275b2b68ec 100644 --- a/crates/wasmtime/src/runtime/rr/core/events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events.rs @@ -132,6 +132,7 @@ where if self == expect { Ok(()) } else { + log::error!("Validation against {:?} failed!", expect); Err(ReplayError::FailedValidation) } } diff --git a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs index c880490901..dfe87bacf8 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs @@ -47,9 +47,11 @@ where store .0 .record_event_validation(|| WasmFuncReturnEvent::from_anyhow_result(&result))?; - store - .0 - .next_replay_event_validation::>(&result)?; + // TODO: After adding validation support, replay with `next_replay_event_validation` + store.0.next_replay_event_and(|_r: WasmFuncReturnEvent| { + log::warn!("Yet to implement validation for WasmFuncReturnEvent; skipping for now"); + Ok(()) + })?; result?; return Ok(()); } diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index fc05090f50..08f99812f6 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -90,7 +90,7 @@ use crate::rr::Validate; #[cfg(feature = "rr")] use crate::rr::{ RREvent, RecordBuffer, RecordSettings, RecordWriter, Recorder, ReplayBuffer, ReplayError, - Replayer, + ReplayReader, ReplaySettings, Replayer, }; #[cfg(feature = "gc")] use crate::runtime::vm::GcRootsList; @@ -1025,6 +1025,20 @@ impl Store { ) -> Result<()> { self.inner.init_recording(recorder, settings) } + + /// Configure a [`Store`] to enable execution replaying + /// + /// This feature must be initialized before instantiating any module within + /// the Store. Replay of events is performed according to provided settings, and + /// read from the provided reader. + #[cfg(feature = "rr")] + pub fn init_replaying( + &mut self, + replayer: impl ReplayReader + 'static, + settings: ReplaySettings, + ) -> Result<()> { + self.inner.init_replaying(replayer, settings) + } } impl<'a, T> StoreContext<'a, T> { @@ -1473,6 +1487,24 @@ impl StoreOpaque { Ok(()) } + #[cfg(feature = "rr")] + pub fn init_replaying( + &mut self, + replayer: impl ReplayReader + 'static, + settings: ReplaySettings, + ) -> Result<()> { + ensure!( + self.instance_count == 0, + "replaying store must not initialize any modules" + ); + ensure!( + !self.engine().is_recording(), + "store recording cannot be enabled when initializing replay" + ); + self.replay_buffer = Some(ReplayBuffer::new_replayer(replayer, settings)?); + Ok(()) + } + #[cfg(feature = "rr")] #[inline(always)] pub fn record_buffer_mut(&mut self) -> Option<&mut RecordBuffer> { diff --git a/src/commands/replay.rs b/src/commands/replay.rs index 9bd9aeb83f..e8e02c7d62 100644 --- a/src/commands/replay.rs +++ b/src/commands/replay.rs @@ -3,8 +3,7 @@ use crate::commands::run::RunCommand; use anyhow::Result; use clap::Parser; -use std::{fs, io::BufReader, path::PathBuf, sync::Arc}; -use wasmtime::{ReplayConfig, ReplaySettings}; +use std::path::PathBuf; #[derive(Parser)] /// Replay-specific options for CLI @@ -16,19 +15,19 @@ pub struct ReplayOptions { /// /// Note: The module used for replay must exactly match that used during recording #[arg(short, long, required = true, value_name = "RECORDED TRACE")] - trace: PathBuf, + pub 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, + pub 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, + pub deser_buffer_size: usize, } /// Execute a deterministic, embedding-agnostic replay of a Wasm modules given its associated recorded trace @@ -48,19 +47,7 @@ impl ReplayCommand { 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( - fs::File::open(&self.replay_opts.trace).unwrap(), - )) - }), - settings: ReplaySettings { - validate: self.replay_opts.validate, - deser_buffer_size: self.replay_opts.deser_buffer_size, - ..Default::default() - }, - }; // Replay uses the `run` command harness - self.run_cmd.execute(Some(replay_cfg)) + self.run_cmd.execute(Some(self.replay_opts)) } } diff --git a/src/commands/run.rs b/src/commands/run.rs index 7456fabc02..3ae205bf5f 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -5,23 +5,25 @@ allow(irrefutable_let_patterns, unreachable_patterns) )] +#[cfg(feature = "rr")] +use crate::commands::ReplayOptions; use crate::common::{Profile, RunCommon, RunTarget}; use anyhow::{Context as _, Error, Result, anyhow, bail}; use clap::Parser; use std::ffi::OsString; +#[cfg(feature = "rr")] +use std::io::BufReader; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::thread; use wasi_common::sync::{Dir, TcpListener, WasiCtxBuilder, ambient_authority}; -#[cfg(feature = "rr")] -use wasmtime::ReplayConfig; use wasmtime::{Engine, Func, Module, Store, StoreLimits, Val, ValType}; use wasmtime_wasi::{WasiCtxView, WasiView}; #[cfg(feature = "rr")] use std::{fs, io}; #[cfg(feature = "rr")] -use wasmtime::RecordSettings; +use wasmtime::{RecordSettings, ReplaySettings}; #[cfg(feature = "wasi-config")] use wasmtime_wasi_config::{WasiConfig, WasiConfigVariables}; #[cfg(feature = "wasi-http")] @@ -93,7 +95,7 @@ impl RunCommand { /// Executes the command. pub fn execute( mut self, - #[cfg(feature = "rr")] replay_cfg: Option, + #[cfg(feature = "rr")] replay_opts: Option, ) -> Result<()> { self.run.common.init_logging()?; @@ -115,8 +117,8 @@ impl RunCommand { } #[cfg(feature = "rr")] - if let Some(cfg) = replay_cfg { - //config.enable_replay(cfg)?; + if replay_opts.is_some() { + config.replaying(true); } let engine = Engine::new(&config)?; @@ -195,6 +197,18 @@ impl RunCommand { store.init_recording(fs::File::create(&path)?, settings)?; } } + + if let Some(opts) = replay_opts { + let settings = ReplaySettings { + validate: opts.validate, + deser_buffer_size: opts.deser_buffer_size, + ..Default::default() + }; + store.init_replaying( + BufReader::new(fs::File::open(opts.trace).unwrap()), + settings, + )?; + } } // Always run the module asynchronously to ensure that the module can be From 1606da1837b0a959e1318b5f4dcffbe54153452b Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Thu, 30 Oct 2025 18:02:03 +0100 Subject: [PATCH 17/73] Added flat abi extraction for interface types --- crates/environ/src/component/types.rs | 167 +++++++++++++++++- crates/environ/src/component/types_builder.rs | 42 +++-- 2 files changed, 192 insertions(+), 17 deletions(-) diff --git a/crates/environ/src/component/types.rs b/crates/environ/src/component/types.rs index fee236413d..91f7985044 100644 --- a/crates/environ/src/component/types.rs +++ b/crates/environ/src/component/types.rs @@ -1,4 +1,4 @@ -use crate::component::{MAX_FLAT_PARAMS, MAX_FLAT_RESULTS}; +use crate::component::{FlatTypesStorage, MAX_FLAT_PARAMS, MAX_FLAT_RESULTS}; use crate::{EntityType, ModuleInternedTypeIndex, ModuleTypes, PrimaryMap}; use crate::{TypeTrace, prelude::*}; use core::hash::{Hash, Hasher}; @@ -364,6 +364,162 @@ impl ComponentTypes { } } + /// Returns the flat storage representation for an interface type. + pub fn flat_types_storage(&self, ty: &InterfaceType) -> FlatTypesStorage { + let mut storage_buf = FlatTypesStorage::new(); + + let push_discrim = + |storage: &mut FlatTypesStorage| storage.push(FlatType::I32, FlatType::I32); + + let push_storage = |storage: &mut FlatTypesStorage, other: FlatTypesStorage| -> bool { + let len = usize::from(storage.len); + let other_len = usize::from(other.len); + if len < (MAX_FLAT_TYPES - other_len + 1) { + storage.memory32[len..len + other_len] + .copy_from_slice(&other.memory32[..other_len]); + storage.memory64[len..len + other_len] + .copy_from_slice(&other.memory64[..other_len]); + storage.len += other.len; + true + } else { + storage.len = MAX_FLAT_TYPES as u8 + 1; + false + } + }; + + let push_storage_variant_case = + |storage: &mut FlatTypesStorage, case: Option| -> bool { + if let Some(case) = case { + if case.len as usize >= MAX_FLAT_TYPES { + storage.len = u8::try_from(MAX_FLAT_TYPES + 1).unwrap(); + false + } else { + let dst = storage + .memory32 + .iter_mut() + .zip(&mut storage.memory64) + .skip(1); + for (i, ((t32, t64), (dst32, dst64))) in case + .memory32 + .iter() + .zip(case.memory64.iter()) + .zip(dst) + .enumerate() + { + if i + 1 < usize::from(storage.len) { + // Populated Index + dst32.join(*t32); + dst64.join(*t64); + } else { + // New Index + storage.len += 1; + *dst32 = *t32; + *dst64 = *t64; + } + } + true + } + } else { + true + } + }; + + let storage = &mut storage_buf; + + match ty { + InterfaceType::U8 + | InterfaceType::S8 + | InterfaceType::Bool + | InterfaceType::U16 + | InterfaceType::S16 + | InterfaceType::U32 + | InterfaceType::S32 + | InterfaceType::Char + | InterfaceType::Own(_) + | InterfaceType::Future(_) + | InterfaceType::Stream(_) + | InterfaceType::ErrorContext(_) + | InterfaceType::Borrow(_) + | InterfaceType::Enum(_) => { + storage.push(FlatType::I32, FlatType::I32); + } + + InterfaceType::U64 | InterfaceType::S64 => { + storage.push(FlatType::I64, FlatType::I64); + } + InterfaceType::Float32 => { + storage.push(FlatType::F32, FlatType::F32); + } + InterfaceType::Float64 => { + storage.push(FlatType::F64, FlatType::F64); + } + InterfaceType::String | InterfaceType::List(_) => { + // Pointer pair + storage.push(FlatType::I32, FlatType::I64); + storage.push(FlatType::I32, FlatType::I64); + } + + InterfaceType::Record(i) => { + for field in self[*i].fields.iter() { + if !push_storage(storage, self.flat_types_storage(&field.ty)) { + break; + } + } + } + InterfaceType::Tuple(i) => { + for ty in self[*i].types.iter() { + if !push_storage(storage, self.flat_types_storage(ty)) { + break; + } + } + } + InterfaceType::Flags(i) => match FlagsSize::from_count(self[*i].names.len()) { + FlagsSize::Size0 => {} + FlagsSize::Size1 | FlagsSize::Size2 => { + storage.push(FlatType::I32, FlatType::I32); + } + FlagsSize::Size4Plus(n) => { + for _ in 0..n { + if !storage.push(FlatType::I32, FlatType::I32) { + break; + } + } + } + }, + InterfaceType::Variant(i) => { + push_discrim(storage); + for case in self[*i].cases.values() { + let case_flat = case.as_ref().map(|ty| self.flat_types_storage(ty)); + if !push_storage_variant_case(storage, case_flat) { + break; + } + } + } + InterfaceType::Option(i) => { + push_discrim(storage); + push_storage_variant_case(storage, None); + push_storage_variant_case(storage, Some(self.flat_types_storage(&self[*i].ty))); + } + InterfaceType::Result(i) => { + push_discrim(storage); + let result = &self[*i]; + push_storage_variant_case( + storage, + result.ok.map(|ty| self.flat_types_storage(&ty)), + ); + push_storage_variant_case( + storage, + result.err.map(|ty| self.flat_types_storage(&ty)), + ); + } + } + assert_eq!( + storage.len as usize, + self.canonical_abi(ty).flat_count(usize::MAX).unwrap() + ); + storage_buf + } + /// Adds a new `table` to the list of resource tables for this component. pub fn push_resource_table(&mut self, table: TypeResourceTable) -> TypeResourceTableIndex { self.resource_tables.push(table) @@ -1185,3 +1341,12 @@ pub enum FlatType { F32, F64, } + +impl FlatType { + const fn byte_size(&self) -> u8 { + match self { + FlatType::I32 | FlatType::F32 => 4, + FlatType::I64 | FlatType::F64 => 8, + } + } +} diff --git a/crates/environ/src/component/types_builder.rs b/crates/environ/src/component/types_builder.rs index b891612269..c8a643bad1 100644 --- a/crates/environ/src/component/types_builder.rs +++ b/crates/environ/src/component/types_builder.rs @@ -812,22 +812,30 @@ where return idx; } -struct FlatTypesStorage { - // This could be represented as `Vec` but on 64-bit architectures - // that's 24 bytes. Otherwise `FlatType` is 1 byte large and - // `MAX_FLAT_TYPES` is 16, so it should ideally be more space-efficient to - // use a flat array instead of a heap-based vector. - memory32: [FlatType; MAX_FLAT_TYPES], - memory64: [FlatType; MAX_FLAT_TYPES], - - // Tracks the number of flat types pushed into this storage. If this is - // `MAX_FLAT_TYPES + 1` then this storage represents an un-reprsentable - // type in flat types. - len: u8, +/// Representation of flat types in 32-bit and 64-bit memory +/// +/// This could be represented as `Vec` but on 64-bit architectures +/// that's 24 bytes. Otherwise `FlatType` is 1 byte large and +/// `MAX_FLAT_TYPES` is 16, so it should ideally be more space-efficient to +/// use a flat array instead of a heap-based vector. +#[derive(Debug)] +pub struct FlatTypesStorage { + /// Representation for 32-bit memory + pub memory32: [FlatType; MAX_FLAT_TYPES], + /// Representation for 64-bit memory + pub memory64: [FlatType; MAX_FLAT_TYPES], + + /// Tracks the number of flat types pushed into this storage. If this is + /// `MAX_FLAT_TYPES + 1` then this storage represents an un-reprsentable + /// type in flat types. + /// + /// This value should be the same on both `memory32` and `memory64` + pub len: u8, } impl FlatTypesStorage { - const fn new() -> FlatTypesStorage { + /// Create a new, empty storage for flat types + pub const fn new() -> FlatTypesStorage { FlatTypesStorage { memory32: [FlatType::I32; MAX_FLAT_TYPES], memory64: [FlatType::I32; MAX_FLAT_TYPES], @@ -835,7 +843,8 @@ impl FlatTypesStorage { } } - fn as_flat_types(&self) -> Option> { + /// Returns a reference to flat type representation + pub fn as_flat_types(&self) -> Option> { let len = usize::from(self.len); if len > MAX_FLAT_TYPES { assert_eq!(len, MAX_FLAT_TYPES + 1); @@ -854,7 +863,7 @@ impl FlatTypesStorage { /// Returns whether the type was actually pushed or whether this list of /// flat types just exceeded the maximum meaning that it is now /// unrepresentable with a flat list of types. - fn push(&mut self, t32: FlatType, t64: FlatType) -> bool { + pub fn push(&mut self, t32: FlatType, t64: FlatType) -> bool { let len = usize::from(self.len); if len < MAX_FLAT_TYPES { self.memory32[len] = t32; @@ -873,7 +882,8 @@ impl FlatTypesStorage { } impl FlatType { - fn join(&mut self, other: FlatType) { + /// Constructs the "joined" representation for two flat types + pub fn join(&mut self, other: FlatType) { if *self == other { return; } From 728769d9f49b7c4c7d40ab372db3b7cf0d30639a Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Sun, 2 Nov 2025 20:55:29 +0100 Subject: [PATCH 18/73] Added flat type encoding/decoding for events; reformatted validation events --- crates/environ/src/component/types.rs | 3 +- crates/environ/src/component/types_builder.rs | 8 + crates/environ/src/types.rs | 9 + crates/wasmtime/src/runtime/component/func.rs | 3 +- .../src/runtime/component/func/host.rs | 28 ++- .../src/runtime/component/func/options.rs | 20 ++- crates/wasmtime/src/runtime/func.rs | 17 +- crates/wasmtime/src/runtime/rr/core.rs | 20 ++- crates/wasmtime/src/runtime/rr/core/events.rs | 111 ++++++++---- .../runtime/rr/core/events/common_events.rs | 6 +- .../rr/core/events/component_events.rs | 169 +++++++++--------- .../src/runtime/rr/core/events/core_events.rs | 17 +- .../src/runtime/rr/hooks/component_hooks.rs | 73 +++++--- .../src/runtime/rr/hooks/core_hooks.rs | 13 +- crates/wasmtime/src/runtime/vm/vmcontext.rs | 18 +- 15 files changed, 316 insertions(+), 199 deletions(-) diff --git a/crates/environ/src/component/types.rs b/crates/environ/src/component/types.rs index 91f7985044..c2cb4a5cb2 100644 --- a/crates/environ/src/component/types.rs +++ b/crates/environ/src/component/types.rs @@ -1343,7 +1343,8 @@ pub enum FlatType { } impl FlatType { - const fn byte_size(&self) -> u8 { + /// Return the size in bytes for this flat type + pub const fn byte_size(&self) -> u8 { match self { FlatType::I32 | FlatType::F32 => 4, FlatType::I64 | FlatType::F64 => 8, diff --git a/crates/environ/src/component/types_builder.rs b/crates/environ/src/component/types_builder.rs index c8a643bad1..09e102a9cf 100644 --- a/crates/environ/src/component/types_builder.rs +++ b/crates/environ/src/component/types_builder.rs @@ -879,6 +879,14 @@ impl FlatTypesStorage { false } } + + /// Generate an iterator over the 32-bit flat encoding + pub fn iter32(&self) -> impl Iterator { + self.memory32 + .iter() + .take(self.len as usize) + .map(|f| f.byte_size()) + } } impl FlatType { diff --git a/crates/environ/src/types.rs b/crates/environ/src/types.rs index eb5a9730dc..a3805e77c7 100644 --- a/crates/environ/src/types.rs +++ b/crates/environ/src/types.rs @@ -233,6 +233,15 @@ impl WasmValType { } } + /// Return the number of bytes needed to represent this value + pub fn byte_size(&self) -> u8 { + match self { + WasmValType::I32 | WasmValType::F32 => 4, + WasmValType::I64 | WasmValType::F64 => 8, + WasmValType::V128 | WasmValType::Ref(_) => 16, + } + } + /// Returns the contained reference type. /// /// Panics if the value type is not a vmgcref diff --git a/crates/wasmtime/src/runtime/component/func.rs b/crates/wasmtime/src/runtime/component/func.rs index 794d0a9933..8d220b5e6e 100644 --- a/crates/wasmtime/src/runtime/component/func.rs +++ b/crates/wasmtime/src/runtime/component/func.rs @@ -596,7 +596,8 @@ impl Func { |store| crate::Func::call_unchecked_raw(store, export, params_and_returns), params_and_returns.as_ref(), self.index, - *self.instance.id().get(store.0).component().checksum(), + self.abi_info(store.0).2, + self.instance.id(), &mut store, )?; } diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 3442c93d32..de5a75a04b 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -257,11 +257,11 @@ where let types = vminstance.component().types().clone(); - rr::component_hooks::record_replay_host_func_entry(storage, &ty, store.0)?; + rr::component_hooks::record_replay_host_func_entry(storage, &types, &ty, store.0)?; - let ty = &types[ty]; - let param_tys = InterfaceType::Tuple(ty.params); - let result_tys = InterfaceType::Tuple(ty.results); + let func_ty = &types[ty]; + let param_tys = InterfaceType::Tuple(func_ty.params); + let result_tys = InterfaceType::Tuple(func_ty.results); let storage_type = if async_ { #[cfg(feature = "component-model-async")] @@ -628,6 +628,8 @@ where ); rr::component_hooks::record_host_func_return( unsafe { storage_as_slice_mut(storage) }, + cx.types, + &ty, cx.store.0, )?; result @@ -641,7 +643,7 @@ where ptr, ); // Recording here is just for marking the return event - rr::component_hooks::record_host_func_return(&[], cx.store.0)?; + rr::component_hooks::record_host_func_return(&[], cx.types, &ty, cx.store.0)?; result } } @@ -811,7 +813,7 @@ where let types = instance.id().get(store.0).component().types().clone(); - rr::component_hooks::record_replay_host_func_entry(storage, &ty, store.0)?; + rr::component_hooks::record_replay_host_func_entry(storage, &types, &ty, store.0)?; let func_ty = &types[ty]; let param_tys = &types[func_ty.params]; @@ -931,7 +933,12 @@ where )?; } assert!(dst.next().is_none()); - rr::component_hooks::record_host_func_return(storage, cx.store.0)?; + rr::component_hooks::record_host_func_return( + storage, + cx.types, + &InterfaceType::Tuple(func_ty.results), + cx.store.0, + )?; } else { let ret_ptr = unsafe { storage[ret_index].assume_init_ref() }; let mut ptr = validate_inbounds_dynamic(&result_tys.abi, cx.as_slice(), ret_ptr)?; @@ -945,7 +952,12 @@ where )?; } // Recording here is just for marking the return event - rr::component_hooks::record_host_func_return(&[], cx.store.0)?; + rr::component_hooks::record_host_func_return( + &[], + cx.types, + &InterfaceType::Tuple(func_ty.results), + cx.store.0, + )?; } unsafe { diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index ce24d07f61..04b469f0e9 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -4,15 +4,17 @@ use crate::component::matching::InstanceType; use crate::component::resources::{HostResourceData, HostResourceIndex, HostResourceTables}; use crate::component::{Instance, ResourceType}; use crate::prelude::*; +#[cfg(all(feature = "rr-component", feature = "rr-validate"))] +use crate::rr::component_events::ResultEvent; +#[cfg(feature = "rr-component")] +use crate::rr::component_hooks::ReplayLoweringPhase; +use crate::rr::{ConstMemorySliceCell, MemorySliceCell}; #[cfg(feature = "rr-component")] use crate::rr::{ RREvent, RecordBuffer, ReplayError, Replayer, component_events::ReallocEntryEvent, }; #[cfg(all(feature = "rr-component", feature = "rr-validate"))] use crate::rr::{Validate, component_events::ReallocReturnEvent}; -#[cfg(feature = "rr-component")] -use crate::rr::component_hooks::ReplayLoweringPhase; -use crate::rr::{ConstMemorySliceCell, MemorySliceCell}; use crate::runtime::vm::component::{ CallContexts, ComponentInstance, InstanceFlags, ResourceTable, ResourceTables, }; @@ -404,9 +406,9 @@ impl<'a, T: 'static> LowerContext<'a, T> { })?; let result = self.realloc_inner(old, old_size, old_align, new_size); #[cfg(all(feature = "rr-component", feature = "rr-validate"))] - self.store - .0 - .record_event_validation(|| ReallocReturnEvent::from_anyhow_result(&result))?; + self.store.0.record_event_validation(|| { + ReallocReturnEvent(ResultEvent::from_anyhow_result(&result)) + })?; result } @@ -636,7 +638,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { if run_validate { _lower_stack.pop().ok_or(ReplayError::InvalidOrdering)?; } - lowering_error = e.ret().map_err(Into::into).err(); + lowering_error = e.0.ret().map_err(Into::into).err(); } RREvent::ComponentLowerMemoryReturn(e) => { #[cfg(feature = "rr-validate")] @@ -645,7 +647,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { .pop() .ok_or(ReplayError::InvalidOrdering)?; } - lowering_error = e.ret().map_err(Into::into).err(); + lowering_error = e.0.ret().map_err(Into::into).err(); } RREvent::ComponentMemorySliceWrite(e) => { // The bounds check is performed here is required here (in the absence of @@ -665,7 +667,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { { #[cfg(feature = "rr-validate")] if run_validate { - lowering_error = _e.validate(&_realloc_stack.pop().unwrap()).err() + lowering_error = _e.0.validate(&_realloc_stack.pop().unwrap()).err() } } RREvent::ComponentLowerFlatEntry(_) => { diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 149e7d401b..3f4ab2f9e1 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -2366,20 +2366,30 @@ impl HostContext { let func = &state.func; let func_type_index = state._ty.index(); - let (num_params, num_results) = { + let (params_size, results_size) = { let type_index = state._ty.index(); let wasm_func_subtype = caller.engine().signatures().borrow(type_index).unwrap(); let wasm_func_type = wasm_func_subtype.unwrap_func(); ( - wasm_func_type.params().len(), - wasm_func_type.returns().len(), + wasm_func_type + .params() + .into_iter() + .map(|x| x.byte_size()) + .collect::>(), + wasm_func_type + .returns() + .into_iter() + .map(|x| x.byte_size()) + .collect::>(), ) }; + let (num_params, num_results) = (params_size.len(), results_size.len()); // 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::core_hooks::record_replay_host_func_entry( unsafe { &args.as_ref()[..num_params] }, + params_size.as_slice(), &func_type_index, caller.store.0, )?; @@ -2425,6 +2435,7 @@ impl HostContext { // Record the return values rr::core_hooks::record_host_func_return( unsafe { &args.as_ref()[..num_results] }, + results_size.as_slice(), &func_type_index, caller.store.0, )?; diff --git a/crates/wasmtime/src/runtime/rr/core.rs b/crates/wasmtime/src/runtime/rr/core.rs index 3fb1efa813..05748a010d 100644 --- a/crates/wasmtime/src/runtime/rr/core.rs +++ b/crates/wasmtime/src/runtime/rr/core.rs @@ -6,12 +6,12 @@ use serde::{Deserialize, Serialize}; // Use component 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 +#[cfg(all(feature = "rr-validate", feature = "rr-component"))] +pub use events::RRFuncArgVals; #[cfg(any(feature = "rr-validate", feature = "rr-component"))] pub use events::Validate; #[cfg(feature = "rr-component")] pub use events::component_events; -#[cfg(all(feature = "rr-validate", feature = "rr-component"))] -pub use events::{RRFuncArgVals, func_argvals_from_raw_slice}; use events::{common_events, component_events as __component_events}; pub use events::{core_events, marker_events}; pub use io::{RecordWriter, ReplayReader}; @@ -535,20 +535,24 @@ mod tests { #[test] #[cfg(all(feature = "rr", feature = "rr-component"))] fn rr_buffers() -> Result<()> { + use wasmtime_environ::component::FlatTypesStorage; + let record_settings = RecordSettings::default(); let tmp = NamedTempFile::new()?; let tmppath = tmp.path().to_str().expect("Filename should be UTF-8"); - 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)]; + let flat = FlatTypesStorage::new(); + flat.push(FlatType::I32, FlatType::I32); + flat.push(FlatType::F32, FlatType::F32); + flat.push(FlatType::I64, FlatType::I64); // Record values let mut recorder = RecordBuffer::new_recorder(Box::new(File::create(tmppath)?), record_settings)?; - recorder - .record_event(|| __component_events::HostFuncReturnEvent::new(values.as_slice()))?; + recorder.record_event(|| { + __component_events::HostFuncReturnEvent::new(values.as_slice(), flat) + })?; recorder.flush()?; let tmp = tmp.into_temp_path(); diff --git a/crates/wasmtime/src/runtime/rr/core/events.rs b/crates/wasmtime/src/runtime/rr/core/events.rs index 275b2b68ec..e65fb402e9 100644 --- a/crates/wasmtime/src/runtime/rr/core/events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events.rs @@ -3,7 +3,7 @@ use super::ReplayError; use crate::ValRaw; use crate::prelude::*; use core::fmt; -use core::mem::{self, MaybeUninit}; +use core::mem::MaybeUninit; use serde::{Deserialize, Serialize}; /// A serde compatible representation of errors produced by actions during @@ -41,60 +41,99 @@ impl fmt::Display for EventActionError { impl core::error::Error for EventActionError {} -type ValRawBytes = [u8; mem::size_of::()]; - -/// 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 -pub trait ValRawBytesConvertable { - fn to_valraw_bytes(self) -> ValRawBytes; - fn from_valraw_bytes(value: ValRawBytes) -> Self; +/// Types that can be serialized/deserialized into/from +/// flat types for record and replay +pub trait FlatBytes { + fn bytes_ref(&self, size: u8) -> &[u8]; + fn from_bytes(value: &[u8]) -> Self; } -impl ValRawBytesConvertable for ValRaw { +impl FlatBytes for ValRaw { #[inline] - fn to_valraw_bytes(self) -> ValRawBytes { - self.as_bytes() + fn bytes_ref(&self, size: u8) -> &[u8] { + &self.get_bytes()[..size as usize] } #[inline] - fn from_valraw_bytes(value: ValRawBytes) -> Self { - ValRaw::from_bytes(value) + fn from_bytes(value: &[u8]) -> Self { + ValRaw::bytes(value) } } -impl ValRawBytesConvertable for MaybeUninit { +impl FlatBytes for MaybeUninit { #[inline] - fn to_valraw_bytes(self) -> ValRawBytes { + fn bytes_ref(&self, size: u8) -> &[u8] { // Uninitialized data is assumed and serialized, so hence // may contain some undefined values - unsafe { self.assume_init() }.to_valraw_bytes() + let val = unsafe { self.assume_init_ref() }; + val.bytes_ref(size) } #[inline] - fn from_valraw_bytes(value: ValRawBytes) -> Self { - MaybeUninit::new(ValRaw::from_valraw_bytes(value)) + fn from_bytes(value: &[u8]) -> Self { + MaybeUninit::new(ValRaw::bytes(value)) } } -pub type RRFuncArgVals = Vec; +/// Representation of flat arguments for function entry/return +#[derive(Serialize, Deserialize, Clone, PartialEq)] +pub struct RRFuncArgVals { + /// Flat data vector of bytes + bytes: Vec, + /// Descriptor vector of sizes of each flat types + /// + /// The length of this vector equals the number of flat types, + /// and the sum of this vector equals the length of `bytes` + sizes: Vec, +} -/// Construct [`RRFuncArgVals`] from raw value buffer -pub fn func_argvals_from_raw_slice(args: &[T]) -> RRFuncArgVals -where - T: ValRawBytesConvertable + Copy, -{ - args.iter().map(|x| x.to_valraw_bytes()).collect() +impl fmt::Debug for RRFuncArgVals { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "RRFuncArgVals ")?; + let mut pos: usize = 0; + let mut list = f.debug_list(); + let hex_fmt = |bytes: &[u8]| { + let hex_string = bytes + .iter() + .rev() + .map(|b| format!("{:02x}", b)) + .collect::(); + format!("0x{}", hex_string) + }; + for flat_size in self.sizes.iter() { + list.entry(&( + flat_size, + hex_fmt(&self.bytes[pos..pos + *flat_size as usize]), + )); + pos += *flat_size as usize; + } + list.finish() + } } -/// Encode [`RRFuncArgVals`] back into raw value buffer -fn func_argvals_into_raw_slice(rr_args: RRFuncArgVals, raw_args: &mut [T]) -where - T: ValRawBytesConvertable, -{ - for (src, dst) in rr_args.into_iter().zip(raw_args.iter_mut()) { - *dst = T::from_valraw_bytes(src); +impl RRFuncArgVals { + /// Construct [`RRFuncArgVals`] from raw value buffer and flat sizes + pub fn from_raw_slice(args: &[T], flat: impl Iterator) -> RRFuncArgVals + where + T: FlatBytes, + { + let mut bytes = Vec::::new(); + let mut sizes = Vec::::new(); + for (flat_size, arg) in flat.zip(args.iter()) { + bytes.extend_from_slice(&arg.bytes_ref(flat_size)); + sizes.push(flat_size); + } + RRFuncArgVals { bytes, sizes } + } + + /// Encode [`RRFuncArgVals`] back into raw value buffer + pub fn into_raw_slice(self, raw_args: &mut [T]) + where + T: FlatBytes, + { + let mut pos = 0; + for (flat_size, dst) in self.sizes.into_iter().zip(raw_args.iter_mut()) { + *dst = T::from_bytes(&self.bytes[pos..pos + flat_size as usize]); + pos += flat_size as usize; + } } } diff --git a/crates/wasmtime/src/runtime/rr/core/events/common_events.rs b/crates/wasmtime/src/runtime/rr/core/events/common_events.rs index 5c483ad6b1..c56a01696d 100644 --- a/crates/wasmtime/src/runtime/rr/core/events/common_events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events/common_events.rs @@ -17,14 +17,14 @@ pub struct HostFuncReturnEvent { } impl HostFuncReturnEvent { // Record - pub fn new(args: &[MaybeUninit]) -> Self { + pub fn new(args: &[MaybeUninit], flat: &[u8]) -> Self { Self { - args: func_argvals_from_raw_slice(args), + args: RRFuncArgVals::from_raw_slice(args, flat.iter().copied()), } } // Replay /// Consume the caller event and encode it back into the slice pub fn move_into_slice(self, args: &mut [MaybeUninit]) { - func_argvals_into_raw_slice(self.args, args); + self.args.into_raw_slice(args); } } diff --git a/crates/wasmtime/src/runtime/rr/core/events/component_events.rs b/crates/wasmtime/src/runtime/rr/core/events/component_events.rs index e730556223..bcba7f6d07 100644 --- a/crates/wasmtime/src/runtime/rr/core/events/component_events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events/component_events.rs @@ -3,11 +3,12 @@ use super::*; use crate::component::Component; use crate::vm::component::libcalls::ResourceDropRet; +use core::ops::Deref; // Re-export common events from this module pub use common_events::*; use wasmtime_environ::{ self, - component::{ExportIndex, InterfaceType, TypeFuncIndex}, + component::{ExportIndex, FlatTypesStorage, InterfaceType, TypeFuncIndex}, }; /// A [`Component`] instantiatation event @@ -38,9 +39,14 @@ pub struct WasmFuncEntryEvent { } impl WasmFuncEntryEvent { // Record - pub fn new(args: &[ValRaw], component: [u8; 32], func_idx: ExportIndex) -> Self { + pub fn new( + args: &[ValRaw], + flat: FlatTypesStorage, + component: [u8; 32], + func_idx: ExportIndex, + ) -> Self { Self { - args: func_argvals_from_raw_slice(args), + args: RRFuncArgVals::from_raw_slice(args, flat.iter32()), component, func_idx, } @@ -49,12 +55,12 @@ impl WasmFuncEntryEvent { // Replay /// Consume the caller event and encode it back into the slice pub fn move_into_slice(self, args: &mut [MaybeUninit]) { - func_argvals_into_raw_slice(self.args, args); + self.args.into_raw_slice(args); } } /// A call event from a Wasm component into the host -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct HostFuncEntryEvent { /// Raw values passed across the call entry boundary args: RRFuncArgVals, @@ -68,24 +74,13 @@ pub struct HostFuncEntryEvent { } impl HostFuncEntryEvent { // Record - pub fn new(args: &[MaybeUninit], ty: TypeFuncIndex) -> Self { + pub fn new(args: &[MaybeUninit], flat: FlatTypesStorage, ty: TypeFuncIndex) -> Self { Self { - args: func_argvals_from_raw_slice(args), + args: RRFuncArgVals::from_raw_slice(args, flat.iter32()), ty: ty, } } } -#[cfg(feature = "rr-validate")] -impl Validate for HostFuncEntryEvent { - fn validate(&self, expect_ty: &TypeFuncIndex) -> Result<(), ReplayError> { - self.log(); - if &self.ty == expect_ty { - Ok(()) - } else { - Err(ReplayError::FailedValidation) - } - } -} /// A reallocation call event in the Component Model canonical ABI /// @@ -123,58 +118,88 @@ pub struct MemorySliceWriteEvent { pub bytes: Vec, } -macro_rules! generic_new_result_events { - ( - $( - $(#[$meta:meta])* - $event:ident -> ($ok_ty:ty,$err_variant:path) - ),* - ) => ( - $( - $(#[$meta])* - #[derive(Debug, Clone, Serialize, Deserialize)] - pub struct $event(Result<$ok_ty, EventActionError>); +/// Result event types that are serialized/deserialized for record and replay. +/// +/// Anyhow result types cannot use blanket PartialEq implementations since +/// anyhow results are not serialized directly. They need to specifically check +/// for divergence between recorded and replayed effects with [EventActionError] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ResultEvent(Result); - impl $event { - pub fn from_anyhow_result(ret: &Result<$ok_ty>) -> Self { - Self(ret.as_ref().map(|t| (*t).clone()).map_err(|e| $err_variant(e.to_string()))) - } - pub fn ret(self) -> Result<$ok_ty, EventActionError> { self.0 } - } +impl Deref for ResultEvent { + type Target = Result; - generic_new_result_events!(@validate_impl $event,$ok_ty,$err_variant); - )* - ); + fn deref(&self) -> &Self::Target { + &self.0 + } +} - (@validate_impl $event:ident,$ok_ty:ty,$err_variant:path) => { - #[cfg(feature = "rr-validate")] - /// Note: Anyhow result types cannot use blanket PartialEq implementations since - /// anyhow results are not serialized directly. They need to specifically check - /// for divergence between recorded and replayed effects with [EventActionError] - impl Validate> for $event { - fn validate(&self, expect_ret: &Result<$ok_ty>) -> Result<(), ReplayError> { - self.log(); - // Cannot just use eq since anyhow::Error and EventActionError cannot be compared - match (self.0.as_ref(), expect_ret.as_ref()) { - (Ok(r), Ok(s)) => { - if r == s { - Ok(()) - } else { - Err(ReplayError::FailedValidation) - } - } - // Return the recorded error - (Err(e), Err(f)) => Err(ReplayError::from($err_variant(format!( - "Replayed Error for {}: {} \nRecorded Error for {}: {}", - stringify!($event), e, stringify!($event), f - )))), - // Diverging errors.. Report as a failed validation - (Ok(_), Err(_)) => Err(ReplayError::FailedValidation), - (Err(_), Ok(_)) => Err(ReplayError::FailedValidation), +impl ResultEvent +where + T: Clone, +{ + pub fn from_anyhow_result(ret: &Result) -> Self { + Self( + ret.as_ref() + .map(|t| (*t).clone()) + .map_err(|e| EventActionError::ReallocError(e.to_string())), + ) + } + pub fn ret(self) -> Result { + self.0 + } +} + +impl Validate> for ResultEvent { + fn validate(&self, expect_ret: &Result) -> Result<(), ReplayError> { + self.log(); + // Cannot just use eq since anyhow::Error and EventActionError cannot be compared + match (self.0.as_ref(), expect_ret.as_ref()) { + (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 Error: {} \nRecorded Error: {}", + e, f + )))), + // Diverging errors.. Report as a failed validation + (Ok(_), Err(_)) => Err(ReplayError::FailedValidation), + (Err(_), Ok(_)) => Err(ReplayError::FailedValidation), } - }; + } +} + +// TODO: Fix all the errors to use independent types + +/// Return from a reallocation call (needed only for validation) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ReallocReturnEvent(pub ResultEvent); + +/// Return from type lowering to flat destination +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LowerFlatReturnEvent(pub ResultEvent<()>); + +/// Return from type lowering to destination in memory +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LowerMemoryReturnEvent(pub ResultEvent<()>); + +/// A return event from a Wasm component function to Host +/// +/// Matches 1:1 with [`WasmFuncEntryEvent`]. +/// +/// Note: Could potential merge with [`HostFuncReturnEvent`] as [`HostToWasmEvent`]? +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WasmFuncReturnEvent(pub ResultEvent); + +impl Validate> for WasmFuncReturnEvent { + fn validate(&self, expect: &Result) -> Result<(), ReplayError> { + self.0.validate(expect) + } } // Macro to generate RR events from the builtin descriptions @@ -273,21 +298,5 @@ macro_rules! builtin_events { (@ret_first $first:tt $($rest:tt)*) => ($first); } -// Return events with anyhow error conversion to EventActionError -generic_new_result_events! { - /// Return from a reallocation call (needed only for validation) - ReallocReturnEvent -> (usize, EventActionError::ReallocError), - /// Return from type lowering to flat destination - LowerFlatReturnEvent -> ((), EventActionError::LowerFlatError), - /// Return from type lowering to destination in memory - LowerMemoryReturnEvent -> ((), EventActionError::LowerMemoryError), - /// A return event from a Wasm component function to Host - /// - /// Matches 1:1 with [`WasmFuncEntryEvent`]. - /// - /// Note: Could potential merge with [`HostFuncEntryEvent`] as [`HostToWasmEvent`]? - WasmFuncReturnEvent -> (RRFuncArgVals, EventActionError::WasmFuncReturnError) -} - // Entry/return events for each builtin function wasmtime_environ::foreach_builtin_component_function!(builtin_events); diff --git a/crates/wasmtime/src/runtime/rr/core/events/core_events.rs b/crates/wasmtime/src/runtime/rr/core/events/core_events.rs index d51e4eefee..88156d8473 100644 --- a/crates/wasmtime/src/runtime/rr/core/events/core_events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events/core_events.rs @@ -5,7 +5,7 @@ use wasmtime_environ::VMSharedTypeIndex; pub use common_events::*; /// A call event from a Core Wasm module into the host -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct HostFuncEntryEvent { /// Raw values passed across the call/return boundary args: RRFuncArgVals, @@ -14,21 +14,10 @@ pub struct HostFuncEntryEvent { } impl HostFuncEntryEvent { // Record - pub fn new(args: &[MaybeUninit], types: VMSharedTypeIndex) -> Self { + pub fn new(args: &[MaybeUninit], flat: &[u8], types: VMSharedTypeIndex) -> Self { Self { - args: func_argvals_from_raw_slice(args), + args: RRFuncArgVals::from_raw_slice(args, flat.iter().copied()), types: types, } } } -#[cfg(feature = "rr-validate")] -impl Validate for HostFuncEntryEvent { - fn validate(&self, expect_types: &VMSharedTypeIndex) -> Result<(), ReplayError> { - self.log(); - if &self.types == expect_types { - Ok(()) - } else { - Err(ReplayError::FailedValidation) - } - } -} diff --git a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs index dfe87bacf8..16605ab3d1 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs @@ -1,19 +1,20 @@ use crate::ValRaw; #[cfg(feature = "component-model")] use crate::component::func::LowerContext; +use crate::component::store::StoreComponentInstanceId; #[cfg(feature = "rr-component")] use crate::rr::component_events::{ - HostFuncReturnEvent, LowerFlatReturnEvent, LowerMemoryReturnEvent, WasmFuncEntryEvent, + HostFuncReturnEvent, LowerFlatReturnEvent, LowerMemoryReturnEvent, ResultEvent, + WasmFuncEntryEvent, }; #[cfg(all(feature = "rr-component", feature = "rr-validate"))] -use crate::rr::{ - RRFuncArgVals, component_events::WasmFuncReturnEvent, func_argvals_from_raw_slice, -}; +use crate::rr::{RRFuncArgVals, component_events::WasmFuncReturnEvent}; use crate::store::StoreOpaque; use crate::{StoreContextMut, prelude::*}; +use alloc::sync::Arc; use core::mem::MaybeUninit; #[cfg(feature = "component-model")] -use wasmtime_environ::component::{ExportIndex, InterfaceType, TypeFuncIndex}; +use wasmtime_environ::component::{ComponentTypes, ExportIndex, InterfaceType, TypeFuncIndex}; /// Indicator type signalling the context during lowering #[cfg(feature = "rr-component")] @@ -29,29 +30,43 @@ pub fn record_replay_wasm_func( wasm_call: F, args: &[ValRaw], func_idx: ExportIndex, - component: [u8; 32], + type_idx: TypeFuncIndex, + id: StoreComponentInstanceId, store: &mut StoreContextMut<'_, T>, ) -> Result<()> where F: FnOnce(&mut StoreContextMut<'_, T>) -> Result<()>, { - let _ = (args, component, func_idx); + let _ = (args, id, func_idx, type_idx); #[cfg(feature = "rr-component")] - store - .0 - .record_event(|| WasmFuncEntryEvent::new(args, component, func_idx))?; - let result = wasm_call(store); + let component = id.get(store.0).component(); + #[cfg(feature = "rr-component")] + let types = component.types(); #[cfg(all(feature = "rr-component", feature = "rr-validate"))] + let flat_results = types.flat_types_storage(&InterfaceType::Tuple(types[type_idx].results)); + #[cfg(feature = "rr-component")] { - let result = result.map(|_| func_argvals_from_raw_slice(args)); + let checksum = *component.checksum(); + let flat_params = types.flat_types_storage(&InterfaceType::Tuple(types[type_idx].params)); store .0 - .record_event_validation(|| WasmFuncReturnEvent::from_anyhow_result(&result))?; - // TODO: After adding validation support, replay with `next_replay_event_validation` - store.0.next_replay_event_and(|_r: WasmFuncReturnEvent| { - log::warn!("Yet to implement validation for WasmFuncReturnEvent; skipping for now"); - Ok(()) + .record_event(|| WasmFuncEntryEvent::new(args, flat_params, checksum, func_idx))?; + } + let result = wasm_call(store); + #[cfg(all(feature = "rr-component", feature = "rr-validate"))] + { + let result = result.map(|_| RRFuncArgVals::from_raw_slice(args, flat_results.iter32())); + store.0.record_event_validation(|| { + WasmFuncReturnEvent(ResultEvent::from_anyhow_result(&result)) })?; + store + .0 + .next_replay_event_validation::>(&result)?; + //// TODO: After adding validation support, replay with `next_replay_event_validation` + //store.0.next_replay_event_and(|_r: WasmFuncReturnEvent| { + // log::warn!("Yet to implement validation for WasmFuncReturnEvent; skipping for now"); + // Ok(()) + //})?; result?; return Ok(()); } @@ -63,16 +78,19 @@ where #[inline] pub fn record_replay_host_func_entry( args: &mut [MaybeUninit], - func_idx: &TypeFuncIndex, + types: &Arc, + type_idx: &TypeFuncIndex, store: &mut StoreOpaque, ) -> Result<()> { #[cfg(all(feature = "rr-component", feature = "rr-validate"))] { use crate::rr::component_events::HostFuncEntryEvent; - store.record_event_validation(|| HostFuncEntryEvent::new(args, func_idx.clone()))?; - store.next_replay_event_validation::(func_idx)?; + let flat_params = types.flat_types_storage(&InterfaceType::Tuple(types[*type_idx].params)); + let event = HostFuncEntryEvent::new(args, flat_params, type_idx.clone()); + store.record_event_validation(|| event.clone())?; + store.next_replay_event_validation::(&event)?; } - let _ = (args, func_idx, store); + let _ = (args, types, type_idx, store); Ok(()) } @@ -80,11 +98,16 @@ pub fn record_replay_host_func_entry( #[inline] pub fn record_host_func_return( args: &[MaybeUninit], + types: &ComponentTypes, + ty: &InterfaceType, store: &mut StoreOpaque, ) -> Result<()> { #[cfg(feature = "rr-component")] - store.record_event(|| HostFuncReturnEvent::new(args))?; - let _ = (args, store); + { + let flat_results = types.flat_types_storage(&ty).iter32().collect::>(); + store.record_event(|| HostFuncReturnEvent::new(args, flat_results.as_slice()))?; + } + let _ = (args, types, ty, store); Ok(()) } @@ -110,7 +133,7 @@ where #[cfg(feature = "rr-component")] cx.store .0 - .record_event(|| LowerMemoryReturnEvent::from_anyhow_result(&store_result))?; + .record_event(|| LowerMemoryReturnEvent(ResultEvent::from_anyhow_result(&store_result)))?; store_result } @@ -135,6 +158,6 @@ where #[cfg(feature = "rr-component")] cx.store .0 - .record_event(|| LowerFlatReturnEvent::from_anyhow_result(&lower_result))?; + .record_event(|| LowerFlatReturnEvent(ResultEvent::from_anyhow_result(&lower_result)))?; lower_result } diff --git a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs index 871a8b311f..b40cda29b5 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs @@ -10,6 +10,7 @@ use wasmtime_environ::VMSharedTypeIndex; /// Record and replay hook operation for host function entry events pub fn record_replay_host_func_entry( args: &[MaybeUninit], + flat: &[u8], ty: &VMSharedTypeIndex, store: &mut StoreOpaque, ) -> Result<()> { @@ -17,10 +18,11 @@ pub fn record_replay_host_func_entry( { // Record/replay the raw parameter args use crate::rr::core_events::HostFuncEntryEvent; - store.record_event_validation(|| HostFuncEntryEvent::new(&args, ty.clone()))?; - store.next_replay_event_validation::(ty)?; + let event = HostFuncEntryEvent::new(&args, flat, ty.clone()); + store.record_event_validation(|| event.clone())?; + store.next_replay_event_validation::(&event)?; } - let _ = (args, ty, store); + let _ = (args, flat, ty, store); Ok(()) } @@ -28,13 +30,14 @@ pub fn record_replay_host_func_entry( /// Record hook operation for host function return events pub fn record_host_func_return( args: &[MaybeUninit], + flat: &[u8], ty: &VMSharedTypeIndex, store: &mut StoreOpaque, ) -> Result<()> { // Record the return values #[cfg(feature = "rr")] - store.record_event(|| HostFuncReturnEvent::new(&args))?; - let _ = (args, ty, store); + store.record_event(|| HostFuncReturnEvent::new(&args, flat))?; + let _ = (args, flat, ty, store); Ok(()) } diff --git a/crates/wasmtime/src/runtime/vm/vmcontext.rs b/crates/wasmtime/src/runtime/vm/vmcontext.rs index 59d95f7dfa..8a2c06b265 100644 --- a/crates/wasmtime/src/runtime/vm/vmcontext.rs +++ b/crates/wasmtime/src/runtime/vm/vmcontext.rs @@ -1392,6 +1392,9 @@ pub union ValRaw { /// /// This value is always stored in a little-endian format. exnref: u32, + + /// A representation of underlying union as a byte vector + bytes: [u8; 16], } // The `ValRaw` type is matched as `wasmtime_val_raw_t` in the C API so these @@ -1606,16 +1609,19 @@ impl ValRaw { exnref } - /// Get the raw bits of the union + /// Get the WebAssembly value's raw bytes #[inline] - pub fn as_bytes(&self) -> [u8; mem::size_of::()] { - unsafe { mem::transmute(*self) } + pub fn get_bytes(&self) -> &[u8; mem::size_of::()] { + unsafe { &self.bytes } + //unsafe { &*(self as *const Self as *const [u8; mem::size_of::()]) } } - /// Construct ValRaw from raw bits + /// Create a WebAssembly value from raw bytes #[inline] - pub fn from_bytes(value: [u8; mem::size_of::()]) -> Self { - unsafe { mem::transmute(value) } + pub fn bytes(value: &[u8]) -> ValRaw { + let mut bytes = [0u8; mem::size_of::()]; + bytes[..value.len()].copy_from_slice(value); + ValRaw { bytes } } } From ccf7d4ef1f150556a9e15448b5a5d12b81727db2 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Mon, 3 Nov 2025 16:39:33 +0100 Subject: [PATCH 19/73] Refactor event errors into trait object --- crates/wasmtime/src/runtime/rr/core.rs | 14 +-- crates/wasmtime/src/runtime/rr/core/events.rs | 41 +++------ .../rr/core/events/component_events.rs | 91 ++++++++++++------- .../src/runtime/vm/component/libcalls.rs | 23 +---- 4 files changed, 75 insertions(+), 94 deletions(-) diff --git a/crates/wasmtime/src/runtime/rr/core.rs b/crates/wasmtime/src/runtime/rr/core.rs index 05748a010d..ec47e0d042 100644 --- a/crates/wasmtime/src/runtime/rr/core.rs +++ b/crates/wasmtime/src/runtime/rr/core.rs @@ -1,7 +1,7 @@ use crate::config::ModuleVersionStrategy; use crate::prelude::*; use core::fmt; -use events::EventActionError; +use events::EventError; use serde::{Deserialize, Serialize}; // Use component events internally even without feature flags enabled // so that [`RREvent`] has a well-defined serialization format, but export @@ -180,13 +180,13 @@ impl RREvent { } /// Error type signalling failures during a replay run -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug)] pub enum ReplayError { EmptyBuffer, FailedValidation, IncorrectEventVariant, InvalidOrdering, - EventActionError(EventActionError), + EventError(Box), } impl fmt::Display for ReplayError { @@ -204,7 +204,7 @@ impl fmt::Display for ReplayError { Self::IncorrectEventVariant => { write!(f, "event method invoked on incorrect variant") } - Self::EventActionError(e) => { + Self::EventError(e) => { write!(f, "{:?}", e) } Self::InvalidOrdering => { @@ -216,9 +216,9 @@ impl fmt::Display for ReplayError { impl core::error::Error for ReplayError {} -impl From for ReplayError { - fn from(value: EventActionError) -> Self { - Self::EventActionError(value) +impl From for ReplayError { + fn from(value: T) -> Self { + Self::EventError(Box::new(value)) } } diff --git a/crates/wasmtime/src/runtime/rr/core/events.rs b/crates/wasmtime/src/runtime/rr/core/events.rs index e65fb402e9..f5e401a6b4 100644 --- a/crates/wasmtime/src/runtime/rr/core/events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events.rs @@ -6,41 +6,22 @@ use core::fmt; use core::mem::MaybeUninit; use serde::{Deserialize, Serialize}; -/// A serde compatible representation of errors produced by actions during -/// initial recording for specific events +/// A serde compatible representation of errors produced during execution +/// of certain 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 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), - LowerFlatError(String), - LowerMemoryError(String), - BuiltinError(String), - WasmFuncReturnError(String), -} - -impl fmt::Display for EventActionError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::ReallocError(s) - | Self::LowerFlatError(s) - | Self::LowerMemoryError(s) - | Self::BuiltinError(s) - | Self::WasmFuncReturnError(s) => { - write!(f, "{}", s) - } - } - } +/// 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 +pub trait EventError: core::error::Error + Send + Sync + 'static { + fn new(t: String) -> Self + where + Self: Sized; + fn get(&self) -> &String; } -impl core::error::Error for EventActionError {} - /// Types that can be serialized/deserialized into/from /// flat types for record and replay pub trait FlatBytes { diff --git a/crates/wasmtime/src/runtime/rr/core/events/component_events.rs b/crates/wasmtime/src/runtime/rr/core/events/component_events.rs index bcba7f6d07..6d10ab34f8 100644 --- a/crates/wasmtime/src/runtime/rr/core/events/component_events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events/component_events.rs @@ -3,7 +3,6 @@ use super::*; use crate::component::Component; use crate::vm::component::libcalls::ResourceDropRet; -use core::ops::Deref; // Re-export common events from this module pub use common_events::*; use wasmtime_environ::{ @@ -118,42 +117,39 @@ pub struct MemorySliceWriteEvent { pub bytes: Vec, } -/// Result event types that are serialized/deserialized for record and replay. +/// Result newtype for events that can be serialized/deserialized for record/replay. /// /// Anyhow result types cannot use blanket PartialEq implementations since /// anyhow results are not serialized directly. They need to specifically check -/// for divergence between recorded and replayed effects with [EventActionError] +/// for divergence between recorded and replayed effects with [EventError] #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ResultEvent(Result); +pub struct ResultEvent(Result); -impl Deref for ResultEvent { - type Target = Result; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl ResultEvent +impl ResultEvent where T: Clone, + E: EventError, { pub fn from_anyhow_result(ret: &Result) -> Self { Self( ret.as_ref() .map(|t| (*t).clone()) - .map_err(|e| EventActionError::ReallocError(e.to_string())), + .map_err(|e| E::new(e.to_string())), ) } - pub fn ret(self) -> Result { + pub fn ret(self) -> Result { self.0 } } -impl Validate> for ResultEvent { +impl Validate> for ResultEvent +where + T: fmt::Debug + PartialEq, + E: EventError, +{ fn validate(&self, expect_ret: &Result) -> Result<(), ReplayError> { self.log(); - // Cannot just use eq since anyhow::Error and EventActionError cannot be compared + // Cannot just use eq since anyhow::Error and EventError cannot be compared match (self.0.as_ref(), expect_ret.as_ref()) { (Ok(r), Ok(s)) => { if r == s { @@ -163,9 +159,10 @@ impl Validate> for ResultEvent { } } // Return the recorded error - (Err(e), Err(f)) => Err(ReplayError::from(EventActionError::ReallocError(format!( - "Replayed Error: {} \nRecorded Error: {}", - e, f + (Err(e), Err(f)) => Err(ReplayError::from(E::new(format!( + "Error from recording: {} \r\n| Error on execution: {}", + e.get(), + f )))), // Diverging errors.. Report as a failed validation (Ok(_), Err(_)) => Err(ReplayError::FailedValidation), @@ -174,19 +171,50 @@ impl Validate> for ResultEvent { } } -// TODO: Fix all the errors to use independent types +macro_rules! event_error_types { + ( + $( + $( #[cfg($attr:meta)] )? + pub struct $ee:ident(..) + ),* + ) => ( + $( + /// Return from a reallocation call (needed only for validation) + #[derive(Debug, Serialize, Deserialize, Clone)] + pub struct $ee(String); + + impl core::error::Error for $ee {} + impl fmt::Display for $ee { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", &self.0) + } + } + impl EventError for $ee { + fn new(t: String) -> Self where Self: Sized { Self(t) } + fn get(&self) -> &String { &self.0 } + } + )* + ); +} + +event_error_types! { + pub struct ReallocError(..), + pub struct LowerFlatError(..), + pub struct LowerMemoryError(..), + pub struct WasmFuncReturnError(..), + pub struct BuiltinError(..) +} -/// Return from a reallocation call (needed only for validation) #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ReallocReturnEvent(pub ResultEvent); +pub struct ReallocReturnEvent(pub ResultEvent); /// Return from type lowering to flat destination #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct LowerFlatReturnEvent(pub ResultEvent<()>); +pub struct LowerFlatReturnEvent(pub ResultEvent<(), LowerFlatError>); /// Return from type lowering to destination in memory #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct LowerMemoryReturnEvent(pub ResultEvent<()>); +pub struct LowerMemoryReturnEvent(pub ResultEvent<(), LowerMemoryError>); /// A return event from a Wasm component function to Host /// @@ -194,7 +222,7 @@ pub struct LowerMemoryReturnEvent(pub ResultEvent<()>); /// /// Note: Could potential merge with [`HostFuncReturnEvent`] as [`HostToWasmEvent`]? #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WasmFuncReturnEvent(pub ResultEvent); +pub struct WasmFuncReturnEvent(pub ResultEvent); impl Validate> for WasmFuncReturnEvent { fn validate(&self, expect: &Result) -> Result<(), ReplayError> { @@ -252,18 +280,11 @@ macro_rules! builtin_events { (@gen_return_events $rr_return:ident -> $($result_opts:tt)*) => { #[derive(Debug, Clone, Serialize, Deserialize)] - pub struct $rr_return(Result); + pub struct $rr_return(pub ResultEvent); impl $rr_return { - pub fn from_anyhow_result(ret: &Result) -> Self { - Self( - ret.as_ref() - .map(|t| t.clone()) - .map_err(|e| EventActionError::BuiltinError(e.to_string())), - ) - } pub fn ret(self) -> Result { - self.0.map_err(|e| e.into()) + self.0.0.map_err(|e| e.into()) } } }; diff --git a/crates/wasmtime/src/runtime/vm/component/libcalls.rs b/crates/wasmtime/src/runtime/vm/component/libcalls.rs index 9339a2a2c6..7b3444de27 100644 --- a/crates/wasmtime/src/runtime/vm/component/libcalls.rs +++ b/crates/wasmtime/src/runtime/vm/component/libcalls.rs @@ -177,7 +177,7 @@ mod trampolines { #[cfg(feature = "rr-validate")] (*$store).record_event_validation::(|| $rr_entry{ $($pname),* }.into())?; let retval = shims!(@invoke $name($store, $instance,) $($pname)*); - (*$store).record_event::(|| $rr_exit::from_anyhow_result(&retval).into())?; + (*$store).record_event::(|| $rr_exit(ResultEvent::from_anyhow_result(&retval)).into())?; retval } } @@ -630,26 +630,6 @@ 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] -fn rr_unsupported_hook(store: &mut dyn VMStore, libcall: &str) -> Result<()> { - #[cfg(feature = "rr-component")] - { - if (*store).replay_enabled() { - bail!("Replay support for libcall {libcall:?} not yet supported!"); - } - #[cfg(feature = "rr-validate")] - { - use crate::rr::marker_events::CustomMessageEvent; - (*store).record_event(|| CustomMessageEvent::from(libcall))?; - } - } - let _ = (store, libcall); - Ok(()) -} - fn resource_new32( store: &mut dyn VMStore, instance: Instance, @@ -721,7 +701,6 @@ fn resource_transfer_borrow( } fn resource_enter_call(store: &mut dyn VMStore, instance: Instance) { - rr_unsupported_hook(store, "resource_enter_call").unwrap(); instance.resource_enter_call(store) } From 742436d35bde704f7b553ab728ffe6ed2606a6b4 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Mon, 3 Nov 2025 18:49:29 +0100 Subject: [PATCH 20/73] Fix flat storage computation case limit; change debugs for builtin events --- crates/environ/src/component/types.rs | 5 ++++- .../runtime/rr/core/events/common_events.rs | 11 +++++++++- .../rr/core/events/component_events.rs | 22 ++++++++++++++----- .../src/runtime/rr/hooks/component_hooks.rs | 9 ++------ .../src/runtime/rr/hooks/core_hooks.rs | 2 +- 5 files changed, 33 insertions(+), 16 deletions(-) diff --git a/crates/environ/src/component/types.rs b/crates/environ/src/component/types.rs index c2cb4a5cb2..83fd7a829d 100644 --- a/crates/environ/src/component/types.rs +++ b/crates/environ/src/component/types.rs @@ -374,7 +374,7 @@ impl ComponentTypes { let push_storage = |storage: &mut FlatTypesStorage, other: FlatTypesStorage| -> bool { let len = usize::from(storage.len); let other_len = usize::from(other.len); - if len < (MAX_FLAT_TYPES - other_len + 1) { + if len + other_len <= MAX_FLAT_TYPES { storage.memory32[len..len + other_len] .copy_from_slice(&other.memory32[..other_len]); storage.memory64[len..len + other_len] @@ -394,6 +394,7 @@ impl ComponentTypes { storage.len = u8::try_from(MAX_FLAT_TYPES + 1).unwrap(); false } else { + // Skip 1 for discriminant let dst = storage .memory32 .iter_mut() @@ -402,6 +403,7 @@ impl ComponentTypes { for (i, ((t32, t64), (dst32, dst64))) in case .memory32 .iter() + .take(case.len as usize) .zip(case.memory64.iter()) .zip(dst) .enumerate() @@ -513,6 +515,7 @@ impl ComponentTypes { ); } } + assert!(storage.len as usize <= MAX_FLAT_TYPES); assert_eq!( storage.len as usize, self.canonical_abi(ty).flat_count(usize::MAX).unwrap() diff --git a/crates/wasmtime/src/runtime/rr/core/events/common_events.rs b/crates/wasmtime/src/runtime/rr/core/events/common_events.rs index c56a01696d..740dc55d37 100644 --- a/crates/wasmtime/src/runtime/rr/core/events/common_events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events/common_events.rs @@ -5,6 +5,7 @@ use super::*; use serde::{Deserialize, Serialize}; +use wasmtime_environ::component::FlatTypesStorage; /// A return event after a host call for a core OR component Wasm /// @@ -17,11 +18,19 @@ pub struct HostFuncReturnEvent { } impl HostFuncReturnEvent { // Record - pub fn new(args: &[MaybeUninit], flat: &[u8]) -> Self { + pub fn new_from_u8(args: &[MaybeUninit], flat: &[u8]) -> Self { Self { args: RRFuncArgVals::from_raw_slice(args, flat.iter().copied()), } } + + #[cfg(feature = "rr-component")] + pub fn new_from_flat_storage(args: &[MaybeUninit], flat: FlatTypesStorage) -> Self { + Self { + args: RRFuncArgVals::from_raw_slice(args, flat.iter32()), + } + } + // Replay /// Consume the caller event and encode it back into the slice pub fn move_into_slice(self, args: &mut [MaybeUninit]) { diff --git a/crates/wasmtime/src/runtime/rr/core/events/component_events.rs b/crates/wasmtime/src/runtime/rr/core/events/component_events.rs index 6d10ab34f8..65b39f312c 100644 --- a/crates/wasmtime/src/runtime/rr/core/events/component_events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events/component_events.rs @@ -160,9 +160,9 @@ where } // Return the recorded error (Err(e), Err(f)) => Err(ReplayError::from(E::new(format!( - "Error from recording: {} \r\n| Error on execution: {}", - e.get(), - f + "Error on execution: {} | Error from recording: {}", + f, + e.get() )))), // Diverging errors.. Report as a failed validation (Ok(_), Err(_)) => Err(ReplayError::FailedValidation), @@ -251,7 +251,7 @@ macro_rules! builtin_events { // All things related to BuiltinReturnEvent enum (@gen_return_enum $($rr_var:ident $event:ident)*) => { - #[derive(Debug, Clone, Serialize, Deserialize)] + #[derive(Clone, Serialize, Deserialize)] pub enum BuiltinReturnEvent { $($rr_var($event),)* } @@ -261,7 +261,7 @@ macro_rules! builtin_events { // All things related to BuiltinEntryEvent enum (@gen_entry_enum $($rr_var:ident $event:ident)*) => { // PartialEq gives all these events `Validate` - #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + #[derive(Clone, PartialEq, Serialize, Deserialize)] pub enum BuiltinEntryEvent { $($rr_var($event),)* } @@ -291,7 +291,8 @@ macro_rules! builtin_events { // Stubbed if `rr_builtin` not provided (@gen_return_events -> $($result_opts:tt)*) => {}; - // Conversion to/from specific return `$event` and `BuiltinEntryEvent` + // Debug traits for $enum (BuiltinReturnEvent/BuiltinEntryEvent) and + // conversion to/from specific `$event` to `$enum` (@from_impls $enum:ident $($rr_var:ident $event:ident)*) => { $( impl From<$event> for $enum { @@ -313,6 +314,15 @@ macro_rules! builtin_events { } } )* + + impl fmt::Debug for $enum { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut res = f.debug_tuple(stringify!($enum)); + match self { + $(Self::$rr_var(e) => res.field(e),)* + }.finish() + } + } }; // Return first value diff --git a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs index 16605ab3d1..111fca28d0 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs @@ -62,11 +62,6 @@ where store .0 .next_replay_event_validation::>(&result)?; - //// TODO: After adding validation support, replay with `next_replay_event_validation` - //store.0.next_replay_event_and(|_r: WasmFuncReturnEvent| { - // log::warn!("Yet to implement validation for WasmFuncReturnEvent; skipping for now"); - // Ok(()) - //})?; result?; return Ok(()); } @@ -104,8 +99,8 @@ pub fn record_host_func_return( ) -> Result<()> { #[cfg(feature = "rr-component")] { - let flat_results = types.flat_types_storage(&ty).iter32().collect::>(); - store.record_event(|| HostFuncReturnEvent::new(args, flat_results.as_slice()))?; + let flat_results = types.flat_types_storage(&ty); + store.record_event(|| HostFuncReturnEvent::new_from_flat_storage(args, flat_results))?; } let _ = (args, types, ty, store); Ok(()) diff --git a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs index b40cda29b5..07713e1afc 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs @@ -36,7 +36,7 @@ pub fn record_host_func_return( ) -> Result<()> { // Record the return values #[cfg(feature = "rr")] - store.record_event(|| HostFuncReturnEvent::new(&args, flat))?; + store.record_event(|| HostFuncReturnEvent::new_from_u8(&args, flat))?; let _ = (args, flat, ty, store); Ok(()) } From a69d77ecd6a8bc298601609daf6f820824868936 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Mon, 3 Nov 2025 19:27:11 +0100 Subject: [PATCH 21/73] Optimize fast path for recording by avoiding flat type computation --- .../rr/core/events/component_events.rs | 6 +++--- .../src/runtime/rr/hooks/component_hooks.rs | 21 ++++++++++++------- .../src/runtime/rr/hooks/core_hooks.rs | 7 ++++--- crates/wasmtime/src/runtime/store.rs | 7 ++++--- 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/crates/wasmtime/src/runtime/rr/core/events/component_events.rs b/crates/wasmtime/src/runtime/rr/core/events/component_events.rs index 65b39f312c..af47fe33d6 100644 --- a/crates/wasmtime/src/runtime/rr/core/events/component_events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events/component_events.rs @@ -224,9 +224,9 @@ pub struct LowerMemoryReturnEvent(pub ResultEvent<(), LowerMemoryError>); #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WasmFuncReturnEvent(pub ResultEvent); -impl Validate> for WasmFuncReturnEvent { - fn validate(&self, expect: &Result) -> Result<(), ReplayError> { - self.0.validate(expect) +impl Validate<&Result> for WasmFuncReturnEvent { + fn validate(&self, expect: &&Result) -> Result<(), ReplayError> { + self.0.validate(*expect) } } diff --git a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs index 111fca28d0..84c30175bb 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs @@ -61,7 +61,9 @@ where })?; store .0 - .next_replay_event_validation::>(&result)?; + .next_replay_event_validation::>( + || &result, + )?; result?; return Ok(()); } @@ -80,10 +82,13 @@ pub fn record_replay_host_func_entry( #[cfg(all(feature = "rr-component", feature = "rr-validate"))] { use crate::rr::component_events::HostFuncEntryEvent; - let flat_params = types.flat_types_storage(&InterfaceType::Tuple(types[*type_idx].params)); - let event = HostFuncEntryEvent::new(args, flat_params, type_idx.clone()); - store.record_event_validation(|| event.clone())?; - store.next_replay_event_validation::(&event)?; + let event = || { + let flat_params = + types.flat_types_storage(&InterfaceType::Tuple(types[*type_idx].params)); + HostFuncEntryEvent::new(args, flat_params, type_idx.clone()) + }; + store.record_event_validation(|| event())?; + store.next_replay_event_validation::(|| event())?; } let _ = (args, types, type_idx, store); Ok(()) @@ -98,10 +103,10 @@ pub fn record_host_func_return( store: &mut StoreOpaque, ) -> Result<()> { #[cfg(feature = "rr-component")] - { + store.record_event(|| { let flat_results = types.flat_types_storage(&ty); - store.record_event(|| HostFuncReturnEvent::new_from_flat_storage(args, flat_results))?; - } + HostFuncReturnEvent::new_from_flat_storage(args, flat_results) + })?; let _ = (args, types, ty, store); Ok(()) } diff --git a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs index 07713e1afc..1d99e35c26 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs @@ -18,9 +18,10 @@ pub fn record_replay_host_func_entry( { // Record/replay the raw parameter args use crate::rr::core_events::HostFuncEntryEvent; - let event = HostFuncEntryEvent::new(&args, flat, ty.clone()); - store.record_event_validation(|| event.clone())?; - store.next_replay_event_validation::(&event)?; + store.record_event_validation(|| HostFuncEntryEvent::new(&args, flat, ty.clone()))?; + store.next_replay_event_validation::(|| { + HostFuncEntryEvent::new(&args, flat, ty.clone()) + })?; } let _ = (args, flat, ty, store); Ok(()) diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index 08f99812f6..16338ad65d 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -1576,16 +1576,17 @@ impl StoreOpaque { /// Convenience wrapper around [`Replayer::next_event_validation`] #[cfg(feature = "rr-validate")] #[inline] - pub(crate) fn next_replay_event_validation( + pub(crate) fn next_replay_event_validation( &mut self, - expect: &Y, + expect: F, ) -> Result<(), ReplayError> where T: TryFrom + Validate, + F: FnOnce() -> Y, ReplayError: From<>::Error>, { if let Some(buf) = self.replay_buffer_mut() { - buf.next_event_validation::(expect) + buf.next_event_validation::(&expect()) } else { Ok(()) } From 1b94378a7093ac4719af5b4ba839f5f3de14f572 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Thu, 6 Nov 2025 11:48:44 -0500 Subject: [PATCH 22/73] Fix: Lowering logic rr for overflow of MAX_FLAT_PARAMS --- crates/environ/src/component/types.rs | 239 +++++++++--------- .../src/runtime/component/func/options.rs | 35 +++ .../src/runtime/component/func/typed.rs | 16 +- crates/wasmtime/src/runtime/rr/core.rs | 3 +- .../src/runtime/rr/hooks/component_hooks.rs | 20 +- 5 files changed, 188 insertions(+), 125 deletions(-) diff --git a/crates/environ/src/component/types.rs b/crates/environ/src/component/types.rs index 83fd7a829d..09bda7bec3 100644 --- a/crates/environ/src/component/types.rs +++ b/crates/environ/src/component/types.rs @@ -364,68 +364,99 @@ impl ComponentTypes { } } - /// Returns the flat storage representation for an interface type. - pub fn flat_types_storage(&self, ty: &InterfaceType) -> FlatTypesStorage { - let mut storage_buf = FlatTypesStorage::new(); + /// Returns the flat storage ABI representation for an interface type. + /// If the flat representation is larger than `limit` number of flat types, returns + /// empty storage. + /// + /// The intention of this method is to determine the flat ABI on host-to-wasm + /// transitions (return from hostcall, or entry into wasmcall). When the type is + /// not encodable in flat types, the values are all lowered to memory, implied by + /// the empty storage + pub fn flat_types_storage(&self, ty: &InterfaceType, limit: usize) -> FlatTypesStorage { + self.flat_types_storage_inner(ty, limit) + .unwrap_or_else(|| FlatTypesStorage::new()) + } - let push_discrim = - |storage: &mut FlatTypesStorage| storage.push(FlatType::I32, FlatType::I32); - - let push_storage = |storage: &mut FlatTypesStorage, other: FlatTypesStorage| -> bool { - let len = usize::from(storage.len); - let other_len = usize::from(other.len); - if len + other_len <= MAX_FLAT_TYPES { - storage.memory32[len..len + other_len] - .copy_from_slice(&other.memory32[..other_len]); - storage.memory64[len..len + other_len] - .copy_from_slice(&other.memory64[..other_len]); - storage.len += other.len; - true - } else { - storage.len = MAX_FLAT_TYPES as u8 + 1; - false - } + fn flat_types_storage_inner( + &self, + ty: &InterfaceType, + limit: usize, + ) -> Option { + // Helper routines + let push = |storage: &mut FlatTypesStorage, t32: FlatType, t64: FlatType| -> bool { + storage.push(t32, t64); + (storage.len as usize) < limit }; + let push_discrim = |storage: &mut FlatTypesStorage| -> bool { + push(storage, FlatType::I32, FlatType::I32) + }; + + let push_storage = + |storage: &mut FlatTypesStorage, other: Option| -> bool { + if let Some(other) = other { + let len = usize::from(storage.len); + let other_len = usize::from(other.len); + if len + other_len <= limit { + storage.memory32[len..len + other_len] + .copy_from_slice(&other.memory32[..other_len]); + storage.memory64[len..len + other_len] + .copy_from_slice(&other.memory64[..other_len]); + storage.len += other.len; + true + } else { + false + } + } else { + false + } + }; + let push_storage_variant_case = - |storage: &mut FlatTypesStorage, case: Option| -> bool { + |storage: &mut FlatTypesStorage, case: Option>| -> bool { if let Some(case) = case { - if case.len as usize >= MAX_FLAT_TYPES { - storage.len = u8::try_from(MAX_FLAT_TYPES + 1).unwrap(); - false - } else { - // Skip 1 for discriminant - let dst = storage - .memory32 - .iter_mut() - .zip(&mut storage.memory64) - .skip(1); - for (i, ((t32, t64), (dst32, dst64))) in case - .memory32 - .iter() - .take(case.len as usize) - .zip(case.memory64.iter()) - .zip(dst) - .enumerate() - { - if i + 1 < usize::from(storage.len) { - // Populated Index - dst32.join(*t32); - dst64.join(*t64); - } else { - // New Index - storage.len += 1; - *dst32 = *t32; - *dst64 = *t64; + if let Some(case) = case { + // Discriminant will make size[case] = limit overshoot + if case.len as usize >= limit { + false + } else { + // Skip 1 for discriminant + let dst = storage + .memory32 + .iter_mut() + .zip(&mut storage.memory64) + .skip(1); + for (i, ((t32, t64), (dst32, dst64))) in case + .memory32 + .iter() + .take(case.len as usize) + .zip(case.memory64.iter()) + .zip(dst) + .enumerate() + { + if i + 1 < usize::from(storage.len) { + // Populated Index + dst32.join(*t32); + dst64.join(*t64); + } else { + // New Index + storage.len += 1; + *dst32 = *t32; + *dst64 = *t64; + } } + true } + } else { true } } else { - true + false } }; + // Logic + let mut storage_buf = FlatTypesStorage::new(); let storage = &mut storage_buf; match ty { @@ -442,85 +473,65 @@ impl ComponentTypes { | InterfaceType::Stream(_) | InterfaceType::ErrorContext(_) | InterfaceType::Borrow(_) - | InterfaceType::Enum(_) => { - storage.push(FlatType::I32, FlatType::I32); - } + | InterfaceType::Enum(_) => push(storage, FlatType::I32, FlatType::I32), - InterfaceType::U64 | InterfaceType::S64 => { - storage.push(FlatType::I64, FlatType::I64); - } - InterfaceType::Float32 => { - storage.push(FlatType::F32, FlatType::F32); - } - InterfaceType::Float64 => { - storage.push(FlatType::F64, FlatType::F64); - } + InterfaceType::U64 | InterfaceType::S64 => push(storage, FlatType::I64, FlatType::I64), + InterfaceType::Float32 => push(storage, FlatType::F32, FlatType::F32), + InterfaceType::Float64 => push(storage, FlatType::F64, FlatType::F64), InterfaceType::String | InterfaceType::List(_) => { // Pointer pair - storage.push(FlatType::I32, FlatType::I64); - storage.push(FlatType::I32, FlatType::I64); + push(storage, FlatType::I32, FlatType::I64) + && push(storage, FlatType::I32, FlatType::I64) } - InterfaceType::Record(i) => { - for field in self[*i].fields.iter() { - if !push_storage(storage, self.flat_types_storage(&field.ty)) { - break; - } - } - } - InterfaceType::Tuple(i) => { - for ty in self[*i].types.iter() { - if !push_storage(storage, self.flat_types_storage(ty)) { - break; - } - } - } + InterfaceType::Record(i) => self[*i].fields.iter().all(|field| { + push_storage(storage, self.flat_types_storage_inner(&field.ty, limit)) + }), + InterfaceType::Tuple(i) => self[*i] + .types + .iter() + .all(|field| push_storage(storage, self.flat_types_storage_inner(field, limit))), InterfaceType::Flags(i) => match FlagsSize::from_count(self[*i].names.len()) { - FlagsSize::Size0 => {} - FlagsSize::Size1 | FlagsSize::Size2 => { - storage.push(FlatType::I32, FlatType::I32); - } - FlagsSize::Size4Plus(n) => { - for _ in 0..n { - if !storage.push(FlatType::I32, FlatType::I32) { - break; - } - } - } + FlagsSize::Size0 => true, + FlagsSize::Size1 | FlagsSize::Size2 => push(storage, FlatType::I32, FlatType::I32), + FlagsSize::Size4Plus(n) => (0..n) + .into_iter() + .all(|_| push(storage, FlatType::I32, FlatType::I32)), }, InterfaceType::Variant(i) => { - push_discrim(storage); - for case in self[*i].cases.values() { - let case_flat = case.as_ref().map(|ty| self.flat_types_storage(ty)); - if !push_storage_variant_case(storage, case_flat) { - break; - } - } + push_discrim(storage) + && self[*i].cases.values().all(|case| { + let case_flat = case + .as_ref() + .map(|ty| self.flat_types_storage_inner(ty, limit)); + push_storage_variant_case(storage, case_flat) + }) } InterfaceType::Option(i) => { - push_discrim(storage); - push_storage_variant_case(storage, None); - push_storage_variant_case(storage, Some(self.flat_types_storage(&self[*i].ty))); + push_discrim(storage) + && push_storage_variant_case(storage, Some(None)) + && push_storage_variant_case( + storage, + Some(self.flat_types_storage_inner(&self[*i].ty, limit)), + ) } InterfaceType::Result(i) => { - push_discrim(storage); - let result = &self[*i]; - push_storage_variant_case( - storage, - result.ok.map(|ty| self.flat_types_storage(&ty)), - ); - push_storage_variant_case( - storage, - result.err.map(|ty| self.flat_types_storage(&ty)), - ); + push_discrim(storage) + && push_storage_variant_case( + storage, + self[*i] + .ok + .map(|ty| self.flat_types_storage_inner(&ty, limit)), + ) + && push_storage_variant_case( + storage, + self[*i] + .err + .map(|ty| self.flat_types_storage_inner(&ty, limit)), + ) } } - assert!(storage.len as usize <= MAX_FLAT_TYPES); - assert_eq!( - storage.len as usize, - self.canonical_abi(ty).flat_count(usize::MAX).unwrap() - ); - storage_buf + .then_some(storage_buf) } /// Adds a new `table` to the list of resource tables for this component. diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index 04b469f0e9..21d688c7ec 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -560,6 +560,40 @@ impl<'a, T: 'static> LowerContext<'a, T> { ) } + /// Perform a replay of only [`ReallocEntryEvent`] + [`ReallocReturnEvent`] events + /// + /// Panics if replay not enabled + #[cfg(feature = "rr-component")] + pub fn replay_realloc(&mut self) -> Result { + let get_event = |cx: &mut Self| cx.store.0.replay_buffer_mut().unwrap().next_event(); + let (record_has_validation, _replay_validate) = { + let buf = self.store.0.replay_buffer_mut().unwrap(); + (buf.trace_settings().add_validation, buf.settings().validate) + }; + + let ptr = match get_event(self)? { + RREvent::ComponentReallocEntry(e) => { + self.realloc_inner(e.old_addr, e.old_size, e.old_align, e.new_size) + } + _ => bail!(ReplayError::IncorrectEventVariant), + }; + + if record_has_validation { + match get_event(self)? { + RREvent::ComponentReallocReturn(e) => + { + #[cfg(feature = "rr-validate")] + if _replay_validate { + e.0.validate(&ptr)? + } + } + _ => bail!(ReplayError::IncorrectEventVariant), + }; + } + + ptr + } + /// Perform a replay of all the type lowering-associated events for this context /// /// These typically include all `Lower*` and `Realloc*` event, along with the putting @@ -570,6 +604,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { /// ## Important Notes /// /// * It is assumed that this is only invoked at the root lower/store calls + /// * Panics if invoked while replay is not enabled /// #[cfg(feature = "rr-component")] pub fn replay_lowering( diff --git a/crates/wasmtime/src/runtime/component/func/typed.rs b/crates/wasmtime/src/runtime/component/func/typed.rs index f8902f622c..e1a49ee541 100644 --- a/crates/wasmtime/src/runtime/component/func/typed.rs +++ b/crates/wasmtime/src/runtime/component/func/typed.rs @@ -490,19 +490,25 @@ where // // Note that `realloc` will bake in a check that the returned pointer is // in-bounds. - let ptr = cx.realloc(0, 0, Params::ALIGN32, Params::SIZE32)?; - - if cx.store.0.replay_enabled() { + let ptr = if cx.store.0.replay_enabled() { #[cfg(feature = "rr-component")] - cx.replay_lowering(None, component_hooks::ReplayLoweringPhase::WasmFuncEntry)? + { + let ptr = cx.replay_realloc()?; + cx.replay_lowering(None, component_hooks::ReplayLoweringPhase::WasmFuncEntry)?; + ptr + } + #[cfg(not(feature = "rr-component"))] + unreachable!() } else { + let ptr = cx.realloc(0, 0, Params::ALIGN32, Params::SIZE32)?; component_hooks::record_lower_memory( |cx, ty, ptr| params.linear_lower_to_memory(cx, ty, ptr), cx, ty, ptr, )?; - } + ptr + }; // Note that the pointer here is stored as a 64-bit integer. This allows // this to work with either 32 or 64-bit memories. For a 32-bit memory diff --git a/crates/wasmtime/src/runtime/rr/core.rs b/crates/wasmtime/src/runtime/rr/core.rs index ec47e0d042..892dea5d3c 100644 --- a/crates/wasmtime/src/runtime/rr/core.rs +++ b/crates/wasmtime/src/runtime/rr/core.rs @@ -108,6 +108,7 @@ macro_rules! rr_event { if let RREvent::$variant(x) = value { Ok(x) } else { + log::error!("Expected {}; got {}", stringify!($event), value); Err(ReplayError::IncorrectEventVariant) } } @@ -290,7 +291,7 @@ pub trait Replayer: Iterator { fn next_event(&mut self) -> Result { let event = self.next().ok_or(ReplayError::EmptyBuffer); if let Ok(e) = &event { - log::debug!("Replay Event => {}", e); + log::debug!("Read replay event => {}", e); } event } diff --git a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs index 84c30175bb..ef11c9a1b6 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs @@ -15,6 +15,8 @@ use alloc::sync::Arc; use core::mem::MaybeUninit; #[cfg(feature = "component-model")] use wasmtime_environ::component::{ComponentTypes, ExportIndex, InterfaceType, TypeFuncIndex}; +#[cfg(all(feature = "rr-component"))] +use wasmtime_environ::component::{MAX_FLAT_PARAMS, MAX_FLAT_RESULTS}; /// Indicator type signalling the context during lowering #[cfg(feature = "rr-component")] @@ -43,11 +45,17 @@ where #[cfg(feature = "rr-component")] let types = component.types(); #[cfg(all(feature = "rr-component", feature = "rr-validate"))] - let flat_results = types.flat_types_storage(&InterfaceType::Tuple(types[type_idx].results)); + let flat_results = types.flat_types_storage( + &InterfaceType::Tuple(types[type_idx].results), + MAX_FLAT_RESULTS, + ); #[cfg(feature = "rr-component")] { let checksum = *component.checksum(); - let flat_params = types.flat_types_storage(&InterfaceType::Tuple(types[type_idx].params)); + let flat_params = types.flat_types_storage( + &InterfaceType::Tuple(types[type_idx].params), + MAX_FLAT_PARAMS, + ); store .0 .record_event(|| WasmFuncEntryEvent::new(args, flat_params, checksum, func_idx))?; @@ -83,8 +91,10 @@ pub fn record_replay_host_func_entry( { use crate::rr::component_events::HostFuncEntryEvent; let event = || { - let flat_params = - types.flat_types_storage(&InterfaceType::Tuple(types[*type_idx].params)); + let flat_params = types.flat_types_storage( + &InterfaceType::Tuple(types[*type_idx].params), + MAX_FLAT_PARAMS, + ); HostFuncEntryEvent::new(args, flat_params, type_idx.clone()) }; store.record_event_validation(|| event())?; @@ -104,7 +114,7 @@ pub fn record_host_func_return( ) -> Result<()> { #[cfg(feature = "rr-component")] store.record_event(|| { - let flat_results = types.flat_types_storage(&ty); + let flat_results = types.flat_types_storage(&ty, MAX_FLAT_RESULTS); HostFuncReturnEvent::new_from_flat_storage(args, flat_results) })?; let _ = (args, types, ty, store); From b15ae6d2bd4707647c2707db6ce852000921483a Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Fri, 7 Nov 2025 13:54:52 -0500 Subject: [PATCH 23/73] Fix: Bug in flat storage variant computation --- crates/environ/src/component/types.rs | 110 +++++++++--------- .../src/runtime/rr/hooks/component_hooks.rs | 17 ++- 2 files changed, 64 insertions(+), 63 deletions(-) diff --git a/crates/environ/src/component/types.rs b/crates/environ/src/component/types.rs index 09bda7bec3..7341c676af 100644 --- a/crates/environ/src/component/types.rs +++ b/crates/environ/src/component/types.rs @@ -373,6 +373,10 @@ impl ComponentTypes { /// not encodable in flat types, the values are all lowered to memory, implied by /// the empty storage pub fn flat_types_storage(&self, ty: &InterfaceType, limit: usize) -> FlatTypesStorage { + assert!( + limit <= MAX_FLAT_TYPES, + "limit exceeding maximum flat types not allowed" + ); self.flat_types_storage_inner(ty, limit) .unwrap_or_else(|| FlatTypesStorage::new()) } @@ -385,7 +389,7 @@ impl ComponentTypes { // Helper routines let push = |storage: &mut FlatTypesStorage, t32: FlatType, t64: FlatType| -> bool { storage.push(t32, t64); - (storage.len as usize) < limit + (storage.len as usize) <= limit }; let push_discrim = |storage: &mut FlatTypesStorage| -> bool { @@ -394,64 +398,62 @@ impl ComponentTypes { let push_storage = |storage: &mut FlatTypesStorage, other: Option| -> bool { - if let Some(other) = other { - let len = usize::from(storage.len); - let other_len = usize::from(other.len); - if len + other_len <= limit { - storage.memory32[len..len + other_len] - .copy_from_slice(&other.memory32[..other_len]); - storage.memory64[len..len + other_len] - .copy_from_slice(&other.memory64[..other_len]); - storage.len += other.len; - true - } else { - false - } - } else { - false - } + other + .and_then(|other| { + let len = usize::from(storage.len); + let other_len = usize::from(other.len); + (len + other_len <= limit).then(|| { + storage.memory32[len..len + other_len] + .copy_from_slice(&other.memory32[..other_len]); + storage.memory64[len..len + other_len] + .copy_from_slice(&other.memory64[..other_len]); + storage.len += other.len; + }) + }) + .is_some() }; + // Case is broken down as: + // * None => No field + // * Some(None) => Invalid storage (overflow) + // * Some(storage) => Valid storage let push_storage_variant_case = |storage: &mut FlatTypesStorage, case: Option>| -> bool { - if let Some(case) = case { - if let Some(case) = case { - // Discriminant will make size[case] = limit overshoot - if case.len as usize >= limit { - false - } else { - // Skip 1 for discriminant - let dst = storage - .memory32 - .iter_mut() - .zip(&mut storage.memory64) - .skip(1); - for (i, ((t32, t64), (dst32, dst64))) in case - .memory32 - .iter() - .take(case.len as usize) - .zip(case.memory64.iter()) - .zip(dst) - .enumerate() - { - if i + 1 < usize::from(storage.len) { - // Populated Index - dst32.join(*t32); - dst64.join(*t64); - } else { - // New Index - storage.len += 1; - *dst32 = *t32; - *dst64 = *t64; + match case { + None => true, + Some(case) => { + case.and_then(|case| { + // Discriminant will make size[case] = limit overshoot + ((1 + case.len as usize) <= limit).then(|| { + // Skip 1 for discriminant + let dst = storage + .memory32 + .iter_mut() + .zip(&mut storage.memory64) + .skip(1); + for (i, ((t32, t64), (dst32, dst64))) in case + .memory32 + .iter() + .take(case.len as usize) + .zip(case.memory64.iter()) + .zip(dst) + .enumerate() + { + if i + 1 < usize::from(storage.len) { + // Populated Index + dst32.join(*t32); + dst64.join(*t64); + } else { + // New Index + storage.len += 1; + *dst32 = *t32; + *dst64 = *t64; + } } - } - true - } - } else { - true + }) + }) + .is_some() } - } else { - false } }; @@ -509,7 +511,7 @@ impl ComponentTypes { } InterfaceType::Option(i) => { push_discrim(storage) - && push_storage_variant_case(storage, Some(None)) + && push_storage_variant_case(storage, None) && push_storage_variant_case( storage, Some(self.flat_types_storage_inner(&self[*i].ty, limit)), diff --git a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs index ef11c9a1b6..009d626d74 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs @@ -41,16 +41,9 @@ where { let _ = (args, id, func_idx, type_idx); #[cfg(feature = "rr-component")] - let component = id.get(store.0).component(); - #[cfg(feature = "rr-component")] - let types = component.types(); - #[cfg(all(feature = "rr-component", feature = "rr-validate"))] - let flat_results = types.flat_types_storage( - &InterfaceType::Tuple(types[type_idx].results), - MAX_FLAT_RESULTS, - ); - #[cfg(feature = "rr-component")] { + let component = id.get(store.0).component(); + let types = component.types(); let checksum = *component.checksum(); let flat_params = types.flat_types_storage( &InterfaceType::Tuple(types[type_idx].params), @@ -63,6 +56,12 @@ where let result = wasm_call(store); #[cfg(all(feature = "rr-component", feature = "rr-validate"))] { + let component = id.get(store.0).component(); + let types = component.types(); + let flat_results = types.flat_types_storage( + &InterfaceType::Tuple(types[type_idx].results), + MAX_FLAT_RESULTS, + ); let result = result.map(|_| RRFuncArgVals::from_raw_slice(args, flat_results.iter32())); store.0.record_event_validation(|| { WasmFuncReturnEvent(ResultEvent::from_anyhow_result(&result)) From 952627ac558d99c66c0cab1a81244cd69fee49e7 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 11 Nov 2025 12:23:17 -0500 Subject: [PATCH 24/73] Initial factoring out of replay driver Things left to complete: * Core wasm support * Improved error handling * Wasmtime CLI integration --- crates/cli-flags/src/lib.rs | 2 +- crates/wasmtime/src/config.rs | 74 +++---- crates/wasmtime/src/engine.rs | 17 +- crates/wasmtime/src/runtime.rs | 4 +- crates/wasmtime/src/runtime/component/func.rs | 24 ++- .../src/runtime/component/func/host.rs | 4 +- .../src/runtime/component/instance.rs | 26 +-- crates/wasmtime/src/runtime/component/mod.rs | 2 +- .../wasmtime/src/runtime/component/store.rs | 3 +- crates/wasmtime/src/runtime/func.rs | 8 +- crates/wasmtime/src/runtime/rr.rs | 10 +- crates/wasmtime/src/runtime/rr/core.rs | 102 ++++++--- crates/wasmtime/src/runtime/rr/core/events.rs | 4 +- .../rr/core/events/component_events.rs | 40 ++-- crates/wasmtime/src/runtime/rr/core/io.rs | 14 +- .../src/runtime/rr/hooks/component_hooks.rs | 19 +- .../wasmtime/src/runtime/rr/replay_driver.rs | 198 ++++++++++++++++++ crates/wasmtime/src/runtime/store.rs | 4 +- 18 files changed, 397 insertions(+), 158 deletions(-) create mode 100644 crates/wasmtime/src/runtime/rr/replay_driver.rs diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs index 17319ff34e..9ccb45b27f 100644 --- a/crates/cli-flags/src/lib.rs +++ b/crates/cli-flags/src/lib.rs @@ -1027,7 +1027,7 @@ impl CommonOptions { _v => (), _ => err, } - config.recording(true); + config.rr(wasmtime::RRConfig::Recording); }, _ => err, } diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index e193acc33e..d21033d8c6 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -110,6 +110,17 @@ impl ModuleVersionStrategy { } } +/// Configuration for record/replay +#[derive(Clone)] +pub enum RRConfig { + /// Recording on store is enabled + Recording, + /// Replaying on store is enabled + Replaying, + /// No record/replay is enabled + None, +} + /// Global configuration options used to create an [`Engine`](crate::Engine) /// and customize its behavior. /// @@ -175,9 +186,7 @@ pub struct Config { pub(crate) macos_use_mach_ports: bool, pub(crate) detect_host_feature: Option Option>, #[cfg(feature = "rr")] - pub(crate) record_support: bool, - #[cfg(feature = "rr")] - pub(crate) replay_support: bool, + pub(crate) rr_config: RRConfig, } /// User-provided configuration for the compiler. @@ -287,9 +296,7 @@ impl Config { #[cfg(not(feature = "std"))] detect_host_feature: None, #[cfg(feature = "rr")] - record_support: false, - #[cfg(feature = "rr")] - replay_support: false, + rr_config: RRConfig::None, }; #[cfg(any(feature = "cranelift", feature = "winch"))] { @@ -2705,11 +2712,9 @@ impl Config { self } - /// Enforce deterministic execution configurations. Currently, means the following: + /// Enforce deterministic execution configurations. Currently, this 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. #[inline] pub fn enforce_determinism(&mut self) -> &mut Self { #[cfg(any(feature = "cranelift", feature = "winch"))] @@ -2718,56 +2723,29 @@ impl Config { self } - /// Remove determinstic execution enforcements (if any) applied - /// by [`Config::enforce_determinism`]. - #[inline] - pub fn remove_determinism_enforcement(&mut self) -> &mut Self { - #[cfg(any(feature = "cranelift", feature = "winch"))] - self.cranelift_nan_canonicalization(false); - self.relaxed_simd_deterministic(false); - self - } - - /// Enable execution trace recording with the provided configuration. + /// Enable execution trace recording or replaying to the configuration /// - /// This method implicitly enforces determinism (see [`Config::enforce_determinism`] - /// for details). + /// When either recording/replaying are enabled, determinism is implicitly + /// enforced (see [`Config::enforce_determinism`] for details) #[cfg(feature = "rr")] #[inline] - pub fn recording(&mut self, enable: bool) -> &mut Self { - if enable { - self.enforce_determinism(); - } else { - self.remove_determinism_enforcement(); - } - self.record_support = enable; - self - } - - /// Enable execution trace replaying with the provided configuration. - /// - /// This method implicitly enforces determinism (see [`Config::enforce_determinism`] - /// for details). - #[cfg(feature = "rr")] - #[inline] - pub fn replaying(&mut self, enable: bool) -> &mut Self { - if enable { - self.enforce_determinism(); - } else { - self.remove_determinism_enforcement(); + pub fn rr(&mut self, cfg: RRConfig) -> &mut Self { + self.rr_config = cfg; + match self.rr_config { + RRConfig::Recording | RRConfig::Replaying => self.enforce_determinism(), + _ => self, } - self.replay_support = enable; - self } /// Evaluates to true if current configuration must respect /// deterministic execution in its configuration. - /// - /// Required for faithful record/replay execution. #[cfg(feature = "rr")] #[inline] pub fn is_determinism_enforced(&mut self) -> bool { - self.record_support || self.replay_support + match self.rr_config { + RRConfig::Recording | RRConfig::Replaying => true, + RRConfig::None => false, + } } } diff --git a/crates/wasmtime/src/engine.rs b/crates/wasmtime/src/engine.rs index 97926d42a7..5a81056e59 100644 --- a/crates/wasmtime/src/engine.rs +++ b/crates/wasmtime/src/engine.rs @@ -1,4 +1,6 @@ use crate::Config; +#[cfg(feature = "rr")] +use crate::RRConfig; use crate::prelude::*; #[cfg(feature = "runtime")] pub use crate::runtime::code_memory::CustomCodeMemory; @@ -258,7 +260,20 @@ impl Engine { #[cfg(feature = "rr")] #[inline] pub fn is_recording(&self) -> bool { - self.config().record_support + match self.config().rr_config { + RRConfig::Recording => true, + _ => false, + } + } + + /// Returns whether the engine is configured to support execution replaying + #[cfg(feature = "rr")] + #[inline] + pub fn is_replaying(&self) -> bool { + match self.config().rr_config { + RRConfig::Replaying => true, + _ => false, + } } /// Detects whether the bytes provided are a precompiled object produced by diff --git a/crates/wasmtime/src/runtime.rs b/crates/wasmtime/src/runtime.rs index 0a6482687b..8c7a8e71c9 100644 --- a/crates/wasmtime/src/runtime.rs +++ b/crates/wasmtime/src/runtime.rs @@ -88,7 +88,9 @@ pub use memory::*; pub use module::{Module, ModuleExport}; pub use resources::*; #[cfg(feature = "rr")] -pub use rr::{RecordSettings, RecordWriter, ReplayReader, ReplaySettings}; +pub use rr::{ + RecordSettings, RecordWriter, ReplayEnvironment, ReplayInstance, ReplayReader, ReplaySettings, +}; #[cfg(all(feature = "async", feature = "call-hook"))] pub use store::CallHookHandler; pub use store::{ diff --git a/crates/wasmtime/src/runtime/component/func.rs b/crates/wasmtime/src/runtime/component/func.rs index 8d220b5e6e..fa1811ffec 100644 --- a/crates/wasmtime/src/runtime/component/func.rs +++ b/crates/wasmtime/src/runtime/component/func.rs @@ -534,7 +534,7 @@ impl Func { /// `LowerParams` and `LowerReturn` type. They must match the type of `self` /// for the params/results that are going to be produced. Additionally /// these types must be representable with a sequence of `ValRaw` values. - unsafe fn call_raw( + pub(crate) unsafe fn call_raw( &self, mut store: StoreContextMut<'_, T>, lower: impl FnOnce( @@ -548,6 +548,19 @@ impl Func { LowerParams: Copy, LowerReturn: Copy, { + #[cfg(feature = "rr-component")] + { + use crate::rr::component_events::WasmFuncBeginEvent; + + let component = *self.instance.id().get(store.0).component().checksum(); + let instance = self.instance.id().instance(); + let func_idx = self.index; + store.0.record_event(|| WasmFuncBeginEvent { + component, + instance, + func_idx, + })?; + } let export = self.lifted_core_func(store.0); #[repr(C)] @@ -592,12 +605,11 @@ impl Func { )) .unwrap(); - component_hooks::record_replay_wasm_func( + component_hooks::record_and_replay_validate_wasm_func( |store| crate::Func::call_unchecked_raw(store, export, params_and_returns), params_and_returns.as_ref(), - self.index, self.abi_info(store.0).2, - self.instance.id(), + self.instance.id().get(store.0).component().types().clone(), &mut store, )?; } @@ -810,7 +822,7 @@ impl Func { Ok(()) } - fn lift_results<'a, 'b>( + pub(crate) fn lift_results<'a, 'b>( cx: &'a mut LiftContext<'b>, results_ty: InterfaceType, src: &'a [ValRaw], @@ -875,7 +887,7 @@ impl Func { /// The `lower` closure provided should perform the actual lowering and /// return the result of the lowering operation which is then returned from /// this function as well. - fn with_lower_context( + pub(crate) fn with_lower_context( self, mut store: StoreContextMut, may_enter: bool, diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index de5a75a04b..031f5d5913 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -257,7 +257,7 @@ where let types = vminstance.component().types().clone(); - rr::component_hooks::record_replay_host_func_entry(storage, &types, &ty, store.0)?; + rr::component_hooks::record_and_replay_validate_host_func_entry(storage, &types, &ty, store.0)?; let func_ty = &types[ty]; let param_tys = InterfaceType::Tuple(func_ty.params); @@ -813,7 +813,7 @@ where let types = instance.id().get(store.0).component().types().clone(); - rr::component_hooks::record_replay_host_func_entry(storage, &types, &ty, store.0)?; + rr::component_hooks::record_and_replay_validate_host_func_entry(storage, &types, &ty, store.0)?; let func_ty = &types[ty]; let param_tys = &types[func_ty.params]; diff --git a/crates/wasmtime/src/runtime/component/instance.rs b/crates/wasmtime/src/runtime/component/instance.rs index 28402a3511..9563175543 100644 --- a/crates/wasmtime/src/runtime/component/instance.rs +++ b/crates/wasmtime/src/runtime/component/instance.rs @@ -902,6 +902,13 @@ impl<'a> Instantiator<'a> { store.store_data_mut().component_instance_mut(self.id) } + /// Convenience helper to return the instance ID of the `ComponentInstance` that's + /// being instantiated + #[cfg(feature = "rr-component")] + fn id(&self) -> ComponentInstanceId { + self.id + } + // NB: This method is only intended to be called during the instantiation // process because the `Arc::get_mut` here is fallible and won't generally // succeed once the instance has been handed to the embedder. Before that @@ -1016,23 +1023,18 @@ impl InstancePre { fn instantiate_impl(&self, mut store: impl AsContextMut) -> Result { let mut store = store.as_context_mut(); - #[cfg(feature = "rr-component")] - { - use crate::rr::{Validate, component_events::InstantiationEvent}; - store - .0 - .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() .allocator() .increment_component_instance_count()?; let mut instantiator = Instantiator::new(&self.component, store.0, &self.imports); + #[cfg(feature = "rr-component")] + { + use crate::rr::component_events::InstantiationEvent; + store.0.record_event(|| { + InstantiationEvent(*self.component.checksum(), instantiator.id()) + })?; + } instantiator.run(&mut store).map_err(|e| { store .engine() diff --git a/crates/wasmtime/src/runtime/component/mod.rs b/crates/wasmtime/src/runtime/component/mod.rs index 437ba2c24d..9af6869ad5 100644 --- a/crates/wasmtime/src/runtime/component/mod.rs +++ b/crates/wasmtime/src/runtime/component/mod.rs @@ -112,7 +112,7 @@ mod linker; mod matching; mod resource_table; mod resources; -mod storage; +pub(crate) mod storage; pub(crate) mod store; pub mod types; mod values; diff --git a/crates/wasmtime/src/runtime/component/store.rs b/crates/wasmtime/src/runtime/component/store.rs index edb251ed5e..411242d4ff 100644 --- a/crates/wasmtime/src/runtime/component/store.rs +++ b/crates/wasmtime/src/runtime/component/store.rs @@ -3,6 +3,7 @@ use crate::store::{StoreData, StoreId, StoreOpaque}; #[cfg(feature = "component-model-async")] use alloc::vec::Vec; use core::pin::Pin; +use serde::{Deserialize, Serialize}; use wasmtime_environ::PrimaryMap; #[derive(Default)] @@ -10,7 +11,7 @@ pub struct ComponentStoreData { instances: PrimaryMap>, } -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Ord, PartialOrd)] pub struct ComponentInstanceId(u32); wasmtime_environ::entity_impl!(ComponentInstanceId); diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 3f4ab2f9e1..9936845ac7 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -2366,7 +2366,7 @@ impl HostContext { let func = &state.func; let func_type_index = state._ty.index(); - let (params_size, results_size) = { + let (flat_size_params, flat_size_results) = { let type_index = state._ty.index(); let wasm_func_subtype = caller.engine().signatures().borrow(type_index).unwrap(); let wasm_func_type = wasm_func_subtype.unwrap_func(); @@ -2383,13 +2383,13 @@ impl HostContext { .collect::>(), ) }; - let (num_params, num_results) = (params_size.len(), results_size.len()); + let (num_params, num_results) = (flat_size_params.len(), flat_size_results.len()); // 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::core_hooks::record_replay_host_func_entry( unsafe { &args.as_ref()[..num_params] }, - params_size.as_slice(), + flat_size_params.as_slice(), &func_type_index, caller.store.0, )?; @@ -2435,7 +2435,7 @@ impl HostContext { // Record the return values rr::core_hooks::record_host_func_return( unsafe { &args.as_ref()[..num_results] }, - results_size.as_slice(), + flat_size_results.as_slice(), &func_type_index, caller.store.0, )?; diff --git a/crates/wasmtime/src/runtime/rr.rs b/crates/wasmtime/src/runtime/rr.rs index 79f730ce36..0b7d61e5ab 100644 --- a/crates/wasmtime/src/runtime/rr.rs +++ b/crates/wasmtime/src/runtime/rr.rs @@ -4,10 +4,16 @@ /// Convenience method hooks for injecting event recording/replaying in the rest of the engine mod hooks; -pub use hooks::{ConstMemorySliceCell, MemorySliceCell, component_hooks, core_hooks}; +pub(crate) use hooks::{ConstMemorySliceCell, MemorySliceCell, component_hooks, core_hooks}; -#[cfg(feature = "rr")] /// Core infrastructure for RR support +#[cfg(feature = "rr")] mod core; #[cfg(feature = "rr")] pub use core::*; + +/// Driver capabilities for executing replays +#[cfg(feature = "rr")] +mod replay_driver; +#[cfg(feature = "rr")] +pub use replay_driver::*; diff --git a/crates/wasmtime/src/runtime/rr/core.rs b/crates/wasmtime/src/runtime/rr/core.rs index 892dea5d3c..4166cc0df4 100644 --- a/crates/wasmtime/src/runtime/rr/core.rs +++ b/crates/wasmtime/src/runtime/rr/core.rs @@ -14,7 +14,7 @@ pub use events::Validate; pub use events::component_events; use events::{common_events, component_events as __component_events}; pub use events::{core_events, marker_events}; -pub use io::{RecordWriter, ReplayReader}; +pub use io::{IOError, RecordWriter, ReplayReader}; /// Settings for execution recording. #[cfg(feature = "rr")] @@ -126,15 +126,20 @@ rr_event! { CustomMessage(marker_events::CustomMessageEvent), // Common events for both core or component wasm - /// Return from host function to either Core Wasm or component + /// Return from host function to either core Wasm or component HostFuncReturn(common_events::HostFuncReturnEvent), - /// Call into host function from Core Wasm + /// Call into host function from core Wasm CoreHostFuncEntry(core_events::HostFuncEntryEvent), // REQUIRED events for replay - /// Call into a Wasm component function from host + /// Starting marker for a Wasm component function call from host + /// + /// This is distinguished from `ComponentWasmFuncEntry` as there may + /// be multiple lowering steps before actually entering the Wasm function + ComponentWasmFuncBegin(__component_events::WasmFuncBeginEvent), + /// Entry from the host into the Wasm component function ComponentWasmFuncEntry(__component_events::WasmFuncEntryEvent), /// Instantiation of a component ComponentInstantiation(__component_events::InstantiationEvent), @@ -187,17 +192,17 @@ pub enum ReplayError { FailedValidation, IncorrectEventVariant, InvalidOrdering, + FailedRead(IOError), EventError(Box), + MissingComponentOrModule, + MissingComponentOrModuleInstance, } impl fmt::Display for ReplayError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::EmptyBuffer => { - 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" - ) + write!(f, "replay buffer is empty") } Self::FailedValidation => { write!(f, "replay event validation failed") @@ -208,9 +213,19 @@ impl fmt::Display for ReplayError { Self::EventError(e) => { write!(f, "{:?}", e) } + Self::FailedRead(e) => { + write!(f, "{}", e)?; + f.write_str("Note: Ensure sufficient `deserialization-buffer-size` in replay settings if you included `validation-metadata` during recording") + } Self::InvalidOrdering => { write!(f, "event occured at an invalid position in the trace") } + Self::MissingComponentOrModule => { + write!(f, "missing component or module for replay") + } + Self::MissingComponentOrModuleInstance => { + write!(f, "missing component or module instance for replay") + } } } } @@ -268,7 +283,7 @@ pub trait Recorder { /// This trait provides the interface for a FIFO replayer that /// essentially operates as an iterator over the recorded events -pub trait Replayer: Iterator { +pub trait Replayer: Iterator> { /// Constructs a reader on buffer fn new_replayer(reader: impl ReplayReader + 'static, settings: ReplaySettings) -> Result where @@ -280,20 +295,15 @@ pub trait Replayer: Iterator { /// Get the settings (embedded within the trace) during recording fn trace_settings(&self) -> &RecordSettings; + ///// Peek at the next event without consuming it + //fn peek(&mut self) -> Option>; + // Provided Methods /// Get the next functional replay event (skips past all non-marker events) - /// - /// ## 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 { - log::debug!("Read replay event => {}", e); - } - event + self.next().ok_or(ReplayError::EmptyBuffer)? } /// Pop the next replay event with an attemped type conversion to expected @@ -301,7 +311,7 @@ pub trait Replayer: Iterator { /// /// ## Errors /// - /// See [`next_event_and`](Replayer::next_event_and) + /// Returns a [`ReplayError::IncorrectEventVariant`] if it failed to convert typecheck event safely #[inline] fn next_event_typed(&mut self) -> Result where @@ -315,8 +325,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_typed`](Replayer::next_event_typed) #[inline] fn next_event_and(&mut self, f: F) -> Result<(), ReplayError> where @@ -436,18 +445,27 @@ pub struct ReplayBuffer { trace_settings: RecordSettings, /// Intermediate static buffer for deserialization deser_buffer: Vec, + /// Whether buffer has been completely read + eof_encountered: bool, + /// Peeked event for lookahead + peeked: Option, } impl Iterator for ReplayBuffer { - type Item = RREvent; + type Item = Result; fn next(&mut self) -> Option { + if self.eof_encountered { + return None; + } + if self.peeked.is_some() { + return self.peeked.take().map(Ok); + } 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; + break 'event_loop Some(Err(ReplayError::FailedRead(e))); } Ok(event) => { if let RREvent::Eof = &event { @@ -455,7 +473,8 @@ impl Iterator for ReplayBuffer { } else if event.is_marker() { continue 'event_loop; } else { - break 'event_loop Some(event); + log::debug!("Read replay event => {}", event); + break 'event_loop Some(Ok(event)); } } } @@ -466,15 +485,22 @@ impl Iterator for ReplayBuffer { impl Drop for ReplayBuffer { fn drop(&mut self) { - if let Some(event) = self.next() { - if let RREvent::Eof = event { + let mut remaining_events = 0; + // Cannot use count() in iterator because IO error may loop indefinitely + while let Some(event) = self.next() { + if let Ok(e) = event { + println!("Remaining event: {:?}", e); } else { - log::warn!( - "Replay buffer is dropped with {} remaining events, - and is likely an invalid/incomplete execution", - self.count() - ); - } + event.unwrap(); + }; + remaining_events += 1; + } + if remaining_events > 0 { + log::warn!( + "Replay buffer is dropped with {} remaining events, + and is likely an invalid/incomplete execution", + remaining_events + ); } } } @@ -510,6 +536,8 @@ impl Replayer for ReplayBuffer { settings, trace_settings, deser_buffer, + eof_encountered: false, + peeked: None, }) } @@ -522,6 +550,14 @@ impl Replayer for ReplayBuffer { fn trace_settings(&self) -> &RecordSettings { &self.trace_settings } + + //#[inline] + //fn peek(&mut self) -> Option> { + // if self.peeked.is_none() { + // self.peeked = self.next(); + // } + // self.peeked.as_ref().map(|r| Ok(r)) + //} } #[cfg(test)] diff --git a/crates/wasmtime/src/runtime/rr/core/events.rs b/crates/wasmtime/src/runtime/rr/core/events.rs index f5e401a6b4..bd3e034681 100644 --- a/crates/wasmtime/src/runtime/rr/core/events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events.rs @@ -96,8 +96,8 @@ impl RRFuncArgVals { where T: FlatBytes, { - let mut bytes = Vec::::new(); - let mut sizes = Vec::::new(); + let mut bytes = Vec::new(); + let mut sizes = Vec::new(); for (flat_size, arg) in flat.zip(args.iter()) { bytes.extend_from_slice(&arg.bytes_ref(flat_size)); sizes.push(flat_size); diff --git a/crates/wasmtime/src/runtime/rr/core/events/component_events.rs b/crates/wasmtime/src/runtime/rr/core/events/component_events.rs index af47fe33d6..db9b30ee2d 100644 --- a/crates/wasmtime/src/runtime/rr/core/events/component_events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events/component_events.rs @@ -1,7 +1,7 @@ //! Module comprising of component model wasm events use super::*; -use crate::component::Component; +use crate::component::ComponentInstanceId; use crate::vm::component::libcalls::ResourceDropRet; // Re-export common events from this module pub use common_events::*; @@ -10,20 +10,24 @@ use wasmtime_environ::{ component::{ExportIndex, FlatTypesStorage, InterfaceType, TypeFuncIndex}, }; -/// A [`Component`] instantiatation event +/// Beginning marker for a Wasm component function call from host #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct InstantiationEvent { - /// A checksum of the component bytecode - component: [u8; 32], +pub struct WasmFuncBeginEvent { + /// Checksum of component containing function + pub component: [u8; 32], + /// Instance ID for the component instance + pub instance: ComponentInstanceId, + /// Export index for the invoked function + pub func_idx: ExportIndex, } -impl InstantiationEvent { - pub fn from_component(component: &Component) -> Self { - Self { - component: *component.checksum(), - } - } -} +/// A [`Component`] instantiatation event +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Ord, PartialOrd)] +pub struct InstantiationEvent( + /// Checksum of the bytecode used to instantiate the component + pub [u8; 32], + pub ComponentInstanceId, +); /// A call event from Host into a Wasm component function /// @@ -32,22 +36,12 @@ impl InstantiationEvent { pub struct WasmFuncEntryEvent { /// Raw values passed across call boundary args: RRFuncArgVals, - /// Checksum of component containing function - component: [u8; 32], - func_idx: ExportIndex, } impl WasmFuncEntryEvent { // Record - pub fn new( - args: &[ValRaw], - flat: FlatTypesStorage, - component: [u8; 32], - func_idx: ExportIndex, - ) -> Self { + pub fn new(args: &[ValRaw], flat: FlatTypesStorage) -> Self { Self { args: RRFuncArgVals::from_raw_slice(args, flat.iter32()), - component, - func_idx, } } diff --git a/crates/wasmtime/src/runtime/rr/core/io.rs b/crates/wasmtime/src/runtime/rr/core/io.rs index 5f8e49e57f..9a25de5c85 100644 --- a/crates/wasmtime/src/runtime/rr/core/io.rs +++ b/crates/wasmtime/src/runtime/rr/core/io.rs @@ -2,9 +2,11 @@ use crate::prelude::*; use postcard; use serde::{Deserialize, Serialize}; +pub type IOError = postcard::Error; + cfg_if::cfg_if! { if #[cfg(feature = "std")] { - use std::io::{Write, Read}; + use std::io::{Write, Seek, Read}; /// An [`Write`] usable for recording in RR /// /// This supports `no_std`, but must be [Send] and [Sync] @@ -12,8 +14,8 @@ cfg_if::cfg_if! { impl RecordWriter for T {} /// An [`Read`] usable for replaying in RR - pub trait ReplayReader: Read + Send + Sync {} - impl ReplayReader for T {} + pub trait ReplayReader: Read + Seek + Send + Sync {} + impl ReplayReader for T {} } else { // `no_std` configuration @@ -28,8 +30,8 @@ cfg_if::cfg_if! { /// 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 {} + pub trait ReplayReader: Read + Seek + Send + Sync {} + impl ReplayReader for T {} } } @@ -55,7 +57,7 @@ where /// /// Currently uses `postcard` deserializer, with optional scratch /// buffer to deserialize into -pub(super) fn from_replay_reader<'a, T, R>(reader: R, scratch: &'a mut [u8]) -> Result +pub(super) fn from_replay_reader<'a, T, R>(reader: R, scratch: &'a mut [u8]) -> Result where T: Deserialize<'a>, R: ReplayReader + 'a, diff --git a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs index 009d626d74..b06ace8a89 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs @@ -1,7 +1,6 @@ use crate::ValRaw; #[cfg(feature = "component-model")] use crate::component::func::LowerContext; -use crate::component::store::StoreComponentInstanceId; #[cfg(feature = "rr-component")] use crate::rr::component_events::{ HostFuncReturnEvent, LowerFlatReturnEvent, LowerMemoryReturnEvent, ResultEvent, @@ -14,7 +13,7 @@ use crate::{StoreContextMut, prelude::*}; use alloc::sync::Arc; use core::mem::MaybeUninit; #[cfg(feature = "component-model")] -use wasmtime_environ::component::{ComponentTypes, ExportIndex, InterfaceType, TypeFuncIndex}; +use wasmtime_environ::component::{ComponentTypes, InterfaceType, TypeFuncIndex}; #[cfg(all(feature = "rr-component"))] use wasmtime_environ::component::{MAX_FLAT_PARAMS, MAX_FLAT_RESULTS}; @@ -28,36 +27,30 @@ pub enum ReplayLoweringPhase { /// Record hook wrapping a wasm component export function invocation and replay /// validation of return value #[inline] -pub fn record_replay_wasm_func( +pub fn record_and_replay_validate_wasm_func( wasm_call: F, args: &[ValRaw], - func_idx: ExportIndex, type_idx: TypeFuncIndex, - id: StoreComponentInstanceId, + types: Arc, store: &mut StoreContextMut<'_, T>, ) -> Result<()> where F: FnOnce(&mut StoreContextMut<'_, T>) -> Result<()>, { - let _ = (args, id, func_idx, type_idx); + let _ = (args, type_idx, &types); #[cfg(feature = "rr-component")] { - let component = id.get(store.0).component(); - let types = component.types(); - let checksum = *component.checksum(); let flat_params = types.flat_types_storage( &InterfaceType::Tuple(types[type_idx].params), MAX_FLAT_PARAMS, ); store .0 - .record_event(|| WasmFuncEntryEvent::new(args, flat_params, checksum, func_idx))?; + .record_event(|| WasmFuncEntryEvent::new(args, flat_params))?; } let result = wasm_call(store); #[cfg(all(feature = "rr-component", feature = "rr-validate"))] { - let component = id.get(store.0).component(); - let types = component.types(); let flat_results = types.flat_types_storage( &InterfaceType::Tuple(types[type_idx].results), MAX_FLAT_RESULTS, @@ -80,7 +73,7 @@ where /// Record/replay hook operation for host function entry events #[inline] -pub fn record_replay_host_func_entry( +pub fn record_and_replay_validate_host_func_entry( args: &mut [MaybeUninit], types: &Arc, type_idx: &TypeFuncIndex, diff --git a/crates/wasmtime/src/runtime/rr/replay_driver.rs b/crates/wasmtime/src/runtime/rr/replay_driver.rs new file mode 100644 index 0000000000..67259274a5 --- /dev/null +++ b/crates/wasmtime/src/runtime/rr/replay_driver.rs @@ -0,0 +1,198 @@ +use crate::component::{self, Component, Val}; +#[cfg(feature = "rr-component")] +use crate::rr::component_events; +use crate::rr::{RREvent, ReplayError, Validate, component_hooks::ReplayLoweringPhase}; +use crate::{AsContextMut, Engine, ReplayReader, ReplaySettings, Store}; +use crate::{Module, ValRaw, prelude::*}; +use alloc::collections::BTreeMap; +use core::mem::MaybeUninit; +#[cfg(feature = "rr-component")] +use wasmtime_environ::component::{MAX_FLAT_PARAMS, MAX_FLAT_RESULTS}; + +/// The environment necessary to produce a [`ReplayInstance`] +#[derive(Clone)] +pub struct ReplayEnvironment { + engine: Engine, + modules: Vec, + components: BTreeMap<[u8; 32], Component>, + settings: ReplaySettings, +} + +impl ReplayEnvironment { + /// Create a new [`ReplayEnvironment`] + pub fn new(engine: &Engine, settings: ReplaySettings) -> Self { + Self { + engine: engine.clone(), + modules: Vec::new(), + components: BTreeMap::new(), + settings, + } + } + + /// Add a [`Module`] to the replay environment + pub fn add_module(&mut self, module: Module) -> &mut Self { + self.modules.push(module); + self + } + + /// Add a [`Component`] to the replay environment + pub fn add_component(&mut self, component: Component) -> &mut Self { + self.components.insert(*component.checksum(), component); + self + } + + /// Instantiate a new [`ReplayInstance`] using a [`ReplayReader`] in context of this environment + pub fn instantiate(&self, reader: impl ReplayReader + 'static) -> Result> { + ReplayInstance::from_environment(self, reader) + } +} + +/// A [`ReplayInstance`] is an object providing a opaquely managed, replayable [`Store`] +/// +/// Debugger capabilities in the future will interact with this object for +/// inserting breakpoints, snapshotting, and restoring state +pub struct ReplayInstance<'a> { + /// The store doesn't need any host data because the trace format and + /// replay is designed to be embedding-agnostic + store: Store<()>, + component_linker: component::Linker<()>, + module_linker: crate::Linker<()>, + modules: &'a Vec, + components: &'a BTreeMap<[u8; 32], Component>, + component_instances: BTreeMap, +} + +impl<'a> ReplayInstance<'a> { + fn from_environment( + env: &'a ReplayEnvironment, + reader: impl ReplayReader + 'static, + ) -> Result { + let mut store = Store::new(&env.engine, ()); + store.init_replaying(reader, env.settings.clone())?; + let mut component_linker = component::Linker::<()>::new(&env.engine); + let mut module_linker = crate::Linker::<()>::new(&env.engine); + // Replays shouldn't use any imports, so stub them all out as traps + for module in &env.modules { + module_linker.define_unknown_imports_as_traps(module)?; + } + for component in env.components.values() { + component_linker.define_unknown_imports_as_traps(component)?; + } + Ok(Self { + store, + component_linker, + module_linker, + modules: &env.modules, + components: &env.components, + component_instances: BTreeMap::new(), + }) + } + + /// Run this replay instance to completion + pub fn run_to_completion(&mut self) -> Result<()> { + while let Some(rr_event) = self + .store + .as_context_mut() + .0 + .replay_buffer_mut() + .expect("unexpected; replay buffer must be initialized within an instance") + .next() + { + // The only valid "top-level" events are: + // * Instantiation events (component/module) + // * Wasm function begin events (component/module) + // + // All other events are transparently dispatched under the context of these top-level events + match rr_event? { + RREvent::ComponentInstantiation(event) => { + #[cfg(feature = "rr-component")] + { + // Find matching component from environment to instantiate + let component = self + .components + .get(&event.0) + .ok_or(ReplayError::MissingComponentOrModule)?; + + let instance = self + .component_linker + .instantiate(self.store.as_context_mut(), component)?; + // Validate the instantiation event + event.validate(&component_events::InstantiationEvent( + *component.checksum(), + instance.id().instance(), + ))?; + + let ret = self.component_instances.insert(event, instance); + // Ensures that an already-instantiated configuration is not re-instantiated + assert!(ret.is_none()); + } + #[cfg(not(feature = "rr-component"))] + { + anyhow!( + "Cannot parse ComponentInstantation replay event without rr-component feature enabled" + ); + } + } + RREvent::ComponentWasmFuncBegin(event) => { + #[cfg(feature = "rr-component")] + { + // Grab the correct component instance + let key = + component_events::InstantiationEvent(event.component, event.instance); + let instance = self + .component_instances + .get_mut(&key) + .ok_or(ReplayError::MissingComponentOrModuleInstance)?; + + // Replay lowering steps and obtain raw value arguments to raw function call + let func = component::Func::from_lifted_func(*instance, event.func_idx); + let store = self.store.as_context_mut(); + + // Call the function + // + // This is almost a mirror of the usage in [`component::Func::call_impl`] + let mut results_storage = [Val::U64(0); MAX_FLAT_RESULTS]; + let mut num_results = 0; + let results = &mut results_storage; + let _return = unsafe { + func.call_raw( + store, + |cx, _, dst: &mut MaybeUninit<[MaybeUninit; MAX_FLAT_PARAMS]>| { + // For lowering, use replay instead of actual lowering + let dst: &mut [MaybeUninit] = dst.assume_init_mut(); + cx.replay_lowering(Some(dst), ReplayLoweringPhase::WasmFuncEntry) + }, + |cx, results_ty, src: &[ValRaw; MAX_FLAT_RESULTS]| { + // Lifting can proceed exactly as normal + let max_flat = MAX_FLAT_RESULTS; + for (result, slot) in + component::Func::lift_results(cx, results_ty, src, max_flat)?.zip(results) + { + *slot = result?; + num_results += 1; + } + Ok(()) + }, + )? + }; + + log::info!( + "Returned {:?} for calling {:?}", + &results_storage[..num_results], + func + ); + } + #[cfg(not(feature = "rr-component"))] + { + anyhow!( + "Cannot parse ComponentWasmFuncBegin replay event without rr-component feature enabled" + ); + } + } + + _ => Err(ReplayError::IncorrectEventVariant)?, + } + } + Ok(()) + } +} diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index 16338ad65d..7ceee62071 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -1498,8 +1498,8 @@ impl StoreOpaque { "replaying store must not initialize any modules" ); ensure!( - !self.engine().is_recording(), - "store recording cannot be enabled when initializing replay" + self.engine().is_replaying(), + "store replaying requires replaying enabled on config" ); self.replay_buffer = Some(ReplayBuffer::new_replayer(replayer, settings)?); Ok(()) From c4645ab59f1807e010c8330f2ff1719a3f55444a Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 11 Nov 2025 14:18:11 -0500 Subject: [PATCH 25/73] Added temporary CLI support (without async) --- crates/wasmtime/src/runtime/rr/core.rs | 6 +- .../wasmtime/src/runtime/rr/replay_driver.rs | 5 +- src/commands/replay.rs | 4 +- src/commands/run.rs | 61 +++++++++++++------ 4 files changed, 50 insertions(+), 26 deletions(-) diff --git a/crates/wasmtime/src/runtime/rr/core.rs b/crates/wasmtime/src/runtime/rr/core.rs index 4166cc0df4..31f8ad5045 100644 --- a/crates/wasmtime/src/runtime/rr/core.rs +++ b/crates/wasmtime/src/runtime/rr/core.rs @@ -488,11 +488,7 @@ impl Drop for ReplayBuffer { let mut remaining_events = 0; // Cannot use count() in iterator because IO error may loop indefinitely while let Some(event) = self.next() { - if let Ok(e) = event { - println!("Remaining event: {:?}", e); - } else { - event.unwrap(); - }; + event.unwrap(); remaining_events += 1; } if remaining_events > 0 { diff --git a/crates/wasmtime/src/runtime/rr/replay_driver.rs b/crates/wasmtime/src/runtime/rr/replay_driver.rs index 67259274a5..866c04ea24 100644 --- a/crates/wasmtime/src/runtime/rr/replay_driver.rs +++ b/crates/wasmtime/src/runtime/rr/replay_driver.rs @@ -2,8 +2,9 @@ use crate::component::{self, Component, Val}; #[cfg(feature = "rr-component")] use crate::rr::component_events; use crate::rr::{RREvent, ReplayError, Validate, component_hooks::ReplayLoweringPhase}; -use crate::{AsContextMut, Engine, ReplayReader, ReplaySettings, Store}; -use crate::{Module, ValRaw, prelude::*}; +use crate::{ + AsContextMut, Engine, Module, ReplayReader, ReplaySettings, Store, ValRaw, prelude::*, +}; use alloc::collections::BTreeMap; use core::mem::MaybeUninit; #[cfg(feature = "rr-component")] diff --git a/src/commands/replay.rs b/src/commands/replay.rs index e8e02c7d62..0cf1aae71f 100644 --- a/src/commands/replay.rs +++ b/src/commands/replay.rs @@ -24,8 +24,8 @@ pub struct ReplayOptions { pub 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 + /// not important for basic functional recording/replaying, but may be required to replay traces where + /// `validate` was enabled for recording #[arg(short, long, default_value_t = 64)] pub deser_buffer_size: usize, } diff --git a/src/commands/run.rs b/src/commands/run.rs index 3ae205bf5f..b3c35e3711 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -17,6 +17,8 @@ use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::thread; use wasi_common::sync::{Dir, TcpListener, WasiCtxBuilder, ambient_authority}; +#[cfg(feature = "rr")] +use wasmtime::ReplayEnvironment; use wasmtime::{Engine, Func, Module, Store, StoreLimits, Val, ValType}; use wasmtime_wasi::{WasiCtxView, WasiView}; @@ -100,7 +102,16 @@ impl RunCommand { self.run.common.init_logging()?; let mut config = self.run.common.config(None)?; + #[cfg(not(feature = "rr"))] config.async_support(true); + #[cfg(feature = "rr")] + if replay_opts.is_some() { + // Replay does not support async yet + config.async_support(false); + config.rr(wasmtime::RRConfig::Replaying); + } else { + config.async_support(true); + } if self.run.common.wasm.timeout.is_some() { config.epoch_interruption(true); @@ -116,11 +127,6 @@ impl RunCommand { None => {} } - #[cfg(feature = "rr")] - if replay_opts.is_some() { - config.replaying(true); - } - let engine = Engine::new(&config)?; // Read the wasm module binary either as `*.wat` or a raw binary. @@ -167,7 +173,13 @@ impl RunCommand { }; let mut store = Store::new(&engine, host); + #[cfg(not(feature = "rr"))] self.populate_with_wasi(&mut linker, &mut store, &main)?; + // For replay, we don't populate WASI + #[cfg(feature = "rr")] + if !replay_opts.is_some() { + self.populate_with_wasi(&mut linker, &mut store, &main)?; + } store.data_mut().limits = self.run.store_limits(); store.limiter(|t| &mut t.limits); @@ -180,6 +192,33 @@ impl RunCommand { #[cfg(feature = "rr")] { + // If this is a replay run, skip to replay setup and run to completion + // + // Note: Right now, replay doesn't inherit any store settings listed + // above. This will have to change in the future. In general, replays will + // need an "almost exact" superset of the run configurations, but with + // certain different options (e.g. fuel consumption). + if let Some(opts) = replay_opts { + let settings = ReplaySettings { + validate: opts.validate, + deser_buffer_size: opts.deser_buffer_size, + ..Default::default() + }; + + let mut renv = ReplayEnvironment::new(&engine, settings); + match main { + RunTarget::Core(m) => { + renv.add_module(m); + } + RunTarget::Component(c) => { + renv.add_component(c); + } + } + let mut instance = renv.instantiate(BufReader::new(fs::File::open(opts.trace)?))?; + return instance.run_to_completion(); + } + + // Recording settings for this execution's store let record = &self.run.common.record; if let Some(path) = &record.path { let default_settings = RecordSettings::default(); @@ -197,18 +236,6 @@ impl RunCommand { store.init_recording(fs::File::create(&path)?, settings)?; } } - - if let Some(opts) = replay_opts { - let settings = ReplaySettings { - validate: opts.validate, - deser_buffer_size: opts.deser_buffer_size, - ..Default::default() - }; - store.init_replaying( - BufReader::new(fs::File::open(opts.trace).unwrap()), - settings, - )?; - } } // Always run the module asynchronously to ensure that the module can be From ffe22ffbcbd8a4c6f4d019acb714422cf7853e3a Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Thu, 13 Nov 2025 13:31:40 -0500 Subject: [PATCH 26/73] Added support for recording all boundaries of `Func` and `TypedFunc`; completed core wasm rr --- Cargo.lock | 1 + Cargo.toml | 1 + crates/c-api/include/wasmtime/extern.h | 8 +- crates/c-api/include/wasmtime/val.h | 2 +- crates/c-api/src/val.rs | 2 +- crates/environ/Cargo.toml | 1 + .../environ/src/compile/module_artifacts.rs | 3 + crates/environ/src/module_artifacts.rs | 3 + crates/wasmtime/Cargo.toml | 2 +- .../src/runtime/component/func/options.rs | 40 +----- .../src/runtime/component/func/typed.rs | 45 ++---- crates/wasmtime/src/runtime/func.rs | 115 +++++++++++---- crates/wasmtime/src/runtime/func/typed.rs | 58 +++++--- crates/wasmtime/src/runtime/instance.rs | 60 +++++--- crates/wasmtime/src/runtime/module.rs | 14 ++ crates/wasmtime/src/runtime/rr.rs | 38 +++++ crates/wasmtime/src/runtime/rr/core.rs | 31 ++-- crates/wasmtime/src/runtime/rr/core/events.rs | 133 +++++++++++++----- .../runtime/rr/core/events/common_events.rs | 25 +++- .../rr/core/events/component_events.rs | 95 ------------- .../src/runtime/rr/core/events/core_events.rs | 47 ++++++- .../src/runtime/rr/hooks/component_hooks.rs | 5 +- .../src/runtime/rr/hooks/core_hooks.rs | 115 +++++++++++---- .../wasmtime/src/runtime/rr/replay_driver.rs | 85 +++++++++-- crates/wasmtime/src/runtime/store.rs | 2 +- crates/wasmtime/src/runtime/store/data.rs | 3 +- .../src/runtime/vm/component/libcalls.rs | 2 +- crates/wasmtime/src/runtime/vm/instance.rs | 6 +- 28 files changed, 607 insertions(+), 335 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 07469f5cf1..3b8664ae97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4662,6 +4662,7 @@ dependencies = [ "semver", "serde", "serde_derive", + "sha2", "smallvec", "target-lexicon", "wasm-encoder 0.236.0", diff --git a/Cargo.toml b/Cargo.toml index 55d7c92da1..f7e315116d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -411,6 +411,7 @@ webpki-roots = "0.26.0" itertools = "0.14.0" base64 = "0.22.1" termcolor = "1.4.1" +sha2 = { version = "0.10.2", default-features = false } # ============================================================================= # diff --git a/crates/c-api/include/wasmtime/extern.h b/crates/c-api/include/wasmtime/extern.h index 51675b970d..df9e0a32ea 100644 --- a/crates/c-api/include/wasmtime/extern.h +++ b/crates/c-api/include/wasmtime/extern.h @@ -30,7 +30,13 @@ typedef struct wasmtime_func { /// this field is otherwise never zero. uint64_t store_id; /// Private field for Wasmtime, undefined if `store_id` is zero. - void *__private; + void *__private1; + /// Private field for Wasmtime + uint32_t *__private2; + /// Private field for Wasmtime + uint32_t *__private3; + /// Private field for Wasmtime + uint32_t *__private4; } wasmtime_func_t; /// \brief Representation of a table in Wasmtime. diff --git a/crates/c-api/include/wasmtime/val.h b/crates/c-api/include/wasmtime/val.h index bdac5b9c76..5e9c145e37 100644 --- a/crates/c-api/include/wasmtime/val.h +++ b/crates/c-api/include/wasmtime/val.h @@ -418,7 +418,7 @@ typedef union wasmtime_val_raw { // Assert that the shape of this type is as expected since it needs to match // Rust. static inline void __wasmtime_val_assertions() { - static_assert(sizeof(wasmtime_valunion_t) == 16, "should be 16-bytes large"); + static_assert(sizeof(wasmtime_valunion_t) == 32, "should be 16-bytes large"); static_assert(__alignof(wasmtime_valunion_t) == 8, "should be 8-byte aligned"); static_assert(sizeof(wasmtime_val_raw_t) == 16, "should be 16 bytes large"); diff --git a/crates/c-api/src/val.rs b/crates/c-api/src/val.rs index efc53cf529..49aaf8fc3c 100644 --- a/crates/c-api/src/val.rs +++ b/crates/c-api/src/val.rs @@ -152,7 +152,7 @@ pub union wasmtime_val_union { } const _: () = { - assert!(std::mem::size_of::() == 16); + assert!(std::mem::size_of::() == 32); assert!(std::mem::align_of::() == std::mem::align_of::()); }; diff --git a/crates/environ/Cargo.toml b/crates/environ/Cargo.toml index ffdf9570d2..0db54909c7 100644 --- a/crates/environ/Cargo.toml +++ b/crates/environ/Cargo.toml @@ -37,6 +37,7 @@ wasmprinter = { workspace = true, optional = true } wasmtime-component-util = { workspace = true, optional = true } semver = { workspace = true, optional = true, features = ['serde'] } smallvec = { workspace = true, features = ['serde'] } +sha2 = { workspace = true } [dev-dependencies] clap = { workspace = true, features = ['default'] } diff --git a/crates/environ/src/compile/module_artifacts.rs b/crates/environ/src/compile/module_artifacts.rs index 4fc3a3792a..eb807e0d10 100644 --- a/crates/environ/src/compile/module_artifacts.rs +++ b/crates/environ/src/compile/module_artifacts.rs @@ -10,6 +10,7 @@ use crate::{ use anyhow::{Result, bail}; use object::SectionKind; use object::write::{Object, SectionId, StandardSegment, WritableBuffer}; +use sha2::{Digest, Sha256}; use std::ops::Range; /// Helper structure to create an ELF file as a compilation artifact. @@ -124,6 +125,7 @@ impl<'a> ObjectBuilder<'a> { debuginfo, has_unparsed_debuginfo, data, + wasm, data_align, passive_data, .. @@ -228,6 +230,7 @@ impl<'a> ObjectBuilder<'a> { has_wasm_debuginfo: self.tunables.parse_wasm_debuginfo, dwarf, }, + checksum: Sha256::digest(wasm).into(), }) } diff --git a/crates/environ/src/module_artifacts.rs b/crates/environ/src/module_artifacts.rs index 0d41a68f84..4a01c38a6f 100644 --- a/crates/environ/src/module_artifacts.rs +++ b/crates/environ/src/module_artifacts.rs @@ -52,6 +52,9 @@ pub struct CompiledModuleInfo { /// General compilation metadata. pub meta: Metadata, + + /// Checksum of the source Wasm binary from which this module was compiled + pub checksum: [u8; 32], } /// The name of a function stored in the diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml index b08ceee230..c8b86ef9e9 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -63,7 +63,7 @@ bitflags = { workspace = true } futures = { workspace = true, features = ["alloc"], optional = true } bytes = { workspace = true, optional = true } embedded-io = { version = "0.6.1", features = ["alloc"], optional = true } -sha2 = { version = "0.10.2", default-features = false } +sha2 = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies.windows-sys] workspace = true diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index 21d688c7ec..d1a943d2db 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -4,8 +4,6 @@ use crate::component::matching::InstanceType; use crate::component::resources::{HostResourceData, HostResourceIndex, HostResourceTables}; use crate::component::{Instance, ResourceType}; use crate::prelude::*; -#[cfg(all(feature = "rr-component", feature = "rr-validate"))] -use crate::rr::component_events::ResultEvent; #[cfg(feature = "rr-component")] use crate::rr::component_hooks::ReplayLoweringPhase; use crate::rr::{ConstMemorySliceCell, MemorySliceCell}; @@ -14,7 +12,7 @@ use crate::rr::{ RREvent, RecordBuffer, ReplayError, Replayer, component_events::ReallocEntryEvent, }; #[cfg(all(feature = "rr-component", feature = "rr-validate"))] -use crate::rr::{Validate, component_events::ReallocReturnEvent}; +use crate::rr::{ResultEvent, Validate, component_events::ReallocReturnEvent}; use crate::runtime::vm::component::{ CallContexts, ComponentInstance, InstanceFlags, ResourceTable, ResourceTables, }; @@ -139,7 +137,7 @@ impl Options { // Invoke the wasm malloc function using its raw and statically known // signature. - let result = unsafe { ReallocFunc::call_raw(store, realloc_ty, realloc, params)? }; + let result = unsafe { ReallocFunc::call_raw(store, realloc_ty, realloc, params, None)? }; if result % old_align != 0 { bail!("realloc return: result not aligned"); @@ -560,40 +558,6 @@ impl<'a, T: 'static> LowerContext<'a, T> { ) } - /// Perform a replay of only [`ReallocEntryEvent`] + [`ReallocReturnEvent`] events - /// - /// Panics if replay not enabled - #[cfg(feature = "rr-component")] - pub fn replay_realloc(&mut self) -> Result { - let get_event = |cx: &mut Self| cx.store.0.replay_buffer_mut().unwrap().next_event(); - let (record_has_validation, _replay_validate) = { - let buf = self.store.0.replay_buffer_mut().unwrap(); - (buf.trace_settings().add_validation, buf.settings().validate) - }; - - let ptr = match get_event(self)? { - RREvent::ComponentReallocEntry(e) => { - self.realloc_inner(e.old_addr, e.old_size, e.old_align, e.new_size) - } - _ => bail!(ReplayError::IncorrectEventVariant), - }; - - if record_has_validation { - match get_event(self)? { - RREvent::ComponentReallocReturn(e) => - { - #[cfg(feature = "rr-validate")] - if _replay_validate { - e.0.validate(&ptr)? - } - } - _ => bail!(ReplayError::IncorrectEventVariant), - }; - } - - ptr - } - /// Perform a replay of all the type lowering-associated events for this context /// /// These typically include all `Lower*` and `Realloc*` event, along with the putting diff --git a/crates/wasmtime/src/runtime/component/func/typed.rs b/crates/wasmtime/src/runtime/component/func/typed.rs index e1a49ee541..a085164cd2 100644 --- a/crates/wasmtime/src/runtime/component/func/typed.rs +++ b/crates/wasmtime/src/runtime/component/func/typed.rs @@ -455,20 +455,11 @@ where dst: &mut MaybeUninit, ) -> Result<()> { assert!(Params::flatten_count() <= MAX_FLAT_PARAMS); - if cx.store.0.replay_enabled() { - #[cfg(feature = "rr-component")] - cx.replay_lowering( - Some(unsafe { storage_as_slice_mut(dst) }), - component_hooks::ReplayLoweringPhase::WasmFuncEntry, - )? - } else { - component_hooks::record_lower_flat( - |cx, ty| params.linear_lower_to_flat(cx, ty, dst), - cx, - ty, - )?; - } - Ok(()) + component_hooks::record_lower_flat( + |cx, ty| params.linear_lower_to_flat(cx, ty, dst), + cx, + ty, + ) } /// Lower parameters onto a heap-allocated location. @@ -490,25 +481,13 @@ where // // Note that `realloc` will bake in a check that the returned pointer is // in-bounds. - let ptr = if cx.store.0.replay_enabled() { - #[cfg(feature = "rr-component")] - { - let ptr = cx.replay_realloc()?; - cx.replay_lowering(None, component_hooks::ReplayLoweringPhase::WasmFuncEntry)?; - ptr - } - #[cfg(not(feature = "rr-component"))] - unreachable!() - } else { - let ptr = cx.realloc(0, 0, Params::ALIGN32, Params::SIZE32)?; - component_hooks::record_lower_memory( - |cx, ty, ptr| params.linear_lower_to_memory(cx, ty, ptr), - cx, - ty, - ptr, - )?; - ptr - }; + let ptr = cx.realloc(0, 0, Params::ALIGN32, Params::SIZE32)?; + component_hooks::record_lower_memory( + |cx, ty, ptr| params.linear_lower_to_memory(cx, ty, ptr), + cx, + ty, + ptr, + )?; // Note that the pointer here is stored as a 64-bit integer. This allows // this to work with either 32 or 64-bit memories. For a 32-bit memory diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 9936845ac7..2ff360b8dc 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -5,7 +5,7 @@ use crate::runtime::vm::{ InterpreterRef, SendSyncPtr, StoreBox, VMArrayCallHostFuncContext, VMCommonStackInformation, VMContext, VMFuncRef, VMFunctionImport, VMOpaqueContext, VMStoreContext, }; -use crate::store::{AutoAssertNoGc, StoreId, StoreOpaque}; +use crate::store::{AutoAssertNoGc, InstanceId, StoreId, StoreOpaque}; use crate::type_registry::RegisteredType; use crate::{ AsContext, AsContextMut, CallHook, Engine, Extern, FuncType, Instance, ModuleExport, Ref, @@ -17,7 +17,8 @@ use core::ffi::c_void; use core::future::Future; use core::mem::{self, MaybeUninit}; use core::ptr::NonNull; -use wasmtime_environ::VMSharedTypeIndex; +use serde::{Deserialize, Serialize}; +use wasmtime_environ::{FuncIndex, VMSharedTypeIndex}; /// A reference to the abstract `nofunc` heap value. /// @@ -102,6 +103,15 @@ impl NoFunc { } } +/// Metadata for the origin of a WebAssembly [`Func`] +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct WasmFuncOrigin { + /// The instance from which the embedded function belongs to. + pub instance: InstanceId, + /// The function index within the module. + pub index: FuncIndex, +} + /// A WebAssembly function which can be called. /// /// This type typically represents an exported function from a WebAssembly @@ -278,13 +288,20 @@ pub struct Func { /// an ambiently provided `StoreOpaque` or similar. Use the /// `self.func_ref()` method instead of this field to perform this check. unsafe_func_ref: SendSyncPtr, + + /// Optional metadata about the origin of this function. + /// + /// This field is populated when a [`Func`] is generated from a known instance + /// (i.e. exported Wasm functions), and is usually `None` for internal + /// Wasm functions and host functions. + origin: Option, } // Double-check that the C representation in `extern.h` matches our in-Rust // representation here in terms of size/alignment/etc. const _: () = { #[repr(C)] - struct C(u64, *mut u8); + struct C(u64, *mut u8, (u32, u32, u32)); assert!(core::mem::size_of::() == core::mem::size_of::()); assert!(core::mem::align_of::() == core::mem::align_of::()); assert!(core::mem::offset_of!(Func, store) == 0); @@ -549,6 +566,7 @@ impl Func { Func { store, unsafe_func_ref: func_ref.into(), + origin: None, } } @@ -1009,9 +1027,17 @@ impl Func { let func_ref = self.vm_func_ref(store.0); let params_and_returns = NonNull::new(params_and_returns).unwrap_or(NonNull::from(&mut [])); - // SAFETY: the safety of this function call is the same as the contract - // of this function. - unsafe { Self::call_unchecked_raw(&mut store, func_ref, params_and_returns) } + rr::core_hooks::record_and_replay_validate_wasm_func( + |mut store| { + // SAFETY: the safety of this function call is the same as the contract + // of this function. + unsafe { Self::call_unchecked_raw(&mut store, func_ref, params_and_returns) } + }, + unsafe { params_and_returns.as_ref() }, + &self.ty(&store), + self.origin.clone(), + &mut store, + ) } pub(crate) unsafe fn call_unchecked_raw( @@ -1124,7 +1150,7 @@ impl Func { /// of arguments as well as making sure everything is from the same `Store`. /// /// This must be called just before `call_impl_do_call`. - fn call_impl_check_args( + pub(crate) fn call_impl_check_args( &self, store: &mut StoreContextMut<'_, T>, params: &[Val], @@ -1164,7 +1190,7 @@ impl Func { /// You must have type checked the arguments by calling /// `call_impl_check_args` immediately before calling this function. It is /// only safe to call this function if that one did not return an error. - unsafe fn call_impl_do_call( + pub(crate) unsafe fn call_impl_do_call( &self, store: &mut StoreContextMut<'_, T>, params: &[Val], @@ -1260,22 +1286,51 @@ impl Func { debug_assert!(val_vec.is_empty()); let nparams = ty.params().len(); val_vec.reserve(nparams + ty.results().len()); - for (i, ty) in ty.params().enumerate() { - val_vec.push(unsafe { Val::from_raw(&mut caller.store, values_vec[i], ty) }) - } - val_vec.extend((0..ty.results().len()).map(|_| Val::null_func_ref())); - let (params, results) = val_vec.split_at_mut(nparams); - func(caller.sub_caller(), params, results)?; - - // Unlike our arguments we need to dynamically check that the return - // values produced are correct. There could be a bug in `func` that - // produces the wrong number, wrong types, or wrong stores of - // values, and we need to catch that here. - for (i, (ret, ty)) in results.iter().zip(ty.results()).enumerate() { - ret.ensure_matches_ty(caller.store.0, &ty) - .context("function attempted to return an incompatible value")?; - values_vec[i] = ret.to_raw(&mut caller.store)?; + // Recording host function entry + let flat_params = ty + .params() + .into_iter() + .map(|x| x.to_wasm_type().byte_size()) + .collect::>(); + + rr::core_hooks::record_replay_host_func_entry( + values_vec, + &flat_params, + &mut caller.store.0, + )?; + + if !caller.store.0.replay_enabled() { + for (i, ty) in ty.params().enumerate() { + val_vec.push(unsafe { Val::from_raw(&mut caller.store, values_vec[i], ty) }) + } + + val_vec.extend((0..ty.results().len()).map(|_| Val::null_func_ref())); + let (params, results) = val_vec.split_at_mut(nparams); + func(caller.sub_caller(), params, results)?; + + // Unlike our arguments we need to dynamically check that the return + // values produced are correct. There could be a bug in `func` that + // produces the wrong number, wrong types, or wrong stores of + // values, and we need to catch that here. + for (i, (ret, ty)) in results.iter().zip(ty.results()).enumerate() { + ret.ensure_matches_ty(caller.store.0, &ty) + .context("function attempted to return an incompatible value")?; + values_vec[i] = ret.to_raw(&mut caller.store)?; + } + + let flat_results = ty + .results() + .into_iter() + .map(|x| x.to_wasm_type().byte_size()) + .collect::>(); + rr::core_hooks::record_host_func_return( + values_vec, + &flat_results, + &mut caller.store.0, + )?; + } else { + rr::core_hooks::replay_host_func_return(values_vec, &mut caller.store.0)?; } // Restore our `val_vec` back into the store so it's usable for the next @@ -1481,6 +1536,16 @@ impl Func { pub(crate) fn hash_key(&self, store: &mut StoreOpaque) -> impl core::hash::Hash + Eq + use<> { self.vm_func_ref(store).as_ptr().addr() } + + /// Set the origin of this function. + pub(crate) fn set_origin(&mut self, origin: WasmFuncOrigin) { + self.origin = Some(origin); + } + + // Get the origin of this function + pub(crate) fn origin(&self) -> Option { + self.origin + } } /// Prepares for entrance into WebAssembly. @@ -2365,7 +2430,6 @@ impl HostContext { }; let func = &state.func; - let func_type_index = state._ty.index(); let (flat_size_params, flat_size_results) = { let type_index = state._ty.index(); let wasm_func_subtype = caller.engine().signatures().borrow(type_index).unwrap(); @@ -2390,7 +2454,6 @@ impl HostContext { rr::core_hooks::record_replay_host_func_entry( unsafe { &args.as_ref()[..num_params] }, flat_size_params.as_slice(), - &func_type_index, caller.store.0, )?; @@ -2436,14 +2499,12 @@ impl HostContext { rr::core_hooks::record_host_func_return( unsafe { &args.as_ref()[..num_results] }, flat_size_results.as_slice(), - &func_type_index, caller.store.0, )?; } else { // Replay the return values rr::core_hooks::replay_host_func_return( unsafe { &mut args.as_mut()[..num_results] }, - &func_type_index, caller.store.0, )?; } diff --git a/crates/wasmtime/src/runtime/func/typed.rs b/crates/wasmtime/src/runtime/func/typed.rs index 7ea40f9826..b39947630f 100644 --- a/crates/wasmtime/src/runtime/func/typed.rs +++ b/crates/wasmtime/src/runtime/func/typed.rs @@ -1,10 +1,11 @@ use super::invoke_wasm_and_catch_traps; use crate::prelude::*; +use crate::rr; use crate::runtime::vm::VMFuncRef; use crate::store::{AutoAssertNoGc, StoreOpaque}; use crate::{ AsContext, AsContextMut, Engine, Func, FuncType, HeapType, NoFunc, RefType, StoreContextMut, - ValRaw, ValType, + ValRaw, ValType, WasmFuncOrigin, }; use core::ffi::c_void; use core::marker; @@ -102,7 +103,7 @@ where ); let func = self.func.vm_func_ref(store.0); - unsafe { Self::call_raw(&mut store, &self.ty, func, params) } + unsafe { Self::call_raw(&mut store, &self.ty, func, params, self.func.origin()) } } /// Invokes this WebAssembly function with the specified parameters. @@ -141,22 +142,24 @@ where store .on_fiber(|store| { let func = self.func.vm_func_ref(store.0); - unsafe { Self::call_raw(store, &self.ty, func, params) } + unsafe { Self::call_raw(store, &self.ty, func, params, self.func().origin()) } }) .await? } - /// Do a raw call of a typed function. + /// Do a raw call of a typed function, with optional recording/replaying of events. /// /// # Safety /// /// `func` must be of the given type, and it additionally must be a valid /// store-owned pointer within the `store` provided. + /// If providing `rr_origin`, it must exactly match the origin information of `func` pub(crate) unsafe fn call_raw( store: &mut StoreContextMut<'_, T>, ty: &FuncType, func: ptr::NonNull, params: Params, + rr_origin: Option, ) -> Result { // double-check that params/results match for this function's type in // debug mode. @@ -191,29 +194,40 @@ where params.store(&mut store, ty, dst)?; } + let storage_len = + mem::size_of_val::>(&mut storage) / mem::size_of::(); + let storage_slice: *mut Storage<_, _> = &mut storage; + let storage_slice = storage_slice.cast::(); + let storage_slice = core::ptr::slice_from_raw_parts_mut(storage_slice, storage_len); + let storage_slice = NonNull::new(storage_slice).unwrap(); + // Try to capture only a single variable (a tuple) in the closure below. // This means the size of the closure is one pointer and is much more // efficient to move in memory. This closure is actually invoked on the // other side of a C++ shim, so it can never be inlined enough to make // the memory go away, so the size matters here for performance. - let mut captures = (func, storage); - - let result = invoke_wasm_and_catch_traps(store, |caller, vm| { - let (func_ref, storage) = &mut captures; - let storage_len = mem::size_of_val::>(storage) / mem::size_of::(); - let storage: *mut Storage<_, _> = storage; - let storage = storage.cast::(); - let storage = core::ptr::slice_from_raw_parts_mut(storage, storage_len); - let storage = NonNull::new(storage).unwrap(); - - // SAFETY: this function's own contract is that `func_ref` is safe - // to call and additionally that the params/results are correctly - // ascribed for this function call to be safe. - unsafe { VMFuncRef::array_call(*func_ref, vm, caller, storage) } - }); - - let (_, storage) = captures; - result?; + let captures = (func, storage_slice); + + let args_and_results = unsafe { storage_slice.as_ref() }; + // For component mode, Realloc uses this method `TypedFunc::call_raw`, but Realloc is its + // own separate event for record/replay purposes. For now, we use the should_record flag to + // distinguish but this could be removed in the future by folding it into the main function call event. + rr::core_hooks::record_and_replay_validate_wasm_func( + |store| { + invoke_wasm_and_catch_traps(store, |caller, vm| { + let (func_ref, storage) = &captures; + + // SAFETY: this function's own contract is that `func_ref` is safe + // to call and additionally that the params/results are correctly + // ascribed for this function call to be safe. + unsafe { VMFuncRef::array_call(*func_ref, vm, caller, *storage) } + }) + }, + args_and_results, + ty, + rr_origin, + store, + )?; let mut store = AutoAssertNoGc::new(store.0); // SAFETY: this function is itself unsafe to ensure that the result type diff --git a/crates/wasmtime/src/runtime/instance.rs b/crates/wasmtime/src/runtime/instance.rs index 7b04d5b594..d538a7671d 100644 --- a/crates/wasmtime/src/runtime/instance.rs +++ b/crates/wasmtime/src/runtime/instance.rs @@ -1,5 +1,7 @@ use crate::linker::{Definition, DefinitionType}; use crate::prelude::*; +#[cfg(feature = "rr")] +use crate::rr::core_events::InstantiationEvent; use crate::runtime::vm::{ self, Imports, ModuleRuntimeInfo, VMFuncRef, VMFunctionImport, VMGlobalImport, VMMemoryImport, VMTableImport, VMTagImport, @@ -185,6 +187,24 @@ impl Instance { Ok(owned_imports) } + /// Check to flag exported memories in Core wasm modules when recording is enabled + #[cfg(feature = "rr")] + fn rr_assert_unexported_memories(module: &Module) -> Result<()> { + // Check for exported memories when recording is enabled. + if module.engine().is_recording() + && module.exports().any(|export| { + if let crate::ExternType::Memory(_) = export.ty() { + true + } else { + false + } + }) + { + bail!("Cannot support recording for core wasm modules when a memory is exported"); + } + Ok(()) + } + /// Internal function to create an instance and run the start function. /// /// This function's unsafety is the same as `Instance::new_raw`. @@ -200,7 +220,16 @@ impl Instance { // SAFETY: the safety contract of `new_started_impl` is the same as this // function. - unsafe { Self::new_started_impl(store, module, imports) } + let result = unsafe { Self::new_started_impl(store, module, imports) }?; + #[cfg(feature = "rr")] + { + Self::rr_assert_unexported_memories(module)?; + // Record the instantiation event + store + .0 + .record_event(|| InstantiationEvent(*module.checksum(), result.id()))?; + } + Ok(result) } /// Internal function to create an instance and run the start function. @@ -242,7 +271,16 @@ impl Instance { .on_fiber(|store| { // SAFETY: the unsafe contract of `new_started_impl` is the same // as this function. - unsafe { Self::new_started_impl(store, module, imports) } + let result = unsafe { Self::new_started_impl(store, module, imports) }?; + #[cfg(feature = "rr")] + { + Self::rr_assert_unexported_memories(module)?; + // Record the instantiation event + store + .0 + .record_event(|| InstantiationEvent(*module.checksum(), result.id()))?; + } + Ok(result) }) .await? } @@ -445,7 +483,7 @@ impl Instance { Some(self._get_export(store, export.entity)) } - fn _get_export(&self, store: &mut StoreOpaque, entity: EntityIndex) -> Extern { + pub(crate) fn _get_export(&self, store: &mut StoreOpaque, entity: EntityIndex) -> Extern { let id = store.id(); // SAFETY: the store `id` owns this instance and all exports contained // within. @@ -932,22 +970,6 @@ fn pre_instantiate_raw( imports.push(&item, store); } - #[cfg(feature = "rr")] - if module.engine().is_recording() - && module.exports().any(|export| { - use crate::ExternType; - if let ExternType::Memory(_) = export.ty() { - true - } else { - false - } - }) - { - bail!( - "Cannot support recording for core wasm modules when a memory is exported; consider using components" - ); - } - Ok(imports) } diff --git a/crates/wasmtime/src/runtime/module.rs b/crates/wasmtime/src/runtime/module.rs index 8ce0161502..a08db04596 100644 --- a/crates/wasmtime/src/runtime/module.rs +++ b/crates/wasmtime/src/runtime/module.rs @@ -157,6 +157,9 @@ struct ModuleInner { /// Runtime offset information for `VMContext`. offsets: VMOffsets, + + /// The SHA-256 checksum of the source binary + checksum: [u8; 32], } impl fmt::Debug for Module { @@ -526,6 +529,7 @@ impl Module { info: CompiledModuleInfo, serializable: bool, ) -> Result { + let checksum = info.checksum; let module = CompiledModule::from_artifacts(code.code_memory().clone(), info, engine.profiler())?; @@ -546,6 +550,7 @@ impl Module { #[cfg(any(feature = "cranelift", feature = "winch"))] serializable, offsets, + checksum, }), }) } @@ -880,6 +885,15 @@ impl Module { &self.inner.engine } + #[allow( + unused, + reason = "used only for verification with wasmtime `rr` feature \ + and requires a lot of unnecessary gating across crates" + )] + pub(crate) fn checksum(&self) -> &[u8; 32] { + &self.inner.checksum + } + /// Returns a summary of the resources required to instantiate this /// [`Module`]. /// diff --git a/crates/wasmtime/src/runtime/rr.rs b/crates/wasmtime/src/runtime/rr.rs index 0b7d61e5ab..f0df61ee86 100644 --- a/crates/wasmtime/src/runtime/rr.rs +++ b/crates/wasmtime/src/runtime/rr.rs @@ -1,6 +1,44 @@ //! Wasmtime's Record and Replay support. //! //! This feature is currently not optimized and under development +use crate::ValRaw; +use ::core::mem::MaybeUninit; + +/// Types that can be serialized/deserialized into/from +/// flat types for record and replay +#[allow( + unused, + reason = "trait used as a bound for hooks despite not calling methods directly" +)] +pub trait FlatBytes { + fn bytes_ref(&self, size: u8) -> &[u8]; + fn from_bytes(value: &[u8]) -> Self; +} + +impl FlatBytes for ValRaw { + #[inline] + fn bytes_ref(&self, size: u8) -> &[u8] { + &self.get_bytes()[..size as usize] + } + #[inline] + fn from_bytes(value: &[u8]) -> Self { + ValRaw::bytes(value) + } +} + +impl FlatBytes for MaybeUninit { + #[inline] + fn bytes_ref(&self, size: u8) -> &[u8] { + // Uninitialized data is assumed and serialized, so hence + // may contain some undefined values + let val = unsafe { self.assume_init_ref() }; + val.bytes_ref(size) + } + #[inline] + fn from_bytes(value: &[u8]) -> Self { + MaybeUninit::new(ValRaw::bytes(value)) + } +} /// Convenience method hooks for injecting event recording/replaying in the rest of the engine mod hooks; diff --git a/crates/wasmtime/src/runtime/rr/core.rs b/crates/wasmtime/src/runtime/rr/core.rs index 31f8ad5045..1f192c2c0f 100644 --- a/crates/wasmtime/src/runtime/rr/core.rs +++ b/crates/wasmtime/src/runtime/rr/core.rs @@ -12,8 +12,8 @@ pub use events::RRFuncArgVals; pub use events::Validate; #[cfg(feature = "rr-component")] pub use events::component_events; +pub use events::{ResultEvent, core_events, marker_events}; use events::{common_events, component_events as __component_events}; -pub use events::{core_events, marker_events}; pub use io::{IOError, RecordWriter, ReplayReader}; /// Settings for execution recording. @@ -126,13 +126,22 @@ rr_event! { CustomMessage(marker_events::CustomMessageEvent), // Common events for both core or component wasm - /// Return from host function to either core Wasm or component + // REQUIRED events + /// Return from host function (core or component) to host HostFuncReturn(common_events::HostFuncReturnEvent), - + // OPTIONAL events + /// Return from Wasm function (core or component) to host + WasmFuncReturn(common_events::WasmFuncReturnEvent), + + // REQUIRED events for replay (Core) + /// Instantiation of a core Wasm module + CoreWasmInstantiation(core_events::InstantiationEvent), + /// Entry from host into a core Wasm function + CoreWasmFuncEntry(core_events::WasmFuncEntryEvent), /// Call into host function from core Wasm CoreHostFuncEntry(core_events::HostFuncEntryEvent), - // REQUIRED events for replay + // REQUIRED events for replay (Component) /// Starting marker for a Wasm component function call from host /// @@ -154,10 +163,8 @@ rr_event! { /// Return from a component builtin ComponentBuiltinReturn(__component_events::BuiltinReturnEvent), - // OPTIONAL events for replay validation + // OPTIONAL events for replay validation (Component) - /// Return from a Wasm component function back to host - ComponentWasmFuncReturn(__component_events::WasmFuncReturnEvent), /// Return from Component ABI realloc call /// /// Since realloc is deterministic, ReallocReturn is optional. @@ -196,6 +203,7 @@ pub enum ReplayError { EventError(Box), MissingComponentOrModule, MissingComponentOrModuleInstance, + IncorrectCoreFuncIndex, } impl fmt::Display for ReplayError { @@ -226,6 +234,9 @@ impl fmt::Display for ReplayError { Self::MissingComponentOrModuleInstance => { write!(f, "missing component or module instance for replay") } + Self::IncorrectCoreFuncIndex => { + write!(f, "incorrect core function index encountered during replay") + } } } } @@ -486,6 +497,7 @@ impl Iterator for ReplayBuffer { impl Drop for ReplayBuffer { fn drop(&mut self) { let mut remaining_events = 0; + log::info!("Replay buffer is being dropped; checking for remaining replay events..."); // Cannot use count() in iterator because IO error may loop indefinitely while let Some(event) = self.next() { event.unwrap(); @@ -493,10 +505,11 @@ impl Drop for ReplayBuffer { } if remaining_events > 0 { log::warn!( - "Replay buffer is dropped with {} remaining events, - and is likely an invalid/incomplete execution", + "{} events were remaining in the replay buffer. This is likely the result of an erroneous/incomplete execution", remaining_events ); + } else { + log::info!("All replay events were successfully processed."); } } } diff --git a/crates/wasmtime/src/runtime/rr/core/events.rs b/crates/wasmtime/src/runtime/rr/core/events.rs index bd3e034681..1fe2552e69 100644 --- a/crates/wasmtime/src/runtime/rr/core/events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events.rs @@ -1,7 +1,8 @@ #[cfg(any(feature = "rr-component", feature = "rr-validate"))] use super::ReplayError; -use crate::ValRaw; -use crate::prelude::*; +use crate::rr::FlatBytes; +use crate::{AsContextMut, Val, prelude::*}; +use crate::{ValRaw, ValType}; use core::fmt; use core::mem::MaybeUninit; use serde::{Deserialize, Serialize}; @@ -22,38 +23,6 @@ pub trait EventError: core::error::Error + Send + Sync + 'static { fn get(&self) -> &String; } -/// Types that can be serialized/deserialized into/from -/// flat types for record and replay -pub trait FlatBytes { - fn bytes_ref(&self, size: u8) -> &[u8]; - fn from_bytes(value: &[u8]) -> Self; -} - -impl FlatBytes for ValRaw { - #[inline] - fn bytes_ref(&self, size: u8) -> &[u8] { - &self.get_bytes()[..size as usize] - } - #[inline] - fn from_bytes(value: &[u8]) -> Self { - ValRaw::bytes(value) - } -} - -impl FlatBytes for MaybeUninit { - #[inline] - fn bytes_ref(&self, size: u8) -> &[u8] { - // Uninitialized data is assumed and serialized, so hence - // may contain some undefined values - let val = unsafe { self.assume_init_ref() }; - val.bytes_ref(size) - } - #[inline] - fn from_bytes(value: &[u8]) -> Self { - MaybeUninit::new(ValRaw::bytes(value)) - } -} - /// Representation of flat arguments for function entry/return #[derive(Serialize, Deserialize, Clone, PartialEq)] pub struct RRFuncArgVals { @@ -116,6 +85,18 @@ impl RRFuncArgVals { pos += flat_size as usize; } } + + /// Generate a vector of [`crate::Val`] from [`RRFuncArgVals`] + pub fn to_val_vec(self, mut store: impl AsContextMut, val_types: Vec) -> Vec { + let mut pos = 0; + let mut vals = Vec::new(); + for (flat_size, val_type) in self.sizes.into_iter().zip(val_types.into_iter()) { + let raw = ValRaw::bytes(&self.bytes[pos..pos + flat_size as usize]); + vals.push(unsafe { Val::from_raw(&mut store, raw, val_type) }); + pos += flat_size as usize; + } + vals + } } /// Trait signifying types that can be validated on replay @@ -158,6 +139,90 @@ where } } +/// Result newtype for events that can be serialized/deserialized for record/replay. +/// +/// Anyhow result types cannot use blanket PartialEq implementations since +/// anyhow results are not serialized directly. They need to specifically check +/// for divergence between recorded and replayed effects with [EventError] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ResultEvent(Result); + +impl ResultEvent +where + T: Clone, + E: EventError, +{ + pub fn from_anyhow_result(ret: &Result) -> Self { + Self( + ret.as_ref() + .map(|t| (*t).clone()) + .map_err(|e| E::new(e.to_string())), + ) + } + pub fn ret(self) -> Result { + self.0 + } +} + +impl Validate> for ResultEvent +where + T: fmt::Debug + PartialEq, + E: EventError, +{ + fn validate(&self, expect_ret: &Result) -> Result<(), ReplayError> { + self.log(); + // Cannot just use eq since anyhow::Error and EventError cannot be compared + match (self.0.as_ref(), expect_ret.as_ref()) { + (Ok(r), Ok(s)) => { + if r == s { + Ok(()) + } else { + Err(ReplayError::FailedValidation) + } + } + // Return the recorded error + (Err(e), Err(f)) => Err(ReplayError::from(E::new(format!( + "Error on execution: {} | Error from recording: {}", + f, + e.get() + )))), + // Diverging errors.. Report as a failed validation + (Ok(_), Err(_)) => Err(ReplayError::FailedValidation), + (Err(_), Ok(_)) => Err(ReplayError::FailedValidation), + } + } +} + +macro_rules! event_error_types { + ( + $( + $( #[cfg($attr:meta)] )? + pub struct $ee:ident(..) + ),* + ) => ( + $( + /// Return from a reallocation call (needed only for validation) + #[derive(Debug, Serialize, Deserialize, Clone)] + pub struct $ee(String); + + impl core::error::Error for $ee {} + impl fmt::Display for $ee { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", &self.0) + } + } + impl EventError for $ee { + fn new(t: String) -> Self where Self: Sized { Self(t) } + fn get(&self) -> &String { &self.0 } + } + )* + ); +} + +event_error_types! { + pub struct WasmFuncReturnError(..) +} + /// Events used as markers for debugging/testing in traces /// /// Marker events should be injectable at any point in a record diff --git a/crates/wasmtime/src/runtime/rr/core/events/common_events.rs b/crates/wasmtime/src/runtime/rr/core/events/common_events.rs index 740dc55d37..3be803cb89 100644 --- a/crates/wasmtime/src/runtime/rr/core/events/common_events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events/common_events.rs @@ -7,7 +7,7 @@ use super::*; use serde::{Deserialize, Serialize}; use wasmtime_environ::component::FlatTypesStorage; -/// A return event after a host call for a core OR component Wasm +/// A return event after a host call to Wasm (core or component) /// /// Matches with either [`component_events::HostFuncEntryEvent`] or /// [`core_events::HostFuncEntryEvent`] @@ -18,7 +18,10 @@ pub struct HostFuncReturnEvent { } impl HostFuncReturnEvent { // Record - pub fn new_from_u8(args: &[MaybeUninit], flat: &[u8]) -> Self { + pub fn new_from_u8(args: &[T], flat: &[u8]) -> Self + where + T: FlatBytes, + { Self { args: RRFuncArgVals::from_raw_slice(args, flat.iter().copied()), } @@ -33,7 +36,23 @@ impl HostFuncReturnEvent { // Replay /// Consume the caller event and encode it back into the slice - pub fn move_into_slice(self, args: &mut [MaybeUninit]) { + pub fn move_into_slice(self, args: &mut [T]) + where + T: FlatBytes, + { self.args.into_raw_slice(args); } } + +/// A return event from a Wasm (core or component) function to host +/// +/// Matches with either [`component_events::WasmFuncEntryEvent`] or +/// [`core_events::WasmFuncEntryEvent`] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WasmFuncReturnEvent(pub ResultEvent); + +impl Validate<&Result> for WasmFuncReturnEvent { + fn validate(&self, expect: &&Result) -> Result<(), ReplayError> { + self.0.validate(*expect) + } +} diff --git a/crates/wasmtime/src/runtime/rr/core/events/component_events.rs b/crates/wasmtime/src/runtime/rr/core/events/component_events.rs index db9b30ee2d..0150f6cffa 100644 --- a/crates/wasmtime/src/runtime/rr/core/events/component_events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events/component_events.rs @@ -111,91 +111,10 @@ pub struct MemorySliceWriteEvent { pub bytes: Vec, } -/// Result newtype for events that can be serialized/deserialized for record/replay. -/// -/// Anyhow result types cannot use blanket PartialEq implementations since -/// anyhow results are not serialized directly. They need to specifically check -/// for divergence between recorded and replayed effects with [EventError] -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ResultEvent(Result); - -impl ResultEvent -where - T: Clone, - E: EventError, -{ - pub fn from_anyhow_result(ret: &Result) -> Self { - Self( - ret.as_ref() - .map(|t| (*t).clone()) - .map_err(|e| E::new(e.to_string())), - ) - } - pub fn ret(self) -> Result { - self.0 - } -} - -impl Validate> for ResultEvent -where - T: fmt::Debug + PartialEq, - E: EventError, -{ - fn validate(&self, expect_ret: &Result) -> Result<(), ReplayError> { - self.log(); - // Cannot just use eq since anyhow::Error and EventError cannot be compared - match (self.0.as_ref(), expect_ret.as_ref()) { - (Ok(r), Ok(s)) => { - if r == s { - Ok(()) - } else { - Err(ReplayError::FailedValidation) - } - } - // Return the recorded error - (Err(e), Err(f)) => Err(ReplayError::from(E::new(format!( - "Error on execution: {} | Error from recording: {}", - f, - e.get() - )))), - // Diverging errors.. Report as a failed validation - (Ok(_), Err(_)) => Err(ReplayError::FailedValidation), - (Err(_), Ok(_)) => Err(ReplayError::FailedValidation), - } - } -} - -macro_rules! event_error_types { - ( - $( - $( #[cfg($attr:meta)] )? - pub struct $ee:ident(..) - ),* - ) => ( - $( - /// Return from a reallocation call (needed only for validation) - #[derive(Debug, Serialize, Deserialize, Clone)] - pub struct $ee(String); - - impl core::error::Error for $ee {} - impl fmt::Display for $ee { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", &self.0) - } - } - impl EventError for $ee { - fn new(t: String) -> Self where Self: Sized { Self(t) } - fn get(&self) -> &String { &self.0 } - } - )* - ); -} - event_error_types! { pub struct ReallocError(..), pub struct LowerFlatError(..), pub struct LowerMemoryError(..), - pub struct WasmFuncReturnError(..), pub struct BuiltinError(..) } @@ -210,20 +129,6 @@ pub struct LowerFlatReturnEvent(pub ResultEvent<(), LowerFlatError>); #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LowerMemoryReturnEvent(pub ResultEvent<(), LowerMemoryError>); -/// A return event from a Wasm component function to Host -/// -/// Matches 1:1 with [`WasmFuncEntryEvent`]. -/// -/// Note: Could potential merge with [`HostFuncReturnEvent`] as [`HostToWasmEvent`]? -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WasmFuncReturnEvent(pub ResultEvent); - -impl Validate<&Result> for WasmFuncReturnEvent { - fn validate(&self, expect: &&Result) -> Result<(), ReplayError> { - self.0.validate(*expect) - } -} - // Macro to generate RR events from the builtin descriptions macro_rules! builtin_events { // Main rule matching component function definitions diff --git a/crates/wasmtime/src/runtime/rr/core/events/core_events.rs b/crates/wasmtime/src/runtime/rr/core/events/core_events.rs index 88156d8473..01e24d25f7 100644 --- a/crates/wasmtime/src/runtime/rr/core/events/core_events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events/core_events.rs @@ -1,23 +1,60 @@ //! Module comprising of core wasm events use super::*; -use wasmtime_environ::VMSharedTypeIndex; +use crate::AsContextMut; +use crate::{Val, ValType, WasmFuncOrigin, store::InstanceId}; // Re-export common events from this module pub use common_events::*; +/// A core Wasm instantiatation event +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Ord, PartialOrd)] +pub struct InstantiationEvent( + /// Checksum of the bytecode used to instantiate the module + pub [u8; 32], + pub InstanceId, +); + +/// A call event from Host into a core Wasm function +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct WasmFuncEntryEvent { + /// Checksum of module containing function + pub module: [u8; 32], + /// Origin (instance + function index) for this function + pub origin: WasmFuncOrigin, + /// Raw values passed across call boundary + pub args: RRFuncArgVals, +} + +impl WasmFuncEntryEvent { + /// Record + pub fn new(module: [u8; 32], origin: WasmFuncOrigin, args: &[ValRaw], flat: &[u8]) -> Self { + Self { + module, + origin, + args: RRFuncArgVals::from_raw_slice(args, flat.iter().copied()), + } + } + + // Replay + /// Consume the caller event and encode it back into the slice + pub fn to_val_vec(self, store: impl AsContextMut, vals: Vec) -> Vec { + self.args.to_val_vec(store, vals) + } +} + /// A call event from a Core Wasm module into the host #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct HostFuncEntryEvent { /// Raw values passed across the call/return boundary args: RRFuncArgVals, - /// Param/return types (required to support replay validation) - types: VMSharedTypeIndex, } impl HostFuncEntryEvent { // Record - pub fn new(args: &[MaybeUninit], flat: &[u8], types: VMSharedTypeIndex) -> Self { + pub fn new(args: &[T], flat: &[u8]) -> Self + where + T: FlatBytes, + { Self { args: RRFuncArgVals::from_raw_slice(args, flat.iter().copied()), - types: types, } } } diff --git a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs index b06ace8a89..488734f90f 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs @@ -2,9 +2,10 @@ use crate::ValRaw; #[cfg(feature = "component-model")] use crate::component::func::LowerContext; #[cfg(feature = "rr-component")] +use crate::rr::ResultEvent; +#[cfg(feature = "rr-component")] use crate::rr::component_events::{ - HostFuncReturnEvent, LowerFlatReturnEvent, LowerMemoryReturnEvent, ResultEvent, - WasmFuncEntryEvent, + HostFuncReturnEvent, LowerFlatReturnEvent, LowerMemoryReturnEvent, WasmFuncEntryEvent, }; #[cfg(all(feature = "rr-component", feature = "rr-validate"))] use crate::rr::{RRFuncArgVals, component_events::WasmFuncReturnEvent}; diff --git a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs index 1d99e35c26..057249df78 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs @@ -1,59 +1,118 @@ -use crate::ValRaw; -use crate::prelude::*; +use crate::rr::FlatBytes; #[cfg(feature = "rr")] -use crate::rr::core_events::HostFuncReturnEvent; +use crate::rr::core_events::{HostFuncReturnEvent, WasmFuncEntryEvent}; +#[cfg(all(feature = "rr", feature = "rr-validate"))] +use crate::rr::{ + RRFuncArgVals, ResultEvent, core_events::HostFuncEntryEvent, core_events::WasmFuncReturnEvent, +}; use crate::store::StoreOpaque; -use core::mem::MaybeUninit; -use wasmtime_environ::VMSharedTypeIndex; +use crate::{FuncType, StoreContextMut, ValRaw, WasmFuncOrigin, prelude::*}; +/// Record and replay hook operation for core wasm function entry events +/// +/// Recording/replay validation DOES NOT happen if origin is `None` #[inline] +pub fn record_and_replay_validate_wasm_func( + wasm_call: F, + args: &[ValRaw], + ty: &FuncType, + origin: Option, + store: &mut StoreContextMut<'_, T>, +) -> Result<()> +where + F: FnOnce(&mut StoreContextMut<'_, T>) -> Result<()>, +{ + let _ = (args, ty, origin); + #[cfg(feature = "rr")] + { + if let Some(origin) = origin { + use crate::store::StoreInstanceId; + + let flat = ty + .params() + .map(|t| t.to_wasm_type().byte_size()) + .collect::>(); + let checksum = *store + .0 + .module_for_instance(StoreInstanceId::new(store.0.id(), origin.instance)) + .unwrap() + .checksum(); + store.0.record_event(|| { + WasmFuncEntryEvent::new(checksum, origin, args, flat.as_slice()) + })?; + } + } + let result = wasm_call(store); + #[cfg(all(feature = "rr", feature = "rr-validate"))] + { + if origin.is_some() { + let flat = ty + .results() + .map(|t| t.to_wasm_type().byte_size()) + .collect::>(); + let result = result.map(|_| RRFuncArgVals::from_raw_slice(args, flat.iter().copied())); + store.0.record_event_validation(|| { + WasmFuncReturnEvent(ResultEvent::from_anyhow_result(&result)) + })?; + store + .0 + .next_replay_event_validation::>( + || &result, + )?; + result?; + } + return Ok(()); + } + #[cfg(not(all(feature = "rr", feature = "rr-validate")))] + return result; +} + /// Record and replay hook operation for host function entry events -pub fn record_replay_host_func_entry( - args: &[MaybeUninit], +#[inline] +pub fn record_replay_host_func_entry( + args: &[T], flat: &[u8], - ty: &VMSharedTypeIndex, store: &mut StoreOpaque, -) -> Result<()> { +) -> Result<()> +where + T: FlatBytes, +{ #[cfg(all(feature = "rr", feature = "rr-validate"))] { // Record/replay the raw parameter args - use crate::rr::core_events::HostFuncEntryEvent; - store.record_event_validation(|| HostFuncEntryEvent::new(&args, flat, ty.clone()))?; + store.record_event_validation(|| HostFuncEntryEvent::new(args, flat))?; store.next_replay_event_validation::(|| { - HostFuncEntryEvent::new(&args, flat, ty.clone()) + HostFuncEntryEvent::new(args, flat) })?; } - let _ = (args, flat, ty, store); + let _ = (args, flat, store); Ok(()) } -#[inline] /// Record hook operation for host function return events -pub fn record_host_func_return( - args: &[MaybeUninit], - flat: &[u8], - ty: &VMSharedTypeIndex, - store: &mut StoreOpaque, -) -> Result<()> { +#[inline] +pub fn record_host_func_return(args: &[T], flat: &[u8], store: &mut StoreOpaque) -> Result<()> +where + T: FlatBytes, +{ // Record the return values #[cfg(feature = "rr")] store.record_event(|| HostFuncReturnEvent::new_from_u8(&args, flat))?; - let _ = (args, flat, ty, store); + let _ = (args, flat, store); Ok(()) } -#[inline] /// Replay hook operation for host function return events -pub fn replay_host_func_return( - args: &mut [MaybeUninit], - ty: &VMSharedTypeIndex, - store: &mut StoreOpaque, -) -> Result<()> { +#[inline] +pub fn replay_host_func_return(args: &mut [T], store: &mut StoreOpaque) -> Result<()> +where + T: FlatBytes, +{ #[cfg(feature = "rr")] store.next_replay_event_and(|event: HostFuncReturnEvent| { event.move_into_slice(args); Ok(()) })?; - let _ = (args, ty, store); + let _ = (args, store); Ok(()) } diff --git a/crates/wasmtime/src/runtime/rr/replay_driver.rs b/crates/wasmtime/src/runtime/rr/replay_driver.rs index 866c04ea24..1ae49847fb 100644 --- a/crates/wasmtime/src/runtime/rr/replay_driver.rs +++ b/crates/wasmtime/src/runtime/rr/replay_driver.rs @@ -1,12 +1,16 @@ -use crate::component::{self, Component, Val}; +#[cfg(feature = "rr-component")] +use crate::component::{self, Component}; #[cfg(feature = "rr-component")] use crate::rr::component_events; -use crate::rr::{RREvent, ReplayError, Validate, component_hooks::ReplayLoweringPhase}; +use crate::rr::{ + RREvent, ReplayError, Validate, component_hooks::ReplayLoweringPhase, core_events, +}; use crate::{ - AsContextMut, Engine, Module, ReplayReader, ReplaySettings, Store, ValRaw, prelude::*, + AsContextMut, Engine, Module, ReplayReader, ReplaySettings, Store, ValRaw, ValType, prelude::*, }; use alloc::collections::BTreeMap; use core::mem::MaybeUninit; +use wasmtime_environ::EntityIndex; #[cfg(feature = "rr-component")] use wasmtime_environ::component::{MAX_FLAT_PARAMS, MAX_FLAT_RESULTS}; @@ -14,7 +18,7 @@ use wasmtime_environ::component::{MAX_FLAT_PARAMS, MAX_FLAT_RESULTS}; #[derive(Clone)] pub struct ReplayEnvironment { engine: Engine, - modules: Vec, + modules: BTreeMap<[u8; 32], Module>, components: BTreeMap<[u8; 32], Component>, settings: ReplaySettings, } @@ -24,7 +28,7 @@ impl ReplayEnvironment { pub fn new(engine: &Engine, settings: ReplaySettings) -> Self { Self { engine: engine.clone(), - modules: Vec::new(), + modules: BTreeMap::new(), components: BTreeMap::new(), settings, } @@ -32,7 +36,7 @@ impl ReplayEnvironment { /// Add a [`Module`] to the replay environment pub fn add_module(&mut self, module: Module) -> &mut Self { - self.modules.push(module); + self.modules.insert(*module.checksum(), module); self } @@ -58,8 +62,9 @@ pub struct ReplayInstance<'a> { store: Store<()>, component_linker: component::Linker<()>, module_linker: crate::Linker<()>, - modules: &'a Vec, + modules: &'a BTreeMap<[u8; 32], Module>, components: &'a BTreeMap<[u8; 32], Component>, + module_instances: BTreeMap, component_instances: BTreeMap, } @@ -73,7 +78,9 @@ impl<'a> ReplayInstance<'a> { let mut component_linker = component::Linker::<()>::new(&env.engine); let mut module_linker = crate::Linker::<()>::new(&env.engine); // Replays shouldn't use any imports, so stub them all out as traps - for module in &env.modules { + for module in env.modules.values() { + // Defining unknown imports as trap seems to not actually trigger the entrypoint? + // Use default values instead for now module_linker.define_unknown_imports_as_traps(module)?; } for component in env.components.values() { @@ -85,6 +92,7 @@ impl<'a> ReplayInstance<'a> { module_linker, modules: &env.modules, components: &env.components, + module_instances: BTreeMap::new(), component_instances: BTreeMap::new(), }) } @@ -101,7 +109,7 @@ impl<'a> ReplayInstance<'a> { { // The only valid "top-level" events are: // * Instantiation events (component/module) - // * Wasm function begin events (component/module) + // * Wasm function begin events (`ComponentWasmFuncBegin` for components and `CoreWasmFuncEntry` for core) // // All other events are transparently dispatched under the context of these top-level events match rr_event? { @@ -152,7 +160,7 @@ impl<'a> ReplayInstance<'a> { // Call the function // // This is almost a mirror of the usage in [`component::Func::call_impl`] - let mut results_storage = [Val::U64(0); MAX_FLAT_RESULTS]; + let mut results_storage = [component::Val::U64(0); MAX_FLAT_RESULTS]; let mut num_results = 0; let results = &mut results_storage; let _return = unsafe { @@ -165,9 +173,8 @@ impl<'a> ReplayInstance<'a> { }, |cx, results_ty, src: &[ValRaw; MAX_FLAT_RESULTS]| { // Lifting can proceed exactly as normal - let max_flat = MAX_FLAT_RESULTS; for (result, slot) in - component::Func::lift_results(cx, results_ty, src, max_flat)?.zip(results) + component::Func::lift_results(cx, results_ty, src, MAX_FLAT_RESULTS)?.zip(results) { *slot = result?; num_results += 1; @@ -190,6 +197,60 @@ impl<'a> ReplayInstance<'a> { ); } } + RREvent::CoreWasmInstantiation(event) => { + // Find matching module from environment to instantiate + let module = self + .modules + .get(&event.0) + .ok_or(ReplayError::MissingComponentOrModule)?; + + let instance = self + .module_linker + .instantiate(self.store.as_context_mut(), module)?; + + // Validate the instantiation event + event.validate(&core_events::InstantiationEvent( + *module.checksum(), + instance.id(), + ))?; + + let ret = self.module_instances.insert(event, instance); + // Ensures that an already-instantiated configuration is not re-instantiated + assert!(ret.is_none()); + } + RREvent::CoreWasmFuncEntry(event) => { + // Grab the correct module instance + let key = core_events::InstantiationEvent(event.module, event.origin.instance); + let instance = self + .module_instances + .get_mut(&key) + .ok_or(ReplayError::MissingComponentOrModule)?; + + let entity = EntityIndex::from(event.origin.index); + let mut store = self.store.as_context_mut(); + let func = instance + ._get_export(store.0, entity) + .into_func() + .ok_or(ReplayError::IncorrectCoreFuncIndex)?; + + let params_ty = func.ty(&store).params().collect::>(); + + // Obtain the argument values for function call + let mut results = vec![crate::Val::I64(0); func.ty(&store).results().len()]; + let params = event.to_val_vec(&mut store, params_ty); + + // Call the function + // + // This is almost a mirror of the usage in [`crate::Func::call`] or [`crate::Func::call_async`] + func.call_impl_check_args(&mut store, ¶ms, &mut results)?; + unsafe { + func.call_impl_do_call( + &mut store, + params.as_slice(), + results.as_mut_slice(), + )?; + } + } _ => Err(ReplayError::IncorrectEventVariant)?, } diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index 7ceee62071..28d4e862c7 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -1032,7 +1032,7 @@ impl Store { /// the Store. Replay of events is performed according to provided settings, and /// read from the provided reader. #[cfg(feature = "rr")] - pub fn init_replaying( + pub(crate) fn init_replaying( &mut self, replayer: impl ReplayReader + 'static, settings: ReplaySettings, diff --git a/crates/wasmtime/src/runtime/store/data.rs b/crates/wasmtime/src/runtime/store/data.rs index 19b0110abe..651397f821 100644 --- a/crates/wasmtime/src/runtime/store/data.rs +++ b/crates/wasmtime/src/runtime/store/data.rs @@ -4,12 +4,13 @@ use crate::{StoreContext, StoreContextMut}; use core::num::NonZeroU64; use core::ops::{Index, IndexMut}; use core::pin::Pin; +use serde::{Deserialize, Serialize}; // This is defined here, in a private submodule, so we can explicitly reexport // it only as `pub(crate)`. This avoids a ton of // crate-private-type-in-public-interface errors that aren't really too // interesting to deal with. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)] pub struct InstanceId(u32); wasmtime_environ::entity_impl!(InstanceId); diff --git a/crates/wasmtime/src/runtime/vm/component/libcalls.rs b/crates/wasmtime/src/runtime/vm/component/libcalls.rs index 7b3444de27..b46be38208 100644 --- a/crates/wasmtime/src/runtime/vm/component/libcalls.rs +++ b/crates/wasmtime/src/runtime/vm/component/libcalls.rs @@ -83,7 +83,7 @@ wasmtime_environ::foreach_builtin_component_function!(define_builtins); mod trampolines { use super::{ComponentInstance, VMComponentContext}; #[cfg(feature = "rr-component")] - use crate::rr::{Replayer, component_events::*}; + use crate::rr::{Replayer, ResultEvent, component_events::*}; use core::ptr::NonNull; macro_rules! shims { diff --git a/crates/wasmtime/src/runtime/vm/instance.rs b/crates/wasmtime/src/runtime/vm/instance.rs index 98199ee2ce..9012ffdac0 100644 --- a/crates/wasmtime/src/runtime/vm/instance.rs +++ b/crates/wasmtime/src/runtime/vm/instance.rs @@ -2,6 +2,7 @@ //! wasm module (except its callstack and register state). An //! `InstanceHandle` is a reference-counting handle for an `Instance`. +use crate::WasmFuncOrigin; use crate::prelude::*; use crate::runtime::vm::const_expr::{ConstEvalContext, ConstExprEvaluator}; use crate::runtime::vm::export::Export; @@ -628,12 +629,15 @@ impl Instance { store: StoreId, index: FuncIndex, ) -> crate::Func { + let instance = self.id; let func_ref = self.get_func_ref(index).unwrap(); // SAFETY: the validity of `func_ref` is guaranteed by the validity of // `self`, and the contract that `store` must own `func_ref` is a // contract of this function itself. - unsafe { crate::Func::from_vm_func_ref(store, func_ref) } + let mut func = unsafe { crate::Func::from_vm_func_ref(store, func_ref) }; + func.set_origin(WasmFuncOrigin { index, instance }); + func } /// Lookup a table by index. From 99763bb489d16458717d60b7d3ab375663de114a Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Thu, 13 Nov 2025 15:45:58 -0500 Subject: [PATCH 27/73] Clean up of `ReplayError` messages --- .../src/runtime/component/func/options.rs | 6 +- crates/wasmtime/src/runtime/rr/core.rs | 77 +++++++++++-------- .../wasmtime/src/runtime/rr/replay_driver.rs | 10 +-- 3 files changed, 52 insertions(+), 41 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index d1a943d2db..e552e40119 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -635,7 +635,9 @@ impl<'a, T: 'static> LowerContext<'a, T> { RREvent::ComponentLowerFlatReturn(e) => { #[cfg(feature = "rr-validate")] if run_validate { - _lower_stack.pop().ok_or(ReplayError::InvalidOrdering)?; + _lower_stack + .pop() + .ok_or(ReplayError::InvalidEventPosition)?; } lowering_error = e.0.ret().map_err(Into::into).err(); } @@ -644,7 +646,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { if run_validate { _lower_store_stack .pop() - .ok_or(ReplayError::InvalidOrdering)?; + .ok_or(ReplayError::InvalidEventPosition)?; } lowering_error = e.0.ret().map_err(Into::into).err(); } diff --git a/crates/wasmtime/src/runtime/rr/core.rs b/crates/wasmtime/src/runtime/rr/core.rs index 1f192c2c0f..edd84247bb 100644 --- a/crates/wasmtime/src/runtime/rr/core.rs +++ b/crates/wasmtime/src/runtime/rr/core.rs @@ -3,6 +3,7 @@ use crate::prelude::*; use core::fmt; use events::EventError; use serde::{Deserialize, Serialize}; +use wasmtime_environ::EntityIndex; // Use component 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 @@ -198,12 +199,14 @@ pub enum ReplayError { EmptyBuffer, FailedValidation, IncorrectEventVariant, - InvalidOrdering, + InvalidEventPosition, FailedRead(IOError), EventError(Box), - MissingComponentOrModule, - MissingComponentOrModuleInstance, - IncorrectCoreFuncIndex, + MissingComponent([u8; 32]), + MissingModule([u8; 32]), + MissingComponentInstance(u32), + MissingModuleInstance(u32), + InvalidCoreFuncIndex(EntityIndex), } impl fmt::Display for ReplayError { @@ -213,10 +216,13 @@ impl fmt::Display for ReplayError { write!(f, "replay buffer is empty") } Self::FailedValidation => { - write!(f, "replay event validation failed") + write!( + f, + "failed validation check during replay; see wasmtime log for error" + ) } Self::IncorrectEventVariant => { - write!(f, "event method invoked on incorrect variant") + write!(f, "event type mismatch during replay") } Self::EventError(e) => { write!(f, "{:?}", e) @@ -225,17 +231,37 @@ impl fmt::Display for ReplayError { write!(f, "{}", e)?; f.write_str("Note: Ensure sufficient `deserialization-buffer-size` in replay settings if you included `validation-metadata` during recording") } - Self::InvalidOrdering => { + Self::InvalidEventPosition => { write!(f, "event occured at an invalid position in the trace") } - Self::MissingComponentOrModule => { - write!(f, "missing component or module for replay") + Self::MissingComponent(checksum) => { + write!( + f, + "missing component binary with checksum 0x{} during replay", + checksum + .iter() + .map(|b| format!("{:02x}", b)) + .collect::() + ) } - Self::MissingComponentOrModuleInstance => { - write!(f, "missing component or module instance for replay") + Self::MissingModule(checksum) => { + write!( + f, + "missing module binary with checksum {:02x?} during replay", + checksum + .iter() + .map(|b| format!("{:02x}", b)) + .collect::() + ) } - Self::IncorrectCoreFuncIndex => { - write!(f, "incorrect core function index encountered during replay") + Self::MissingComponentInstance(id) => { + write!(f, "missing component instance ID {:?} during replay", id) + } + Self::MissingModuleInstance(id) => { + write!(f, "missing module instance ID {:?} during replay", id) + } + Self::InvalidCoreFuncIndex(index) => { + write!(f, "replay core func ({:?}) during replay is invalid", index) } } } @@ -306,9 +332,6 @@ pub trait Replayer: Iterator> { /// Get the settings (embedded within the trace) during recording fn trace_settings(&self) -> &RecordSettings; - ///// Peek at the next event without consuming it - //fn peek(&mut self) -> Option>; - // Provided Methods /// Get the next functional replay event (skips past all non-marker events) @@ -458,8 +481,6 @@ pub struct ReplayBuffer { deser_buffer: Vec, /// Whether buffer has been completely read eof_encountered: bool, - /// Peeked event for lookahead - peeked: Option, } impl Iterator for ReplayBuffer { @@ -469,9 +490,6 @@ impl Iterator for ReplayBuffer { if self.eof_encountered { return None; } - if self.peeked.is_some() { - return self.peeked.take().map(Ok); - } let ret = 'event_loop: loop { let result = io::from_replay_reader(&mut self.reader, &mut self.deser_buffer); match result { @@ -499,17 +517,17 @@ impl Drop for ReplayBuffer { let mut remaining_events = 0; log::info!("Replay buffer is being dropped; checking for remaining replay events..."); // Cannot use count() in iterator because IO error may loop indefinitely - while let Some(event) = self.next() { - event.unwrap(); + while let Some(e) = self.next() { + e.unwrap(); remaining_events += 1; } if remaining_events > 0 { log::warn!( - "{} events were remaining in the replay buffer. This is likely the result of an erroneous/incomplete execution", + "{} events were not used in the replay buffer. This is likely the result of an erroneous/incomplete execution", remaining_events ); } else { - log::info!("All replay events were successfully processed."); + log::debug!("All replay events were successfully processed."); } } } @@ -546,7 +564,6 @@ impl Replayer for ReplayBuffer { trace_settings, deser_buffer, eof_encountered: false, - peeked: None, }) } @@ -559,14 +576,6 @@ impl Replayer for ReplayBuffer { fn trace_settings(&self) -> &RecordSettings { &self.trace_settings } - - //#[inline] - //fn peek(&mut self) -> Option> { - // if self.peeked.is_none() { - // self.peeked = self.next(); - // } - // self.peeked.as_ref().map(|r| Ok(r)) - //} } #[cfg(test)] diff --git a/crates/wasmtime/src/runtime/rr/replay_driver.rs b/crates/wasmtime/src/runtime/rr/replay_driver.rs index 1ae49847fb..6223aba464 100644 --- a/crates/wasmtime/src/runtime/rr/replay_driver.rs +++ b/crates/wasmtime/src/runtime/rr/replay_driver.rs @@ -120,7 +120,7 @@ impl<'a> ReplayInstance<'a> { let component = self .components .get(&event.0) - .ok_or(ReplayError::MissingComponentOrModule)?; + .ok_or(ReplayError::MissingComponent(event.0))?; let instance = self .component_linker @@ -151,7 +151,7 @@ impl<'a> ReplayInstance<'a> { let instance = self .component_instances .get_mut(&key) - .ok_or(ReplayError::MissingComponentOrModuleInstance)?; + .ok_or(ReplayError::MissingComponentInstance(key.1.as_u32()))?; // Replay lowering steps and obtain raw value arguments to raw function call let func = component::Func::from_lifted_func(*instance, event.func_idx); @@ -202,7 +202,7 @@ impl<'a> ReplayInstance<'a> { let module = self .modules .get(&event.0) - .ok_or(ReplayError::MissingComponentOrModule)?; + .ok_or(ReplayError::MissingModule(event.0))?; let instance = self .module_linker @@ -224,14 +224,14 @@ impl<'a> ReplayInstance<'a> { let instance = self .module_instances .get_mut(&key) - .ok_or(ReplayError::MissingComponentOrModule)?; + .ok_or(ReplayError::MissingModuleInstance(key.1.as_u32()))?; let entity = EntityIndex::from(event.origin.index); let mut store = self.store.as_context_mut(); let func = instance ._get_export(store.0, entity) .into_func() - .ok_or(ReplayError::IncorrectCoreFuncIndex)?; + .ok_or(ReplayError::InvalidCoreFuncIndex(entity))?; let params_ty = func.ty(&store).params().collect::>(); From 54df80be8ee072ad757ebfc80bbb6d87c803aadb Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Thu, 13 Nov 2025 18:10:16 -0500 Subject: [PATCH 28/73] Add doc example for replay driver --- .../wasmtime/src/runtime/rr/replay_driver.rs | 269 ++++++++++-------- 1 file changed, 145 insertions(+), 124 deletions(-) diff --git a/crates/wasmtime/src/runtime/rr/replay_driver.rs b/crates/wasmtime/src/runtime/rr/replay_driver.rs index 6223aba464..9d37f605cd 100644 --- a/crates/wasmtime/src/runtime/rr/replay_driver.rs +++ b/crates/wasmtime/src/runtime/rr/replay_driver.rs @@ -56,6 +56,26 @@ impl ReplayEnvironment { /// /// Debugger capabilities in the future will interact with this object for /// inserting breakpoints, snapshotting, and restoring state +/// +/// # Example +/// +/// ``` +/// use wasmtime::*; +/// use wasmtime::component::Component; +/// +/// fn main() -> Result<()> { +/// let config = Config::new(); +/// config.rr(RRConfig::Recording); +/// let engine = Engine::new(&config)?; +/// let mut renv = ReplayEnvironment::new(&engine, ReplaySettings::default()); +/// renv.add_component(Component::from_file(&engine, /* path to component file */)?); +/// // You can add more components, or modules with renv.add_module(module); +/// // .... +/// let mut instance = renv.instantiate(BufReader::new(/* path to trace file */))?; +/// instance.run_to_completion()?; +/// Ok(()) +/// } +/// ``` pub struct ReplayInstance<'a> { /// The store doesn't need any host data because the trace format and /// replay is designed to be embedding-agnostic @@ -97,74 +117,66 @@ impl<'a> ReplayInstance<'a> { }) } - /// Run this replay instance to completion - pub fn run_to_completion(&mut self) -> Result<()> { - while let Some(rr_event) = self - .store - .as_context_mut() - .0 - .replay_buffer_mut() - .expect("unexpected; replay buffer must be initialized within an instance") - .next() - { - // The only valid "top-level" events are: - // * Instantiation events (component/module) - // * Wasm function begin events (`ComponentWasmFuncBegin` for components and `CoreWasmFuncEntry` for core) - // - // All other events are transparently dispatched under the context of these top-level events - match rr_event? { - RREvent::ComponentInstantiation(event) => { - #[cfg(feature = "rr-component")] - { - // Find matching component from environment to instantiate - let component = self - .components - .get(&event.0) - .ok_or(ReplayError::MissingComponent(event.0))?; + /// Run a single top-level event from the instance + /// + /// "Top-level" events are those explicitly invoked events, namely: + /// * Instantiation events (component/module) + /// * Wasm function begin events (`ComponentWasmFuncBegin` for components and `CoreWasmFuncEntry` for core) + /// + /// All other events are transparently dispatched under the context of these top-level events + pub fn run_single_top_level_event(&mut self, rr_event: RREvent) -> Result<()> { + match rr_event { + RREvent::ComponentInstantiation(event) => { + #[cfg(feature = "rr-component")] + { + // Find matching component from environment to instantiate + let component = self + .components + .get(&event.0) + .ok_or(ReplayError::MissingComponent(event.0))?; - let instance = self - .component_linker - .instantiate(self.store.as_context_mut(), component)?; - // Validate the instantiation event - event.validate(&component_events::InstantiationEvent( - *component.checksum(), - instance.id().instance(), - ))?; + let instance = self + .component_linker + .instantiate(self.store.as_context_mut(), component)?; + // Validate the instantiation event + event.validate(&component_events::InstantiationEvent( + *component.checksum(), + instance.id().instance(), + ))?; - let ret = self.component_instances.insert(event, instance); - // Ensures that an already-instantiated configuration is not re-instantiated - assert!(ret.is_none()); - } - #[cfg(not(feature = "rr-component"))] - { - anyhow!( - "Cannot parse ComponentInstantation replay event without rr-component feature enabled" - ); - } + let ret = self.component_instances.insert(event, instance); + // Ensures that an already-instantiated configuration is not re-instantiated + assert!(ret.is_none()); + } + #[cfg(not(feature = "rr-component"))] + { + anyhow!( + "Cannot parse ComponentInstantation replay event without rr-component feature enabled" + ); } - RREvent::ComponentWasmFuncBegin(event) => { - #[cfg(feature = "rr-component")] - { - // Grab the correct component instance - let key = - component_events::InstantiationEvent(event.component, event.instance); - let instance = self - .component_instances - .get_mut(&key) - .ok_or(ReplayError::MissingComponentInstance(key.1.as_u32()))?; + } + RREvent::ComponentWasmFuncBegin(event) => { + #[cfg(feature = "rr-component")] + { + // Grab the correct component instance + let key = component_events::InstantiationEvent(event.component, event.instance); + let instance = self + .component_instances + .get_mut(&key) + .ok_or(ReplayError::MissingComponentInstance(key.1.as_u32()))?; - // Replay lowering steps and obtain raw value arguments to raw function call - let func = component::Func::from_lifted_func(*instance, event.func_idx); - let store = self.store.as_context_mut(); + // Replay lowering steps and obtain raw value arguments to raw function call + let func = component::Func::from_lifted_func(*instance, event.func_idx); + let store = self.store.as_context_mut(); - // Call the function - // - // This is almost a mirror of the usage in [`component::Func::call_impl`] - let mut results_storage = [component::Val::U64(0); MAX_FLAT_RESULTS]; - let mut num_results = 0; - let results = &mut results_storage; - let _return = unsafe { - func.call_raw( + // Call the function + // + // This is almost a mirror of the usage in [`component::Func::call_impl`] + let mut results_storage = [component::Val::U64(0); MAX_FLAT_RESULTS]; + let mut num_results = 0; + let results = &mut results_storage; + let _return = unsafe { + func.call_raw( store, |cx, _, dst: &mut MaybeUninit<[MaybeUninit; MAX_FLAT_PARAMS]>| { // For lowering, use replay instead of actual lowering @@ -182,78 +194,87 @@ impl<'a> ReplayInstance<'a> { Ok(()) }, )? - }; + }; - log::info!( - "Returned {:?} for calling {:?}", - &results_storage[..num_results], - func - ); - } - #[cfg(not(feature = "rr-component"))] - { - anyhow!( - "Cannot parse ComponentWasmFuncBegin replay event without rr-component feature enabled" - ); - } + log::info!( + "Returned {:?} for calling {:?}", + &results_storage[..num_results], + func + ); } - RREvent::CoreWasmInstantiation(event) => { - // Find matching module from environment to instantiate - let module = self - .modules - .get(&event.0) - .ok_or(ReplayError::MissingModule(event.0))?; + #[cfg(not(feature = "rr-component"))] + { + anyhow!( + "Cannot parse ComponentWasmFuncBegin replay event without rr-component feature enabled" + ); + } + } + RREvent::CoreWasmInstantiation(event) => { + // Find matching module from environment to instantiate + let module = self + .modules + .get(&event.0) + .ok_or(ReplayError::MissingModule(event.0))?; - let instance = self - .module_linker - .instantiate(self.store.as_context_mut(), module)?; + let instance = self + .module_linker + .instantiate(self.store.as_context_mut(), module)?; - // Validate the instantiation event - event.validate(&core_events::InstantiationEvent( - *module.checksum(), - instance.id(), - ))?; + // Validate the instantiation event + event.validate(&core_events::InstantiationEvent( + *module.checksum(), + instance.id(), + ))?; - let ret = self.module_instances.insert(event, instance); - // Ensures that an already-instantiated configuration is not re-instantiated - assert!(ret.is_none()); - } - RREvent::CoreWasmFuncEntry(event) => { - // Grab the correct module instance - let key = core_events::InstantiationEvent(event.module, event.origin.instance); - let instance = self - .module_instances - .get_mut(&key) - .ok_or(ReplayError::MissingModuleInstance(key.1.as_u32()))?; + let ret = self.module_instances.insert(event, instance); + // Ensures that an already-instantiated configuration is not re-instantiated + assert!(ret.is_none()); + } + RREvent::CoreWasmFuncEntry(event) => { + // Grab the correct module instance + let key = core_events::InstantiationEvent(event.module, event.origin.instance); + let instance = self + .module_instances + .get_mut(&key) + .ok_or(ReplayError::MissingModuleInstance(key.1.as_u32()))?; - let entity = EntityIndex::from(event.origin.index); - let mut store = self.store.as_context_mut(); - let func = instance - ._get_export(store.0, entity) - .into_func() - .ok_or(ReplayError::InvalidCoreFuncIndex(entity))?; + let entity = EntityIndex::from(event.origin.index); + let mut store = self.store.as_context_mut(); + let func = instance + ._get_export(store.0, entity) + .into_func() + .ok_or(ReplayError::InvalidCoreFuncIndex(entity))?; - let params_ty = func.ty(&store).params().collect::>(); + let params_ty = func.ty(&store).params().collect::>(); - // Obtain the argument values for function call - let mut results = vec![crate::Val::I64(0); func.ty(&store).results().len()]; - let params = event.to_val_vec(&mut store, params_ty); + // Obtain the argument values for function call + let mut results = vec![crate::Val::I64(0); func.ty(&store).results().len()]; + let params = event.to_val_vec(&mut store, params_ty); - // Call the function - // - // This is almost a mirror of the usage in [`crate::Func::call`] or [`crate::Func::call_async`] - func.call_impl_check_args(&mut store, ¶ms, &mut results)?; - unsafe { - func.call_impl_do_call( - &mut store, - params.as_slice(), - results.as_mut_slice(), - )?; - } + // Call the function + // + // This is almost a mirror of the usage in [`crate::Func::call`] or [`crate::Func::call_async`] + func.call_impl_check_args(&mut store, ¶ms, &mut results)?; + unsafe { + func.call_impl_do_call(&mut store, params.as_slice(), results.as_mut_slice())?; } - - _ => Err(ReplayError::IncorrectEventVariant)?, } + + _ => Err(ReplayError::IncorrectEventVariant)?, + } + Ok(()) + } + /// Run this replay instance to completion + pub fn run_to_completion(&mut self) -> Result<()> { + while let Some(rr_event) = self + .store + .as_context_mut() + .0 + .replay_buffer_mut() + .expect("unexpected; replay buffer must be initialized within an instance") + .next() + { + self.run_single_top_level_event(rr_event?)?; } Ok(()) } From a5e936cad320a084bbd7716d4ad2ceebd2c0c79b Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Thu, 13 Nov 2025 18:55:34 -0500 Subject: [PATCH 29/73] Rename and condense events for clarity --- .../src/runtime/component/func/options.rs | 4 +- .../src/runtime/component/instance.rs | 5 +- crates/wasmtime/src/runtime/func.rs | 4 +- crates/wasmtime/src/runtime/instance.rs | 14 +++--- crates/wasmtime/src/runtime/rr/core/events.rs | 22 ++++++++- .../runtime/rr/core/events/common_events.rs | 30 +----------- .../rr/core/events/component_events.rs | 47 ++++--------------- .../src/runtime/rr/core/events/core_events.rs | 42 +++-------------- .../src/runtime/rr/hooks/component_hooks.rs | 14 ++++-- .../src/runtime/rr/hooks/core_hooks.rs | 34 +++++++++----- .../wasmtime/src/runtime/rr/replay_driver.rs | 40 +++++++++------- 11 files changed, 104 insertions(+), 152 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index e552e40119..4cebf7c4a5 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -605,7 +605,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); + e.args.into_raw_slice(storage); } complete = true; } @@ -619,7 +619,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); + e.args.into_raw_slice(storage); } complete = true; } diff --git a/crates/wasmtime/src/runtime/component/instance.rs b/crates/wasmtime/src/runtime/component/instance.rs index 9563175543..df941c9156 100644 --- a/crates/wasmtime/src/runtime/component/instance.rs +++ b/crates/wasmtime/src/runtime/component/instance.rs @@ -1031,8 +1031,9 @@ impl InstancePre { #[cfg(feature = "rr-component")] { use crate::rr::component_events::InstantiationEvent; - store.0.record_event(|| { - InstantiationEvent(*self.component.checksum(), instantiator.id()) + store.0.record_event(|| InstantiationEvent { + component: *self.component.checksum(), + instance: instantiator.id(), })?; } instantiator.run(&mut store).map_err(|e| { diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 2ff360b8dc..266caa19aa 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -1294,7 +1294,7 @@ impl Func { .map(|x| x.to_wasm_type().byte_size()) .collect::>(); - rr::core_hooks::record_replay_host_func_entry( + rr::core_hooks::record_and_replay_validate_host_func_entry( values_vec, &flat_params, &mut caller.store.0, @@ -2451,7 +2451,7 @@ impl HostContext { // 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::core_hooks::record_replay_host_func_entry( + rr::core_hooks::record_and_replay_validate_host_func_entry( unsafe { &args.as_ref()[..num_params] }, flat_size_params.as_slice(), caller.store.0, diff --git a/crates/wasmtime/src/runtime/instance.rs b/crates/wasmtime/src/runtime/instance.rs index d538a7671d..1da79546b4 100644 --- a/crates/wasmtime/src/runtime/instance.rs +++ b/crates/wasmtime/src/runtime/instance.rs @@ -225,9 +225,10 @@ impl Instance { { Self::rr_assert_unexported_memories(module)?; // Record the instantiation event - store - .0 - .record_event(|| InstantiationEvent(*module.checksum(), result.id()))?; + store.0.record_event(|| InstantiationEvent { + module: *module.checksum(), + instance: result.id(), + })?; } Ok(result) } @@ -276,9 +277,10 @@ impl Instance { { Self::rr_assert_unexported_memories(module)?; // Record the instantiation event - store - .0 - .record_event(|| InstantiationEvent(*module.checksum(), result.id()))?; + store.0.record_event(|| InstantiationEvent { + module: *module.checksum(), + instance: result.id(), + })?; } Ok(result) }) diff --git a/crates/wasmtime/src/runtime/rr/core/events.rs b/crates/wasmtime/src/runtime/rr/core/events.rs index 1fe2552e69..1345cd62e5 100644 --- a/crates/wasmtime/src/runtime/rr/core/events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events.rs @@ -4,8 +4,9 @@ use crate::rr::FlatBytes; use crate::{AsContextMut, Val, prelude::*}; use crate::{ValRaw, ValType}; use core::fmt; -use core::mem::MaybeUninit; use serde::{Deserialize, Serialize}; +#[cfg(feature = "rr-component")] +use wasmtime_environ::component::FlatTypesStorage; /// A serde compatible representation of errors produced during execution /// of certain events @@ -60,7 +61,7 @@ impl fmt::Debug for RRFuncArgVals { } impl RRFuncArgVals { - /// Construct [`RRFuncArgVals`] from raw value buffer and flat sizes + /// Construct [`RRFuncArgVals`] from raw value buffer and a flat size iterator pub fn from_raw_slice(args: &[T], flat: impl Iterator) -> RRFuncArgVals where T: FlatBytes, @@ -74,6 +75,23 @@ impl RRFuncArgVals { RRFuncArgVals { bytes, sizes } } + /// Construct [`RRFuncArgVals`] from raw value buffer and a [`FlatTypesStorage`] + #[cfg(feature = "rr-component")] + pub fn from_flat_storage(args: &[T], flat: FlatTypesStorage) -> RRFuncArgVals + where + T: FlatBytes, + { + RRFuncArgVals::from_raw_slice(args, flat.iter32()) + } + + /// Construct [`RRFuncArgVals`] from raw value buffer and a `[&u8]` slice + pub fn from_flat_u8(args: &[T], flat: &[u8]) -> RRFuncArgVals + where + T: FlatBytes, + { + RRFuncArgVals::from_raw_slice(args, flat.iter().copied()) + } + /// Encode [`RRFuncArgVals`] back into raw value buffer pub fn into_raw_slice(self, raw_args: &mut [T]) where diff --git a/crates/wasmtime/src/runtime/rr/core/events/common_events.rs b/crates/wasmtime/src/runtime/rr/core/events/common_events.rs index 3be803cb89..c67cd48666 100644 --- a/crates/wasmtime/src/runtime/rr/core/events/common_events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events/common_events.rs @@ -5,7 +5,6 @@ use super::*; use serde::{Deserialize, Serialize}; -use wasmtime_environ::component::FlatTypesStorage; /// A return event after a host call to Wasm (core or component) /// @@ -14,34 +13,7 @@ use wasmtime_environ::component::FlatTypesStorage; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HostFuncReturnEvent { /// Raw values passed across the call/return boundary - args: RRFuncArgVals, -} -impl HostFuncReturnEvent { - // Record - pub fn new_from_u8(args: &[T], flat: &[u8]) -> Self - where - T: FlatBytes, - { - Self { - args: RRFuncArgVals::from_raw_slice(args, flat.iter().copied()), - } - } - - #[cfg(feature = "rr-component")] - pub fn new_from_flat_storage(args: &[MaybeUninit], flat: FlatTypesStorage) -> Self { - Self { - args: RRFuncArgVals::from_raw_slice(args, flat.iter32()), - } - } - - // Replay - /// Consume the caller event and encode it back into the slice - pub fn move_into_slice(self, args: &mut [T]) - where - T: FlatBytes, - { - self.args.into_raw_slice(args); - } + pub args: RRFuncArgVals, } /// A return event from a Wasm (core or component) function to host diff --git a/crates/wasmtime/src/runtime/rr/core/events/component_events.rs b/crates/wasmtime/src/runtime/rr/core/events/component_events.rs index 0150f6cffa..99d51c51d3 100644 --- a/crates/wasmtime/src/runtime/rr/core/events/component_events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events/component_events.rs @@ -7,7 +7,7 @@ use crate::vm::component::libcalls::ResourceDropRet; pub use common_events::*; use wasmtime_environ::{ self, - component::{ExportIndex, FlatTypesStorage, InterfaceType, TypeFuncIndex}, + component::{ExportIndex, InterfaceType}, }; /// Beginning marker for a Wasm component function call from host @@ -23,56 +23,25 @@ pub struct WasmFuncBeginEvent { /// A [`Component`] instantiatation event #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Ord, PartialOrd)] -pub struct InstantiationEvent( +pub struct InstantiationEvent { /// Checksum of the bytecode used to instantiate the component - pub [u8; 32], - pub ComponentInstanceId, -); + pub component: [u8; 32], + /// Instance ID for the instantiated component + pub instance: ComponentInstanceId, +} /// A call event from Host into a Wasm component function -/// -/// Note: Could potential merge with [`HostFuncReturnEvent`] as [`WasmToHostEvent`]? #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WasmFuncEntryEvent { /// Raw values passed across call boundary - args: RRFuncArgVals, -} -impl WasmFuncEntryEvent { - // Record - pub fn new(args: &[ValRaw], flat: FlatTypesStorage) -> Self { - Self { - args: RRFuncArgVals::from_raw_slice(args, flat.iter32()), - } - } - - // Replay - /// Consume the caller event and encode it back into the slice - pub fn move_into_slice(self, args: &mut [MaybeUninit]) { - self.args.into_raw_slice(args); - } + pub args: RRFuncArgVals, } /// A call event from a Wasm component into the host #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct HostFuncEntryEvent { /// Raw values passed across the call entry boundary - args: RRFuncArgVals, - - /// Function index (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. - ty: TypeFuncIndex, -} -impl HostFuncEntryEvent { - // Record - pub fn new(args: &[MaybeUninit], flat: FlatTypesStorage, ty: TypeFuncIndex) -> Self { - Self { - args: RRFuncArgVals::from_raw_slice(args, flat.iter32()), - ty: ty, - } - } + pub args: RRFuncArgVals, } /// A reallocation call event in the Component Model canonical ABI diff --git a/crates/wasmtime/src/runtime/rr/core/events/core_events.rs b/crates/wasmtime/src/runtime/rr/core/events/core_events.rs index 01e24d25f7..e66ffa3b01 100644 --- a/crates/wasmtime/src/runtime/rr/core/events/core_events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events/core_events.rs @@ -1,17 +1,17 @@ //! Module comprising of core wasm events use super::*; -use crate::AsContextMut; -use crate::{Val, ValType, WasmFuncOrigin, store::InstanceId}; +use crate::{WasmFuncOrigin, store::InstanceId}; // Re-export common events from this module pub use common_events::*; /// A core Wasm instantiatation event #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Ord, PartialOrd)] -pub struct InstantiationEvent( +pub struct InstantiationEvent { /// Checksum of the bytecode used to instantiate the module - pub [u8; 32], - pub InstanceId, -); + pub module: [u8; 32], + /// Instance ID for the instantiated module + pub instance: InstanceId, +} /// A call event from Host into a core Wasm function #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] @@ -24,37 +24,9 @@ pub struct WasmFuncEntryEvent { pub args: RRFuncArgVals, } -impl WasmFuncEntryEvent { - /// Record - pub fn new(module: [u8; 32], origin: WasmFuncOrigin, args: &[ValRaw], flat: &[u8]) -> Self { - Self { - module, - origin, - args: RRFuncArgVals::from_raw_slice(args, flat.iter().copied()), - } - } - - // Replay - /// Consume the caller event and encode it back into the slice - pub fn to_val_vec(self, store: impl AsContextMut, vals: Vec) -> Vec { - self.args.to_val_vec(store, vals) - } -} - /// A call event from a Core Wasm module into the host #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct HostFuncEntryEvent { /// Raw values passed across the call/return boundary - args: RRFuncArgVals, -} -impl HostFuncEntryEvent { - // Record - pub fn new(args: &[T], flat: &[u8]) -> Self - where - T: FlatBytes, - { - Self { - args: RRFuncArgVals::from_raw_slice(args, flat.iter().copied()), - } - } + pub args: RRFuncArgVals, } diff --git a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs index 488734f90f..6ca33b6dd5 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs @@ -45,9 +45,9 @@ where &InterfaceType::Tuple(types[type_idx].params), MAX_FLAT_PARAMS, ); - store - .0 - .record_event(|| WasmFuncEntryEvent::new(args, flat_params))?; + store.0.record_event(|| WasmFuncEntryEvent { + args: RRFuncArgVals::from_flat_storage(args, flat_params), + })?; } let result = wasm_call(store); #[cfg(all(feature = "rr-component", feature = "rr-validate"))] @@ -88,7 +88,9 @@ pub fn record_and_replay_validate_host_func_entry( &InterfaceType::Tuple(types[*type_idx].params), MAX_FLAT_PARAMS, ); - HostFuncEntryEvent::new(args, flat_params, type_idx.clone()) + HostFuncEntryEvent { + args: RRFuncArgVals::from_flat_storage(args, flat_params), + } }; store.record_event_validation(|| event())?; store.next_replay_event_validation::(|| event())?; @@ -108,7 +110,9 @@ pub fn record_host_func_return( #[cfg(feature = "rr-component")] store.record_event(|| { let flat_results = types.flat_types_storage(&ty, MAX_FLAT_RESULTS); - HostFuncReturnEvent::new_from_flat_storage(args, flat_results) + HostFuncReturnEvent { + args: RRFuncArgVals::from_flat_storage(args, flat_results), + } })?; let _ = (args, types, ty, store); Ok(()) diff --git a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs index 057249df78..a5a7012725 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs @@ -5,6 +5,8 @@ use crate::rr::core_events::{HostFuncReturnEvent, WasmFuncEntryEvent}; use crate::rr::{ RRFuncArgVals, ResultEvent, core_events::HostFuncEntryEvent, core_events::WasmFuncReturnEvent, }; +#[cfg(feature = "rr")] +use crate::store::StoreInstanceId; use crate::store::StoreOpaque; use crate::{FuncType, StoreContextMut, ValRaw, WasmFuncOrigin, prelude::*}; @@ -26,19 +28,21 @@ where #[cfg(feature = "rr")] { if let Some(origin) = origin { - use crate::store::StoreInstanceId; - - let flat = ty - .params() - .map(|t| t.to_wasm_type().byte_size()) - .collect::>(); let checksum = *store .0 .module_for_instance(StoreInstanceId::new(store.0.id(), origin.instance)) .unwrap() .checksum(); store.0.record_event(|| { - WasmFuncEntryEvent::new(checksum, origin, args, flat.as_slice()) + let flat = ty + .params() + .map(|t| t.to_wasm_type().byte_size()) + .collect::>(); + WasmFuncEntryEvent { + module: checksum, + origin, + args: RRFuncArgVals::from_flat_u8(args, &flat), + } })?; } } @@ -69,7 +73,7 @@ where /// Record and replay hook operation for host function entry events #[inline] -pub fn record_replay_host_func_entry( +pub fn record_and_replay_validate_host_func_entry( args: &[T], flat: &[u8], store: &mut StoreOpaque, @@ -80,9 +84,11 @@ where #[cfg(all(feature = "rr", feature = "rr-validate"))] { // Record/replay the raw parameter args - store.record_event_validation(|| HostFuncEntryEvent::new(args, flat))?; - store.next_replay_event_validation::(|| { - HostFuncEntryEvent::new(args, flat) + store.record_event_validation(|| HostFuncEntryEvent { + args: RRFuncArgVals::from_flat_u8(args, flat), + })?; + store.next_replay_event_validation::(|| HostFuncEntryEvent { + args: RRFuncArgVals::from_flat_u8(args, flat), })?; } let _ = (args, flat, store); @@ -97,7 +103,9 @@ where { // Record the return values #[cfg(feature = "rr")] - store.record_event(|| HostFuncReturnEvent::new_from_u8(&args, flat))?; + store.record_event(|| HostFuncReturnEvent { + args: RRFuncArgVals::from_flat_u8(args, flat), + })?; let _ = (args, flat, store); Ok(()) } @@ -110,7 +118,7 @@ where { #[cfg(feature = "rr")] store.next_replay_event_and(|event: HostFuncReturnEvent| { - event.move_into_slice(args); + event.args.into_raw_slice(args); Ok(()) })?; let _ = (args, store); diff --git a/crates/wasmtime/src/runtime/rr/replay_driver.rs b/crates/wasmtime/src/runtime/rr/replay_driver.rs index 9d37f605cd..50f46f4a30 100644 --- a/crates/wasmtime/src/runtime/rr/replay_driver.rs +++ b/crates/wasmtime/src/runtime/rr/replay_driver.rs @@ -132,17 +132,17 @@ impl<'a> ReplayInstance<'a> { // Find matching component from environment to instantiate let component = self .components - .get(&event.0) - .ok_or(ReplayError::MissingComponent(event.0))?; + .get(&event.component) + .ok_or(ReplayError::MissingComponent(event.component))?; let instance = self .component_linker .instantiate(self.store.as_context_mut(), component)?; // Validate the instantiation event - event.validate(&component_events::InstantiationEvent( - *component.checksum(), - instance.id().instance(), - ))?; + event.validate(&component_events::InstantiationEvent { + component: *component.checksum(), + instance: instance.id().instance(), + })?; let ret = self.component_instances.insert(event, instance); // Ensures that an already-instantiated configuration is not re-instantiated @@ -159,11 +159,14 @@ impl<'a> ReplayInstance<'a> { #[cfg(feature = "rr-component")] { // Grab the correct component instance - let key = component_events::InstantiationEvent(event.component, event.instance); + let key = component_events::InstantiationEvent { + component: event.component, + instance: event.instance, + }; let instance = self .component_instances .get_mut(&key) - .ok_or(ReplayError::MissingComponentInstance(key.1.as_u32()))?; + .ok_or(ReplayError::MissingComponentInstance(key.instance.as_u32()))?; // Replay lowering steps and obtain raw value arguments to raw function call let func = component::Func::from_lifted_func(*instance, event.func_idx); @@ -213,18 +216,18 @@ impl<'a> ReplayInstance<'a> { // Find matching module from environment to instantiate let module = self .modules - .get(&event.0) - .ok_or(ReplayError::MissingModule(event.0))?; + .get(&event.module) + .ok_or(ReplayError::MissingModule(event.module))?; let instance = self .module_linker .instantiate(self.store.as_context_mut(), module)?; // Validate the instantiation event - event.validate(&core_events::InstantiationEvent( - *module.checksum(), - instance.id(), - ))?; + event.validate(&core_events::InstantiationEvent { + module: *module.checksum(), + instance: instance.id(), + })?; let ret = self.module_instances.insert(event, instance); // Ensures that an already-instantiated configuration is not re-instantiated @@ -232,11 +235,14 @@ impl<'a> ReplayInstance<'a> { } RREvent::CoreWasmFuncEntry(event) => { // Grab the correct module instance - let key = core_events::InstantiationEvent(event.module, event.origin.instance); + let key = core_events::InstantiationEvent { + module: event.module, + instance: event.origin.instance, + }; let instance = self .module_instances .get_mut(&key) - .ok_or(ReplayError::MissingModuleInstance(key.1.as_u32()))?; + .ok_or(ReplayError::MissingModuleInstance(key.instance.as_u32()))?; let entity = EntityIndex::from(event.origin.index); let mut store = self.store.as_context_mut(); @@ -249,7 +255,7 @@ impl<'a> ReplayInstance<'a> { // Obtain the argument values for function call let mut results = vec![crate::Val::I64(0); func.ty(&store).results().len()]; - let params = event.to_val_vec(&mut store, params_ty); + let params = event.args.to_val_vec(&mut store, params_ty); // Call the function // From d6892f3197eceb8f64e4413fbe229a06fb88deee Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Thu, 13 Nov 2025 19:24:04 -0500 Subject: [PATCH 30/73] Convert all flat type construction to zero-copy iterators --- crates/wasmtime/src/runtime/func.rs | 47 +++++++------------ crates/wasmtime/src/runtime/rr/core/events.rs | 18 +++---- .../src/runtime/rr/hooks/component_hooks.rs | 2 +- .../src/runtime/rr/hooks/core_hooks.rs | 45 +++++++++--------- .../wasmtime/src/runtime/rr/replay_driver.rs | 4 +- 5 files changed, 51 insertions(+), 65 deletions(-) diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 266caa19aa..3d7561d3c9 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -1291,12 +1291,11 @@ impl Func { let flat_params = ty .params() .into_iter() - .map(|x| x.to_wasm_type().byte_size()) - .collect::>(); + .map(|x| x.to_wasm_type().byte_size()); rr::core_hooks::record_and_replay_validate_host_func_entry( values_vec, - &flat_params, + flat_params, &mut caller.store.0, )?; @@ -1322,13 +1321,8 @@ impl Func { let flat_results = ty .results() .into_iter() - .map(|x| x.to_wasm_type().byte_size()) - .collect::>(); - rr::core_hooks::record_host_func_return( - values_vec, - &flat_results, - &mut caller.store.0, - )?; + .map(|x| x.to_wasm_type().byte_size()); + rr::core_hooks::record_host_func_return(values_vec, flat_results, &mut caller.store.0)?; } else { rr::core_hooks::replay_host_func_return(values_vec, &mut caller.store.0)?; } @@ -2430,30 +2424,23 @@ impl HostContext { }; let func = &state.func; - let (flat_size_params, flat_size_results) = { - let type_index = state._ty.index(); - let wasm_func_subtype = caller.engine().signatures().borrow(type_index).unwrap(); - let wasm_func_type = wasm_func_subtype.unwrap_func(); - ( - wasm_func_type - .params() - .into_iter() - .map(|x| x.byte_size()) - .collect::>(), - wasm_func_type - .returns() - .into_iter() - .map(|x| x.byte_size()) - .collect::>(), - ) - }; - let (num_params, num_results) = (flat_size_params.len(), flat_size_results.len()); + let type_index = state._ty.index(); + let wasm_func_subtype = caller.engine().signatures().borrow(type_index).unwrap(); + let wasm_func_type = wasm_func_subtype.unwrap_func(); + let (num_params, flat_size_params) = ( + wasm_func_type.params().len(), + wasm_func_type.params().into_iter().map(|x| x.byte_size()), + ); + let (num_results, flat_size_results) = ( + wasm_func_type.returns().len(), + wasm_func_type.returns().into_iter().map(|x| x.byte_size()), + ); // 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::core_hooks::record_and_replay_validate_host_func_entry( unsafe { &args.as_ref()[..num_params] }, - flat_size_params.as_slice(), + flat_size_params, caller.store.0, )?; @@ -2498,7 +2485,7 @@ impl HostContext { // Record the return values rr::core_hooks::record_host_func_return( unsafe { &args.as_ref()[..num_results] }, - flat_size_results.as_slice(), + flat_size_results, caller.store.0, )?; } else { diff --git a/crates/wasmtime/src/runtime/rr/core/events.rs b/crates/wasmtime/src/runtime/rr/core/events.rs index 1345cd62e5..669ddc033c 100644 --- a/crates/wasmtime/src/runtime/rr/core/events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events.rs @@ -62,7 +62,8 @@ impl fmt::Debug for RRFuncArgVals { impl RRFuncArgVals { /// Construct [`RRFuncArgVals`] from raw value buffer and a flat size iterator - pub fn from_raw_slice(args: &[T], flat: impl Iterator) -> RRFuncArgVals + #[inline] + pub fn from_flat_iter(args: &[T], flat: impl Iterator) -> RRFuncArgVals where T: FlatBytes, { @@ -77,22 +78,16 @@ impl RRFuncArgVals { /// Construct [`RRFuncArgVals`] from raw value buffer and a [`FlatTypesStorage`] #[cfg(feature = "rr-component")] + #[inline] pub fn from_flat_storage(args: &[T], flat: FlatTypesStorage) -> RRFuncArgVals where T: FlatBytes, { - RRFuncArgVals::from_raw_slice(args, flat.iter32()) - } - - /// Construct [`RRFuncArgVals`] from raw value buffer and a `[&u8]` slice - pub fn from_flat_u8(args: &[T], flat: &[u8]) -> RRFuncArgVals - where - T: FlatBytes, - { - RRFuncArgVals::from_raw_slice(args, flat.iter().copied()) + RRFuncArgVals::from_flat_iter(args, flat.iter32()) } /// Encode [`RRFuncArgVals`] back into raw value buffer + #[inline] pub fn into_raw_slice(self, raw_args: &mut [T]) where T: FlatBytes, @@ -104,7 +99,8 @@ impl RRFuncArgVals { } } - /// Generate a vector of [`crate::Val`] from [`RRFuncArgVals`] + /// Generate a vector of [`crate::Val`] from [`RRFuncArgVals`] and [`ValType`]s + #[inline] pub fn to_val_vec(self, mut store: impl AsContextMut, val_types: Vec) -> Vec { let mut pos = 0; let mut vals = Vec::new(); diff --git a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs index 6ca33b6dd5..6ea9a58e74 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs @@ -56,7 +56,7 @@ where &InterfaceType::Tuple(types[type_idx].results), MAX_FLAT_RESULTS, ); - let result = result.map(|_| RRFuncArgVals::from_raw_slice(args, flat_results.iter32())); + let result = result.map(|_| RRFuncArgVals::from_flat_iter(args, flat_results.iter32())); store.0.record_event_validation(|| { WasmFuncReturnEvent(ResultEvent::from_anyhow_result(&result)) })?; diff --git a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs index a5a7012725..43a49136e3 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs @@ -34,14 +34,11 @@ where .unwrap() .checksum(); store.0.record_event(|| { - let flat = ty - .params() - .map(|t| t.to_wasm_type().byte_size()) - .collect::>(); + let flat = ty.params().map(|t| t.to_wasm_type().byte_size()); WasmFuncEntryEvent { module: checksum, origin, - args: RRFuncArgVals::from_flat_u8(args, &flat), + args: RRFuncArgVals::from_flat_iter(args, flat), } })?; } @@ -50,11 +47,8 @@ where #[cfg(all(feature = "rr", feature = "rr-validate"))] { if origin.is_some() { - let flat = ty - .results() - .map(|t| t.to_wasm_type().byte_size()) - .collect::>(); - let result = result.map(|_| RRFuncArgVals::from_raw_slice(args, flat.iter().copied())); + let flat = ty.results().map(|t| t.to_wasm_type().byte_size()); + let result = result.map(|_| RRFuncArgVals::from_flat_iter(args, flat)); store.0.record_event_validation(|| { WasmFuncReturnEvent(ResultEvent::from_anyhow_result(&result)) })?; @@ -75,38 +69,47 @@ where #[inline] pub fn record_and_replay_validate_host_func_entry( args: &[T], - flat: &[u8], + flat: impl Iterator, store: &mut StoreOpaque, ) -> Result<()> where T: FlatBytes, { + let _ = (args, &flat, &store); #[cfg(all(feature = "rr", feature = "rr-validate"))] { // Record/replay the raw parameter args - store.record_event_validation(|| HostFuncEntryEvent { - args: RRFuncArgVals::from_flat_u8(args, flat), - })?; - store.next_replay_event_validation::(|| HostFuncEntryEvent { - args: RRFuncArgVals::from_flat_u8(args, flat), - })?; + if store.replay_enabled() { + store.next_replay_event_validation::(|| { + HostFuncEntryEvent { + args: RRFuncArgVals::from_flat_iter(args, flat), + } + })?; + } else { + store.record_event_validation(|| HostFuncEntryEvent { + args: RRFuncArgVals::from_flat_iter(args, flat), + })?; + } } - let _ = (args, flat, store); Ok(()) } /// Record hook operation for host function return events #[inline] -pub fn record_host_func_return(args: &[T], flat: &[u8], store: &mut StoreOpaque) -> Result<()> +pub fn record_host_func_return( + args: &[T], + flat: impl Iterator, + store: &mut StoreOpaque, +) -> Result<()> where T: FlatBytes, { + let _ = (args, &flat, &store); // Record the return values #[cfg(feature = "rr")] store.record_event(|| HostFuncReturnEvent { - args: RRFuncArgVals::from_flat_u8(args, flat), + args: RRFuncArgVals::from_flat_iter(args, flat), })?; - let _ = (args, flat, store); Ok(()) } diff --git a/crates/wasmtime/src/runtime/rr/replay_driver.rs b/crates/wasmtime/src/runtime/rr/replay_driver.rs index 50f46f4a30..d49bb602a8 100644 --- a/crates/wasmtime/src/runtime/rr/replay_driver.rs +++ b/crates/wasmtime/src/runtime/rr/replay_driver.rs @@ -6,7 +6,7 @@ use crate::rr::{ RREvent, ReplayError, Validate, component_hooks::ReplayLoweringPhase, core_events, }; use crate::{ - AsContextMut, Engine, Module, ReplayReader, ReplaySettings, Store, ValRaw, ValType, prelude::*, + AsContextMut, Engine, Module, ReplayReader, ReplaySettings, Store, ValRaw, prelude::*, }; use alloc::collections::BTreeMap; use core::mem::MaybeUninit; @@ -251,7 +251,7 @@ impl<'a> ReplayInstance<'a> { .into_func() .ok_or(ReplayError::InvalidCoreFuncIndex(entity))?; - let params_ty = func.ty(&store).params().collect::>(); + let params_ty = func.ty(&store).params().collect::>(); // Obtain the argument values for function call let mut results = vec![crate::Val::I64(0); func.ty(&store).results().len()]; From 3fed3d9c52bcec875ab933892971f82aa155ac6b Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Thu, 13 Nov 2025 20:10:12 -0500 Subject: [PATCH 31/73] Host function record/replay hooks split up --- .../src/runtime/component/func/host.rs | 15 +++-- crates/wasmtime/src/runtime/func.rs | 37 ++++++----- .../src/runtime/rr/hooks/component_hooks.rs | 61 +++++++++++++------ .../src/runtime/rr/hooks/core_hooks.rs | 39 +++++++----- 4 files changed, 95 insertions(+), 57 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 031f5d5913..42590f7275 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -257,12 +257,13 @@ where let types = vminstance.component().types().clone(); - rr::component_hooks::record_and_replay_validate_host_func_entry(storage, &types, &ty, store.0)?; - let func_ty = &types[ty]; let param_tys = InterfaceType::Tuple(func_ty.params); let result_tys = InterfaceType::Tuple(func_ty.results); + rr::component_hooks::record_validate_host_func_entry(storage, &types, &ty, store.0)?; + rr::component_hooks::replay_validate_host_func_entry(storage, &types, &ty, store.0)?; + let storage_type = if async_ { #[cfg(feature = "component-model-async")] { @@ -373,7 +374,9 @@ where { match storage_type { #[cfg(feature = "component-model-async")] - StorageType::Async(_) => unreachable!("`rr` should not be configurable with async"), + StorageType::Async(_) => { + unreachable!("`rr` should not be configurable with `component-model-async`") + } StorageType::Sync(mut storage) => { unsafe { flags.set_may_leave(false); @@ -813,13 +816,12 @@ where let types = instance.id().get(store.0).component().types().clone(); - rr::component_hooks::record_and_replay_validate_host_func_entry(storage, &types, &ty, store.0)?; - let func_ty = &types[ty]; let param_tys = &types[func_ty.params]; let result_tys = &types[func_ty.results]; if !store.0.replay_enabled() { + rr::component_hooks::record_validate_host_func_entry(storage, &types, &ty, store.0)?; let mut params_and_results = Vec::new(); let mut lift = &mut LiftContext::new(store.0.store_opaque_mut(), &options, instance); lift.enter_call(); @@ -967,9 +969,10 @@ where cx.exit_call()?; } } else { + rr::component_hooks::replay_validate_host_func_entry(storage, &types, &ty, store.0)?; #[cfg(feature = "rr-component")] if async_ { - unreachable!("`rr` should not be configurable with async"); + unreachable!("`rr` should not be configurable with `component-model-async`"); } else { unsafe { flags.set_may_leave(false); diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 3d7561d3c9..2027f46fe9 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -1287,19 +1287,17 @@ impl Func { let nparams = ty.params().len(); val_vec.reserve(nparams + ty.results().len()); - // Recording host function entry let flat_params = ty .params() .into_iter() .map(|x| x.to_wasm_type().byte_size()); - rr::core_hooks::record_and_replay_validate_host_func_entry( - values_vec, - flat_params, - &mut caller.store.0, - )?; - if !caller.store.0.replay_enabled() { + rr::core_hooks::record_validate_host_func_entry( + values_vec, + flat_params, + &mut caller.store.0, + )?; for (i, ty) in ty.params().enumerate() { val_vec.push(unsafe { Val::from_raw(&mut caller.store, values_vec[i], ty) }) } @@ -1324,6 +1322,11 @@ impl Func { .map(|x| x.to_wasm_type().byte_size()); rr::core_hooks::record_host_func_return(values_vec, flat_results, &mut caller.store.0)?; } else { + rr::core_hooks::replay_validate_host_func_entry( + values_vec, + flat_params, + &mut caller.store.0, + )?; rr::core_hooks::replay_host_func_return(values_vec, &mut caller.store.0)?; } @@ -2436,15 +2439,13 @@ impl HostContext { wasm_func_type.returns().into_iter().map(|x| x.byte_size()), ); - // 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::core_hooks::record_and_replay_validate_host_func_entry( - unsafe { &args.as_ref()[..num_params] }, - flat_size_params, - caller.store.0, - )?; - if !caller.store.0.replay_enabled() { + // Don't need auto-assert GC store here since we aren't using P, just raw args for recording + rr::core_hooks::record_validate_host_func_entry( + unsafe { &args.as_ref()[..num_params] }, + flat_size_params, + caller.store.0, + )?; let ret = 'ret: { if let Err(trap) = caller.store.0.call_hook(CallHook::CallingHost) { break 'ret R::fallible_from_error(trap); @@ -2489,6 +2490,12 @@ impl HostContext { caller.store.0, )?; } else { + rr::core_hooks::replay_validate_host_func_entry( + unsafe { &args.as_ref()[..num_params] }, + flat_size_params, + caller.store.0, + )?; + // Replay the return values rr::core_hooks::replay_host_func_return( unsafe { &mut args.as_mut()[..num_results] }, diff --git a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs index 6ea9a58e74..7ad98bbfba 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs @@ -3,6 +3,8 @@ use crate::ValRaw; use crate::component::func::LowerContext; #[cfg(feature = "rr-component")] use crate::rr::ResultEvent; +#[cfg(all(feature = "rr-component", feature = "rr-validate"))] +use crate::rr::component_events::HostFuncEntryEvent; #[cfg(feature = "rr-component")] use crate::rr::component_events::{ HostFuncReturnEvent, LowerFlatReturnEvent, LowerMemoryReturnEvent, WasmFuncEntryEvent, @@ -40,15 +42,15 @@ where { let _ = (args, type_idx, &types); #[cfg(feature = "rr-component")] - { + store.0.record_event(|| { let flat_params = types.flat_types_storage( &InterfaceType::Tuple(types[type_idx].params), MAX_FLAT_PARAMS, ); - store.0.record_event(|| WasmFuncEntryEvent { + WasmFuncEntryEvent { args: RRFuncArgVals::from_flat_storage(args, flat_params), - })?; - } + } + })?; let result = wasm_call(store); #[cfg(all(feature = "rr-component", feature = "rr-validate"))] { @@ -72,29 +74,32 @@ where return result; } -/// Record/replay hook operation for host function entry events +/// Record hook operation for host function entry events #[inline] -pub fn record_and_replay_validate_host_func_entry( +pub fn record_validate_host_func_entry( args: &mut [MaybeUninit], types: &Arc, type_idx: &TypeFuncIndex, store: &mut StoreOpaque, ) -> Result<()> { #[cfg(all(feature = "rr-component", feature = "rr-validate"))] - { - use crate::rr::component_events::HostFuncEntryEvent; - let event = || { - let flat_params = types.flat_types_storage( - &InterfaceType::Tuple(types[*type_idx].params), - MAX_FLAT_PARAMS, - ); - HostFuncEntryEvent { - args: RRFuncArgVals::from_flat_storage(args, flat_params), - } - }; - store.record_event_validation(|| event())?; - store.next_replay_event_validation::(|| event())?; - } + store.record_event_validation(|| create_host_func_entry_event(args, types, type_idx))?; + let _ = (args, types, type_idx, store); + Ok(()) +} + +/// Replay hook operation for host function entry events +#[inline] +pub fn replay_validate_host_func_entry( + args: &mut [MaybeUninit], + types: &Arc, + type_idx: &TypeFuncIndex, + store: &mut StoreOpaque, +) -> Result<()> { + #[cfg(all(feature = "rr-component", feature = "rr-validate"))] + store.next_replay_event_validation::(|| { + create_host_func_entry_event(args, types, type_idx) + })?; let _ = (args, types, type_idx, store); Ok(()) } @@ -168,3 +173,19 @@ where .record_event(|| LowerFlatReturnEvent(ResultEvent::from_anyhow_result(&lower_result)))?; lower_result } + +#[cfg(all(feature = "rr-component", feature = "rr-validate"))] +#[inline(always)] +fn create_host_func_entry_event( + args: &mut [MaybeUninit], + types: &Arc, + type_idx: &TypeFuncIndex, +) -> HostFuncEntryEvent { + let flat_params = types.flat_types_storage( + &InterfaceType::Tuple(types[*type_idx].params), + MAX_FLAT_PARAMS, + ); + HostFuncEntryEvent { + args: RRFuncArgVals::from_flat_storage(args, flat_params), + } +} diff --git a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs index 43a49136e3..b8cc63ab5f 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs @@ -65,9 +65,9 @@ where return result; } -/// Record and replay hook operation for host function entry events +/// Record hook operation for host function entry events #[inline] -pub fn record_and_replay_validate_host_func_entry( +pub fn record_validate_host_func_entry( args: &[T], flat: impl Iterator, store: &mut StoreOpaque, @@ -77,20 +77,9 @@ where { let _ = (args, &flat, &store); #[cfg(all(feature = "rr", feature = "rr-validate"))] - { - // Record/replay the raw parameter args - if store.replay_enabled() { - store.next_replay_event_validation::(|| { - HostFuncEntryEvent { - args: RRFuncArgVals::from_flat_iter(args, flat), - } - })?; - } else { - store.record_event_validation(|| HostFuncEntryEvent { - args: RRFuncArgVals::from_flat_iter(args, flat), - })?; - } - } + store.record_event_validation(|| HostFuncEntryEvent { + args: RRFuncArgVals::from_flat_iter(args, flat), + })?; Ok(()) } @@ -113,6 +102,24 @@ where Ok(()) } +/// Replay hook operation for host function entry events +#[inline] +pub fn replay_validate_host_func_entry( + args: &[T], + flat: impl Iterator, + store: &mut StoreOpaque, +) -> Result<()> +where + T: FlatBytes, +{ + let _ = (args, &flat, &store); + #[cfg(all(feature = "rr", feature = "rr-validate"))] + store.next_replay_event_validation::(|| HostFuncEntryEvent { + args: RRFuncArgVals::from_flat_iter(args, flat), + })?; + Ok(()) +} + /// Replay hook operation for host function return events #[inline] pub fn replay_host_func_return(args: &mut [T], store: &mut StoreOpaque) -> Result<()> From 5258425143acc433f887f3b909d272abecaa7a2e Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Thu, 13 Nov 2025 20:27:21 -0500 Subject: [PATCH 32/73] Move event errors to respective usage --- crates/wasmtime/src/runtime/rr/core/events/common_events.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/wasmtime/src/runtime/rr/core/events/common_events.rs b/crates/wasmtime/src/runtime/rr/core/events/common_events.rs index c67cd48666..e86ab81944 100644 --- a/crates/wasmtime/src/runtime/rr/core/events/common_events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events/common_events.rs @@ -28,3 +28,7 @@ impl Validate<&Result> for WasmFuncReturnEvent { self.0.validate(*expect) } } + +event_error_types! { + pub struct WasmFuncReturnError(..) +} From 79da1aa21aa8bdc983a6753873a61105bfb59539 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Thu, 13 Nov 2025 20:29:41 -0500 Subject: [PATCH 33/73] Fix: Unsaved file --- crates/wasmtime/src/runtime/rr/core/events.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/wasmtime/src/runtime/rr/core/events.rs b/crates/wasmtime/src/runtime/rr/core/events.rs index 669ddc033c..dc207987fc 100644 --- a/crates/wasmtime/src/runtime/rr/core/events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events.rs @@ -233,10 +233,6 @@ macro_rules! event_error_types { ); } -event_error_types! { - pub struct WasmFuncReturnError(..) -} - /// Events used as markers for debugging/testing in traces /// /// Marker events should be injectable at any point in a record From 41af9bb05ea7027db7ee9f213eb08655cbd0a72d Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Fri, 14 Nov 2025 14:55:09 -0500 Subject: [PATCH 34/73] Support async for RR --- .../wasmtime/src/runtime/rr/replay_driver.rs | 242 ++++++++++++++++-- crates/wasmtime/src/runtime/store.rs | 6 +- 2 files changed, 225 insertions(+), 23 deletions(-) diff --git a/crates/wasmtime/src/runtime/rr/replay_driver.rs b/crates/wasmtime/src/runtime/rr/replay_driver.rs index d49bb602a8..d7cdaf3a29 100644 --- a/crates/wasmtime/src/runtime/rr/replay_driver.rs +++ b/crates/wasmtime/src/runtime/rr/replay_driver.rs @@ -9,6 +9,7 @@ use crate::{ AsContextMut, Engine, Module, ReplayReader, ReplaySettings, Store, ValRaw, prelude::*, }; use alloc::collections::BTreeMap; +use alloc::sync::Arc; use core::mem::MaybeUninit; use wasmtime_environ::EntityIndex; #[cfg(feature = "rr-component")] @@ -24,7 +25,7 @@ pub struct ReplayEnvironment { } impl ReplayEnvironment { - /// Create a new [`ReplayEnvironment`] + /// Construct a new [`ReplayEnvironment`] from scratch pub fn new(engine: &Engine, settings: ReplaySettings) -> Self { Self { engine: engine.clone(), @@ -47,8 +48,18 @@ impl ReplayEnvironment { } /// Instantiate a new [`ReplayInstance`] using a [`ReplayReader`] in context of this environment - pub fn instantiate(&self, reader: impl ReplayReader + 'static) -> Result> { - ReplayInstance::from_environment(self, reader) + pub fn instantiate(&self, reader: impl ReplayReader + 'static) -> Result> { + let store = Store::new(&self.engine, ()); + ReplayInstance::<()>::from_environment_and_store(self.clone(), store, reader) + } + + /// Like [`Self::instantiate`] but allows providing a custom [`Store`] generator + pub fn instantiate_with_store( + &self, + store_gen: impl FnOnce() -> Store, + reader: impl ReplayReader + 'static, + ) -> Result> { + ReplayInstance::from_environment_and_store(self.clone(), store_gen(), reader) } } @@ -76,47 +87,47 @@ impl ReplayEnvironment { /// Ok(()) /// } /// ``` -pub struct ReplayInstance<'a> { - /// The store doesn't need any host data because the trace format and - /// replay is designed to be embedding-agnostic - store: Store<()>, - component_linker: component::Linker<()>, - module_linker: crate::Linker<()>, - modules: &'a BTreeMap<[u8; 32], Module>, - components: &'a BTreeMap<[u8; 32], Component>, +pub struct ReplayInstance { + env: Arc, + store: Store, + component_linker: component::Linker, + module_linker: crate::Linker, module_instances: BTreeMap, component_instances: BTreeMap, } -impl<'a> ReplayInstance<'a> { - fn from_environment( - env: &'a ReplayEnvironment, +impl ReplayInstance { + fn from_environment_and_store( + env: ReplayEnvironment, + mut store: Store, reader: impl ReplayReader + 'static, ) -> Result { - let mut store = Store::new(&env.engine, ()); + let env = Arc::new(env); store.init_replaying(reader, env.settings.clone())?; - let mut component_linker = component::Linker::<()>::new(&env.engine); - let mut module_linker = crate::Linker::<()>::new(&env.engine); + let mut component_linker = component::Linker::::new(&env.engine); + let mut module_linker = crate::Linker::::new(&env.engine); // Replays shouldn't use any imports, so stub them all out as traps for module in env.modules.values() { - // Defining unknown imports as trap seems to not actually trigger the entrypoint? - // Use default values instead for now module_linker.define_unknown_imports_as_traps(module)?; } for component in env.components.values() { component_linker.define_unknown_imports_as_traps(component)?; } Ok(Self { + env, store, component_linker, module_linker, - modules: &env.modules, - components: &env.components, module_instances: BTreeMap::new(), component_instances: BTreeMap::new(), }) } + /// Obtain a reference to the internal [`Store`] + pub fn store(&self) -> &Store { + &self.store + } + /// Run a single top-level event from the instance /// /// "Top-level" events are those explicitly invoked events, namely: @@ -131,6 +142,7 @@ impl<'a> ReplayInstance<'a> { { // Find matching component from environment to instantiate let component = self + .env .components .get(&event.component) .ok_or(ReplayError::MissingComponent(event.component))?; @@ -215,6 +227,7 @@ impl<'a> ReplayInstance<'a> { RREvent::CoreWasmInstantiation(event) => { // Find matching module from environment to instantiate let module = self + .env .modules .get(&event.module) .ok_or(ReplayError::MissingModule(event.module))?; @@ -259,7 +272,7 @@ impl<'a> ReplayInstance<'a> { // Call the function // - // This is almost a mirror of the usage in [`crate::Func::call`] or [`crate::Func::call_async`] + // This is almost a mirror of the usage in [`crate::Func::call_impl`] func.call_impl_check_args(&mut store, ¶ms, &mut results)?; unsafe { func.call_impl_do_call(&mut store, params.as_slice(), results.as_mut_slice())?; @@ -270,6 +283,172 @@ impl<'a> ReplayInstance<'a> { } Ok(()) } + + /// Exactly like [`Self::run_single_top_level_event`] but uses async stores and calls + #[cfg(feature = "async")] + pub async fn run_single_top_level_event_async(&mut self, rr_event: RREvent) -> Result<()> + where + T: Send, + { + match rr_event { + RREvent::ComponentInstantiation(event) => { + #[cfg(feature = "rr-component")] + { + // Find matching component from environment to instantiate + let component = self + .env + .components + .get(&event.component) + .ok_or(ReplayError::MissingComponent(event.component))?; + + let instance = self + .component_linker + .instantiate_async(self.store.as_context_mut(), component) + .await?; + // Validate the instantiation event + event.validate(&component_events::InstantiationEvent { + component: *component.checksum(), + instance: instance.id().instance(), + })?; + + let ret = self.component_instances.insert(event, instance); + // Ensures that an already-instantiated configuration is not re-instantiated + assert!(ret.is_none()); + } + #[cfg(not(feature = "rr-component"))] + { + anyhow!( + "Cannot parse ComponentInstantation replay event without rr-component feature enabled" + ); + } + } + RREvent::ComponentWasmFuncBegin(event) => { + #[cfg(feature = "rr-component")] + { + // Grab the correct component instance + let key = component_events::InstantiationEvent { + component: event.component, + instance: event.instance, + }; + let instance = self + .component_instances + .get_mut(&key) + .ok_or(ReplayError::MissingComponentInstance(key.instance.as_u32()))?; + + // Replay lowering steps and obtain raw value arguments to raw function call + let func = component::Func::from_lifted_func(*instance, event.func_idx); + let store = self.store.as_context_mut(); + + // Call the function + // + // This is almost a mirror of the usage in [`component::Func::call_impl`] + let mut results_storage = [component::Val::U64(0); MAX_FLAT_RESULTS]; + let mut num_results = 0; + let results = &mut results_storage; + let _return = unsafe { + async { + func.call_raw( + store, + |cx, _, dst: &mut MaybeUninit<[MaybeUninit; MAX_FLAT_PARAMS]>| { + // For lowering, use replay instead of actual lowering + let dst: &mut [MaybeUninit] = dst.assume_init_mut(); + cx.replay_lowering(Some(dst), ReplayLoweringPhase::WasmFuncEntry) + }, + |cx, results_ty, src: &[ValRaw; MAX_FLAT_RESULTS]| { + // Lifting can proceed exactly as normal + for (result, slot) in + component::Func::lift_results(cx, results_ty, src, MAX_FLAT_RESULTS)?.zip(results) + { + *slot = result?; + num_results += 1; + } + Ok(()) + }, + ) + }.await?; + }; + + log::info!( + "Returned {:?} for calling {:?}", + &results_storage[..num_results], + func + ); + } + #[cfg(not(feature = "rr-component"))] + { + anyhow!( + "Cannot parse ComponentWasmFuncBegin replay event without rr-component feature enabled" + ); + } + } + RREvent::CoreWasmInstantiation(event) => { + // Find matching module from environment to instantiate + let module = self + .env + .modules + .get(&event.module) + .ok_or(ReplayError::MissingModule(event.module))?; + + let instance = self + .module_linker + .instantiate_async(self.store.as_context_mut(), module) + .await?; + + // Validate the instantiation event + event.validate(&core_events::InstantiationEvent { + module: *module.checksum(), + instance: instance.id(), + })?; + + let ret = self.module_instances.insert(event, instance); + // Ensures that an already-instantiated configuration is not re-instantiated + assert!(ret.is_none()); + } + RREvent::CoreWasmFuncEntry(event) => { + // Grab the correct module instance + let key = core_events::InstantiationEvent { + module: event.module, + instance: event.origin.instance, + }; + let instance = self + .module_instances + .get_mut(&key) + .ok_or(ReplayError::MissingModuleInstance(key.instance.as_u32()))?; + + let entity = EntityIndex::from(event.origin.index); + let mut store = self.store.as_context_mut(); + let func = instance + ._get_export(store.0, entity) + .into_func() + .ok_or(ReplayError::InvalidCoreFuncIndex(entity))?; + + let params_ty = func.ty(&store).params().collect::>(); + + // Obtain the argument values for function call + let mut results = vec![crate::Val::I64(0); func.ty(&store).results().len()]; + let params = event.args.to_val_vec(&mut store, params_ty); + + // Call the function + // + // This is almost a mirror of the usage in [`crate::Func::call_impl`] + func.call_impl_check_args(&mut store, ¶ms, &mut results)?; + unsafe { + async { + func.call_impl_do_call( + &mut store, + params.as_slice(), + results.as_mut_slice(), + ) + } + .await?; + } + } + + _ => Err(ReplayError::IncorrectEventVariant)?, + } + Ok(()) + } + /// Run this replay instance to completion pub fn run_to_completion(&mut self) -> Result<()> { while let Some(rr_event) = self @@ -284,4 +463,23 @@ impl<'a> ReplayInstance<'a> { } Ok(()) } + + /// Exactly like [`Self::run_to_completion`] but uses async stores and calls + #[cfg(feature = "async")] + pub async fn run_to_completion_async(&mut self) -> Result<()> + where + T: Send, + { + while let Some(rr_event) = self + .store + .as_context_mut() + .0 + .replay_buffer_mut() + .expect("unexpected; replay buffer must be initialized within an instance") + .next() + { + self.run_single_top_level_event_async(rr_event?).await?; + } + Ok(()) + } } diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index 28d4e862c7..a399827574 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -1488,7 +1488,7 @@ impl StoreOpaque { } #[cfg(feature = "rr")] - pub fn init_replaying( + pub(crate) fn init_replaying( &mut self, replayer: impl ReplayReader + 'static, settings: ReplaySettings, @@ -1501,6 +1501,10 @@ impl StoreOpaque { self.engine().is_replaying(), "store replaying requires replaying enabled on config" ); + ensure!( + !self.engine().is_recording(), + "store replaying cannot be enabled while recording is enabled" + ); self.replay_buffer = Some(ReplayBuffer::new_replayer(replayer, settings)?); Ok(()) } From 573e30822bf27cd55dc719a274d39c962da5b1ef Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Fri, 14 Nov 2025 15:42:19 -0500 Subject: [PATCH 35/73] Supported run CLI with async --- .../wasmtime/src/runtime/rr/replay_driver.rs | 5 + src/commands/run.rs | 216 +++++++++++------- 2 files changed, 134 insertions(+), 87 deletions(-) diff --git a/crates/wasmtime/src/runtime/rr/replay_driver.rs b/crates/wasmtime/src/runtime/rr/replay_driver.rs index d7cdaf3a29..f808ae62c2 100644 --- a/crates/wasmtime/src/runtime/rr/replay_driver.rs +++ b/crates/wasmtime/src/runtime/rr/replay_driver.rs @@ -128,6 +128,11 @@ impl ReplayInstance { &self.store } + /// Consume the [`ReplayInstance`] and extract the internal [`Store`] + pub fn extract_store(self) -> Store { + self.store + } + /// Run a single top-level event from the instance /// /// "Top-level" events are those explicitly invoked events, namely: diff --git a/src/commands/run.rs b/src/commands/run.rs index b3c35e3711..05247c5bc2 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -16,6 +16,8 @@ use std::io::BufReader; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::thread; +#[cfg(feature = "rr")] +use tokio::time::error::Elapsed; use wasi_common::sync::{Dir, TcpListener, WasiCtxBuilder, ambient_authority}; #[cfg(feature = "rr")] use wasmtime::ReplayEnvironment; @@ -101,16 +103,16 @@ impl RunCommand { ) -> Result<()> { self.run.common.init_logging()?; - let mut config = self.run.common.config(None)?; + #[cfg(feature = "rr")] + let is_replaying = replay_opts.is_some(); #[cfg(not(feature = "rr"))] + let is_replaying = false; + + let mut config = self.run.common.config(None)?; config.async_support(true); #[cfg(feature = "rr")] - if replay_opts.is_some() { - // Replay does not support async yet - config.async_support(false); + if is_replaying { config.rr(wasmtime::RRConfig::Replaying); - } else { - config.async_support(true); } if self.run.common.wasm.timeout.is_some() { @@ -173,13 +175,7 @@ impl RunCommand { }; let mut store = Store::new(&engine, host); - #[cfg(not(feature = "rr"))] self.populate_with_wasi(&mut linker, &mut store, &main)?; - // For replay, we don't populate WASI - #[cfg(feature = "rr")] - if !replay_opts.is_some() { - self.populate_with_wasi(&mut linker, &mut store, &main)?; - } store.data_mut().limits = self.run.store_limits(); store.limiter(|t| &mut t.limits); @@ -192,32 +188,6 @@ impl RunCommand { #[cfg(feature = "rr")] { - // If this is a replay run, skip to replay setup and run to completion - // - // Note: Right now, replay doesn't inherit any store settings listed - // above. This will have to change in the future. In general, replays will - // need an "almost exact" superset of the run configurations, but with - // certain different options (e.g. fuel consumption). - if let Some(opts) = replay_opts { - let settings = ReplaySettings { - validate: opts.validate, - deser_buffer_size: opts.deser_buffer_size, - ..Default::default() - }; - - let mut renv = ReplayEnvironment::new(&engine, settings); - match main { - RunTarget::Core(m) => { - renv.add_module(m); - } - RunTarget::Component(c) => { - renv.add_component(c); - } - } - let mut instance = renv.instantiate(BufReader::new(fs::File::open(opts.trace)?))?; - return instance.run_to_completion(); - } - // Recording settings for this execution's store let record = &self.run.common.record; if let Some(path) = &record.path { @@ -251,61 +221,133 @@ impl RunCommand { .wasm .timeout .unwrap_or(std::time::Duration::MAX); - let result = runtime.block_on(async { - tokio::time::timeout(dur, async { - let mut profiled_modules: Vec<(String, Module)> = Vec::new(); - if let RunTarget::Core(m) = &main { - profiled_modules.push(("".to_string(), m.clone())); - } - // Load the preload wasm modules. - for (name, path) in self.preloads.iter() { - // Read the wasm module binary either as `*.wat` or a raw binary - let preload_target = self.run.load_module(&engine, path)?; - let preload_module = match preload_target { - RunTarget::Core(m) => m, - #[cfg(feature = "component-model")] - RunTarget::Component(_) => { - bail!("components cannot be loaded with `--preload`") - } - }; - profiled_modules.push((name.to_string(), preload_module.clone())); - - // Add the module's functions to the linker. - match &mut linker { - #[cfg(feature = "cranelift")] - CliLinker::Core(linker) => { - linker - .module_async(&mut store, name, &preload_module) - .await - .context(format!( - "failed to process preload `{}` at `{}`", - name, - path.display() - ))?; - } - #[cfg(not(feature = "cranelift"))] - CliLinker::Core(_) => { - bail!("support for --preload disabled at compile time"); - } - #[cfg(feature = "component-model")] - CliLinker::Component(_) => { - bail!("--preload cannot be used with components"); + let result = if is_replaying { + // Note: While replaying, we need to retrieveIn general, replays will need an "almost exact" superset of + // the run configurations, but with potentially certain different options (e.g. fuel consumption). + #[cfg(feature = "rr")] + { + struct OkReplayT(Store); + struct ErrReplayT(Error, Store); + let opts = replay_opts.unwrap(); + let settings = ReplaySettings { + validate: opts.validate, + deser_buffer_size: opts.deser_buffer_size, + ..Default::default() + }; + + let mut renv = ReplayEnvironment::new(&engine, settings); + match &main { + RunTarget::Core(m) => { + renv.add_module(m.clone()); + } + RunTarget::Component(c) => { + renv.add_component(c.clone()); + } + } + let mut replay_instance = renv.instantiate_with_store( + || store, + BufReader::new(fs::File::open(opts.trace)?), + )?; + + let result_replay_run: Result, Elapsed> = runtime + .block_on(async { + tokio::time::timeout(dur, async { + let res = replay_instance.run_to_completion_async().await; + match res { + Ok(_) => Ok(OkReplayT(replay_instance.extract_store())), + Err(e) => Err(ErrReplayT(e, replay_instance.extract_store())), + } + }) + .await + }); + match result_replay_run { + Ok(Ok(OkReplayT(s))) => { + store = s; + Ok(Ok(())) + } + Ok(Err(ErrReplayT(e, s))) => { + store = s; + Ok(Err(e)) + } + Err(elapsed) => { + eprintln!( + "Error: {:?}", + Err::<(), Error>(anyhow::Error::from(wasmtime::Trap::Interrupt)) + .with_context(|| format!("timed out after {elapsed}")) + ); + cfg_if::cfg_if! { + if #[cfg(unix)] { + std::process::exit(rustix::process::EXIT_SIGNALED_SIGABRT); + } else if #[cfg(windows)] { + // https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/abort?view=vs-2019 + std::process::exit(3); + } } } } + } + #[cfg(not(feature = "rr"))] + { + unreachable!("should be unreachable when `rr` feature is disabled"); + } + } else { + runtime.block_on(async { + tokio::time::timeout(dur, async { + let mut profiled_modules: Vec<(String, Module)> = Vec::new(); + if let RunTarget::Core(m) = &main { + profiled_modules.push(("".to_string(), m.clone())); + } - self.load_main_module(&mut store, &mut linker, &main, profiled_modules) - .await - .with_context(|| { - format!( - "failed to run main module `{}`", - self.module_and_args[0].to_string_lossy() - ) - }) + // Load the preload wasm modules. + for (name, path) in self.preloads.iter() { + // Read the wasm module binary either as `*.wat` or a raw binary + let preload_target = self.run.load_module(&engine, path)?; + let preload_module = match preload_target { + RunTarget::Core(m) => m, + #[cfg(feature = "component-model")] + RunTarget::Component(_) => { + bail!("components cannot be loaded with `--preload`") + } + }; + profiled_modules.push((name.to_string(), preload_module.clone())); + + // Add the module's functions to the linker. + match &mut linker { + #[cfg(feature = "cranelift")] + CliLinker::Core(linker) => { + linker + .module_async(&mut store, name, &preload_module) + .await + .context(format!( + "failed to process preload `{}` at `{}`", + name, + path.display() + ))?; + } + #[cfg(not(feature = "cranelift"))] + CliLinker::Core(_) => { + bail!("support for --preload disabled at compile time"); + } + #[cfg(feature = "component-model")] + CliLinker::Component(_) => { + bail!("--preload cannot be used with components"); + } + } + } + + self.load_main_module(&mut store, &mut linker, &main, profiled_modules) + .await + .with_context(|| { + format!( + "failed to run main module `{}`", + self.module_and_args[0].to_string_lossy() + ) + }) + }) + .await }) - .await - }); + }; // Load the main wasm module. match result.unwrap_or_else(|elapsed| { From 2e4b712b288bfb9ce875e3574e9620beb21faf45 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Fri, 14 Nov 2025 16:09:40 -0500 Subject: [PATCH 36/73] Remove module checksums on wasm function begin events; only rely on instance id --- crates/wasmtime/src/runtime/component/func.rs | 9 +-- .../rr/core/events/component_events.rs | 2 - .../src/runtime/rr/core/events/core_events.rs | 2 - .../src/runtime/rr/hooks/core_hooks.rs | 8 --- .../wasmtime/src/runtime/rr/replay_driver.rs | 55 +++++++------------ 5 files changed, 23 insertions(+), 53 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func.rs b/crates/wasmtime/src/runtime/component/func.rs index fa1811ffec..a5f9cf96f6 100644 --- a/crates/wasmtime/src/runtime/component/func.rs +++ b/crates/wasmtime/src/runtime/component/func.rs @@ -552,14 +552,11 @@ impl Func { { use crate::rr::component_events::WasmFuncBeginEvent; - let component = *self.instance.id().get(store.0).component().checksum(); let instance = self.instance.id().instance(); let func_idx = self.index; - store.0.record_event(|| WasmFuncBeginEvent { - component, - instance, - func_idx, - })?; + store + .0 + .record_event(|| WasmFuncBeginEvent { instance, func_idx })?; } let export = self.lifted_core_func(store.0); diff --git a/crates/wasmtime/src/runtime/rr/core/events/component_events.rs b/crates/wasmtime/src/runtime/rr/core/events/component_events.rs index 99d51c51d3..bd5e1ac06f 100644 --- a/crates/wasmtime/src/runtime/rr/core/events/component_events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events/component_events.rs @@ -13,8 +13,6 @@ use wasmtime_environ::{ /// Beginning marker for a Wasm component function call from host #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct WasmFuncBeginEvent { - /// Checksum of component containing function - pub component: [u8; 32], /// Instance ID for the component instance pub instance: ComponentInstanceId, /// Export index for the invoked function diff --git a/crates/wasmtime/src/runtime/rr/core/events/core_events.rs b/crates/wasmtime/src/runtime/rr/core/events/core_events.rs index e66ffa3b01..1ba231ae7d 100644 --- a/crates/wasmtime/src/runtime/rr/core/events/core_events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events/core_events.rs @@ -16,8 +16,6 @@ pub struct InstantiationEvent { /// A call event from Host into a core Wasm function #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct WasmFuncEntryEvent { - /// Checksum of module containing function - pub module: [u8; 32], /// Origin (instance + function index) for this function pub origin: WasmFuncOrigin, /// Raw values passed across call boundary diff --git a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs index b8cc63ab5f..8883bd02a0 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs @@ -5,8 +5,6 @@ use crate::rr::core_events::{HostFuncReturnEvent, WasmFuncEntryEvent}; use crate::rr::{ RRFuncArgVals, ResultEvent, core_events::HostFuncEntryEvent, core_events::WasmFuncReturnEvent, }; -#[cfg(feature = "rr")] -use crate::store::StoreInstanceId; use crate::store::StoreOpaque; use crate::{FuncType, StoreContextMut, ValRaw, WasmFuncOrigin, prelude::*}; @@ -28,15 +26,9 @@ where #[cfg(feature = "rr")] { if let Some(origin) = origin { - let checksum = *store - .0 - .module_for_instance(StoreInstanceId::new(store.0.id(), origin.instance)) - .unwrap() - .checksum(); store.0.record_event(|| { let flat = ty.params().map(|t| t.to_wasm_type().byte_size()); WasmFuncEntryEvent { - module: checksum, origin, args: RRFuncArgVals::from_flat_iter(args, flat), } diff --git a/crates/wasmtime/src/runtime/rr/replay_driver.rs b/crates/wasmtime/src/runtime/rr/replay_driver.rs index f808ae62c2..4a210cc4d3 100644 --- a/crates/wasmtime/src/runtime/rr/replay_driver.rs +++ b/crates/wasmtime/src/runtime/rr/replay_driver.rs @@ -1,10 +1,11 @@ #[cfg(feature = "rr-component")] -use crate::component::{self, Component}; +use crate::component::{self, Component, ComponentInstanceId}; #[cfg(feature = "rr-component")] use crate::rr::component_events; use crate::rr::{ RREvent, ReplayError, Validate, component_hooks::ReplayLoweringPhase, core_events, }; +use crate::store::InstanceId; use crate::{ AsContextMut, Engine, Module, ReplayReader, ReplaySettings, Store, ValRaw, prelude::*, }; @@ -92,8 +93,9 @@ pub struct ReplayInstance { store: Store, component_linker: component::Linker, module_linker: crate::Linker, - module_instances: BTreeMap, - component_instances: BTreeMap, + module_instances: BTreeMap, + #[cfg(feature = "rr-component")] + component_instances: BTreeMap, } impl ReplayInstance { @@ -119,6 +121,7 @@ impl ReplayInstance { component_linker, module_linker, module_instances: BTreeMap::new(), + #[cfg(feature = "rr-component")] component_instances: BTreeMap::new(), }) } @@ -161,9 +164,8 @@ impl ReplayInstance { instance: instance.id().instance(), })?; - let ret = self.component_instances.insert(event, instance); - // Ensures that an already-instantiated configuration is not re-instantiated - assert!(ret.is_none()); + self.component_instances + .insert(instance.id().instance(), instance); } #[cfg(not(feature = "rr-component"))] { @@ -176,14 +178,11 @@ impl ReplayInstance { #[cfg(feature = "rr-component")] { // Grab the correct component instance - let key = component_events::InstantiationEvent { - component: event.component, - instance: event.instance, - }; + let key = event.instance; let instance = self .component_instances .get_mut(&key) - .ok_or(ReplayError::MissingComponentInstance(key.instance.as_u32()))?; + .ok_or(ReplayError::MissingComponentInstance(key.as_u32()))?; // Replay lowering steps and obtain raw value arguments to raw function call let func = component::Func::from_lifted_func(*instance, event.func_idx); @@ -247,20 +246,15 @@ impl ReplayInstance { instance: instance.id(), })?; - let ret = self.module_instances.insert(event, instance); - // Ensures that an already-instantiated configuration is not re-instantiated - assert!(ret.is_none()); + self.module_instances.insert(instance.id(), instance); } RREvent::CoreWasmFuncEntry(event) => { // Grab the correct module instance - let key = core_events::InstantiationEvent { - module: event.module, - instance: event.origin.instance, - }; + let key = event.origin.instance; let instance = self .module_instances .get_mut(&key) - .ok_or(ReplayError::MissingModuleInstance(key.instance.as_u32()))?; + .ok_or(ReplayError::MissingModuleInstance(key.as_u32()))?; let entity = EntityIndex::from(event.origin.index); let mut store = self.store.as_context_mut(); @@ -316,9 +310,8 @@ impl ReplayInstance { instance: instance.id().instance(), })?; - let ret = self.component_instances.insert(event, instance); - // Ensures that an already-instantiated configuration is not re-instantiated - assert!(ret.is_none()); + self.component_instances + .insert(instance.id().instance(), instance); } #[cfg(not(feature = "rr-component"))] { @@ -331,14 +324,11 @@ impl ReplayInstance { #[cfg(feature = "rr-component")] { // Grab the correct component instance - let key = component_events::InstantiationEvent { - component: event.component, - instance: event.instance, - }; + let key = event.instance; let instance = self .component_instances .get_mut(&key) - .ok_or(ReplayError::MissingComponentInstance(key.instance.as_u32()))?; + .ok_or(ReplayError::MissingComponentInstance(key.as_u32()))?; // Replay lowering steps and obtain raw value arguments to raw function call let func = component::Func::from_lifted_func(*instance, event.func_idx); @@ -405,20 +395,15 @@ impl ReplayInstance { instance: instance.id(), })?; - let ret = self.module_instances.insert(event, instance); - // Ensures that an already-instantiated configuration is not re-instantiated - assert!(ret.is_none()); + self.module_instances.insert(instance.id(), instance); } RREvent::CoreWasmFuncEntry(event) => { // Grab the correct module instance - let key = core_events::InstantiationEvent { - module: event.module, - instance: event.origin.instance, - }; + let key = event.origin.instance; let instance = self .module_instances .get_mut(&key) - .ok_or(ReplayError::MissingModuleInstance(key.instance.as_u32()))?; + .ok_or(ReplayError::MissingModuleInstance(key.as_u32()))?; let entity = EntityIndex::from(event.origin.index); let mut store = self.store.as_context_mut(); From c89684e19248098e921d641bfaeae923dc098360 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Fri, 14 Nov 2025 17:30:43 -0500 Subject: [PATCH 37/73] Remove `rr-validate` feature given minimal performance improvements --- Cargo.toml | 1 - crates/cli-flags/Cargo.toml | 1 - crates/cli-flags/src/lib.rs | 5 --- crates/wasmtime/Cargo.toml | 4 -- .../src/runtime/component/func/options.rs | 39 ++++++---------- crates/wasmtime/src/runtime/rr/core.rs | 21 +++++---- crates/wasmtime/src/runtime/rr/core/events.rs | 8 +--- .../rr/core/events/component_events.rs | 2 - .../src/runtime/rr/core/events/core_events.rs | 2 - .../src/runtime/rr/hooks/component_hooks.rs | 24 +++++----- .../src/runtime/rr/hooks/core_hooks.rs | 14 +++--- .../wasmtime/src/runtime/rr/replay_driver.rs | 45 ++++++++++++------- crates/wasmtime/src/runtime/store.rs | 8 ++-- .../src/runtime/vm/component/libcalls.rs | 4 -- src/commands/replay.rs | 4 -- src/commands/run.rs | 6 ++- 16 files changed, 79 insertions(+), 109 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f7e315116d..362a9f7895 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -526,7 +526,6 @@ pulley = ["wasmtime-cli-flags/pulley"] stack-switching = ["wasmtime/stack-switching", "wasmtime-cli-flags/stack-switching"] rr = ["wasmtime/rr", "wasmtime-cli-flags/rr"] rr-component = ["rr", "wasmtime/rr-component"] -rr-validate = ["rr", "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 062dd85b14..2874fb092e 100644 --- a/crates/cli-flags/Cargo.toml +++ b/crates/cli-flags/Cargo.toml @@ -41,4 +41,3 @@ 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 9ccb45b27f..7a37262c62 100644 --- a/crates/cli-flags/src/lib.rs +++ b/crates/cli-flags/src/lib.rs @@ -1022,11 +1022,6 @@ impl CommonOptions { match_feature! { ["rr" : &record.path] _path => { - match_feature! { - ["rr-validate": record.validation_metadata] - _v => (), - _ => err, - } config.rr(wasmtime::RRConfig::Recording); }, _ => err, diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml index c8b86ef9e9..b865e5422c 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -410,7 +410,3 @@ rr-component = ["component-model", "rr"] # Enable support for the common base infrastructure of record/replay # By default, this supports core wasm `rr`. For components, enable `rr-component` rr = ["dep:embedded-io"] - -# Enables record/replay with support for additional validation signatures/checks. -rr-validate = ["rr"] - diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index 4cebf7c4a5..8dc0b8896b 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -11,7 +11,7 @@ use crate::rr::{ConstMemorySliceCell, MemorySliceCell}; use crate::rr::{ RREvent, RecordBuffer, ReplayError, Replayer, component_events::ReallocEntryEvent, }; -#[cfg(all(feature = "rr-component", feature = "rr-validate"))] +#[cfg(feature = "rr-component")] use crate::rr::{ResultEvent, Validate, component_events::ReallocReturnEvent}; use crate::runtime::vm::component::{ CallContexts, ComponentInstance, InstanceFlags, ResourceTable, ResourceTables, @@ -403,7 +403,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { new_size, })?; let result = self.realloc_inner(old, old_size, old_align, new_size); - #[cfg(all(feature = "rr-component", feature = "rr-validate"))] + #[cfg(feature = "rr-component")] self.store.0.record_event_validation(|| { ReallocReturnEvent(ResultEvent::from_anyhow_result(&result)) })?; @@ -576,23 +576,19 @@ impl<'a, T: 'static> LowerContext<'a, T> { mut result_storage: Option<&mut [MaybeUninit]>, phase: ReplayLoweringPhase, ) -> 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(()); } 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()?; - #[cfg(feature = "rr-validate")] let run_validate = buf.settings().validate && buf.trace_settings().add_validation; match event { RREvent::HostFuncReturn(e) => { @@ -624,27 +620,22 @@ impl<'a, T: 'static> LowerContext<'a, T> { complete = true; } RREvent::ComponentReallocEntry(e) => { - let _result = + let result = self.realloc_inner(e.old_addr, e.old_size, e.old_align, e.new_size); - #[cfg(feature = "rr-validate")] if run_validate { - _realloc_stack.push(_result); + realloc_stack.push(result); } } // No return value to validate for lower/lower-store; store error and just check that entry happened before RREvent::ComponentLowerFlatReturn(e) => { - #[cfg(feature = "rr-validate")] if run_validate { - _lower_stack - .pop() - .ok_or(ReplayError::InvalidEventPosition)?; + lower_stack.pop().ok_or(ReplayError::InvalidEventPosition)?; } lowering_error = e.0.ret().map_err(Into::into).err(); } RREvent::ComponentLowerMemoryReturn(e) => { - #[cfg(feature = "rr-validate")] if run_validate { - _lower_store_stack + lower_store_stack .pop() .ok_or(ReplayError::InvalidEventPosition)?; } @@ -664,25 +655,21 @@ impl<'a, T: 'static> LowerContext<'a, T> { bail!("Cannot call into host during lowering") } // Unwrapping should never occur on valid executions since *Entry should be before *Return in trace - RREvent::ComponentReallocReturn(_e) => - { - #[cfg(feature = "rr-validate")] + RREvent::ComponentReallocReturn(e) => { if run_validate { - lowering_error = _e.0.validate(&_realloc_stack.pop().unwrap()).err() + lowering_error = e.0.validate(&realloc_stack.pop().unwrap()).err() } } RREvent::ComponentLowerFlatEntry(_) => { // All we want here is ensuring Entry occurs before Return - #[cfg(feature = "rr-validate")] if run_validate { - _lower_stack.push(()) + lower_stack.push(()) } } RREvent::ComponentLowerMemoryEntry(_) => { // All we want here is ensuring Entry occurs before Return - #[cfg(feature = "rr-validate")] if run_validate { - _lower_store_stack.push(()) + lower_store_stack.push(()) } } diff --git a/crates/wasmtime/src/runtime/rr/core.rs b/crates/wasmtime/src/runtime/rr/core.rs index edd84247bb..17b072f700 100644 --- a/crates/wasmtime/src/runtime/rr/core.rs +++ b/crates/wasmtime/src/runtime/rr/core.rs @@ -7,18 +7,14 @@ use wasmtime_environ::EntityIndex; // Use component 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 -#[cfg(all(feature = "rr-validate", feature = "rr-component"))] -pub use events::RRFuncArgVals; -#[cfg(any(feature = "rr-validate", feature = "rr-component"))] pub use events::Validate; #[cfg(feature = "rr-component")] pub use events::component_events; -pub use events::{ResultEvent, core_events, marker_events}; -use events::{common_events, component_events as __component_events}; +use events::component_events as __component_events; +pub use events::{RRFuncArgVals, ResultEvent, common_events, core_events, marker_events}; pub use io::{IOError, RecordWriter, ReplayReader}; /// Settings for execution recording. -#[cfg(feature = "rr")] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RecordSettings { /// Flag to include additional signatures for replay validation. @@ -27,7 +23,6 @@ pub struct RecordSettings { pub event_window_size: usize, } -#[cfg(feature = "rr")] impl Default for RecordSettings { fn default() -> Self { Self { @@ -38,7 +33,6 @@ impl Default for RecordSettings { } /// Settings for execution replay. -#[cfg(feature = "rr")] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReplaySettings { /// Flag to include additional signatures for replay validation. @@ -47,7 +41,6 @@ pub struct ReplaySettings { pub deser_buffer_size: usize, } -#[cfg(feature = "rr")] impl Default for ReplaySettings { fn default() -> Self { Self { @@ -304,7 +297,6 @@ pub trait Recorder { /// Record a event only when validation is requested #[inline] - #[cfg(feature = "rr-validate")] fn record_event_validation(&mut self, f: F) -> Result<()> where T: Into, @@ -379,7 +371,6 @@ 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, @@ -568,11 +559,19 @@ impl Replayer for ReplayBuffer { } #[inline] + #[allow( + unused, + reason = "method only used for gated validation, but will be extended in the future" + )] fn settings(&self) -> &ReplaySettings { &self.settings } #[inline] + #[allow( + unused, + reason = "method only used for gated validation, but will be extended in the future" + )] fn trace_settings(&self) -> &RecordSettings { &self.trace_settings } diff --git a/crates/wasmtime/src/runtime/rr/core/events.rs b/crates/wasmtime/src/runtime/rr/core/events.rs index dc207987fc..6cb158bc0b 100644 --- a/crates/wasmtime/src/runtime/rr/core/events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events.rs @@ -1,4 +1,3 @@ -#[cfg(any(feature = "rr-component", feature = "rr-validate"))] use super::ReplayError; use crate::rr::FlatBytes; use crate::{AsContextMut, Val, prelude::*}; @@ -116,13 +115,11 @@ impl RRFuncArgVals { /// Trait signifying types that can be validated on replay /// /// All `PartialEq` 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 (e.g. [`component_events::InstantiationEvent`]). +/// Note however that some [`Validate`] implementations are present and +/// required for a faithful replay (e.g. [`component_events::InstantiationEvent`]). /// /// In terms of usage, an event that implements `Validate` can call /// any RR validation methods on a `Store` -#[cfg(any(feature = "rr-component", feature = "rr-validate"))] pub trait Validate { /// Perform a validation of the event to ensure replay consistency fn validate(&self, expect: &T) -> Result<(), ReplayError>; @@ -136,7 +133,6 @@ pub trait Validate { } } -#[cfg(any(feature = "rr-component", feature = "rr-validate"))] impl Validate for T where T: PartialEq + fmt::Debug, diff --git a/crates/wasmtime/src/runtime/rr/core/events/component_events.rs b/crates/wasmtime/src/runtime/rr/core/events/component_events.rs index bd5e1ac06f..3ecedd91a4 100644 --- a/crates/wasmtime/src/runtime/rr/core/events/component_events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events/component_events.rs @@ -3,8 +3,6 @@ use super::*; use crate::component::ComponentInstanceId; use crate::vm::component::libcalls::ResourceDropRet; -// Re-export common events from this module -pub use common_events::*; use wasmtime_environ::{ self, component::{ExportIndex, InterfaceType}, diff --git a/crates/wasmtime/src/runtime/rr/core/events/core_events.rs b/crates/wasmtime/src/runtime/rr/core/events/core_events.rs index 1ba231ae7d..abcae38aca 100644 --- a/crates/wasmtime/src/runtime/rr/core/events/core_events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events/core_events.rs @@ -1,8 +1,6 @@ //! Module comprising of core wasm events use super::*; use crate::{WasmFuncOrigin, store::InstanceId}; -// Re-export common events from this module -pub use common_events::*; /// A core Wasm instantiatation event #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Ord, PartialOrd)] diff --git a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs index 7ad98bbfba..3339859e6a 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs @@ -2,15 +2,15 @@ use crate::ValRaw; #[cfg(feature = "component-model")] use crate::component::func::LowerContext; #[cfg(feature = "rr-component")] -use crate::rr::ResultEvent; -#[cfg(all(feature = "rr-component", feature = "rr-validate"))] +use crate::rr::common_events::WasmFuncReturnEvent; +#[cfg(feature = "rr-component")] use crate::rr::component_events::HostFuncEntryEvent; #[cfg(feature = "rr-component")] use crate::rr::component_events::{ - HostFuncReturnEvent, LowerFlatReturnEvent, LowerMemoryReturnEvent, WasmFuncEntryEvent, + LowerFlatReturnEvent, LowerMemoryReturnEvent, WasmFuncEntryEvent, }; -#[cfg(all(feature = "rr-component", feature = "rr-validate"))] -use crate::rr::{RRFuncArgVals, component_events::WasmFuncReturnEvent}; +#[cfg(feature = "rr-component")] +use crate::rr::{RRFuncArgVals, ResultEvent, common_events::HostFuncReturnEvent}; use crate::store::StoreOpaque; use crate::{StoreContextMut, prelude::*}; use alloc::sync::Arc; @@ -52,7 +52,7 @@ where } })?; let result = wasm_call(store); - #[cfg(all(feature = "rr-component", feature = "rr-validate"))] + #[cfg(feature = "rr-component")] { let flat_results = types.flat_types_storage( &InterfaceType::Tuple(types[type_idx].results), @@ -70,7 +70,7 @@ where result?; return Ok(()); } - #[cfg(not(all(feature = "rr-component", feature = "rr-validate")))] + #[cfg(not(feature = "rr-component"))] return result; } @@ -82,7 +82,7 @@ pub fn record_validate_host_func_entry( type_idx: &TypeFuncIndex, store: &mut StoreOpaque, ) -> Result<()> { - #[cfg(all(feature = "rr-component", feature = "rr-validate"))] + #[cfg(feature = "rr-component")] store.record_event_validation(|| create_host_func_entry_event(args, types, type_idx))?; let _ = (args, types, type_idx, store); Ok(()) @@ -96,7 +96,7 @@ pub fn replay_validate_host_func_entry( type_idx: &TypeFuncIndex, store: &mut StoreOpaque, ) -> Result<()> { - #[cfg(all(feature = "rr-component", feature = "rr-validate"))] + #[cfg(feature = "rr-component")] store.next_replay_event_validation::(|| { create_host_func_entry_event(args, types, type_idx) })?; @@ -134,7 +134,7 @@ pub fn record_lower_memory( where F: FnOnce(&mut LowerContext<'_, T>, InterfaceType, usize) -> Result<()>, { - #[cfg(all(feature = "rr-component", feature = "rr-validate"))] + #[cfg(feature = "rr-component")] { use crate::rr::component_events::LowerMemoryEntryEvent; cx.store @@ -159,7 +159,7 @@ pub fn record_lower_flat( where F: FnOnce(&mut LowerContext<'_, T>, InterfaceType) -> Result<()>, { - #[cfg(all(feature = "rr-component", feature = "rr-validate"))] + #[cfg(feature = "rr-component")] { use crate::rr::component_events::LowerFlatEntryEvent; cx.store @@ -174,7 +174,7 @@ where lower_result } -#[cfg(all(feature = "rr-component", feature = "rr-validate"))] +#[cfg(feature = "rr-component")] #[inline(always)] fn create_host_func_entry_event( args: &mut [MaybeUninit], diff --git a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs index 8883bd02a0..12417eac78 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs @@ -1,9 +1,9 @@ use crate::rr::FlatBytes; #[cfg(feature = "rr")] -use crate::rr::core_events::{HostFuncReturnEvent, WasmFuncEntryEvent}; -#[cfg(all(feature = "rr", feature = "rr-validate"))] use crate::rr::{ - RRFuncArgVals, ResultEvent, core_events::HostFuncEntryEvent, core_events::WasmFuncReturnEvent, + RRFuncArgVals, ResultEvent, common_events::HostFuncReturnEvent, + common_events::WasmFuncReturnEvent, core_events::HostFuncEntryEvent, + core_events::WasmFuncEntryEvent, }; use crate::store::StoreOpaque; use crate::{FuncType, StoreContextMut, ValRaw, WasmFuncOrigin, prelude::*}; @@ -36,7 +36,7 @@ where } } let result = wasm_call(store); - #[cfg(all(feature = "rr", feature = "rr-validate"))] + #[cfg(feature = "rr")] { if origin.is_some() { let flat = ty.results().map(|t| t.to_wasm_type().byte_size()); @@ -53,7 +53,7 @@ where } return Ok(()); } - #[cfg(not(all(feature = "rr", feature = "rr-validate")))] + #[cfg(not(feature = "rr"))] return result; } @@ -68,7 +68,7 @@ where T: FlatBytes, { let _ = (args, &flat, &store); - #[cfg(all(feature = "rr", feature = "rr-validate"))] + #[cfg(feature = "rr")] store.record_event_validation(|| HostFuncEntryEvent { args: RRFuncArgVals::from_flat_iter(args, flat), })?; @@ -105,7 +105,7 @@ where T: FlatBytes, { let _ = (args, &flat, &store); - #[cfg(all(feature = "rr", feature = "rr-validate"))] + #[cfg(feature = "rr")] store.next_replay_event_validation::(|| HostFuncEntryEvent { args: RRFuncArgVals::from_flat_iter(args, flat), })?; diff --git a/crates/wasmtime/src/runtime/rr/replay_driver.rs b/crates/wasmtime/src/runtime/rr/replay_driver.rs index 4a210cc4d3..b25190f86d 100644 --- a/crates/wasmtime/src/runtime/rr/replay_driver.rs +++ b/crates/wasmtime/src/runtime/rr/replay_driver.rs @@ -1,16 +1,16 @@ -#[cfg(feature = "rr-component")] -use crate::component::{self, Component, ComponentInstanceId}; -#[cfg(feature = "rr-component")] -use crate::rr::component_events; -use crate::rr::{ - RREvent, ReplayError, Validate, component_hooks::ReplayLoweringPhase, core_events, -}; +use crate::rr::Validate; +use crate::rr::{RREvent, ReplayError, core_events}; use crate::store::InstanceId; +use crate::{AsContextMut, Engine, Module, ReplayReader, ReplaySettings, Store, prelude::*}; +#[cfg(feature = "rr-component")] use crate::{ - AsContextMut, Engine, Module, ReplayReader, ReplaySettings, Store, ValRaw, prelude::*, + ValRaw, component, component::Component, component::ComponentInstanceId, rr::component_events, + rr::component_hooks, }; -use alloc::collections::BTreeMap; -use alloc::sync::Arc; +use alloc::{collections::BTreeMap, sync::Arc}; +#[cfg(not(feature = "rr-component"))] +use anyhow::bail; +#[cfg(feature = "rr-component")] use core::mem::MaybeUninit; use wasmtime_environ::EntityIndex; #[cfg(feature = "rr-component")] @@ -21,6 +21,7 @@ use wasmtime_environ::component::{MAX_FLAT_PARAMS, MAX_FLAT_RESULTS}; pub struct ReplayEnvironment { engine: Engine, modules: BTreeMap<[u8; 32], Module>, + #[cfg(feature = "rr-component")] components: BTreeMap<[u8; 32], Component>, settings: ReplaySettings, } @@ -31,6 +32,7 @@ impl ReplayEnvironment { Self { engine: engine.clone(), modules: BTreeMap::new(), + #[cfg(feature = "rr-component")] components: BTreeMap::new(), settings, } @@ -43,6 +45,7 @@ impl ReplayEnvironment { } /// Add a [`Component`] to the replay environment + #[cfg(feature = "rr-component")] pub fn add_component(&mut self, component: Component) -> &mut Self { self.components.insert(*component.checksum(), component); self @@ -91,6 +94,7 @@ impl ReplayEnvironment { pub struct ReplayInstance { env: Arc, store: Store, + #[cfg(feature = "rr-component")] component_linker: component::Linker, module_linker: crate::Linker, module_instances: BTreeMap, @@ -106,18 +110,21 @@ impl ReplayInstance { ) -> Result { let env = Arc::new(env); store.init_replaying(reader, env.settings.clone())?; - let mut component_linker = component::Linker::::new(&env.engine); let mut module_linker = crate::Linker::::new(&env.engine); // Replays shouldn't use any imports, so stub them all out as traps for module in env.modules.values() { module_linker.define_unknown_imports_as_traps(module)?; } + #[cfg(feature = "rr-component")] + let mut component_linker = component::Linker::::new(&env.engine); + #[cfg(feature = "rr-component")] for component in env.components.values() { component_linker.define_unknown_imports_as_traps(component)?; } Ok(Self { env, store, + #[cfg(feature = "rr-component")] component_linker, module_linker, module_instances: BTreeMap::new(), @@ -146,6 +153,7 @@ impl ReplayInstance { pub fn run_single_top_level_event(&mut self, rr_event: RREvent) -> Result<()> { match rr_event { RREvent::ComponentInstantiation(event) => { + let _ = event; #[cfg(feature = "rr-component")] { // Find matching component from environment to instantiate @@ -169,12 +177,13 @@ impl ReplayInstance { } #[cfg(not(feature = "rr-component"))] { - anyhow!( + bail!( "Cannot parse ComponentInstantation replay event without rr-component feature enabled" ); } } RREvent::ComponentWasmFuncBegin(event) => { + let _ = event; #[cfg(feature = "rr-component")] { // Grab the correct component instance @@ -200,7 +209,7 @@ impl ReplayInstance { |cx, _, dst: &mut MaybeUninit<[MaybeUninit; MAX_FLAT_PARAMS]>| { // For lowering, use replay instead of actual lowering let dst: &mut [MaybeUninit] = dst.assume_init_mut(); - cx.replay_lowering(Some(dst), ReplayLoweringPhase::WasmFuncEntry) + cx.replay_lowering(Some(dst), component_hooks::ReplayLoweringPhase::WasmFuncEntry) }, |cx, results_ty, src: &[ValRaw; MAX_FLAT_RESULTS]| { // Lifting can proceed exactly as normal @@ -223,7 +232,7 @@ impl ReplayInstance { } #[cfg(not(feature = "rr-component"))] { - anyhow!( + bail!( "Cannot parse ComponentWasmFuncBegin replay event without rr-component feature enabled" ); } @@ -291,6 +300,7 @@ impl ReplayInstance { { match rr_event { RREvent::ComponentInstantiation(event) => { + let _ = event; #[cfg(feature = "rr-component")] { // Find matching component from environment to instantiate @@ -315,12 +325,13 @@ impl ReplayInstance { } #[cfg(not(feature = "rr-component"))] { - anyhow!( + bail!( "Cannot parse ComponentInstantation replay event without rr-component feature enabled" ); } } RREvent::ComponentWasmFuncBegin(event) => { + let _ = event; #[cfg(feature = "rr-component")] { // Grab the correct component instance @@ -347,7 +358,7 @@ impl ReplayInstance { |cx, _, dst: &mut MaybeUninit<[MaybeUninit; MAX_FLAT_PARAMS]>| { // For lowering, use replay instead of actual lowering let dst: &mut [MaybeUninit] = dst.assume_init_mut(); - cx.replay_lowering(Some(dst), ReplayLoweringPhase::WasmFuncEntry) + cx.replay_lowering(Some(dst), component_hooks::ReplayLoweringPhase::WasmFuncEntry) }, |cx, results_ty, src: &[ValRaw; MAX_FLAT_RESULTS]| { // Lifting can proceed exactly as normal @@ -371,7 +382,7 @@ impl ReplayInstance { } #[cfg(not(feature = "rr-component"))] { - anyhow!( + bail!( "Cannot parse ComponentWasmFuncBegin replay event without rr-component feature enabled" ); } diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index a399827574..af075b3bdd 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -85,12 +85,10 @@ use crate::component::concurrent; use crate::fiber; use crate::module::RegisteredModuleId; use crate::prelude::*; -#[cfg(feature = "rr-validate")] -use crate::rr::Validate; #[cfg(feature = "rr")] use crate::rr::{ RREvent, RecordBuffer, RecordSettings, RecordWriter, Recorder, ReplayBuffer, ReplayError, - ReplayReader, ReplaySettings, Replayer, + ReplayReader, ReplaySettings, Replayer, Validate, }; #[cfg(feature = "gc")] use crate::runtime::vm::GcRootsList; @@ -1542,7 +1540,7 @@ impl StoreOpaque { /// if validation is enabled for recording /// /// Convenience wrapper around [`Recorder::record_event_validation`] - #[cfg(feature = "rr-validate")] + #[cfg(feature = "rr")] #[inline(always)] pub(crate) fn record_event_validation(&mut self, f: F) -> Result<()> where @@ -1578,7 +1576,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-validate")] + #[cfg(feature = "rr")] #[inline] pub(crate) fn next_replay_event_validation( &mut self, diff --git a/crates/wasmtime/src/runtime/vm/component/libcalls.rs b/crates/wasmtime/src/runtime/vm/component/libcalls.rs index b46be38208..17db9aebaf 100644 --- a/crates/wasmtime/src/runtime/vm/component/libcalls.rs +++ b/crates/wasmtime/src/runtime/vm/component/libcalls.rs @@ -167,14 +167,12 @@ mod trampolines { #[cfg(feature = "rr-component")] { if let Some(buf) = (*$store).replay_buffer_mut() { - #[cfg(feature = "rr-validate")] buf.next_event_validation::(&$rr_entry{ $($pname),* }.into())?; // Replay the return value let builtin_ret_event = buf.next_event_typed::()?; $rr_exit::try_from(builtin_ret_event)?.ret() } else { // Recording entry/return - #[cfg(feature = "rr-validate")] (*$store).record_event_validation::(|| $rr_entry{ $($pname),* }.into())?; let retval = shims!(@invoke $name($store, $instance,) $($pname)*); (*$store).record_event::(|| $rr_exit(ResultEvent::from_anyhow_result(&retval)).into())?; @@ -193,11 +191,9 @@ mod trampolines { { if let Some(_buf) = (*$store).replay_buffer_mut() { // Just perform replay validation, if required - #[cfg(feature = "rr-validate")] _buf.next_event_validation::(&$rr_entry{ $($pname),* }.into()).unwrap(); } else { // Record entry only; return is not present - #[cfg(feature = "rr-validate")] (*$store).record_event_validation::(|| $rr_entry{ $($pname),* }.into()).unwrap(); shims!(@invoke $name($store, $instance,) $($pname)*) } diff --git a/src/commands/replay.rs b/src/commands/replay.rs index 0cf1aae71f..51a7114a77 100644 --- a/src/commands/replay.rs +++ b/src/commands/replay.rs @@ -43,10 +43,6 @@ 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"); - } // Replay uses the `run` command harness self.run_cmd.execute(Some(self.replay_opts)) } diff --git a/src/commands/run.rs b/src/commands/run.rs index 05247c5bc2..d1682b65e1 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -241,8 +241,10 @@ impl RunCommand { RunTarget::Core(m) => { renv.add_module(m.clone()); } - RunTarget::Component(c) => { - renv.add_component(c.clone()); + #[cfg(feature = "component-model")] + RunTarget::Component(_c) => { + #[cfg(feature = "rr-component")] + renv.add_component(_c.clone()); } } let mut replay_instance = renv.instantiate_with_store( From 796c1ededc8a538fa6bf695bde60b522bd9e831b Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Mon, 17 Nov 2025 18:23:10 -0500 Subject: [PATCH 38/73] Added tests for rr; reduce func size with packed option; commonize host func entry event --- crates/c-api/include/wasmtime/extern.h | 6 +- crates/c-api/include/wasmtime/val.h | 2 +- crates/c-api/src/val.rs | 2 +- .../src/runtime/component/func/options.rs | 4 +- crates/wasmtime/src/runtime/func.rs | 29 +- crates/wasmtime/src/runtime/rr.rs | 3 +- crates/wasmtime/src/runtime/rr/core.rs | 443 ++++++++++++++++-- .../runtime/rr/core/events/common_events.rs | 12 +- .../rr/core/events/component_events.rs | 7 - .../src/runtime/rr/core/events/core_events.rs | 7 - .../src/runtime/rr/hooks/component_hooks.rs | 4 +- .../src/runtime/rr/hooks/core_hooks.rs | 4 +- crates/wasmtime/src/runtime/values.rs | 8 +- .../src/runtime/vm/component/libcalls.rs | 2 +- crates/wasmtime/src/runtime/vm/vmcontext.rs | 1 - 15 files changed, 460 insertions(+), 74 deletions(-) diff --git a/crates/c-api/include/wasmtime/extern.h b/crates/c-api/include/wasmtime/extern.h index df9e0a32ea..f9e0858124 100644 --- a/crates/c-api/include/wasmtime/extern.h +++ b/crates/c-api/include/wasmtime/extern.h @@ -32,11 +32,9 @@ typedef struct wasmtime_func { /// Private field for Wasmtime, undefined if `store_id` is zero. void *__private1; /// Private field for Wasmtime - uint32_t *__private2; - /// Private field for Wasmtime - uint32_t *__private3; + uint32_t __private2; /// Private field for Wasmtime - uint32_t *__private4; + uint32_t __private3; } wasmtime_func_t; /// \brief Representation of a table in Wasmtime. diff --git a/crates/c-api/include/wasmtime/val.h b/crates/c-api/include/wasmtime/val.h index 5e9c145e37..98d4747305 100644 --- a/crates/c-api/include/wasmtime/val.h +++ b/crates/c-api/include/wasmtime/val.h @@ -418,7 +418,7 @@ typedef union wasmtime_val_raw { // Assert that the shape of this type is as expected since it needs to match // Rust. static inline void __wasmtime_val_assertions() { - static_assert(sizeof(wasmtime_valunion_t) == 32, "should be 16-bytes large"); + static_assert(sizeof(wasmtime_valunion_t) == 24, "should be 24-bytes large"); static_assert(__alignof(wasmtime_valunion_t) == 8, "should be 8-byte aligned"); static_assert(sizeof(wasmtime_val_raw_t) == 16, "should be 16 bytes large"); diff --git a/crates/c-api/src/val.rs b/crates/c-api/src/val.rs index 49aaf8fc3c..5aa625b1fa 100644 --- a/crates/c-api/src/val.rs +++ b/crates/c-api/src/val.rs @@ -152,7 +152,7 @@ pub union wasmtime_val_union { } const _: () = { - assert!(std::mem::size_of::() == 32); + assert!(std::mem::size_of::() == 24); assert!(std::mem::align_of::() == std::mem::align_of::()); }; diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index 8dc0b8896b..28c692a209 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -651,8 +651,8 @@ impl<'a, T: 'static> LowerContext<'a, T> { // // 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 into host during lowering") + RREvent::HostFuncEntry(_) => { + 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) => { diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 2027f46fe9..adfbcd5a7b 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -18,7 +18,9 @@ use core::future::Future; use core::mem::{self, MaybeUninit}; use core::ptr::NonNull; use serde::{Deserialize, Serialize}; -use wasmtime_environ::{FuncIndex, VMSharedTypeIndex}; +use wasmtime_environ::{ + FuncIndex, VMSharedTypeIndex, packed_option::PackedOption, packed_option::ReservedValue, +}; /// A reference to the abstract `nofunc` heap value. /// @@ -112,6 +114,19 @@ pub struct WasmFuncOrigin { pub index: FuncIndex, } +impl ReservedValue for WasmFuncOrigin { + fn reserved_value() -> Self { + WasmFuncOrigin { + instance: InstanceId::reserved_value(), + index: FuncIndex::reserved_value(), + } + } + + fn is_reserved_value(&self) -> bool { + self.instance.is_reserved_value() && self.index.is_reserved_value() + } +} + /// A WebAssembly function which can be called. /// /// This type typically represents an exported function from a WebAssembly @@ -294,14 +309,14 @@ pub struct Func { /// This field is populated when a [`Func`] is generated from a known instance /// (i.e. exported Wasm functions), and is usually `None` for internal /// Wasm functions and host functions. - origin: Option, + origin: PackedOption, } // Double-check that the C representation in `extern.h` matches our in-Rust // representation here in terms of size/alignment/etc. const _: () = { #[repr(C)] - struct C(u64, *mut u8, (u32, u32, u32)); + struct C(u64, *mut u8, (u32, u32)); assert!(core::mem::size_of::() == core::mem::size_of::()); assert!(core::mem::align_of::() == core::mem::align_of::()); assert!(core::mem::offset_of!(Func, store) == 0); @@ -566,7 +581,7 @@ impl Func { Func { store, unsafe_func_ref: func_ref.into(), - origin: None, + origin: PackedOption::default(), } } @@ -1035,7 +1050,7 @@ impl Func { }, unsafe { params_and_returns.as_ref() }, &self.ty(&store), - self.origin.clone(), + self.origin.expand(), &mut store, ) } @@ -1536,12 +1551,12 @@ impl Func { /// Set the origin of this function. pub(crate) fn set_origin(&mut self, origin: WasmFuncOrigin) { - self.origin = Some(origin); + self.origin = PackedOption::from(origin); } // Get the origin of this function pub(crate) fn origin(&self) -> Option { - self.origin + self.origin.expand() } } diff --git a/crates/wasmtime/src/runtime/rr.rs b/crates/wasmtime/src/runtime/rr.rs index f0df61ee86..b9fcd2d698 100644 --- a/crates/wasmtime/src/runtime/rr.rs +++ b/crates/wasmtime/src/runtime/rr.rs @@ -30,7 +30,8 @@ impl FlatBytes for MaybeUninit { #[inline] fn bytes_ref(&self, size: u8) -> &[u8] { // Uninitialized data is assumed and serialized, so hence - // may contain some undefined values + // may contain some undefined values. But these are irrelevant + // when serializing to `RRFuncArgVals` let val = unsafe { self.assume_init_ref() }; val.bytes_ref(size) } diff --git a/crates/wasmtime/src/runtime/rr/core.rs b/crates/wasmtime/src/runtime/rr/core.rs index 17b072f700..0c88459c37 100644 --- a/crates/wasmtime/src/runtime/rr/core.rs +++ b/crates/wasmtime/src/runtime/rr/core.rs @@ -124,6 +124,8 @@ rr_event! { /// Return from host function (core or component) to host HostFuncReturn(common_events::HostFuncReturnEvent), // OPTIONAL events + /// Call into host function from Wasm (core or component) + HostFuncEntry(common_events::HostFuncEntryEvent), /// Return from Wasm function (core or component) to host WasmFuncReturn(common_events::WasmFuncReturnEvent), @@ -132,8 +134,6 @@ rr_event! { CoreWasmInstantiation(core_events::InstantiationEvent), /// Entry from host into a core Wasm function CoreWasmFuncEntry(core_events::WasmFuncEntryEvent), - /// Call into host function from core Wasm - CoreHostFuncEntry(core_events::HostFuncEntryEvent), // REQUIRED events for replay (Component) @@ -165,8 +165,6 @@ rr_event! { /// Any error is subsumed by the containing LowerReturn/LowerStoreReturn /// that triggered realloc ComponentReallocReturn(__component_events::ReallocReturnEvent), - /// Call into host function from component - ComponentHostFuncEntry(__component_events::HostFuncEntryEvent), /// Call into type lowering for flat destination ComponentLowerFlatEntry(__component_events::LowerFlatEntryEvent), /// Call into type lowering for memory destination @@ -581,32 +579,29 @@ impl Replayer for ReplayBuffer { mod tests { use super::*; use crate::ValRaw; - use core::mem::MaybeUninit; + use crate::WasmFuncOrigin; + use crate::store::InstanceId; + use crate::vm::component::libcalls::ResourceDropRet; use std::fs::File; use std::path::Path; use tempfile::{NamedTempFile, TempPath}; + use wasmtime_environ::FuncIndex; - #[test] - #[cfg(all(feature = "rr", feature = "rr-component"))] - fn rr_buffers() -> Result<()> { - use wasmtime_environ::component::FlatTypesStorage; - + fn rr_harness(record_fn: S, replay_fn: T) -> Result<()> + where + S: FnOnce(&mut RecordBuffer) -> Result<()>, + T: FnOnce(&mut ReplayBuffer) -> Result<()>, + { + // Record information let record_settings = RecordSettings::default(); let tmp = NamedTempFile::new()?; let tmppath = tmp.path().to_str().expect("Filename should be UTF-8"); - let values = vec![ValRaw::i32(1), ValRaw::f32(2), ValRaw::i64(3)]; - let flat = FlatTypesStorage::new(); - flat.push(FlatType::I32, FlatType::I32); - flat.push(FlatType::F32, FlatType::F32); - flat.push(FlatType::I64, FlatType::I64); - // Record values let mut recorder = RecordBuffer::new_recorder(Box::new(File::create(tmppath)?), record_settings)?; - recorder.record_event(|| { - __component_events::HostFuncReturnEvent::new(values.as_slice(), flat) - })?; + + record_fn(&mut recorder)?; recorder.flush()?; let tmp = tmp.into_temp_path(); @@ -618,22 +613,408 @@ mod tests { // Assert that replayed values are identical let mut replayer = ReplayBuffer::new_replayer(Box::new(File::open(tmppath)?), replay_settings)?; - let mut result_values = values.clone(); - replayer.next_event_and(|event: __component_events::HostFuncReturnEvent| { - event.move_into_slice(result_values.as_mut_slice()); - - // Check replay `values` matches record `values` - for (a, b) in values.iter().zip(result_values.iter()) { - unsafe { - assert!(a.assume_init().as_bytes() == b.assume_init().as_bytes()); - } - } - Ok(()) - })?; + + replay_fn(&mut replayer)?; // Check queue is empty assert!(replayer.next().is_none()); + Ok(()) + } + fn verify_equal_slices( + record_vals: &[ValRaw], + replay_vals: &[ValRaw], + flat_sizes: &[u8], + ) -> Result<()> { + for ((a, b), sz) in record_vals + .iter() + .zip(replay_vals.iter()) + .zip(flat_sizes.iter()) + { + let a_slice: &[u8] = &a.get_bytes()[..*sz as usize]; + let b_slice: &[u8] = &b.get_bytes()[..*sz as usize]; + assert!( + a_slice == b_slice, + "Recorded values {:?} and replayed values {:?} do not match", + a_slice, + b_slice + ); + } Ok(()) } + + #[test] + fn host_func() -> Result<()> { + let values = vec![ValRaw::f64(20), ValRaw::i32(10), ValRaw::i64(30)]; + let flat_sizes: Vec = vec![8, 4, 8]; + + let return_values = vec![ValRaw::i32(1), ValRaw::f32(2), ValRaw::i64(3)]; + let return_flat_sizes: Vec = vec![4, 4, 8]; + let mut return_replay_values = values.clone(); + + rr_harness( + |recorder| { + recorder.record_event(|| common_events::HostFuncEntryEvent { + args: RRFuncArgVals::from_flat_iter(&values, flat_sizes.iter().copied()), + })?; + recorder.record_event(|| common_events::HostFuncReturnEvent { + args: RRFuncArgVals::from_flat_iter( + &return_values, + return_flat_sizes.iter().copied(), + ), + }) + }, + |replayer| { + replayer.next_event_and(|event: common_events::HostFuncEntryEvent| { + event.validate(&common_events::HostFuncEntryEvent { + args: RRFuncArgVals::from_flat_iter(&values, flat_sizes.iter().copied()), + }) + })?; + replayer.next_event_and(|event: common_events::HostFuncReturnEvent| { + event.args.into_raw_slice(&mut return_replay_values); + Ok(()) + })?; + verify_equal_slices(&return_values, &return_replay_values, &return_flat_sizes) + }, + ) + } + + #[test] + fn wasm_func_entry() -> Result<()> { + let values = vec![ValRaw::i32(42), ValRaw::f64(314), ValRaw::i64(84)]; + let flat_sizes: Vec = vec![4, 8, 8]; + let origin = WasmFuncOrigin { + instance: InstanceId::from_u32(15), + index: FuncIndex::from_u32(7), + }; + let mut replay_values = values.clone(); + let mut replay_origin = None; + + let return_values = vec![ValRaw::f32(7), ValRaw::f32(8), ValRaw::v128(21)]; + let return_flat_sizes: Vec = vec![4, 4, 16]; + let mut return_replay_values = values.clone(); + + rr_harness( + |recorder| { + recorder.record_event(|| core_events::WasmFuncEntryEvent { + origin: origin.clone(), + args: RRFuncArgVals::from_flat_iter(&values, flat_sizes.iter().copied()), + })?; + recorder.record_event(|| __component_events::WasmFuncEntryEvent { + args: RRFuncArgVals::from_flat_iter( + &return_values, + return_flat_sizes.iter().copied(), + ), + }) + }, + |replayer| { + replayer.next_event_and(|event: core_events::WasmFuncEntryEvent| { + replay_origin = Some(event.origin); + event.args.into_raw_slice(&mut replay_values); + Ok(()) + })?; + assert!(origin == replay_origin.unwrap()); + verify_equal_slices(&values, &replay_values, &flat_sizes)?; + + replayer.next_event_and(|event: __component_events::WasmFuncEntryEvent| { + event.args.into_raw_slice(&mut return_replay_values); + Ok(()) + })?; + verify_equal_slices(&return_values, &return_replay_values, &return_flat_sizes) + }, + ) + } + + #[test] + fn builtin_event_entry() -> Result<()> { + use __component_events::{ + BuiltinEntryEvent, ResourceDropEntryEvent, ResourceEnterCallEntryEvent, + ResourceExitCallEntryEvent, ResourceTransferBorrowEntryEvent, + ResourceTransferOwnEntryEvent, + }; + let events: Vec = vec![ + BuiltinEntryEvent::ResourceDrop(ResourceDropEntryEvent { + resource: 42, + idx: 10, + }), + BuiltinEntryEvent::ResourceTransferOwn(ResourceTransferOwnEntryEvent { + src_idx: 5, + src_table: 1, + dst_table: 2, + }), + BuiltinEntryEvent::ResourceTransferBorrow(ResourceTransferBorrowEntryEvent { + src_idx: 7, + src_table: 3, + dst_table: 4, + }), + BuiltinEntryEvent::ResourceEnterCall(ResourceEnterCallEntryEvent {}), + BuiltinEntryEvent::ResourceExitCall(ResourceExitCallEntryEvent {}), + ]; + + rr_harness( + |recorder| { + for event in &events { + recorder.record_event(|| event.clone())?; + } + Ok(()) + }, + |replayer| { + for event in &events { + replayer.next_event_and(|replay_event: BuiltinEntryEvent| { + assert!(*event == replay_event); + Ok(()) + })?; + } + Ok(()) + }, + ) + } + + #[test] + fn builtin_event_return() -> Result<()> { + use __component_events::{ + BuiltinError, BuiltinReturnEvent, ResourceDropReturnEvent, ResourceExitCallReturnEvent, + ResourceRep32ReturnEvent, ResourceTransferBorrowReturnEvent, + ResourceTransferOwnReturnEvent, + }; + let events: Vec = vec![ + BuiltinReturnEvent::ResourceDrop(ResourceDropReturnEvent( + ResultEvent::from_anyhow_result(&Ok(ResourceDropRet::default())), + )), + BuiltinReturnEvent::ResourceRep32(ResourceRep32ReturnEvent( + ResultEvent::from_anyhow_result(&Ok(123)), + )), + BuiltinReturnEvent::ResourceTransferOwn(ResourceTransferOwnReturnEvent( + ResultEvent::from_anyhow_result(&Ok(42)), + )), + BuiltinReturnEvent::ResourceTransferBorrow(ResourceTransferBorrowReturnEvent( + ResultEvent::from_anyhow_result(&Ok(17)), + )), + BuiltinReturnEvent::ResourceExitCall(ResourceExitCallReturnEvent( + ResultEvent::from_anyhow_result(&Err(anyhow::anyhow!("Exit call failed!"))), + )), + ]; + + rr_harness( + |recorder| { + for event in &events { + recorder.record_event(|| event.clone())?; + } + Ok(()) + }, + |replayer| { + for event in &events { + replayer.next_event_and(|replay_event: BuiltinReturnEvent| { + match (replay_event, event) { + ( + BuiltinReturnEvent::ResourceDrop(e), + BuiltinReturnEvent::ResourceDrop(expected), + ) => { + assert_eq!(e.ret().unwrap(), expected.clone().ret().unwrap()); + } + ( + BuiltinReturnEvent::ResourceRep32(e), + BuiltinReturnEvent::ResourceRep32(expected), + ) => { + assert_eq!(e.ret().unwrap(), expected.clone().ret().unwrap()); + } + ( + BuiltinReturnEvent::ResourceTransferOwn(e), + BuiltinReturnEvent::ResourceTransferOwn(expected), + ) => { + assert_eq!(e.ret().unwrap(), expected.clone().ret().unwrap()); + } + ( + BuiltinReturnEvent::ResourceTransferBorrow(e), + BuiltinReturnEvent::ResourceTransferBorrow(expected), + ) => { + assert_eq!(e.ret().unwrap(), expected.clone().ret().unwrap()); + } + ( + BuiltinReturnEvent::ResourceExitCall(e), + BuiltinReturnEvent::ResourceExitCall(expected), + ) => { + assert_eq!( + e.ret() + .unwrap_err() + .downcast_ref::() + .unwrap() + .get(), + expected + .clone() + .ret() + .unwrap_err() + .downcast_ref::() + .unwrap() + .get() + ); + } + _ => unreachable!(), + }; + Ok(()) + })?; + } + Ok(()) + }, + ) + } + + #[test] + fn lower_flat_events() -> Result<()> { + use __component_events::{LowerFlatEntryEvent, LowerFlatReturnEvent}; + use wasmtime_environ::component::InterfaceType; + + let entry = LowerFlatEntryEvent { + ty: InterfaceType::U32, + }; + let return_event = LowerFlatReturnEvent(ResultEvent::from_anyhow_result(&Ok(()))); + + rr_harness( + |recorder| { + recorder.record_event(|| entry.clone())?; + recorder.record_event(|| return_event.clone())?; + Ok(()) + }, + |replayer| { + replayer.next_event_and(|e: LowerFlatEntryEvent| { + assert_eq!(e.ty, InterfaceType::U32); + Ok(()) + })?; + replayer.next_event_and(|e: LowerFlatReturnEvent| { + assert!(e.0.ret().is_ok()); + Ok(()) + })?; + Ok(()) + }, + ) + } + + #[test] + fn lower_memory_events() -> Result<()> { + use __component_events::{LowerMemoryEntryEvent, LowerMemoryReturnEvent}; + use wasmtime_environ::component::InterfaceType; + + let entry = LowerMemoryEntryEvent { + ty: InterfaceType::String, + offset: 1024, + }; + let return_event = LowerMemoryReturnEvent(ResultEvent::from_anyhow_result(&Ok(()))); + + rr_harness( + |recorder| { + recorder.record_event(|| entry.clone())?; + recorder.record_event(|| return_event.clone())?; + Ok(()) + }, + |replayer| { + replayer.next_event_and(|e: LowerMemoryEntryEvent| { + assert_eq!(e.ty, InterfaceType::String); + assert_eq!(e.offset, 1024); + Ok(()) + })?; + replayer.next_event_and(|e: LowerMemoryReturnEvent| { + assert!(e.0.ret().is_ok()); + Ok(()) + })?; + Ok(()) + }, + ) + } + + #[test] + fn realloc_events() -> Result<()> { + use __component_events::{ReallocEntryEvent, ReallocReturnEvent}; + + let entry = ReallocEntryEvent { + old_addr: 0x1000, + old_size: 64, + old_align: 8, + new_size: 128, + }; + let return_event = ReallocReturnEvent(ResultEvent::from_anyhow_result(&Ok(0x2000))); + + rr_harness( + |recorder| { + recorder.record_event(|| entry.clone())?; + recorder.record_event(|| return_event.clone())?; + Ok(()) + }, + |replayer| { + replayer.next_event_and(|e: ReallocEntryEvent| { + assert_eq!(e.old_addr, 0x1000); + assert_eq!(e.old_size, 64); + assert_eq!(e.old_align, 8); + assert_eq!(e.new_size, 128); + Ok(()) + })?; + replayer.next_event_and(|e: ReallocReturnEvent| { + assert_eq!(e.0.ret().unwrap(), 0x2000); + Ok(()) + })?; + Ok(()) + }, + ) + } + + #[test] + fn memory_slice_write_event() -> Result<()> { + use __component_events::MemorySliceWriteEvent; + + let event = MemorySliceWriteEvent { + offset: 512, + bytes: vec![0x01, 0x02, 0x03, 0x04, 0xFF], + }; + + rr_harness( + |recorder| { + recorder.record_event(|| event.clone())?; + Ok(()) + }, + |replayer| { + replayer.next_event_and(|e: MemorySliceWriteEvent| { + assert_eq!(e.offset, 512); + assert_eq!(e.bytes, vec![0x01, 0x02, 0x03, 0x04, 0xFF]); + Ok(()) + })?; + Ok(()) + }, + ) + } + + #[test] + fn instantiation_event() -> Result<()> { + use crate::component::ComponentInstanceId; + use crate::store::InstanceId; + use __component_events::InstantiationEvent as ComponentInstantiationEvent; + use core_events::InstantiationEvent as CoreInstantiationEvent; + + let component_event = ComponentInstantiationEvent { + component: [0xAB; 32], + instance: ComponentInstanceId::from_u32(42), + }; + + let core_event = CoreInstantiationEvent { + module: [0xCD; 32], + instance: InstanceId::from_u32(17), + }; + + rr_harness( + |recorder| { + recorder.record_event(|| component_event.clone())?; + recorder.record_event(|| core_event.clone())?; + Ok(()) + }, + |replayer| { + replayer.next_event_and(|e: ComponentInstantiationEvent| { + e.validate(&component_event)?; + Ok(()) + })?; + replayer.next_event_and(|e: CoreInstantiationEvent| { + e.validate(&core_event)?; + Ok(()) + })?; + Ok(()) + }, + ) + } } diff --git a/crates/wasmtime/src/runtime/rr/core/events/common_events.rs b/crates/wasmtime/src/runtime/rr/core/events/common_events.rs index e86ab81944..d27f916fd2 100644 --- a/crates/wasmtime/src/runtime/rr/core/events/common_events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events/common_events.rs @@ -6,10 +6,18 @@ use super::*; use serde::{Deserialize, Serialize}; +/// A call event from Wasm (core or component) into the host +/// +/// Matches with [`HostFuncReturnEvent`] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct HostFuncEntryEvent { + /// Raw values passed across the call/return boundary + pub args: RRFuncArgVals, +} + /// A return event after a host call to Wasm (core or component) /// -/// Matches with either [`component_events::HostFuncEntryEvent`] or -/// [`core_events::HostFuncEntryEvent`] +/// Matches with [`HostFuncEntryEvent`] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HostFuncReturnEvent { /// Raw values passed across the call/return boundary diff --git a/crates/wasmtime/src/runtime/rr/core/events/component_events.rs b/crates/wasmtime/src/runtime/rr/core/events/component_events.rs index 3ecedd91a4..77bd50623e 100644 --- a/crates/wasmtime/src/runtime/rr/core/events/component_events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events/component_events.rs @@ -33,13 +33,6 @@ pub struct WasmFuncEntryEvent { pub args: RRFuncArgVals, } -/// A call event from a Wasm component into the host -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct HostFuncEntryEvent { - /// Raw values passed across the call entry boundary - pub args: RRFuncArgVals, -} - /// A reallocation call event in the Component Model canonical ABI /// /// Usually performed during lowering of complex [`ComponentType`]s to Wasm diff --git a/crates/wasmtime/src/runtime/rr/core/events/core_events.rs b/crates/wasmtime/src/runtime/rr/core/events/core_events.rs index abcae38aca..e81d84e546 100644 --- a/crates/wasmtime/src/runtime/rr/core/events/core_events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events/core_events.rs @@ -19,10 +19,3 @@ pub struct WasmFuncEntryEvent { /// Raw values passed across call boundary pub args: RRFuncArgVals, } - -/// A call event from a Core Wasm module into the host -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct HostFuncEntryEvent { - /// Raw values passed across the call/return boundary - pub args: RRFuncArgVals, -} diff --git a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs index 3339859e6a..6dea91a098 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs @@ -2,9 +2,7 @@ use crate::ValRaw; #[cfg(feature = "component-model")] use crate::component::func::LowerContext; #[cfg(feature = "rr-component")] -use crate::rr::common_events::WasmFuncReturnEvent; -#[cfg(feature = "rr-component")] -use crate::rr::component_events::HostFuncEntryEvent; +use crate::rr::common_events::{HostFuncEntryEvent, WasmFuncReturnEvent}; #[cfg(feature = "rr-component")] use crate::rr::component_events::{ LowerFlatReturnEvent, LowerMemoryReturnEvent, WasmFuncEntryEvent, diff --git a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs index 12417eac78..c081db845d 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs @@ -1,8 +1,8 @@ use crate::rr::FlatBytes; #[cfg(feature = "rr")] use crate::rr::{ - RRFuncArgVals, ResultEvent, common_events::HostFuncReturnEvent, - common_events::WasmFuncReturnEvent, core_events::HostFuncEntryEvent, + RRFuncArgVals, ResultEvent, common_events::HostFuncEntryEvent, + common_events::HostFuncReturnEvent, common_events::WasmFuncReturnEvent, core_events::WasmFuncEntryEvent, }; use crate::store::StoreOpaque; diff --git a/crates/wasmtime/src/runtime/values.rs b/crates/wasmtime/src/runtime/values.rs index 260f235127..d910fcf380 100644 --- a/crates/wasmtime/src/runtime/values.rs +++ b/crates/wasmtime/src/runtime/values.rs @@ -1181,9 +1181,9 @@ mod tests { || cfg!(target_arch = "riscv64") || cfg!(target_arch = "arm") { - 24 + 32 } else if cfg!(target_arch = "x86") { - 20 + 28 } else { panic!("unsupported architecture") }; @@ -1200,9 +1200,9 @@ mod tests { || cfg!(target_arch = "riscv64") || cfg!(target_arch = "arm") { - 24 + 32 } else if cfg!(target_arch = "x86") { - 20 + 28 } else { panic!("unsupported architecture") }; diff --git a/crates/wasmtime/src/runtime/vm/component/libcalls.rs b/crates/wasmtime/src/runtime/vm/component/libcalls.rs index 17db9aebaf..c9b9aba28c 100644 --- a/crates/wasmtime/src/runtime/vm/component/libcalls.rs +++ b/crates/wasmtime/src/runtime/vm/component/libcalls.rs @@ -658,7 +658,7 @@ fn resource_drop( )) } -#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] pub struct ResourceDropRet(Option); unsafe impl HostResultHasUnwindSentinel for ResourceDropRet { diff --git a/crates/wasmtime/src/runtime/vm/vmcontext.rs b/crates/wasmtime/src/runtime/vm/vmcontext.rs index 8a2c06b265..d1c491c708 100644 --- a/crates/wasmtime/src/runtime/vm/vmcontext.rs +++ b/crates/wasmtime/src/runtime/vm/vmcontext.rs @@ -1613,7 +1613,6 @@ impl ValRaw { #[inline] pub fn get_bytes(&self) -> &[u8; mem::size_of::()] { unsafe { &self.bytes } - //unsafe { &*(self as *const Self as *const [u8; mem::size_of::()]) } } /// Create a WebAssembly value from raw bytes From b9f26525154012d93c6c71b26a1fb546622a2381 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 18 Nov 2025 21:00:24 -0500 Subject: [PATCH 39/73] Fix warnings for CI --- crates/environ/src/component/types.rs | 90 ++++++++++++++- crates/environ/src/component/types_builder.rs | 90 --------------- .../src/runtime/component/func/host.rs | 2 +- crates/wasmtime/src/runtime/func.rs | 10 +- crates/wasmtime/src/runtime/func/typed.rs | 3 +- crates/wasmtime/src/runtime/rr.rs | 6 +- crates/wasmtime/src/runtime/rr/hooks.rs | 105 +---------------- .../src/runtime/rr/hooks/component_hooks.rs | 106 +++++++++++++++++- 8 files changed, 203 insertions(+), 209 deletions(-) diff --git a/crates/environ/src/component/types.rs b/crates/environ/src/component/types.rs index 84e5f9874e..8853290467 100644 --- a/crates/environ/src/component/types.rs +++ b/crates/environ/src/component/types.rs @@ -1,4 +1,4 @@ -use crate::component::{FlatTypesStorage, MAX_FLAT_PARAMS, MAX_FLAT_RESULTS}; +use crate::component::{MAX_FLAT_PARAMS, MAX_FLAT_RESULTS}; use crate::{EntityType, ModuleInternedTypeIndex, ModuleTypes, PrimaryMap}; use crate::{TypeTrace, prelude::*}; use core::hash::{Hash, Hasher}; @@ -1386,6 +1386,83 @@ const fn max_flat(a: Option, b: Option) -> Option { } } +/// Representation of flat types in 32-bit and 64-bit memory +/// +/// This could be represented as `Vec` but on 64-bit architectures +/// that's 24 bytes. Otherwise `FlatType` is 1 byte large and +/// `MAX_FLAT_TYPES` is 16, so it should ideally be more space-efficient to +/// use a flat array instead of a heap-based vector. +#[derive(Debug)] +pub struct FlatTypesStorage { + /// Representation for 32-bit memory + pub memory32: [FlatType; MAX_FLAT_TYPES], + /// Representation for 64-bit memory + pub memory64: [FlatType; MAX_FLAT_TYPES], + + /// Tracks the number of flat types pushed into this storage. If this is + /// `MAX_FLAT_TYPES + 1` then this storage represents an un-reprsentable + /// type in flat types. + /// + /// This value should be the same on both `memory32` and `memory64` + pub len: u8, +} + +impl FlatTypesStorage { + /// Create a new, empty storage for flat types + pub const fn new() -> FlatTypesStorage { + FlatTypesStorage { + memory32: [FlatType::I32; MAX_FLAT_TYPES], + memory64: [FlatType::I32; MAX_FLAT_TYPES], + len: 0, + } + } + + /// Returns a reference to flat type representation + pub fn as_flat_types(&self) -> Option> { + let len = usize::from(self.len); + if len > MAX_FLAT_TYPES { + assert_eq!(len, MAX_FLAT_TYPES + 1); + None + } else { + Some(FlatTypes { + memory32: &self.memory32[..len], + memory64: &self.memory64[..len], + }) + } + } + + /// Pushes a new flat type into this list using `t32` for 32-bit memories + /// and `t64` for 64-bit memories. + /// + /// Returns whether the type was actually pushed or whether this list of + /// flat types just exceeded the maximum meaning that it is now + /// unrepresentable with a flat list of types. + pub fn push(&mut self, t32: FlatType, t64: FlatType) -> bool { + let len = usize::from(self.len); + if len < MAX_FLAT_TYPES { + self.memory32[len] = t32; + self.memory64[len] = t64; + self.len += 1; + true + } else { + // If this was the first one to go over then flag the length as + // being incompatible with a flat representation. + if len == MAX_FLAT_TYPES { + self.len += 1; + } + false + } + } + + /// Generate an iterator over the 32-bit flat encoding + pub fn iter32(&self) -> impl Iterator { + self.memory32 + .iter() + .take(self.len as usize) + .map(|f| f.byte_size()) + } +} + /// Flat representation of a type in just core wasm types. pub struct FlatTypes<'a> { /// The flat representation of this type in 32-bit memories. @@ -1417,6 +1494,17 @@ pub enum FlatType { } impl FlatType { + /// Constructs the "joined" representation for two flat types + pub fn join(&mut self, other: FlatType) { + if *self == other { + return; + } + *self = match (*self, other) { + (FlatType::I32, FlatType::F32) | (FlatType::F32, FlatType::I32) => FlatType::I32, + _ => FlatType::I64, + }; + } + /// Return the size in bytes for this flat type pub const fn byte_size(&self) -> u8 { match self { diff --git a/crates/environ/src/component/types_builder.rs b/crates/environ/src/component/types_builder.rs index 5c132b1519..7340e1b258 100644 --- a/crates/environ/src/component/types_builder.rs +++ b/crates/environ/src/component/types_builder.rs @@ -840,96 +840,6 @@ where return idx; } -/// Representation of flat types in 32-bit and 64-bit memory -/// -/// This could be represented as `Vec` but on 64-bit architectures -/// that's 24 bytes. Otherwise `FlatType` is 1 byte large and -/// `MAX_FLAT_TYPES` is 16, so it should ideally be more space-efficient to -/// use a flat array instead of a heap-based vector. -#[derive(Debug)] -pub struct FlatTypesStorage { - /// Representation for 32-bit memory - pub memory32: [FlatType; MAX_FLAT_TYPES], - /// Representation for 64-bit memory - pub memory64: [FlatType; MAX_FLAT_TYPES], - - /// Tracks the number of flat types pushed into this storage. If this is - /// `MAX_FLAT_TYPES + 1` then this storage represents an un-reprsentable - /// type in flat types. - /// - /// This value should be the same on both `memory32` and `memory64` - pub len: u8, -} - -impl FlatTypesStorage { - /// Create a new, empty storage for flat types - pub const fn new() -> FlatTypesStorage { - FlatTypesStorage { - memory32: [FlatType::I32; MAX_FLAT_TYPES], - memory64: [FlatType::I32; MAX_FLAT_TYPES], - len: 0, - } - } - - /// Returns a reference to flat type representation - pub fn as_flat_types(&self) -> Option> { - let len = usize::from(self.len); - if len > MAX_FLAT_TYPES { - assert_eq!(len, MAX_FLAT_TYPES + 1); - None - } else { - Some(FlatTypes { - memory32: &self.memory32[..len], - memory64: &self.memory64[..len], - }) - } - } - - /// Pushes a new flat type into this list using `t32` for 32-bit memories - /// and `t64` for 64-bit memories. - /// - /// Returns whether the type was actually pushed or whether this list of - /// flat types just exceeded the maximum meaning that it is now - /// unrepresentable with a flat list of types. - pub fn push(&mut self, t32: FlatType, t64: FlatType) -> bool { - let len = usize::from(self.len); - if len < MAX_FLAT_TYPES { - self.memory32[len] = t32; - self.memory64[len] = t64; - self.len += 1; - true - } else { - // If this was the first one to go over then flag the length as - // being incompatible with a flat representation. - if len == MAX_FLAT_TYPES { - self.len += 1; - } - false - } - } - - /// Generate an iterator over the 32-bit flat encoding - pub fn iter32(&self) -> impl Iterator { - self.memory32 - .iter() - .take(self.len as usize) - .map(|f| f.byte_size()) - } -} - -impl FlatType { - /// Constructs the "joined" representation for two flat types - pub fn join(&mut self, other: FlatType) { - if *self == other { - return; - } - *self = match (*self, other) { - (FlatType::I32, FlatType::F32) | (FlatType::F32, FlatType::I32) => FlatType::I32, - _ => FlatType::I64, - }; - } -} - #[derive(Default)] struct TypeInformationCache { records: PrimaryMap, diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 079507339c..120ccd7c28 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -867,7 +867,7 @@ where storage[0] = MaybeUninit::new(ValRaw::i32(status as i32)); rr::component_hooks::record_host_func_return( - &mut storage[..1], + &storage[..1], types, &InterfaceType::U32, store.0, diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 1b4ef3e629..079654e6e4 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -1303,10 +1303,7 @@ impl Func { let nparams = ty.params().len(); val_vec.reserve(nparams + ty.results().len()); - let flat_params = ty - .params() - .into_iter() - .map(|x| x.to_wasm_type().byte_size()); + let flat_params = ty.params().map(|x| x.to_wasm_type().byte_size()); if !caller.store.0.replay_enabled() { rr::core_hooks::record_validate_host_func_entry( @@ -1332,10 +1329,7 @@ impl Func { values_vec[i] = ret.to_raw(&mut caller.store)?; } - let flat_results = ty - .results() - .into_iter() - .map(|x| x.to_wasm_type().byte_size()); + let flat_results = ty.results().map(|x| x.to_wasm_type().byte_size()); rr::core_hooks::record_host_func_return(values_vec, flat_results, &mut caller.store.0)?; } else { rr::core_hooks::replay_validate_host_func_entry( diff --git a/crates/wasmtime/src/runtime/func/typed.rs b/crates/wasmtime/src/runtime/func/typed.rs index b39947630f..ddd56655de 100644 --- a/crates/wasmtime/src/runtime/func/typed.rs +++ b/crates/wasmtime/src/runtime/func/typed.rs @@ -194,8 +194,7 @@ where params.store(&mut store, ty, dst)?; } - let storage_len = - mem::size_of_val::>(&mut storage) / mem::size_of::(); + let storage_len = mem::size_of_val::>(&storage) / mem::size_of::(); let storage_slice: *mut Storage<_, _> = &mut storage; let storage_slice = storage_slice.cast::(); let storage_slice = core::ptr::slice_from_raw_parts_mut(storage_slice, storage_len); diff --git a/crates/wasmtime/src/runtime/rr.rs b/crates/wasmtime/src/runtime/rr.rs index b9fcd2d698..2b4196c0d0 100644 --- a/crates/wasmtime/src/runtime/rr.rs +++ b/crates/wasmtime/src/runtime/rr.rs @@ -43,7 +43,11 @@ impl FlatBytes for MaybeUninit { /// Convenience method hooks for injecting event recording/replaying in the rest of the engine mod hooks; -pub(crate) use hooks::{ConstMemorySliceCell, MemorySliceCell, component_hooks, core_hooks}; +pub(crate) use hooks::core_hooks; +#[cfg(feature = "component-model")] +pub(crate) use hooks::{ + component_hooks, component_hooks::ConstMemorySliceCell, component_hooks::MemorySliceCell, +}; /// Core infrastructure for RR support #[cfg(feature = "rr")] diff --git a/crates/wasmtime/src/runtime/rr/hooks.rs b/crates/wasmtime/src/runtime/rr/hooks.rs index a63269d06a..a126791dea 100644 --- a/crates/wasmtime/src/runtime/rr/hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks.rs @@ -1,108 +1,5 @@ -#[cfg(feature = "rr-component")] -use crate::rr::{RecordBuffer, Recorder, component_events::MemorySliceWriteEvent}; - -use core::ops::{Deref, DerefMut}; - /// Component specific RR hooks that use `component-model` feature gating +#[cfg(feature = "component-model")] pub mod component_hooks; /// Core RR hooks pub mod core_hooks; - -/// 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> { - pub bytes: &'a mut [u8], - #[cfg(feature = "rr-component")] - pub offset: usize, - #[cfg(feature = "rr-component")] - pub 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) { - #[cfg(feature = "rr-component")] - if let Some(buf) = &mut self.recorder { - buf.record_event(|| MemorySliceWriteEvent { - offset: self.offset, - bytes: self.bytes.to_vec(), - }) - .unwrap(); - } - } -} - -/// 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 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), -/// 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> { - pub bytes: &'a mut [u8; N], - #[cfg(feature = "rr-component")] - pub offset: usize, - #[cfg(feature = "rr-component")] - pub 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) { - #[cfg(feature = "rr-component")] - if let Some(buf) = &mut self.recorder { - buf.record_event(|| MemorySliceWriteEvent { - offset: self.offset, - bytes: self.bytes.to_vec(), - }) - .unwrap(); - } - } -} diff --git a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs index 5d31172fbc..109e80ba6b 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs @@ -1,5 +1,9 @@ use crate::ValRaw; -#[cfg(feature = "component-model")] +#[cfg(feature = "rr-component")] +use crate::rr::{RecordBuffer, Recorder, component_events::MemorySliceWriteEvent}; + +use core::ops::{Deref, DerefMut}; + use crate::component::func::LowerContext; #[cfg(feature = "rr-component")] use crate::rr::common_events::{HostFuncEntryEvent, WasmFuncReturnEvent}; @@ -14,7 +18,6 @@ use crate::store::StoreOpaque; use crate::{StoreContextMut, prelude::*}; use alloc::sync::Arc; use core::mem::MaybeUninit; -#[cfg(feature = "component-model")] use wasmtime_environ::component::{ComponentTypes, InterfaceType, TypeFuncIndex}; #[cfg(all(feature = "rr-component"))] use wasmtime_environ::component::{MAX_FLAT_PARAMS, MAX_FLAT_RESULTS}; @@ -180,3 +183,102 @@ fn create_host_func_entry_event( args: RRFuncArgVals::from_flat_storage(args, flat_params), } } + +/// 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> { + pub bytes: &'a mut [u8], + #[cfg(feature = "rr-component")] + pub offset: usize, + #[cfg(feature = "rr-component")] + pub 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) { + #[cfg(feature = "rr-component")] + if let Some(buf) = &mut self.recorder { + buf.record_event(|| MemorySliceWriteEvent { + offset: self.offset, + bytes: self.bytes.to_vec(), + }) + .unwrap(); + } + } +} + +/// 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 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), +/// 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> { + pub bytes: &'a mut [u8; N], + #[cfg(feature = "rr-component")] + pub offset: usize, + #[cfg(feature = "rr-component")] + pub 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) { + #[cfg(feature = "rr-component")] + if let Some(buf) = &mut self.recorder { + buf.record_event(|| MemorySliceWriteEvent { + offset: self.offset, + bytes: self.bytes.to_vec(), + }) + .unwrap(); + } + } +} From b72725679374258628d0079db18b6e96514ffb8e Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 18 Nov 2025 21:57:21 -0500 Subject: [PATCH 40/73] Fix missing exit call during merge --- crates/wasmtime/src/runtime/component/func/host.rs | 1 + crates/wasmtime/src/runtime/rr/core.rs | 2 +- crates/wasmtime/src/runtime/rr/replay_driver.rs | 10 ++++++++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 120ccd7c28..4b1966c050 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -371,6 +371,7 @@ where unsafe { flags.set_may_leave(true); } + lower.exit_call()?; } return Ok(()); diff --git a/crates/wasmtime/src/runtime/rr/core.rs b/crates/wasmtime/src/runtime/rr/core.rs index 9be54e3921..3be5b541b1 100644 --- a/crates/wasmtime/src/runtime/rr/core.rs +++ b/crates/wasmtime/src/runtime/rr/core.rs @@ -603,7 +603,7 @@ mod tests { RecordBuffer::new_recorder(Box::new(File::create(tmppath)?), record_settings)?; record_fn(&mut recorder)?; - recorder.flush()?; + drop(recorder); let tmp = tmp.into_temp_path(); let tmppath = >::as_ref(&tmp) diff --git a/crates/wasmtime/src/runtime/rr/replay_driver.rs b/crates/wasmtime/src/runtime/rr/replay_driver.rs index b25190f86d..45163c1b3f 100644 --- a/crates/wasmtime/src/runtime/rr/replay_driver.rs +++ b/crates/wasmtime/src/runtime/rr/replay_driver.rs @@ -287,7 +287,10 @@ impl ReplayInstance { } } - _ => Err(ReplayError::IncorrectEventVariant)?, + _ => { + log::error!("Unexpected non-top-level RR event: {:?}", rr_event); + Err(ReplayError::IncorrectEventVariant)? + } } Ok(()) } @@ -445,7 +448,10 @@ impl ReplayInstance { } } - _ => Err(ReplayError::IncorrectEventVariant)?, + _ => { + log::error!("Unexpected non-top-level RR event: {:?}", rr_event); + Err(ReplayError::IncorrectEventVariant)? + } } Ok(()) } From 89c915f3822a72a500c2d4c00c9895cee115cf21 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Wed, 19 Nov 2025 11:13:25 -0500 Subject: [PATCH 41/73] Fix: shortcut error return for wasm func rr interposition --- crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs | 6 ++++-- crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs | 8 ++++++-- crates/wasmtime/src/runtime/rr/replay_driver.rs | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs index 109e80ba6b..d5b5b53975 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs @@ -71,10 +71,12 @@ where || &result, )?; result?; - return Ok(()); + Ok(()) } #[cfg(not(feature = "rr-component"))] - return result; + { + result + } } /// Record hook operation for host function entry events diff --git a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs index c081db845d..00097b5214 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs @@ -50,11 +50,15 @@ where || &result, )?; result?; + Ok(()) + } else { + result } - return Ok(()); } #[cfg(not(feature = "rr"))] - return result; + { + result + } } /// Record hook operation for host function entry events diff --git a/crates/wasmtime/src/runtime/rr/replay_driver.rs b/crates/wasmtime/src/runtime/rr/replay_driver.rs index 45163c1b3f..ae476db071 100644 --- a/crates/wasmtime/src/runtime/rr/replay_driver.rs +++ b/crates/wasmtime/src/runtime/rr/replay_driver.rs @@ -74,7 +74,7 @@ impl ReplayEnvironment { /// /// # Example /// -/// ``` +/// ```no_run /// use wasmtime::*; /// use wasmtime::component::Component; /// From a9ff0004b41394b39fc0201dd3d5ff7b6322a81d Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Wed, 19 Nov 2025 14:31:10 -0500 Subject: [PATCH 42/73] Fix doctest and add writer extraction --- crates/wasmtime/src/runtime/rr/core.rs | 27 ++++---- crates/wasmtime/src/runtime/rr/core/io.rs | 11 ++-- .../wasmtime/src/runtime/rr/replay_driver.rs | 66 +++++++++++++++---- crates/wasmtime/src/runtime/store.rs | 43 ++++++++++-- 4 files changed, 114 insertions(+), 33 deletions(-) diff --git a/crates/wasmtime/src/runtime/rr/core.rs b/crates/wasmtime/src/runtime/rr/core.rs index 3be5b541b1..db86f2e8c5 100644 --- a/crates/wasmtime/src/runtime/rr/core.rs +++ b/crates/wasmtime/src/runtime/rr/core.rs @@ -269,7 +269,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: impl RecordWriter + 'static, settings: RecordSettings) -> Result + fn new_recorder(writer: impl RecordWriter, settings: RecordSettings) -> Result where Self: Sized; @@ -283,6 +283,9 @@ pub trait Recorder { T: Into, F: FnOnce() -> T; + /// Consumes this [`Recorder`] and returns its underlying writer + fn into_writer(self) -> Result>; + /// Trigger an explicit flush of any buffered data to the writer /// /// Buffer should be emptied during this process @@ -408,21 +411,17 @@ impl RecordBuffer { } Ok(()) } -} -impl Drop for RecordBuffer { - fn drop(&mut self) { + /// End the trace and flush any remaining data + pub fn finish(&mut self) -> Result<()> { // Insert End of trace delimiter - self.push_event(RREvent::Eof).unwrap(); - self.flush().unwrap(); + self.push_event(RREvent::Eof)?; + self.flush() } } impl Recorder for RecordBuffer { - fn new_recorder( - mut writer: impl RecordWriter + 'static, - settings: RecordSettings, - ) -> Result { + fn new_recorder(mut writer: impl RecordWriter, 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(&settings, &mut writer)?; @@ -444,6 +443,12 @@ impl Recorder for RecordBuffer { self.push_event(event) } + #[inline] + fn into_writer(mut self) -> Result> { + self.finish()?; + Ok(self.writer) + } + fn flush(&mut self) -> Result<()> { log::debug!("Flushing record buffer..."); for e in self.buf.drain(..) { @@ -603,7 +608,7 @@ mod tests { RecordBuffer::new_recorder(Box::new(File::create(tmppath)?), record_settings)?; record_fn(&mut recorder)?; - drop(recorder); + recorder.finish()?; let tmp = tmp.into_temp_path(); let tmppath = >::as_ref(&tmp) diff --git a/crates/wasmtime/src/runtime/rr/core/io.rs b/crates/wasmtime/src/runtime/rr/core/io.rs index 9a25de5c85..0e347b3f49 100644 --- a/crates/wasmtime/src/runtime/rr/core/io.rs +++ b/crates/wasmtime/src/runtime/rr/core/io.rs @@ -1,4 +1,5 @@ use crate::prelude::*; +use core::any::Any; use postcard; use serde::{Deserialize, Serialize}; @@ -10,8 +11,8 @@ cfg_if::cfg_if! { /// 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 {} + pub trait RecordWriter: Write + Send + Sync + Any {} + impl RecordWriter for T {} /// An [`Read`] usable for replaying in RR pub trait ReplayReader: Read + Seek + Send + Sync {} @@ -24,8 +25,8 @@ cfg_if::cfg_if! { /// 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 {} + pub trait RecordWriter: Write + Send + Sync + Any {} + impl RecordWriter for T {} /// An [`Read`] usable for replaying in RR /// @@ -38,7 +39,7 @@ cfg_if::cfg_if! { /// Serialize and write `value` to a `RecordWriter` /// /// Currently uses `postcard` serializer -pub(super) fn to_record_writer(value: &T, writer: W) -> Result<()> +pub(super) fn to_record_writer(value: &T, writer: &mut W) -> Result<()> where T: Serialize + ?Sized, W: RecordWriter, diff --git a/crates/wasmtime/src/runtime/rr/replay_driver.rs b/crates/wasmtime/src/runtime/rr/replay_driver.rs index ae476db071..85ebafe44f 100644 --- a/crates/wasmtime/src/runtime/rr/replay_driver.rs +++ b/crates/wasmtime/src/runtime/rr/replay_driver.rs @@ -74,22 +74,62 @@ impl ReplayEnvironment { /// /// # Example /// -/// ```no_run +/// ``` /// use wasmtime::*; /// use wasmtime::component::Component; +/// # use std::io::Cursor; +/// # use wasmtime::component; +/// # use core::any::Any; +/// # fn main() -> Result<()> { +/// let component_str: &str = r#" +/// (component +/// (core module $m +/// (func (export "main") (result i32) +/// i32.const 42 +/// ) +/// ) +/// (core instance $i (instantiate $m)) +/// +/// (func (export "main") (result u32) +/// (canon lift (core func $i "main")) +/// ) +/// ) +/// "#; +/// +/// # let record_settings = RecordSettings::default(); +/// # let mut config = Config::new(); +/// # config.rr(RRConfig::Recording); +/// +/// # let engine = Engine::new(&config)?; +/// # let component = Component::new(&engine, component_str)?; +/// # let mut linker = component::Linker::new(&engine); +/// +/// # let writer: Cursor> = Cursor::new(Vec::new()); +/// # let mut store = Store::new(&engine, ()); +/// # store.init_recording(writer, record_settings)?; +/// +/// # let instance = linker.instantiate(&mut store, &component)?; +/// # let func = instance.get_typed_func::<(), (u32,)>(&mut store, "main")?; +/// # let _ = func.call(&mut store, ()); +/// +/// # let trace_box = store.into_record_writer()?; +/// # let any_box: Box = trace_box; +/// # let mut trace_reader = any_box.downcast::>>().unwrap(); +/// # trace_reader.set_position(0); +/// +/// // let trace_reader = ... (obtain a ReplayReader over the recorded trace from somewhere) /// -/// fn main() -> Result<()> { -/// let config = Config::new(); -/// config.rr(RRConfig::Recording); -/// let engine = Engine::new(&config)?; -/// let mut renv = ReplayEnvironment::new(&engine, ReplaySettings::default()); -/// renv.add_component(Component::from_file(&engine, /* path to component file */)?); -/// // You can add more components, or modules with renv.add_module(module); -/// // .... -/// let mut instance = renv.instantiate(BufReader::new(/* path to trace file */))?; -/// instance.run_to_completion()?; -/// Ok(()) -/// } +/// let mut config = Config::new(); +/// config.rr(RRConfig::Replaying); +/// let engine = Engine::new(&config)?; +/// let mut renv = ReplayEnvironment::new(&engine, ReplaySettings::default()); +/// renv.add_component(Component::new(&engine, component_str)?); +/// // You can add more components, or modules with renv.add_module(module); +/// // .... +/// let mut instance = renv.instantiate(trace_reader)?; +/// instance.run_to_completion()?; +/// # Ok(()) +/// # } /// ``` pub struct ReplayInstance { env: Arc, diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index 3cb960d25a..d4fcd00949 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -900,6 +900,27 @@ impl Store { } } + /// Consumes this [`Store`], destroying it and obtain + /// the [`RecordWriter`] initialized for recording. + /// + /// ## Notes + /// + /// The [`RecordWriter`] is never internally updated besides + /// with the `init_recording` API + #[cfg(feature = "rr")] + pub fn into_record_writer(mut self) -> Result> { + // See [`Store::into_data`] and the `Drop` implementation of + // `Store` for documentation on this operation + self.run_manual_drop_routines(); + + unsafe { + let mut inner = ManuallyDrop::take(&mut self.inner); + core::mem::forget(self); + ManuallyDrop::drop(&mut inner.data_no_provenance); + inner.inner.into_record_writer() + } + } + /// Configures the [`ResourceLimiter`] used to limit resource creation /// within this [`Store`]. /// @@ -1318,13 +1339,13 @@ impl Store { /// Configure a [`Store`] to enable execution recording /// - /// This feature must be initialized before instantiating any module within + /// This must be initialized before instantiating any module within /// the Store. Recording of events is performed according to provided settings, and /// written to the provided writer. #[cfg(feature = "rr")] pub fn init_recording( &mut self, - recorder: impl RecordWriter + 'static, + recorder: impl RecordWriter, settings: RecordSettings, ) -> Result<()> { self.inner.init_recording(recorder, settings) @@ -1332,7 +1353,7 @@ impl Store { /// Configure a [`Store`] to enable execution replaying /// - /// This feature must be initialized before instantiating any module within + /// This must be initialized before instantiating any module within /// the Store. Replay of events is performed according to provided settings, and /// read from the provided reader. #[cfg(feature = "rr")] @@ -1856,7 +1877,7 @@ impl StoreOpaque { #[cfg(feature = "rr")] pub fn init_recording( &mut self, - recorder: impl RecordWriter + 'static, + recorder: impl RecordWriter, settings: RecordSettings, ) -> Result<()> { ensure!( @@ -1871,6 +1892,14 @@ impl StoreOpaque { Ok(()) } + #[cfg(feature = "rr")] + pub fn into_record_writer(mut self) -> Result> { + self.record_buffer + .take() + .ok_or_else(|| anyhow::anyhow!("record buffer in store was not initialized"))? + .into_writer() + } + #[cfg(feature = "rr")] pub(crate) fn init_replaying( &mut self, @@ -3036,6 +3065,12 @@ impl Drop for StoreOpaque { } } } + + // Flush any remaining recording data + #[cfg(feature = "rr")] + if let Some(buf) = self.record_buffer_mut() { + buf.finish().unwrap(); + } } } From 18da3082e577944a332b2847d4a4159a760fe48f Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Wed, 19 Nov 2025 17:35:47 -0500 Subject: [PATCH 43/73] Fix: replay on host functions skipping lift --- .../src/runtime/component/func/host.rs | 132 ++++++++++-------- crates/wasmtime/src/runtime/rr/core.rs | 12 +- .../src/runtime/rr/hooks/component_hooks.rs | 3 + .../wasmtime/src/runtime/rr/replay_driver.rs | 76 +++++----- src/commands/replay.rs | 9 -- 5 files changed, 126 insertions(+), 106 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 4b1966c050..46ca47a938 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -774,48 +774,46 @@ where let param_tys = &types[func_ty.params]; let result_tys = &types[func_ty.results]; - let mut params_and_results = Vec::new(); - let mut lift = &mut LiftContext::new(store.0.store_opaque_mut(), options, instance); - lift.enter_call(); let max_flat = if async_ { MAX_FLAT_ASYNC_PARAMS } else { MAX_FLAT_PARAMS }; - let ty = ComponentFunc::from(ty, &lift.instance_type()); - let ret_index = unsafe { - dynamic_params_load( - &mut lift, - types, - storage, - param_tys, - &mut params_and_results, - max_flat, - )? - }; - let result_start = params_and_results.len(); - for _ in 0..result_tys.types.len() { - params_and_results.push(Val::Bool(false)); - } + // This top-level switch determines whether or not we're in replay mode or + // not. In replay mode, we skip all lifting and execution of host functions and + // just replay lowering effects observed in the trace + if !store.0.replay_enabled() { + let mut params_and_results = Vec::new(); + let mut lift = &mut LiftContext::new(store.0.store_opaque_mut(), options, instance); + lift.enter_call(); + let ty = ComponentFunc::from(ty, &lift.instance_type()); + + let ret_index = unsafe { + dynamic_params_load( + &mut lift, + types, + storage, + param_tys, + &mut params_and_results, + max_flat, + )? + }; + let result_start = params_and_results.len(); + for _ in 0..result_tys.types.len() { + params_and_results.push(Val::Bool(false)); + } - rr::component_hooks::record_validate_host_func_entry( - storage, - types, - &InterfaceType::Tuple(func_ty.params), - store.0.store_opaque_mut(), - )?; - rr::component_hooks::replay_validate_host_func_entry( - storage, - types, - &InterfaceType::Tuple(func_ty.params), - store.0.store_opaque_mut(), - )?; + rr::component_hooks::record_validate_host_func_entry( + storage, + types, + &InterfaceType::Tuple(func_ty.params), + store.0.store_opaque_mut(), + )?; - if async_ { - #[cfg(feature = "component-model-async")] - { - if !store.0.replay_enabled() { + if async_ { + #[cfg(feature = "component-model-async")] + { let retptr = if result_tys.types.len() == 0 { 0 } else { @@ -873,27 +871,15 @@ where &InterfaceType::U32, store.0, )?; - } else { - // Skip lifting/lowering logic, and just replaying the lowering state - #[cfg(feature = "rr-component")] - { - let mut cx = LowerContext::new(store, options, instance); - cx.replay_lowering( - Some(&mut storage[..1]), - ReplayLoweringPhase::HostFuncReturn, - )?; - } } - } - #[cfg(not(feature = "component-model-async"))] - { - unreachable!( - "async-lowered imports should have failed validation \ + #[cfg(not(feature = "component-model-async"))] + { + unreachable!( + "async-lowered imports should have failed validation \ when `component-model-async` feature disabled" - ); - } - } else { - if !store.0.replay_enabled() { + ); + } + } else { let future = closure(store.as_context_mut(), ty, params_and_results, result_start); let result_vals = concurrent::poll_and_block(store.0, future, caller_instance)?; let result_vals = &result_vals[result_start..]; @@ -946,11 +932,29 @@ where } cx.exit_call()?; - } else { + } + } else { + rr::component_hooks::replay_validate_host_func_entry( + storage, + types, + &InterfaceType::Tuple(func_ty.params), + store.0.store_opaque_mut(), + )?; + // Replay host function path: Just lower the results from the trace + #[cfg(feature = "rr-component")] + { + let mut cx = LowerContext::new(store, options, instance); // Skip lifting/lowering logic, and just replaying the lowering state - #[cfg(feature = "rr-component")] - { - let mut cx = LowerContext::new(store, options, instance); + if async_ { + #[cfg(feature = "component-model-async")] + cx.replay_lowering(Some(&mut storage[..1]), ReplayLoweringPhase::HostFuncReturn)?; + #[cfg(not(feature = "component-model-async"))] + unreachable!( + "async-lowered imports should have failed validation \ + when `component-model-async` feature disabled" + ); + } else { + let ret_index = unsafe { dynamic_params_load_replay(param_tys, max_flat) }; // Copy the entire contiguous storage slice instead of looping if let Some(_cnt) = result_tys.abi.flat_count(MAX_FLAT_RESULTS) { cx.replay_lowering(Some(storage), ReplayLoweringPhase::HostFuncReturn)?; @@ -962,6 +966,10 @@ where } } } + #[cfg(not(feature = "rr-component"))] + { + unreachable!("cannot reach host function replay when `rr-component` is disabled"); + } } Ok(()) @@ -1007,6 +1015,16 @@ unsafe fn dynamic_params_load( } } +/// Replay of return values from `dynamic_params_load`. Keep in sync +#[cfg(feature = "rr-component")] +unsafe fn dynamic_params_load_replay(param_tys: &TypeTuple, max_flat_params: usize) -> usize { + if let Some(param_count) = param_tys.abi.flat_count(max_flat_params) { + param_count + } else { + 1 + } +} + pub(crate) fn validate_inbounds_dynamic( abi: &CanonicalAbiInfo, memory: &[u8], diff --git a/crates/wasmtime/src/runtime/rr/core.rs b/crates/wasmtime/src/runtime/rr/core.rs index db86f2e8c5..0b2a22cf49 100644 --- a/crates/wasmtime/src/runtime/rr/core.rs +++ b/crates/wasmtime/src/runtime/rr/core.rs @@ -509,17 +509,17 @@ impl Iterator for ReplayBuffer { impl Drop for ReplayBuffer { fn drop(&mut self) { - let mut remaining_events = 0; - log::info!("Replay buffer is being dropped; checking for remaining replay events..."); + let mut remaining = false; + log::debug!("Replay buffer is being dropped; checking for remaining replay events..."); // Cannot use count() in iterator because IO error may loop indefinitely while let Some(e) = self.next() { e.unwrap(); - remaining_events += 1; + remaining = true; + break; } - if remaining_events > 0 { + if remaining { log::warn!( - "{} events were not used in the replay buffer. This is likely the result of an erroneous/incomplete execution", - remaining_events + "Some events were not used in the replay buffer. This is likely the result of an erroneous/incomplete execution", ); } else { log::debug!("All replay events were successfully processed."); diff --git a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs index d5b5b53975..5cc26ccc66 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs @@ -57,6 +57,9 @@ where let result = wasm_call(store); #[cfg(feature = "rr-component")] { + if let Err(e) = &result { + log::warn!("Wasm function call exited with error: {:?}", e); + } let flat_results = types.flat_types_storage_or_pointer( &InterfaceType::Tuple(types[type_idx].results), MAX_FLAT_RESULTS, diff --git a/crates/wasmtime/src/runtime/rr/replay_driver.rs b/crates/wasmtime/src/runtime/rr/replay_driver.rs index 85ebafe44f..bce47a8dd0 100644 --- a/crates/wasmtime/src/runtime/rr/replay_driver.rs +++ b/crates/wasmtime/src/runtime/rr/replay_driver.rs @@ -328,7 +328,7 @@ impl ReplayInstance { } _ => { - log::error!("Unexpected non-top-level RR event: {:?}", rr_event); + log::error!("Unexpected top-level RR event: {:?}", rr_event); Err(ReplayError::IncorrectEventVariant)? } } @@ -386,7 +386,7 @@ impl ReplayInstance { // Replay lowering steps and obtain raw value arguments to raw function call let func = component::Func::from_lifted_func(*instance, event.func_idx); - let store = self.store.as_context_mut(); + let mut store = self.store.as_context_mut(); // Call the function // @@ -394,28 +394,40 @@ impl ReplayInstance { let mut results_storage = [component::Val::U64(0); MAX_FLAT_RESULTS]; let mut num_results = 0; let results = &mut results_storage; - let _return = unsafe { - async { - func.call_raw( - store, - |cx, _, dst: &mut MaybeUninit<[MaybeUninit; MAX_FLAT_PARAMS]>| { - // For lowering, use replay instead of actual lowering - let dst: &mut [MaybeUninit] = dst.assume_init_mut(); - cx.replay_lowering(Some(dst), component_hooks::ReplayLoweringPhase::WasmFuncEntry) - }, - |cx, results_ty, src: &[ValRaw; MAX_FLAT_RESULTS]| { - // Lifting can proceed exactly as normal - for (result, slot) in - component::Func::lift_results(cx, results_ty, src, MAX_FLAT_RESULTS)?.zip(results) - { - *slot = result?; - num_results += 1; - } - Ok(()) - }, + let _return = store + .on_fiber(|store| unsafe { + func.call_raw( + store.as_context_mut(), + |cx, + _, + dst: &mut MaybeUninit< + [MaybeUninit; MAX_FLAT_PARAMS], + >| { + // For lowering, use replay instead of actual lowering + let dst: &mut [MaybeUninit] = dst.assume_init_mut(); + cx.replay_lowering( + Some(dst), + component_hooks::ReplayLoweringPhase::WasmFuncEntry, + ) + }, + |cx, results_ty, src: &[ValRaw; MAX_FLAT_RESULTS]| { + // Lifting can proceed exactly as normal + for (result, slot) in component::Func::lift_results( + cx, + results_ty, + src, + MAX_FLAT_RESULTS, + )? + .zip(results) + { + *slot = result?; + num_results += 1; + } + Ok(()) + }, ) - }.await?; - }; + }) + .await??; log::info!( "Returned {:?} for calling {:?}", @@ -476,20 +488,16 @@ impl ReplayInstance { // // This is almost a mirror of the usage in [`crate::Func::call_impl`] func.call_impl_check_args(&mut store, ¶ms, &mut results)?; - unsafe { - async { - func.call_impl_do_call( - &mut store, - params.as_slice(), - results.as_mut_slice(), - ) - } - .await?; - } + store + .on_fiber(|store| unsafe { + let mut ctx = store.as_context_mut(); + func.call_impl_do_call(&mut ctx, params.as_slice(), results.as_mut_slice()) + }) + .await??; } _ => { - log::error!("Unexpected non-top-level RR event: {:?}", rr_event); + log::error!("Unexpected top-level RR event: {:?}", rr_event); Err(ReplayError::IncorrectEventVariant)? } } diff --git a/src/commands/replay.rs b/src/commands/replay.rs index 1603a299e3..ebbf4cfb97 100644 --- a/src/commands/replay.rs +++ b/src/commands/replay.rs @@ -108,15 +108,6 @@ impl ReplayCommand { .timeout .unwrap_or(std::time::Duration::MAX); - //let result: Result, Elapsed> = - // tokio::time::timeout(dur, async { - // let res = replay_instance.run_to_completion_async().await; - // match res { - // Ok(_) => Ok(OkReplayT(replay_instance.extract_store())), - // Err(e) => Err(ErrReplayT(e, replay_instance.extract_store())), - // } - // }) - // .await; let result: Result, Elapsed> = tokio::time::timeout(dur, async { replay_instance.run_to_completion_async().await }) From 875a4b1f73a7bbf9f56a638e438aa8539470d27d Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Thu, 20 Nov 2025 17:59:43 -0500 Subject: [PATCH 44/73] Add support for concurrent call recording --- .../src/runtime/component/concurrent.rs | 38 +++++++++- crates/wasmtime/src/runtime/component/func.rs | 27 +++---- crates/wasmtime/src/runtime/func.rs | 74 ++++++++++++++++--- crates/wasmtime/src/runtime/func/typed.rs | 2 + crates/wasmtime/src/runtime/rr.rs | 2 +- crates/wasmtime/src/runtime/rr/hooks.rs | 23 ++++++ .../src/runtime/rr/hooks/component_hooks.rs | 21 +++++- 7 files changed, 155 insertions(+), 32 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index f1420c24e3..0575712888 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -53,6 +53,7 @@ use crate::component::func::{self, Func}; use crate::component::{HasData, HasSelf, Instance, Resource, ResourceTable, ResourceTableError}; use crate::fiber::{self, StoreFiber, StoreFiberYield}; +use crate::rr::{RRWasmFuncType, component_hooks}; use crate::store::{Store, StoreId, StoreInner, StoreOpaque, StoreToken}; use crate::vm::component::{CallContext, ComponentInstance, InstanceFlags, ResourceTables}; use crate::vm::{AlwaysMut, SendSyncPtr, VMFuncRef, VMMemoryDefinition, VMStore}; @@ -1715,7 +1716,8 @@ impl Instance { /// /// SAFETY: The raw pointer arguments must be valid references to guest /// functions (with the appropriate signatures) when the closures queued by - /// this function are called. + /// this function are called. For RR, the rr_handle must be a valid `Func` + /// corresponding to `callee` if provided unsafe fn queue_call( self, mut store: StoreContextMut, @@ -1727,6 +1729,7 @@ impl Instance { async_: bool, callback: Option>, post_return: Option>, + rr_handle: Option, ) -> Result<()> { /// Return a closure which will call the specified function in the scope /// of the specified task. @@ -1741,7 +1744,8 @@ impl Instance { /// nothing. /// /// SAFETY: `callee` must be a valid `*mut VMFuncRef` at the time when - /// the returned closure is called. + /// the returned closure is called. For RR, the handle must be a valid `Func` + /// corresponding to `callee` if provided unsafe fn make_call( store: StoreContextMut, guest_thread: QualifiedThreadId, @@ -1749,6 +1753,7 @@ impl Instance { param_count: usize, result_count: usize, flags: Option, + rr_handle: Option, ) -> impl FnOnce(&mut dyn VMStore) -> Result<[MaybeUninit; MAX_FLAT_PARAMS]> + Send + Sync @@ -1766,6 +1771,14 @@ impl Instance { let may_enter_after_call = task.call_post_return_automatically(); let lower = task.lower_params.take().unwrap(); + if let Some(func) = rr_handle { + component_hooks::record_wasm_func_begin( + func.instance().id().instance(), + func.index(), + store.store_opaque_mut(), + )?; + } + lower(store, &mut storage[..param_count])?; let mut store = token.as_context_mut(store); @@ -1776,7 +1789,21 @@ impl Instance { if let Some(mut flags) = flags { flags.set_may_enter(false); } - crate::Func::call_unchecked_raw( + + let rr_type = if let Some(func) = rr_handle { + let type_idx = func.abi_info(store.0).2; + let types = func + .instance() + .id() + .get(store.0) + .component() + .types() + .clone(); + RRWasmFuncType::Component { type_idx, types } + } else { + RRWasmFuncType::None + }; + crate::Func::call_unchecked_raw_with_rr( &mut store, callee.as_non_null(), NonNull::new( @@ -1784,7 +1811,9 @@ impl Instance { as *mut [MaybeUninit] as _, ) .unwrap(), + rr_type, )?; + if let Some(mut flags) = flags { flags.set_may_enter(may_enter_after_call); } @@ -1805,6 +1834,7 @@ impl Instance { param_count, result_count, flags, + rr_handle, ) }; @@ -2330,6 +2360,7 @@ impl Instance { (flags & START_FLAG_ASYNC_CALLEE) != 0, NonNull::new(callback).map(SendSyncPtr::new), NonNull::new(post_return).map(SendSyncPtr::new), + None, )?; } @@ -5047,6 +5078,7 @@ fn queue_call0( is_concurrent, callback, post_return.map(SendSyncPtr::new), + Some(handle), ) } } diff --git a/crates/wasmtime/src/runtime/component/func.rs b/crates/wasmtime/src/runtime/component/func.rs index 41cc56b1e2..2ff7e8c5da 100644 --- a/crates/wasmtime/src/runtime/component/func.rs +++ b/crates/wasmtime/src/runtime/component/func.rs @@ -4,7 +4,7 @@ use crate::component::storage::storage_as_slice; use crate::component::types::ComponentFunc; use crate::component::values::Val; use crate::prelude::*; -use crate::rr::component_hooks; +use crate::rr::{RRWasmFuncType, component_hooks}; use crate::runtime::vm::component::{ComponentInstance, InstanceFlags, ResourceTables}; use crate::runtime::vm::{Export, VMFuncRef}; use crate::store::StoreOpaque; @@ -581,16 +581,12 @@ impl Func { LowerParams: Copy, LowerReturn: Copy, { - #[cfg(feature = "rr-component")] - { - use crate::rr::component_events::WasmFuncBeginEvent; + component_hooks::record_wasm_func_begin( + self.instance.id().instance(), + self.index, + store.0, + )?; - let instance = self.instance.id().instance(); - let func_idx = self.index; - store - .0 - .record_event(|| WasmFuncBeginEvent { instance, func_idx })?; - } let export = self.lifted_core_func(store.0); #[repr(C)] @@ -635,12 +631,13 @@ impl Func { )) .unwrap(); - component_hooks::record_and_replay_validate_wasm_func( - |store| crate::Func::call_unchecked_raw(store, export, params_and_returns), - params_and_returns.as_ref(), - self.abi_info(store.0).2, - self.instance.id().get(store.0).component().types().clone(), + let type_idx = self.abi_info(store.0).2; + let types = self.instance.id().get(store.0).component().types().clone(); + crate::Func::call_unchecked_raw_with_rr( &mut store, + export, + params_and_returns, + RRWasmFuncType::Component { type_idx, types }, )?; } diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 079654e6e4..bc298f6af1 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; +use crate::rr::{self, RRWasmFuncType}; use crate::runtime::Uninhabited; use crate::runtime::vm::{ self, InterpreterRef, SendSyncPtr, StoreBox, VMArrayCallHostFuncContext, @@ -1043,17 +1043,69 @@ impl Func { let func_ref = self.vm_func_ref(store.0); let params_and_returns = NonNull::new(params_and_returns).unwrap_or(NonNull::from(&mut [])); - rr::core_hooks::record_and_replay_validate_wasm_func( - |mut store| { - // SAFETY: the safety of this function call is the same as the contract - // of this function. - unsafe { Self::call_unchecked_raw(&mut store, func_ref, params_and_returns) } + unsafe { + let ty = &self.ty(&store); + let origin = self.origin.expand(); + Self::call_unchecked_raw_with_rr( + &mut store, + func_ref, + params_and_returns, + RRWasmFuncType::Core { ty, origin }, + ) + } + } + + /// Same as [`Func::call_unchecked_raw`] but enables recording and replaying + /// hooks for func entry/exit based on whether the intended call was from a component or + /// core wasm module. + /// + /// This method is essentially a wrapper over [`Func::call_unchecked_raw`] and operates exactly like it + /// when recording/replay is not enabled, so use this for all calls that will potentially need + /// to be recorded/replayed. + /// + /// `RRWasmFuncType::None` can be used to disable recording/replaying and passthrough to `call_unchecked_raw`. + /// Eventually we can replace all occurences of `call_unchecked_raw` with this method with `RRWasmFuncType::None`` + pub(crate) unsafe fn call_unchecked_raw_with_rr( + mut store: &mut StoreContextMut<'_, T>, + func_ref: NonNull, + params_and_returns: NonNull<[ValRaw]>, + rr: RRWasmFuncType, + ) -> Result<()> { + // SAFETY: the safety of this function call is the same as the contract + // of this function. + match rr { + RRWasmFuncType::Core { ty, origin } => { + rr::core_hooks::record_and_replay_validate_wasm_func( + |mut store| { + // SAFETY: the safety of this function call is the same as the contract + // of this function. + unsafe { + Self::call_unchecked_raw(&mut store, func_ref, params_and_returns) + } + }, + unsafe { params_and_returns.as_ref() }, + ty, + origin, + &mut store, + ) + } + #[cfg(feature = "component-model")] + RRWasmFuncType::Component { type_idx, types } => { + rr::component_hooks::record_and_replay_validate_wasm_func( + |mut store| unsafe { + Self::call_unchecked_raw(&mut store, func_ref, params_and_returns) + }, + unsafe { params_and_returns.as_ref() }, + type_idx, + types, + &mut store, + ) + } + // Passthrough + RRWasmFuncType::None => unsafe { + Self::call_unchecked_raw(&mut store, func_ref, params_and_returns) }, - unsafe { params_and_returns.as_ref() }, - &self.ty(&store), - self.origin.expand(), - &mut store, - ) + } } pub(crate) unsafe fn call_unchecked_raw( diff --git a/crates/wasmtime/src/runtime/func/typed.rs b/crates/wasmtime/src/runtime/func/typed.rs index ddd56655de..86acd4fe7f 100644 --- a/crates/wasmtime/src/runtime/func/typed.rs +++ b/crates/wasmtime/src/runtime/func/typed.rs @@ -211,6 +211,8 @@ where // For component mode, Realloc uses this method `TypedFunc::call_raw`, but Realloc is its // own separate event for record/replay purposes. For now, we use the should_record flag to // distinguish but this could be removed in the future by folding it into the main function call event. + // + // Note: This can't use [`crate::Func::call_unchecked_raw_with_rr`] directly because of the closure capture rr::core_hooks::record_and_replay_validate_wasm_func( |store| { invoke_wasm_and_catch_traps(store, |caller, vm| { diff --git a/crates/wasmtime/src/runtime/rr.rs b/crates/wasmtime/src/runtime/rr.rs index 2b4196c0d0..a6dabe193c 100644 --- a/crates/wasmtime/src/runtime/rr.rs +++ b/crates/wasmtime/src/runtime/rr.rs @@ -43,7 +43,7 @@ impl FlatBytes for MaybeUninit { /// Convenience method hooks for injecting event recording/replaying in the rest of the engine mod hooks; -pub(crate) use hooks::core_hooks; +pub(crate) use hooks::{RRWasmFuncType, core_hooks}; #[cfg(feature = "component-model")] pub(crate) use hooks::{ component_hooks, component_hooks::ConstMemorySliceCell, component_hooks::MemorySliceCell, diff --git a/crates/wasmtime/src/runtime/rr/hooks.rs b/crates/wasmtime/src/runtime/rr/hooks.rs index a126791dea..996026c2ae 100644 --- a/crates/wasmtime/src/runtime/rr/hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks.rs @@ -3,3 +3,26 @@ pub mod component_hooks; /// Core RR hooks pub mod core_hooks; + +use crate::{FuncType, WasmFuncOrigin}; +#[cfg(feature = "component-model")] +use alloc::sync::Arc; +#[cfg(feature = "component-model")] +use wasmtime_environ::component::{ComponentTypes, TypeFuncIndex}; + +/// Wasm function type information for RR hooks +pub enum RRWasmFuncType<'a> { + /// No RR hooks to be performed + None, + /// Core RR hooks to be performed + Core { + ty: &'a FuncType, + origin: Option, + }, + /// Component RR hooks to be performed + #[cfg(feature = "component-model")] + Component { + type_idx: TypeFuncIndex, + types: Arc, + }, +} diff --git a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs index 5cc26ccc66..192e69e67d 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs @@ -1,4 +1,5 @@ use crate::ValRaw; +use crate::component::ComponentInstanceId; #[cfg(feature = "rr-component")] use crate::rr::{RecordBuffer, Recorder, component_events::MemorySliceWriteEvent}; @@ -10,7 +11,7 @@ use crate::rr::common_events::{HostFuncEntryEvent, WasmFuncReturnEvent}; #[cfg(feature = "rr-component")] use crate::rr::component_events::{ LowerFlatEntryEvent, LowerFlatReturnEvent, LowerMemoryEntryEvent, LowerMemoryReturnEvent, - WasmFuncEntryEvent, + WasmFuncBeginEvent, WasmFuncEntryEvent, }; #[cfg(feature = "rr-component")] use crate::rr::{RRFuncArgVals, ResultEvent, common_events::HostFuncReturnEvent}; @@ -18,7 +19,7 @@ use crate::store::StoreOpaque; use crate::{StoreContextMut, prelude::*}; use alloc::sync::Arc; use core::mem::MaybeUninit; -use wasmtime_environ::component::{ComponentTypes, InterfaceType, TypeFuncIndex}; +use wasmtime_environ::component::{ComponentTypes, ExportIndex, InterfaceType, TypeFuncIndex}; #[cfg(all(feature = "rr-component"))] use wasmtime_environ::component::{MAX_FLAT_PARAMS, MAX_FLAT_RESULTS}; @@ -30,6 +31,22 @@ pub enum ReplayLoweringPhase { HostFuncReturn, } +/// Record hook for initiating wasm component function call +/// +/// This differs from WasmFuncEntryEvent since this is pre-lowering, and +/// WasmFuncEntryEvent is post-lowering +#[inline] +pub fn record_wasm_func_begin( + instance: ComponentInstanceId, + func_idx: ExportIndex, + store: &mut StoreOpaque, +) -> Result<()> { + #[cfg(feature = "rr-component")] + store.record_event(|| WasmFuncBeginEvent { instance, func_idx })?; + let _ = (instance, func_idx, store); + Ok(()) +} + /// Record hook wrapping a wasm component export function invocation and replay /// validation of return value #[inline] From 4b32230bf71b6c4e2444240c6b58f82cc928fcc0 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Thu, 20 Nov 2025 18:26:19 -0500 Subject: [PATCH 45/73] Fix warnings and replace call_unchecked_raw to use RR arg --- .../src/runtime/component/concurrent.rs | 8 +++++- crates/wasmtime/src/runtime/component/func.rs | 3 ++- .../src/runtime/component/resources/any.rs | 10 +++++++- crates/wasmtime/src/runtime/func.rs | 25 ++++++++----------- crates/wasmtime/src/runtime/rr/hooks.rs | 5 ++-- 5 files changed, 31 insertions(+), 20 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 0575712888..5ccaa6caa3 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -1803,7 +1803,7 @@ impl Instance { } else { RRWasmFuncType::None }; - crate::Func::call_unchecked_raw_with_rr( + crate::Func::call_unchecked_raw( &mut store, callee.as_non_null(), NonNull::new( @@ -1992,6 +1992,7 @@ impl Instance { &mut store, func.as_non_null(), slice::from_ref(&post_return_arg).into(), + RRWasmFuncType::None, )?; } } @@ -2137,6 +2138,7 @@ impl Instance { // for details) we know it takes count parameters and returns // `dst.len()` results. unsafe { + // No RR on guest->guest calls crate::Func::call_unchecked_raw( &mut store, start.as_non_null(), @@ -2144,6 +2146,7 @@ impl Instance { &mut src[..count.max(dst.len())] as *mut [MaybeUninit] as _, ) .unwrap(), + RRWasmFuncType::None, )?; } dst.copy_from_slice(&src[..dst.len()]); @@ -2172,10 +2175,12 @@ impl Instance { // for details) we know it takes `src.len()` parameters and // returns up to 1 result. unsafe { + // No RR on guest->guest calls crate::Func::call_unchecked_raw( &mut store, return_.as_non_null(), my_src.as_mut_slice().into(), + RRWasmFuncType::None, )?; } let state = store.0.concurrent_state_mut(); @@ -2265,6 +2270,7 @@ impl Instance { &mut store, function.as_non_null(), params.as_mut_slice().into(), + RRWasmFuncType::None, )?; flags.set_may_enter(may_enter_after_call); } diff --git a/crates/wasmtime/src/runtime/component/func.rs b/crates/wasmtime/src/runtime/component/func.rs index 2ff7e8c5da..e7f5fa2ba2 100644 --- a/crates/wasmtime/src/runtime/component/func.rs +++ b/crates/wasmtime/src/runtime/component/func.rs @@ -633,7 +633,7 @@ impl Func { let type_idx = self.abi_info(store.0).2; let types = self.instance.id().get(store.0).component().types().clone(); - crate::Func::call_unchecked_raw_with_rr( + crate::Func::call_unchecked_raw( &mut store, export, params_and_returns, @@ -791,6 +791,7 @@ impl Func { func, NonNull::new(core::ptr::slice_from_raw_parts(&post_return_arg, 1).cast_mut()) .unwrap(), + RRWasmFuncType::None, )?; } diff --git a/crates/wasmtime/src/runtime/component/resources/any.rs b/crates/wasmtime/src/runtime/component/resources/any.rs index 279038f383..1e6d618c99 100644 --- a/crates/wasmtime/src/runtime/component/resources/any.rs +++ b/crates/wasmtime/src/runtime/component/resources/any.rs @@ -211,7 +211,15 @@ impl ResourceAny { // destructors have al been previously type-checked and are guaranteed // to take one i32 argument and return no results, so the parameters // here should be configured correctly. - unsafe { crate::Func::call_unchecked_raw(store, dtor, NonNull::from(&mut args)) } + unsafe { + // No recording since builtins are recorded + crate::Func::call_unchecked_raw( + store, + dtor, + NonNull::from(&mut args), + crate::rr::RRWasmFuncType::None, + ) + } } fn lower_to_index(&self, cx: &mut LowerContext<'_, U>, ty: InterfaceType) -> Result { diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index bc298f6af1..c48f3e3886 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -1046,7 +1046,7 @@ impl Func { unsafe { let ty = &self.ty(&store); let origin = self.origin.expand(); - Self::call_unchecked_raw_with_rr( + Self::call_unchecked_raw( &mut store, func_ref, params_and_returns, @@ -1055,17 +1055,11 @@ impl Func { } } - /// Same as [`Func::call_unchecked_raw`] but enables recording and replaying - /// hooks for func entry/exit based on whether the intended call was from a component or - /// core wasm module. + /// Raw, unchecked call to the underlying func_ref. /// - /// This method is essentially a wrapper over [`Func::call_unchecked_raw`] and operates exactly like it - /// when recording/replay is not enabled, so use this for all calls that will potentially need - /// to be recorded/replayed. - /// - /// `RRWasmFuncType::None` can be used to disable recording/replaying and passthrough to `call_unchecked_raw`. - /// Eventually we can replace all occurences of `call_unchecked_raw` with this method with `RRWasmFuncType::None`` - pub(crate) unsafe fn call_unchecked_raw_with_rr( + /// This method contains record/replay hooks for either component or core wasm source that + /// invoked it based on `rr`. `RRWasmFuncType::None` can be used to disable record/replay for the call + pub(crate) unsafe fn call_unchecked_raw( mut store: &mut StoreContextMut<'_, T>, func_ref: NonNull, params_and_returns: NonNull<[ValRaw]>, @@ -1080,7 +1074,7 @@ impl Func { // SAFETY: the safety of this function call is the same as the contract // of this function. unsafe { - Self::call_unchecked_raw(&mut store, func_ref, params_and_returns) + Self::call_unchecked_raw_inner(&mut store, func_ref, params_and_returns) } }, unsafe { params_and_returns.as_ref() }, @@ -1093,7 +1087,7 @@ impl Func { RRWasmFuncType::Component { type_idx, types } => { rr::component_hooks::record_and_replay_validate_wasm_func( |mut store| unsafe { - Self::call_unchecked_raw(&mut store, func_ref, params_and_returns) + Self::call_unchecked_raw_inner(&mut store, func_ref, params_and_returns) }, unsafe { params_and_returns.as_ref() }, type_idx, @@ -1102,13 +1096,14 @@ impl Func { ) } // Passthrough + #[cfg(feature = "component-model")] RRWasmFuncType::None => unsafe { - Self::call_unchecked_raw(&mut store, func_ref, params_and_returns) + Self::call_unchecked_raw_inner(&mut store, func_ref, params_and_returns) }, } } - pub(crate) unsafe fn call_unchecked_raw( + unsafe fn call_unchecked_raw_inner( store: &mut StoreContextMut<'_, T>, func_ref: NonNull, params_and_returns: NonNull<[ValRaw]>, diff --git a/crates/wasmtime/src/runtime/rr/hooks.rs b/crates/wasmtime/src/runtime/rr/hooks.rs index 996026c2ae..0f3ae22712 100644 --- a/crates/wasmtime/src/runtime/rr/hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks.rs @@ -12,8 +12,6 @@ use wasmtime_environ::component::{ComponentTypes, TypeFuncIndex}; /// Wasm function type information for RR hooks pub enum RRWasmFuncType<'a> { - /// No RR hooks to be performed - None, /// Core RR hooks to be performed Core { ty: &'a FuncType, @@ -25,4 +23,7 @@ pub enum RRWasmFuncType<'a> { type_idx: TypeFuncIndex, types: Arc, }, + /// No RR hooks to be performed + #[cfg(feature = "component-model")] + None, } From 5d85d48e0cac88a4a78c6c79e9fa95adcb4b9175 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Mon, 24 Nov 2025 14:01:37 -0500 Subject: [PATCH 46/73] Added post return event to rr --- crates/wasmtime/src/runtime/component/func.rs | 7 ++- crates/wasmtime/src/runtime/rr/core.rs | 2 + .../rr/core/events/component_events.rs | 9 ++++ .../src/runtime/rr/hooks/component_hooks.rs | 17 ++++++- .../wasmtime/src/runtime/rr/replay_driver.rs | 44 +++++++++++++++++++ 5 files changed, 77 insertions(+), 2 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/func.rs b/crates/wasmtime/src/runtime/component/func.rs index e7f5fa2ba2..0bb8b15da7 100644 --- a/crates/wasmtime/src/runtime/component/func.rs +++ b/crates/wasmtime/src/runtime/component/func.rs @@ -704,7 +704,12 @@ impl Func { /// This only works with functions defined within a synchronous store. #[inline] pub fn post_return(&self, mut store: impl AsContextMut) -> Result<()> { - let store = store.as_context_mut(); + let mut store = store.as_context_mut(); + component_hooks::record_wasm_func_post_return( + self.instance.id().instance(), + self.index, + &mut store, + )?; assert!( !store.0.async_support(), "must use `post_return_async` when async support is enabled on the config" diff --git a/crates/wasmtime/src/runtime/rr/core.rs b/crates/wasmtime/src/runtime/rr/core.rs index 0b2a22cf49..d5ebddaef4 100644 --- a/crates/wasmtime/src/runtime/rr/core.rs +++ b/crates/wasmtime/src/runtime/rr/core.rs @@ -156,6 +156,8 @@ rr_event! { ComponentMemorySliceWrite(__component_events::MemorySliceWriteEvent), /// Return from a component builtin ComponentBuiltinReturn(__component_events::BuiltinReturnEvent), + /// Call to `post_return` (after the function call) + ComponentPostReturn(__component_events::PostReturnEvent), // OPTIONAL events for replay validation (Component) diff --git a/crates/wasmtime/src/runtime/rr/core/events/component_events.rs b/crates/wasmtime/src/runtime/rr/core/events/component_events.rs index 77bd50623e..4c1d4551e1 100644 --- a/crates/wasmtime/src/runtime/rr/core/events/component_events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events/component_events.rs @@ -26,6 +26,15 @@ pub struct InstantiationEvent { pub instance: ComponentInstanceId, } +/// A call to `post_return` (after the function call) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PostReturnEvent { + /// Instance ID for the component instance + pub instance: ComponentInstanceId, + /// Export index for the function on which post_return is invoked + pub func_idx: ExportIndex, +} + /// A call event from Host into a Wasm component function #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WasmFuncEntryEvent { diff --git a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs index 192e69e67d..1501329d2b 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs @@ -11,7 +11,7 @@ use crate::rr::common_events::{HostFuncEntryEvent, WasmFuncReturnEvent}; #[cfg(feature = "rr-component")] use crate::rr::component_events::{ LowerFlatEntryEvent, LowerFlatReturnEvent, LowerMemoryEntryEvent, LowerMemoryReturnEvent, - WasmFuncBeginEvent, WasmFuncEntryEvent, + PostReturnEvent, WasmFuncBeginEvent, WasmFuncEntryEvent, }; #[cfg(feature = "rr-component")] use crate::rr::{RRFuncArgVals, ResultEvent, common_events::HostFuncReturnEvent}; @@ -47,6 +47,21 @@ pub fn record_wasm_func_begin( Ok(()) } +/// Record hook for wasm component function post_return call +#[inline] +pub fn record_wasm_func_post_return( + instance: ComponentInstanceId, + func_idx: ExportIndex, + store: &mut StoreContextMut<'_, T>, +) -> Result<()> { + #[cfg(feature = "rr-component")] + store + .0 + .record_event(|| PostReturnEvent { instance, func_idx })?; + let _ = (instance, func_idx, store); + Ok(()) +} + /// Record hook wrapping a wasm component export function invocation and replay /// validation of return value #[inline] diff --git a/crates/wasmtime/src/runtime/rr/replay_driver.rs b/crates/wasmtime/src/runtime/rr/replay_driver.rs index bce47a8dd0..bd8c5e019f 100644 --- a/crates/wasmtime/src/runtime/rr/replay_driver.rs +++ b/crates/wasmtime/src/runtime/rr/replay_driver.rs @@ -277,6 +277,28 @@ impl ReplayInstance { ); } } + RREvent::ComponentPostReturn(event) => { + #[cfg(feature = "rr-component")] + { + // Grab the correct component instance + let key = event.instance; + let instance = self + .component_instances + .get_mut(&key) + .ok_or(ReplayError::MissingComponentInstance(key.as_u32()))?; + + let func = component::Func::from_lifted_func(*instance, event.func_idx); + let mut store = self.store.as_context_mut(); + + func.post_return(&mut store)?; + } + #[cfg(not(feature = "rr-component"))] + { + bail!( + "Cannot parse ComponentPostReturn replay event without rr-component feature enabled" + ); + } + } RREvent::CoreWasmInstantiation(event) => { // Find matching module from environment to instantiate let module = self @@ -442,6 +464,28 @@ impl ReplayInstance { ); } } + RREvent::ComponentPostReturn(event) => { + #[cfg(feature = "rr-component")] + { + // Grab the correct component instance + let key = event.instance; + let instance = self + .component_instances + .get_mut(&key) + .ok_or(ReplayError::MissingComponentInstance(key.as_u32()))?; + + let func = component::Func::from_lifted_func(*instance, event.func_idx); + let mut store = self.store.as_context_mut(); + + func.post_return_async(&mut store).await?; + } + #[cfg(not(feature = "rr-component"))] + { + bail!( + "Cannot parse ComponentPostReturn replay event without rr-component feature enabled" + ); + } + } RREvent::CoreWasmInstantiation(event) => { // Find matching module from environment to instantiate let module = self From 6fc37bd98fa829393941a0770552b5f93c2fce63 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Mon, 24 Nov 2025 14:25:24 -0500 Subject: [PATCH 47/73] Fix: warnings for replay driver --- crates/wasmtime/src/runtime/rr/replay_driver.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/wasmtime/src/runtime/rr/replay_driver.rs b/crates/wasmtime/src/runtime/rr/replay_driver.rs index bd8c5e019f..08e6bd5abf 100644 --- a/crates/wasmtime/src/runtime/rr/replay_driver.rs +++ b/crates/wasmtime/src/runtime/rr/replay_driver.rs @@ -193,7 +193,6 @@ impl ReplayInstance { pub fn run_single_top_level_event(&mut self, rr_event: RREvent) -> Result<()> { match rr_event { RREvent::ComponentInstantiation(event) => { - let _ = event; #[cfg(feature = "rr-component")] { // Find matching component from environment to instantiate @@ -217,13 +216,13 @@ impl ReplayInstance { } #[cfg(not(feature = "rr-component"))] { + let _ = event; bail!( "Cannot parse ComponentInstantation replay event without rr-component feature enabled" ); } } RREvent::ComponentWasmFuncBegin(event) => { - let _ = event; #[cfg(feature = "rr-component")] { // Grab the correct component instance @@ -272,6 +271,7 @@ impl ReplayInstance { } #[cfg(not(feature = "rr-component"))] { + let _ = event; bail!( "Cannot parse ComponentWasmFuncBegin replay event without rr-component feature enabled" ); @@ -294,6 +294,7 @@ impl ReplayInstance { } #[cfg(not(feature = "rr-component"))] { + let _ = event; bail!( "Cannot parse ComponentPostReturn replay event without rr-component feature enabled" ); @@ -365,7 +366,6 @@ impl ReplayInstance { { match rr_event { RREvent::ComponentInstantiation(event) => { - let _ = event; #[cfg(feature = "rr-component")] { // Find matching component from environment to instantiate @@ -390,13 +390,13 @@ impl ReplayInstance { } #[cfg(not(feature = "rr-component"))] { + let _ = event; bail!( "Cannot parse ComponentInstantation replay event without rr-component feature enabled" ); } } RREvent::ComponentWasmFuncBegin(event) => { - let _ = event; #[cfg(feature = "rr-component")] { // Grab the correct component instance @@ -459,6 +459,7 @@ impl ReplayInstance { } #[cfg(not(feature = "rr-component"))] { + let _ = event; bail!( "Cannot parse ComponentWasmFuncBegin replay event without rr-component feature enabled" ); @@ -481,6 +482,7 @@ impl ReplayInstance { } #[cfg(not(feature = "rr-component"))] { + let _ = event; bail!( "Cannot parse ComponentPostReturn replay event without rr-component feature enabled" ); From 5d710efcc38477095507555c0986d1eb0658d169 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Mon, 24 Nov 2025 17:02:42 -0500 Subject: [PATCH 48/73] Add tests for all of rr; fix post-return not recording on async --- crates/wasmtime/src/runtime/component/func.rs | 12 +- tests/all/main.rs | 2 + tests/all/rr.rs | 1406 +++++++++++++++++ 3 files changed, 1414 insertions(+), 6 deletions(-) create mode 100644 tests/all/rr.rs diff --git a/crates/wasmtime/src/runtime/component/func.rs b/crates/wasmtime/src/runtime/component/func.rs index 0bb8b15da7..d7eac7e667 100644 --- a/crates/wasmtime/src/runtime/component/func.rs +++ b/crates/wasmtime/src/runtime/component/func.rs @@ -704,12 +704,7 @@ impl Func { /// This only works with functions defined within a synchronous store. #[inline] pub fn post_return(&self, mut store: impl AsContextMut) -> Result<()> { - let mut store = store.as_context_mut(); - component_hooks::record_wasm_func_post_return( - self.instance.id().instance(), - self.index, - &mut store, - )?; + let store = store.as_context_mut(); assert!( !store.0.async_support(), "must use `post_return_async` when async support is enabled on the config" @@ -738,6 +733,11 @@ impl Func { fn post_return_impl(&self, mut store: impl AsContextMut) -> Result<()> { let mut store = store.as_context_mut(); + component_hooks::record_wasm_func_post_return( + self.instance.id().instance(), + self.index, + &mut store, + )?; let index = self.index; let vminstance = self.instance.id().get(store.0); diff --git a/tests/all/main.rs b/tests/all/main.rs index 820dbc6ba8..b09e4a665a 100644 --- a/tests/all/main.rs +++ b/tests/all/main.rs @@ -44,6 +44,8 @@ mod piped_tests; mod pooling_allocator; mod pulley; mod relocs; +#[cfg(feature = "rr")] +mod rr; mod stack_creator; mod stack_overflow; mod store; diff --git a/tests/all/rr.rs b/tests/all/rr.rs new file mode 100644 index 0000000000..7ab950ab9a --- /dev/null +++ b/tests/all/rr.rs @@ -0,0 +1,1406 @@ +use anyhow::Result; +use std::future::Future; +use std::io::Cursor; +use std::pin::Pin; +use wasmtime::{ + Config, Engine, Linker, Module, OptLevel, RRConfig, RecordSettings, ReplayEnvironment, + ReplaySettings, Store, +}; + +#[cfg(feature = "rr-component")] +use wasmtime::component::{Component, HasSelf, Linker as ComponentLinker, bindgen}; + +struct TestState; + +impl TestState { + fn new() -> Self { + TestState + } +} + +fn init_logger() { + let _ = env_logger::try_init(); +} + +fn create_recording_engine(is_async: bool) -> Result { + let mut config = Config::new(); + config + .debug_info(true) + .cranelift_opt_level(OptLevel::None) + .rr(RRConfig::Recording); + if is_async { + config.async_support(true); + } + Engine::new(&config) +} + +fn create_replay_engine(is_async: bool) -> Result { + let mut config = Config::new(); + config + .debug_info(true) + .cranelift_opt_level(OptLevel::None) + .rr(RRConfig::Replaying); + if is_async { + config.async_support(true); + } + Engine::new(&config) +} + +/// Run a core module test with recording and replay +fn run_core_module_test(module_wat: &str, setup_linker: F, test_fn: R) -> Result<()> +where + F: Fn(&mut Linker) -> Result<()>, + R: for<'a> Fn( + &'a mut Store, + &'a wasmtime::Instance, + bool, + ) -> Pin> + Send + 'a>>, +{ + init_logger(); + + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + + // Run with in sync/async mode with/without validation + for is_async in [false, true] { + for validation in [false, true] { + let run = async { + run_core_module_test_with_validation( + module_wat, + &setup_linker, + &test_fn, + validation, + is_async, + ) + .await?; + Ok::<(), anyhow::Error>(()) + }; + + rt.block_on(run)?; + } + } + + Ok(()) +} + +async fn run_core_module_test_with_validation( + module_wat: &str, + setup_linker: &F, + test_fn: &R, + validate: bool, + is_async: bool, +) -> Result<()> +where + F: Fn(&mut Linker) -> Result<()>, + R: for<'a> Fn( + &'a mut Store, + &'a wasmtime::Instance, + bool, + ) -> Pin> + Send + 'a>>, +{ + // === RECORDING PHASE === + let engine = create_recording_engine(is_async)?; + let module = Module::new(&engine, module_wat)?; + + let mut linker = Linker::new(&engine); + setup_linker(&mut linker)?; + + let writer: Cursor> = Cursor::new(Vec::new()); + let mut store = Store::new(&engine, TestState::new()); + let record_settings = RecordSettings { + add_validation: validate, + ..Default::default() + }; + store.init_recording(writer, record_settings)?; + + let instance = if is_async { + linker.instantiate_async(&mut store, &module).await? + } else { + linker.instantiate(&mut store, &module)? + }; + + test_fn(&mut store, &instance, is_async).await?; + + // Extract the recording + let trace_box = store.into_record_writer()?; + let any_box: Box = trace_box; + let mut trace_reader = any_box.downcast::>>().unwrap(); + trace_reader.set_position(0); + + // === REPLAY PHASE === + let engine = create_replay_engine(is_async)?; + let module = Module::new(&engine, module_wat)?; + + let replay_settings = ReplaySettings { + validate, + ..Default::default() + }; + let mut renv = ReplayEnvironment::new(&engine, replay_settings); + renv.add_module(module); + + let mut replay_instance = renv.instantiate(*trace_reader)?; + if is_async { + replay_instance.run_to_completion_async().await?; + } else { + replay_instance.run_to_completion()?; + } + + Ok(()) +} + +/// Run a component test with recording and replay, testing both with and without validation +#[cfg(feature = "rr-component")] +fn run_component_test(component_wat: &str, setup_linker: F, test_fn: R) -> Result<()> +where + F: Fn(&mut ComponentLinker) -> Result<()> + Clone, + R: for<'a> Fn( + &'a mut Store, + &'a wasmtime::component::Instance, + bool, + ) -> Pin> + Send + 'a>> + + Clone, +{ + init_logger(); + + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + + // Run with in sync/async mode with/without validation + for is_async in [false, true] { + for validation in [false, true] { + let run = async { + run_component_test_with_validation( + component_wat, + setup_linker.clone(), + test_fn.clone(), + validation, + is_async, + ) + .await?; + Ok::<(), anyhow::Error>(()) + }; + + rt.block_on(run)?; + } + } + + Ok(()) +} + +/// Run a component test with recording and replay with specified validation setting +#[cfg(feature = "rr-component")] +async fn run_component_test_with_validation( + component_wat: &str, + setup_linker: F, + test_fn: R, + validate: bool, + is_async: bool, +) -> Result<()> +where + F: Fn(&mut ComponentLinker) -> Result<()>, + R: for<'a> Fn( + &'a mut Store, + &'a wasmtime::component::Instance, + bool, + ) -> Pin> + Send + 'a>>, +{ + // === RECORDING PHASE === + log::info!("Recording | Validate: {}, Async: {}", validate, is_async); + let engine = create_recording_engine(is_async)?; + let component = Component::new(&engine, component_wat)?; + + let mut linker = ComponentLinker::new(&engine); + setup_linker(&mut linker)?; + + let writer: Cursor> = Cursor::new(Vec::new()); + let mut store = Store::new(&engine, TestState::new()); + let record_settings = RecordSettings { + add_validation: validate, + ..Default::default() + }; + store.init_recording(writer, record_settings)?; + + let instance = if is_async { + linker.instantiate_async(&mut store, &component).await? + } else { + linker.instantiate(&mut store, &component)? + }; + + test_fn(&mut store, &instance, is_async).await?; + + // Extract the recording + let trace_box = store.into_record_writer()?; + let any_box: Box = trace_box; + let mut trace_reader = any_box.downcast::>>().unwrap(); + trace_reader.set_position(0); + + // === REPLAY PHASE === + log::info!("Replaying | Validate: {}, Async: {}", validate, is_async); + let engine = create_replay_engine(is_async)?; + let component = Component::new(&engine, component_wat)?; + let replay_settings = ReplaySettings { + validate, + ..Default::default() + }; + let mut renv = ReplayEnvironment::new(&engine, replay_settings); + renv.add_component(component); + + let mut replay_instance = renv.instantiate(*trace_reader)?; + if is_async { + replay_instance.run_to_completion_async().await?; + } else { + replay_instance.run_to_completion()?; + } + + Ok(()) +} + +// ============================================================================ +// Core Module Tests +// ============================================================================ + +#[test] +fn test_core_module_with_host_double() -> Result<()> { + let module_wat = r#" + (module + (import "env" "double" (func $double (param i32) (result i32))) + (func (export "main") (param i32) (result i32) + local.get 0 + call $double + ) + ) + "#; + + run_core_module_test( + module_wat, + |linker| { + linker.func_wrap("env", "double", |param: i32| param * 2)?; + Ok(()) + }, + |store, instance, is_async| { + Box::pin(async move { + let run = instance.get_typed_func::(&mut *store, "main")?; + let result = if is_async { + let result = run.call_async(&mut *store, 42).await?; + run.call_async(&mut *store, result).await? + } else { + let result = run.call(&mut *store, 42)?; + run.call(&mut *store, result)? + }; + assert_eq!(result, 168); + Ok(()) + }) + }, + ) +} + +#[test] +fn test_core_module_with_multiple_host_imports() -> Result<()> { + let module_wat = r#" + (module + (import "env" "double" (func $double (param i32) (result i32))) + (import "env" "complex" (func $complex (param i32 i64) (result i32 i64 f32))) + (func (export "main") (param i32) (result i32) + local.get 0 + call $double + call $double + i64.const 10 + call $complex + drop + drop + i64.const 5 + call $complex + drop + drop + ) + ) + "#; + + run_core_module_test( + module_wat, + |linker| { + linker.func_wrap("env", "double", |param: i32| param * 2)?; + linker.func_wrap("env", "complex", |p1: i32, p2: i64| -> (i32, i64, f32) { + ((p1 as f32).sqrt() as i32, (p1 * p1) as i64 * p2, 8.66) + })?; + Ok(()) + }, + |store, instance, is_async| { + Box::pin(async move { + let run = instance.get_typed_func::(&mut *store, "main")?; + let result = if is_async { + run.call_async(&mut *store, 42).await? + } else { + run.call(&mut *store, 42)? + }; + assert_eq!(result, 3); // sqrt(sqrt(42*2*2)) = sqrt(12) = 3 + Ok(()) + }) + }, + ) +} + +#[test] +#[should_panic] +fn test_recording_panics_for_core_module_memory_export() { + let module_wat = r#" + (module + (memory (export "memory") 1) + ) + "#; + + run_core_module_test(module_wat, |_| Ok(()), |_, _, _| Box::pin(async { Ok(()) })).unwrap(); +} + +// ============================================================================ +// Component Model Tests with Host Imports +// ============================================================================ + +// Few Parameters and Few Results (not exceeding MAX_FLAT_PARAMS=16 and +// MAX_FLAT_RESULTS=1) +#[test] +#[cfg(feature = "rr-component")] +fn test_component_under_max_params_results() -> Result<()> { + mod test { + use super::*; + + pub fn run() -> Result<()> { + bindgen!({ + inline: r#" + package component:test-package; + + interface env { + add: func(a: u32, b: u32) -> u32; + multiply: func(x: s64, y: s64, z: s64) -> s64; + } + + world my-world { + import env; + export calculate: func(a: u32, b: u32, c: s64) -> s64; + } + "#, + world: "my-world", + }); + + impl component::test_package::env::Host for TestState { + fn add(&mut self, a: u32, b: u32) -> u32 { + a + b + } + + fn multiply(&mut self, x: i64, y: i64, z: i64) -> i64 { + x * y * z + } + } + + let component_wat = r#" + (component + (import "component:test-package/env" (instance $env + (export "add" (func (param "a" u32) (param "b" u32) (result u32))) + (export "multiply" (func (param "x" s64) (param "y" s64) (param "z" s64) (result s64))) + )) + + (core module $m + (import "host" "add" (func $add (param i32 i32) (result i32))) + (import "host" "multiply" (func $multiply (param i64 i64 i64) (result i64))) + + (func (export "calculate") (param i32 i32 i64) (result i64) + local.get 0 + local.get 1 + call $add + i64.extend_i32_u + local.get 2 + i64.const 2 + call $multiply + ) + ) + + (core func $add (canon lower (func $env "add"))) + (core func $multiply (canon lower (func $env "multiply"))) + (core instance $m_inst (instantiate $m + (with "host" (instance + (export "add" (func $add)) + (export "multiply" (func $multiply)) + )) + )) + + (func (export "calculate") (param "a" u32) (param "b" u32) (param "c" s64) (result s64) + (canon lift (core func $m_inst "calculate")) + ) + ) + "#; + + run_component_test( + component_wat, + |linker| { + MyWorld::add_to_linker::<_, HasSelf<_>>(linker, |state: &mut TestState| state)?; + Ok(()) + }, + |store, instance, is_async| { + Box::pin(async move { + let func = instance + .get_typed_func::<(u32, u32, i64), (i64,)>(&mut *store, "calculate")?; + + let result = if is_async { + let (res,) = func.call_async(&mut *store, (10, 20, 3)).await?; + func.post_return_async(&mut *store).await?; + let (res,) = func + .call_async(&mut *store, (res.try_into().unwrap(), 0, 3)) + .await?; + func.post_return_async(&mut *store).await?; + res + } else { + let (res,) = func.call(&mut *store, (10, 20, 3))?; + func.post_return(&mut *store)?; + let (res,) = func.call(&mut *store, (res as u32, 0, 3))?; + func.post_return(&mut *store)?; + res + }; + assert_eq!(result, 1080); + Ok(()) + }) + }, + ) + } + } + + test::run() +} + +// Large Record (exceeding MAX_FLAT_PARAMS=16 and MAX_FLAT_RESULTS=1) +#[test] +#[cfg(feature = "rr-component")] +fn test_component_over_max_params_results() -> Result<()> { + mod test { + use super::*; + + pub fn run() -> Result<()> { + bindgen!({ + inline: r#" + package component:test-package; + + interface env { + record big-data { + f1: u32, f2: u32, f3: u32, f4: u32, + f5: u32, f6: u32, f7: u32, f8: u32, + f9: u32, f10: u32, f11: u32, f12: u32, + f13: u32, f14: u32, f15: u32, f16: u32, + f17: u32, f18: u32, f19: u32, f20: u32, + } + + process-record: func(data: big-data) -> big-data; + } + + world my-world { + import env; + } + "#, + world: "my-world", + }); + + use component::test_package::env::{BigData, Host}; + + impl Host for TestState { + fn process_record(&mut self, mut data: BigData) -> BigData { + // Double all the fields + data.f1 *= 2; + data.f2 *= 2; + data.f3 *= 2; + data.f4 *= 2; + data.f5 *= 2; + data.f6 *= 2; + data.f7 *= 2; + data.f8 *= 2; + data.f9 *= 2; + data.f10 *= 2; + data.f11 *= 2; + data.f12 *= 2; + data.f13 *= 2; + data.f14 *= 2; + data.f15 *= 2; + data.f16 *= 2; + data.f17 *= 2; + data.f18 *= 2; + data.f19 *= 2; + data.f20 *= 2; + data + } + } + + let component_wat = format!( + r#" + (component + (type (;0;) + (instance + (type (;0;) (record (field "f1" u32) (field "f2" u32) (field "f3" u32) (field "f4" u32) (field "f5" u32) (field "f6" u32) (field "f7" u32) (field "f8" u32) (field "f9" u32) (field "f10" u32) (field "f11" u32) (field "f12" u32) (field "f13" u32) (field "f14" u32) (field "f15" u32) (field "f16" u32) (field "f17" u32) (field "f18" u32) (field "f19" u32) (field "f20" u32))) + (export (;1;) "big-data" (type (eq 0))) + (type (;2;) (func (param "data" 1) (result 1))) + (export (;0;) "process-record" (func (type 2))) + ) + ) + (import "component:test-package/env" (instance (;0;) (type 0))) + (alias export 0 "big-data" (type (;1;))) + (alias export 0 "process-record" (func $host)) + (import "big-data" (type (;2;) (eq 1))) + (core module $m + (type (;0;) (func (param i32 i32))) + (type (;2;) (func (param i32) (result i32))) + (import "env" "process-record" (func $process_record (type 0))) + (memory (export "memory") 1) + {realloc} + (func (export "main") (param $input_ptr i32) (result i32) + (local $output_ptr i32) + i32.const 116 + local.set $output_ptr + local.get $input_ptr + local.get $output_ptr + call $process_record + local.get $output_ptr + ) + (data (;0;) (i32.const 16) "\01\00\00\00\02\00\00\00\03\00\00\00\04\00\00\00\05\00\00\00\06\00\00\00\07\00\00\00\08\00\00\00\09\00\00\00\0a\00\00\00\0b\00\00\00\0c\00\00\00\0d\00\00\00\0e\00\00\00\0f\00\00\00\10\00\00\00\11\00\00\00\12\00\00\00\13\00\00\00\14\00\00\00") + ) + {shims} + {instantiation} + ) + "#, + realloc = cabi_realloc_wat(), + shims = shims_wat("i32 i32"), + instantiation = + instantiation_wat("process-record", "(param \"data\" 2) (result 2)") + ); + + run_component_test( + &component_wat, + |linker| { + component::test_package::env::add_to_linker::<_, HasSelf<_>>( + linker, + |state: &mut TestState| state, + )?; + Ok(()) + }, + |store, instance, is_async| { + Box::pin(async move { + // Call the main export with test data + let main_func = instance + .get_typed_func::<(BigData,), (BigData,)>(&mut *store, "run")?; + + let test_data = BigData { + f1: 1, + f2: 2, + f3: 3, + f4: 4, + f5: 5, + f6: 6, + f7: 7, + f8: 8, + f9: 9, + f10: 10, + f11: 11, + f12: 12, + f13: 13, + f14: 14, + f15: 15, + f16: 16, + f17: 17, + f18: 18, + f19: 19, + f20: 20, + }; + + let (result,) = if is_async { + let res = main_func.call_async(&mut *store, (test_data,)).await?; + main_func.post_return_async(&mut *store).await?; + res + } else { + let res = main_func.call(&mut *store, (test_data,))?; + main_func.post_return(&mut *store)?; + res + }; + + // All fields should be doubled + assert_eq!(result.f1, 2); + assert_eq!(result.f10, 20); + assert_eq!(result.f20, 40); + + Ok(()) + }) + }, + ) + } + } + + test::run() +} + +#[test] +#[cfg(feature = "rr-component")] +fn test_component_tuple() -> Result<()> { + mod test { + use super::*; + + pub fn run() -> Result<()> { + bindgen!({ + inline: r#" + package component:test; + + interface env { + swap-tuple: func(val: tuple) -> tuple; + } + + world my-world { + import env; + } + "#, + world: "my-world", + }); + + use component::test::env::Host; + + impl Host for TestState { + fn swap_tuple(&mut self, val: (u32, u32)) -> (u32, u32) { + (val.1, val.0) + } + } + + let component_wat = format!( + r#" + (component + (import "component:test/env" (instance $env + (export "swap-tuple" (func (param "val" (tuple u32 u32)) (result (tuple u32 u32)))) + )) + (alias export $env "swap-tuple" (func $host)) + (core module $m + (import "env" "swap" (func $swap (param i32 i32 i32))) + (memory (export "memory") 1) + {realloc} + (func (export "main") (result i32) + (local $retptr i32) + i32.const 100 + local.set $retptr + + i32.const 10 + i32.const 20 + local.get $retptr + call $swap + + local.get $retptr + ) + ) + {shims} + {instantiation} + ) + "#, + realloc = cabi_realloc_wat(), + shims = shims_wat("i32 i32 i32"), + instantiation = instantiation_wat("swap", "(result (tuple u32 u32))") + ); + + run_component_test( + &component_wat, + |linker| { + component::test::env::add_to_linker::<_, HasSelf<_>>( + linker, + |state: &mut TestState| state, + )?; + Ok(()) + }, + |store, instance, is_async| { + Box::pin(async move { + let func = + instance.get_typed_func::<(), ((u32, u32),)>(&mut *store, "run")?; + let (result,) = if is_async { + let res = func.call_async(&mut *store, ()).await?; + func.post_return_async(&mut *store).await?; + res + } else { + let res = func.call(&mut *store, ())?; + func.post_return(&mut *store)?; + res + }; + assert_eq!(result, (20, 10)); + Ok(()) + }) + }, + ) + } + } + + test::run() +} + +#[test] +#[cfg(feature = "rr-component")] +fn test_component_string() -> Result<()> { + mod test { + use super::*; + + pub fn run() -> Result<()> { + bindgen!({ + inline: r#" + package component:test; + + interface env { + reverse-string: func(s: string) -> string; + } + + world my-world { + import env; + } + "#, + world: "my-world", + }); + + use component::test::env::Host; + + impl Host for TestState { + fn reverse_string(&mut self, s: String) -> String { + s.chars().rev().collect() + } + } + + let component_wat = format!( + r#" + (component + (import "component:test/env" (instance $env + (export "reverse-string" (func (param "s" string) (result string))) + )) + (alias export $env "reverse-string" (func $host)) + (core module $m + (import "env" "reverse" (func $reverse (param i32 i32 i32))) + (memory (export "memory") 1) + {realloc} + (func (export "main") (result i32) + (local $retptr i32) + i32.const 100 + local.set $retptr + + ;; Call reverse("hello") + ;; "hello" is at offset 16, len 5 + i32.const 16 + i32.const 5 + local.get $retptr + call $reverse + + ;; Return retptr which points to (ptr, len) + local.get $retptr + ) + (data (i32.const 16) "hello") + ) + {shims} + {instantiation} + ) + "#, + realloc = cabi_realloc_wat(), + shims = shims_wat("i32 i32 i32"), + instantiation = instantiation_wat("reverse", "(result string)") + ); + + run_component_test( + &component_wat, + |linker| { + component::test::env::add_to_linker::<_, HasSelf<_>>( + linker, + |state: &mut TestState| state, + )?; + Ok(()) + }, + |store, instance, is_async| { + Box::pin(async move { + let func = instance.get_typed_func::<(), (String,)>(&mut *store, "run")?; + let (result,) = if is_async { + let res = func.call_async(&mut *store, ()).await?; + func.post_return_async(&mut *store).await?; + res + } else { + let res = func.call(&mut *store, ())?; + func.post_return(&mut *store)?; + res + }; + assert_eq!(result, "olleh"); + Ok(()) + }) + }, + ) + } + } + + test::run() +} + +#[test] +#[cfg(feature = "rr-component")] +fn test_component_variant() -> Result<()> { + mod test { + use super::*; + + pub fn run() -> Result<()> { + bindgen!({ + inline: r#" + package component:test; + + interface env { + variant shape { + circle(f32), + rectangle(tuple) + } + transform: func(s: shape) -> shape; + } + + world my-world { + import env; + } + "#, + world: "my-world", + }); + + use component::test::env::{Host, Shape}; + + impl Host for TestState { + fn transform(&mut self, s: Shape) -> Shape { + match s { + Shape::Circle(r) => Shape::Circle(r * 2.0), + Shape::Rectangle((w, h)) => Shape::Rectangle((h, w)), + } + } + } + + let component_wat = format!( + r#" + (component + (type (;0;) + (instance + (type (;0;) (tuple f32 f32)) + (type (;1;) (variant (case "circle" f32) (case "rectangle" 0))) + (export (;2;) "shape" (type (eq 1))) + (type (;3;) (func (param "s" 2) (result 2))) + (export (;0;) "transform" (func (type 3))) + ) + ) + (import "component:test/env" (instance (;0;) (type 0))) + (alias export 0 "shape" (type (;1;))) + (alias export 0 "transform" (func $host)) + (core module $m + (import "env" "transform" (func $transform (param i32 f32 f32 i32))) + (memory (export "memory") 1) + {realloc} + (func (export "main") (result i32) + (local $retptr i32) + i32.const 100 + local.set $retptr + + i32.const 0 ;; discriminant = Circle + f32.const 10.0 ;; payload 1 + f32.const 0.0 ;; payload 2 + local.get $retptr + call $transform + + local.get $retptr + ) + ) + {shims} + {instantiation} + ) + "#, + realloc = cabi_realloc_wat(), + shims = shims_wat("i32 f32 f32 i32"), + instantiation = instantiation_wat("transform", "(result 1)") + ); + + run_component_test( + &component_wat, + |linker| { + component::test::env::add_to_linker::<_, HasSelf<_>>( + linker, + |state: &mut TestState| state, + )?; + Ok(()) + }, + |store, instance, is_async| { + Box::pin(async move { + let func = instance.get_typed_func::<(), (Shape,)>(&mut *store, "run")?; + let (result,) = if is_async { + let res = func.call_async(&mut *store, ()).await?; + func.post_return_async(&mut *store).await?; + res + } else { + let res = func.call(&mut *store, ())?; + func.post_return(&mut *store)?; + res + }; + + match result { + Shape::Circle(r) => assert_eq!(r, 20.0), + _ => panic!("Expected Circle"), + } + Ok(()) + }) + }, + ) + } + } + + test::run() +} + +#[test] +#[cfg(feature = "rr-component")] +fn test_component_result() -> Result<()> { + mod test { + use super::*; + + pub fn run() -> Result<()> { + bindgen!({ + inline: r#" + package component:test; + + interface env { + convert: func(r: result) -> result; + } + + world my-world { + import env; + } + "#, + world: "my-world", + }); + + use component::test::env::Host; + + impl Host for TestState { + fn convert(&mut self, r: Result) -> Result { + match r { + Ok(val) => Ok(val.to_string()), + Err(msg) => Err(msg.len() as u32), + } + } + } + + let component_wat = format!( + r#" + (component + (type (;0;) + (instance + (type (;0;) (result u32 (error string))) + (type (;1;) (result string (error u32))) + (type (;2;) (func (param "r" 0) (result 1))) + (export (;0;) "convert" (func (type 2))) + ) + ) + (import "component:test/env" (instance (;0;) (type 0))) + (alias export 0 "convert" (func $host)) + (core module $m + (import "env" "convert" (func $convert (param i32 i32 i32 i32))) + (memory (export "memory") 1) + {realloc} + (func (export "main") (result i32) + (local $retptr i32) + i32.const 100 + local.set $retptr + i32.const 0 + i32.const 42 + i32.const 0 + local.get $retptr + call $convert + local.get $retptr + i32.load + i32.const 0 + i32.ne + if + unreachable + end + local.get $retptr + i32.load offset=8 + i32.const 2 + i32.ne + if + unreachable + end + i32.const 1 + i32.const 16 + i32.const 5 + local.get $retptr + call $convert + local.get $retptr + i32.load + i32.const 1 + i32.ne + if + unreachable + end + local.get $retptr + i32.load offset=4 + i32.const 5 + i32.ne + if + unreachable + end + i32.const 1 + ) + (data (i32.const 16) "hello") + ) + {shims} + {instantiation} + ) + "#, + realloc = cabi_realloc_wat(), + shims = shims_wat("i32 i32 i32 i32"), + instantiation = instantiation_wat("convert", "(result u32)") + ); + + run_component_test( + &component_wat, + |linker| { + component::test::env::add_to_linker::<_, HasSelf<_>>( + linker, + |state: &mut TestState| state, + )?; + Ok(()) + }, + |store, instance, is_async| { + Box::pin(async move { + let func = instance.get_typed_func::<(), (u32,)>(&mut *store, "run")?; + let (result,) = if is_async { + let res = func.call_async(&mut *store, ()).await?; + func.post_return_async(&mut *store).await?; + res + } else { + let res = func.call(&mut *store, ())?; + func.post_return(&mut *store)?; + res + }; + assert_eq!(result, 1); + Ok(()) + }) + }, + ) + } + } + + test::run() +} + +#[test] +#[cfg(feature = "rr-component")] +fn test_component_list() -> Result<()> { + mod test { + use super::*; + + pub fn run() -> Result<()> { + bindgen!({ + inline: r#" + package component:test; + + interface env { + reverse-list: func(l: list) -> list; + } + + world my-world { + import env; + } + "#, + world: "my-world", + }); + + use component::test::env::Host; + + impl Host for TestState { + fn reverse_list(&mut self, l: Vec) -> Vec { + l.into_iter().rev().collect() + } + } + + let component_wat = format!( + r#" + (component + (type (;0;) + (instance + (type (;0;) (list u32)) + (type (;1;) (func (param "l" 0) (result 0))) + (export (;0;) "reverse-list" (func (type 1))) + ) + ) + (import "component:test/env" (instance (;0;) (type 0))) + (alias export 0 "reverse-list" (func $host)) + (core module $m + (import "env" "reverse" (func $reverse (param i32 i32 i32))) + (memory (export "memory") 1) + {realloc} + (func (export "main") (result i32) + (local $retptr i32) + i32.const 100 + local.set $retptr + i32.const 16 + i32.const 3 + local.get $retptr + call $reverse + local.get $retptr + i32.load offset=4 + i32.const 3 + i32.ne + if + unreachable + end + local.get $retptr + i32.load + local.set $retptr + local.get $retptr + i32.load + i32.const 3 + i32.ne + if + unreachable + end + local.get $retptr + i32.load offset=4 + i32.const 2 + i32.ne + if + unreachable + end + local.get $retptr + i32.load offset=8 + i32.const 1 + i32.ne + if + unreachable + end + i32.const 1 + ) + (data (i32.const 16) "\01\00\00\00\02\00\00\00\03\00\00\00") + ) + {shims} + {instantiation} + ) + "#, + realloc = cabi_realloc_wat(), + shims = shims_wat("i32 i32 i32"), + instantiation = instantiation_wat("reverse", "(result u32)") + ); + + run_component_test( + &component_wat, + |linker| { + component::test::env::add_to_linker::<_, HasSelf<_>>( + linker, + |state: &mut TestState| state, + )?; + Ok(()) + }, + |store, instance, is_async| { + Box::pin(async move { + let func = instance.get_typed_func::<(), (u32,)>(&mut *store, "run")?; + let (result,) = if is_async { + let res = func.call_async(&mut *store, ()).await?; + func.post_return_async(&mut *store).await?; + res + } else { + let res = func.call(&mut *store, ())?; + func.post_return(&mut *store)?; + res + }; + assert_eq!(result, 1); + Ok(()) + }) + }, + ) + } + } + + test::run() +} + +#[test] +#[cfg(feature = "rr-component")] +fn test_component_option() -> Result<()> { + mod test { + use super::*; + + pub fn run() -> Result<()> { + bindgen!({ + inline: r#" + package component:test; + + interface env { + inc-option: func(o: option) -> option; + } + + world my-world { + import env; + } + "#, + world: "my-world", + }); + + use component::test::env::Host; + + impl Host for TestState { + fn inc_option(&mut self, o: Option) -> Option { + o.map(|x| x + 1) + } + } + + let component_wat = format!( + r#" + (component + (type (;0;) + (instance + (type (;0;) (option u32)) + (type (;1;) (func (param "o" 0) (result 0))) + (export (;0;) "inc-option" (func (type 1))) + ) + ) + (import "component:test/env" (instance (;0;) (type 0))) + (alias export 0 "inc-option" (func $host)) + (core module $m + (import "env" "inc" (func $inc (param i32 i32 i32))) + (memory (export "memory") 1) + {realloc} + (func (export "main") (result i32) + (local $retptr i32) + i32.const 100 + local.set $retptr + i32.const 1 + i32.const 10 + local.get $retptr + call $inc + local.get $retptr + i32.load + i32.const 1 + i32.ne + if + unreachable + end + local.get $retptr + i32.load offset=4 + i32.const 11 + i32.ne + if + unreachable + end + i32.const 0 + i32.const 0 + local.get $retptr + call $inc + local.get $retptr + i32.load + if + unreachable + end + i32.const 1 + ) + ) + {shims} + {instantiation} + ) + "#, + realloc = cabi_realloc_wat(), + shims = shims_wat("i32 i32 i32"), + instantiation = instantiation_wat("inc", "(result u32)") + ); + + run_component_test( + &component_wat, + |linker| { + component::test::env::add_to_linker::<_, HasSelf<_>>( + linker, + |state: &mut TestState| state, + )?; + Ok(()) + }, + |store, instance, is_async| { + Box::pin(async move { + let func = instance.get_typed_func::<(), (u32,)>(&mut *store, "run")?; + let (result,) = if is_async { + let res = func.call_async(&mut *store, ()).await?; + func.post_return_async(&mut *store).await?; + res + } else { + let res = func.call(&mut *store, ())?; + func.post_return(&mut *store)?; + res + }; + assert_eq!(result, 1); + Ok(()) + }) + }, + ) + } + } + + test::run() +} + +#[cfg(feature = "rr-component")] +fn cabi_realloc_wat() -> String { + r#" + (global $bump (mut i32) (i32.const 256)) + (export "cabi_realloc" (func $realloc)) + (func $realloc (param $old_ptr i32) (param $old_size i32) (param $align i32) (param $new_size i32) (result i32) + (local $result i32) + global.get $bump + local.get $align + i32.const 1 + i32.sub + i32.add + local.get $align + i32.const 1 + i32.sub + i32.const -1 + i32.xor + i32.and + local.set $result + local.get $result + local.get $new_size + i32.add + global.set $bump + local.get $result + ) + "#.to_string() +} + +#[cfg(feature = "rr-component")] +fn shims_wat(params: &str) -> String { + let count = params.split_whitespace().count(); + let locals_get = (0..count) + .map(|i| format!("local.get {}", i)) + .collect::>() + .join("\n"); + format!( + r#" + (core module $shim1 + (table (export "$imports") 1 funcref) + (func (export "0") (param {params}) + {locals_get} + i32.const 0 + call_indirect (param {params}) + ) + ) + (core module $shim2 + (import "" "0" (func (param {params}))) + (import "" "$imports" (table 1 funcref)) + (elem (i32.const 0) func 0) + ) + "# + ) +} + +#[cfg(feature = "rr-component")] +fn instantiation_wat(core_name: &str, lift_sig: &str) -> String { + format!( + r#" + (core instance $s1 (instantiate $shim1)) + (alias core export $s1 "0" (core func $indirect)) + (core instance $env_inst (export "{core_name}" (func $indirect))) + (core instance $inst (instantiate $m (with "env" (instance $env_inst)))) + (alias core export $inst "memory" (core memory $mem)) + (alias core export $inst "cabi_realloc" (core func $realloc)) + (alias core export $s1 "$imports" (core table $tbl)) + (core func $lowered (canon lower (func $host) (memory $mem) (realloc $realloc))) + (core instance $tbl_inst (export "$imports" (table $tbl)) (export "0" (func $lowered))) + (core instance (instantiate $shim2 (with "" (instance $tbl_inst)))) + (alias core export $inst "main" (core func $run)) + (func (export "run") {lift_sig} (canon lift (core func $run) (memory $mem) (realloc $realloc))) + "# + ) +} From df6d92089328dff7beea34b24f5a0cdc3409513a Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 25 Nov 2025 14:12:40 -0500 Subject: [PATCH 49/73] Remove explicit `rr-component` feature flag --- Cargo.toml | 1 - crates/wasmtime/Cargo.toml | 3 - crates/wasmtime/build.rs | 4 ++ .../src/runtime/component/func/host.rs | 12 ++-- .../src/runtime/component/func/options.rs | 28 ++++----- .../src/runtime/component/instance.rs | 8 +-- crates/wasmtime/src/runtime/rr/core.rs | 2 +- crates/wasmtime/src/runtime/rr/core/events.rs | 4 +- .../src/runtime/rr/hooks/component_hooks.rs | 50 +++++++-------- .../wasmtime/src/runtime/rr/replay_driver.rs | 62 +++++++++---------- .../src/runtime/vm/component/libcalls.rs | 10 +-- src/commands/replay.rs | 5 +- tests/all/rr.rs | 28 ++++----- 13 files changed, 109 insertions(+), 108 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 15c8603841..738ca3551d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -510,7 +510,6 @@ component-model-async = [ "dep:futures", ] rr = ["wasmtime/rr", "wasmtime-cli-flags/rr", "run"] -rr-component = ["rr", "wasmtime/rr-component"] # This feature, when enabled, will statically compile out all logging statements # throughout Wasmtime and its dependencies. diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml index 70fd1b4859..707032e253 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -428,8 +428,5 @@ debug = [ # Enables support for defining compile-time builtins. compile-time-builtins = ['dep:wasm-compose', 'dep:tempfile'] -# Enables support for record/replay for components -rr-component = ["component-model", "rr"] # Enable support for the common base infrastructure of record/replay -# By default, this supports core wasm `rr`. For components, enable `rr-component` rr = ["dep:embedded-io"] diff --git a/crates/wasmtime/build.rs b/crates/wasmtime/build.rs index c80e4bf505..0e3f64d810 100644 --- a/crates/wasmtime/build.rs +++ b/crates/wasmtime/build.rs @@ -33,6 +33,10 @@ fn main() { custom_cfg("has_custom_sync", has_custom_sync); custom_cfg("has_host_compiler_backend", has_host_compiler_backend); + // Component model support for record/replay + let rr_component = cfg!(feature = "rr") && cfg!(feature = "component-model"); + custom_cfg("rr_component", rr_component); + // If this OS isn't supported and no debug-builtins or if Cranelift doesn't support // the host or there's no need to build these helpers. #[cfg(feature = "runtime")] diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 46ca47a938..3c98184440 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -8,7 +8,7 @@ use crate::component::types::ComponentFunc; use crate::component::{ComponentNamedList, ComponentType, Instance, Lift, Lower, Val}; use crate::prelude::*; use crate::rr; -#[cfg(feature = "rr-component")] +#[cfg(rr_component)] use crate::rr::component_hooks::ReplayLoweringPhase; use crate::runtime::vm::component::{ ComponentInstance, VMComponentContext, VMLowering, VMLoweringCallee, @@ -941,7 +941,7 @@ where store.0.store_opaque_mut(), )?; // Replay host function path: Just lower the results from the trace - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] { let mut cx = LowerContext::new(store, options, instance); // Skip lifting/lowering logic, and just replaying the lowering state @@ -966,9 +966,11 @@ where } } } - #[cfg(not(feature = "rr-component"))] + #[cfg(not(rr_component))] { - unreachable!("cannot reach host function replay when `rr-component` is disabled"); + unreachable!( + "cannot reach host function replay when rr and component-model is disabled" + ); } } @@ -1016,7 +1018,7 @@ unsafe fn dynamic_params_load( } /// Replay of return values from `dynamic_params_load`. Keep in sync -#[cfg(feature = "rr-component")] +#[cfg(rr_component)] unsafe fn dynamic_params_load_replay(param_tys: &TypeTuple, max_flat_params: usize) -> usize { if let Some(param_count) = param_tys.abi.flat_count(max_flat_params) { param_count diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index 9ecaa64f4c..beba154f04 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -1,5 +1,5 @@ use crate::StoreContextMut; -#[cfg(feature = "rr-component")] +#[cfg(rr_component)] use crate::ValRaw; use crate::component::concurrent::ConcurrentState; use crate::component::matching::InstanceType; @@ -7,7 +7,7 @@ use crate::component::resources::{HostResourceData, HostResourceIndex, HostResou use crate::component::{Instance, ResourceType}; use crate::prelude::*; use crate::rr::{ConstMemorySliceCell, MemorySliceCell}; -#[cfg(feature = "rr-component")] +#[cfg(rr_component)] use crate::rr::{ RREvent, RecordBuffer, ReplayError, Replayer, ResultEvent, Validate, component_events::ReallocEntryEvent, component_events::ReallocReturnEvent, @@ -19,7 +19,7 @@ use crate::runtime::vm::component::{ }; use crate::store::{StoreId, StoreOpaque}; use alloc::sync::Arc; -#[cfg(feature = "rr-component")] +#[cfg(rr_component)] use core::mem::MaybeUninit; use core::pin::Pin; use core::ptr::NonNull; @@ -126,7 +126,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { } /// Return the `InstanceFlags` for this context - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] fn instance_flags(&self) -> InstanceFlags { let vminstance = self.instance.id().get(self.store.0); let opts = self.options(); @@ -139,7 +139,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { /// # Panics /// /// See [`as_slice`](Self::as_slice) - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] fn as_slice_mut_with_recorder(&mut self) -> (&mut [u8], Option<&mut RecordBuffer>) { self.instance .options_memory_mut_with_recorder(self.store.0, self.options) @@ -243,7 +243,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { old_align: u32, new_size: usize, ) -> Result { - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] self.store.0.record_event(|| ReallocEntryEvent { old_addr: old, old_size, @@ -251,7 +251,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { new_size, })?; let result = self.realloc_inner(old, old_size, old_align, new_size); - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] self.store.0.record_event_validation(|| { ReallocReturnEvent(ResultEvent::from_anyhow_result(&result)) })?; @@ -271,7 +271,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { #[inline] pub fn get(&mut self, offset: usize) -> ConstMemorySliceCell<'_, N> { cfg_if::cfg_if! { - if #[cfg(feature = "rr-component")] { + if #[cfg(rr_component)] { let (slice_mut, recorder) = self.as_slice_mut_with_recorder(); } else { let slice_mut = self.as_slice_mut(); @@ -289,9 +289,9 @@ impl<'a, T: 'static> LowerContext<'a, T> { // of `unsafe`. ConstMemorySliceCell { bytes: slice_mut[offset..].first_chunk_mut().unwrap(), - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] offset: offset, - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] recorder: recorder, } } @@ -305,7 +305,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { #[inline] pub fn get_dyn(&mut self, offset: usize, size: usize) -> MemorySliceCell<'_> { cfg_if::cfg_if! { - if #[cfg(feature = "rr-component")] { + if #[cfg(rr_component)] { let (slice_mut, recorder) = self.as_slice_mut_with_recorder(); } else { let slice_mut = self.as_slice_mut(); @@ -313,9 +313,9 @@ impl<'a, T: 'static> LowerContext<'a, T> { } MemorySliceCell { bytes: &mut slice_mut[offset..][..size], - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] offset: offset, - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] recorder: recorder, } } @@ -418,7 +418,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { /// * It is assumed that this is only invoked at the root lower/store calls /// * Panics if invoked while replay is not enabled /// - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] pub fn replay_lowering( &mut self, mut result_storage: Option<&mut [MaybeUninit]>, diff --git a/crates/wasmtime/src/runtime/component/instance.rs b/crates/wasmtime/src/runtime/component/instance.rs index ebeb41e9d3..95a8bed78a 100644 --- a/crates/wasmtime/src/runtime/component/instance.rs +++ b/crates/wasmtime/src/runtime/component/instance.rs @@ -8,7 +8,7 @@ use crate::component::{ use crate::instance::OwnedImports; use crate::linker::DefinitionType; use crate::prelude::*; -#[cfg(feature = "rr-component")] +#[cfg(rr_component)] use crate::rr::RecordBuffer; use crate::runtime::vm::component::{ CallContexts, ComponentInstance, ResourceTables, TypedResource, TypedResourceIndex, @@ -530,7 +530,7 @@ impl Instance { } } - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] pub(crate) fn options_memory_mut_with_recorder<'a>( &self, store: &'a mut StoreOpaque, @@ -1080,7 +1080,7 @@ impl<'a> Instantiator<'a> { /// Convenience helper to return the instance ID of the `ComponentInstance` that's /// being instantiated - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] fn id(&self) -> ComponentInstanceId { self.id } @@ -1193,7 +1193,7 @@ impl InstancePre { .allocator() .increment_component_instance_count()?; let mut instantiator = Instantiator::new(&self.component, store.0, &self.imports); - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] { use crate::rr::component_events::InstantiationEvent; store.0.record_event(|| InstantiationEvent { diff --git a/crates/wasmtime/src/runtime/rr/core.rs b/crates/wasmtime/src/runtime/rr/core.rs index d5ebddaef4..04a911e39c 100644 --- a/crates/wasmtime/src/runtime/rr/core.rs +++ b/crates/wasmtime/src/runtime/rr/core.rs @@ -8,7 +8,7 @@ use wasmtime_environ::EntityIndex; // so that [`RREvent`] has a well-defined serialization format, but export // it for other modules only when enabled pub use events::Validate; -#[cfg(feature = "rr-component")] +#[cfg(rr_component)] pub use events::component_events; use events::component_events as __component_events; pub use events::{RRFuncArgVals, ResultEvent, common_events, core_events, marker_events}; diff --git a/crates/wasmtime/src/runtime/rr/core/events.rs b/crates/wasmtime/src/runtime/rr/core/events.rs index 6cb158bc0b..5ba8380feb 100644 --- a/crates/wasmtime/src/runtime/rr/core/events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events.rs @@ -4,7 +4,7 @@ use crate::{AsContextMut, Val, prelude::*}; use crate::{ValRaw, ValType}; use core::fmt; use serde::{Deserialize, Serialize}; -#[cfg(feature = "rr-component")] +#[cfg(rr_component)] use wasmtime_environ::component::FlatTypesStorage; /// A serde compatible representation of errors produced during execution @@ -76,7 +76,7 @@ impl RRFuncArgVals { } /// Construct [`RRFuncArgVals`] from raw value buffer and a [`FlatTypesStorage`] - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] #[inline] pub fn from_flat_storage(args: &[T], flat: FlatTypesStorage) -> RRFuncArgVals where diff --git a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs index 1501329d2b..b4c877709a 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs @@ -1,30 +1,30 @@ use crate::ValRaw; use crate::component::ComponentInstanceId; -#[cfg(feature = "rr-component")] +#[cfg(rr_component)] use crate::rr::{RecordBuffer, Recorder, component_events::MemorySliceWriteEvent}; use core::ops::{Deref, DerefMut}; use crate::component::func::LowerContext; -#[cfg(feature = "rr-component")] +#[cfg(rr_component)] use crate::rr::common_events::{HostFuncEntryEvent, WasmFuncReturnEvent}; -#[cfg(feature = "rr-component")] +#[cfg(rr_component)] use crate::rr::component_events::{ LowerFlatEntryEvent, LowerFlatReturnEvent, LowerMemoryEntryEvent, LowerMemoryReturnEvent, PostReturnEvent, WasmFuncBeginEvent, WasmFuncEntryEvent, }; -#[cfg(feature = "rr-component")] +#[cfg(rr_component)] use crate::rr::{RRFuncArgVals, ResultEvent, common_events::HostFuncReturnEvent}; use crate::store::StoreOpaque; use crate::{StoreContextMut, prelude::*}; use alloc::sync::Arc; use core::mem::MaybeUninit; use wasmtime_environ::component::{ComponentTypes, ExportIndex, InterfaceType, TypeFuncIndex}; -#[cfg(all(feature = "rr-component"))] +#[cfg(all(rr_component))] use wasmtime_environ::component::{MAX_FLAT_PARAMS, MAX_FLAT_RESULTS}; /// Indicator type signalling the context during lowering -#[cfg(feature = "rr-component")] +#[cfg(rr_component)] #[derive(Debug)] pub enum ReplayLoweringPhase { WasmFuncEntry, @@ -41,7 +41,7 @@ pub fn record_wasm_func_begin( func_idx: ExportIndex, store: &mut StoreOpaque, ) -> Result<()> { - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] store.record_event(|| WasmFuncBeginEvent { instance, func_idx })?; let _ = (instance, func_idx, store); Ok(()) @@ -54,7 +54,7 @@ pub fn record_wasm_func_post_return( func_idx: ExportIndex, store: &mut StoreContextMut<'_, T>, ) -> Result<()> { - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] store .0 .record_event(|| PostReturnEvent { instance, func_idx })?; @@ -76,7 +76,7 @@ where F: FnOnce(&mut StoreContextMut<'_, T>) -> Result<()>, { let _ = (args, type_idx, &types); - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] store.0.record_event(|| { let flat_params = types.flat_types_storage_or_pointer( &InterfaceType::Tuple(types[type_idx].params), @@ -87,7 +87,7 @@ where } })?; let result = wasm_call(store); - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] { if let Err(e) = &result { log::warn!("Wasm function call exited with error: {:?}", e); @@ -108,7 +108,7 @@ where result?; Ok(()) } - #[cfg(not(feature = "rr-component"))] + #[cfg(not(rr_component))] { result } @@ -122,7 +122,7 @@ pub fn record_validate_host_func_entry( param_tys: &InterfaceType, store: &mut StoreOpaque, ) -> Result<()> { - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] store.record_event_validation(|| create_host_func_entry_event(args, types, param_tys))?; let _ = (args, types, param_tys, store); Ok(()) @@ -136,7 +136,7 @@ pub fn replay_validate_host_func_entry( param_tys: &InterfaceType, store: &mut StoreOpaque, ) -> Result<()> { - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] store.next_replay_event_validation::(|| { create_host_func_entry_event(args, types, param_tys) })?; @@ -152,7 +152,7 @@ pub fn record_host_func_return( ty: &InterfaceType, store: &mut StoreOpaque, ) -> Result<()> { - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] store.record_event(|| { let flat_results = types.flat_types_storage_or_pointer(&ty, MAX_FLAT_RESULTS); HostFuncReturnEvent { @@ -174,12 +174,12 @@ pub fn record_lower_memory( where F: FnOnce(&mut LowerContext<'_, T>, InterfaceType, usize) -> Result<()>, { - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] cx.store .0 .record_event_validation(|| LowerMemoryEntryEvent { ty, offset })?; let store_result = lower_store(cx, ty, offset); - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] cx.store .0 .record_event(|| LowerMemoryReturnEvent(ResultEvent::from_anyhow_result(&store_result)))?; @@ -196,19 +196,19 @@ pub fn record_lower_flat( where F: FnOnce(&mut LowerContext<'_, T>, InterfaceType) -> Result<()>, { - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] cx.store .0 .record_event_validation(|| LowerFlatEntryEvent { ty })?; let lower_result = lower(cx, ty); - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] cx.store .0 .record_event(|| LowerFlatReturnEvent(ResultEvent::from_anyhow_result(&lower_result)))?; lower_result } -#[cfg(feature = "rr-component")] +#[cfg(rr_component)] #[inline(always)] fn create_host_func_entry_event( args: &mut [MaybeUninit], @@ -230,9 +230,9 @@ fn create_host_func_entry_event( /// of these types. pub struct MemorySliceCell<'a> { pub bytes: &'a mut [u8], - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] pub offset: usize, - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] pub recorder: Option<&'a mut RecordBuffer>, } impl<'a> Deref for MemorySliceCell<'a> { @@ -249,7 +249,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")] + #[cfg(rr_component)] if let Some(buf) = &mut self.recorder { buf.record_event(|| MemorySliceWriteEvent { offset: self.offset, @@ -290,9 +290,9 @@ impl Drop for MemorySliceCell<'_> { /// [`realloc`](LowerContext::realloc), preventing any interleavings in between the borrow and drop of the slice. pub struct ConstMemorySliceCell<'a, const N: usize> { pub bytes: &'a mut [u8; N], - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] pub offset: usize, - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] pub recorder: Option<&'a mut RecordBuffer>, } impl<'a, const N: usize> Deref for ConstMemorySliceCell<'a, N> { @@ -309,7 +309,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")] + #[cfg(rr_component)] if let Some(buf) = &mut self.recorder { buf.record_event(|| MemorySliceWriteEvent { offset: self.offset, diff --git a/crates/wasmtime/src/runtime/rr/replay_driver.rs b/crates/wasmtime/src/runtime/rr/replay_driver.rs index 08e6bd5abf..4ebe089990 100644 --- a/crates/wasmtime/src/runtime/rr/replay_driver.rs +++ b/crates/wasmtime/src/runtime/rr/replay_driver.rs @@ -2,18 +2,18 @@ use crate::rr::Validate; use crate::rr::{RREvent, ReplayError, core_events}; use crate::store::InstanceId; use crate::{AsContextMut, Engine, Module, ReplayReader, ReplaySettings, Store, prelude::*}; -#[cfg(feature = "rr-component")] +#[cfg(rr_component)] use crate::{ ValRaw, component, component::Component, component::ComponentInstanceId, rr::component_events, rr::component_hooks, }; use alloc::{collections::BTreeMap, sync::Arc}; -#[cfg(not(feature = "rr-component"))] +#[cfg(not(rr_component))] use anyhow::bail; -#[cfg(feature = "rr-component")] +#[cfg(rr_component)] use core::mem::MaybeUninit; use wasmtime_environ::EntityIndex; -#[cfg(feature = "rr-component")] +#[cfg(rr_component)] use wasmtime_environ::component::{MAX_FLAT_PARAMS, MAX_FLAT_RESULTS}; /// The environment necessary to produce a [`ReplayInstance`] @@ -21,7 +21,7 @@ use wasmtime_environ::component::{MAX_FLAT_PARAMS, MAX_FLAT_RESULTS}; pub struct ReplayEnvironment { engine: Engine, modules: BTreeMap<[u8; 32], Module>, - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] components: BTreeMap<[u8; 32], Component>, settings: ReplaySettings, } @@ -32,7 +32,7 @@ impl ReplayEnvironment { Self { engine: engine.clone(), modules: BTreeMap::new(), - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] components: BTreeMap::new(), settings, } @@ -45,7 +45,7 @@ impl ReplayEnvironment { } /// Add a [`Component`] to the replay environment - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] pub fn add_component(&mut self, component: Component) -> &mut Self { self.components.insert(*component.checksum(), component); self @@ -134,11 +134,11 @@ impl ReplayEnvironment { pub struct ReplayInstance { env: Arc, store: Store, - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] component_linker: component::Linker, module_linker: crate::Linker, module_instances: BTreeMap, - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] component_instances: BTreeMap, } @@ -155,20 +155,20 @@ impl ReplayInstance { for module in env.modules.values() { module_linker.define_unknown_imports_as_traps(module)?; } - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] let mut component_linker = component::Linker::::new(&env.engine); - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] for component in env.components.values() { component_linker.define_unknown_imports_as_traps(component)?; } Ok(Self { env, store, - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] component_linker, module_linker, module_instances: BTreeMap::new(), - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] component_instances: BTreeMap::new(), }) } @@ -193,7 +193,7 @@ impl ReplayInstance { pub fn run_single_top_level_event(&mut self, rr_event: RREvent) -> Result<()> { match rr_event { RREvent::ComponentInstantiation(event) => { - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] { // Find matching component from environment to instantiate let component = self @@ -214,16 +214,16 @@ impl ReplayInstance { self.component_instances .insert(instance.id().instance(), instance); } - #[cfg(not(feature = "rr-component"))] + #[cfg(not(rr_component))] { let _ = event; bail!( - "Cannot parse ComponentInstantation replay event without rr-component feature enabled" + "Cannot parse ComponentInstantation replay event without rr and component-model feature enabled" ); } } RREvent::ComponentWasmFuncBegin(event) => { - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] { // Grab the correct component instance let key = event.instance; @@ -269,16 +269,16 @@ impl ReplayInstance { func ); } - #[cfg(not(feature = "rr-component"))] + #[cfg(not(rr_component))] { let _ = event; bail!( - "Cannot parse ComponentWasmFuncBegin replay event without rr-component feature enabled" + "Cannot parse ComponentWasmFuncBegin replay event without rr and component-model feature enabled" ); } } RREvent::ComponentPostReturn(event) => { - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] { // Grab the correct component instance let key = event.instance; @@ -292,11 +292,11 @@ impl ReplayInstance { func.post_return(&mut store)?; } - #[cfg(not(feature = "rr-component"))] + #[cfg(not(rr_component))] { let _ = event; bail!( - "Cannot parse ComponentPostReturn replay event without rr-component feature enabled" + "Cannot parse ComponentPostReturn replay event without rr and component-model feature enabled" ); } } @@ -366,7 +366,7 @@ impl ReplayInstance { { match rr_event { RREvent::ComponentInstantiation(event) => { - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] { // Find matching component from environment to instantiate let component = self @@ -388,16 +388,16 @@ impl ReplayInstance { self.component_instances .insert(instance.id().instance(), instance); } - #[cfg(not(feature = "rr-component"))] + #[cfg(not(rr_component))] { let _ = event; bail!( - "Cannot parse ComponentInstantation replay event without rr-component feature enabled" + "Cannot parse ComponentInstantation replay event without rr and component-model feature enabled" ); } } RREvent::ComponentWasmFuncBegin(event) => { - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] { // Grab the correct component instance let key = event.instance; @@ -457,16 +457,16 @@ impl ReplayInstance { func ); } - #[cfg(not(feature = "rr-component"))] + #[cfg(not(rr_component))] { let _ = event; bail!( - "Cannot parse ComponentWasmFuncBegin replay event without rr-component feature enabled" + "Cannot parse ComponentWasmFuncBegin replay event without rr and component-model feature enabled" ); } } RREvent::ComponentPostReturn(event) => { - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] { // Grab the correct component instance let key = event.instance; @@ -480,11 +480,11 @@ impl ReplayInstance { func.post_return_async(&mut store).await?; } - #[cfg(not(feature = "rr-component"))] + #[cfg(not(rr_component))] { let _ = event; bail!( - "Cannot parse ComponentPostReturn replay event without rr-component feature enabled" + "Cannot parse ComponentPostReturn replay event without rr and component-model feature enabled" ); } } diff --git a/crates/wasmtime/src/runtime/vm/component/libcalls.rs b/crates/wasmtime/src/runtime/vm/component/libcalls.rs index 5e231f6b60..5c22653aee 100644 --- a/crates/wasmtime/src/runtime/vm/component/libcalls.rs +++ b/crates/wasmtime/src/runtime/vm/component/libcalls.rs @@ -84,7 +84,7 @@ wasmtime_environ::foreach_builtin_component_function!(define_builtins); /// implementation following this submodule. mod trampolines { use super::{ComponentInstance, VMComponentContext}; - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] use crate::rr::{Replayer, ResultEvent, component_events::*}; use core::ptr::NonNull; @@ -163,11 +163,11 @@ mod trampolines { // main invoke rule with a record/replay hook wrapper around the above invoke rules // when `rr_builtin`` is provided (@invoke [$rr_entry:ident, $rr_exit:ident] $name:ident($store:ident, $instance:ident,) $($pname:ident)*) => ({ - #[cfg(not(feature = "rr-component"))] + #[cfg(not(rr_component))] { shims!(@invoke $name($store, $instance,) $($pname)*) } - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] { if let Some(buf) = (*$store).replay_buffer_mut() { buf.next_event_validation::(&$rr_entry{ $($pname),* }.into())?; @@ -186,11 +186,11 @@ mod trampolines { // same as above rule for builtins *without* a return value (@invoke [$rr_entry:ident] $name:ident($store:ident, $instance:ident,) $($pname:ident)*) => ({ - #[cfg(not(feature = "rr-component"))] + #[cfg(not(rr_component))] { shims!(@invoke $name($store, $instance,) $($pname)*) } - #[cfg(feature = "rr-component")] + #[cfg(rr_component)] { if let Some(_buf) = (*$store).replay_buffer_mut() { // Just perform replay validation, if required diff --git a/src/commands/replay.rs b/src/commands/replay.rs index ebbf4cfb97..9a9faafe71 100644 --- a/src/commands/replay.rs +++ b/src/commands/replay.rs @@ -92,9 +92,8 @@ impl ReplayCommand { renv.add_module(m.clone()); } #[cfg(feature = "component-model")] - RunTarget::Component(_c) => { - #[cfg(feature = "rr-component")] - renv.add_component(_c.clone()); + RunTarget::Component(c) => { + renv.add_component(c.clone()); } } let mut replay_instance = diff --git a/tests/all/rr.rs b/tests/all/rr.rs index 7ab950ab9a..de4c0466ac 100644 --- a/tests/all/rr.rs +++ b/tests/all/rr.rs @@ -7,7 +7,7 @@ use wasmtime::{ ReplaySettings, Store, }; -#[cfg(feature = "rr-component")] +#[cfg(feature = "component-model")] use wasmtime::component::{Component, HasSelf, Linker as ComponentLinker, bindgen}; struct TestState; @@ -150,7 +150,7 @@ where } /// Run a component test with recording and replay, testing both with and without validation -#[cfg(feature = "rr-component")] +#[cfg(feature = "component-model")] fn run_component_test(component_wat: &str, setup_linker: F, test_fn: R) -> Result<()> where F: Fn(&mut ComponentLinker) -> Result<()> + Clone, @@ -190,7 +190,7 @@ where } /// Run a component test with recording and replay with specified validation setting -#[cfg(feature = "rr-component")] +#[cfg(feature = "component-model")] async fn run_component_test_with_validation( component_wat: &str, setup_linker: F, @@ -361,7 +361,7 @@ fn test_recording_panics_for_core_module_memory_export() { // Few Parameters and Few Results (not exceeding MAX_FLAT_PARAMS=16 and // MAX_FLAT_RESULTS=1) #[test] -#[cfg(feature = "rr-component")] +#[cfg(feature = "component-model")] fn test_component_under_max_params_results() -> Result<()> { mod test { use super::*; @@ -470,7 +470,7 @@ fn test_component_under_max_params_results() -> Result<()> { // Large Record (exceeding MAX_FLAT_PARAMS=16 and MAX_FLAT_RESULTS=1) #[test] -#[cfg(feature = "rr-component")] +#[cfg(feature = "component-model")] fn test_component_over_max_params_results() -> Result<()> { mod test { use super::*; @@ -634,7 +634,7 @@ fn test_component_over_max_params_results() -> Result<()> { } #[test] -#[cfg(feature = "rr-component")] +#[cfg(feature = "component-model")] fn test_component_tuple() -> Result<()> { mod test { use super::*; @@ -730,7 +730,7 @@ fn test_component_tuple() -> Result<()> { } #[test] -#[cfg(feature = "rr-component")] +#[cfg(feature = "component-model")] fn test_component_string() -> Result<()> { mod test { use super::*; @@ -829,7 +829,7 @@ fn test_component_string() -> Result<()> { } #[test] -#[cfg(feature = "rr-component")] +#[cfg(feature = "component-model")] fn test_component_variant() -> Result<()> { mod test { use super::*; @@ -944,7 +944,7 @@ fn test_component_variant() -> Result<()> { } #[test] -#[cfg(feature = "rr-component")] +#[cfg(feature = "component-model")] fn test_component_result() -> Result<()> { mod test { use super::*; @@ -1081,7 +1081,7 @@ fn test_component_result() -> Result<()> { } #[test] -#[cfg(feature = "rr-component")] +#[cfg(feature = "component-model")] fn test_component_list() -> Result<()> { mod test { use super::*; @@ -1211,7 +1211,7 @@ fn test_component_list() -> Result<()> { } #[test] -#[cfg(feature = "rr-component")] +#[cfg(feature = "component-model")] fn test_component_option() -> Result<()> { mod test { use super::*; @@ -1331,7 +1331,7 @@ fn test_component_option() -> Result<()> { test::run() } -#[cfg(feature = "rr-component")] +#[cfg(feature = "component-model")] fn cabi_realloc_wat() -> String { r#" (global $bump (mut i32) (i32.const 256)) @@ -1359,7 +1359,7 @@ fn cabi_realloc_wat() -> String { "#.to_string() } -#[cfg(feature = "rr-component")] +#[cfg(feature = "component-model")] fn shims_wat(params: &str) -> String { let count = params.split_whitespace().count(); let locals_get = (0..count) @@ -1385,7 +1385,7 @@ fn shims_wat(params: &str) -> String { ) } -#[cfg(feature = "rr-component")] +#[cfg(feature = "component-model")] fn instantiation_wat(core_name: &str, lift_sig: &str) -> String { format!( r#" From ae0905dcb0cd261064b649fd3acb856fc4a2b62e Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 25 Nov 2025 18:36:06 -0500 Subject: [PATCH 50/73] Resolve straightforward comments --- Cargo.lock | 1 - crates/cli-flags/src/lib.rs | 8 ++--- .../environ/src/compile/module_artifacts.rs | 4 +-- crates/environ/src/component/artifacts.rs | 6 ++-- crates/environ/src/component/types.rs | 3 +- crates/environ/src/module_artifacts.rs | 27 ++++++++++++++-- crates/wasmtime/Cargo.toml | 1 - crates/wasmtime/src/compile.rs | 6 ++-- crates/wasmtime/src/config.rs | 2 +- .../src/runtime/component/component.rs | 8 ++--- .../src/runtime/component/func/options.rs | 17 +++++----- crates/wasmtime/src/runtime/func.rs | 4 +++ crates/wasmtime/src/runtime/module.rs | 8 ++--- crates/wasmtime/src/runtime/rr.rs | 14 ++++---- crates/wasmtime/src/runtime/rr/core.rs | 17 +++++----- crates/wasmtime/src/runtime/rr/core/events.rs | 6 ++-- .../rr/core/events/component_events.rs | 4 +-- .../src/runtime/rr/core/events/core_events.rs | 3 +- .../src/runtime/rr/hooks/component_hooks.rs | 24 +++++++------- .../wasmtime/src/runtime/rr/replay_driver.rs | 12 +++---- crates/wasmtime/src/runtime/store.rs | 32 ++++++++----------- crates/wasmtime/src/runtime/vm/vmcontext.rs | 13 +++++--- src/commands/replay.rs | 30 +++++++++-------- src/commands/run.rs | 4 +-- tests/all/rr.rs | 4 +-- 25 files changed, 146 insertions(+), 112 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 41687c26b9..5886bed31d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4515,7 +4515,6 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "sha2", "smallvec", "target-lexicon", "tempfile", diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs index 2c476aa944..d043bc7826 100644 --- a/crates/cli-flags/src/lib.rs +++ b/crates/cli-flags/src/lib.rs @@ -501,7 +501,7 @@ 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 (empty string "" for void endpoint) + /// Filename for the recorded execution trace (or empty string to skip writing a file). pub path: Option, /// Include (optional) signatures to facilitate validation checks during replay /// (see `wasmtime replay` for details). @@ -568,12 +568,12 @@ pub struct CommonOptions { /// 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 + /// Generates a serialized trace of the Wasm module execution that captures all /// non-determinism observable by the module. This trace can subsequently be /// re-executed in a determinstic, embedding-agnostic manner (see the `wasmtime replay` command). /// - /// Note: Minimal configs for deterministic Wasm semantics will be - /// enforced during recording by default (NaN canonicalization, deterministic relaxed SIMD) + /// Note: Minimal configuration options for deterministic Wasm semantics will be + /// enforced during recording by default (NaN canonicalization, deterministic relaxed SIMD). #[arg(short = 'R', long = "record", value_name = "KEY[=VAL[,..]]")] #[serde(skip)] record_raw: Vec>, diff --git a/crates/environ/src/compile/module_artifacts.rs b/crates/environ/src/compile/module_artifacts.rs index 55f61f3880..36358cb46b 100644 --- a/crates/environ/src/compile/module_artifacts.rs +++ b/crates/environ/src/compile/module_artifacts.rs @@ -1,6 +1,7 @@ //! Definitions of runtime structures and metadata which are serialized into ELF //! with `postcard` as part of a module's compilation process. +use crate::WasmChecksum; use crate::prelude::*; use crate::{ CompiledModuleInfo, DebugInfoData, FunctionName, MemoryInitialization, Metadata, @@ -9,7 +10,6 @@ use crate::{ use anyhow::{Result, bail}; use object::SectionKind; use object::write::{Object, SectionId, StandardSegment, WritableBuffer}; -use sha2::{Digest, Sha256}; use std::ops::Range; /// Helper structure to create an ELF file as a compilation artifact. @@ -222,7 +222,7 @@ impl<'a> ObjectBuilder<'a> { has_wasm_debuginfo: self.tunables.parse_wasm_debuginfo, dwarf, }, - checksum: Sha256::digest(wasm).into(), + checksum: WasmChecksum::from_binary(wasm), }) } diff --git a/crates/environ/src/component/artifacts.rs b/crates/environ/src/component/artifacts.rs index 7252f25075..725c2bd4c9 100644 --- a/crates/environ/src/component/artifacts.rs +++ b/crates/environ/src/component/artifacts.rs @@ -2,7 +2,7 @@ //! which are serialized with `bincode` into output ELF files. use crate::{ - CompiledFunctionsTable, CompiledModuleInfo, PrimaryMap, StaticModuleIndex, + CompiledFunctionsTable, CompiledModuleInfo, PrimaryMap, StaticModuleIndex, WasmChecksum, component::{Component, ComponentTypes, TypeComponentIndex}, }; use serde_derive::{Deserialize, Serialize}; @@ -20,8 +20,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], + /// A checksum of the source Wasm binary from which the component was compiled. + pub checksum: WasmChecksum, } /// Runtime state that a component retains to support its operation. diff --git a/crates/environ/src/component/types.rs b/crates/environ/src/component/types.rs index 8853290467..438eccec31 100644 --- a/crates/environ/src/component/types.rs +++ b/crates/environ/src/component/types.rs @@ -379,7 +379,7 @@ impl ComponentTypes { /// The intention of this method is to determine the flat ABI on host-to-wasm /// transitions (return from hostcall, or entry into wasmcall). When the type is /// not encodable in flat types, the values are all lowered to memory, implied by - /// the pointer storage + /// the pointer storage. pub fn flat_types_storage_or_pointer( &self, ty: &InterfaceType, @@ -391,6 +391,7 @@ impl ComponentTypes { ); self.flat_types_storage_inner(ty, limit).unwrap_or_else(|| { let mut flat = FlatTypesStorage::new(); + // Pointer representation for wasm32 and wasm64 respectively flat.push(FlatType::I32, FlatType::I64); flat }) diff --git a/crates/environ/src/module_artifacts.rs b/crates/environ/src/module_artifacts.rs index dcd68f63d6..a223e848ec 100644 --- a/crates/environ/src/module_artifacts.rs +++ b/crates/environ/src/module_artifacts.rs @@ -8,6 +8,7 @@ use core::{fmt, u32}; use core::{iter, str}; use cranelift_entity::{EntityRef, PrimaryMap}; use serde_derive::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; /// Description of where a function is located in the text section of a /// compiled image. @@ -28,6 +29,28 @@ impl FunctionLoc { } } +/// The checksum of a Wasm binary. +/// +/// Allows for features requiring the exact same Wasm Module (e.g. deterministic replay) +/// to verify that the binary used matches the one originally compiled. +#[derive(Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Debug, Serialize, Deserialize)] +pub struct WasmChecksum([u8; 32]); + +impl WasmChecksum { + /// Construct a [`WasmChecksum`] from the given wasm binary. + pub fn from_binary(bin: &[u8]) -> WasmChecksum { + WasmChecksum(Sha256::digest(bin).into()) + } +} + +impl core::ops::Deref for WasmChecksum { + type Target = [u8; 32]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + /// A builder for a `CompiledFunctionsTable`. pub struct CompiledFunctionsTableBuilder { inner: CompiledFunctionsTable, @@ -546,8 +569,8 @@ pub struct CompiledModuleInfo { /// Sorted list, by function index, of names we have for this module. pub func_names: Vec, - /// Checksum of the source Wasm binary from which this module was compiled - pub checksum: [u8; 32], + /// Checksum of the source Wasm binary from which this module was compiled. + pub checksum: WasmChecksum, } /// The name of a function stored in the diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml index 707032e253..111d804b9d 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -64,7 +64,6 @@ bitflags = { workspace = true } futures = { workspace = true, features = ["alloc"], optional = true } bytes = { workspace = true, optional = true } embedded-io = { version = "0.6.1", features = ["alloc"], optional = true } -sha2 = { workspace = true } [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 884d1200bf..08862ef68d 100644 --- a/crates/wasmtime/src/compile.rs +++ b/crates/wasmtime/src/compile.rs @@ -26,8 +26,6 @@ use crate::Engine; use crate::hash_map::HashMap; use crate::hash_set::HashSet; use crate::prelude::*; -#[cfg(feature = "component-model")] -use sha2::{Digest, Sha256}; use std::{any::Any, borrow::Cow, collections::BTreeMap, mem, ops::Range}; use call_graph::CallGraph; @@ -38,7 +36,7 @@ use wasmtime_environ::{ CompiledFunctionsTableBuilder, CompiledModuleInfo, Compiler, DefinedFuncIndex, FilePos, FinishedObject, FuncKey, FunctionBodyData, InliningCompiler, IntraModuleInlining, ModuleEnvironment, ModuleTranslation, ModuleTypes, ModuleTypesBuilder, ObjectKind, PrimaryMap, - StaticModuleIndex, Tunables, + StaticModuleIndex, Tunables, WasmChecksum, }; mod call_graph; @@ -208,7 +206,7 @@ pub(crate) fn build_component_artifacts( ty, types, static_modules: compilation_artifacts.modules, - checksum: Sha256::digest(binary).into(), + checksum: WasmChecksum::from_binary(binary), }; object.serialize_info(&artifacts); diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 974a25009d..ba1831a53a 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -92,7 +92,7 @@ impl Default for ModuleVersionStrategy { impl core::hash::Hash for ModuleVersionStrategy { fn hash(&self, hasher: &mut H) { match self { - Self::WasmtimeVersion => env!("CARGO_PKG_VERSION_MAJOR").hash(hasher), + Self::WasmtimeVersion => env!("CARGO_PKG_VERSION").hash(hasher), Self::Custom(s) => s.hash(hasher), Self::None => {} }; diff --git a/crates/wasmtime/src/runtime/component/component.rs b/crates/wasmtime/src/runtime/component/component.rs index 810b976eae..9aae1bc703 100644 --- a/crates/wasmtime/src/runtime/component/component.rs +++ b/crates/wasmtime/src/runtime/component/component.rs @@ -20,7 +20,7 @@ use wasmtime_environ::component::{ GlobalInitializer, InstantiateModule, NameMapNoIntern, OptionsIndex, StaticModuleIndex, TrampolineIndex, TypeComponentIndex, TypeFuncIndex, UnsafeIntrinsic, VMComponentOffsets, }; -use wasmtime_environ::{Abi, CompiledFunctionsTable, FuncKey, TypeTrace}; +use wasmtime_environ::{Abi, CompiledFunctionsTable, FuncKey, TypeTrace, WasmChecksum}; use wasmtime_environ::{FunctionLoc, HostPtr, ObjectKind, PrimaryMap}; /// A compiled WebAssembly Component. @@ -95,8 +95,8 @@ struct ComponentInner { /// locks when calling `realloc` via `TypedFunc::call_raw`. realloc_func_type: Arc, - /// The SHA-256 checksum of the source binary - checksum: [u8; 32], + /// The checksum of the source binary from which the module was compiled. + checksum: WasmChecksum, } pub(crate) struct AllCallFuncPointers { @@ -875,7 +875,7 @@ impl Component { reason = "used only for verification with wasmtime `rr` feature \ and requires a lot of unnecessary gating across crates" )] - pub(crate) fn checksum(&self) -> &[u8; 32] { + pub(crate) fn checksum(&self) -> &WasmChecksum { &self.inner.checksum } diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index beba154f04..a839a6e93a 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -6,7 +6,7 @@ use crate::component::matching::InstanceType; use crate::component::resources::{HostResourceData, HostResourceIndex, HostResourceTables}; use crate::component::{Instance, ResourceType}; use crate::prelude::*; -use crate::rr::{ConstMemorySliceCell, MemorySliceCell}; +use crate::rr::{DynamicMemorySlice, FixedMemorySlice}; #[cfg(rr_component)] use crate::rr::{ RREvent, RecordBuffer, ReplayError, Replayer, ResultEvent, Validate, @@ -269,7 +269,7 @@ 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). #[inline] - pub fn get(&mut self, offset: usize) -> ConstMemorySliceCell<'_, N> { + pub fn get(&mut self, offset: usize) -> FixedMemorySlice<'_, N> { cfg_if::cfg_if! { if #[cfg(rr_component)] { let (slice_mut, recorder) = self.as_slice_mut_with_recorder(); @@ -287,7 +287,7 @@ 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`. - ConstMemorySliceCell { + FixedMemorySlice { bytes: slice_mut[offset..].first_chunk_mut().unwrap(), #[cfg(rr_component)] offset: offset, @@ -303,7 +303,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { /// /// Refer to [`get`](Self::get). #[inline] - pub fn get_dyn(&mut self, offset: usize, size: usize) -> MemorySliceCell<'_> { + pub fn get_dyn(&mut self, offset: usize, size: usize) -> DynamicMemorySlice<'_> { cfg_if::cfg_if! { if #[cfg(rr_component)] { let (slice_mut, recorder) = self.as_slice_mut_with_recorder(); @@ -311,7 +311,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { let slice_mut = self.as_slice_mut(); } } - MemorySliceCell { + DynamicMemorySlice { bytes: &mut slice_mut[offset..][..size], #[cfg(rr_component)] offset: offset, @@ -505,7 +505,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { // Realloc or any lowering methods cannot call back to the host. Hence, you cannot // have host calls entries during this method RREvent::HostFuncEntry(_) => { - bail!("Cannot call back into host during lowering") + 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) => { @@ -525,8 +525,9 @@ impl<'a, T: 'static> LowerContext<'a, T> { lower_store_stack.push(()) } } - - _ => bail!("Invalid event \'{:?}\' encountered during lowering", event), + _ => { + bail!("Invalid event \'{:?}\' encountered during lowering", event); + } }; } diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index c48f3e3886..47d8d6c4af 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -107,6 +107,10 @@ impl NoFunc { } /// Metadata for the origin of a WebAssembly [`Func`] +/// +/// This type captures minimally enough state about a [`Func`] that +/// needs to be recorded on host-to-Wasm invocations so that the +/// it can be reconstructed for replay. #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct WasmFuncOrigin { /// The instance from which the embedded function belongs to. diff --git a/crates/wasmtime/src/runtime/module.rs b/crates/wasmtime/src/runtime/module.rs index 1faadcdc66..0497e50152 100644 --- a/crates/wasmtime/src/runtime/module.rs +++ b/crates/wasmtime/src/runtime/module.rs @@ -22,7 +22,7 @@ use wasmparser::{Parser, ValidPayload, Validator}; use wasmtime_environ::FrameTable; use wasmtime_environ::{ CompiledFunctionsTable, CompiledModuleInfo, EntityIndex, HostPtr, ModuleTypes, ObjectKind, - TypeTrace, VMOffsets, VMSharedTypeIndex, + TypeTrace, VMOffsets, VMSharedTypeIndex, WasmChecksum, }; #[cfg(feature = "gc")] use wasmtime_unwinder::ExceptionTable; @@ -162,8 +162,8 @@ struct ModuleInner { /// Runtime offset information for `VMContext`. offsets: VMOffsets, - /// The SHA-256 checksum of the source binary - checksum: [u8; 32], + /// The checksum of the source binary from which this module was compiled. + checksum: WasmChecksum, } impl fmt::Debug for Module { @@ -900,7 +900,7 @@ impl Module { reason = "used only for verification with wasmtime `rr` feature \ and requires a lot of unnecessary gating across crates" )] - pub(crate) fn checksum(&self) -> &[u8; 32] { + pub(crate) fn checksum(&self) -> &WasmChecksum { &self.inner.checksum } diff --git a/crates/wasmtime/src/runtime/rr.rs b/crates/wasmtime/src/runtime/rr.rs index a6dabe193c..c94bb18fc3 100644 --- a/crates/wasmtime/src/runtime/rr.rs +++ b/crates/wasmtime/src/runtime/rr.rs @@ -11,33 +11,33 @@ use ::core::mem::MaybeUninit; reason = "trait used as a bound for hooks despite not calling methods directly" )] pub trait FlatBytes { - fn bytes_ref(&self, size: u8) -> &[u8]; + fn bytes(&self, size: u8) -> &[u8]; fn from_bytes(value: &[u8]) -> Self; } impl FlatBytes for ValRaw { #[inline] - fn bytes_ref(&self, size: u8) -> &[u8] { + fn bytes(&self, size: u8) -> &[u8] { &self.get_bytes()[..size as usize] } #[inline] fn from_bytes(value: &[u8]) -> Self { - ValRaw::bytes(value) + ValRaw::from_bytes(value) } } impl FlatBytes for MaybeUninit { #[inline] - fn bytes_ref(&self, size: u8) -> &[u8] { + fn bytes(&self, size: u8) -> &[u8] { // Uninitialized data is assumed and serialized, so hence // may contain some undefined values. But these are irrelevant // when serializing to `RRFuncArgVals` let val = unsafe { self.assume_init_ref() }; - val.bytes_ref(size) + val.bytes(size) } #[inline] fn from_bytes(value: &[u8]) -> Self { - MaybeUninit::new(ValRaw::bytes(value)) + MaybeUninit::new(ValRaw::from_bytes(value)) } } @@ -46,7 +46,7 @@ mod hooks; pub(crate) use hooks::{RRWasmFuncType, core_hooks}; #[cfg(feature = "component-model")] pub(crate) use hooks::{ - component_hooks, component_hooks::ConstMemorySliceCell, component_hooks::MemorySliceCell, + component_hooks, component_hooks::DynamicMemorySlice, component_hooks::FixedMemorySlice, }; /// Core infrastructure for RR support diff --git a/crates/wasmtime/src/runtime/rr/core.rs b/crates/wasmtime/src/runtime/rr/core.rs index 04a911e39c..8e6f4a578e 100644 --- a/crates/wasmtime/src/runtime/rr/core.rs +++ b/crates/wasmtime/src/runtime/rr/core.rs @@ -3,7 +3,7 @@ use crate::prelude::*; use core::fmt; use events::EventError; use serde::{Deserialize, Serialize}; -use wasmtime_environ::EntityIndex; +use wasmtime_environ::{EntityIndex, WasmChecksum}; // Use component 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 @@ -38,14 +38,14 @@ 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, + pub deserialize_buffer_size: usize, } impl Default for ReplaySettings { fn default() -> Self { Self { validate: false, - deser_buffer_size: 64, + deserialize_buffer_size: 64, } } } @@ -195,8 +195,8 @@ pub enum ReplayError { InvalidEventPosition, FailedRead(IOError), EventError(Box), - MissingComponent([u8; 32]), - MissingModule([u8; 32]), + MissingComponent(WasmChecksum), + MissingModule(WasmChecksum), MissingComponentInstance(u32), MissingModuleInstance(u32), InvalidCoreFuncIndex(EntityIndex), @@ -552,7 +552,7 @@ impl Replayer for ReplayBuffer { ); } - let deser_buffer = vec![0; settings.deser_buffer_size]; + let deser_buffer = vec![0; settings.deserialize_buffer_size]; let reader = Box::new(reader); Ok(ReplayBuffer { @@ -996,14 +996,15 @@ mod tests { use crate::store::InstanceId; use __component_events::InstantiationEvent as ComponentInstantiationEvent; use core_events::InstantiationEvent as CoreInstantiationEvent; + use wasmtime_environ::WasmChecksum; let component_event = ComponentInstantiationEvent { - component: [0xAB; 32], + component: WasmChecksum::from_binary(&[0xAB; 256]), instance: ComponentInstanceId::from_u32(42), }; let core_event = CoreInstantiationEvent { - module: [0xCD; 32], + module: WasmChecksum::from_binary(&[0xCD; 256]), instance: InstanceId::from_u32(17), }; diff --git a/crates/wasmtime/src/runtime/rr/core/events.rs b/crates/wasmtime/src/runtime/rr/core/events.rs index 5ba8380feb..ba6ad5c79d 100644 --- a/crates/wasmtime/src/runtime/rr/core/events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events.rs @@ -69,7 +69,7 @@ impl RRFuncArgVals { let mut bytes = Vec::new(); let mut sizes = Vec::new(); for (flat_size, arg) in flat.zip(args.iter()) { - bytes.extend_from_slice(&arg.bytes_ref(flat_size)); + bytes.extend_from_slice(&arg.bytes(flat_size)); sizes.push(flat_size); } RRFuncArgVals { bytes, sizes } @@ -104,7 +104,9 @@ impl RRFuncArgVals { let mut pos = 0; let mut vals = Vec::new(); for (flat_size, val_type) in self.sizes.into_iter().zip(val_types.into_iter()) { - let raw = ValRaw::bytes(&self.bytes[pos..pos + flat_size as usize]); + let raw = ValRaw::from_bytes(&self.bytes[pos..pos + flat_size as usize]); + // SAFETY: The safety contract here is the same as that of [`Val::from_raw`]. + // The caller must ensure that raw has the type provided. vals.push(unsafe { Val::from_raw(&mut store, raw, val_type) }); pos += flat_size as usize; } diff --git a/crates/wasmtime/src/runtime/rr/core/events/component_events.rs b/crates/wasmtime/src/runtime/rr/core/events/component_events.rs index 4c1d4551e1..fabecb2a8e 100644 --- a/crates/wasmtime/src/runtime/rr/core/events/component_events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events/component_events.rs @@ -4,7 +4,7 @@ use super::*; use crate::component::ComponentInstanceId; use crate::vm::component::libcalls::ResourceDropRet; use wasmtime_environ::{ - self, + self, WasmChecksum, component::{ExportIndex, InterfaceType}, }; @@ -21,7 +21,7 @@ pub struct WasmFuncBeginEvent { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Ord, PartialOrd)] pub struct InstantiationEvent { /// Checksum of the bytecode used to instantiate the component - pub component: [u8; 32], + pub component: WasmChecksum, /// Instance ID for the instantiated component pub instance: ComponentInstanceId, } diff --git a/crates/wasmtime/src/runtime/rr/core/events/core_events.rs b/crates/wasmtime/src/runtime/rr/core/events/core_events.rs index e81d84e546..e0f8a2feb9 100644 --- a/crates/wasmtime/src/runtime/rr/core/events/core_events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events/core_events.rs @@ -1,12 +1,13 @@ //! Module comprising of core wasm events use super::*; use crate::{WasmFuncOrigin, store::InstanceId}; +use wasmtime_environ::WasmChecksum; /// A core Wasm instantiatation event #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Ord, PartialOrd)] pub struct InstantiationEvent { /// Checksum of the bytecode used to instantiate the module - pub module: [u8; 32], + pub module: WasmChecksum, /// Instance ID for the instantiated module pub instance: InstanceId, } diff --git a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs index b4c877709a..056eb0b961 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs @@ -221,32 +221,32 @@ fn create_host_func_entry_event( } } -/// Same as [`ConstMemorySliceCell`] except allows for dynamically sized slices. +/// Same as [`FixedMemorySlice`] 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 +/// See [`FixedMemorySlice`] for detailed description on the role /// of these types. -pub struct MemorySliceCell<'a> { +pub struct DynamicMemorySlice<'a> { pub bytes: &'a mut [u8], #[cfg(rr_component)] pub offset: usize, #[cfg(rr_component)] pub recorder: Option<&'a mut RecordBuffer>, } -impl<'a> Deref for MemorySliceCell<'a> { +impl<'a> Deref for DynamicMemorySlice<'a> { type Target = [u8]; fn deref(&self) -> &Self::Target { self.bytes } } -impl DerefMut for MemorySliceCell<'_> { +impl DerefMut for DynamicMemorySlice<'_> { fn deref_mut(&mut self) -> &mut Self::Target { self.bytes } } -impl Drop for MemorySliceCell<'_> { +impl Drop for DynamicMemorySlice<'_> { /// Drop serves as a recording hook for stores to the memory slice fn drop(&mut self) { #[cfg(rr_component)] @@ -264,7 +264,7 @@ impl Drop for MemorySliceCell<'_> { /// /// # Purpose and Usage (Read Carefully!) /// -/// This type (and its dynamic counterpart [`MemorySliceCell`]) are critical to +/// This type (and its dynamic counterpart [`DynamicMemorySlice`]) 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: @@ -275,7 +275,7 @@ impl Drop for MemorySliceCell<'_> { /// 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`] +/// ([`FixedMemorySlice`]/[`DynamicMemorySlice`]), which implement [`Drop`] /// allowing us a hook to just capture the final aggregate changes made to guest memory by the host. /// /// ## Critical Invariants @@ -288,25 +288,25 @@ impl Drop for MemorySliceCell<'_> { /// 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> { +pub struct FixedMemorySlice<'a, const N: usize> { pub bytes: &'a mut [u8; N], #[cfg(rr_component)] pub offset: usize, #[cfg(rr_component)] pub recorder: Option<&'a mut RecordBuffer>, } -impl<'a, const N: usize> Deref for ConstMemorySliceCell<'a, N> { +impl<'a, const N: usize> Deref for FixedMemorySlice<'a, N> { type Target = [u8; N]; fn deref(&self) -> &Self::Target { self.bytes } } -impl<'a, const N: usize> DerefMut for ConstMemorySliceCell<'a, N> { +impl<'a, const N: usize> DerefMut for FixedMemorySlice<'a, N> { fn deref_mut(&mut self) -> &mut Self::Target { self.bytes } } -impl<'a, const N: usize> Drop for ConstMemorySliceCell<'a, N> { +impl<'a, const N: usize> Drop for FixedMemorySlice<'a, N> { /// Drops serves as a recording hook for stores to the memory slice fn drop(&mut self) { #[cfg(rr_component)] diff --git a/crates/wasmtime/src/runtime/rr/replay_driver.rs b/crates/wasmtime/src/runtime/rr/replay_driver.rs index 4ebe089990..07d1e38d8d 100644 --- a/crates/wasmtime/src/runtime/rr/replay_driver.rs +++ b/crates/wasmtime/src/runtime/rr/replay_driver.rs @@ -12,17 +12,17 @@ use alloc::{collections::BTreeMap, sync::Arc}; use anyhow::bail; #[cfg(rr_component)] use core::mem::MaybeUninit; -use wasmtime_environ::EntityIndex; #[cfg(rr_component)] use wasmtime_environ::component::{MAX_FLAT_PARAMS, MAX_FLAT_RESULTS}; +use wasmtime_environ::{EntityIndex, WasmChecksum}; /// The environment necessary to produce a [`ReplayInstance`] #[derive(Clone)] pub struct ReplayEnvironment { engine: Engine, - modules: BTreeMap<[u8; 32], Module>, + modules: BTreeMap, #[cfg(rr_component)] - components: BTreeMap<[u8; 32], Component>, + components: BTreeMap, settings: ReplaySettings, } @@ -106,7 +106,7 @@ impl ReplayEnvironment { /// /// # let writer: Cursor> = Cursor::new(Vec::new()); /// # let mut store = Store::new(&engine, ()); -/// # store.init_recording(writer, record_settings)?; +/// # store.record(writer, record_settings)?; /// /// # let instance = linker.instantiate(&mut store, &component)?; /// # let func = instance.get_typed_func::<(), (u32,)>(&mut store, "main")?; @@ -190,7 +190,7 @@ impl ReplayInstance { /// * Wasm function begin events (`ComponentWasmFuncBegin` for components and `CoreWasmFuncEntry` for core) /// /// All other events are transparently dispatched under the context of these top-level events - pub fn run_single_top_level_event(&mut self, rr_event: RREvent) -> Result<()> { + fn run_single_top_level_event(&mut self, rr_event: RREvent) -> Result<()> { match rr_event { RREvent::ComponentInstantiation(event) => { #[cfg(rr_component)] @@ -360,7 +360,7 @@ impl ReplayInstance { /// Exactly like [`Self::run_single_top_level_event`] but uses async stores and calls #[cfg(feature = "async")] - pub async fn run_single_top_level_event_async(&mut self, rr_event: RREvent) -> Result<()> + async fn run_single_top_level_event_async(&mut self, rr_event: RREvent) -> Result<()> where T: Send, { diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index d4fcd00949..88f055d145 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -900,13 +900,12 @@ impl Store { } } - /// Consumes this [`Store`], destroying it and obtain + /// Consumes this [`Store`], destroying it and obtaining /// the [`RecordWriter`] initialized for recording. /// - /// ## Notes - /// - /// The [`RecordWriter`] is never internally updated besides - /// with the `init_recording` API + /// The intended use case of this method is to "take back" the + /// [`RecordWriter`] after all the actions to be recorded within + /// the store have been performed. #[cfg(feature = "rr")] pub fn into_record_writer(mut self) -> Result> { // See [`Store::into_data`] and the `Drop` implementation of @@ -1339,16 +1338,12 @@ impl Store { /// Configure a [`Store`] to enable execution recording /// - /// This must be initialized before instantiating any module within - /// the Store. Recording of events is performed according to provided settings, and + /// This must be perfomed before instantiating any module within + /// the Store. Events are recorded based on the provided settings, and /// written to the provided writer. #[cfg(feature = "rr")] - pub fn init_recording( - &mut self, - recorder: impl RecordWriter, - settings: RecordSettings, - ) -> Result<()> { - self.inner.init_recording(recorder, settings) + pub fn record(&mut self, recorder: impl RecordWriter, settings: RecordSettings) -> Result<()> { + self.inner.record(recorder, settings) } /// Configure a [`Store`] to enable execution replaying @@ -1356,6 +1351,11 @@ impl Store { /// This must be initialized before instantiating any module within /// the Store. Replay of events is performed according to provided settings, and /// read from the provided reader. + /// + /// ## Note + /// + /// This is not a public API; replay executions are wrapped by the `ReplayEnvironment` + /// and `ReplayInstance` abstraction. #[cfg(feature = "rr")] pub(crate) fn init_replaying( &mut self, @@ -1875,11 +1875,7 @@ impl StoreOpaque { } #[cfg(feature = "rr")] - pub fn init_recording( - &mut self, - recorder: impl RecordWriter, - settings: RecordSettings, - ) -> Result<()> { + pub fn record(&mut self, recorder: impl RecordWriter, settings: RecordSettings) -> Result<()> { ensure!( self.instance_count == 0, "store recording must be initialized before instantiating any modules or components" diff --git a/crates/wasmtime/src/runtime/vm/vmcontext.rs b/crates/wasmtime/src/runtime/vm/vmcontext.rs index 5abf782856..d6f65182d4 100644 --- a/crates/wasmtime/src/runtime/vm/vmcontext.rs +++ b/crates/wasmtime/src/runtime/vm/vmcontext.rs @@ -1485,7 +1485,10 @@ pub union ValRaw { /// This value is always stored in a little-endian format. exnref: u32, - /// A representation of underlying union as a byte vector + /// A representation of underlying union as a byte vector. + /// + /// The size of this bytes array should exactly match the size of the union, + /// and be updated accordingly if the size changes. bytes: [u8; 16], } @@ -1495,6 +1498,8 @@ pub union ValRaw { const _: () = { assert!(mem::size_of::() == 16); assert!(mem::align_of::() == mem::align_of::()); + // Assert that the `bytes` field is the same size as the union itself. + assert!(mem::size_of::() == mem::size_of_val(ValRaw::i64(0).get_bytes())); }; // This type is just a bag-of-bits so it's up to the caller to figure out how @@ -1558,7 +1563,7 @@ impl ValRaw { /// Creates a WebAssembly `i64` value #[inline] - pub fn i64(i: i64) -> ValRaw { + pub const fn i64(i: i64) -> ValRaw { ValRaw { i64: i.to_le() } } @@ -1704,13 +1709,13 @@ impl ValRaw { /// Get the WebAssembly value's raw bytes #[inline] - pub fn get_bytes(&self) -> &[u8; mem::size_of::()] { + pub const fn get_bytes(&self) -> &[u8; mem::size_of::()] { unsafe { &self.bytes } } /// Create a WebAssembly value from raw bytes #[inline] - pub fn bytes(value: &[u8]) -> ValRaw { + pub fn from_bytes(value: &[u8]) -> ValRaw { let mut bytes = [0u8; mem::size_of::()]; bytes[..value.len()].copy_from_slice(value); ValRaw { bytes } diff --git a/src/commands/replay.rs b/src/commands/replay.rs index 9a9faafe71..2a2402ef6f 100644 --- a/src/commands/replay.rs +++ b/src/commands/replay.rs @@ -10,31 +10,35 @@ use tokio::time::error::Elapsed; use wasmtime::{Engine, ReplayEnvironment, ReplaySettings, Store}; #[derive(Parser)] -/// Replay-specific options for CLI +/// Replay-specific options for CLI. pub struct ReplayOptions { - /// The path of the recorded trace + /// 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 + /// Execution traces can be obtained with the -R option on other Wasmtime commands + /// (e.g. `wasmtime run` or `wasmtime serve`). See `wasmtime run -R help` for + /// relevant information on recording execution. /// - /// Note: The module used for replay must exactly match that used during recording + /// Note: The module used for replay must exactly match that used during recording. #[arg(short, long, required = true, value_name = "RECORDED TRACE")] pub trace: PathBuf, /// Dynamic checks of record signatures to validate replay consistency. /// /// Requires record traces to be generated with `validation_metadata` enabled. + /// This resembles an internal "safety" assert and verifies extra non-essential events + /// (e.g. return args from Wasm function calls or entry args into host function calls) also + /// match during replay. A failed validation will abort the replay run with an error. #[arg(short, long, default_value_t = false)] pub validate: bool, /// Size of static buffer needed to deserialized variable-length types like String. This is not /// not important for basic functional recording/replaying, but may be required to replay traces where - /// `validate` was enabled for recording + /// `validate` was enabled for recording. #[arg(short, long, default_value_t = 64)] - pub deser_buffer_size: usize, + pub deserialize_buffer_size: usize, } -/// Execute a deterministic, embedding-agnostic replay of a Wasm modules given its associated recorded trace +/// Execute a deterministic, embedding-agnostic replay of a Wasm modules given its associated recorded trace. #[derive(Parser)] pub struct ReplayCommand { #[command(flatten)] @@ -68,9 +72,9 @@ impl ReplayCommand { }) } - /// Execute the store with the replay settings + /// Execute the store with the replay settings. /// - /// Applies similar configurations to `instantiate_and_run` + /// Applies similar configurations to `instantiate_and_run`. async fn instantiate_and_run_replay( self, engine: &Engine, @@ -82,7 +86,7 @@ impl ReplayCommand { // the run configurations, but with potentially certain different options (e.g. fuel consumption). let settings = ReplaySettings { validate: opts.validate, - deser_buffer_size: opts.deser_buffer_size, + deserialize_buffer_size: opts.deserialize_buffer_size, ..Default::default() }; @@ -112,10 +116,10 @@ impl ReplayCommand { }) .await; - // Extract the store for error handling below + // Extract the store for error handling below. let store = replay_instance.extract_store(); - // This is basically the same finish logic as `instantiate_and_run` + // This is basically the same finish logic as `instantiate_and_run`. match result.unwrap_or_else(|elapsed| { Err(anyhow::Error::from(wasmtime::Trap::Interrupt)) .with_context(|| format!("timed out after {elapsed}")) diff --git a/src/commands/run.rs b/src/commands/run.rs index 18defeafef..2e36b79990 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -230,9 +230,9 @@ impl RunCommand { .unwrap_or(default_settings.event_window_size), }; if path.trim().is_empty() { - store.init_recording(io::sink(), settings)?; + store.record(io::sink(), settings)?; } else { - store.init_recording(fs::File::create(&path)?, settings)?; + store.record(fs::File::create(&path)?, settings)?; } } } diff --git a/tests/all/rr.rs b/tests/all/rr.rs index de4c0466ac..d41c611d69 100644 --- a/tests/all/rr.rs +++ b/tests/all/rr.rs @@ -112,7 +112,7 @@ where add_validation: validate, ..Default::default() }; - store.init_recording(writer, record_settings)?; + store.record(writer, record_settings)?; let instance = if is_async { linker.instantiate_async(&mut store, &module).await? @@ -220,7 +220,7 @@ where add_validation: validate, ..Default::default() }; - store.init_recording(writer, record_settings)?; + store.record(writer, record_settings)?; let instance = if is_async { linker.instantiate_async(&mut store, &component).await? From 192cfd4eb560fcd36596fdd49038176a3ea7a758 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 25 Nov 2025 19:14:12 -0500 Subject: [PATCH 51/73] Fold component-model feature into rr --- crates/wasmtime/Cargo.toml | 2 +- crates/wasmtime/build.rs | 4 - .../src/runtime/component/func/host.rs | 12 +- .../src/runtime/component/func/options.rs | 28 +- .../src/runtime/component/instance.rs | 23 +- crates/wasmtime/src/runtime/rr/core.rs | 58 ++- crates/wasmtime/src/runtime/rr/core/events.rs | 2 - .../src/runtime/rr/hooks/component_hooks.rs | 63 ++-- .../src/runtime/rr/hooks/core_hooks.rs | 3 + .../wasmtime/src/runtime/rr/replay_driver.rs | 343 +++++++----------- .../src/runtime/vm/component/libcalls.rs | 10 +- 11 files changed, 234 insertions(+), 314 deletions(-) diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml index 111d804b9d..9dfe029203 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -428,4 +428,4 @@ debug = [ compile-time-builtins = ['dep:wasm-compose', 'dep:tempfile'] # Enable support for the common base infrastructure of record/replay -rr = ["dep:embedded-io"] +rr = ["component-model", "dep:embedded-io"] diff --git a/crates/wasmtime/build.rs b/crates/wasmtime/build.rs index 0e3f64d810..c80e4bf505 100644 --- a/crates/wasmtime/build.rs +++ b/crates/wasmtime/build.rs @@ -33,10 +33,6 @@ fn main() { custom_cfg("has_custom_sync", has_custom_sync); custom_cfg("has_host_compiler_backend", has_host_compiler_backend); - // Component model support for record/replay - let rr_component = cfg!(feature = "rr") && cfg!(feature = "component-model"); - custom_cfg("rr_component", rr_component); - // If this OS isn't supported and no debug-builtins or if Cranelift doesn't support // the host or there's no need to build these helpers. #[cfg(feature = "runtime")] diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 3c98184440..39e30ddb4d 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -8,7 +8,7 @@ use crate::component::types::ComponentFunc; use crate::component::{ComponentNamedList, ComponentType, Instance, Lift, Lower, Val}; use crate::prelude::*; use crate::rr; -#[cfg(rr_component)] +#[cfg(feature = "rr")] use crate::rr::component_hooks::ReplayLoweringPhase; use crate::runtime::vm::component::{ ComponentInstance, VMComponentContext, VMLowering, VMLoweringCallee, @@ -941,7 +941,7 @@ where store.0.store_opaque_mut(), )?; // Replay host function path: Just lower the results from the trace - #[cfg(rr_component)] + #[cfg(feature = "rr")] { let mut cx = LowerContext::new(store, options, instance); // Skip lifting/lowering logic, and just replaying the lowering state @@ -966,11 +966,9 @@ where } } } - #[cfg(not(rr_component))] + #[cfg(not(feature = "rr"))] { - unreachable!( - "cannot reach host function replay when rr and component-model is disabled" - ); + unreachable!("cannot reach host function replay when rr is disabled"); } } @@ -1018,7 +1016,7 @@ unsafe fn dynamic_params_load( } /// Replay of return values from `dynamic_params_load`. Keep in sync -#[cfg(rr_component)] +#[cfg(feature = "rr")] unsafe fn dynamic_params_load_replay(param_tys: &TypeTuple, max_flat_params: usize) -> usize { if let Some(param_count) = param_tys.abi.flat_count(max_flat_params) { param_count diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index a839a6e93a..cad56f239c 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -1,5 +1,5 @@ use crate::StoreContextMut; -#[cfg(rr_component)] +#[cfg(feature = "rr")] use crate::ValRaw; use crate::component::concurrent::ConcurrentState; use crate::component::matching::InstanceType; @@ -7,7 +7,7 @@ use crate::component::resources::{HostResourceData, HostResourceIndex, HostResou use crate::component::{Instance, ResourceType}; use crate::prelude::*; use crate::rr::{DynamicMemorySlice, FixedMemorySlice}; -#[cfg(rr_component)] +#[cfg(feature = "rr")] use crate::rr::{ RREvent, RecordBuffer, ReplayError, Replayer, ResultEvent, Validate, component_events::ReallocEntryEvent, component_events::ReallocReturnEvent, @@ -19,7 +19,7 @@ use crate::runtime::vm::component::{ }; use crate::store::{StoreId, StoreOpaque}; use alloc::sync::Arc; -#[cfg(rr_component)] +#[cfg(feature = "rr")] use core::mem::MaybeUninit; use core::pin::Pin; use core::ptr::NonNull; @@ -126,7 +126,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { } /// Return the `InstanceFlags` for this context - #[cfg(rr_component)] + #[cfg(feature = "rr")] fn instance_flags(&self) -> InstanceFlags { let vminstance = self.instance.id().get(self.store.0); let opts = self.options(); @@ -139,7 +139,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { /// # Panics /// /// See [`as_slice`](Self::as_slice) - #[cfg(rr_component)] + #[cfg(feature = "rr")] fn as_slice_mut_with_recorder(&mut self) -> (&mut [u8], Option<&mut RecordBuffer>) { self.instance .options_memory_mut_with_recorder(self.store.0, self.options) @@ -243,7 +243,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { old_align: u32, new_size: usize, ) -> Result { - #[cfg(rr_component)] + #[cfg(feature = "rr")] self.store.0.record_event(|| ReallocEntryEvent { old_addr: old, old_size, @@ -251,7 +251,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { new_size, })?; let result = self.realloc_inner(old, old_size, old_align, new_size); - #[cfg(rr_component)] + #[cfg(feature = "rr")] self.store.0.record_event_validation(|| { ReallocReturnEvent(ResultEvent::from_anyhow_result(&result)) })?; @@ -271,7 +271,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { #[inline] pub fn get(&mut self, offset: usize) -> FixedMemorySlice<'_, N> { cfg_if::cfg_if! { - if #[cfg(rr_component)] { + if #[cfg(feature = "rr")] { let (slice_mut, recorder) = self.as_slice_mut_with_recorder(); } else { let slice_mut = self.as_slice_mut(); @@ -289,9 +289,9 @@ impl<'a, T: 'static> LowerContext<'a, T> { // of `unsafe`. FixedMemorySlice { bytes: slice_mut[offset..].first_chunk_mut().unwrap(), - #[cfg(rr_component)] + #[cfg(feature = "rr")] offset: offset, - #[cfg(rr_component)] + #[cfg(feature = "rr")] recorder: recorder, } } @@ -305,7 +305,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { #[inline] pub fn get_dyn(&mut self, offset: usize, size: usize) -> DynamicMemorySlice<'_> { cfg_if::cfg_if! { - if #[cfg(rr_component)] { + if #[cfg(feature = "rr")] { let (slice_mut, recorder) = self.as_slice_mut_with_recorder(); } else { let slice_mut = self.as_slice_mut(); @@ -313,9 +313,9 @@ impl<'a, T: 'static> LowerContext<'a, T> { } DynamicMemorySlice { bytes: &mut slice_mut[offset..][..size], - #[cfg(rr_component)] + #[cfg(feature = "rr")] offset: offset, - #[cfg(rr_component)] + #[cfg(feature = "rr")] recorder: recorder, } } @@ -418,7 +418,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { /// * It is assumed that this is only invoked at the root lower/store calls /// * Panics if invoked while replay is not enabled /// - #[cfg(rr_component)] + #[cfg(feature = "rr")] pub fn replay_lowering( &mut self, mut result_storage: Option<&mut [MaybeUninit]>, diff --git a/crates/wasmtime/src/runtime/component/instance.rs b/crates/wasmtime/src/runtime/component/instance.rs index 95a8bed78a..af302b6759 100644 --- a/crates/wasmtime/src/runtime/component/instance.rs +++ b/crates/wasmtime/src/runtime/component/instance.rs @@ -8,8 +8,8 @@ use crate::component::{ use crate::instance::OwnedImports; use crate::linker::DefinitionType; use crate::prelude::*; -#[cfg(rr_component)] -use crate::rr::RecordBuffer; +#[cfg(feature = "rr")] +use crate::rr::{RecordBuffer, component_events::InstantiationEvent}; use crate::runtime::vm::component::{ CallContexts, ComponentInstance, ResourceTables, TypedResource, TypedResourceIndex, }; @@ -530,7 +530,7 @@ impl Instance { } } - #[cfg(rr_component)] + #[cfg(feature = "rr")] pub(crate) fn options_memory_mut_with_recorder<'a>( &self, store: &'a mut StoreOpaque, @@ -1080,7 +1080,7 @@ impl<'a> Instantiator<'a> { /// Convenience helper to return the instance ID of the `ComponentInstance` that's /// being instantiated - #[cfg(rr_component)] + #[cfg(feature = "rr")] fn id(&self) -> ComponentInstanceId { self.id } @@ -1193,14 +1193,13 @@ impl InstancePre { .allocator() .increment_component_instance_count()?; let mut instantiator = Instantiator::new(&self.component, store.0, &self.imports); - #[cfg(rr_component)] - { - use crate::rr::component_events::InstantiationEvent; - store.0.record_event(|| InstantiationEvent { - component: *self.component.checksum(), - instance: instantiator.id(), - })?; - } + + #[cfg(feature = "rr")] + store.0.record_event(|| InstantiationEvent { + component: *self.component.checksum(), + instance: instantiator.id(), + })?; + instantiator.run(&mut store).await.map_err(|e| { store .engine() diff --git a/crates/wasmtime/src/runtime/rr/core.rs b/crates/wasmtime/src/runtime/rr/core.rs index 8e6f4a578e..785978c1a4 100644 --- a/crates/wasmtime/src/runtime/rr/core.rs +++ b/crates/wasmtime/src/runtime/rr/core.rs @@ -2,17 +2,13 @@ use crate::config::ModuleVersionStrategy; use crate::prelude::*; use core::fmt; use events::EventError; +pub use events::{ + RRFuncArgVals, ResultEvent, Validate, common_events, component_events, core_events, + marker_events, +}; +pub use io::{IOError, RecordWriter, ReplayReader}; use serde::{Deserialize, Serialize}; use wasmtime_environ::{EntityIndex, WasmChecksum}; -// Use component 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; -#[cfg(rr_component)] -pub use events::component_events; -use events::component_events as __component_events; -pub use events::{RRFuncArgVals, ResultEvent, common_events, core_events, marker_events}; -pub use io::{IOError, RecordWriter, ReplayReader}; /// Settings for execution recording. #[derive(Debug, Clone, Serialize, Deserialize)] @@ -141,23 +137,23 @@ rr_event! { /// /// This is distinguished from `ComponentWasmFuncEntry` as there may /// be multiple lowering steps before actually entering the Wasm function - ComponentWasmFuncBegin(__component_events::WasmFuncBeginEvent), + ComponentWasmFuncBegin(component_events::WasmFuncBeginEvent), /// Entry from the host into the Wasm component function - ComponentWasmFuncEntry(__component_events::WasmFuncEntryEvent), + ComponentWasmFuncEntry(component_events::WasmFuncEntryEvent), /// Instantiation of a component - ComponentInstantiation(__component_events::InstantiationEvent), + ComponentInstantiation(component_events::InstantiationEvent), /// Component ABI realloc call in linear wasm memory - ComponentReallocEntry(__component_events::ReallocEntryEvent), + ComponentReallocEntry(component_events::ReallocEntryEvent), /// Return from a type lowering operation - ComponentLowerFlatReturn(__component_events::LowerFlatReturnEvent), + ComponentLowerFlatReturn(component_events::LowerFlatReturnEvent), /// Return from a store during a type lowering operation - ComponentLowerMemoryReturn(__component_events::LowerMemoryReturnEvent), + ComponentLowerMemoryReturn(component_events::LowerMemoryReturnEvent), /// An attempt to obtain a mutable slice into Wasm linear memory - ComponentMemorySliceWrite(__component_events::MemorySliceWriteEvent), + ComponentMemorySliceWrite(component_events::MemorySliceWriteEvent), /// Return from a component builtin - ComponentBuiltinReturn(__component_events::BuiltinReturnEvent), + ComponentBuiltinReturn(component_events::BuiltinReturnEvent), /// Call to `post_return` (after the function call) - ComponentPostReturn(__component_events::PostReturnEvent), + ComponentPostReturn(component_events::PostReturnEvent), // OPTIONAL events for replay validation (Component) @@ -166,13 +162,13 @@ rr_event! { /// Since realloc is deterministic, ReallocReturn is optional. /// Any error is subsumed by the containing LowerReturn/LowerStoreReturn /// that triggered realloc - ComponentReallocReturn(__component_events::ReallocReturnEvent), + ComponentReallocReturn(component_events::ReallocReturnEvent), /// Call into type lowering for flat destination - ComponentLowerFlatEntry(__component_events::LowerFlatEntryEvent), + ComponentLowerFlatEntry(component_events::LowerFlatEntryEvent), /// Call into type lowering for memory destination - ComponentLowerMemoryEntry(__component_events::LowerMemoryEntryEvent), + ComponentLowerMemoryEntry(component_events::LowerMemoryEntryEvent), /// Call into a component builtin - ComponentBuiltinEntry(__component_events::BuiltinEntryEvent) + ComponentBuiltinEntry(component_events::BuiltinEntryEvent) } impl RREvent { @@ -708,7 +704,7 @@ mod tests { origin: origin.clone(), args: RRFuncArgVals::from_flat_iter(&values, flat_sizes.iter().copied()), })?; - recorder.record_event(|| __component_events::WasmFuncEntryEvent { + recorder.record_event(|| component_events::WasmFuncEntryEvent { args: RRFuncArgVals::from_flat_iter( &return_values, return_flat_sizes.iter().copied(), @@ -724,7 +720,7 @@ mod tests { assert!(origin == replay_origin.unwrap()); verify_equal_slices(&values, &replay_values, &flat_sizes)?; - replayer.next_event_and(|event: __component_events::WasmFuncEntryEvent| { + replayer.next_event_and(|event: component_events::WasmFuncEntryEvent| { event.args.into_raw_slice(&mut return_replay_values); Ok(()) })?; @@ -735,7 +731,7 @@ mod tests { #[test] fn builtin_event_entry() -> Result<()> { - use __component_events::{ + use component_events::{ BuiltinEntryEvent, ResourceDropEntryEvent, ResourceEnterCallEntryEvent, ResourceExitCallEntryEvent, ResourceTransferBorrowEntryEvent, ResourceTransferOwnEntryEvent, @@ -781,7 +777,7 @@ mod tests { #[test] fn builtin_event_return() -> Result<()> { - use __component_events::{ + use component_events::{ BuiltinError, BuiltinReturnEvent, ResourceDropReturnEvent, ResourceExitCallReturnEvent, ResourceRep32ReturnEvent, ResourceTransferBorrowReturnEvent, ResourceTransferOwnReturnEvent, @@ -870,7 +866,7 @@ mod tests { #[test] fn lower_flat_events() -> Result<()> { - use __component_events::{LowerFlatEntryEvent, LowerFlatReturnEvent}; + use component_events::{LowerFlatEntryEvent, LowerFlatReturnEvent}; use wasmtime_environ::component::InterfaceType; let entry = LowerFlatEntryEvent { @@ -900,7 +896,7 @@ mod tests { #[test] fn lower_memory_events() -> Result<()> { - use __component_events::{LowerMemoryEntryEvent, LowerMemoryReturnEvent}; + use component_events::{LowerMemoryEntryEvent, LowerMemoryReturnEvent}; use wasmtime_environ::component::InterfaceType; let entry = LowerMemoryEntryEvent { @@ -932,7 +928,7 @@ mod tests { #[test] fn realloc_events() -> Result<()> { - use __component_events::{ReallocEntryEvent, ReallocReturnEvent}; + use component_events::{ReallocEntryEvent, ReallocReturnEvent}; let entry = ReallocEntryEvent { old_addr: 0x1000, @@ -967,7 +963,7 @@ mod tests { #[test] fn memory_slice_write_event() -> Result<()> { - use __component_events::MemorySliceWriteEvent; + use component_events::MemorySliceWriteEvent; let event = MemorySliceWriteEvent { offset: 512, @@ -994,7 +990,7 @@ mod tests { fn instantiation_event() -> Result<()> { use crate::component::ComponentInstanceId; use crate::store::InstanceId; - use __component_events::InstantiationEvent as ComponentInstantiationEvent; + use component_events::InstantiationEvent as ComponentInstantiationEvent; use core_events::InstantiationEvent as CoreInstantiationEvent; use wasmtime_environ::WasmChecksum; diff --git a/crates/wasmtime/src/runtime/rr/core/events.rs b/crates/wasmtime/src/runtime/rr/core/events.rs index ba6ad5c79d..c9c4a6c4f5 100644 --- a/crates/wasmtime/src/runtime/rr/core/events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events.rs @@ -4,7 +4,6 @@ use crate::{AsContextMut, Val, prelude::*}; use crate::{ValRaw, ValType}; use core::fmt; use serde::{Deserialize, Serialize}; -#[cfg(rr_component)] use wasmtime_environ::component::FlatTypesStorage; /// A serde compatible representation of errors produced during execution @@ -76,7 +75,6 @@ impl RRFuncArgVals { } /// Construct [`RRFuncArgVals`] from raw value buffer and a [`FlatTypesStorage`] - #[cfg(rr_component)] #[inline] pub fn from_flat_storage(args: &[T], flat: FlatTypesStorage) -> RRFuncArgVals where diff --git a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs index 056eb0b961..3d17d05afa 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs @@ -1,30 +1,27 @@ use crate::ValRaw; -use crate::component::ComponentInstanceId; -#[cfg(rr_component)] -use crate::rr::{RecordBuffer, Recorder, component_events::MemorySliceWriteEvent}; - -use core::ops::{Deref, DerefMut}; - -use crate::component::func::LowerContext; -#[cfg(rr_component)] +use crate::component::{ComponentInstanceId, func::LowerContext}; +#[cfg(feature = "rr")] use crate::rr::common_events::{HostFuncEntryEvent, WasmFuncReturnEvent}; -#[cfg(rr_component)] +#[cfg(feature = "rr")] use crate::rr::component_events::{ LowerFlatEntryEvent, LowerFlatReturnEvent, LowerMemoryEntryEvent, LowerMemoryReturnEvent, - PostReturnEvent, WasmFuncBeginEvent, WasmFuncEntryEvent, + MemorySliceWriteEvent, PostReturnEvent, WasmFuncBeginEvent, WasmFuncEntryEvent, +}; +#[cfg(feature = "rr")] +use crate::rr::{ + RRFuncArgVals, RecordBuffer, Recorder, ResultEvent, common_events::HostFuncReturnEvent, }; -#[cfg(rr_component)] -use crate::rr::{RRFuncArgVals, ResultEvent, common_events::HostFuncReturnEvent}; use crate::store::StoreOpaque; use crate::{StoreContextMut, prelude::*}; use alloc::sync::Arc; use core::mem::MaybeUninit; +use core::ops::{Deref, DerefMut}; use wasmtime_environ::component::{ComponentTypes, ExportIndex, InterfaceType, TypeFuncIndex}; -#[cfg(all(rr_component))] +#[cfg(all(feature = "rr"))] use wasmtime_environ::component::{MAX_FLAT_PARAMS, MAX_FLAT_RESULTS}; /// Indicator type signalling the context during lowering -#[cfg(rr_component)] +#[cfg(feature = "rr")] #[derive(Debug)] pub enum ReplayLoweringPhase { WasmFuncEntry, @@ -41,7 +38,7 @@ pub fn record_wasm_func_begin( func_idx: ExportIndex, store: &mut StoreOpaque, ) -> Result<()> { - #[cfg(rr_component)] + #[cfg(feature = "rr")] store.record_event(|| WasmFuncBeginEvent { instance, func_idx })?; let _ = (instance, func_idx, store); Ok(()) @@ -54,7 +51,7 @@ pub fn record_wasm_func_post_return( func_idx: ExportIndex, store: &mut StoreContextMut<'_, T>, ) -> Result<()> { - #[cfg(rr_component)] + #[cfg(feature = "rr")] store .0 .record_event(|| PostReturnEvent { instance, func_idx })?; @@ -76,7 +73,7 @@ where F: FnOnce(&mut StoreContextMut<'_, T>) -> Result<()>, { let _ = (args, type_idx, &types); - #[cfg(rr_component)] + #[cfg(feature = "rr")] store.0.record_event(|| { let flat_params = types.flat_types_storage_or_pointer( &InterfaceType::Tuple(types[type_idx].params), @@ -87,7 +84,7 @@ where } })?; let result = wasm_call(store); - #[cfg(rr_component)] + #[cfg(feature = "rr")] { if let Err(e) = &result { log::warn!("Wasm function call exited with error: {:?}", e); @@ -108,7 +105,7 @@ where result?; Ok(()) } - #[cfg(not(rr_component))] + #[cfg(not(feature = "rr"))] { result } @@ -122,7 +119,7 @@ pub fn record_validate_host_func_entry( param_tys: &InterfaceType, store: &mut StoreOpaque, ) -> Result<()> { - #[cfg(rr_component)] + #[cfg(feature = "rr")] store.record_event_validation(|| create_host_func_entry_event(args, types, param_tys))?; let _ = (args, types, param_tys, store); Ok(()) @@ -136,7 +133,7 @@ pub fn replay_validate_host_func_entry( param_tys: &InterfaceType, store: &mut StoreOpaque, ) -> Result<()> { - #[cfg(rr_component)] + #[cfg(feature = "rr")] store.next_replay_event_validation::(|| { create_host_func_entry_event(args, types, param_tys) })?; @@ -152,7 +149,7 @@ pub fn record_host_func_return( ty: &InterfaceType, store: &mut StoreOpaque, ) -> Result<()> { - #[cfg(rr_component)] + #[cfg(feature = "rr")] store.record_event(|| { let flat_results = types.flat_types_storage_or_pointer(&ty, MAX_FLAT_RESULTS); HostFuncReturnEvent { @@ -174,12 +171,12 @@ pub fn record_lower_memory( where F: FnOnce(&mut LowerContext<'_, T>, InterfaceType, usize) -> Result<()>, { - #[cfg(rr_component)] + #[cfg(feature = "rr")] cx.store .0 .record_event_validation(|| LowerMemoryEntryEvent { ty, offset })?; let store_result = lower_store(cx, ty, offset); - #[cfg(rr_component)] + #[cfg(feature = "rr")] cx.store .0 .record_event(|| LowerMemoryReturnEvent(ResultEvent::from_anyhow_result(&store_result)))?; @@ -196,19 +193,19 @@ pub fn record_lower_flat( where F: FnOnce(&mut LowerContext<'_, T>, InterfaceType) -> Result<()>, { - #[cfg(rr_component)] + #[cfg(feature = "rr")] cx.store .0 .record_event_validation(|| LowerFlatEntryEvent { ty })?; let lower_result = lower(cx, ty); - #[cfg(rr_component)] + #[cfg(feature = "rr")] cx.store .0 .record_event(|| LowerFlatReturnEvent(ResultEvent::from_anyhow_result(&lower_result)))?; lower_result } -#[cfg(rr_component)] +#[cfg(feature = "rr")] #[inline(always)] fn create_host_func_entry_event( args: &mut [MaybeUninit], @@ -230,9 +227,9 @@ fn create_host_func_entry_event( /// of these types. pub struct DynamicMemorySlice<'a> { pub bytes: &'a mut [u8], - #[cfg(rr_component)] + #[cfg(feature = "rr")] pub offset: usize, - #[cfg(rr_component)] + #[cfg(feature = "rr")] pub recorder: Option<&'a mut RecordBuffer>, } impl<'a> Deref for DynamicMemorySlice<'a> { @@ -249,7 +246,7 @@ impl DerefMut for DynamicMemorySlice<'_> { impl Drop for DynamicMemorySlice<'_> { /// Drop serves as a recording hook for stores to the memory slice fn drop(&mut self) { - #[cfg(rr_component)] + #[cfg(feature = "rr")] if let Some(buf) = &mut self.recorder { buf.record_event(|| MemorySliceWriteEvent { offset: self.offset, @@ -290,9 +287,9 @@ impl Drop for DynamicMemorySlice<'_> { /// [`realloc`](LowerContext::realloc), preventing any interleavings in between the borrow and drop of the slice. pub struct FixedMemorySlice<'a, const N: usize> { pub bytes: &'a mut [u8; N], - #[cfg(rr_component)] + #[cfg(feature = "rr")] pub offset: usize, - #[cfg(rr_component)] + #[cfg(feature = "rr")] pub recorder: Option<&'a mut RecordBuffer>, } impl<'a, const N: usize> Deref for FixedMemorySlice<'a, N> { @@ -309,7 +306,7 @@ impl<'a, const N: usize> DerefMut for FixedMemorySlice<'a, N> { impl<'a, const N: usize> Drop for FixedMemorySlice<'a, N> { /// Drops serves as a recording hook for stores to the memory slice fn drop(&mut self) { - #[cfg(rr_component)] + #[cfg(feature = "rr")] if let Some(buf) = &mut self.recorder { buf.record_event(|| MemorySliceWriteEvent { offset: self.offset, diff --git a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs index 00097b5214..7b1b7c3a09 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs @@ -39,6 +39,9 @@ where #[cfg(feature = "rr")] { if origin.is_some() { + if let Err(e) = &result { + log::warn!("Wasm function call exited with error: {:?}", e); + } let flat = ty.results().map(|t| t.to_wasm_type().byte_size()); let result = result.map(|_| RRFuncArgVals::from_flat_iter(args, flat)); store.0.record_event_validation(|| { diff --git a/crates/wasmtime/src/runtime/rr/replay_driver.rs b/crates/wasmtime/src/runtime/rr/replay_driver.rs index 07d1e38d8d..bbfc4ce5ef 100644 --- a/crates/wasmtime/src/runtime/rr/replay_driver.rs +++ b/crates/wasmtime/src/runtime/rr/replay_driver.rs @@ -1,18 +1,12 @@ -use crate::rr::Validate; -use crate::rr::{RREvent, ReplayError, core_events}; +use crate::rr::{RREvent, ReplayError, Validate, core_events}; use crate::store::InstanceId; use crate::{AsContextMut, Engine, Module, ReplayReader, ReplaySettings, Store, prelude::*}; -#[cfg(rr_component)] use crate::{ ValRaw, component, component::Component, component::ComponentInstanceId, rr::component_events, rr::component_hooks, }; use alloc::{collections::BTreeMap, sync::Arc}; -#[cfg(not(rr_component))] -use anyhow::bail; -#[cfg(rr_component)] use core::mem::MaybeUninit; -#[cfg(rr_component)] use wasmtime_environ::component::{MAX_FLAT_PARAMS, MAX_FLAT_RESULTS}; use wasmtime_environ::{EntityIndex, WasmChecksum}; @@ -21,7 +15,6 @@ use wasmtime_environ::{EntityIndex, WasmChecksum}; pub struct ReplayEnvironment { engine: Engine, modules: BTreeMap, - #[cfg(rr_component)] components: BTreeMap, settings: ReplaySettings, } @@ -32,7 +25,6 @@ impl ReplayEnvironment { Self { engine: engine.clone(), modules: BTreeMap::new(), - #[cfg(rr_component)] components: BTreeMap::new(), settings, } @@ -45,7 +37,6 @@ impl ReplayEnvironment { } /// Add a [`Component`] to the replay environment - #[cfg(rr_component)] pub fn add_component(&mut self, component: Component) -> &mut Self { self.components.insert(*component.checksum(), component); self @@ -134,11 +125,9 @@ impl ReplayEnvironment { pub struct ReplayInstance { env: Arc, store: Store, - #[cfg(rr_component)] component_linker: component::Linker, module_linker: crate::Linker, module_instances: BTreeMap, - #[cfg(rr_component)] component_instances: BTreeMap, } @@ -155,20 +144,16 @@ impl ReplayInstance { for module in env.modules.values() { module_linker.define_unknown_imports_as_traps(module)?; } - #[cfg(rr_component)] let mut component_linker = component::Linker::::new(&env.engine); - #[cfg(rr_component)] for component in env.components.values() { component_linker.define_unknown_imports_as_traps(component)?; } Ok(Self { env, store, - #[cfg(rr_component)] component_linker, module_linker, module_instances: BTreeMap::new(), - #[cfg(rr_component)] component_instances: BTreeMap::new(), }) } @@ -193,112 +178,90 @@ impl ReplayInstance { fn run_single_top_level_event(&mut self, rr_event: RREvent) -> Result<()> { match rr_event { RREvent::ComponentInstantiation(event) => { - #[cfg(rr_component)] - { - // Find matching component from environment to instantiate - let component = self - .env - .components - .get(&event.component) - .ok_or(ReplayError::MissingComponent(event.component))?; - - let instance = self - .component_linker - .instantiate(self.store.as_context_mut(), component)?; - // Validate the instantiation event - event.validate(&component_events::InstantiationEvent { - component: *component.checksum(), - instance: instance.id().instance(), - })?; - - self.component_instances - .insert(instance.id().instance(), instance); - } - #[cfg(not(rr_component))] - { - let _ = event; - bail!( - "Cannot parse ComponentInstantation replay event without rr and component-model feature enabled" - ); - } + // Find matching component from environment to instantiate + let component = self + .env + .components + .get(&event.component) + .ok_or(ReplayError::MissingComponent(event.component))?; + + let instance = self + .component_linker + .instantiate(self.store.as_context_mut(), component)?; + // Validate the instantiation event + event.validate(&component_events::InstantiationEvent { + component: *component.checksum(), + instance: instance.id().instance(), + })?; + + self.component_instances + .insert(instance.id().instance(), instance); } RREvent::ComponentWasmFuncBegin(event) => { - #[cfg(rr_component)] - { - // Grab the correct component instance - let key = event.instance; - let instance = self - .component_instances - .get_mut(&key) - .ok_or(ReplayError::MissingComponentInstance(key.as_u32()))?; - - // Replay lowering steps and obtain raw value arguments to raw function call - let func = component::Func::from_lifted_func(*instance, event.func_idx); - let store = self.store.as_context_mut(); - - // Call the function - // - // This is almost a mirror of the usage in [`component::Func::call_impl`] - let mut results_storage = [component::Val::U64(0); MAX_FLAT_RESULTS]; - let mut num_results = 0; - let results = &mut results_storage; - let _return = unsafe { - func.call_raw( - store, - |cx, _, dst: &mut MaybeUninit<[MaybeUninit; MAX_FLAT_PARAMS]>| { - // For lowering, use replay instead of actual lowering - let dst: &mut [MaybeUninit] = dst.assume_init_mut(); - cx.replay_lowering(Some(dst), component_hooks::ReplayLoweringPhase::WasmFuncEntry) - }, - |cx, results_ty, src: &[ValRaw; MAX_FLAT_RESULTS]| { - // Lifting can proceed exactly as normal - for (result, slot) in - component::Func::lift_results(cx, results_ty, src, MAX_FLAT_RESULTS)?.zip(results) - { - *slot = result?; - num_results += 1; - } - Ok(()) - }, - )? - }; + // Grab the correct component instance + let key = event.instance; + let instance = self + .component_instances + .get_mut(&key) + .ok_or(ReplayError::MissingComponentInstance(key.as_u32()))?; - log::info!( - "Returned {:?} for calling {:?}", - &results_storage[..num_results], - func - ); - } - #[cfg(not(rr_component))] - { - let _ = event; - bail!( - "Cannot parse ComponentWasmFuncBegin replay event without rr and component-model feature enabled" - ); - } + // Replay lowering steps and obtain raw value arguments to raw function call + let func = component::Func::from_lifted_func(*instance, event.func_idx); + let store = self.store.as_context_mut(); + + // Call the function + // + // This is almost a mirror of the usage in [`component::Func::call_impl`] + let mut results_storage = [component::Val::U64(0); MAX_FLAT_RESULTS]; + let mut num_results = 0; + let results = &mut results_storage; + let _return = unsafe { + func.call_raw( + store, + |cx, _, dst: &mut MaybeUninit<[MaybeUninit; MAX_FLAT_PARAMS]>| { + // For lowering, use replay instead of actual lowering + let dst: &mut [MaybeUninit] = dst.assume_init_mut(); + cx.replay_lowering( + Some(dst), + component_hooks::ReplayLoweringPhase::WasmFuncEntry, + ) + }, + |cx, results_ty, src: &[ValRaw; MAX_FLAT_RESULTS]| { + // Lifting can proceed exactly as normal + for (result, slot) in component::Func::lift_results( + cx, + results_ty, + src, + MAX_FLAT_RESULTS, + )? + .zip(results) + { + *slot = result?; + num_results += 1; + } + Ok(()) + }, + )? + }; + + log::info!( + "Returned {:?} for calling {:?}", + &results_storage[..num_results], + func + ); } RREvent::ComponentPostReturn(event) => { - #[cfg(rr_component)] - { - // Grab the correct component instance - let key = event.instance; - let instance = self - .component_instances - .get_mut(&key) - .ok_or(ReplayError::MissingComponentInstance(key.as_u32()))?; - - let func = component::Func::from_lifted_func(*instance, event.func_idx); - let mut store = self.store.as_context_mut(); - - func.post_return(&mut store)?; - } - #[cfg(not(rr_component))] - { - let _ = event; - bail!( - "Cannot parse ComponentPostReturn replay event without rr and component-model feature enabled" - ); - } + // Grab the correct component instance + let key = event.instance; + let instance = self + .component_instances + .get_mut(&key) + .ok_or(ReplayError::MissingComponentInstance(key.as_u32()))?; + + let func = component::Func::from_lifted_func(*instance, event.func_idx); + let mut store = self.store.as_context_mut(); + + func.post_return(&mut store)?; } RREvent::CoreWasmInstantiation(event) => { // Find matching module from environment to instantiate @@ -366,59 +329,47 @@ impl ReplayInstance { { match rr_event { RREvent::ComponentInstantiation(event) => { - #[cfg(rr_component)] - { - // Find matching component from environment to instantiate - let component = self - .env - .components - .get(&event.component) - .ok_or(ReplayError::MissingComponent(event.component))?; - - let instance = self - .component_linker - .instantiate_async(self.store.as_context_mut(), component) - .await?; - // Validate the instantiation event - event.validate(&component_events::InstantiationEvent { - component: *component.checksum(), - instance: instance.id().instance(), - })?; - - self.component_instances - .insert(instance.id().instance(), instance); - } - #[cfg(not(rr_component))] - { - let _ = event; - bail!( - "Cannot parse ComponentInstantation replay event without rr and component-model feature enabled" - ); - } + // Find matching component from environment to instantiate + let component = self + .env + .components + .get(&event.component) + .ok_or(ReplayError::MissingComponent(event.component))?; + + let instance = self + .component_linker + .instantiate_async(self.store.as_context_mut(), component) + .await?; + // Validate the instantiation event + event.validate(&component_events::InstantiationEvent { + component: *component.checksum(), + instance: instance.id().instance(), + })?; + + self.component_instances + .insert(instance.id().instance(), instance); } RREvent::ComponentWasmFuncBegin(event) => { - #[cfg(rr_component)] - { - // Grab the correct component instance - let key = event.instance; - let instance = self - .component_instances - .get_mut(&key) - .ok_or(ReplayError::MissingComponentInstance(key.as_u32()))?; - - // Replay lowering steps and obtain raw value arguments to raw function call - let func = component::Func::from_lifted_func(*instance, event.func_idx); - let mut store = self.store.as_context_mut(); - - // Call the function - // - // This is almost a mirror of the usage in [`component::Func::call_impl`] - let mut results_storage = [component::Val::U64(0); MAX_FLAT_RESULTS]; - let mut num_results = 0; - let results = &mut results_storage; - let _return = store - .on_fiber(|store| unsafe { - func.call_raw( + // Grab the correct component instance + let key = event.instance; + let instance = self + .component_instances + .get_mut(&key) + .ok_or(ReplayError::MissingComponentInstance(key.as_u32()))?; + + // Replay lowering steps and obtain raw value arguments to raw function call + let func = component::Func::from_lifted_func(*instance, event.func_idx); + let mut store = self.store.as_context_mut(); + + // Call the function + // + // This is almost a mirror of the usage in [`component::Func::call_impl`] + let mut results_storage = [component::Val::U64(0); MAX_FLAT_RESULTS]; + let mut num_results = 0; + let results = &mut results_storage; + let _return = store + .on_fiber(|store| unsafe { + func.call_raw( store.as_context_mut(), |cx, _, @@ -448,45 +399,27 @@ impl ReplayInstance { Ok(()) }, ) - }) - .await??; - - log::info!( - "Returned {:?} for calling {:?}", - &results_storage[..num_results], - func - ); - } - #[cfg(not(rr_component))] - { - let _ = event; - bail!( - "Cannot parse ComponentWasmFuncBegin replay event without rr and component-model feature enabled" - ); - } + }) + .await??; + + log::info!( + "Returned {:?} for calling {:?}", + &results_storage[..num_results], + func + ); } RREvent::ComponentPostReturn(event) => { - #[cfg(rr_component)] - { - // Grab the correct component instance - let key = event.instance; - let instance = self - .component_instances - .get_mut(&key) - .ok_or(ReplayError::MissingComponentInstance(key.as_u32()))?; - - let func = component::Func::from_lifted_func(*instance, event.func_idx); - let mut store = self.store.as_context_mut(); - - func.post_return_async(&mut store).await?; - } - #[cfg(not(rr_component))] - { - let _ = event; - bail!( - "Cannot parse ComponentPostReturn replay event without rr and component-model feature enabled" - ); - } + // Grab the correct component instance + let key = event.instance; + let instance = self + .component_instances + .get_mut(&key) + .ok_or(ReplayError::MissingComponentInstance(key.as_u32()))?; + + let func = component::Func::from_lifted_func(*instance, event.func_idx); + let mut store = self.store.as_context_mut(); + + func.post_return_async(&mut store).await?; } RREvent::CoreWasmInstantiation(event) => { // Find matching module from environment to instantiate diff --git a/crates/wasmtime/src/runtime/vm/component/libcalls.rs b/crates/wasmtime/src/runtime/vm/component/libcalls.rs index 5c22653aee..6bae7ec379 100644 --- a/crates/wasmtime/src/runtime/vm/component/libcalls.rs +++ b/crates/wasmtime/src/runtime/vm/component/libcalls.rs @@ -84,7 +84,7 @@ wasmtime_environ::foreach_builtin_component_function!(define_builtins); /// implementation following this submodule. mod trampolines { use super::{ComponentInstance, VMComponentContext}; - #[cfg(rr_component)] + #[cfg(feature = "rr")] use crate::rr::{Replayer, ResultEvent, component_events::*}; use core::ptr::NonNull; @@ -163,11 +163,11 @@ mod trampolines { // main invoke rule with a record/replay hook wrapper around the above invoke rules // when `rr_builtin`` is provided (@invoke [$rr_entry:ident, $rr_exit:ident] $name:ident($store:ident, $instance:ident,) $($pname:ident)*) => ({ - #[cfg(not(rr_component))] + #[cfg(not(feature = "rr"))] { shims!(@invoke $name($store, $instance,) $($pname)*) } - #[cfg(rr_component)] + #[cfg(feature = "rr")] { if let Some(buf) = (*$store).replay_buffer_mut() { buf.next_event_validation::(&$rr_entry{ $($pname),* }.into())?; @@ -186,11 +186,11 @@ mod trampolines { // same as above rule for builtins *without* a return value (@invoke [$rr_entry:ident] $name:ident($store:ident, $instance:ident,) $($pname:ident)*) => ({ - #[cfg(not(rr_component))] + #[cfg(not(feature = "rr"))] { shims!(@invoke $name($store, $instance,) $($pname)*) } - #[cfg(rr_component)] + #[cfg(feature = "rr")] { if let Some(_buf) = (*$store).replay_buffer_mut() { // Just perform replay validation, if required From 522bb60acc7a66978f436d09fc92bb64ee873bb5 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Fri, 28 Nov 2025 15:54:52 -0500 Subject: [PATCH 52/73] Add support for core re-entrancy + test --- crates/wasmtime/src/runtime/func.rs | 4 +- crates/wasmtime/src/runtime/rr/core.rs | 28 ++-- .../src/runtime/rr/hooks/core_hooks.rs | 81 ++++++++++-- .../wasmtime/src/runtime/rr/replay_driver.rs | 125 +++++++++++++----- crates/wasmtime/src/runtime/store.rs | 18 --- src/commands/replay.rs | 67 ++++++---- tests/all/rr.rs | 91 +++++++++++-- 7 files changed, 297 insertions(+), 117 deletions(-) diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 47d8d6c4af..83f32e9c3e 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -1388,7 +1388,7 @@ impl Func { flat_params, &mut caller.store.0, )?; - rr::core_hooks::replay_host_func_return(values_vec, &mut caller.store.0)?; + rr::core_hooks::replay_host_func_return(values_vec, &mut caller)?; } // Restore our `val_vec` back into the store so it's usable for the next @@ -2544,7 +2544,7 @@ impl HostContext { // Replay the return values rr::core_hooks::replay_host_func_return( unsafe { &mut args.as_mut()[..num_results] }, - caller.store.0, + &mut caller, )?; } diff --git a/crates/wasmtime/src/runtime/rr/core.rs b/crates/wasmtime/src/runtime/rr/core.rs index 785978c1a4..e0f4cf2a69 100644 --- a/crates/wasmtime/src/runtime/rr/core.rs +++ b/crates/wasmtime/src/runtime/rr/core.rs @@ -346,22 +346,6 @@ pub trait Replayer: Iterator> { 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 - /// - /// See [`next_event_typed`](Replayer::next_event_typed) - #[inline] - fn next_event_and(&mut self, f: F) -> Result<(), ReplayError> - where - T: TryFrom, - ReplayError: From<>::Error>, - F: FnOnce(T) -> Result<(), ReplayError>, - { - let call_event = self.next_event_typed()?; - Ok(f(call_event)?) - } - /// Conditionally process the next validation recorded event and if /// replay validation is enabled, run the validation check /// @@ -591,6 +575,18 @@ mod tests { use tempfile::{NamedTempFile, TempPath}; use wasmtime_environ::FuncIndex; + impl ReplayBuffer { + fn next_event_and(&mut self, f: F) -> Result<(), ReplayError> + where + T: TryFrom, + ReplayError: From<>::Error>, + F: FnOnce(T) -> Result<(), ReplayError>, + { + let event = self.next_event_typed::()?; + f(event) + } + } + fn rr_harness(record_fn: S, replay_fn: T) -> Result<()> where S: FnOnce(&mut RecordBuffer) -> Result<()>, diff --git a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs index 7b1b7c3a09..2c28de4d2e 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs @@ -1,12 +1,14 @@ use crate::rr::FlatBytes; #[cfg(feature = "rr")] use crate::rr::{ - RRFuncArgVals, ResultEvent, common_events::HostFuncEntryEvent, - common_events::HostFuncReturnEvent, common_events::WasmFuncReturnEvent, - core_events::WasmFuncEntryEvent, + RREvent, RRFuncArgVals, ReplayError, ReplayHostContext, Replayer, ResultEvent, + common_events::HostFuncEntryEvent, common_events::HostFuncReturnEvent, + common_events::WasmFuncReturnEvent, core_events::WasmFuncEntryEvent, }; use crate::store::StoreOpaque; -use crate::{FuncType, StoreContextMut, ValRaw, WasmFuncOrigin, prelude::*}; +use crate::{Caller, FuncType, StoreContextMut, ValRaw, WasmFuncOrigin, prelude::*}; +#[cfg(feature = "rr")] +use wasmtime_environ::EntityIndex; /// Record and replay hook operation for core wasm function entry events /// @@ -121,15 +123,74 @@ where /// Replay hook operation for host function return events #[inline] -pub fn replay_host_func_return(args: &mut [T], store: &mut StoreOpaque) -> Result<()> +pub fn replay_host_func_return( + args: &mut [T], + caller: &mut Caller<'_, U>, +) -> Result<()> where T: FlatBytes, { #[cfg(feature = "rr")] - store.next_replay_event_and(|event: HostFuncReturnEvent| { - event.args.into_raw_slice(args); - Ok(()) - })?; - let _ = (args, store); + { + // Core wasm can be re-entrant, so we need to check for this + let mut complete = false; + while !complete { + let buf = caller.store.0.replay_buffer_mut().unwrap(); + let event = buf.next_event()?; + match event { + RREvent::HostFuncReturn(event) => { + event.args.into_raw_slice(args); + complete = true; + } + // Re-entrant call into wasm function: this resembles the implementation in [`ReplayInstance`] + RREvent::CoreWasmFuncEntry(event) => { + let entity = EntityIndex::from(event.origin.index); + + // SAFETY: The store's data is always of type `ReplayHostContext` when created by + // the replay driver. As an additional guarantee, we assert that replay is indeed + // truly enabled. + assert!(caller.store.0.replay_enabled()); + let replay_data = unsafe { + let raw_ptr: *const U = caller.store.data(); + &*(raw_ptr as *const ReplayHostContext) + }; + + // Grab the correct module instance + let instance = replay_data.get_module_instance(event.origin.instance)?; + + let mut store = &mut caller.store; + let func = instance + ._get_export(store.0, entity) + .into_func() + .ok_or(ReplayError::InvalidCoreFuncIndex(entity))?; + + let params_ty = func.ty(&store).params().collect::>(); + + // Obtain the argument values for function call + let mut results = vec![crate::Val::I64(0); func.ty(&store).results().len()]; + let params = event.args.to_val_vec(&mut store, params_ty); + + // Call the function + // + // This is almost a mirror of the usage in [`crate::Func::call_impl`] + func.call_impl_check_args(&mut store, ¶ms, &mut results)?; + unsafe { + func.call_impl_do_call( + &mut store, + params.as_slice(), + results.as_mut_slice(), + )?; + } + } + _ => { + bail!( + "Unexpected event during core wasm host function replay: {:?}", + event + ); + } + } + } + } + let _ = (args, caller); Ok(()) } diff --git a/crates/wasmtime/src/runtime/rr/replay_driver.rs b/crates/wasmtime/src/runtime/rr/replay_driver.rs index bbfc4ce5ef..8ca4bc6bab 100644 --- a/crates/wasmtime/src/runtime/rr/replay_driver.rs +++ b/crates/wasmtime/src/runtime/rr/replay_driver.rs @@ -43,25 +43,73 @@ impl ReplayEnvironment { } /// Instantiate a new [`ReplayInstance`] using a [`ReplayReader`] in context of this environment - pub fn instantiate(&self, reader: impl ReplayReader + 'static) -> Result> { - let store = Store::new(&self.engine, ()); - ReplayInstance::<()>::from_environment_and_store(self.clone(), store, reader) + pub fn instantiate(&self, reader: impl ReplayReader + 'static) -> Result { + self.instantiate_with(reader, |_| Ok(()), |_| Ok(()), |_| Ok(())) } - /// Like [`Self::instantiate`] but allows providing a custom [`Store`] generator - pub fn instantiate_with_store( + /// Like [`Self::instantiate`] but allows providing a custom modifier functions for + /// [`Store`], [`crate::Linker`], and [`component::Linker`] within the replay + pub fn instantiate_with( &self, - store_gen: impl FnOnce() -> Store, reader: impl ReplayReader + 'static, - ) -> Result> { - ReplayInstance::from_environment_and_store(self.clone(), store_gen(), reader) + store_fn: impl FnOnce(&mut Store) -> Result<()>, + module_linker_fn: impl FnOnce(&mut crate::Linker) -> Result<()>, + component_linker_fn: impl FnOnce(&mut component::Linker) -> Result<()>, + ) -> Result { + let mut store = Store::new( + &self.engine, + ReplayHostContext { + module_instances: BTreeMap::new(), + }, + ); + store_fn(&mut store)?; + store.init_replaying(reader, self.settings.clone())?; + + ReplayInstance::from_environment_and_store( + self.clone(), + store, + module_linker_fn, + component_linker_fn, + ) + } +} + +/// The host context tied to the store during replay. +/// +/// This context encapsulates the state from the replay environment that are +/// required to be accessible within the Store. This is an opaque type from the +/// public API perspective. +pub struct ReplayHostContext { + /// A tracker of instantiated modules. + /// + /// Core wasm modules can be re-entrant and invoke methods from other instances, and this + /// needs to be accessible within host functions + module_instances: BTreeMap, +} + +impl ReplayHostContext { + /// Insert a module instance into the context's tracking map. + fn insert_module_instance(&mut self, id: InstanceId, instance: crate::Instance) { + self.module_instances.insert(id, instance); + } + + /// Get a module instance from the context's tracking map + /// + /// This is necessary for core wasm to identify re-entrant calls during replay. + pub(crate) fn get_module_instance( + &self, + id: InstanceId, + ) -> Result<&crate::Instance, ReplayError> { + self.module_instances + .get(&id) + .ok_or(ReplayError::MissingModuleInstance(id.as_u32())) } } -/// A [`ReplayInstance`] is an object providing a opaquely managed, replayable [`Store`] +/// A [`ReplayInstance`] is an object providing a opaquely managed, replayable [`Store`]. /// /// Debugger capabilities in the future will interact with this object for -/// inserting breakpoints, snapshotting, and restoring state +/// inserting breakpoints, snapshotting, and restoring state. /// /// # Example /// @@ -122,32 +170,36 @@ impl ReplayEnvironment { /// # Ok(()) /// # } /// ``` -pub struct ReplayInstance { +pub struct ReplayInstance { env: Arc, - store: Store, - component_linker: component::Linker, - module_linker: crate::Linker, + store: Store, + component_linker: component::Linker, + module_linker: crate::Linker, module_instances: BTreeMap, component_instances: BTreeMap, } -impl ReplayInstance { +impl ReplayInstance { fn from_environment_and_store( env: ReplayEnvironment, - mut store: Store, - reader: impl ReplayReader + 'static, + store: Store, + module_linker_fn: impl FnOnce(&mut crate::Linker) -> Result<()>, + component_linker_fn: impl FnOnce(&mut component::Linker) -> Result<()>, ) -> Result { let env = Arc::new(env); - store.init_replaying(reader, env.settings.clone())?; - let mut module_linker = crate::Linker::::new(&env.engine); + let mut module_linker = crate::Linker::::new(&env.engine); // Replays shouldn't use any imports, so stub them all out as traps for module in env.modules.values() { module_linker.define_unknown_imports_as_traps(module)?; } - let mut component_linker = component::Linker::::new(&env.engine); + module_linker_fn(&mut module_linker)?; + + let mut component_linker = component::Linker::::new(&env.engine); for component in env.components.values() { component_linker.define_unknown_imports_as_traps(component)?; } + component_linker_fn(&mut component_linker)?; + Ok(Self { env, store, @@ -158,23 +210,23 @@ impl ReplayInstance { }) } - /// Obtain a reference to the internal [`Store`] - pub fn store(&self) -> &Store { + /// Obtain a reference to the internal [`Store`]. + pub fn store(&self) -> &Store { &self.store } - /// Consume the [`ReplayInstance`] and extract the internal [`Store`] - pub fn extract_store(self) -> Store { + /// Consume the [`ReplayInstance`] and extract the internal [`Store`]. + pub fn extract_store(self) -> Store { self.store } - /// Run a single top-level event from the instance + /// Run a single top-level event from the instance. /// /// "Top-level" events are those explicitly invoked events, namely: /// * Instantiation events (component/module) /// * Wasm function begin events (`ComponentWasmFuncBegin` for components and `CoreWasmFuncEntry` for core) /// - /// All other events are transparently dispatched under the context of these top-level events + /// All other events are transparently dispatched under the context of these top-level events. fn run_single_top_level_event(&mut self, rr_event: RREvent) -> Result<()> { match rr_event { RREvent::ComponentInstantiation(event) => { @@ -281,7 +333,12 @@ impl ReplayInstance { instance: instance.id(), })?; + // Insert into host context tracking as well self.module_instances.insert(instance.id(), instance); + self.store + .as_context_mut() + .data_mut() + .insert_module_instance(instance.id(), instance); } RREvent::CoreWasmFuncEntry(event) => { // Grab the correct module instance @@ -321,12 +378,9 @@ impl ReplayInstance { Ok(()) } - /// Exactly like [`Self::run_single_top_level_event`] but uses async stores and calls + /// Exactly like [`Self::run_single_top_level_event`] but uses async stores and calls. #[cfg(feature = "async")] - async fn run_single_top_level_event_async(&mut self, rr_event: RREvent) -> Result<()> - where - T: Send, - { + async fn run_single_top_level_event_async(&mut self, rr_event: RREvent) -> Result<()> { match rr_event { RREvent::ComponentInstantiation(event) => { // Find matching component from environment to instantiate @@ -441,6 +495,10 @@ impl ReplayInstance { })?; self.module_instances.insert(instance.id(), instance); + self.store + .as_context_mut() + .data_mut() + .insert_module_instance(instance.id(), instance); } RREvent::CoreWasmFuncEntry(event) => { // Grab the correct module instance @@ -500,10 +558,7 @@ impl ReplayInstance { /// Exactly like [`Self::run_to_completion`] but uses async stores and calls #[cfg(feature = "async")] - pub async fn run_to_completion_async(&mut self) -> Result<()> - where - T: Send, - { + pub async fn run_to_completion_async(&mut self) -> Result<()> { while let Some(rr_event) = self .store .as_context_mut() diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index 88f055d145..47a2453e17 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -1965,24 +1965,6 @@ 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) -> Result<(), ReplayError>, - { - if let Some(buf) = self.replay_buffer_mut() { - buf.next_event_and(f) - } else { - Ok(()) - } - } - /// 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 /// diff --git a/src/commands/replay.rs b/src/commands/replay.rs index 2a2402ef6f..112c0b5b4f 100644 --- a/src/commands/replay.rs +++ b/src/commands/replay.rs @@ -1,13 +1,13 @@ //! Implementation of the `wasmtime replay` command -use crate::commands::run::{Host, RunCommand}; +use crate::commands::run::RunCommand; use crate::common::RunTarget; -use anyhow::{Context, Result}; +use anyhow::{Context, Result, bail}; use clap::Parser; use std::path::PathBuf; use std::{fs, io}; use tokio::time::error::Elapsed; -use wasmtime::{Engine, ReplayEnvironment, ReplaySettings, Store}; +use wasmtime::{Engine, ReplayEnvironment, ReplaySettings}; #[derive(Parser)] /// Replay-specific options for CLI. @@ -64,10 +64,8 @@ impl ReplayCommand { .run_cmd .run .load_module(&engine, self.run_cmd.module_and_args[0].as_ref())?; - let (store, _) = self.run_cmd.new_store_and_linker(&engine, &main)?; - self.instantiate_and_run_replay(&engine, &main, store) - .await?; + self.run_replay(&engine, &main).await?; Ok(()) }) } @@ -75,13 +73,16 @@ impl ReplayCommand { /// Execute the store with the replay settings. /// /// Applies similar configurations to `instantiate_and_run`. - async fn instantiate_and_run_replay( - self, - engine: &Engine, - main: &RunTarget, - store: Store, - ) -> Result<()> { + async fn run_replay(self, engine: &Engine, main: &RunTarget) -> Result<()> { let opts = self.replay_opts; + + // Validate coredump-on-trap argument + if let Some(path) = &self.run_cmd.run.common.debug.coredump { + if path.contains("%") { + bail!("the coredump-on-trap path does not support patterns yet.") + } + } + // In general, replays will need an "almost exact" superset of // the run configurations, but with potentially certain different options (e.g. fuel consumption). let settings = ReplaySettings { @@ -100,8 +101,31 @@ impl ReplayCommand { renv.add_component(c.clone()); } } - let mut replay_instance = - renv.instantiate_with_store(|| store, io::BufReader::new(fs::File::open(opts.trace)?))?; + + let allow_unknown_exports = self.run_cmd.run.common.wasm.unknown_exports_allow; + let mut replay_instance = renv.instantiate_with( + io::BufReader::new(fs::File::open(opts.trace)?), + |store| { + // If fuel has been configured, we want to add the configured + // fuel amount to this store. + if let Some(fuel) = self.run_cmd.run.common.wasm.fuel { + store.set_fuel(fuel)?; + } + Ok(()) + }, + |module_linker| { + if let Some(enable) = allow_unknown_exports { + module_linker.allow_unknown_exports(enable); + } + Ok(()) + }, + |_component_linker| { + if allow_unknown_exports.is_some() { + bail!("--allow-unknown-exports not supported with components"); + } + Ok(()) + }, + )?; let dur = self .run_cmd @@ -116,9 +140,6 @@ impl ReplayCommand { }) .await; - // Extract the store for error handling below. - let store = replay_instance.extract_store(); - // This is basically the same finish logic as `instantiate_and_run`. match result.unwrap_or_else(|elapsed| { Err(anyhow::Error::from(wasmtime::Trap::Interrupt)) @@ -126,18 +147,8 @@ impl ReplayCommand { }) { Ok(_) => Ok(()), Err(e) => { - // Exit the process if Wasmtime understands the error; - // otherwise, fall back on Rust's default error printing/return - // code. - if store.data().legacy_p1_ctx.is_some() { - return Err(wasi_common::maybe_exit_on_error(e)); - } else if store.data().wasip1_ctx.is_some() { - if let Some(exit) = e.downcast_ref::() { - std::process::exit(exit.0); - } - } if e.is::() { - eprintln!("Error: {e:?}"); + eprintln!("Error returned from replay: {e:?}"); cfg_if::cfg_if! { if #[cfg(unix)] { std::process::exit(rustix::process::EXIT_SIGNALED_SIGABRT); diff --git a/tests/all/rr.rs b/tests/all/rr.rs index d41c611d69..18ea7df573 100644 --- a/tests/all/rr.rs +++ b/tests/all/rr.rs @@ -49,7 +49,7 @@ fn create_replay_engine(is_async: bool) -> Result { /// Run a core module test with recording and replay fn run_core_module_test(module_wat: &str, setup_linker: F, test_fn: R) -> Result<()> where - F: Fn(&mut Linker) -> Result<()>, + F: Fn(&mut Linker, bool) -> Result<()>, R: for<'a> Fn( &'a mut Store, &'a wasmtime::Instance, @@ -64,7 +64,7 @@ where // Run with in sync/async mode with/without validation for is_async in [false, true] { - for validation in [false, true] { + for validation in [true, false] { let run = async { run_core_module_test_with_validation( module_wat, @@ -92,7 +92,7 @@ async fn run_core_module_test_with_validation( is_async: bool, ) -> Result<()> where - F: Fn(&mut Linker) -> Result<()>, + F: Fn(&mut Linker, bool) -> Result<()>, R: for<'a> Fn( &'a mut Store, &'a wasmtime::Instance, @@ -104,7 +104,7 @@ where let module = Module::new(&engine, module_wat)?; let mut linker = Linker::new(&engine); - setup_linker(&mut linker)?; + setup_linker(&mut linker, is_async)?; let writer: Cursor> = Cursor::new(Vec::new()); let mut store = Store::new(&engine, TestState::new()); @@ -169,7 +169,7 @@ where // Run with in sync/async mode with/without validation for is_async in [false, true] { - for validation in [false, true] { + for validation in [true, false] { let run = async { run_component_test_with_validation( component_wat, @@ -275,7 +275,7 @@ fn test_core_module_with_host_double() -> Result<()> { run_core_module_test( module_wat, - |linker| { + |linker, _| { linker.func_wrap("env", "double", |param: i32| param * 2)?; Ok(()) }, @@ -320,7 +320,7 @@ fn test_core_module_with_multiple_host_imports() -> Result<()> { run_core_module_test( module_wat, - |linker| { + |linker, _| { linker.func_wrap("env", "double", |param: i32| param * 2)?; linker.func_wrap("env", "complex", |p1: i32, p2: i64| -> (i32, i64, f32) { ((p1 as f32).sqrt() as i32, (p1 * p1) as i64 * p2, 8.66) @@ -342,6 +342,76 @@ fn test_core_module_with_multiple_host_imports() -> Result<()> { ) } +#[test] +fn test_core_module_reentrancy() -> Result<()> { + let module_wat = r#" + (module + (import "env" "host_call" (func $host_call (param i32) (result i32))) + (func (export "main") (param i32) (result i32) + local.get 0 + call $host_call + ) + (func (export "wasm_callback") (param i32) (result i32) + local.get 0 + i32.const 1 + i32.add + ) + ) + "#; + + run_core_module_test( + module_wat, + |linker, is_async| { + if is_async { + linker.func_wrap_async( + "env", + "host_call", + |mut caller: wasmtime::Caller<'_, TestState>, (param,): (i32,)| { + Box::new(async move { + let func = caller + .get_export("wasm_callback") + .unwrap() + .into_func() + .unwrap(); + let typed = func.typed::(&caller)?; + typed.call_async(&mut caller, param).await + }) + }, + )?; + } else { + linker.func_wrap( + "env", + "host_call", + |mut caller: wasmtime::Caller<'_, TestState>, + param: i32| + -> wasmtime::Result { + let func = caller + .get_export("wasm_callback") + .unwrap() + .into_func() + .unwrap(); + let typed = func.typed::(&caller)?; + typed.call(&mut caller, param) + }, + )?; + } + Ok(()) + }, + |store, instance, is_async| { + Box::pin(async move { + let run = instance.get_typed_func::(&mut *store, "main")?; + let result = if is_async { + run.call_async(&mut *store, 42).await? + } else { + run.call(&mut *store, 42)? + }; + assert_eq!(result, 43); + Ok(()) + }) + }, + ) +} + #[test] #[should_panic] fn test_recording_panics_for_core_module_memory_export() { @@ -351,7 +421,12 @@ fn test_recording_panics_for_core_module_memory_export() { ) "#; - run_core_module_test(module_wat, |_| Ok(()), |_, _, _| Box::pin(async { Ok(()) })).unwrap(); + run_core_module_test( + module_wat, + |_, _| Ok(()), + |_, _, _| Box::pin(async { Ok(()) }), + ) + .unwrap(); } // ============================================================================ From a6c9708cae3f814b1aee3735fd2312cf68755296 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Fri, 28 Nov 2025 16:32:38 -0500 Subject: [PATCH 53/73] Refactor rr configuration for commands --- src/commands/replay.rs | 4 +- src/commands/run.rs | 106 +++++++++++++++++++++++++++++------------ src/commands/serve.rs | 3 ++ src/commands/wizer.rs | 4 +- 4 files changed, 82 insertions(+), 35 deletions(-) diff --git a/src/commands/replay.rs b/src/commands/replay.rs index 112c0b5b4f..a3492041a3 100644 --- a/src/commands/replay.rs +++ b/src/commands/replay.rs @@ -1,6 +1,6 @@ //! Implementation of the `wasmtime replay` command -use crate::commands::run::RunCommand; +use crate::commands::run::{Replaying, RunCommand}; use crate::common::RunTarget; use anyhow::{Context, Result, bail}; use clap::Parser; @@ -59,7 +59,7 @@ impl ReplayCommand { runtime.block_on(async { self.run_cmd.run.common.init_logging()?; - let engine = self.run_cmd.new_engine(true)?; + let engine = self.run_cmd.new_engine(Replaying::Yes)?; let main = self .run_cmd .run diff --git a/src/commands/run.rs b/src/commands/run.rs index 2e36b79990..6e2f080db9 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -12,11 +12,7 @@ use std::ffi::OsString; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::thread; -#[cfg(feature = "rr")] -use std::{fs, io}; use wasi_common::sync::{Dir, TcpListener, WasiCtxBuilder, ambient_authority}; -#[cfg(feature = "rr")] -use wasmtime::RecordSettings; use wasmtime::{Engine, Func, Module, Store, StoreLimits, Val, ValType}; use wasmtime_wasi::{WasiCtxView, WasiView}; @@ -105,6 +101,76 @@ pub enum CliInstance { Component(wasmtime::component::Instance), } +/// Flag to indicate whether we are performing a replay run or not. +pub enum Replaying { + /// Replay mode enabled. + Yes, + /// Replay mode disabled. + No, +} + +/// Implementation of record/replay configuration and setup +#[cfg(feature = "rr")] +pub mod rr_impl { + use super::{Replaying, Result, Store}; + use std::{fs, io}; + use wasmtime::{Config, RecordSettings}; + use wasmtime_cli_flags::RecordOptions; + + /// Setup replay configuration on the given [`Config`] + pub fn config_replay(config: &mut Config, replaying: Replaying) { + if let Replaying::Yes = replaying { + config.rr(wasmtime::RRConfig::Replaying); + } + } + + /// Setup record configuration on the given [`Store`] + pub fn recording_for_store( + store: &mut Store, + record: &RecordOptions, + ) -> Result<()> { + if let Some(path) = &record.path { + let default_settings = RecordSettings::default(); + let 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), + }; + if path.trim().is_empty() { + store.record(io::sink(), settings)?; + } else { + store.record(fs::File::create(&path)?, settings)?; + } + } + Ok(()) + } +} + +/// Implementation of record/replay configuration and setup +#[cfg(not(feature = "rr"))] +pub mod rr_impl { + use super::{Replaying, Result, Store}; + use wasmtime::Config; + use wasmtime_cli_flags::RecordOptions; + + /// Setup replay configuration on the given [`Config`] + pub fn config_replay(config: &mut Config, replaying: Replaying) { + let _ = (config, replaying); + } + + /// Setup record configuration on the given [`Store`] + pub fn recording_for_store( + store: &mut Store, + record: &RecordOptions, + ) -> Result<()> { + let _ = (store, record); + Ok(()) + } +} + impl RunCommand { /// Executes the command. #[cfg(feature = "run")] @@ -117,7 +183,7 @@ impl RunCommand { runtime.block_on(async { self.run.common.init_logging()?; - let engine = self.new_engine(false)?; + let engine = self.new_engine(Replaying::No)?; let main = self .run .load_module(&engine, self.module_and_args[0].as_ref())?; @@ -130,13 +196,11 @@ impl RunCommand { } /// Creates a new `Engine` with the configuration for this command. - pub fn new_engine(&mut self, _is_replaying: bool) -> Result { + pub fn new_engine(&mut self, replaying: Replaying) -> Result { let mut config = self.run.common.config(None)?; config.async_support(true); - #[cfg(feature = "rr")] - if _is_replaying { - config.rr(wasmtime::RRConfig::Replaying); - } + + rr_impl::config_replay(&mut config, replaying); if self.run.common.wasm.timeout.is_some() { config.epoch_interruption(true); @@ -215,27 +279,7 @@ impl RunCommand { store.set_fuel(fuel)?; } - #[cfg(feature = "rr")] - { - // Recording settings for this execution's store - let record = &self.run.common.record; - if let Some(path) = &record.path { - let default_settings = RecordSettings::default(); - let 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), - }; - if path.trim().is_empty() { - store.record(io::sink(), settings)?; - } else { - store.record(fs::File::create(&path)?, settings)?; - } - } - } + rr_impl::recording_for_store(&mut store, &self.run.common.record)?; Ok((store, linker)) } diff --git a/src/commands/serve.rs b/src/commands/serve.rs index 00f5e2a89c..b80b4b971f 100644 --- a/src/commands/serve.rs +++ b/src/commands/serve.rs @@ -1,3 +1,4 @@ +use crate::commands::rr_impl; use crate::common::{Profile, RunCommon, RunTarget}; use anyhow::{Context as _, Result, bail}; use bytes::Bytes; @@ -310,6 +311,8 @@ impl ServeCommand { store.set_fuel(fuel)?; } + rr_impl::recording_for_store(&mut store, &self.run.common.record)?; + Ok(store) } diff --git a/src/commands/wizer.rs b/src/commands/wizer.rs index ee213f6978..90077ec348 100644 --- a/src/commands/wizer.rs +++ b/src/commands/wizer.rs @@ -1,4 +1,4 @@ -use crate::commands::run::{CliInstance, Preloads, RunCommand}; +use crate::commands::run::{CliInstance, Preloads, Replaying, RunCommand}; use crate::common::{RunCommon, RunTarget}; use anyhow::{Context, Result}; use std::fs; @@ -86,7 +86,7 @@ impl WizerCommand { module_and_args: vec![self.input.clone().into()], preloads: self.preloads.clone(), }; - let engine = run.new_engine(false)?; + let engine = run.new_engine(Replaying::No)?; // Instrument the input wasm with wizer. let (cx, main) = if is_component { From 7f3561e420a3ef6465176432616162d79a67913d Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Fri, 28 Nov 2025 22:36:02 -0500 Subject: [PATCH 54/73] Fix config settings and assertions --- crates/wasmtime/src/config.rs | 67 +++++++++++-------- crates/wasmtime/src/engine.rs | 2 +- .../src/runtime/component/instance.rs | 1 + crates/wasmtime/src/runtime/instance.rs | 9 ++- crates/wasmtime/src/runtime/rr/core/io.rs | 2 +- crates/wasmtime/src/runtime/store.rs | 14 ++++ 6 files changed, 61 insertions(+), 34 deletions(-) diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index ba1831a53a..083881e7d9 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -1076,11 +1076,6 @@ 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" - ); self.tunables.relaxed_simd_deterministic = Some(enable); self } @@ -1424,11 +1419,6 @@ 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" - ); let val = if enable { "true" } else { "false" }; self.compiler_config .settings @@ -2344,7 +2334,7 @@ impl Config { target_lexicon::Triple::host() } - pub(crate) fn validate(&self) -> Result<(Tunables, WasmFeatures)> { + pub(crate) fn validate(&mut self) -> Result<(Tunables, WasmFeatures)> { let features = self.features(); // First validate that the selected compiler backend and configuration @@ -2385,6 +2375,15 @@ impl Config { bail!("exceptions support requires garbage collection (GC) to be enabled in the build"); } + #[cfg(feature = "rr")] + match &self.rr_config { + RRConfig::Recording | RRConfig::Replaying => { + self.validate_determinism_conflicts()?; + self.enforce_determinism(); + } + _ => {} + }; + let mut tunables = Tunables::default_for_target(&self.compiler_target())?; // If no target is explicitly specified then further refine `tunables` @@ -2907,8 +2906,8 @@ impl Config { } /// Enforce deterministic execution configurations. Currently, this means the following: - /// * Enabling NaN canonicalization with [`Config::cranelift_nan_canonicalization`] - /// * Enabling deterministic relaxed SIMD with [`Config::relaxed_simd_deterministic`] + /// * Enabling NaN canonicalization with [`Config::cranelift_nan_canonicalization`]. + /// * Enabling deterministic relaxed SIMD with [`Config::relaxed_simd_deterministic`]. #[inline] pub fn enforce_determinism(&mut self) -> &mut Self { #[cfg(any(feature = "cranelift", feature = "winch"))] @@ -2917,29 +2916,39 @@ impl Config { self } - /// Enable execution trace recording or replaying to the configuration + /// Validate if the current configuration has conflicting overrides that prevent + /// execution determinism. Returns an error if a conflict exists. /// - /// When either recording/replaying are enabled, determinism is implicitly - /// enforced (see [`Config::enforce_determinism`] for details) - #[cfg(feature = "rr")] + /// Note: Keep this in sync with [`Config::enforce_determinism`]. #[inline] - pub fn rr(&mut self, cfg: RRConfig) -> &mut Self { - self.rr_config = cfg; - match self.rr_config { - RRConfig::Recording | RRConfig::Replaying => self.enforce_determinism(), - _ => self, + #[cfg(feature = "rr")] + pub(crate) fn validate_determinism_conflicts(&self) -> Result<()> { + if let Some(v) = self.tunables.relaxed_simd_deterministic { + if v == false { + bail!("Relaxed deterministic SIMD cannot be disabled when determinism is enforced"); + } + } + if let Some(v) = self + .compiler_config + .settings + .get("enable_nan_canonicalization") + { + if v != "true" { + bail!("NaN canonicalization cannot be disabled when determinism is enforced"); + } } + Ok(()) } - /// Evaluates to true if current configuration must respect - /// deterministic execution in its configuration. + /// Enable execution trace recording or replaying to the configuration. + /// + /// When either recording/replaying are enabled, determinism is implicitly + /// enforced (see [`Config::enforce_determinism`] for details). #[cfg(feature = "rr")] #[inline] - pub fn is_determinism_enforced(&mut self) -> bool { - match self.rr_config { - RRConfig::Recording | RRConfig::Replaying => true, - RRConfig::None => false, - } + pub fn rr(&mut self, cfg: RRConfig) -> &mut Self { + self.rr_config = cfg; + self } } diff --git a/crates/wasmtime/src/engine.rs b/crates/wasmtime/src/engine.rs index d5df2d6aed..943589da5a 100644 --- a/crates/wasmtime/src/engine.rs +++ b/crates/wasmtime/src/engine.rs @@ -97,7 +97,7 @@ impl Engine { /// the compiler setting `unwind_info` to `true`, but explicitly /// disable these two compiler settings will cause errors. pub fn new(config: &Config) -> Result { - let config = config.clone(); + let mut config = config.clone(); let (mut tunables, features) = config.validate()?; #[cfg(feature = "runtime")] diff --git a/crates/wasmtime/src/runtime/component/instance.rs b/crates/wasmtime/src/runtime/component/instance.rs index af302b6759..42258bdd6d 100644 --- a/crates/wasmtime/src/runtime/component/instance.rs +++ b/crates/wasmtime/src/runtime/component/instance.rs @@ -1194,6 +1194,7 @@ impl InstancePre { .increment_component_instance_count()?; let mut instantiator = Instantiator::new(&self.component, store.0, &self.imports); + store.0.validate_rr_config()?; #[cfg(feature = "rr")] store.0.record_event(|| InstantiationEvent { component: *self.component.checksum(), diff --git a/crates/wasmtime/src/runtime/instance.rs b/crates/wasmtime/src/runtime/instance.rs index bd69e41d16..49fd9d32ac 100644 --- a/crates/wasmtime/src/runtime/instance.rs +++ b/crates/wasmtime/src/runtime/instance.rs @@ -261,11 +261,13 @@ impl Instance { // function. unsafe { Instance::new_raw(store, limiter.as_mut(), module, imports).await? } }; + + store.0.validate_rr_config()?; if !from_component { #[cfg(feature = "rr")] { // Components already record instantiation, so do not record their internal modules - rr_assert_unexported_memories(module)?; + rr_validate_module_unexported_memory(module)?; store.0.record_event(|| InstantiationEvent { module: *module.checksum(), instance: instance.id(), @@ -1007,9 +1009,10 @@ fn typecheck( Ok(()) } -/// Check to flag exported memories in Core wasm modules when recording is enabled +/// Ensure that memories are not exported memories in Core wasm modules when +/// recording is enabled #[cfg(feature = "rr")] -fn rr_assert_unexported_memories(module: &Module) -> Result<()> { +fn rr_validate_module_unexported_memory(module: &Module) -> Result<()> { // Check for exported memories when recording is enabled. if module.engine().is_recording() && module.exports().any(|export| { diff --git a/crates/wasmtime/src/runtime/rr/core/io.rs b/crates/wasmtime/src/runtime/rr/core/io.rs index 0e347b3f49..1865da837d 100644 --- a/crates/wasmtime/src/runtime/rr/core/io.rs +++ b/crates/wasmtime/src/runtime/rr/core/io.rs @@ -20,7 +20,7 @@ cfg_if::cfg_if! { } else { // `no_std` configuration - use embedded_io::{Read, Write}; + use embedded_io::{Read, Seek, Write}; /// An [`Write`] usable for recording in RR /// diff --git a/crates/wasmtime/src/runtime/store.rs b/crates/wasmtime/src/runtime/store.rs index 47a2453e17..052c346f52 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -2001,6 +2001,20 @@ impl StoreOpaque { } } + /// Ensures that the Store truly has a record sink/replay source when the + /// engine is setup for record/replay respectively. + pub(crate) fn validate_rr_config(&self) -> Result<()> { + #[cfg(feature = "rr")] + { + if self.engine().is_recording() && !self.record_buffer.is_some() { + bail!("Store must have a record buffer when the engine is setup for recording"); + } else if self.engine().is_replaying() && !self.replay_buffer.is_some() { + bail!("Store must have a replay source when the engine is setup for replaying"); + } + } + Ok(()) + } + #[inline(never)] async fn allocate_gc_store( &mut self, From 491a368fb0ef3a7cef6372faab41c9afe998a814 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Sat, 29 Nov 2025 14:05:27 -0500 Subject: [PATCH 55/73] Refactor host function rr into separate replay/normal flows --- Cargo.toml | 2 +- crates/cli-flags/Cargo.toml | 2 +- crates/environ/src/component.rs | 3 + crates/wasmtime/src/config.rs | 1 + .../src/runtime/component/func/host.rs | 411 ++++++++++-------- crates/wasmtime/src/runtime/func.rs | 178 +++++--- crates/wasmtime/src/runtime/rr.rs | 8 + .../src/runtime/rr/hooks/component_hooks.rs | 1 + 8 files changed, 353 insertions(+), 253 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 738ca3551d..1c30e39742 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -509,7 +509,7 @@ component-model-async = [ "wasmtime-wasi-http?/p3", "dep:futures", ] -rr = ["wasmtime/rr", "wasmtime-cli-flags/rr", "run"] +rr = ["wasmtime/rr", "component-model", "wasmtime-cli-flags/rr", "run"] # This feature, when enabled, will statically compile out all logging statements # throughout Wasmtime and its dependencies. diff --git a/crates/cli-flags/Cargo.toml b/crates/cli-flags/Cargo.toml index f39b778cd4..724f0c899b 100644 --- a/crates/cli-flags/Cargo.toml +++ b/crates/cli-flags/Cargo.toml @@ -41,4 +41,4 @@ memory-protection-keys = ["wasmtime/memory-protection-keys"] pulley = ["wasmtime/pulley"] stack-switching = ["wasmtime/stack-switching"] debug = ["wasmtime/debug"] -rr = ["wasmtime/rr"] +rr = ["wasmtime/rr", "component-model"] diff --git a/crates/environ/src/component.rs b/crates/environ/src/component.rs index a00242d2dc..82ad98ad85 100644 --- a/crates/environ/src/component.rs +++ b/crates/environ/src/component.rs @@ -81,6 +81,9 @@ pub use self::types_builder::*; /// Helper macro, like `foreach_transcoder`, to iterate over builtins for /// components unrelated to transcoding. +/// +/// Note: RR is not supported for component model async builtins yet; enabling +/// both will currently throw a compile error. #[macro_export] macro_rules! foreach_builtin_component_function { ($mac:ident) => { diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 083881e7d9..b752c86b2b 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -2928,6 +2928,7 @@ impl Config { bail!("Relaxed deterministic SIMD cannot be disabled when determinism is enforced"); } } + #[cfg(any(feature = "cranelift", feature = "winch"))] if let Some(v) = self .compiler_config .settings diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 39e30ddb4d..32cee8c4d3 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -8,12 +8,11 @@ use crate::component::types::ComponentFunc; use crate::component::{ComponentNamedList, ComponentType, Instance, Lift, Lower, Val}; use crate::prelude::*; use crate::rr; -#[cfg(feature = "rr")] -use crate::rr::component_hooks::ReplayLoweringPhase; use crate::runtime::vm::component::{ ComponentInstance, VMComponentContext, VMLowering, VMLoweringCallee, }; use crate::runtime::vm::{SendSyncPtr, VMOpaqueContext, VMStore}; +use crate::vm::component::InstanceFlags; use crate::{AsContextMut, CallHook, StoreContextMut, ValRaw}; use alloc::sync::Arc; use core::any::Any; @@ -23,7 +22,7 @@ use core::pin::Pin; use core::ptr::NonNull; use wasmtime_environ::component::{ CanonicalAbiInfo, ComponentTypes, InterfaceType, MAX_FLAT_ASYNC_PARAMS, MAX_FLAT_PARAMS, - MAX_FLAT_RESULTS, OptionsIndex, TypeFuncIndex, TypeTuple, + MAX_FLAT_RESULTS, OptionsIndex, RuntimeComponentInstanceIndex, TypeFuncIndex, TypeTuple, }; pub struct HostFunc { @@ -755,12 +754,12 @@ where T: 'static, { let (component, store) = instance.component_and_store_mut(store.0); - let mut store = StoreContextMut(store); + let store = StoreContextMut(store); let vminstance = instance.id().get(store.0); let opts = &component.env_component().options[options]; let async_ = opts.async_; let caller_instance = opts.instance; - let mut flags = vminstance.instance_flags(caller_instance); + let flags = vminstance.instance_flags(caller_instance); // Perform a dynamic check that this instance can indeed be left. Exiting // the component is disallowed, for example, when the `realloc` function @@ -770,209 +769,259 @@ where } let types = component.types(); + + unsafe { + // This top-level switch determines whether or not we're in replay mode or + // not. In replay mode, we skip all lifting and execution of host functions and + // just replay lowering effects observed in the trace + if store.0.replay_enabled() { + call_host_dynamic_replay(store, instance, ty, types, options, storage, async_) + } else { + call_host_dynamic_impl( + store, + instance, + ty, + types, + options, + storage, + async_, + caller_instance, + flags, + closure, + ) + } + } +} + +unsafe fn call_host_dynamic_impl( + mut store: StoreContextMut<'_, T>, + instance: Instance, + ty: TypeFuncIndex, + types: &Arc, + options: OptionsIndex, + storage: &mut [MaybeUninit], + async_: bool, + caller_instance: RuntimeComponentInstanceIndex, + mut flags: InstanceFlags, + closure: F, +) -> Result<()> +where + F: Fn( + StoreContextMut<'_, T>, + ComponentFunc, + Vec, + usize, + ) -> Pin>> + Send + 'static>> + + Send + + Sync + + 'static, + T: 'static, +{ let func_ty = &types[ty]; let param_tys = &types[func_ty.params]; let result_tys = &types[func_ty.results]; + let mut params_and_results = Vec::new(); + let mut lift = &mut LiftContext::new(store.0.store_opaque_mut(), options, instance); + lift.enter_call(); let max_flat = if async_ { MAX_FLAT_ASYNC_PARAMS } else { MAX_FLAT_PARAMS }; + let ty = ComponentFunc::from(ty, &lift.instance_type()); - // This top-level switch determines whether or not we're in replay mode or - // not. In replay mode, we skip all lifting and execution of host functions and - // just replay lowering effects observed in the trace - if !store.0.replay_enabled() { - let mut params_and_results = Vec::new(); - let mut lift = &mut LiftContext::new(store.0.store_opaque_mut(), options, instance); - lift.enter_call(); - let ty = ComponentFunc::from(ty, &lift.instance_type()); + let ret_index = unsafe { + dynamic_params_load( + &mut lift, + types, + storage, + param_tys, + &mut params_and_results, + max_flat, + )? + }; + let result_start = params_and_results.len(); + for _ in 0..result_tys.types.len() { + params_and_results.push(Val::Bool(false)); + } - let ret_index = unsafe { - dynamic_params_load( - &mut lift, - types, - storage, - param_tys, - &mut params_and_results, - max_flat, - )? - }; - let result_start = params_and_results.len(); - for _ in 0..result_tys.types.len() { - params_and_results.push(Val::Bool(false)); - } + rr::component_hooks::record_validate_host_func_entry( + storage, + types, + &InterfaceType::Tuple(func_ty.params), + store.0.store_opaque_mut(), + )?; - rr::component_hooks::record_validate_host_func_entry( - storage, - types, - &InterfaceType::Tuple(func_ty.params), - store.0.store_opaque_mut(), - )?; + if async_ { + #[cfg(feature = "component-model-async")] + { + let retptr = if result_tys.types.len() == 0 { + 0 + } else { + let retptr = unsafe { storage[ret_index].assume_init() }; + let mut lower = LowerContext::new(store.as_context_mut(), options, instance); + validate_inbounds_dynamic(&result_tys.abi, lower.as_slice_mut(), &retptr)? + }; - if async_ { - #[cfg(feature = "component-model-async")] - { - let retptr = if result_tys.types.len() == 0 { - 0 - } else { - let retptr = unsafe { storage[ret_index].assume_init() }; - let mut lower = LowerContext::new(store.as_context_mut(), options, instance); - validate_inbounds_dynamic(&result_tys.abi, lower.as_slice_mut(), &retptr)? - }; - - let future = closure(store.as_context_mut(), ty, params_and_results, result_start); - - let task = - instance.first_poll(store.as_context_mut(), future, caller_instance, { - let result_tys = func_ty.results; - move |store: StoreContextMut, result_vals: Vec| { - unsafe { - flags.set_may_leave(false); - } - - let mut lower = LowerContext::new(store, options, instance); - let result_tys = &lower.types[result_tys]; - let result_vals = &result_vals[result_start..]; - assert_eq!(result_vals.len(), result_tys.types.len()); - let mut ptr = retptr; - for (val, ty) in result_vals.iter().zip(result_tys.types.iter()) { - let offset = - lower.types.canonical_abi(ty).next_field32_size(&mut ptr); - rr::component_hooks::record_lower_memory( - |cx, ty, ptr| val.store(cx, ty, ptr), - &mut lower, - *ty, - offset, - )?; - } - - unsafe { - flags.set_may_leave(true); - } - - lower.exit_call()?; - - Ok(()) - } - })?; - - let status = if let Some(task) = task { - Status::Started.pack(Some(task)) - } else { - Status::Returned.pack(None) - }; - - storage[0] = MaybeUninit::new(ValRaw::i32(status as i32)); - rr::component_hooks::record_host_func_return( - &storage[..1], - types, - &InterfaceType::U32, - store.0, - )?; - } - #[cfg(not(feature = "component-model-async"))] - { - unreachable!( - "async-lowered imports should have failed validation \ - when `component-model-async` feature disabled" - ); - } - } else { let future = closure(store.as_context_mut(), ty, params_and_results, result_start); - let result_vals = concurrent::poll_and_block(store.0, future, caller_instance)?; - let result_vals = &result_vals[result_start..]; - unsafe { - flags.set_may_leave(false); - } + let task = instance.first_poll(store.as_context_mut(), future, caller_instance, { + let result_tys = func_ty.results; + move |store: StoreContextMut, result_vals: Vec| { + unsafe { + flags.set_may_leave(false); + } - let mut cx = LowerContext::new(store, options, 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()) { - rr::component_hooks::record_lower_flat( - |cx, ty| val.lower(cx, ty, &mut dst), - &mut cx, - *ty, - )?; + let mut lower = LowerContext::new(store, options, instance); + let result_tys = &lower.types[result_tys]; + let result_vals = &result_vals[result_start..]; + assert_eq!(result_vals.len(), result_tys.types.len()); + let mut ptr = retptr; + for (val, ty) in result_vals.iter().zip(result_tys.types.iter()) { + let offset = lower.types.canonical_abi(ty).next_field32_size(&mut ptr); + val.store(&mut lower, *ty, offset)?; + } + + unsafe { + flags.set_may_leave(true); + } + + lower.exit_call()?; + + Ok(()) } - assert!(dst.next().is_none()); - rr::component_hooks::record_host_func_return( - storage, - cx.types, - &InterfaceType::Tuple(func_ty.results), - cx.store.0, - )?; + })?; + + let status = if let Some(task) = task { + Status::Started.pack(Some(task)) } else { - let ret_ptr = unsafe { 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 = cx.types.canonical_abi(ty).next_field32_size(&mut ptr); - rr::component_hooks::record_lower_memory( - |cx, ty, ptr| val.store(cx, ty, ptr), - &mut cx, - *ty, - offset, - )?; - } - // Lower store into pointer - rr::component_hooks::record_host_func_return( - &storage[ret_index..ret_index + 1], - cx.types, - &InterfaceType::U32, - cx.store.0, + Status::Returned.pack(None) + }; + + storage[0] = MaybeUninit::new(ValRaw::i32(status as i32)); + } + #[cfg(not(feature = "component-model-async"))] + { + unreachable!( + "async-lowered imports should have failed validation \ + when `component-model-async` feature disabled" + ); + } + } else { + let future = closure(store.as_context_mut(), ty, params_and_results, result_start); + let result_vals = concurrent::poll_and_block(store.0, future, caller_instance)?; + let result_vals = &result_vals[result_start..]; + + unsafe { + flags.set_may_leave(false); + } + + let mut cx = LowerContext::new(store, options, 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()) { + rr::component_hooks::record_lower_flat( + |cx, ty| val.lower(cx, ty, &mut dst), + &mut cx, + *ty, )?; } + assert!(dst.next().is_none()); + rr::component_hooks::record_host_func_return( + storage, + cx.types, + &InterfaceType::Tuple(func_ty.results), + cx.store.0, + )?; + } else { + let ret_ptr = unsafe { 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 = cx.types.canonical_abi(ty).next_field32_size(&mut ptr); + rr::component_hooks::record_lower_memory( + |cx, ty, ptr| val.store(cx, ty, ptr), + &mut cx, + *ty, + offset, + )?; + } + // Lower store into pointer + rr::component_hooks::record_host_func_return( + &storage[ret_index..ret_index + 1], + cx.types, + &InterfaceType::U32, + cx.store.0, + )?; + } - unsafe { - flags.set_may_leave(true); + unsafe { + flags.set_may_leave(true); + } + + cx.exit_call()?; + } + + Ok(()) +} + +unsafe fn call_host_dynamic_replay( + store: StoreContextMut<'_, T>, + instance: Instance, + ty: TypeFuncIndex, + types: &Arc, + options: OptionsIndex, + storage: &mut [MaybeUninit], + async_: bool, +) -> Result<()> { + #[cfg(feature = "rr")] + { + use crate::rr::component_hooks::ReplayLoweringPhase; + // Mirror of `dynamic_params_load` for replay. Keep in sync + fn dynamic_params_load_replay(param_tys: &TypeTuple, max_flat_params: usize) -> usize { + if let Some(param_count) = param_tys.abi.flat_count(max_flat_params) { + param_count + } else { + 1 } + } - cx.exit_call()?; + if async_ { + unreachable!( + "Replay logic should be unreachable with component async-ABI (currently unsupported)" + ); } - } else { + let func_ty = &types[ty]; + let param_tys = &types[func_ty.params]; + let result_tys = &types[func_ty.results]; + rr::component_hooks::replay_validate_host_func_entry( storage, types, &InterfaceType::Tuple(func_ty.params), store.0.store_opaque_mut(), )?; - // Replay host function path: Just lower the results from the trace - #[cfg(feature = "rr")] - { - let mut cx = LowerContext::new(store, options, instance); - // Skip lifting/lowering logic, and just replaying the lowering state - if async_ { - #[cfg(feature = "component-model-async")] - cx.replay_lowering(Some(&mut storage[..1]), ReplayLoweringPhase::HostFuncReturn)?; - #[cfg(not(feature = "component-model-async"))] - unreachable!( - "async-lowered imports should have failed validation \ - when `component-model-async` feature disabled" - ); - } else { - let ret_index = unsafe { dynamic_params_load_replay(param_tys, max_flat) }; - // Copy the entire contiguous storage slice instead of looping - if let Some(_cnt) = result_tys.abi.flat_count(MAX_FLAT_RESULTS) { - cx.replay_lowering(Some(storage), ReplayLoweringPhase::HostFuncReturn)?; - } else { - cx.replay_lowering( - Some(&mut storage[ret_index..ret_index + 1]), - ReplayLoweringPhase::HostFuncReturn, - )?; - } - } - } - #[cfg(not(feature = "rr"))] - { - unreachable!("cannot reach host function replay when rr is disabled"); + + let mut cx = LowerContext::new(store, options, instance); + + // Skip lifting/lowering logic, and just replaying the lowering state + let ret_index = dynamic_params_load_replay(param_tys, MAX_FLAT_PARAMS); + // Copy the entire contiguous storage slice instead of looping + if let Some(_cnt) = result_tys.abi.flat_count(MAX_FLAT_RESULTS) { + cx.replay_lowering(Some(storage), ReplayLoweringPhase::HostFuncReturn)?; + } else { + cx.replay_lowering( + Some(&mut storage[ret_index..ret_index + 1]), + ReplayLoweringPhase::HostFuncReturn, + )?; } + Ok(()) + } + #[cfg(not(feature = "rr"))] + { + let _ = (store, instance, ty, types, options, storage, async_); + unreachable!("Host function replay logic should be unreached when `rr` is disabled"); } - - Ok(()) } /// Loads the parameters for a dynamic host function call into `params` @@ -1015,16 +1064,6 @@ unsafe fn dynamic_params_load( } } -/// Replay of return values from `dynamic_params_load`. Keep in sync -#[cfg(feature = "rr")] -unsafe fn dynamic_params_load_replay(param_tys: &TypeTuple, max_flat_params: usize) -> usize { - if let Some(param_count) = param_tys.abi.flat_count(max_flat_params) { - param_count - } else { - 1 - } -} - pub(crate) fn validate_inbounds_dynamic( abi: &CanonicalAbiInfo, memory: &[u8], diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 83f32e9c3e..3804b0ae3f 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -2449,9 +2449,8 @@ impl HostContext { // Note that this function is intentionally scoped into a // separate closure to fit everything inside `enter_host_from_wasm` // below. - let run = move |mut caller: Caller<'_, T>| { - let mut args = - NonNull::slice_from_raw_parts(args.cast::>(), args_len); + let run = move |caller: Caller<'_, T>| { + let args = NonNull::slice_from_raw_parts(args.cast::>(), args_len); // SAFETY: it's a safety contract of this function itself that // `callee_vmctx` is safe to read. let state = unsafe { @@ -2484,71 +2483,30 @@ impl HostContext { wasm_func_type.returns().into_iter().map(|x| x.byte_size()), ); - if !caller.store.0.replay_enabled() { - // Don't need auto-assert GC store here since we aren't using P, just raw args for recording - rr::core_hooks::record_validate_host_func_entry( - unsafe { &args.as_ref()[..num_params] }, - flat_size_params, - caller.store.0, - )?; - 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) } - }; - // SAFETY: this function requires `args` to be valid and the - // `WasmTyList` trait means that everything should be correctly - // ascribed/typed, making this valid to load from. - unsafe { 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() - }; - - if !ret.compatible_with_store(caller.store.0) { - bail!("host function attempted to return cross-`Store` value to Wasm") + unsafe { + // This top-level switch determines whether or not we're in replay mode or + // not. In replay mode, we skip execution of host functions and + // just replay the return value effects observed in the trace + if caller.store.0.replay_enabled() { + Self::array_call_trampoline_replay( + caller, + args, + num_params, + flat_size_params, + num_results, + ) } else { - let mut store = if R::may_gc() { - AutoAssertNoGc::new(caller.store.0) - } else { - unsafe { AutoAssertNoGc::disabled(caller.store.0) } - }; - // SAFETY: this function requires that `args` is safe for this - // type signature, and the guarantees of `WasmRet` means that - // everything should be typed appropriately. - unsafe { ret.store(&mut store, args.as_mut())? }; + Self::array_call_trampoline_impl( + caller, + args, + func, + num_params, + flat_size_params, + num_results, + flat_size_results, + ) } - // Record the return values - rr::core_hooks::record_host_func_return( - unsafe { &args.as_ref()[..num_results] }, - flat_size_results, - caller.store.0, - )?; - } else { - rr::core_hooks::replay_validate_host_func_entry( - unsafe { &args.as_ref()[..num_params] }, - flat_size_params, - caller.store.0, - )?; - - // Replay the return values - rr::core_hooks::replay_host_func_return( - unsafe { &mut args.as_mut()[..num_results] }, - &mut caller, - )?; } - - Ok(()) }; // With nothing else on the stack move `run` into this @@ -2564,6 +2522,96 @@ impl HostContext { }) } } + + unsafe fn array_call_trampoline_replay( + mut caller: Caller<'_, T>, + mut args: NonNull<[MaybeUninit]>, + num_params: usize, + flat_size_params: impl Iterator, + num_results: usize, + ) -> Result<()> + where + T: 'static, + { + rr::core_hooks::replay_validate_host_func_entry( + unsafe { &args.as_ref()[..num_params] }, + flat_size_params, + caller.store.0, + )?; + rr::core_hooks::replay_host_func_return( + unsafe { &mut args.as_mut()[..num_results] }, + &mut caller, + )?; + Ok(()) + } + + unsafe fn array_call_trampoline_impl( + mut caller: Caller<'_, T>, + mut args: NonNull<[MaybeUninit]>, + func: &F, + num_params: usize, + flat_size_params: impl Iterator, + num_results: usize, + flat_size_results: impl Iterator, + ) -> Result<()> + where + F: Fn(Caller<'_, T>, P) -> R + 'static, + P: WasmTyList, + R: WasmRet, + T: 'static, + { + // Don't need auto-assert GC store here since we aren't using P, just raw args for recording + rr::core_hooks::record_validate_host_func_entry( + unsafe { &args.as_ref()[..num_params] }, + flat_size_params, + caller.store.0, + )?; + + let ret = 'ret: { + if let Err(trap) = caller.store.0.call_hook(CallHook::CallingHost) { + break 'ret R::fallible_from_error(trap); + } + + let mut store = if P::may_gc() { + AutoAssertNoGc::new(caller.store.0) + } else { + unsafe { AutoAssertNoGc::disabled(caller.store.0) } + }; + // SAFETY: this function requires `args` to be valid and the + // `WasmTyList` trait means that everything should be correctly + // ascribed/typed, making this valid to load from. + let params = unsafe { P::load(&mut store, args.as_mut()) }; + let _ = &mut store; + drop(store); + + 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() + }; + + 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) } + }; + // SAFETY: this function requires that `args` is safe for this + // type signature, and the guarantees of `WasmRet` means that + // everything should be typed appropriately. + unsafe { ret.store(&mut store, args.as_mut())? }; + } + // Record the return values + rr::core_hooks::record_host_func_return( + unsafe { &args.as_ref()[..num_results] }, + flat_size_results, + caller.store.0, + )?; + Ok(()) + } } /// Representation of a host-defined function. diff --git a/crates/wasmtime/src/runtime/rr.rs b/crates/wasmtime/src/runtime/rr.rs index c94bb18fc3..e6af646720 100644 --- a/crates/wasmtime/src/runtime/rr.rs +++ b/crates/wasmtime/src/runtime/rr.rs @@ -4,6 +4,14 @@ use crate::ValRaw; use ::core::mem::MaybeUninit; +/// Component-async-ABI is not supported for record/replay yet; add a feature gate +//const _: () = { +// #[cfg(all(feature = "rr", feature = "component-model-async"))] +// compile_error!( +// "The `component-model-async` feature is not supported with the `rr` feature yet" +// ); +//}; + /// Types that can be serialized/deserialized into/from /// flat types for record and replay #[allow( diff --git a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs index 3d17d05afa..0b04367b74 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs @@ -127,6 +127,7 @@ pub fn record_validate_host_func_entry( /// Replay hook operation for host function entry events #[inline] +#[cfg(feature = "rr")] pub fn replay_validate_host_func_entry( args: &mut [MaybeUninit], types: &Arc, From fd09d720a727666e328518e76b81fe9ccae59334 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Sat, 29 Nov 2025 14:33:53 -0500 Subject: [PATCH 56/73] Add safety contract for uninit valraw byte conversion --- crates/wasmtime/src/runtime/rr.rs | 26 ++++++++++++------- crates/wasmtime/src/runtime/rr/core/events.rs | 2 +- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/crates/wasmtime/src/runtime/rr.rs b/crates/wasmtime/src/runtime/rr.rs index e6af646720..57facce276 100644 --- a/crates/wasmtime/src/runtime/rr.rs +++ b/crates/wasmtime/src/runtime/rr.rs @@ -2,7 +2,7 @@ //! //! This feature is currently not optimized and under development use crate::ValRaw; -use ::core::mem::MaybeUninit; +use ::core::{mem::MaybeUninit, slice}; /// Component-async-ABI is not supported for record/replay yet; add a feature gate //const _: () = { @@ -19,13 +19,13 @@ use ::core::mem::MaybeUninit; reason = "trait used as a bound for hooks despite not calling methods directly" )] pub trait FlatBytes { - fn bytes(&self, size: u8) -> &[u8]; + unsafe fn bytes(&self, size: u8) -> &[u8]; fn from_bytes(value: &[u8]) -> Self; } impl FlatBytes for ValRaw { #[inline] - fn bytes(&self, size: u8) -> &[u8] { + unsafe fn bytes(&self, size: u8) -> &[u8] { &self.get_bytes()[..size as usize] } #[inline] @@ -36,12 +36,20 @@ impl FlatBytes for ValRaw { impl FlatBytes for MaybeUninit { #[inline] - fn bytes(&self, size: u8) -> &[u8] { - // Uninitialized data is assumed and serialized, so hence - // may contain some undefined values. But these are irrelevant - // when serializing to `RRFuncArgVals` - let val = unsafe { self.assume_init_ref() }; - val.bytes(size) + /// SAFETY: the caller must ensure that 'size' number of bytes provided + /// are initialized for the underlying ValRaw. + /// When serializing for record/replay, uninitialized parts of the ValRaw + /// are not relevant, so this only accesses initialized values as long as + /// the size contract is upheld. + unsafe fn bytes(&self, size: u8) -> &[u8] { + // The cleanest way for this would use MaybeUninit::as_bytes and an assume_init(), + // but that is currently only available in nightly. + let ptr = self.as_ptr().cast::>(); + // SAFETY: the caller must ensure that 'size' bytes are initialized + unsafe { + let s = slice::from_raw_parts(ptr, size as usize); + &*(s as *const [MaybeUninit] as *const [u8]) + } } #[inline] fn from_bytes(value: &[u8]) -> Self { diff --git a/crates/wasmtime/src/runtime/rr/core/events.rs b/crates/wasmtime/src/runtime/rr/core/events.rs index c9c4a6c4f5..8bba175c37 100644 --- a/crates/wasmtime/src/runtime/rr/core/events.rs +++ b/crates/wasmtime/src/runtime/rr/core/events.rs @@ -68,7 +68,7 @@ impl RRFuncArgVals { let mut bytes = Vec::new(); let mut sizes = Vec::new(); for (flat_size, arg) in flat.zip(args.iter()) { - bytes.extend_from_slice(&arg.bytes(flat_size)); + bytes.extend_from_slice(unsafe { &arg.bytes(flat_size) }); sizes.push(flat_size); } RRFuncArgVals { bytes, sizes } From 01ce52f471cb5f5c433a9e70ab9e0d19b10a1a51 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Sun, 30 Nov 2025 20:04:12 -0500 Subject: [PATCH 57/73] Fixed validation of instantiation before start fn; add component builtin test --- .../src/runtime/component/instance.rs | 15 +- crates/wasmtime/src/runtime/instance.rs | 40 +-- crates/wasmtime/src/runtime/rr/core.rs | 10 +- crates/wasmtime/src/runtime/rr/hooks.rs | 40 ++- .../src/runtime/rr/hooks/component_hooks.rs | 39 ++- .../src/runtime/rr/hooks/core_hooks.rs | 70 ++++- .../wasmtime/src/runtime/rr/replay_driver.rs | 243 +++++++++--------- tests/all/rr.rs | 105 ++++++++ 8 files changed, 371 insertions(+), 191 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/instance.rs b/crates/wasmtime/src/runtime/component/instance.rs index 42258bdd6d..c5f838a540 100644 --- a/crates/wasmtime/src/runtime/component/instance.rs +++ b/crates/wasmtime/src/runtime/component/instance.rs @@ -9,7 +9,8 @@ use crate::instance::OwnedImports; use crate::linker::DefinitionType; use crate::prelude::*; #[cfg(feature = "rr")] -use crate::rr::{RecordBuffer, component_events::InstantiationEvent}; +use crate::rr::RecordBuffer; +use crate::rr::component_hooks; use crate::runtime::vm::component::{ CallContexts, ComponentInstance, ResourceTables, TypedResource, TypedResourceIndex, }; @@ -1080,7 +1081,6 @@ impl<'a> Instantiator<'a> { /// Convenience helper to return the instance ID of the `ComponentInstance` that's /// being instantiated - #[cfg(feature = "rr")] fn id(&self) -> ComponentInstanceId { self.id } @@ -1194,12 +1194,13 @@ impl InstancePre { .increment_component_instance_count()?; let mut instantiator = Instantiator::new(&self.component, store.0, &self.imports); + // Record/replay hooks store.0.validate_rr_config()?; - #[cfg(feature = "rr")] - store.0.record_event(|| InstantiationEvent { - component: *self.component.checksum(), - instance: instantiator.id(), - })?; + component_hooks::record_and_replay_validate_instantiation( + &mut store, + *self.component.checksum(), + instantiator.id(), + )?; instantiator.run(&mut store).await.map_err(|e| { store diff --git a/crates/wasmtime/src/runtime/instance.rs b/crates/wasmtime/src/runtime/instance.rs index 49fd9d32ac..0bfd926cbe 100644 --- a/crates/wasmtime/src/runtime/instance.rs +++ b/crates/wasmtime/src/runtime/instance.rs @@ -1,7 +1,6 @@ use crate::linker::{Definition, DefinitionType}; use crate::prelude::*; -#[cfg(feature = "rr")] -use crate::rr::core_events::InstantiationEvent; +use crate::rr::core_hooks; use crate::runtime::vm::{ self, Imports, ModuleRuntimeInfo, VMFuncRef, VMFunctionImport, VMGlobalImport, VMMemoryImport, VMStore, VMTableImport, VMTagImport, @@ -262,18 +261,18 @@ impl Instance { unsafe { Instance::new_raw(store, limiter.as_mut(), module, imports).await? } }; + // Record/replay hooks store.0.validate_rr_config()?; if !from_component { - #[cfg(feature = "rr")] - { - // Components already record instantiation, so do not record their internal modules - rr_validate_module_unexported_memory(module)?; - store.0.record_event(|| InstantiationEvent { - module: *module.checksum(), - instance: instance.id(), - })?; - } + // Components already record instantiation, so do not record their internal modules + core_hooks::rr_validate_module_unexported_memory(&module)?; + core_hooks::record_and_replay_validate_instantiation( + store, + *module.checksum(), + instance.id(), + )?; } + if let Some(start) = start { if store.0.async_support() { #[cfg(feature = "async")] @@ -1008,22 +1007,3 @@ fn typecheck( } Ok(()) } - -/// Ensure that memories are not exported memories in Core wasm modules when -/// recording is enabled -#[cfg(feature = "rr")] -fn rr_validate_module_unexported_memory(module: &Module) -> Result<()> { - // Check for exported memories when recording is enabled. - if module.engine().is_recording() - && module.exports().any(|export| { - if let crate::ExternType::Memory(_) = export.ty() { - true - } else { - false - } - }) - { - bail!("Cannot support recording for core wasm modules when a memory is exported"); - } - Ok(()) -} diff --git a/crates/wasmtime/src/runtime/rr/core.rs b/crates/wasmtime/src/runtime/rr/core.rs index e0f4cf2a69..c2b58f7bb5 100644 --- a/crates/wasmtime/src/runtime/rr/core.rs +++ b/crates/wasmtime/src/runtime/rr/core.rs @@ -576,14 +576,20 @@ mod tests { use wasmtime_environ::FuncIndex; impl ReplayBuffer { + /// Pop the next replay event and calls `f` with a expected event type + /// + /// ## Errors + /// + /// See [`next_event_typed`](Replayer::next_event_typed) + #[inline] fn next_event_and(&mut self, f: F) -> Result<(), ReplayError> where T: TryFrom, ReplayError: From<>::Error>, F: FnOnce(T) -> Result<(), ReplayError>, { - let event = self.next_event_typed::()?; - f(event) + let call_event = self.next_event_typed()?; + Ok(f(call_event)?) } } diff --git a/crates/wasmtime/src/runtime/rr/hooks.rs b/crates/wasmtime/src/runtime/rr/hooks.rs index 0f3ae22712..b8549c929e 100644 --- a/crates/wasmtime/src/runtime/rr/hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks.rs @@ -1,15 +1,17 @@ -/// Component specific RR hooks that use `component-model` feature gating -#[cfg(feature = "component-model")] -pub mod component_hooks; -/// Core RR hooks -pub mod core_hooks; - use crate::{FuncType, WasmFuncOrigin}; +#[cfg(feature = "rr")] +use crate::{StoreContextMut, rr::ReplayHostContext}; #[cfg(feature = "component-model")] use alloc::sync::Arc; #[cfg(feature = "component-model")] use wasmtime_environ::component::{ComponentTypes, TypeFuncIndex}; +/// Component specific RR hooks that use `component-model` feature gating +#[cfg(feature = "component-model")] +pub mod component_hooks; +/// Core RR hooks +pub mod core_hooks; + /// Wasm function type information for RR hooks pub enum RRWasmFuncType<'a> { /// Core RR hooks to be performed @@ -27,3 +29,29 @@ pub enum RRWasmFuncType<'a> { #[cfg(feature = "component-model")] None, } + +/// Obtain the replay host context from the store. +/// +/// SAFETY: The store's data is always of type `ReplayHostContext` when created by +/// the replay driver. As an additional guarantee, we assert that replay is indeed +/// truly enabled. +#[cfg(feature = "rr")] +unsafe fn replay_data_from_store<'a, T: 'static>( + store: &StoreContextMut<'a, T>, +) -> &'a ReplayHostContext { + assert!(store.0.replay_enabled()); + let raw_ptr: *const T = store.data(); + unsafe { &*(raw_ptr as *const ReplayHostContext) } +} + +/// Same as [replay_data_from_store], but mutable +/// +/// SAFETY: See [replay_data_from_store] +#[cfg(feature = "rr")] +unsafe fn replay_data_from_store_mut<'a, T: 'static>( + store: &mut StoreContextMut<'a, T>, +) -> &'a mut ReplayHostContext { + assert!(store.0.replay_enabled()); + let raw_ptr: *mut T = store.data_mut(); + unsafe { &mut *(raw_ptr as *mut ReplayHostContext) } +} diff --git a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs index 0b04367b74..71ec3524bb 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs @@ -1,21 +1,23 @@ +#[cfg(feature = "rr")] +use super::replay_data_from_store_mut; use crate::ValRaw; use crate::component::{ComponentInstanceId, func::LowerContext}; #[cfg(feature = "rr")] -use crate::rr::common_events::{HostFuncEntryEvent, WasmFuncReturnEvent}; +use crate::rr::common_events::{HostFuncEntryEvent, HostFuncReturnEvent, WasmFuncReturnEvent}; #[cfg(feature = "rr")] use crate::rr::component_events::{ - LowerFlatEntryEvent, LowerFlatReturnEvent, LowerMemoryEntryEvent, LowerMemoryReturnEvent, - MemorySliceWriteEvent, PostReturnEvent, WasmFuncBeginEvent, WasmFuncEntryEvent, + InstantiationEvent, LowerFlatEntryEvent, LowerFlatReturnEvent, LowerMemoryEntryEvent, + LowerMemoryReturnEvent, MemorySliceWriteEvent, PostReturnEvent, WasmFuncBeginEvent, + WasmFuncEntryEvent, }; #[cfg(feature = "rr")] -use crate::rr::{ - RRFuncArgVals, RecordBuffer, Recorder, ResultEvent, common_events::HostFuncReturnEvent, -}; +use crate::rr::{RRFuncArgVals, RecordBuffer, Recorder, ResultEvent, Validate}; use crate::store::StoreOpaque; use crate::{StoreContextMut, prelude::*}; use alloc::sync::Arc; use core::mem::MaybeUninit; use core::ops::{Deref, DerefMut}; +use wasmtime_environ::WasmChecksum; use wasmtime_environ::component::{ComponentTypes, ExportIndex, InterfaceType, TypeFuncIndex}; #[cfg(all(feature = "rr"))] use wasmtime_environ::component::{MAX_FLAT_PARAMS, MAX_FLAT_RESULTS}; @@ -206,6 +208,31 @@ where lower_result } +/// Hook for recording a component instantiation event and validating the +/// instantiation during replay. +#[inline] +pub fn record_and_replay_validate_instantiation( + store: &mut StoreContextMut<'_, T>, + component: WasmChecksum, + instance: ComponentInstanceId, +) -> Result<()> { + #[cfg(feature = "rr")] + { + store.0.record_event(|| InstantiationEvent { + component, + instance, + })?; + if store.0.replay_enabled() { + let replay_data = unsafe { replay_data_from_store_mut(store) }; + replay_data.take_current_component_instantiation().expect( + "replay driver should have set component instantiate data before trying to validate it", + ).validate(&InstantiationEvent { component, instance })?; + } + } + let _ = (store, component, instance); + Ok(()) +} + #[cfg(feature = "rr")] #[inline(always)] fn create_host_func_entry_event( diff --git a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs index 2c28de4d2e..1d2a26f626 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs @@ -1,14 +1,18 @@ +#[cfg(feature = "rr")] +use super::{replay_data_from_store, replay_data_from_store_mut}; use crate::rr::FlatBytes; #[cfg(feature = "rr")] use crate::rr::{ - RREvent, RRFuncArgVals, ReplayError, ReplayHostContext, Replayer, ResultEvent, + RREvent, RRFuncArgVals, ReplayError, Replayer, ResultEvent, Validate, common_events::HostFuncEntryEvent, common_events::HostFuncReturnEvent, - common_events::WasmFuncReturnEvent, core_events::WasmFuncEntryEvent, + common_events::WasmFuncReturnEvent, core_events::InstantiationEvent, + core_events::WasmFuncEntryEvent, }; -use crate::store::StoreOpaque; -use crate::{Caller, FuncType, StoreContextMut, ValRaw, WasmFuncOrigin, prelude::*}; +use crate::store::{InstanceId, StoreOpaque}; +use crate::{Caller, FuncType, Module, StoreContextMut, ValRaw, WasmFuncOrigin, prelude::*}; #[cfg(feature = "rr")] use wasmtime_environ::EntityIndex; +use wasmtime_environ::WasmChecksum; /// Record and replay hook operation for core wasm function entry events /// @@ -121,7 +125,7 @@ where Ok(()) } -/// Replay hook operation for host function return events +/// Replay hook operation for host function return events. #[inline] pub fn replay_host_func_return( args: &mut [T], @@ -146,14 +150,9 @@ where RREvent::CoreWasmFuncEntry(event) => { let entity = EntityIndex::from(event.origin.index); - // SAFETY: The store's data is always of type `ReplayHostContext` when created by - // the replay driver. As an additional guarantee, we assert that replay is indeed - // truly enabled. - assert!(caller.store.0.replay_enabled()); - let replay_data = unsafe { - let raw_ptr: *const U = caller.store.data(); - &*(raw_ptr as *const ReplayHostContext) - }; + // Unwrapping the `replay_buffer_mut()` above ensures that we are in replay mode + // passing the safety contract for `replay_data_from_store` + let replay_data = unsafe { replay_data_from_store(&caller.store) }; // Grab the correct module instance let instance = replay_data.get_module_instance(event.origin.instance)?; @@ -194,3 +193,48 @@ where let _ = (args, caller); Ok(()) } + +/// Hook for recording a module instantiation event and validating the +/// instantiation during replay. +pub fn record_and_replay_validate_instantiation( + store: &mut StoreContextMut<'_, T>, + module: WasmChecksum, + instance: InstanceId, +) -> Result<()> { + #[cfg(feature = "rr")] + { + store + .0 + .record_event(|| InstantiationEvent { module, instance })?; + if store.0.replay_enabled() { + let replay_data = unsafe { replay_data_from_store_mut(store) }; + replay_data.take_current_module_instantiation().expect( + "replay driver should have set module instantiate data before trying to validate it", + ).validate(&InstantiationEvent { module, instance })?; + } + } + let _ = (store, module, instance); + Ok(()) +} + +/// Ensure that memories are not exported memories in Core wasm modules when +/// recording is enabled. +pub fn rr_validate_module_unexported_memory(module: &Module) -> Result<()> { + // Check for exported memories when recording is enabled. + #[cfg(feature = "rr")] + { + if module.engine().is_recording() + && module.exports().any(|export| { + if let crate::ExternType::Memory(_) = export.ty() { + true + } else { + false + } + }) + { + bail!("Cannot support recording for core wasm modules when a memory is exported"); + } + } + let _ = module; + Ok(()) +} diff --git a/crates/wasmtime/src/runtime/rr/replay_driver.rs b/crates/wasmtime/src/runtime/rr/replay_driver.rs index 8ca4bc6bab..87024e2b1f 100644 --- a/crates/wasmtime/src/runtime/rr/replay_driver.rs +++ b/crates/wasmtime/src/runtime/rr/replay_driver.rs @@ -1,9 +1,8 @@ -use crate::rr::{RREvent, ReplayError, Validate, core_events}; +use crate::rr::{RREvent, ReplayError, component_events, core_events}; use crate::store::InstanceId; use crate::{AsContextMut, Engine, Module, ReplayReader, ReplaySettings, Store, prelude::*}; use crate::{ - ValRaw, component, component::Component, component::ComponentInstanceId, rr::component_events, - rr::component_hooks, + ValRaw, component, component::Component, component::ComponentInstanceId, rr::component_hooks, }; use alloc::{collections::BTreeMap, sync::Arc}; use core::mem::MaybeUninit; @@ -42,6 +41,18 @@ impl ReplayEnvironment { self } + fn get_component(&self, checksum: WasmChecksum) -> Result<&Component, ReplayError> { + self.components + .get(&checksum) + .ok_or(ReplayError::MissingComponent(checksum)) + } + + fn get_module(&self, checksum: WasmChecksum) -> Result<&Module, ReplayError> { + self.modules + .get(&checksum) + .ok_or(ReplayError::MissingModule(checksum)) + } + /// Instantiate a new [`ReplayInstance`] using a [`ReplayReader`] in context of this environment pub fn instantiate(&self, reader: impl ReplayReader + 'static) -> Result { self.instantiate_with(reader, |_| Ok(()), |_| Ok(()), |_| Ok(())) @@ -60,6 +71,8 @@ impl ReplayEnvironment { &self.engine, ReplayHostContext { module_instances: BTreeMap::new(), + current_module_instantiation: None, + current_component_instantiation: None, }, ); store_fn(&mut store)?; @@ -83,16 +96,21 @@ pub struct ReplayHostContext { /// A tracker of instantiated modules. /// /// Core wasm modules can be re-entrant and invoke methods from other instances, and this - /// needs to be accessible within host functions + /// needs to be accessible within host functions. module_instances: BTreeMap, + /// The currently executing module instantiation event. + /// + /// This must be set by the driver prior to instantiation and cleared after + /// used internally for validation. + current_module_instantiation: Option, + /// The currently executing component instantiation event. + /// + /// This must be set by the driver prior to instantiation and cleared after + /// used internally for validation. + current_component_instantiation: Option, } impl ReplayHostContext { - /// Insert a module instance into the context's tracking map. - fn insert_module_instance(&mut self, id: InstanceId, instance: crate::Instance) { - self.module_instances.insert(id, instance); - } - /// Get a module instance from the context's tracking map /// /// This is necessary for core wasm to identify re-entrant calls during replay. @@ -104,6 +122,22 @@ impl ReplayHostContext { .get(&id) .ok_or(ReplayError::MissingModuleInstance(id.as_u32())) } + + /// Take the current module instantiation event from the context, leaving + /// `None` in its place. + pub(crate) fn take_current_module_instantiation( + &mut self, + ) -> Option { + self.current_module_instantiation.take() + } + + /// Take the current component instantiation event from the context, leaving + /// `None` in its place. + pub(crate) fn take_current_component_instantiation( + &mut self, + ) -> Option { + self.current_component_instantiation.take() + } } /// A [`ReplayInstance`] is an object providing a opaquely managed, replayable [`Store`]. @@ -175,8 +209,39 @@ pub struct ReplayInstance { store: Store, component_linker: component::Linker, module_linker: crate::Linker, - module_instances: BTreeMap, - component_instances: BTreeMap, + module_instances: ModuleInstanceMap, + component_instances: ComponentInstanceMap, +} + +struct ComponentInstanceMap(BTreeMap); + +impl ComponentInstanceMap { + fn new() -> Self { + Self(BTreeMap::new()) + } + + fn get_mut( + &mut self, + id: ComponentInstanceId, + ) -> Result<&mut component::Instance, ReplayError> { + self.0 + .get_mut(&id) + .ok_or(ReplayError::MissingComponentInstance(id.as_u32())) + } +} + +struct ModuleInstanceMap(BTreeMap); + +impl ModuleInstanceMap { + fn new() -> Self { + Self(BTreeMap::new()) + } + + fn get_mut(&mut self, id: InstanceId) -> Result<&mut crate::Instance, ReplayError> { + self.0 + .get_mut(&id) + .ok_or(ReplayError::MissingModuleInstance(id.as_u32())) + } } impl ReplayInstance { @@ -205,8 +270,8 @@ impl ReplayInstance { store, component_linker, module_linker, - module_instances: BTreeMap::new(), - component_instances: BTreeMap::new(), + module_instances: ModuleInstanceMap::new(), + component_instances: ComponentInstanceMap::new(), }) } @@ -220,6 +285,22 @@ impl ReplayInstance { self.store } + fn insert_component_instance(&mut self, instance: component::Instance) { + self.component_instances + .0 + .insert(instance.id().instance(), instance); + } + + fn insert_module_instance(&mut self, instance: crate::Instance) { + self.module_instances.0.insert(instance.id(), instance); + // Insert into host context tracking as well, for re-entrancy calls + self.store + .as_context_mut() + .data_mut() + .module_instances + .insert(instance.id(), instance); + } + /// Run a single top-level event from the instance. /// /// "Top-level" events are those explicitly invoked events, namely: @@ -230,37 +311,20 @@ impl ReplayInstance { fn run_single_top_level_event(&mut self, rr_event: RREvent) -> Result<()> { match rr_event { RREvent::ComponentInstantiation(event) => { - // Find matching component from environment to instantiate - let component = self - .env - .components - .get(&event.component) - .ok_or(ReplayError::MissingComponent(event.component))?; - + let component = self.env.get_component(event.component)?; + // Set current instantiation event for validation + self.store.data_mut().current_component_instantiation = Some(event); let instance = self .component_linker .instantiate(self.store.as_context_mut(), component)?; - // Validate the instantiation event - event.validate(&component_events::InstantiationEvent { - component: *component.checksum(), - instance: instance.id().instance(), - })?; - - self.component_instances - .insert(instance.id().instance(), instance); + self.insert_component_instance(instance); } RREvent::ComponentWasmFuncBegin(event) => { - // Grab the correct component instance - let key = event.instance; - let instance = self - .component_instances - .get_mut(&key) - .ok_or(ReplayError::MissingComponentInstance(key.as_u32()))?; + let instance = self.component_instances.get_mut(event.instance)?; // Replay lowering steps and obtain raw value arguments to raw function call let func = component::Func::from_lifted_func(*instance, event.func_idx); let store = self.store.as_context_mut(); - // Call the function // // This is almost a mirror of the usage in [`component::Func::call_impl`] @@ -303,51 +367,22 @@ impl ReplayInstance { ); } RREvent::ComponentPostReturn(event) => { - // Grab the correct component instance - let key = event.instance; - let instance = self - .component_instances - .get_mut(&key) - .ok_or(ReplayError::MissingComponentInstance(key.as_u32()))?; - + let instance = self.component_instances.get_mut(event.instance)?; let func = component::Func::from_lifted_func(*instance, event.func_idx); let mut store = self.store.as_context_mut(); - func.post_return(&mut store)?; } RREvent::CoreWasmInstantiation(event) => { - // Find matching module from environment to instantiate - let module = self - .env - .modules - .get(&event.module) - .ok_or(ReplayError::MissingModule(event.module))?; - + let module = self.env.get_module(event.module)?; + // Set current instantiation event for validation + self.store.data_mut().current_module_instantiation = Some(event); let instance = self .module_linker .instantiate(self.store.as_context_mut(), module)?; - - // Validate the instantiation event - event.validate(&core_events::InstantiationEvent { - module: *module.checksum(), - instance: instance.id(), - })?; - - // Insert into host context tracking as well - self.module_instances.insert(instance.id(), instance); - self.store - .as_context_mut() - .data_mut() - .insert_module_instance(instance.id(), instance); + self.insert_module_instance(instance); } RREvent::CoreWasmFuncEntry(event) => { - // Grab the correct module instance - let key = event.origin.instance; - let instance = self - .module_instances - .get_mut(&key) - .ok_or(ReplayError::MissingModuleInstance(key.as_u32()))?; - + let instance = self.module_instances.get_mut(event.origin.instance)?; let entity = EntityIndex::from(event.origin.index); let mut store = self.store.as_context_mut(); let func = instance @@ -360,7 +395,6 @@ impl ReplayInstance { // Obtain the argument values for function call let mut results = vec![crate::Val::I64(0); func.ty(&store).results().len()]; let params = event.args.to_val_vec(&mut store, params_ty); - // Call the function // // This is almost a mirror of the usage in [`crate::Func::call_impl`] @@ -383,38 +417,21 @@ impl ReplayInstance { async fn run_single_top_level_event_async(&mut self, rr_event: RREvent) -> Result<()> { match rr_event { RREvent::ComponentInstantiation(event) => { - // Find matching component from environment to instantiate - let component = self - .env - .components - .get(&event.component) - .ok_or(ReplayError::MissingComponent(event.component))?; - + let component = self.env.get_component(event.component)?; + // Set current instantiation event for validation + self.store.data_mut().current_component_instantiation = Some(event); let instance = self .component_linker .instantiate_async(self.store.as_context_mut(), component) .await?; - // Validate the instantiation event - event.validate(&component_events::InstantiationEvent { - component: *component.checksum(), - instance: instance.id().instance(), - })?; - - self.component_instances - .insert(instance.id().instance(), instance); + self.insert_component_instance(instance); } RREvent::ComponentWasmFuncBegin(event) => { - // Grab the correct component instance - let key = event.instance; - let instance = self - .component_instances - .get_mut(&key) - .ok_or(ReplayError::MissingComponentInstance(key.as_u32()))?; + let instance = self.component_instances.get_mut(event.instance)?; // Replay lowering steps and obtain raw value arguments to raw function call let func = component::Func::from_lifted_func(*instance, event.func_idx); let mut store = self.store.as_context_mut(); - // Call the function // // This is almost a mirror of the usage in [`component::Func::call_impl`] @@ -463,51 +480,23 @@ impl ReplayInstance { ); } RREvent::ComponentPostReturn(event) => { - // Grab the correct component instance - let key = event.instance; - let instance = self - .component_instances - .get_mut(&key) - .ok_or(ReplayError::MissingComponentInstance(key.as_u32()))?; - + let instance = self.component_instances.get_mut(event.instance)?; let func = component::Func::from_lifted_func(*instance, event.func_idx); let mut store = self.store.as_context_mut(); - func.post_return_async(&mut store).await?; } RREvent::CoreWasmInstantiation(event) => { - // Find matching module from environment to instantiate - let module = self - .env - .modules - .get(&event.module) - .ok_or(ReplayError::MissingModule(event.module))?; - + let module = self.env.get_module(event.module)?; + // Set current instantiation event for validation + self.store.data_mut().current_module_instantiation = Some(event); let instance = self .module_linker .instantiate_async(self.store.as_context_mut(), module) .await?; - - // Validate the instantiation event - event.validate(&core_events::InstantiationEvent { - module: *module.checksum(), - instance: instance.id(), - })?; - - self.module_instances.insert(instance.id(), instance); - self.store - .as_context_mut() - .data_mut() - .insert_module_instance(instance.id(), instance); + self.insert_module_instance(instance); } RREvent::CoreWasmFuncEntry(event) => { - // Grab the correct module instance - let key = event.origin.instance; - let instance = self - .module_instances - .get_mut(&key) - .ok_or(ReplayError::MissingModuleInstance(key.as_u32()))?; - + let instance = self.module_instances.get_mut(event.origin.instance)?; let entity = EntityIndex::from(event.origin.index); let mut store = self.store.as_context_mut(); let func = instance diff --git a/tests/all/rr.rs b/tests/all/rr.rs index 18ea7df573..cc3c63997a 100644 --- a/tests/all/rr.rs +++ b/tests/all/rr.rs @@ -1406,6 +1406,111 @@ fn test_component_option() -> Result<()> { test::run() } +#[cfg(feature = "component-model")] +#[test] +fn test_component_builtins() -> Result<()> { + run_component_test( + r#" + (component + (type $r (resource (rep i32))) + (core func $rep (canon resource.rep $r)) + (core func $new (canon resource.new $r)) + (core func $drop (canon resource.drop $r)) + + (import "host-double" (func $host_double (param "v" s32) (result s32))) + (core func $host_double_core (canon lower (func $host_double))) + + (core module $m + (import "" "rep" (func $rep (param i32) (result i32))) + (import "" "new" (func $new (param i32) (result i32))) + (import "" "drop" (func $drop (param i32))) + (import "" "host_double" (func $host_double (param i32) (result i32))) + + (func $start + (local $r1 i32) + (local $r2 i32) + + ;; resources assigned sequentially + (local.set $r1 (call $new (i32.const 100))) + (if (i32.ne (local.get $r1) (i32.const 1)) (then (unreachable))) + + (local.set $r2 (call $new (i32.const 200))) + (if (i32.ne (local.get $r2) (i32.const 2)) (then (unreachable))) + + ;; representations all look good + (if (i32.ne (call $rep (local.get $r1)) (i32.const 100)) (then (unreachable))) + (if (i32.ne (call $rep (local.get $r2)) (i32.const 200)) (then (unreachable))) + + ;; reallocate r2 + (call $drop (local.get $r2)) + (local.set $r2 (call $new (i32.const 400))) + + ;; should have reused index 1 + (if (i32.ne (local.get $r2) (i32.const 2)) (then (unreachable))) + + ;; representations all look good + (if (i32.ne (call $rep (local.get $r1)) (i32.const 100)) (then (unreachable))) + (if (i32.ne (call $rep (local.get $r2)) (i32.const 400)) (then (unreachable))) + + ;; deallocate everything + (call $drop (local.get $r1)) + (call $drop (local.get $r2)) + ) + (start $start) + + (func $run (result i32) + (local $r1 i32) + (local $val i32) + + ;; Create a new resource + (local.set $r1 (call $new (i32.const 500))) + + ;; Get its representation + (local.set $val (call $rep (local.get $r1))) + + ;; Double it using host function + (local.set $val (call $host_double (local.get $val))) + + ;; Drop the resource + (call $drop (local.get $r1)) + + local.get $val + ) + (export "run" (func $run)) + ) + (core instance $i (instantiate $m + (with "" (instance + (export "rep" (func $rep)) + (export "new" (func $new)) + (export "drop" (func $drop)) + (export "host_double" (func $host_double_core)) + )) + )) + (func $run_comp (result s32) (canon lift (core func $i "run"))) + (export "run" (func $run_comp)) + ) + "#, + |linker| { + linker + .root() + .func_wrap("host-double", |_, (v,): (i32,)| Ok((v * 2,)))?; + Ok(()) + }, + |store, instance, is_async| { + Box::pin(async move { + let run = instance.get_typed_func::<(), (i32,)>(&mut *store, "run")?; + let (result,) = if is_async { + run.call_async(&mut *store, ()).await? + } else { + run.call(&mut *store, ())? + }; + assert_eq!(result, 1000); + Ok(()) + }) + }, + ) +} + #[cfg(feature = "component-model")] fn cabi_realloc_wat() -> String { r#" From d8ad8a635d4517dceebca2d76a16a5e3db632c3c Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Sun, 30 Nov 2025 21:31:01 -0500 Subject: [PATCH 58/73] Split replay/normal runs for invoke host --- crates/wasmtime/src/runtime/func.rs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 3804b0ae3f..acf8b9db55 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -1354,9 +1354,8 @@ impl Func { let nparams = ty.params().len(); val_vec.reserve(nparams + ty.results().len()); - let flat_params = ty.params().map(|x| x.to_wasm_type().byte_size()); - - if !caller.store.0.replay_enabled() { + let mut run_impl = |caller: &mut Caller<'_, T>, values_vec: &mut [ValRaw]| -> Result<()> { + let flat_params = ty.params().map(|x| x.to_wasm_type().byte_size()); rr::core_hooks::record_validate_host_func_entry( values_vec, flat_params, @@ -1382,13 +1381,26 @@ impl Func { let flat_results = ty.results().map(|x| x.to_wasm_type().byte_size()); rr::core_hooks::record_host_func_return(values_vec, flat_results, &mut caller.store.0)?; - } else { + Ok(()) + }; + + let replay_impl = |caller: &mut Caller<'_, T>, values_vec: &mut [ValRaw]| -> Result<()> { + let flat_params = ty.params().map(|x| x.to_wasm_type().byte_size()); rr::core_hooks::replay_validate_host_func_entry( values_vec, flat_params, &mut caller.store.0, )?; - rr::core_hooks::replay_host_func_return(values_vec, &mut caller)?; + rr::core_hooks::replay_host_func_return(values_vec, caller)?; + Ok(()) + }; + + if caller.store.0.replay_enabled() { + // In replay mode, we skip execution of host functions and + // just replay the return value effects observed in the trace + replay_impl(&mut caller, values_vec)?; + } else { + run_impl(&mut caller, values_vec)?; } // Restore our `val_vec` back into the store so it's usable for the next From ec990da14d5c91182986214acfd3e3b54c1d86c4 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Mon, 1 Dec 2025 11:49:34 -0500 Subject: [PATCH 59/73] Remove embedded-io dependency for no_std rr --- Cargo.lock | 1 - crates/wasmtime/Cargo.toml | 3 +- crates/wasmtime/src/runtime/rr/core.rs | 4 +- crates/wasmtime/src/runtime/rr/core/io.rs | 67 ++++++++++++----------- 4 files changed, 39 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5886bed31d..885ca882fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4489,7 +4489,6 @@ 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 9dfe029203..b82aae3c15 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -63,7 +63,6 @@ hashbrown = { workspace = true, features = ["default-hasher"] } bitflags = { workspace = true } futures = { workspace = true, features = ["alloc"], optional = true } bytes = { workspace = true, optional = true } -embedded-io = { version = "0.6.1", features = ["alloc"], optional = true } [target.'cfg(target_os = "windows")'.dependencies.windows-sys] workspace = true @@ -428,4 +427,4 @@ debug = [ compile-time-builtins = ['dep:wasm-compose', 'dep:tempfile'] # Enable support for the common base infrastructure of record/replay -rr = ["component-model", "dep:embedded-io"] +rr = ["component-model"] diff --git a/crates/wasmtime/src/runtime/rr/core.rs b/crates/wasmtime/src/runtime/rr/core.rs index c2b58f7bb5..b6e934473b 100644 --- a/crates/wasmtime/src/runtime/rr/core.rs +++ b/crates/wasmtime/src/runtime/rr/core.rs @@ -6,7 +6,7 @@ pub use events::{ RRFuncArgVals, ResultEvent, Validate, common_events, component_events, core_events, marker_events, }; -pub use io::{IOError, RecordWriter, ReplayReader}; +pub use io::{RecordWriter, ReplayReader}; use serde::{Deserialize, Serialize}; use wasmtime_environ::{EntityIndex, WasmChecksum}; @@ -189,7 +189,7 @@ pub enum ReplayError { FailedValidation, IncorrectEventVariant, InvalidEventPosition, - FailedRead(IOError), + FailedRead(anyhow::Error), EventError(Box), MissingComponent(WasmChecksum), MissingModule(WasmChecksum), diff --git a/crates/wasmtime/src/runtime/rr/core/io.rs b/crates/wasmtime/src/runtime/rr/core/io.rs index 1865da837d..fbae0ae802 100644 --- a/crates/wasmtime/src/runtime/rr/core/io.rs +++ b/crates/wasmtime/src/runtime/rr/core/io.rs @@ -3,36 +3,35 @@ use core::any::Any; use postcard; use serde::{Deserialize, Serialize}; -pub type IOError = postcard::Error; - cfg_if::cfg_if! { if #[cfg(feature = "std")] { use std::io::{Write, Seek, Read}; - /// An [`Write`] usable for recording in RR - /// - /// This supports `no_std`, but must be [Send] and [Sync] + /// A writer for recording in RR. pub trait RecordWriter: Write + Send + Sync + Any {} impl RecordWriter for T {} - /// An [`Read`] usable for replaying in RR + /// A reader for replaying in RR. pub trait ReplayReader: Read + Seek + Send + Sync {} impl ReplayReader for T {} } else { - // `no_std` configuration - use embedded_io::{Read, Seek, Write}; + use core::{convert::AsRef, iter::Extend}; - /// An [`Write`] usable for recording in RR - /// - /// This supports `no_std`, but must be [Send] and [Sync] - pub trait RecordWriter: Write + Send + Sync + Any {} - impl RecordWriter for T {} + /// A writer for recording in RR. + pub trait RecordWriter: Extend + Send + Sync + Any {} + impl + Send + Sync + Any> RecordWriter for T {} - /// An [`Read`] usable for replaying in RR + /// A reader for replaying in RR. /// - /// This supports `no_std`, but must be [Send] and [Sync] - pub trait ReplayReader: Read + Seek + Send + Sync {} - impl ReplayReader for T {} + /// In `no_std`, types must provide explicit read/seek capabilities + /// to a underlying byte slice through these methods. + pub trait ReplayReader: AsRef<[u8]> + Send + Sync { + /// Advance the reader's internal cursor by `cnt` bytes + fn advance(&mut self, cnt: usize); + /// Seek to an absolute position `pos` in the reader + fn seek(&mut self, pos: usize); + } + } } @@ -44,12 +43,13 @@ 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)?; - } + #[cfg(feature = "std")] + { + postcard::to_io(value, writer)?; + } + #[cfg(not(feature = "std"))] + { + postcard::to_extend(value, writer)?; } Ok(()) } @@ -58,16 +58,21 @@ where /// /// Currently uses `postcard` deserializer, with optional scratch /// buffer to deserialize into -pub(super) fn from_replay_reader<'a, T, R>(reader: R, scratch: &'a mut [u8]) -> Result +pub(super) fn from_replay_reader<'a, T, R>(reader: &'a mut R, scratch: &'a mut [u8]) -> Result where T: Deserialize<'a>, - R: ReplayReader + 'a, + R: ReplayReader, { - cfg_if::cfg_if! { - if #[cfg(feature = "std")] { - Ok(postcard::from_io((reader, scratch))?.0) - } else { - Ok(postcard::from_eio((reader, scratch))?.0) - } + #[cfg(feature = "std")] + { + Ok(postcard::from_io((reader, scratch))?.0) + } + #[cfg(not(feature = "std"))] + { + let bytes = reader.as_ref(); + let original_len = bytes.len(); + let (value, new) = postcard::take_from_bytes(bytes)?; + reader.advance(new.len() - original_len); + Ok(value) } } From 0e3d350d1c60aaef39abb3d66a4f183f61c49b3b Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Mon, 1 Dec 2025 12:48:24 -0500 Subject: [PATCH 60/73] Fix warning --- crates/wasmtime/src/compile.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/wasmtime/src/compile.rs b/crates/wasmtime/src/compile.rs index 08862ef68d..a4045e74ce 100644 --- a/crates/wasmtime/src/compile.rs +++ b/crates/wasmtime/src/compile.rs @@ -29,15 +29,15 @@ use crate::prelude::*; use std::{any::Any, borrow::Cow, collections::BTreeMap, mem, ops::Range}; use call_graph::CallGraph; -#[cfg(feature = "component-model")] -use wasmtime_environ::component::Translator; use wasmtime_environ::{ Abi, BuiltinFunctionIndex, CompiledFunctionBody, CompiledFunctionsTable, CompiledFunctionsTableBuilder, CompiledModuleInfo, Compiler, DefinedFuncIndex, FilePos, FinishedObject, FuncKey, FunctionBodyData, InliningCompiler, IntraModuleInlining, ModuleEnvironment, ModuleTranslation, ModuleTypes, ModuleTypesBuilder, ObjectKind, PrimaryMap, - StaticModuleIndex, Tunables, WasmChecksum, + StaticModuleIndex, Tunables, }; +#[cfg(feature = "component-model")] +use wasmtime_environ::{WasmChecksum, component::Translator}; mod call_graph; mod scc; From 568a83b783fdb06fae50d138a39be03f2520deb4 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Fri, 12 Dec 2025 14:10:44 -0500 Subject: [PATCH 61/73] Fixed no_std support; disallow cm-async calls with rr; add CI tests for no_std --- .github/workflows/main.yml | 1 + Cargo.toml | 1 - crates/environ/Cargo.toml | 4 +- .../src/runtime/component/concurrent.rs | 5 + crates/wasmtime/src/runtime/rr/core.rs | 4 +- crates/wasmtime/src/runtime/rr/core/io.rs | 130 +++++++++++++++--- tests/all/rr.rs | 30 ++-- 7 files changed, 139 insertions(+), 36 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 364a2fa940..3e54f16b2a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -547,6 +547,7 @@ jobs: test: > cargo check -p wasmtime --no-default-features --features runtime,component-model && cargo check -p wasmtime --no-default-features --features runtime,gc,component-model,async,debug-builtins && + cargo check -p wasmtime --no-default-features --features runtime,rr,component-model && cargo check -p cranelift-control --no-default-features && cargo check -p pulley-interpreter --features encode,decode,disas,interp && cargo check -p wasmtime-wasi-io --no-default-features diff --git a/Cargo.toml b/Cargo.toml index 1c30e39742..eb37211713 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -425,7 +425,6 @@ tokio-util = "0.7.16" arbtest = "0.3.2" rayon = "1.5.3" regex = "1.9.1" -sha2 = { version = "0.10.2", default-features = false } # ============================================================================= # diff --git a/crates/environ/Cargo.toml b/crates/environ/Cargo.toml index 1f317340b6..ab7de07f42 100644 --- a/crates/environ/Cargo.toml +++ b/crates/environ/Cargo.toml @@ -37,7 +37,9 @@ wasmprinter = { workspace = true, optional = true } wasmtime-component-util = { workspace = true, optional = true } semver = { workspace = true, optional = true, features = ['serde'] } smallvec = { workspace = true, features = ['serde'] } -sha2 = { workspace = true } +# NB: LLVM unable to compile sha2 with 'asm' in 'no_std' as of Rustc/Cargo version 1.91.1. +# Currently uses 'force-soft' in 'no_std', but remove if fixed. +sha2 = { version = "0.10.2", default-features = false, features = ['force-soft'] } [dev-dependencies] clap = { workspace = true, features = ['default'] } diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 5ccaa6caa3..726eca57c6 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -5022,6 +5022,11 @@ pub(crate) fn queue_call( mut store: StoreContextMut, prepared: PreparedCall, ) -> Result)>> + Send + 'static + use> { + #[cfg(feature = "rr")] + assert!( + !(store.engine().is_recording() || store.engine().is_replaying()), + "component model async ABI not supported during recording or replaying" + ); let PreparedCall { handle, thread, diff --git a/crates/wasmtime/src/runtime/rr/core.rs b/crates/wasmtime/src/runtime/rr/core.rs index b6e934473b..e4e5829223 100644 --- a/crates/wasmtime/src/runtime/rr/core.rs +++ b/crates/wasmtime/src/runtime/rr/core.rs @@ -434,7 +434,7 @@ impl Recorder for RecordBuffer { fn flush(&mut self) -> Result<()> { log::debug!("Flushing record buffer..."); for e in self.buf.drain(..) { - io::to_record_writer(&e, &mut self.writer)?; + io::to_record_writer(&e, &mut *self.writer)?; } return Ok(()); } @@ -467,7 +467,7 @@ impl Iterator for ReplayBuffer { return None; } let ret = 'event_loop: loop { - let result = io::from_replay_reader(&mut self.reader, &mut self.deser_buffer); + let result = io::from_replay_reader(&mut *self.reader, &mut self.deser_buffer); match result { Err(e) => { break 'event_loop Some(Err(ReplayError::FailedRead(e))); diff --git a/crates/wasmtime/src/runtime/rr/core/io.rs b/crates/wasmtime/src/runtime/rr/core/io.rs index fbae0ae802..1e655e3f1c 100644 --- a/crates/wasmtime/src/runtime/rr/core/io.rs +++ b/crates/wasmtime/src/runtime/rr/core/io.rs @@ -15,23 +15,118 @@ cfg_if::cfg_if! { impl ReplayReader for T {} } else { - use core::{convert::AsRef, iter::Extend}; + use core::iter::Extend; + use postcard::{ser_flavors, de_flavors}; + + type PcError = postcard::Error; + type PcResult = postcard::Result; /// A writer for recording in RR. - pub trait RecordWriter: Extend + Send + Sync + Any {} - impl + Send + Sync + Any> RecordWriter for T {} + /// + /// In `no_std`, types must provide explicit write capabilities. + pub trait RecordWriter: Send + Sync + Any { + /// Write all the bytes from `buf` to the writer + fn write(&mut self, buf: &[u8]) -> Result; + /// Flush the writer + fn flush(&mut self) -> Result<()>; + } + impl > RecordWriter for T { + fn write(&mut self, buf: &[u8]) -> Result { + self.extend(buf.iter().copied()); + Ok(buf.len()) + } + fn flush(&mut self) -> Result<()> { + Ok(()) + } + } /// A reader for replaying in RR. /// - /// In `no_std`, types must provide explicit read/seek capabilities - /// to a underlying byte slice through these methods. - pub trait ReplayReader: AsRef<[u8]> + Send + Sync { - /// Advance the reader's internal cursor by `cnt` bytes - fn advance(&mut self, cnt: usize); - /// Seek to an absolute position `pos` in the reader + /// In `no_std`, types must provide explicit read/seek capabilities. + pub trait ReplayReader: Send + Sync { + /// Read bytes into `buf`, returning number of bytes read. + fn read(&mut self, buf: &mut [u8]) -> Result; + /// Seek to an absolute position `pos` in the reader. fn seek(&mut self, pos: usize); } + /// Resembles a `WriteFlavor` in postcard + struct RecordWriterFlavor<'a, W: RecordWriter + ?Sized> { + writer: &'a mut W, + } + + impl<'a, W: RecordWriter + ?Sized> ser_flavors::Flavor for RecordWriterFlavor<'a, W> { + type Output = (); + + #[inline] + fn try_push(&mut self, data: u8) -> PcResult<()> { + self.writer.write(&[data]).map_err(|_| PcError::SerializeBufferFull)?; + Ok(()) + } + #[inline] + fn try_extend(&mut self, data: &[u8]) -> PcResult<()> { + self.writer.write(data).map_err(|_| PcError::SerializeBufferFull)?; + Ok(()) + } + #[inline] + fn finalize(self) -> PcResult { + self.writer.flush().map_err(|_| PcError::SerializeBufferFull)?; + Ok(()) + } + } + + struct ReplayReaderFlavor<'a, 'b, R: ReplayReader + ?Sized> { + reader: &'a mut R, + scratch: &'b mut [u8], + } + + impl<'de, 'a, 'b, R: ReplayReader + ?Sized> de_flavors::Flavor<'de> for ReplayReaderFlavor<'a, 'b, R> + where 'b: 'de, 'a: 'de + { + type Remainder = (); + type Source = (); + + #[inline] + fn pop(&mut self) -> PcResult { + let scratch = core::mem::replace(&mut self.scratch, &mut []); + if scratch.is_empty() { + return PcResult::Err(PcError::DeserializeUnexpectedEnd); + } + let (slice, rest) = scratch.split_at_mut(1); + self.scratch = rest; + + match self.reader.read(slice) { + Ok(1) => Ok(slice[0]), + _ => PcResult::Err(PcError::DeserializeUnexpectedEnd), + } + } + + #[inline] + fn try_take_n(&mut self, ct: usize) -> postcard::Result<&'de [u8]> { + let scratch = core::mem::replace(&mut self.scratch, &mut []); + if scratch.len() < ct { + return PcResult::Err(PcError::DeserializeUnexpectedEnd); + } + let (slice, rest) = scratch.split_at_mut(ct); + self.scratch = rest; + + let mut total_read = 0; + while total_read < ct { + match self.reader.read(&mut slice[total_read..]) { + Ok(0) => return PcResult::Err(PcError::DeserializeUnexpectedEnd), + Ok(n) => total_read += n, + Err(_) => return PcResult::Err(PcError::DeserializeUnexpectedEnd), + } + } + + Ok(slice) + } + + #[inline] + fn finalize(self) -> PcResult { + Ok(()) + } + } } } @@ -41,7 +136,7 @@ cfg_if::cfg_if! { pub(super) fn to_record_writer(value: &T, writer: &mut W) -> Result<()> where T: Serialize + ?Sized, - W: RecordWriter, + W: RecordWriter + ?Sized, { #[cfg(feature = "std")] { @@ -49,7 +144,8 @@ where } #[cfg(not(feature = "std"))] { - postcard::to_extend(value, writer)?; + let flavor = RecordWriterFlavor { writer }; + postcard::serialize_with_flavor(value, flavor)?; } Ok(()) } @@ -61,7 +157,7 @@ where pub(super) fn from_replay_reader<'a, T, R>(reader: &'a mut R, scratch: &'a mut [u8]) -> Result where T: Deserialize<'a>, - R: ReplayReader, + R: ReplayReader + ?Sized, { #[cfg(feature = "std")] { @@ -69,10 +165,10 @@ where } #[cfg(not(feature = "std"))] { - let bytes = reader.as_ref(); - let original_len = bytes.len(); - let (value, new) = postcard::take_from_bytes(bytes)?; - reader.advance(new.len() - original_len); - Ok(value) + let flavor = ReplayReaderFlavor { reader, scratch }; + let mut deserializer = postcard::Deserializer::from_flavor(flavor); + let t = T::deserialize(&mut deserializer)?; + deserializer.finalize()?; + Ok(t) } } diff --git a/tests/all/rr.rs b/tests/all/rr.rs index cc3c63997a..11161e2cf9 100644 --- a/tests/all/rr.rs +++ b/tests/all/rr.rs @@ -7,7 +7,7 @@ use wasmtime::{ ReplaySettings, Store, }; -#[cfg(feature = "component-model")] +#[cfg(all(feature = "component-model", feature = "component-model-async"))] use wasmtime::component::{Component, HasSelf, Linker as ComponentLinker, bindgen}; struct TestState; @@ -150,7 +150,7 @@ where } /// Run a component test with recording and replay, testing both with and without validation -#[cfg(feature = "component-model")] +#[cfg(all(feature = "component-model", feature = "component-model-async"))] fn run_component_test(component_wat: &str, setup_linker: F, test_fn: R) -> Result<()> where F: Fn(&mut ComponentLinker) -> Result<()> + Clone, @@ -190,7 +190,7 @@ where } /// Run a component test with recording and replay with specified validation setting -#[cfg(feature = "component-model")] +#[cfg(all(feature = "component-model", feature = "component-model-async"))] async fn run_component_test_with_validation( component_wat: &str, setup_linker: F, @@ -436,7 +436,7 @@ fn test_recording_panics_for_core_module_memory_export() { // Few Parameters and Few Results (not exceeding MAX_FLAT_PARAMS=16 and // MAX_FLAT_RESULTS=1) #[test] -#[cfg(feature = "component-model")] +#[cfg(all(feature = "component-model", feature = "component-model-async"))] fn test_component_under_max_params_results() -> Result<()> { mod test { use super::*; @@ -545,7 +545,7 @@ fn test_component_under_max_params_results() -> Result<()> { // Large Record (exceeding MAX_FLAT_PARAMS=16 and MAX_FLAT_RESULTS=1) #[test] -#[cfg(feature = "component-model")] +#[cfg(all(feature = "component-model", feature = "component-model-async"))] fn test_component_over_max_params_results() -> Result<()> { mod test { use super::*; @@ -709,7 +709,7 @@ fn test_component_over_max_params_results() -> Result<()> { } #[test] -#[cfg(feature = "component-model")] +#[cfg(all(feature = "component-model", feature = "component-model-async"))] fn test_component_tuple() -> Result<()> { mod test { use super::*; @@ -805,7 +805,7 @@ fn test_component_tuple() -> Result<()> { } #[test] -#[cfg(feature = "component-model")] +#[cfg(all(feature = "component-model", feature = "component-model-async"))] fn test_component_string() -> Result<()> { mod test { use super::*; @@ -904,7 +904,7 @@ fn test_component_string() -> Result<()> { } #[test] -#[cfg(feature = "component-model")] +#[cfg(all(feature = "component-model", feature = "component-model-async"))] fn test_component_variant() -> Result<()> { mod test { use super::*; @@ -1019,7 +1019,7 @@ fn test_component_variant() -> Result<()> { } #[test] -#[cfg(feature = "component-model")] +#[cfg(all(feature = "component-model", feature = "component-model-async"))] fn test_component_result() -> Result<()> { mod test { use super::*; @@ -1156,7 +1156,7 @@ fn test_component_result() -> Result<()> { } #[test] -#[cfg(feature = "component-model")] +#[cfg(all(feature = "component-model", feature = "component-model-async"))] fn test_component_list() -> Result<()> { mod test { use super::*; @@ -1286,7 +1286,7 @@ fn test_component_list() -> Result<()> { } #[test] -#[cfg(feature = "component-model")] +#[cfg(all(feature = "component-model", feature = "component-model-async"))] fn test_component_option() -> Result<()> { mod test { use super::*; @@ -1406,7 +1406,7 @@ fn test_component_option() -> Result<()> { test::run() } -#[cfg(feature = "component-model")] +#[cfg(all(feature = "component-model", feature = "component-model-async"))] #[test] fn test_component_builtins() -> Result<()> { run_component_test( @@ -1511,7 +1511,7 @@ fn test_component_builtins() -> Result<()> { ) } -#[cfg(feature = "component-model")] +#[cfg(all(feature = "component-model", feature = "component-model-async"))] fn cabi_realloc_wat() -> String { r#" (global $bump (mut i32) (i32.const 256)) @@ -1539,7 +1539,7 @@ fn cabi_realloc_wat() -> String { "#.to_string() } -#[cfg(feature = "component-model")] +#[cfg(all(feature = "component-model", feature = "component-model-async"))] fn shims_wat(params: &str) -> String { let count = params.split_whitespace().count(); let locals_get = (0..count) @@ -1565,7 +1565,7 @@ fn shims_wat(params: &str) -> String { ) } -#[cfg(feature = "component-model")] +#[cfg(all(feature = "component-model", feature = "component-model-async"))] fn instantiation_wat(core_name: &str, lift_sig: &str) -> String { format!( r#" From 1fcfc405ed14cf9d905a9e2c1cab6e0a104cc371 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Fri, 12 Dec 2025 16:22:33 -0500 Subject: [PATCH 62/73] Fix typos --- tests/all/rr.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/all/rr.rs b/tests/all/rr.rs index 11161e2cf9..e80af88b7d 100644 --- a/tests/all/rr.rs +++ b/tests/all/rr.rs @@ -7,7 +7,7 @@ use wasmtime::{ ReplaySettings, Store, }; -#[cfg(all(feature = "component-model", feature = "component-model-async"))] +#[cfg(all(feature = "component-model", not(feature = "component-model-async")))] use wasmtime::component::{Component, HasSelf, Linker as ComponentLinker, bindgen}; struct TestState; @@ -150,7 +150,7 @@ where } /// Run a component test with recording and replay, testing both with and without validation -#[cfg(all(feature = "component-model", feature = "component-model-async"))] +#[cfg(all(feature = "component-model", not(feature = "component-model-async")))] fn run_component_test(component_wat: &str, setup_linker: F, test_fn: R) -> Result<()> where F: Fn(&mut ComponentLinker) -> Result<()> + Clone, @@ -190,7 +190,7 @@ where } /// Run a component test with recording and replay with specified validation setting -#[cfg(all(feature = "component-model", feature = "component-model-async"))] +#[cfg(all(feature = "component-model", not(feature = "component-model-async")))] async fn run_component_test_with_validation( component_wat: &str, setup_linker: F, @@ -436,7 +436,7 @@ fn test_recording_panics_for_core_module_memory_export() { // Few Parameters and Few Results (not exceeding MAX_FLAT_PARAMS=16 and // MAX_FLAT_RESULTS=1) #[test] -#[cfg(all(feature = "component-model", feature = "component-model-async"))] +#[cfg(all(feature = "component-model", not(feature = "component-model-async")))] fn test_component_under_max_params_results() -> Result<()> { mod test { use super::*; @@ -545,7 +545,7 @@ fn test_component_under_max_params_results() -> Result<()> { // Large Record (exceeding MAX_FLAT_PARAMS=16 and MAX_FLAT_RESULTS=1) #[test] -#[cfg(all(feature = "component-model", feature = "component-model-async"))] +#[cfg(all(feature = "component-model", not(feature = "component-model-async")))] fn test_component_over_max_params_results() -> Result<()> { mod test { use super::*; @@ -709,7 +709,7 @@ fn test_component_over_max_params_results() -> Result<()> { } #[test] -#[cfg(all(feature = "component-model", feature = "component-model-async"))] +#[cfg(all(feature = "component-model", not(feature = "component-model-async")))] fn test_component_tuple() -> Result<()> { mod test { use super::*; @@ -805,7 +805,7 @@ fn test_component_tuple() -> Result<()> { } #[test] -#[cfg(all(feature = "component-model", feature = "component-model-async"))] +#[cfg(all(feature = "component-model", not(feature = "component-model-async")))] fn test_component_string() -> Result<()> { mod test { use super::*; @@ -904,7 +904,7 @@ fn test_component_string() -> Result<()> { } #[test] -#[cfg(all(feature = "component-model", feature = "component-model-async"))] +#[cfg(all(feature = "component-model", not(feature = "component-model-async")))] fn test_component_variant() -> Result<()> { mod test { use super::*; @@ -1019,7 +1019,7 @@ fn test_component_variant() -> Result<()> { } #[test] -#[cfg(all(feature = "component-model", feature = "component-model-async"))] +#[cfg(all(feature = "component-model", not(feature = "component-model-async")))] fn test_component_result() -> Result<()> { mod test { use super::*; @@ -1156,7 +1156,7 @@ fn test_component_result() -> Result<()> { } #[test] -#[cfg(all(feature = "component-model", feature = "component-model-async"))] +#[cfg(all(feature = "component-model", not(feature = "component-model-async")))] fn test_component_list() -> Result<()> { mod test { use super::*; @@ -1286,7 +1286,7 @@ fn test_component_list() -> Result<()> { } #[test] -#[cfg(all(feature = "component-model", feature = "component-model-async"))] +#[cfg(all(feature = "component-model", not(feature = "component-model-async")))] fn test_component_option() -> Result<()> { mod test { use super::*; @@ -1406,7 +1406,7 @@ fn test_component_option() -> Result<()> { test::run() } -#[cfg(all(feature = "component-model", feature = "component-model-async"))] +#[cfg(all(feature = "component-model", not(feature = "component-model-async")))] #[test] fn test_component_builtins() -> Result<()> { run_component_test( @@ -1511,7 +1511,7 @@ fn test_component_builtins() -> Result<()> { ) } -#[cfg(all(feature = "component-model", feature = "component-model-async"))] +#[cfg(all(feature = "component-model", not(feature = "component-model-async")))] fn cabi_realloc_wat() -> String { r#" (global $bump (mut i32) (i32.const 256)) @@ -1539,7 +1539,7 @@ fn cabi_realloc_wat() -> String { "#.to_string() } -#[cfg(all(feature = "component-model", feature = "component-model-async"))] +#[cfg(all(feature = "component-model", not(feature = "component-model-async")))] fn shims_wat(params: &str) -> String { let count = params.split_whitespace().count(); let locals_get = (0..count) @@ -1565,7 +1565,7 @@ fn shims_wat(params: &str) -> String { ) } -#[cfg(all(feature = "component-model", feature = "component-model-async"))] +#[cfg(all(feature = "component-model", not(feature = "component-model-async")))] fn instantiation_wat(core_name: &str, lift_sig: &str) -> String { format!( r#" From 9a66fcf98db06f2a061df30f091c14ab958a9c13 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Mon, 15 Dec 2025 18:27:03 -0500 Subject: [PATCH 63/73] Factored out core RR interface into separate crate; fix rr tests to just disable async with component-model-async --- Cargo.lock | 13 + Cargo.toml | 1 + crates/environ/src/component.rs | 5 + crates/rr/Cargo.toml | 28 ++ .../src/runtime/rr/core => rr/src}/events.rs | 66 +-- .../core => rr/src}/events/common_events.rs | 0 .../src}/events/component_events.rs | 58 ++- crates/rr/src/events/core_events.rs | 54 +++ .../src/runtime/rr/core => rr/src}/io.rs | 6 +- crates/rr/src/lib.rs | 373 ++++++++++++++ crates/wasmtime/Cargo.toml | 6 +- .../src/runtime/component/func/options.rs | 2 +- crates/wasmtime/src/runtime/rr.rs | 21 +- .../src/runtime/rr/{core.rs => backend.rs} | 455 ++++-------------- .../src/runtime/rr/core/events/core_events.rs | 22 - .../src/runtime/rr/hooks/component_hooks.rs | 42 +- .../src/runtime/rr/hooks/core_hooks.rs | 23 +- .../wasmtime/src/runtime/rr/replay_driver.rs | 58 ++- .../src/runtime/vm/component/libcalls.rs | 4 - tests/all/rr.rs | 44 +- 20 files changed, 716 insertions(+), 565 deletions(-) create mode 100644 crates/rr/Cargo.toml rename crates/{wasmtime/src/runtime/rr/core => rr/src}/events.rs (73%) rename crates/{wasmtime/src/runtime/rr/core => rr/src}/events/common_events.rs (100%) rename crates/{wasmtime/src/runtime/rr/core => rr/src}/events/component_events.rs (79%) create mode 100644 crates/rr/src/events/core_events.rs rename crates/{wasmtime/src/runtime/rr/core => rr/src}/io.rs (96%) create mode 100644 crates/rr/src/lib.rs rename crates/wasmtime/src/runtime/rr/{core.rs => backend.rs} (62%) delete mode 100644 crates/wasmtime/src/runtime/rr/core/events/core_events.rs diff --git a/Cargo.lock b/Cargo.lock index 885ca882fe..ab14ec1e69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4536,6 +4536,7 @@ dependencies = [ "wasmtime-internal-versioned-export-macros", "wasmtime-internal-winch", "wasmtime-internal-wmemcheck", + "wasmtime-rr", "wasmtime-test-util", "wat", "windows-sys 0.61.2", @@ -4973,6 +4974,18 @@ dependencies = [ name = "wasmtime-internal-wmemcheck" version = "40.0.0" +[[package]] +name = "wasmtime-rr" +version = "40.0.0" +dependencies = [ + "anyhow", + "cfg-if", + "log", + "postcard", + "serde", + "wasmtime-environ", +] + [[package]] name = "wasmtime-test-macros" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index eb37211713..f994895866 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -272,6 +272,7 @@ wasmtime-slab = { path = "crates/slab", version = "=40.0.0", package = 'wasmtime wasmtime-jit-icache-coherence = { path = "crates/jit-icache-coherence", version = "=40.0.0", package = 'wasmtime-internal-jit-icache-coherence' } wasmtime-wit-bindgen = { path = "crates/wit-bindgen", version = "=40.0.0", package = 'wasmtime-internal-wit-bindgen' } wasmtime-math = { path = "crates/math", version = "=40.0.0", package = 'wasmtime-internal-math' } +wasmtime-rr = { path = "crates/rr", version = "40.0.0" } wasmtime-unwinder = { path = "crates/unwinder", version = "=40.0.0", package = 'wasmtime-internal-unwinder' } wasmtime-wizer = { path = "crates/wizer", version = "40.0.0" } diff --git a/crates/environ/src/component.rs b/crates/environ/src/component.rs index 82ad98ad85..9e2f2be015 100644 --- a/crates/environ/src/component.rs +++ b/crates/environ/src/component.rs @@ -17,6 +17,7 @@ //! //! Note that this entire module is gated behind the `component-model` Cargo //! feature. +use serde::{Deserialize, Serialize}; /// Canonical ABI-defined constant for the maximum number of "flat" parameters /// to a wasm function, or the maximum number of parameters a core wasm function @@ -272,3 +273,7 @@ declare_builtin_index! { /// An index type for component builtin functions. pub struct ComponentBuiltinFunctionIndex: foreach_builtin_component_function; } + +/// Return value from `resource.drop` builtin. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +pub struct ResourceDropRet(pub Option); diff --git a/crates/rr/Cargo.toml b/crates/rr/Cargo.toml new file mode 100644 index 0000000000..90e9ccd3e1 --- /dev/null +++ b/crates/rr/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "wasmtime-rr" +version.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +documentation = "https://docs.rs/wasmtime-rr" +description = "Record-replay interface for Wasm modules and components" +license = "Apache-2.0 WITH LLVM-exception" +repository = "https://github.com/bytecodealliance/wasmtime" + +[lints] +workspace = true + +[package.metadata.docs.rs] +all-features = true + +[dependencies] +wasmtime-environ = { workspace = true, features = ["component-model"] } +serde = { workspace = true } +postcard = { workspace = true } +anyhow = { workspace = true } +log = { workspace = true } +cfg-if = { workspace = true } + +[features] +default = ["std"] +std = ["postcard/use-std", "wasmtime-environ/std"] diff --git a/crates/wasmtime/src/runtime/rr/core/events.rs b/crates/rr/src/events.rs similarity index 73% rename from crates/wasmtime/src/runtime/rr/core/events.rs rename to crates/rr/src/events.rs index 8bba175c37..8f11610caf 100644 --- a/crates/wasmtime/src/runtime/rr/core/events.rs +++ b/crates/rr/src/events.rs @@ -1,10 +1,7 @@ -use super::ReplayError; -use crate::rr::FlatBytes; -use crate::{AsContextMut, Val, prelude::*}; -use crate::{ValRaw, ValType}; +use crate::ReplayError; +use anyhow::Result; use core::fmt; use serde::{Deserialize, Serialize}; -use wasmtime_environ::component::FlatTypesStorage; /// A serde compatible representation of errors produced during execution /// of certain events @@ -26,12 +23,12 @@ pub trait EventError: core::error::Error + Send + Sync + 'static { #[derive(Serialize, Deserialize, Clone, PartialEq)] pub struct RRFuncArgVals { /// Flat data vector of bytes - bytes: Vec, + pub bytes: Vec, /// Descriptor vector of sizes of each flat types /// /// The length of this vector equals the number of flat types, /// and the sum of this vector equals the length of `bytes` - sizes: Vec, + pub sizes: Vec, } impl fmt::Debug for RRFuncArgVals { @@ -58,60 +55,6 @@ impl fmt::Debug for RRFuncArgVals { } } -impl RRFuncArgVals { - /// Construct [`RRFuncArgVals`] from raw value buffer and a flat size iterator - #[inline] - pub fn from_flat_iter(args: &[T], flat: impl Iterator) -> RRFuncArgVals - where - T: FlatBytes, - { - let mut bytes = Vec::new(); - let mut sizes = Vec::new(); - for (flat_size, arg) in flat.zip(args.iter()) { - bytes.extend_from_slice(unsafe { &arg.bytes(flat_size) }); - sizes.push(flat_size); - } - RRFuncArgVals { bytes, sizes } - } - - /// Construct [`RRFuncArgVals`] from raw value buffer and a [`FlatTypesStorage`] - #[inline] - pub fn from_flat_storage(args: &[T], flat: FlatTypesStorage) -> RRFuncArgVals - where - T: FlatBytes, - { - RRFuncArgVals::from_flat_iter(args, flat.iter32()) - } - - /// Encode [`RRFuncArgVals`] back into raw value buffer - #[inline] - pub fn into_raw_slice(self, raw_args: &mut [T]) - where - T: FlatBytes, - { - let mut pos = 0; - for (flat_size, dst) in self.sizes.into_iter().zip(raw_args.iter_mut()) { - *dst = T::from_bytes(&self.bytes[pos..pos + flat_size as usize]); - pos += flat_size as usize; - } - } - - /// Generate a vector of [`crate::Val`] from [`RRFuncArgVals`] and [`ValType`]s - #[inline] - pub fn to_val_vec(self, mut store: impl AsContextMut, val_types: Vec) -> Vec { - let mut pos = 0; - let mut vals = Vec::new(); - for (flat_size, val_type) in self.sizes.into_iter().zip(val_types.into_iter()) { - let raw = ValRaw::from_bytes(&self.bytes[pos..pos + flat_size as usize]); - // SAFETY: The safety contract here is the same as that of [`Val::from_raw`]. - // The caller must ensure that raw has the type provided. - vals.push(unsafe { Val::from_raw(&mut store, raw, val_type) }); - pos += flat_size as usize; - } - vals - } -} - /// Trait signifying types that can be validated on replay /// /// All `PartialEq` types are directly validatable with themselves. @@ -234,7 +177,6 @@ macro_rules! event_error_types { /// 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 diff --git a/crates/wasmtime/src/runtime/rr/core/events/common_events.rs b/crates/rr/src/events/common_events.rs similarity index 100% rename from crates/wasmtime/src/runtime/rr/core/events/common_events.rs rename to crates/rr/src/events/common_events.rs diff --git a/crates/wasmtime/src/runtime/rr/core/events/component_events.rs b/crates/rr/src/events/component_events.rs similarity index 79% rename from crates/wasmtime/src/runtime/rr/core/events/component_events.rs rename to crates/rr/src/events/component_events.rs index fabecb2a8e..70d7131f8e 100644 --- a/crates/wasmtime/src/runtime/rr/core/events/component_events.rs +++ b/crates/rr/src/events/component_events.rs @@ -1,50 +1,53 @@ //! Module comprising of component model wasm events use super::*; -use crate::component::ComponentInstanceId; -use crate::vm::component::libcalls::ResourceDropRet; use wasmtime_environ::{ self, WasmChecksum, - component::{ExportIndex, InterfaceType}, + component::{ExportIndex, InterfaceType, ResourceDropRet}, }; -/// Beginning marker for a Wasm component function call from host +/// Representation of a component instance identifier during record/replay. +/// +/// This ID is tied to component instantiation events in the trace, and used +/// during replay to refer to specific component instances. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Ord, PartialOrd)] +pub struct RRComponentInstanceId(pub u32); + +/// Beginning marker for a Wasm component function call from host. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct WasmFuncBeginEvent { - /// Instance ID for the component instance - pub instance: ComponentInstanceId, - /// Export index for the invoked function - pub func_idx: ExportIndex, + /// Instance ID for the component instance. + pub instance: RRComponentInstanceId, + /// Export index for the invoked function. + pub func_index: ExportIndex, } -/// A [`Component`] instantiatation event +/// A instantiatation event for a Wasm component. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Ord, PartialOrd)] pub struct InstantiationEvent { /// Checksum of the bytecode used to instantiate the component pub component: WasmChecksum, - /// Instance ID for the instantiated component - pub instance: ComponentInstanceId, + /// Instance ID for the instantiated component. + pub instance: RRComponentInstanceId, } -/// A call to `post_return` (after the function call) +/// A call to `post_return` (after the function call). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PostReturnEvent { - /// Instance ID for the component instance - pub instance: ComponentInstanceId, - /// Export index for the function on which post_return is invoked - pub func_idx: ExportIndex, + /// Instance ID for the component instance. + pub instance: RRComponentInstanceId, + /// Export index for the function on which post_return is invoked. + pub func_index: ExportIndex, } -/// A call event from Host into a Wasm component function +/// A call event from Host into a Wasm component function. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WasmFuncEntryEvent { - /// Raw values passed across call boundary + /// Raw values passed across call boundary. pub args: RRFuncArgVals, } -/// A reallocation call event in the Component Model canonical ABI -/// -/// Usually performed during lowering of complex [`ComponentType`]s to Wasm +/// A reallocation call event in the Wasm Component Model canonical ABI. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReallocEntryEvent { pub old_addr: usize, @@ -53,13 +56,13 @@ pub struct ReallocEntryEvent { pub new_size: usize, } -/// Entry to a type lowering invocation to flat destination +/// Entry to a type lowering invocation to flat destination. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LowerFlatEntryEvent { pub ty: InterfaceType, } -/// Entry to type lowering invocation to destination in memory +/// Entry to type lowering invocation to destination in memory. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LowerMemoryEntryEvent { pub ty: InterfaceType, @@ -69,6 +72,7 @@ pub struct LowerMemoryEntryEvent { /// 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. @@ -85,18 +89,20 @@ event_error_types! { pub struct BuiltinError(..) } +/// Return from a reallocation call in the Component Model canonical ABI, providing +/// the address of allocation if successful. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReallocReturnEvent(pub ResultEvent); -/// Return from type lowering to flat destination +/// Return from type lowering to flat destination. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LowerFlatReturnEvent(pub ResultEvent<(), LowerFlatError>); -/// Return from type lowering to destination in memory +/// Return from type lowering to destination in memory. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LowerMemoryReturnEvent(pub ResultEvent<(), LowerMemoryError>); -// Macro to generate RR events from the builtin descriptions +// Macro to generate RR events from the builtin descriptions. macro_rules! builtin_events { // Main rule matching component function definitions ( diff --git a/crates/rr/src/events/core_events.rs b/crates/rr/src/events/core_events.rs new file mode 100644 index 0000000000..48347e78fa --- /dev/null +++ b/crates/rr/src/events/core_events.rs @@ -0,0 +1,54 @@ +//! Module comprising of core wasm events +use super::*; +use wasmtime_environ::{EntityIndex, FuncIndex, WasmChecksum}; + +/// Representation of a Wasm module instance identifier during record/replay. +/// +/// This ID is tied to module instantiation events in the trace, and used +/// during replay to refer to specific module instances. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Ord, PartialOrd)] +pub struct RRModuleInstanceId(pub u32); + +/// Representation of a Wasm module function index during record/replay. +/// +/// This index is used to identify target call functions within a module during replay. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Ord, PartialOrd)] +pub struct RRModuleFuncIndex(pub u32); + +impl Into for RRModuleFuncIndex { + fn into(self) -> FuncIndex { + FuncIndex::from_u32(self.0) + } +} + +impl Into for RRModuleFuncIndex { + fn into(self) -> EntityIndex { + EntityIndex::from(Into::::into(self)) + } +} + +impl From for RRModuleFuncIndex { + fn from(f: FuncIndex) -> Self { + RRModuleFuncIndex(f.as_u32()) + } +} + +/// A core Wasm module instantiatation event. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Ord, PartialOrd)] +pub struct InstantiationEvent { + /// Checksum of the bytecode used to instantiate the module. + pub module: WasmChecksum, + /// Instance ID for the instantiated module. + pub instance: RRModuleInstanceId, +} + +/// A call event from Host into a core Wasm function. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct WasmFuncEntryEvent { + /// Instance ID for the instantiated module. + pub instance: RRModuleInstanceId, + /// Function index of callee within the module. + pub func_index: RRModuleFuncIndex, + /// Raw values passed across call boundary + pub args: RRFuncArgVals, +} diff --git a/crates/wasmtime/src/runtime/rr/core/io.rs b/crates/rr/src/io.rs similarity index 96% rename from crates/wasmtime/src/runtime/rr/core/io.rs rename to crates/rr/src/io.rs index 1e655e3f1c..d7ba18d381 100644 --- a/crates/wasmtime/src/runtime/rr/core/io.rs +++ b/crates/rr/src/io.rs @@ -1,4 +1,4 @@ -use crate::prelude::*; +use anyhow::Result; use core::any::Any; use postcard; use serde::{Deserialize, Serialize}; @@ -133,7 +133,7 @@ cfg_if::cfg_if! { /// Serialize and write `value` to a `RecordWriter` /// /// Currently uses `postcard` serializer -pub(super) fn to_record_writer(value: &T, writer: &mut W) -> Result<()> +pub fn to_record_writer(value: &T, writer: &mut W) -> Result<()> where T: Serialize + ?Sized, W: RecordWriter + ?Sized, @@ -154,7 +154,7 @@ where /// /// Currently uses `postcard` deserializer, with optional scratch /// buffer to deserialize into -pub(super) fn from_replay_reader<'a, T, R>(reader: &'a mut R, scratch: &'a mut [u8]) -> Result +pub fn from_replay_reader<'a, T, R>(reader: &'a mut R, scratch: &'a mut [u8]) -> Result where T: Deserialize<'a>, R: ReplayReader + ?Sized, diff --git a/crates/rr/src/lib.rs b/crates/rr/src/lib.rs new file mode 100644 index 0000000000..f8d81b285d --- /dev/null +++ b/crates/rr/src/lib.rs @@ -0,0 +1,373 @@ +use anyhow::Result; +use core::fmt; +pub use events::{ + EventError, RRFuncArgVals, ResultEvent, Validate, common_events, component_events, core_events, + marker_events, +}; +pub use io::{RecordWriter, ReplayReader, from_replay_reader, to_record_writer}; +use serde::{Deserialize, Serialize}; +use wasmtime_environ::{EntityIndex, WasmChecksum}; + +use events::{component_events::RRComponentInstanceId, core_events::RRModuleInstanceId}; + +/// Settings for execution recording. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RecordSettings { + /// 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 RecordSettings { + fn default() -> Self { + Self { + add_validation: false, + event_window_size: 16, + } + } +} + +/// Settings for execution replay. +#[derive(Debug, Clone, Serialize, Deserialize)] +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 deserialize_buffer_size: usize, +} + +impl Default for ReplaySettings { + fn default() -> Self { + Self { + validate: false, + deserialize_buffer_size: 64, + } + } +} + +/// Encapsulation of event types comprising an [`RREvent`] sum type +mod events; +/// I/O support for reading and writing traces +mod io; + +/// Macro template for [`RREvent`] and its conversion to/from specific +/// event types +macro_rules! rr_event { + ( + $( + $(#[doc = $doc:literal])* + $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, Serialize, Deserialize)] + pub enum RREvent { + /// Event signalling the end of a trace + Eof, + $( + $(#[doc = $doc])* + $variant($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), + )* + } + } + } + + $( + 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 { + log::error!("Expected {}; got {}", stringify!($event), value); + Err(ReplayError::IncorrectEventVariant) + } + } + } + )* + ); +} + +// Set of supported record/replay events +rr_event! { + // Marker events + /// Nop Event + Nop(marker_events::NopEvent), + /// A custom message + CustomMessage(marker_events::CustomMessageEvent), + + // Common events for both core or component wasm + // REQUIRED events + /// Return from host function (core or component) to host + HostFuncReturn(common_events::HostFuncReturnEvent), + // OPTIONAL events + /// Call into host function from Wasm (core or component) + HostFuncEntry(common_events::HostFuncEntryEvent), + /// Return from Wasm function (core or component) to host + WasmFuncReturn(common_events::WasmFuncReturnEvent), + + // REQUIRED events for replay (Core) + /// Instantiation of a core Wasm module + CoreWasmInstantiation(core_events::InstantiationEvent), + /// Entry from host into a core Wasm function + CoreWasmFuncEntry(core_events::WasmFuncEntryEvent), + + // REQUIRED events for replay (Component) + + /// Starting marker for a Wasm component function call from host + /// + /// This is distinguished from `ComponentWasmFuncEntry` as there may + /// be multiple lowering steps before actually entering the Wasm function + ComponentWasmFuncBegin(component_events::WasmFuncBeginEvent), + /// Entry from the host into the Wasm component function + ComponentWasmFuncEntry(component_events::WasmFuncEntryEvent), + /// Instantiation of a component + ComponentInstantiation(component_events::InstantiationEvent), + /// Component ABI realloc call in linear wasm memory + ComponentReallocEntry(component_events::ReallocEntryEvent), + /// Return from a type lowering operation + ComponentLowerFlatReturn(component_events::LowerFlatReturnEvent), + /// Return from a store during a type lowering operation + ComponentLowerMemoryReturn(component_events::LowerMemoryReturnEvent), + /// An attempt to obtain a mutable slice into Wasm linear memory + ComponentMemorySliceWrite(component_events::MemorySliceWriteEvent), + /// Return from a component builtin + ComponentBuiltinReturn(component_events::BuiltinReturnEvent), + /// Call to `post_return` (after the function call) + ComponentPostReturn(component_events::PostReturnEvent), + + // OPTIONAL events for replay validation (Component) + + /// Return from Component ABI realloc call + /// + /// Since realloc is deterministic, ReallocReturn is optional. + /// Any error is subsumed by the containing LowerReturn/LowerStoreReturn + /// that triggered realloc + ComponentReallocReturn(component_events::ReallocReturnEvent), + /// Call into type lowering for flat destination + ComponentLowerFlatEntry(component_events::LowerFlatEntryEvent), + /// Call into type lowering for memory destination + ComponentLowerMemoryEntry(component_events::LowerMemoryEntryEvent), + /// Call into a component builtin + ComponentBuiltinEntry(component_events::BuiltinEntryEvent) +} + +impl RREvent { + /// Indicates whether current event is a marker event + #[inline] + pub fn is_marker(&self) -> bool { + match self { + Self::Nop(_) | Self::CustomMessage(_) => true, + _ => false, + } + } +} + +/// Error type signalling failures during a replay run +#[derive(Debug)] +pub enum ReplayError { + EmptyBuffer, + FailedValidation, + IncorrectEventVariant, + InvalidEventPosition, + FailedRead(anyhow::Error), + EventError(Box), + MissingComponent(WasmChecksum), + MissingModule(WasmChecksum), + MissingComponentInstance(RRComponentInstanceId), + MissingModuleInstance(RRModuleInstanceId), + InvalidCoreFuncIndex(EntityIndex), +} + +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, + "failed validation check during replay; see wasmtime log for error" + ) + } + Self::IncorrectEventVariant => { + write!(f, "event type mismatch during replay") + } + Self::EventError(e) => { + write!(f, "{:?}", e) + } + Self::FailedRead(e) => { + write!(f, "{}", e)?; + f.write_str("Note: Ensure sufficient `deserialization-buffer-size` in replay settings if you included `validation-metadata` during recording") + } + Self::InvalidEventPosition => { + write!(f, "event occured at an invalid position in the trace") + } + Self::MissingComponent(checksum) => { + write!( + f, + "missing component binary with checksum 0x{} during replay", + checksum + .iter() + .map(|b| format!("{:02x}", b)) + .collect::() + ) + } + Self::MissingModule(checksum) => { + write!( + f, + "missing module binary with checksum {:02x?} during replay", + checksum + .iter() + .map(|b| format!("{:02x}", b)) + .collect::() + ) + } + Self::MissingComponentInstance(id) => { + write!(f, "missing component instance ID {:?} during replay", id) + } + Self::MissingModuleInstance(id) => { + write!(f, "missing module instance ID {:?} during replay", id) + } + Self::InvalidCoreFuncIndex(index) => { + write!(f, "replay core func ({:?}) during replay is invalid", index) + } + } + } +} + +impl core::error::Error for ReplayError {} + +impl From for ReplayError { + fn from(value: T) -> Self { + Self::EventError(Box::new(value)) + } +} + +/// This trait provides the interface for a FIFO recorder +pub trait Recorder { + /// Construct a recorder with the writer backend + fn new_recorder(writer: impl RecordWriter, settings: RecordSettings) -> Result + where + Self: Sized; + + /// Record the event generated by `f` + /// + /// ## Error + /// + /// Propogates from underlying writer + fn record_event(&mut self, f: F) -> Result<()> + where + T: Into, + F: FnOnce() -> T; + + /// Consumes this [`Recorder`] and returns its underlying writer + fn into_writer(self) -> Result>; + + /// Trigger an explicit flush of any buffered data to the writer + /// + /// Buffer should be emptied during this process + fn flush(&mut self) -> Result<()>; + + /// Get settings associated with the recording process + fn settings(&self) -> &RecordSettings; + + // Provided methods + + /// Record a event only when validation is requested + #[inline] + fn record_event_validation(&mut self, f: F) -> Result<()> + where + T: Into, + F: FnOnce() -> T, + { + let settings = self.settings(); + if settings.add_validation { + self.record_event(f)?; + } + Ok(()) + } +} + +/// 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(reader: impl ReplayReader + 'static, settings: ReplaySettings) -> Result + where + Self: Sized; + + /// Get settings associated with the replay process + fn settings(&self) -> &ReplaySettings; + + /// Get the settings (embedded within the trace) during recording + fn trace_settings(&self) -> &RecordSettings; + + // Provided Methods + + /// Get the next functional replay event (skips past all non-marker events) + #[inline] + fn next_event(&mut self) -> Result { + self.next().ok_or(ReplayError::EmptyBuffer)? + } + + /// Pop the next replay event with an attemped type conversion to expected + /// event type + /// + /// ## Errors + /// + /// Returns a [`ReplayError::IncorrectEventVariant`] if it failed to convert typecheck event safely + #[inline] + fn next_event_typed(&mut self) -> Result + where + T: TryFrom, + ReplayError: From<>::Error>, + { + T::try_from(self.next_event()?).map_err(|e| e.into()) + } + + /// 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_validation(&mut self, expect: &Y) -> Result<(), ReplayError> + where + T: TryFrom + Validate, + ReplayError: From<>::Error>, + { + if self.trace_settings().add_validation { + let event = self.next_event_typed::()?; + if self.settings().validate { + event.validate(expect) + } else { + Ok(()) + } + } else { + Ok(()) + } + } +} diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml index b82aae3c15..5b0bca8fbf 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -31,6 +31,7 @@ wasmtime-slab = { workspace = true, optional = true } wasmtime-versioned-export-macros = { workspace = true } wasmtime-wmemcheck = { workspace = true, optional = true } wasmtime-math = { workspace = true } +wasmtime-rr = { workspace = true, optional = true } pulley-interpreter = { workspace = true } target-lexicon = { workspace = true } wasmparser = { workspace = true } @@ -427,4 +428,7 @@ debug = [ compile-time-builtins = ['dep:wasm-compose', 'dep:tempfile'] # Enable support for the common base infrastructure of record/replay -rr = ["component-model"] +rr = [ + "dep:wasmtime-rr", + "component-model" +] diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index cad56f239c..aaf0979821 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -9,7 +9,7 @@ use crate::prelude::*; use crate::rr::{DynamicMemorySlice, FixedMemorySlice}; #[cfg(feature = "rr")] use crate::rr::{ - RREvent, RecordBuffer, ReplayError, Replayer, ResultEvent, Validate, + RREvent, RRFuncArgValsConvertable, RecordBuffer, ReplayError, Replayer, ResultEvent, Validate, component_events::ReallocEntryEvent, component_events::ReallocReturnEvent, component_hooks::ReplayLoweringPhase, }; diff --git a/crates/wasmtime/src/runtime/rr.rs b/crates/wasmtime/src/runtime/rr.rs index 57facce276..f8eacee77c 100644 --- a/crates/wasmtime/src/runtime/rr.rs +++ b/crates/wasmtime/src/runtime/rr.rs @@ -1,16 +1,11 @@ //! Wasmtime's Record and Replay support. //! -//! This feature is currently not optimized and under development -use crate::ValRaw; -use ::core::{mem::MaybeUninit, slice}; +//! This provides necessary bindings of the [`wasmtime-rr`] crate into the +//! Wasmtime runtime, as well as convenience traits and methods for working +//! with Wasmtime's internal representations. -/// Component-async-ABI is not supported for record/replay yet; add a feature gate -//const _: () = { -// #[cfg(all(feature = "rr", feature = "component-model-async"))] -// compile_error!( -// "The `component-model-async` feature is not supported with the `rr` feature yet" -// ); -//}; +use crate::ValRaw; +use core::{mem::MaybeUninit, slice}; /// Types that can be serialized/deserialized into/from /// flat types for record and replay @@ -65,11 +60,11 @@ pub(crate) use hooks::{ component_hooks, component_hooks::DynamicMemorySlice, component_hooks::FixedMemorySlice, }; -/// Core infrastructure for RR support +/// Core backend for RR support #[cfg(feature = "rr")] -mod core; +mod backend; #[cfg(feature = "rr")] -pub use core::*; +pub use backend::*; /// Driver capabilities for executing replays #[cfg(feature = "rr")] diff --git a/crates/wasmtime/src/runtime/rr/core.rs b/crates/wasmtime/src/runtime/rr/backend.rs similarity index 62% rename from crates/wasmtime/src/runtime/rr/core.rs rename to crates/wasmtime/src/runtime/rr/backend.rs index e4e5829223..12016c8af9 100644 --- a/crates/wasmtime/src/runtime/rr/core.rs +++ b/crates/wasmtime/src/runtime/rr/backend.rs @@ -1,374 +1,109 @@ -use crate::config::ModuleVersionStrategy; +use super::FlatBytes; +use crate::component::ComponentInstanceId; use crate::prelude::*; -use core::fmt; -use events::EventError; -pub use events::{ - RRFuncArgVals, ResultEvent, Validate, common_events, component_events, core_events, - marker_events, +use crate::store::InstanceId; +use crate::{AsContextMut, ModuleVersionStrategy, Val, ValRaw, ValType}; +use wasmtime_environ::component::FlatTypesStorage; + +// Public Re-exports +pub use wasmtime_rr::{RecordSettings, RecordWriter, ReplayError, ReplayReader, ReplaySettings}; +// Crate-internal re-exports +pub(crate) use wasmtime_rr::{ + RREvent, RRFuncArgVals, Recorder, Replayer, ResultEvent, Validate, common_events, + component_events::{self, RRComponentInstanceId}, + core_events::{self, RRModuleInstanceId}, + from_replay_reader, to_record_writer, }; -pub use io::{RecordWriter, ReplayReader}; -use serde::{Deserialize, Serialize}; -use wasmtime_environ::{EntityIndex, WasmChecksum}; - -/// Settings for execution recording. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RecordSettings { - /// 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 RecordSettings { - fn default() -> Self { - Self { - add_validation: false, - event_window_size: 16, - } - } -} - -/// Settings for execution replay. -#[derive(Debug, Clone, Serialize, Deserialize)] -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 deserialize_buffer_size: usize, -} - -impl Default for ReplaySettings { - fn default() -> Self { - Self { - validate: false, - deserialize_buffer_size: 64, - } - } -} - -/// Encapsulation of event types comprising an [`RREvent`] sum type -mod events; -/// I/O support for reading and writing traces -mod io; - -/// Macro template for [`RREvent`] and its conversion to/from specific -/// event types -macro_rules! rr_event { - ( - $( - $(#[doc = $doc:literal])* - $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, Serialize, Deserialize)] - pub enum RREvent { - /// Event signalling the end of a trace - Eof, - $( - $(#[doc = $doc])* - $variant($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), - )* - } - } - } - - $( - 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 { - log::error!("Expected {}; got {}", stringify!($event), value); - Err(ReplayError::IncorrectEventVariant) - } - } - } - )* - ); -} - -// Set of supported record/replay events -rr_event! { - // Marker events - /// Nop Event - Nop(marker_events::NopEvent), - /// A custom message - CustomMessage(marker_events::CustomMessageEvent), - - // Common events for both core or component wasm - // REQUIRED events - /// Return from host function (core or component) to host - HostFuncReturn(common_events::HostFuncReturnEvent), - // OPTIONAL events - /// Call into host function from Wasm (core or component) - HostFuncEntry(common_events::HostFuncEntryEvent), - /// Return from Wasm function (core or component) to host - WasmFuncReturn(common_events::WasmFuncReturnEvent), - - // REQUIRED events for replay (Core) - /// Instantiation of a core Wasm module - CoreWasmInstantiation(core_events::InstantiationEvent), - /// Entry from host into a core Wasm function - CoreWasmFuncEntry(core_events::WasmFuncEntryEvent), - - // REQUIRED events for replay (Component) - - /// Starting marker for a Wasm component function call from host - /// - /// This is distinguished from `ComponentWasmFuncEntry` as there may - /// be multiple lowering steps before actually entering the Wasm function - ComponentWasmFuncBegin(component_events::WasmFuncBeginEvent), - /// Entry from the host into the Wasm component function - ComponentWasmFuncEntry(component_events::WasmFuncEntryEvent), - /// Instantiation of a component - ComponentInstantiation(component_events::InstantiationEvent), - /// Component ABI realloc call in linear wasm memory - ComponentReallocEntry(component_events::ReallocEntryEvent), - /// Return from a type lowering operation - ComponentLowerFlatReturn(component_events::LowerFlatReturnEvent), - /// Return from a store during a type lowering operation - ComponentLowerMemoryReturn(component_events::LowerMemoryReturnEvent), - /// An attempt to obtain a mutable slice into Wasm linear memory - ComponentMemorySliceWrite(component_events::MemorySliceWriteEvent), - /// Return from a component builtin - ComponentBuiltinReturn(component_events::BuiltinReturnEvent), - /// Call to `post_return` (after the function call) - ComponentPostReturn(component_events::PostReturnEvent), - - // OPTIONAL events for replay validation (Component) - - /// Return from Component ABI realloc call - /// - /// Since realloc is deterministic, ReallocReturn is optional. - /// Any error is subsumed by the containing LowerReturn/LowerStoreReturn - /// that triggered realloc - ComponentReallocReturn(component_events::ReallocReturnEvent), - /// Call into type lowering for flat destination - ComponentLowerFlatEntry(component_events::LowerFlatEntryEvent), - /// Call into type lowering for memory destination - ComponentLowerMemoryEntry(component_events::LowerMemoryEntryEvent), - /// Call into a component builtin - ComponentBuiltinEntry(component_events::BuiltinEntryEvent) -} - -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)] -pub enum ReplayError { - EmptyBuffer, - FailedValidation, - IncorrectEventVariant, - InvalidEventPosition, - FailedRead(anyhow::Error), - EventError(Box), - MissingComponent(WasmChecksum), - MissingModule(WasmChecksum), - MissingComponentInstance(u32), - MissingModuleInstance(u32), - InvalidCoreFuncIndex(EntityIndex), -} - -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, - "failed validation check during replay; see wasmtime log for error" - ) - } - Self::IncorrectEventVariant => { - write!(f, "event type mismatch during replay") - } - Self::EventError(e) => { - write!(f, "{:?}", e) - } - Self::FailedRead(e) => { - write!(f, "{}", e)?; - f.write_str("Note: Ensure sufficient `deserialization-buffer-size` in replay settings if you included `validation-metadata` during recording") - } - Self::InvalidEventPosition => { - write!(f, "event occured at an invalid position in the trace") - } - Self::MissingComponent(checksum) => { - write!( - f, - "missing component binary with checksum 0x{} during replay", - checksum - .iter() - .map(|b| format!("{:02x}", b)) - .collect::() - ) - } - Self::MissingModule(checksum) => { - write!( - f, - "missing module binary with checksum {:02x?} during replay", - checksum - .iter() - .map(|b| format!("{:02x}", b)) - .collect::() - ) - } - Self::MissingComponentInstance(id) => { - write!(f, "missing component instance ID {:?} during replay", id) - } - Self::MissingModuleInstance(id) => { - write!(f, "missing module instance ID {:?} during replay", id) - } - Self::InvalidCoreFuncIndex(index) => { - write!(f, "replay core func ({:?}) during replay is invalid", index) - } - } - } -} - -impl core::error::Error for ReplayError {} - -impl From for ReplayError { - fn from(value: T) -> Self { - Self::EventError(Box::new(value)) - } -} -/// This trait provides the interface for a FIFO recorder -pub trait Recorder { - /// Construct a recorder with the writer backend - fn new_recorder(writer: impl RecordWriter, settings: RecordSettings) -> Result +pub trait RRFuncArgValsConvertable { + /// Construct [`RRFuncArgVals`] from raw value buffer and a flat size iterator + fn from_flat_iter(args: &[T], flat: impl Iterator) -> Self where - Self: Sized; + T: FlatBytes; - /// Record the event generated by `f` - /// - /// ## Error - /// - /// Propogates from underlying writer - fn record_event(&mut self, f: F) -> Result<()> + /// Construct [`RRFuncArgVals`] from raw value buffer and a [`FlatTypesStorage`] + fn from_flat_storage(args: &[T], flat: FlatTypesStorage) -> RRFuncArgVals where - T: Into, - F: FnOnce() -> T; - - /// Consumes this [`Recorder`] and returns its underlying writer - fn into_writer(self) -> Result>; + T: FlatBytes; - /// Trigger an explicit flush of any buffered data to the writer - /// - /// Buffer should be emptied during this process - fn flush(&mut self) -> Result<()>; - - /// Get settings associated with the recording process - fn settings(&self) -> &RecordSettings; + /// Encode [`RRFuncArgVals`] back into raw value buffer + fn into_raw_slice(self, raw_args: &mut [T]) + where + T: FlatBytes; - // Provided methods + /// Generate a vector of [`crate::Val`] from [`RRFuncArgVals`] and [`ValType`]s + fn to_val_vec(self, store: impl AsContextMut, val_types: Vec) -> Vec; +} - /// Record a event only when validation is requested +impl RRFuncArgValsConvertable for RRFuncArgVals { #[inline] - fn record_event_validation(&mut self, f: F) -> Result<()> + fn from_flat_iter(args: &[T], flat: impl Iterator) -> RRFuncArgVals where - T: Into, - F: FnOnce() -> T, + T: FlatBytes, { - let settings = self.settings(); - if settings.add_validation { - self.record_event(f)?; + let mut bytes = Vec::new(); + let mut sizes = Vec::new(); + for (flat_size, arg) in flat.zip(args.iter()) { + bytes.extend_from_slice(unsafe { &arg.bytes(flat_size) }); + sizes.push(flat_size); } - Ok(()) + RRFuncArgVals { bytes, sizes } } -} -/// 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(reader: impl ReplayReader + 'static, settings: ReplaySettings) -> Result - where - Self: Sized; - - /// Get settings associated with the replay process - fn settings(&self) -> &ReplaySettings; - - /// Get the settings (embedded within the trace) during recording - fn trace_settings(&self) -> &RecordSettings; - - // Provided Methods - - /// Get the next functional replay event (skips past all non-marker events) + /// Construct [`RRFuncArgVals`] from raw value buffer and a [`FlatTypesStorage`] #[inline] - fn next_event(&mut self) -> Result { - self.next().ok_or(ReplayError::EmptyBuffer)? + fn from_flat_storage(args: &[T], flat: FlatTypesStorage) -> RRFuncArgVals + where + T: FlatBytes, + { + RRFuncArgVals::from_flat_iter(args, flat.iter32()) } - /// Pop the next replay event with an attemped type conversion to expected - /// event type - /// - /// ## Errors - /// - /// Returns a [`ReplayError::IncorrectEventVariant`] if it failed to convert typecheck event safely + /// Encode [`RRFuncArgVals`] back into raw value buffer #[inline] - fn next_event_typed(&mut self) -> Result + fn into_raw_slice(self, raw_args: &mut [T]) where - T: TryFrom, - ReplayError: From<>::Error>, + T: FlatBytes, { - T::try_from(self.next_event()?).map_err(|e| e.into()) + let mut pos = 0; + for (flat_size, dst) in self.sizes.into_iter().zip(raw_args.iter_mut()) { + *dst = T::from_bytes(&self.bytes[pos..pos + flat_size as usize]); + pos += flat_size as usize; + } } - /// 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 + /// Generate a vector of [`crate::Val`] from [`RRFuncArgVals`] and [`ValType`]s #[inline] - fn next_event_validation(&mut self, expect: &Y) -> Result<(), ReplayError> - where - T: TryFrom + Validate, - ReplayError: From<>::Error>, - { - if self.trace_settings().add_validation { - let event = self.next_event_typed::()?; - if self.settings().validate { - event.validate(expect) - } else { - Ok(()) - } - } else { - Ok(()) + fn to_val_vec(self, mut store: impl AsContextMut, val_types: Vec) -> Vec { + let mut pos = 0; + let mut vals = Vec::new(); + for (flat_size, val_type) in self.sizes.into_iter().zip(val_types.into_iter()) { + let raw = ValRaw::from_bytes(&self.bytes[pos..pos + flat_size as usize]); + // SAFETY: The safety contract here is the same as that of [`Val::from_raw`]. + // The caller must ensure that raw has the type provided. + vals.push(unsafe { Val::from_raw(&mut store, raw, val_type) }); + pos += flat_size as usize; } + vals + } +} + +// Conversions from Wasmtime types to RR types +impl Into for ComponentInstanceId { + fn into(self) -> RRComponentInstanceId { + RRComponentInstanceId(self.as_u32()) + } +} + +impl Into for InstanceId { + fn into(self) -> RRModuleInstanceId { + RRModuleInstanceId(self.as_u32()) + } +} + +impl From for InstanceId { + fn from(rr_id: RRModuleInstanceId) -> Self { + InstanceId::from_u32(rr_id.0) } } @@ -405,8 +140,8 @@ impl RecordBuffer { impl Recorder for RecordBuffer { fn new_recorder(mut writer: impl RecordWriter, 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(&settings, &mut writer)?; + to_record_writer(ModuleVersionStrategy::WasmtimeVersion.as_str(), &mut writer)?; + to_record_writer(&settings, &mut writer)?; Ok(RecordBuffer { buf: Vec::new(), writer: Box::new(writer), @@ -434,7 +169,7 @@ impl Recorder for RecordBuffer { fn flush(&mut self) -> Result<()> { log::debug!("Flushing record buffer..."); for e in self.buf.drain(..) { - io::to_record_writer(&e, &mut *self.writer)?; + to_record_writer(&e, &mut *self.writer)?; } return Ok(()); } @@ -467,7 +202,7 @@ impl Iterator for ReplayBuffer { return None; } let ret = 'event_loop: loop { - let result = io::from_replay_reader(&mut *self.reader, &mut self.deser_buffer); + let result = from_replay_reader(&mut *self.reader, &mut self.deser_buffer); match result { Err(e) => { break 'event_loop Some(Err(ReplayError::FailedRead(e))); @@ -516,7 +251,7 @@ impl Replayer for ReplayBuffer { ) -> Result { let mut scratch = [0u8; 12]; // Ensure module versions match - let version = io::from_replay_reader::<&str, _>(&mut reader, &mut scratch)?; + let version = from_replay_reader::<&str, _>(&mut reader, &mut scratch)?; assert_eq!( version, ModuleVersionStrategy::WasmtimeVersion.as_str(), @@ -524,7 +259,7 @@ impl Replayer for ReplayBuffer { ); // Read the recording settings - let trace_settings: RecordSettings = io::from_replay_reader(&mut reader, &mut scratch)?; + let trace_settings: RecordSettings = from_replay_reader(&mut reader, &mut scratch)?; if settings.validate && !trace_settings.add_validation { log::warn!( @@ -569,11 +304,11 @@ mod tests { use crate::ValRaw; use crate::WasmFuncOrigin; use crate::store::InstanceId; - use crate::vm::component::libcalls::ResourceDropRet; use std::fs::File; use std::path::Path; use tempfile::{NamedTempFile, TempPath}; - use wasmtime_environ::FuncIndex; + use wasmtime_environ::{FuncIndex, component::ResourceDropRet}; + use wasmtime_rr::EventError; impl ReplayBuffer { /// Pop the next replay event and calls `f` with a expected event type @@ -703,7 +438,8 @@ mod tests { rr_harness( |recorder| { recorder.record_event(|| core_events::WasmFuncEntryEvent { - origin: origin.clone(), + instance: origin.instance.into(), + func_index: origin.index.into(), args: RRFuncArgVals::from_flat_iter(&values, flat_sizes.iter().copied()), })?; recorder.record_event(|| component_events::WasmFuncEntryEvent { @@ -715,7 +451,10 @@ mod tests { }, |replayer| { replayer.next_event_and(|event: core_events::WasmFuncEntryEvent| { - replay_origin = Some(event.origin); + replay_origin = Some(WasmFuncOrigin { + instance: event.instance.into(), + index: event.func_index.into(), + }); event.args.into_raw_slice(&mut replay_values); Ok(()) })?; @@ -998,12 +737,12 @@ mod tests { let component_event = ComponentInstantiationEvent { component: WasmChecksum::from_binary(&[0xAB; 256]), - instance: ComponentInstanceId::from_u32(42), + instance: ComponentInstanceId::from_u32(42).into(), }; let core_event = CoreInstantiationEvent { module: WasmChecksum::from_binary(&[0xCD; 256]), - instance: InstanceId::from_u32(17), + instance: InstanceId::from_u32(17).into(), }; rr_harness( diff --git a/crates/wasmtime/src/runtime/rr/core/events/core_events.rs b/crates/wasmtime/src/runtime/rr/core/events/core_events.rs deleted file mode 100644 index e0f8a2feb9..0000000000 --- a/crates/wasmtime/src/runtime/rr/core/events/core_events.rs +++ /dev/null @@ -1,22 +0,0 @@ -//! Module comprising of core wasm events -use super::*; -use crate::{WasmFuncOrigin, store::InstanceId}; -use wasmtime_environ::WasmChecksum; - -/// A core Wasm instantiatation event -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Ord, PartialOrd)] -pub struct InstantiationEvent { - /// Checksum of the bytecode used to instantiate the module - pub module: WasmChecksum, - /// Instance ID for the instantiated module - pub instance: InstanceId, -} - -/// A call event from Host into a core Wasm function -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct WasmFuncEntryEvent { - /// Origin (instance + function index) for this function - pub origin: WasmFuncOrigin, - /// Raw values passed across call boundary - pub args: RRFuncArgVals, -} diff --git a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs index 71ec3524bb..7ec16b4c06 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs @@ -3,15 +3,15 @@ use super::replay_data_from_store_mut; use crate::ValRaw; use crate::component::{ComponentInstanceId, func::LowerContext}; #[cfg(feature = "rr")] -use crate::rr::common_events::{HostFuncEntryEvent, HostFuncReturnEvent, WasmFuncReturnEvent}; -#[cfg(feature = "rr")] -use crate::rr::component_events::{ - InstantiationEvent, LowerFlatEntryEvent, LowerFlatReturnEvent, LowerMemoryEntryEvent, - LowerMemoryReturnEvent, MemorySliceWriteEvent, PostReturnEvent, WasmFuncBeginEvent, - WasmFuncEntryEvent, +use crate::rr::{ + RRFuncArgVals, RRFuncArgValsConvertable, RecordBuffer, Recorder, ResultEvent, Validate, + common_events::{HostFuncEntryEvent, HostFuncReturnEvent, WasmFuncReturnEvent}, + component_events::{ + InstantiationEvent, LowerFlatEntryEvent, LowerFlatReturnEvent, LowerMemoryEntryEvent, + LowerMemoryReturnEvent, MemorySliceWriteEvent, PostReturnEvent, WasmFuncBeginEvent, + WasmFuncEntryEvent, + }, }; -#[cfg(feature = "rr")] -use crate::rr::{RRFuncArgVals, RecordBuffer, Recorder, ResultEvent, Validate}; use crate::store::StoreOpaque; use crate::{StoreContextMut, prelude::*}; use alloc::sync::Arc; @@ -19,7 +19,7 @@ use core::mem::MaybeUninit; use core::ops::{Deref, DerefMut}; use wasmtime_environ::WasmChecksum; use wasmtime_environ::component::{ComponentTypes, ExportIndex, InterfaceType, TypeFuncIndex}; -#[cfg(all(feature = "rr"))] +#[cfg(feature = "rr")] use wasmtime_environ::component::{MAX_FLAT_PARAMS, MAX_FLAT_RESULTS}; /// Indicator type signalling the context during lowering @@ -37,12 +37,15 @@ pub enum ReplayLoweringPhase { #[inline] pub fn record_wasm_func_begin( instance: ComponentInstanceId, - func_idx: ExportIndex, + func_index: ExportIndex, store: &mut StoreOpaque, ) -> Result<()> { #[cfg(feature = "rr")] - store.record_event(|| WasmFuncBeginEvent { instance, func_idx })?; - let _ = (instance, func_idx, store); + store.record_event(|| WasmFuncBeginEvent { + instance: instance.into(), + func_index: func_index.into(), + })?; + let _ = (instance, func_index, store); Ok(()) } @@ -50,14 +53,15 @@ pub fn record_wasm_func_begin( #[inline] pub fn record_wasm_func_post_return( instance: ComponentInstanceId, - func_idx: ExportIndex, + func_index: ExportIndex, store: &mut StoreContextMut<'_, T>, ) -> Result<()> { #[cfg(feature = "rr")] - store - .0 - .record_event(|| PostReturnEvent { instance, func_idx })?; - let _ = (instance, func_idx, store); + store.0.record_event(|| PostReturnEvent { + instance: instance.into(), + func_index: func_index.into(), + })?; + let _ = (instance, func_index, store); Ok(()) } @@ -220,13 +224,13 @@ pub fn record_and_replay_validate_instantiation( { store.0.record_event(|| InstantiationEvent { component, - instance, + instance: instance.into(), })?; if store.0.replay_enabled() { let replay_data = unsafe { replay_data_from_store_mut(store) }; replay_data.take_current_component_instantiation().expect( "replay driver should have set component instantiate data before trying to validate it", - ).validate(&InstantiationEvent { component, instance })?; + ).validate(&InstantiationEvent { component, instance: instance.into() })?; } } let _ = (store, component, instance); diff --git a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs index 1d2a26f626..631f78dd8e 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs @@ -3,10 +3,9 @@ use super::{replay_data_from_store, replay_data_from_store_mut}; use crate::rr::FlatBytes; #[cfg(feature = "rr")] use crate::rr::{ - RREvent, RRFuncArgVals, ReplayError, Replayer, ResultEvent, Validate, - common_events::HostFuncEntryEvent, common_events::HostFuncReturnEvent, - common_events::WasmFuncReturnEvent, core_events::InstantiationEvent, - core_events::WasmFuncEntryEvent, + RREvent, RRFuncArgVals, RRFuncArgValsConvertable, ReplayError, Replayer, ResultEvent, Validate, + common_events::{HostFuncEntryEvent, HostFuncReturnEvent, WasmFuncReturnEvent}, + core_events::{InstantiationEvent, WasmFuncEntryEvent}, }; use crate::store::{InstanceId, StoreOpaque}; use crate::{Caller, FuncType, Module, StoreContextMut, ValRaw, WasmFuncOrigin, prelude::*}; @@ -35,7 +34,8 @@ where store.0.record_event(|| { let flat = ty.params().map(|t| t.to_wasm_type().byte_size()); WasmFuncEntryEvent { - origin, + instance: origin.instance.into(), + func_index: origin.index.into(), args: RRFuncArgVals::from_flat_iter(args, flat), } })?; @@ -148,14 +148,14 @@ where } // Re-entrant call into wasm function: this resembles the implementation in [`ReplayInstance`] RREvent::CoreWasmFuncEntry(event) => { - let entity = EntityIndex::from(event.origin.index); + let entity = Into::::into(event.func_index); // Unwrapping the `replay_buffer_mut()` above ensures that we are in replay mode // passing the safety contract for `replay_data_from_store` let replay_data = unsafe { replay_data_from_store(&caller.store) }; // Grab the correct module instance - let instance = replay_data.get_module_instance(event.origin.instance)?; + let instance = replay_data.get_module_instance(event.instance)?; let mut store = &mut caller.store; let func = instance @@ -203,14 +203,15 @@ pub fn record_and_replay_validate_instantiation( ) -> Result<()> { #[cfg(feature = "rr")] { - store - .0 - .record_event(|| InstantiationEvent { module, instance })?; + store.0.record_event(|| InstantiationEvent { + module, + instance: instance.into(), + })?; if store.0.replay_enabled() { let replay_data = unsafe { replay_data_from_store_mut(store) }; replay_data.take_current_module_instantiation().expect( "replay driver should have set module instantiate data before trying to validate it", - ).validate(&InstantiationEvent { module, instance })?; + ).validate(&InstantiationEvent { module, instance: instance.into() })?; } } let _ = (store, module, instance); diff --git a/crates/wasmtime/src/runtime/rr/replay_driver.rs b/crates/wasmtime/src/runtime/rr/replay_driver.rs index 87024e2b1f..419a875904 100644 --- a/crates/wasmtime/src/runtime/rr/replay_driver.rs +++ b/crates/wasmtime/src/runtime/rr/replay_driver.rs @@ -1,9 +1,10 @@ -use crate::rr::{RREvent, ReplayError, component_events, core_events}; -use crate::store::InstanceId; -use crate::{AsContextMut, Engine, Module, ReplayReader, ReplaySettings, Store, prelude::*}; -use crate::{ - ValRaw, component, component::Component, component::ComponentInstanceId, rr::component_hooks, +use crate::rr::{ + RREvent, RRFuncArgValsConvertable, ReplayError, + component_events::{self, RRComponentInstanceId}, + core_events::{self, RRModuleInstanceId}, }; +use crate::{AsContextMut, Engine, Module, ReplayReader, ReplaySettings, Store, prelude::*}; +use crate::{ValRaw, component, component::Component, rr::component_hooks}; use alloc::{collections::BTreeMap, sync::Arc}; use core::mem::MaybeUninit; use wasmtime_environ::component::{MAX_FLAT_PARAMS, MAX_FLAT_RESULTS}; @@ -97,7 +98,7 @@ pub struct ReplayHostContext { /// /// Core wasm modules can be re-entrant and invoke methods from other instances, and this /// needs to be accessible within host functions. - module_instances: BTreeMap, + module_instances: BTreeMap, /// The currently executing module instantiation event. /// /// This must be set by the driver prior to instantiation and cleared after @@ -116,11 +117,11 @@ impl ReplayHostContext { /// This is necessary for core wasm to identify re-entrant calls during replay. pub(crate) fn get_module_instance( &self, - id: InstanceId, + id: RRModuleInstanceId, ) -> Result<&crate::Instance, ReplayError> { self.module_instances .get(&id) - .ok_or(ReplayError::MissingModuleInstance(id.as_u32())) + .ok_or(ReplayError::MissingModuleInstance(id)) } /// Take the current module instantiation event from the context, leaving @@ -213,7 +214,7 @@ pub struct ReplayInstance { component_instances: ComponentInstanceMap, } -struct ComponentInstanceMap(BTreeMap); +struct ComponentInstanceMap(BTreeMap); impl ComponentInstanceMap { fn new() -> Self { @@ -222,25 +223,24 @@ impl ComponentInstanceMap { fn get_mut( &mut self, - id: ComponentInstanceId, + id: RRComponentInstanceId, ) -> Result<&mut component::Instance, ReplayError> { self.0 .get_mut(&id) - .ok_or(ReplayError::MissingComponentInstance(id.as_u32())) + .ok_or(ReplayError::MissingComponentInstance(id)) } } -struct ModuleInstanceMap(BTreeMap); - +struct ModuleInstanceMap(BTreeMap); impl ModuleInstanceMap { fn new() -> Self { Self(BTreeMap::new()) } - fn get_mut(&mut self, id: InstanceId) -> Result<&mut crate::Instance, ReplayError> { + fn get_mut(&mut self, id: RRModuleInstanceId) -> Result<&mut crate::Instance, ReplayError> { self.0 .get_mut(&id) - .ok_or(ReplayError::MissingModuleInstance(id.as_u32())) + .ok_or(ReplayError::MissingModuleInstance(id)) } } @@ -288,17 +288,19 @@ impl ReplayInstance { fn insert_component_instance(&mut self, instance: component::Instance) { self.component_instances .0 - .insert(instance.id().instance(), instance); + .insert(instance.id().instance().into(), instance); } fn insert_module_instance(&mut self, instance: crate::Instance) { - self.module_instances.0.insert(instance.id(), instance); + self.module_instances + .0 + .insert(instance.id().into(), instance); // Insert into host context tracking as well, for re-entrancy calls self.store .as_context_mut() .data_mut() .module_instances - .insert(instance.id(), instance); + .insert(instance.id().into(), instance); } /// Run a single top-level event from the instance. @@ -323,7 +325,8 @@ impl ReplayInstance { let instance = self.component_instances.get_mut(event.instance)?; // Replay lowering steps and obtain raw value arguments to raw function call - let func = component::Func::from_lifted_func(*instance, event.func_idx); + let func = + component::Func::from_lifted_func(*instance, Into::into(event.func_index)); let store = self.store.as_context_mut(); // Call the function // @@ -368,7 +371,8 @@ impl ReplayInstance { } RREvent::ComponentPostReturn(event) => { let instance = self.component_instances.get_mut(event.instance)?; - let func = component::Func::from_lifted_func(*instance, event.func_idx); + let func = + component::Func::from_lifted_func(*instance, Into::into(event.func_index)); let mut store = self.store.as_context_mut(); func.post_return(&mut store)?; } @@ -382,8 +386,8 @@ impl ReplayInstance { self.insert_module_instance(instance); } RREvent::CoreWasmFuncEntry(event) => { - let instance = self.module_instances.get_mut(event.origin.instance)?; - let entity = EntityIndex::from(event.origin.index); + let instance = self.module_instances.get_mut(event.instance.into())?; + let entity = Into::::into(event.func_index); let mut store = self.store.as_context_mut(); let func = instance ._get_export(store.0, entity) @@ -430,7 +434,8 @@ impl ReplayInstance { let instance = self.component_instances.get_mut(event.instance)?; // Replay lowering steps and obtain raw value arguments to raw function call - let func = component::Func::from_lifted_func(*instance, event.func_idx); + let func = + component::Func::from_lifted_func(*instance, Into::into(event.func_index)); let mut store = self.store.as_context_mut(); // Call the function // @@ -481,7 +486,8 @@ impl ReplayInstance { } RREvent::ComponentPostReturn(event) => { let instance = self.component_instances.get_mut(event.instance)?; - let func = component::Func::from_lifted_func(*instance, event.func_idx); + let func = + component::Func::from_lifted_func(*instance, Into::into(event.func_index)); let mut store = self.store.as_context_mut(); func.post_return_async(&mut store).await?; } @@ -496,8 +502,8 @@ impl ReplayInstance { self.insert_module_instance(instance); } RREvent::CoreWasmFuncEntry(event) => { - let instance = self.module_instances.get_mut(event.origin.instance)?; - let entity = EntityIndex::from(event.origin.index); + let instance = self.module_instances.get_mut(event.instance)?; + let entity = Into::::into(event.func_index); let mut store = self.store.as_context_mut(); let func = instance ._get_export(store.0, entity) diff --git a/crates/wasmtime/src/runtime/vm/component/libcalls.rs b/crates/wasmtime/src/runtime/vm/component/libcalls.rs index 6bae7ec379..5d8fbc9518 100644 --- a/crates/wasmtime/src/runtime/vm/component/libcalls.rs +++ b/crates/wasmtime/src/runtime/vm/component/libcalls.rs @@ -11,7 +11,6 @@ use crate::runtime::vm::{HostResultHasUnwindSentinel, VMStore, VmSafe}; use core::cell::Cell; use core::ptr::NonNull; use core::slice; -use serde::{Deserialize, Serialize}; use wasmtime_environ::component::*; const UTF16_TAG: usize = 1 << 31; @@ -670,9 +669,6 @@ fn resource_drop( )?)) } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] -pub struct ResourceDropRet(Option); - unsafe impl HostResultHasUnwindSentinel for ResourceDropRet { type Abi = u64; const SENTINEL: u64 = u64::MAX; diff --git a/tests/all/rr.rs b/tests/all/rr.rs index e80af88b7d..108dfd7e9c 100644 --- a/tests/all/rr.rs +++ b/tests/all/rr.rs @@ -1,15 +1,16 @@ +//! This module does NOT run tests with `call_async` if `component-model-async` +//! feature is disabled since async ABI is not supported for RR builds yet. + use anyhow::Result; use std::future::Future; use std::io::Cursor; use std::pin::Pin; +use wasmtime::component::{Component, HasSelf, Linker as ComponentLinker, bindgen}; use wasmtime::{ Config, Engine, Linker, Module, OptLevel, RRConfig, RecordSettings, ReplayEnvironment, ReplaySettings, Store, }; -#[cfg(all(feature = "component-model", not(feature = "component-model-async")))] -use wasmtime::component::{Component, HasSelf, Linker as ComponentLinker, bindgen}; - struct TestState; impl TestState { @@ -28,9 +29,14 @@ fn create_recording_engine(is_async: bool) -> Result { .debug_info(true) .cranelift_opt_level(OptLevel::None) .rr(RRConfig::Recording); + #[cfg(feature = "component-model-async")] if is_async { config.async_support(true); } + #[cfg(not(feature = "component-model-async"))] + { + let _ = is_async; + } Engine::new(&config) } @@ -40,9 +46,14 @@ fn create_replay_engine(is_async: bool) -> Result { .debug_info(true) .cranelift_opt_level(OptLevel::None) .rr(RRConfig::Replaying); + #[cfg(feature = "component-model-async")] if is_async { config.async_support(true); } + #[cfg(not(feature = "component-model-async"))] + { + let _ = is_async; + } Engine::new(&config) } @@ -62,8 +73,12 @@ where .enable_all() .build()?; + #[cfg(feature = "component-model-async")] + let async_modes = [false]; + #[cfg(not(feature = "component-model-async"))] + let async_modes = [false, true]; // Run with in sync/async mode with/without validation - for is_async in [false, true] { + for is_async in async_modes { for validation in [true, false] { let run = async { run_core_module_test_with_validation( @@ -150,7 +165,6 @@ where } /// Run a component test with recording and replay, testing both with and without validation -#[cfg(all(feature = "component-model", not(feature = "component-model-async")))] fn run_component_test(component_wat: &str, setup_linker: F, test_fn: R) -> Result<()> where F: Fn(&mut ComponentLinker) -> Result<()> + Clone, @@ -167,8 +181,13 @@ where .enable_all() .build()?; + #[cfg(feature = "component-model-async")] + let async_modes = [false]; + #[cfg(not(feature = "component-model-async"))] + let async_modes = [false, true]; + // Run with in sync/async mode with/without validation - for is_async in [false, true] { + for is_async in async_modes { for validation in [true, false] { let run = async { run_component_test_with_validation( @@ -190,7 +209,6 @@ where } /// Run a component test with recording and replay with specified validation setting -#[cfg(all(feature = "component-model", not(feature = "component-model-async")))] async fn run_component_test_with_validation( component_wat: &str, setup_linker: F, @@ -436,7 +454,6 @@ fn test_recording_panics_for_core_module_memory_export() { // Few Parameters and Few Results (not exceeding MAX_FLAT_PARAMS=16 and // MAX_FLAT_RESULTS=1) #[test] -#[cfg(all(feature = "component-model", not(feature = "component-model-async")))] fn test_component_under_max_params_results() -> Result<()> { mod test { use super::*; @@ -545,7 +562,6 @@ fn test_component_under_max_params_results() -> Result<()> { // Large Record (exceeding MAX_FLAT_PARAMS=16 and MAX_FLAT_RESULTS=1) #[test] -#[cfg(all(feature = "component-model", not(feature = "component-model-async")))] fn test_component_over_max_params_results() -> Result<()> { mod test { use super::*; @@ -709,7 +725,6 @@ fn test_component_over_max_params_results() -> Result<()> { } #[test] -#[cfg(all(feature = "component-model", not(feature = "component-model-async")))] fn test_component_tuple() -> Result<()> { mod test { use super::*; @@ -805,7 +820,6 @@ fn test_component_tuple() -> Result<()> { } #[test] -#[cfg(all(feature = "component-model", not(feature = "component-model-async")))] fn test_component_string() -> Result<()> { mod test { use super::*; @@ -904,7 +918,6 @@ fn test_component_string() -> Result<()> { } #[test] -#[cfg(all(feature = "component-model", not(feature = "component-model-async")))] fn test_component_variant() -> Result<()> { mod test { use super::*; @@ -1019,7 +1032,6 @@ fn test_component_variant() -> Result<()> { } #[test] -#[cfg(all(feature = "component-model", not(feature = "component-model-async")))] fn test_component_result() -> Result<()> { mod test { use super::*; @@ -1156,7 +1168,6 @@ fn test_component_result() -> Result<()> { } #[test] -#[cfg(all(feature = "component-model", not(feature = "component-model-async")))] fn test_component_list() -> Result<()> { mod test { use super::*; @@ -1286,7 +1297,6 @@ fn test_component_list() -> Result<()> { } #[test] -#[cfg(all(feature = "component-model", not(feature = "component-model-async")))] fn test_component_option() -> Result<()> { mod test { use super::*; @@ -1406,7 +1416,6 @@ fn test_component_option() -> Result<()> { test::run() } -#[cfg(all(feature = "component-model", not(feature = "component-model-async")))] #[test] fn test_component_builtins() -> Result<()> { run_component_test( @@ -1511,7 +1520,6 @@ fn test_component_builtins() -> Result<()> { ) } -#[cfg(all(feature = "component-model", not(feature = "component-model-async")))] fn cabi_realloc_wat() -> String { r#" (global $bump (mut i32) (i32.const 256)) @@ -1539,7 +1547,6 @@ fn cabi_realloc_wat() -> String { "#.to_string() } -#[cfg(all(feature = "component-model", not(feature = "component-model-async")))] fn shims_wat(params: &str) -> String { let count = params.split_whitespace().count(); let locals_get = (0..count) @@ -1565,7 +1572,6 @@ fn shims_wat(params: &str) -> String { ) } -#[cfg(all(feature = "component-model", not(feature = "component-model-async")))] fn instantiation_wat(core_name: &str, lift_sig: &str) -> String { format!( r#" From 569b09818a428e28270ad33e3f01d23d8ae12ec2 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Mon, 15 Dec 2025 21:56:28 -0500 Subject: [PATCH 64/73] Fix lints --- crates/rr/src/events.rs | 8 +++---- crates/rr/src/events/component_events.rs | 1 - crates/rr/src/events/core_events.rs | 12 +++++----- crates/rr/src/lib.rs | 14 ++++++------ .../src/runtime/component/func/options.rs | 10 ++++----- crates/wasmtime/src/runtime/rr/backend.rs | 20 ++++++++--------- .../src/runtime/rr/hooks/component_hooks.rs | 6 ++--- .../src/runtime/rr/hooks/core_hooks.rs | 9 +++----- .../wasmtime/src/runtime/rr/replay_driver.rs | 22 ++++++++----------- tests/all/rr.rs | 6 ++--- 10 files changed, 49 insertions(+), 59 deletions(-) diff --git a/crates/rr/src/events.rs b/crates/rr/src/events.rs index 8f11610caf..366dc010a2 100644 --- a/crates/rr/src/events.rs +++ b/crates/rr/src/events.rs @@ -40,9 +40,9 @@ impl fmt::Debug for RRFuncArgVals { let hex_string = bytes .iter() .rev() - .map(|b| format!("{:02x}", b)) + .map(|b| format!("{b:02x}")) .collect::(); - format!("0x{}", hex_string) + format!("0x{hex_string}") }; for flat_size in self.sizes.iter() { list.entry(&( @@ -72,7 +72,7 @@ pub trait Validate { where Self: fmt::Debug, { - log::debug!("Validating => {:?}", self); + log::debug!("Validating => {self:?}"); } } @@ -86,7 +86,7 @@ where if self == expect { Ok(()) } else { - log::error!("Validation against {:?} failed!", expect); + log::error!("Validation against {expect:?} failed!"); Err(ReplayError::FailedValidation) } } diff --git a/crates/rr/src/events/component_events.rs b/crates/rr/src/events/component_events.rs index 70d7131f8e..dbdb7f168c 100644 --- a/crates/rr/src/events/component_events.rs +++ b/crates/rr/src/events/component_events.rs @@ -177,7 +177,6 @@ macro_rules! builtin_events { type Error = ReplayError; fn try_from(value: $enum) -> Result { - #[allow(irrefutable_let_patterns)] if let $enum::$rr_var(x) = value { Ok(x) } else { diff --git a/crates/rr/src/events/core_events.rs b/crates/rr/src/events/core_events.rs index 48347e78fa..88316b1c37 100644 --- a/crates/rr/src/events/core_events.rs +++ b/crates/rr/src/events/core_events.rs @@ -15,15 +15,15 @@ pub struct RRModuleInstanceId(pub u32); #[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Ord, PartialOrd)] pub struct RRModuleFuncIndex(pub u32); -impl Into for RRModuleFuncIndex { - fn into(self) -> FuncIndex { - FuncIndex::from_u32(self.0) +impl From for FuncIndex { + fn from(r: RRModuleFuncIndex) -> Self { + FuncIndex::from_u32(r.0) } } -impl Into for RRModuleFuncIndex { - fn into(self) -> EntityIndex { - EntityIndex::from(Into::::into(self)) +impl From for EntityIndex { + fn from(r: RRModuleFuncIndex) -> Self { + EntityIndex::from(FuncIndex::from(r)) } } diff --git a/crates/rr/src/lib.rs b/crates/rr/src/lib.rs index f8d81b285d..4085811d1b 100644 --- a/crates/rr/src/lib.rs +++ b/crates/rr/src/lib.rs @@ -214,10 +214,10 @@ impl fmt::Display for ReplayError { write!(f, "event type mismatch during replay") } Self::EventError(e) => { - write!(f, "{:?}", e) + write!(f, "{e:?}") } Self::FailedRead(e) => { - write!(f, "{}", e)?; + write!(f, "{e}")?; f.write_str("Note: Ensure sufficient `deserialization-buffer-size` in replay settings if you included `validation-metadata` during recording") } Self::InvalidEventPosition => { @@ -229,7 +229,7 @@ impl fmt::Display for ReplayError { "missing component binary with checksum 0x{} during replay", checksum .iter() - .map(|b| format!("{:02x}", b)) + .map(|b| format!("{b:02x}")) .collect::() ) } @@ -239,18 +239,18 @@ impl fmt::Display for ReplayError { "missing module binary with checksum {:02x?} during replay", checksum .iter() - .map(|b| format!("{:02x}", b)) + .map(|b| format!("{b:02x}")) .collect::() ) } Self::MissingComponentInstance(id) => { - write!(f, "missing component instance ID {:?} during replay", id) + write!(f, "missing component instance ID {id:?} during replay") } Self::MissingModuleInstance(id) => { - write!(f, "missing module instance ID {:?} during replay", id) + write!(f, "missing module instance ID {id:?} during replay") } Self::InvalidCoreFuncIndex(index) => { - write!(f, "replay core func ({:?}) during replay is invalid", index) + write!(f, "replay core func ({index:?}) during replay is invalid") } } } diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index aaf0979821..5004b6f9d3 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -290,9 +290,9 @@ impl<'a, T: 'static> LowerContext<'a, T> { FixedMemorySlice { bytes: slice_mut[offset..].first_chunk_mut().unwrap(), #[cfg(feature = "rr")] - offset: offset, + offset, #[cfg(feature = "rr")] - recorder: recorder, + recorder, } } @@ -314,9 +314,9 @@ impl<'a, T: 'static> LowerContext<'a, T> { DynamicMemorySlice { bytes: &mut slice_mut[offset..][..size], #[cfg(feature = "rr")] - offset: offset, + offset, #[cfg(feature = "rr")] - recorder: recorder, + recorder, } } @@ -526,7 +526,7 @@ impl<'a, T: 'static> LowerContext<'a, T> { } } _ => { - bail!("Invalid event \'{:?}\' encountered during lowering", event); + bail!("Invalid event \'{event:?}\' encountered during lowering"); } }; } diff --git a/crates/wasmtime/src/runtime/rr/backend.rs b/crates/wasmtime/src/runtime/rr/backend.rs index 12016c8af9..b9b2764c5b 100644 --- a/crates/wasmtime/src/runtime/rr/backend.rs +++ b/crates/wasmtime/src/runtime/rr/backend.rs @@ -89,15 +89,15 @@ impl RRFuncArgValsConvertable for RRFuncArgVals { } // Conversions from Wasmtime types to RR types -impl Into for ComponentInstanceId { - fn into(self) -> RRComponentInstanceId { - RRComponentInstanceId(self.as_u32()) +impl From for RRComponentInstanceId { + fn from(id: ComponentInstanceId) -> Self { + RRComponentInstanceId(id.as_u32()) } } -impl Into for InstanceId { - fn into(self) -> RRModuleInstanceId { - RRModuleInstanceId(self.as_u32()) +impl From for RRModuleInstanceId { + fn from(id: InstanceId) -> Self { + RRModuleInstanceId(id.as_u32()) } } @@ -145,7 +145,7 @@ impl Recorder for RecordBuffer { Ok(RecordBuffer { buf: Vec::new(), writer: Box::new(writer), - settings: settings, + settings, }) } @@ -214,7 +214,7 @@ impl Iterator for ReplayBuffer { } else if event.is_marker() { continue 'event_loop; } else { - log::debug!("Read replay event => {}", event); + log::debug!("Read replay event => {event}"); break 'event_loop Some(Ok(event)); } } @@ -376,9 +376,7 @@ mod tests { let b_slice: &[u8] = &b.get_bytes()[..*sz as usize]; assert!( a_slice == b_slice, - "Recorded values {:?} and replayed values {:?} do not match", - a_slice, - b_slice + "Recorded values {a_slice:?} and replayed values {b_slice:?} do not match" ); } Ok(()) diff --git a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs index 7ec16b4c06..6ab2e513e4 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs @@ -43,7 +43,7 @@ pub fn record_wasm_func_begin( #[cfg(feature = "rr")] store.record_event(|| WasmFuncBeginEvent { instance: instance.into(), - func_index: func_index.into(), + func_index, })?; let _ = (instance, func_index, store); Ok(()) @@ -59,7 +59,7 @@ pub fn record_wasm_func_post_return( #[cfg(feature = "rr")] store.0.record_event(|| PostReturnEvent { instance: instance.into(), - func_index: func_index.into(), + func_index, })?; let _ = (instance, func_index, store); Ok(()) @@ -93,7 +93,7 @@ where #[cfg(feature = "rr")] { if let Err(e) = &result { - log::warn!("Wasm function call exited with error: {:?}", e); + log::warn!("Wasm function call exited with error: {e:?}"); } let flat_results = types.flat_types_storage_or_pointer( &InterfaceType::Tuple(types[type_idx].results), diff --git a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs index 631f78dd8e..1e5a28ade8 100644 --- a/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs +++ b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs @@ -46,7 +46,7 @@ where { if origin.is_some() { if let Err(e) = &result { - log::warn!("Wasm function call exited with error: {:?}", e); + log::warn!("Wasm function call exited with error: {e:?}"); } let flat = ty.results().map(|t| t.to_wasm_type().byte_size()); let result = result.map(|_| RRFuncArgVals::from_flat_iter(args, flat)); @@ -148,7 +148,7 @@ where } // Re-entrant call into wasm function: this resembles the implementation in [`ReplayInstance`] RREvent::CoreWasmFuncEntry(event) => { - let entity = Into::::into(event.func_index); + let entity = EntityIndex::from(event.func_index); // Unwrapping the `replay_buffer_mut()` above ensures that we are in replay mode // passing the safety contract for `replay_data_from_store` @@ -182,10 +182,7 @@ where } } _ => { - bail!( - "Unexpected event during core wasm host function replay: {:?}", - event - ); + bail!("Unexpected event during core wasm host function replay: {event:?}",); } } } diff --git a/crates/wasmtime/src/runtime/rr/replay_driver.rs b/crates/wasmtime/src/runtime/rr/replay_driver.rs index 419a875904..ff05a75f1e 100644 --- a/crates/wasmtime/src/runtime/rr/replay_driver.rs +++ b/crates/wasmtime/src/runtime/rr/replay_driver.rs @@ -325,8 +325,7 @@ impl ReplayInstance { let instance = self.component_instances.get_mut(event.instance)?; // Replay lowering steps and obtain raw value arguments to raw function call - let func = - component::Func::from_lifted_func(*instance, Into::into(event.func_index)); + let func = component::Func::from_lifted_func(*instance, event.func_index); let store = self.store.as_context_mut(); // Call the function // @@ -371,8 +370,7 @@ impl ReplayInstance { } RREvent::ComponentPostReturn(event) => { let instance = self.component_instances.get_mut(event.instance)?; - let func = - component::Func::from_lifted_func(*instance, Into::into(event.func_index)); + let func = component::Func::from_lifted_func(*instance, event.func_index); let mut store = self.store.as_context_mut(); func.post_return(&mut store)?; } @@ -386,8 +384,8 @@ impl ReplayInstance { self.insert_module_instance(instance); } RREvent::CoreWasmFuncEntry(event) => { - let instance = self.module_instances.get_mut(event.instance.into())?; - let entity = Into::::into(event.func_index); + let instance = self.module_instances.get_mut(event.instance)?; + let entity = EntityIndex::from(event.func_index); let mut store = self.store.as_context_mut(); let func = instance ._get_export(store.0, entity) @@ -409,7 +407,7 @@ impl ReplayInstance { } _ => { - log::error!("Unexpected top-level RR event: {:?}", rr_event); + log::error!("Unexpected top-level RR event: {rr_event:?}"); Err(ReplayError::IncorrectEventVariant)? } } @@ -434,8 +432,7 @@ impl ReplayInstance { let instance = self.component_instances.get_mut(event.instance)?; // Replay lowering steps and obtain raw value arguments to raw function call - let func = - component::Func::from_lifted_func(*instance, Into::into(event.func_index)); + let func = component::Func::from_lifted_func(*instance, event.func_index); let mut store = self.store.as_context_mut(); // Call the function // @@ -486,8 +483,7 @@ impl ReplayInstance { } RREvent::ComponentPostReturn(event) => { let instance = self.component_instances.get_mut(event.instance)?; - let func = - component::Func::from_lifted_func(*instance, Into::into(event.func_index)); + let func = component::Func::from_lifted_func(*instance, event.func_index); let mut store = self.store.as_context_mut(); func.post_return_async(&mut store).await?; } @@ -503,7 +499,7 @@ impl ReplayInstance { } RREvent::CoreWasmFuncEntry(event) => { let instance = self.module_instances.get_mut(event.instance)?; - let entity = Into::::into(event.func_index); + let entity = EntityIndex::from(event.func_index); let mut store = self.store.as_context_mut(); let func = instance ._get_export(store.0, entity) @@ -529,7 +525,7 @@ impl ReplayInstance { } _ => { - log::error!("Unexpected top-level RR event: {:?}", rr_event); + log::error!("Unexpected top-level RR event: {rr_event:?}"); Err(ReplayError::IncorrectEventVariant)? } } diff --git a/tests/all/rr.rs b/tests/all/rr.rs index 108dfd7e9c..b62be6f856 100644 --- a/tests/all/rr.rs +++ b/tests/all/rr.rs @@ -225,7 +225,7 @@ where ) -> Pin> + Send + 'a>>, { // === RECORDING PHASE === - log::info!("Recording | Validate: {}, Async: {}", validate, is_async); + log::info!("Recording | Validate: {validate}, Async: {is_async}"); let engine = create_recording_engine(is_async)?; let component = Component::new(&engine, component_wat)?; @@ -255,7 +255,7 @@ where trace_reader.set_position(0); // === REPLAY PHASE === - log::info!("Replaying | Validate: {}, Async: {}", validate, is_async); + log::info!("Replaying | Validate: {validate}, Async: {is_async}"); let engine = create_replay_engine(is_async)?; let component = Component::new(&engine, component_wat)?; let replay_settings = ReplaySettings { @@ -1550,7 +1550,7 @@ fn cabi_realloc_wat() -> String { fn shims_wat(params: &str) -> String { let count = params.split_whitespace().count(); let locals_get = (0..count) - .map(|i| format!("local.get {}", i)) + .map(|i| format!("local.get {i}")) .collect::>() .join("\n"); format!( From a7bcb841102c4d7e675943eb1071d9096698706d Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 16 Dec 2025 14:23:06 -0500 Subject: [PATCH 65/73] Fully encapsulate trace as `RREvent`s for new record/replay embedders --- crates/rr/src/events.rs | 24 --------- crates/rr/src/events/common_events.rs | 35 +++++++++++++ crates/rr/src/io.rs | 21 ++++---- crates/rr/src/lib.rs | 55 +++++++++++++------- crates/wasmtime/src/runtime/rr/backend.rs | 62 ++++++++++------------- 5 files changed, 109 insertions(+), 88 deletions(-) diff --git a/crates/rr/src/events.rs b/crates/rr/src/events.rs index 366dc010a2..a36b1f1a50 100644 --- a/crates/rr/src/events.rs +++ b/crates/rr/src/events.rs @@ -172,30 +172,6 @@ macro_rules! event_error_types { ); } -/// 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 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 for CustomMessageEvent - where - T: Into, - { - fn from(v: T) -> Self { - Self(v.into()) - } - } -} - pub mod common_events; pub mod component_events; pub mod core_events; diff --git a/crates/rr/src/events/common_events.rs b/crates/rr/src/events/common_events.rs index d27f916fd2..be602c5a2f 100644 --- a/crates/rr/src/events/common_events.rs +++ b/crates/rr/src/events/common_events.rs @@ -4,6 +4,7 @@ //! or [`core_events`] use super::*; +use crate::RecordSettings; use serde::{Deserialize, Serialize}; /// A call event from Wasm (core or component) into the host @@ -40,3 +41,37 @@ impl Validate<&Result> for WasmFuncReturnEvent { event_error_types! { pub struct WasmFuncReturnError(..) } + +/// Signature of recorded trace. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TraceSignatureEvent { + /// Checksum of the trace contents. + /// + /// This can be used to verify integrity of the trace during replay. + pub checksum: String, + /// Settings used during trace recording. + pub settings: RecordSettings, +} + +impl Validate for TraceSignatureEvent { + fn validate(&self, expect: &str) -> Result<(), ReplayError> { + self.log(); + if self.checksum == expect { + Ok(()) + } else { + Err(ReplayError::FailedValidation) + } + } +} + +/// A diagnostic event for custom String messages. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CustomMessageEvent(pub String); +impl From for CustomMessageEvent +where + T: Into, +{ + fn from(v: T) -> Self { + Self(v.into()) + } +} diff --git a/crates/rr/src/io.rs b/crates/rr/src/io.rs index d7ba18d381..c4470d9e0d 100644 --- a/crates/rr/src/io.rs +++ b/crates/rr/src/io.rs @@ -1,7 +1,7 @@ +use super::RREvent; use anyhow::Result; use core::any::Any; use postcard; -use serde::{Deserialize, Serialize}; cfg_if::cfg_if! { if #[cfg(feature = "std")] { @@ -130,12 +130,12 @@ cfg_if::cfg_if! { } } -/// Serialize and write `value` to a `RecordWriter` +/// Serialize and write an `RREvent` to a `RecordWriter` /// -/// Currently uses `postcard` serializer -pub fn to_record_writer(value: &T, writer: &mut W) -> Result<()> +/// This is the lowest-level underlying writer function for RR events, +/// helpful for implementing `Recorder`s. Currently uses [`postcard`] serializer. +pub fn to_record_writer(value: &RREvent, writer: &mut W) -> Result<()> where - T: Serialize + ?Sized, W: RecordWriter + ?Sized, { #[cfg(feature = "std")] @@ -150,13 +150,12 @@ where Ok(()) } -/// Read and deserialize a `value` from a `ReplayReader`. +/// Read and deserialize an `RREvent` from a `ReplayReader`. /// -/// Currently uses `postcard` deserializer, with optional scratch -/// buffer to deserialize into -pub fn from_replay_reader<'a, T, R>(reader: &'a mut R, scratch: &'a mut [u8]) -> Result +/// This is the lowest-level underlying reader function for RR events, +/// helpful for implementing `Replayer`s. Currently uses [`postcard`] deserializer. +pub fn from_replay_reader<'a, R>(reader: &'a mut R, scratch: &'a mut [u8]) -> Result where - T: Deserialize<'a>, R: ReplayReader + ?Sized, { #[cfg(feature = "std")] @@ -167,7 +166,7 @@ where { let flavor = ReplayReaderFlavor { reader, scratch }; let mut deserializer = postcard::Deserializer::from_flavor(flavor); - let t = T::deserialize(&mut deserializer)?; + let t = serde::Deserialize::deserialize(&mut deserializer)?; deserializer.finalize()?; Ok(t) } diff --git a/crates/rr/src/lib.rs b/crates/rr/src/lib.rs index 4085811d1b..8d323dc080 100644 --- a/crates/rr/src/lib.rs +++ b/crates/rr/src/lib.rs @@ -2,7 +2,6 @@ use anyhow::Result; use core::fmt; pub use events::{ EventError, RRFuncArgVals, ResultEvent, Validate, common_events, component_events, core_events, - marker_events, }; pub use io::{RecordWriter, ReplayReader, from_replay_reader, to_record_writer}; use serde::{Deserialize, Serialize}; @@ -54,12 +53,17 @@ mod io; /// Macro template for [`RREvent`] and its conversion to/from specific /// event types macro_rules! rr_event { - ( - $( - $(#[doc = $doc:literal])* - $variant:ident($event:ty) - ),* - ) => ( + ( + $( + $(#[doc = $doc_np:literal])* + $variant_no_payload:ident + ),* + ; + $( + $(#[doc = $doc:literal])* + $variant:ident($event:ty) + ),* + ) => ( /// A single, unified, low-level recording/replay event /// /// This type is the narrow waist for serialization/deserialization. @@ -67,8 +71,10 @@ macro_rules! rr_event { /// of parameter/return types) may drop down to one or more [`RREvent`]s #[derive(Debug, Clone, Serialize, Deserialize)] pub enum RREvent { - /// Event signalling the end of a trace - Eof, + $( + $(#[doc = $doc_np])* + $variant_no_payload, + )* $( $(#[doc = $doc])* $variant($event), @@ -78,9 +84,11 @@ 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), + Self::$variant_no_payload => write!(f, "{}", stringify!($variant_no_payload)), + )* + $( + Self::$variant(payload) => write!(f, "{:?}", payload), )* } } @@ -104,16 +112,25 @@ 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), + Nop, + /// Event signalling the end of a trace + Eof + ; + /// The signature of the trace, enabling trace integrity during replay. + /// + /// This is always at the start of any valid trace. + TraceSignature(common_events::TraceSignatureEvent), + /// A custom message in the trace, useful for diagnostics. + /// + /// Does not affect trace replay functionality + CustomMessage(common_events::CustomMessageEvent), // Common events for both core or component wasm // REQUIRED events @@ -172,11 +189,11 @@ rr_event! { } impl RREvent { - /// Indicates whether current event is a marker event + /// Indicates whether current event is a diagnostic event #[inline] - pub fn is_marker(&self) -> bool { + pub fn is_diagnostic(&self) -> bool { match self { - Self::Nop(_) | Self::CustomMessage(_) => true, + Self::Nop | Self::CustomMessage(_) => true, _ => false, } } diff --git a/crates/wasmtime/src/runtime/rr/backend.rs b/crates/wasmtime/src/runtime/rr/backend.rs index b9b2764c5b..9773d3f37e 100644 --- a/crates/wasmtime/src/runtime/rr/backend.rs +++ b/crates/wasmtime/src/runtime/rr/backend.rs @@ -138,15 +138,18 @@ impl RecordBuffer { } impl Recorder for RecordBuffer { - fn new_recorder(mut writer: impl RecordWriter, settings: RecordSettings) -> Result { - // Replay requires the Module version and record settings - to_record_writer(ModuleVersionStrategy::WasmtimeVersion.as_str(), &mut writer)?; - to_record_writer(&settings, &mut writer)?; - Ok(RecordBuffer { + fn new_recorder(writer: impl RecordWriter, settings: RecordSettings) -> Result { + let settings_local = settings.clone(); + let mut buf = RecordBuffer { buf: Vec::new(), writer: Box::new(writer), settings, - }) + }; + buf.record_event(|| common_events::TraceSignatureEvent { + checksum: ModuleVersionStrategy::WasmtimeVersion.as_str().to_string(), + settings: settings_local, + })?; + Ok(buf) } #[inline] @@ -211,7 +214,7 @@ impl Iterator for ReplayBuffer { if let RREvent::Eof = &event { self.eof_encountered = true; break 'event_loop None; - } else if event.is_marker() { + } else if event.is_diagnostic() { continue 'event_loop; } else { log::debug!("Read replay event => {event}"); @@ -245,38 +248,29 @@ impl Drop for ReplayBuffer { } impl Replayer for ReplayBuffer { - fn new_replayer( - mut reader: impl ReplayReader + 'static, - settings: ReplaySettings, - ) -> Result { - let mut scratch = [0u8; 12]; - // Ensure module versions match - let version = from_replay_reader::<&str, _>(&mut reader, &mut scratch)?; - assert_eq!( - version, - ModuleVersionStrategy::WasmtimeVersion.as_str(), - "Wasmtime version mismatch between engine used for record and replay" - ); - - // Read the recording settings - let trace_settings: RecordSettings = from_replay_reader(&mut reader, &mut scratch)?; - - if settings.validate && !trace_settings.add_validation { + fn new_replayer(reader: impl ReplayReader + 'static, settings: ReplaySettings) -> Result { + let mut buf = ReplayBuffer { + reader: Box::new(reader), + deser_buffer: vec![0; settings.deserialize_buffer_size], + settings, + // This doesn't matter now; will override after reading header + trace_settings: RecordSettings::default(), + eof_encountered: false, + }; + + let signature: common_events::TraceSignatureEvent = buf.next_event_typed()?; + // Ensure the trace integrity + signature.validate(ModuleVersionStrategy::WasmtimeVersion.as_str())?; + // Update the trace settings + buf.trace_settings = signature.settings; + + if buf.settings.validate && !buf.trace_settings.add_validation { log::warn!( "Replay validation will be omitted since the recorded trace has no validation metadata..." ); } - let deser_buffer = vec![0; settings.deserialize_buffer_size]; - let reader = Box::new(reader); - - Ok(ReplayBuffer { - reader, - settings, - trace_settings, - deser_buffer, - eof_encountered: false, - }) + Ok(buf) } #[inline] From fc79643b175c1e8206389b50c9c905fbed447ac5 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 16 Dec 2025 16:23:01 -0500 Subject: [PATCH 66/73] Remove force soft for no_std, works in release mode --- crates/environ/Cargo.toml | 4 +--- crates/rr/Cargo.toml | 1 - crates/wasmtime/Cargo.toml | 1 + 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/environ/Cargo.toml b/crates/environ/Cargo.toml index ab7de07f42..c1f3cfca61 100644 --- a/crates/environ/Cargo.toml +++ b/crates/environ/Cargo.toml @@ -37,9 +37,7 @@ wasmprinter = { workspace = true, optional = true } wasmtime-component-util = { workspace = true, optional = true } semver = { workspace = true, optional = true, features = ['serde'] } smallvec = { workspace = true, features = ['serde'] } -# NB: LLVM unable to compile sha2 with 'asm' in 'no_std' as of Rustc/Cargo version 1.91.1. -# Currently uses 'force-soft' in 'no_std', but remove if fixed. -sha2 = { version = "0.10.2", default-features = false, features = ['force-soft'] } +sha2 = { version = "0.10.2", default-features = false } [dev-dependencies] clap = { workspace = true, features = ['default'] } diff --git a/crates/rr/Cargo.toml b/crates/rr/Cargo.toml index 90e9ccd3e1..cb9235d706 100644 --- a/crates/rr/Cargo.toml +++ b/crates/rr/Cargo.toml @@ -24,5 +24,4 @@ log = { workspace = true } cfg-if = { workspace = true } [features] -default = ["std"] std = ["postcard/use-std", "wasmtime-environ/std"] diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml index 5b0bca8fbf..7d21837b7b 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -347,6 +347,7 @@ std = [ "dep:rustix", "wasmtime-jit-icache-coherence", "wasmtime-jit-debug?/std", + "wasmtime-rr?/std" ] # Enables support for the `Store::call_hook` API which enables injecting custom From 8bc4c349ffdc02e45183e09b640b61333562bb15 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Tue, 16 Dec 2025 16:46:05 -0500 Subject: [PATCH 67/73] Fix new rr crate to be no_std --- Cargo.lock | 1 + crates/rr/Cargo.toml | 1 + crates/rr/src/events.rs | 2 +- crates/rr/src/io.rs | 4 ++-- crates/rr/src/lib.rs | 24 +++++++++++++++--------- 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ab14ec1e69..db474a09da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4983,6 +4983,7 @@ dependencies = [ "log", "postcard", "serde", + "serde_derive", "wasmtime-environ", ] diff --git a/crates/rr/Cargo.toml b/crates/rr/Cargo.toml index cb9235d706..53064bc75c 100644 --- a/crates/rr/Cargo.toml +++ b/crates/rr/Cargo.toml @@ -18,6 +18,7 @@ all-features = true [dependencies] wasmtime-environ = { workspace = true, features = ["component-model"] } serde = { workspace = true } +serde_derive = { workspace = true } postcard = { workspace = true } anyhow = { workspace = true } log = { workspace = true } diff --git a/crates/rr/src/events.rs b/crates/rr/src/events.rs index a36b1f1a50..fecfaa84e3 100644 --- a/crates/rr/src/events.rs +++ b/crates/rr/src/events.rs @@ -1,4 +1,4 @@ -use crate::ReplayError; +use crate::{ReplayError, prelude::*}; use anyhow::Result; use core::fmt; use serde::{Deserialize, Serialize}; diff --git a/crates/rr/src/io.rs b/crates/rr/src/io.rs index c4470d9e0d..b4bd5dda1e 100644 --- a/crates/rr/src/io.rs +++ b/crates/rr/src/io.rs @@ -1,10 +1,10 @@ -use super::RREvent; -use anyhow::Result; +use crate::{RREvent, prelude::*}; use core::any::Any; use postcard; cfg_if::cfg_if! { if #[cfg(feature = "std")] { + extern crate std; use std::io::{Write, Seek, Read}; /// A writer for recording in RR. pub trait RecordWriter: Write + Send + Sync + Any {} diff --git a/crates/rr/src/lib.rs b/crates/rr/src/lib.rs index 8d323dc080..98f3de6ea5 100644 --- a/crates/rr/src/lib.rs +++ b/crates/rr/src/lib.rs @@ -1,13 +1,24 @@ -use anyhow::Result; -use core::fmt; +#![no_std] + +pub(crate) mod prelude { + pub use anyhow::{self, Result}; + pub use serde::{Deserialize, Serialize}; + pub use wasmtime_environ::prelude::*; +} + +use crate::prelude::*; +pub use core::fmt; pub use events::{ EventError, RRFuncArgVals, ResultEvent, Validate, common_events, component_events, core_events, }; +use events::{component_events::RRComponentInstanceId, core_events::RRModuleInstanceId}; pub use io::{RecordWriter, ReplayReader, from_replay_reader, to_record_writer}; -use serde::{Deserialize, Serialize}; use wasmtime_environ::{EntityIndex, WasmChecksum}; -use events::{component_events::RRComponentInstanceId, core_events::RRModuleInstanceId}; +/// Encapsulation of event types comprising an [`RREvent`] sum type +mod events; +/// I/O support for reading and writing traces +mod io; /// Settings for execution recording. #[derive(Debug, Clone, Serialize, Deserialize)] @@ -45,11 +56,6 @@ impl Default for ReplaySettings { } } -/// Encapsulation of event types comprising an [`RREvent`] sum type -mod events; -/// I/O support for reading and writing traces -mod io; - /// Macro template for [`RREvent`] and its conversion to/from specific /// event types macro_rules! rr_event { From a4987fe920c05af3c2387602ceab48d0bd9da768 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Wed, 17 Dec 2025 18:40:24 -0500 Subject: [PATCH 68/73] Add necessary wasmtime environ types as re-exports through rr --- crates/rr/src/events.rs | 2 -- crates/rr/src/events/component_events.rs | 5 +---- crates/rr/src/events/core_events.rs | 2 +- crates/rr/src/lib.rs | 6 +++++- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/crates/rr/src/events.rs b/crates/rr/src/events.rs index fecfaa84e3..802af99ef4 100644 --- a/crates/rr/src/events.rs +++ b/crates/rr/src/events.rs @@ -1,7 +1,5 @@ use crate::{ReplayError, prelude::*}; -use anyhow::Result; use core::fmt; -use serde::{Deserialize, Serialize}; /// A serde compatible representation of errors produced during execution /// of certain events diff --git a/crates/rr/src/events/component_events.rs b/crates/rr/src/events/component_events.rs index dbdb7f168c..f7c1b25c21 100644 --- a/crates/rr/src/events/component_events.rs +++ b/crates/rr/src/events/component_events.rs @@ -1,10 +1,7 @@ //! Module comprising of component model wasm events use super::*; -use wasmtime_environ::{ - self, WasmChecksum, - component::{ExportIndex, InterfaceType, ResourceDropRet}, -}; +use crate::{ExportIndex, InterfaceType, ResourceDropRet, WasmChecksum}; /// Representation of a component instance identifier during record/replay. /// diff --git a/crates/rr/src/events/core_events.rs b/crates/rr/src/events/core_events.rs index 88316b1c37..141576371f 100644 --- a/crates/rr/src/events/core_events.rs +++ b/crates/rr/src/events/core_events.rs @@ -1,6 +1,6 @@ //! Module comprising of core wasm events use super::*; -use wasmtime_environ::{EntityIndex, FuncIndex, WasmChecksum}; +use crate::{EntityIndex, FuncIndex, WasmChecksum}; /// Representation of a Wasm module instance identifier during record/replay. /// diff --git a/crates/rr/src/lib.rs b/crates/rr/src/lib.rs index 98f3de6ea5..298a17dc5b 100644 --- a/crates/rr/src/lib.rs +++ b/crates/rr/src/lib.rs @@ -13,7 +13,11 @@ pub use events::{ }; use events::{component_events::RRComponentInstanceId, core_events::RRModuleInstanceId}; pub use io::{RecordWriter, ReplayReader, from_replay_reader, to_record_writer}; -use wasmtime_environ::{EntityIndex, WasmChecksum}; +// Export necessary environ types for interactions with the crate +pub use wasmtime_environ::{ + EntityIndex, FuncIndex, WasmChecksum, + component::{ExportIndex, InterfaceType, ResourceDropRet}, +}; /// Encapsulation of event types comprising an [`RREvent`] sum type mod events; From 498af085f9ec9e69e38d839f37c5376604292d7d Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Thu, 18 Dec 2025 13:03:52 -0500 Subject: [PATCH 69/73] Add rr as a crate member --- Cargo.toml | 1 + crates/rr/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f994895866..d3d4d852c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -166,6 +166,7 @@ members = [ "crates/wizer/tests/regex-test", "crates/wizer/benches/regex-bench", "crates/wizer/benches/uap-bench", + "crates/rr", "examples/fib-debug/wasm", "examples/wasm", "examples/tokio/wasm", diff --git a/crates/rr/Cargo.toml b/crates/rr/Cargo.toml index 53064bc75c..a53fff3b3e 100644 --- a/crates/rr/Cargo.toml +++ b/crates/rr/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "wasmtime-rr" version.workspace = true -authors.workspace = true +authors = ["Arjun Ramesh"] edition.workspace = true rust-version.workspace = true documentation = "https://docs.rs/wasmtime-rr" From 682a3e47059853f5939c7241019d48fca28f58e8 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Thu, 18 Dec 2025 13:23:27 -0500 Subject: [PATCH 70/73] Re-export component and module instance ids through rr --- Cargo.toml | 1 - crates/rr/src/lib.rs | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d3d4d852c1..f994895866 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -166,7 +166,6 @@ members = [ "crates/wizer/tests/regex-test", "crates/wizer/benches/regex-bench", "crates/wizer/benches/uap-bench", - "crates/rr", "examples/fib-debug/wasm", "examples/wasm", "examples/tokio/wasm", diff --git a/crates/rr/src/lib.rs b/crates/rr/src/lib.rs index 298a17dc5b..f882a447ab 100644 --- a/crates/rr/src/lib.rs +++ b/crates/rr/src/lib.rs @@ -9,9 +9,10 @@ pub(crate) mod prelude { use crate::prelude::*; pub use core::fmt; pub use events::{ - EventError, RRFuncArgVals, ResultEvent, Validate, common_events, component_events, core_events, + EventError, RRFuncArgVals, ResultEvent, Validate, common_events, + component_events::{self, RRComponentInstanceId}, + core_events::{self, RRModuleInstanceId}, }; -use events::{component_events::RRComponentInstanceId, core_events::RRModuleInstanceId}; pub use io::{RecordWriter, ReplayReader, from_replay_reader, to_record_writer}; // Export necessary environ types for interactions with the crate pub use wasmtime_environ::{ From 668d1458a4a6802696b37f16d6a8ed37fd000863 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Thu, 18 Dec 2025 13:35:16 -0500 Subject: [PATCH 71/73] Rename rr crate to wasm-crimp --- Cargo.lock | 28 +++++++++---------- Cargo.toml | 2 +- crates/{rr => wasm-crimp}/Cargo.toml | 4 +-- crates/{rr => wasm-crimp}/src/events.rs | 0 .../src/events/common_events.rs | 0 .../src/events/component_events.rs | 0 .../src/events/core_events.rs | 0 crates/{rr => wasm-crimp}/src/io.rs | 0 crates/{rr => wasm-crimp}/src/lib.rs | 0 crates/wasmtime/Cargo.toml | 6 ++-- crates/wasmtime/src/runtime/rr/backend.rs | 4 +-- 11 files changed, 22 insertions(+), 22 deletions(-) rename crates/{rr => wasm-crimp}/Cargo.toml (97%) rename crates/{rr => wasm-crimp}/src/events.rs (100%) rename crates/{rr => wasm-crimp}/src/events/common_events.rs (100%) rename crates/{rr => wasm-crimp}/src/events/component_events.rs (100%) rename crates/{rr => wasm-crimp}/src/events/core_events.rs (100%) rename crates/{rr => wasm-crimp}/src/io.rs (100%) rename crates/{rr => wasm-crimp}/src/lib.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index db474a09da..64f4a3830f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4331,6 +4331,19 @@ dependencies = [ "wat", ] +[[package]] +name = "wasm-crimp" +version = "40.0.0" +dependencies = [ + "anyhow", + "cfg-if", + "log", + "postcard", + "serde", + "serde_derive", + "wasmtime-environ", +] + [[package]] name = "wasm-encoder" version = "0.241.2" @@ -4519,6 +4532,7 @@ dependencies = [ "tempfile", "tokio", "wasm-compose", + "wasm-crimp", "wasm-encoder", "wasm-wave", "wasmparser 0.241.2", @@ -4536,7 +4550,6 @@ dependencies = [ "wasmtime-internal-versioned-export-macros", "wasmtime-internal-winch", "wasmtime-internal-wmemcheck", - "wasmtime-rr", "wasmtime-test-util", "wat", "windows-sys 0.61.2", @@ -4974,19 +4987,6 @@ dependencies = [ name = "wasmtime-internal-wmemcheck" version = "40.0.0" -[[package]] -name = "wasmtime-rr" -version = "40.0.0" -dependencies = [ - "anyhow", - "cfg-if", - "log", - "postcard", - "serde", - "serde_derive", - "wasmtime-environ", -] - [[package]] name = "wasmtime-test-macros" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index f994895866..485ffc7573 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -272,7 +272,6 @@ wasmtime-slab = { path = "crates/slab", version = "=40.0.0", package = 'wasmtime wasmtime-jit-icache-coherence = { path = "crates/jit-icache-coherence", version = "=40.0.0", package = 'wasmtime-internal-jit-icache-coherence' } wasmtime-wit-bindgen = { path = "crates/wit-bindgen", version = "=40.0.0", package = 'wasmtime-internal-wit-bindgen' } wasmtime-math = { path = "crates/math", version = "=40.0.0", package = 'wasmtime-internal-math' } -wasmtime-rr = { path = "crates/rr", version = "40.0.0" } wasmtime-unwinder = { path = "crates/unwinder", version = "=40.0.0", package = 'wasmtime-internal-unwinder' } wasmtime-wizer = { path = "crates/wizer", version = "40.0.0" } @@ -284,6 +283,7 @@ wiggle-generate = { path = "crates/wiggle/generate", version = "=40.0.0" } wasi-common = { path = "crates/wasi-common", version = "=40.0.0", default-features = false } pulley-interpreter = { path = 'pulley', version = "=40.0.0" } pulley-macros = { path = 'pulley/macros', version = "=40.0.0" } +wasm-crimp = { path = "crates/wasm-crimp", version = "=40.0.0" } # Cranelift crates in this workspace cranelift-assembler-x64 = { path = "cranelift/assembler-x64", version = "0.127.0" } diff --git a/crates/rr/Cargo.toml b/crates/wasm-crimp/Cargo.toml similarity index 97% rename from crates/rr/Cargo.toml rename to crates/wasm-crimp/Cargo.toml index a53fff3b3e..d42235a250 100644 --- a/crates/rr/Cargo.toml +++ b/crates/wasm-crimp/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "wasmtime-rr" -version.workspace = true +name = "wasm-crimp" authors = ["Arjun Ramesh"] +version.workspace = true edition.workspace = true rust-version.workspace = true documentation = "https://docs.rs/wasmtime-rr" diff --git a/crates/rr/src/events.rs b/crates/wasm-crimp/src/events.rs similarity index 100% rename from crates/rr/src/events.rs rename to crates/wasm-crimp/src/events.rs diff --git a/crates/rr/src/events/common_events.rs b/crates/wasm-crimp/src/events/common_events.rs similarity index 100% rename from crates/rr/src/events/common_events.rs rename to crates/wasm-crimp/src/events/common_events.rs diff --git a/crates/rr/src/events/component_events.rs b/crates/wasm-crimp/src/events/component_events.rs similarity index 100% rename from crates/rr/src/events/component_events.rs rename to crates/wasm-crimp/src/events/component_events.rs diff --git a/crates/rr/src/events/core_events.rs b/crates/wasm-crimp/src/events/core_events.rs similarity index 100% rename from crates/rr/src/events/core_events.rs rename to crates/wasm-crimp/src/events/core_events.rs diff --git a/crates/rr/src/io.rs b/crates/wasm-crimp/src/io.rs similarity index 100% rename from crates/rr/src/io.rs rename to crates/wasm-crimp/src/io.rs diff --git a/crates/rr/src/lib.rs b/crates/wasm-crimp/src/lib.rs similarity index 100% rename from crates/rr/src/lib.rs rename to crates/wasm-crimp/src/lib.rs diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml index 7d21837b7b..9abcb06c92 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -31,7 +31,7 @@ wasmtime-slab = { workspace = true, optional = true } wasmtime-versioned-export-macros = { workspace = true } wasmtime-wmemcheck = { workspace = true, optional = true } wasmtime-math = { workspace = true } -wasmtime-rr = { workspace = true, optional = true } +wasm-crimp = { workspace = true, optional = true } pulley-interpreter = { workspace = true } target-lexicon = { workspace = true } wasmparser = { workspace = true } @@ -347,7 +347,7 @@ std = [ "dep:rustix", "wasmtime-jit-icache-coherence", "wasmtime-jit-debug?/std", - "wasmtime-rr?/std" + "wasm-crimp?/std" ] # Enables support for the `Store::call_hook` API which enables injecting custom @@ -430,6 +430,6 @@ compile-time-builtins = ['dep:wasm-compose', 'dep:tempfile'] # Enable support for the common base infrastructure of record/replay rr = [ - "dep:wasmtime-rr", + "dep:wasm-crimp", "component-model" ] diff --git a/crates/wasmtime/src/runtime/rr/backend.rs b/crates/wasmtime/src/runtime/rr/backend.rs index 9773d3f37e..7263425e47 100644 --- a/crates/wasmtime/src/runtime/rr/backend.rs +++ b/crates/wasmtime/src/runtime/rr/backend.rs @@ -6,9 +6,9 @@ use crate::{AsContextMut, ModuleVersionStrategy, Val, ValRaw, ValType}; use wasmtime_environ::component::FlatTypesStorage; // Public Re-exports -pub use wasmtime_rr::{RecordSettings, RecordWriter, ReplayError, ReplayReader, ReplaySettings}; +pub use wasm_crimp::{RecordSettings, RecordWriter, ReplayError, ReplayReader, ReplaySettings}; // Crate-internal re-exports -pub(crate) use wasmtime_rr::{ +pub(crate) use wasm_crimp::{ RREvent, RRFuncArgVals, Recorder, Replayer, ResultEvent, Validate, common_events, component_events::{self, RRComponentInstanceId}, core_events::{self, RRModuleInstanceId}, From 83fb223a2f866d40e7c28fe3565bfb406b78eee0 Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Thu, 18 Dec 2025 13:39:08 -0500 Subject: [PATCH 72/73] Fix: missed rename for tests --- crates/wasmtime/src/runtime/rr/backend.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/wasmtime/src/runtime/rr/backend.rs b/crates/wasmtime/src/runtime/rr/backend.rs index 7263425e47..66f0ea1ae7 100644 --- a/crates/wasmtime/src/runtime/rr/backend.rs +++ b/crates/wasmtime/src/runtime/rr/backend.rs @@ -301,8 +301,8 @@ mod tests { use std::fs::File; use std::path::Path; use tempfile::{NamedTempFile, TempPath}; + use wasm_crimp::EventError; use wasmtime_environ::{FuncIndex, component::ResourceDropRet}; - use wasmtime_rr::EventError; impl ReplayBuffer { /// Pop the next replay event and calls `f` with a expected event type From dfd1f87ced77ca870c34232e37cb774a5681366d Mon Sep 17 00:00:00 2001 From: Arjun Ramesh Date: Fri, 19 Dec 2025 14:00:55 -0500 Subject: [PATCH 73/73] Move crimp crate back into wasmtime --- Cargo.lock | 14 ---------- Cargo.toml | 1 - crates/wasm-crimp/Cargo.toml | 28 ------------------- crates/wasmtime/Cargo.toml | 7 +---- crates/wasmtime/src/runtime/rr/backend.rs | 11 ++++---- .../src/runtime/rr/backend/crimp.rs} | 17 +++-------- .../src/runtime/rr/backend/crimp}/events.rs | 4 ++- .../rr/backend/crimp}/events/common_events.rs | 1 - .../backend/crimp}/events/component_events.rs | 3 +- .../rr/backend/crimp}/events/core_events.rs | 2 +- .../src/runtime/rr/backend/crimp}/io.rs | 3 +- 11 files changed, 18 insertions(+), 73 deletions(-) delete mode 100644 crates/wasm-crimp/Cargo.toml rename crates/{wasm-crimp/src/lib.rs => wasmtime/src/runtime/rr/backend/crimp.rs} (97%) rename crates/{wasm-crimp/src => wasmtime/src/runtime/rr/backend/crimp}/events.rs (98%) rename crates/{wasm-crimp/src => wasmtime/src/runtime/rr/backend/crimp}/events/common_events.rs (98%) rename crates/{wasm-crimp/src => wasmtime/src/runtime/rr/backend/crimp}/events/component_events.rs (98%) rename crates/{wasm-crimp/src => wasmtime/src/runtime/rr/backend/crimp}/events/core_events.rs (96%) rename crates/{wasm-crimp/src => wasmtime/src/runtime/rr/backend/crimp}/io.rs (99%) diff --git a/Cargo.lock b/Cargo.lock index 64f4a3830f..885ca882fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4331,19 +4331,6 @@ dependencies = [ "wat", ] -[[package]] -name = "wasm-crimp" -version = "40.0.0" -dependencies = [ - "anyhow", - "cfg-if", - "log", - "postcard", - "serde", - "serde_derive", - "wasmtime-environ", -] - [[package]] name = "wasm-encoder" version = "0.241.2" @@ -4532,7 +4519,6 @@ dependencies = [ "tempfile", "tokio", "wasm-compose", - "wasm-crimp", "wasm-encoder", "wasm-wave", "wasmparser 0.241.2", diff --git a/Cargo.toml b/Cargo.toml index 485ffc7573..eb37211713 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -283,7 +283,6 @@ wiggle-generate = { path = "crates/wiggle/generate", version = "=40.0.0" } wasi-common = { path = "crates/wasi-common", version = "=40.0.0", default-features = false } pulley-interpreter = { path = 'pulley', version = "=40.0.0" } pulley-macros = { path = 'pulley/macros', version = "=40.0.0" } -wasm-crimp = { path = "crates/wasm-crimp", version = "=40.0.0" } # Cranelift crates in this workspace cranelift-assembler-x64 = { path = "cranelift/assembler-x64", version = "0.127.0" } diff --git a/crates/wasm-crimp/Cargo.toml b/crates/wasm-crimp/Cargo.toml deleted file mode 100644 index d42235a250..0000000000 --- a/crates/wasm-crimp/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "wasm-crimp" -authors = ["Arjun Ramesh"] -version.workspace = true -edition.workspace = true -rust-version.workspace = true -documentation = "https://docs.rs/wasmtime-rr" -description = "Record-replay interface for Wasm modules and components" -license = "Apache-2.0 WITH LLVM-exception" -repository = "https://github.com/bytecodealliance/wasmtime" - -[lints] -workspace = true - -[package.metadata.docs.rs] -all-features = true - -[dependencies] -wasmtime-environ = { workspace = true, features = ["component-model"] } -serde = { workspace = true } -serde_derive = { workspace = true } -postcard = { workspace = true } -anyhow = { workspace = true } -log = { workspace = true } -cfg-if = { workspace = true } - -[features] -std = ["postcard/use-std", "wasmtime-environ/std"] diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml index 9abcb06c92..b82aae3c15 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -31,7 +31,6 @@ wasmtime-slab = { workspace = true, optional = true } wasmtime-versioned-export-macros = { workspace = true } wasmtime-wmemcheck = { workspace = true, optional = true } wasmtime-math = { workspace = true } -wasm-crimp = { workspace = true, optional = true } pulley-interpreter = { workspace = true } target-lexicon = { workspace = true } wasmparser = { workspace = true } @@ -347,7 +346,6 @@ std = [ "dep:rustix", "wasmtime-jit-icache-coherence", "wasmtime-jit-debug?/std", - "wasm-crimp?/std" ] # Enables support for the `Store::call_hook` API which enables injecting custom @@ -429,7 +427,4 @@ debug = [ compile-time-builtins = ['dep:wasm-compose', 'dep:tempfile'] # Enable support for the common base infrastructure of record/replay -rr = [ - "dep:wasm-crimp", - "component-model" -] +rr = ["component-model"] diff --git a/crates/wasmtime/src/runtime/rr/backend.rs b/crates/wasmtime/src/runtime/rr/backend.rs index 66f0ea1ae7..b03eb7ba51 100644 --- a/crates/wasmtime/src/runtime/rr/backend.rs +++ b/crates/wasmtime/src/runtime/rr/backend.rs @@ -5,11 +5,10 @@ use crate::store::InstanceId; use crate::{AsContextMut, ModuleVersionStrategy, Val, ValRaw, ValType}; use wasmtime_environ::component::FlatTypesStorage; -// Public Re-exports -pub use wasm_crimp::{RecordSettings, RecordWriter, ReplayError, ReplayReader, ReplaySettings}; -// Crate-internal re-exports -pub(crate) use wasm_crimp::{ - RREvent, RRFuncArgVals, Recorder, Replayer, ResultEvent, Validate, common_events, +mod crimp; +pub use crimp::{ + RREvent, RRFuncArgVals, RecordSettings, RecordWriter, Recorder, ReplayError, ReplayReader, + ReplaySettings, Replayer, ResultEvent, Validate, common_events, component_events::{self, RRComponentInstanceId}, core_events::{self, RRModuleInstanceId}, from_replay_reader, to_record_writer, @@ -298,10 +297,10 @@ mod tests { use crate::ValRaw; use crate::WasmFuncOrigin; use crate::store::InstanceId; + use crimp::EventError; use std::fs::File; use std::path::Path; use tempfile::{NamedTempFile, TempPath}; - use wasm_crimp::EventError; use wasmtime_environ::{FuncIndex, component::ResourceDropRet}; impl ReplayBuffer { diff --git a/crates/wasm-crimp/src/lib.rs b/crates/wasmtime/src/runtime/rr/backend/crimp.rs similarity index 97% rename from crates/wasm-crimp/src/lib.rs rename to crates/wasmtime/src/runtime/rr/backend/crimp.rs index f882a447ab..4abf4d9f4a 100644 --- a/crates/wasm-crimp/src/lib.rs +++ b/crates/wasmtime/src/runtime/rr/backend/crimp.rs @@ -1,24 +1,15 @@ -#![no_std] - -pub(crate) mod prelude { - pub use anyhow::{self, Result}; - pub use serde::{Deserialize, Serialize}; - pub use wasmtime_environ::prelude::*; -} +//! The CRIMP record-replay interface specification. use crate::prelude::*; -pub use core::fmt; +use core::fmt; pub use events::{ EventError, RRFuncArgVals, ResultEvent, Validate, common_events, component_events::{self, RRComponentInstanceId}, core_events::{self, RRModuleInstanceId}, }; pub use io::{RecordWriter, ReplayReader, from_replay_reader, to_record_writer}; -// Export necessary environ types for interactions with the crate -pub use wasmtime_environ::{ - EntityIndex, FuncIndex, WasmChecksum, - component::{ExportIndex, InterfaceType, ResourceDropRet}, -}; +use serde::{Deserialize, Serialize}; +use wasmtime_environ::{EntityIndex, WasmChecksum}; /// Encapsulation of event types comprising an [`RREvent`] sum type mod events; diff --git a/crates/wasm-crimp/src/events.rs b/crates/wasmtime/src/runtime/rr/backend/crimp/events.rs similarity index 98% rename from crates/wasm-crimp/src/events.rs rename to crates/wasmtime/src/runtime/rr/backend/crimp/events.rs index 802af99ef4..415fcfd36d 100644 --- a/crates/wasm-crimp/src/events.rs +++ b/crates/wasmtime/src/runtime/rr/backend/crimp/events.rs @@ -1,5 +1,7 @@ -use crate::{ReplayError, prelude::*}; +use super::ReplayError; +use crate::prelude::*; use core::fmt; +use serde::{Deserialize, Serialize}; /// A serde compatible representation of errors produced during execution /// of certain events diff --git a/crates/wasm-crimp/src/events/common_events.rs b/crates/wasmtime/src/runtime/rr/backend/crimp/events/common_events.rs similarity index 98% rename from crates/wasm-crimp/src/events/common_events.rs rename to crates/wasmtime/src/runtime/rr/backend/crimp/events/common_events.rs index be602c5a2f..671e3da4c8 100644 --- a/crates/wasm-crimp/src/events/common_events.rs +++ b/crates/wasmtime/src/runtime/rr/backend/crimp/events/common_events.rs @@ -5,7 +5,6 @@ use super::*; use crate::RecordSettings; -use serde::{Deserialize, Serialize}; /// A call event from Wasm (core or component) into the host /// diff --git a/crates/wasm-crimp/src/events/component_events.rs b/crates/wasmtime/src/runtime/rr/backend/crimp/events/component_events.rs similarity index 98% rename from crates/wasm-crimp/src/events/component_events.rs rename to crates/wasmtime/src/runtime/rr/backend/crimp/events/component_events.rs index f7c1b25c21..fa23b3dfac 100644 --- a/crates/wasm-crimp/src/events/component_events.rs +++ b/crates/wasmtime/src/runtime/rr/backend/crimp/events/component_events.rs @@ -1,7 +1,8 @@ //! Module comprising of component model wasm events use super::*; -use crate::{ExportIndex, InterfaceType, ResourceDropRet, WasmChecksum}; +use wasmtime_environ::WasmChecksum; +use wasmtime_environ::component::{ExportIndex, InterfaceType, ResourceDropRet}; /// Representation of a component instance identifier during record/replay. /// diff --git a/crates/wasm-crimp/src/events/core_events.rs b/crates/wasmtime/src/runtime/rr/backend/crimp/events/core_events.rs similarity index 96% rename from crates/wasm-crimp/src/events/core_events.rs rename to crates/wasmtime/src/runtime/rr/backend/crimp/events/core_events.rs index 141576371f..88316b1c37 100644 --- a/crates/wasm-crimp/src/events/core_events.rs +++ b/crates/wasmtime/src/runtime/rr/backend/crimp/events/core_events.rs @@ -1,6 +1,6 @@ //! Module comprising of core wasm events use super::*; -use crate::{EntityIndex, FuncIndex, WasmChecksum}; +use wasmtime_environ::{EntityIndex, FuncIndex, WasmChecksum}; /// Representation of a Wasm module instance identifier during record/replay. /// diff --git a/crates/wasm-crimp/src/io.rs b/crates/wasmtime/src/runtime/rr/backend/crimp/io.rs similarity index 99% rename from crates/wasm-crimp/src/io.rs rename to crates/wasmtime/src/runtime/rr/backend/crimp/io.rs index b4bd5dda1e..2be79a271b 100644 --- a/crates/wasm-crimp/src/io.rs +++ b/crates/wasmtime/src/runtime/rr/backend/crimp/io.rs @@ -1,4 +1,5 @@ -use crate::{RREvent, prelude::*}; +use super::RREvent; +use crate::prelude::*; use core::any::Any; use postcard;