Skip to content
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

Filter Duplicate Input Execution #2771

Open
wants to merge 24 commits into
base: main
Choose a base branch
from

Conversation

riesentoaster
Copy link
Contributor

See #2759.

Some mutators report MutationResult::Mutated, even if nothing actually changes about the input. HashMutator is a wrapper around other mutators that hashes inputs pre- and post-mutation to ensure MutationResult::Mutated is only reported if something actually changed.

This may be worth using on slow targets, where the hashing is quicker than the unnecessary additional executions of the target for previously tried inputs.

riesentoaster and others added 17 commits December 6, 2024 17:02
* fixing empty multipart name

* fixing clippy

* improve flexibility of DumpToDiskStage

* adding note to MIGRATION.md
Updates the requirements on [bindgen](https://github.com/rust-lang/rust-bindgen) to permit the latest version.
- [Release notes](https://github.com/rust-lang/rust-bindgen/releases)
- [Changelog](https://github.com/rust-lang/rust-bindgen/blob/main/CHANGELOG.md)
- [Commits](rust-lang/rust-bindgen@v0.70.1...v0.71.1)

---
updated-dependencies:
- dependency-name: bindgen
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* no from stage

* fixer

* doc fix

* how was this working????

* more fixes

* delete more

* rq

* cargo-fuzz

* m

* aa
* go

* fixing stuf

* hello from windows

* more

* lolg

* lolf

* fix

* a

---------

Co-authored-by: Your Name <[email protected]>
* Maybe fix CI

* does this help?

* Very dirty 'fix'
* fixing empty multipart name

* fixing clippy

* New rules for the contributing (AFLplusplus#2752)

* Rules

* more

* aa

* Improve Flexibility of DumpToDiskStage (AFLplusplus#2753)

* fixing empty multipart name

* fixing clippy

* improve flexibility of DumpToDiskStage

* adding note to MIGRATION.md

* Introduce WrappingMutator

* introducing mutators for int types

* fixing no_std

* random fixes

* Add hash derivation for WrappingInput

* Revert fixes that broke things

* Derive Default on WrappingInput

* Add unit tests

* Fixes according to code review

* introduce mappable ValueInputs

* remove unnecessary comments

* Elide more lifetimes

* remove dead code

* simplify hashing

* improve docs

* improve randomization

* rename method to align with standard library

* add typedefs for int types for ValueMutRefInput

* rename test

* add safety notice to trait function

* improve randomize performance for i128/u128

* rename macro

* improve comment

* actually check return values in test

* make 128 bit int randomize even more efficient

* shifting signed values

---------

Co-authored-by: Dongjia "toka" Zhang <[email protected]>
Co-authored-by: Dominik Maier <[email protected]>
@domenukk
Copy link
Member

domenukk commented Dec 15, 2024

HashFilterMutator?

Or even HashMutationFilter

@domenukk
Copy link
Member

As I stated in the discussion thread, I think a method for rejecting inputs that were already tried would be more useful (but I don't know your use case, so..)
Maybe using a Bloomfilter on the executor, or similar..

@riesentoaster
Copy link
Contributor Author

riesentoaster commented Dec 15, 2024

As I stated in the discussion thread, I think a method for rejecting inputs that were already tried would be more useful (but I don't know your use case, so..)

I'm targeting the TCP/IP stack of an OS, so each execution takes in the order of magnitude of 1s, although most of that is spent in wait states (hence previous work like overcommit). Even still, the added runtime of this would be nothing compared to the execution, so this felt like an easy win.

Maybe using a Bloomfilter on the executor, or similar..

Something like this would definitely further improve the situation. Do you suggest creating a wrapping executor that returns either ExitKind::Ok or a new ExitKind::Skipped if the input was previously evaluated? This seems like a bodged-on solution as well though, since observers/feedbacks still run — we probably don't even want to call the executor in such cases.

Tracing this back it seems most appropriate in the stage? But that seems not that generic. So maybe in Fuzzer (resp. it's Evaluator impl)?

I'm also not sure if there's an opportunity here to combine this somehow with CentralizedLauncher?

@domenukk
Copy link
Member

I think it could simply wrap an executor, yeah. And have an extra observation that's "skipped" -if it's true the testcase isn't interesting. Should be easy enough to do.

\We can still merge this PR as well, but the feedback should be renamed IMHO.

@riesentoaster
Copy link
Contributor Author

How about something like this?

@riesentoaster
Copy link
Contributor Author

I'll do some performance comparisons later today. Initial runs suggest that adding even a 10µs sleep to the harness reduces the performance penalty to <5%.

I might also see how many duplicate inputs actually appear. But for now I feel like for slow targets this very well might be worth using.

@riesentoaster
Copy link
Contributor Author

riesentoaster commented Dec 18, 2024

Alright, some performance tests. Running against the libfuzzer_libpng example fuzzer:

  1. Without the bloom filter, I'm getting a throughput of ~100k/s.
  2. With the bloom filter, I get ~85k/s.
  3. The rate of duplicate vs. new inputs increases over time, after 1min and 2min it was 0.6%, after 3min it was 1%, after 4min 2%, after 5min 4.4%, after ~7min it reaches 10%, after ~13mins 40%. At this point I assume most inputs are going to be duplicates.

All these numbers obviously depend on the exact fuzzers:

  1. When the corpus count reaches a plateau, duplicate inputs become increasingly likely
  2. When the number of possible mutations is small, duplicate inputs are more likely
  3. If the execution time of the target is larger, the added runtime may be less than the runtime saved from not executing an input twice.
  4. The bloom filter requires quite a bit of memory, so if that is your limitation, not using it and instead spawning additional instances may be worth it

Overall, I feel like this may be worth having in the library.

Btw: There is no easy way of adding metadata to the state such that it is printed by monitors, right? Otherwise, calculating the number/rate of duplicates may be an interesting addition.

@domenukk
Copy link
Member

There is an easy way, using UserStats see... however other things do UserStats. For example the stability in the calibration stage.

@@ -56,28 +56,13 @@ license = "MIT OR Apache-2.0"
# Internal deps
libafl = { path = "./libafl", version = "0.14.1", default-features = false }
libafl_bolts = { path = "./libafl_bolts", version = "0.14.1", default-features = false }
libafl_cc = { path = "./libafl_cc", version = "0.14.1", default-features = false }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's just testing for I assume?

@@ -8,8 +8,9 @@ authors = [
edition = "2021"

[features]
default = ["std"]
default = ["std", "bloom_filter"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The feature flag should be called by the name of the feature, not by implementation detail. Maybe something like. "reexecution_filter" or similar?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bloom_input_filter would be a middle ground(?)

@tokatoka
Copy link
Member

tokatoka commented Dec 18, 2024

wait i think you can just do this with IfElseStage
you can put your bloom filter into the closure to IfElseStage and execute the main stage of the fuzzer depending on the evaluated result of the filter

@domenukk
Copy link
Member

wait i think you can just do this with IfElseStage you can put your bloom filter into the closure to IfElseStage and execute the main stage of the fuzzer depending on the evaluated result of the filter

I think we want to do mix and match it for any stage, how would that work?

Then again, we definitely should not do it for Calibration stage...

Probably we need it stage-specific? Or executor specific with the abiltiy to disable it at runtime (?)

@tokatoka
Copy link
Member

I think we want to do mix and match it for any stage, how would that work?

i think it's about how you order the stage tuples

for example if you just want to do filtering for calibration then your stages tuple but not for the others then
it would look like

let main = IfStages::new(bloom_filter, tuple_list!(others))
let stages = tuple_list!(calibration, main)

if you want to disable any stage using the filter, you can just wrap it with IfStages then it'll work

@domenukk
Copy link
Member

domenukk commented Dec 18, 2024

The goal is to filter inputs for every single execution, not just skip a stage for a specific scheduled testcase

@tokatoka
Copy link
Member

tokatoka commented Dec 18, 2024

i don't understand...
what's the difference of skip a stage for a specific scheduled testcase and filter inputs for every single execution,.
if we have the ability to skip all stages on specified conditions, then isn't that equal to filtering inputs for every single execution?

@tokatoka
Copy link
Member

tokatoka commented Dec 18, 2024

Can you wait a bit? @riesentoaster

I think this should go in executor hooks instead of fuzzer.rs
because

  • (in general you shouldn't add logics here, i said above)
  • it's about how executor decides it should execute new inputs or not
  • you edited evaluate_input_events, but this is not the only entrypoint for harness execution. people can just call executor.run_target and in that case your filter is not taking effect.

i'll change executor hooks to early return depending on its result. with that we can do this with executor hook

@riesentoaster
Copy link
Contributor Author

riesentoaster commented Dec 18, 2024

I appreciate your thoughts!

My approach of doing it in the fuzzer as opposed to the executor was chosen to prevent all the logic around the executor (like observers and feedbacks) from running as well if the input is deemed uninteresting.

@riesentoaster
Copy link
Contributor Author

And a meta-thought about your idea of doing it in a stage: that may be the best idea for this in the end if we end up agreeing on it. It has however two additional downsides: discoverability and missconfiguration. I've stumbled across these somewhat regularly while building LibAFL based fuzzers. One needs to understand LibAFL very well to not make mistakes that significantly hamper your performance because you either don't know the functionality even exists or because you make false assumptions based on the limited docs and end up misusing stuff.

This is a fundamentally hard problem for complex libraries, but having a second constructor that would show up in autocomplete suggestions of your IDE seem more discoverable and foolproof to me.

This is a more general observation, and only partly an argument for this specific discussion. And besides a lot more documentation, I'm not sure if we could change the basic architecture or add some patterns or whatever to make this better as well.

Idk.

@riesentoaster riesentoaster changed the title Introduce HashMutator Filter Duplicate Input Execution Dec 18, 2024
@riesentoaster
Copy link
Contributor Author

riesentoaster commented Dec 18, 2024

wait i think you can just do this with IfElseStage
you can put your bloom filter into the closure to IfElseStage and execute the main stage of the fuzzer depending on the evaluated result of the filter

Btw: This will not work, since the stage might mutate the input and execute it multiple times while the filtering is only possible at the start of the stage. So while the input at the beginning of the mutational stage might not have been seen before, mutations might still transform it into a version we've already executed before. Also: The input at the beginning of the stage comes from the corpus, right? So it has been executed before by definition. So implementing this as a wrapper stage seems not possible. Implementing this within stages would require changes to every mutational stage.

I'm in favour of doing it in the fuzzer tbh. Implementations in the executor still require observers/feedbacks to run, implementations in the stage don't really work either.

@tokatoka
Copy link
Member

Btw: This will not work

yes. after talking to domenuk i realized what we want to is to filter against every generated input. so it's impossible to do with stages

@riesentoaster
Copy link
Contributor Author

So do I fix the things @domenukk mentioned in the beginning and we merge this approach? Or how do we continue?

@domenukk
Copy link
Member

The idea is to add the option to return ExitKind::Skipped to Executors and give ExecutorHooks the option to return these.
Then we can use the bloom filter inside executor hooks. We'll probably also want to find a way to make calibration still possible, like, have some way to tell the hook to let this input through.

@riesentoaster
Copy link
Contributor Author

Correct me if I'm wrong, but wouldn't this still run the observers and feedback every time?

@domenukk
Copy link
Member

Not if we change the executors to not execute in this case

@riesentoaster
Copy link
Contributor Author

But those are not run from within the executors but instead in the fuzzer, no?

Or do you want to change this as well?

@tokatoka
Copy link
Member

@riesentoaster
i see what you point as the problem..
putting this in the executor will not cancel the observer run or feedback runs indeed

@tokatoka
Copy link
Member

I think the problems is that we are using several types of APIs to call the harness target.
We can call it from fuzzer.evaluate_input() or we can call it from executor.run_target (See how messed things are in stages/*)

I think adding your change to one of them before unifying the use of them is not good

@tokatoka
Copy link
Member

Another solution is that since your stuff will work mostly with MutationalStage or PowerMutationalStage then we can just add filter to those files only

then domenukk's

We'll probably also want to find a way to make calibration still possible, like, have some way to tell the hook to let this input through.

this problem is solved

@domenukk
Copy link
Member

Also GenStage and TuneableMutationalStage at least. Sounds like a good solution but we need to be careful not to forget things

@riesentoaster
Copy link
Contributor Author

Another solution is that since your stuff will work mostly with MutationalStage or PowerMutationalStage then we can just add filter to those files only

Also GenStage and TuneableMutationalStage at least. Sounds like a good solution but we need to be careful not to forget things

This sounds like a lot of code duplication.

I think the problems is that we are using several types of APIs to call the harness target. We can call it from fuzzer.evaluate_input() or we can call it from executor.run_target (See how messed things are in stages/*)

I like the ability to just call the executor without any observers or anything around it, since it may be helpful to run just the target.

I personally think the functionality in this PR should be implemented wherever the observers and executor are called during the fuzzing loop. If that is in the fuzzer, so be it. If we want it elsewhere, move the logic that calls observers/executors there, too. It's a single function. Anything else is just hacky.

Btw: Why does executor.run_target need a reference to the fuzzer? That seems like a cyclic dependency, since it would otherwise mostly be called through the fuzzer. I think it's exclusively used to run observers in InProcessExecutors, but those have observer tuples all over the place anyways, so I don't think this should be necessary.

@domenukk
Copy link
Member

It's the right amount of code duplication: each stage should probably decide for itself if it needs to filter inputs or not.

@domenukk
Copy link
Member

Unrelated, if you can remove some trait bounds you're more than welcome to open a PR :)

@domenukk
Copy link
Member

We could have a run_with_filter method on the executor trait, maybe that'd reduce the shared code?

@riesentoaster
Copy link
Contributor Author

We could have a run_with_filter method on the executor trait, maybe that'd reduce the shared code?

And have a default implementation that calls run_target? Put the functionality of EvaluatorObservers there and call the function from StdFuzzer? That'd work I think.

@riesentoaster
Copy link
Contributor Author

riesentoaster commented Dec 20, 2024

I've thought about this some more. To me, filtering the evaluation of an input belongs in the Evaluator trait. That's stage-independent and encompasses anything that is part of the evaluation. It is just the right place for this, anything else I've thought of is just jumping through hoops to implement something worse.

  1. If you absolutely do not want to touch StdFuzzer, the best alternative I see is implementing a second Fuzzer that essentially wraps StdFuzzer, forwarding all functions except what is necessary for the filtering. That's just a lot of boilerplate code.
  2. Alternatively, I could try to put as much common logic as possible in a new structure that is used by both StdFuzzer and the new FilteringFuzzer or whatever we end up calling it. This would lead to a rewrite of the internal architecture, but no changes to the logic or outer interface, and the new StdFuzzer would have no filtering logic in it.
  3. Finally, I've again come to like the approach of this PR. It should not introduce any slowdowns for StdFuzzer because the check for the unfiltered option is static and can be optimised away by the compiler. And it introduces as little additional and changed code as possible.

Would you be willing to entertain any of these ideas? My current favourite is 3 > 1 > 2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants