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/// # Panics
22///
23/// This function panics to express failure as expected by the `arbitrary`
24/// fuzzer infrastructure. It may fail during assembly, disassembly, or when
25/// comparing the disassembled strings.
26pub fn roundtrip(inst: &Inst<FuzzRegs>) {
27    // The bundled capstone build does not disassemble AVX-VNNI instructions, so
28    // the roundtrip oracle has no reference to compare against; skip them. Their
29    // encodings are covered by dedicated filetests.
30    if features_mention(inst.features(), Feature::avx_vnni) {
31        return;
32    }
33
34    // Check that we can actually assemble this instruction.
35    let assembled = assemble(inst);
36    let expected = disassemble(&assembled, inst);
37
38    // Check that our pretty-printed output matches the known-good output. Trim
39    // off the instruction offset first.
40    let expected = expected.split_once(' ').unwrap().1;
41    let actual = inst.to_string();
42    if expected != actual && expected.trim() != fix_up(&actual) {
43        println!("> {inst}");
44        println!("  debug: {inst:x?}");
45        println!("  assembled: {}", pretty_print_hexadecimal(&assembled));
46        println!("  expected (capstone): {expected}");
47        println!("  actual (to_string):  {actual}");
48        assert_eq!(expected, &actual);
49    }
50}
51
52/// Whether an instruction's feature term references `target`; used to skip
53/// instructions the disassembler oracle cannot handle.
54fn features_mention(features: &Features, target: Feature) -> bool {
55    match features {
56        Features::And(a, b) | Features::Or(a, b) => {
57            features_mention(a, target) || features_mention(b, target)
58        }
59        Features::Feature(f) => *f == target,
60    }
61}
62
63/// Use this assembler to emit machine code into a byte buffer.
64///
65/// This will skip any traps or label registrations, but this is fine for the
66/// single-instruction disassembly we're doing here.
67fn assemble(inst: &Inst<FuzzRegs>) -> Vec<u8> {
68    let mut sink = TestCodeSink::default();
69    inst.encode(&mut sink);
70    sink.patch_labels_as_if_they_referred_to_end();
71    sink.buf
72}
73
74#[derive(Default)]
75struct TestCodeSink {
76    buf: Vec<u8>,
77    offsets_using_label: Vec<usize>,
78}
79
80impl TestCodeSink {
81    /// References to labels, e.g. RIP-relative addressing, is stored with an
82    /// adjustment that takes into account the distance from the relative offset
83    /// to the end of the instruction, where the offset is relative to. That
84    /// means that to indeed make the offset relative to the end of the
85    /// instruction, which is what we pretend all labels are bound to, it's
86    /// required that this adjustment is taken into account.
87    ///
88    /// This function will iterate over all labels bound to this code sink and
89    /// pretend the label is found at the end of the `buf`. That means that the
90    /// distance from the label to the end of `buf` minus 4, which is the width
91    /// of the offset, is added to what's already present in the encoding buffer.
92    ///
93    /// This is effectively undoing the `bytes_at_end` adjustment that's part of
94    /// `Amode::RipRelative` addressing.
95    fn patch_labels_as_if_they_referred_to_end(&mut self) {
96        let len = i32::try_from(self.buf.len()).unwrap();
97        for offset in self.offsets_using_label.iter() {
98            let range = self.buf[*offset..].first_chunk_mut::<4>().unwrap();
99            let offset = i32::try_from(*offset).unwrap() + 4;
100            let rel_distance = len - offset;
101            *range = (i32::from_le_bytes(*range) + rel_distance).to_le_bytes();
102        }
103    }
104}
105
106impl CodeSink for TestCodeSink {
107    fn put1(&mut self, v: u8) {
108        self.buf.extend_from_slice(&[v]);
109    }
110
111    fn put2(&mut self, v: u16) {
112        self.buf.extend_from_slice(&v.to_le_bytes());
113    }
114
115    fn put4(&mut self, v: u32) {
116        self.buf.extend_from_slice(&v.to_le_bytes());
117    }
118
119    fn put8(&mut self, v: u64) {
120        self.buf.extend_from_slice(&v.to_le_bytes());
121    }
122
123    fn add_trap(&mut self, _: TrapCode) {}
124
125    fn use_target(&mut self, _: DeferredTarget) {
126        let offset = self.buf.len();
127        self.offsets_using_label.push(offset);
128    }
129
130    fn known_offset(&self, target: KnownOffset) -> i32 {
131        panic!("unsupported known target {target:?}")
132    }
133}
134
135/// Building a new `Capstone` each time is suboptimal (TODO).
136fn disassemble(assembled: &[u8], original: &Inst<FuzzRegs>) -> String {
137    let cs = Capstone::new()
138        .x86()
139        .mode(x86::ArchMode::Mode64)
140        .syntax(x86::ArchSyntax::Att)
141        .detail(true)
142        .build()
143        .expect("failed to create Capstone object");
144    let insts = cs
145        .disasm_all(assembled, 0x0)
146        .expect("failed to disassemble");
147
148    if insts.len() != 1 {
149        println!("> {original}");
150        println!("  debug: {original:x?}");
151        println!("  assembled: {}", pretty_print_hexadecimal(&assembled));
152        assert_eq!(insts.len(), 1, "not a single instruction");
153    }
154
155    let inst = insts.first().expect("at least one instruction");
156    if assembled.len() != inst.len() {
157        println!("> {original}");
158        println!("  debug: {original:x?}");
159        println!("  assembled: {}", pretty_print_hexadecimal(&assembled));
160        println!(
161            "  capstone-assembled: {}",
162            pretty_print_hexadecimal(inst.bytes())
163        );
164        assert_eq!(assembled.len(), inst.len(), "extra bytes not disassembled");
165    }
166
167    inst.to_string()
168}
169
170fn pretty_print_hexadecimal(hex: &[u8]) -> String {
171    use core::fmt::Write;
172    let mut s = String::with_capacity(hex.len() * 2);
173    for b in hex {
174        write!(&mut s, "{b:02X}").unwrap();
175    }
176    s
177}
178
179/// See `replace_signed_immediates`.
180macro_rules! hex_print_signed_imm {
181    ($hex:expr, $from:ty => $to:ty) => {{
182        let imm = <$from>::from_str_radix($hex, 16).unwrap() as $to;
183        let mut simm = String::new();
184        if imm < 0 {
185            simm.push_str("-");
186        }
187        let abs = match imm.checked_abs() {
188            Some(i) => i,
189            None => <$to>::MIN,
190        };
191        if imm > -10 && imm < 10 {
192            simm.push_str(&format!("{:x}", abs));
193        } else {
194            simm.push_str(&format!("0x{:x}", abs));
195        }
196        simm
197    }};
198}
199
200/// Replace signed immediates in the disassembly with their unsigned hexadecimal
201/// equivalent. This is only necessary to match `capstone`'s complex
202/// pretty-printing rules; e.g. `capstone` will:
203/// - omit the `0x` prefix when printing `0x0` as `0`.
204/// - omit the `0x` prefix when print small values (less than 10)
205/// - print negative values as `-0x...` (signed hex) instead of `0xff...`
206///   (normal hex)
207/// - print `mov` immediates as base-10 instead of base-16 (?!).
208fn replace_signed_immediates(dis: &str) -> alloc::borrow::Cow<'_, str> {
209    match dis.find('$') {
210        None => dis.into(),
211        Some(idx) => {
212            let (prefix, rest) = dis.split_at(idx + 1); // Skip the '$'.
213            let (_, rest) = chomp("-", rest); // Skip the '-' if it's there.
214            let (_, rest) = chomp("0x", rest); // Skip the '0x' if it's there.
215            let n = rest.chars().take_while(char::is_ascii_hexdigit).count();
216            let (hex, rest) = rest.split_at(n); // Split at next non-hex character.
217            let simm = if dis.starts_with("mov") {
218                u64::from_str_radix(hex, 16).unwrap().to_string()
219            } else {
220                match hex.len() {
221                    1 | 2 => hex_print_signed_imm!(hex, u8 => i8),
222                    4 => hex_print_signed_imm!(hex, u16 => i16),
223                    8 => hex_print_signed_imm!(hex, u32 => i32),
224                    16 => hex_print_signed_imm!(hex, u64 => i64),
225                    _ => panic!("unexpected length for hex: {hex}"),
226                }
227            };
228            format!("{prefix}{simm}{rest}").into()
229        }
230    }
231}
232
233// See `replace_signed_immediates`.
234fn chomp<'a>(pat: &str, s: &'a str) -> (&'a str, &'a str) {
235    if s.starts_with(pat) {
236        s.split_at(pat.len())
237    } else {
238        ("", s)
239    }
240}
241
242#[test]
243fn replace() {
244    assert_eq!(
245        replace_signed_immediates("andl $0xffffff9a, %r11d"),
246        "andl $-0x66, %r11d"
247    );
248    assert_eq!(
249        replace_signed_immediates("xorq $0xffffffffffffffbc, 0x7f139ecc(%r9)"),
250        "xorq $-0x44, 0x7f139ecc(%r9)"
251    );
252    assert_eq!(
253        replace_signed_immediates("subl $0x3ca77a19, -0x1a030f40(%r14)"),
254        "subl $0x3ca77a19, -0x1a030f40(%r14)"
255    );
256    assert_eq!(
257        replace_signed_immediates("movq $0xffffffff864ae103, %rsi"),
258        "movq $18446744071667638531, %rsi"
259    );
260}
261
262/// Remove everything after the first semicolon in the disassembly and trim any
263/// trailing spaces. This is necessary to remove the implicit operands we end up
264/// printing for Cranelift's sake.
265fn remove_after_semicolon(dis: &str) -> &str {
266    match dis.find(';') {
267        None => dis,
268        Some(idx) => {
269            let (prefix, _) = dis.split_at(idx);
270            prefix.trim()
271        }
272    }
273}
274
275#[test]
276fn remove_after_parenthesis_test() {
277    assert_eq!(
278        remove_after_semicolon("imulb 0x7658eddd(%rcx) ;; implicit: %ax"),
279        "imulb 0x7658eddd(%rcx)"
280    );
281}
282
283/// Run some post-processing on the disassembly to make it match Capstone.
284fn fix_up(dis: &str) -> alloc::borrow::Cow<'_, str> {
285    let dis = remove_after_semicolon(dis);
286    replace_signed_immediates(&dis)
287}
288
289/// Fuzz-specific registers.
290///
291/// For the fuzzer, we do not need any fancy register types; see [`FuzzReg`].
292#[derive(Clone, Arbitrary, Debug)]
293pub struct FuzzRegs;
294
295impl Registers for FuzzRegs {
296    type ReadGpr = FuzzReg;
297    type ReadWriteGpr = FuzzReg;
298    type WriteGpr = FuzzReg;
299    type ReadXmm = FuzzReg;
300    type ReadWriteXmm = FuzzReg;
301    type WriteXmm = FuzzReg;
302}
303
304/// A simple `u8` register type for fuzzing only.
305#[derive(Clone, Copy, Debug, PartialEq)]
306pub struct FuzzReg(u8);
307
308impl<'a> Arbitrary<'a> for FuzzReg {
309    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
310        Ok(Self(u.int_in_range(0..=15)?))
311    }
312}
313
314impl AsReg for FuzzReg {
315    fn new(enc: u8) -> Self {
316        Self(enc)
317    }
318    fn enc(&self) -> u8 {
319        self.0
320    }
321}
322
323impl Arbitrary<'_> for AmodeOffset {
324    fn arbitrary(u: &mut Unstructured<'_>) -> Result<Self> {
325        // Custom implementation to try to generate some "interesting" offsets.
326        // For example choose either an arbitrary 8-bit or 32-bit number as the
327        // base, and then optionally shift that number to the left to create
328        // multiples of constants. This can help stress some of the more
329        // interesting encodings in EVEX instructions for example.
330        let base = if u.arbitrary()? {
331            i32::from(u.arbitrary::<i8>()?)
332        } else {
333            u.arbitrary::<i32>()?
334        };
335        Ok(match u.int_in_range(0..=5)? {
336            0 => AmodeOffset::ZERO,
337            n => AmodeOffset::new(base << (n - 1)),
338        })
339    }
340}
341
342impl Arbitrary<'_> for AmodeOffsetPlusKnownOffset {
343    fn arbitrary(u: &mut Unstructured<'_>) -> Result<Self> {
344        // For now, we don't generate offsets (TODO).
345        Ok(Self {
346            simm32: AmodeOffset::arbitrary(u)?,
347            offset: None,
348        })
349    }
350}
351
352impl<R: AsReg, const E: u8> Arbitrary<'_> for Fixed<R, E> {
353    fn arbitrary(_: &mut Unstructured<'_>) -> Result<Self> {
354        Ok(Self::new(E))
355    }
356}
357
358impl<R: AsReg> Arbitrary<'_> for NonRspGpr<R> {
359    fn arbitrary(u: &mut Unstructured<'_>) -> Result<Self> {
360        use crate::gpr::enc::*;
361        let gpr = u.choose(&[
362            RAX, RCX, RDX, RBX, RBP, RSI, RDI, R8, R9, R10, R11, R12, R13, R14, R15,
363        ])?;
364        Ok(Self::new(R::new(*gpr)))
365    }
366}
367impl<'a, R: AsReg> Arbitrary<'a> for Gpr<R> {
368    fn arbitrary(u: &mut Unstructured<'a>) -> Result<Self> {
369        Ok(Self(R::new(u.int_in_range(0..=15)?)))
370    }
371}
372impl<'a, R: AsReg> Arbitrary<'a> for Xmm<R> {
373    fn arbitrary(u: &mut Unstructured<'a>) -> Result<Self> {
374        Ok(Self(R::new(u.int_in_range(0..=15)?)))
375    }
376}
377
378/// Helper trait that's used to be the same as `Registers` except with an extra
379/// `for<'a> Arbitrary<'a>` bound on all of the associated types.
380pub trait RegistersArbitrary:
381    Registers<
382        ReadGpr: for<'a> Arbitrary<'a>,
383        ReadWriteGpr: for<'a> Arbitrary<'a>,
384        WriteGpr: for<'a> Arbitrary<'a>,
385        ReadXmm: for<'a> Arbitrary<'a>,
386        ReadWriteXmm: for<'a> Arbitrary<'a>,
387        WriteXmm: for<'a> Arbitrary<'a>,
388    >
389{
390}
391
392impl<R> RegistersArbitrary for R
393where
394    R: Registers,
395    R::ReadGpr: for<'a> Arbitrary<'a>,
396    R::ReadWriteGpr: for<'a> Arbitrary<'a>,
397    R::WriteGpr: for<'a> Arbitrary<'a>,
398    R::ReadXmm: for<'a> Arbitrary<'a>,
399    R::ReadWriteXmm: for<'a> Arbitrary<'a>,
400    R::WriteXmm: for<'a> Arbitrary<'a>,
401{
402}
403
404#[cfg(test)]
405mod test {
406    use super::*;
407    use arbtest::arbtest;
408    use std::sync::atomic::{AtomicUsize, Ordering};
409
410    #[test]
411    fn smoke() {
412        let count = AtomicUsize::new(0);
413        arbtest(|u| {
414            let inst: Inst<FuzzRegs> = u.arbitrary()?;
415            roundtrip(&inst);
416            println!("#{}: {inst}", count.fetch_add(1, Ordering::SeqCst));
417            Ok(())
418        })
419        .budget_ms(1_000);
420
421        // This will run the `roundtrip` fuzzer for one second. To repeatably
422        // test a single input, append `.seed(0x<failing seed>)`.
423    }
424
425    #[test]
426    fn callq() {
427        for i in -500..500 {
428            println!("immediate: {i}");
429            let inst = crate::inst::callq_d::new(i);
430            roundtrip(&inst.into());
431        }
432    }
433}