-
Notifications
You must be signed in to change notification settings - Fork 23
smite-ir: add program minimizers and wire them into AFL++ custom trim #54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
eab6dee
smite-ir: add Operation::has_side_effects predicate
erickcestari 6bbd27e
smite-ir: add dead-code and common-subexpression minimizers
erickcestari e8a361a
smite-ir-mutator: implement afl_custom_trim with minimizer pipeline
erickcestari 54651be
smite-ir-mutator: add opt-in logging with trim before/after dump
erickcestari File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,8 +16,15 @@ | |
| //! - `AFL_FRAMESHIFT_DISABLE=1` -- disable AFL++'s `FrameShift` analysis that | ||
| //! bypasses our custom mutators. This was an AFL++ bug fixed upstream in | ||
| //! commit eddb2701b022351fb34b696ccf923bb856e9d953. | ||
| //! - `AFL_DISABLE_TRIM=1` -- this library does not implement custom trim and | ||
| //! AFL++'s default byte-level trim would corrupt our structured programs. | ||
| //! | ||
| //! # Logging | ||
| //! | ||
| //! Logging is opt-in: [`afl_custom_init`] installs a logger only when | ||
| //! `RUST_LOG` is set. With `RUST_LOG` unset, every `log::*!` callsite is a | ||
| //! no-op and AFL's stderr stays clean. Useful filters: | ||
| //! - `RUST_LOG=smite_ir_mutator::trim=debug` -- print the decoded `Program` | ||
| //! before and after each successful trim. | ||
| //! - `RUST_LOG=debug` -- everything this crate emits. | ||
| //! | ||
| //! # Buffer ownership | ||
| //! | ||
|
|
@@ -33,6 +40,7 @@ use rand::rngs::SmallRng; | |
| use rand::{RngExt, SeedableRng}; | ||
|
|
||
| use smite_ir::generators::{NodeAnnouncementGenerator, OpenChannelGenerator}; | ||
| use smite_ir::minimizers::{CommonSubexpressionEliminator, DeadCodeEliminator, Minimizer}; | ||
| use smite_ir::mutators::{InputSwapMutator, OperationParamMutator}; | ||
| use smite_ir::{Generator, Mutator, Program, ProgramBuilder}; | ||
|
|
||
|
|
@@ -152,14 +160,26 @@ fn warn_on_unset_afl_env() { | |
| /// Allocates a new [`MutatorState`] and returns an opaque pointer to it. AFL++ | ||
| /// passes this pointer back on every function call as the `data` argument. | ||
| /// | ||
| /// Also installs `simple_logger` if `RUST_LOG` is set, so `log::*!` callsites | ||
| /// in this crate become live. Without `RUST_LOG` no logger is installed and | ||
| /// every callsite is a no-op. Set | ||
| /// `RUST_LOG=smite_ir_mutator::trim=debug` to see trim before/after dumps. | ||
| /// | ||
| /// # Safety | ||
| /// | ||
| /// The returned pointer is heap-allocated via `Box::into_raw` and must be freed | ||
| /// by a matching call to [`afl_custom_deinit`]. | ||
| /// | ||
| /// # Panics | ||
| /// | ||
| /// Panics if `RUST_LOG` is set and `simple_logger` fails to initialize. | ||
| #[unsafe(no_mangle)] | ||
| pub unsafe extern "C" fn afl_custom_init(_afl: *const c_void, seed: c_uint) -> *mut c_void { | ||
| #[cfg(not(test))] | ||
| warn_on_unset_afl_env(); | ||
| if std::env::var_os("RUST_LOG").is_some() { | ||
| simple_logger::init_with_env().expect("logger initializes"); | ||
| } | ||
| Box::into_raw(Box::new(MutatorState::new(seed))).cast::<c_void>() | ||
| } | ||
|
|
||
|
|
@@ -228,6 +248,115 @@ pub unsafe extern "C" fn afl_custom_fuzz( | |
| len | ||
| } | ||
|
|
||
| /// Runs the full minimizer pipeline (`DeadCodeEliminator` then | ||
| /// `CommonSubexpressionEliminator`) on the corpus entry and stages the | ||
| /// resulting candidate for [`afl_custom_trim`] to hand back. | ||
| /// | ||
| /// Both minimizers are deterministic in-process transforms safe in IR | ||
| /// semantics, so we don't need iterative AFL feedback. We compose them | ||
| /// once and offer a single candidate. AFL still gets to verify it (its | ||
| /// coverage cksum is the source of truth); on rejection AFL silently | ||
| /// discards the candidate and keeps the original corpus entry. | ||
| /// | ||
| /// AFL drives the trim loop with `while (stage_cur < stage_max)`, where | ||
| /// `stage_max` is this function's return value and `stage_cur` is updated | ||
| /// from [`afl_custom_post_trim`]'s return. | ||
| /// | ||
| /// # Returns | ||
| /// | ||
| /// - `1` if there's a candidate to offer (decode succeeded, validate | ||
| /// passed, and the trim actually shrank the program). AFL enters the | ||
| /// trim loop for one iteration. | ||
| /// - `0` if there's nothing to do (decode/validate failed, or the trim | ||
| /// was a no-op). AFL skips trim entirely. | ||
| /// - Negative would signal a fatal error to AFL; we never produce one. | ||
| /// | ||
| /// # Safety | ||
| /// | ||
| /// - `data` must be a pointer returned by [`afl_custom_init`]. | ||
| /// - `buf` must point to `buf_size` readable bytes. | ||
| #[unsafe(no_mangle)] | ||
| pub unsafe extern "C" fn afl_custom_init_trim( | ||
| data: *mut c_void, | ||
| buf: *mut u8, | ||
| buf_size: usize, | ||
| ) -> i32 { | ||
| let state = unsafe { &mut *data.cast::<MutatorState>() }; | ||
|
|
||
| let input = unsafe { slice::from_raw_parts(buf, buf_size) }; | ||
| let Some(program) = decode_and_validate(input) else { | ||
| return 0; | ||
| }; | ||
|
|
||
| let before = log::log_enabled!(target: "smite_ir_mutator::trim", log::Level::Debug) | ||
| .then(|| program.clone()); | ||
|
|
||
| let mut trimmed = program; | ||
| let dce_changed = DeadCodeEliminator.minimize(&mut trimmed); | ||
| let cse_changed = CommonSubexpressionEliminator.minimize(&mut trimmed); | ||
| if (!dce_changed && !cse_changed) || !state.serialize(&trimmed, buf_size) { | ||
| return 0; | ||
| } | ||
|
|
||
| if let Some(before) = before { | ||
| log::debug!( | ||
| target: "smite_ir_mutator::trim", | ||
| "dce={dce_changed} cse={cse_changed}\n--- before ---\n{before}--- after ---\n{trimmed}---" | ||
| ); | ||
| } | ||
|
|
||
| 1 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We may also need to update |
||
| } | ||
|
|
||
| /// Hands the pre-serialized trimmed candidate back to AFL. | ||
| /// | ||
| /// The pointer written into `*out_buf` borrows from `MutatorState::out_buf` | ||
| /// and is valid until the next call into this library; AFL copies the | ||
| /// bytes before re-entering us. We always write a non-null pointer (even | ||
| /// on the zero-length path) to satisfy AFL's `if (unlikely(!retbuf)) | ||
| /// FATAL(...)` check. | ||
| /// | ||
| /// # Returns | ||
| /// | ||
| /// - `> 0` on the first call after [`afl_custom_init_trim`]: the byte | ||
| /// length of the candidate at `*out_buf`. | ||
| /// - `0` afterwards. AFL treats this as "skip this iteration" rather than | ||
| /// a stop signal; the loop terminates via [`afl_custom_post_trim`]'s | ||
| /// return. | ||
| /// | ||
| /// # Safety | ||
| /// | ||
| /// - `data` must be a pointer returned by [`afl_custom_init`]. | ||
| /// - `out_buf` must be a valid, writable pointer to a `*const u8` slot. | ||
| #[unsafe(no_mangle)] | ||
| pub unsafe extern "C" fn afl_custom_trim(data: *mut c_void, out_buf: *mut *const u8) -> usize { | ||
| let state = unsafe { &mut *data.cast::<MutatorState>() }; | ||
| unsafe { *out_buf = state.out_buf.as_ptr() }; | ||
| state.out_buf.len() | ||
| } | ||
|
|
||
| /// Always returns `1` to terminate AFL's trim loop after a single | ||
| /// iteration. | ||
| /// | ||
| /// AFL drives trim with `while (stage_cur < stage_max)` and assigns | ||
| /// `stage_cur` from this function's return value. With `stage_max = 1` | ||
| /// (set by [`afl_custom_init_trim`]), returning `1` makes the condition | ||
| /// `1 < 1` false and breaks the loop. | ||
| /// | ||
| /// `success` indicates whether the candidate's coverage cksum matched the | ||
| /// original. We don't need to act on it: AFL itself either persists the | ||
| /// trimmed buffer (on success) or keeps the original corpus entry (on | ||
| /// failure), and we don't track partial state across iterations because | ||
| /// there's only one. | ||
| /// | ||
| /// # Safety | ||
| /// | ||
| /// - `data` must be a pointer returned by [`afl_custom_init`]. | ||
| #[unsafe(no_mangle)] | ||
| pub unsafe extern "C" fn afl_custom_post_trim(_data: *mut c_void, _success: u8) -> i32 { | ||
| 1 | ||
| } | ||
|
|
||
| /// Marker symbol that tells AFL++ not to populate `add_buf` for | ||
| /// [`afl_custom_fuzz`]. AFL++ never actually calls this function -- it only | ||
| /// checks for the symbol's presence via `dlsym` and, if found, skips picking a | ||
|
|
@@ -338,6 +467,19 @@ mod tests { | |
| postcard::to_allocvec(&builder.build()).expect("postcard serialization") | ||
| } | ||
|
|
||
| /// `seed_program_bytes()` plus an unreferenced `LoadAmount`, so the | ||
| /// pipeline has something for DCE to drop (and thus `init_trim` | ||
| /// returns `1`). | ||
| fn reducible_seed_bytes() -> Vec<u8> { | ||
| let bytes = seed_program_bytes(); | ||
| let mut program: Program = postcard::from_bytes(&bytes).expect("decode"); | ||
| program.instructions.push(smite_ir::Instruction { | ||
| operation: smite_ir::Operation::LoadAmount(0xdead_beef), | ||
| inputs: vec![], | ||
| }); | ||
| postcard::to_allocvec(&program).expect("encode") | ||
| } | ||
|
|
||
| #[test] | ||
| fn init_returns_nonnull() { | ||
| let state = State::new(0); | ||
|
|
@@ -435,4 +577,106 @@ mod tests { | |
| // crash either. | ||
| unsafe { afl_custom_splice_optout(ptr::null_mut()) }; | ||
| } | ||
|
|
||
| // -- Trim tests -- | ||
|
|
||
| fn init_trim_via_ffi(state: &State, mut input: Vec<u8>) -> i32 { | ||
| unsafe { afl_custom_init_trim(state.0, input.as_mut_ptr(), input.len()) } | ||
| } | ||
|
|
||
| fn trim_via_ffi(state: &State) -> (*const u8, usize) { | ||
| let mut out: *const u8 = ptr::null(); | ||
| let len = unsafe { afl_custom_trim(state.0, &raw mut out) }; | ||
| (out, len) | ||
| } | ||
|
|
||
| fn post_trim_via_ffi(state: &State, success: bool) -> i32 { | ||
| unsafe { afl_custom_post_trim(state.0, u8::from(success)) } | ||
| } | ||
|
|
||
| #[test] | ||
| fn trim_init_returns_1_when_reduction_possible() { | ||
| let state = State::new(0); | ||
| let rv = init_trim_via_ffi(&state, reducible_seed_bytes()); | ||
| assert_eq!(rv, 1); | ||
| } | ||
|
|
||
| #[test] | ||
| fn trim_init_returns_0_when_no_reduction_possible() { | ||
| // Generator output has no dead code or duplicate loads; the | ||
| // pipeline is a no-op, so we tell AFL to skip trim entirely. | ||
| let state = State::new(0); | ||
| let rv = init_trim_via_ffi(&state, seed_program_bytes()); | ||
| assert_eq!(rv, 0); | ||
| } | ||
|
|
||
| #[test] | ||
| fn trim_init_returns_0_for_garbage() { | ||
| let state = State::new(0); | ||
| let rv = init_trim_via_ffi(&state, vec![0xFF; 16]); | ||
| assert_eq!(rv, 0); | ||
| } | ||
|
|
||
| #[test] | ||
| fn trim_yields_candidate_after_init() { | ||
| let state = State::new(0); | ||
| init_trim_via_ffi(&state, reducible_seed_bytes()); | ||
| let (out, len) = trim_via_ffi(&state); | ||
| assert!(len > 0); | ||
| decode_and_validate(out, len); | ||
| } | ||
|
|
||
| #[test] | ||
| fn trim_post_trim_returns_1_to_terminate_loop() { | ||
| let state = State::new(0); | ||
| init_trim_via_ffi(&state, reducible_seed_bytes()); | ||
| let _ = trim_via_ffi(&state); | ||
| // post_trim returns 1 unconditionally — it's the load-bearing | ||
| // termination signal that pushes AFL's `stage_cur` to `stage_max`. | ||
| assert_eq!(post_trim_via_ffi(&state, true), 1); | ||
| assert_eq!(post_trim_via_ffi(&state, false), 1); | ||
| } | ||
|
|
||
| #[test] | ||
| fn trim_init_does_not_overwrite_sequence() { | ||
| // Trim is not a mutation; `last_sequence` (used by `describe` to | ||
| // name queue entries from fuzz) must survive both the no-op and | ||
| // successful trim paths. | ||
| for (label, input, expected_rv) in [ | ||
| ("no-op", seed_program_bytes(), 0), | ||
| ("success", reducible_seed_bytes(), 1), | ||
| ] { | ||
| let state = State::new(0); | ||
| // Run a fuzz call so last_sequence has known contents. | ||
| let _ = fuzz_via_ffi(&state, Vec::new(), 1 << 16); | ||
| let before = unsafe { CStr::from_ptr(afl_custom_describe(state.0, 256)) } | ||
| .to_str() | ||
| .expect("valid utf-8") | ||
| .to_string(); | ||
| let rv = init_trim_via_ffi(&state, input); | ||
| assert_eq!(rv, expected_rv, "{label}"); | ||
| let after = unsafe { CStr::from_ptr(afl_custom_describe(state.0, 256)) } | ||
| .to_str() | ||
| .expect("valid utf-8") | ||
| .to_string(); | ||
| assert_eq!(before, after, "{label}"); | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn trim_candidate_is_smaller_than_input() { | ||
| let original_bytes = reducible_seed_bytes(); | ||
| let original_program: Program = postcard::from_bytes(&original_bytes).expect("decode"); | ||
|
|
||
| let state = State::new(0); | ||
| init_trim_via_ffi(&state, original_bytes); | ||
|
|
||
| let (out, len) = trim_via_ffi(&state); | ||
| assert!(len > 0, "trim should yield a candidate"); | ||
| let trimmed = decode_and_validate(out, len); | ||
| assert!( | ||
| trimmed.instructions.len() < original_program.instructions.len(), | ||
| "trim should shrink instruction count" | ||
| ); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| //! IR program minimizers. | ||
| //! | ||
| //! A [`Minimizer`] reduces a [`Program`] to a smaller, behaviourally | ||
| //! equivalent version in a single pass. Both transforms are safe in IR | ||
| //! semantics, so they don't need an oracle to drive the search. | ||
| //! | ||
| //! Run them in pipeline order for best results: | ||
| //! 1. [`DeadCodeEliminator`] — drop dead instructions and reindex | ||
| //! 2. [`CommonSubexpressionEliminator`] — merge equivalent pure expressions | ||
|
|
||
| mod cse; | ||
| mod dead_code; | ||
|
|
||
| pub use cse::CommonSubexpressionEliminator; | ||
| pub use dead_code::DeadCodeEliminator; | ||
|
|
||
| use super::Program; | ||
|
|
||
| /// A minimizer that reduces an IR program in one call. | ||
| pub trait Minimizer { | ||
| /// Reduces `program` in place to a smaller, behaviourally equivalent | ||
| /// version. Returns `true` if the program was modified. | ||
| fn minimize(&self, program: &mut Program) -> bool; | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.