Skip to content

Commit bd834bb

Browse files
authored
Add DumpTargetBytesToDiskStage to dump complex inputs to disk (#3494)
* Add DumpTargetBytesToDiskStage to dump complex inputs to disk * Remove redundant code * clippy * docs
1 parent 124e207 commit bd834bb

File tree

4 files changed

+455
-252
lines changed

4 files changed

+455
-252
lines changed

crates/libafl/src/stages/dump.rs

Lines changed: 222 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,91 @@ use libafl_bolts::impl_serdeany;
1515
use serde::{Deserialize, Serialize};
1616

1717
use crate::{
18-
Error, HasMetadata,
18+
Error,
19+
common::HasMetadata,
1920
corpus::{Corpus, CorpusId, Testcase},
20-
inputs::Input,
21+
fuzzer::HasTargetBytesConverter,
22+
inputs::{Input, ToTargetBytes},
2123
stages::{Restartable, Stage},
2224
state::{HasCorpus, HasRand, HasSolutions},
2325
};
2426

27+
/// The default function to generate a filename for a testcase
28+
pub fn generate_filename<I: Input>(testcase: &Testcase<I>, id: &CorpusId) -> String {
29+
[
30+
Some(id.0.to_string()),
31+
testcase.filename().clone(),
32+
testcase
33+
.input()
34+
.as_ref()
35+
.map(|t| t.generate_name(Some(*id))),
36+
]
37+
.iter()
38+
.flatten()
39+
.map(String::as_str)
40+
.collect::<Vec<_>>()
41+
.join("-")
42+
}
43+
44+
/// The default function to create the directories if they don't exist
45+
fn create_dirs_if_needed<A, B>(corpus_dir: A, solutions_dir: B) -> Result<(PathBuf, PathBuf), Error>
46+
where
47+
A: Into<PathBuf>,
48+
B: Into<PathBuf>,
49+
{
50+
let corpus_dir = corpus_dir.into();
51+
if let Err(e) = fs::create_dir(&corpus_dir) {
52+
if !corpus_dir.is_dir() {
53+
return Err(Error::os_error(
54+
e,
55+
format!("Error creating directory {}", corpus_dir.display()),
56+
));
57+
}
58+
}
59+
let solutions_dir = solutions_dir.into();
60+
if let Err(e) = fs::create_dir(&solutions_dir) {
61+
if !solutions_dir.is_dir() {
62+
return Err(Error::os_error(
63+
e,
64+
format!("Error creating directory {}", solutions_dir.display()),
65+
));
66+
}
67+
}
68+
Ok((corpus_dir, solutions_dir))
69+
}
70+
71+
/// The default function to dump a corpus to disk
72+
fn dump_from_corpus<C, F, G, I, P>(
73+
corpus: &C,
74+
dir: &Path,
75+
generate_filename: &mut G,
76+
get_bytes: &mut F,
77+
start_id: Option<CorpusId>,
78+
) -> Result<(), Error>
79+
where
80+
C: Corpus<I>,
81+
I: Input,
82+
F: FnMut(&mut Testcase<I>) -> Result<Vec<u8>, Error>,
83+
G: FnMut(&Testcase<I>, &CorpusId) -> P,
84+
P: AsRef<Path>,
85+
{
86+
let mut id = start_id.or_else(|| corpus.first());
87+
while let Some(i) = id {
88+
let testcase = corpus.get(i)?;
89+
let fname = dir.join(generate_filename(&testcase.borrow(), &i));
90+
91+
let mut testcase = testcase.borrow_mut();
92+
corpus.load_input_into(&mut testcase)?;
93+
let bytes = get_bytes(&mut testcase)?;
94+
95+
let mut f = File::create(fname)?;
96+
f.write_all(&bytes)?;
97+
98+
id = corpus.next(i);
99+
}
100+
Ok(())
101+
}
102+
25103
/// Metadata used to store information about disk dump indexes for names
26104
#[cfg_attr(
27105
any(not(feature = "serdeany_autoreg"), miri),
@@ -50,6 +128,7 @@ where
50128
CB1: FnMut(&Testcase<I>, &S) -> Vec<u8>,
51129
CB2: FnMut(&Testcase<I>, &CorpusId) -> P,
52130
S: HasCorpus<I> + HasSolutions<I> + HasRand + HasMetadata,
131+
I: Input,
53132
P: AsRef<Path>,
54133
{
55134
#[inline]
@@ -60,7 +139,40 @@ where
60139
state: &mut S,
61140
_manager: &mut EM,
62141
) -> Result<(), Error> {
63-
self.dump_state_to_disk(state)
142+
let (last_corpus, last_solution) =
143+
if let Some(meta) = state.metadata_map().get::<DumpToDiskMetadata>() {
144+
(
145+
meta.last_corpus.and_then(|x| state.corpus().next(x)),
146+
meta.last_solution.and_then(|x| state.solutions().next(x)),
147+
)
148+
} else {
149+
(state.corpus().first(), state.solutions().first())
150+
};
151+
152+
let mut get_bytes = |tc: &mut Testcase<I>| Ok((self.to_bytes)(tc, state));
153+
154+
dump_from_corpus(
155+
state.corpus(),
156+
&self.corpus_dir,
157+
&mut self.generate_filename,
158+
&mut get_bytes,
159+
last_corpus,
160+
)?;
161+
162+
dump_from_corpus(
163+
state.solutions(),
164+
&self.solutions_dir,
165+
&mut self.generate_filename,
166+
&mut get_bytes,
167+
last_solution,
168+
)?;
169+
170+
state.add_metadata(DumpToDiskMetadata {
171+
last_corpus: state.corpus().last(),
172+
last_solution: state.solutions().last(),
173+
});
174+
175+
Ok(())
64176
}
65177
}
66178

@@ -94,29 +206,11 @@ where
94206
{
95207
Self::new_with_custom_filenames(
96208
to_bytes,
97-
Self::generate_filename, // This is now of type `fn(&Testcase<EM::Input>, &CorpusId) -> String`
209+
generate_filename, // This is now of type `fn(&Testcase<EM::Input>, &CorpusId) -> String`
98210
corpus_dir,
99211
solutions_dir,
100212
)
101213
}
102-
103-
/// Default `generate_filename` function.
104-
#[expect(clippy::trivially_copy_pass_by_ref)]
105-
fn generate_filename(testcase: &Testcase<I>, id: &CorpusId) -> String {
106-
[
107-
Some(id.0.to_string()),
108-
testcase.filename().clone(),
109-
testcase
110-
.input()
111-
.as_ref()
112-
.map(|t| t.generate_name(Some(*id))),
113-
]
114-
.iter()
115-
.flatten()
116-
.map(String::as_str)
117-
.collect::<Vec<_>>()
118-
.join("-")
119-
}
120214
}
121215

122216
impl<CB1, CB2, EM, I, S, Z> DumpToDiskStage<CB1, CB2, EM, I, S, Z>
@@ -134,24 +228,7 @@ where
134228
A: Into<PathBuf>,
135229
B: Into<PathBuf>,
136230
{
137-
let corpus_dir = corpus_dir.into();
138-
if let Err(e) = fs::create_dir(&corpus_dir) {
139-
if !corpus_dir.is_dir() {
140-
return Err(Error::os_error(
141-
e,
142-
format!("Error creating directory {}", corpus_dir.display()),
143-
));
144-
}
145-
}
146-
let solutions_dir = solutions_dir.into();
147-
if let Err(e) = fs::create_dir(&solutions_dir) {
148-
if !solutions_dir.is_dir() {
149-
return Err(Error::os_error(
150-
e,
151-
format!("Error creating directory {}", solutions_dir.display()),
152-
));
153-
}
154-
}
231+
let (corpus_dir, solutions_dir) = create_dirs_if_needed(corpus_dir, solutions_dir)?;
155232
Ok(Self {
156233
to_bytes,
157234
generate_filename,
@@ -160,15 +237,38 @@ where
160237
phantom: PhantomData,
161238
})
162239
}
240+
}
241+
242+
/// A stage that dumps the corpus and the solutions to disk,
243+
/// using the fuzzer's [`crate::fuzzer::HasTargetBytesConverter`] (if available).
244+
///
245+
/// Set the converter using the fuzzer builder's [`crate::fuzzer::StdFuzzerBuilder::target_bytes_converter`].
246+
#[derive(Debug)]
247+
pub struct DumpTargetBytesToDiskStage<CB, EM, I, S, Z> {
248+
solutions_dir: PathBuf,
249+
corpus_dir: PathBuf,
250+
generate_filename: CB,
251+
phantom: PhantomData<(EM, I, S, Z)>,
252+
}
163253

254+
impl<CB, E, EM, I, S, P, Z> Stage<E, EM, S, Z> for DumpTargetBytesToDiskStage<CB, EM, I, S, Z>
255+
where
256+
CB: FnMut(&Testcase<I>, &CorpusId) -> P,
257+
S: HasCorpus<I> + HasSolutions<I> + HasRand + HasMetadata,
258+
P: AsRef<Path>,
259+
Z: HasTargetBytesConverter,
260+
Z::Converter: ToTargetBytes<I>,
261+
I: Input,
262+
{
164263
#[inline]
165-
fn dump_state_to_disk<P: AsRef<Path>>(&mut self, state: &mut S) -> Result<(), Error>
166-
where
167-
S: HasCorpus<I>,
168-
CB1: FnMut(&Testcase<I>, &S) -> Vec<u8>,
169-
CB2: FnMut(&Testcase<I>, &CorpusId) -> P,
170-
{
171-
let (mut corpus_id, mut solutions_id) =
264+
fn perform(
265+
&mut self,
266+
fuzzer: &mut Z,
267+
_executor: &mut E,
268+
state: &mut S,
269+
_manager: &mut EM,
270+
) -> Result<(), Error> {
271+
let (last_corpus, last_solution) =
172272
if let Some(meta) = state.metadata_map().get::<DumpToDiskMetadata>() {
173273
(
174274
meta.last_corpus.and_then(|x| state.corpus().next(x)),
@@ -178,33 +278,26 @@ where
178278
(state.corpus().first(), state.solutions().first())
179279
};
180280

181-
while let Some(i) = corpus_id {
182-
let mut testcase = state.corpus().get(i)?.borrow_mut();
183-
state.corpus().load_input_into(&mut testcase)?;
184-
let bytes = (self.to_bytes)(&testcase, state);
281+
let mut get_bytes = |tc: &mut Testcase<I>| {
282+
let input = tc.input().as_ref().unwrap();
283+
Ok(fuzzer.to_target_bytes(input).to_vec())
284+
};
185285

186-
let fname = self
187-
.corpus_dir
188-
.join((self.generate_filename)(&testcase, &i));
189-
let mut f = File::create(fname)?;
190-
drop(f.write_all(&bytes));
191-
192-
corpus_id = state.corpus().next(i);
193-
}
286+
dump_from_corpus(
287+
state.corpus(),
288+
&self.corpus_dir,
289+
&mut self.generate_filename,
290+
&mut get_bytes,
291+
last_corpus,
292+
)?;
194293

195-
while let Some(i) = solutions_id {
196-
let mut testcase = state.solutions().get(i)?.borrow_mut();
197-
state.solutions().load_input_into(&mut testcase)?;
198-
let bytes = (self.to_bytes)(&testcase, state);
199-
200-
let fname = self
201-
.solutions_dir
202-
.join((self.generate_filename)(&testcase, &i));
203-
let mut f = File::create(fname)?;
204-
drop(f.write_all(&bytes));
205-
206-
solutions_id = state.solutions().next(i);
207-
}
294+
dump_from_corpus(
295+
state.solutions(),
296+
&self.solutions_dir,
297+
&mut self.generate_filename,
298+
&mut get_bytes,
299+
last_solution,
300+
)?;
208301

209302
state.add_metadata(DumpToDiskMetadata {
210303
last_corpus: state.corpus().last(),
@@ -214,3 +307,60 @@ where
214307
Ok(())
215308
}
216309
}
310+
311+
impl<EM, I, S, Z> Restartable<S>
312+
for DumpTargetBytesToDiskStage<fn(&Testcase<I>, &CorpusId) -> String, EM, I, S, Z>
313+
{
314+
#[inline]
315+
fn should_restart(&mut self, _state: &mut S) -> Result<bool, Error> {
316+
// Not executing the target, so restart safety is not needed
317+
Ok(true)
318+
}
319+
320+
#[inline]
321+
fn clear_progress(&mut self, _state: &mut S) -> Result<(), Error> {
322+
// Not executing the target, so restart safety is not needed
323+
Ok(())
324+
}
325+
}
326+
327+
/// Implementation for `DumpTargetBytesToDiskStage` with a default `generate_filename` function.
328+
impl<EM, I, S, Z> DumpTargetBytesToDiskStage<fn(&Testcase<I>, &CorpusId) -> String, EM, I, S, Z>
329+
where
330+
S: HasSolutions<I> + HasRand + HasMetadata,
331+
I: Input,
332+
{
333+
/// Create a new [`DumpTargetBytesToDiskStage`] with a default `generate_filename` function.
334+
pub fn new<A, B>(corpus_dir: A, solutions_dir: B) -> Result<Self, Error>
335+
where
336+
A: Into<PathBuf>,
337+
B: Into<PathBuf>,
338+
{
339+
Self::new_with_custom_filenames(generate_filename, corpus_dir, solutions_dir)
340+
}
341+
}
342+
343+
impl<CB, EM, I, S, Z> DumpTargetBytesToDiskStage<CB, EM, I, S, Z>
344+
where
345+
S: HasMetadata + HasSolutions<I>,
346+
I: Input,
347+
{
348+
/// Create a new [`DumpTargetBytesToDiskStage`] with a custom `generate_filename` function.
349+
pub fn new_with_custom_filenames<A, B>(
350+
generate_filename: CB,
351+
corpus_dir: A,
352+
solutions_dir: B,
353+
) -> Result<Self, Error>
354+
where
355+
A: Into<PathBuf>,
356+
B: Into<PathBuf>,
357+
{
358+
let (corpus_dir, solutions_dir) = create_dirs_if_needed(corpus_dir, solutions_dir)?;
359+
Ok(Self {
360+
generate_filename,
361+
solutions_dir,
362+
corpus_dir,
363+
phantom: PhantomData,
364+
})
365+
}
366+
}
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1-
libpng-*
1+
libpng-*
2+
corpus_bytes
3+
crashes
4+
crashes_bytes

0 commit comments

Comments
 (0)