Skip to content

Commit d7d605c

Browse files
authored
asm: re-allow prop-testing with cargo test (#10185)
In #10110, I originally intended to use `arbitrary` implementations in two ways: for long-running fuzz testing (e.g., with OSS-Fuzz) but also for quick property testing with `cargo test`. This latter use case could replace the tedious emit tests we had to write in `cranelift-codegen` _and_ find corner cases that we otherwise might not explore. It helped me during development: just run `cargo test` to check if anything is obviously wrong. `arbtest` seemed to be able to run ~1000 test cases and found mistakes well within the one second time limit I gave it. @alexcrichton improved #10110 by avoiding `Arbitrary` implementations everywhere and unconditionally depending on the `arbitrary` crate. This was the right change, but it removed the ability to property test using `cargo test`. What this change does is retain the general intent of his change (no extra dependencies) but add `Arbitrary` implementations for `cfg(test)` as well to run property tests during `cargo test`. The only downside I see here is the added complexity when conditionally compiling the fuzz-related bits: `#[cfg(any(test, feature = "fuzz"))]`. Perhaps there is a better way to do this, but this seemed to work fine. Let me know what you think.
1 parent 85de89d commit d7d605c

File tree

12 files changed

+189
-327
lines changed

12 files changed

+189
-327
lines changed

Cargo.lock

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cranelift/assembler-x64/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@ rust-version.workspace = true
88

99
[dependencies]
1010
arbitrary = { workspace = true, features = ["derive"], optional = true }
11+
capstone = { workspace = true, optional = true }
1112

1213
[dev-dependencies]
14+
arbitrary = { workspace = true, features = ["derive"] }
1315
arbtest = "0.3.1"
16+
capstone = { workspace = true }
1417

1518
[build-dependencies]
1619
cranelift-assembler-x64-meta = { path = "meta", version = "0.118.0" }
@@ -23,4 +26,4 @@ similar_names = { level = "allow", priority = 1 }
2326
wildcard_imports = { level = "allow", priority = 1 }
2427

2528
[features]
26-
arbitrary = ['dep:arbitrary']
29+
fuzz = ['dep:arbitrary', 'dep:capstone']

cranelift/assembler-x64/fuzz/Cargo.lock

Lines changed: 0 additions & 148 deletions
This file was deleted.

cranelift/assembler-x64/fuzz/Cargo.toml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@ cargo-fuzz = true
1010

1111
[dependencies]
1212
libfuzzer-sys = { workspace = true }
13-
cranelift-assembler-x64 = { path = "..", features = ['arbitrary'] }
14-
capstone = { workspace = true }
15-
arbitrary = { workspace = true, features = ['derive'] }
13+
cranelift-assembler-x64 = { path = "..", features = ['fuzz'] }
1614

1715
[[bin]]
1816
name = "roundtrip"
Lines changed: 3 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,8 @@
11
#![no_main]
22

3-
use arbitrary::Arbitrary;
4-
use capstone::arch::{BuildsCapstone, BuildsCapstoneSyntax};
5-
use cranelift_assembler_x64::{AsReg, Inst, Registers};
3+
use cranelift_assembler_x64::{fuzz, Inst};
64
use libfuzzer_sys::fuzz_target;
75

8-
// Generate a random assembly instruction and check its encoding and
9-
// pretty-printing against a known-good disassembler.
10-
//
11-
// # Panics
12-
//
13-
// This function panics to express failure as expected by the `arbitrary`
14-
// fuzzer infrastructure. It may fail during assembly, disassembly, or when
15-
// comparing the disassembled strings.
16-
fuzz_target!(|inst: Inst<FuzzRegs>| {
17-
// Check that we can actually assemble this instruction.
18-
let assembled = assemble(&inst);
19-
let expected = disassemble(&assembled);
20-
21-
// Check that our pretty-printed output matches the known-good output.
22-
let expected = expected.split_once(' ').unwrap().1;
23-
let actual = inst.to_string();
24-
if expected != actual {
25-
println!("> {inst}");
26-
println!(" debug: {inst:x?}");
27-
println!(" assembled: {}", pretty_print_hexadecimal(&assembled));
28-
assert_eq!(expected, &actual);
29-
}
6+
fuzz_target!(|inst: Inst<fuzz::FuzzRegs>| {
7+
fuzz::roundtrip(&inst);
308
});
31-
32-
/// Use this assembler to emit machine code into a byte buffer.
33-
///
34-
/// This will skip any traps or label registrations, but this is fine for the
35-
/// single-instruction disassembly we're doing here.
36-
fn assemble(insn: &Inst<FuzzRegs>) -> Vec<u8> {
37-
let mut buffer = Vec::new();
38-
let offsets: Vec<i32> = Vec::new();
39-
insn.encode(&mut buffer, &offsets);
40-
buffer
41-
}
42-
43-
/// Building a new `Capstone` each time is suboptimal (TODO).
44-
fn disassemble(assembled: &[u8]) -> String {
45-
let cs = capstone::Capstone::new()
46-
.x86()
47-
.mode(capstone::arch::x86::ArchMode::Mode64)
48-
.syntax(capstone::arch::x86::ArchSyntax::Att)
49-
.detail(true)
50-
.build()
51-
.expect("failed to create Capstone object");
52-
let insns = cs
53-
.disasm_all(assembled, 0x0)
54-
.expect("failed to disassemble");
55-
assert_eq!(insns.len(), 1, "not a single instruction: {assembled:x?}");
56-
let insn = insns.first().expect("at least one instruction");
57-
assert_eq!(assembled.len(), insn.len());
58-
insn.to_string()
59-
}
60-
61-
fn pretty_print_hexadecimal(hex: &[u8]) -> String {
62-
use std::fmt::Write;
63-
let mut s = String::with_capacity(hex.len() * 2);
64-
for b in hex {
65-
write!(&mut s, "{b:02X}").unwrap();
66-
}
67-
s
68-
}
69-
70-
/// Fuzz-specific registers.
71-
///
72-
/// For the fuzzer, we do not need any fancy register types; see [`FuzzReg`].
73-
#[derive(Arbitrary, Debug)]
74-
pub struct FuzzRegs;
75-
76-
impl Registers for FuzzRegs {
77-
type ReadGpr = FuzzReg;
78-
type ReadWriteGpr = FuzzReg;
79-
}
80-
81-
/// A simple `u8` register type for fuzzing only
82-
#[derive(Clone, Copy, Debug)]
83-
pub struct FuzzReg(u8);
84-
85-
impl<'a> Arbitrary<'a> for FuzzReg {
86-
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
87-
Ok(Self::new(u.int_in_range(0..=15)?))
88-
}
89-
}
90-
91-
impl AsReg for FuzzReg {
92-
fn new(enc: u8) -> Self {
93-
Self(enc)
94-
}
95-
fn enc(&self) -> u8 {
96-
self.0
97-
}
98-
}
99-
100-
#[cfg(test)]
101-
mod test {
102-
use super::*;
103-
use arbtest::arbtest;
104-
use std::sync::atomic::{AtomicUsize, Ordering};
105-
106-
#[test]
107-
fn smoke() {
108-
let count = AtomicUsize::new(0);
109-
arbtest(|u| {
110-
let inst: Inst<FuzzRegs> = u.arbitrary()?;
111-
roundtrip(&inst);
112-
println!("#{}: {inst}", count.fetch_add(1, Ordering::SeqCst));
113-
Ok(())
114-
})
115-
.budget_ms(1_000);
116-
117-
// This will run the `roundtrip` fuzzer for one second. To repeatably
118-
// test a single input, append `.seed(0x<failing seed>)`.
119-
}
120-
}

cranelift/assembler-x64/meta/src/generate.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,16 @@ fn generate_inst_enum(f: &mut Formatter, insts: &[dsl::Inst]) {
8686
/// `#[derive(...)]`
8787
fn generate_derive(f: &mut Formatter) {
8888
f.line("#[derive(Clone, Debug)]", None);
89-
f.line("#[cfg_attr(feature = \"arbitrary\", derive(arbitrary::Arbitrary))]", None);
89+
f.line("#[cfg_attr(any(test, feature = \"fuzz\"), derive(arbitrary::Arbitrary))]", None);
9090
}
9191

9292
/// Adds a custom bound to the `Arbitrary` implementation which ensures that
9393
/// the associated registers are all `Arbitrary` as well.
9494
fn generate_derive_arbitrary_bounds(f: &mut Formatter) {
95-
f.line("#[cfg_attr(feature = \"arbitrary\", arbitrary(bound = \"R: crate::arbitrary_impls::RegistersArbitrary\"))]", None);
95+
f.line(
96+
"#[cfg_attr(any(test, feature = \"fuzz\"), arbitrary(bound = \"R: crate::fuzz::RegistersArbitrary\"))]",
97+
None,
98+
);
9699
}
97100

98101
/// `impl std::fmt::Display for Inst { ... }`

cranelift/assembler-x64/src/api.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,17 +67,17 @@ impl CodeSink for Vec<u8> {
6767

6868
/// Wrap [`CodeSink`]-specific labels.
6969
#[derive(Debug, Clone)]
70-
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
70+
#[cfg_attr(any(test, feature = "fuzz"), derive(arbitrary::Arbitrary))]
7171
pub struct Label(pub u32);
7272

7373
/// Wrap [`CodeSink`]-specific constant keys.
7474
#[derive(Debug, Clone)]
75-
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
75+
#[cfg_attr(any(test, feature = "fuzz"), derive(arbitrary::Arbitrary))]
7676
pub struct Constant(pub u32);
7777

7878
/// Wrap [`CodeSink`]-specific trap codes.
7979
#[derive(Debug, Clone, Copy)]
80-
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
80+
#[cfg_attr(any(test, feature = "fuzz"), derive(arbitrary::Arbitrary))]
8181
pub struct TrapCode(pub NonZeroU8);
8282

8383
/// A table mapping `KnownOffset` identifiers to their `i32` offset values.

0 commit comments

Comments
 (0)