diff --git a/crates/cranelift/src/func_environ.rs b/crates/cranelift/src/func_environ.rs index c4495b09c1b8..36548395bc92 100644 --- a/crates/cranelift/src/func_environ.rs +++ b/crates/cranelift/src/func_environ.rs @@ -33,8 +33,9 @@ use wasmtime_environ::{ EngineOrModuleTypeIndex, FrameStateSlotBuilder, FrameValType, FuncIndex, FuncKey, GlobalConstValue, GlobalIndex, IndexType, Memory, MemoryIndex, MemoryTunables, Module, ModuleInternedTypeIndex, ModuleTranslation, ModuleTypesBuilder, PtrSize, Table, TableIndex, - TagIndex, Tunables, TypeConvert, TypeIndex, VMOffsets, WasmCompositeInnerType, WasmFuncType, - WasmHeapTopType, WasmHeapType, WasmRefType, WasmResult, WasmStorageType, WasmValType, + TableInitialValue, TagIndex, Tunables, TypeConvert, TypeIndex, VMOffsets, + WasmCompositeInnerType, WasmFuncType, WasmHeapTopType, WasmHeapType, WasmRefType, WasmResult, + WasmStorageType, WasmValType, }; use wasmtime_environ::{FUNCREF_INIT_BIT, FUNCREF_MASK}; @@ -1656,20 +1657,26 @@ impl FuncEnvironment<'_> { self.reference_type(table.ref_type.heap_type).0.bytes() }; + // A table is fixed-size if min == max or if translation proved it + // is never mutated. Either way, the base address and bound are + // constant for the instance's lifetime, so `base_gv` can be + // `readonly + can_move` and the bound check becomes a static + // compare against `table.limits.min`. + let immutable = !self.translation.module.tables_mutated[index]; + let fixed_size = immutable || Some(table.limits.min) == table.limits.max; + let base_gv = func.create_global_value(ir::GlobalValueData::Load { base: ptr, offset: Offset32::new(base_offset), global_type: pointer_type, - flags: if Some(table.limits.min) == table.limits.max { - // A fixed-size table can't be resized so its base address won't - // change. + flags: if fixed_size { MemFlagsData::trusted().with_readonly().with_can_move() } else { MemFlagsData::trusted() }, }); - let bound = if Some(table.limits.min) == table.limits.max { + let bound = if fixed_size { TableSize::Static { bound: table.limits.min, } @@ -1941,6 +1948,14 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> { callee: ir::Value, call_args: &[ir::Value], ) -> WasmResult> { + // Fast path: if we can statically resolve this indirect call to a + // single defined function (immutable funcref table + constant + // callee index + matching signature), emit a direct call instead. + // See `try_static_resolve_indirect_call`. + if let Some(target) = self.try_static_resolve_indirect_call(table_index, ty_index, callee) { + return self.direct_call(target, sig_ref, call_args).map(Some); + } + let (code_ptr, callee_vmctx) = match self.check_and_load_code_and_callee_vmctx( features, table_index, @@ -1956,6 +1971,163 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> { .map(Some) } + /// Try to statically resolve a `call_indirect` to a direct call. + /// Succeeds when (1) the table is immutable, (2) the callee index is an + /// `iconst`, (3) the slot is precomputable from a static `elem` + /// segment, and (4) the resolved function's signature matches the + /// site's declared `ty_index`. + fn try_static_resolve_indirect_call( + &self, + table_index: TableIndex, + ty_index: TypeIndex, + callee: ir::Value, + ) -> Option { + let translation = self.env.translation; + let module = &translation.module; + + // (1) Table must be provably immutable. Imports are pre-marked + // mutated by `ModuleEnvironment::translate`. + if translation.module.tables_mutated[table_index] { + return None; + } + let defined_table = module.defined_table_index(table_index)?; + + // (2) Callee must be a constant `iconst`. + let dfg = &self.builder.func.dfg; + let inst = dfg.value_def(callee).inst()?; + let imm = match dfg.insts[inst] { + ir::InstructionData::UnaryImm { + opcode: ir::Opcode::Iconst, + imm, + } => imm, + _ => return None, + }; + let callee_ty = dfg.value_type(callee); + let callee_idx_u64 = imm + .zero_extend_from_width(callee_ty.bits()) + .bits() + .cast_unsigned(); + + // (3) Slot must be precomputable. + let init = module + .table_initialization + .initial_values + .get(defined_table)?; + let precomputed = match init { + TableInitialValue::Null { precomputed } => precomputed, + // A fully-expression-driven initializer can't be resolved at + // compile time. Bail. + TableInitialValue::Expr(_) => return None, + }; + let slot = usize::try_from(callee_idx_u64).ok()?; + if slot >= precomputed.len() { + return None; + } + let target = precomputed[slot]; + // `FuncIndex::reserved_value()` is the "no entry" sentinel — + // this slot wasn't covered by any static `elem` segment. + if target.is_reserved_value() { + return None; + } + + // (4) Signature match. The site's declared `ty_index` and the + // target function's declared signature must intern to the same + // module type index. + let expected_ty = module.types[ty_index].unwrap_module_type_index(); + let target_ty = module.functions[target] + .signature + .unwrap_module_type_index(); + if expected_ty != target_ty { + return None; + } + + Some(target) + } + + /// True iff `table_index`'s precomputed `elem` contents cover every + /// in-bounds slot with a concrete `FuncIndex`. Caller must have proven + /// the table is immutable; when this is true, the funcref-NULL check + /// on the call_indirect hot path is provably redundant (bounds checks + /// still emit and trap on OOB). + fn precomputed_table_has_no_null_slots(&self, table_index: TableIndex) -> bool { + let module = &self.env.translation.module; + let Some(defined_table) = module.defined_table_index(table_index) else { + return false; + }; + let Some(init) = module + .table_initialization + .initial_values + .get(defined_table) + else { + return false; + }; + let precomputed = match init { + TableInitialValue::Null { precomputed } => precomputed, + TableInitialValue::Expr(_) => return false, + }; + if precomputed.is_empty() { + return false; + } + // Slots beyond `precomputed.len()` are null at runtime. Coverage + // up to `limits.min` is required (caller proved immutable, so the + // table can't be grown beyond min). + let table_min = module.tables[table_index].limits.min; + if (precomputed.len() as u64) < table_min { + return false; + } + precomputed.iter().all(|f| !f.is_reserved_value()) + } + + fn try_elide_sig_check_for_immutable_table( + &self, + table_index: TableIndex, + ty_index: TypeIndex, + ) -> bool { + let translation = self.env.translation; + let module = &translation.module; + + if translation.module.tables_mutated[table_index] { + return false; + } + let defined_table = match module.defined_table_index(table_index) { + Some(d) => d, + None => return false, + }; + + let init = match module + .table_initialization + .initial_values + .get(defined_table) + { + Some(i) => i, + None => return false, + }; + let precomputed = match init { + TableInitialValue::Null { precomputed } => precomputed, + TableInitialValue::Expr(_) => return false, + }; + + if precomputed.is_empty() { + return false; + } + + let expected_ty = module.types[ty_index].unwrap_module_type_index(); + for &func_idx in precomputed.iter() { + // Null slots will trap on the funcref-NULL load anyway. + if func_idx.is_reserved_value() { + continue; + } + let actual_ty = module.functions[func_idx] + .signature + .unwrap_module_type_index(); + if actual_ty != expected_ty { + return false; + } + } + + true + } + fn check_and_load_code_and_callee_vmctx( &mut self, features: &WasmFeatures, @@ -2018,6 +2190,20 @@ impl<'a, 'func, 'module_env> Call<'a, 'func, 'module_env> { // table of typed functions and that type matches `ty_index`, then // there's no need to perform a typecheck. match table.ref_type.heap_type { + // Untyped `funcref` tables normally need a runtime sig check. + // Elide it when the table is immutable and every precomputed + // slot has the same signature as the call site's `ty_index`. + WasmHeapType::Func + if self.try_elide_sig_check_for_immutable_table(table_index, ty_index) => + { + // If every slot is also non-null, drop `may_be_null` so + // the funcref-NULL check is elided too. Sound only under + // the immutability check above. + let may_be_null = table.ref_type.nullable + && !self.precomputed_table_has_no_null_slots(table_index); + return CheckIndirectCallTypeSignature::StaticMatch { may_be_null }; + } + // Functions do not have a statically known type in the table, a // typecheck is required. Fall through to below to perform the // actual typecheck. diff --git a/crates/environ/src/compile/module_environ.rs b/crates/environ/src/compile/module_environ.rs index d06ece9d94a3..edd15801415c 100644 --- a/crates/environ/src/compile/module_environ.rs +++ b/crates/environ/src/compile/module_environ.rs @@ -247,6 +247,17 @@ impl<'a, 'data> ModuleEnvironment<'a, 'data> { self.translate_payload(payload?)?; } + // Precompute static funcref-table contents from `elem` segments + // before Cranelift lowering, so the `call_indirect` optimizations + // in `func_environ.rs` see the populated `precomputed` lists. + // Must run before `analyze_table_mutability` so that pass can + // mark tables with leftover (unfoldable) segments as mutated. + if self.tunables.table_lazy_init { + self.result.try_func_table_init(); + } + + analyze_table_mutability(&mut self.result)?; + Ok(self.result) } @@ -1358,3 +1369,67 @@ impl ModuleTranslation<'_> { self.module.table_initialization.segments = segments.try_collect().panic_on_oom(); } } + +/// Populate `translation.module.tables_mutated`: a table is marked mutated +/// if it's imported, exported, the destination of any `table.set` / +/// `table.fill` / `table.copy` / `table.grow` / `table.init` opcode in any +/// defined function body, or the target of an active `elem` segment that +/// wasn't folded into `initial_values[t].precomputed` by +/// `try_func_table_init` (`try_func_table_init` must have already run). +fn analyze_table_mutability<'data>(translation: &mut ModuleTranslation<'data>) -> Result<()> { + let num_tables = translation.module.tables.len(); + if num_tables == 0 { + return Ok(()); + } + + // Imports: caller can mutate via wasmtime API. Exports: host can + // mutate the same way. Both are conservatively non-stable. + let num_imported = translation.module.num_imported_tables; + for i in 0..num_imported { + translation.module.tables_mutated[TableIndex::from_u32(i as u32)] = true; + } + for (_, entity_index) in &translation.module.exports { + if let EntityIndex::Table(table_index) = entity_index { + translation.module.tables_mutated[*table_index] = true; + } + } + + // Walk function bodies for mutation opcodes. O(total opcodes). + for (_, body_data) in &translation.function_body_inputs { + let mut reader = body_data.body.get_operators_reader()?; + while !reader.eof() { + use wasmparser::Operator; + match reader.read()? { + Operator::TableSet { table } + | Operator::TableFill { table } + | Operator::TableGrow { table } => { + translation.module.tables_mutated[TableIndex::from_u32(table)] = true; + } + Operator::TableCopy { + dst_table, + src_table: _, + } => { + // `src_table` is read-only in `table.copy`; only the + // destination is mutated. + translation.module.tables_mutated[TableIndex::from_u32(dst_table)] = true; + } + Operator::TableInit { + table, + elem_index: _, + } => { + translation.module.tables_mutated[TableIndex::from_u32(table)] = true; + } + _ => {} + } + } + } + + // Active `elem` segments left in the segments list (not folded into + // `precomputed` by `try_func_table_init`) run at instantiation and + // can overwrite slots arbitrarily. + for segment in translation.module.table_initialization.segments.iter() { + translation.module.tables_mutated[segment.table_index] = true; + } + + Ok(()) +} diff --git a/crates/environ/src/module.rs b/crates/environ/src/module.rs index 29dc0d298e3c..d3f05b7d96c7 100644 --- a/crates/environ/src/module.rs +++ b/crates/environ/src/module.rs @@ -3,7 +3,7 @@ use crate::prelude::*; use crate::*; use core::ops::Range; -use cranelift_entity::{EntityRef, packed_option::ReservedValue}; +use cranelift_entity::{EntityRef, SecondaryMap, packed_option::ReservedValue}; use serde_derive::{Deserialize, Serialize}; /// A WebAssembly linear memory initializer. @@ -362,6 +362,13 @@ pub struct Module { /// WebAssembly tables. pub tables: TryPrimaryMap, + /// Per-table flag set during translation: `true` iff the table is + /// imported, exported, or any opcode in the module mutates it + /// (`table.set` / `table.fill` / `table.copy[dst]` / `table.grow` / + /// `table.init`). Read by Cranelift to gate `call_indirect` + /// optimizations and by the runtime to decide eager-init. + pub tables_mutated: SecondaryMap, + /// WebAssembly linear memory plans. pub memories: TryPrimaryMap, @@ -415,6 +422,7 @@ impl Module { num_escaped_funcs: Default::default(), functions: Default::default(), tables: Default::default(), + tables_mutated: Default::default(), memories: Default::default(), globals: Default::default(), global_initializers: Default::default(), @@ -675,6 +683,39 @@ impl Module { EntityIndex::Tag(i) => self.tags.is_valid(i), } } + + /// True iff `table_index` is a funcref table with stable, non-null + /// contents for the lifetime of any instance: immutable + /// (`tables_mutated == false`), has a `TableInitialValue::Null` + /// precomputed image covering at least `limits.min` slots, and every + /// slot is a concrete `FuncIndex`. + /// + /// Used by the runtime to decide whether to eagerly populate the + /// table from `precomputed` at instance creation, and by Cranelift to + /// elide the lazy-init `brif` on `call_indirect` through this table. + pub fn is_eagerly_initialized_funcref_table(&self, table_index: TableIndex) -> bool { + if self.tables_mutated[table_index] { + return false; + } + let Some(defined_table) = self.defined_table_index(table_index) else { + return false; + }; + let Some(init) = self.table_initialization.initial_values.get(defined_table) else { + return false; + }; + let precomputed = match init { + TableInitialValue::Null { precomputed } => precomputed, + TableInitialValue::Expr(_) => return false, + }; + if precomputed.is_empty() { + return false; + } + let table_min = self.tables[table_index].limits.min; + if (precomputed.len() as u64) < table_min { + return false; + } + precomputed.iter().all(|f| !f.is_reserved_value()) + } } impl TypeTrace for Module { @@ -705,6 +746,7 @@ impl TypeTrace for Module { needs_gc_heap: _, functions, tables, + tables_mutated: _, memories: _, globals, global_initializers: _, @@ -756,6 +798,7 @@ impl TypeTrace for Module { needs_gc_heap: _, functions, tables, + tables_mutated: _, memories: _, globals, global_initializers: _, diff --git a/crates/environ/tests/table_mutability.rs b/crates/environ/tests/table_mutability.rs new file mode 100644 index 000000000000..800a6ec9c11b --- /dev/null +++ b/crates/environ/tests/table_mutability.rs @@ -0,0 +1,436 @@ +//! Integration tests for `analyze_table_mutability` and the surrounding +//! precompute ordering invariants. +//! +//! The per-table mutability bit is the foundation of the `call_indirect` +//! optimizations in `crates/cranelift/src/func_environ.rs` +//! (constant-index direct call, sig-check elision, NULL elision, bound- +//! load elision). A false negative here — failing to mark a table as +//! mutated when it actually is — would silently turn correct calls into +//! incorrect direct calls or skip required runtime checks. A false +//! positive — marking an immutable table as mutated — is merely a missed +//! optimization. Pin the analysis behaviour with focused module-level +//! tests so any regression surfaces immediately, not after a downstream +//! optimization fires on a now-invalid premise. +//! +//! Test scenario inspiration drawn from comparable bugs in peer +//! interpreters that have shipped fixes for analogous IC-invalidation +//! mistakes: +//! +//! - **Luau** (`LOP_NAMECALL`): inline cache had to be invalidated on +//! `table.insert` / metatable change. Analogous wasm risk: `table.grow` +//! not invalidating an immutability proof, so see `table_grow_marks…`. +//! - **JavaScriptCore** (`ic_table`): inline-cache corruption from missed +//! shape transitions. Analogous risk: over-marking, e.g. `table.copy` +//! wrongly marking the SOURCE table as mutated would forbid downstream +//! optimizations on a perfectly read-only table. See +//! `table_copy_marks_destination_only_not_source`. +//! - **Hermes** (`HiddenClass` cache): property cache misses with +//! `Object.defineProperty`. Analogous risk: `table.init` (active- +//! segment init at runtime) being treated as a no-op rather than a +//! write. See `table_init_marks_destination`. +//! +//! Lives in `tests/` rather than as a `#[cfg(test)] mod` inside +//! `module_environ.rs` because the latter triggers a pre-existing +//! upstream compile failure in `key.rs` / `module_artifacts.rs` (their +//! `arbitrary::Arbitrary` derives are stale relative to the workspace's +//! pinned `arbitrary 1.4.2`). Integration tests build against the lib +//! as a normal dependency and so do not set `cfg(test)` on +//! `wasmtime-environ` itself. + +use wasmparser::{Parser, Validator, WasmFeatures}; +use wasmtime_environ::{ + ModuleEnvironment, ModuleTypesBuilder, StaticModuleIndex, TableIndex, Tunables, +}; + +/// Translate `wat` and return the resulting `tables_mutated` bits, in +/// table-index order. Helper to keep individual tests short. +fn translate_and_get_mutability(wat: &str) -> Vec { + let bytes = wat::parse_str(wat).expect("WAT parse failed"); + let tunables = Tunables::default_host(); + // WASM2 covers reference-types + bulk-memory, which is what every + // table-mutating opcode below needs (`table.set`, `table.fill`, + // `table.grow`, `table.copy`, `table.init`, `elem.drop`). + let features = WasmFeatures::WASM2; + let mut validator = Validator::new_with_features(features); + let mut types = ModuleTypesBuilder::new(&validator); + let env = ModuleEnvironment::new( + &tunables, + &mut validator, + &mut types, + StaticModuleIndex::from_u32(0), + ); + let parser = Parser::new(0); + let translation = env.translate(parser, &bytes).expect("translate failed"); + let n: u32 = translation.module.tables.len().try_into().unwrap(); + (0..n) + .map(|i| translation.module.tables_mutated[TableIndex::from_u32(i)]) + .collect() +} + +/// A table only used as the source of `call_indirect` and `table.get` is +/// provably immutable. (Both ops READ the table; neither writes it.) The +/// table is intentionally NOT exported — exported tables are +/// conservatively pre-marked as mutated (see +/// `exported_tables_are_pre_marked` for the export case) since the host +/// can mutate them via the public wasmtime API. +#[test] +fn read_only_table_is_immutable() { + let bits = translate_and_get_mutability( + r#" + (module + (table 4 funcref) + (func $f (result i32) i32.const 42) + (elem (i32.const 0) $f $f $f $f) + (func (export "call_zero") (result i32) + i32.const 0 + call_indirect (param) (result i32)) + (func (export "read_zero") (result funcref) + i32.const 0 + table.get 0)) + "#, + ); + assert_eq!(bits, vec![false], "no opcode mutated this table"); +} + +/// Exported tables are always pre-marked as mutated, regardless of +/// whether any opcode in this module touches them. The host can call +/// `Table::set` / `Table::grow` via the public wasmtime API on any +/// exported table, and another module that imports the export can also +/// mutate it. Without this rule, downstream optimizations would +/// happily elide null traps and sig checks on exported tables on the +/// (false) assumption that the table contents are stable. +#[test] +fn exported_tables_are_pre_marked() { + let bits = translate_and_get_mutability( + r#" + (module + (table (export "t") 4 funcref) + (func $f (result i32) i32.const 42) + (elem (i32.const 0) $f $f $f $f)) + "#, + ); + assert_eq!(bits, vec![true]); +} + +/// `table.set` marks its destination as mutated. +#[test] +fn table_set_marks_destination() { + let bits = translate_and_get_mutability( + r#" + (module + (table 4 funcref) + (func $f (result i32) i32.const 0) + (func (export "do_set") + i32.const 1 + ref.func $f + table.set 0)) + "#, + ); + assert_eq!(bits, vec![true]); +} + +/// `table.fill` marks its destination as mutated. +#[test] +fn table_fill_marks_destination() { + let bits = translate_and_get_mutability( + r#" + (module + (table 4 funcref) + (func $f (result i32) i32.const 0) + (func (export "do_fill") + i32.const 0 + ref.func $f + i32.const 4 + table.fill 0)) + "#, + ); + assert_eq!(bits, vec![true]); +} + +/// `table.grow` is treated as mutating — analogous to Luau's NAMECALL IC +/// needing to invalidate on table-shape change. +#[test] +fn table_grow_marks_destination() { + let bits = translate_and_get_mutability( + r#" + (module + (table 4 funcref) + (func (export "do_grow") (result i32) + ref.null func + i32.const 1 + table.grow 0)) + "#, + ); + assert_eq!(bits, vec![true]); +} + +/// `table.copy` marks the DESTINATION but explicitly NOT the source. The +/// source is read-only (its contents aren't changed by the op); marking +/// it as mutated would forbid downstream optimizations from treating it +/// as immutable, which would be incorrect over-conservatism — the JSC +/// `ic_table` analogue. +#[test] +fn table_copy_marks_destination_only_not_source() { + let bits = translate_and_get_mutability( + r#" + (module + (table $dst (export "dst") 4 funcref) + (table $src 4 funcref) + (func $f (result i32) i32.const 0) + (elem (table $src) (i32.const 0) func $f $f $f $f) + (func (export "do_copy") + i32.const 0 ;; dst offset + i32.const 0 ;; src offset + i32.const 4 ;; len + table.copy $dst $src)) + "#, + ); + assert_eq!( + bits, + vec![true, false], + "dst should be mutated, src should remain immutable", + ); +} + +/// `table.init` writes to the destination table from a passive elem +/// segment, so it is treated as mutation (the destination's contents +/// change at runtime). +#[test] +fn table_init_marks_destination() { + let bits = translate_and_get_mutability( + r#" + (module + (table 4 funcref) + (func $f (result i32) i32.const 0) + (elem $e funcref (ref.func $f) (ref.func $f)) + (func (export "do_init") + i32.const 0 ;; dst + i32.const 0 ;; src offset within elem + i32.const 2 ;; len + table.init 0 $e)) + "#, + ); + assert_eq!(bits, vec![true]); +} + +/// `elem.drop` drops a passive element segment but does NOT write to any +/// table — distinct from `table.init` which DOES write. A pessimistic +/// implementation that marked all tables as mutated on `elem.drop` would +/// hand out false positives and shut off optimizations on perfectly- +/// immutable tables. +#[test] +fn elem_drop_does_not_mark_tables() { + let bits = translate_and_get_mutability( + r#" + (module + (table 4 funcref) + (func $f (result i32) i32.const 0) + (elem $e funcref (ref.func $f)) + (func (export "do_drop") + elem.drop $e)) + "#, + ); + assert_eq!(bits, vec![false]); +} + +/// Imported tables are always pre-marked as mutated, regardless of +/// whether any opcode in this module touches them. The importer can +/// mutate the table in ways this module can't see. +#[test] +fn imported_tables_are_pre_marked() { + let bits = translate_and_get_mutability( + r#" + (module + (import "host" "t" (table 4 funcref))) + "#, + ); + assert_eq!(bits, vec![true]); +} + +/// A mutation in ONE function correctly marks the table — the analysis +/// has to walk every function body, not just the first. +#[test] +fn mutation_in_any_function_counts() { + let bits = translate_and_get_mutability( + r#" + (module + (table 4 funcref) + (func $f (result i32) i32.const 0) + (func (export "innocent") (result i32) + i32.const 0 + call_indirect (param) (result i32)) + (func (export "guilty") + i32.const 0 + ref.func $f + table.set 0)) + "#, + ); + assert_eq!(bits, vec![true]); +} + +/// Two tables, one mutated, one not. The analysis tracks per-table — a +/// mutation on one must not leak to the other. +#[test] +fn mutation_isolated_to_target_table() { + let bits = translate_and_get_mutability( + r#" + (module + (table $a 4 funcref) + (table $b 4 funcref) + (func $f (result i32) i32.const 0) + (func (export "mut_a") + i32.const 0 + ref.func $f + table.set $a)) + "#, + ); + assert_eq!( + bits, + vec![true, false], + "$a should be mutated, $b should remain immutable", + ); +} + +/// Translating without any tables at all must not panic. (Defensive: the +/// analysis indexes a `SecondaryMap` keyed by `TableIndex`, and we want +/// to confirm an empty module produces an empty result rather than e.g. +/// a default-allocated single entry.) +#[test] +fn module_with_no_tables_produces_empty_mutability_vec() { + let bits = translate_and_get_mutability( + r#" + (module + (func (export "noop"))) + "#, + ); + assert!(bits.is_empty(), "no tables ⇒ no mutability bits"); +} + +// ----------------------------------------------------------------------------- +// Leftover active elem segments — soundness gate for the call_indirect +// elisions in `crates/cranelift/src/func_environ.rs`. +// +// `try_func_table_init` folds *some* active `elem` segments into +// `table_initialization.initial_values[t].precomputed` at compile time +// and drains them from `table_initialization.segments`. Any segment it +// can't fold (dynamic offset, `Expressions` form, out-of-range, ...) +// stays in the `segments` list and runs at instantiation time *after* +// the precomputed image is applied — potentially overwriting slots +// that downstream optimizations have read from `precomputed` and +// assumed stable. Pin the analyzer to mark every such target as +// mutated so the elisions correctly bail out. Caught by review on +// PR #2 (`https://github.com/rebeckerspecialties/wasmtime/pull/2# +// discussion_r3193374159` and `…#discussion_r3193374164`). +// ----------------------------------------------------------------------------- + +/// A second elem segment whose offset is `(global.get $g)` (an imported +/// global, resolved at instantiation time) cannot be folded into +/// `precomputed` — `try_func_table_init` only folds segments with a +/// constant `i32.const`/`i64.const` offset. So the segment stays in +/// `table_initialization.segments` and overwrites slots at instance +/// time. Without the leftover-segment pass in `analyze_table_mutability`, +/// the table would be marked immutable and the type-confusion soundness +/// bug fires (a function of a different signature could end up at slot +/// 0 of an "immutable" table, defeating +/// `try_elide_sig_check_for_immutable_table`). +#[test] +fn dynamic_offset_leftover_segment_marks_table_mutated() { + let bits = translate_and_get_mutability( + r#" + (module + (import "" "g" (global $g i32)) + (table 4 4 funcref) + (func $f (result i32) i32.const 42) + (elem (i32.const 0) func $f) + (elem (offset (global.get $g)) func $f)) + "#, + ); + assert_eq!( + bits, + vec![true], + "leftover segment with dynamic offset must mark its target table mutated" + ); +} + +/// A segment in `Expressions` form (`funcref (item ref.func ...)`) +/// rather than `Functions` form is also rejected by +/// `try_func_table_init` and stays in `segments`. Same soundness +/// argument as the dynamic-offset case: the segment's evaluation +/// happens at instantiation time and can produce arbitrary funcrefs +/// (including null via `ref.null func`), which would invalidate any +/// elision proof that read from `precomputed`. +#[test] +fn expressions_form_leftover_segment_marks_table_mutated() { + let bits = translate_and_get_mutability( + r#" + (module + (table 4 4 funcref) + (func $f (result i32) i32.const 42) + (elem (i32.const 0) funcref (item ref.func $f) (item ref.null func))) + "#, + ); + assert_eq!( + bits, + vec![true], + "Expressions-form leftover segment must mark its target table mutated" + ); +} + +/// `try_func_table_init` short-circuits the *whole* segment-folding +/// loop on the first segment it can't fold — including any later +/// segments that target a different table. This preserves wasm's +/// trap-ordering semantics (the failing segment might trap, in which +/// case later segments shouldn't have been applied either). The +/// upshot: a single dynamic-offset segment can leave many leftover +/// segments behind. Verify the analyzer marks every targeted table, +/// not just the one whose segment broke the loop. +#[test] +fn leftover_segments_after_short_circuit_mark_all_targets() { + let bits = translate_and_get_mutability( + r#" + (module + (import "" "g" (global $g i32)) + (table $t0 4 4 funcref) + (table $t1 4 4 funcref) + (func $f (result i32) i32.const 42) + ;; First segment for t0 has dynamic offset → breaks the + ;; folding loop → both this segment AND the later t1 + ;; segment stay in `table_initialization.segments`. + (elem (table $t0) (offset (global.get $g)) func $f) + (elem (table $t1) (i32.const 0) func $f)) + "#, + ); + assert_eq!( + bits, + vec![true, true], + "both targets of leftover segments must be marked mutated" + ); +} + +/// Independence sanity check: a leftover segment for table 0 must NOT +/// mark a separate table 1 that has only foldable segments. Mirrors +/// the `mutation_isolated_to_target_table` test for runtime opcodes. +#[test] +fn leftover_segment_marks_only_its_target_table() { + let bits = translate_and_get_mutability( + r#" + (module + (import "" "g" (global $g i32)) + (table $t0 4 4 funcref) + (table $t1 4 4 funcref) + (func $f (result i32) i32.const 42) + ;; Foldable segment for t1 — applied first, before any + ;; segment for t0 is reached. (Wasm specifies segments are + ;; processed in order, but `try_func_table_init` walks them + ;; in order too, so applying t1 first matches.) + (elem (table $t1) (i32.const 0) func $f $f $f $f) + ;; Dynamic-offset (leftover) segment for t0. + (elem (table $t0) (offset (global.get $g)) func $f)) + "#, + ); + assert_eq!( + bits, + vec![true, false], + "leftover-segment marking must not bleed into other tables" + ); +} diff --git a/crates/wasmtime/src/runtime/vm/instance/allocator.rs b/crates/wasmtime/src/runtime/vm/instance/allocator.rs index e2f6ae082455..f4afbffbdddb 100644 --- a/crates/wasmtime/src/runtime/vm/instance/allocator.rs +++ b/crates/wasmtime/src/runtime/vm/instance/allocator.rs @@ -15,7 +15,7 @@ use core::{mem, ptr}; use wasmtime_environ::{ DefinedMemoryIndex, DefinedTableIndex, EntityRef, HostPtr, InitMemory, MemoryInitialization, MemoryInitializer, MemoryKind, Module, SizeOverflow, TableInitialValue, TableSegmentElements, - Trap, VMOffsets, WasmRefType, + Trap, VMOffsets, WasmRefType, packed_option::ReservedValue, }; #[cfg(feature = "gc")] @@ -535,16 +535,39 @@ async fn initialize_tables( module: &Module, ) -> Result<()> { let mut store = OpaqueRootScope::new(store); - for (table, init) in module.table_initialization.initial_values.iter() { + for (defined_table, init) in module.table_initialization.initial_values.iter() { match init { - // Tables are always initially null-initialized at this time - TableInitialValue::Null { precomputed: _ } => {} + // Tables are normally null-initialized here and populated + // lazily by `Instance::get_table_with_lazy_init`. For tables + // covered by `is_eagerly_initialized_funcref_table`, the + // Cranelift codegen has elided the lazy-init brif on + // `call_indirect`, so we must populate eagerly for soundness. + TableInitialValue::Null { precomputed } => { + let table_index = module.table_index(defined_table); + if !module.is_eagerly_initialized_funcref_table(table_index) { + continue; + } + let (mut instance, registry) = + store.instance_and_module_registry_mut(context.instance); + for (i, func_idx) in precomputed.iter().enumerate() { + debug_assert!(!func_idx.is_reserved_value()); + let func_ref = instance.as_mut().get_func_ref(registry, *func_idx); + let result = instance + .as_mut() + .get_defined_table(defined_table) + .set_func(i as u64, func_ref); + if let Err(trap) = result { + // OOB here implies the predicate is wrong. + return Err(trap.into()); + } + } + } TableInitialValue::Expr(expr) => { let init = const_evaluator .eval(&mut store, limiter.as_deref_mut(), context, expr) .await?; - let idx = module.table_index(table); + let idx = module.table_index(defined_table); let id = store.id(); let table = store .instance_mut(context.instance) diff --git a/tests/all/leftover_elem_segment_soundness.rs b/tests/all/leftover_elem_segment_soundness.rs new file mode 100644 index 000000000000..02d2a6363ac7 --- /dev/null +++ b/tests/all/leftover_elem_segment_soundness.rs @@ -0,0 +1,228 @@ +//! Regression tests for the leftover-active-`elem`-segment soundness +//! gate on the `call_indirect` elisions added in commits 2-4 of the +//! per-table-mutability stack. +//! +//! Originally caught by an automated reviewer on PR #2 of the +//! rebeckerspecialties/wasmtime fork +//! (`https://github.com/rebeckerspecialties/wasmtime/pull/2# +//! discussion_r3193374159` and `…#discussion_r3193374164`). The bug +//! shape is: +//! +//! - `try_static_resolve_indirect_call`, +//! `try_elide_sig_check_for_immutable_table`, and +//! `precomputed_table_has_no_null_slots` read from +//! `module.table_initialization.initial_values[t].precomputed`. +//! - `try_func_table_init` only folds *some* active `elem` segments +//! into `precomputed`. Anything with a dynamic offset, an +//! `Expressions`-form payload, an out-of-range top, etc. stays in +//! `module.table_initialization.segments` and runs at instantiation +//! time *after* the precomputed image is applied. +//! - Without the soundness gate, the elisions trust `precomputed` but +//! a leftover segment can overwrite slots with a different funcref, +//! a function of a different signature, or a null. That defeats the +//! sig-check, the null-check, and (for constant-index calls) the +//! direct-call rewrite. +//! +//! The fix: `analyze_table_mutability` now also marks any table +//! targeted by a leftover segment as mutated, so the predicates above +//! correctly bail out. These tests instantiate the witness modules +//! end-to-end and assert that the runtime catches the segment-driven +//! mismatch — direct evidence the elisions did NOT fire. + +use wasmtime::*; + +/// Type-confusion witness: precomputed says slot 0 is a `() -> i32` +/// function, but a leftover segment with a dynamic offset (resolved +/// at instantiate-time from an imported global) overwrites slot 0 +/// with a `() -> f64` function. The runtime sig check must fire and +/// trap the call_indirect; if the sig-check elision had erroneously +/// fired, the call would silently dispatch into `$f_f64` and pop an +/// `i32` off a wrong-shape return. +#[test] +#[cfg_attr(miri, ignore)] +fn leftover_segment_dynamic_offset_keeps_sig_check() -> Result<()> { + let wat = r#" + (module + (import "" "g" (global $g i32)) + (type $sig_i32 (func (result i32))) + (type $sig_f64 (func (result f64))) + (table 4 4 funcref) + (func $f_i32 (type $sig_i32) i32.const 42) + (func $f_f64 (type $sig_f64) f64.const 3.14) + + ;; Foldable: precomputed[0] = $f_i32 (sig_i32). + (elem (i32.const 0) func $f_i32) + + ;; Leftover (dynamic offset): runs at instantiation, can + ;; overwrite precomputed[0] with $f_f64 (sig_f64). + (elem (offset (global.get $g)) func $f_f64) + + (func (export "call_at_zero") (result i32) + i32.const 0 + call_indirect (type $sig_i32))) + "#; + + let engine = Engine::default(); + let module = Module::new(&engine, wat)?; + let mut store = Store::new(&engine, ()); + + // Resolve $g to 0 so the leftover segment overwrites slot 0 with + // the wrong-signature function. + let g = Global::new( + &mut store, + GlobalType::new(ValType::I32, Mutability::Const), + Val::I32(0), + )?; + let instance = Instance::new(&mut store, &module, &[g.into()])?; + + let call_at_zero = instance.get_typed_func::<(), i32>(&mut store, "call_at_zero")?; + let err = call_at_zero + .call(&mut store, ()) + .expect_err("call_indirect with sig mismatch must trap"); + + let trap = err + .downcast_ref::() + .copied() + .unwrap_or_else(|| panic!("expected wasmtime Trap, got: {err:#}")); + assert_eq!( + trap, + Trap::BadSignature, + "the leftover-segment-overwrites-with-different-sig case must \ + hit the runtime sig check, not the (unsoundly-elided) static \ + match" + ); + Ok(()) +} + +/// Null-deref witness: precomputed says every in-bounds slot is a +/// concrete funcref (no nulls), but an `Expressions`-form leftover +/// segment writes a `ref.null func` into slot 0. The runtime +/// funcref-NULL check must fire; if the null-check elision had +/// erroneously fired (`may_be_null = false`), the call would +/// dereference a null funcref pointer. +#[test] +#[cfg_attr(miri, ignore)] +fn leftover_segment_expressions_form_null_keeps_null_check() -> Result<()> { + let wat = r#" + (module + (type $sig (func (result i32))) + (table 3 3 funcref) + (func $f1 (type $sig) i32.const 1) + (func $f2 (type $sig) i32.const 2) + (func $f3 (type $sig) i32.const 3) + + ;; Foldable: precomputed = [$f1, $f2, $f3] (no nulls). + (elem (i32.const 0) func $f1 $f2 $f3) + + ;; Leftover: Expressions-form segment, not foldable. Writes + ;; null into slot 0 at instantiation time. + (elem (i32.const 0) funcref (item ref.null func)) + + (func (export "call_at_zero") (result i32) + i32.const 0 + call_indirect (type $sig))) + "#; + + let engine = Engine::default(); + let module = Module::new(&engine, wat)?; + let mut store = Store::new(&engine, ()); + let instance = Instance::new(&mut store, &module, &[])?; + + let call_at_zero = instance.get_typed_func::<(), i32>(&mut store, "call_at_zero")?; + let err = call_at_zero + .call(&mut store, ()) + .expect_err("call_indirect through a null funcref must trap"); + + let trap = err + .downcast_ref::() + .copied() + .unwrap_or_else(|| panic!("expected wasmtime Trap, got: {err:#}")); + assert_eq!( + trap, + Trap::IndirectCallToNull, + "the leftover-segment-overwrites-with-null case must hit the \ + runtime funcref-NULL check" + ); + Ok(()) +} + +/// Stale-direct-call witness: precomputed says slot 0 is `$f_a`, but +/// a leftover segment overwrites slot 0 with `$f_b`. With both +/// functions having the same signature, the sig check would NOT trap +/// — so this specifically tests that +/// `try_static_resolve_indirect_call` (the constant-index direct-call +/// rewrite) is suppressed: if it had fired on the unsound premise, +/// the call would dispatch to `$f_a` returning 1; with the fix, the +/// runtime indirection finds `$f_b` returning 2. +#[test] +#[cfg_attr(miri, ignore)] +fn leftover_segment_dynamic_offset_keeps_runtime_dispatch() -> Result<()> { + let wat = r#" + (module + (import "" "g" (global $g i32)) + (type $sig (func (result i32))) + (table 4 4 funcref) + (func $f_a (type $sig) i32.const 1) + (func $f_b (type $sig) i32.const 2) + + ;; precomputed[0] = $f_a → would direct-call to $f_a. + (elem (i32.const 0) func $f_a) + + ;; Leftover writes $f_b at offset $g. + (elem (offset (global.get $g)) func $f_b) + + (func (export "call_at_zero") (result i32) + i32.const 0 + call_indirect (type $sig))) + "#; + + let engine = Engine::default(); + let module = Module::new(&engine, wat)?; + let mut store = Store::new(&engine, ()); + let g = Global::new( + &mut store, + GlobalType::new(ValType::I32, Mutability::Const), + Val::I32(0), + )?; + let instance = Instance::new(&mut store, &module, &[g.into()])?; + + let call_at_zero = instance.get_typed_func::<(), i32>(&mut store, "call_at_zero")?; + let result = call_at_zero.call(&mut store, ())?; + assert_eq!( + result, 2, + "leftover segment must overwrite precomputed[0]; if direct-call \ + rewrite had fired we'd get 1 from $f_a" + ); + Ok(()) +} + +/// Negative control: the same module shape but WITHOUT a leftover +/// segment compiles cleanly and dispatches through the (now-elided +/// or not, depends on Cranelift opts) direct path. This pins the +/// witness modules above as actually exercising the leftover-segment +/// codepath, not some unrelated trap. +#[test] +#[cfg_attr(miri, ignore)] +fn no_leftover_segment_baseline_dispatches_correctly() -> Result<()> { + let wat = r#" + (module + (type $sig (func (result i32))) + (table 4 4 funcref) + (func $f_a (type $sig) i32.const 1) + + (elem (i32.const 0) func $f_a) + + (func (export "call_at_zero") (result i32) + i32.const 0 + call_indirect (type $sig))) + "#; + + let engine = Engine::default(); + let module = Module::new(&engine, wat)?; + let mut store = Store::new(&engine, ()); + let instance = Instance::new(&mut store, &module, &[])?; + let call_at_zero = instance.get_typed_func::<(), i32>(&mut store, "call_at_zero")?; + let result = call_at_zero.call(&mut store, ())?; + assert_eq!(result, 1); + Ok(()) +} diff --git a/tests/all/main.rs b/tests/all/main.rs index ec61178b3478..66bf7ab56e3d 100644 --- a/tests/all/main.rs +++ b/tests/all/main.rs @@ -34,6 +34,7 @@ mod import_indexes; mod instance; mod intrinsics; mod invoke_func_via_table; +mod leftover_elem_segment_soundness; mod limits; mod linker; mod memory; diff --git a/tests/disas/call-indirect-immutable-elide-null.wat b/tests/disas/call-indirect-immutable-elide-null.wat new file mode 100644 index 000000000000..fc90e6ed90eb --- /dev/null +++ b/tests/disas/call-indirect-immutable-elide-null.wat @@ -0,0 +1,111 @@ +;;! target = "x86_64" + +;; Immutable funcref table where every slot is filled by the elem +;; segment (no "no-entry" gaps). With both the sig check AND the +;; funcref-NULL check elided, the dispatch path is reduced to: +;; - bounds check (static) +;; - lazy-init brif + masking +;; - load code+vmctx +;; - call_indirect +;; +;; In particular the cold block that handles the runtime trap-on-null +;; path should not exist after the funcref load: the static-match path +;; with `may_be_null = false` skips both the sig check and any +;; downstream null-handling. + +(module + (table 3 3 funcref) + + (func $f1 (result i32) i32.const 1) + (func $f2 (result i32) i32.const 2) + (func $f3 (result i32) i32.const 3) + + (func (export "call_it") (param i32) (result i32) + local.get 0 + call_indirect (result i32)) + + ;; Fully cover the table — no null slot anywhere. + (elem (i32.const 0) func $f1 $f2 $f3)) +;; function u0:0(i64 vmctx, i64) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @003f v3 = iconst.i32 1 +;; @0041 jump block1 +;; +;; block1: +;; @0041 return v3 ; v3 = 1 +;; } +;; +;; function u0:1(i64 vmctx, i64) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @0044 v3 = iconst.i32 2 +;; @0046 jump block1 +;; +;; block1: +;; @0046 return v3 ; v3 = 2 +;; } +;; +;; function u0:2(i64 vmctx, i64) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @0049 v3 = iconst.i32 3 +;; @004b jump block1 +;; +;; block1: +;; @004b return v3 ; v3 = 3 +;; } +;; +;; function u0:3(i64 vmctx, i64, i32) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; gv3 = vmctx +;; gv4 = load.i64 notrap aligned readonly can_move gv3+48 +;; sig0 = (i64 vmctx, i64) -> i32 tail +;; sig1 = (i64 vmctx, i32, i64) -> i64 tail +;; fn0 = colocated u805306368:7 sig1 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64, v2: i32): +;; @0050 v4 = iconst.i32 3 +;; @0050 v5 = icmp uge v2, v4 ; v4 = 3 +;; @0050 v6 = uextend.i64 v2 +;; @0050 v7 = load.i64 notrap aligned readonly can_move v0+48 +;; v23 = iconst.i64 3 +;; @0050 v8 = ishl v6, v23 ; v23 = 3 +;; @0050 v9 = iadd v7, v8 +;; @0050 v10 = iconst.i64 0 +;; @0050 v11 = select_spectre_guard v5, v10, v9 ; v10 = 0 +;; @0050 v12 = load.i64 user6 aligned table v11 +;; v22 = iconst.i64 -2 +;; @0050 v13 = band v12, v22 ; v22 = -2 +;; @0050 brif v12, block3(v13), block2 +;; +;; block2 cold: +;; @0050 v15 = iconst.i32 0 +;; @0050 v17 = uextend.i64 v2 +;; @0050 v18 = call fn0(v0, v15, v17) ; v15 = 0 +;; @0050 jump block3(v18) +;; +;; block3(v14: i64): +;; @0050 v19 = load.i64 notrap aligned readonly v14+8 +;; @0050 v20 = load.i64 notrap aligned readonly v14+24 +;; @0050 v21 = call_indirect sig0, v19(v20, v0) +;; @0053 jump block1 +;; +;; block1: +;; @0053 return v21 +;; } diff --git a/tests/disas/call-indirect-immutable-elide-sig.wat b/tests/disas/call-indirect-immutable-elide-sig.wat new file mode 100644 index 000000000000..7215dbae9d2f --- /dev/null +++ b/tests/disas/call-indirect-immutable-elide-sig.wat @@ -0,0 +1,110 @@ +;;! target = "x86_64" + +;; Immutable funcref table where every elem-segment entry has the same +;; declared type as the call site. This module's `tables_mutated` bit +;; for table 0 is clear (no opcode in any function writes to it), and +;; all three slots resolve to the same module type as the call site. +;; That triggers `try_elide_sig_check_for_immutable_table` → +;; `CheckIndirectCallTypeSignature::StaticMatch`, removing the runtime +;; signature load + compare from the dispatch hot path. +;; +;; Look for the absence of `load.i32 user6 aligned readonly v_+16` (the +;; sig-id load) and the matching `icmp eq / trapz user7` on the call +;; site. Compare with `indirect-call-no-caching.wat` for the +;; non-elided shape. + +(module + (table 10 10 funcref) + + (func $f1 (result i32) i32.const 1) + (func $f2 (result i32) i32.const 2) + (func $f3 (result i32) i32.const 3) + + (func (export "call_it") (param i32) (result i32) + local.get 0 + call_indirect (result i32)) + + (elem (i32.const 0) func $f1 $f2 $f3)) +;; function u0:0(i64 vmctx, i64) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @003f v3 = iconst.i32 1 +;; @0041 jump block1 +;; +;; block1: +;; @0041 return v3 ; v3 = 1 +;; } +;; +;; function u0:1(i64 vmctx, i64) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @0044 v3 = iconst.i32 2 +;; @0046 jump block1 +;; +;; block1: +;; @0046 return v3 ; v3 = 2 +;; } +;; +;; function u0:2(i64 vmctx, i64) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @0049 v3 = iconst.i32 3 +;; @004b jump block1 +;; +;; block1: +;; @004b return v3 ; v3 = 3 +;; } +;; +;; function u0:3(i64 vmctx, i64, i32) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; gv3 = vmctx +;; gv4 = load.i64 notrap aligned readonly can_move gv3+48 +;; sig0 = (i64 vmctx, i64) -> i32 tail +;; sig1 = (i64 vmctx, i32, i64) -> i64 tail +;; fn0 = colocated u805306368:7 sig1 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64, v2: i32): +;; @0050 v4 = iconst.i32 10 +;; @0050 v5 = icmp uge v2, v4 ; v4 = 10 +;; @0050 v6 = uextend.i64 v2 +;; @0050 v7 = load.i64 notrap aligned readonly can_move v0+48 +;; v23 = iconst.i64 3 +;; @0050 v8 = ishl v6, v23 ; v23 = 3 +;; @0050 v9 = iadd v7, v8 +;; @0050 v10 = iconst.i64 0 +;; @0050 v11 = select_spectre_guard v5, v10, v9 ; v10 = 0 +;; @0050 v12 = load.i64 user6 aligned table v11 +;; v22 = iconst.i64 -2 +;; @0050 v13 = band v12, v22 ; v22 = -2 +;; @0050 brif v12, block3(v13), block2 +;; +;; block2 cold: +;; @0050 v15 = iconst.i32 0 +;; @0050 v17 = uextend.i64 v2 +;; @0050 v18 = call fn0(v0, v15, v17) ; v15 = 0 +;; @0050 jump block3(v18) +;; +;; block3(v14: i64): +;; @0050 v19 = load.i64 user7 aligned readonly v14+8 +;; @0050 v20 = load.i64 notrap aligned readonly v14+24 +;; @0050 v21 = call_indirect sig0, v19(v20, v0) +;; @0053 jump block1 +;; +;; block1: +;; @0053 return v21 +;; } diff --git a/tests/disas/call-indirect-immutable-static-bound.wat b/tests/disas/call-indirect-immutable-static-bound.wat new file mode 100644 index 000000000000..f06ffaa57811 --- /dev/null +++ b/tests/disas/call-indirect-immutable-static-bound.wat @@ -0,0 +1,110 @@ +;;! target = "x86_64" + +;; Table declared with min < max (a "dynamic-declared" table) that is +;; never written to in the module. Without the per-table mutability +;; bit, Cranelift would emit `load.i64 v0+56` per dispatch to fetch +;; the current bound. With it, `make_table` lowers to +;; `TableSize::Static` and the bound becomes an immediate. +;; +;; Look for: bounds-check `iconst.i32 16` (the declared min, used as +;; static bound) and NO `load.i64 ... v0+56` for the current_elements +;; field. (`+48` for the funcref base is still loaded — that's the +;; element-data pointer, separate from the bound.) + +(module + ;; min=16, max=64 — distinct, so without our optimization the + ;; bound would be loaded per dispatch from `current_elements`. + (table 16 64 funcref) + + (func $f1 (result i32) i32.const 1) + (func $f2 (result i32) i32.const 2) + (func $f3 (result i32) i32.const 3) + + (func (export "call_it") (param i32) (result i32) + local.get 0 + call_indirect (result i32)) + + (elem (i32.const 0) func $f1 $f2 $f3)) +;; function u0:0(i64 vmctx, i64) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @003f v3 = iconst.i32 1 +;; @0041 jump block1 +;; +;; block1: +;; @0041 return v3 ; v3 = 1 +;; } +;; +;; function u0:1(i64 vmctx, i64) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @0044 v3 = iconst.i32 2 +;; @0046 jump block1 +;; +;; block1: +;; @0046 return v3 ; v3 = 2 +;; } +;; +;; function u0:2(i64 vmctx, i64) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @0049 v3 = iconst.i32 3 +;; @004b jump block1 +;; +;; block1: +;; @004b return v3 ; v3 = 3 +;; } +;; +;; function u0:3(i64 vmctx, i64, i32) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; gv3 = vmctx +;; gv4 = load.i64 notrap aligned readonly can_move gv3+48 +;; sig0 = (i64 vmctx, i64) -> i32 tail +;; sig1 = (i64 vmctx, i32, i64) -> i64 tail +;; fn0 = colocated u805306368:7 sig1 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64, v2: i32): +;; @0050 v4 = iconst.i32 16 +;; @0050 v5 = icmp uge v2, v4 ; v4 = 16 +;; @0050 v6 = uextend.i64 v2 +;; @0050 v7 = load.i64 notrap aligned readonly can_move v0+48 +;; v23 = iconst.i64 3 +;; @0050 v8 = ishl v6, v23 ; v23 = 3 +;; @0050 v9 = iadd v7, v8 +;; @0050 v10 = iconst.i64 0 +;; @0050 v11 = select_spectre_guard v5, v10, v9 ; v10 = 0 +;; @0050 v12 = load.i64 user6 aligned table v11 +;; v22 = iconst.i64 -2 +;; @0050 v13 = band v12, v22 ; v22 = -2 +;; @0050 brif v12, block3(v13), block2 +;; +;; block2 cold: +;; @0050 v15 = iconst.i32 0 +;; @0050 v17 = uextend.i64 v2 +;; @0050 v18 = call fn0(v0, v15, v17) ; v15 = 0 +;; @0050 jump block3(v18) +;; +;; block3(v14: i64): +;; @0050 v19 = load.i64 user7 aligned readonly v14+8 +;; @0050 v20 = load.i64 notrap aligned readonly v14+24 +;; @0050 v21 = call_indirect sig0, v19(v20, v0) +;; @0053 jump block1 +;; +;; block1: +;; @0053 return v21 +;; } diff --git a/tests/disas/call-indirect-leftover-segment-keeps-null-check.wat b/tests/disas/call-indirect-leftover-segment-keeps-null-check.wat new file mode 100644 index 000000000000..f233404ecf94 --- /dev/null +++ b/tests/disas/call-indirect-leftover-segment-keeps-null-check.wat @@ -0,0 +1,131 @@ +;;! target = "x86_64" + +;; Counterpart to `call-indirect-immutable-elide-null.wat`. The static +;; `elem` segment fully covers the table (`(elem (i32.const 0) func $f1 +;; $f2 $f3)`), so a casual look at `precomputed_table_has_no_null_slots` +;; says "no nulls". But a second segment in `Expressions` form +;; (`funcref (item ref.null func)`) cannot be folded into `precomputed` +;; — `try_func_table_init` only folds `Functions`-form segments — so +;; it stays in `table_initialization.segments` and runs at instantiation +;; time. That segment writes a null funcref into in-bounds slot 0. +;; +;; `analyze_table_mutability` marks tables targeted by leftover segments +;; as mutated, which kicks both +;; `try_elide_sig_check_for_immutable_table` and (transitively) the +;; `may_be_null = false` lowering off this site. The compiled call +;; therefore keeps the runtime sig check + funcref-NULL trap, and the +;; runtime correctly traps on the null funcref written by the leftover +;; segment instead of dereferencing it. +;; +;; Soundness motivation: skipping the null check on a slot a leftover +;; segment can null out is a null-deref bug. See PR #2 review threads +;; `discussion_r3193374159` and `discussion_r3193374164` on +;; rebeckerspecialties/wasmtime. + +(module + (table 3 3 funcref) + + (func $f1 (result i32) i32.const 1) + (func $f2 (result i32) i32.const 2) + (func $f3 (result i32) i32.const 3) + + (func (export "call_it") (param i32) (result i32) + local.get 0 + call_indirect (result i32)) + + ;; Foldable: full coverage with concrete funcrefs. Without the + ;; second segment below, this would trigger both elisions. + (elem (i32.const 0) func $f1 $f2 $f3) + + ;; Expressions-form segment containing a null. Not foldable, runs + ;; at instantiation, can null out an in-bounds slot. Marks the + ;; table mutated, suppressing the elisions. + (elem (i32.const 0) funcref (item ref.null func))) +;; function u0:0(i64 vmctx, i64) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @0047 v3 = iconst.i32 1 +;; @0049 jump block1 +;; +;; block1: +;; @0049 return v3 ; v3 = 1 +;; } +;; +;; function u0:1(i64 vmctx, i64) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @004c v3 = iconst.i32 2 +;; @004e jump block1 +;; +;; block1: +;; @004e return v3 ; v3 = 2 +;; } +;; +;; function u0:2(i64 vmctx, i64) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @0051 v3 = iconst.i32 3 +;; @0053 jump block1 +;; +;; block1: +;; @0053 return v3 ; v3 = 3 +;; } +;; +;; function u0:3(i64 vmctx, i64, i32) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; gv3 = vmctx +;; gv4 = load.i64 notrap aligned readonly can_move gv3+48 +;; sig0 = (i64 vmctx, i64) -> i32 tail +;; sig1 = (i64 vmctx, i32, i64) -> i64 tail +;; fn0 = colocated u805306368:7 sig1 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64, v2: i32): +;; @0058 v4 = iconst.i32 3 +;; @0058 v5 = icmp uge v2, v4 ; v4 = 3 +;; @0058 v6 = uextend.i64 v2 +;; @0058 v7 = load.i64 notrap aligned readonly can_move v0+48 +;; v28 = iconst.i64 3 +;; @0058 v8 = ishl v6, v28 ; v28 = 3 +;; @0058 v9 = iadd v7, v8 +;; @0058 v10 = iconst.i64 0 +;; @0058 v11 = select_spectre_guard v5, v10, v9 ; v10 = 0 +;; @0058 v12 = load.i64 user6 aligned table v11 +;; v27 = iconst.i64 -2 +;; @0058 v13 = band v12, v27 ; v27 = -2 +;; @0058 brif v12, block3(v13), block2 +;; +;; block2 cold: +;; @0058 v15 = iconst.i32 0 +;; @0058 v17 = uextend.i64 v2 +;; @0058 v18 = call fn0(v0, v15, v17) ; v15 = 0 +;; @0058 jump block3(v18) +;; +;; block3(v14: i64): +;; @0058 v20 = load.i64 notrap aligned readonly can_move v0+40 +;; @0058 v21 = load.i32 notrap aligned readonly can_move v20 +;; @0058 v22 = load.i32 user7 aligned readonly v14+16 +;; @0058 v23 = icmp eq v22, v21 +;; @0058 trapz v23, user8 +;; @0058 v24 = load.i64 notrap aligned readonly v14+8 +;; @0058 v25 = load.i64 notrap aligned readonly v14+24 +;; @0058 v26 = call_indirect sig0, v24(v25, v0) +;; @005b jump block1 +;; +;; block1: +;; @005b return v26 +;; } diff --git a/tests/disas/call-indirect-leftover-segment-keeps-sigcheck.wat b/tests/disas/call-indirect-leftover-segment-keeps-sigcheck.wat new file mode 100644 index 000000000000..ccacbb2059a2 --- /dev/null +++ b/tests/disas/call-indirect-leftover-segment-keeps-sigcheck.wat @@ -0,0 +1,134 @@ +;;! target = "x86_64" + +;; Counterpart to `call-indirect-immutable-elide-sig.wat`. Same shape — +;; uniform call-site type, no `table.set`/`table.fill`/etc. — but a +;; second elem segment with a dynamic offset (`(global.get $g)` of an +;; imported global) cannot be folded into the precomputed image by +;; `try_func_table_init`. It stays in `table_initialization.segments` +;; and runs at instantiation time, potentially overwriting precomputed +;; slots with a different funcref. `analyze_table_mutability` marks +;; this table as conservatively mutated, which disables sig-check +;; elision in `try_elide_sig_check_for_immutable_table`. +;; +;; Look for the runtime sig load + compare on the call site (the +;; `tables_mutated`-clear elided form is in +;; `call-indirect-immutable-elide-sig.wat`): +;; load.i32 user6 aligned readonly v_+16 +;; icmp eq +;; trapz user7 +;; +;; Soundness motivation: a leftover segment can introduce a function +;; with a different signature at the same slot, and skipping the sig +;; check would produce type confusion at the call site. See PR #2 +;; review threads `discussion_r3193374159` and +;; `discussion_r3193374164` on rebeckerspecialties/wasmtime for the +;; original report. + +(module + (import "" "g" (global $g i32)) + + (table 10 10 funcref) + + (func $f1 (result i32) i32.const 1) + (func $f2 (result i32) i32.const 2) + (func $f3 (result i32) i32.const 3) + + (func (export "call_it") (param i32) (result i32) + local.get 0 + call_indirect (result i32)) + + ;; Foldable segment: constant offset, Functions form. This *does* + ;; populate `precomputed[0..3] = [f1, f2, f3]`. + (elem (i32.const 0) func $f1 $f2 $f3) + + ;; Leftover segment: dynamic offset → not foldable → stays in + ;; `table_initialization.segments` → marks the table mutated. + (elem (offset (global.get $g)) func $f1)) +;; function u0:0(i64 vmctx, i64) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @004e v3 = iconst.i32 1 +;; @0050 jump block1 +;; +;; block1: +;; @0050 return v3 ; v3 = 1 +;; } +;; +;; function u0:1(i64 vmctx, i64) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @0053 v3 = iconst.i32 2 +;; @0055 jump block1 +;; +;; block1: +;; @0055 return v3 ; v3 = 2 +;; } +;; +;; function u0:2(i64 vmctx, i64) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @0058 v3 = iconst.i32 3 +;; @005a jump block1 +;; +;; block1: +;; @005a return v3 ; v3 = 3 +;; } +;; +;; function u0:3(i64 vmctx, i64, i32) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; gv3 = vmctx +;; gv4 = load.i64 notrap aligned readonly can_move gv3+72 +;; sig0 = (i64 vmctx, i64) -> i32 tail +;; sig1 = (i64 vmctx, i32, i64) -> i64 tail +;; fn0 = colocated u805306368:7 sig1 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64, v2: i32): +;; @005f v4 = iconst.i32 10 +;; @005f v5 = icmp uge v2, v4 ; v4 = 10 +;; @005f v6 = uextend.i64 v2 +;; @005f v7 = load.i64 notrap aligned readonly can_move v0+72 +;; v28 = iconst.i64 3 +;; @005f v8 = ishl v6, v28 ; v28 = 3 +;; @005f v9 = iadd v7, v8 +;; @005f v10 = iconst.i64 0 +;; @005f v11 = select_spectre_guard v5, v10, v9 ; v10 = 0 +;; @005f v12 = load.i64 user6 aligned table v11 +;; v27 = iconst.i64 -2 +;; @005f v13 = band v12, v27 ; v27 = -2 +;; @005f brif v12, block3(v13), block2 +;; +;; block2 cold: +;; @005f v15 = iconst.i32 0 +;; @005f v17 = uextend.i64 v2 +;; @005f v18 = call fn0(v0, v15, v17) ; v15 = 0 +;; @005f jump block3(v18) +;; +;; block3(v14: i64): +;; @005f v20 = load.i64 notrap aligned readonly can_move v0+40 +;; @005f v21 = load.i32 notrap aligned readonly can_move v20 +;; @005f v22 = load.i32 user7 aligned readonly v14+16 +;; @005f v23 = icmp eq v22, v21 +;; @005f trapz v23, user8 +;; @005f v24 = load.i64 notrap aligned readonly v14+8 +;; @005f v25 = load.i64 notrap aligned readonly v14+24 +;; @005f v26 = call_indirect sig0, v24(v25, v0) +;; @0062 jump block1 +;; +;; block1: +;; @0062 return v26 +;; } diff --git a/tests/disas/call-indirect-mutable-keeps-sigcheck.wat b/tests/disas/call-indirect-mutable-keeps-sigcheck.wat new file mode 100644 index 000000000000..38f54153324c --- /dev/null +++ b/tests/disas/call-indirect-mutable-keeps-sigcheck.wat @@ -0,0 +1,150 @@ +;;! target = "x86_64" + +;; Counterpart to `call-indirect-immutable-elide-sig.wat`. Same module +;; shape — same elem segment, same uniform call-site type — but one +;; function writes to the table via `table.set`. That sets the +;; `tables_mutated` bit and disables sig-check elision. +;; +;; Look for the runtime sig load + compare on the call site: +;; load.i32 user6 aligned readonly v_+16 +;; icmp eq +;; trapz user7 +;; (versus the elided form in the immutable test). + +(module + (table 10 10 funcref) + + (func $f1 (result i32) i32.const 1) + (func $f2 (result i32) i32.const 2) + (func $f3 (result i32) i32.const 3) + + ;; Mutator: this clears the immutability proof for table 0. + (func (export "mutate") (param i32) + local.get 0 + ref.func $f1 + table.set 0) + + (func (export "call_it") (param i32) (result i32) + local.get 0 + call_indirect (result i32)) + + (elem (i32.const 0) func $f1 $f2 $f3)) +;; function u0:0(i64 vmctx, i64) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @004d v3 = iconst.i32 1 +;; @004f jump block1 +;; +;; block1: +;; @004f return v3 ; v3 = 1 +;; } +;; +;; function u0:1(i64 vmctx, i64) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @0052 v3 = iconst.i32 2 +;; @0054 jump block1 +;; +;; block1: +;; @0054 return v3 ; v3 = 2 +;; } +;; +;; function u0:2(i64 vmctx, i64) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64): +;; @0057 v3 = iconst.i32 3 +;; @0059 jump block1 +;; +;; block1: +;; @0059 return v3 ; v3 = 3 +;; } +;; +;; function u0:3(i64 vmctx, i64, i32) tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; gv3 = vmctx +;; gv4 = load.i64 notrap aligned readonly can_move gv3+48 +;; sig0 = (i64 vmctx, i32) -> i64 tail +;; fn0 = colocated u805306368:6 sig0 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64, v2: i32): +;; @005e v3 = iconst.i32 0 +;; @005e v5 = call fn0(v0, v3) ; v3 = 0 +;; @0060 v6 = iconst.i32 10 +;; @0060 v7 = icmp uge v2, v6 ; v6 = 10 +;; @0060 v8 = uextend.i64 v2 +;; @0060 v9 = load.i64 notrap aligned readonly can_move v0+48 +;; v16 = iconst.i64 3 +;; @0060 v10 = ishl v8, v16 ; v16 = 3 +;; @0060 v11 = iadd v9, v10 +;; @0060 v12 = iconst.i64 0 +;; @0060 v13 = select_spectre_guard v7, v12, v11 ; v12 = 0 +;; v15 = iconst.i64 1 +;; @0060 v14 = bor v5, v15 ; v15 = 1 +;; @0060 store user6 aligned table v14, v13 +;; @0062 jump block1 +;; +;; block1: +;; @0062 return +;; } +;; +;; function u0:4(i64 vmctx, i64, i32) -> i32 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1+24 +;; gv3 = vmctx +;; gv4 = load.i64 notrap aligned readonly can_move gv3+48 +;; sig0 = (i64 vmctx, i64) -> i32 tail +;; sig1 = (i64 vmctx, i32, i64) -> i64 tail +;; fn0 = colocated u805306368:7 sig1 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64, v2: i32): +;; @0067 v4 = iconst.i32 10 +;; @0067 v5 = icmp uge v2, v4 ; v4 = 10 +;; @0067 v6 = uextend.i64 v2 +;; @0067 v7 = load.i64 notrap aligned readonly can_move v0+48 +;; v28 = iconst.i64 3 +;; @0067 v8 = ishl v6, v28 ; v28 = 3 +;; @0067 v9 = iadd v7, v8 +;; @0067 v10 = iconst.i64 0 +;; @0067 v11 = select_spectre_guard v5, v10, v9 ; v10 = 0 +;; @0067 v12 = load.i64 user6 aligned table v11 +;; v27 = iconst.i64 -2 +;; @0067 v13 = band v12, v27 ; v27 = -2 +;; @0067 brif v12, block3(v13), block2 +;; +;; block2 cold: +;; @0067 v15 = iconst.i32 0 +;; @0067 v17 = uextend.i64 v2 +;; @0067 v18 = call fn0(v0, v15, v17) ; v15 = 0 +;; @0067 jump block3(v18) +;; +;; block3(v14: i64): +;; @0067 v20 = load.i64 notrap aligned readonly can_move v0+40 +;; @0067 v21 = load.i32 notrap aligned readonly can_move v20 +;; @0067 v22 = load.i32 user7 aligned readonly v14+16 +;; @0067 v23 = icmp eq v22, v21 +;; @0067 trapz v23, user8 +;; @0067 v24 = load.i64 notrap aligned readonly v14+8 +;; @0067 v25 = load.i64 notrap aligned readonly v14+24 +;; @0067 v26 = call_indirect sig0, v24(v25, v0) +;; @006a jump block1 +;; +;; block1: +;; @006a return v26 +;; } diff --git a/tests/disas/indirect-call-no-caching.wat b/tests/disas/indirect-call-no-caching.wat index 6d4322dc7fe0..461c2bd09c64 100644 --- a/tests/disas/indirect-call-no-caching.wat +++ b/tests/disas/indirect-call-no-caching.wat @@ -78,14 +78,14 @@ ;; @0050 v5 = icmp uge v2, v4 ; v4 = 10 ;; @0050 v6 = uextend.i64 v2 ;; @0050 v7 = load.i64 notrap aligned readonly can_move v0+48 -;; v28 = iconst.i64 3 -;; @0050 v8 = ishl v6, v28 ; v28 = 3 +;; v23 = iconst.i64 3 +;; @0050 v8 = ishl v6, v23 ; v23 = 3 ;; @0050 v9 = iadd v7, v8 ;; @0050 v10 = iconst.i64 0 ;; @0050 v11 = select_spectre_guard v5, v10, v9 ; v10 = 0 ;; @0050 v12 = load.i64 user6 aligned table v11 -;; v27 = iconst.i64 -2 -;; @0050 v13 = band v12, v27 ; v27 = -2 +;; v22 = iconst.i64 -2 +;; @0050 v13 = band v12, v22 ; v22 = -2 ;; @0050 brif v12, block3(v13), block2 ;; ;; block2 cold: @@ -95,16 +95,11 @@ ;; @0050 jump block3(v18) ;; ;; block3(v14: i64): -;; @0050 v20 = load.i64 notrap aligned readonly can_move v0+40 -;; @0050 v21 = load.i32 notrap aligned readonly can_move v20 -;; @0050 v22 = load.i32 user7 aligned readonly v14+16 -;; @0050 v23 = icmp eq v22, v21 -;; @0050 trapz v23, user8 -;; @0050 v24 = load.i64 notrap aligned readonly v14+8 -;; @0050 v25 = load.i64 notrap aligned readonly v14+24 -;; @0050 v26 = call_indirect sig0, v24(v25, v0) +;; @0050 v19 = load.i64 user7 aligned readonly v14+8 +;; @0050 v20 = load.i64 notrap aligned readonly v14+24 +;; @0050 v21 = call_indirect sig0, v19(v20, v0) ;; @0053 jump block1 ;; ;; block1: -;; @0053 return v26 +;; @0053 return v21 ;; } diff --git a/tests/disas/readonly-funcrefs.wat b/tests/disas/readonly-funcrefs.wat index a2895d7b6a15..63ac248e7eb6 100644 --- a/tests/disas/readonly-funcrefs.wat +++ b/tests/disas/readonly-funcrefs.wat @@ -48,13 +48,13 @@ ;; @0031 v9 = iconst.i64 0 ;; @0031 v6 = load.i64 notrap aligned readonly can_move v0+48 ;; @0031 v5 = uextend.i64 v2 -;; v26 = iconst.i64 3 -;; @0031 v7 = ishl v5, v26 ; v26 = 3 +;; v21 = iconst.i64 3 +;; @0031 v7 = ishl v5, v21 ; v21 = 3 ;; @0031 v8 = iadd v6, v7 ;; @0031 v10 = select_spectre_guard v4, v9, v8 ; v9 = 0 ;; @0031 v11 = load.i64 user6 aligned table v10 -;; v25 = iconst.i64 -2 -;; @0031 v12 = band v11, v25 ; v25 = -2 +;; v20 = iconst.i64 -2 +;; @0031 v12 = band v11, v20 ; v20 = -2 ;; @0031 brif v11, block3(v12), block2 ;; ;; block2 cold: @@ -63,14 +63,9 @@ ;; @0031 jump block3(v17) ;; ;; block3(v13: i64): -;; @0031 v21 = load.i32 user7 aligned readonly v13+16 -;; @0031 v19 = load.i64 notrap aligned readonly can_move v0+40 -;; @0031 v20 = load.i32 notrap aligned readonly can_move v19 -;; @0031 v22 = icmp eq v21, v20 -;; @0031 trapz v22, user8 -;; @0031 v23 = load.i64 notrap aligned readonly v13+8 -;; @0031 v24 = load.i64 notrap aligned readonly v13+24 -;; @0031 call_indirect sig0, v23(v24, v0) +;; @0031 v18 = load.i64 user7 aligned readonly v13+8 +;; @0031 v19 = load.i64 notrap aligned readonly v13+24 +;; @0031 call_indirect sig0, v18(v19, v0) ;; @0034 jump block1 ;; ;; block1: