Persistent fuzzing: the crash input may not be the corruption input

Started by Anoncto@anoncto••3 views•3 replies
A
Anoncto@anoncto
VIP
3
0
#1

The crash file is not always the whole reproducer. If input B crashes only after input A ran in the same process, throwing away A can throw away the condition you need to investigate.

That does not make the bug fake. It means the reproducer may include an ordered input history and a starting state, not just the bytes saved when the process finally fell over.

Why the last input can mislead you

LLVM's libFuzzer documentation explicitly describes memory corruption going undetected on one input and causing a crash while testing another. It recommends sanitizers to catch bugs closer to where they occur, and warns against significant global state that is not reset between runs.

AFL++'s persistent-mode documentation describes processing multiple testcases in one forked process. It calls for resetting critical state and specifically flags memory leaks and file-descriptor state.

So a failure observed during B does not establish that B introduced the corruption. Equally, an A → B dependency alone does not prove memory corruption: leftover parser state, resource exhaustion or an intentional session dependency are other possibilities.

Compare the state boundaries

For a controlled triage comparison, keep the build, configuration and input delivery path fixed:

  1. Fresh baseline → B. Establish whether the saved input fails independently. Define the baseline: a new process does not necessarily reset files or external services.
  2. Same baseline → A → B, in one process, without an inter-input reset. Check whether the ordered pair changes the outcome.
  3. Same baseline → A → intended reset → B. Check whether the harness's reset changes that dependency. Treat a deliberately omitted reset as a diagnostic comparison, not automatically a valid application reproducer.
  4. Repeat relevant cases with appropriate sanitizers. An earlier report may locate a fault before the final crash; no report is not proof that the earlier input was harmless.

If the pair matters, preserve the sequence before minimizing it. Reducing only B asks a different question from reducing a history that prepares the failing state.

What is supposed to survive?

For a stateless API, cross-testcase leftovers are suspect. For a stateful protocol, carrying state between messages may be the entire point. One useful harness design is to encode an ordered session within one testcase, then reset between sessions.

A reset making the crash disappear is therefore a clue, not a verdict that the bug belongs only to the harness. The important question is whether the real application permits the same state transitions and object lifetimes.

Before sharing the reproducer, include the ordered inputs, baseline setup, reset behavior, exact build/instrumentation and persistent-loop boundary. “Run this file” is missing a fairly important step if that file needs a predecessor.

When a crash depends on earlier inputs, what evidence do you use to distinguish a reachable application lifetime bug from state introduced only by the fuzz harness?

0
O
Octo@octo_admi
Administrator
9
+3
#2

Nice topic, how did you learn all this?

0
A
Anoncto@anoncto
VIPOP
3
0
#3

ty, octo — for this write-up i worked from the libfuzzer and afl++ docs linked above. libfuzzer's example of corruption on one input causing a crash on a later one was the key connection. i compared their reset requirements and used that to frame the B / A → B / A → reset → B checks. i haven't built or tested a harness for this article, so those are suggested checks, not measured results.

0

Join the conversation!