Conversation
maflcko
left a comment
There was a problem hiding this comment.
nice. left a comment, but feel free to ignore.
| # Assertions | ||
|
|
||
| Fuzzamoto implements a feedback-guided assertion system inspired by | ||
| [Antithesis's sometimes | ||
| assertions](https://antithesis.com/docs/best_practices/sometimes_assertions/), | ||
| designed to both validate program correctness and guide fuzzing toward | ||
| interesting execution states. | ||
|
|
||
| The assertion system is only available when fuzzing with | ||
| [`fuzzamoto-libafl`](../usage/libafl.md). |
There was a problem hiding this comment.
The docs you link to are a bit vague around "sometimes assertions" being able to fail. Also, in the current implementation they are not able to fail, even if the fuzzing completes?
I am thinking, it could make sense to have both:
- A macro to guide the fuzz engine to mark a condition as interesting and provide the fuzz engine with some kind of distance to reach it. Maybe this could be called
reachability_goal(...)(orcoverage_goal(...)) - A macro to ensure different interesting "leaf" coverage is hit when iterating over a pre-generated, static set of (minimized) fuzz inputs in one folder. Maybe this could be called
reachability_assert(...)(orcoverage_assert(...)).
Such "reachability asserts" could help to more reliably trigger major coverage drops (google/oss-fuzz#11398). Not sure if this is in-scope for fuzzamoto, but I wanted to mention it, because it seems interesting in the greater fuzzing scope.
There was a problem hiding this comment.
The docs you link to are a bit vague around "sometimes assertions" being able to fail.
The way I understand it, failure for these assertions is defined as "over all tests we ran, the condition was never true", so in the context here, the following assertion would fail if none of the generated testcases ever managed to submit a valid transaction to the mempool.
assert_sometimes!(cond: mempool_size > 0, "Mempool is not empty");This means that a failed assertion might flip to being satisfied over time as the fuzzing potentially discovers the right input. And in the other direction, a satisfied assertion might flip into failure if for some reason the coverage/state can no longer be reached.
Also, in the current implementation they are not able to fail, even if the fuzzing completes?
Yea in my implementation the sometimes assertions don't fail in a normal sense, but their status for a specific campaign can be inspected by looking at the assertion.txt file that is generated, e.g.:
✗ Sometimes gt(127, 1000): Mempool may contain more than 1000 txs
✓ Always cond(true): One active tip must exist
✓ Always lt(960096, 5000000): Mempool usage does not exceed the maximum
✓ Always lte(525000000000, 2100000000000000): Coin supply is within expected limits
✓ Sometimes cond(true): Block tip may change
✓ Sometimes cond(true): Mempool is not empty
✓ Sometimes cond(true): Node under test should send addr messages
I think I'll export the assertion "results" in a machine readable way, such that surrounding infrastructure can keep track and report issues based on them (e.g. coverage drops). Maybe also add some stats about the assertions to the stdout fuzzer output.
Such "reachability asserts" could help to more reliably trigger
I'll think about your suggested macros but I feel like the macros I have now could already satisfy those needs?
There was a problem hiding this comment.
I think I'll export the assertion "results" in a machine readable way, such that surrounding infrastructure can keep track and report issues based on them (e.g. coverage drops). Maybe also add some stats about the assertions to the stdout fuzzer output.
Such "reachability asserts" could help to more reliably trigger
I'll think about your suggested macros but I feel like the macros I have now could already satisfy those needs?
Yes, if your macro is designed so that surrounding infra decides how to treat a coverage miss, then both needs (explicitly assert on coverage, or just set a silent coverage goal) are satisfied. The only difference is that one approach puts the treatment of coverage misses in the source code directly, the other approach puts the treatment in the surrounding infra.
I guess a third option could also be to keep a single macro and set an env var to denote how the macro should behave: COVERAGE_ASSERT=0/1.
But all of this can trivially be implemented later and any approach looks good to me.
Crypt-iQ
left a comment
There was a problem hiding this comment.
Reviewed the code and it's clear and makes sense to me, will run. Left some clarifying questions.
| #[serde(rename_all = "snake_case")] | ||
| pub enum Assertion { | ||
| Condition(bool), | ||
| LessThan(u64, u64), |
There was a problem hiding this comment.
would there ever be a use for supporting signed types?
| ("Sometimes", detail, msg) | ||
| } | ||
| AssertionScope::Always(inner, msg) => { | ||
| fires = !fires; |
There was a problem hiding this comment.
wondering why this gets inverted? Is it because it was already inverted when assertion.evaluate calls distance?
| let previous = self.assertions.get(&new.message()); | ||
|
|
||
| let result = match (previous, &new) { | ||
| (None, new) => new.evaluate() || !self.only_always_assertions, |
There was a problem hiding this comment.
why is !self.only_always_assertions needed? Is it so that a Sometimes entry can be put into self.assertions and then later evaluate_assertion calls get closer in distance?
fuzzamoto-libafl/src/stages/mod.rs
Outdated
| .map(|m| m.list.clone()) | ||
| .unwrap_or(vec![]); | ||
|
|
||
| if !self.minimizing_crash && novelties.is_empty() { |
There was a problem hiding this comment.
does this skip minimization if there were no novelties?
There was a problem hiding this comment.
Yes because an input might be interesting due to the assertion feedback but not from a coverage perspective. I need to extend the minimize to take the assertion feedback into account (i.e. minimize with the goal of retaining new coverage as well as the novel assertion states)
| @@ -1,3 +1,4 @@ | |||
| #include <assert.h> | |||
There was a problem hiding this comment.
curious why this is needed?
fuzzamoto/src/assertions.rs
Outdated
| pub fn log_assertion(assertion: &AssertionScope) { | ||
| if let Ok(json) = serde_json::to_string(assertion) { | ||
| unsafe { | ||
| nyx_println(json.as_ptr() as *const i8, json.len()); |
There was a problem hiding this comment.
clarifying question: this lets the StdOutObserver pick this up (since under the hood it is using NyxHelper and nyx_stdout) and then is used in AssertionFeedback is_interesting?
|
|
||
| Compared to traditional aggregate source code coverage reports, sometimes | ||
| assertions provide additional insights into the explored state space. For | ||
| example, a coverage report would tell us that a chain reorganization occured |
| Example usage: | ||
|
|
||
| ```rust | ||
| assert_sometimes!(cond: mempool_size > 0, "Mempool is not empty"); |
There was a problem hiding this comment.
q: where is the macro used? in the scenario?
There was a problem hiding this comment.
It can be used in any code that runs inside the vm (scenario, oracles, targets, ...)
59416e4 to
5ed436f
Compare
|
Just rebased, haven't addressed review feedback yet |
5ed436f to
8a91e66
Compare
8a91e66 to
7503930
Compare
No description provided.