Skip to content

Commit c5cad56

Browse files
authored
feat: add configurable snapshot restore cadence for semi/full persistent fuzzing (#285)
1 parent d089411 commit c5cad56

File tree

6 files changed

+225
-159
lines changed

6 files changed

+225
-159
lines changed

docs/src/config/common-options.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ is desired.
1414
- [Enable and Set the Checkpoint Path](#enable-and-set-the-checkpoint-path)
1515
- [Enable Random Corpus Generation](#enable-random-corpus-generation)
1616
- [Set an Iteration Limit](#set-an-iteration-limit)
17+
- [Set Snapshot Restore Interval](#set-snapshot-restore-interval)
1718
- [Adding Tokens From Target Software](#adding-tokens-from-target-software)
1819
- [Setting an Architecture Hint](#setting-an-architecture-hint)
1920
- [Adding a Trace Processor](#adding-a-trace-processor)
@@ -204,6 +205,25 @@ This is useful for CI fuzzing or for testing. The limit can be set with:
204205
@tsffs.iteration_limit = 1000
205206
```
206207

208+
### Set Snapshot Restore Interval
209+
210+
By default, TSFFS restores the initial snapshot at every iteration boundary.
211+
This can be changed to support semi-persistent or fully persistent execution:
212+
213+
```python
214+
# Default behavior: restore every iteration
215+
@tsffs.snapshot_restore_interval = 1
216+
217+
# Semi-persistent: restore every 100 iterations
218+
@tsffs.snapshot_restore_interval = 100
219+
220+
# Fully persistent: never restore after startup
221+
@tsffs.snapshot_restore_interval = 0
222+
```
223+
224+
Values greater than 1 restore every N iterations, where N is the configured value.
225+
This option only accepts integer values (`0`, `1`, or `N > 1`).
226+
207227
### Adding Tokens From Target Software
208228

209229
The fuzzer has a mutator which will insert, remove, and mutate tokens in testcases. This

src/haps/mod.rs

Lines changed: 110 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@ use simics::{
2222
debug, get_processor_number, info, trace, warn,
2323
};
2424

25+
enum IterationControl {
26+
Continue,
27+
StopRequested,
28+
}
29+
30+
enum IterationCount {
31+
NoCount,
32+
Timeout,
33+
Solution,
34+
}
35+
2536
impl Tsffs {
2637
fn on_simulation_stopped_magic_start(&mut self, magic_number: MagicNumber) -> Result<()> {
2738
if !self.have_initial_snapshot() {
@@ -91,6 +102,80 @@ impl Tsffs {
91102
self.on_simulation_stopped_solution(SolutionKind::Manual)
92103
}
93104

105+
fn finish_iteration(
106+
&mut self,
107+
exit_kind: ExitKind,
108+
iteration_count: IterationCount,
109+
missing_start_info_message: &str,
110+
) -> Result<IterationControl> {
111+
// 1) Count this iteration as complete.
112+
self.iterations += 1;
113+
114+
// 2) Enforce iteration cap before scheduling/resuming work for next iteration.
115+
if self.iteration_limit != 0 && self.iterations >= self.iteration_limit {
116+
let duration = SystemTime::now().duration_since(
117+
*self
118+
.start_time
119+
.get()
120+
.ok_or_else(|| anyhow!("Start time was not set"))?,
121+
)?;
122+
123+
// Set the log level so this message always prints
124+
set_log_level(self.as_conf_object_mut(), LogLevel::Info)?;
125+
126+
info!(
127+
self.as_conf_object(),
128+
"Configured iteration count {} reached. Stopping after {} seconds ({} exec/s).",
129+
self.iterations,
130+
duration.as_secs_f32(),
131+
self.iterations as f32 / duration.as_secs_f32()
132+
);
133+
134+
self.send_shutdown()?;
135+
136+
if self.quit_on_iteration_limit {
137+
quit(0)?;
138+
} else {
139+
return Ok(IterationControl::StopRequested);
140+
}
141+
}
142+
143+
// 3) Update outcome counters where this stop reason contributes to stats.
144+
match iteration_count {
145+
IterationCount::NoCount => {}
146+
IterationCount::Timeout => self.timeouts += 1,
147+
IterationCount::Solution => self.solutions += 1,
148+
}
149+
150+
let fuzzer_tx = self
151+
.fuzzer_tx
152+
.get()
153+
.ok_or_else(|| anyhow!("No fuzzer tx channel"))?;
154+
155+
// 4) Publish this iteration result back to the fuzzer loop.
156+
fuzzer_tx.send(exit_kind)?;
157+
158+
// 5) Restore to initial snapshot according to the configured restore interval.
159+
if self.should_restore_snapshot_this_iteration() {
160+
self.restore_initial_snapshot()?;
161+
}
162+
163+
// 6) Reset AFL edge chaining state for the next execution.
164+
self.coverage_prev_loc = 0;
165+
166+
// 7) Persist testcase bytes when start metadata is available.
167+
if self.start_info.get().is_some() {
168+
self.get_and_write_testcase()?;
169+
} else {
170+
debug!(self.as_conf_object(), "{missing_start_info_message}");
171+
}
172+
173+
// 8) Arm timeout for the next iteration run.
174+
self.post_timeout_event()?;
175+
176+
Ok(IterationControl::Continue)
177+
}
178+
94179
fn on_simulation_stopped_magic_stop(&mut self) -> Result<()> {
95180
if !self.have_initial_snapshot() {
96181
warn!(
@@ -117,56 +202,14 @@ impl Tsffs {
117202
return Ok(());
118203
}
119204

120-
self.iterations += 1;
121-
122-
if self.iteration_limit != 0 && self.iterations >= self.iteration_limit {
123-
let duration = SystemTime::now().duration_since(
124-
*self
125-
.start_time
126-
.get()
127-
.ok_or_else(|| anyhow!("Start time was not set"))?,
128-
)?;
129-
130-
// Set the log level so this message always prints
131-
set_log_level(self.as_conf_object_mut(), LogLevel::Info)?;
132-
133-
info!(
134-
self.as_conf_object(),
135-
"Configured iteration count {} reached. Stopping after {} seconds ({} exec/s).",
136-
self.iterations,
137-
duration.as_secs_f32(),
138-
self.iterations as f32 / duration.as_secs_f32()
139-
);
140-
141-
self.send_shutdown()?;
142-
143-
if self.quit_on_iteration_limit {
144-
quit(0)?;
145-
} else {
146-
return Ok(());
147-
}
148-
}
149-
150-
let fuzzer_tx = self
151-
.fuzzer_tx
152-
.get()
153-
.ok_or_else(|| anyhow!("No fuzzer tx channel"))?;
154-
155-
fuzzer_tx.send(ExitKind::Ok)?;
156-
157-
self.restore_initial_snapshot()?;
158-
self.coverage_prev_loc = 0;
159-
160-
if self.start_info.get().is_some() {
161-
self.get_and_write_testcase()?;
162-
} else {
163-
debug!(
164-
self.as_conf_object(),
165-
"Missing start buffer or size, not writing testcase."
166-
);
205+
// Normal stop path: report successful completion without solution/timeout counters.
206+
if let IterationControl::StopRequested = self.finish_iteration(
207+
ExitKind::Ok,
208+
IterationCount::NoCount,
209+
"Missing start buffer or size, not writing testcase.",
210+
)? {
211+
return Ok(());
167212
}
168-
169-
self.post_timeout_event()?;
170213
}
171214

172215
if self.save_all_execution_traces {
@@ -328,56 +371,14 @@ impl Tsffs {
328371
return Ok(());
329372
}
330373

331-
self.iterations += 1;
332-
333-
if self.iteration_limit != 0 && self.iterations >= self.iteration_limit {
334-
let duration = SystemTime::now().duration_since(
335-
*self
336-
.start_time
337-
.get()
338-
.ok_or_else(|| anyhow!("Start time was not set"))?,
339-
)?;
340-
341-
// Set the log level so this message always prints
342-
set_log_level(self.as_conf_object_mut(), LogLevel::Info)?;
343-
344-
info!(
345-
self.as_conf_object(),
346-
"Configured iteration count {} reached. Stopping after {} seconds ({} exec/s).",
347-
self.iterations,
348-
duration.as_secs_f32(),
349-
self.iterations as f32 / duration.as_secs_f32()
350-
);
351-
352-
self.send_shutdown()?;
353-
354-
if self.quit_on_iteration_limit {
355-
quit(0)?;
356-
} else {
357-
return Ok(());
358-
}
359-
}
360-
361-
let fuzzer_tx = self
362-
.fuzzer_tx
363-
.get()
364-
.ok_or_else(|| anyhow!("No fuzzer tx channel"))?;
365-
366-
fuzzer_tx.send(ExitKind::Ok)?;
367-
368-
self.restore_initial_snapshot()?;
369-
self.coverage_prev_loc = 0;
370-
371-
if self.start_info.get().is_some() {
372-
self.get_and_write_testcase()?;
373-
} else {
374-
debug!(
375-
self.as_conf_object(),
376-
"Missing start buffer or size, not writing testcase. This may be due to using manual no-buffer harnessing."
377-
);
374+
// Manual stop behaves like normal completion for accounting purposes.
375+
if let IterationControl::StopRequested = self.finish_iteration(
376+
ExitKind::Ok,
377+
IterationCount::NoCount,
378+
"Missing start buffer or size, not writing testcase. This may be due to using manual no-buffer harnessing.",
379+
)? {
380+
return Ok(());
378381
}
379-
380-
self.post_timeout_event()?;
381382
}
382383

383384
if self.save_all_execution_traces {
@@ -424,65 +425,21 @@ impl Tsffs {
424425
return Ok(());
425426
}
426427

427-
self.iterations += 1;
428-
429-
if self.iteration_limit != 0 && self.iterations >= self.iteration_limit {
430-
let duration = SystemTime::now().duration_since(
431-
*self
432-
.start_time
433-
.get()
434-
.ok_or_else(|| anyhow!("Start time was not set"))?,
435-
)?;
436-
437-
// Set the log level so this message always prints
438-
set_log_level(self.as_conf_object_mut(), LogLevel::Info)?;
439-
440-
info!(
441-
self.as_conf_object(),
442-
"Configured iteration count {} reached. Stopping after {} seconds ({} exec/s).",
443-
self.iterations,
444-
duration.as_secs_f32(),
445-
self.iterations as f32 / duration.as_secs_f32()
446-
);
447-
448-
self.send_shutdown()?;
449-
450-
if self.quit_on_iteration_limit {
451-
quit(0)?;
452-
} else {
453-
return Ok(());
454-
}
455-
}
456-
457-
let fuzzer_tx = self
458-
.fuzzer_tx
459-
.get()
460-
.ok_or_else(|| anyhow!("No fuzzer tx channel"))?;
461-
462-
match kind {
463-
SolutionKind::Timeout => {
464-
self.timeouts += 1;
465-
fuzzer_tx.send(ExitKind::Timeout)?
466-
}
428+
let (exit_kind, iteration_count) = match kind {
429+
SolutionKind::Timeout => (ExitKind::Timeout, IterationCount::Timeout),
467430
SolutionKind::Exception | SolutionKind::Breakpoint | SolutionKind::Manual => {
468-
self.solutions += 1;
469-
fuzzer_tx.send(ExitKind::Crash)?
431+
(ExitKind::Crash, IterationCount::Solution)
470432
}
471-
}
472-
473-
self.restore_initial_snapshot()?;
474-
self.coverage_prev_loc = 0;
433+
};
475434

476-
if self.start_info.get().is_some() {
477-
self.get_and_write_testcase()?;
478-
} else {
479-
debug!(
480-
self.as_conf_object(),
481-
"Missing start buffer or size, not writing testcase."
482-
);
435+
// Solution/timeout path: classify exit kind and increment corresponding counters.
436+
if let IterationControl::StopRequested = self.finish_iteration(
437+
exit_kind,
438+
iteration_count,
439+
"Missing start buffer or size, not writing testcase.",
440+
)? {
441+
return Ok(());
483442
}
484-
485-
self.post_timeout_event()?;
486443
}
487444

488445
if self.save_all_execution_traces {

src/interfaces/fuzz.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,9 +222,10 @@ impl Tsffs {
222222

223223
/// Interface method to manually signal to stop a testcase execution. When this
224224
/// method is called, the current testcase execution will be stopped as if it had
225-
/// finished executing normally, and the state will be restored to the state at the
226-
/// initial snapshot. This method is particularly useful in callbacks triggered on
227-
/// breakpoints or other complex conditions. This method does
225+
/// finished executing normally, and the state will be restored according to
226+
/// `snapshot_restore_interval` (restoring every iteration by default). This method is
227+
/// particularly useful in callbacks triggered on breakpoints or other complex
228+
/// conditions. This method does
228229
/// not need to be called if `set_stop_on_harness` is enabled.
229230
pub fn stop(&mut self) -> Result<()> {
230231
debug!(self.as_conf_object(), "stop");
@@ -237,7 +238,8 @@ impl Tsffs {
237238
/// Interface method to manually signal to stop execution with a solution condition.
238239
/// When this method is called, the current testcase execution will be stopped as if
239240
/// it had finished executing with an exception or timeout, and the state will be
240-
/// restored to the state at the initial snapshot.
241+
/// restored according to `snapshot_restore_interval` (restoring every iteration by
242+
/// default).
241243
pub fn solution(&mut self, id: u64, message: *mut c_char) -> Result<()> {
242244
let message = unsafe { CStr::from_ptr(message) }.to_str()?;
243245

0 commit comments

Comments
 (0)