Skip to main content

cranelift_assembler_x64/
fuzz.rs

1//! A fuzz testing oracle for roundtrip assembly-disassembly.
2//!
3//! This contains manual implementations of the `Arbitrary` trait for types
4//! throughout this crate to avoid depending on the `arbitrary` crate
5//! unconditionally (use the `fuzz` feature instead).
6
7use std::string::{String, ToString};
8use std::vec::Vec;
9use std::{format, println};
10
11use crate::{
12    AmodeOffset, AmodeOffsetPlusKnownOffset, AsReg, CodeSink, DeferredTarget, Feature, Features,
13    Fixed, Gpr, Inst, KnownOffset, NonRspGpr, Registers, TrapCode, Xmm,
14};
15use arbitrary::{Arbitrary, Result, Unstructured};
16use capstone::{Capstone, arch::BuildsCapstone, arch::BuildsCapstoneSyntax, arch::x86};
17
18/// Take a random assembly instruction and check its encoding and
19/// pretty-printing against a known-good disassembler.
20///
21/// This uses Capstone as the disassembler oracle; see `roundtrip_with` for the
22/// oracle-agnostic core.
23///
24/// # Panics
25///
26/// This function panics to express failure as expected by the `arbitrary`
27/// fuzzer infrastructure. It may fail during assembly, disassembly, or when
28/// comparing the disassembled strings.
29pub fn roundtrip(inst: &Inst<FuzzRegs>) {
30    // The bundled capstone build does not disassemble AVX-VNNI instructions, so
31    // the roundtrip oracle has no reference to compare against; skip them. Their
32    // encodings are covered by dedicated filetests.
33    if features_mention(inst.features(), Feature::avx_vnni) {
34        return;
35    }
36
37    roundtrip_with(
38        inst,
39        "capstone",
40        disassemble_capstone,
41        capstone_matches,
42        |i| format!("{i}"),
43    );
44}
45
46/// Like [`roundtrip`], but uses Intel XED as the disassembler oracle instead of
47/// Capstone.
48///
49/// XED understands newer encodings (e.g. APX) that the bundled Capstone does
50/// not, so this is a useful second oracle. It is only available with the
51/// `fuzz-xed` feature (which requires building XED from source).
52///
53/// # Panics
54///
55/// See [`roundtrip`].
56#[cfg(all(feature = "fuzz-xed", target_arch = "x86_64", target_os = "linux"))]
57pub fn roundtrip_xed(inst: &Inst<FuzzRegs>) {
58    roundtrip_with(inst, "xed", disassemble_xed, xed_matches, |i| {
59        format!("{i:#}")
60    });
61}
62
63/// The oracle-agnostic core of [`roundtrip`]: assemble `inst`, disassemble the
64/// resulting bytes with the provided `disassemble` oracle, and check that the
65/// oracle's output matches how `render` prints the instruction, as judged by
66/// the oracle-specific `matches` predicate.
67///
68/// `render` selects the syntax to compare against: `{inst}` is the assembler's
69/// own (what Cranelift's disassembly and the `precise-output` filetests use),
70/// while `{inst:#}` is XED's. Printing directly in the oracle's dialect avoids
71/// having to reconcile the two strings afterwards.
72///
73/// The `oracle` name is only used to label diagnostic output on failure.
74fn roundtrip_with(
75    inst: &Inst<FuzzRegs>,
76    oracle: &str,
77    disassemble: impl Fn(&[u8], &Inst<FuzzRegs>) -> String,
78    matches: impl Fn(&str, &str) -> bool,
79    render: impl Fn(&Inst<FuzzRegs>) -> String,
80) {
81    // Check that we can actually assemble this instruction.
82    let assembled = assemble(inst);
83    let expected = disassemble(&assembled, inst);
84
85    // Check that our pretty-printed output matches the known-good output. Trim
86    // off the instruction offset first.
87    let expected = expected.split_once(' ').unwrap().1;
88    let actual = render(inst);
89    if !matches(expected, &actual) {
90        println!("> {inst}");
91        println!("  debug: {inst:x?}");
92        println!("  assembled: {}", pretty_print_hexadecimal(&assembled));
93        println!("  expected ({oracle}): {expected}");
94        println!("  actual (to_string):  {actual}");
95        assert_eq!(expected, &actual);
96    }
97}
98
99/// Whether an instruction's feature term references `target`; used to skip
100/// instructions the disassembler oracle cannot handle.
101fn features_mention(features: &Features, target: Feature) -> bool {
102    match features {
103        Features::And(a, b) | Features::Or(a, b) => {
104            features_mention(a, target) || features_mention(b, target)
105        }
106        Features::Feature(f) => *f == target,
107    }
108}
109
110/// Comparison predicate for the Capstone oracle: exact match, or match after
111/// applying Capstone-specific normalization ([`fix_up`]) to the assembler
112/// output.
113fn capstone_matches(expected: &str, actual: &str) -> bool {
114    expected == actual || expected.trim() == fix_up(actual)
115}
116
117/// Use this assembler to emit machine code into a byte buffer.
118///
119/// This will skip any traps or label registrations, but this is fine for the
120/// single-instruction disassembly we're doing here.
121fn assemble(inst: &Inst<FuzzRegs>) -> Vec<u8> {
122    let mut sink = TestCodeSink::default();
123    inst.encode(&mut sink);
124    sink.patch_labels_as_if_they_referred_to_end();
125    sink.buf
126}
127
128#[derive(Default)]
129struct TestCodeSink {
130    buf: Vec<u8>,
131    offsets_using_label: Vec<usize>,
132}
133
134impl TestCodeSink {
135    /// References to labels, e.g. RIP-relative addressing, is stored with an
136    /// adjustment that takes into account the distance from the relative offset
137    /// to the end of the instruction, where the offset is relative to. That
138    /// means that to indeed make the offset relative to the end of the
139    /// instruction, which is what we pretend all labels are bound to, it's
140    /// required that this adjustment is taken into account.
141    ///
142    /// This function will iterate over all labels bound to this code sink and
143    /// pretend the label is found at the end of the `buf`. That means that the
144    /// distance from the label to the end of `buf` minus 4, which is the width
145    /// of the offset, is added to what's already present in the encoding buffer.
146    ///
147    /// This is effectively undoing the `bytes_at_end` adjustment that's part of
148    /// `Amode::RipRelative` addressing.
149    fn patch_labels_as_if_they_referred_to_end(&mut self) {
150        let len = i32::try_from(self.buf.len()).unwrap();
151        for offset in self.offsets_using_label.iter() {
152            let range = self.buf[*offset..].first_chunk_mut::<4>().unwrap();
153            let offset = i32::try_from(*offset).unwrap() + 4;
154            let rel_distance = len - offset;
155            *range = (i32::from_le_bytes(*range) + rel_distance).to_le_bytes();
156        }
157    }
158}
159
160impl CodeSink for TestCodeSink {
161    fn put1(&mut self, v: u8) {
162        self.buf.extend_from_slice(&[v]);
163    }
164
165    fn put2(&mut self, v: u16) {
166        self.buf.extend_from_slice(&v.to_le_bytes());
167    }
168
169    fn put4(&mut self, v: u32) {
170        self.buf.extend_from_slice(&v.to_le_bytes());
171    }
172
173    fn put8(&mut self, v: u64) {
174        self.buf.extend_from_slice(&v.to_le_bytes());
175    }
176
177    fn add_trap(&mut self, _: TrapCode) {}
178
179    fn use_target(&mut self, _: DeferredTarget) {
180        let offset = self.buf.len();
181        self.offsets_using_label.push(offset);
182    }
183
184    fn known_offset(&self, target: KnownOffset) -> i32 {
185        panic!("unsupported known target {target:?}")
186    }
187}
188
189/// Disassemble a single instruction with Capstone, returning its AT&T-syntax
190/// string. This is the default [`roundtrip`] oracle.
191///
192/// Building a new `Capstone` each time is suboptimal (TODO).
193fn disassemble_capstone(assembled: &[u8], original: &Inst<FuzzRegs>) -> String {
194    let cs = Capstone::new()
195        .x86()
196        .mode(x86::ArchMode::Mode64)
197        .syntax(x86::ArchSyntax::Att)
198        .detail(true)
199        .build()
200        .expect("failed to create Capstone object");
201    let insts = cs
202        .disasm_all(assembled, 0x0)
203        .expect("failed to disassemble");
204
205    if insts.len() != 1 {
206        println!("> {original}");
207        println!("  debug: {original:x?}");
208        println!("  assembled: {}", pretty_print_hexadecimal(&assembled));
209        assert_eq!(insts.len(), 1, "not a single instruction");
210    }
211
212    let inst = insts.first().expect("at least one instruction");
213    if assembled.len() != inst.len() {
214        println!("> {original}");
215        println!("  debug: {original:x?}");
216        println!("  assembled: {}", pretty_print_hexadecimal(&assembled));
217        println!(
218            "  capstone-assembled: {}",
219            pretty_print_hexadecimal(inst.bytes())
220        );
221        assert_eq!(assembled.len(), inst.len(), "extra bytes not disassembled");
222    }
223
224    inst.to_string()
225}
226
227/// Disassemble a single instruction with Intel XED, returning a string in the
228/// same shape as [`disassemble_capstone`] (a leading offset token, a space,
229/// then the AT&T-syntax instruction) so that [`roundtrip_with`] can compare it
230/// uniformly.
231#[cfg(all(feature = "fuzz-xed", target_arch = "x86_64", target_os = "linux"))]
232fn disassemble_xed(assembled: &[u8], original: &Inst<FuzzRegs>) -> String {
233    use core::ffi::c_void;
234    use std::sync::Once;
235    use xed_sys::*;
236
237    // XED requires a one-time global table initialization before any decode.
238    static INIT: Once = Once::new();
239    // SAFETY: `xed_tables_init` is safe to call; `Once` guarantees it runs
240    // exactly once even across threads.
241    INIT.call_once(|| unsafe { xed_tables_init() });
242
243    // SAFETY: all of the following are standard XED decode/format calls
244    // operating on stack-allocated, properly initialized structures.
245    unsafe {
246        let mut xedd: xed_decoded_inst_t = core::mem::zeroed();
247        xed_decoded_inst_zero(&mut xedd);
248        xed_decoded_inst_set_mode(&mut xedd, XED_MACHINE_MODE_LONG_64, XED_ADDRESS_WIDTH_64b);
249
250        let error = xed_decode(
251            &mut xedd,
252            assembled.as_ptr(),
253            assembled.len() as core::ffi::c_uint,
254        );
255        if error != XED_ERROR_NONE {
256            println!("> {original}");
257            println!("  debug: {original:x?}");
258            println!("  assembled: {}", pretty_print_hexadecimal(assembled));
259            let name = core::ffi::CStr::from_ptr(xed_error_enum_t2str(error));
260            panic!("xed failed to decode: {}", name.to_string_lossy());
261        }
262
263        // XED must consume exactly the bytes we emitted; a shorter length means
264        // trailing bytes were not part of the instruction.
265        let decoded_len = xed_decoded_inst_get_length(&xedd) as usize;
266        if decoded_len != assembled.len() {
267            println!("> {original}");
268            println!("  debug: {original:x?}");
269            println!("  assembled: {}", pretty_print_hexadecimal(assembled));
270            assert_eq!(
271                decoded_len,
272                assembled.len(),
273                "xed did not consume all bytes"
274            );
275        }
276
277        // Format in AT&T syntax to match the assembler's own pretty-printing.
278        let mut buf = [0i8; 256];
279        let ok = xed_format_context(
280            XED_SYNTAX_ATT,
281            &xedd,
282            buf.as_mut_ptr(),
283            buf.len() as core::ffi::c_int,
284            0,
285            core::ptr::null_mut::<c_void>(),
286            None,
287        );
288        assert!(ok != 0, "xed failed to format instruction");
289
290        let disasm = core::ffi::CStr::from_ptr(buf.as_ptr())
291            .to_string_lossy()
292            .into_owned();
293
294        // Prepend a fake offset token so the shape matches Capstone's
295        // `0x0: <inst>` output that `roundtrip_with` expects.
296        format!("0: {disasm}")
297    }
298}
299
300fn pretty_print_hexadecimal(hex: &[u8]) -> String {
301    use core::fmt::Write;
302    let mut s = String::with_capacity(hex.len() * 2);
303    for b in hex {
304        write!(&mut s, "{b:02X}").unwrap();
305    }
306    s
307}
308
309/// See `replace_signed_immediates`.
310macro_rules! hex_print_signed_imm {
311    ($hex:expr, $from:ty => $to:ty) => {{
312        let imm = <$from>::from_str_radix($hex, 16).unwrap() as $to;
313        let mut simm = String::new();
314        if imm < 0 {
315            simm.push_str("-");
316        }
317        let abs = match imm.checked_abs() {
318            Some(i) => i,
319            None => <$to>::MIN,
320        };
321        if imm > -10 && imm < 10 {
322            simm.push_str(&format!("{:x}", abs));
323        } else {
324            simm.push_str(&format!("0x{:x}", abs));
325        }
326        simm
327    }};
328}
329
330/// Replace signed immediates in the disassembly with their unsigned hexadecimal
331/// equivalent. This is only necessary to match `capstone`'s complex
332/// pretty-printing rules; e.g. `capstone` will:
333/// - omit the `0x` prefix when printing `0x0` as `0`.
334/// - omit the `0x` prefix when print small values (less than 10)
335/// - print negative values as `-0x...` (signed hex) instead of `0xff...`
336///   (normal hex)
337/// - print `mov` immediates as base-10 instead of base-16 (?!).
338fn replace_signed_immediates(dis: &str) -> alloc::borrow::Cow<'_, str> {
339    match dis.find('$') {
340        None => dis.into(),
341        Some(idx) => {
342            let (prefix, rest) = dis.split_at(idx + 1); // Skip the '$'.
343            let (_, rest) = chomp("-", rest); // Skip the '-' if it's there.
344            let (_, rest) = chomp("0x", rest); // Skip the '0x' if it's there.
345            let n = rest.chars().take_while(char::is_ascii_hexdigit).count();
346            let (hex, rest) = rest.split_at(n); // Split at next non-hex character.
347            let simm = if dis.starts_with("mov") {
348                u64::from_str_radix(hex, 16).unwrap().to_string()
349            } else {
350                match hex.len() {
351                    1 | 2 => hex_print_signed_imm!(hex, u8 => i8),
352                    4 => hex_print_signed_imm!(hex, u16 => i16),
353                    8 => hex_print_signed_imm!(hex, u32 => i32),
354                    16 => hex_print_signed_imm!(hex, u64 => i64),
355                    _ => panic!("unexpected length for hex: {hex}"),
356                }
357            };
358            format!("{prefix}{simm}{rest}").into()
359        }
360    }
361}
362
363// See `replace_signed_immediates`.
364fn chomp<'a>(pat: &str, s: &'a str) -> (&'a str, &'a str) {
365    if s.starts_with(pat) {
366        s.split_at(pat.len())
367    } else {
368        ("", s)
369    }
370}
371
372#[test]
373fn replace() {
374    assert_eq!(
375        replace_signed_immediates("andl $0xffffff9a, %r11d"),
376        "andl $-0x66, %r11d"
377    );
378    assert_eq!(
379        replace_signed_immediates("xorq $0xffffffffffffffbc, 0x7f139ecc(%r9)"),
380        "xorq $-0x44, 0x7f139ecc(%r9)"
381    );
382    assert_eq!(
383        replace_signed_immediates("subl $0x3ca77a19, -0x1a030f40(%r14)"),
384        "subl $0x3ca77a19, -0x1a030f40(%r14)"
385    );
386    assert_eq!(
387        replace_signed_immediates("movq $0xffffffff864ae103, %rsi"),
388        "movq $18446744071667638531, %rsi"
389    );
390}
391
392/// Remove everything after the first semicolon in the disassembly and trim any
393/// trailing spaces. This is necessary to remove the implicit operands we end up
394/// printing for Cranelift's sake.
395fn remove_after_semicolon(dis: &str) -> &str {
396    match dis.find(';') {
397        None => dis,
398        Some(idx) => {
399            let (prefix, _) = dis.split_at(idx);
400            prefix.trim()
401        }
402    }
403}
404
405#[test]
406fn remove_after_parenthesis_test() {
407    assert_eq!(
408        remove_after_semicolon("imulb 0x7658eddd(%rcx) ;; implicit: %ax"),
409        "imulb 0x7658eddd(%rcx)"
410    );
411}
412
413/// Run some post-processing on the disassembly to make it match Capstone.
414fn fix_up(dis: &str) -> alloc::borrow::Cow<'_, str> {
415    let dis = remove_after_semicolon(dis);
416    replace_signed_immediates(&dis)
417}
418
419/// Comparison predicate for the XED oracle.
420///
421/// The assembler renders the instruction in XED's own dialect (`{inst:#}`), so
422/// the two strings agree exactly apart from the extra padding XED inserts after
423/// the mnemonic.
424#[cfg(all(feature = "fuzz-xed", target_arch = "x86_64", target_os = "linux"))]
425fn xed_matches(expected: &str, actual: &str) -> bool {
426    expected.split_whitespace().eq(actual.split_whitespace())
427}
428
429/// Fuzz-specific registers.
430///
431/// For the fuzzer, we do not need any fancy register types; see [`FuzzReg`].
432#[derive(Clone, Arbitrary, Debug)]
433pub struct FuzzRegs;
434
435impl Registers for FuzzRegs {
436    type ReadGpr = FuzzReg;
437    type ReadWriteGpr = FuzzReg;
438    type WriteGpr = FuzzReg;
439    type ReadXmm = FuzzReg;
440    type ReadWriteXmm = FuzzReg;
441    type WriteXmm = FuzzReg;
442}
443
444/// A simple `u8` register type for fuzzing only.
445#[derive(Clone, Copy, Debug, PartialEq)]
446pub struct FuzzReg(u8);
447
448impl<'a> Arbitrary<'a> for FuzzReg {
449    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
450        Ok(Self(u.int_in_range(0..=15)?))
451    }
452}
453
454impl AsReg for FuzzReg {
455    fn new(enc: u8) -> Self {
456        Self(enc)
457    }
458    fn enc(&self) -> u8 {
459        self.0
460    }
461}
462
463impl Arbitrary<'_> for AmodeOffset {
464    fn arbitrary(u: &mut Unstructured<'_>) -> Result<Self> {
465        // Custom implementation to try to generate some "interesting" offsets.
466        // For example choose either an arbitrary 8-bit or 32-bit number as the
467        // base, and then optionally shift that number to the left to create
468        // multiples of constants. This can help stress some of the more
469        // interesting encodings in EVEX instructions for example.
470        let base = if u.arbitrary()? {
471            i32::from(u.arbitrary::<i8>()?)
472        } else {
473            u.arbitrary::<i32>()?
474        };
475        Ok(match u.int_in_range(0..=5)? {
476            0 => AmodeOffset::ZERO,
477            n => AmodeOffset::new(base << (n - 1)),
478        })
479    }
480}
481
482impl Arbitrary<'_> for AmodeOffsetPlusKnownOffset {
483    fn arbitrary(u: &mut Unstructured<'_>) -> Result<Self> {
484        // For now, we don't generate offsets (TODO).
485        Ok(Self {
486            simm32: AmodeOffset::arbitrary(u)?,
487            offset: None,
488        })
489    }
490}
491
492impl<R: AsReg, const E: u8> Arbitrary<'_> for Fixed<R, E> {
493    fn arbitrary(_: &mut Unstructured<'_>) -> Result<Self> {
494        Ok(Self::new(E))
495    }
496}
497
498impl<R: AsReg> Arbitrary<'_> for NonRspGpr<R> {
499    fn arbitrary(u: &mut Unstructured<'_>) -> Result<Self> {
500        use crate::gpr::enc::*;
501        let gpr = u.choose(&[
502            RAX, RCX, RDX, RBX, RBP, RSI, RDI, R8, R9, R10, R11, R12, R13, R14, R15,
503        ])?;
504        Ok(Self::new(R::new(*gpr)))
505    }
506}
507impl<'a, R: AsReg> Arbitrary<'a> for Gpr<R> {
508    fn arbitrary(u: &mut Unstructured<'a>) -> Result<Self> {
509        Ok(Self(R::new(u.int_in_range(0..=15)?)))
510    }
511}
512impl<'a, R: AsReg> Arbitrary<'a> for Xmm<R> {
513    fn arbitrary(u: &mut Unstructured<'a>) -> Result<Self> {
514        Ok(Self(R::new(u.int_in_range(0..=15)?)))
515    }
516}
517
518/// Helper trait that's used to be the same as `Registers` except with an extra
519/// `for<'a> Arbitrary<'a>` bound on all of the associated types.
520pub trait RegistersArbitrary:
521    Registers<
522        ReadGpr: for<'a> Arbitrary<'a>,
523        ReadWriteGpr: for<'a> Arbitrary<'a>,
524        WriteGpr: for<'a> Arbitrary<'a>,
525        ReadXmm: for<'a> Arbitrary<'a>,
526        ReadWriteXmm: for<'a> Arbitrary<'a>,
527        WriteXmm: for<'a> Arbitrary<'a>,
528    >
529{
530}
531
532impl<R> RegistersArbitrary for R
533where
534    R: Registers,
535    R::ReadGpr: for<'a> Arbitrary<'a>,
536    R::ReadWriteGpr: for<'a> Arbitrary<'a>,
537    R::WriteGpr: for<'a> Arbitrary<'a>,
538    R::ReadXmm: for<'a> Arbitrary<'a>,
539    R::ReadWriteXmm: for<'a> Arbitrary<'a>,
540    R::WriteXmm: for<'a> Arbitrary<'a>,
541{
542}
543
544#[cfg(test)]
545mod test {
546    use super::*;
547    use arbtest::arbtest;
548    use std::sync::atomic::{AtomicUsize, Ordering};
549
550    #[test]
551    fn smoke() {
552        let count = AtomicUsize::new(0);
553        arbtest(|u| {
554            let inst: Inst<FuzzRegs> = u.arbitrary()?;
555            roundtrip(&inst);
556            println!("#{}: {inst}", count.fetch_add(1, Ordering::SeqCst));
557            Ok(())
558        })
559        .budget_ms(1_000);
560
561        // This will run the `roundtrip` fuzzer for one second. To repeatably
562        // test a single input, append `.seed(0x<failing seed>)`.
563    }
564
565    #[test]
566    fn callq() {
567        for i in -500..500 {
568            println!("immediate: {i}");
569            let inst = crate::inst::callq_d::new(i);
570            roundtrip(&inst.into());
571        }
572    }
573
574    /// Same as [`smoke`], but exercises the Intel XED oracle. Only available
575    /// with the `fuzz-xed` feature.
576    ///
577    /// The instruction is printed in XED's dialect (`{inst:#}`) so that the two
578    /// can be compared directly. Run explicitly with
579    /// `cargo test --features fuzz-xed -- smoke_xed`.
580    #[cfg(all(feature = "fuzz-xed", target_arch = "x86_64", target_os = "linux"))]
581    #[test]
582    fn smoke_xed() {
583        let count = AtomicUsize::new(0);
584        arbtest(|u| {
585            let inst: Inst<FuzzRegs> = u.arbitrary()?;
586            roundtrip_xed(&inst);
587            println!("#{}: {inst}", count.fetch_add(1, Ordering::SeqCst));
588            Ok(())
589        })
590        .budget_ms(1_000);
591    }
592}