Parking Game Fuzzer
Parking game fuzzer notes (can be used as secondary fuzzing notes)
https://github.com/h4ck0lympus/parking-game-fuzzer
1. The Core Fuzzing Loop
At the highest level, a mutational fuzzer repeats:
choose parent
->mutate parent
->execute candidate
->observe result
->decide whether to preserve it
->repeatConceptual pseudocode:
while no solution:
parent = scheduler.choose(corpus)
candidate = mutator.mutate(parent)
exit_kind = executor.run(candidate)
observations = observers.results()
if objective.is_interesting(observations, exit_kind):
solutions.add(candidate)
if feedback.is_interesting(observations, exit_kind):
corpus.add(candidate)The central LibAFL idea is that each responsibility is represented by a component and trait that can be replaced independently.
2. LibAFL Component Map
| Component | Main question |
|---|---|
Input | What object is being mutated? |
State | What persistent information does the fuzzer own? |
Scheduler | Which corpus testcase becomes the next parent? |
Mutator | How is the parent changed? |
Executor | How is the candidate run? |
Observer | What happened during execution? |
Feedback | Should this candidate enter the corpus? |
Objective | Is this candidate a solution? |
Stage | What operations happen during one fuzzing iteration? |
EventManager | How are statistics and events reported? |
Testcase metadata | What persistent information belongs to one saved input? |
State metadata | What persistent information belongs to the whole campaign? |
Important distinction:
Corpus testcase metadata:
belongs to one saved input
State metadata:
belongs to the entire fuzzing campaignExamples:
ViewMetadata:
legal moves for one saved board state
FinalStateMetadata:
complete snapshot for one saved board state
CrashRateMetadata:
global crash count for the whole session3. The Parking-Game Input Model
The fuzzer input is a small domain-specific program:
pub struct PGInput {
moves: Vec<(NonZeroUsize, Direction)>,
}Each element means:
(car number, one-square direction)Example:
(car 2, Right)
(car 2, Right)
(car 4, Down)This means:
move car 2 right by one square
move car 2 right by one square
move car 4 down by one squareThe important mindset is:
The fuzzer is not mutating arbitrary bytes. It is mutating a sequence of semantic actions.
4. Executor Responsibilities
The executor receives:
input: &PGInputand applies those moves to a parking-game state.
Conceptual behavior:
choose starting game state
->apply moves in order
->invalid move?
yes: return ExitKind::Crash
no: send final board to observers
->return ExitKind::OkA naming hazard:
state: &mut S
LibAFL state
State<T>
parking-game stateMentally rename them:
libafl_state
game_stateLibAFL state contains:
RNG
corpus
solutions
metadata
current testcase
execution countParking-game state contains:
board
cars
positions5. Observers Are Temporary Per-Execution Data
FinalStateObserver
Stores the final board state after a successful execution.
Used for:
- hashing the resulting board,
- detecting a new board state,
- saving snapshots later.
ViewObserver
For every car, records:
- backward direction,
- forward direction,
- distance before collision,
- nearest obstacle.
Example:
car 3:
backward = Left, distance 2, obstacle wall
forward = Right, distance 1, obstacle car 5Observer lifecycle:
pre_exec()
clears old data
executor runs
final_board()
fills observations for this executionThis means:
Observer data is temporary and does not automatically remain associated with a corpus input.
6. Nested Option and Board Lookup
A key Rust/data-model lesson came from:
board.get(position)Its effective type was:
Option<Option<NonZeroUsize>>Interpret the layers separately:
None
coordinate is outside the board
Some(None)
coordinate exists, but the square is empty
Some(Some(car))
coordinate exists and contains a carGeneral rule:
Before using
unwrap()onOption,Result, or nested enums, enumerate every possible variant.
If a case is expected control flow, it should not be handled with unwrap().
7. step_until_seen and Off-by-One Reasoning
The goal is to find:
- the nearest obstacle,
- the number of legal one-square moves before collision.
For each direction:
inspect next relevant square
->outside board: wall
->empty: increment distance and continue
->occupied: return obstacle and distanceDefinition:
An immediately adjacent obstacle has distance zero.
Example:
oo2The objective car cannot move right:
distance = 0Example:
oo.2It can move once:
distance = 1A bug appeared when both offset and distance were added to the coordinate while both were incremented. That skipped cells.
A veteran debugging method for coordinate loops:
- Write down the first three iterations.
- Track each variable in a table.
- Verify no cell is skipped or revisited unexpectedly.
- Test the adjacent-obstacle case.
- Test the wall case.
- Test one and multiple empty cells.
8. Feedback Has Two Separate Jobs
A LibAFL Feedback may participate in two phases.
Decision phase
is_interesting(...)Question:
Should this candidate enter the corpus?Metadata phase
append_metadata(...)Question:
Now that LibAFL has decided to preserve this candidate,
what information should be attached to the real testcase?Critical lesson:
Do not create a temporary local
Testcaseinsideis_interestingand attach metadata to it. That temporary object is dropped and is not the testcase LibAFL stores.
9. Producer–Consumer Metadata Pattern
Part 2: Legal-move metadata
ViewObserver
produces temporary legal-move information
ViewFeedback
copies it into ViewMetadata on the saved testcase
PGTailMutator
consumes ViewMetadata from the selected parent testcasePart 3: Snapshot metadata
FinalStateObserver
produces temporary final State<T>
FinalStateFeedback
copies it into FinalStateMetadata on the saved testcase
PGExecutor
consumes FinalStateMetadata to restore a parent snapshotGeneral rule:
Whenever a component reads metadata, find the component responsible for writing it.
Useful repository searches:
rg "add_metadata"
rg "metadata::<"
rg "metadata_mut::<"
rg "append_metadata"10. Corpus Metadata vs State Metadata
Testcase metadata
Stored on one corpus entry:
Testcase
├── input: PGInput
└── metadata
├── ViewMetadata
└── FinalStateMetadataState metadata
Stored globally:
StdState
└── metadata
└── CrashRateMetadataUse testcase metadata when information is specific to one saved input.
Use state metadata when information belongs to the whole fuzzing session.
11. CrashRateFeedback
Its responsibility:
count executions classified as Crash
report crashes / executionsIt does not decide corpus admission.
Correct conceptual behavior:
if ExitKind::Crash:
increment num_crashes
return falseWhy false?
CrashRateFeedback is a statistics collector,
not guidance.If it returned true:
invalid parking move
->considered interesting
->inserted into corpusThat is undesirable.
Metadata initialization
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct CrashRateMetadata {
pub num_crashes: u64,
}Its initializer inserts:
CrashRateMetadata { num_crashes: 0 }into LibAFL state metadata.
Reporting
crashes = CrashRateMetadata.num_crashes
executions = state execution counter
value = UserStatsValue::Ratio(crashes, executions)Prefer ? over unwrap() when the enclosing method already returns Result.
12. Feedback Composition: Boolean Logic Plus Side Effects
Corpus guidance
The actual corpus-preservation rule:
not crashed AND new final stateWhy short-circuit AND?
if execution crashed:
do not inspect final-state observerThe observer may not contain a completed state after a crash.
Objective
A solution should satisfy:
not crashed AND solvedMetadata/statistics feedbacks
These return false but perform useful work:
CrashRateFeedback
ViewFeedback
FinalStateFeedbackThe complete logic is:
false OR false OR false OR valid_new_state
=
valid_new_stateImportant:
In feedback composition, reason about both truth values and side effects.
Do not put a metadata-only feedback first in AND_FAST, because it returns false and may prevent the rest from executing.
13. Eager vs Short-Circuit Composition
Short-circuit AND
A && BIf A is false, skip B.
Use when:
- later feedback depends on successful execution,
- observer data may be absent on failure,
- skipping work is correct.
Example:
not crashed AND_FAST new-stateEager OR
Evaluate all components.
Use when:
- statistics must be updated,
- metadata must be attached,
- components have required side effects.
Example:
CrashRateFeedback
OR ViewFeedback
OR FinalStateFeedback
OR valid_new_stateGeneral lesson:
Framework combinators can alter which callbacks run, not merely the final Boolean result.
14. PGRandMutator
Behavior:
choose random car
choose random direction
choose random insertion index
insert moveCorrect insertion-index domain
For a vector of length n, valid insertion indices are:
0 through n inclusiveThere are n + 1 positions.
[A, B, C]
0: before A
1: between A and B
2: between B and C
3: after CUsing the number of cars as the insertion bound was a semantic domain mismatch.
Weaknesses
- Only inserts moves.
- Cannot delete bad moves.
- Cannot replace bad moves.
- Input length grows monotonically.
- Ignores car orientation.
- Ignores obstacles.
- Frequently generates invalid moves.
- Inserting in the middle can invalidate a previously valid suffix.
- Cannot directly repair an earlier bad action.
15. PGTailMutator
The tail mutator uses semantic metadata to append only valid moves.
High-level algorithm
borrow selected parent testcase
->read ViewMetadata
->enumerate all legal high-level choices
->release testcase borrow
->randomly choose one
->append the same one-square move distance timesA high-level choice:
(car, direction, distance)Example:
(car 2, Right, 3)becomes:
(car 2, Right)
(car 2, Right)
(car 2, Right)Enumerating all choices
If a car can move left by three:
(car, Left, 1)
(car, Left, 2)
(car, Left, 3)Enumerate both:
backward view
forward viewfor every car.
Choose one high-level movement
Do not choose several unrelated choices from the same old metadata.
Why?
choice 1 changes the board
choice 2 was validated against the old board
choice 2 may no longer be validInstead:
choose one legal movement
apply it for the selected distanceEmpty choices
If every car is blocked:
choices.is_empty()
->MutationResult::Skipped16. Borrowing Pattern in PGTailMutator
The testcase is borrowed from state.
While that borrow is alive, Rust will reject:
state.rand_mut()because this requires mutable access to the same state.
Correct lifecycle:
borrow testcase
->borrow ViewMetadata
->copy required information into owned Vec
->end testcase borrow
->access mutable RNGA scope is cleaner than manual drop:
let choices = {
let testcase = state.current_testcase()?;
let metadata = testcase.metadata::<ViewMetadata<T>>()?;
let mut choices = Vec::new();
// build owned choices
choices
};
// borrow ended here
// state.rand_mut() is legal
General Rust rule:
Convert borrowed framework data into owned local data before releasing the borrow.
17. Snapshot Fuzzing
Snapshot fuzzing avoids replaying the unchanged parent prefix.
Suppose:
parent:
[A, B, C]
candidate:
[A, B, C, D, D]Without snapshot
initial board
->A
->B
->C
->D
->DWith snapshot
restore board after A, B, C
->D
->DRequired invariant
The parent input must be a prefix of the candidate:
candidate.starts_with(parent)Otherwise:
snapshot does not correspond to the candidate prefix
->illegal stateExecutor inputs
The executor uses:
current testcase:
parent input
FinalStateMetadata snapshot
run_target input:
mutated candidateIts snapshot-selection block conceptually returns:
(starting game state, moves still needing execution)Fallback path:
(initial state, all candidate moves)Snapshot path:
(parent snapshot, candidate suffix)Critical bug
After computing:
let (game_state, moves) = ...the move loop must iterate over:
movesnot:
input.moves()Otherwise the prefix is replayed on top of a state that already includes it.
18. PGInput::is_prefix_of
A useful helper expresses the domain invariant:
parent.is_prefix_of(candidate)Conceptually:
candidate.moves.starts_with(parent.moves)Then:
prefix_len = parent.moves.len
suffix = candidate.moves[prefix_len..]This is an example of improving code readability by turning a low-level slice comparison into a named domain operation.
19. Artificial Delay and Snapshot Measurement
The exercise adds:
sleep one microsecond after each executed moveThe delay belongs inside the move loop.
Without snapshots:
100 prefix moves + 2 tail moves
= 102 delaysWith snapshots:
restore snapshot + 2 tail moves
= 2 delaysGeneral benchmarking principle:
Place artificial cost where the real repeated cost would occur.
20. Rust Concepts Encountered
20.1 Option<T> vs &Option<T>
Given:
&Option<PGInput>calling .unwrap() attempts to consume the Option.
Use:
.as_ref()to transform:
&Option<T>
->Option<&T>Then:
Option<&T>
->&Twithout moving the owned value.
20.2 unwrap() consumes its receiver
Approximate signature:
unwrap(self) -> TWhen the receiver is borrowed, ask whether ownership can legally move.
20.3 Copy vs Clone
Copy is appropriate for small scalar values:
u8
u16
u64
many small enumsClone is used for owned structures:
PGInput
State<T>
Vec<T>Do not clone when borrowing is enough.
20.4 Turbofish
metadata::<ViewMetadata<T>>()The generic type tells Rust what metadata type to retrieve.
Correct:
state.method::<Type>()Incorrect:
state::<Type>20.5 Generic trait bounds
If generic S must support:
metadata access
execution count
RNG access
current testcase accessthe needed traits may include:
HasMetadata
HasExecutions
HasRand
HasCurrentTestcaseReusable method:
Need to call method X on generic T
->find the trait defining X
->add T: ThatTrait20.6 Tuple destructuring
If an iterator item is:
(NonZeroUsize, &ViewFrom<T>)write:
for (car, view_from) in metadata.views()A tuple itself does not have backward() or forward() methods.
20.7 Vec methods
| Goal | Method |
|---|---|
| Add one element | push(value) |
| Insert at index | insert(index, value) |
| Move all items from another vector | append(&mut other) |
| Add items from an iterator | extend(iter) |
20.8 () is unit, not an empty collection
let choices = ();creates unit.
Use:
Vec::new()for a growable list.
20.9 Ignored expression results
This:
match x {
... => Ok(true)
}
Ok(false)discards the result of the match and returns Ok(false).
Always ask:
Who consumes the value produced by this expression?
20.10 Closures for local control flow
The executor uses a closure so return can exit only the snapshot-selection block while returning:
(game_state, moves)The outer ? propagates any error from the closure.
21. How to Read Rust Compiler Errors
Step 1: Read only the first error
Later errors may be cascading consequences.
Step 2: Extract expected and found types
Example:
expected Testcase<PGInput>
found &_Ask:
What type did I explicitly demand?
What type did the method actually return?Step 3: Inspect method signatures
Use:
- IDE hover,
- Go to definition,
- local crate source,
cargo doc --open.
Step 4: Build a type ladder
Example:
parent.input()
&Option<PGInput>
.as_ref()
Option<&PGInput>
.unwrap()
&PGInputStep 5: Force a diagnostic when needed
let x: () = some_expression;The compiler will report the real inferred type.
Step 6: Treat clone suggestions cautiously
The compiler may suggest cloning because it resolves ownership.
Ask:
Do I truly need ownership?
Would borrowing be sufficient?22. Repository Navigation Workflow
Experienced engineers do not understand an entire large repository at once.
They isolate the smallest connected code path needed for the current question.
Start with a concrete question
Too broad:
How does this repository work?Useful:
Where does `moves` come from?
Who writes ViewMetadata?
Who calls append_metadata?
Which trait provides executions()?Trace backward
Where did this value originate?Example:
moves
← input.moves()
← input: &PGInput
← executor argument
← mutator/test constructed candidateTrace forward
Where does it go?Example:
moves
->executor loop
->board.shift_car
->observers
->feedback/objectiveUseful searches
rg "struct PGInput"
rg "impl PGInput"
rg "fn moves"
rg "PGInput::new"
rg "add_metadata"
rg "metadata::<"
rg "append_metadata"
rg "trait HasExecutions"
rg "\.executions()"Read tests early
Tests reveal:
- construction patterns,
- expected values,
- edge cases,
- intended wiring,
- lifecycle ordering.
Build a repository map
input.rs
input representation
executor.rs
execution semantics
observers.rs
temporary execution data
feedbacks.rs
guidance, objective support, metadata production
mutators.rs
candidate generation
stages.rs
fuzzing iteration behavior
main.rs
component assembly23. Separate Domain Logic from Framework Plumbing
Classify each line.
Example:
input.moves()
domain input access
board.shift_car(...)
parking-game operation
state.metadata::<...>()
LibAFL state plumbing
self.observers.final_board_all(...)
observer framework integration
Ok(ExitKind::Crash)
LibAFL execution classificationThis prevents simultaneous confusion across:
Rust language
LibAFL framework
parking-game rules24. Form and Verify Hypotheses
A strong code-reading cycle:
Example:
Hypothesis:
final_board_all calls final_board on every observer.
Verification:
search final_board_all
inspect tuple recursion
run observer testYou do not need complete certainty before proceeding. You need a testable model.
25. Common Mistakes and General Lessons
Using number of cars as insertion bound
Lesson:
same primitive type does not imply same semantic domainPrefer names such as:
num_cars
num_moves
num_insertion_positionsCounting ExitKind::Ok as a crash
Lesson:
write a behavior table before the matchReturning true from CrashRateFeedback
Lesson:
an event occurred
≠
this feedback should preserve the inputAttaching metadata to a temporary testcase
Lesson:
find the framework-owned object that survivesExpecting observer data to persist
Lesson:
observers are temporary
testcase metadata is persistentRestoring a snapshot but replaying all moves
Lesson:
verify downstream code actually uses the newly computed variableUsing short-circuit AND with metadata-only feedback
Lesson:
truth value and required side effects both matter–
26. Design Checklist for a New Fuzzer
Input
What does one candidate represent?
Raw bytes, tokens, AST, commands, state-machine actions?Mutation
What invariants must remain valid?
Insert, delete, replace, splice?
Can semantic state improve mutation?Execution
How is the target run?
Fresh process, persistent process, in-process call, snapshot restore?
What constitutes Crash, Timeout, OOM, or Ok?Observation
Coverage?
Final semantic state?
Protocol state?
Score?
Resource usage?Feedback
What makes an input worth preserving?
New coverage?
New state?
Improved score?Objective
What counts as a solution?
Crash?
Solved puzzle?
Invariant violation?
Target state?Metadata
What belongs globally?
What belongs to one testcase?
Who writes it?
Who reads it?Scheduler
FIFO?
Coverage-based?
Depth-based?
Score-based?
Shortest-input first?Stages
One mutation?
Repeated mutation?
Deterministic enumeration?
Calibration?
Trimming?–
27. Final Parking-Game Fuzzer Mental Model
–
Useful commands:
cargo check
cargo test <test-name> -- --nocapture
cargo doc --open
cargo tree
rg "symbol_name" .
rg "trait TraitName" ~/.cargo/registry/src
rg "method_name::<" ~/.cargo/registry/src–
28. Final Principles
- Types are executable documentation.
- Metadata needs an explicit producer and consumer.
- Observer data is temporary unless copied into testcase metadata.
- Feedback truth values and side effects are distinct.
- Short-circuit composition can skip required callbacks.
- Semantic mutators are often dramatically better than blind random mutation.
- Snapshot execution is correct only when the parent is a verified prefix.
- Borrowed framework data often needs to be converted into owned local data before mutating state.
- Read tests and call sites, not only definitions.
- Experienced engineers do not know everything in advance. They reduce uncertainty systematically.