diff --git a/.github/workflows/publish-artifacts.yml b/.github/workflows/publish-artifacts.yml index 3f6565e2f2..1030cc973a 100644 --- a/.github/workflows/publish-artifacts.yml +++ b/.github/workflows/publish-artifacts.yml @@ -13,7 +13,8 @@ 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 diff --git a/Cargo.lock b/Cargo.lock index c156f405b4..885ca882fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4695,6 +4695,7 @@ dependencies = [ "semver", "serde", "serde_derive", + "sha2", "smallvec", "target-lexicon", "wasm-encoder", diff --git a/Cargo.toml b/Cargo.toml index 81c1b53615..1c30e39742 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -425,6 +425,7 @@ 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 } # ============================================================================= # @@ -508,6 +509,7 @@ component-model-async = [ "wasmtime-wasi-http?/p3", "dep:futures", ] +rr = ["wasmtime/rr", "component-model", "wasmtime-cli-flags/rr", "run"] # This feature, when enabled, will statically compile out all logging statements # throughout Wasmtime and its dependencies. diff --git a/crates/c-api/include/wasmtime/extern.h b/crates/c-api/include/wasmtime/extern.h index 51675b970d..f9e0858124 100644 --- a/crates/c-api/include/wasmtime/extern.h +++ b/crates/c-api/include/wasmtime/extern.h @@ -30,7 +30,11 @@ 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; } wasmtime_func_t; /// \brief Representation of a table in Wasmtime. diff --git a/crates/cli-flags/Cargo.toml b/crates/cli-flags/Cargo.toml index ab5bebba76..724f0c899b 100644 --- a/crates/cli-flags/Cargo.toml +++ b/crates/cli-flags/Cargo.toml @@ -41,3 +41,4 @@ memory-protection-keys = ["wasmtime/memory-protection-keys"] pulley = ["wasmtime/pulley"] stack-switching = ["wasmtime/stack-switching"] debug = ["wasmtime/debug"] +rr = ["wasmtime/rr", "component-model"] diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs index f5acb0c41a..d043bc7826 100644 --- a/crates/cli-flags/src/lib.rs +++ b/crates/cli-flags/src/lib.rs @@ -497,6 +497,25 @@ wasmtime_option_group! { } } +wasmtime_option_group! { + #[derive(PartialEq, Clone, Deserialize)] + #[serde(rename_all = "kebab-case", deny_unknown_fields)] + pub struct RecordOptions { + /// Filename for the recorded execution trace (or empty string to skip writing a file). + pub path: Option, + /// Include (optional) signatures to facilitate validation checks during replay + /// (see `wasmtime replay` for details). + pub validation_metadata: Option, + /// Window size of internal buffering for record events (large windows offer more opportunities + /// for coalescing events at the cost of memory usage). + pub event_window_size: Option, + } + + enum Record { + ... + } +} + #[derive(Debug, Clone, PartialEq)] pub struct WasiNnGraph { pub format: String, @@ -547,6 +566,18 @@ pub struct CommonOptions { #[serde(skip)] wasi_raw: Vec>, + /// Options to enable and configure execution recording, `-R help` to see all. + /// + /// Generates a serialized trace of the Wasm module execution that captures all + /// non-determinism observable by the module. This trace can subsequently be + /// re-executed in a determinstic, embedding-agnostic manner (see the `wasmtime replay` command). + /// + /// Note: Minimal configuration options for deterministic Wasm semantics will be + /// enforced during recording by default (NaN canonicalization, deterministic relaxed SIMD). + #[arg(short = 'R', long = "record", value_name = "KEY[=VAL[,..]]")] + #[serde(skip)] + record_raw: Vec>, + // These fields are filled in by the `configure` method below via the // options parsed from the CLI above. This is what the CLI should use. #[arg(skip)] @@ -573,6 +604,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)] @@ -619,12 +654,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, } @@ -642,12 +679,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(()) } @@ -1004,6 +1043,15 @@ impl CommonOptions { config.gc_support(enable); } + let record = &self.record; + match_feature! { + ["rr" : &record.path] + _path => { + config.rr(wasmtime::RRConfig::Recording); + }, + _ => err, + } + Ok(config) } @@ -1113,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(); @@ -1235,6 +1284,8 @@ impl fmt::Display for CommonOptions { wasm, wasi_raw, wasi, + record_raw, + record, configured, target, config, @@ -1251,6 +1302,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(); @@ -1258,6 +1310,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() @@ -1268,6 +1321,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 { @@ -1285,6 +1343,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/Cargo.toml b/crates/environ/Cargo.toml index 378096f1a6..1f317340b6 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 a4301a1fed..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, @@ -118,6 +119,7 @@ impl<'a> ObjectBuilder<'a> { debuginfo, has_unparsed_debuginfo, data, + wasm, data_align, passive_data, .. @@ -220,6 +222,7 @@ impl<'a> ObjectBuilder<'a> { has_wasm_debuginfo: self.tunables.parse_wasm_debuginfo, dwarf, }, + checksum: WasmChecksum::from_binary(wasm), }) } diff --git a/crates/environ/src/component.rs b/crates/environ/src/component.rs index e1c7962d38..82ad98ad85 100644 --- a/crates/environ/src/component.rs +++ b/crates/environ/src/component.rs @@ -81,22 +81,69 @@ 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) => { $mac! { + #[rr_builtin( + variant = ResourceNew32, + entry = ResourceNew32EntryEvent, + exit = ResourceNew32ReturnEvent, + success_ty = u32 + )] resource_new32(vmctx: vmctx, caller_instance: u32, resource: u32, rep: u32) -> u64; + + #[rr_builtin( + variant = ResourceRep32, + entry = ResourceRep32EntryEvent, + exit = ResourceRep32ReturnEvent, + success_ty = u32 + )] resource_rep32(vmctx: vmctx, caller_instance: u32, 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( + variant = ResourceDrop, + entry = ResourceDropEntryEvent, + exit = ResourceDropReturnEvent, + success_ty = ResourceDropRet + )] resource_drop(vmctx: vmctx, caller_instance: u32, resource: u32, idx: u32) -> u64; + #[rr_builtin( + variant = ResourceTransferOwn, + entry = ResourceTransferOwnEntryEvent, + exit = ResourceTransferOwnReturnEvent, + 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, + success_ty = u32 + )] resource_transfer_borrow(vmctx: vmctx, src_idx: u32, src_table: u32, dst_table: u32) -> u64; + + #[rr_builtin( + variant = ResourceEnterCall, + entry = ResourceEnterCallEntryEvent + )] resource_enter_call(vmctx: vmctx); + + #[rr_builtin( + variant = ResourceExitCall, + entry = ResourceExitCallEntryEvent, + exit = ResourceExitCallReturnEvent, + success_ty = () + )] resource_exit_call(vmctx: vmctx) -> bool; #[cfg(feature = "component-model-async")] diff --git a/crates/environ/src/component/artifacts.rs b/crates/environ/src/component/artifacts.rs index 22b9c2ce79..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,6 +20,8 @@ pub struct ComponentArtifacts { pub types: ComponentTypes, /// Serialized metadata about all included core wasm modules. pub static_modules: PrimaryMap, + /// 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 8db6b800c4..438eccec31 100644 --- a/crates/environ/src/component/types.rs +++ b/crates/environ/src/component/types.rs @@ -372,6 +372,186 @@ impl ComponentTypes { } } + /// Returns the flat storage ABI representation for an interface type. + /// If the flat representation is larger than `limit` number of flat types, returns + /// storage with a pointer. + /// + /// 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. + pub fn flat_types_storage_or_pointer( + &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(|| { + let mut flat = FlatTypesStorage::new(); + // Pointer representation for wasm32 and wasm64 respectively + flat.push(FlatType::I32, FlatType::I64); + flat + }) + } + + 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 { + 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 { + 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; + } + } + }) + }) + .is_some() + } + } + }; + + // Logic + let mut storage_buf = FlatTypesStorage::new(); + 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(_) => push(storage, FlatType::I32, FlatType::I32), + + 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 + push(storage, FlatType::I32, FlatType::I64) + && push(storage, FlatType::I32, FlatType::I64) + } + + 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 => 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) + && 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_inner(&self[*i].ty, limit)), + ) + } + InterfaceType::Result(i) => { + 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)), + ) + } + } + .then_some(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) @@ -1207,6 +1387,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. @@ -1236,3 +1493,24 @@ pub enum FlatType { F32, F64, } + +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 { + 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 22f6a9fb7b..7340e1b258 100644 --- a/crates/environ/src/component/types_builder.rs +++ b/crates/environ/src/component/types_builder.rs @@ -840,78 +840,6 @@ 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, -} - -impl FlatTypesStorage { - const fn new() -> FlatTypesStorage { - FlatTypesStorage { - memory32: [FlatType::I32; MAX_FLAT_TYPES], - memory64: [FlatType::I32; MAX_FLAT_TYPES], - len: 0, - } - } - - 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. - 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 - } - } -} - -impl FlatType { - 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/environ/src/module_artifacts.rs b/crates/environ/src/module_artifacts.rs index c5c0643c43..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, @@ -545,6 +568,9 @@ 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: WasmChecksum, } /// The name of a function stored in the diff --git a/crates/environ/src/types.rs b/crates/environ/src/types.rs index 72dcc9fe3c..8f25f54f8b 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/Cargo.toml b/crates/wasmtime/Cargo.toml index 87636c9343..b82aae3c15 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -425,3 +425,6 @@ debug = [ # Enables support for defining compile-time builtins. compile-time-builtins = ['dep:wasm-compose', 'dep:tempfile'] + +# Enable support for the common base infrastructure of record/replay +rr = ["component-model"] diff --git a/crates/wasmtime/src/compile.rs b/crates/wasmtime/src/compile.rs index 5ebb978d07..a4045e74ce 100644 --- a/crates/wasmtime/src/compile.rs +++ b/crates/wasmtime/src/compile.rs @@ -29,8 +29,6 @@ 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, @@ -38,6 +36,8 @@ use wasmtime_environ::{ ModuleEnvironment, ModuleTranslation, ModuleTypes, ModuleTypesBuilder, ObjectKind, PrimaryMap, StaticModuleIndex, Tunables, }; +#[cfg(feature = "component-model")] +use wasmtime_environ::{WasmChecksum, component::Translator}; mod call_graph; mod scc; @@ -206,6 +206,7 @@ pub(crate) fn build_component_artifacts( ty, types, static_modules: compilation_artifacts.modules, + checksum: WasmChecksum::from_binary(binary), }; object.serialize_info(&artifacts); diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 252814466f..b752c86b2b 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -99,6 +99,28 @@ impl core::hash::Hash for ModuleVersionStrategy { } } +impl ModuleVersionStrategy { + /// Get the string-encoding version of the module. + pub fn as_str(&self) -> &str { + match &self { + Self::WasmtimeVersion => env!("CARGO_PKG_VERSION_MAJOR"), + Self::Custom(c) => c, + Self::None => "", + } + } +} + +/// Configuration for record/replay +#[derive(Clone)] +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. /// @@ -164,6 +186,8 @@ pub struct Config { pub(crate) macos_use_mach_ports: bool, pub(crate) detect_host_feature: Option Option>, pub(crate) x86_float_abi_ok: Option, + #[cfg(feature = "rr")] + pub(crate) rr_config: RRConfig, } /// User-provided configuration for the compiler. @@ -273,6 +297,8 @@ impl Config { #[cfg(not(feature = "std"))] detect_host_feature: None, x86_float_abi_ok: None, + #[cfg(feature = "rr")] + rr_config: RRConfig::None, }; #[cfg(any(feature = "cranelift", feature = "winch"))] { @@ -2308,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 @@ -2349,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` @@ -2869,6 +2904,53 @@ impl Config { self.x86_float_abi_ok = Some(enable); self } + + /// 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`]. + #[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 + } + + /// Validate if the current configuration has conflicting overrides that prevent + /// execution determinism. Returns an error if a conflict exists. + /// + /// Note: Keep this in sync with [`Config::enforce_determinism`]. + #[inline] + #[cfg(feature = "rr")] + pub(crate) fn validate_determinism_conflicts(&self) -> Result<()> { + if let Some(v) = self.tunables.relaxed_simd_deterministic { + if v == false { + bail!("Relaxed deterministic SIMD cannot be disabled when determinism is enforced"); + } + } + #[cfg(any(feature = "cranelift", feature = "winch"))] + if let Some(v) = self + .compiler_config + .settings + .get("enable_nan_canonicalization") + { + if v != "true" { + bail!("NaN canonicalization cannot be disabled when determinism is enforced"); + } + } + Ok(()) + } + + /// Enable execution trace recording or replaying to the configuration. + /// + /// When either recording/replaying are enabled, determinism is implicitly + /// enforced (see [`Config::enforce_determinism`] for details). + #[cfg(feature = "rr")] + #[inline] + pub fn rr(&mut self, cfg: RRConfig) -> &mut Self { + self.rr_config = cfg; + self + } } impl Default for Config { diff --git a/crates/wasmtime/src/engine.rs b/crates/wasmtime/src/engine.rs index dad9dfaa0d..943589da5a 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; @@ -95,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")] @@ -253,6 +255,26 @@ impl Engine { self.config().async_support } + /// Returns whether the engine is configured to support execution recording + #[cfg(feature = "rr")] + #[inline] + pub fn is_recording(&self) -> bool { + 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 /// Wasmtime. /// diff --git a/crates/wasmtime/src/engine/serialization.rs b/crates/wasmtime/src/engine/serialization.rs index d1f843896b..8666a598b5 100644 --- a/crates/wasmtime/src/engine/serialization.rs +++ b/crates/wasmtime/src/engine/serialization.rs @@ -95,19 +95,13 @@ pub fn check_compatible(engine: &Engine, mmap: &[u8], expected: ObjectKind) -> R }; match &engine.config().module_version { - ModuleVersionStrategy::WasmtimeVersion => { - let version = core::str::from_utf8(version)?; - if version != env!("CARGO_PKG_VERSION_MAJOR") { - bail!("Module was compiled with incompatible Wasmtime version '{version}'"); - } - } - ModuleVersionStrategy::Custom(v) => { + ModuleVersionStrategy::None => { /* ignore the version info, accept all */ } + _ => { let version = core::str::from_utf8(&version)?; - if version != v { + if version != engine.config().module_version.as_str() { bail!("Module was compiled with incompatible version '{version}'"); } } - ModuleVersionStrategy::None => { /* ignore the version info, accept all */ } } postcard::from_bytes::>(data)?.check_compatible(engine) } @@ -121,11 +115,7 @@ pub fn append_compiler_info(engine: &Engine, obj: &mut Object<'_>, metadata: &Me ); let mut data = Vec::new(); data.push(VERSION); - let version = match &engine.config().module_version { - ModuleVersionStrategy::WasmtimeVersion => env!("CARGO_PKG_VERSION_MAJOR"), - ModuleVersionStrategy::Custom(c) => c, - ModuleVersionStrategy::None => "", - }; + let version = engine.config().module_version.as_str(); // This precondition is checked in Config::module_version: assert!( version.len() < 256, diff --git a/crates/wasmtime/src/runtime.rs b/crates/wasmtime/src/runtime.rs index a0ff5e0ec6..7ed63e9969 100644 --- a/crates/wasmtime/src/runtime.rs +++ b/crates/wasmtime/src/runtime.rs @@ -48,6 +48,7 @@ pub(crate) mod module; #[cfg(feature = "debug-builtins")] pub(crate) mod native_debug; pub(crate) mod resources; +pub(crate) mod rr; pub(crate) mod store; pub(crate) mod trampoline; pub(crate) mod trap; @@ -90,6 +91,10 @@ pub use linker::*; pub use memory::*; pub use module::{Module, ModuleExport}; pub use resources::*; +#[cfg(feature = "rr")] +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/component.rs b/crates/wasmtime/src/runtime/component/component.rs index 2990248d9e..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. @@ -94,6 +94,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 checksum of the source binary from which the module was compiled. + checksum: WasmChecksum, } pub(crate) struct AllCallFuncPointers { @@ -407,6 +410,7 @@ impl Component { table: index, mut types, mut static_modules, + checksum, } = match artifacts { Some(artifacts) => artifacts, None => postcard::from_bytes(code_memory.wasmtime_info())?, @@ -461,6 +465,7 @@ impl Component { info, index, realloc_func_type, + checksum, }), }) } @@ -865,6 +870,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) -> &WasmChecksum { + &self.inner.checksum + } + /// Returns the `Export::LiftedFunction` metadata associated with `export`. /// /// # Panics diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index f1420c24e3..5ccaa6caa3 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,6 +1789,20 @@ impl Instance { if let Some(mut flags) = flags { flags.set_may_enter(false); } + + 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( &mut store, callee.as_non_null(), @@ -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, ) }; @@ -1962,6 +1992,7 @@ impl Instance { &mut store, func.as_non_null(), slice::from_ref(&post_return_arg).into(), + RRWasmFuncType::None, )?; } } @@ -2107,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(), @@ -2114,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()]); @@ -2142,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(); @@ -2235,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); } @@ -2330,6 +2366,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 +5084,7 @@ fn queue_call0( is_concurrent, callback, post_return.map(SendSyncPtr::new), + Some(handle), ) } } 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 4ea8d48fa2..d54b843786 100644 --- a/crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs +++ b/crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs @@ -2960,7 +2960,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)?; @@ -3032,9 +3032,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 { @@ -3838,7 +3838,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.rs b/crates/wasmtime/src/runtime/component/func.rs index 366ffd7165..d7eac7e667 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::ComponentFunc; use crate::component::values::Val; use crate::prelude::*; +use crate::rr::{RRWasmFuncType, component_hooks}; use crate::runtime::vm::component::{ComponentInstance, InstanceFlags, ResourceTables}; use crate::runtime::vm::{Export, VMFuncRef}; use crate::store::StoreOpaque; @@ -566,7 +567,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( @@ -580,6 +581,12 @@ impl Func { LowerParams: Copy, LowerReturn: Copy, { + component_hooks::record_wasm_func_begin( + self.instance.id().instance(), + self.index, + store.0, + )?; + let export = self.lifted_core_func(store.0); #[repr(C)] @@ -618,14 +625,19 @@ impl Func { // and `ComponentType` implementations, hence `ComponentType` being an // `unsafe` trait. unsafe { + 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(); + + 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( &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(), + params_and_returns, + RRWasmFuncType::Component { type_idx, types }, )?; } @@ -721,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); @@ -779,6 +796,7 @@ impl Func { func, NonNull::new(core::ptr::slice_from_raw_parts(&post_return_arg, 1).cast_mut()) .unwrap(), + RRWasmFuncType::None, )?; } @@ -817,7 +835,9 @@ impl Func { params .iter() .zip(params_ty.types.iter()) - .try_for_each(|(param, ty)| param.lower(cx, *ty, dst)) + .try_for_each(|(param, ty)| { + component_hooks::record_lower_flat(|cx, ty| param.lower(cx, ty, dst), cx, *ty) + }) } else { Self::store_args(cx, ¶ms_ty, params, dst) } @@ -834,7 +854,13 @@ impl Func { let mut offset = ptr; for (ty, arg) in params_ty.types.iter().zip(args) { let abi = cx.types.canonical_abi(ty); - arg.store(cx, *ty, abi.next_field32_size(&mut offset))?; + let ptr = abi.next_field32_size(&mut offset); + component_hooks::record_lower_memory( + |cx, ty, ptr| arg.store(cx, ty, ptr), + cx, + *ty, + ptr, + )?; } dst[0].write(ValRaw::i64(ptr as i64)); @@ -842,7 +868,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], @@ -907,7 +933,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 a573342303..32cee8c4d3 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/crates/wasmtime/src/runtime/component/func/host.rs @@ -3,14 +3,16 @@ use crate::component::concurrent; use crate::component::concurrent::{Accessor, Status}; use crate::component::func::{LiftContext, LowerContext}; 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::types::ComponentFunc; use crate::component::{ComponentNamedList, ComponentType, Instance, Lift, Lower, Val}; use crate::prelude::*; +use crate::rr; 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; @@ -20,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 { @@ -265,6 +267,8 @@ where let param_tys = InterfaceType::Tuple(ty.params); let result_tys = InterfaceType::Tuple(ty.results); + rr::component_hooks::record_validate_host_func_entry(storage, &types, ¶m_tys, store.0)?; + if async_ { #[cfg(feature = "component-model-async")] { @@ -300,7 +304,12 @@ where flags.set_may_leave(false); } let mut lower = LowerContext::new(store, options, instance); - ret.linear_lower_to_memory(&mut lower, result_tys, retptr)?; + rr::component_hooks::record_lower_memory( + |cx, ty, ptr| ret.linear_lower_to_memory(cx, ty, ptr), + &mut lower, + result_tys, + retptr, + )?; unsafe { flags.set_may_leave(true); } @@ -584,10 +593,36 @@ 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::component_hooks::record_lower_flat( + |cx, ty| ret.linear_lower_to_flat(cx, ty, storage), + cx, + ty, + ); + rr::component_hooks::record_host_func_return( + unsafe { storage_as_slice_mut(storage) }, + cx.types, + &ty, + 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 offset = validate_inbounds::(cx.as_slice(), ptr)?; + let result = rr::component_hooks::record_lower_memory( + |cx, ty, offset| ret.linear_lower_to_memory(cx, ty, offset), + cx, + ty, + offset, + ); + // Record the pointer + rr::component_hooks::record_host_func_return( + &[MaybeUninit::new(*ptr)], + cx.types, + &InterfaceType::U32, + cx.store.0, + )?; + result } } } @@ -719,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 @@ -734,6 +769,54 @@ 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]; @@ -763,6 +846,13 @@ where 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(), + )?; + if async_ { #[cfg(feature = "component-model-async")] { @@ -776,7 +866,7 @@ where let future = closure(store.as_context_mut(), ty, params_and_results, result_start); - let task = instance.first_poll(store, future, caller_instance, { + 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 { @@ -831,16 +921,38 @@ 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()) { - val.lower(&mut cx, *ty, &mut dst)?; + 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 = types.canonical_abi(ty).next_field32_size(&mut ptr); - val.store(&mut cx, *ty, offset)?; + 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 { @@ -853,6 +965,65 @@ where 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 + } + } + + if async_ { + unreachable!( + "Replay logic should be unreachable with component async-ABI (currently unsupported)" + ); + } + 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(), + )?; + + 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"); + } +} + /// Loads the parameters for a dynamic host function call into `params` /// /// Returns the number of flat `storage` values consumed. diff --git a/crates/wasmtime/src/runtime/component/func/options.rs b/crates/wasmtime/src/runtime/component/func/options.rs index 4c4ec2588c..cad56f239c 100644 --- a/crates/wasmtime/src/runtime/component/func/options.rs +++ b/crates/wasmtime/src/runtime/component/func/options.rs @@ -1,15 +1,26 @@ use crate::StoreContextMut; +#[cfg(feature = "rr")] +use crate::ValRaw; use crate::component::concurrent::ConcurrentState; use crate::component::matching::InstanceType; use crate::component::resources::{HostResourceData, HostResourceIndex, HostResourceTables}; use crate::component::{Instance, ResourceType}; use crate::prelude::*; +use crate::rr::{DynamicMemorySlice, FixedMemorySlice}; +#[cfg(feature = "rr")] +use crate::rr::{ + RREvent, RecordBuffer, ReplayError, Replayer, ResultEvent, Validate, + component_events::ReallocEntryEvent, component_events::ReallocReturnEvent, + component_hooks::ReplayLoweringPhase, +}; use crate::runtime::vm::VMFuncRef; use crate::runtime::vm::component::{ CallContexts, ComponentInstance, HandleTable, InstanceFlags, ResourceTables, }; use crate::store::{StoreId, StoreOpaque}; use alloc::sync::Arc; +#[cfg(feature = "rr")] +use core::mem::MaybeUninit; use core::pin::Pin; use core::ptr::NonNull; use wasmtime_environ::component::{ @@ -114,7 +125,37 @@ impl<'a, T: 'static> LowerContext<'a, T> { &self.instance().component().env_component().options[self.options] } - /// Returns a view into memory as a mutable slice of bytes. + /// Return the `InstanceFlags` for this context + #[cfg(feature = "rr")] + fn instance_flags(&self) -> InstanceFlags { + let vminstance = self.instance.id().get(self.store.0); + let opts = self.options(); + vminstance.instance_flags(opts.instance) + } + + /// 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")] + 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) + } + + /// 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(&self) -> &[u8] { + self.instance.options_memory(self.store.0, self.options) + } + + /// Returns a view into memory as an mutable slice of bytes. /// /// # Panics /// @@ -125,13 +166,13 @@ impl<'a, T: 'static> LowerContext<'a, T> { } /// Invokes the memory allocation function (which is style after `realloc`) - /// with the specified parameters. + /// with the specified parameters. This method has no record/replay scaffolding /// /// # Panics /// /// This will panic if realloc hasn't been configured for this lowering via /// its canonical options. - pub fn realloc( + pub fn realloc_inner( &mut self, old: usize, old_size: usize, @@ -162,7 +203,13 @@ impl<'a, T: 'static> LowerContext<'a, T> { // Invoke the wasm malloc function using its raw and statically known // signature. let result = unsafe { - ReallocFunc::call_raw(&mut StoreContextMut(store), &realloc_ty, realloc, params)? + ReallocFunc::call_raw( + &mut StoreContextMut(store), + &realloc_ty, + realloc, + params, + None, + )? }; if result % old_align != 0 { @@ -182,6 +229,35 @@ impl<'a, T: 'static> LowerContext<'a, T> { Ok(result) } + /// Wrapper around [Self::realloc_inner], but with record/replay scaffolding. + /// This should be the intended entrypoint for realloc calls in most situations. + /// + /// # 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")] + 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(feature = "rr")] + self.store.0.record_event_validation(|| { + ReallocReturnEvent(ResultEvent::from_anyhow_result(&result)) + })?; + result + } + /// Returns a fixed mutable slice of memory `N` bytes large starting at /// offset `N`, panicking on out-of-bounds. /// @@ -192,7 +268,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) -> FixedMemorySlice<'_, N> { + cfg_if::cfg_if! { + if #[cfg(feature = "rr")] { + 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 @@ -203,7 +287,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() + FixedMemorySlice { + bytes: slice_mut[offset..].first_chunk_mut().unwrap(), + #[cfg(feature = "rr")] + offset: offset, + #[cfg(feature = "rr")] + 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) -> DynamicMemorySlice<'_> { + cfg_if::cfg_if! { + if #[cfg(feature = "rr")] { + let (slice_mut, recorder) = self.as_slice_mut_with_recorder(); + } else { + let slice_mut = self.as_slice_mut(); + } + } + DynamicMemorySlice { + bytes: &mut slice_mut[offset..][..size], + #[cfg(feature = "rr")] + offset: offset, + #[cfg(feature = "rr")] + recorder: recorder, + } } /// Lowers an `own` resource into the guest, converting the `rep` specified @@ -292,6 +406,138 @@ 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 the putting + /// the resulting value into storage (if provided) on the following termination conditions: + /// - `HostFunctionReturnEvent` when phase == HostFuncReturn + /// - `WasmFunctionEntryEvent` when phase == WasmFuncEntry + /// + /// ## 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")] + pub fn replay_lowering( + &mut self, + mut result_storage: Option<&mut [MaybeUninit]>, + phase: ReplayLoweringPhase, + ) -> Result<()> { + if self.store.0.replay_buffer_mut().is_none() { + return Ok(()); + } + + unsafe { + self.instance_flags().set_may_leave(false); + } + + 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()?; + let run_validate = buf.settings().validate && buf.trace_settings().add_validation; + match event { + RREvent::HostFuncReturn(e) => { + 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.args.into_raw_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()); + } + if let Some(storage) = result_storage.as_deref_mut() { + e.args.into_raw_slice(storage); + } + complete = true; + } + RREvent::ComponentReallocEntry(e) => { + let result = + self.realloc_inner(e.old_addr, e.old_size, e.old_align, e.new_size); + 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::ComponentLowerFlatReturn(e) => { + if run_validate { + lower_stack.pop().ok_or(ReplayError::InvalidEventPosition)?; + } + lowering_error = e.0.ret().map_err(Into::into).err(); + } + RREvent::ComponentLowerMemoryReturn(e) => { + if run_validate { + lower_store_stack + .pop() + .ok_or(ReplayError::InvalidEventPosition)?; + } + 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 + // 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::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) => { + if run_validate { + lowering_error = e.0.validate(&realloc_stack.pop().unwrap()).err() + } + } + RREvent::ComponentLowerFlatEntry(_) => { + // All we want here is ensuring Entry occurs before Return + if run_validate { + lower_stack.push(()) + } + } + RREvent::ComponentLowerMemoryEntry(_) => { + // All we want here is ensuring Entry occurs before Return + if run_validate { + lower_store_stack.push(()) + } + } + _ => { + bail!("Invalid event \'{:?}\' encountered during lowering", event); + } + }; + } + + unsafe { + self.instance_flags().set_may_leave(true); + } + + 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 dceddd3285..66360fe952 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}; use crate::component::matching::InstanceType; use crate::component::storage::{storage_as_slice, storage_as_slice_mut}; use crate::prelude::*; +use crate::rr::component_hooks; use crate::{AsContextMut, StoreContext, StoreContextMut, ValRaw}; use alloc::borrow::Cow; use core::fmt; @@ -472,8 +473,11 @@ where dst: &mut MaybeUninit, ) -> Result<()> { assert!(Params::flatten_count() <= MAX_FLAT_PARAMS); - params.linear_lower_to_flat(cx, ty, dst)?; - 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. @@ -496,7 +500,12 @@ 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)?; + 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 @@ -1107,7 +1116,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()); @@ -1207,7 +1216,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(()) } @@ -1229,7 +1238,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. @@ -1499,7 +1508,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())) } @@ -1516,13 +1526,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)?; } @@ -1534,7 +1545,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. @@ -1553,8 +1564,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 @@ -1573,11 +1585,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 fb1e99a09c..c5f838a540 100644 --- a/crates/wasmtime/src/runtime/component/instance.rs +++ b/crates/wasmtime/src/runtime/component/instance.rs @@ -8,6 +8,9 @@ use crate::component::{ use crate::instance::OwnedImports; use crate::linker::DefinitionType; use crate::prelude::*; +#[cfg(feature = "rr")] +use crate::rr::RecordBuffer; +use crate::rr::component_hooks; use crate::runtime::vm::component::{ CallContexts, ComponentInstance, ResourceTables, TypedResource, TypedResourceIndex, }; @@ -528,6 +531,24 @@ impl Instance { } } + #[cfg(feature = "rr")] + pub(crate) fn options_memory_mut_with_recorder<'a>( + &self, + store: &'a mut StoreOpaque, + options: OptionsIndex, + ) -> (&'a mut [u8], Option<&'a mut RecordBuffer>) { + let memory = match self.options_memory_raw(store, options) { + Some(m) => m, + None => return (&mut [], store.record_buffer_mut()), + }; + unsafe { + let memory = memory.as_ref(); + let slice = + core::slice::from_raw_parts_mut(memory.base.as_ptr(), memory.current_length()); + (slice, store.record_buffer_mut()) + } + } + /// Helper function to simultaneously get a borrow to this instance's /// component as well as the store that this component is contained within. /// @@ -870,7 +891,7 @@ impl<'a> Instantiator<'a> { // if required. let i = unsafe { - crate::Instance::new_started(store, module, imports.as_ref()).await? + crate::Instance::new_started(store, module, imports.as_ref(), true).await? }; self.instance_mut(store.0).push_instance_id(i.id()); } @@ -1058,6 +1079,12 @@ 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 + 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 @@ -1166,6 +1193,15 @@ impl InstancePre { .allocator() .increment_component_instance_count()?; let mut instantiator = Instantiator::new(&self.component, store.0, &self.imports); + + // Record/replay hooks + store.0.validate_rr_config()?; + component_hooks::record_and_replay_validate_instantiation( + &mut store, + *self.component.checksum(), + instantiator.id(), + )?; + instantiator.run(&mut store).await.map_err(|e| { store .engine() diff --git a/crates/wasmtime/src/runtime/component/mod.rs b/crates/wasmtime/src/runtime/component/mod.rs index 2d28f9596b..3fc715148d 100644 --- a/crates/wasmtime/src/runtime/component/mod.rs +++ b/crates/wasmtime/src/runtime/component/mod.rs @@ -105,14 +105,14 @@ mod component; #[cfg(feature = "component-model-async")] pub(crate) mod concurrent; -mod func; +pub(crate) mod func; mod has_data; mod instance; 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/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/component/store.rs b/crates/wasmtime/src/runtime/component/store.rs index 762c031706..ea7501634d 100644 --- a/crates/wasmtime/src/runtime/component/store.rs +++ b/crates/wasmtime/src/runtime/component/store.rs @@ -5,6 +5,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)] @@ -12,7 +13,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 7de4f2229b..acf8b9db55 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::{self, RRWasmFuncType}; use crate::runtime::Uninhabited; use crate::runtime::vm::{ self, InterpreterRef, SendSyncPtr, StoreBox, VMArrayCallHostFuncContext, @@ -17,7 +18,10 @@ 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, packed_option::PackedOption, packed_option::ReservedValue, +}; /// A reference to the abstract `nofunc` heap value. /// @@ -102,6 +106,32 @@ 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. + pub instance: InstanceId, + /// The function index within the module. + 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 @@ -278,13 +308,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: 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); + 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); @@ -549,6 +586,7 @@ impl Func { Func { store, unsafe_func_ref: func_ref.into(), + origin: PackedOption::default(), } } @@ -1009,12 +1047,67 @@ 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 [])); + unsafe { + let ty = &self.ty(&store); + let origin = self.origin.expand(); + Self::call_unchecked_raw( + &mut store, + func_ref, + params_and_returns, + RRWasmFuncType::Core { ty, origin }, + ) + } + } + + /// Raw, unchecked call to the underlying func_ref. + /// + /// 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]>, + rr: RRWasmFuncType, + ) -> Result<()> { // 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) } + 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_inner(&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_inner(&mut store, func_ref, params_and_returns) + }, + unsafe { params_and_returns.as_ref() }, + type_idx, + types, + &mut store, + ) + } + // Passthrough + #[cfg(feature = "component-model")] + RRWasmFuncType::None => unsafe { + 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]>, @@ -1124,7 +1217,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 +1257,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 +1353,54 @@ 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)?; + 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, + &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) }) + } + + 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().map(|x| x.to_wasm_type().byte_size()); + rr::core_hooks::record_host_func_return(values_vec, flat_results, &mut caller.store.0)?; + 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, 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 @@ -1481,6 +1606,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 = PackedOption::from(origin); + } + + // Get the origin of this function + pub(crate) fn origin(&self) -> Option { + self.origin.expand() + } } /// Prepares for entrance into WebAssembly. @@ -2326,9 +2461,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 { @@ -2349,44 +2483,41 @@ 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 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()), + ); - let mut store = if P::may_gc() { - AutoAssertNoGc::new(caller.store.0) + 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 { - 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); + Self::array_call_trampoline_impl( + caller, + args, + func, + num_params, + flat_size_params, + num_results, + flat_size_results, + ) } - 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. - let ret = unsafe { ret.store(&mut store, args.as_mut())? }; - Ok(ret) } }; @@ -2403,6 +2534,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/func/typed.rs b/crates/wasmtime/src/runtime/func/typed.rs index 7ea40f9826..86acd4fe7f 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,41 @@ where params.store(&mut store, ty, dst)?; } + 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); + 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. + // + // 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| { + 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 3445a4a401..0bfd926cbe 100644 --- a/crates/wasmtime/src/runtime/instance.rs +++ b/crates/wasmtime/src/runtime/instance.rs @@ -1,5 +1,6 @@ use crate::linker::{Definition, DefinitionType}; use crate::prelude::*; +use crate::rr::core_hooks; use crate::runtime::vm::{ self, Imports, ModuleRuntimeInfo, VMFuncRef, VMFunctionImport, VMGlobalImport, VMMemoryImport, VMStore, VMTableImport, VMTagImport, @@ -120,7 +121,9 @@ impl Instance { // `typecheck_externs` above which satisfies the condition that all // the imports are valid for this module. assert!(!store.0.async_support()); - vm::assert_ready(unsafe { Instance::new_started(&mut store, module, imports.as_ref()) }) + vm::assert_ready(unsafe { + Instance::new_started(&mut store, module, imports.as_ref(), false) + }) } /// Same as [`Instance::new`], except for usage in [asynchronous stores]. @@ -203,7 +206,7 @@ impl Instance { let mut store = store.as_context_mut(); let imports = Instance::typecheck_externs(store.0, module, imports)?; // See `new` for notes on this unsafety - unsafe { Instance::new_started(&mut store, module, imports.as_ref()).await } + unsafe { Instance::new_started(&mut store, module, imports.as_ref(), false).await } } fn typecheck_externs( @@ -249,6 +252,7 @@ impl Instance { store: &mut StoreContextMut<'_, T>, module: &Module, imports: Imports<'_>, + from_component: bool, ) -> Result { let (instance, start) = { let (mut limiter, store) = store.0.resource_limiter_and_store_opaque(); @@ -256,6 +260,19 @@ impl Instance { // function. unsafe { Instance::new_raw(store, limiter.as_mut(), module, imports).await? } }; + + // Record/replay hooks + store.0.validate_rr_config()?; + if !from_component { + // 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")] @@ -475,7 +492,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. @@ -872,7 +889,7 @@ impl InstancePre { // in match the module we're instantiating. assert!(!store.0.async_support()); vm::assert_ready(unsafe { - Instance::new_started(&mut store, &self.module, imports.as_ref()) + Instance::new_started(&mut store, &self.module, imports.as_ref(), false) }) } @@ -903,7 +920,7 @@ impl InstancePre { // This unsafety should be handled by the type-checking performed by the // constructor of `InstancePre` to assert that all the imports we're passing // in match the module we're instantiating. - unsafe { Instance::new_started(&mut store, &self.module, imports.as_ref()).await } + unsafe { Instance::new_started(&mut store, &self.module, imports.as_ref(), false).await } } } diff --git a/crates/wasmtime/src/runtime/module.rs b/crates/wasmtime/src/runtime/module.rs index 30492b8b9e..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; @@ -161,6 +161,9 @@ struct ModuleInner { /// Runtime offset information for `VMContext`. offsets: VMOffsets, + + /// The checksum of the source binary from which this module was compiled. + checksum: WasmChecksum, } impl fmt::Debug for Module { @@ -532,6 +535,7 @@ impl Module { index: Arc, serializable: bool, ) -> Result { + let checksum = info.checksum; let module = CompiledModule::from_artifacts( code.code_memory().clone(), info, @@ -556,6 +560,7 @@ impl Module { #[cfg(any(feature = "cranelift", feature = "winch"))] serializable, offsets, + checksum, }), }) } @@ -890,6 +895,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) -> &WasmChecksum { + &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 new file mode 100644 index 0000000000..57facce276 --- /dev/null +++ b/crates/wasmtime/src/runtime/rr.rs @@ -0,0 +1,78 @@ +//! Wasmtime's Record and Replay support. +//! +//! This feature is currently not optimized and under development +use crate::ValRaw; +use ::core::{mem::MaybeUninit, slice}; + +/// 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( + unused, + reason = "trait used as a bound for hooks despite not calling methods directly" +)] +pub trait FlatBytes { + unsafe fn bytes(&self, size: u8) -> &[u8]; + fn from_bytes(value: &[u8]) -> Self; +} + +impl FlatBytes for ValRaw { + #[inline] + unsafe fn bytes(&self, size: u8) -> &[u8] { + &self.get_bytes()[..size as usize] + } + #[inline] + fn from_bytes(value: &[u8]) -> Self { + ValRaw::from_bytes(value) + } +} + +impl FlatBytes for MaybeUninit { + #[inline] + /// 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 { + MaybeUninit::new(ValRaw::from_bytes(value)) + } +} + +/// Convenience method hooks for injecting event recording/replaying in the rest of the engine +mod hooks; +pub(crate) use hooks::{RRWasmFuncType, core_hooks}; +#[cfg(feature = "component-model")] +pub(crate) use hooks::{ + component_hooks, component_hooks::DynamicMemorySlice, component_hooks::FixedMemorySlice, +}; + +/// 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 new file mode 100644 index 0000000000..b6e934473b --- /dev/null +++ b/crates/wasmtime/src/runtime/rr/core.rs @@ -0,0 +1,1028 @@ +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::{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 + 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(()) + } + } +} + +/// 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(()) + } + + /// End the trace and flush any remaining data + pub fn finish(&mut self) -> Result<()> { + // Insert End of trace delimiter + self.push_event(RREvent::Eof)?; + self.flush() + } +} + +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)?; + Ok(RecordBuffer { + buf: Vec::new(), + writer: Box::new(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) + } + + #[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(..) { + 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, + /// Whether buffer has been completely read + eof_encountered: bool, +} + +impl Iterator for ReplayBuffer { + type Item = Result; + + fn next(&mut self) -> Option { + if self.eof_encountered { + return None; + } + let ret = 'event_loop: loop { + 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))); + } + Ok(event) => { + if let RREvent::Eof = &event { + self.eof_encountered = true; + break 'event_loop None; + } else if event.is_marker() { + continue 'event_loop; + } else { + log::debug!("Read replay event => {}", event); + break 'event_loop Some(Ok(event)); + } + } + } + }; + ret + } +} + +impl Drop for ReplayBuffer { + fn drop(&mut self) { + 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 = true; + break; + } + if remaining { + log::warn!( + "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."); + } + } +} + +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 = 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.deserialize_buffer_size]; + let reader = Box::new(reader); + + Ok(ReplayBuffer { + reader, + settings, + trace_settings, + deser_buffer, + eof_encountered: false, + }) + } + + #[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 + } +} + +#[cfg(test)] +mod tests { + use super::*; + 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; + + 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 call_event = self.next_event_typed()?; + Ok(f(call_event)?) + } + } + + 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"); + + // Record values + let mut recorder = + RecordBuffer::new_recorder(Box::new(File::create(tmppath)?), record_settings)?; + + record_fn(&mut recorder)?; + recorder.finish()?; + + 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)?; + + 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 { + caller_instance: 3, + 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; + use wasmtime_environ::WasmChecksum; + + let component_event = ComponentInstantiationEvent { + component: WasmChecksum::from_binary(&[0xAB; 256]), + instance: ComponentInstanceId::from_u32(42), + }; + + let core_event = CoreInstantiationEvent { + module: WasmChecksum::from_binary(&[0xCD; 256]), + 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.rs b/crates/wasmtime/src/runtime/rr/core/events.rs new file mode 100644 index 0000000000..8bba175c37 --- /dev/null +++ b/crates/wasmtime/src/runtime/rr/core/events.rs @@ -0,0 +1,259 @@ +use super::ReplayError; +use crate::rr::FlatBytes; +use crate::{AsContextMut, Val, prelude::*}; +use crate::{ValRaw, ValType}; +use core::fmt; +use serde::{Deserialize, Serialize}; +use wasmtime_environ::component::FlatTypesStorage; + +/// 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 +pub trait EventError: core::error::Error + Send + Sync + 'static { + fn new(t: String) -> Self + where + Self: Sized; + fn get(&self) -> &String; +} + +/// 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, +} + +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() + } +} + +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. +/// 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` +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); + } +} + +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 { + log::error!("Validation against {:?} failed!", expect); + Err(ReplayError::FailedValidation) + } + } +} + +/// 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 } + } + )* + ); +} + +/// 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 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/wasmtime/src/runtime/rr/core/events/common_events.rs b/crates/wasmtime/src/runtime/rr/core/events/common_events.rs new file mode 100644 index 0000000000..d27f916fd2 --- /dev/null +++ b/crates/wasmtime/src/runtime/rr/core/events/common_events.rs @@ -0,0 +1,42 @@ +//! 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 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 [`HostFuncEntryEvent`] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HostFuncReturnEvent { + /// Raw values passed across the call/return boundary + pub args: RRFuncArgVals, +} + +/// 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) + } +} + +event_error_types! { + pub struct WasmFuncReturnError(..) +} diff --git a/crates/wasmtime/src/runtime/rr/core/events/component_events.rs b/crates/wasmtime/src/runtime/rr/core/events/component_events.rs new file mode 100644 index 0000000000..fabecb2a8e --- /dev/null +++ b/crates/wasmtime/src/runtime/rr/core/events/component_events.rs @@ -0,0 +1,199 @@ +//! 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}, +}; + +/// 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, +} + +/// 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 component: WasmChecksum, + /// Instance ID for the instantiated component + 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 { + /// 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 +#[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 to flat destination +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LowerFlatEntryEvent { + pub ty: InterfaceType, +} + +/// Entry to type lowering invocation to destination in memory +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LowerMemoryEntryEvent { + 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, +} + +event_error_types! { + pub struct ReallocError(..), + pub struct LowerFlatError(..), + pub struct LowerMemoryError(..), + pub struct BuiltinError(..) +} + +#[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<(), LowerFlatError>); + +/// 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_rules! builtin_events { + // Main rule matching component function definitions + ( + $( + $( #[cfg($attr:meta)] )? + $( #[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_entry_enum $($($rr_var $rr_entry)?)*); + // Prioitize ret_succ if provided + $( + builtin_events!(@gen_entry_events $($rr_entry)? $($pname, $param)*); + builtin_events!(@gen_return_events $($($rr_return)?)? -> $($($rr_succ)?)? $($result)?); + )* + ); + + // All things related to BuiltinReturnEvent enum + (@gen_return_enum $($rr_var:ident $event:ident)*) => { + #[derive(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(Clone, PartialEq, Serialize, Deserialize)] + pub enum BuiltinEntryEvent { + $($rr_var($event),)* + } + builtin_events!(@from_impls BuiltinEntryEvent $($rr_var $event)*); + }; + + + (@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(pub ResultEvent); + + impl $rr_return { + pub fn ret(self) -> Result { + self.0.0.map_err(|e| e.into()) + } + } + }; + // Stubbed if `rr_builtin` not provided + (@gen_return_events -> $($result_opts:tt)*) => {}; + + // 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 { + 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) + } + } + } + )* + + 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 + (@ret_first $first:tt $($rest:tt)*) => ($first); +} + +// 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 new file mode 100644 index 0000000000..e0f8a2feb9 --- /dev/null +++ b/crates/wasmtime/src/runtime/rr/core/events/core_events.rs @@ -0,0 +1,22 @@ +//! 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/core/io.rs b/crates/wasmtime/src/runtime/rr/core/io.rs new file mode 100644 index 0000000000..fbae0ae802 --- /dev/null +++ b/crates/wasmtime/src/runtime/rr/core/io.rs @@ -0,0 +1,78 @@ +use crate::prelude::*; +use core::any::Any; +use postcard; +use serde::{Deserialize, Serialize}; + +cfg_if::cfg_if! { + if #[cfg(feature = "std")] { + use std::io::{Write, Seek, Read}; + /// A writer for recording in RR. + pub trait RecordWriter: Write + Send + Sync + Any {} + impl RecordWriter for T {} + + /// A reader for replaying in RR. + pub trait ReplayReader: Read + Seek + Send + Sync {} + impl ReplayReader for T {} + + } else { + use core::{convert::AsRef, iter::Extend}; + + /// A writer for recording in RR. + pub trait RecordWriter: Extend + Send + Sync + Any {} + impl + Send + Sync + Any> RecordWriter for T {} + + /// 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 + fn seek(&mut self, pos: usize); + } + + } +} + +/// Serialize and write `value` to a `RecordWriter` +/// +/// Currently uses `postcard` serializer +pub(super) fn to_record_writer(value: &T, writer: &mut W) -> Result<()> +where + T: Serialize + ?Sized, + W: RecordWriter, +{ + #[cfg(feature = "std")] + { + postcard::to_io(value, writer)?; + } + #[cfg(not(feature = "std"))] + { + postcard::to_extend(value, writer)?; + } + Ok(()) +} + +/// Read and deserialize a `value` from a `ReplayReader`. +/// +/// 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 +where + T: Deserialize<'a>, + R: ReplayReader, +{ + #[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) + } +} diff --git a/crates/wasmtime/src/runtime/rr/hooks.rs b/crates/wasmtime/src/runtime/rr/hooks.rs new file mode 100644 index 0000000000..b8549c929e --- /dev/null +++ b/crates/wasmtime/src/runtime/rr/hooks.rs @@ -0,0 +1,57 @@ +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 + Core { + ty: &'a FuncType, + origin: Option, + }, + /// Component RR hooks to be performed + #[cfg(feature = "component-model")] + Component { + type_idx: TypeFuncIndex, + types: Arc, + }, + /// No RR hooks to be performed + #[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 new file mode 100644 index 0000000000..71ec3524bb --- /dev/null +++ b/crates/wasmtime/src/runtime/rr/hooks/component_hooks.rs @@ -0,0 +1,346 @@ +#[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, HostFuncReturnEvent, WasmFuncReturnEvent}; +#[cfg(feature = "rr")] +use crate::rr::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; +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}; + +/// Indicator type signalling the context during lowering +#[cfg(feature = "rr")] +#[derive(Debug)] +pub enum ReplayLoweringPhase { + WasmFuncEntry, + 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")] + store.record_event(|| WasmFuncBeginEvent { instance, func_idx })?; + let _ = (instance, func_idx, store); + 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")] + 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] +pub fn record_and_replay_validate_wasm_func( + wasm_call: F, + args: &[ValRaw], + type_idx: TypeFuncIndex, + types: Arc, + store: &mut StoreContextMut<'_, T>, +) -> Result<()> +where + F: FnOnce(&mut StoreContextMut<'_, T>) -> Result<()>, +{ + let _ = (args, type_idx, &types); + #[cfg(feature = "rr")] + store.0.record_event(|| { + let flat_params = types.flat_types_storage_or_pointer( + &InterfaceType::Tuple(types[type_idx].params), + MAX_FLAT_PARAMS, + ); + WasmFuncEntryEvent { + args: RRFuncArgVals::from_flat_storage(args, flat_params), + } + })?; + let result = wasm_call(store); + #[cfg(feature = "rr")] + { + 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, + ); + let result = result.map(|_| RRFuncArgVals::from_flat_iter(args, flat_results.iter32())); + store.0.record_event_validation(|| { + WasmFuncReturnEvent(ResultEvent::from_anyhow_result(&result)) + })?; + store + .0 + .next_replay_event_validation::>( + || &result, + )?; + result?; + Ok(()) + } + #[cfg(not(feature = "rr"))] + { + result + } +} + +/// Record hook operation for host function entry events +#[inline] +pub fn record_validate_host_func_entry( + args: &mut [MaybeUninit], + types: &Arc, + param_tys: &InterfaceType, + store: &mut StoreOpaque, +) -> Result<()> { + #[cfg(feature = "rr")] + store.record_event_validation(|| create_host_func_entry_event(args, types, param_tys))?; + let _ = (args, types, param_tys, store); + Ok(()) +} + +/// Replay hook operation for host function entry events +#[inline] +#[cfg(feature = "rr")] +pub fn replay_validate_host_func_entry( + args: &mut [MaybeUninit], + types: &Arc, + param_tys: &InterfaceType, + store: &mut StoreOpaque, +) -> Result<()> { + #[cfg(feature = "rr")] + store.next_replay_event_validation::(|| { + create_host_func_entry_event(args, types, param_tys) + })?; + let _ = (args, types, param_tys, store); + Ok(()) +} + +/// Record hook operation for host function return events +#[inline] +pub fn record_host_func_return( + args: &[MaybeUninit], + types: &ComponentTypes, + ty: &InterfaceType, + store: &mut StoreOpaque, +) -> Result<()> { + #[cfg(feature = "rr")] + store.record_event(|| { + let flat_results = types.flat_types_storage_or_pointer(&ty, MAX_FLAT_RESULTS); + HostFuncReturnEvent { + args: RRFuncArgVals::from_flat_storage(args, flat_results), + } + })?; + let _ = (args, types, ty, 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(feature = "rr")] + cx.store + .0 + .record_event_validation(|| LowerMemoryEntryEvent { ty, offset })?; + let store_result = lower_store(cx, ty, offset); + #[cfg(feature = "rr")] + cx.store + .0 + .record_event(|| LowerMemoryReturnEvent(ResultEvent::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(feature = "rr")] + cx.store + .0 + .record_event_validation(|| LowerFlatEntryEvent { ty })?; + let lower_result = lower(cx, ty); + #[cfg(feature = "rr")] + cx.store + .0 + .record_event(|| LowerFlatReturnEvent(ResultEvent::from_anyhow_result(&lower_result)))?; + 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( + args: &mut [MaybeUninit], + types: &Arc, + param_tys: &InterfaceType, +) -> HostFuncEntryEvent { + let flat_params = types.flat_types_storage_or_pointer(param_tys, MAX_FLAT_PARAMS); + HostFuncEntryEvent { + args: RRFuncArgVals::from_flat_storage(args, flat_params), + } +} + +/// 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 [`FixedMemorySlice`] for detailed description on the role +/// of these types. +pub struct DynamicMemorySlice<'a> { + pub bytes: &'a mut [u8], + #[cfg(feature = "rr")] + pub offset: usize, + #[cfg(feature = "rr")] + pub recorder: Option<&'a mut RecordBuffer>, +} +impl<'a> Deref for DynamicMemorySlice<'a> { + type Target = [u8]; + fn deref(&self) -> &Self::Target { + self.bytes + } +} +impl DerefMut for DynamicMemorySlice<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.bytes + } +} +impl Drop for DynamicMemorySlice<'_> { + /// Drop serves as a recording hook for stores to the memory slice + fn drop(&mut self) { + #[cfg(feature = "rr")] + 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 [`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: +/// +/// 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 +/// ([`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 +/// +/// 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 FixedMemorySlice<'a, const N: usize> { + pub bytes: &'a mut [u8; N], + #[cfg(feature = "rr")] + pub offset: usize, + #[cfg(feature = "rr")] + pub recorder: Option<&'a mut RecordBuffer>, +} +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 FixedMemorySlice<'a, N> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.bytes + } +} +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(feature = "rr")] + 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/core_hooks.rs b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs new file mode 100644 index 0000000000..1d2a26f626 --- /dev/null +++ b/crates/wasmtime/src/runtime/rr/hooks/core_hooks.rs @@ -0,0 +1,240 @@ +#[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, Replayer, ResultEvent, Validate, + common_events::HostFuncEntryEvent, common_events::HostFuncReturnEvent, + common_events::WasmFuncReturnEvent, core_events::InstantiationEvent, + core_events::WasmFuncEntryEvent, +}; +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 +/// +/// 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 { + store.0.record_event(|| { + let flat = ty.params().map(|t| t.to_wasm_type().byte_size()); + WasmFuncEntryEvent { + origin, + args: RRFuncArgVals::from_flat_iter(args, flat), + } + })?; + } + } + let result = wasm_call(store); + #[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(|| { + WasmFuncReturnEvent(ResultEvent::from_anyhow_result(&result)) + })?; + store + .0 + .next_replay_event_validation::>( + || &result, + )?; + result?; + Ok(()) + } else { + result + } + } + #[cfg(not(feature = "rr"))] + { + result + } +} + +/// Record hook operation for host function entry events +#[inline] +pub fn record_validate_host_func_entry( + args: &[T], + flat: impl Iterator, + store: &mut StoreOpaque, +) -> Result<()> +where + T: FlatBytes, +{ + let _ = (args, &flat, &store); + #[cfg(feature = "rr")] + store.record_event_validation(|| HostFuncEntryEvent { + args: RRFuncArgVals::from_flat_iter(args, flat), + })?; + Ok(()) +} + +/// Record hook operation for host function return events +#[inline] +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_iter(args, flat), + })?; + 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(feature = "rr")] + 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], + caller: &mut Caller<'_, U>, +) -> Result<()> +where + T: FlatBytes, +{ + #[cfg(feature = "rr")] + { + // 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); + + // 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 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(()) +} + +/// 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 new file mode 100644 index 0000000000..87024e2b1f --- /dev/null +++ b/crates/wasmtime/src/runtime/rr/replay_driver.rs @@ -0,0 +1,563 @@ +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 alloc::{collections::BTreeMap, sync::Arc}; +use core::mem::MaybeUninit; +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, + components: BTreeMap, + settings: ReplaySettings, +} + +impl ReplayEnvironment { + /// Construct a new [`ReplayEnvironment`] from scratch + pub fn new(engine: &Engine, settings: ReplaySettings) -> Self { + Self { + engine: engine.clone(), + modules: BTreeMap::new(), + components: BTreeMap::new(), + settings, + } + } + + /// Add a [`Module`] to the replay environment + pub fn add_module(&mut self, module: Module) -> &mut Self { + self.modules.insert(*module.checksum(), 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 + } + + 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(())) + } + + /// 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, + reader: impl ReplayReader + 'static, + 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(), + current_module_instantiation: None, + current_component_instantiation: None, + }, + ); + 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, + /// 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 { + /// 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())) + } + + /// 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`]. +/// +/// Debugger capabilities in the future will interact with this object for +/// inserting breakpoints, snapshotting, and restoring state. +/// +/// # Example +/// +/// ``` +/// 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.record(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) +/// +/// 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, + store: Store, + component_linker: component::Linker, + module_linker: crate::Linker, + 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 { + fn from_environment_and_store( + env: ReplayEnvironment, + 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); + 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)?; + } + 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, + component_linker, + module_linker, + module_instances: ModuleInstanceMap::new(), + component_instances: ComponentInstanceMap::new(), + }) + } + + /// 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 { + 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: + /// * 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. + fn run_single_top_level_event(&mut self, rr_event: RREvent) -> Result<()> { + match rr_event { + RREvent::ComponentInstantiation(event) => { + 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)?; + self.insert_component_instance(instance); + } + RREvent::ComponentWasmFuncBegin(event) => { + 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`] + 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) => { + 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) => { + 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)?; + 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 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 { + func.call_impl_do_call(&mut store, params.as_slice(), results.as_mut_slice())?; + } + } + + _ => { + log::error!("Unexpected top-level RR event: {:?}", rr_event); + Err(ReplayError::IncorrectEventVariant)? + } + } + Ok(()) + } + + /// 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<()> { + match rr_event { + RREvent::ComponentInstantiation(event) => { + 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?; + self.insert_component_instance(instance); + } + RREvent::ComponentWasmFuncBegin(event) => { + 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`] + 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, + _, + 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??; + + log::info!( + "Returned {:?} for calling {:?}", + &results_storage[..num_results], + func + ); + } + RREvent::ComponentPostReturn(event) => { + 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) => { + 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?; + 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 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)?; + 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 top-level RR event: {:?}", rr_event); + 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(()) + } + + /// 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<()> { + 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 710ff8ca68..052c346f52 100644 --- a/crates/wasmtime/src/runtime/store.rs +++ b/crates/wasmtime/src/runtime/store.rs @@ -91,6 +91,11 @@ use crate::component::concurrent; use crate::fiber; use crate::module::RegisteredModuleId; use crate::prelude::*; +#[cfg(feature = "rr")] +use crate::rr::{ + RREvent, RecordBuffer, RecordSettings, RecordWriter, Recorder, ReplayBuffer, ReplayError, + ReplayReader, ReplaySettings, Replayer, Validate, +}; #[cfg(feature = "gc")] use crate::runtime::vm::GcRootsList; #[cfg(feature = "stack-switching")] @@ -549,6 +554,18 @@ 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, } /// Self-pointer to `StoreInner` from within a `StoreOpaque` which is chiefly @@ -757,6 +774,10 @@ impl Store { executor: Executor::new(engine), #[cfg(feature = "component-model")] concurrent_state: Default::default(), + #[cfg(feature = "rr")] + record_buffer: None, + #[cfg(feature = "rr")] + replay_buffer: None, }; let mut inner = Box::new(StoreInner { inner, @@ -879,6 +900,26 @@ impl Store { } } + /// Consumes this [`Store`], destroying it and obtaining + /// the [`RecordWriter`] initialized for recording. + /// + /// 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 + // `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`]. /// @@ -1294,6 +1335,35 @@ impl Store { pub fn clear_debug_handler(&mut self) { self.inner.debug_handler = None; } + + /// Configure a [`Store`] to enable execution recording + /// + /// 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 record(&mut self, recorder: impl RecordWriter, settings: RecordSettings) -> Result<()> { + self.inner.record(recorder, settings) + } + + /// Configure a [`Store`] to enable execution replaying + /// + /// 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, + replayer: impl ReplayReader + 'static, + settings: ReplaySettings, + ) -> Result<()> { + self.inner.init_replaying(replayer, settings) + } } impl<'a, T> StoreContext<'a, T> { @@ -1804,6 +1874,147 @@ impl StoreOpaque { self.allocate_gc_store(limiter).await } + #[cfg(feature = "rr")] + 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" + ); + 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")] + 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, + replayer: impl ReplayReader + 'static, + settings: ReplaySettings, + ) -> Result<()> { + ensure!( + self.instance_count == 0, + "replaying store must not initialize any modules" + ); + ensure!( + 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(()) + } + + #[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")] + #[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 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")] + #[inline] + pub(crate) fn next_replay_event_validation( + &mut self, + 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()) + } 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 + } + } + } + + /// 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, @@ -2846,6 +3057,12 @@ impl Drop for StoreOpaque { } } } + + // Flush any remaining recording data + #[cfg(feature = "rr")] + if let Some(buf) = self.record_buffer_mut() { + buf.finish().unwrap(); + } } } diff --git a/crates/wasmtime/src/runtime/store/data.rs b/crates/wasmtime/src/runtime/store/data.rs index 71599f8794..a098bcfebe 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/values.rs b/crates/wasmtime/src/runtime/values.rs index 15aa627b8b..98c1b665e3 100644 --- a/crates/wasmtime/src/runtime/values.rs +++ b/crates/wasmtime/src/runtime/values.rs @@ -1163,9 +1163,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") }; @@ -1182,9 +1182,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.rs b/crates/wasmtime/src/runtime/vm/component.rs index 94a0889c22..c59f403591 100644 --- a/crates/wasmtime/src/runtime/vm/component.rs +++ b/crates/wasmtime/src/runtime/vm/component.rs @@ -34,7 +34,7 @@ use wasmtime_environ::{HostPtr, PrimaryMap, VMSharedTypeIndex}; const INVALID_PTR: usize = 0xdead_dead_beef_beef_u64 as usize; mod handle_table; -mod libcalls; +pub(crate) mod libcalls; mod resources; pub use self::handle_table::{HandleTable, RemovedResource}; diff --git a/crates/wasmtime/src/runtime/vm/component/libcalls.rs b/crates/wasmtime/src/runtime/vm/component/libcalls.rs index 5df911dfb6..6bae7ec379 100644 --- a/crates/wasmtime/src/runtime/vm/component/libcalls.rs +++ b/crates/wasmtime/src/runtime/vm/component/libcalls.rs @@ -11,6 +11,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; @@ -83,12 +84,15 @@ wasmtime_environ::foreach_builtin_component_function!(define_builtins); /// implementation following this submodule. mod trampolines { use super::{ComponentInstance, VMComponentContext}; + #[cfg(feature = "rr")] + use crate::rr::{Replayer, ResultEvent, component_events::*}; use core::ptr::NonNull; macro_rules! shims { ( $( $( #[cfg($attr:meta)] )? + $( #[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 )?; )* ) => ( @@ -104,7 +108,7 @@ mod trampolines { let ret = unsafe { ComponentInstance::enter_host_from_wasm(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)*) @@ -155,6 +159,52 @@ 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"))] + { + shims!(@invoke $name($store, $instance,) $($pname)*) + } + #[cfg(feature = "rr")] + { + if let Some(buf) = (*$store).replay_buffer_mut() { + 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 + (*$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())?; + retval + } + } + }); + + // 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"))] + { + shims!(@invoke $name($store, $instance,) $($pname)*) + } + #[cfg(feature = "rr")] + { + if let Some(_buf) = (*$store).replay_buffer_mut() { + // Just perform replay validation, if required + _buf.next_event_validation::(&$rr_entry{ $($pname),* }.into()).unwrap(); + } else { + // Record entry only; return is not present + (*$store).record_event_validation::(|| $rr_entry{ $($pname),* }.into()).unwrap(); + shims!(@invoke $name($store, $instance,) $($pname)*) + } + } + }); + + + } wasmtime_environ::foreach_builtin_component_function!(shims); @@ -620,7 +670,8 @@ fn resource_drop( )?)) } -struct ResourceDropRet(Option); +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +pub struct ResourceDropRet(Option); unsafe impl HostResultHasUnwindSentinel for ResourceDropRet { type Abi = u64; diff --git a/crates/wasmtime/src/runtime/vm/instance.rs b/crates/wasmtime/src/runtime/vm/instance.rs index dedf27b79e..b418525caa 100644 --- a/crates/wasmtime/src/runtime/vm/instance.rs +++ b/crates/wasmtime/src/runtime/vm/instance.rs @@ -3,6 +3,7 @@ //! `InstanceHandle` is a reference-counting handle for an `Instance`. use crate::OpaqueRootScope; +use crate::WasmFuncOrigin; use crate::prelude::*; use crate::runtime::vm::const_expr::{ConstEvalContext, ConstExprEvaluator}; use crate::runtime::vm::export::{Export, ExportMemory}; @@ -553,12 +554,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. diff --git a/crates/wasmtime/src/runtime/vm/interpreter.rs b/crates/wasmtime/src/runtime/vm/interpreter.rs index f67e088d10..42c51ae564 100644 --- a/crates/wasmtime/src/runtime/vm/interpreter.rs +++ b/crates/wasmtime/src/runtime/vm/interpreter.rs @@ -400,6 +400,7 @@ impl InterpreterRef<'_> { ( $( $( #[cfg($attr:meta)] )? + $( #[rr_builtin($($rr:tt)*)] )? $name:ident($($pname:ident: $param:ident ),* ) $(-> $result:ident)?; )* ) => { diff --git a/crates/wasmtime/src/runtime/vm/vmcontext.rs b/crates/wasmtime/src/runtime/vm/vmcontext.rs index 61cbba81c7..d6f65182d4 100644 --- a/crates/wasmtime/src/runtime/vm/vmcontext.rs +++ b/crates/wasmtime/src/runtime/vm/vmcontext.rs @@ -1484,6 +1484,12 @@ pub union ValRaw { /// /// This value is always stored in a little-endian format. exnref: u32, + + /// 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], } // The `ValRaw` type is matched as `wasmtime_val_raw_t` in the C API so these @@ -1492,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 @@ -1555,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() } } @@ -1698,6 +1706,20 @@ impl ValRaw { assert!(cfg!(feature = "gc") || exnref == 0); exnref } + + /// Get the WebAssembly value's raw bytes + #[inline] + pub const fn get_bytes(&self) -> &[u8; mem::size_of::()] { + unsafe { &self.bytes } + } + + /// Create a WebAssembly value from raw bytes + #[inline] + pub fn from_bytes(value: &[u8]) -> ValRaw { + let mut bytes = [0u8; mem::size_of::()]; + bytes[..value.len()].copy_from_slice(value); + ValRaw { bytes } + } } /// 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 bfcddfa354..4e632fbbaa 100644 --- a/src/bin/wasmtime.rs +++ b/src/bin/wasmtime.rs @@ -92,6 +92,19 @@ enum Subcommand { #[cfg(feature = "wizer")] Wizer(wasmtime_cli::commands::WizerCommand), + + /// 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(all(feature = "run", feature = "rr"))] + Replay(wasmtime_cli::commands::ReplayCommand), } impl Wasmtime { @@ -132,6 +145,9 @@ impl Wasmtime { #[cfg(feature = "wizer")] Subcommand::Wizer(c) => c.execute(), + + #[cfg(feature = "rr")] + Subcommand::Replay(c) => c.execute(), } } } diff --git a/src/commands.rs b/src/commands.rs index 625d22f0e8..3b083270dc 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -44,3 +44,8 @@ pub use self::objdump::*; mod wizer; #[cfg(feature = "wizer")] pub use self::wizer::*; + +#[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..a3492041a3 --- /dev/null +++ b/src/commands/replay.rs @@ -0,0 +1,165 @@ +//! Implementation of the `wasmtime replay` command + +use crate::commands::run::{Replaying, RunCommand}; +use crate::common::RunTarget; +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}; + +#[derive(Parser)] +/// Replay-specific options for CLI. +pub struct ReplayOptions { + /// The path of the recorded trace. + /// + /// 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. + #[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. + #[arg(short, long, default_value_t = 64)] + pub deserialize_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(mut self) -> Result<()> { + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_time() + .enable_io() + .build()?; + + runtime.block_on(async { + self.run_cmd.run.common.init_logging()?; + + let engine = self.run_cmd.new_engine(Replaying::Yes)?; + let main = self + .run_cmd + .run + .load_module(&engine, self.run_cmd.module_and_args[0].as_ref())?; + + self.run_replay(&engine, &main).await?; + Ok(()) + }) + } + + /// Execute the store with the replay settings. + /// + /// Applies similar configurations to `instantiate_and_run`. + 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 { + validate: opts.validate, + deserialize_buffer_size: opts.deserialize_buffer_size, + ..Default::default() + }; + + let mut renv = ReplayEnvironment::new(&engine, settings); + match &main { + RunTarget::Core(m) => { + renv.add_module(m.clone()); + } + #[cfg(feature = "component-model")] + RunTarget::Component(c) => { + renv.add_component(c.clone()); + } + } + + 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 + .run + .common + .wasm + .timeout + .unwrap_or(std::time::Duration::MAX); + + let result: Result, Elapsed> = tokio::time::timeout(dur, async { + replay_instance.run_to_completion_async().await + }) + .await; + + // 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}")) + }) { + Ok(_) => Ok(()), + Err(e) => { + if e.is::() { + eprintln!("Error returned from replay: {e:?}"); + 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); + } + } + } + Err(e) + } + } + } +} diff --git a/src/commands/run.rs b/src/commands/run.rs index bfe2fb99f4..6e2f080db9 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -101,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")] @@ -113,7 +183,7 @@ impl RunCommand { runtime.block_on(async { self.run.common.init_logging()?; - let engine = self.new_engine()?; + let engine = self.new_engine(Replaying::No)?; let main = self .run .load_module(&engine, self.module_and_args[0].as_ref())?; @@ -126,10 +196,12 @@ impl RunCommand { } /// Creates a new `Engine` with the configuration for this command. - pub fn new_engine(&mut self) -> Result { + pub fn new_engine(&mut self, replaying: Replaying) -> Result { let mut config = self.run.common.config(None)?; config.async_support(true); + rr_impl::config_replay(&mut config, replaying); + if self.run.common.wasm.timeout.is_some() { config.epoch_interruption(true); } @@ -207,6 +279,8 @@ impl RunCommand { store.set_fuel(fuel)?; } + rr_impl::recording_for_store(&mut store, &self.run.common.record)?; + Ok((store, linker)) } @@ -228,6 +302,7 @@ impl RunCommand { .wasm .timeout .unwrap_or(std::time::Duration::MAX); + let result = tokio::time::timeout(dur, async { let mut profiled_modules: Vec<(String, Module)> = Vec::new(); if let RunTarget::Core(m) = &main { @@ -1169,7 +1244,7 @@ impl RunCommand { pub struct Host { // Legacy wasip1 context using `wasi_common`, not set unless opted-in-to // with the CLI. - legacy_p1_ctx: Option, + pub(crate) legacy_p1_ctx: Option, // Context for both WASIp1 and WASIp2 (and beyond) for the `wasmtime_wasi` // crate. This has both `wasmtime_wasi::WasiCtx` as well as a @@ -1178,7 +1253,7 @@ pub struct Host { // The Mutex is only needed to satisfy the Sync constraint but we never // actually perform any locking on it as we use Mutex::get_mut for every // access. - wasip1_ctx: Option>>, + pub(crate) wasip1_ctx: Option>>, #[cfg(feature = "wasi-nn")] wasi_nn_wit: Option>, 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 0664ee314b..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()?; + let engine = run.new_engine(Replaying::No)?; // Instrument the input wasm with wizer. let (cx, main) = if is_component { 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..cc3c63997a --- /dev/null +++ b/tests/all/rr.rs @@ -0,0 +1,1586 @@ +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 = "component-model")] +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, bool) -> 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 [true, false] { + 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, bool) -> 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, is_async)?; + + 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.record(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 = "component-model")] +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 [true, false] { + 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 = "component-model")] +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.record(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] +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() { + 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 = "component-model")] +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 = "component-model")] +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 = "component-model")] +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 = "component-model")] +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 = "component-model")] +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 = "component-model")] +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 = "component-model")] +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 = "component-model")] +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 = "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#" + (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 = "component-model")] +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 = "component-model")] +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))) + "# + ) +}