afl_stat/
lib.rs

1//! This crate implement parsing AFL status from `fuzzer_status` file generated by AFL instances.
2//!
3//! All status data is included in the [`AFLStat`] struct. Use `AFLStat::load` function to
4//! load it from a specific `fuzzer_stats` file, or use `AFLStat::parse` function to parse a
5//! [`AFLStat`] struct from the content of a `fuzzer_stats` file.
6//!
7//! [`AFLStat`]: struct.AFLStat.html
8//!
9
10use std::collections::HashMap;
11use std::fmt::{Display, Formatter};
12use std::fs::File;
13use std::io::{BufReader, Read};
14use std::path::Path;
15use std::str::FromStr;
16
17///
18/// The error type in this crate.
19///
20#[derive(Debug)]
21pub enum Error {
22    /// An IO error occured.
23    IoError(std::io::Error),
24
25    /// Failed to parse `fuzzer_stats` file.
26    ParseError,
27}
28
29impl From<std::io::Error> for Error {
30    fn from(e: std::io::Error) -> Self {
31        Self::IoError(e)
32    }
33}
34
35impl std::error::Error for Error { }
36
37impl Display for Error {
38    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
39        match self {
40            Error::IoError(e) => f.write_fmt(format_args!("{}", e)),
41            Error::ParseError => f.write_str("parse error"),
42        }
43    }
44}
45
46/// Result type in this crate.
47pub type Result<T> = std::result::Result<T, Error>;
48
49/// AFL status data.
50///
51/// This is the primary struct in this crate.
52#[derive(Clone, Default, Debug)]
53pub struct AFLStat {
54    pub start_time: u64,
55    pub last_update: u64,
56    pub fuzzer_pid: u32,
57    pub cycles_done: i32,
58    pub execs_done: i64,
59    pub execs_per_sec: f64,
60    pub paths_total: i64,
61    pub paths_favored: i64,
62    pub paths_found: i64,
63    pub paths_imported: i64,
64    pub max_depth: i32,
65    pub cur_path: i64,
66    pub pending_favs: i64,
67    pub pending_total: i64,
68    pub variable_paths: i64,
69    pub stability: f64,
70    pub bitmap_cvg: f64,
71    pub unique_crashes: i32,
72    pub unique_hangs: i32,
73    pub last_path: u64,
74    pub last_crash: u64,
75    pub last_hang: u64,
76    pub execs_since_crash: i64,
77    pub exec_timeout: i32,
78    pub slowest_exec_ms: i32,
79    pub peak_rss_mb: i32,
80    pub afl_banner: String,
81    pub afl_version: String,
82    pub target_mode: String,
83    pub command_line: String,
84}
85
86impl AFLStat {
87    /// Parse the content of `fuzzer_stats` file.
88    pub fn parse(text: &str) -> Result<Self> {
89        let mut d = HashMap::<String, String>::new();
90        for ln in text.lines() {
91            if ln.trim().is_empty() {
92                continue;
93            }
94
95            let ln_data: Vec<&str> = ln.split(':').map(|s| s.trim()).collect();
96            if ln_data.len() != 2 {
97                return Err(Error::ParseError);
98            }
99            d.insert(ln_data[0].to_owned(), ln_data[1].to_owned());
100        }
101
102        Self::parse_dict(&d)
103    }
104
105    fn parse_dict(d: &HashMap<String, String>) -> Result<Self> {
106        let mut stat = Self::default();
107
108        if let Some(value) = d.get("start_time") {
109            stat.start_time = Self::parse_value(value)?;
110        }
111
112        if let Some(value) = d.get("last_update") {
113            stat.last_update = Self::parse_value(value)?;
114        }
115
116        if let Some(value) = d.get("fuzzer_pid") {
117            stat.fuzzer_pid = Self::parse_value(value)?;
118        }
119
120        if let Some(value) = d.get("cycles_done") {
121            stat.cycles_done = Self::parse_value(value)?;
122        }
123
124        if let Some(value) = d.get("execs_done") {
125            stat.execs_done = Self::parse_value(value)?;
126        }
127
128        if let Some(value) = d.get("execs_per_sec") {
129            stat.execs_per_sec = Self::parse_value(value)?;
130        }
131
132        if let Some(value) = d.get("paths_total") {
133            stat.paths_total = Self::parse_value(value)?;
134        }
135
136        if let Some(value) = d.get("paths_favored") {
137            stat.paths_favored = Self::parse_value(value)?;
138        }
139
140        if let Some(value) = d.get("paths_found") {
141            stat.paths_found = Self::parse_value(value)?;
142        }
143
144        if let Some(value) = d.get("paths_imported") {
145            stat.paths_imported = Self::parse_value(value)?;
146        }
147
148        if let Some(value) = d.get("max_depth") {
149            stat.max_depth = Self::parse_value(value)?;
150        }
151
152        if let Some(value) = d.get("cur_path") {
153            stat.cur_path = Self::parse_value(value)?;
154        }
155
156        if let Some(value) = d.get("pending_favs") {
157            stat.pending_favs = Self::parse_value(value)?;
158        }
159
160        if let Some(value) = d.get("pending_total") {
161            stat.pending_total = Self::parse_value(value)?;
162        }
163
164        if let Some(value) = d.get("variable_paths") {
165            stat.variable_paths = Self::parse_value(value)?;
166        }
167
168        if let Some(value) = d.get("stability") {
169            stat.stability = Self::parse_percentage(value)?;
170        }
171
172        if let Some(value) = d.get("bitmap_cvg") {
173            stat.bitmap_cvg = Self::parse_percentage(value)?;
174        }
175
176        if let Some(value) = d.get("unique_crashes") {
177            stat.unique_crashes = Self::parse_value(value)?;
178        }
179
180        if let Some(value) = d.get("unique_hangs") {
181            stat.unique_hangs = Self::parse_value(value)?;
182        }
183
184        if let Some(value) = d.get("last_path") {
185            stat.last_path = Self::parse_value(value)?;
186        }
187
188        if let Some(value) = d.get("last_crash") {
189            stat.last_crash = Self::parse_value(value)?;
190        }
191
192        if let Some(value) = d.get("last_hang") {
193            stat.last_hang = Self::parse_value(value)?;
194        }
195
196        if let Some(value) = d.get("execs_since_crash") {
197            stat.execs_since_crash = Self::parse_value(value)?;
198        }
199
200        if let Some(value) = d.get("exec_timeout") {
201            stat.exec_timeout = Self::parse_value(value)?;
202        }
203
204        if let Some(value) = d.get("slowest_exec_ms") {
205            stat.slowest_exec_ms = Self::parse_value(value)?;
206        }
207
208        if let Some(value) = d.get("peak_rss_mb") {
209            stat.peak_rss_mb = Self::parse_value(value)?;
210        }
211
212        if let Some(value) = d.get("afl_banner") {
213            stat.afl_banner = value.clone();
214        }
215
216        if let Some(value) = d.get("afl_version") {
217            stat.afl_version = value.clone();
218        }
219
220        if let Some(value) = d.get("target_mode") {
221            stat.target_mode = value.clone();
222        }
223
224        if let Some(value) = d.get("command_line") {
225            stat.command_line = value.clone();
226        }
227
228        Ok(stat)
229    }
230
231    fn parse_value<T>(text: &str) -> Result<T>
232        where T: FromStr {
233        T::from_str(text)
234            .map_err(|_| Error::ParseError)
235    }
236
237    fn parse_percentage(text: &str) -> Result<f64> {
238        if text.is_empty() {
239            return Err(Error::ParseError);
240        }
241
242        if !text.ends_with('%') {
243            return Err(Error::ParseError);
244        }
245
246        let text = text.trim_end_matches('%');
247        f64::from_str(text)
248            .map(|x| x / 100.0)
249            .map_err(|_| Error::ParseError)
250    }
251
252    /// Load AFL status data from the given `fuzzer_stats` file.
253    ///
254    /// ### Example
255    ///
256    /// ```ignore
257    /// let stat = AFLStat::load("path/to/fuzz/dir/fuzzer_stats").unwrap();
258    /// ```
259    pub fn load(stat_file: &Path) -> Result<Self> {
260        let text = {
261            let file = File::open(stat_file)?;
262            let mut reader = BufReader::new(file);
263            let mut buf = String::new();
264            reader.read_to_string(&mut buf)?;
265            buf
266        };
267        Self::parse(&text)
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    #[test]
276    fn parse_value_ok() {
277        assert_eq!(10, AFLStat::parse_value::<i32>("10").unwrap());
278    }
279
280    #[test]
281    fn parse_value_bad() {
282        assert!(AFLStat::parse_value::<i32>("10x").is_err());
283    }
284
285    #[test]
286    fn parse_percentage_ok() {
287        assert_eq!(0.25, AFLStat::parse_percentage("25%").unwrap());
288        assert_eq!(1.25, AFLStat::parse_percentage("125%").unwrap());
289    }
290
291    #[test]
292    fn parse_percentage_bad() {
293        assert!(AFLStat::parse_percentage("%").is_err());
294        assert!(AFLStat::parse_percentage("0.25").is_err());
295    }
296
297    #[test]
298    fn parse_afl_stat_from_dict_ok() {
299        let mut d = HashMap::<String, String>::new();
300        d.insert(String::from("start_time"), String::from("1587396831"));
301        d.insert(String::from("last_update"), String::from("1587488608"));
302        d.insert(String::from("fuzzer_pid"), String::from("23661"));
303        d.insert(String::from("cycles_done"), String::from("1"));
304        d.insert(String::from("execs_done"), String::from("354214"));
305        d.insert(String::from("execs_per_sec"), String::from("3.86"));
306        d.insert(String::from("paths_total"), String::from("6204"));
307        d.insert(String::from("paths_favored"), String::from("631"));
308        d.insert(String::from("paths_found"), String::from("451"));
309        d.insert(String::from("paths_imported"), String::from("4642"));
310        d.insert(String::from("max_depth"), String::from("3"));
311        d.insert(String::from("cur_path"), String::from("2580"));
312        d.insert(String::from("pending_favs"), String::from("513"));
313        d.insert(String::from("pending_total"), String::from("5983"));
314        d.insert(String::from("variable_paths"), String::from("6198"));
315        d.insert(String::from("stability"), String::from("63.45%"));
316        d.insert(String::from("bitmap_cvg"), String::from("91.79%"));
317        d.insert(String::from("unique_crashes"), String::from("43"));
318        d.insert(String::from("unique_hangs"), String::from("245"));
319        d.insert(String::from("last_path"), String::from("1587488588"));
320        d.insert(String::from("last_crash"), String::from("1587487142"));
321        d.insert(String::from("last_hang"), String::from("1587486568"));
322        d.insert(String::from("execs_since_crash"), String::from("354214"));
323        d.insert(String::from("exec_timeout"), String::from("1000"));
324        d.insert(String::from("slowest_exec_ms"), String::from("1250"));
325        d.insert(String::from("peak_rss_mb"), String::from("0"));
326        d.insert(String::from("afl_banner"), String::from("fuzzer0"));
327        d.insert(String::from("afl_version"), String::from("++2.62c"));
328        d.insert(String::from("target_mode"), String::from("default"));
329        d.insert(String::from("command_line"), String::from("afl-fuzz arg1 arg2"));
330        let stat = AFLStat::parse_dict(&d).unwrap();
331        assert_correct_stat(&stat);
332    }
333
334    #[test]
335    fn parse_afl_stat_from_dict_bad() {
336        // The `stability` field is changed: the percentage sign (%) is removed.
337        // This should trigger an error from `AFLStat::parse_dict`.
338        let mut d = HashMap::<String, String>::new();
339        d.insert(String::from("start_time"), String::from("1587396831"));
340        d.insert(String::from("last_update"), String::from("1587488608"));
341        d.insert(String::from("fuzzer_pid"), String::from("23661"));
342        d.insert(String::from("cycles_done"), String::from("1"));
343        d.insert(String::from("execs_done"), String::from("354214"));
344        d.insert(String::from("execs_per_sec"), String::from("3.86"));
345        d.insert(String::from("paths_total"), String::from("6204"));
346        d.insert(String::from("paths_favored"), String::from("631"));
347        d.insert(String::from("paths_found"), String::from("451"));
348        d.insert(String::from("paths_imported"), String::from("4642"));
349        d.insert(String::from("max_depth"), String::from("3"));
350        d.insert(String::from("cur_path"), String::from("2580"));
351        d.insert(String::from("pending_favs"), String::from("513"));
352        d.insert(String::from("pending_total"), String::from("5983"));
353        d.insert(String::from("variable_paths"), String::from("6198"));
354        d.insert(String::from("stability"), String::from("63.45"));
355        d.insert(String::from("bitmap_cvg"), String::from("91.79%"));
356        d.insert(String::from("unique_crashes"), String::from("43"));
357        d.insert(String::from("unique_hangs"), String::from("245"));
358        d.insert(String::from("last_path"), String::from("1587488588"));
359        d.insert(String::from("last_crash"), String::from("1587487142"));
360        d.insert(String::from("last_hang"), String::from("1587486568"));
361        d.insert(String::from("execs_since_crash"), String::from("354214"));
362        d.insert(String::from("exec_timeout"), String::from("1000"));
363        d.insert(String::from("slowest_exec_ms"), String::from("1250"));
364        d.insert(String::from("peak_rss_mb"), String::from("0"));
365        d.insert(String::from("afl_banner"), String::from("fuzzer0"));
366        d.insert(String::from("afl_version"), String::from("++2.62c"));
367        d.insert(String::from("target_mode"), String::from("default"));
368        d.insert(String::from("command_line"), String::from("afl-fuzz arg1 arg2"));
369        assert!(AFLStat::parse_dict(&d).is_err());
370    }
371
372    #[test]
373    fn parse_afl_stat_ok() {
374        let raw_stat = r#"
375            start_time        : 1587396831
376            last_update       : 1587488608
377            fuzzer_pid        : 23661
378            cycles_done       : 1
379            execs_done        : 354214
380            execs_per_sec     : 3.86
381            paths_total       : 6204
382            paths_favored     : 631
383            paths_found       : 451
384            paths_imported    : 4642
385            max_depth         : 3
386            cur_path          : 2580
387            pending_favs      : 513
388            pending_total     : 5983
389            variable_paths    : 6198
390            stability         : 63.45%
391            bitmap_cvg        : 91.79%
392            unique_crashes    : 43
393            unique_hangs      : 245
394            last_path         : 1587488588
395            last_crash        : 1587487142
396            last_hang         : 1587486568
397            execs_since_crash : 354214
398            exec_timeout      : 1000
399            slowest_exec_ms   : 1250
400            peak_rss_mb       : 0
401            afl_banner        : fuzzer0
402            afl_version       : ++2.62c
403            target_mode       : default
404            command_line      : afl-fuzz arg1 arg2
405        "#;
406        let stat = AFLStat::parse(raw_stat).unwrap();
407        assert_correct_stat(&stat);
408    }
409
410    fn assert_correct_stat(stat: &AFLStat) {
411        assert_eq!(1587396831, stat.start_time);
412        assert_eq!(1587488608, stat.last_update);
413        assert_eq!(23661, stat.fuzzer_pid);
414        assert_eq!(1, stat.cycles_done);
415        assert_eq!(354214, stat.execs_done);
416        assert!((3.86 - stat.execs_per_sec).abs() < 1e-8);
417        assert_eq!(6204, stat.paths_total);
418        assert_eq!(631, stat.paths_favored);
419        assert_eq!(451, stat.paths_found);
420        assert_eq!(4642, stat.paths_imported);
421        assert_eq!(3, stat.max_depth);
422        assert_eq!(2580, stat.cur_path);
423        assert_eq!(513, stat.pending_favs);
424        assert_eq!(5983, stat.pending_total);
425        assert_eq!(6198, stat.variable_paths);
426        assert!((0.6345 - stat.stability).abs() < 1e-8);
427        assert!((0.9179 - stat.bitmap_cvg).abs() < 1e-8);
428        assert_eq!(43, stat.unique_crashes);
429        assert_eq!(245, stat.unique_hangs);
430        assert_eq!(1587488588, stat.last_path);
431        assert_eq!(1587487142, stat.last_crash);
432        assert_eq!(1587486568, stat.last_hang);
433        assert_eq!(354214, stat.execs_since_crash);
434        assert_eq!(1000, stat.exec_timeout);
435        assert_eq!(1250, stat.slowest_exec_ms);
436        assert_eq!(0, stat.peak_rss_mb);
437        assert_eq!("fuzzer0", stat.afl_banner);
438        assert_eq!("++2.62c", stat.afl_version);
439        assert_eq!("default", stat.target_mode);
440        assert_eq!("afl-fuzz arg1 arg2", stat.command_line);
441    }
442
443    #[test]
444    fn parse_afl_stat_bad() {
445        // The `stability` field is modified: the percentage sign (%) is removed. This is expected
446        // to trigger a parse error.
447        let raw_stat = r#"
448            start_time        : 1587396831
449            last_update       : 1587488608
450            fuzzer_pid        : 23661
451            cycles_done       : 1
452            execs_done        : 354214
453            execs_per_sec     : 3.86
454            paths_total       : 6204
455            paths_favored     : 631
456            paths_found       : 451
457            paths_imported    : 4642
458            max_depth         : 3
459            cur_path          : 2580
460            pending_favs      : 513
461            pending_total     : 5983
462            variable_paths    : 6198
463            stability         : 63.45
464            bitmap_cvg        : 91.79%
465            unique_crashes    : 43
466            unique_hangs      : 245
467            last_path         : 1587488588
468            last_crash        : 1587487142
469            last_hang         : 1587486568
470            execs_since_crash : 354214
471            exec_timeout      : 1000
472            slowest_exec_ms   : 1250
473            peak_rss_mb       : 0
474            afl_banner        : fuzzer0
475            afl_version       : ++2.62c
476            target_mode       : default
477            command_line      : afl-fuzz arg1 arg2
478        "#;
479        assert!(AFLStat::parse(raw_stat).is_err());
480    }
481}