diff --git a/Dockerfile.coverage b/Dockerfile.coverage index 36d678ae..4071f224 100644 --- a/Dockerfile.coverage +++ b/Dockerfile.coverage @@ -90,6 +90,7 @@ COPY ./fuzzamoto-libafl/src/ src/ WORKDIR /fuzzamoto/fuzzamoto-scenarios COPY ./fuzzamoto-scenarios/Cargo.toml . COPY ./fuzzamoto-scenarios/bin/ bin/ +COPY ./fuzzamoto-scenarios/rpcs.txt rpcs.txt WORKDIR /fuzzamoto COPY ./Cargo.toml . diff --git a/fuzzamoto-cli/src/commands/coverage.rs b/fuzzamoto-cli/src/commands/coverage.rs index 26769dac..e4185df1 100644 --- a/fuzzamoto-cli/src/commands/coverage.rs +++ b/fuzzamoto-cli/src/commands/coverage.rs @@ -1,5 +1,8 @@ use crate::error::{CliError, Result}; -use crate::utils::{file_ops, process}; +use crate::utils::{ + file_ops::{self, ensure_dir_exists}, + process, +}; use std::path::PathBuf; pub struct CoverageCommand; @@ -9,17 +12,19 @@ impl CoverageCommand { output: PathBuf, corpus: PathBuf, bitcoind: PathBuf, - scenario: PathBuf, + scenarios: Vec, profraws: Option>, run_only: bool, ) -> Result<()> { file_ops::ensure_file_exists(&bitcoind)?; - file_ops::ensure_file_exists(&scenario)?; + for scenario in &scenarios { + file_ops::ensure_file_exists(scenario.path())?; + } if run_only { let corpus_files = file_ops::read_dir_files(&corpus)?; for corpus_file in corpus_files { - if let Err(e) = Self::run_one_input(&output, &corpus_file, &bitcoind, &scenario) { + if let Err(e) = Self::run_one_input(&output, &corpus_file, &bitcoind, &scenarios) { log::error!("Failed to run input ({:?}): {}", corpus_file, e); } } @@ -33,7 +38,8 @@ impl CoverageCommand { log::info!("{:?}", corpus_files); // Run scenario for each corpus file for corpus_file in corpus_files { - if let Err(e) = Self::run_one_input(&output, &corpus_file, &bitcoind, &scenario) + if let Err(e) = + Self::run_one_input(&output, &corpus_file, &bitcoind, &scenarios) { log::error!("Failed to run input ({:?}): {}", corpus_file, e); } @@ -52,7 +58,7 @@ impl CoverageCommand { output: &PathBuf, input: &PathBuf, bitcoind: &PathBuf, - scenario: &PathBuf, + scenarios: &Vec, ) -> Result<()> { log::info!("Running scenario with input: {}", input.display()); @@ -66,8 +72,9 @@ impl CoverageCommand { ("FUZZAMOTO_INPUT", input.to_str().unwrap()), ("RUST_LOG", "debug"), ]; - - process::run_scenario_command(scenario, bitcoind, &env_vars)?; + for scenario in scenarios { + process::run_scenario_command(scenario, bitcoind, &env_vars)?; + } Ok(()) } @@ -143,3 +150,103 @@ impl CoverageCommand { Ok(merged) } } + +#[derive(Clone, Copy, Debug)] +pub enum ScenarioType { + CompactBlocks, + Generic, + HttpServer, + ImportMempool, + IR, + RPCGeneric, + WalletMigration, +} + +impl ScenarioType { + pub fn from_filename(filename: &str) -> Option { + match filename { + "scenario-compact-blocks" => Some(Self::CompactBlocks), + "scenario-generic" => Some(Self::Generic), + "scenario-http-server" => Some(Self::HttpServer), + "scenario-import-mempool" => Some(Self::ImportMempool), + "scenario-ir" => Some(Self::IR), + "scenario-rpc-generic" => Some(Self::RPCGeneric), + "scenario-wallet-migration" => Some(Self::WalletMigration), + _ => None, + } + } +} + +pub struct Scenario { + path: PathBuf, + ty: ScenarioType, +} + +impl Scenario { + pub fn from_path(path: &PathBuf) -> Option { + let filename = path.file_name()?.to_str()?; + let ty = ScenarioType::from_filename(filename)?; + Some(Self { + path: path.to_path_buf(), + ty, + }) + } + + pub fn name(&self) -> &str { + match self.ty { + ScenarioType::CompactBlocks => "scenario-compact-blocks", + ScenarioType::Generic => "scenario-generic", + ScenarioType::HttpServer => "scenario-http-server", + ScenarioType::ImportMempool => "scenario-import-mempool", + ScenarioType::IR => "scenario-ir", + ScenarioType::RPCGeneric => "scenario-rpc-generic", + ScenarioType::WalletMigration => "scenario-wallet-migration", + } + } + + pub fn path(&self) -> &PathBuf { + &self.path + } + + pub fn ty(&self) -> ScenarioType { + self.ty + } +} + +impl std::fmt::Display for Scenario { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name()) + } +} + +impl std::fmt::Debug for Scenario { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name()) + } +} + +pub fn scan_scenario_dir(dir: &PathBuf) -> Result> { + ensure_dir_exists(dir)?; + + let mut scenarios = Vec::new(); + + for entry in std::fs::read_dir(dir)? { + let path = entry?.path(); + if let Some(scenario) = Scenario::from_path(&path) { + scenarios.push(scenario); + } + } + + log::info!( + "Found {} scenarios in {:?}: {:?}", + scenarios.len(), + dir, + scenarios + .iter() + .map(|s| s.to_string()) + .collect::>() + .join(", ") + ); + + Ok(scenarios) +} diff --git a/fuzzamoto-cli/src/main.rs b/fuzzamoto-cli/src/main.rs index 7f991623..6124b2ad 100644 --- a/fuzzamoto-cli/src/main.rs +++ b/fuzzamoto-cli/src/main.rs @@ -7,7 +7,11 @@ use commands::*; use error::Result; use std::path::PathBuf; -use crate::commands::coverage_batch::CoverageBatchCommand; +use crate::{ + commands::coverage_batch::CoverageBatchCommand, + coverage::{Scenario, scan_scenario_dir}, + error::CliError, +}; #[derive(Parser)] #[command(author, version, about, long_about = None)] @@ -68,7 +72,13 @@ enum Commands { long, help = "Path to the fuzzamoto scenario binary that should be run with coverage measurer" )] - scenario: PathBuf, + scenario: Option, + #[arg( + long, + conflicts_with = "scenario", + help = "Path to directory containing scenario-* binaries" + )] + scenario_dir: Option, #[arg( long, value_name = "PROFRAWS", @@ -147,16 +157,45 @@ fn main() -> Result<()> { corpus, bitcoind, scenario, + scenario_dir, profraws, run_only, - } => CoverageCommand::execute( - output.clone(), - corpus.clone(), - bitcoind.clone(), - scenario.clone(), - profraws.clone(), - *run_only, - ), + } => { + let scenarios: Vec = if let Some(dir) = scenario_dir { + scan_scenario_dir(&dir)? + } else if let Some(s) = scenario { + match Scenario::from_path(s) { + Some(s) => vec![s], + None => { + return Err( + CliError::InvalidInput(format!("Invalid scenario: {:?}", s)).into() + ); + } + } + } else { + return Err(CliError::InvalidInput( + "Must specify either --scenario or --scenario-dir".to_string(), + ) + .into()); + }; + + if scenarios.is_empty() { + return Err(CliError::InvalidInput("No scenarios found".to_string()).into()); + } + + for scenario in &scenarios { + log::info!("Running coverage measurement on {:?}", scenario); + } + + CoverageCommand::execute( + output.clone(), + corpus.clone(), + bitcoind.clone(), + scenarios, + profraws.clone(), + *run_only, + ) + } Commands::CoverageBatch { output, corpus, diff --git a/fuzzamoto-cli/src/utils/file_ops.rs b/fuzzamoto-cli/src/utils/file_ops.rs index e9b56c59..2793b4b6 100644 --- a/fuzzamoto-cli/src/utils/file_ops.rs +++ b/fuzzamoto-cli/src/utils/file_ops.rs @@ -15,7 +15,14 @@ pub fn copy_file_to_dir(src: &Path, dst_dir: &Path) -> Result<()> { } pub fn ensure_file_exists(path: &Path) -> Result<()> { - if !path.exists() { + if !path.exists() || !path.is_file() { + return Err(CliError::FileNotFound(path.display().to_string())); + } + Ok(()) +} + +pub fn ensure_dir_exists(path: &Path) -> Result<()> { + if !path.exists() || !path.is_dir() { return Err(CliError::FileNotFound(path.display().to_string())); } Ok(()) diff --git a/fuzzamoto-cli/src/utils/process.rs b/fuzzamoto-cli/src/utils/process.rs index 3fff32f8..8b63c64d 100644 --- a/fuzzamoto-cli/src/utils/process.rs +++ b/fuzzamoto-cli/src/utils/process.rs @@ -1,4 +1,7 @@ -use crate::error::{CliError, Result}; +use crate::{ + coverage::{Scenario, ScenarioType}, + error::{CliError, Result}, +}; use std::path::Path; use std::process::{Command, Stdio}; @@ -56,13 +59,18 @@ pub fn run_command_with_output( } pub fn run_scenario_command( - scenario: &Path, + scenario: &Scenario, bitcoind: &Path, env_vars: &[(&str, &str)], ) -> Result<()> { - let mut cmd = Command::new(scenario); + let mut cmd = Command::new(scenario.path()); + cmd.arg(bitcoind); + if matches!(scenario.ty(), ScenarioType::RPCGeneric) { + cmd.arg("/fuzzamoto/fuzzamoto-scenarios/rpcs.txt"); + } + for (key, value) in env_vars { cmd.env(key, value); } diff --git a/fuzzamoto-scenarios/bin/rpc_generic.rs b/fuzzamoto-scenarios/bin/rpc_generic.rs index 5855592e..084c9368 100644 --- a/fuzzamoto-scenarios/bin/rpc_generic.rs +++ b/fuzzamoto-scenarios/bin/rpc_generic.rs @@ -199,8 +199,8 @@ struct RpcScenario { impl<'a> Scenario<'a, TestCase> for RpcScenario { fn new(args: &[String]) -> Result { let target = BitcoinCoreTarget::from_path(&args[1])?; - let rpcs = - fs::read_to_string(&args[2]).map_err(|e| format!("Failed to parse file: {}", e))?; + let rpcs = fs::read_to_string(&args[2]) + .map_err(|e| format!("Failed to parse file {}: {}", args[2], e))?; // Note that any change in the file may invalidate existing seeds let mut available_rpcs: Vec = vec![];