From 79446867485c493a7d0715e3ee16e1df2e148d54 Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Fri, 19 Sep 2025 18:15:52 -0700 Subject: [PATCH 1/3] Run fuzzing during `cargo test`; reorganize fuzzers This change moves the meat of the code for fuzzing `regalloc2` into the main crate's `fuzzing` module. This has several minor benefits (easier to find, better LSP detection, same workspace for `cargo fmt|clippy`) but the major one is we can not only build, but also _run_ some fuzz tests with: ``` cargo test --features fuzzing ``` The fuzz modules now use `arbtest` to run one second worth of fuzzing. This should be helpful both during development and in CI. The second part of this change is a reorganization of the fuzz targets themselves. In looking at them carefully, I realized: - `ion` and `ion_checker` were doing essentially the same thing; in fact, we probably always want to run the checker after allocation - `ssagen` was not necessary: we could get the same functionality by flipping the `enable_ssa_checker` flag on `fastalloc|ion::run()` - we never fuzzed with annotations off (not sure why we would not want to but... shrug) This led to randomly flipping the annotations and SSA checking flags, renaming `ion_checker.rs -> ion.rs` (replacing `ion.rs`) and `fastalloc_checker.rs -> fastalloc.rs`. `ssagen.rs` is now gone, subsumed by now being able to check the SSA in both `ion.rs` and `fastalloc.rs`. This naming change will undoubtedly require some re-configuration in OSSFuzz but that can happen later. --- Cargo.toml | 7 +- fuzz/Cargo.toml | 16 +-- fuzz/fuzz_targets/domtree.rs | 134 +---------------------- fuzz/fuzz_targets/fastalloc.rs | 13 +++ fuzz/fuzz_targets/fastalloc_checker.rs | 46 -------- fuzz/fuzz_targets/ion.rs | 19 +--- fuzz/fuzz_targets/ion_checker.rs | 54 --------- fuzz/fuzz_targets/moves.rs | 131 +--------------------- fuzz/fuzz_targets/ssagen.rs | 46 -------- src/fuzzing/domtree.rs | 146 +++++++++++++++++++++++++ src/fuzzing/fastalloc.rs | 68 ++++++++++++ src/fuzzing/func.rs | 4 +- src/fuzzing/ion.rs | 86 +++++++++++++++ src/fuzzing/mod.rs | 32 +----- src/fuzzing/moves.rs | 141 ++++++++++++++++++++++++ 15 files changed, 479 insertions(+), 464 deletions(-) create mode 100644 fuzz/fuzz_targets/fastalloc.rs delete mode 100644 fuzz/fuzz_targets/fastalloc_checker.rs delete mode 100644 fuzz/fuzz_targets/ion_checker.rs delete mode 100644 fuzz/fuzz_targets/ssagen.rs create mode 100644 src/fuzzing/domtree.rs create mode 100644 src/fuzzing/fastalloc.rs create mode 100644 src/fuzzing/ion.rs create mode 100644 src/fuzzing/moves.rs diff --git a/Cargo.toml b/Cargo.toml index 48eb4321..38dcacfd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,11 +26,12 @@ serde = { version = "1.0.136", features = [ ], default-features = false, optional = true } # The below are only needed for fuzzing. -libfuzzer-sys = { version = "0.4.2", optional = true } +arbitrary = { version = "1.4.2", optional = true } +arbtest = { version = "0.3.2", optional = true } bumpalo = { version = "3.16.0", features = ["allocator-api2"] } allocator-api2 = { version = "0.2.18", default-features = false, features = ["alloc"] } -# When testing regalloc2 by itself, enable debug assertions and overflow checks +# When testing regalloc2 by itself, enable debug assertions and overflow checks. [profile.release] debug = true debug-assertions = true @@ -49,7 +50,7 @@ checker = [] trace-log = [] # Exposes the internal API for fuzzing. -fuzzing = ["libfuzzer-sys", "checker", "trace-log"] +fuzzing = ["arbitrary", "arbtest", "checker", "trace-log"] # Enables serde for exposed types. enable-serde = ["serde"] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 8c96ee11..66c30ca9 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -26,8 +26,8 @@ test = false doc = false [[bin]] -name = "ssagen" -path = "fuzz_targets/ssagen.rs" +name = "fastalloc" +path = "fuzz_targets/fastalloc.rs" test = false doc = false @@ -43,18 +43,6 @@ path = "fuzz_targets/moves.rs" test = false doc = false -[[bin]] -name = "ion_checker" -path = "fuzz_targets/ion_checker.rs" -test = false -doc = false - -[[bin]] -name = "fastalloc_checker" -path = "fuzz_targets/fastalloc_checker.rs" -test = false -doc = false - # Enable debug assertions and overflow checks when fuzzing [profile.release] debug = true diff --git a/fuzz/fuzz_targets/domtree.rs b/fuzz/fuzz_targets/domtree.rs index 72c9f12d..fadca8c4 100644 --- a/fuzz/fuzz_targets/domtree.rs +++ b/fuzz/fuzz_targets/domtree.rs @@ -4,134 +4,10 @@ */ #![no_main] -use regalloc2::fuzzing::arbitrary::{Arbitrary, Result, Unstructured}; -use regalloc2::fuzzing::{domtree, fuzz_target, postorder}; -use regalloc2::Block; -use std::collections::HashSet; +use libfuzzer_sys::fuzz_target; +use regalloc2::fuzzing::domtree; -#[derive(Clone, Debug)] -struct CFG { - num_blocks: usize, - preds: Vec>, - succs: Vec>, -} - -impl Arbitrary<'_> for CFG { - fn arbitrary(u: &mut Unstructured) -> Result { - let num_blocks = u.int_in_range(1..=1000)?; - let mut succs = vec![]; - for _ in 0..num_blocks { - let mut block_succs = vec![]; - for _ in 0..u.int_in_range(0..=5)? { - block_succs.push(Block::new(u.int_in_range(0..=(num_blocks - 1))?)); - } - succs.push(block_succs); - } - let mut preds = vec![]; - for _ in 0..num_blocks { - preds.push(vec![]); - } - for from in 0..num_blocks { - for succ in &succs[from] { - preds[succ.index()].push(Block::new(from)); - } - } - Ok(CFG { - num_blocks, - preds, - succs, - }) - } -} - -#[derive(Clone, Debug)] -struct Path { - blocks: Vec, -} - -impl Path { - fn choose_from_cfg(cfg: &CFG, u: &mut Unstructured) -> Result { - let succs = u.int_in_range(0..=(2 * cfg.num_blocks))?; - let mut block = Block::new(0); - let mut blocks = vec![]; - blocks.push(block); - for _ in 0..succs { - if cfg.succs[block.index()].is_empty() { - break; - } - block = *u.choose(&cfg.succs[block.index()])?; - blocks.push(block); - } - Ok(Path { blocks }) - } -} - -fn check_idom_violations(idom: &[Block], path: &Path) { - // "a dom b" means that any path from the entry block through the CFG that - // contains a and b will contain a before b. - // - // To test this, for any given block b_i, we have the set S of b_0 .. b_{i-1}, - // and we walk up the domtree from b_i to get all blocks that dominate b_i; - // each such block must appear in S. (Otherwise, we have a counterexample - // for which dominance says it should appear in the path prefix, but it does - // not.) - let mut visited = HashSet::new(); - visited.insert(Block::new(0)); - for block in &path.blocks { - let mut parent = idom[block.index()]; - let mut domset = HashSet::new(); - domset.insert(*block); - while parent.is_valid() { - assert!(visited.contains(&parent)); - domset.insert(parent); - let next = idom[parent.index()]; - parent = next; - } - - // Check that `dominates()` returns true for every block in domset, - // and false for every other block. - for domblock in 0..idom.len() { - let domblock = Block::new(domblock); - assert_eq!( - domset.contains(&domblock), - domtree::dominates(idom, domblock, *block) - ); - } - visited.insert(*block); - } -} - -#[derive(Clone, Debug)] -struct TestCase { - cfg: CFG, - path: Path, -} - -impl Arbitrary<'_> for TestCase { - fn arbitrary(u: &mut Unstructured) -> Result { - let cfg = CFG::arbitrary(u)?; - let path = Path::choose_from_cfg(&cfg, u)?; - Ok(TestCase { cfg, path }) - } -} - -fuzz_target!(|testcase: TestCase| { - let mut postorder = vec![]; - postorder::calculate( - testcase.cfg.num_blocks, - Block::new(0), - &mut vec![], - &mut postorder, - |block| &testcase.cfg.succs[block.index()], - ); - let mut idom = vec![]; - domtree::calculate( - testcase.cfg.num_blocks, - |block| &testcase.cfg.preds[block.index()], - &postorder[..], - &mut vec![], - &mut idom, - Block::new(0), - ); - check_idom_violations(&idom[..], &testcase.path); +fuzz_target!(|test_case: domtree::TestCase| { + let _ = env_logger::try_init(); + domtree::check(test_case); }); diff --git a/fuzz/fuzz_targets/fastalloc.rs b/fuzz/fuzz_targets/fastalloc.rs new file mode 100644 index 00000000..13ec475c --- /dev/null +++ b/fuzz/fuzz_targets/fastalloc.rs @@ -0,0 +1,13 @@ +/* + * Released under the terms of the Apache 2.0 license with LLVM + * exception. See `LICENSE` for details. + */ + +#![no_main] +use libfuzzer_sys::fuzz_target; +use regalloc2::fuzzing::fastalloc; + +fuzz_target!(|test_case: fastalloc::TestCase| { + let _ = env_logger::try_init(); + fastalloc::check(test_case); +}); diff --git a/fuzz/fuzz_targets/fastalloc_checker.rs b/fuzz/fuzz_targets/fastalloc_checker.rs deleted file mode 100644 index 42988944..00000000 --- a/fuzz/fuzz_targets/fastalloc_checker.rs +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Released under the terms of the Apache 2.0 license with LLVM - * exception. See `LICENSE` for details. - */ - -#![no_main] -use regalloc2::fuzzing::arbitrary::{Arbitrary, Result, Unstructured}; -use regalloc2::fuzzing::checker::Checker; -use regalloc2::fuzzing::func::{Func, Options}; -use regalloc2::fuzzing::fuzz_target; - -#[derive(Clone, Debug)] -struct TestCase { - func: Func, -} - -impl Arbitrary<'_> for TestCase { - fn arbitrary(u: &mut Unstructured) -> Result { - Ok(TestCase { - func: Func::arbitrary_with_options( - u, - &Options { - reused_inputs: true, - fixed_regs: true, - fixed_nonallocatable: true, - clobbers: true, - reftypes: false, - callsite_ish_constraints: true, - }, - )?, - }) - } -} - -fuzz_target!(|testcase: TestCase| { - let func = testcase.func; - let _ = env_logger::try_init(); - log::trace!("func:\n{:?}", func); - let env = regalloc2::fuzzing::func::machine_env(); - let out = - regalloc2::fuzzing::fastalloc::run(&func, &env, true, false).expect("regalloc did not succeed"); - - let mut checker = Checker::new(&func, &env); - checker.prepare(&out); - checker.run().expect("checker failed"); -}); diff --git a/fuzz/fuzz_targets/ion.rs b/fuzz/fuzz_targets/ion.rs index b48417bb..288b81c9 100644 --- a/fuzz/fuzz_targets/ion.rs +++ b/fuzz/fuzz_targets/ion.rs @@ -4,21 +4,10 @@ */ #![no_main] -use regalloc2::fuzzing::func::Func; -use regalloc2::fuzzing::fuzz_target; +use libfuzzer_sys::fuzz_target; +use regalloc2::fuzzing::ion; -fuzz_target!(|func: Func| { +fuzz_target!(|test_case: ion::TestCase| { let _ = env_logger::try_init(); - log::trace!("func:\n{:?}", func); - let env = regalloc2::fuzzing::func::machine_env(); - - thread_local! { - // We test that ctx is cleared properly between runs. - static CTX: std::cell::RefCell = std::cell::RefCell::default(); - } - - CTX.with(|ctx| { - let _out = regalloc2::fuzzing::ion::run(&func, &env, &mut *ctx.borrow_mut(), false, false) - .expect("regalloc did not succeed"); - }); + ion::check(test_case); }); diff --git a/fuzz/fuzz_targets/ion_checker.rs b/fuzz/fuzz_targets/ion_checker.rs deleted file mode 100644 index 46a4d02e..00000000 --- a/fuzz/fuzz_targets/ion_checker.rs +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Released under the terms of the Apache 2.0 license with LLVM - * exception. See `LICENSE` for details. - */ - -#![no_main] -use regalloc2::fuzzing::arbitrary::{Arbitrary, Result, Unstructured}; -use regalloc2::fuzzing::checker::Checker; -use regalloc2::fuzzing::func::{Func, Options}; -use regalloc2::fuzzing::fuzz_target; - -#[derive(Clone, Debug)] -struct TestCase { - func: Func, -} - -impl Arbitrary<'_> for TestCase { - fn arbitrary(u: &mut Unstructured) -> Result { - Ok(TestCase { - func: Func::arbitrary_with_options( - u, - &Options { - reused_inputs: true, - fixed_regs: true, - fixed_nonallocatable: true, - clobbers: true, - reftypes: true, - callsite_ish_constraints: true, - }, - )?, - }) - } -} - -fuzz_target!(|testcase: TestCase| { - let func = testcase.func; - let _ = env_logger::try_init(); - log::trace!("func:\n{:?}", func); - let env = regalloc2::fuzzing::func::machine_env(); - - thread_local! { - // We test that ctx is cleared properly between runs. - static CTX: std::cell::RefCell = std::cell::RefCell::default(); - } - - CTX.with(|ctx| { - regalloc2::fuzzing::ion::run(&func, &env, &mut *ctx.borrow_mut(), true, false) - .expect("regalloc did not succeed"); - - let mut checker = Checker::new(&func, &env); - checker.prepare(&ctx.borrow().output); - checker.run().expect("checker failed"); - }); -}); diff --git a/fuzz/fuzz_targets/moves.rs b/fuzz/fuzz_targets/moves.rs index f741e5ea..1e800598 100644 --- a/fuzz/fuzz_targets/moves.rs +++ b/fuzz/fuzz_targets/moves.rs @@ -4,133 +4,10 @@ */ #![no_main] -use regalloc2::fuzzing::arbitrary::{Arbitrary, Result, Unstructured}; -use regalloc2::fuzzing::fuzz_target; -use regalloc2::fuzzing::moves::{MoveAndScratchResolver, ParallelMoves}; -use regalloc2::{Allocation, PReg, RegClass, SpillSlot}; -use std::collections::{HashMap, HashSet}; +use libfuzzer_sys::fuzz_target; +use regalloc2::fuzzing::moves; -fn is_stack_alloc(alloc: Allocation) -> bool { - // Treat registers 20..=29 as fixed stack slots. - if let Some(reg) = alloc.as_reg() { - reg.index() > 20 - } else { - alloc.is_stack() - } -} - -#[derive(Clone, Debug)] -struct TestCase { - moves: Vec<(Allocation, Allocation)>, - available_pregs: Vec, -} - -impl Arbitrary<'_> for TestCase { - fn arbitrary(u: &mut Unstructured) -> Result { - let mut ret = TestCase { - moves: vec![], - available_pregs: vec![], - }; - let mut written = HashSet::new(); - // An arbitrary sequence of moves between registers 0 to 29 - // inclusive. - while bool::arbitrary(u)? { - let src = if bool::arbitrary(u)? { - let reg = u.int_in_range(0..=29)?; - Allocation::reg(PReg::new(reg, RegClass::Int)) - } else { - let slot = u.int_in_range(0..=31)?; - Allocation::stack(SpillSlot::new(slot)) - }; - let dst = if bool::arbitrary(u)? { - let reg = u.int_in_range(0..=29)?; - Allocation::reg(PReg::new(reg, RegClass::Int)) - } else { - let slot = u.int_in_range(0..=31)?; - Allocation::stack(SpillSlot::new(slot)) - }; - - // Stop if we are going to write a reg more than once: - // that creates an invalid parallel move set. - if written.contains(&dst) { - break; - } - written.insert(dst); - - ret.moves.push((src, dst)); - } - - // We might have some unallocated registers free for scratch - // space... - for i in 0..u.int_in_range(0..=2)? { - let reg = PReg::new(30 + i, RegClass::Int); - ret.available_pregs.push(Allocation::reg(reg)); - } - Ok(ret) - } -} - -fuzz_target!(|testcase: TestCase| { +fuzz_target!(|test_case: moves::TestCase| { let _ = env_logger::try_init(); - let mut par = ParallelMoves::new(); - for &(src, dst) in &testcase.moves { - par.add(src, dst, ()); - } - - let moves = par.resolve(); - log::trace!("raw resolved moves: {:?}", moves); - - // Resolve uses of scratch reg and stack-to-stack moves with the - // scratch resolver. - let mut avail = testcase.available_pregs.clone(); - let find_free_reg = || avail.pop(); - let mut next_slot = 32; - let get_stackslot = || { - let slot = next_slot; - next_slot += 1; - Allocation::stack(SpillSlot::new(slot)) - }; - let preferred_victim = PReg::new(0, RegClass::Int); - let scratch_resolver = MoveAndScratchResolver { - find_free_reg, - get_stackslot, - is_stack_alloc, - borrowed_scratch_reg: preferred_victim, - }; - let moves = scratch_resolver.compute(moves); - log::trace!("resolved moves: {:?}", moves); - - // Compute the final source reg for each dest reg in the original - // parallel-move set. - let mut final_src_per_dest: HashMap = HashMap::new(); - for &(src, dst) in &testcase.moves { - final_src_per_dest.insert(dst, src); - } - log::trace!("expected final state: {:?}", final_src_per_dest); - - // Simulate the sequence of moves. - let mut locations: HashMap = HashMap::new(); - for (src, dst, _) in moves { - let data = locations.get(&src).cloned().unwrap_or(src); - locations.insert(dst, data); - } - log::trace!("simulated final state: {:?}", locations); - - // Assert that the expected register-moves occurred. - for (reg, data) in locations { - if let Some(&expected_data) = final_src_per_dest.get(®) { - assert_eq!(expected_data, data); - } else { - if data != reg { - // If not just the original value, then this location - // has been modified, but it was not part of the - // original parallel move. It must have been an - // available preg or a scratch stackslot. - assert!( - testcase.available_pregs.contains(®) - || (reg.is_stack() && reg.as_stack().unwrap().index() >= 32) - ); - } - } - } + moves::check(test_case); }); diff --git a/fuzz/fuzz_targets/ssagen.rs b/fuzz/fuzz_targets/ssagen.rs deleted file mode 100644 index 1acb804b..00000000 --- a/fuzz/fuzz_targets/ssagen.rs +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Released under the terms of the Apache 2.0 license with LLVM - * exception. See `LICENSE` for details. - */ - -#![no_main] -use regalloc2::fuzzing::arbitrary::{Arbitrary, Result, Unstructured}; -use regalloc2::fuzzing::cfg::{CFGInfo, CFGInfoCtx}; -use regalloc2::fuzzing::func::{Func, Options}; -use regalloc2::fuzzing::fuzz_target; -use regalloc2::ssa::validate_ssa; - -#[derive(Debug)] -struct TestCase { - f: Func, -} - -impl Arbitrary<'_> for TestCase { - fn arbitrary(u: &mut Unstructured) -> Result { - Ok(TestCase { - f: Func::arbitrary_with_options( - u, - &Options { - reused_inputs: true, - fixed_regs: true, - fixed_nonallocatable: true, - clobbers: true, - reftypes: true, - callsite_ish_constraints: true, - }, - )?, - }) - } -} - -fuzz_target!(|t: TestCase| { - thread_local! { - // We test that ctx is cleared properly between runs. - static CFG_INFO: std::cell::RefCell<(CFGInfo, CFGInfoCtx)> = std::cell::RefCell::default(); - } - - CFG_INFO.with_borrow_mut(|(cfginfo, ctx)| { - cfginfo.init(&t.f, ctx).expect("could not create CFG info"); - validate_ssa(&t.f, &cfginfo).expect("invalid SSA"); - }); -}); diff --git a/src/fuzzing/domtree.rs b/src/fuzzing/domtree.rs new file mode 100644 index 00000000..8822ff38 --- /dev/null +++ b/src/fuzzing/domtree.rs @@ -0,0 +1,146 @@ +//! Fuzz the dominator-tree calculation. + +use crate::{domtree, postorder, Block}; +use arbitrary::{Arbitrary, Result, Unstructured}; +use std::collections::HashSet; +use std::{vec, vec::Vec}; + +#[derive(Clone, Debug)] +struct CFG { + num_blocks: usize, + preds: Vec>, + succs: Vec>, +} + +impl Arbitrary<'_> for CFG { + fn arbitrary(u: &mut Unstructured) -> Result { + let num_blocks = u.int_in_range(1..=1000)?; + let mut succs = vec![]; + for _ in 0..num_blocks { + let mut block_succs = vec![]; + for _ in 0..u.int_in_range(0..=5)? { + block_succs.push(Block::new(u.int_in_range(0..=(num_blocks - 1))?)); + } + succs.push(block_succs); + } + let mut preds = vec![]; + for _ in 0..num_blocks { + preds.push(vec![]); + } + for from in 0..num_blocks { + for succ in &succs[from] { + preds[succ.index()].push(Block::new(from)); + } + } + Ok(CFG { + num_blocks, + preds, + succs, + }) + } +} + +#[derive(Clone, Debug)] +struct Path { + blocks: Vec, +} + +impl Path { + fn choose_from_cfg(cfg: &CFG, u: &mut Unstructured) -> Result { + let succs = u.int_in_range(0..=(2 * cfg.num_blocks))?; + let mut block = Block::new(0); + let mut blocks = vec![]; + blocks.push(block); + for _ in 0..succs { + if cfg.succs[block.index()].is_empty() { + break; + } + block = *u.choose(&cfg.succs[block.index()])?; + blocks.push(block); + } + Ok(Path { blocks }) + } +} + +fn check_idom_violations(idom: &[Block], path: &Path) { + // "a dom b" means that any path from the entry block through the CFG that + // contains a and b will contain a before b. + // + // To test this, for any given block b_i, we have the set S of b_0 .. + // b_{i-1}, and we walk up the domtree from b_i to get all blocks that + // dominate b_i; each such block must appear in S. (Otherwise, we have a + // counterexample for which dominance says it should appear in the path + // prefix, but it does not.) + let mut visited = HashSet::new(); + visited.insert(Block::new(0)); + for block in &path.blocks { + let mut parent = idom[block.index()]; + let mut domset = HashSet::new(); + domset.insert(*block); + while parent.is_valid() { + assert!(visited.contains(&parent)); + domset.insert(parent); + let next = idom[parent.index()]; + parent = next; + } + + // Check that `dominates()` returns true for every block in domset, + // and false for every other block. + for domblock in 0..idom.len() { + let domblock = Block::new(domblock); + assert_eq!( + domset.contains(&domblock), + domtree::dominates(idom, domblock, *block) + ); + } + visited.insert(*block); + } +} + +/// A control-flow graph ([`CFG`]) and a [`Path`] through it. +#[derive(Clone, Debug)] +pub struct TestCase { + cfg: CFG, + path: Path, +} + +impl Arbitrary<'_> for TestCase { + fn arbitrary(u: &mut Unstructured) -> Result { + let cfg = CFG::arbitrary(u)?; + let path = Path::choose_from_cfg(&cfg, u)?; + Ok(TestCase { cfg, path }) + } +} + +pub fn check(t: TestCase) { + let mut postorder = vec![]; + postorder::calculate( + t.cfg.num_blocks, + Block::new(0), + &mut vec![], + &mut postorder, + |block| &t.cfg.succs[block.index()], + ) + .unwrap(); + + let mut idom = vec![]; + domtree::calculate( + t.cfg.num_blocks, + |block| &t.cfg.preds[block.index()], + &postorder[..], + &mut vec![], + &mut idom, + Block::new(0), + ); + check_idom_violations(&idom[..], &t.path); +} + +#[test] +fn smoke() { + arbtest::arbtest(|u| { + let test_case = TestCase::arbitrary(u)?; + check(test_case); + Ok(()) + }) + .budget_ms(1_000); +} diff --git a/src/fuzzing/fastalloc.rs b/src/fuzzing/fastalloc.rs new file mode 100644 index 00000000..9ff05245 --- /dev/null +++ b/src/fuzzing/fastalloc.rs @@ -0,0 +1,68 @@ +//! Fuzz the `fastalloc` register allocator. + +use crate::{checker, fastalloc, fuzzing::func}; +use arbitrary::{Arbitrary, Result, Unstructured}; + +/// `fastalloc`-specific options for generating functions. +const OPTIONS: func::Options = func::Options { + reused_inputs: true, + fixed_regs: true, + fixed_nonallocatable: true, + clobbers: true, + reftypes: false, + callsite_ish_constraints: true, +}; + +/// A convenience wrapper to generate a [`func::Func`] with `fastalloc`-specific +/// options enabled. +#[derive(Clone, Debug)] +pub struct TestCase { + func: func::Func, + annotate: bool, + check_ssa: bool, +} + +impl Arbitrary<'_> for TestCase { + fn arbitrary(u: &mut Unstructured) -> Result { + let func = func::Func::arbitrary_with_options(u, &OPTIONS)?; + let annotate = bool::arbitrary(u)?; + let check_ssa = bool::arbitrary(u)?; + Ok(TestCase { + func, + annotate, + check_ssa, + }) + } +} + +/// Test a single function with the `fastalloc` allocator. +/// +/// This also: +/// - optionally creates annotations +/// - optionally verifies the incoming SSA +/// - runs the [`checker`]. +pub fn check(t: TestCase) { + let TestCase { + func, + annotate, + check_ssa, + } = &t; + log::trace!("func:\n{func:?}"); + + let env = func::machine_env(); + let out = fastalloc::run(func, &env, *annotate, *check_ssa).expect("regalloc did not succeed"); + + let mut checker = checker::Checker::new(func, &env); + checker.prepare(&out); + checker.run().expect("checker failed"); +} + +#[test] +fn smoke() { + arbtest::arbtest(|u| { + let test_case = TestCase::arbitrary(u)?; + check(test_case); + Ok(()) + }) + .budget_ms(1_000); +} diff --git a/src/fuzzing/func.rs b/src/fuzzing/func.rs index 7466d90e..7a026af7 100644 --- a/src/fuzzing/func.rs +++ b/src/fuzzing/func.rs @@ -11,8 +11,8 @@ use crate::{ use alloc::vec::Vec; use alloc::{format, vec}; -use super::arbitrary::Result as ArbitraryResult; -use super::arbitrary::{Arbitrary, Unstructured}; +use arbitrary::Result as ArbitraryResult; +use arbitrary::{Arbitrary, Unstructured}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum InstOpcode { diff --git a/src/fuzzing/ion.rs b/src/fuzzing/ion.rs new file mode 100644 index 00000000..b2ce9b08 --- /dev/null +++ b/src/fuzzing/ion.rs @@ -0,0 +1,86 @@ +//! Fuzz the `ion` register allocator. + +use crate::{checker, fuzzing::func, ion}; +use arbitrary::{Arbitrary, Result, Unstructured}; +use core::cell::RefCell; +use std::thread_local; + +/// `ion`-specific options for generating functions. +const OPTIONS: func::Options = func::Options { + reused_inputs: true, + fixed_regs: true, + fixed_nonallocatable: true, + clobbers: true, + reftypes: true, + callsite_ish_constraints: true, +}; + +/// A convenience wrapper to generate a [`func::Func`] with `ion`-specific +/// options enabled. +#[derive(Clone, Debug)] +pub struct TestCase { + func: func::Func, + annotate: bool, + check_ssa: bool, +} + +impl Arbitrary<'_> for TestCase { + fn arbitrary(u: &mut Unstructured) -> Result { + let options = func::Options { + reused_inputs: true, + fixed_regs: true, + fixed_nonallocatable: true, + clobbers: true, + reftypes: true, + callsite_ish_constraints: true, + }; + let func = func::Func::arbitrary_with_options(u, &options)?; + let annotate = bool::arbitrary(u)?; + let check_ssa = bool::arbitrary(u)?; + Ok(TestCase { + func, + annotate, + check_ssa, + }) + } +} + +/// Test a single function with the `ion` allocator. +/// +/// This also: +/// - optionally creates annotations +/// - optionally verifies the incoming SSA +/// - runs the [`checker`]. +pub fn check(t: TestCase) { + let TestCase { + func, + annotate, + check_ssa, + } = &t; + log::trace!("func:\n{func:?}"); + + let env = func::machine_env(); + thread_local! { + // We test that ctx is cleared properly between runs. + static CTX: RefCell = RefCell::default(); + } + + CTX.with(|ctx| { + ion::run(func, &env, &mut *ctx.borrow_mut(), *annotate, *check_ssa) + .expect("regalloc did not succeed"); + + let mut checker = checker::Checker::new(func, &env); + checker.prepare(&ctx.borrow().output); + checker.run().expect("checker failed"); + }); +} + +#[test] +fn smoke() { + arbtest::arbtest(|u| { + let test_case = TestCase::arbitrary(u)?; + check(test_case); + Ok(()) + }) + .budget_ms(1_000); +} diff --git a/src/fuzzing/mod.rs b/src/fuzzing/mod.rs index 1aa619ec..9461524a 100644 --- a/src/fuzzing/mod.rs +++ b/src/fuzzing/mod.rs @@ -3,32 +3,8 @@ * exception. See `LICENSE` for details. */ -//! Utilities for fuzzing. - +pub mod domtree; +pub mod fastalloc; pub mod func; - -// Re-exports for fuzz targets. - -pub mod domtree { - pub use crate::domtree::*; -} -pub mod postorder { - pub use crate::postorder::*; -} -pub mod moves { - pub use crate::moves::*; -} -pub mod cfg { - pub use crate::cfg::*; -} -pub mod ion { - pub use crate::ion::*; -} -pub mod fastalloc { - pub use crate::fastalloc::*; -} -pub mod checker { - pub use crate::checker::*; -} - -pub use libfuzzer_sys::{arbitrary, fuzz_target}; +pub mod ion; +pub mod moves; diff --git a/src/fuzzing/moves.rs b/src/fuzzing/moves.rs new file mode 100644 index 00000000..4478fc71 --- /dev/null +++ b/src/fuzzing/moves.rs @@ -0,0 +1,141 @@ +//! Fuzz the parallel-move resolver. + +use crate::moves::{MoveAndScratchResolver, ParallelMoves}; +use crate::{Allocation, PReg, RegClass, SpillSlot}; +use arbitrary::{Arbitrary, Result, Unstructured}; +use std::collections::{HashMap, HashSet}; +use std::{vec, vec::Vec}; + +fn is_stack_alloc(alloc: Allocation) -> bool { + // Treat registers 20..=29 as fixed stack slots. + if let Some(reg) = alloc.as_reg() { + reg.index() > 20 + } else { + alloc.is_stack() + } +} + +/// +#[derive(Clone, Debug)] +pub struct TestCase { + moves: Vec<(Allocation, Allocation)>, + available_pregs: Vec, +} + +impl Arbitrary<'_> for TestCase { + fn arbitrary(u: &mut Unstructured) -> Result { + let mut ret = TestCase { + moves: vec![], + available_pregs: vec![], + }; + let mut written = HashSet::new(); + // An arbitrary sequence of moves between registers 0 to 29 + // inclusive. + while bool::arbitrary(u)? { + let src = if bool::arbitrary(u)? { + let reg = u.int_in_range(0..=29)?; + Allocation::reg(PReg::new(reg, RegClass::Int)) + } else { + let slot = u.int_in_range(0..=31)?; + Allocation::stack(SpillSlot::new(slot)) + }; + let dst = if bool::arbitrary(u)? { + let reg = u.int_in_range(0..=29)?; + Allocation::reg(PReg::new(reg, RegClass::Int)) + } else { + let slot = u.int_in_range(0..=31)?; + Allocation::stack(SpillSlot::new(slot)) + }; + + // Stop if we are going to write a reg more than once: + // that creates an invalid parallel move set. + if written.contains(&dst) { + break; + } + written.insert(dst); + + ret.moves.push((src, dst)); + } + + // We might have some unallocated registers free for scratch + // space... + for i in 0..u.int_in_range(0..=2)? { + let reg = PReg::new(30 + i, RegClass::Int); + ret.available_pregs.push(Allocation::reg(reg)); + } + Ok(ret) + } +} + +pub fn check(t: TestCase) { + let mut par = ParallelMoves::new(); + for &(src, dst) in &t.moves { + par.add(src, dst, ()); + } + + let moves = par.resolve(); + log::trace!("raw resolved moves: {:?}", moves); + + // Resolve uses of scratch reg and stack-to-stack moves with the scratch + // resolver. + let mut avail = t.available_pregs.clone(); + let find_free_reg = || avail.pop(); + let mut next_slot = 32; + let get_stackslot = || { + let slot = next_slot; + next_slot += 1; + Allocation::stack(SpillSlot::new(slot)) + }; + let preferred_victim = PReg::new(0, RegClass::Int); + let scratch_resolver = MoveAndScratchResolver { + find_free_reg, + get_stackslot, + is_stack_alloc, + borrowed_scratch_reg: preferred_victim, + }; + let moves = scratch_resolver.compute(moves); + log::trace!("resolved moves: {:?}", moves); + + // Compute the final source reg for each dest reg in the original + // parallel-move set. + let mut final_src_per_dest: HashMap = HashMap::new(); + for &(src, dst) in &t.moves { + final_src_per_dest.insert(dst, src); + } + log::trace!("expected final state: {:?}", final_src_per_dest); + + // Simulate the sequence of moves. + let mut locations: HashMap = HashMap::new(); + for (src, dst, _) in moves { + let data = locations.get(&src).cloned().unwrap_or(src); + locations.insert(dst, data); + } + log::trace!("simulated final state: {:?}", locations); + + // Assert that the expected register-moves occurred. + for (reg, data) in locations { + if let Some(&expected_data) = final_src_per_dest.get(®) { + assert_eq!(expected_data, data); + } else { + if data != reg { + // If not just the original value, then this location has been + // modified, but it was not part of the original parallel move. + // It must have been an available preg or a scratch stackslot. + assert!( + t.available_pregs.contains(®) + || (reg.is_stack() && reg.as_stack().unwrap().index() >= 32) + ); + } + } + } +} + +#[test] +fn smoke() { + arbtest::arbtest(|u| { + let test_case = TestCase::arbitrary(u)?; + check(test_case); + Ok(()) + }) + .budget_ms(1_000); +} From f725a885ea900c157cf09884bbf39e88215eec60 Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Mon, 22 Sep 2025 09:37:48 -0700 Subject: [PATCH 2/3] ci: build all fuzz targets; run fuzz targets under `cargo test` Now that we can run the fuzzers with `arbtest`, we use `cargo test --all-features` to run all of these for one second. This allows cleaning up the building of the fuzz targets themselves and the static set of smoke tests in `*.bin` for the old `ion_checker`. --- .github/workflows/rust.yml | 14 +++++--------- fuzz/smoketest/ion_checker.bin | Bin 2779 -> 0 bytes 2 files changed, 5 insertions(+), 9 deletions(-) delete mode 100644 fuzz/smoketest/ion_checker.bin diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 9bcdcb42..13498626 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -27,7 +27,7 @@ jobs: - name: Build run: cargo build - name: Run tests - run: cargo test --all --verbose + run: cargo test --all --all-features --verbose # Make sure the code typechecks with non-default features enabled. features: @@ -72,11 +72,7 @@ jobs: run: rustup toolchain install nightly - name: Install cargo-fuzz run: cargo +nightly install cargo-fuzz - - name: Build ssagen fuzzing target - run: cargo +nightly fuzz build ssagen - - name: Build moves fuzzing target - run: cargo +nightly fuzz build moves - - name: Build ion fuzzing target - run: cargo +nightly fuzz build ion - - name: Build and smoke-test ion_checker fuzzing target - run: cargo +nightly fuzz run ion_checker ./fuzz/smoketest/ion_checker.bin + - name: Build all fuzz targets + run: cargo +nightly fuzz build + # Note: all fuzzers are run with `arbtest` during `cargo test` with the + # `fuzzing` feature enabled. diff --git a/fuzz/smoketest/ion_checker.bin b/fuzz/smoketest/ion_checker.bin deleted file mode 100644 index 5156f22792d0392e0b66617f2e8702bcd4a5d798..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2779 zcmd^BJ!q3b7=A8jTQihEOGt*$7JphTmUeKdgMyPw5f{NK$>*R$7iY;5x;XeV1ph!! zM8PT2DSHstf>Y-&LqV_zQb|n<=li~Q_vK5QPirbD_(1Nx_wL@GyWID@ha3!Jlzs!4 z10y}m&AJJIb&q-9A(>}rV3`-!7#q^=ZIZ@iWg`s3mbZB)9Vje(To6oPBt3s;`L$6O z-r^($(yLnEG^Xrhddo;+2pA`yVHgc{cMz`0bl7Os$cyaT)M^(6mj&0n#U3XcZU$l4 z;1D)*KkCiAOd6MYw(oD_l(oe)od*V2%jI$#N?W|t`nq0nN+hPDp4Jsj|C+-hpuW)= zKvfANYdhxEee-?@emQWPVracT`|6b)bC1n|(j;c<^*V={kXT9%8kkG7??D;ExD~)- z9(=^jQLUe`)aedHk2RPjJEzYm1(V9>*?lwX{51(ku`(<;^5F{)b+8f`c=<-<$H54tmk{o0g!B#S6nTJH7;?C0;_ebACk zueASM*8kq^>TLQyzFl8?-mVp*_t+Io`Vh3f5d^nl3vG9cs3b>x>#k&bI|%r!Hy1Z8 zmaMqsR1v(On|7*1J473IotHyQ9%t%AlKpY{vHrN+ad8r7CyoD{%XwB<{+HG4x4vpV z!wxA6vl55%*ulu%6ZXTu312B@)6tpq)8W(T&3;d4ri-*UY^h<3IQGHWJ~3-1W#c*f z#yQ{=PY$336znsja=KV7jundvDl=0WJ*z%vG8y--p=S-3WqC|_r_;Uy#*>nDN%b%O zfg*LD9L0(WX^D^s6&-(R(Ze8z!(_qrJ92z*zHm9Tx8F!m;fmO^(se%-5Cq12#}q+K Z#(-=oV1iNZV$eH70U7IX()ryjegJnaJ^=s# From c71e99abc8a5ab893e020f6a90aee76089e055d7 Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Tue, 23 Sep 2025 14:28:41 -0700 Subject: [PATCH 3/3] Use `Options::DEFAULT` to fill out `Options` constructor Now-upstreamed changes require the `Options` structure to define more fields. We use a `const` variable here (versus `impl Default`) so it can be used in other `const` contexts. --- src/fuzzing/fastalloc.rs | 1 + src/fuzzing/func.rs | 33 ++++++++++++++++----------------- src/fuzzing/ion.rs | 11 ++--------- 3 files changed, 19 insertions(+), 26 deletions(-) diff --git a/src/fuzzing/fastalloc.rs b/src/fuzzing/fastalloc.rs index 9ff05245..840b278f 100644 --- a/src/fuzzing/fastalloc.rs +++ b/src/fuzzing/fastalloc.rs @@ -11,6 +11,7 @@ const OPTIONS: func::Options = func::Options { clobbers: true, reftypes: false, callsite_ish_constraints: true, + ..func::Options::DEFAULT }; /// A convenience wrapper to generate a [`func::Func`] with `fastalloc`-specific diff --git a/src/fuzzing/func.rs b/src/fuzzing/func.rs index 87a02a36..1ba234ba 100644 --- a/src/fuzzing/func.rs +++ b/src/fuzzing/func.rs @@ -376,27 +376,26 @@ pub struct Options { pub num_clobbers_per_inst: RangeInclusive, } -impl core::default::Default for Options { - fn default() -> Self { - Options { - reused_inputs: false, - fixed_regs: false, - fixed_nonallocatable: false, - clobbers: false, - reftypes: false, - callsite_ish_constraints: false, - num_blocks: 1..=100, - num_vregs_per_block: 5..=15, - num_uses_per_inst: 0..=10, - num_callsite_ish_vregs_per_inst: 0..=20, - num_clobbers_per_inst: 0..=10, - } - } +impl Options { + /// Default options for generating functions. + pub const DEFAULT: Self = Self { + reused_inputs: false, + fixed_regs: false, + fixed_nonallocatable: false, + clobbers: false, + reftypes: false, + callsite_ish_constraints: false, + num_blocks: 1..=100, + num_vregs_per_block: 5..=15, + num_uses_per_inst: 0..=10, + num_callsite_ish_vregs_per_inst: 0..=20, + num_clobbers_per_inst: 0..=10, + }; } impl Arbitrary<'_> for Func { fn arbitrary(u: &mut Unstructured) -> ArbitraryResult { - Func::arbitrary_with_options(u, &Options::default()) + Func::arbitrary_with_options(u, &Options::DEFAULT) } } diff --git a/src/fuzzing/ion.rs b/src/fuzzing/ion.rs index b2ce9b08..e64b5a31 100644 --- a/src/fuzzing/ion.rs +++ b/src/fuzzing/ion.rs @@ -13,6 +13,7 @@ const OPTIONS: func::Options = func::Options { clobbers: true, reftypes: true, callsite_ish_constraints: true, + ..func::Options::DEFAULT }; /// A convenience wrapper to generate a [`func::Func`] with `ion`-specific @@ -26,15 +27,7 @@ pub struct TestCase { impl Arbitrary<'_> for TestCase { fn arbitrary(u: &mut Unstructured) -> Result { - let options = func::Options { - reused_inputs: true, - fixed_regs: true, - fixed_nonallocatable: true, - clobbers: true, - reftypes: true, - callsite_ish_constraints: true, - }; - let func = func::Func::arbitrary_with_options(u, &options)?; + let func = func::Func::arbitrary_with_options(u, &OPTIONS)?; let annotate = bool::arbitrary(u)?; let check_ssa = bool::arbitrary(u)?; Ok(TestCase {