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 roundtrip_with(
38 inst,
39 "capstone",
40 disassemble_capstone,
41 capstone_matches,
42 |i| format!("{i}"),
43 );
44}
45
46#[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
63fn 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 let assembled = assemble(inst);
83 let expected = disassemble(&assembled, inst);
84
85 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
99fn 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
110fn capstone_matches(expected: &str, actual: &str) -> bool {
114 expected == actual || expected.trim() == fix_up(actual)
115}
116
117fn 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 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
189fn 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#[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 static INIT: Once = Once::new();
239 INIT.call_once(|| unsafe { xed_tables_init() });
242
243 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 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 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 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
309macro_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
330fn 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); let (_, rest) = chomp("-", rest); let (_, rest) = chomp("0x", rest); let n = rest.chars().take_while(char::is_ascii_hexdigit).count();
346 let (hex, rest) = rest.split_at(n); 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
363fn 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
392fn 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
413fn fix_up(dis: &str) -> alloc::borrow::Cow<'_, str> {
415 let dis = remove_after_semicolon(dis);
416 replace_signed_immediates(&dis)
417}
418
419#[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#[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#[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 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 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
518pub 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 }
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 #[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}