Initial commit

This commit is contained in:
Tanguy Duhamel
2025-09-29 21:26:41 +02:00
parent f0fd367ed8
commit 323a434c73
208 changed files with 72069 additions and 53 deletions
@@ -0,0 +1,49 @@
"""
Fuzzing Modules
This package contains modules for various fuzzing techniques and tools.
Available modules:
- LibFuzzer: LLVM's coverage-guided fuzzing engine
- AFL++: Advanced American Fuzzy Lop with modern features
- AFL-RS: Rust-based AFL implementation
- Atheris: Python fuzzing engine for finding bugs in Python code
- Cargo Fuzz: Rust fuzzing integration with libFuzzer
- Go-Fuzz: Coverage-guided fuzzing for Go packages
- OSS-Fuzz: Google's continuous fuzzing for open source
"""
# Copyright (c) 2025 FuzzingLabs
#
# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file
# at the root of this repository for details.
#
# After the Change Date (four years from publication), this version of the
# Licensed Work will be made available under the Apache License, Version 2.0.
# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0
#
# Additional attribution and requirements are provided in the NOTICE file.
from typing import List, Type
from ..base import BaseModule
# Module registry for automatic discovery
FUZZING_MODULES: List[Type[BaseModule]] = []
def register_module(module_class: Type[BaseModule]):
"""Register a fuzzing module"""
FUZZING_MODULES.append(module_class)
return module_class
def get_available_modules() -> List[Type[BaseModule]]:
"""Get all available fuzzing modules"""
return FUZZING_MODULES.copy()
# Import modules to trigger registration
from .libfuzzer import LibFuzzerModule
from .aflplusplus import AFLPlusPlusModule
from .aflrs import AFLRSModule
from .atheris import AtherisModule
from .cargo_fuzz import CargoFuzzModule
from .go_fuzz import GoFuzzModule
from .oss_fuzz import OSSFuzzModule
@@ -0,0 +1,734 @@
"""
AFL++ Fuzzing Module
This module uses AFL++ (Advanced American Fuzzy Lop) for coverage-guided
fuzzing with modern features and optimizations.
"""
# Copyright (c) 2025 FuzzingLabs
#
# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file
# at the root of this repository for details.
#
# After the Change Date (four years from publication), this version of the
# Licensed Work will be made available under the Apache License, Version 2.0.
# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0
#
# Additional attribution and requirements are provided in the NOTICE file.
import asyncio
import json
import os
from pathlib import Path
from typing import Dict, Any, List
import subprocess
import logging
import re
from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult
from . import register_module
logger = logging.getLogger(__name__)
@register_module
class AFLPlusPlusModule(BaseModule):
"""AFL++ advanced fuzzing module"""
def get_metadata(self) -> ModuleMetadata:
"""Get module metadata"""
return ModuleMetadata(
name="aflplusplus",
version="4.09c",
description="Advanced American Fuzzy Lop with modern features for coverage-guided fuzzing",
author="FuzzForge Team",
category="fuzzing",
tags=["coverage-guided", "american-fuzzy-lop", "advanced", "mutation", "instrumentation"],
input_schema={
"type": "object",
"properties": {
"target_binary": {
"type": "string",
"description": "Path to the target binary (compiled with afl-gcc/afl-clang)"
},
"input_dir": {
"type": "string",
"description": "Directory containing seed input files"
},
"output_dir": {
"type": "string",
"default": "afl_output",
"description": "Output directory for AFL++ results"
},
"dictionary": {
"type": "string",
"description": "Dictionary file for fuzzing keywords"
},
"timeout": {
"type": "integer",
"default": 1000,
"description": "Timeout for each execution (ms)"
},
"memory_limit": {
"type": "integer",
"default": 50,
"description": "Memory limit for child process (MB)"
},
"skip_deterministic": {
"type": "boolean",
"default": false,
"description": "Skip deterministic mutations"
},
"no_arith": {
"type": "boolean",
"default": false,
"description": "Skip arithmetic mutations"
},
"shuffle_queue": {
"type": "boolean",
"default": false,
"description": "Shuffle queue entries"
},
"max_total_time": {
"type": "integer",
"default": 3600,
"description": "Maximum total fuzzing time (seconds)"
},
"power_schedule": {
"type": "string",
"enum": ["explore", "fast", "coe", "lin", "quad", "exploit", "rare"],
"default": "fast",
"description": "Power schedule algorithm"
},
"mutation_mode": {
"type": "string",
"enum": ["default", "old", "mopt"],
"default": "default",
"description": "Mutation mode to use"
},
"parallel_fuzzing": {
"type": "boolean",
"default": false,
"description": "Enable parallel fuzzing with multiple instances"
},
"fuzzer_instances": {
"type": "integer",
"default": 1,
"description": "Number of parallel fuzzer instances"
},
"master_instance": {
"type": "string",
"default": "master",
"description": "Name for master fuzzer instance"
},
"slave_prefix": {
"type": "string",
"default": "slave",
"description": "Prefix for slave fuzzer instances"
},
"hang_timeout": {
"type": "integer",
"default": 1000,
"description": "Timeout for detecting hangs (ms)"
},
"crash_mode": {
"type": "boolean",
"default": false,
"description": "Run in crash exploration mode"
},
"target_args": {
"type": "array",
"items": {"type": "string"},
"description": "Arguments to pass to target binary"
},
"env_vars": {
"type": "object",
"description": "Environment variables to set"
},
"ignore_finds": {
"type": "boolean",
"default": false,
"description": "Ignore existing findings and start fresh"
},
"force_deterministic": {
"type": "boolean",
"default": false,
"description": "Force deterministic mutations"
}
}
},
output_schema={
"type": "object",
"properties": {
"findings": {
"type": "array",
"items": {
"type": "object",
"properties": {
"crash_id": {"type": "string"},
"crash_file": {"type": "string"},
"crash_type": {"type": "string"},
"signal": {"type": "string"}
}
}
}
}
}
)
def validate_config(self, config: Dict[str, Any]) -> bool:
"""Validate configuration"""
target_binary = config.get("target_binary")
if not target_binary:
raise ValueError("target_binary is required for AFL++")
input_dir = config.get("input_dir")
if not input_dir:
raise ValueError("input_dir is required for AFL++")
return True
async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult:
"""Execute AFL++ fuzzing"""
self.start_timer()
try:
# Validate inputs
self.validate_config(config)
self.validate_workspace(workspace)
logger.info("Running AFL++ fuzzing campaign")
# Check prerequisites
await self._check_afl_prerequisites(workspace)
# Setup directories and files
target_binary, input_dir, output_dir = self._setup_afl_directories(config, workspace)
# Run AFL++ fuzzing
findings = await self._run_afl_fuzzing(target_binary, input_dir, output_dir, config, workspace)
# Create summary
summary = self._create_summary(findings, output_dir)
logger.info(f"AFL++ found {len(findings)} crashes")
return self.create_result(
findings=findings,
status="success",
summary=summary
)
except Exception as e:
logger.error(f"AFL++ module failed: {e}")
return self.create_result(
findings=[],
status="failed",
error=str(e)
)
async def _check_afl_prerequisites(self, workspace: Path):
"""Check AFL++ prerequisites and system setup"""
try:
# Check if afl-fuzz exists
process = await asyncio.create_subprocess_exec(
"which", "afl-fuzz",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
raise RuntimeError("afl-fuzz not found. Please install AFL++")
# Check core dump pattern (important for AFL)
try:
with open("/proc/sys/kernel/core_pattern", "r") as f:
core_pattern = f.read().strip()
if core_pattern != "core":
logger.warning(f"Core dump pattern is '{core_pattern}', AFL++ may not work optimally")
except Exception:
logger.warning("Could not check core dump pattern")
except Exception as e:
logger.warning(f"AFL++ prerequisite check failed: {e}")
def _setup_afl_directories(self, config: Dict[str, Any], workspace: Path):
"""Setup AFL++ directories and validate files"""
# Check target binary
target_binary = workspace / config["target_binary"]
if not target_binary.exists():
raise FileNotFoundError(f"Target binary not found: {target_binary}")
# Check input directory
input_dir = workspace / config["input_dir"]
if not input_dir.exists():
raise FileNotFoundError(f"Input directory not found: {input_dir}")
# Check if input directory has files
input_files = list(input_dir.glob("*"))
if not input_files:
raise ValueError(f"Input directory is empty: {input_dir}")
# Create output directory
output_dir = workspace / config.get("output_dir", "afl_output")
output_dir.mkdir(exist_ok=True)
return target_binary, input_dir, output_dir
async def _run_afl_fuzzing(self, target_binary: Path, input_dir: Path, output_dir: Path, config: Dict[str, Any], workspace: Path) -> List[ModuleFinding]:
"""Run AFL++ fuzzing"""
findings = []
try:
if config.get("parallel_fuzzing", False):
findings = await self._run_parallel_fuzzing(
target_binary, input_dir, output_dir, config, workspace
)
else:
findings = await self._run_single_fuzzing(
target_binary, input_dir, output_dir, config, workspace
)
except Exception as e:
logger.warning(f"Error running AFL++ fuzzing: {e}")
return findings
async def _run_single_fuzzing(self, target_binary: Path, input_dir: Path, output_dir: Path, config: Dict[str, Any], workspace: Path) -> List[ModuleFinding]:
"""Run single-instance AFL++ fuzzing"""
findings = []
try:
# Build AFL++ command
cmd = ["afl-fuzz"]
# Add input and output directories
cmd.extend(["-i", str(input_dir)])
cmd.extend(["-o", str(output_dir)])
# Add dictionary if specified
dictionary = config.get("dictionary")
if dictionary:
dict_path = workspace / dictionary
if dict_path.exists():
cmd.extend(["-x", str(dict_path)])
# Add timeout
timeout = config.get("timeout", 1000)
cmd.extend(["-t", str(timeout)])
# Add memory limit
memory_limit = config.get("memory_limit", 50)
cmd.extend(["-m", str(memory_limit)])
# Add power schedule
power_schedule = config.get("power_schedule", "fast")
cmd.extend(["-p", power_schedule])
# Add mutation options
if config.get("skip_deterministic", False):
cmd.append("-d")
if config.get("no_arith", False):
cmd.append("-a")
if config.get("shuffle_queue", False):
cmd.append("-Z")
# Add hang timeout
hang_timeout = config.get("hang_timeout", 1000)
cmd.extend(["-T", str(hang_timeout)])
# Add crash mode
if config.get("crash_mode", False):
cmd.append("-C")
# Add ignore finds
if config.get("ignore_finds", False):
cmd.append("-f")
# Add force deterministic
if config.get("force_deterministic", False):
cmd.append("-D")
# Add target binary and arguments
cmd.append("--")
cmd.append(str(target_binary))
target_args = config.get("target_args", [])
cmd.extend(target_args)
# Set up environment
env = os.environ.copy()
env_vars = config.get("env_vars", {})
env.update(env_vars)
# Set AFL environment variables
env["AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES"] = "1" # Avoid interactive prompts
env["AFL_SKIP_CPUFREQ"] = "1" # Skip CPU frequency checks
logger.debug(f"Running command: {' '.join(cmd)}")
# Run AFL++ with timeout
max_total_time = config.get("max_total_time", 3600)
try:
process = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=workspace,
env=env
)
# Wait for specified time then terminate
try:
stdout, stderr = await asyncio.wait_for(
process.communicate(), timeout=max_total_time
)
except asyncio.TimeoutError:
logger.info(f"AFL++ fuzzing timed out after {max_total_time} seconds")
process.terminate()
try:
await asyncio.wait_for(process.wait(), timeout=10)
except asyncio.TimeoutError:
process.kill()
await process.wait()
# Parse results from output directory
findings = self._parse_afl_results(output_dir, workspace)
except Exception as e:
logger.warning(f"Error running AFL++ process: {e}")
except Exception as e:
logger.warning(f"Error in single fuzzing: {e}")
return findings
async def _run_parallel_fuzzing(self, target_binary: Path, input_dir: Path, output_dir: Path, config: Dict[str, Any], workspace: Path) -> List[ModuleFinding]:
"""Run parallel AFL++ fuzzing"""
findings = []
try:
fuzzer_instances = config.get("fuzzer_instances", 2)
master_name = config.get("master_instance", "master")
slave_prefix = config.get("slave_prefix", "slave")
processes = []
# Start master instance
master_cmd = await self._build_afl_command(
target_binary, input_dir, output_dir, config, workspace,
instance_name=master_name, is_master=True
)
master_process = await asyncio.create_subprocess_exec(
*master_cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=workspace,
env=self._get_afl_env(config)
)
processes.append(master_process)
# Start slave instances
for i in range(1, fuzzer_instances):
slave_name = f"{slave_prefix}{i:02d}"
slave_cmd = await self._build_afl_command(
target_binary, input_dir, output_dir, config, workspace,
instance_name=slave_name, is_master=False
)
slave_process = await asyncio.create_subprocess_exec(
*slave_cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=workspace,
env=self._get_afl_env(config)
)
processes.append(slave_process)
# Wait for specified time then terminate all
max_total_time = config.get("max_total_time", 3600)
try:
await asyncio.sleep(max_total_time)
finally:
# Terminate all processes
for process in processes:
if process.returncode is None:
process.terminate()
try:
await asyncio.wait_for(process.wait(), timeout=10)
except asyncio.TimeoutError:
process.kill()
await process.wait()
# Parse results from output directory
findings = self._parse_afl_results(output_dir, workspace)
except Exception as e:
logger.warning(f"Error in parallel fuzzing: {e}")
return findings
async def _build_afl_command(self, target_binary: Path, input_dir: Path, output_dir: Path, config: Dict[str, Any], workspace: Path, instance_name: str, is_master: bool) -> List[str]:
"""Build AFL++ command for a fuzzer instance"""
cmd = ["afl-fuzz"]
# Add input and output directories
cmd.extend(["-i", str(input_dir)])
cmd.extend(["-o", str(output_dir)])
# Add instance name
if is_master:
cmd.extend(["-M", instance_name])
else:
cmd.extend(["-S", instance_name])
# Add other options (same as single fuzzing)
dictionary = config.get("dictionary")
if dictionary:
dict_path = workspace / dictionary
if dict_path.exists():
cmd.extend(["-x", str(dict_path)])
cmd.extend(["-t", str(config.get("timeout", 1000))])
cmd.extend(["-m", str(config.get("memory_limit", 50))])
cmd.extend(["-p", config.get("power_schedule", "fast")])
if config.get("skip_deterministic", False):
cmd.append("-d")
if config.get("no_arith", False):
cmd.append("-a")
# Add target
cmd.append("--")
cmd.append(str(target_binary))
cmd.extend(config.get("target_args", []))
return cmd
def _get_afl_env(self, config: Dict[str, Any]) -> Dict[str, str]:
"""Get environment variables for AFL++"""
env = os.environ.copy()
env.update(config.get("env_vars", {}))
env["AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES"] = "1"
env["AFL_SKIP_CPUFREQ"] = "1"
return env
def _parse_afl_results(self, output_dir: Path, workspace: Path) -> List[ModuleFinding]:
"""Parse AFL++ results from output directory"""
findings = []
try:
# Look for crashes directory
crashes_dirs = []
# Single instance
crashes_dir = output_dir / "crashes"
if crashes_dir.exists():
crashes_dirs.append(crashes_dir)
# Multiple instances
for instance_dir in output_dir.iterdir():
if instance_dir.is_dir():
instance_crashes = instance_dir / "crashes"
if instance_crashes.exists():
crashes_dirs.append(instance_crashes)
# Process crash files
for crashes_dir in crashes_dirs:
crash_files = [f for f in crashes_dir.iterdir() if f.is_file() and f.name.startswith("id:")]
for crash_file in crash_files:
finding = self._create_afl_crash_finding(crash_file, workspace)
if finding:
findings.append(finding)
except Exception as e:
logger.warning(f"Error parsing AFL++ results: {e}")
return findings
def _create_afl_crash_finding(self, crash_file: Path, workspace: Path) -> ModuleFinding:
"""Create finding from AFL++ crash file"""
try:
# Parse crash filename for information
filename = crash_file.name
crash_info = self._parse_afl_filename(filename)
# Try to read crash file (limited size)
crash_content = ""
try:
crash_data = crash_file.read_bytes()[:1000]
crash_content = crash_data.hex()[:200] # Hex representation, limited
except Exception:
pass
# Determine severity based on signal
severity = self._get_crash_severity(crash_info.get("signal", ""))
# Create relative path
try:
rel_path = crash_file.relative_to(workspace)
file_path = str(rel_path)
except ValueError:
file_path = str(crash_file)
finding = self.create_finding(
title=f"AFL++ Crash: {crash_info.get('signal', 'Unknown')}",
description=f"AFL++ discovered a crash with signal {crash_info.get('signal', 'unknown')} in the target program",
severity=severity,
category=self._get_crash_category(crash_info.get("signal", "")),
file_path=file_path,
recommendation=self._get_afl_crash_recommendation(crash_info.get("signal", "")),
metadata={
"crash_id": crash_info.get("id", ""),
"signal": crash_info.get("signal", ""),
"src": crash_info.get("src", ""),
"crash_file": crash_file.name,
"crash_content_hex": crash_content,
"fuzzer": "afl++"
}
)
return finding
except Exception as e:
logger.warning(f"Error creating AFL++ crash finding: {e}")
return None
def _parse_afl_filename(self, filename: str) -> Dict[str, str]:
"""Parse AFL++ crash filename for information"""
info = {}
try:
# AFL++ crash filename format: id:XXXXXX,sig:XX,src:XXXXXX,op:XXX,rep:X
parts = filename.split(',')
for part in parts:
if ':' in part:
key, value = part.split(':', 1)
info[key] = value
except Exception:
pass
return info
def _get_crash_severity(self, signal: str) -> str:
"""Determine severity based on crash signal"""
if not signal:
return "medium"
signal_lower = signal.lower()
# Critical signals indicating memory corruption
if signal in ["11", "sigsegv", "segv"]: # Segmentation fault
return "critical"
elif signal in ["6", "sigabrt", "abrt"]: # Abort
return "high"
elif signal in ["4", "sigill", "ill"]: # Illegal instruction
return "high"
elif signal in ["8", "sigfpe", "fpe"]: # Floating point exception
return "medium"
elif signal in ["9", "sigkill", "kill"]: # Kill signal
return "medium"
else:
return "medium"
def _get_crash_category(self, signal: str) -> str:
"""Determine category based on crash signal"""
if not signal:
return "program_crash"
if signal in ["11", "sigsegv", "segv"]:
return "memory_corruption"
elif signal in ["6", "sigabrt", "abrt"]:
return "assertion_failure"
elif signal in ["4", "sigill", "ill"]:
return "illegal_instruction"
elif signal in ["8", "sigfpe", "fpe"]:
return "arithmetic_error"
else:
return "program_crash"
def _get_afl_crash_recommendation(self, signal: str) -> str:
"""Generate recommendation based on crash signal"""
if signal in ["11", "sigsegv", "segv"]:
return "Segmentation fault detected. Investigate memory access patterns, check for buffer overflows, null pointer dereferences, or use-after-free bugs."
elif signal in ["6", "sigabrt", "abrt"]:
return "Program abort detected. Check for assertion failures, memory allocation errors, or explicit abort() calls in the code."
elif signal in ["4", "sigill", "ill"]:
return "Illegal instruction detected. Check for code corruption, invalid function pointers, or architecture-specific instruction issues."
elif signal in ["8", "sigfpe", "fpe"]:
return "Floating point exception detected. Check for division by zero, arithmetic overflow, or invalid floating point operations."
else:
return f"Program crash with signal {signal} detected. Analyze the crash dump and input to identify the root cause."
def _create_summary(self, findings: List[ModuleFinding], output_dir: Path) -> Dict[str, Any]:
"""Create analysis summary"""
severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
category_counts = {}
signal_counts = {}
for finding in findings:
# Count by severity
severity_counts[finding.severity] += 1
# Count by category
category = finding.category
category_counts[category] = category_counts.get(category, 0) + 1
# Count by signal
signal = finding.metadata.get("signal", "unknown")
signal_counts[signal] = signal_counts.get(signal, 0) + 1
# Try to read AFL++ statistics
stats = self._read_afl_stats(output_dir)
return {
"total_findings": len(findings),
"severity_counts": severity_counts,
"category_counts": category_counts,
"signal_counts": signal_counts,
"unique_crashes": len(set(f.metadata.get("crash_id", "") for f in findings)),
"afl_stats": stats
}
def _read_afl_stats(self, output_dir: Path) -> Dict[str, Any]:
"""Read AFL++ fuzzer statistics"""
stats = {}
try:
# Look for fuzzer_stats file in single or multiple instance setup
stats_files = []
# Single instance
single_stats = output_dir / "fuzzer_stats"
if single_stats.exists():
stats_files.append(single_stats)
# Multiple instances
for instance_dir in output_dir.iterdir():
if instance_dir.is_dir():
instance_stats = instance_dir / "fuzzer_stats"
if instance_stats.exists():
stats_files.append(instance_stats)
# Read first stats file found
if stats_files:
with open(stats_files[0], 'r') as f:
for line in f:
if ':' in line:
key, value = line.strip().split(':', 1)
stats[key.strip()] = value.strip()
except Exception as e:
logger.warning(f"Error reading AFL++ stats: {e}")
return stats
+678
View File
@@ -0,0 +1,678 @@
"""
AFL-RS Fuzzing Module
This module uses AFL-RS (AFL in Rust) for high-performance coverage-guided fuzzing
with modern Rust implementations and optimizations.
"""
# Copyright (c) 2025 FuzzingLabs
#
# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file
# at the root of this repository for details.
#
# After the Change Date (four years from publication), this version of the
# Licensed Work will be made available under the Apache License, Version 2.0.
# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0
#
# Additional attribution and requirements are provided in the NOTICE file.
import asyncio
import json
import os
from pathlib import Path
from typing import Dict, Any, List
import subprocess
import logging
import re
from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult
from . import register_module
logger = logging.getLogger(__name__)
@register_module
class AFLRSModule(BaseModule):
"""AFL-RS Rust-based fuzzing module"""
def get_metadata(self) -> ModuleMetadata:
"""Get module metadata"""
return ModuleMetadata(
name="aflrs",
version="0.2.0",
description="High-performance AFL implementation in Rust with modern fuzzing features",
author="FuzzForge Team",
category="fuzzing",
tags=["coverage-guided", "rust", "afl", "high-performance", "modern"],
input_schema={
"type": "object",
"properties": {
"target_binary": {
"type": "string",
"description": "Path to the target binary (compiled with AFL-RS instrumentation)"
},
"input_dir": {
"type": "string",
"description": "Directory containing seed input files"
},
"output_dir": {
"type": "string",
"default": "aflrs_output",
"description": "Output directory for AFL-RS results"
},
"dictionary": {
"type": "string",
"description": "Dictionary file for token-based mutations"
},
"timeout": {
"type": "integer",
"default": 1000,
"description": "Timeout for each execution (ms)"
},
"memory_limit": {
"type": "integer",
"default": 50,
"description": "Memory limit for target process (MB)"
},
"max_total_time": {
"type": "integer",
"default": 3600,
"description": "Maximum total fuzzing time (seconds)"
},
"cpu_cores": {
"type": "integer",
"default": 1,
"description": "Number of CPU cores to use"
},
"mutation_depth": {
"type": "integer",
"default": 4,
"description": "Maximum depth for cascaded mutations"
},
"skip_deterministic": {
"type": "boolean",
"default": false,
"description": "Skip deterministic mutations"
},
"power_schedule": {
"type": "string",
"enum": ["explore", "fast", "coe", "lin", "quad", "exploit", "rare", "mmopt", "seek"],
"default": "fast",
"description": "Power scheduling algorithm"
},
"custom_mutators": {
"type": "array",
"items": {"type": "string"},
"description": "Custom mutator libraries to load"
},
"cmplog": {
"type": "boolean",
"default": true,
"description": "Enable CmpLog for comparison logging"
},
"redqueen": {
"type": "boolean",
"default": true,
"description": "Enable RedQueen input-to-state correspondence"
},
"unicorn_mode": {
"type": "boolean",
"default": false,
"description": "Enable Unicorn mode for emulation"
},
"persistent_mode": {
"type": "boolean",
"default": false,
"description": "Enable persistent mode for faster execution"
},
"target_args": {
"type": "array",
"items": {"type": "string"},
"description": "Arguments to pass to target binary"
},
"env_vars": {
"type": "object",
"description": "Environment variables to set"
},
"ignore_timeouts": {
"type": "boolean",
"default": false,
"description": "Ignore timeout signals and continue fuzzing"
},
"ignore_crashes": {
"type": "boolean",
"default": false,
"description": "Ignore crashes and continue fuzzing"
},
"sync_dir": {
"type": "string",
"description": "Directory for syncing with other AFL instances"
},
"sync_id": {
"type": "string",
"description": "Fuzzer ID for syncing"
}
}
},
output_schema={
"type": "object",
"properties": {
"findings": {
"type": "array",
"items": {
"type": "object",
"properties": {
"crash_id": {"type": "string"},
"crash_file": {"type": "string"},
"signal": {"type": "string"},
"execution_time": {"type": "integer"}
}
}
}
}
}
)
def validate_config(self, config: Dict[str, Any]) -> bool:
"""Validate configuration"""
target_binary = config.get("target_binary")
if not target_binary:
raise ValueError("target_binary is required for AFL-RS")
input_dir = config.get("input_dir")
if not input_dir:
raise ValueError("input_dir is required for AFL-RS")
cpu_cores = config.get("cpu_cores", 1)
if cpu_cores < 1:
raise ValueError("cpu_cores must be at least 1")
return True
async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult:
"""Execute AFL-RS fuzzing"""
self.start_timer()
try:
# Validate inputs
self.validate_config(config)
self.validate_workspace(workspace)
logger.info("Running AFL-RS fuzzing campaign")
# Check AFL-RS installation
await self._check_aflrs_installation()
# Setup directories and files
target_binary, input_dir, output_dir = self._setup_aflrs_directories(config, workspace)
# Run AFL-RS fuzzing
findings = await self._run_aflrs_fuzzing(target_binary, input_dir, output_dir, config, workspace)
# Create summary
summary = self._create_summary(findings, output_dir)
logger.info(f"AFL-RS found {len(findings)} crashes")
return self.create_result(
findings=findings,
status="success",
summary=summary
)
except Exception as e:
logger.error(f"AFL-RS module failed: {e}")
return self.create_result(
findings=[],
status="failed",
error=str(e)
)
async def _check_aflrs_installation(self):
"""Check if AFL-RS is installed and available"""
try:
# Check if aflrs is available (assuming aflrs binary)
process = await asyncio.create_subprocess_exec(
"which", "aflrs",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
# Try alternative AFL-RS command names
alt_commands = ["afl-fuzz-rs", "afl-rs", "cargo-afl"]
found = False
for cmd in alt_commands:
process = await asyncio.create_subprocess_exec(
"which", cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
if process.returncode == 0:
found = True
break
if not found:
raise RuntimeError("AFL-RS not found. Please install AFL-RS or ensure it's in PATH")
except Exception as e:
logger.warning(f"AFL-RS installation check failed: {e}")
def _setup_aflrs_directories(self, config: Dict[str, Any], workspace: Path):
"""Setup AFL-RS directories and validate files"""
# Check target binary
target_binary = workspace / config["target_binary"]
if not target_binary.exists():
raise FileNotFoundError(f"Target binary not found: {target_binary}")
# Check input directory
input_dir = workspace / config["input_dir"]
if not input_dir.exists():
raise FileNotFoundError(f"Input directory not found: {input_dir}")
# Validate input files exist
input_files = list(input_dir.glob("*"))
if not input_files:
raise ValueError(f"Input directory is empty: {input_dir}")
# Create output directory
output_dir = workspace / config.get("output_dir", "aflrs_output")
output_dir.mkdir(exist_ok=True)
return target_binary, input_dir, output_dir
async def _run_aflrs_fuzzing(self, target_binary: Path, input_dir: Path, output_dir: Path, config: Dict[str, Any], workspace: Path) -> List[ModuleFinding]:
"""Run AFL-RS fuzzing"""
findings = []
try:
# Build AFL-RS command
cmd = await self._build_aflrs_command(target_binary, input_dir, output_dir, config, workspace)
# Set up environment
env = self._setup_aflrs_environment(config)
logger.debug(f"Running command: {' '.join(cmd)}")
# Run AFL-RS with timeout
max_total_time = config.get("max_total_time", 3600)
try:
process = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=workspace,
env=env
)
# Wait for specified time then terminate
try:
stdout, stderr = await asyncio.wait_for(
process.communicate(), timeout=max_total_time
)
logger.info(f"AFL-RS completed after {max_total_time} seconds")
except asyncio.TimeoutError:
logger.info(f"AFL-RS fuzzing timed out after {max_total_time} seconds, terminating")
process.terminate()
try:
await asyncio.wait_for(process.wait(), timeout=10)
except asyncio.TimeoutError:
process.kill()
await process.wait()
# Parse results
findings = self._parse_aflrs_results(output_dir, workspace)
except Exception as e:
logger.warning(f"Error running AFL-RS process: {e}")
except Exception as e:
logger.warning(f"Error in AFL-RS fuzzing: {e}")
return findings
async def _build_aflrs_command(self, target_binary: Path, input_dir: Path, output_dir: Path, config: Dict[str, Any], workspace: Path) -> List[str]:
"""Build AFL-RS command"""
# Try to determine the correct AFL-RS command
aflrs_cmd = "aflrs" # Default
# Try alternative command names
alt_commands = ["aflrs", "afl-fuzz-rs", "afl-rs"]
for cmd in alt_commands:
try:
process = await asyncio.create_subprocess_exec(
"which", cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
if process.returncode == 0:
aflrs_cmd = cmd
break
except Exception:
continue
cmd = [aflrs_cmd]
# Add input and output directories
cmd.extend(["-i", str(input_dir)])
cmd.extend(["-o", str(output_dir)])
# Add dictionary if specified
dictionary = config.get("dictionary")
if dictionary:
dict_path = workspace / dictionary
if dict_path.exists():
cmd.extend(["-x", str(dict_path)])
# Add timeout and memory limit
cmd.extend(["-t", str(config.get("timeout", 1000))])
cmd.extend(["-m", str(config.get("memory_limit", 50))])
# Add CPU cores
cpu_cores = config.get("cpu_cores", 1)
if cpu_cores > 1:
cmd.extend(["-j", str(cpu_cores)])
# Add mutation depth
mutation_depth = config.get("mutation_depth", 4)
cmd.extend(["-d", str(mutation_depth)])
# Add power schedule
power_schedule = config.get("power_schedule", "fast")
cmd.extend(["-p", power_schedule])
# Add skip deterministic
if config.get("skip_deterministic", False):
cmd.append("-D")
# Add custom mutators
custom_mutators = config.get("custom_mutators", [])
for mutator in custom_mutators:
cmd.extend(["-c", mutator])
# Add advanced features
if config.get("cmplog", True):
cmd.append("-l")
if config.get("redqueen", True):
cmd.append("-I")
if config.get("unicorn_mode", False):
cmd.append("-U")
if config.get("persistent_mode", False):
cmd.append("-P")
# Add ignore options
if config.get("ignore_timeouts", False):
cmd.append("-T")
if config.get("ignore_crashes", False):
cmd.append("-C")
# Add sync options
sync_dir = config.get("sync_dir")
if sync_dir:
cmd.extend(["-F", sync_dir])
sync_id = config.get("sync_id")
if sync_id:
cmd.extend(["-S", sync_id])
# Add target binary and arguments
cmd.append("--")
cmd.append(str(target_binary))
target_args = config.get("target_args", [])
cmd.extend(target_args)
return cmd
def _setup_aflrs_environment(self, config: Dict[str, Any]) -> Dict[str, str]:
"""Setup environment variables for AFL-RS"""
env = os.environ.copy()
# Add user-specified environment variables
env_vars = config.get("env_vars", {})
env.update(env_vars)
# Set AFL-RS specific environment variables
env["AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES"] = "1"
env["AFL_SKIP_CPUFREQ"] = "1"
# Enable advanced features if requested
if config.get("cmplog", True):
env["AFL_USE_CMPLOG"] = "1"
if config.get("redqueen", True):
env["AFL_USE_REDQUEEN"] = "1"
return env
def _parse_aflrs_results(self, output_dir: Path, workspace: Path) -> List[ModuleFinding]:
"""Parse AFL-RS results from output directory"""
findings = []
try:
# Look for crashes directory
crashes_dir = output_dir / "crashes"
if not crashes_dir.exists():
logger.info("No crashes directory found in AFL-RS output")
return findings
# Process crash files
crash_files = [f for f in crashes_dir.iterdir() if f.is_file() and not f.name.startswith(".")]
for crash_file in crash_files:
finding = self._create_aflrs_crash_finding(crash_file, workspace)
if finding:
findings.append(finding)
except Exception as e:
logger.warning(f"Error parsing AFL-RS results: {e}")
return findings
def _create_aflrs_crash_finding(self, crash_file: Path, workspace: Path) -> ModuleFinding:
"""Create finding from AFL-RS crash file"""
try:
# Parse crash filename
filename = crash_file.name
crash_info = self._parse_aflrs_filename(filename)
# Try to read crash file (limited size)
crash_content = ""
crash_size = 0
try:
crash_data = crash_file.read_bytes()
crash_size = len(crash_data)
# Store first 500 bytes as hex
crash_content = crash_data[:500].hex()
except Exception:
pass
# Determine severity based on signal or crash type
signal = crash_info.get("signal", "")
severity = self._get_crash_severity(signal)
# Create relative path
try:
rel_path = crash_file.relative_to(workspace)
file_path = str(rel_path)
except ValueError:
file_path = str(crash_file)
finding = self.create_finding(
title=f"AFL-RS Crash: {signal or 'Unknown Signal'}",
description=f"AFL-RS discovered a crash in the target program{' with signal ' + signal if signal else ''}",
severity=severity,
category=self._get_crash_category(signal),
file_path=file_path,
recommendation=self._get_crash_recommendation(signal),
metadata={
"crash_id": crash_info.get("id", ""),
"signal": signal,
"execution_time": crash_info.get("time", ""),
"crash_file": crash_file.name,
"crash_size": crash_size,
"crash_content_hex": crash_content,
"fuzzer": "aflrs"
}
)
return finding
except Exception as e:
logger.warning(f"Error creating AFL-RS crash finding: {e}")
return None
def _parse_aflrs_filename(self, filename: str) -> Dict[str, str]:
"""Parse AFL-RS crash filename for information"""
info = {}
try:
# AFL-RS may use similar format to AFL++
# Example: id_000000_sig_11_src_000000_time_12345_op_havoc_rep_128
parts = filename.replace("id:", "id_").replace("sig:", "sig_").replace("src:", "src_").replace("time:", "time_").replace("op:", "op_").replace("rep:", "rep_").split("_")
i = 0
while i < len(parts) - 1:
if parts[i] in ["id", "sig", "src", "time", "op", "rep"]:
info[parts[i]] = parts[i + 1]
i += 2
else:
i += 1
except Exception:
# Fallback: try to extract signal from filename
signal_match = re.search(r'sig[_:]?(\d+)', filename)
if signal_match:
info["signal"] = signal_match.group(1)
return info
def _get_crash_severity(self, signal: str) -> str:
"""Determine crash severity based on signal"""
if not signal:
return "medium"
try:
sig_num = int(signal)
except ValueError:
return "medium"
# Map common signals to severity
if sig_num == 11: # SIGSEGV
return "critical"
elif sig_num == 6: # SIGABRT
return "high"
elif sig_num == 4: # SIGILL
return "high"
elif sig_num == 8: # SIGFPE
return "medium"
elif sig_num == 9: # SIGKILL
return "medium"
else:
return "medium"
def _get_crash_category(self, signal: str) -> str:
"""Determine crash category based on signal"""
if not signal:
return "program_crash"
try:
sig_num = int(signal)
except ValueError:
return "program_crash"
if sig_num == 11: # SIGSEGV
return "memory_corruption"
elif sig_num == 6: # SIGABRT
return "assertion_failure"
elif sig_num == 4: # SIGILL
return "illegal_instruction"
elif sig_num == 8: # SIGFPE
return "arithmetic_error"
else:
return "program_crash"
def _get_crash_recommendation(self, signal: str) -> str:
"""Generate recommendation based on crash signal"""
if not signal:
return "Analyze the crash input to reproduce and debug the issue."
try:
sig_num = int(signal)
except ValueError:
return "Analyze the crash input to reproduce and debug the issue."
if sig_num == 11: # SIGSEGV
return "Segmentation fault detected. Check for buffer overflows, null pointer dereferences, use-after-free, or invalid memory access patterns."
elif sig_num == 6: # SIGABRT
return "Program abort detected. Check for assertion failures, memory corruption detected by allocator, or explicit abort calls."
elif sig_num == 4: # SIGILL
return "Illegal instruction detected. Check for code corruption, invalid function pointers, or architecture-specific issues."
elif sig_num == 8: # SIGFPE
return "Floating point exception detected. Check for division by zero, arithmetic overflow, or invalid floating point operations."
else:
return f"Program terminated with signal {signal}. Analyze the crash input and use debugging tools to identify the root cause."
def _create_summary(self, findings: List[ModuleFinding], output_dir: Path) -> Dict[str, Any]:
"""Create analysis summary"""
severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
category_counts = {}
signal_counts = {}
for finding in findings:
# Count by severity
severity_counts[finding.severity] += 1
# Count by category
category = finding.category
category_counts[category] = category_counts.get(category, 0) + 1
# Count by signal
signal = finding.metadata.get("signal", "unknown")
signal_counts[signal] = signal_counts.get(signal, 0) + 1
# Try to read AFL-RS statistics
stats = self._read_aflrs_stats(output_dir)
return {
"total_findings": len(findings),
"severity_counts": severity_counts,
"category_counts": category_counts,
"signal_counts": signal_counts,
"unique_crashes": len(set(f.metadata.get("crash_id", "") for f in findings)),
"aflrs_stats": stats
}
def _read_aflrs_stats(self, output_dir: Path) -> Dict[str, Any]:
"""Read AFL-RS fuzzer statistics"""
stats = {}
try:
# Look for AFL-RS stats file
stats_file = output_dir / "fuzzer_stats"
if stats_file.exists():
with open(stats_file, 'r') as f:
for line in f:
if ':' in line:
key, value = line.strip().split(':', 1)
stats[key.strip()] = value.strip()
# Also look for AFL-RS specific files
plot_data = output_dir / "plot_data"
if plot_data.exists():
stats["plot_data_available"] = True
except Exception as e:
logger.warning(f"Error reading AFL-RS stats: {e}")
return stats
+774
View File
@@ -0,0 +1,774 @@
"""
Atheris Fuzzing Module
This module uses Atheris for fuzzing Python code to find bugs and security
vulnerabilities in Python applications and libraries.
"""
# Copyright (c) 2025 FuzzingLabs
#
# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file
# at the root of this repository for details.
#
# After the Change Date (four years from publication), this version of the
# Licensed Work will be made available under the Apache License, Version 2.0.
# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0
#
# Additional attribution and requirements are provided in the NOTICE file.
import asyncio
import json
import os
import sys
from pathlib import Path
from typing import Dict, Any, List
import subprocess
import logging
import traceback
from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult
from . import register_module
logger = logging.getLogger(__name__)
@register_module
class AtherisModule(BaseModule):
"""Atheris Python fuzzing module"""
def get_metadata(self) -> ModuleMetadata:
"""Get module metadata"""
return ModuleMetadata(
name="atheris",
version="2.3.0",
description="Coverage-guided Python fuzzing engine for finding bugs in Python code",
author="FuzzForge Team",
category="fuzzing",
tags=["python", "coverage-guided", "native", "sanitizers", "libfuzzer"],
input_schema={
"type": "object",
"properties": {
"target_script": {
"type": "string",
"description": "Path to the Python script containing the fuzz target function"
},
"target_function": {
"type": "string",
"default": "TestOneInput",
"description": "Name of the target function to fuzz"
},
"corpus_dir": {
"type": "string",
"description": "Directory containing initial corpus files"
},
"dict_file": {
"type": "string",
"description": "Dictionary file for fuzzing keywords"
},
"max_total_time": {
"type": "integer",
"default": 600,
"description": "Maximum total time to run fuzzing (seconds)"
},
"max_len": {
"type": "integer",
"default": 4096,
"description": "Maximum length of test input"
},
"timeout": {
"type": "integer",
"default": 25,
"description": "Timeout for individual test cases (seconds)"
},
"runs": {
"type": "integer",
"default": -1,
"description": "Number of individual test runs (-1 for unlimited)"
},
"jobs": {
"type": "integer",
"default": 1,
"description": "Number of fuzzing jobs to run in parallel"
},
"print_final_stats": {
"type": "boolean",
"default": true,
"description": "Print final statistics"
},
"print_pcs": {
"type": "boolean",
"default": false,
"description": "Print newly covered PCs"
},
"print_coverage": {
"type": "boolean",
"default": true,
"description": "Print coverage information"
},
"artifact_prefix": {
"type": "string",
"default": "crash-",
"description": "Prefix for artifact files"
},
"seed": {
"type": "integer",
"description": "Random seed for reproducibility"
},
"python_path": {
"type": "array",
"items": {"type": "string"},
"description": "Additional Python paths to add to sys.path"
},
"enable_sanitizers": {
"type": "boolean",
"default": true,
"description": "Enable Python-specific sanitizers and checks"
},
"detect_leaks": {
"type": "boolean",
"default": true,
"description": "Detect memory leaks in native extensions"
},
"detect_stack_use_after_return": {
"type": "boolean",
"default": false,
"description": "Detect stack use-after-return"
},
"setup_code": {
"type": "string",
"description": "Python code to execute before fuzzing starts"
},
"enable_value_profile": {
"type": "boolean",
"default": false,
"description": "Enable value profiling for better mutation"
},
"shrink": {
"type": "boolean",
"default": true,
"description": "Try to shrink the corpus"
},
"only_ascii": {
"type": "boolean",
"default": false,
"description": "Only generate ASCII inputs"
}
}
},
output_schema={
"type": "object",
"properties": {
"findings": {
"type": "array",
"items": {
"type": "object",
"properties": {
"exception_type": {"type": "string"},
"exception_message": {"type": "string"},
"stack_trace": {"type": "string"},
"crash_input": {"type": "string"}
}
}
}
}
}
)
def validate_config(self, config: Dict[str, Any]) -> bool:
"""Validate configuration"""
target_script = config.get("target_script")
if not target_script:
raise ValueError("target_script is required for Atheris")
max_total_time = config.get("max_total_time", 600)
if max_total_time <= 0:
raise ValueError("max_total_time must be positive")
return True
async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult:
"""Execute Atheris Python fuzzing"""
self.start_timer()
try:
# Validate inputs
self.validate_config(config)
self.validate_workspace(workspace)
logger.info("Running Atheris Python fuzzing")
# Check Atheris installation
await self._check_atheris_installation()
# Validate target script
target_script = workspace / config["target_script"]
if not target_script.exists():
raise FileNotFoundError(f"Target script not found: {target_script}")
# Run Atheris fuzzing
findings = await self._run_atheris_fuzzing(target_script, config, workspace)
# Create summary
summary = self._create_summary(findings)
logger.info(f"Atheris found {len(findings)} issues")
return self.create_result(
findings=findings,
status="success",
summary=summary
)
except Exception as e:
logger.error(f"Atheris module failed: {e}")
return self.create_result(
findings=[],
status="failed",
error=str(e)
)
async def _check_atheris_installation(self):
"""Check if Atheris is installed"""
try:
process = await asyncio.create_subprocess_exec(
sys.executable, "-c", "import atheris; print(atheris.__version__)",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
raise RuntimeError("Atheris not installed. Install with: pip install atheris")
version = stdout.decode().strip()
logger.info(f"Using Atheris version: {version}")
except Exception as e:
raise RuntimeError(f"Atheris installation check failed: {e}")
async def _run_atheris_fuzzing(self, target_script: Path, config: Dict[str, Any], workspace: Path) -> List[ModuleFinding]:
"""Run Atheris fuzzing"""
findings = []
try:
# Create output directory for artifacts
output_dir = workspace / "atheris_output"
output_dir.mkdir(exist_ok=True)
# Create wrapper script for fuzzing
wrapper_script = await self._create_atheris_wrapper(target_script, config, workspace, output_dir)
# Build Atheris command
cmd = [sys.executable, str(wrapper_script)]
# Add corpus directory
corpus_dir = config.get("corpus_dir")
if corpus_dir:
corpus_path = workspace / corpus_dir
if corpus_path.exists():
cmd.append(str(corpus_path))
# Set up environment
env = self._setup_atheris_environment(config)
logger.debug(f"Running command: {' '.join(cmd)}")
# Run Atheris with timeout
max_total_time = config.get("max_total_time", 600)
try:
process = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=workspace,
env=env
)
# Wait for specified time then terminate
try:
stdout, stderr = await asyncio.wait_for(
process.communicate(), timeout=max_total_time
)
except asyncio.TimeoutError:
logger.info(f"Atheris fuzzing timed out after {max_total_time} seconds")
process.terminate()
try:
await asyncio.wait_for(process.wait(), timeout=10)
except asyncio.TimeoutError:
process.kill()
await process.wait()
# Parse results
findings = self._parse_atheris_output(
stdout.decode(), stderr.decode(), output_dir, workspace
)
# Look for crash files
crash_findings = self._parse_crash_files(output_dir, workspace)
findings.extend(crash_findings)
except Exception as e:
logger.warning(f"Error running Atheris process: {e}")
except Exception as e:
logger.warning(f"Error in Atheris fuzzing: {e}")
return findings
async def _create_atheris_wrapper(self, target_script: Path, config: Dict[str, Any], workspace: Path, output_dir: Path) -> Path:
"""Create wrapper script for Atheris fuzzing"""
wrapper_path = workspace / "atheris_wrapper.py"
wrapper_code = f'''#!/usr/bin/env python3
import sys
import os
import atheris
import traceback
# Add Python paths
python_paths = {config.get("python_path", [])}
for path in python_paths:
if path not in sys.path:
sys.path.insert(0, path)
# Add workspace to Python path
sys.path.insert(0, r"{workspace}")
# Setup code
setup_code = """{config.get("setup_code", "")}"""
if setup_code:
exec(setup_code)
# Import target script
target_module_name = "{target_script.stem}"
sys.path.insert(0, r"{target_script.parent}")
try:
target_module = __import__(target_module_name)
target_function = getattr(target_module, "{config.get("target_function", "TestOneInput")}")
except Exception as e:
print(f"Failed to import target: {{e}}")
sys.exit(1)
# Wrapper function to catch exceptions
original_target = target_function
def wrapped_target(data):
try:
return original_target(data)
except Exception as e:
# Write crash information
crash_info = {{
"exception_type": type(e).__name__,
"exception_message": str(e),
"stack_trace": traceback.format_exc(),
"input_data": data[:1000].hex() if isinstance(data, bytes) else str(data)[:1000]
}}
crash_file = r"{output_dir}" + "/crash_" + type(e).__name__ + ".txt"
with open(crash_file, "a") as f:
f.write(f"Exception: {{type(e).__name__}}\\n")
f.write(f"Message: {{str(e)}}\\n")
f.write(f"Stack trace:\\n{{traceback.format_exc()}}\\n")
f.write(f"Input data (first 1000 chars/bytes): {{crash_info['input_data']}}\\n")
f.write("-" * 80 + "\\n")
# Re-raise to let Atheris handle it
raise
if __name__ == "__main__":
# Configure Atheris
atheris.Setup(sys.argv, wrapped_target)
# Set Atheris options
options = []
options.append(f"-max_total_time={{config.get('max_total_time', 600)}}")
options.append(f"-max_len={{config.get('max_len', 4096)}}")
options.append(f"-timeout={{config.get('timeout', 25)}}")
options.append(f"-runs={{config.get('runs', -1)}}")
if {config.get('jobs', 1)} > 1:
options.append(f"-jobs={{config.get('jobs', 1)}}")
if {config.get('print_final_stats', True)}:
options.append("-print_final_stats=1")
else:
options.append("-print_final_stats=0")
if {config.get('print_pcs', False)}:
options.append("-print_pcs=1")
if {config.get('print_coverage', True)}:
options.append("-print_coverage=1")
artifact_prefix = "{config.get('artifact_prefix', 'crash-')}"
options.append(f"-artifact_prefix={{r'{output_dir}'}}/" + artifact_prefix)
seed = {config.get('seed')}
if seed is not None:
options.append(f"-seed={{seed}}")
if {config.get('enable_value_profile', False)}:
options.append("-use_value_profile=1")
if {config.get('shrink', True)}:
options.append("-shrink=1")
if {config.get('only_ascii', False)}:
options.append("-only_ascii=1")
dict_file = "{config.get('dict_file', '')}"
if dict_file:
dict_path = r"{workspace}" + "/" + dict_file
if os.path.exists(dict_path):
options.append(f"-dict={{dict_path}}")
# Add options to sys.argv
sys.argv.extend(options)
# Start fuzzing
atheris.Fuzz()
'''
with open(wrapper_path, 'w') as f:
f.write(wrapper_code)
return wrapper_path
def _setup_atheris_environment(self, config: Dict[str, Any]) -> Dict[str, str]:
"""Setup environment variables for Atheris"""
env = os.environ.copy()
# Enable sanitizers if requested
if config.get("enable_sanitizers", True):
env["ASAN_OPTIONS"] = env.get("ASAN_OPTIONS", "") + ":detect_leaks=1:halt_on_error=1"
if config.get("detect_leaks", True):
env["ASAN_OPTIONS"] = env.get("ASAN_OPTIONS", "") + ":detect_leaks=1"
if config.get("detect_stack_use_after_return", False):
env["ASAN_OPTIONS"] = env.get("ASAN_OPTIONS", "") + ":detect_stack_use_after_return=1"
return env
def _parse_atheris_output(self, stdout: str, stderr: str, output_dir: Path, workspace: Path) -> List[ModuleFinding]:
"""Parse Atheris output for crashes and issues"""
findings = []
try:
# Combine stdout and stderr
full_output = stdout + "\n" + stderr
# Look for Python exceptions in output
exception_patterns = [
r"Traceback \(most recent call last\):(.*?)(?=\n\w|\nDONE|\n=|\Z)",
r"Exception: (\w+).*?\nMessage: (.*?)\nStack trace:\n(.*?)(?=\n-{20,}|\Z)"
]
for pattern in exception_patterns:
import re
matches = re.findall(pattern, full_output, re.DOTALL | re.MULTILINE)
for match in matches:
finding = self._create_exception_finding(match, full_output, output_dir)
if finding:
findings.append(finding)
except Exception as e:
logger.warning(f"Error parsing Atheris output: {e}")
return findings
def _parse_crash_files(self, output_dir: Path, workspace: Path) -> List[ModuleFinding]:
"""Parse crash files created by wrapper"""
findings = []
try:
# Look for crash files
crash_files = list(output_dir.glob("crash_*.txt"))
for crash_file in crash_files:
findings.extend(self._parse_crash_file(crash_file, workspace))
# Also look for Atheris artifact files
artifact_files = list(output_dir.glob("crash-*"))
for artifact_file in artifact_files:
finding = self._create_artifact_finding(artifact_file, workspace)
if finding:
findings.append(finding)
except Exception as e:
logger.warning(f"Error parsing crash files: {e}")
return findings
def _parse_crash_file(self, crash_file: Path, workspace: Path) -> List[ModuleFinding]:
"""Parse individual crash file"""
findings = []
try:
content = crash_file.read_text()
# Split by separator
crash_entries = content.split("-" * 80)
for entry in crash_entries:
if not entry.strip():
continue
finding = self._parse_crash_entry(entry, crash_file, workspace)
if finding:
findings.append(finding)
except Exception as e:
logger.warning(f"Error parsing crash file {crash_file}: {e}")
return findings
def _parse_crash_entry(self, entry: str, crash_file: Path, workspace: Path) -> ModuleFinding:
"""Parse individual crash entry"""
try:
lines = entry.strip().split('\n')
exception_type = ""
exception_message = ""
stack_trace = ""
input_data = ""
current_section = None
stack_lines = []
for line in lines:
if line.startswith("Exception: "):
exception_type = line.replace("Exception: ", "")
elif line.startswith("Message: "):
exception_message = line.replace("Message: ", "")
elif line.startswith("Stack trace:"):
current_section = "stack"
elif line.startswith("Input data"):
current_section = "input"
input_data = line.split(":", 1)[1].strip() if ":" in line else ""
elif current_section == "stack":
stack_lines.append(line)
stack_trace = '\n'.join(stack_lines)
if not exception_type:
return None
# Determine severity based on exception type
severity = self._get_exception_severity(exception_type)
# Create relative path
try:
rel_path = crash_file.relative_to(workspace)
file_path = str(rel_path)
except ValueError:
file_path = str(crash_file)
finding = self.create_finding(
title=f"Atheris Exception: {exception_type}",
description=f"Atheris discovered a Python exception: {exception_type}{': ' + exception_message if exception_message else ''}",
severity=severity,
category=self._get_exception_category(exception_type),
file_path=file_path,
recommendation=self._get_exception_recommendation(exception_type, exception_message),
metadata={
"exception_type": exception_type,
"exception_message": exception_message,
"stack_trace": stack_trace[:2000] if stack_trace else "", # Limit size
"crash_input_preview": input_data[:500] if input_data else "",
"fuzzer": "atheris"
}
)
return finding
except Exception as e:
logger.warning(f"Error parsing crash entry: {e}")
return None
def _create_exception_finding(self, match, full_output: str, output_dir: Path) -> ModuleFinding:
"""Create finding from exception match"""
try:
if isinstance(match, tuple) and len(match) >= 1:
# Handle different match formats
if len(match) == 3: # Exception format
exception_type, exception_message, stack_trace = match
else:
stack_trace = match[0]
exception_type = "Unknown"
exception_message = ""
else:
stack_trace = str(match)
exception_type = "Unknown"
exception_message = ""
# Try to extract exception type from stack trace
if not exception_type or exception_type == "Unknown":
lines = stack_trace.split('\n')
for line in reversed(lines):
if ':' in line and any(exc in line for exc in ['Error', 'Exception', 'Warning']):
exception_type = line.split(':')[0].strip()
exception_message = line.split(':', 1)[1].strip() if ':' in line else ""
break
severity = self._get_exception_severity(exception_type)
finding = self.create_finding(
title=f"Atheris Exception: {exception_type}",
description=f"Atheris discovered a Python exception during fuzzing: {exception_type}",
severity=severity,
category=self._get_exception_category(exception_type),
file_path=None,
recommendation=self._get_exception_recommendation(exception_type, exception_message),
metadata={
"exception_type": exception_type,
"exception_message": exception_message,
"stack_trace": stack_trace[:2000] if stack_trace else "",
"fuzzer": "atheris"
}
)
return finding
except Exception as e:
logger.warning(f"Error creating exception finding: {e}")
return None
def _create_artifact_finding(self, artifact_file: Path, workspace: Path) -> ModuleFinding:
"""Create finding from Atheris artifact file"""
try:
# Try to read artifact content (limited)
artifact_content = ""
try:
content_bytes = artifact_file.read_bytes()[:1000]
artifact_content = content_bytes.hex()
except Exception:
pass
# Create relative path
try:
rel_path = artifact_file.relative_to(workspace)
file_path = str(rel_path)
except ValueError:
file_path = str(artifact_file)
finding = self.create_finding(
title="Atheris Crash Artifact",
description=f"Atheris generated a crash artifact file: {artifact_file.name}",
severity="medium",
category="program_crash",
file_path=file_path,
recommendation="Analyze the crash artifact to reproduce and debug the issue. The artifact contains the input that caused the crash.",
metadata={
"artifact_type": "crash",
"artifact_file": artifact_file.name,
"artifact_content_hex": artifact_content,
"fuzzer": "atheris"
}
)
return finding
except Exception as e:
logger.warning(f"Error creating artifact finding: {e}")
return None
def _get_exception_severity(self, exception_type: str) -> str:
"""Determine severity based on exception type"""
if not exception_type:
return "medium"
exception_lower = exception_type.lower()
# Critical security issues
if any(term in exception_lower for term in ["segmentationfault", "accessviolation", "memoryerror"]):
return "critical"
# High severity exceptions
elif any(term in exception_lower for term in ["attributeerror", "typeerror", "indexerror", "keyerror", "valueerror"]):
return "high"
# Medium severity exceptions
elif any(term in exception_lower for term in ["assertionerror", "runtimeerror", "ioerror", "oserror"]):
return "medium"
# Lower severity exceptions
elif any(term in exception_lower for term in ["warning", "deprecation"]):
return "low"
else:
return "medium"
def _get_exception_category(self, exception_type: str) -> str:
"""Determine category based on exception type"""
if not exception_type:
return "python_exception"
exception_lower = exception_type.lower()
if any(term in exception_lower for term in ["memory", "segmentation", "access"]):
return "memory_corruption"
elif any(term in exception_lower for term in ["attribute", "type"]):
return "type_error"
elif any(term in exception_lower for term in ["index", "key", "value"]):
return "data_error"
elif any(term in exception_lower for term in ["io", "os", "file"]):
return "io_error"
elif any(term in exception_lower for term in ["assertion"]):
return "assertion_failure"
else:
return "python_exception"
def _get_exception_recommendation(self, exception_type: str, exception_message: str) -> str:
"""Generate recommendation based on exception type"""
if not exception_type:
return "Analyze the exception and fix the underlying code issue."
exception_lower = exception_type.lower()
if "attributeerror" in exception_lower:
return "Fix AttributeError by ensuring objects have the expected attributes before accessing them. Add proper error handling and validation."
elif "typeerror" in exception_lower:
return "Fix TypeError by ensuring correct data types are used. Add type checking and validation for function parameters."
elif "indexerror" in exception_lower:
return "Fix IndexError by adding bounds checking before accessing list/array elements. Validate indices are within valid range."
elif "keyerror" in exception_lower:
return "Fix KeyError by checking if keys exist in dictionaries before accessing them. Use .get() method or proper key validation."
elif "valueerror" in exception_lower:
return "Fix ValueError by validating input values before processing. Add proper input sanitization and validation."
elif "memoryerror" in exception_lower:
return "Fix MemoryError by optimizing memory usage, processing data in chunks, or increasing available memory."
elif "assertionerror" in exception_lower:
return "Fix AssertionError by reviewing assertion conditions and ensuring they properly validate the expected state."
else:
return f"Fix the {exception_type} exception by analyzing the root cause and implementing appropriate error handling and validation."
def _create_summary(self, findings: List[ModuleFinding]) -> Dict[str, Any]:
"""Create analysis summary"""
severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
category_counts = {}
exception_counts = {}
for finding in findings:
# Count by severity
severity_counts[finding.severity] += 1
# Count by category
category = finding.category
category_counts[category] = category_counts.get(category, 0) + 1
# Count by exception type
exception_type = finding.metadata.get("exception_type", "unknown")
exception_counts[exception_type] = exception_counts.get(exception_type, 0) + 1
return {
"total_findings": len(findings),
"severity_counts": severity_counts,
"category_counts": category_counts,
"exception_counts": exception_counts,
"unique_exceptions": len(exception_counts),
"python_specific_issues": sum(category_counts.get(cat, 0) for cat in ["type_error", "data_error", "python_exception"])
}
@@ -0,0 +1,572 @@
"""
Cargo Fuzz Module
This module uses cargo-fuzz for fuzzing Rust code with libFuzzer integration.
"""
# Copyright (c) 2025 FuzzingLabs
#
# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file
# at the root of this repository for details.
#
# After the Change Date (four years from publication), this version of the
# Licensed Work will be made available under the Apache License, Version 2.0.
# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0
#
# Additional attribution and requirements are provided in the NOTICE file.
import asyncio
import json
import os
from pathlib import Path
from typing import Dict, Any, List, Tuple
import subprocess
import logging
import httpx
import re
from datetime import datetime, timedelta
try:
from prefect import get_run_context
except ImportError:
# Fallback for when not running in Prefect context
get_run_context = None
from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult
from . import register_module
logger = logging.getLogger(__name__)
@register_module
class CargoFuzzModule(BaseModule):
"""Cargo Fuzz Rust fuzzing module"""
def get_metadata(self) -> ModuleMetadata:
"""Get module metadata"""
return ModuleMetadata(
name="cargo_fuzz",
version="0.11.2",
description="Rust fuzzing integration with libFuzzer using cargo-fuzz",
author="FuzzForge Team",
category="fuzzing",
tags=["rust", "libfuzzer", "cargo", "coverage-guided", "sanitizers"],
input_schema={
"type": "object",
"properties": {
"project_dir": {
"type": "string",
"description": "Path to Rust project directory (with Cargo.toml)"
},
"fuzz_target": {
"type": "string",
"description": "Name of the fuzz target to run"
},
"max_total_time": {
"type": "integer",
"default": 600,
"description": "Maximum total time to run fuzzing (seconds)"
},
"jobs": {
"type": "integer",
"default": 1,
"description": "Number of worker processes"
},
"corpus_dir": {
"type": "string",
"description": "Custom corpus directory"
},
"artifacts_dir": {
"type": "string",
"description": "Custom artifacts directory"
},
"sanitizer": {
"type": "string",
"enum": ["address", "memory", "thread", "leak", "none"],
"default": "address",
"description": "Sanitizer to use"
},
"release": {
"type": "boolean",
"default": False,
"description": "Use release mode"
},
"debug_assertions": {
"type": "boolean",
"default": True,
"description": "Enable debug assertions"
}
}
},
output_schema={
"type": "object",
"properties": {
"findings": {
"type": "array",
"items": {
"type": "object",
"properties": {
"crash_type": {"type": "string"},
"artifact_path": {"type": "string"},
"stack_trace": {"type": "string"}
}
}
}
}
}
)
def validate_config(self, config: Dict[str, Any]) -> bool:
"""Validate configuration"""
project_dir = config.get("project_dir")
if not project_dir:
raise ValueError("project_dir is required")
fuzz_target = config.get("fuzz_target")
if not fuzz_target:
raise ValueError("fuzz_target is required")
return True
async def execute(self, config: Dict[str, Any], workspace: Path, stats_callback=None) -> ModuleResult:
"""Execute cargo-fuzz fuzzing"""
self.start_timer()
try:
# Initialize last observed stats for summary propagation
self._last_stats = {
'executions': 0,
'executions_per_sec': 0.0,
'crashes': 0,
'corpus_size': 0,
'elapsed_time': 0,
}
# Validate inputs
self.validate_config(config)
self.validate_workspace(workspace)
logger.info("Running cargo-fuzz Rust fuzzing")
# Check installation
await self._check_cargo_fuzz_installation()
# Setup project
project_dir = workspace / config["project_dir"]
await self._setup_cargo_fuzz_project(project_dir, config)
# Run fuzzing
findings = await self._run_cargo_fuzz(project_dir, config, workspace, stats_callback)
# Create summary and enrich with last observed runtime stats
summary = self._create_summary(findings)
try:
summary.update({
'executions': self._last_stats.get('executions', 0),
'executions_per_sec': self._last_stats.get('executions_per_sec', 0.0),
'corpus_size': self._last_stats.get('corpus_size', 0),
'crashes': self._last_stats.get('crashes', 0),
'elapsed_time': self._last_stats.get('elapsed_time', 0),
})
except Exception:
pass
logger.info(f"cargo-fuzz found {len(findings)} issues")
return self.create_result(
findings=findings,
status="success",
summary=summary
)
except Exception as e:
logger.error(f"cargo-fuzz module failed: {e}")
return self.create_result(
findings=[],
status="failed",
error=str(e)
)
async def _check_cargo_fuzz_installation(self):
"""Check if cargo-fuzz is installed"""
try:
process = await asyncio.create_subprocess_exec(
"cargo", "fuzz", "--version",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
raise RuntimeError("cargo-fuzz not installed. Install with: cargo install cargo-fuzz")
except Exception as e:
raise RuntimeError(f"cargo-fuzz installation check failed: {e}")
async def _setup_cargo_fuzz_project(self, project_dir: Path, config: Dict[str, Any]):
"""Setup cargo-fuzz project"""
if not project_dir.exists():
raise FileNotFoundError(f"Project directory not found: {project_dir}")
cargo_toml = project_dir / "Cargo.toml"
if not cargo_toml.exists():
raise FileNotFoundError(f"Cargo.toml not found in {project_dir}")
# Check if fuzz directory exists, if not initialize
fuzz_dir = project_dir / "fuzz"
if not fuzz_dir.exists():
logger.info("Initializing cargo-fuzz project")
process = await asyncio.create_subprocess_exec(
"cargo", "fuzz", "init",
cwd=project_dir,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
await process.communicate()
async def _run_cargo_fuzz(self, project_dir: Path, config: Dict[str, Any], workspace: Path, stats_callback=None) -> List[ModuleFinding]:
"""Run cargo-fuzz with real-time statistics reporting"""
findings = []
# Get run_id from Prefect context for statistics reporting
run_id = None
if get_run_context:
try:
context = get_run_context()
run_id = str(context.flow_run.id)
except Exception:
logger.warning("Could not get run_id from Prefect context")
try:
# Build command
cmd = ["cargo", "fuzz", "run", config["fuzz_target"]]
# Add options
if config.get("jobs", 1) > 1:
cmd.extend(["--", f"-jobs={config['jobs']}"])
max_time = config.get("max_total_time", 600)
cmd.extend(["--", f"-max_total_time={max_time}"])
# Set sanitizer
sanitizer = config.get("sanitizer", "address")
if sanitizer != "none":
cmd.append(f"--sanitizer={sanitizer}")
if config.get("release", False):
cmd.append("--release")
# Set environment
env = os.environ.copy()
if config.get("debug_assertions", True):
env["RUSTFLAGS"] = env.get("RUSTFLAGS", "") + " -C debug-assertions=on"
logger.debug(f"Running command: {' '.join(cmd)}")
# Run with streaming output processing for real-time stats
try:
process = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT, # Merge stderr into stdout
cwd=project_dir,
env=env
)
# Process output in real-time
stdout_data, stderr_data = await self._process_streaming_output(
process, max_time, config, stats_callback
)
# Parse final results
findings = self._parse_cargo_fuzz_output(
stdout_data, stderr_data, project_dir, workspace, config
)
except Exception as e:
logger.warning(f"Error running cargo-fuzz: {e}")
except Exception as e:
logger.warning(f"Error in cargo-fuzz execution: {e}")
return findings
def _parse_cargo_fuzz_output(self, stdout: str, stderr: str, project_dir: Path, workspace: Path, config: Dict[str, Any]) -> List[ModuleFinding]:
"""Parse cargo-fuzz output"""
findings = []
try:
full_output = stdout + "\n" + stderr
# Look for crash artifacts
artifacts_dir = project_dir / "fuzz" / "artifacts" / config["fuzz_target"]
if artifacts_dir.exists():
for artifact in artifacts_dir.iterdir():
if artifact.is_file():
finding = self._create_artifact_finding(artifact, workspace, full_output)
if finding:
findings.append(finding)
except Exception as e:
logger.warning(f"Error parsing cargo-fuzz output: {e}")
return findings
def _create_artifact_finding(self, artifact_path: Path, workspace: Path, output: str) -> ModuleFinding:
"""Create finding from artifact file"""
try:
# Try to determine crash type from filename or content
crash_type = "crash"
if "leak" in artifact_path.name.lower():
crash_type = "memory_leak"
elif "timeout" in artifact_path.name.lower():
crash_type = "timeout"
# Extract stack trace from output
stack_trace = self._extract_stack_trace_from_output(output, artifact_path.name)
try:
rel_path = artifact_path.relative_to(workspace)
file_path = str(rel_path)
except ValueError:
file_path = str(artifact_path)
severity = "high" if "crash" in crash_type else "medium"
finding = self.create_finding(
title=f"cargo-fuzz {crash_type.title()}",
description=f"cargo-fuzz discovered a {crash_type} in the Rust code",
severity=severity,
category=self._get_crash_category(crash_type),
file_path=file_path,
recommendation=self._get_crash_recommendation(crash_type),
metadata={
"crash_type": crash_type,
"artifact_path": str(artifact_path),
"stack_trace": stack_trace,
"fuzzer": "cargo_fuzz"
}
)
return finding
except Exception as e:
logger.warning(f"Error creating artifact finding: {e}")
return None
def _extract_stack_trace_from_output(self, output: str, artifact_name: str) -> str:
"""Extract stack trace from output"""
try:
lines = output.split('\n')
stack_lines = []
in_stack = False
for line in lines:
if artifact_name in line or "stack backtrace:" in line.lower():
in_stack = True
continue
if in_stack:
if line.strip() and ("at " in line or "::" in line or line.strip().startswith("0:")):
stack_lines.append(line.strip())
elif not line.strip() and stack_lines:
break
return '\n'.join(stack_lines[:20]) # Limit stack trace size
except Exception:
return ""
def _get_crash_category(self, crash_type: str) -> str:
"""Get category for crash type"""
if "leak" in crash_type:
return "memory_leak"
elif "timeout" in crash_type:
return "performance_issues"
else:
return "memory_safety"
def _get_crash_recommendation(self, crash_type: str) -> str:
"""Get recommendation for crash type"""
if "leak" in crash_type:
return "Fix memory leak by ensuring proper cleanup of allocated resources. Review memory management patterns."
elif "timeout" in crash_type:
return "Fix timeout by optimizing performance, avoiding infinite loops, and implementing reasonable bounds."
else:
return "Fix the crash by analyzing the stack trace and addressing memory safety issues."
async def _process_streaming_output(self, process, max_time: int, config: Dict[str, Any], stats_callback=None) -> Tuple[str, str]:
"""Process cargo-fuzz output in real-time and report statistics"""
stdout_lines = []
start_time = datetime.utcnow()
last_update = start_time
stats_data = {
'executions': 0,
'executions_per_sec': 0.0,
'crashes': 0,
'corpus_size': 0,
'elapsed_time': 0
}
# Get run_id from Prefect context for statistics reporting
run_id = None
if get_run_context:
try:
context = get_run_context()
run_id = str(context.flow_run.id)
except Exception:
logger.debug("Could not get run_id from Prefect context")
try:
# Emit an initial baseline update so dashboards show activity immediately
try:
await self._send_stats_via_callback(stats_callback, run_id, stats_data)
except Exception:
pass
# Monitor process output in chunks to capture libFuzzer carriage-return updates
buffer = ""
while True:
try:
chunk = await asyncio.wait_for(process.stdout.read(4096), timeout=1.0)
if not chunk:
# Process finished
break
buffer += chunk.decode('utf-8', errors='ignore')
# Split on both newline and carriage return
if "\n" in buffer or "\r" in buffer:
parts = re.split(r"[\r\n]", buffer)
buffer = parts[-1]
for part in parts[:-1]:
line = part.strip()
if not line:
continue
stdout_lines.append(line)
self._parse_stats_from_line(line, stats_data)
except asyncio.TimeoutError:
# No output this second; continue to periodic update check
pass
# Periodic update (even if there was no output)
current_time = datetime.utcnow()
stats_data['elapsed_time'] = int((current_time - start_time).total_seconds())
if current_time - last_update >= timedelta(seconds=3):
try:
self._last_stats = dict(stats_data)
except Exception:
pass
await self._send_stats_via_callback(stats_callback, run_id, stats_data)
last_update = current_time
# Check if max time exceeded
if stats_data['elapsed_time'] >= max_time:
logger.info("Max time reached, terminating cargo-fuzz")
process.terminate()
break
# Wait for process to complete
await process.wait()
# Send final stats update
try:
self._last_stats = dict(stats_data)
except Exception:
pass
await self._send_stats_via_callback(stats_callback, run_id, stats_data)
except Exception as e:
logger.warning(f"Error processing streaming output: {e}")
stdout_data = '\n'.join(stdout_lines)
return stdout_data, ""
def _parse_stats_from_line(self, line: str, stats_data: Dict[str, Any]):
"""Parse statistics from a cargo-fuzz output line"""
try:
# cargo-fuzz typically shows stats like:
# "#12345: DONE cov: 1234 ft: 5678 corp: 9/10Mb exec/s: 1500 rss: 234Mb"
# "#12345: NEW cov: 1234 ft: 5678 corp: 9/10Mb exec/s: 1500 rss: 234Mb L: 45/67 MS: 3 ..."
# Extract execution count (the #number)
exec_match = re.search(r'#(\d+)(?::)?', line)
if exec_match:
stats_data['executions'] = int(exec_match.group(1))
else:
# libFuzzer stats format alternative
exec_alt = re.search(r'stat::number_of_executed_units:\s*(\d+)', line)
if exec_alt:
stats_data['executions'] = int(exec_alt.group(1))
else:
exec_alt2 = re.search(r'executed units:?\s*(\d+)', line, re.IGNORECASE)
if exec_alt2:
stats_data['executions'] = int(exec_alt2.group(1))
# Extract executions per second
exec_per_sec_match = re.search(r'exec/s:\s*([0-9\.]+)', line)
if exec_per_sec_match:
stats_data['executions_per_sec'] = float(exec_per_sec_match.group(1))
else:
eps_alt = re.search(r'stat::execs_per_sec:\s*([0-9\.]+)', line)
if eps_alt:
stats_data['executions_per_sec'] = float(eps_alt.group(1))
# Extract corpus size (corp: X/YMb)
corp_match = re.search(r'corp(?:us)?:\s*(\d+)', line)
if corp_match:
stats_data['corpus_size'] = int(corp_match.group(1))
# Look for crash indicators
if any(keyword in line.lower() for keyword in ['crash', 'assert', 'panic', 'abort']):
stats_data['crashes'] += 1
except Exception as e:
logger.debug(f"Error parsing stats from line '{line}': {e}")
async def _send_stats_via_callback(self, stats_callback, run_id: str, stats_data: Dict[str, Any]):
"""Send statistics update via callback function"""
if not stats_callback or not run_id:
return
try:
# Prepare statistics payload
stats_payload = {
"run_id": run_id,
"workflow": "language_fuzzing",
"executions": stats_data['executions'],
"executions_per_sec": stats_data['executions_per_sec'],
"crashes": stats_data['crashes'],
"unique_crashes": stats_data['crashes'], # Assume all crashes are unique for now
"corpus_size": stats_data['corpus_size'],
"elapsed_time": stats_data['elapsed_time'],
"timestamp": datetime.utcnow().isoformat()
}
# Call the callback function provided by the Prefect task
await stats_callback(stats_payload)
logger.info(
"LIVE STATS SENT: exec=%s eps=%.2f crashes=%s corpus=%s elapsed=%s",
stats_data['executions'],
stats_data['executions_per_sec'],
stats_data['crashes'],
stats_data['corpus_size'],
stats_data['elapsed_time'],
)
except Exception as e:
logger.debug(f"Error sending stats via callback: {e}")
def _create_summary(self, findings: List[ModuleFinding]) -> Dict[str, Any]:
"""Create analysis summary"""
severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
category_counts = {}
for finding in findings:
severity_counts[finding.severity] += 1
category_counts[finding.category] = category_counts.get(finding.category, 0) + 1
return {
"total_findings": len(findings),
"severity_counts": severity_counts,
"category_counts": category_counts
}
+384
View File
@@ -0,0 +1,384 @@
"""
Go-Fuzz Module
This module uses go-fuzz for coverage-guided fuzzing of Go packages.
"""
# Copyright (c) 2025 FuzzingLabs
#
# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file
# at the root of this repository for details.
#
# After the Change Date (four years from publication), this version of the
# Licensed Work will be made available under the Apache License, Version 2.0.
# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0
#
# Additional attribution and requirements are provided in the NOTICE file.
import asyncio
import json
import os
from pathlib import Path
from typing import Dict, Any, List
import subprocess
import logging
from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult
from . import register_module
logger = logging.getLogger(__name__)
@register_module
class GoFuzzModule(BaseModule):
"""Go-Fuzz Go language fuzzing module"""
def get_metadata(self) -> ModuleMetadata:
"""Get module metadata"""
return ModuleMetadata(
name="go_fuzz",
version="1.2.0",
description="Coverage-guided fuzzing for Go packages using go-fuzz",
author="FuzzForge Team",
category="fuzzing",
tags=["go", "golang", "coverage-guided", "packages"],
input_schema={
"type": "object",
"properties": {
"package_path": {
"type": "string",
"description": "Path to Go package to fuzz"
},
"fuzz_function": {
"type": "string",
"default": "Fuzz",
"description": "Name of the fuzz function"
},
"workdir": {
"type": "string",
"default": "go_fuzz_workdir",
"description": "Working directory for go-fuzz"
},
"procs": {
"type": "integer",
"default": 1,
"description": "Number of parallel processes"
},
"timeout": {
"type": "integer",
"default": 600,
"description": "Total fuzzing timeout (seconds)"
},
"race": {
"type": "boolean",
"default": false,
"description": "Enable race detector"
},
"minimize": {
"type": "boolean",
"default": true,
"description": "Minimize crashers"
},
"sonar": {
"type": "boolean",
"default": false,
"description": "Enable sonar mode"
}
}
},
output_schema={
"type": "object",
"properties": {
"findings": {
"type": "array",
"items": {
"type": "object",
"properties": {
"crash_type": {"type": "string"},
"crash_file": {"type": "string"},
"stack_trace": {"type": "string"}
}
}
}
}
}
)
def validate_config(self, config: Dict[str, Any]) -> bool:
"""Validate configuration"""
package_path = config.get("package_path")
if not package_path:
raise ValueError("package_path is required")
return True
async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult:
"""Execute go-fuzz fuzzing"""
self.start_timer()
try:
# Validate inputs
self.validate_config(config)
self.validate_workspace(workspace)
logger.info("Running go-fuzz Go fuzzing")
# Check installation
await self._check_go_fuzz_installation()
# Setup
package_path = workspace / config["package_path"]
workdir = workspace / config.get("workdir", "go_fuzz_workdir")
# Build and run
findings = await self._run_go_fuzz(package_path, workdir, config, workspace)
# Create summary
summary = self._create_summary(findings)
logger.info(f"go-fuzz found {len(findings)} issues")
return self.create_result(
findings=findings,
status="success",
summary=summary
)
except Exception as e:
logger.error(f"go-fuzz module failed: {e}")
return self.create_result(
findings=[],
status="failed",
error=str(e)
)
async def _check_go_fuzz_installation(self):
"""Check if go-fuzz is installed"""
try:
process = await asyncio.create_subprocess_exec(
"go-fuzz", "--help",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
await process.communicate()
if process.returncode != 0:
# Try building
process = await asyncio.create_subprocess_exec(
"go", "install", "github.com/dvyukov/go-fuzz/go-fuzz@latest",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
await process.communicate()
except Exception as e:
raise RuntimeError(f"go-fuzz installation failed: {e}")
async def _run_go_fuzz(self, package_path: Path, workdir: Path, config: Dict[str, Any], workspace: Path) -> List[ModuleFinding]:
"""Run go-fuzz"""
findings = []
try:
# Create workdir
workdir.mkdir(exist_ok=True)
# Build
await self._build_go_fuzz(package_path, config)
# Run fuzzing
cmd = ["go-fuzz", "-bin", f"{package_path.name}-fuzz.zip", "-workdir", str(workdir)]
if config.get("procs", 1) > 1:
cmd.extend(["-procs", str(config["procs"])])
if config.get("race", False):
cmd.append("-race")
if config.get("sonar", False):
cmd.append("-sonar")
timeout = config.get("timeout", 600)
try:
process = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=package_path.parent
)
try:
stdout, stderr = await asyncio.wait_for(
process.communicate(), timeout=timeout
)
except asyncio.TimeoutError:
process.terminate()
await process.wait()
# Parse results
findings = self._parse_go_fuzz_results(workdir, workspace, config)
except Exception as e:
logger.warning(f"Error running go-fuzz: {e}")
except Exception as e:
logger.warning(f"Error in go-fuzz execution: {e}")
return findings
async def _build_go_fuzz(self, package_path: Path, config: Dict[str, Any]):
"""Build go-fuzz binary"""
cmd = ["go-fuzz-build"]
if config.get("race", False):
cmd.append("-race")
process = await asyncio.create_subprocess_exec(
*cmd,
cwd=package_path,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
raise RuntimeError(f"go-fuzz-build failed: {stderr.decode()}")
def _parse_go_fuzz_results(self, workdir: Path, workspace: Path, config: Dict[str, Any]) -> List[ModuleFinding]:
"""Parse go-fuzz results"""
findings = []
try:
# Look for crashers
crashers_dir = workdir / "crashers"
if crashers_dir.exists():
for crash_file in crashers_dir.iterdir():
if crash_file.is_file() and not crash_file.name.startswith("."):
finding = self._create_crash_finding(crash_file, workspace)
if finding:
findings.append(finding)
# Look for suppressions (potential issues)
suppressions_dir = workdir / "suppressions"
if suppressions_dir.exists():
for supp_file in suppressions_dir.iterdir():
if supp_file.is_file():
finding = self._create_suppression_finding(supp_file, workspace)
if finding:
findings.append(finding)
except Exception as e:
logger.warning(f"Error parsing go-fuzz results: {e}")
return findings
def _create_crash_finding(self, crash_file: Path, workspace: Path) -> ModuleFinding:
"""Create finding from crash file"""
try:
# Read crash output
crash_content = ""
if crash_file.name.endswith(".output"):
crash_content = crash_file.read_text()
# Determine crash type
crash_type = "panic"
if "runtime error" in crash_content:
crash_type = "runtime_error"
elif "race" in crash_content:
crash_type = "race_condition"
try:
rel_path = crash_file.relative_to(workspace)
file_path = str(rel_path)
except ValueError:
file_path = str(crash_file)
finding = self.create_finding(
title=f"go-fuzz {crash_type.title()}",
description=f"go-fuzz discovered a {crash_type} in the Go code",
severity=self._get_crash_severity(crash_type),
category=self._get_crash_category(crash_type),
file_path=file_path,
recommendation=self._get_crash_recommendation(crash_type),
metadata={
"crash_type": crash_type,
"crash_file": str(crash_file),
"stack_trace": crash_content[:1000],
"fuzzer": "go_fuzz"
}
)
return finding
except Exception as e:
logger.warning(f"Error creating crash finding: {e}")
return None
def _create_suppression_finding(self, supp_file: Path, workspace: Path) -> ModuleFinding:
"""Create finding from suppression file"""
try:
try:
rel_path = supp_file.relative_to(workspace)
file_path = str(rel_path)
except ValueError:
file_path = str(supp_file)
finding = self.create_finding(
title="go-fuzz Potential Issue",
description="go-fuzz identified a potential issue that was suppressed",
severity="low",
category="potential_issue",
file_path=file_path,
recommendation="Review suppressed issue to determine if it requires attention.",
metadata={
"suppression_file": str(supp_file),
"fuzzer": "go_fuzz"
}
)
return finding
except Exception as e:
logger.warning(f"Error creating suppression finding: {e}")
return None
def _get_crash_severity(self, crash_type: str) -> str:
"""Get crash severity"""
if crash_type == "race_condition":
return "high"
elif crash_type == "runtime_error":
return "high"
else:
return "medium"
def _get_crash_category(self, crash_type: str) -> str:
"""Get crash category"""
if crash_type == "race_condition":
return "race_condition"
elif crash_type == "runtime_error":
return "runtime_error"
else:
return "program_crash"
def _get_crash_recommendation(self, crash_type: str) -> str:
"""Get crash recommendation"""
if crash_type == "race_condition":
return "Fix race condition by adding proper synchronization (mutexes, channels, etc.)"
elif crash_type == "runtime_error":
return "Fix runtime error by adding bounds checking and proper error handling"
else:
return "Analyze the crash and fix the underlying issue"
def _create_summary(self, findings: List[ModuleFinding]) -> Dict[str, Any]:
"""Create analysis summary"""
severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
category_counts = {}
for finding in findings:
severity_counts[finding.severity] += 1
category_counts[finding.category] = category_counts.get(finding.category, 0) + 1
return {
"total_findings": len(findings),
"severity_counts": severity_counts,
"category_counts": category_counts
}
@@ -0,0 +1,705 @@
"""
LibFuzzer Fuzzing Module
This module uses LibFuzzer (LLVM's coverage-guided fuzzing engine) to find
bugs and security vulnerabilities in C/C++ code.
"""
# Copyright (c) 2025 FuzzingLabs
#
# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file
# at the root of this repository for details.
#
# After the Change Date (four years from publication), this version of the
# Licensed Work will be made available under the Apache License, Version 2.0.
# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0
#
# Additional attribution and requirements are provided in the NOTICE file.
import asyncio
import json
import os
from pathlib import Path
from typing import Dict, Any, List
import subprocess
import logging
import re
from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult
from . import register_module
logger = logging.getLogger(__name__)
@register_module
class LibFuzzerModule(BaseModule):
"""LibFuzzer coverage-guided fuzzing module"""
def get_metadata(self) -> ModuleMetadata:
"""Get module metadata"""
return ModuleMetadata(
name="libfuzzer",
version="17.0.0",
description="LLVM's coverage-guided fuzzing engine for finding bugs in C/C++ code",
author="FuzzForge Team",
category="fuzzing",
tags=["coverage-guided", "c", "cpp", "llvm", "sanitizers", "memory-safety"],
input_schema={
"type": "object",
"properties": {
"target_binary": {
"type": "string",
"description": "Path to the fuzz target binary (compiled with -fsanitize=fuzzer)"
},
"corpus_dir": {
"type": "string",
"description": "Directory containing initial corpus files"
},
"dict_file": {
"type": "string",
"description": "Dictionary file for fuzzing keywords"
},
"max_total_time": {
"type": "integer",
"default": 600,
"description": "Maximum total time to run fuzzing (seconds)"
},
"max_len": {
"type": "integer",
"default": 4096,
"description": "Maximum length of test input"
},
"timeout": {
"type": "integer",
"default": 25,
"description": "Timeout for individual test cases (seconds)"
},
"runs": {
"type": "integer",
"default": -1,
"description": "Number of individual test runs (-1 for unlimited)"
},
"jobs": {
"type": "integer",
"default": 1,
"description": "Number of fuzzing jobs to run in parallel"
},
"workers": {
"type": "integer",
"default": 1,
"description": "Number of workers for parallel fuzzing"
},
"reload": {
"type": "integer",
"default": 1,
"description": "Reload the main corpus periodically"
},
"print_final_stats": {
"type": "boolean",
"default": true,
"description": "Print final statistics"
},
"print_pcs": {
"type": "boolean",
"default": false,
"description": "Print newly covered PCs"
},
"print_funcs": {
"type": "boolean",
"default": false,
"description": "Print newly covered functions"
},
"print_coverage": {
"type": "boolean",
"default": true,
"description": "Print coverage information"
},
"shrink": {
"type": "boolean",
"default": true,
"description": "Try to shrink the corpus"
},
"reduce_inputs": {
"type": "boolean",
"default": true,
"description": "Try to reduce the size of inputs"
},
"use_value_profile": {
"type": "boolean",
"default": false,
"description": "Use value profile for fuzzing"
},
"sanitizers": {
"type": "array",
"items": {"type": "string", "enum": ["address", "memory", "undefined", "thread", "leak"]},
"default": ["address"],
"description": "Sanitizers to use during fuzzing"
},
"artifact_prefix": {
"type": "string",
"default": "crash-",
"description": "Prefix for artifact files"
},
"exact_artifact_path": {
"type": "string",
"description": "Exact path for artifact files"
},
"fork": {
"type": "integer",
"default": 0,
"description": "Fork mode (number of simultaneous processes)"
},
"ignore_crashes": {
"type": "boolean",
"default": false,
"description": "Ignore crashes and continue fuzzing"
},
"ignore_timeouts": {
"type": "boolean",
"default": false,
"description": "Ignore timeouts and continue fuzzing"
},
"ignore_ooms": {
"type": "boolean",
"default": false,
"description": "Ignore out-of-memory and continue fuzzing"
}
}
},
output_schema={
"type": "object",
"properties": {
"findings": {
"type": "array",
"items": {
"type": "object",
"properties": {
"crash_type": {"type": "string"},
"crash_file": {"type": "string"},
"stack_trace": {"type": "string"},
"sanitizer": {"type": "string"}
}
}
}
}
}
)
def validate_config(self, config: Dict[str, Any]) -> bool:
"""Validate configuration"""
target_binary = config.get("target_binary")
if not target_binary:
raise ValueError("target_binary is required for LibFuzzer")
max_total_time = config.get("max_total_time", 600)
if max_total_time <= 0:
raise ValueError("max_total_time must be positive")
return True
async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult:
"""Execute LibFuzzer fuzzing"""
self.start_timer()
try:
# Validate inputs
self.validate_config(config)
self.validate_workspace(workspace)
logger.info("Running LibFuzzer fuzzing campaign")
# Check if target binary exists
target_binary = workspace / config["target_binary"]
if not target_binary.exists():
raise FileNotFoundError(f"Target binary not found: {target_binary}")
# Run LibFuzzer
findings = await self._run_libfuzzer(target_binary, config, workspace)
# Create summary
summary = self._create_summary(findings)
logger.info(f"LibFuzzer found {len(findings)} issues")
return self.create_result(
findings=findings,
status="success",
summary=summary
)
except Exception as e:
logger.error(f"LibFuzzer module failed: {e}")
return self.create_result(
findings=[],
status="failed",
error=str(e)
)
async def _run_libfuzzer(self, target_binary: Path, config: Dict[str, Any], workspace: Path) -> List[ModuleFinding]:
"""Run LibFuzzer fuzzing"""
findings = []
try:
# Create output directory for artifacts
output_dir = workspace / "libfuzzer_output"
output_dir.mkdir(exist_ok=True)
# Build LibFuzzer command
cmd = [str(target_binary)]
# Add corpus directory
corpus_dir = config.get("corpus_dir")
if corpus_dir:
corpus_path = workspace / corpus_dir
if corpus_path.exists():
cmd.append(str(corpus_path))
else:
logger.warning(f"Corpus directory not found: {corpus_path}")
# Add dictionary file
dict_file = config.get("dict_file")
if dict_file:
dict_path = workspace / dict_file
if dict_path.exists():
cmd.append(f"-dict={dict_path}")
# Add fuzzing parameters
cmd.append(f"-max_total_time={config.get('max_total_time', 600)}")
cmd.append(f"-max_len={config.get('max_len', 4096)}")
cmd.append(f"-timeout={config.get('timeout', 25)}")
cmd.append(f"-runs={config.get('runs', -1)}")
if config.get("jobs", 1) > 1:
cmd.append(f"-jobs={config['jobs']}")
if config.get("workers", 1) > 1:
cmd.append(f"-workers={config['workers']}")
cmd.append(f"-reload={config.get('reload', 1)}")
# Add output options
if config.get("print_final_stats", True):
cmd.append("-print_final_stats=1")
if config.get("print_pcs", False):
cmd.append("-print_pcs=1")
if config.get("print_funcs", False):
cmd.append("-print_funcs=1")
if config.get("print_coverage", True):
cmd.append("-print_coverage=1")
# Add corpus management options
if config.get("shrink", True):
cmd.append("-shrink=1")
if config.get("reduce_inputs", True):
cmd.append("-reduce_inputs=1")
if config.get("use_value_profile", False):
cmd.append("-use_value_profile=1")
# Add artifact options
artifact_prefix = config.get("artifact_prefix", "crash-")
cmd.append(f"-artifact_prefix={output_dir / artifact_prefix}")
exact_artifact_path = config.get("exact_artifact_path")
if exact_artifact_path:
cmd.append(f"-exact_artifact_path={output_dir / exact_artifact_path}")
# Add fork mode
fork = config.get("fork", 0)
if fork > 0:
cmd.append(f"-fork={fork}")
# Add ignore options
if config.get("ignore_crashes", False):
cmd.append("-ignore_crashes=1")
if config.get("ignore_timeouts", False):
cmd.append("-ignore_timeouts=1")
if config.get("ignore_ooms", False):
cmd.append("-ignore_ooms=1")
# Set up environment for sanitizers
env = os.environ.copy()
sanitizers = config.get("sanitizers", ["address"])
self._setup_sanitizer_environment(env, sanitizers)
logger.debug(f"Running command: {' '.join(cmd)}")
# Run LibFuzzer
process = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=workspace,
env=env
)
stdout, stderr = await process.communicate()
# Parse results
findings = self._parse_libfuzzer_output(
stdout.decode(), stderr.decode(), output_dir, workspace, sanitizers
)
# Look for crash files
crash_findings = self._parse_crash_files(output_dir, workspace, sanitizers)
findings.extend(crash_findings)
except Exception as e:
logger.warning(f"Error running LibFuzzer: {e}")
return findings
def _setup_sanitizer_environment(self, env: Dict[str, str], sanitizers: List[str]):
"""Set up environment variables for sanitizers"""
if "address" in sanitizers:
env["ASAN_OPTIONS"] = env.get("ASAN_OPTIONS", "") + ":halt_on_error=0:abort_on_error=1"
if "memory" in sanitizers:
env["MSAN_OPTIONS"] = env.get("MSAN_OPTIONS", "") + ":halt_on_error=0:abort_on_error=1"
if "undefined" in sanitizers:
env["UBSAN_OPTIONS"] = env.get("UBSAN_OPTIONS", "") + ":halt_on_error=0:abort_on_error=1"
if "thread" in sanitizers:
env["TSAN_OPTIONS"] = env.get("TSAN_OPTIONS", "") + ":halt_on_error=0:abort_on_error=1"
if "leak" in sanitizers:
env["LSAN_OPTIONS"] = env.get("LSAN_OPTIONS", "") + ":halt_on_error=0:abort_on_error=1"
def _parse_libfuzzer_output(self, stdout: str, stderr: str, output_dir: Path, workspace: Path, sanitizers: List[str]) -> List[ModuleFinding]:
"""Parse LibFuzzer output for crashes and issues"""
findings = []
try:
# Combine stdout and stderr for analysis
full_output = stdout + "\n" + stderr
# Look for crash indicators
crash_patterns = [
r"ERROR: AddressSanitizer: (.+)",
r"ERROR: MemorySanitizer: (.+)",
r"ERROR: UndefinedBehaviorSanitizer: (.+)",
r"ERROR: ThreadSanitizer: (.+)",
r"ERROR: LeakSanitizer: (.+)",
r"SUMMARY: (.+Sanitizer): (.+)",
r"==\d+==ERROR: libFuzzer: (.+)"
]
for pattern in crash_patterns:
matches = re.finditer(pattern, full_output, re.MULTILINE)
for match in matches:
finding = self._create_crash_finding(
match, full_output, output_dir, sanitizers
)
if finding:
findings.append(finding)
# Look for timeout and OOM issues
if "TIMEOUT" in full_output:
finding = self._create_timeout_finding(full_output, output_dir)
if finding:
findings.append(finding)
if "out-of-memory" in full_output.lower() or "oom" in full_output.lower():
finding = self._create_oom_finding(full_output, output_dir)
if finding:
findings.append(finding)
except Exception as e:
logger.warning(f"Error parsing LibFuzzer output: {e}")
return findings
def _parse_crash_files(self, output_dir: Path, workspace: Path, sanitizers: List[str]) -> List[ModuleFinding]:
"""Parse crash artifact files"""
findings = []
try:
# Look for crash files
crash_patterns = ["crash-*", "leak-*", "timeout-*", "oom-*"]
for pattern in crash_patterns:
crash_files = list(output_dir.glob(pattern))
for crash_file in crash_files:
finding = self._create_artifact_finding(crash_file, workspace, sanitizers)
if finding:
findings.append(finding)
except Exception as e:
logger.warning(f"Error parsing crash files: {e}")
return findings
def _create_crash_finding(self, match, full_output: str, output_dir: Path, sanitizers: List[str]) -> ModuleFinding:
"""Create finding from crash match"""
try:
crash_type = match.group(1) if match.groups() else "Unknown crash"
# Extract stack trace
stack_trace = self._extract_stack_trace(full_output, match.start())
# Determine sanitizer
sanitizer = self._identify_sanitizer(match.group(0), sanitizers)
# Determine severity based on crash type
severity = self._get_crash_severity(crash_type, sanitizer)
# Create finding
finding = self.create_finding(
title=f"LibFuzzer Crash: {crash_type}",
description=f"LibFuzzer detected a crash with {sanitizer}: {crash_type}",
severity=severity,
category=self._get_crash_category(crash_type),
file_path=None, # LibFuzzer doesn't always provide specific files
recommendation=self._get_crash_recommendation(crash_type, sanitizer),
metadata={
"crash_type": crash_type,
"sanitizer": sanitizer,
"stack_trace": stack_trace[:2000] if stack_trace else "", # Limit size
"fuzzer": "libfuzzer"
}
)
return finding
except Exception as e:
logger.warning(f"Error creating crash finding: {e}")
return None
def _create_timeout_finding(self, output: str, output_dir: Path) -> ModuleFinding:
"""Create finding for timeout issues"""
try:
finding = self.create_finding(
title="LibFuzzer Timeout",
description="LibFuzzer detected a timeout during fuzzing, indicating potential infinite loop or performance issue",
severity="medium",
category="performance_issues",
file_path=None,
recommendation="Review the code for potential infinite loops, excessive computation, or blocking operations that could cause timeouts.",
metadata={
"issue_type": "timeout",
"fuzzer": "libfuzzer"
}
)
return finding
except Exception as e:
logger.warning(f"Error creating timeout finding: {e}")
return None
def _create_oom_finding(self, output: str, output_dir: Path) -> ModuleFinding:
"""Create finding for out-of-memory issues"""
try:
finding = self.create_finding(
title="LibFuzzer Out-of-Memory",
description="LibFuzzer detected an out-of-memory condition during fuzzing, indicating potential memory leak or excessive allocation",
severity="medium",
category="memory_management",
file_path=None,
recommendation="Review memory allocation patterns, check for memory leaks, and consider implementing proper bounds checking.",
metadata={
"issue_type": "out_of_memory",
"fuzzer": "libfuzzer"
}
)
return finding
except Exception as e:
logger.warning(f"Error creating OOM finding: {e}")
return None
def _create_artifact_finding(self, crash_file: Path, workspace: Path, sanitizers: List[str]) -> ModuleFinding:
"""Create finding from crash artifact file"""
try:
crash_type = crash_file.name.split('-')[0] # e.g., "crash", "leak", "timeout"
# Try to read crash file content (limited)
crash_content = ""
try:
crash_content = crash_file.read_bytes()[:1000].decode('utf-8', errors='ignore')
except Exception:
pass
# Determine severity
severity = self._get_artifact_severity(crash_type)
finding = self.create_finding(
title=f"LibFuzzer Artifact: {crash_type}",
description=f"LibFuzzer generated a {crash_type} artifact file indicating a potential issue",
severity=severity,
category=self._get_crash_category(crash_type),
file_path=str(crash_file.relative_to(workspace)),
recommendation=self._get_artifact_recommendation(crash_type),
metadata={
"artifact_type": crash_type,
"artifact_file": str(crash_file.name),
"crash_content_preview": crash_content,
"fuzzer": "libfuzzer"
}
)
return finding
except Exception as e:
logger.warning(f"Error creating artifact finding: {e}")
return None
def _extract_stack_trace(self, output: str, start_pos: int) -> str:
"""Extract stack trace from output"""
try:
lines = output[start_pos:].split('\n')
stack_lines = []
for line in lines[:50]: # Limit to first 50 lines
if any(indicator in line for indicator in ["#0", "#1", "#2", "at ", "in "]):
stack_lines.append(line.strip())
elif stack_lines and not line.strip():
break
return '\n'.join(stack_lines)
except Exception:
return ""
def _identify_sanitizer(self, crash_line: str, sanitizers: List[str]) -> str:
"""Identify which sanitizer detected the issue"""
crash_lower = crash_line.lower()
if "addresssanitizer" in crash_lower:
return "AddressSanitizer"
elif "memorysanitizer" in crash_lower:
return "MemorySanitizer"
elif "undefinedbehaviorsanitizer" in crash_lower:
return "UndefinedBehaviorSanitizer"
elif "threadsanitizer" in crash_lower:
return "ThreadSanitizer"
elif "leaksanitizer" in crash_lower:
return "LeakSanitizer"
elif "libfuzzer" in crash_lower:
return "LibFuzzer"
else:
return "Unknown"
def _get_crash_severity(self, crash_type: str, sanitizer: str) -> str:
"""Determine severity based on crash type and sanitizer"""
crash_lower = crash_type.lower()
# Critical issues
if any(term in crash_lower for term in ["heap-buffer-overflow", "stack-buffer-overflow", "use-after-free", "double-free"]):
return "critical"
# High severity issues
elif any(term in crash_lower for term in ["heap-use-after-free", "stack-use-after-return", "global-buffer-overflow"]):
return "high"
# Medium severity issues
elif any(term in crash_lower for term in ["uninitialized", "leak", "race", "deadlock"]):
return "medium"
# Default to high for any crash
else:
return "high"
def _get_crash_category(self, crash_type: str) -> str:
"""Determine category based on crash type"""
crash_lower = crash_type.lower()
if any(term in crash_lower for term in ["buffer-overflow", "heap-buffer", "stack-buffer", "global-buffer"]):
return "buffer_overflow"
elif any(term in crash_lower for term in ["use-after-free", "double-free", "invalid-free"]):
return "memory_corruption"
elif any(term in crash_lower for term in ["uninitialized", "uninit"]):
return "uninitialized_memory"
elif any(term in crash_lower for term in ["leak"]):
return "memory_leak"
elif any(term in crash_lower for term in ["race", "data-race"]):
return "race_condition"
elif any(term in crash_lower for term in ["timeout"]):
return "performance_issues"
elif any(term in crash_lower for term in ["oom", "out-of-memory"]):
return "memory_management"
else:
return "memory_safety"
def _get_artifact_severity(self, artifact_type: str) -> str:
"""Determine severity for artifact types"""
if artifact_type == "crash":
return "high"
elif artifact_type == "leak":
return "medium"
elif artifact_type in ["timeout", "oom"]:
return "medium"
else:
return "low"
def _get_crash_recommendation(self, crash_type: str, sanitizer: str) -> str:
"""Generate recommendation based on crash type"""
crash_lower = crash_type.lower()
if "buffer-overflow" in crash_lower:
return "Fix buffer overflow by implementing proper bounds checking, using safe string functions, and validating array indices."
elif "use-after-free" in crash_lower:
return "Fix use-after-free by setting pointers to NULL after freeing, using smart pointers, or redesigning object lifetime management."
elif "double-free" in crash_lower:
return "Fix double-free by ensuring each allocation has exactly one corresponding free, or use RAII patterns."
elif "uninitialized" in crash_lower:
return "Initialize all variables before use and ensure proper constructor implementation."
elif "leak" in crash_lower:
return "Fix memory leak by ensuring all allocated memory is properly freed, use smart pointers, or implement proper cleanup routines."
elif "race" in crash_lower:
return "Fix data race by using proper synchronization mechanisms like mutexes, atomic operations, or lock-free data structures."
else:
return f"Address the {crash_type} issue detected by {sanitizer}. Review code for memory safety and proper resource management."
def _get_artifact_recommendation(self, artifact_type: str) -> str:
"""Generate recommendation for artifact types"""
if artifact_type == "crash":
return "Analyze the crash artifact file to reproduce the issue and identify the root cause. Fix the underlying bug that caused the crash."
elif artifact_type == "leak":
return "Investigate the memory leak by analyzing allocation patterns and ensuring proper cleanup of resources."
elif artifact_type == "timeout":
return "Optimize code performance to prevent timeouts, check for infinite loops, and implement reasonable time limits."
elif artifact_type == "oom":
return "Reduce memory usage, implement proper memory management, and add bounds checking for allocations."
else:
return f"Analyze the {artifact_type} artifact to understand and fix the underlying issue."
def _create_summary(self, findings: List[ModuleFinding]) -> Dict[str, Any]:
"""Create analysis summary"""
severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
category_counts = {}
sanitizer_counts = {}
crash_type_counts = {}
for finding in findings:
# Count by severity
severity_counts[finding.severity] += 1
# Count by category
category = finding.category
category_counts[category] = category_counts.get(category, 0) + 1
# Count by sanitizer
sanitizer = finding.metadata.get("sanitizer", "unknown")
sanitizer_counts[sanitizer] = sanitizer_counts.get(sanitizer, 0) + 1
# Count by crash type
crash_type = finding.metadata.get("crash_type", finding.metadata.get("issue_type", "unknown"))
crash_type_counts[crash_type] = crash_type_counts.get(crash_type, 0) + 1
return {
"total_findings": len(findings),
"severity_counts": severity_counts,
"category_counts": category_counts,
"sanitizer_counts": sanitizer_counts,
"crash_type_counts": crash_type_counts,
"memory_safety_issues": category_counts.get("memory_safety", 0) +
category_counts.get("buffer_overflow", 0) +
category_counts.get("memory_corruption", 0),
"performance_issues": category_counts.get("performance_issues", 0)
}
+547
View File
@@ -0,0 +1,547 @@
"""
OSS-Fuzz Module
This module integrates with Google's OSS-Fuzz for continuous fuzzing
of open source projects.
"""
# Copyright (c) 2025 FuzzingLabs
#
# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file
# at the root of this repository for details.
#
# After the Change Date (four years from publication), this version of the
# Licensed Work will be made available under the Apache License, Version 2.0.
# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0
#
# Additional attribution and requirements are provided in the NOTICE file.
import asyncio
import json
import os
from pathlib import Path
from typing import Dict, Any, List
import subprocess
import logging
from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult
from . import register_module
logger = logging.getLogger(__name__)
@register_module
class OSSFuzzModule(BaseModule):
"""OSS-Fuzz continuous fuzzing module"""
def get_metadata(self) -> ModuleMetadata:
"""Get module metadata"""
return ModuleMetadata(
name="oss_fuzz",
version="1.0.0",
description="Google's continuous fuzzing for open source projects integration",
author="FuzzForge Team",
category="fuzzing",
tags=["oss-fuzz", "continuous", "google", "open-source", "docker"],
input_schema={
"type": "object",
"properties": {
"project_name": {
"type": "string",
"description": "OSS-Fuzz project name"
},
"source_dir": {
"type": "string",
"description": "Source directory to fuzz"
},
"build_script": {
"type": "string",
"default": "build.sh",
"description": "Build script path"
},
"dockerfile": {
"type": "string",
"default": "Dockerfile",
"description": "Dockerfile path"
},
"project_yaml": {
"type": "string",
"default": "project.yaml",
"description": "Project configuration file"
},
"sanitizer": {
"type": "string",
"enum": ["address", "memory", "undefined", "coverage"],
"default": "address",
"description": "Sanitizer to use"
},
"architecture": {
"type": "string",
"enum": ["x86_64", "i386"],
"default": "x86_64",
"description": "Target architecture"
},
"fuzzing_engine": {
"type": "string",
"enum": ["libfuzzer", "afl", "honggfuzz"],
"default": "libfuzzer",
"description": "Fuzzing engine to use"
},
"timeout": {
"type": "integer",
"default": 3600,
"description": "Fuzzing timeout (seconds)"
},
"check_build": {
"type": "boolean",
"default": true,
"description": "Check if build is successful"
},
"reproduce_bugs": {
"type": "boolean",
"default": false,
"description": "Try to reproduce existing bugs"
}
}
},
output_schema={
"type": "object",
"properties": {
"findings": {
"type": "array",
"items": {
"type": "object",
"properties": {
"bug_type": {"type": "string"},
"reproducer": {"type": "string"},
"stack_trace": {"type": "string"},
"sanitizer": {"type": "string"}
}
}
}
}
}
)
def validate_config(self, config: Dict[str, Any]) -> bool:
"""Validate configuration"""
project_name = config.get("project_name")
if not project_name:
raise ValueError("project_name is required")
source_dir = config.get("source_dir")
if not source_dir:
raise ValueError("source_dir is required")
return True
async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult:
"""Execute OSS-Fuzz integration"""
self.start_timer()
try:
# Validate inputs
self.validate_config(config)
self.validate_workspace(workspace)
logger.info("Running OSS-Fuzz integration")
# Check Docker
await self._check_docker()
# Clone/update OSS-Fuzz if needed
oss_fuzz_dir = await self._setup_oss_fuzz(workspace)
# Setup project
await self._setup_project(oss_fuzz_dir, config, workspace)
# Build and run
findings = await self._run_oss_fuzz(oss_fuzz_dir, config, workspace)
# Create summary
summary = self._create_summary(findings)
logger.info(f"OSS-Fuzz found {len(findings)} issues")
return self.create_result(
findings=findings,
status="success",
summary=summary
)
except Exception as e:
logger.error(f"OSS-Fuzz module failed: {e}")
return self.create_result(
findings=[],
status="failed",
error=str(e)
)
async def _check_docker(self):
"""Check if Docker is available"""
try:
process = await asyncio.create_subprocess_exec(
"docker", "--version",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
raise RuntimeError("Docker not available. OSS-Fuzz requires Docker.")
except Exception as e:
raise RuntimeError(f"Docker check failed: {e}")
async def _setup_oss_fuzz(self, workspace: Path) -> Path:
"""Setup OSS-Fuzz repository"""
oss_fuzz_dir = workspace / "oss-fuzz"
if not oss_fuzz_dir.exists():
logger.info("Cloning OSS-Fuzz repository")
process = await asyncio.create_subprocess_exec(
"git", "clone", "https://github.com/google/oss-fuzz.git",
cwd=workspace,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
raise RuntimeError(f"Failed to clone OSS-Fuzz: {stderr.decode()}")
return oss_fuzz_dir
async def _setup_project(self, oss_fuzz_dir: Path, config: Dict[str, Any], workspace: Path):
"""Setup OSS-Fuzz project"""
project_name = config["project_name"]
project_dir = oss_fuzz_dir / "projects" / project_name
# Create project directory if it doesn't exist
project_dir.mkdir(parents=True, exist_ok=True)
# Copy source if provided
source_dir = workspace / config["source_dir"]
if source_dir.exists():
# Create symlink or copy source
logger.info(f"Setting up source directory: {source_dir}")
# Setup required files if they don't exist
await self._create_project_files(project_dir, config, workspace)
async def _create_project_files(self, project_dir: Path, config: Dict[str, Any], workspace: Path):
"""Create required OSS-Fuzz project files"""
# Create Dockerfile if it doesn't exist
dockerfile = project_dir / config.get("dockerfile", "Dockerfile")
if not dockerfile.exists():
dockerfile_content = f'''FROM gcr.io/oss-fuzz-base/base-builder
COPY . $SRC/{config["project_name"]}
WORKDIR $SRC/{config["project_name"]}
COPY {config.get("build_script", "build.sh")} $SRC/
'''
dockerfile.write_text(dockerfile_content)
# Create build.sh if it doesn't exist
build_script = project_dir / config.get("build_script", "build.sh")
if not build_script.exists():
build_content = f'''#!/bin/bash -eu
# Build script for {config["project_name"]}
# Add your build commands here
echo "Building {config['project_name']}..."
'''
build_script.write_text(build_content)
build_script.chmod(0o755)
# Create project.yaml if it doesn't exist
project_yaml = project_dir / config.get("project_yaml", "project.yaml")
if not project_yaml.exists():
yaml_content = f'''homepage: "https://example.com"
language: c++
primary_contact: "security@example.com"
auto_ccs:
- "fuzzing@example.com"
sanitizers:
- {config.get("sanitizer", "address")}
architectures:
- {config.get("architecture", "x86_64")}
fuzzing_engines:
- {config.get("fuzzing_engine", "libfuzzer")}
'''
project_yaml.write_text(yaml_content)
async def _run_oss_fuzz(self, oss_fuzz_dir: Path, config: Dict[str, Any], workspace: Path) -> List[ModuleFinding]:
"""Run OSS-Fuzz"""
findings = []
try:
project_name = config["project_name"]
sanitizer = config.get("sanitizer", "address")
architecture = config.get("architecture", "x86_64")
# Build project
if config.get("check_build", True):
await self._build_project(oss_fuzz_dir, project_name, sanitizer, architecture)
# Check build
await self._check_build(oss_fuzz_dir, project_name, sanitizer, architecture)
# Run fuzzing (limited time for this integration)
timeout = min(config.get("timeout", 300), 300) # Max 5 minutes for demo
findings = await self._run_fuzzing(oss_fuzz_dir, project_name, sanitizer, timeout, workspace)
# Reproduce bugs if requested
if config.get("reproduce_bugs", False):
repro_findings = await self._reproduce_bugs(oss_fuzz_dir, project_name, workspace)
findings.extend(repro_findings)
except Exception as e:
logger.warning(f"Error running OSS-Fuzz: {e}")
return findings
async def _build_project(self, oss_fuzz_dir: Path, project_name: str, sanitizer: str, architecture: str):
"""Build OSS-Fuzz project"""
cmd = [
"python3", "infra/helper.py", "build_image", project_name
]
process = await asyncio.create_subprocess_exec(
*cmd,
cwd=oss_fuzz_dir,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
logger.warning(f"Build image failed: {stderr.decode()}")
async def _check_build(self, oss_fuzz_dir: Path, project_name: str, sanitizer: str, architecture: str):
"""Check OSS-Fuzz build"""
cmd = [
"python3", "infra/helper.py", "check_build", project_name
]
process = await asyncio.create_subprocess_exec(
*cmd,
cwd=oss_fuzz_dir,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
logger.warning(f"Build check failed: {stderr.decode()}")
async def _run_fuzzing(self, oss_fuzz_dir: Path, project_name: str, sanitizer: str, timeout: int, workspace: Path) -> List[ModuleFinding]:
"""Run OSS-Fuzz fuzzing"""
findings = []
try:
# This is a simplified version - real OSS-Fuzz runs for much longer
cmd = [
"python3", "infra/helper.py", "run_fuzzer", project_name,
"--", f"-max_total_time={timeout}"
]
process = await asyncio.create_subprocess_exec(
*cmd,
cwd=oss_fuzz_dir,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
try:
stdout, stderr = await asyncio.wait_for(
process.communicate(), timeout=timeout + 60
)
except asyncio.TimeoutError:
process.terminate()
await process.wait()
# Parse output for crashes
full_output = stdout.decode() + stderr.decode()
findings = self._parse_oss_fuzz_output(full_output, workspace, sanitizer)
except Exception as e:
logger.warning(f"Error in OSS-Fuzz execution: {e}")
return findings
async def _reproduce_bugs(self, oss_fuzz_dir: Path, project_name: str, workspace: Path) -> List[ModuleFinding]:
"""Reproduce existing bugs"""
findings = []
try:
# Look for existing testcases or artifacts
testcases_dir = oss_fuzz_dir / "projects" / project_name / "testcases"
if testcases_dir.exists():
for testcase in testcases_dir.iterdir():
if testcase.is_file():
finding = self._create_testcase_finding(testcase, workspace)
if finding:
findings.append(finding)
except Exception as e:
logger.warning(f"Error reproducing bugs: {e}")
return findings
def _parse_oss_fuzz_output(self, output: str, workspace: Path, sanitizer: str) -> List[ModuleFinding]:
"""Parse OSS-Fuzz output"""
findings = []
try:
# Look for common crash indicators
lines = output.split('\n')
crash_info = None
for line in lines:
if "ERROR:" in line and any(term in line for term in ["AddressSanitizer", "MemorySanitizer", "UBSan"]):
crash_info = {
"type": self._extract_crash_type(line),
"sanitizer": sanitizer,
"line": line
}
elif crash_info and line.strip().startswith("#"):
# Stack trace line
if "stack_trace" not in crash_info:
crash_info["stack_trace"] = []
crash_info["stack_trace"].append(line.strip())
if crash_info:
finding = self._create_oss_fuzz_finding(crash_info, workspace)
if finding:
findings.append(finding)
except Exception as e:
logger.warning(f"Error parsing OSS-Fuzz output: {e}")
return findings
def _create_oss_fuzz_finding(self, crash_info: Dict[str, Any], workspace: Path) -> ModuleFinding:
"""Create finding from OSS-Fuzz crash"""
try:
bug_type = crash_info.get("type", "unknown")
sanitizer = crash_info.get("sanitizer", "unknown")
stack_trace = '\n'.join(crash_info.get("stack_trace", [])[:20])
severity = self._get_oss_fuzz_severity(bug_type)
finding = self.create_finding(
title=f"OSS-Fuzz {bug_type.title()}",
description=f"OSS-Fuzz detected a {bug_type} using {sanitizer} sanitizer",
severity=severity,
category=self._get_oss_fuzz_category(bug_type),
file_path=None,
recommendation=self._get_oss_fuzz_recommendation(bug_type, sanitizer),
metadata={
"bug_type": bug_type,
"sanitizer": sanitizer,
"stack_trace": stack_trace,
"fuzzer": "oss_fuzz"
}
)
return finding
except Exception as e:
logger.warning(f"Error creating OSS-Fuzz finding: {e}")
return None
def _create_testcase_finding(self, testcase_file: Path, workspace: Path) -> ModuleFinding:
"""Create finding from testcase file"""
try:
try:
rel_path = testcase_file.relative_to(workspace)
file_path = str(rel_path)
except ValueError:
file_path = str(testcase_file)
finding = self.create_finding(
title="OSS-Fuzz Testcase",
description=f"OSS-Fuzz testcase found: {testcase_file.name}",
severity="info",
category="testcase",
file_path=file_path,
recommendation="Analyze testcase to understand potential issues",
metadata={
"testcase_file": str(testcase_file),
"fuzzer": "oss_fuzz"
}
)
return finding
except Exception as e:
logger.warning(f"Error creating testcase finding: {e}")
return None
def _extract_crash_type(self, line: str) -> str:
"""Extract crash type from error line"""
if "heap-buffer-overflow" in line:
return "heap_buffer_overflow"
elif "stack-buffer-overflow" in line:
return "stack_buffer_overflow"
elif "use-after-free" in line:
return "use_after_free"
elif "double-free" in line:
return "double_free"
elif "memory leak" in line:
return "memory_leak"
else:
return "unknown_crash"
def _get_oss_fuzz_severity(self, bug_type: str) -> str:
"""Get severity for OSS-Fuzz bug type"""
if bug_type in ["heap_buffer_overflow", "stack_buffer_overflow", "use_after_free", "double_free"]:
return "critical"
elif bug_type == "memory_leak":
return "medium"
else:
return "high"
def _get_oss_fuzz_category(self, bug_type: str) -> str:
"""Get category for OSS-Fuzz bug type"""
if "overflow" in bug_type:
return "buffer_overflow"
elif "free" in bug_type:
return "memory_corruption"
elif "leak" in bug_type:
return "memory_leak"
else:
return "memory_safety"
def _get_oss_fuzz_recommendation(self, bug_type: str, sanitizer: str) -> str:
"""Get recommendation for OSS-Fuzz finding"""
if "overflow" in bug_type:
return "Fix buffer overflow by implementing proper bounds checking and using safe string functions."
elif "use_after_free" in bug_type:
return "Fix use-after-free by ensuring proper object lifetime management and setting pointers to NULL after freeing."
elif "double_free" in bug_type:
return "Fix double-free by ensuring each allocation has exactly one corresponding free operation."
elif "leak" in bug_type:
return "Fix memory leak by ensuring all allocated memory is properly freed in all code paths."
else:
return f"Address the {bug_type} issue detected by OSS-Fuzz with {sanitizer} sanitizer."
def _create_summary(self, findings: List[ModuleFinding]) -> Dict[str, Any]:
"""Create analysis summary"""
severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
category_counts = {}
sanitizer_counts = {}
for finding in findings:
severity_counts[finding.severity] += 1
category_counts[finding.category] = category_counts.get(finding.category, 0) + 1
sanitizer = finding.metadata.get("sanitizer", "unknown")
sanitizer_counts[sanitizer] = sanitizer_counts.get(sanitizer, 0) + 1
return {
"total_findings": len(findings),
"severity_counts": severity_counts,
"category_counts": category_counts,
"sanitizer_counts": sanitizer_counts
}