diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 852d0b870ba1..c5147fe12519 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -717,6 +717,12 @@ jobs: env: RUSTFLAGS: "--cfg arc_try_new" + # Check the x64 assembler against the Intel XED disassembler, which knows + # about newer encodings that the bundled Capstone cannot decode. This is + # opt-in via `fuzz-xed` because XED is built from source; the C compiler and + # Python that requires are already present on this runner. + - run: cargo test -p cranelift-assembler-x64 --features fuzz-xed + # Smoke test the coverage script. - run: rustup target add wasm32-unknown-unknown - run: rustup component add llvm-tools @@ -735,7 +741,7 @@ jobs: - uses: ./.github/actions/apt-get-install with: packages: ocaml-nox ocamlbuild ocaml-findlib libzarith-ocaml-dev - - run: cargo fuzz check --dev + - run: cargo fuzz check --dev --features fuzz-xed - run: cargo fuzz check --dev --fuzz-dir ./cranelift/isle/fuzz - run: cargo fuzz check --dev --fuzz-dir ./crates/environ/fuzz --features component-model - run: cargo fuzz check --dev --fuzz-dir ./cranelift/assembler-x64/fuzz diff --git a/Cargo.lock b/Cargo.lock index 72d86667c217..e890d367f22c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -692,6 +692,7 @@ dependencies = [ "arbtest", "capstone", "cranelift-assembler-x64-meta", + "xed-sys", ] [[package]] @@ -760,7 +761,7 @@ dependencies = [ "similar", "smallvec 1.15.1", "souper-ir", - "target-lexicon", + "target-lexicon 0.13.5", "wasmtime-internal-core", ] @@ -821,7 +822,7 @@ dependencies = [ "serde_derive", "similar", "smallvec 1.15.1", - "target-lexicon", + "target-lexicon 0.13.5", "thiserror 2.0.17", "toml", "wasmtime-internal-unwinder", @@ -838,7 +839,7 @@ dependencies = [ "log", "similar", "smallvec 1.15.1", - "target-lexicon", + "target-lexicon 0.13.5", ] [[package]] @@ -850,7 +851,7 @@ dependencies = [ "cranelift", "cranelift-native", "rand 0.10.1", - "target-lexicon", + "target-lexicon 0.13.5", ] [[package]] @@ -967,7 +968,7 @@ dependencies = [ "log", "memmap2", "region", - "target-lexicon", + "target-lexicon 0.13.5", "wasmtime-internal-jit-icache-coherence", "wasmtime-internal-unwinder", "windows-sys 0.61.2", @@ -991,7 +992,7 @@ version = "0.135.0" dependencies = [ "cranelift-codegen", "libc", - "target-lexicon", + "target-lexicon 0.13.5", ] [[package]] @@ -1007,7 +1008,7 @@ dependencies = [ "gimli 0.33.0", "log", "object 0.39.0", - "target-lexicon", + "target-lexicon 0.13.5", ] [[package]] @@ -1017,7 +1018,7 @@ dependencies = [ "anyhow", "cranelift-codegen", "smallvec 1.15.1", - "target-lexicon", + "target-lexicon 0.13.5", ] [[package]] @@ -1061,7 +1062,7 @@ dependencies = [ "rustc-hash", "serde", "similar", - "target-lexicon", + "target-lexicon 0.13.5", "thiserror 2.0.17", "toml", "walkdir", @@ -3709,6 +3710,12 @@ dependencies = [ "xattr", ] +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "target-lexicon" version = "0.13.5" @@ -4606,7 +4613,7 @@ dependencies = [ "serde_derive", "serde_json", "smallvec 1.15.1", - "target-lexicon", + "target-lexicon 0.13.5", "tempfile", "tokio", "wasm-compose", @@ -4638,7 +4645,7 @@ version = "48.0.0" dependencies = [ "clap", "shuffling-allocator", - "target-lexicon", + "target-lexicon 0.13.5", "wasmtime", "wasmtime-cli-flags", "wasmtime-wasi", @@ -4712,7 +4719,7 @@ dependencies = [ "serde_json", "similar", "smallvec 1.15.1", - "target-lexicon", + "target-lexicon 0.13.5", "tempfile", "termcolor", "test-programs-artifacts", @@ -4788,7 +4795,7 @@ dependencies = [ "serde_derive", "sha2", "smallvec 1.15.1", - "target-lexicon", + "target-lexicon 0.13.5", "wasm-encoder 0.254.0", "wasmparser 0.254.0", "wasmprinter", @@ -4835,7 +4842,7 @@ dependencies = [ "quote", "rand 0.10.1", "smallvec 1.15.1", - "target-lexicon", + "target-lexicon 0.13.5", "wasmparser 0.254.0", "wasmtime", "wasmtime-fuzzing", @@ -4860,7 +4867,7 @@ dependencies = [ "serde", "serde_json", "smallvec 1.15.1", - "target-lexicon", + "target-lexicon 0.13.5", "tempfile", "tokio", "v8", @@ -4958,7 +4965,7 @@ dependencies = [ "object 0.39.0", "pulley-interpreter", "smallvec 1.15.1", - "target-lexicon", + "target-lexicon 0.13.5", "thiserror 2.0.17", "wasmparser 0.254.0", "wasmtime-environ", @@ -4988,7 +4995,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "target-lexicon", + "target-lexicon 0.13.5", "wasmprinter", "wasmtime", "wasmtime-environ", @@ -5073,7 +5080,7 @@ dependencies = [ "gimli 0.33.0", "log", "object 0.39.0", - "target-lexicon", + "target-lexicon 0.13.5", "wasmparser 0.254.0", "wasmtime-environ", "wasmtime-internal-cranelift", @@ -5120,7 +5127,7 @@ dependencies = [ "rand 0.10.1", "serde", "serde_derive", - "target-lexicon", + "target-lexicon 0.13.5", "toml", "wasmparser 0.254.0", "wasmprinter", @@ -5448,7 +5455,7 @@ dependencies = [ "gimli 0.33.0", "regalloc2", "smallvec 1.15.1", - "target-lexicon", + "target-lexicon 0.13.5", "thiserror 2.0.17", "wasmparser 0.254.0", "wasmtime-environ", @@ -5886,6 +5893,16 @@ dependencies = [ "rustix 1.1.4", ] +[[package]] +name = "xed-sys" +version = "0.6.0+xed-2024.05.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078a73fb5587d5b3bb21262d2b38cf6a1e821ece8f7cbf9f82e61ca6f8a8df0e" +dependencies = [ + "cc", + "target-lexicon 0.12.16", +] + [[package]] name = "yoke" version = "0.7.5" diff --git a/Cargo.toml b/Cargo.toml index fbb9c60ef25c..bdfba85d5c2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -410,6 +410,7 @@ clap = { version = "4.5.48", default-features = false, features = ["std", "deriv clap_complete = "4.5.58" hashbrown = { version = "0.17", default-features = false } capstone = { version = "0.14.0", default-features = false, features = ['full', 'arch_x86', 'arch_riscv', 'arch_arm64', 'arch_sysz'] } +xed-sys = { version = "0.6" } smallvec = { version = "1.15.1", features = ["union"] } tracing = { version = "0.1.41", default-features = false } bitflags = "2.9.4" diff --git a/cranelift/assembler-x64/Cargo.toml b/cranelift/assembler-x64/Cargo.toml index 35a7534b6210..b93f375f30ef 100644 --- a/cranelift/assembler-x64/Cargo.toml +++ b/cranelift/assembler-x64/Cargo.toml @@ -10,6 +10,15 @@ rust-version.workspace = true arbitrary = { workspace = true, features = ["derive"], optional = true } capstone = { workspace = true, optional = true } +# XED is built from C source by its own Python build system, which is picky +# about its environment: it only recognizes x86 host CPUs, and it invokes LLVM +# toolchain binaries (e.g. `llvm-ar`) that are not present on every runner. +# Restrict it to x86_64 Linux -- the only configuration where the oracle +# actually runs, i.e. the `fuzz-xed` CI job and OSS-Fuzz -- so that +# `--all-features` builds keep working elsewhere (see `ci/run-tests.py`). +[target.'cfg(all(target_arch = "x86_64", target_os = "linux"))'.dependencies] +xed-sys = { workspace = true, optional = true } + [dev-dependencies] arbitrary = { workspace = true, features = ["derive"] } arbtest = { workspace = true } @@ -23,3 +32,8 @@ workspace = true [features] fuzz = ['dep:arbitrary', 'dep:capstone'] +# Adds Intel XED as a second, optional disassembler oracle for the roundtrip +# fuzzer. This is additive on top of `fuzz` (Capstone remains the default +# oracle); XED is only built when this feature is explicitly enabled, and only +# on x86_64 Linux. Note that building XED requires Python 3.9+ and a C compiler. +fuzz-xed = ['fuzz', 'dep:xed-sys'] diff --git a/cranelift/assembler-x64/meta/src/dsl.rs b/cranelift/assembler-x64/meta/src/dsl.rs index 475aa7d8d237..69e7778d386c 100644 --- a/cranelift/assembler-x64/meta/src/dsl.rs +++ b/cranelift/assembler-x64/meta/src/dsl.rs @@ -113,6 +113,162 @@ impl Inst { }); self } + + /// The mnemonics XED prints for this encoding: `(register form, memory + /// form)`. XED picks a different spelling than we do in three systematic + /// ways, all handled here: + /// + /// 1. it drops the AT&T operand-size suffix we carry (`addl` -> `add`); + /// 2. it prefers a different condition-code alias (`cmovae` -> `cmovnb`); + /// 3. it appends a width marker when an operand is *memory*, sized by that + /// operand (`addsd` -> `addsdq`, `cmovnb` -> `cmovnbl`). + /// + /// The two forms differ only when (3) applies, which is why this returns a + /// pair: the choice is made at runtime from the `r/m` operand. + #[must_use] + pub fn xed_mnemonics(&self) -> (String, String) { + // `lock_addb` is printed `lock addb`; see `custom::mnemonic`. + let mut base = match self.mnemonic.strip_prefix("lock_") { + Some(rest) => format!("lock {rest}"), + None => self.mnemonic.clone(), + }; + + // (1) Drop our size suffix when it matches the width of some operand. + // Mnemonics XED spells differently outright keep their suffix (it is + // part of the name), as do condition-code mnemonics whose trailing + // letter is the condition itself (`setb`) rather than a size. + if !is_exception(&base) && !is_condition_family(&base) && !keeps_suffix(&base, &self.format) + { + let widths: Vec<_> = self + .format + .locations() + .map(|l| width_marker(l.bits())) + .collect(); + if base.len() > 2 && widths.iter().any(|w| base.ends_with(w)) { + base.truncate(base.len() - 1); + } + } + + // (2) XED's preferred spelling for the families that have aliases. + base = rename(&base); + + // (3) A width marker is appended when the operand is memory, sized by + // that operand. `lea` names an address rather than accessing it, so + // never takes one; `push`/`pop` always do, even for registers; and for + // mnemonics that already name their width the marker repeats it + // (`pextrb` -> `pextrbb`). + if matches!(base.as_str(), "push" | "pop") { + let bits = self.format.locations().next().map_or(64, Location::bits); + let m = format!("{base}{}", width_marker(bits)); + return (m.clone(), m); + } + let mem = match self.format.uses_memory() { + Some(_) if base == "lea" => base.clone(), + Some(_) if keeps_suffix(&base, &self.format) => { + // The mnemonic already names its width; XED repeats it using + // its own letter (`pextrd` is 32-bit, so `pextrdl`). + let named = match &base[base.len() - 1..] { + "b" => 8, + "w" => 16, + "d" => 32, + _ => 64, + }; + format!("{base}{}", width_marker(named)) + } + Some(loc) => format!("{base}{}", width_marker(loc.bits())), + None => base.clone(), + }; + (base, mem) + } +} + +/// XED's single-letter marker for an operand width. +fn width_marker(bits: u16) -> &'static str { + match bits { + 8 => "b", + 16 => "w", + 32 => "l", + 64 => "q", + 128 => "x", + 256 => "y", + 512 => "z", + b => unreachable!("no XED width marker for {b} bits"), + } +} + +/// Mnemonics XED spells differently outright. +const XED_RENAMES: &[(&str, &str)] = &[ + ("cbtw", "data16 cbw"), + ("cltd", "cdq"), + ("cltq", "cdqe"), + ("cqto", "cqo"), + ("cwtd", "data16 cwd"), + ("cwtl", "cwde"), + ("movabsq", "mov"), + ("movslq", "movsxd"), +]; + +fn is_exception(m: &str) -> bool { + XED_RENAMES.iter().any(|(o, _)| *o == m) +} + +/// The sixteen x64 condition codes, as spelled in our mnemonics. +const CONDITIONS: &[&str] = &[ + "a", "ae", "b", "be", "e", "g", "ge", "l", "le", "ne", "no", "np", "ns", "o", "p", "s", +]; + +/// Whether `m` is a `cmov`/`set`/`j` mnemonic carrying a valid condition code. +fn is_condition_family(m: &str) -> bool { + ["cmov", "set", "j"] + .iter() + .filter_map(|p| m.strip_prefix(p)) + .any(|cc| CONDITIONS.contains(&cc)) +} + +/// Mnemonics whose trailing size letter names the instruction (`pextrb` +/// extracts a *byte*) rather than decorating it in the AT&T style. +fn keeps_suffix(m: &str, format: &Format) -> bool { + const INTRINSIC: &str = + "pextr pinsr vpextr vpinsr vpbroadcast pmovsxd pmovzxd vpmovsxd vpmovzxd"; + INTRINSIC.split(' ').any(|p| m.starts_with(p)) + // `movq` is a GPR move (suffix) or an XMM move (name) by operand type. + || (matches!(m, "movq" | "vmovq") && format.locations().any(|l| l.bits() == 128)) +} + +/// Rewrite a suffix-stripped mnemonic into XED's preferred spelling. +fn rename(base: &str) -> String { + if let Some((_, x)) = XED_RENAMES.iter().find(|(o, _)| *o == base) { + return (*x).to_string(); + } + + // `movzbl`/`movswq`/... collapse to plain `movzx`/`movsx`; after the size + // suffix is stripped a single source-width letter remains. + for (prefix, xed) in [("movz", "movzx"), ("movs", "movsx")] { + if let Some(rest) = base.strip_prefix(prefix) { + if rest.len() == 1 && "bwlq".contains(rest) { + return xed.to_string(); + } + } + } + + // Condition-code families: XED prefers the negated spelling for six of the + // sixteen conditions. + for prefix in ["cmov", "set", "j"] { + if let Some(cc) = base.strip_prefix(prefix) { + let renamed = match cc { + "a" => "nbe", + "ae" => "nb", + "e" => "z", + "g" => "nle", + "ge" => "nl", + "ne" => "nz", + _ => return base.to_string(), + }; + return format!("{prefix}{renamed}"); + } + } + + base.to_string() } impl core::fmt::Display for Inst { diff --git a/cranelift/assembler-x64/meta/src/dsl/format.rs b/cranelift/assembler-x64/meta/src/dsl/format.rs index 3167bb1e41b5..ac976ffd7b64 100644 --- a/cranelift/assembler-x64/meta/src/dsl/format.rs +++ b/cranelift/assembler-x64/meta/src/dsl/format.rs @@ -377,6 +377,13 @@ impl Location { } } + /// Return `true` if this operand is always memory (i.e., not a `r/m` that + /// may hold a register). + #[must_use] + pub fn is_memory_only(&self) -> bool { + matches!(self.kind(), OperandKind::Mem(_)) + } + /// Return `true` if any of the operands accepts a register (i.e., not an /// immediate); return `false` otherwise. #[must_use] diff --git a/cranelift/assembler-x64/meta/src/generate/format.rs b/cranelift/assembler-x64/meta/src/generate/format.rs index a0664463bf7d..e1e4a8bb3bfb 100644 --- a/cranelift/assembler-x64/meta/src/generate/format.rs +++ b/cranelift/assembler-x64/meta/src/generate/format.rs @@ -65,6 +65,20 @@ impl dsl::Format { ordered_ops.join(", ") } + /// Like [`Self::generate_att_style_operands`], but omits the fixed `%xmm0` + /// mask operand, which XED leaves implicit. + #[must_use] + pub(crate) fn generate_xed_style_operands(&self) -> String { + let ordered_ops: Vec<_> = self + .operands + .iter() + .filter(|o| !o.implicit && o.location != dsl::Location::xmm0) + .rev() + .map(|o| format!("{{{}}}", o.location)) + .collect(); + ordered_ops.join(", ") + } + #[must_use] pub(crate) fn generate_implicit_operands(&self) -> String { let ops: Vec<_> = self diff --git a/cranelift/assembler-x64/meta/src/generate/inst.rs b/cranelift/assembler-x64/meta/src/generate/inst.rs index 6d5e8b8862fe..a2eb137356ef 100644 --- a/cranelift/assembler-x64/meta/src/generate/inst.rs +++ b/cranelift/assembler-x64/meta/src/generate/inst.rs @@ -60,6 +60,7 @@ impl dsl::Inst { self.generate_new_function(f); f.empty_line(); self.generate_mnemonic_function(f); + self.generate_xed_mnemonic_function(f); f.empty_line(); self.generate_encode_function(f); f.empty_line(); @@ -118,6 +119,38 @@ impl dsl::Inst { ); } + /// `fn xed_mnemonic(&self) -> Cow<'static, str> { ... }` + /// + /// This is the mnemonic Intel XED prints for this encoding; it differs from + /// [`Self::generate_mnemonic_function`] often enough that the fuzzer's XED + /// oracle compares against it rather than reconciling the two after the + /// fact. See [`dsl::Inst::xed_mnemonics`]. + pub fn generate_xed_mnemonic_function(&self, f: &mut Formatter) { + let (reg, mem) = self.xed_mnemonics(); + fmtln!(f, "#[must_use]"); + fmtln!(f, "#[inline]"); + f.add_block( + &format!("pub fn xed_mnemonic(&self) -> alloc::borrow::Cow<'static, str>"), + |f| { + let borrow = |m| format!("alloc::borrow::Cow::Borrowed(\"{m}\")"); + let loc = self.format.uses_memory(); + match loc { + // The marker is only appended when the `r/m` operand holds + // memory; a memory-only operand always does. + Some(loc) if reg != mem && !loc.is_memory_only() => { + fmtln!(f, "if self.{loc}.is_memory() {{"); + f.indent(|f| fmtln!(f, "{}", borrow(&mem))); + fmtln!(f, "}} else {{"); + f.indent(|f| fmtln!(f, "{}", borrow(®))); + fmtln!(f, "}}"); + } + Some(_) if reg != mem => fmtln!(f, "{}", borrow(&mem)), + _ => fmtln!(f, "{}", borrow(®)), + } + }, + ); + } + /// `fn encode(&self, ...) { ... }` fn generate_encode_function(&self, f: &mut Formatter) { use dsl::Customization::*; @@ -275,7 +308,7 @@ impl dsl::Inst { return; } - fmtln!(f, "let name = self.mnemonic();"); + fmtln!(f, "let name = if f.alternate() {{ self.xed_mnemonic() }} else {{ self.mnemonic() }};"); if self.format.operands.is_empty() { fmtln!(f, "f.write_str(&name)"); return; @@ -283,9 +316,16 @@ impl dsl::Inst { for op in self.format.operands.iter() { let location = op.location; let to_string = location.generate_to_string(op.extension); - fmtln!(f, "let {location} = {to_string};"); + match location.generate_to_string_xed(op.extension) { + Some(xed) => fmtln!( + f, + "let {location} = if f.alternate() {{ {xed} }} else {{ {to_string} }};" + ), + None => fmtln!(f, "let {location} = {to_string};"), + } } let ordered_ops = self.format.generate_att_style_operands(); + let xed_ops = self.format.generate_xed_style_operands(); let mut implicit_ops = self.format.generate_implicit_operands(); if self.has_trap { fmtln!(f, "let trap = self.trap;"); @@ -295,7 +335,20 @@ impl dsl::Inst { implicit_ops.push_str(", {trap}"); } } - fmtln!(f, "write!(f, \"{{name}} {ordered_ops}{implicit_ops}\")"); + if implicit_ops.is_empty() && xed_ops == ordered_ops { + fmtln!(f, "write!(f, \"{{name}} {ordered_ops}\")"); + } else { + // The implicit-operand and trap annotations are + // ours; XED has no equivalent, and it leaves the + // fixed `%xmm0` mask implicit too. + fmtln!(f, "if f.alternate() {{"); + f.indent(|f| fmtln!(f, "write!(f, \"{{name}} {xed_ops}\")")); + fmtln!(f, "}} else {{"); + f.indent(|f| { + fmtln!(f, "write!(f, \"{{name}} {ordered_ops}{implicit_ops}\")") + }); + fmtln!(f, "}}"); + } }, ); }, diff --git a/cranelift/assembler-x64/meta/src/generate/operand.rs b/cranelift/assembler-x64/meta/src/generate/operand.rs index a2ed80a10153..aaa611b191fe 100644 --- a/cranelift/assembler-x64/meta/src/generate/operand.rs +++ b/cranelift/assembler-x64/meta/src/generate/operand.rs @@ -71,6 +71,24 @@ impl dsl::Location { } } + /// Like [`Self::generate_to_string`], but renders in XED's dialect; + /// `None` when the operand prints identically in both. + #[must_use] + pub fn generate_to_string_xed(&self, _extension: dsl::Extension) -> Option { + use dsl::Location::*; + match self { + rm8 | rm16 | rm32 | rm64 => { + let size = self.generate_size().expect("r/m operands have a size"); + Some(format!("self.{self}.to_string_xed({size})")) + } + xmm_m8 | xmm_m16 | xmm_m32 | xmm_m64 | xmm_m128 => { + Some(format!("self.{self}.to_string_xed()")) + } + m8 | m16 | m32 | m64 | m128 => Some(format!("alloc::format!(\"{{:#}}\", self.{self})")), + _ => None, + } + } + /// `Size::` #[must_use] fn generate_size(&self) -> Option<&str> { diff --git a/cranelift/assembler-x64/src/custom.rs b/cranelift/assembler-x64/src/custom.rs index 25c63e239cf4..2e7116e4bdd2 100644 --- a/cranelift/assembler-x64/src/custom.rs +++ b/cranelift/assembler-x64/src/custom.rs @@ -190,7 +190,8 @@ pub mod mnemonic { pub mod display { use crate::inst; - use crate::{Amode, Gpr, GprMem, Registers, Size}; + use crate::{Amode, AsReg, Gpr, GprMem, Registers, Size, XmmMem}; + use alloc::string::String; use alloc::string::ToString; use core::fmt; @@ -201,6 +202,10 @@ pub mod display { pub fn callq_m(f: &mut fmt::Formatter, inst: &inst::callq_m) -> fmt::Result { let inst::callq_m { rm64 } = inst; + // XED writes the target plainly, without the indirect `*`. + if f.alternate() { + return write!(f, "callq {}", rm64.to_string_xed(Size::Quadword)); + } let op = rm64.to_string(Size::Quadword); write!(f, "callq *{op}") } @@ -221,52 +226,54 @@ pub mod display { } } - pub fn cmpss_a(f: &mut fmt::Formatter, inst: &inst::cmpss_a) -> fmt::Result { - let xmm1 = inst.xmm1.to_string(); - let xmm_m32 = inst.xmm_m32.to_string(); - let pred = inst.imm8.value(); - if pred > 7 { - let imm8 = inst.imm8.to_string(); - write!(f, "cmpss {imm8}, {xmm_m32}, {xmm1}") + /// Render a possibly-memory XMM operand in the dialect `f` asks for. + fn xmm_mem(f: &fmt::Formatter, op: &XmmMem) -> String { + if f.alternate() { + op.to_string_xed() } else { - write!(f, "cmp{}ss {xmm_m32}, {xmm1}", pred_as_str(pred)) + op.to_string() } } - pub fn cmpsd_a(f: &mut fmt::Formatter, inst: &inst::cmpsd_a) -> fmt::Result { - let xmm1 = inst.xmm1.to_string(); - let xmm_m64 = inst.xmm_m64.to_string(); - let pred = inst.imm8.value(); - if pred > 7 { - let imm8 = inst.imm8.to_string(); - write!(f, "cmpsd {imm8}, {xmm_m64}, {xmm1}") + /// XED appends a width marker to the generic `cmp*` form when the operand + /// is memory; see `dsl::Inst::xed_mnemonics`. + fn marker( + f: &fmt::Formatter, + op: &XmmMem, + marker: &'static str, + ) -> &'static str { + if f.alternate() && op.is_memory() { + marker } else { - write!(f, "cmp{}sd {xmm_m64}, {xmm1}", pred_as_str(pred)) + "" } } - pub fn cmpps_a(f: &mut fmt::Formatter, inst: &inst::cmpps_a) -> fmt::Result { - let xmm1 = inst.xmm1.to_string(); - let xmm_m128 = inst.xmm_m128.to_string(); - let pred = inst.imm8.value(); - if pred > 7 { - let imm8 = inst.imm8.to_string(); - write!(f, "cmpps {imm8}, {xmm_m128}, {xmm1}") - } else { - write!(f, "cmp{}ps {xmm_m128}, {xmm1}", pred_as_str(pred)) - } + /// The compare pseudo-ops: we name the predicate in the mnemonic + /// (`cmpeqss`), while XED always writes the generic form with an explicit + /// predicate immediate. + macro_rules! cmp { + ($($name:ident($sfx:tt, $op:ident, $mark:tt);)*) => ($( + pub fn $name(f: &mut fmt::Formatter, inst: &inst::$name) -> fmt::Result { + let xmm1 = inst.xmm1.to_string(); + let op = xmm_mem(f, &inst.$op); + let pred = inst.imm8.value(); + if f.alternate() || pred > 7 { + let m = marker(f, &inst.$op, $mark); + let imm8 = inst.imm8.to_string(); + write!(f, concat!("cmp", $sfx, "{} {}, {}, {}"), m, imm8, op, xmm1) + } else { + write!(f, concat!("cmp{}", $sfx, " {}, {}"), pred_as_str(pred), op, xmm1) + } + } + )*) } - pub fn cmppd_a(f: &mut fmt::Formatter, inst: &inst::cmppd_a) -> fmt::Result { - let xmm1 = inst.xmm1.to_string(); - let xmm_m128 = inst.xmm_m128.to_string(); - let pred = inst.imm8.value(); - if pred > 7 { - let imm8 = inst.imm8.to_string(); - write!(f, "cmppd {imm8}, {xmm_m128}, {xmm1}") - } else { - write!(f, "cmp{}pd {xmm_m128}, {xmm1}", pred_as_str(pred)) - } + cmp! { + cmpss_a("ss", xmm_m32, "l"); + cmpsd_a("sd", xmm_m64, "q"); + cmpps_a("ps", xmm_m128, "x"); + cmppd_a("pd", xmm_m128, "x"); } /// Return the predicate string used for the immediate of a `vcmp*` @@ -309,72 +316,31 @@ pub mod display { } } - pub fn vcmpss_b(f: &mut fmt::Formatter, inst: &inst::vcmpss_b) -> fmt::Result { - let xmm1 = inst.xmm1.to_string(); - let xmm2 = inst.xmm2.to_string(); - let xmm_m32 = inst.xmm_m32.to_string(); - let pred = inst.imm8.value(); - if pred > 0x1f { - let imm8 = inst.imm8.to_string(); - write!(f, "vcmpss {imm8}, {xmm_m32}, {xmm2}, {xmm1}") - } else { - write!( - f, - "vcmp{}ss {xmm_m32}, {xmm2}, {xmm1}", - vex_pred_as_str(pred) - ) - } - } - - pub fn vcmpsd_b(f: &mut fmt::Formatter, inst: &inst::vcmpsd_b) -> fmt::Result { - let xmm1 = inst.xmm1.to_string(); - let xmm2 = inst.xmm2.to_string(); - let xmm_m64 = inst.xmm_m64.to_string(); - let pred = inst.imm8.value(); - if pred > 0x1f { - let imm8 = inst.imm8.to_string(); - write!(f, "vcmpsd {imm8}, {xmm_m64}, {xmm2}, {xmm1}") - } else { - write!( - f, - "vcmp{}sd {xmm_m64}, {xmm2}, {xmm1}", - vex_pred_as_str(pred) - ) - } + /// The VEX compare pseudo-ops; see the `cmp!` macro above. + macro_rules! vcmp { + ($($name:ident($sfx:tt, $op:ident, $mark:tt);)*) => ($( + pub fn $name(f: &mut fmt::Formatter, inst: &inst::$name) -> fmt::Result { + let xmm1 = inst.xmm1.to_string(); + let xmm2 = inst.xmm2.to_string(); + let op = xmm_mem(f, &inst.$op); + let pred = inst.imm8.value(); + if f.alternate() || pred > 0x1f { + let m = marker(f, &inst.$op, $mark); + let imm8 = inst.imm8.to_string(); + write!(f, concat!("vcmp", $sfx, "{} {}, {}, {}, {}"), m, imm8, op, xmm2, xmm1) + } else { + let p = vex_pred_as_str(pred); + write!(f, concat!("vcmp{}", $sfx, " {}, {}, {}"), p, op, xmm2, xmm1) + } + } + )*) } - pub fn vcmpps_b(f: &mut fmt::Formatter, inst: &inst::vcmpps_b) -> fmt::Result { - let xmm1 = inst.xmm1.to_string(); - let xmm2 = inst.xmm2.to_string(); - let xmm_m128 = inst.xmm_m128.to_string(); - let pred = inst.imm8.value(); - if pred > 0x1f { - let imm8 = inst.imm8.to_string(); - write!(f, "vcmpps {imm8}, {xmm_m128}, {xmm2}, {xmm1}") - } else { - write!( - f, - "vcmp{}ps {xmm_m128}, {xmm2}, {xmm1}", - vex_pred_as_str(pred) - ) - } - } - - pub fn vcmppd_b(f: &mut fmt::Formatter, inst: &inst::vcmppd_b) -> fmt::Result { - let xmm1 = inst.xmm1.to_string(); - let xmm2 = inst.xmm2.to_string(); - let xmm_m128 = inst.xmm_m128.to_string(); - let pred = inst.imm8.value(); - if pred > 0x1f { - let imm8 = inst.imm8.to_string(); - write!(f, "vcmppd {imm8}, {xmm_m128}, {xmm2}, {xmm1}") - } else { - write!( - f, - "vcmp{}pd {xmm_m128}, {xmm2}, {xmm1}", - vex_pred_as_str(pred) - ) - } + vcmp! { + vcmpss_b("ss", xmm_m32, "l"); + vcmpsd_b("sd", xmm_m64, "q"); + vcmpps_b("ps", xmm_m128, "x"); + vcmppd_b("pd", xmm_m128, "x"); } pub fn nop_1b(f: &mut fmt::Formatter, _: &inst::nop_1b) -> fmt::Result { @@ -382,7 +348,12 @@ pub mod display { } pub fn nop_2b(f: &mut fmt::Formatter, _: &inst::nop_2b) -> fmt::Result { - write!(f, "nop") + // XED spells the operand-size prefix out for the two-byte form. + if f.alternate() { + write!(f, "data16 nop") + } else { + write!(f, "nop") + } } pub fn nop_3b(f: &mut fmt::Formatter, _: &inst::nop_3b) -> fmt::Result { @@ -394,11 +365,19 @@ pub mod display { } pub fn nop_5b(f: &mut fmt::Formatter, _: &inst::nop_5b) -> fmt::Result { - write!(f, "nopl (%rax, %rax)") + if f.alternate() { + write!(f, "nopl (%rax,%rax,1)") + } else { + write!(f, "nopl (%rax, %rax)") + } } pub fn nop_6b(f: &mut fmt::Formatter, _: &inst::nop_6b) -> fmt::Result { - write!(f, "nopw (%rax, %rax)") + if f.alternate() { + write!(f, "nopw (%rax,%rax,1)") + } else { + write!(f, "nopw (%rax, %rax)") + } } pub fn nop_7b(f: &mut fmt::Formatter, _: &inst::nop_7b) -> fmt::Result { @@ -406,11 +385,19 @@ pub mod display { } pub fn nop_8b(f: &mut fmt::Formatter, _: &inst::nop_8b) -> fmt::Result { - write!(f, "nopl (%rax, %rax)") + if f.alternate() { + write!(f, "nopl (%rax,%rax,1)") + } else { + write!(f, "nopl (%rax, %rax)") + } } pub fn nop_9b(f: &mut fmt::Formatter, _: &inst::nop_9b) -> fmt::Result { - write!(f, "nopw (%rax, %rax)") + if f.alternate() { + write!(f, "nopw (%rax,%rax,1)") + } else { + write!(f, "nopw (%rax, %rax)") + } } pub fn xchgb_rm( @@ -453,7 +440,11 @@ pub mod display { size: Size, ) -> fmt::Result { let reg = reg.to_string(size); - let mem = mem.to_string(); + let mem = if f.alternate() { + alloc::format!("{mem:#}") + } else { + mem.to_string() + }; let suffix = match size { Size::Byte => "b", Size::Word => "w", @@ -570,6 +561,18 @@ pub mod display { size: Size, ) -> fmt::Result { let reg = rm.to_string(size); + // XED always states the implicit shift count, and drops the AT&T size + // suffix when the operand is a register. + if f.alternate() { + let op = rm.to_string_xed(size); + let name = match rm { + GprMem::Gpr(_) => mnemonic + .strip_suffix(['b', 'w', 'l', 'q']) + .unwrap_or(mnemonic), + GprMem::Mem(_) => mnemonic, + }; + return write!(f, "{name} $0x1, {op}"); + } match rm { GprMem::Gpr(_) => write!(f, "{mnemonic} $1, {reg}"), GprMem::Mem(_) => write!(f, "{mnemonic} {reg}"), @@ -578,8 +581,15 @@ pub mod display { pub fn jmpq_m(f: &mut fmt::Formatter<'_>, jmp: &inst::jmpq_m) -> fmt::Result { let inst::jmpq_m { rm64 } = jmp; - let rm64 = rm64.to_string(Size::Quadword); - write!(f, "jmpq *{rm64}") + // XED writes the target plainly, without the indirect `*`, and names a + // register target `jmp` rather than `jmpq`. + if f.alternate() { + let op = rm64.to_string_xed(Size::Quadword); + let name = if rm64.is_memory() { "jmpq" } else { "jmp" }; + return write!(f, "{name} {op}"); + } + let op = rm64.to_string(Size::Quadword); + write!(f, "jmpq *{op}") } pub fn jmp_d8(f: &mut fmt::Formatter<'_>, jmp: &inst::jmp_d8) -> fmt::Result { @@ -596,12 +606,14 @@ pub mod display { ($($mnemonic:tt = $j8:ident / $j32:ident;)*) => ($( pub fn $j8(f: &mut fmt::Formatter<'_>, jmp: &inst::$j8) -> fmt::Result { let inst::$j8 { imm8 } = jmp; - display_displacement(f, $mnemonic, i64::from(imm8.value()) + 2) + let name = if f.alternate() { jmp.xed_mnemonic() } else { $mnemonic.into() }; + display_displacement(f, &name, i64::from(imm8.value()) + 2) } pub fn $j32(f: &mut fmt::Formatter<'_>, jmp: &inst::$j32) -> fmt::Result { let inst::$j32 { imm32 } = jmp; - display_displacement(f, $mnemonic, i64::from(imm32.value()) + 6) + let name = if f.alternate() { jmp.xed_mnemonic() } else { $mnemonic.into() }; + display_displacement(f, &name, i64::from(imm32.value()) + 6) } )*) } @@ -630,7 +642,8 @@ pub mod display { mnemonic: &str, displacement: i64, ) -> fmt::Result { - if displacement >= 0 && displacement < 10 { + // XED always writes the target in hexadecimal. + if !f.alternate() && displacement >= 0 && displacement < 10 { write!(f, "{mnemonic} {displacement}") } else { write!(f, "{mnemonic} {displacement:#x}") diff --git a/cranelift/assembler-x64/src/fuzz.rs b/cranelift/assembler-x64/src/fuzz.rs index adb0fd192100..3efe0acfc472 100644 --- a/cranelift/assembler-x64/src/fuzz.rs +++ b/cranelift/assembler-x64/src/fuzz.rs @@ -18,6 +18,9 @@ use capstone::{Capstone, arch::BuildsCapstone, arch::BuildsCapstoneSyntax, arch: /// Take a random assembly instruction and check its encoding and /// pretty-printing against a known-good disassembler. /// +/// This uses Capstone as the disassembler oracle; see `roundtrip_with` for the +/// oracle-agnostic core. +/// /// # Panics /// /// This function panics to express failure as expected by the `arbitrary` @@ -31,6 +34,50 @@ pub fn roundtrip(inst: &Inst) { return; } + roundtrip_with( + inst, + "capstone", + disassemble_capstone, + capstone_matches, + |i| format!("{i}"), + ); +} + +/// Like [`roundtrip`], but uses Intel XED as the disassembler oracle instead of +/// Capstone. +/// +/// XED understands newer encodings (e.g. APX) that the bundled Capstone does +/// not, so this is a useful second oracle. It is only available with the +/// `fuzz-xed` feature (which requires building XED from source). +/// +/// # Panics +/// +/// See [`roundtrip`]. +#[cfg(all(feature = "fuzz-xed", target_arch = "x86_64", target_os = "linux"))] +pub fn roundtrip_xed(inst: &Inst) { + roundtrip_with(inst, "xed", disassemble_xed, xed_matches, |i| { + format!("{i:#}") + }); +} + +/// The oracle-agnostic core of [`roundtrip`]: assemble `inst`, disassemble the +/// resulting bytes with the provided `disassemble` oracle, and check that the +/// oracle's output matches how `render` prints the instruction, as judged by +/// the oracle-specific `matches` predicate. +/// +/// `render` selects the syntax to compare against: `{inst}` is the assembler's +/// own (what Cranelift's disassembly and the `precise-output` filetests use), +/// while `{inst:#}` is XED's. Printing directly in the oracle's dialect avoids +/// having to reconcile the two strings afterwards. +/// +/// The `oracle` name is only used to label diagnostic output on failure. +fn roundtrip_with( + inst: &Inst, + oracle: &str, + disassemble: impl Fn(&[u8], &Inst) -> String, + matches: impl Fn(&str, &str) -> bool, + render: impl Fn(&Inst) -> String, +) { // Check that we can actually assemble this instruction. let assembled = assemble(inst); let expected = disassemble(&assembled, inst); @@ -38,12 +85,12 @@ pub fn roundtrip(inst: &Inst) { // Check that our pretty-printed output matches the known-good output. Trim // off the instruction offset first. let expected = expected.split_once(' ').unwrap().1; - let actual = inst.to_string(); - if expected != actual && expected.trim() != fix_up(&actual) { + let actual = render(inst); + if !matches(expected, &actual) { println!("> {inst}"); println!(" debug: {inst:x?}"); println!(" assembled: {}", pretty_print_hexadecimal(&assembled)); - println!(" expected (capstone): {expected}"); + println!(" expected ({oracle}): {expected}"); println!(" actual (to_string): {actual}"); assert_eq!(expected, &actual); } @@ -60,6 +107,13 @@ fn features_mention(features: &Features, target: Feature) -> bool { } } +/// Comparison predicate for the Capstone oracle: exact match, or match after +/// applying Capstone-specific normalization ([`fix_up`]) to the assembler +/// output. +fn capstone_matches(expected: &str, actual: &str) -> bool { + expected == actual || expected.trim() == fix_up(actual) +} + /// Use this assembler to emit machine code into a byte buffer. /// /// This will skip any traps or label registrations, but this is fine for the @@ -132,8 +186,11 @@ impl CodeSink for TestCodeSink { } } +/// Disassemble a single instruction with Capstone, returning its AT&T-syntax +/// string. This is the default [`roundtrip`] oracle. +/// /// Building a new `Capstone` each time is suboptimal (TODO). -fn disassemble(assembled: &[u8], original: &Inst) -> String { +fn disassemble_capstone(assembled: &[u8], original: &Inst) -> String { let cs = Capstone::new() .x86() .mode(x86::ArchMode::Mode64) @@ -167,6 +224,79 @@ fn disassemble(assembled: &[u8], original: &Inst) -> String { inst.to_string() } +/// Disassemble a single instruction with Intel XED, returning a string in the +/// same shape as [`disassemble_capstone`] (a leading offset token, a space, +/// then the AT&T-syntax instruction) so that [`roundtrip_with`] can compare it +/// uniformly. +#[cfg(all(feature = "fuzz-xed", target_arch = "x86_64", target_os = "linux"))] +fn disassemble_xed(assembled: &[u8], original: &Inst) -> String { + use core::ffi::c_void; + use std::sync::Once; + use xed_sys::*; + + // XED requires a one-time global table initialization before any decode. + static INIT: Once = Once::new(); + // SAFETY: `xed_tables_init` is safe to call; `Once` guarantees it runs + // exactly once even across threads. + INIT.call_once(|| unsafe { xed_tables_init() }); + + // SAFETY: all of the following are standard XED decode/format calls + // operating on stack-allocated, properly initialized structures. + unsafe { + let mut xedd: xed_decoded_inst_t = core::mem::zeroed(); + xed_decoded_inst_zero(&mut xedd); + xed_decoded_inst_set_mode(&mut xedd, XED_MACHINE_MODE_LONG_64, XED_ADDRESS_WIDTH_64b); + + let error = xed_decode( + &mut xedd, + assembled.as_ptr(), + assembled.len() as core::ffi::c_uint, + ); + if error != XED_ERROR_NONE { + println!("> {original}"); + println!(" debug: {original:x?}"); + println!(" assembled: {}", pretty_print_hexadecimal(assembled)); + let name = core::ffi::CStr::from_ptr(xed_error_enum_t2str(error)); + panic!("xed failed to decode: {}", name.to_string_lossy()); + } + + // XED must consume exactly the bytes we emitted; a shorter length means + // trailing bytes were not part of the instruction. + let decoded_len = xed_decoded_inst_get_length(&xedd) as usize; + if decoded_len != assembled.len() { + println!("> {original}"); + println!(" debug: {original:x?}"); + println!(" assembled: {}", pretty_print_hexadecimal(assembled)); + assert_eq!( + decoded_len, + assembled.len(), + "xed did not consume all bytes" + ); + } + + // Format in AT&T syntax to match the assembler's own pretty-printing. + let mut buf = [0i8; 256]; + let ok = xed_format_context( + XED_SYNTAX_ATT, + &xedd, + buf.as_mut_ptr(), + buf.len() as core::ffi::c_int, + 0, + core::ptr::null_mut::(), + None, + ); + assert!(ok != 0, "xed failed to format instruction"); + + let disasm = core::ffi::CStr::from_ptr(buf.as_ptr()) + .to_string_lossy() + .into_owned(); + + // Prepend a fake offset token so the shape matches Capstone's + // `0x0: ` output that `roundtrip_with` expects. + format!("0: {disasm}") + } +} + fn pretty_print_hexadecimal(hex: &[u8]) -> String { use core::fmt::Write; let mut s = String::with_capacity(hex.len() * 2); @@ -286,6 +416,16 @@ fn fix_up(dis: &str) -> alloc::borrow::Cow<'_, str> { replace_signed_immediates(&dis) } +/// Comparison predicate for the XED oracle. +/// +/// The assembler renders the instruction in XED's own dialect (`{inst:#}`), so +/// the two strings agree exactly apart from the extra padding XED inserts after +/// the mnemonic. +#[cfg(all(feature = "fuzz-xed", target_arch = "x86_64", target_os = "linux"))] +fn xed_matches(expected: &str, actual: &str) -> bool { + expected.split_whitespace().eq(actual.split_whitespace()) +} + /// Fuzz-specific registers. /// /// For the fuzzer, we do not need any fancy register types; see [`FuzzReg`]. @@ -430,4 +570,23 @@ mod test { roundtrip(&inst.into()); } } + + /// Same as [`smoke`], but exercises the Intel XED oracle. Only available + /// with the `fuzz-xed` feature. + /// + /// The instruction is printed in XED's dialect (`{inst:#}`) so that the two + /// can be compared directly. Run explicitly with + /// `cargo test --features fuzz-xed -- smoke_xed`. + #[cfg(all(feature = "fuzz-xed", target_arch = "x86_64", target_os = "linux"))] + #[test] + fn smoke_xed() { + let count = AtomicUsize::new(0); + arbtest(|u| { + let inst: Inst = u.arbitrary()?; + roundtrip_xed(&inst); + println!("#{}: {inst}", count.fetch_add(1, Ordering::SeqCst)); + Ok(()) + }) + .budget_ms(1_000); + } } diff --git a/cranelift/assembler-x64/src/mem.rs b/cranelift/assembler-x64/src/mem.rs index e104f9dfd15f..5dc033c0d907 100644 --- a/cranelift/assembler-x64/src/mem.rs +++ b/cranelift/assembler-x64/src/mem.rs @@ -104,21 +104,24 @@ impl From for AmodeOffset { impl core::fmt::LowerHex for AmodeOffset { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { // This rather complex implementation is necessary to match how - // `capstone` pretty-prints memory immediates. + // `capstone` pretty-prints memory immediates; XED (the alternate form) + // always uses hexadecimal. if self.0 == 0 { return Ok(()); } if self.0 < 0 { write!(f, "-")?; } - if self.0 > 9 || self.0 < -9 { + if f.alternate() || self.0 > 9 || self.0 < -9 { write!(f, "0x")?; } let abs = match self.0.checked_abs() { Some(i) => i, None => -2_147_483_648, }; - core::fmt::LowerHex::fmt(&abs, f) + // Not `LowerHex::fmt(&abs, f)`: `f` may carry the alternate flag, which + // would make the integer emit a second `0x`. + write!(f, "{abs:x}") } } @@ -177,12 +180,18 @@ pub enum DeferredTarget { impl core::fmt::Display for Amode { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { let pointer_width = Size::Quadword; + // XED prints no space after the commas and always states the scale. + let xed = f.alternate(); match self { Amode::ImmReg { simm32, base, .. } => { // Note: size is always 8; the address is 64 bits, // even if the addressed operand is smaller. let base = base.to_string(Some(pointer_width)); - write!(f, "{simm32:x}({base})") + if xed { + write!(f, "{simm32:#x}({base})") + } else { + write!(f, "{simm32:x}({base})") + } } Amode::ImmRegRegShift { simm32, @@ -194,7 +203,9 @@ impl core::fmt::Display for Amode { let base = base.to_string(Some(pointer_width)); let index = index.to_string(pointer_width); let shift = scale.shift(); - if shift > 1 { + if xed { + write!(f, "{simm32:#x}({base},{index},{shift})") + } else if shift > 1 { write!(f, "{simm32:x}({base}, {index}, {shift})") } else { write!(f, "{simm32:x}({base}, {index})") @@ -265,6 +276,12 @@ pub enum GprMem { } impl GprMem { + /// Whether this operand is a memory reference. + #[must_use] + pub fn is_memory(&self) -> bool { + matches!(self, GprMem::Mem(_)) + } + /// Pretty-print the operand. pub fn to_string(&self, size: Size) -> String { match self { @@ -273,6 +290,14 @@ impl GprMem { } } + /// Pretty-print the operand in XED's dialect; see [`Amode`]'s `Display`. + pub fn to_string_xed(&self, size: Size) -> String { + match self { + GprMem::Gpr(gpr) => gpr.to_string(Some(size)), + GprMem::Mem(amode) => alloc::format!("{amode:#}"), + } + } + /// Return the [`RexPrefix`] for each variant of this [`GprMem`]. #[must_use] pub(crate) fn as_rex_prefix(&self, enc_reg: u8, has_w_bit: bool, uses_8bit: bool) -> RexPrefix { @@ -334,6 +359,12 @@ pub enum XmmMem { } impl XmmMem { + /// Whether this operand is a memory reference. + #[must_use] + pub fn is_memory(&self) -> bool { + matches!(self, XmmMem::Mem(_)) + } + /// Pretty-print the operand. pub fn to_string(&self) -> String { match self { @@ -342,6 +373,14 @@ impl XmmMem { } } + /// Pretty-print the operand in XED's dialect; see [`Amode`]'s `Display`. + pub fn to_string_xed(&self) -> String { + match self { + XmmMem::Xmm(xmm) => xmm.to_string(None), + XmmMem::Mem(amode) => alloc::format!("{amode:#}"), + } + } + /// Return the [`RexPrefix`] for each variant of this [`XmmMem`]. #[must_use] pub(crate) fn as_rex_prefix(&self, enc_reg: u8, has_w_bit: bool, uses_8bit: bool) -> RexPrefix { diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 6dec73feaf7a..a5d6780b33f4 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -52,6 +52,9 @@ default = ['fuzz-spec-interpreter', 'v8'] fuzz-spec-interpreter = ['wasmtime-fuzzing/fuzz-spec-interpreter'] v8 = ['wasmtime-fuzzing/v8'] chaos = ["cranelift-control/chaos"] +# Check the x64 assembler against Intel XED in the `misc` fuzzer. Off by default +# because XED is built from source, which requires a C compiler and Python. +fuzz-xed = ["cranelift-assembler-x64/fuzz-xed"] [[bin]] name = "compile" diff --git a/fuzz/fuzz_targets/misc.rs b/fuzz/fuzz_targets/misc.rs index 16f9838231e9..7daa41144891 100644 --- a/fuzz/fuzz_targets/misc.rs +++ b/fuzz/fuzz_targets/misc.rs @@ -69,6 +69,7 @@ run_fuzzers! { api_calls dominator_tree gc_access + assembler_roundtrip_xed } fn pulley_roundtrip(u: Unstructured<'_>) -> Result<()> { @@ -83,6 +84,26 @@ fn assembler_roundtrip(u: Unstructured<'_>) -> Result<()> { Ok(()) } +/// Same as [`assembler_roundtrip`], but checks the assembler against Intel XED +/// rather than Capstone. XED understands newer encodings (e.g. APX) that the +/// bundled Capstone cannot decode. +/// +/// Building XED requires a C compiler and Python, so this is gated behind the +/// `fuzz-xed` feature and is a no-op without it. The fuzzer is always listed in +/// `run_fuzzers!` regardless so that the input-byte discriminants of the other +/// fuzzers stay stable. +fn assembler_roundtrip_xed(u: Unstructured<'_>) -> Result<()> { + #[cfg(all(feature = "fuzz-xed", target_arch = "x86_64", target_os = "linux"))] + { + use cranelift_assembler_x64::{Inst, fuzz}; + let inst: Inst = Arbitrary::arbitrary_take_rest(u)?; + fuzz::roundtrip_xed(&inst); + } + #[cfg(not(all(feature = "fuzz-xed", target_arch = "x86_64", target_os = "linux")))] + let _ = u; + Ok(()) +} + fn memory_accesses(u: Unstructured<'_>) -> Result<()> { wasmtime_fuzzing::oracles::memory::check_memory_accesses(Arbitrary::arbitrary_take_rest(u)?); Ok(()) diff --git a/supply-chain/config.toml b/supply-chain/config.toml index 5dce8044f95d..d51f32d6f79f 100644 --- a/supply-chain/config.toml +++ b/supply-chain/config.toml @@ -475,6 +475,10 @@ criteria = "safe-to-deploy" version = "2.14.0" criteria = "safe-to-deploy" +[[exemptions.target-lexicon]] +version = "0.12.16" +criteria = "safe-to-deploy" + [[exemptions.tempfile]] version = "3.3.0" criteria = "safe-to-deploy" @@ -527,6 +531,10 @@ criteria = "safe-to-deploy" version = "0.4.0" criteria = "safe-to-deploy" +[[exemptions.xed-sys]] +version = "0.6.0+xed-2024.05.20" +criteria = "safe-to-deploy" + [[exemptions.zip]] version = "0.6.6" criteria = "safe-to-deploy"