1use 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
18pub fn roundtrip(inst: &Inst<FuzzRegs>) {
30 if features_mention(inst.features(), Feature::avx_vnni) {
34 return;
35 }
36
37 if features_mention(inst.features(), Feature::apx) {
41 return;
42 }
43
44 roundtrip_with(
45 inst,
46 "capstone",
47 disassemble_capstone,
48 capstone_matches,
49 |i| format!("{i}"),
50 );
51}
52
53#[cfg(all(feature = "fuzz-xed", target_arch = "x86_64", target_os = "linux"))]
64pub fn roundtrip_xed(inst: &Inst<FuzzRegs>) {
65 roundtrip_with(inst, "xed", disassemble_xed, xed_matches, |i| {
66 format!("{i:#}")
67 });
68}
69
70fn roundtrip_with(
82 inst: &Inst<FuzzRegs>,
83 oracle: &str,
84 disassemble: impl Fn(&[u8], &Inst<FuzzRegs>) -> String,
85 matches: impl Fn(&str, &str) -> bool,
86 render: impl Fn(&Inst<FuzzRegs>) -> String,
87) {
88 let assembled = assemble(inst);
90 let expected = disassemble(&assembled, inst);
91
92 let expected = expected.split_once(' ').unwrap().1;
95 let actual = render(inst);
96 if !matches(expected, &actual) {
97 println!("> {inst}");
98 println!(" debug: {inst:x?}");
99 println!(" assembled: {}", pretty_print_hexadecimal(&assembled));
100 println!(" expected ({oracle}): {expected}");
101 println!(" actual (to_string): {actual}");
102 assert_eq!(expected, &actual);
103 }
104}
105
106fn features_mention(features: &Features, target: Feature) -> bool {
109 match features {
110 Features::And(a, b) | Features::Or(a, b) => {
111 features_mention(a, target) || features_mention(b, target)
112 }
113 Features::Feature(f) => *f == target,
114 }
115}
116
117fn capstone_matches(expected: &str, actual: &str) -> bool {
121 expected == actual || expected.trim() == fix_up(actual)
122}
123
124fn assemble(inst: &Inst<FuzzRegs>) -> Vec<u8> {
129 let mut sink = TestCodeSink::default();
130 inst.encode(&mut sink);
131 sink.patch_labels_as_if_they_referred_to_end();
132 sink.buf
133}
134
135#[derive(Default)]
136struct TestCodeSink {
137 buf: Vec<u8>,
138 offsets_using_label: Vec<usize>,
139}
140
141impl TestCodeSink {
142 fn patch_labels_as_if_they_referred_to_end(&mut self) {
157 let len = i32::try_from(self.buf.len()).unwrap();
158 for offset in self.offsets_using_label.iter() {
159 let range = self.buf[*offset..].first_chunk_mut::<4>().unwrap();
160 let offset = i32::try_from(*offset).unwrap() + 4;
161 let rel_distance = len - offset;
162 *range = (i32::from_le_bytes(*range) + rel_distance).to_le_bytes();
163 }
164 }
165}
166
167impl CodeSink for TestCodeSink {
168 fn put1(&mut self, v: u8) {
169 self.buf.extend_from_slice(&[v]);
170 }
171
172 fn put2(&mut self, v: u16) {
173 self.buf.extend_from_slice(&v.to_le_bytes());
174 }
175
176 fn put4(&mut self, v: u32) {
177 self.buf.extend_from_slice(&v.to_le_bytes());
178 }
179
180 fn put8(&mut self, v: u64) {
181 self.buf.extend_from_slice(&v.to_le_bytes());
182 }
183
184 fn add_trap(&mut self, _: TrapCode) {}
185
186 fn use_target(&mut self, _: DeferredTarget) {
187 let offset = self.buf.len();
188 self.offsets_using_label.push(offset);
189 }
190
191 fn known_offset(&self, target: KnownOffset) -> i32 {
192 panic!("unsupported known target {target:?}")
193 }
194}
195
196fn disassemble_capstone(assembled: &[u8], original: &Inst<FuzzRegs>) -> String {
201 let cs = Capstone::new()
202 .x86()
203 .mode(x86::ArchMode::Mode64)
204 .syntax(x86::ArchSyntax::Att)
205 .detail(true)
206 .build()
207 .expect("failed to create Capstone object");
208 let insts = cs
209 .disasm_all(assembled, 0x0)
210 .expect("failed to disassemble");
211
212 if insts.len() != 1 {
213 println!("> {original}");
214 println!(" debug: {original:x?}");
215 println!(" assembled: {}", pretty_print_hexadecimal(&assembled));
216 assert_eq!(insts.len(), 1, "not a single instruction");
217 }
218
219 let inst = insts.first().expect("at least one instruction");
220 if assembled.len() != inst.len() {
221 println!("> {original}");
222 println!(" debug: {original:x?}");
223 println!(" assembled: {}", pretty_print_hexadecimal(&assembled));
224 println!(
225 " capstone-assembled: {}",
226 pretty_print_hexadecimal(inst.bytes())
227 );
228 assert_eq!(assembled.len(), inst.len(), "extra bytes not disassembled");
229 }
230
231 inst.to_string()
232}
233
234#[cfg(all(feature = "fuzz-xed", target_arch = "x86_64", target_os = "linux"))]
239fn disassemble_xed(assembled: &[u8], original: &Inst<FuzzRegs>) -> String {
240 use core::ffi::c_void;
241 use std::sync::Once;
242 use xed_sys::*;
243
244 static INIT: Once = Once::new();
246 INIT.call_once(|| unsafe { xed_tables_init() });
249
250 unsafe {
253 let mut xedd: xed_decoded_inst_t = core::mem::zeroed();
254 xed_decoded_inst_zero(&mut xedd);
255 xed_decoded_inst_set_mode(&mut xedd, XED_MACHINE_MODE_LONG_64, XED_ADDRESS_WIDTH_64b);
256
257 let error = xed_decode(
258 &mut xedd,
259 assembled.as_ptr(),
260 assembled.len() as core::ffi::c_uint,
261 );
262 if error != XED_ERROR_NONE {
263 println!("> {original}");
264 println!(" debug: {original:x?}");
265 println!(" assembled: {}", pretty_print_hexadecimal(assembled));
266 let name = core::ffi::CStr::from_ptr(xed_error_enum_t2str(error));
267 panic!("xed failed to decode: {}", name.to_string_lossy());
268 }
269
270 let decoded_len = xed_decoded_inst_get_length(&xedd) as usize;
273 if decoded_len != assembled.len() {
274 println!("> {original}");
275 println!(" debug: {original:x?}");
276 println!(" assembled: {}", pretty_print_hexadecimal(assembled));
277 assert_eq!(
278 decoded_len,
279 assembled.len(),
280 "xed did not consume all bytes"
281 );
282 }
283
284 let mut buf = [0i8; 256];
286 let ok = xed_format_context(
287 XED_SYNTAX_ATT,
288 &xedd,
289 buf.as_mut_ptr(),
290 buf.len() as core::ffi::c_int,
291 0,
292 core::ptr::null_mut::<c_void>(),
293 None,
294 );
295 assert!(ok != 0, "xed failed to format instruction");
296
297 let disasm = core::ffi::CStr::from_ptr(buf.as_ptr())
298 .to_string_lossy()
299 .into_owned();
300
301 format!("0: {disasm}")
304 }
305}
306
307fn pretty_print_hexadecimal(hex: &[u8]) -> String {
308 use core::fmt::Write;
309 let mut s = String::with_capacity(hex.len() * 2);
310 for b in hex {
311 write!(&mut s, "{b:02X}").unwrap();
312 }
313 s
314}
315
316macro_rules! hex_print_signed_imm {
318 ($hex:expr, $from:ty => $to:ty) => {{
319 let imm = <$from>::from_str_radix($hex, 16).unwrap() as $to;
320 let mut simm = String::new();
321 if imm < 0 {
322 simm.push_str("-");
323 }
324 let abs = match imm.checked_abs() {
325 Some(i) => i,
326 None => <$to>::MIN,
327 };
328 if imm > -10 && imm < 10 {
329 simm.push_str(&format!("{:x}", abs));
330 } else {
331 simm.push_str(&format!("0x{:x}", abs));
332 }
333 simm
334 }};
335}
336
337fn replace_signed_immediates(dis: &str) -> alloc::borrow::Cow<'_, str> {
346 match dis.find('$') {
347 None => dis.into(),
348 Some(idx) => {
349 let (prefix, rest) = dis.split_at(idx + 1); let (_, rest) = chomp("-", rest); let (_, rest) = chomp("0x", rest); let n = rest.chars().take_while(char::is_ascii_hexdigit).count();
353 let (hex, rest) = rest.split_at(n); let simm = if dis.starts_with("mov") {
355 u64::from_str_radix(hex, 16).unwrap().to_string()
356 } else {
357 match hex.len() {
358 1 | 2 => hex_print_signed_imm!(hex, u8 => i8),
359 4 => hex_print_signed_imm!(hex, u16 => i16),
360 8 => hex_print_signed_imm!(hex, u32 => i32),
361 16 => hex_print_signed_imm!(hex, u64 => i64),
362 _ => panic!("unexpected length for hex: {hex}"),
363 }
364 };
365 format!("{prefix}{simm}{rest}").into()
366 }
367 }
368}
369
370fn chomp<'a>(pat: &str, s: &'a str) -> (&'a str, &'a str) {
372 if s.starts_with(pat) {
373 s.split_at(pat.len())
374 } else {
375 ("", s)
376 }
377}
378
379#[test]
380fn replace() {
381 assert_eq!(
382 replace_signed_immediates("andl $0xffffff9a, %r11d"),
383 "andl $-0x66, %r11d"
384 );
385 assert_eq!(
386 replace_signed_immediates("xorq $0xffffffffffffffbc, 0x7f139ecc(%r9)"),
387 "xorq $-0x44, 0x7f139ecc(%r9)"
388 );
389 assert_eq!(
390 replace_signed_immediates("subl $0x3ca77a19, -0x1a030f40(%r14)"),
391 "subl $0x3ca77a19, -0x1a030f40(%r14)"
392 );
393 assert_eq!(
394 replace_signed_immediates("movq $0xffffffff864ae103, %rsi"),
395 "movq $18446744071667638531, %rsi"
396 );
397}
398
399fn remove_after_semicolon(dis: &str) -> &str {
403 match dis.find(';') {
404 None => dis,
405 Some(idx) => {
406 let (prefix, _) = dis.split_at(idx);
407 prefix.trim()
408 }
409 }
410}
411
412#[test]
413fn remove_after_parenthesis_test() {
414 assert_eq!(
415 remove_after_semicolon("imulb 0x7658eddd(%rcx) ;; implicit: %ax"),
416 "imulb 0x7658eddd(%rcx)"
417 );
418}
419
420fn fix_up(dis: &str) -> alloc::borrow::Cow<'_, str> {
422 let dis = remove_after_semicolon(dis);
423 replace_signed_immediates(&dis)
424}
425
426#[cfg(all(feature = "fuzz-xed", target_arch = "x86_64", target_os = "linux"))]
432fn xed_matches(expected: &str, actual: &str) -> bool {
433 expected.split_whitespace().eq(actual.split_whitespace())
434}
435
436#[derive(Clone, Arbitrary, Debug)]
440pub struct FuzzRegs;
441
442impl Registers for FuzzRegs {
443 type ReadGpr = FuzzReg;
444 type ReadWriteGpr = FuzzReg;
445 type WriteGpr = FuzzReg;
446 type ReadXmm = FuzzReg;
447 type ReadWriteXmm = FuzzReg;
448 type WriteXmm = FuzzReg;
449}
450
451#[derive(Clone, Copy, Debug, PartialEq)]
453pub struct FuzzReg(u8);
454
455impl<'a> Arbitrary<'a> for FuzzReg {
456 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
457 Ok(Self(u.int_in_range(0..=15)?))
458 }
459}
460
461impl AsReg for FuzzReg {
462 fn new(enc: u8) -> Self {
463 Self(enc)
464 }
465 fn enc(&self) -> u8 {
466 self.0
467 }
468}
469
470impl Arbitrary<'_> for AmodeOffset {
471 fn arbitrary(u: &mut Unstructured<'_>) -> Result<Self> {
472 let base = if u.arbitrary()? {
478 i32::from(u.arbitrary::<i8>()?)
479 } else {
480 u.arbitrary::<i32>()?
481 };
482 Ok(match u.int_in_range(0..=5)? {
483 0 => AmodeOffset::ZERO,
484 n => AmodeOffset::new(base << (n - 1)),
485 })
486 }
487}
488
489impl Arbitrary<'_> for AmodeOffsetPlusKnownOffset {
490 fn arbitrary(u: &mut Unstructured<'_>) -> Result<Self> {
491 Ok(Self {
493 simm32: AmodeOffset::arbitrary(u)?,
494 offset: None,
495 })
496 }
497}
498
499impl<R: AsReg, const E: u8> Arbitrary<'_> for Fixed<R, E> {
500 fn arbitrary(_: &mut Unstructured<'_>) -> Result<Self> {
501 Ok(Self::new(E))
502 }
503}
504
505impl<R: AsReg> Arbitrary<'_> for NonRspGpr<R> {
506 fn arbitrary(u: &mut Unstructured<'_>) -> Result<Self> {
507 use crate::gpr::enc::*;
508 let gpr = u.choose(&[
509 RAX, RCX, RDX, RBX, RBP, RSI, RDI, R8, R9, R10, R11, R12, R13, R14, R15,
510 ])?;
511 Ok(Self::new(R::new(*gpr)))
512 }
513}
514impl<'a, R: AsReg> Arbitrary<'a> for Gpr<R> {
515 fn arbitrary(u: &mut Unstructured<'a>) -> Result<Self> {
516 Ok(Self(R::new(u.int_in_range(0..=15)?)))
517 }
518}
519impl<'a, R: AsReg> Arbitrary<'a> for Xmm<R> {
520 fn arbitrary(u: &mut Unstructured<'a>) -> Result<Self> {
521 Ok(Self(R::new(u.int_in_range(0..=15)?)))
522 }
523}
524
525pub trait RegistersArbitrary:
528 Registers<
529 ReadGpr: for<'a> Arbitrary<'a>,
530 ReadWriteGpr: for<'a> Arbitrary<'a>,
531 WriteGpr: for<'a> Arbitrary<'a>,
532 ReadXmm: for<'a> Arbitrary<'a>,
533 ReadWriteXmm: for<'a> Arbitrary<'a>,
534 WriteXmm: for<'a> Arbitrary<'a>,
535 >
536{
537}
538
539impl<R> RegistersArbitrary for R
540where
541 R: Registers,
542 R::ReadGpr: for<'a> Arbitrary<'a>,
543 R::ReadWriteGpr: for<'a> Arbitrary<'a>,
544 R::WriteGpr: for<'a> Arbitrary<'a>,
545 R::ReadXmm: for<'a> Arbitrary<'a>,
546 R::ReadWriteXmm: for<'a> Arbitrary<'a>,
547 R::WriteXmm: for<'a> Arbitrary<'a>,
548{
549}
550
551#[cfg(test)]
552mod test {
553 use super::*;
554 use arbtest::arbtest;
555 use std::sync::atomic::{AtomicUsize, Ordering};
556
557 #[test]
558 fn smoke() {
559 let count = AtomicUsize::new(0);
560 arbtest(|u| {
561 let inst: Inst<FuzzRegs> = u.arbitrary()?;
562 roundtrip(&inst);
563 println!("#{}: {inst}", count.fetch_add(1, Ordering::SeqCst));
564 Ok(())
565 })
566 .budget_ms(1_000);
567
568 }
571
572 #[test]
573 fn callq() {
574 for i in -500..500 {
575 println!("immediate: {i}");
576 let inst = crate::inst::callq_d::new(i);
577 roundtrip(&inst.into());
578 }
579 }
580
581 #[cfg(all(feature = "fuzz-xed", target_arch = "x86_64", target_os = "linux"))]
588 #[test]
589 fn smoke_xed() {
590 let count = AtomicUsize::new(0);
591 arbtest(|u| {
592 let inst: Inst<FuzzRegs> = u.arbitrary()?;
593 roundtrip_xed(&inst);
594 println!("#{}: {inst}", count.fetch_add(1, Ordering::SeqCst));
595 Ok(())
596 })
597 .budget_ms(1_000);
598 }
599
600 #[test]
622 fn apx_addq_rvm_ndd_encoding() {
623 use crate::inst::addq_rvm;
624 let inst = addq_rvm::<FuzzRegs>::new(FuzzReg::new(1), FuzzReg::new(0), FuzzReg::new(2));
628 let assembled = assemble(&inst.into());
629 assert_eq!(pretty_print_hexadecimal(&assembled), "62F4FC1801CA");
630 }
631
632 #[test]
641 fn apx_addq_rvm_ndd_disp8_is_unscaled() {
642 use crate::inst::addq_rvm;
643 use crate::mem::{Amode, AmodeOffset, AmodeOffsetPlusKnownOffset, GprMem};
644
645 let mem: GprMem<FuzzReg, FuzzReg> = GprMem::Mem(Amode::ImmReg {
646 base: FuzzReg::new(7),
647 simm32: AmodeOffsetPlusKnownOffset {
648 simm32: AmodeOffset::new(0x50),
649 offset: None,
650 },
651 trap: None,
652 });
653 let inst = addq_rvm::<FuzzRegs>::new(FuzzReg::new(7), FuzzReg::new(7), mem);
654 let assembled = assemble(&inst.into());
655 assert_eq!(pretty_print_hexadecimal(&assembled), "62F4C418017F50");
656 }
657}