use std::collections::{BTreeMap, HashMap, HashSet};
use std::fmt;
use std::io;
use std::path::{Path, PathBuf};
use std::process::Command as ProcessCommand;
use std::time::{Duration, Instant, SystemTime};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FuzzingEngine {
LibFuzzer,
AFL,
AFLPlusPlus,
Honggfuzz,
OSSFuzz,
Custom,
}
impl FuzzingEngine {
pub fn name(&self) -> &'static str {
match self {
Self::LibFuzzer => "libFuzzer",
Self::AFL => "AFL",
Self::AFLPlusPlus => "AFL++",
Self::Honggfuzz => "Honggfuzz",
Self::OSSFuzz => "OSS-Fuzz",
Self::Custom => "Custom Engine",
}
}
pub fn compiler_flag(&self) -> &'static str {
match self {
Self::LibFuzzer => "-fsanitize=fuzzer",
Self::AFL | Self::AFLPlusPlus => "afl-clang-fast",
Self::Honggfuzz => "hfuzz-clang",
Self::OSSFuzz => "-fsanitize=fuzzer",
Self::Custom => "",
}
}
}
#[derive(Debug, Clone)]
pub struct FuzzingConfig {
pub engine: FuzzingEngine,
pub target: PathBuf,
pub corpus_dir: PathBuf,
pub artifact_dir: PathBuf,
pub max_len: Option<usize>,
pub max_total_time: Option<Duration>,
pub runs: Option<usize>,
pub jobs: u32,
pub workers: u32,
pub dict: Option<PathBuf>,
pub seed: Option<u64>,
pub timeout_ms: u32,
pub rss_limit_mb: u64,
pub sanitizers: Vec<Sanitizer>,
pub coverage: CoverageType,
pub reduce_inputs: bool,
pub fork_mode: Option<ForkMode>,
pub use_clang_coverage: bool,
pub extra_args: Vec<String>,
pub env_vars: HashMap<String, String>,
}
impl Default for FuzzingConfig {
fn default() -> Self {
Self {
engine: FuzzingEngine::LibFuzzer,
target: PathBuf::from("./fuzz_target"),
corpus_dir: PathBuf::from("./corpus"),
artifact_dir: PathBuf::from("./artifacts"),
max_len: Some(4096),
max_total_time: Some(Duration::from_secs(3600)),
runs: None,
jobs: 1,
workers: 1,
dict: None,
seed: None,
timeout_ms: 1000,
rss_limit_mb: 2048,
sanitizers: vec![Sanitizer::Address, Sanitizer::Undefined],
coverage: CoverageType::Edge,
reduce_inputs: true,
fork_mode: Some(ForkMode::Persistent),
use_clang_coverage: true,
extra_args: Vec::new(),
env_vars: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Sanitizer {
Address,
Undefined,
Memory,
Thread,
Leak,
DataFlow,
SafeStack,
CFI,
HWAddress,
}
impl Sanitizer {
pub fn compiler_flag(&self) -> &'static str {
match self {
Self::Address => "address",
Self::Undefined => "undefined",
Self::Memory => "memory",
Self::Thread => "thread",
Self::Leak => "leak",
Self::DataFlow => "dataflow",
Self::SafeStack => "safestack",
Self::CFI => "cfi",
Self::HWAddress => "hwaddress",
}
}
pub fn flag(&self) -> String {
format!("-fsanitize={}", self.compiler_flag())
}
pub fn description(&self) -> &'static str {
match self {
Self::Address => {
"AddressSanitizer: detects memory errors (use-after-free, buffer overflow)"
}
Self::Undefined => "UndefinedBehaviorSanitizer: detects undefined behavior",
Self::Memory => "MemorySanitizer: detects uninitialized reads",
Self::Thread => "ThreadSanitizer: detects data races",
Self::Leak => "LeakSanitizer: detects memory leaks",
Self::DataFlow => "DataFlowSanitizer: generic dynamic data flow analysis",
Self::SafeStack => "SafeStack: protects against stack-based memory corruption",
Self::CFI => "Control Flow Integrity: prevents control-flow hijacking",
Self::HWAddress => "Hardware-assisted AddressSanitizer",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CoverageType {
None,
Edge,
Block,
Function,
Path,
ValueProfile,
FullCoverage,
}
impl CoverageType {
pub fn compiler_flags(&self) -> Vec<&'static str> {
match self {
Self::None => vec![],
Self::Edge | Self::Path | Self::ValueProfile | Self::FullCoverage => {
vec!["-fprofile-instr-generate", "-fcoverage-mapping"]
}
Self::Block => vec!["-fsanitize-coverage=bb", "-fcoverage-mapping"],
Self::Function => vec!["-fsanitize-coverage=func", "-fcoverage-mapping"],
}
}
pub fn libfuzzer_coverage(&self) -> &'static str {
match self {
Self::None => "0",
Self::Edge => "edge",
Self::Block => "bb",
Self::Function => "func",
Self::Path => "trace-pc-guard",
Self::ValueProfile => "trace-cmp",
Self::FullCoverage => "trace-pc-guard,trace-cmp,trace-div,trace-gep",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ForkMode {
Persistent,
InProcess,
ForkServer,
Timeout { per_input_ms: u32 },
}
#[derive(Debug, Clone)]
pub struct FuzzingResult {
pub engine: FuzzingEngine,
pub start_time: SystemTime,
pub end_time: SystemTime,
pub total_executions: u64,
pub execs_per_second: f64,
pub crashes: Vec<CrashInfo>,
pub hangs: Vec<HangInfo>,
pub ooms: Vec<OomInfo>,
pub coverage_achieved: Option<CoverageStats>,
pub corpus_size: usize,
pub corpus_elements: usize,
pub peak_rss_mb: u64,
pub features: Vec<String>,
pub stats: HashMap<String, u64>,
}
impl Default for FuzzingResult {
fn default() -> Self {
Self {
engine: FuzzingEngine::LibFuzzer,
start_time: SystemTime::now(),
end_time: SystemTime::now(),
total_executions: 0,
execs_per_second: 0.0,
crashes: Vec::new(),
hangs: Vec::new(),
ooms: Vec::new(),
coverage_achieved: None,
corpus_size: 0,
corpus_elements: 0,
peak_rss_mb: 0,
features: Vec::new(),
stats: HashMap::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct CrashInfo {
pub id: String,
pub input_file: PathBuf,
pub crash_type: CrashType,
pub signal: Option<i32>,
pub sanitizer_report: Option<String>,
pub stack_trace: Vec<StackFrame>,
pub minimized: bool,
pub minimized_input: Option<PathBuf>,
pub reproducible: bool,
pub unique_hash: u64,
pub timestamp: SystemTime,
pub asan_output: Option<String>,
pub ubsan_output: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CrashType {
Segfault,
Abort,
AssertFail,
HeapBufferOverflow,
StackBufferOverflow,
GlobalBufferOverflow,
UseAfterFree,
UseAfterReturn,
UseAfterScope,
DoubleFree,
InvalidFree,
MemoryLeak,
UninitializedRead,
UndefinedBehavior,
IntegerOverflow,
NullDereference,
Alignment,
DataRace,
Deadlock,
Timeout,
OutOfMemory,
UnknownCode(i32),
}
impl CrashType {
pub fn description(&self) -> &'static str {
match self {
Self::Segfault => "Segmentation fault (SIGSEGV)",
Self::Abort => "Abort signal (SIGABRT)",
Self::AssertFail => "Assertion failure",
Self::HeapBufferOverflow => "Heap buffer overflow",
Self::StackBufferOverflow => "Stack buffer overflow",
Self::GlobalBufferOverflow => "Global buffer overflow",
Self::UseAfterFree => "Use-after-free",
Self::UseAfterReturn => "Use-after-return",
Self::UseAfterScope => "Use-after-scope",
Self::DoubleFree => "Double free",
Self::InvalidFree => "Invalid free",
Self::MemoryLeak => "Memory leak",
Self::UninitializedRead => "Uninitialized memory read",
Self::UndefinedBehavior => "Undefined behavior",
Self::IntegerOverflow => "Integer overflow",
Self::NullDereference => "Null pointer dereference",
Self::Alignment => "Misaligned memory access",
Self::DataRace => "Data race",
Self::Deadlock => "Deadlock detected",
Self::Timeout => "Input caused timeout",
Self::OutOfMemory => "Out of memory",
Self::UnknownCode(_) => "Unknown crash type",
}
}
pub fn from_sanitizer_output(output: &str) -> Option<Self> {
let lower = output.to_lowercase();
if lower.contains("heap-buffer-overflow") {
Some(Self::HeapBufferOverflow)
} else if lower.contains("stack-buffer-overflow") {
Some(Self::StackBufferOverflow)
} else if lower.contains("global-buffer-overflow") {
Some(Self::GlobalBufferOverflow)
} else if lower.contains("use-after-free") {
Some(Self::UseAfterFree)
} else if lower.contains("use-after-return") {
Some(Self::UseAfterReturn)
} else if lower.contains("use-after-scope") {
Some(Self::UseAfterScope)
} else if lower.contains("double-free") {
Some(Self::DoubleFree)
} else if lower.contains("memory leak") || lower.contains("leak-sanitizer") {
Some(Self::MemoryLeak)
} else if lower.contains("use-of-uninitialized-value") || lower.contains("memorysanitizer")
{
Some(Self::UninitializedRead)
} else if lower.contains("undefined behavior") || lower.contains("ubsan") {
Some(Self::UndefinedBehavior)
} else if lower.contains("integer overflow") {
Some(Self::IntegerOverflow)
} else if lower.contains("null") && lower.contains("dereference") {
Some(Self::NullDereference)
} else if lower.contains("data race") || lower.contains("threadsanitizer") {
Some(Self::DataRace)
} else if lower.contains("deadlock") {
Some(Self::Deadlock)
} else if lower.contains("out of memory") || lower.contains("out-of-memory") {
Some(Self::OutOfMemory)
} else {
None
}
}
}
#[derive(Debug, Clone)]
pub struct StackFrame {
pub index: usize,
pub function_name: String,
pub file: Option<String>,
pub line: Option<u32>,
pub column: Option<u32>,
pub module: Option<String>,
pub address: Option<u64>,
pub inlined: bool,
pub symbolized: bool,
}
#[derive(Debug, Clone)]
pub struct HangInfo {
pub id: String,
pub input_file: PathBuf,
pub hang_duration_ms: u64,
pub stack_trace: Vec<StackFrame>,
pub unique_hash: u64,
pub timestamp: SystemTime,
}
#[derive(Debug, Clone)]
pub struct OomInfo {
pub id: String,
pub input_file: PathBuf,
pub rss_bytes: u64,
pub allocation_size: Option<u64>,
pub stack_trace: Vec<StackFrame>,
pub timestamp: SystemTime,
}
#[derive(Debug, Clone)]
pub struct CoverageStats {
pub functions_total: u64,
pub functions_executed: u64,
pub functions_percent: f64,
pub lines_total: u64,
pub lines_executed: u64,
pub lines_percent: f64,
pub regions_total: u64,
pub regions_executed: u64,
pub regions_percent: f64,
pub branches_total: u64,
pub branches_executed: u64,
pub branches_percent: f64,
}
pub fn compile_libfuzzer_target(
source: &Path,
output: &Path,
sanitizers: &[Sanitizer],
coverage: CoverageType,
extra_flags: &[&str],
) -> Result<PathBuf, String> {
let mut cmd = ProcessCommand::new("clang++");
cmd.arg("-fsanitize=fuzzer");
for sanitizer in sanitizers {
cmd.arg(sanitizer.flag());
}
for flag in coverage.compiler_flags() {
cmd.arg(flag);
}
cmd.arg("-O1");
cmd.arg("-g");
cmd.arg("-fno-omit-frame-pointer");
for flag in extra_flags {
cmd.arg(flag);
}
cmd.arg("-o").arg(output);
cmd.arg(source);
let result = cmd
.output()
.map_err(|e| format!("Failed to compile fuzz target: {}", e))?;
if !result.status.success() {
return Err(String::from_utf8_lossy(&result.stderr).to_string());
}
Ok(output.to_path_buf())
}
pub fn generate_libfuzzer_target(function_name: &str, body: &str) -> String {
format!(
r#"
#include <cstdint>
#include <cstddef>
#include <cstring>
// Fuzz target: {}
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {{
if (size < 4) return 0; // Need at least 4 bytes
{}
return 0;
}}
// Optional initialization
extern "C" int LLVMFuzzerInitialize(int *argc, char ***argv) {{
return 0;
}}
"#,
function_name, body
)
}
pub fn generate_fuzzed_data_provider_target(name: &str, protocol: &str) -> String {
format!(
r#"
#include <cstdint>
#include <cstddef>
#include <string>
#include <vector>
#include <fuzzer/FuzzedDataProvider.h>
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {{
FuzzedDataProvider provider(data, size);
// Auto-generated for protocol: {}
// Consume data based on the protocol
std::string str_val = provider.ConsumeRandomLengthString(size);
int int_val = provider.ConsumeIntegral<int>();
bool bool_val = provider.ConsumeBool();
double double_val = provider.ConsumeFloatingPoint<double>();
// Fuzz the target protocol handler
{}
return 0;
}}
"#,
name, protocol
)
}
pub fn run_libfuzzer(config: &FuzzingConfig) -> Result<FuzzingResult, String> {
let start = Instant::now();
let start_time = SystemTime::now();
let mut cmd = ProcessCommand::new(&config.target);
cmd.arg(&config.corpus_dir);
cmd.arg(&format!(
"-artifact_prefix={}/",
config.artifact_dir.display()
));
if let Some(max_len) = config.max_len {
cmd.arg(&format!("-max_len={}", max_len));
}
if let Some(timeout) = config.max_total_time {
cmd.arg(&format!("-max_total_time={}", timeout.as_secs()));
}
if let Some(runs) = config.runs {
cmd.arg(&format!("-runs={}", runs));
}
cmd.arg(&format!("-jobs={}", config.jobs));
cmd.arg(&format!("-workers={}", config.workers));
cmd.arg(&format!("-timeout={}", config.timeout_ms / 1000));
cmd.arg(&format!("-rss_limit_mb={}", config.rss_limit_mb));
if let Some(ref dict) = config.dict {
cmd.arg(&format!("-dict={}", dict.display()));
}
if let Some(seed) = config.seed {
cmd.arg(&format!("-seed={}", seed));
}
if config.reduce_inputs {
cmd.arg("-reduce_inputs=1");
}
if let Some(fork_mode) = config.fork_mode {
match fork_mode {
ForkMode::Persistent => {
cmd.arg("-fork=1");
}
ForkMode::InProcess => {
}
ForkMode::ForkServer => {
cmd.arg("-fork=1");
cmd.arg("-ignore_ooms=1");
cmd.arg("-ignore_timeouts=1");
cmd.arg("-ignore_crashes=1");
}
ForkMode::Timeout { per_input_ms } => {
cmd.arg(&format!("-timeout={}", per_input_ms / 1000));
}
}
}
for arg in &config.extra_args {
cmd.arg(arg);
}
for (key, value) in &config.env_vars {
cmd.env(key, value);
}
let output = cmd
.output()
.map_err(|e| format!("Failed to run fuzzer: {}", e))?;
let mut result = FuzzingResult {
engine: FuzzingEngine::LibFuzzer,
start_time,
end_time: SystemTime::now(),
total_executions: 0,
execs_per_second: 0.0,
crashes: Vec::new(),
hangs: Vec::new(),
ooms: Vec::new(),
coverage_achieved: None,
corpus_size: 0,
corpus_elements: 0,
peak_rss_mb: 0,
features: Vec::new(),
stats: HashMap::new(),
};
let stdout = String::from_utf8_lossy(&output.stdout);
result.total_executions = parse_libfuzzer_execs(&stdout);
result.execs_per_second = parse_libfuzzer_execs_per_sec(&stdout);
let elapsed = start.elapsed();
if elapsed.as_secs() > 0 {
result.execs_per_second = result.total_executions as f64 / elapsed.as_secs_f64();
}
Ok(result)
}
fn parse_libfuzzer_execs(output: &str) -> u64 {
for line in output.lines() {
if let Some(hash_pos) = line.find('#') {
let rest = &line[hash_pos + 1..];
if let Some(tab_pos) = rest.find('\t') {
return rest[..tab_pos].parse().unwrap_or(0);
}
}
}
0
}
fn parse_libfuzzer_execs_per_sec(output: &str) -> f64 {
for line in output.lines() {
if line.contains("exec/s") {
let parts: Vec<&str> = line.split_whitespace().collect();
for (i, part) in parts.iter().enumerate() {
if *part == "exec/s:" && i + 1 < parts.len() {
return parts[i + 1].parse().unwrap_or(0.0);
}
}
}
}
0.0
}
#[derive(Debug, Clone)]
pub struct AflContext {
pub input_dir: PathBuf,
pub output_dir: PathBuf,
pub master_fuzzer: bool,
pub slave_id: Option<String>,
pub power_schedule: Option<AflPowerSchedule>,
pub use_cmplog: bool,
pub use_comparison_coverage: bool,
pub deterministic_only: bool,
pub skip_deterministic: bool,
}
impl Default for AflContext {
fn default() -> Self {
Self {
input_dir: PathBuf::from("./in"),
output_dir: PathBuf::from("./out"),
master_fuzzer: true,
slave_id: None,
power_schedule: Some(AflPowerSchedule::Fast),
use_cmplog: true,
use_comparison_coverage: true,
deterministic_only: false,
skip_deterministic: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AflPowerSchedule {
Explore,
Exploit,
Fast,
Coeff,
Lin,
Quad,
Rare,
MMopt,
Seek,
}
impl AflPowerSchedule {
pub fn as_arg(&self) -> &'static str {
match self {
Self::Explore => "explore",
Self::Exploit => "exploit",
Self::Fast => "fast",
Self::Coeff => "coeff",
Self::Lin => "lin",
Self::Quad => "quad",
Self::Rare => "rare",
Self::MMopt => "mmopt",
Self::Seek => "seek",
}
}
}
pub fn compile_afl_target(
source: &Path,
output: &Path,
use_asan: bool,
use_ubsan: bool,
extra_flags: &[&str],
) -> Result<PathBuf, String> {
let compiler = if std::path::Path::new("/usr/local/bin/afl-clang-fast").exists() {
"/usr/local/bin/afl-clang-fast"
} else {
"afl-clang-fast"
};
let mut cmd = ProcessCommand::new(compiler);
cmd.arg("-O2");
cmd.arg("-g");
cmd.arg("-fno-omit-frame-pointer");
if use_asan {
cmd.arg("-fsanitize=address");
}
if use_ubsan {
cmd.arg("-fsanitize=undefined");
}
for flag in extra_flags {
cmd.arg(flag);
}
cmd.arg("-o").arg(output);
cmd.arg(source);
let result = cmd
.output()
.map_err(|e| format!("Failed to compile AFL target: {}", e))?;
if !result.status.success() {
return Err(String::from_utf8_lossy(&result.stderr).to_string());
}
Ok(output.to_path_buf())
}
pub fn generate_afl_target(name: &str, body: &str) -> String {
format!(
r#"
#include <stdint.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
// Fuzz target for AFL: {}
int main(int argc, char **argv) {{
// AFL persistent mode / fork server setup
#ifdef __AFL_HAVE_MANUAL_CONTROL
__AFL_INIT();
#endif
unsigned char buf[65536];
ssize_t len;
while (__AFL_LOOP(10000)) {{
// Read input
len = read(0, buf, sizeof(buf));
if (len < 0) break;
{}
}}
return 0;
}}
"#,
name, body
)
}
pub fn run_afl(config: &AflContext, target: &Path) -> Result<FuzzingResult, String> {
let mut cmd = ProcessCommand::new("afl-fuzz");
cmd.arg("-i").arg(&config.input_dir);
cmd.arg("-o").arg(&config.output_dir);
if config.master_fuzzer {
cmd.arg("-M").arg("fuzzer01");
} else if let Some(ref slave_id) = config.slave_id {
cmd.arg("-S").arg(slave_id);
}
if let Some(ref schedule) = config.power_schedule {
cmd.arg("-p").arg(schedule.as_arg());
}
if config.use_cmplog {
cmd.arg("-c").arg("0");
}
if config.use_comparison_coverage {
cmd.arg("-l").arg("2");
}
if config.deterministic_only {
cmd.arg("-d");
}
if config.skip_deterministic {
cmd.arg("-D");
}
cmd.arg(target);
cmd.arg("@@");
let _output = cmd.output();
Ok(FuzzingResult::default())
}
#[derive(Debug, Clone)]
pub struct AflPlusPlusFeatures {
pub cmplog_enabled: bool,
pub laf_intel_enabled: bool,
pub redqueen_enabled: bool,
pub instrim_enabled: bool,
pub nyx_mode: bool,
pub unicorn_mode: bool,
}
impl Default for AflPlusPlusFeatures {
fn default() -> Self {
Self {
cmplog_enabled: true,
laf_intel_enabled: true,
redqueen_enabled: true,
instrim_enabled: false,
nyx_mode: false,
unicorn_mode: false,
}
}
}
pub fn compile_aflpp_target(
source: &Path,
output: &Path,
features: &AflPlusPlusFeatures,
extra_flags: &[&str],
) -> Result<PathBuf, String> {
let mut cmd = ProcessCommand::new("afl-clang-fast");
cmd.arg("-O2").arg("-g");
if features.laf_intel_enabled {
cmd.env("AFL_LLVM_LAF_ALL", "1");
}
if features.cmplog_enabled {
cmd.env("AFL_LLVM_CMPLOG", "1");
}
if features.redqueen_enabled {
cmd.env("AFL_LLVM_REDQUEEN", "1");
}
if features.instrim_enabled {
cmd.env("AFL_LLVM_INSTRIM", "1");
cmd.env("AFL_LLVM_INSTRIM_LOOPHEAD", "1");
}
for flag in extra_flags {
cmd.arg(flag);
}
cmd.arg("-o").arg(output);
cmd.arg(source);
let result = cmd
.output()
.map_err(|e| format!("Failed to compile AFL++ target: {}", e))?;
if !result.status.success() {
return Err(String::from_utf8_lossy(&result.stderr).to_string());
}
Ok(output.to_path_buf())
}
#[derive(Debug, Clone)]
pub struct HonggfuzzContext {
pub input_dir: PathBuf,
pub output_dir: PathBuf,
pub threads: u32,
pub timeout: u32,
pub mutation_rate: Option<f64>,
pub dictionary: Option<PathBuf>,
pub asan: bool,
pub ubsan: bool,
pub msan: bool,
pub linux_perf_bts_edge: bool,
pub linux_perf_ipt_block: bool,
}
impl Default for HonggfuzzContext {
fn default() -> Self {
Self {
input_dir: PathBuf::from("./in"),
output_dir: PathBuf::from("./out"),
threads: 4,
timeout: 10,
mutation_rate: None,
dictionary: None,
asan: true,
ubsan: true,
msan: false,
linux_perf_bts_edge: false,
linux_perf_ipt_block: false,
}
}
}
pub fn compile_honggfuzz_target(
source: &Path,
output: &Path,
ctx: &HonggfuzzContext,
extra_flags: &[&str],
) -> Result<PathBuf, String> {
let mut cmd = ProcessCommand::new("hfuzz-clang++");
cmd.arg("-O2");
cmd.arg("-g");
cmd.arg("-fno-omit-frame-pointer");
cmd.arg("-fno-optimize-sibling-calls");
cmd.arg("-fsanitize-coverage=trace-pc-guard,indirect-calls,trace-cmp");
if ctx.asan {
cmd.arg("-fsanitize=address");
}
if ctx.ubsan {
cmd.arg("-fsanitize=undefined");
}
if ctx.msan {
cmd.arg("-fsanitize=memory");
}
for flag in extra_flags {
cmd.arg(flag);
}
cmd.arg("-o").arg(output);
cmd.arg(source);
let result = cmd
.output()
.map_err(|e| format!("Failed to compile Honggfuzz target: {}", e))?;
if !result.status.success() {
return Err(String::from_utf8_lossy(&result.stderr).to_string());
}
Ok(output.to_path_buf())
}
pub fn run_honggfuzz(config: &HonggfuzzContext, target: &Path) -> Result<FuzzingResult, String> {
let mut cmd = ProcessCommand::new("honggfuzz");
cmd.arg("-f").arg(&config.input_dir);
cmd.arg("-W").arg(&config.output_dir);
cmd.arg(format!("-n{}", config.threads));
cmd.arg(format!("-t{}", config.timeout));
if let Some(ref dict) = config.dictionary {
cmd.arg("-w").arg(dict);
}
if config.linux_perf_bts_edge {
cmd.arg("--linux_perf_bts_edge");
}
if config.linux_perf_ipt_block {
cmd.arg("--linux_perf_ipt_block");
}
cmd.arg("--").arg(target);
cmd.arg("___FILE___");
let _output = cmd.output();
Ok(FuzzingResult::default())
}
pub fn generate_honggfuzz_target(name: &str, body: &str) -> String {
format!(
r#"
#include <stdint.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
// Honggfuzz target: {}
// Usage: hfuzz-clang++ -o fuzz_target fuzz_target.cpp
// honggfuzz -f input_corpus -W output -- ./fuzz_target
extern int HonggfuzzNetDriver_main(int argc, char **argv);
int HonggfuzzNetDriver_main(int argc, char **argv) {{
if (argc != 2) {{
fprintf(stderr, "Usage: %s <input_file>\n", argv[0]);
return 1;
}}
size_t len;
char *buf;
FILE *f = fopen(argv[1], "rb");
if (!f) return 1;
fseek(f, 0, SEEK_END);
len = ftell(f);
fseek(f, 0, SEEK_SET);
buf = (char *)malloc(len);
fread(buf, 1, len, f);
fclose(f);
{}
free(buf);
return 0;
}}
"#,
name, body
)
}
#[derive(Debug, Clone)]
pub struct OssFuzzProject {
pub name: String,
pub primary_contact: String,
pub auto_ccs: Vec<String>,
pub vendor_ccs: Vec<String>,
pub language: String,
pub fuzzing_engines: Vec<FuzzingEngine>,
pub sanitizers: Vec<Sanitizer>,
pub architectures: Vec<String>,
pub main_repo: String,
pub file_github_issue: bool,
pub coverage_extra_args: Vec<String>,
pub help_url: Option<String>,
}
pub fn generate_ossfuzz_build_sh(project: &OssFuzzProject, fuzz_targets: &[&str]) -> String {
let mut script = String::new();
script.push_str("#!/bin/bash -eu\n");
script.push_str("# OSS-Fuzz build.sh for ");
script.push_str(&project.name);
script.push_str("\n\n");
script.push_str("# Build the project\n");
script.push_str("cmake -B build -G Ninja \\\n");
script.push_str(" -DCMAKE_BUILD_TYPE=Release \\\n");
script.push_str(" -DCMAKE_C_COMPILER=clang \\\n");
script.push_str(" -DCMAKE_CXX_COMPILER=clang++ \\\n");
script.push_str(" -DCMAKE_C_FLAGS=\"$CFLAGS\" \\\n");
script.push_str(" -DCMAKE_CXX_FLAGS=\"$CXXFLAGS\" \\\n");
script.push_str(" -DCMAKE_EXE_LINKER_FLAGS=\"$CXXFLAGS\"\n");
script.push_str("cmake --build build\n\n");
script.push_str("# Build fuzz targets\n");
for target in fuzz_targets {
script.push_str(&format!(
r#"
$CXX $CXXFLAGS -std=c++17 \
-I src/include \
-c fuzz/fuzz_{target}.cpp \
-o fuzz_{target}.o
$CXX $CXXFLAGS -std=c++17 \
fuzz_{target}.o \
$LIB_FUZZING_ENGINE \
build/libmyproject.a \
-o $OUT/fuzz_{target}
"#,
target = target
));
}
script.push_str("\n# Copy any seed corpora\n");
script.push_str("cp -r fuzz/corpora/*.zip $OUT/\n");
script
}
pub fn generate_ossfuzz_dockerfile(project: &OssFuzzProject) -> String {
format!(
r#"# Copyright {year} Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
FROM gcr.io/oss-fuzz-base/base-builder
# Install dependencies
RUN apt-get update && apt-get install -y \
cmake \
ninja-build \
git \
build-essential
# Clone the project
RUN git clone {repo} {name}
# Copy seed corpora
COPY *.zip $SRC/
WORKDIR {name}
COPY build.sh $SRC/
"#,
year = 2024,
name = project.name,
repo = project.main_repo,
)
}
pub fn generate_ossfuzz_project_yaml(project: &OssFuzzProject) -> String {
format!(
r#"homepage: "{homepage}"
primary_contact: "{contact}"
language: c++
fuzzing_engines:
- libfuzzer
- afl
- honggfuzz
sanitizers:
- address
- undefined
- memory
architectures:
- x86_64
- i386
main_repo: '{repo}'
auto_ccs:
{auto_ccs}
vendor_ccs:
{vendor_ccs}
coverage_extra_args: -ignore-filename-regex=.*\\.c
"#,
homepage = format!("https://github.com/ossfuzz/{}", project.name),
contact = project.primary_contact,
repo = project.main_repo,
auto_ccs = project
.auto_ccs
.iter()
.map(|c| format!(" - \"{}\"", c))
.collect::<Vec<_>>()
.join("\n"),
vendor_ccs = project
.vendor_ccs
.iter()
.map(|c| format!(" - \"{}\"", c))
.collect::<Vec<_>>()
.join("\n"),
)
}
#[derive(Debug, Clone)]
pub struct CorpusManager {
pub corpus_dir: PathBuf,
pub backup_dir: Option<PathBuf>,
pub max_size: usize,
pub min_input_size: usize,
}
impl Default for CorpusManager {
fn default() -> Self {
Self {
corpus_dir: PathBuf::from("./corpus"),
backup_dir: None,
max_size: 1_048_576, min_input_size: 1,
}
}
}
impl CorpusManager {
pub fn init_corpus(&self) -> io::Result<()> {
std::fs::create_dir_all(&self.corpus_dir)?;
Ok(())
}
pub fn list_corpus(&self) -> io::Result<Vec<PathBuf>> {
let mut entries = Vec::new();
if self.corpus_dir.exists() {
for entry in std::fs::read_dir(&self.corpus_dir)? {
let entry = entry?;
if entry.file_type()?.is_file() {
entries.push(entry.path());
}
}
}
Ok(entries)
}
pub fn corpus_stats(&self) -> io::Result<(usize, u64)> {
let entries = self.list_corpus()?;
let count = entries.len();
let total_size: u64 = entries
.iter()
.filter_map(|p| std::fs::metadata(p).ok())
.map(|m| m.len())
.sum();
Ok((count, total_size))
}
pub fn minimize_corpus(&self, fuzz_target: &Path) -> Result<usize, String> {
let mut cmd = ProcessCommand::new(fuzz_target);
cmd.arg("-merge=1");
cmd.arg(&self.corpus_dir);
cmd.arg(&self.corpus_dir);
let output = cmd
.output()
.map_err(|e| format!("Failed to minimize corpus: {}", e))?;
if !output.status.success() {
return Err(String::from_utf8_lossy(&output.stderr).to_string());
}
let stdout = String::from_utf8_lossy(&output.stdout);
let count = stdout.lines().filter(|l| l.contains("MERGE-OUTER")).count();
Ok(count)
}
pub fn merge_corpora(&self, fuzz_target: &Path, other_corpus: &Path) -> Result<usize, String> {
let mut cmd = ProcessCommand::new(fuzz_target);
cmd.arg("-merge=1");
cmd.arg(&self.corpus_dir);
cmd.arg(other_corpus);
let output = cmd
.output()
.map_err(|e| format!("Failed to merge corpora: {}", e))?;
if !output.status.success() {
return Err(String::from_utf8_lossy(&output.stderr).to_string());
}
let count = String::from_utf8_lossy(&output.stdout)
.lines()
.filter(|l| l.contains("MERGE-OUTER"))
.count();
Ok(count)
}
pub fn regression_test(
&self,
fuzz_target: &Path,
timeout_per_input_ms: u32,
) -> Result<Vec<CrashInfo>, String> {
let mut crashes = Vec::new();
let entries = self.list_corpus().map_err(|e| e.to_string())?;
for entry in &entries {
let mut cmd = ProcessCommand::new(fuzz_target);
cmd.arg(&format!("-timeout={}", timeout_per_input_ms / 1000));
cmd.arg(entry);
let output = cmd.output().map_err(|e| format!("Failed to run: {}", e))?;
if !output.status.success() {
let crash = CrashInfo {
id: format!(
"regression-{}",
entry.file_name().unwrap_or_default().to_string_lossy()
),
input_file: entry.clone(),
crash_type: CrashType::from_sanitizer_output(&String::from_utf8_lossy(
&output.stderr,
))
.unwrap_or(CrashType::Abort),
signal: output.status.code(),
sanitizer_report: Some(String::from_utf8_lossy(&output.stderr).to_string()),
stack_trace: Vec::new(),
minimized: false,
minimized_input: None,
reproducible: true,
unique_hash: 0,
timestamp: SystemTime::now(),
asan_output: None,
ubsan_output: None,
};
crashes.push(crash);
}
}
Ok(crashes)
}
pub fn deduplicate_corpus(&self) -> io::Result<usize> {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut seen: HashMap<u64, PathBuf> = HashMap::new();
let mut removed = 0usize;
let entries = self.list_corpus()?;
for entry in &entries {
if let Ok(data) = std::fs::read(entry) {
let mut hasher = DefaultHasher::new();
data.hash(&mut hasher);
let hash = hasher.finish();
if seen.contains_key(&hash) {
std::fs::remove_file(entry)?;
removed += 1;
} else {
seen.insert(hash, entry.clone());
}
}
}
Ok(removed)
}
pub fn backup_corpus(&self) -> io::Result<PathBuf> {
let backup_dir = self
.backup_dir
.clone()
.unwrap_or_else(|| PathBuf::from("./corpus_backup"));
std::fs::create_dir_all(&backup_dir)?;
for entry in self.list_corpus()? {
let filename = entry.file_name().unwrap();
let dest = backup_dir.join(filename);
std::fs::copy(&entry, &dest)?;
}
Ok(backup_dir)
}
}
#[derive(Debug, Clone)]
pub struct CrashTriage {
pub crash_dir: PathBuf,
pub reports_dir: PathBuf,
pub symbolize: bool,
pub symbol_path: Option<PathBuf>,
pub asan_options: String,
pub ubsan_options: String,
pub msan_options: String,
pub lsan_options: String,
}
impl Default for CrashTriage {
fn default() -> Self {
Self {
crash_dir: PathBuf::from("./artifacts"),
reports_dir: PathBuf::from("./reports"),
symbolize: true,
symbol_path: None,
asan_options: "abort_on_error=1:detect_leaks=1:symbolize=1".to_string(),
ubsan_options: "print_stacktrace=1:symbolize=1".to_string(),
msan_options: "poison_in_dtor=1".to_string(),
lsan_options: "report_objects=1".to_string(),
}
}
}
impl CrashTriage {
pub fn minimize_crash(
&self,
fuzz_target: &Path,
crash_input: &Path,
) -> Result<PathBuf, String> {
let minimized = self.crash_dir.join(format!(
"minimized-{}",
crash_input
.file_name()
.unwrap_or_default()
.to_string_lossy()
));
let mut cmd = ProcessCommand::new(fuzz_target);
cmd.arg("-minimize_crash=1");
cmd.arg(&format!("-exact_artifact_path={}", minimized.display()));
cmd.arg(crash_input);
let output = cmd
.output()
.map_err(|e| format!("Minimization failed: {}", e))?;
if !output.status.success() {
return Err(String::from_utf8_lossy(&output.stderr).to_string());
}
if !minimized.exists() {
return Err("Minimized file not created".to_string());
}
Ok(minimized)
}
pub fn generate_crash_report(
&self,
crash: &CrashInfo,
fuzz_target: &Path,
) -> Result<PathBuf, String> {
std::fs::create_dir_all(&self.reports_dir).map_err(|e| e.to_string())?;
let report_path = self.reports_dir.join(format!(
"crash-report-{}.md",
chrono_now().unwrap_or("unknown".to_string())
));
let mut report = String::new();
report.push_str(&format!("# Crash Report: {}\n\n", crash.id));
report.push_str(&format!(
"- **Crash type**: {}\n",
crash.crash_type.description()
));
report.push_str(&format!("- **Timestamp**: {:?}\n", crash.timestamp));
report.push_str(&format!(
"- **Input file**: {}\n",
crash.input_file.display()
));
report.push_str(&format!("- **Signal**: {:?}\n", crash.signal));
report.push_str(&format!("- **Reproducible**: {}\n", crash.reproducible));
report.push_str(&format!("- **Minimized**: {}\n\n", crash.minimized));
report.push_str("## Reproduction\n\n");
report.push_str(&format!(
"```bash\n{} < {}\n```\n\n",
fuzz_target.display(),
crash.input_file.display()
));
report.push_str("## Stack Trace\n\n");
report.push_str("```\n");
for frame in &crash.stack_trace {
report.push_str(&format!(
" #{} {} at {}:{}\n",
frame.index,
frame.function_name,
frame.file.as_deref().unwrap_or("??"),
frame.line.unwrap_or(0),
));
}
report.push_str("```\n\n");
if let Some(ref asan) = crash.asan_output {
report.push_str("## AddressSanitizer Output\n\n");
report.push_str("```\n");
report.push_str(asan);
report.push_str("\n```\n\n");
}
if let Some(ref ubsan) = crash.ubsan_output {
report.push_str("## UndefinedBehaviorSanitizer Output\n\n");
report.push_str("```\n");
report.push_str(ubsan);
report.push_str("\n```\n\n");
}
if let Some(ref sanitizer) = crash.sanitizer_report {
report.push_str("## Sanitizer Report\n\n");
report.push_str("```\n");
report.push_str(sanitizer);
report.push_str("\n```\n");
}
std::fs::write(&report_path, &report).map_err(|e| e.to_string())?;
Ok(report_path)
}
pub fn symbolicate_stacktrace(
&self,
binary_path: &Path,
addresses: &[u64],
) -> Result<Vec<StackFrame>, String> {
let mut frames = Vec::new();
for (i, addr) in addresses.iter().enumerate() {
let mut cmd = ProcessCommand::new("llvm-symbolizer");
cmd.arg(format!("--obj={}", binary_path.display()));
cmd.arg(format!("--output-style=LLVM"));
cmd.arg(format!("{:#x}", addr));
let output = cmd.output().map_err(|e| e.to_string())?;
let stdout = String::from_utf8_lossy(&output.stdout);
let mut frame = StackFrame {
index: i,
function_name: String::new(),
file: None,
line: None,
column: None,
module: Some(binary_path.display().to_string()),
address: Some(*addr),
inlined: false,
symbolized: false,
};
for line in stdout.lines() {
if let Some(pos) = line.find(':') {
let path_part = &line[..pos];
let rest = &line[pos + 1..];
if let Some(next_colon) = rest.find(':') {
frame.file = Some(path_part.to_string());
frame.line = rest[..next_colon].parse().ok();
frame.column = rest[next_colon + 1..].parse().ok();
frame.symbolized = true;
}
}
}
if frame.symbolized {
frames.push(frame);
}
}
Ok(frames)
}
pub fn classify_crashes(crashes: &[CrashInfo]) -> HashMap<CrashType, Vec<&CrashInfo>> {
let mut classified: HashMap<CrashType, Vec<&CrashInfo>> = HashMap::new();
for crash in crashes {
classified.entry(crash.crash_type).or_default().push(crash);
}
classified
}
pub fn crash_summary(crashes: &[CrashInfo]) -> String {
let classified = Self::classify_crashes(crashes);
let mut summary = String::new();
summary.push_str(&format!(
"# Fuzzing Crash Summary\n\nTotal crashes: {}\n\n",
crashes.len()
));
for (crash_type, crashes_of_type) in &classified {
summary.push_str(&format!(
"- **{}**: {} crash(es)\n",
crash_type.description(),
crashes_of_type.len()
));
}
summary.push_str("\n## Details\n\n");
for crash in crashes {
summary.push_str(&format!(
"- `{}` — {} — {}\n",
crash.id,
crash.crash_type.description(),
crash.input_file.display()
));
}
summary
}
}
#[derive(Debug, Clone)]
pub struct ProtobufMutatorFuzzer {
pub proto_file: PathBuf,
pub proto_message: String,
pub fuzz_target_function: String,
pub use_lpm: bool,
pub custom_mutator: bool,
}
impl ProtobufMutatorFuzzer {
pub fn generate_target(&self) -> String {
format!(
r#"
#include <libprotobuf-mutator/src/libfuzzer/libfuzzer_macro.h>
#include "{proto_header}"
// Protobuf-based fuzzer for {message}
DEFINE_PROTO_FUZZER(const {message}& input) {{
// TODO: Fill in with actual fuzzing logic
{body}
}}
"#,
proto_header = self
.proto_file
.file_stem()
.map(|s| format!("{}.pb.h", s.to_string_lossy()))
.unwrap_or_else(|| "input.pb.h".to_string()),
message = self.proto_message,
body = self.fuzz_target_function,
)
}
pub fn compile(
&self,
source: &Path,
output: &Path,
sanitizers: &[Sanitizer],
) -> Result<PathBuf, String> {
let mut cmd = ProcessCommand::new("clang++");
cmd.arg("-fsanitize=fuzzer");
for sanitizer in sanitizers {
cmd.arg(sanitizer.flag());
}
cmd.arg("-O1").arg("-g");
cmd.arg("-lprotobuf-mutator-libfuzzer");
cmd.arg("-lprotobuf-mutator");
cmd.arg("-lprotobuf");
cmd.arg("-o").arg(output);
cmd.arg(source);
let result = cmd
.output()
.map_err(|e| format!("Failed to compile: {}", e))?;
if !result.status.success() {
return Err(String::from_utf8_lossy(&result.stderr).to_string());
}
Ok(output.to_path_buf())
}
}
#[derive(Debug, Clone)]
pub struct FuzzingPipeline {
pub name: String,
pub stages: Vec<FuzzingStage>,
pub config: FuzzingConfig,
pub results: Vec<FuzzingResult>,
pub crash_triage: CrashTriage,
pub corpus_manager: CorpusManager,
}
#[derive(Debug, Clone)]
pub enum FuzzingStage {
Build,
CompileTargets,
SeedCorpus,
Fuzz { duration: Duration },
MinimizeCorpus,
MergeCorpora,
RegressionTest,
CrashTriage,
CoverageReport,
Cleanup,
}
impl FuzzingPipeline {
pub fn new(name: &str, config: FuzzingConfig) -> Self {
Self {
name: name.to_string(),
stages: vec![
FuzzingStage::CompileTargets,
FuzzingStage::SeedCorpus,
FuzzingStage::Fuzz {
duration: Duration::from_secs(3600),
},
FuzzingStage::MinimizeCorpus,
FuzzingStage::RegressionTest,
FuzzingStage::CrashTriage,
],
config,
results: Vec::new(),
crash_triage: CrashTriage::default(),
corpus_manager: CorpusManager::default(),
}
}
pub fn run(&mut self) -> Result<Vec<FuzzingResult>, String> {
for _stage in &self.stages {
if let Ok(result) = run_libfuzzer(&self.config) {
if !result.crashes.is_empty() {
for crash in &result.crashes {
let _ = self
.crash_triage
.generate_crash_report(crash, &self.config.target);
}
}
self.results.push(result);
}
}
Ok(self.results.clone())
}
pub fn run_regression(&self) -> Result<Vec<CrashInfo>, String> {
self.corpus_manager
.regression_test(&self.config.target, self.config.timeout_ms)
}
}
fn chrono_now() -> Option<String> {
if let Ok(dur) = SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
let secs = dur.as_secs();
let days = secs / 86400;
let hours = (secs % 86400) / 3600;
let minutes = (secs % 3600) / 60;
let seconds = secs % 60;
Some(format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
1970 + (days / 365),
((days % 365) / 30) + 1,
(days % 30) + 1,
hours,
minutes,
seconds
))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fuzzing_engine_names() {
assert_eq!(FuzzingEngine::LibFuzzer.name(), "libFuzzer");
assert_eq!(FuzzingEngine::AFL.name(), "AFL");
assert_eq!(FuzzingEngine::Honggfuzz.name(), "Honggfuzz");
}
#[test]
fn test_sanitizer_flags() {
assert_eq!(Sanitizer::Address.flag(), "-fsanitize=address");
assert_eq!(Sanitizer::Undefined.flag(), "-fsanitize=undefined");
assert_eq!(Sanitizer::Memory.flag(), "-fsanitize=memory");
}
#[test]
fn test_sanitizer_descriptions() {
assert!(Sanitizer::Address
.description()
.contains("AddressSanitizer"));
assert!(Sanitizer::Undefined
.description()
.contains("UndefinedBehaviorSanitizer"));
}
#[test]
fn test_coverage_type_compiler_flags() {
let flags = CoverageType::Edge.compiler_flags();
assert!(flags.contains(&"-fprofile-instr-generate"));
assert!(flags.contains(&"-fcoverage-mapping"));
let no_flags = CoverageType::None.compiler_flags();
assert!(no_flags.is_empty());
}
#[test]
fn test_coverage_libfuzzer_flags() {
assert_eq!(CoverageType::Edge.libfuzzer_coverage(), "edge");
assert_eq!(
CoverageType::FullCoverage.libfuzzer_coverage(),
"trace-pc-guard,trace-cmp,trace-div,trace-gep"
);
}
#[test]
fn test_crash_type_classification() {
assert_eq!(
CrashType::from_sanitizer_output("ERROR: AddressSanitizer: heap-buffer-overflow"),
Some(CrashType::HeapBufferOverflow)
);
assert_eq!(
CrashType::from_sanitizer_output(
"==12345==ERROR: LeakSanitizer: detected memory leaks"
),
Some(CrashType::MemoryLeak)
);
assert_eq!(
CrashType::from_sanitizer_output("Use-after-free detected"),
Some(CrashType::UseAfterFree)
);
assert_eq!(CrashType::from_sanitizer_output("No error here"), None);
}
#[test]
fn test_crash_type_descriptions() {
assert!(CrashType::Segfault.description().contains("SIGSEGV"));
assert!(CrashType::HeapBufferOverflow
.description()
.contains("Heap buffer"));
assert!(CrashType::DoubleFree.description().contains("Double free"));
}
#[test]
fn test_libfuzzer_target_generation() {
let target = generate_libfuzzer_target("parse_data", "parse_buffer(data, size);");
assert!(target.contains("LLVMFuzzerTestOneInput"));
assert!(target.contains("parse_buffer"));
assert!(target.contains("LLVMFuzzerInitialize"));
}
#[test]
fn test_fuzzed_data_provider_target() {
let target = generate_fuzzed_data_provider_target("test", "HTTP");
assert!(target.contains("FuzzedDataProvider"));
assert!(target.contains("ConsumeRandomLengthString"));
assert!(target.contains("ConsumeIntegral"));
}
#[test]
fn test_afl_target_generation() {
let target = generate_afl_target("http_parse", "parse_http(buf, len);");
assert!(target.contains("__AFL_LOOP"));
assert!(target.contains("__AFL_INIT"));
assert!(target.contains("parse_http"));
}
#[test]
fn test_honggfuzz_target_generation() {
let target = generate_honggfuzz_target("json_parse", "parse_json(buf, len);");
assert!(target.contains("HonggfuzzNetDriver_main"));
assert!(target.contains("parse_json"));
assert!(target.contains("fopen"));
}
#[test]
fn test_afl_power_schedules() {
assert_eq!(AflPowerSchedule::Explore.as_arg(), "explore");
assert_eq!(AflPowerSchedule::Fast.as_arg(), "fast");
assert_eq!(AflPowerSchedule::Seek.as_arg(), "seek");
}
#[test]
fn test_fork_mode() {
assert_eq!(ForkMode::Persistent, ForkMode::Persistent);
assert_ne!(ForkMode::InProcess, ForkMode::ForkServer);
}
#[test]
fn test_parse_libfuzzer_execs() {
let output = "#12345\tNEW cov: 12345 ft: 5432 corp: 123 exec/s: 1000 rss: 100Mb";
let execs = parse_libfuzzer_execs(output);
assert_eq!(execs, 12345);
}
#[test]
fn test_parse_libfuzzer_execs_per_sec() {
let output = "exec/s: 500.5 total: 1000";
let speed = parse_libfuzzer_execs_per_sec(output);
assert_eq!(speed, 500.5);
}
#[test]
fn test_corpus_manager_init() {
let tmp = std::env::temp_dir().join("test_corpus_init");
let mgr = CorpusManager {
corpus_dir: tmp.clone(),
backup_dir: None,
max_size: 1_048_576,
min_input_size: 1,
};
let result = mgr.init_corpus();
assert!(result.is_ok());
std::fs::remove_dir_all(&tmp).ok();
}
#[test]
fn test_corpus_manager_stats() {
let tmp = std::env::temp_dir().join("test_corpus_stats");
std::fs::create_dir_all(&tmp).unwrap();
std::fs::write(tmp.join("input1"), b"AAAA").unwrap();
std::fs::write(tmp.join("input2"), b"BBBBBBBB").unwrap();
let mgr = CorpusManager {
corpus_dir: tmp.clone(),
backup_dir: None,
max_size: 1_048_576,
min_input_size: 1,
};
let (count, size) = mgr.corpus_stats().unwrap();
assert_eq!(count, 2);
assert_eq!(size, 12);
std::fs::remove_dir_all(&tmp).ok();
}
#[test]
fn test_corpus_manager_deduplicate() {
let tmp = std::env::temp_dir().join("test_corpus_dedup");
std::fs::create_dir_all(&tmp).unwrap();
std::fs::write(tmp.join("input1"), b"AAAA").unwrap();
std::fs::write(tmp.join("input2"), b"AAAA").unwrap(); std::fs::write(tmp.join("input3"), b"BBBB").unwrap();
let mgr = CorpusManager {
corpus_dir: tmp.clone(),
backup_dir: None,
max_size: 1_048_576,
min_input_size: 1,
};
let removed = mgr.deduplicate_corpus().unwrap();
assert_eq!(removed, 1);
let (count, _) = mgr.corpus_stats().unwrap();
assert_eq!(count, 2);
std::fs::remove_dir_all(&tmp).ok();
}
#[test]
fn test_crash_triage_classify() {
let crashes = vec![
CrashInfo {
id: "1".to_string(),
input_file: PathBuf::from("crash1"),
crash_type: CrashType::HeapBufferOverflow,
signal: Some(11),
sanitizer_report: None,
stack_trace: Vec::new(),
minimized: false,
minimized_input: None,
reproducible: true,
unique_hash: 0,
timestamp: SystemTime::now(),
asan_output: None,
ubsan_output: None,
},
CrashInfo {
id: "2".to_string(),
input_file: PathBuf::from("crash2"),
crash_type: CrashType::HeapBufferOverflow,
signal: Some(6),
sanitizer_report: None,
stack_trace: Vec::new(),
minimized: false,
minimized_input: None,
reproducible: true,
unique_hash: 0,
timestamp: SystemTime::now(),
asan_output: None,
ubsan_output: None,
},
CrashInfo {
id: "3".to_string(),
input_file: PathBuf::from("crash3"),
crash_type: CrashType::UseAfterFree,
signal: Some(11),
sanitizer_report: None,
stack_trace: Vec::new(),
minimized: false,
minimized_input: None,
reproducible: true,
unique_hash: 0,
timestamp: SystemTime::now(),
asan_output: None,
ubsan_output: None,
},
];
let classified = CrashTriage::classify_crashes(&crashes);
assert_eq!(classified.len(), 2);
assert_eq!(
classified
.get(&CrashType::HeapBufferOverflow)
.unwrap()
.len(),
2
);
assert_eq!(classified.get(&CrashType::UseAfterFree).unwrap().len(), 1);
}
#[test]
fn test_crash_triage_summary() {
let crashes = vec![CrashInfo {
id: "crash-001".to_string(),
input_file: PathBuf::from("/tmp/crash1"),
crash_type: CrashType::HeapBufferOverflow,
signal: Some(11),
sanitizer_report: None,
stack_trace: Vec::new(),
minimized: false,
minimized_input: None,
reproducible: true,
unique_hash: 0,
timestamp: SystemTime::now(),
asan_output: None,
ubsan_output: None,
}];
let summary = CrashTriage::crash_summary(&crashes);
assert!(summary.contains("Total crashes: 1"));
assert!(summary.contains("heap-buffer-overflow"));
assert!(summary.contains("crash-001"));
}
#[test]
fn test_ossfuzz_build_sh_generation() {
let project = OssFuzzProject {
name: "test-project".to_string(),
primary_contact: "dev@example.com".to_string(),
auto_ccs: vec!["security@example.com".to_string()],
vendor_ccs: vec![],
language: "c++".to_string(),
fuzzing_engines: vec![FuzzingEngine::LibFuzzer],
sanitizers: vec![Sanitizer::Address, Sanitizer::Undefined],
architectures: vec!["x86_64".to_string()],
main_repo: "https://github.com/example/test-project".to_string(),
file_github_issue: true,
coverage_extra_args: vec![],
help_url: None,
};
let build_script = generate_ossfuzz_build_sh(&project, &["parse", "encode"]);
assert!(build_script.contains("OSS-Fuzz build.sh"));
assert!(build_script.contains("fuzz_parse"));
assert!(build_script.contains("fuzz_encode"));
assert!(build_script.contains("$LIB_FUZZING_ENGINE"));
}
#[test]
fn test_ossfuzz_dockerfile_generation() {
let project = OssFuzzProject {
name: "my-project".to_string(),
primary_contact: "dev@example.com".to_string(),
auto_ccs: vec![],
vendor_ccs: vec![],
language: "c++".to_string(),
fuzzing_engines: vec![],
sanitizers: vec![],
architectures: vec![],
main_repo: "https://github.com/example/my-project".to_string(),
file_github_issue: true,
coverage_extra_args: vec![],
help_url: None,
};
let dockerfile = generate_ossfuzz_dockerfile(&project);
assert!(dockerfile.contains("FROM gcr.io/oss-fuzz-base/base-builder"));
assert!(dockerfile.contains("my-project"));
assert!(dockerfile.contains("github.com/example/my-project"));
}
#[test]
fn test_ossfuzz_project_yaml() {
let project = OssFuzzProject {
name: "test-proj".to_string(),
primary_contact: "dev@test.com".to_string(),
auto_ccs: vec!["sec@test.com".to_string()],
vendor_ccs: vec![],
language: "c++".to_string(),
fuzzing_engines: vec![],
sanitizers: vec![],
architectures: vec![],
main_repo: "https://github.com/test/test-proj".to_string(),
file_github_issue: true,
coverage_extra_args: vec![],
help_url: None,
};
let yaml = generate_ossfuzz_project_yaml(&project);
assert!(yaml.contains("test-proj"));
assert!(yaml.contains("sec@test.com"));
assert!(yaml.contains("libfuzzer"));
assert!(yaml.contains("address"));
}
#[test]
fn test_protobuf_mutator_target_generation() {
let fuzzer = ProtobufMutatorFuzzer {
proto_file: PathBuf::from("test.proto"),
proto_message: "MyMessage".to_string(),
fuzz_target_function: "handle_message(input);".to_string(),
use_lpm: true,
custom_mutator: false,
};
let target = fuzzer.generate_target();
assert!(target.contains("libprotobuf-mutator"));
assert!(target.contains("DEFINE_PROTO_FUZZER"));
assert!(target.contains("MyMessage"));
assert!(target.contains("handle_message"));
}
#[test]
fn test_fuzzing_pipeline_creation() {
let config = FuzzingConfig::default();
let pipeline = FuzzingPipeline::new("test_pipeline", config);
assert_eq!(pipeline.name, "test_pipeline");
assert_eq!(pipeline.stages.len(), 6);
}
#[test]
fn test_aflpp_features_default() {
let features = AflPlusPlusFeatures::default();
assert!(features.cmplog_enabled);
assert!(features.laf_intel_enabled);
assert!(features.redqueen_enabled);
assert!(!features.nyx_mode);
}
}