diff --git a/src/agent/onefuzz-task/src/local/coverage.rs b/src/agent/onefuzz-task/src/local/coverage.rs index a5d53a09f0..d091b70695 100644 --- a/src/agent/onefuzz-task/src/local/coverage.rs +++ b/src/agent/onefuzz-task/src/local/coverage.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use std::path::PathBuf; +use std::{collections::HashMap, path::PathBuf}; use crate::{ local::common::{ @@ -15,11 +15,17 @@ use crate::{ }, }; use anyhow::Result; +use async_trait::async_trait; use clap::{Arg, ArgAction, Command}; use flume::Sender; +use onefuzz::syncdir::SyncedDir; +use schemars::JsonSchema; use storage_queue::QueueClient; -use super::common::{SyncCountDirMonitor, UiEvent}; +use super::{ + common::{SyncCountDirMonitor, UiEvent}, + template::{RunContext, Template}, +}; pub fn build_coverage_config( args: &clap::ArgMatches, @@ -127,3 +133,60 @@ pub fn args(name: &'static str) -> Command { .about("execute a local-only coverage task") .args(&build_shared_args(false)) } + +#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] +pub struct Coverage { + target_exe: PathBuf, + target_env: HashMap, + target_options: Vec, + target_timeout: Option, + module_allowlist: Option, + source_allowlist: Option, + input_queue: Option, + readonly_inputs: Vec, + coverage: PathBuf, +} + +#[async_trait] +impl Template for Coverage { + async fn run(&self, context: &RunContext) -> Result<()> { + let ri: Result> = self + .readonly_inputs + .iter() + .enumerate() + .map(|(index, input)| context.to_sync_dir(format!("readonly_inputs_{index}"), input)) + .collect(); + + let input_q = if let Some(w) = &self.input_queue { + Some(context.monitor_dir(w).await?) + } else { + None + }; + + let coverage_config = crate::tasks::coverage::generic::Config { + target_exe: self.target_exe.clone(), + target_env: self.target_env.clone(), + target_options: self.target_options.clone(), + target_timeout: None, + readonly_inputs: ri?, + input_queue: input_q, + common: CommonConfig { + task_id: uuid::Uuid::new_v4(), + ..context.common.clone() + }, + coverage_filter: None, + coverage: context.to_monitored_sync_dir("coverage", self.coverage.clone())?, + module_allowlist: self.module_allowlist.clone(), + source_allowlist: self.source_allowlist.clone(), + }; + + context + .spawn(async move { + let mut coverage = + crate::tasks::coverage::generic::CoverageTask::new(coverage_config); + coverage.run().await + }) + .await; + Ok(()) + } +} diff --git a/src/agent/onefuzz-task/src/local/libfuzzer_basic.yml b/src/agent/onefuzz-task/src/local/example_templates/libfuzzer_basic.yml similarity index 88% rename from src/agent/onefuzz-task/src/local/libfuzzer_basic.yml rename to src/agent/onefuzz-task/src/local/example_templates/libfuzzer_basic.yml index da7100f89d..7210893809 100644 --- a/src/agent/onefuzz-task/src/local/libfuzzer_basic.yml +++ b/src/agent/onefuzz-task/src/local/example_templates/libfuzzer_basic.yml @@ -1,4 +1,4 @@ -# yaml-language-server: $schema=schema.json +# yaml-language-server: $schema=../schema.json # What I had to do to get this working: # 1. Update target_exe to point to the target exe @@ -26,7 +26,7 @@ tasks: crashes: *crash reports: "./reports" unique_reports: "./unique_reports" - no_repro: "./noe_repro" + no_repro: "./no_repro" check_fuzzer_help: true - type: "Coverage" @@ -36,8 +36,3 @@ tasks: input_queue: *inputs readonly_inputs: [*inputs] coverage: "./coverage" - - # - type: Analysis - # <<: *target_args - - diff --git a/src/agent/onefuzz-task/src/local/example_templates/radamsa.yml b/src/agent/onefuzz-task/src/local/example_templates/radamsa.yml new file mode 100644 index 0000000000..fc73c48e9d --- /dev/null +++ b/src/agent/onefuzz-task/src/local/example_templates/radamsa.yml @@ -0,0 +1,21 @@ +# yaml-language-server: $schema=../schema.json + +# This template file demonstrates how to configure a radamsa task + +target_args: &target_args + target_env: {} + target_exe: "C:\\temp\\onefuzz\\integration\\windows-libfuzzer\\fuzz.exe" + target_options: [] + +tasks: + - type: Generator + <<: *target_args + crashes: "./crashes" + generator_env: {} + generator_exe: "./path/to/generator" + generator_options: [] + readonly_inputs: ["./path/to/readonly-inputs"] + rename_output: true + + - type: Report + <<: *target_args diff --git a/src/agent/onefuzz-task/src/local/generic_analysis.rs b/src/agent/onefuzz-task/src/local/generic_analysis.rs index fb202a9432..3d3e2fafc8 100644 --- a/src/agent/onefuzz-task/src/local/generic_analysis.rs +++ b/src/agent/onefuzz-task/src/local/generic_analysis.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use std::path::PathBuf; +use std::{collections::HashMap, path::PathBuf}; use crate::{ local::common::{ @@ -16,10 +16,14 @@ use crate::{ }, }; use anyhow::Result; +use async_trait::async_trait; use clap::{Arg, Command}; use flume::Sender; +use schemars::JsonSchema; use storage_queue::QueueClient; +use super::template::{RunContext, Template}; + pub fn build_analysis_config( args: &clap::ArgMatches, input_queue: Option, @@ -131,3 +135,73 @@ pub fn args(name: &'static str) -> Command { .about("execute a local-only generic analysis") .args(&build_shared_args(true)) } + +#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] +pub struct Analysis { + analyzer_exe: String, + analyzer_options: Vec, + analyzer_env: HashMap, + target_exe: PathBuf, + target_options: Vec, + input_queue: Option, + crashes: Option, + analysis: PathBuf, + tools: PathBuf, + reports: Option, + unique_reports: Option, + no_repro: Option, +} + +#[async_trait] +impl Template for Analysis { + async fn run(&self, context: &RunContext) -> Result<()> { + let input_q = if let Some(w) = &self.input_queue { + Some(context.monitor_dir(w).await?) + } else { + None + }; + + let analysis_config = crate::tasks::analysis::generic::Config { + analyzer_exe: self.analyzer_exe.clone(), + analyzer_options: self.analyzer_options.clone(), + analyzer_env: self.analyzer_env.clone(), + + target_exe: self.target_exe.clone(), + target_options: self.target_options.clone(), + input_queue: input_q, + crashes: self + .crashes + .as_ref() + .and_then(|path| context.to_monitored_sync_dir("crashes", path).ok()), + + analysis: context.to_monitored_sync_dir("analysis", self.analysis.clone())?, + tools: context + .to_monitored_sync_dir("tools", self.tools.clone()) + .ok(), + + reports: self + .reports + .as_ref() + .and_then(|path| context.to_monitored_sync_dir("reports", path).ok()), + unique_reports: self + .unique_reports + .as_ref() + .and_then(|path| context.to_monitored_sync_dir("unique_reports", path).ok()), + no_repro: self + .no_repro + .as_ref() + .and_then(|path| context.to_monitored_sync_dir("no_repro", path).ok()), + + common: CommonConfig { + task_id: uuid::Uuid::new_v4(), + ..context.common.clone() + }, + }; + + context + .spawn(async move { crate::tasks::analysis::generic::run(analysis_config).await }) + .await; + + Ok(()) + } +} diff --git a/src/agent/onefuzz-task/src/local/generic_crash_report.rs b/src/agent/onefuzz-task/src/local/generic_crash_report.rs index aebd45aa08..6b0e2fccad 100644 --- a/src/agent/onefuzz-task/src/local/generic_crash_report.rs +++ b/src/agent/onefuzz-task/src/local/generic_crash_report.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use std::path::PathBuf; +use std::{collections::HashMap, path::PathBuf}; use crate::{ local::common::{ @@ -13,13 +13,19 @@ use crate::{ tasks::{ config::CommonConfig, report::generic::{Config, ReportTask}, + utils::default_bool_true, }, }; use anyhow::Result; +use async_trait::async_trait; use clap::{Arg, ArgAction, Command}; use flume::Sender; +use futures::future::OptionFuture; +use schemars::JsonSchema; use storage_queue::QueueClient; +use super::template::{RunContext, Template}; + pub fn build_report_config( args: &clap::ArgMatches, input_queue: Option, @@ -140,3 +146,91 @@ pub fn args(name: &'static str) -> Command { .about("execute a local-only generic crash report") .args(&build_shared_args()) } + +#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] +pub struct CrashReport { + target_exe: PathBuf, + target_options: Vec, + target_env: HashMap, + + input_queue: Option, + crashes: Option, + reports: Option, + unique_reports: Option, + no_repro: Option, + + target_timeout: Option, + + #[serde(default)] + check_asan_log: bool, + #[serde(default = "default_bool_true")] + check_debugger: bool, + #[serde(default)] + check_retry_count: u64, + + #[serde(default = "default_bool_true")] + check_queue: bool, + + #[serde(default)] + minimized_stack_depth: Option, +} +#[async_trait] +impl Template for CrashReport { + async fn run(&self, context: &RunContext) -> Result<()> { + let input_q_fut: OptionFuture<_> = self + .input_queue + .iter() + .map(|w| context.monitor_dir(w)) + .next() + .into(); + let input_q = input_q_fut.await.transpose()?; + + let crash_report_config = crate::tasks::report::generic::Config { + target_exe: self.target_exe.clone(), + target_env: self.target_env.clone(), + target_options: self.target_options.clone(), + target_timeout: self.target_timeout, + + input_queue: input_q, + crashes: self + .crashes + .clone() + .map(|c| context.to_monitored_sync_dir("crashes", c)) + .transpose()?, + reports: self + .reports + .clone() + .map(|c| context.to_monitored_sync_dir("reports", c)) + .transpose()?, + unique_reports: self + .unique_reports + .clone() + .map(|c| context.to_monitored_sync_dir("unique_reports", c)) + .transpose()?, + no_repro: self + .no_repro + .clone() + .map(|c| context.to_monitored_sync_dir("no_repro", c)) + .transpose()?, + + check_asan_log: self.check_asan_log, + check_debugger: self.check_debugger, + check_retry_count: self.check_retry_count, + check_queue: self.check_queue, + minimized_stack_depth: self.minimized_stack_depth, + common: CommonConfig { + task_id: uuid::Uuid::new_v4(), + ..context.common.clone() + }, + }; + + context + .spawn(async move { + let mut report = + crate::tasks::report::generic::ReportTask::new(crash_report_config); + report.managed_run().await + }) + .await; + Ok(()) + } +} diff --git a/src/agent/onefuzz-task/src/local/generic_generator.rs b/src/agent/onefuzz-task/src/local/generic_generator.rs index a6b0777348..823ba221d6 100644 --- a/src/agent/onefuzz-task/src/local/generic_generator.rs +++ b/src/agent/onefuzz-task/src/local/generic_generator.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use std::path::PathBuf; +use std::{collections::HashMap, path::PathBuf}; use crate::{ local::common::{ @@ -14,11 +14,17 @@ use crate::{ tasks::{ config::CommonConfig, fuzz::generator::{Config, GeneratorTask}, + utils::default_bool_true, }, }; use anyhow::Result; +use async_trait::async_trait; use clap::{Arg, ArgAction, Command}; use flume::Sender; +use onefuzz::syncdir::SyncedDir; +use schemars::JsonSchema; + +use super::template::{RunContext, Template}; pub fn build_fuzz_config( args: &clap::ArgMatches, @@ -144,3 +150,75 @@ pub fn args(name: &'static str) -> Command { .about("execute a local-only generator fuzzing task") .args(&build_shared_args()) } + +#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] +pub struct Generator { + generator_exe: String, + generator_env: HashMap, + generator_options: Vec, + readonly_inputs: Vec, + crashes: PathBuf, + tools: Option, + + target_exe: PathBuf, + target_env: HashMap, + target_options: Vec, + target_timeout: Option, + #[serde(default)] + check_asan_log: bool, + #[serde(default = "default_bool_true")] + check_debugger: bool, + #[serde(default)] + check_retry_count: u64, + rename_output: bool, + ensemble_sync_delay: Option, +} + +#[async_trait] +impl Template for Generator { + async fn run(&self, context: &RunContext) -> Result<()> { + let generator_config = crate::tasks::fuzz::generator::Config { + generator_exe: self.generator_exe.clone(), + generator_env: self.generator_env.clone(), + generator_options: self.generator_options.clone(), + + readonly_inputs: self + .readonly_inputs + .iter() + .enumerate() + .map(|(index, roi_pb)| { + context.to_monitored_sync_dir(format!("read_only_inputs_{index}"), roi_pb) + }) + .collect::>>()?, + crashes: context.to_monitored_sync_dir("crashes", self.crashes.clone())?, + tools: self + .tools + .as_ref() + .and_then(|path_buf| context.to_monitored_sync_dir("tools", path_buf).ok()), + + target_exe: self.target_exe.clone(), + target_env: self.target_env.clone(), + target_options: self.target_options.clone(), + target_timeout: self.target_timeout, + + check_asan_log: self.check_asan_log, + check_debugger: self.check_debugger, + check_retry_count: self.check_retry_count, + + rename_output: self.rename_output, + ensemble_sync_delay: self.ensemble_sync_delay, + common: CommonConfig { + task_id: uuid::Uuid::new_v4(), + ..context.common.clone() + }, + }; + + context + .spawn(async move { + let generator = crate::tasks::fuzz::generator::GeneratorTask::new(generator_config); + generator.run().await + }) + .await; + Ok(()) + } +} diff --git a/src/agent/onefuzz-task/src/local/libfuzzer.rs b/src/agent/onefuzz-task/src/local/libfuzzer.rs index f3dc1778e0..56dff7dbe3 100644 --- a/src/agent/onefuzz-task/src/local/libfuzzer.rs +++ b/src/agent/onefuzz-task/src/local/libfuzzer.rs @@ -20,19 +20,29 @@ use crate::{ }, }, tasks::{ - analysis::generic::run as run_analysis, config::CommonConfig, - fuzz::libfuzzer::generic::LibFuzzerFuzzTask, - regression::libfuzzer::LibFuzzerRegressionTask, report::libfuzzer_report::ReportTask, + analysis::generic::run as run_analysis, + config::CommonConfig, + fuzz::libfuzzer::{common::default_workers, generic::LibFuzzerFuzzTask}, + regression::libfuzzer::LibFuzzerRegressionTask, + report::libfuzzer_report::ReportTask, + utils::default_bool_true, }, }; use anyhow::Result; +use async_trait::async_trait; use clap::Command; use flume::Sender; -use onefuzz::utils::try_wait_all_join_handles; -use std::collections::HashSet; +use onefuzz::{syncdir::SyncedDir, utils::try_wait_all_join_handles}; +use schemars::JsonSchema; +use std::{ + collections::{HashMap, HashSet}, + path::PathBuf, +}; use tokio::task::spawn; use uuid::Uuid; +use super::template::{RunContext, Template}; + pub async fn run(args: &clap::ArgMatches, event_sender: Option>) -> Result<()> { let context = build_local_context(args, true, event_sender.clone()).await?; let fuzz_config = build_fuzz_config(args, context.common_config.clone(), event_sender.clone())?; @@ -152,3 +162,62 @@ pub fn args(name: &'static str) -> Command { app } + +#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] +pub struct LibFuzzer { + inputs: PathBuf, + readonly_inputs: Vec, + crashes: PathBuf, + crashdumps: Option, + target_exe: PathBuf, + target_env: HashMap, + target_options: Vec, + target_workers: Option, + ensemble_sync_delay: Option, + #[serde(default = "default_bool_true")] + check_fuzzer_help: bool, + #[serde(default)] + expect_crash_on_failure: bool, +} + +#[async_trait] +impl Template for LibFuzzer { + async fn run(&self, context: &RunContext) -> Result<()> { + let ri: Result> = self + .readonly_inputs + .iter() + .enumerate() + .map(|(index, input)| context.to_sync_dir(format!("readonly_inputs_{index}"), input)) + .collect(); + + let libfuzzer_config = crate::tasks::fuzz::libfuzzer::generic::Config { + inputs: context.to_monitored_sync_dir("inputs", &self.inputs)?, + readonly_inputs: Some(ri?), + crashes: context.to_monitored_sync_dir("crashes", &self.crashes)?, + crashdumps: self + .crashdumps + .as_ref() + .and_then(|path| context.to_monitored_sync_dir("crashdumps", path).ok()), + target_exe: self.target_exe.clone(), + target_env: self.target_env.clone(), + target_options: self.target_options.clone(), + target_workers: self.target_workers.unwrap_or(default_workers()), + ensemble_sync_delay: self.ensemble_sync_delay, + check_fuzzer_help: self.check_fuzzer_help, + expect_crash_on_failure: self.expect_crash_on_failure, + extra: (), + common: CommonConfig { + task_id: uuid::Uuid::new_v4(), + ..context.common.clone() + }, + }; + + context + .spawn(async move { + let fuzzer = LibFuzzerFuzzTask::new(libfuzzer_config)?; + fuzzer.run().await + }) + .await; + Ok(()) + } +} diff --git a/src/agent/onefuzz-task/src/local/libfuzzer_crash_report.rs b/src/agent/onefuzz-task/src/local/libfuzzer_crash_report.rs index be6ac1cefd..c1ab283575 100644 --- a/src/agent/onefuzz-task/src/local/libfuzzer_crash_report.rs +++ b/src/agent/onefuzz-task/src/local/libfuzzer_crash_report.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use std::path::PathBuf; +use std::{collections::HashMap, path::PathBuf}; use crate::{ local::common::{ @@ -13,13 +13,19 @@ use crate::{ tasks::{ config::CommonConfig, report::libfuzzer_report::{Config, ReportTask}, + utils::default_bool_true, }, }; use anyhow::Result; +use async_trait::async_trait; use clap::{Arg, ArgAction, Command}; use flume::Sender; +use futures::future::OptionFuture; +use schemars::JsonSchema; use storage_queue::QueueClient; +use super::template::{RunContext, Template}; + pub fn build_report_config( args: &clap::ArgMatches, input_queue: Option, @@ -129,3 +135,87 @@ pub fn args(name: &'static str) -> Command { .about("execute a local-only libfuzzer crash report task") .args(&build_shared_args()) } + +#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] +pub struct LibfuzzerCrashReport { + target_exe: PathBuf, + target_env: HashMap, + target_options: Vec, + target_timeout: Option, + input_queue: Option, + crashes: Option, + reports: Option, + unique_reports: Option, + no_repro: Option, + + #[serde(default = "default_bool_true")] + check_fuzzer_help: bool, + + #[serde(default)] + check_retry_count: u64, + + #[serde(default)] + minimized_stack_depth: Option, + + #[serde(default = "default_bool_true")] + check_queue: bool, +} + +#[async_trait] +impl Template for LibfuzzerCrashReport { + async fn run(&self, context: &RunContext) -> Result<()> { + let input_q_fut: OptionFuture<_> = self + .input_queue + .iter() + .map(|w| context.monitor_dir(w)) + .next() + .into(); + let input_q = input_q_fut.await.transpose()?; + + let libfuzzer_crash_config = crate::tasks::report::libfuzzer_report::Config { + target_exe: self.target_exe.clone(), + target_env: self.target_env.clone(), + target_options: self.target_options.clone(), + target_timeout: self.target_timeout, + input_queue: input_q, + crashes: self + .crashes + .clone() + .map(|c| context.to_monitored_sync_dir("crashes", c)) + .transpose()?, + reports: self + .reports + .clone() + .map(|c| context.to_monitored_sync_dir("reports", c)) + .transpose()?, + unique_reports: self + .unique_reports + .clone() + .map(|c| context.to_monitored_sync_dir("unique_reports", c)) + .transpose()?, + no_repro: self + .no_repro + .clone() + .map(|c| context.to_monitored_sync_dir("no_repro", c)) + .transpose()?, + + check_fuzzer_help: self.check_fuzzer_help, + check_retry_count: self.check_retry_count, + minimized_stack_depth: self.minimized_stack_depth, + check_queue: self.check_queue, + common: CommonConfig { + task_id: uuid::Uuid::new_v4(), + ..context.common.clone() + }, + }; + + context + .spawn(async move { + let mut libfuzzer_report = + crate::tasks::report::libfuzzer_report::ReportTask::new(libfuzzer_crash_config); + libfuzzer_report.managed_run().await + }) + .await; + Ok(()) + } +} diff --git a/src/agent/onefuzz-task/src/local/libfuzzer_merge.rs b/src/agent/onefuzz-task/src/local/libfuzzer_merge.rs index dff3a106e0..69c9df820b 100644 --- a/src/agent/onefuzz-task/src/local/libfuzzer_merge.rs +++ b/src/agent/onefuzz-task/src/local/libfuzzer_merge.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use std::path::PathBuf; +use std::{collections::HashMap, path::PathBuf}; use crate::{ local::common::{ @@ -13,13 +13,20 @@ use crate::{ tasks::{ config::CommonConfig, merge::libfuzzer_merge::{spawn, Config}, + utils::default_bool_true, }, }; use anyhow::Result; +use async_trait::async_trait; use clap::{Arg, ArgAction, Command}; use flume::Sender; +use futures::future::OptionFuture; +use onefuzz::syncdir::SyncedDir; +use schemars::JsonSchema; use storage_queue::QueueClient; +use super::template::{RunContext, Template}; + pub fn build_merge_config( args: &clap::ArgMatches, input_queue: Option, @@ -86,3 +93,62 @@ pub fn args(name: &'static str) -> Command { .about("execute a local-only libfuzzer crash report task") .args(&build_shared_args()) } + +#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] +pub struct LibfuzzerMerge { + target_exe: PathBuf, + target_env: HashMap, + target_options: Vec, + input_queue: Option, + inputs: Vec, + unique_inputs: PathBuf, + preserve_existing_outputs: bool, + + #[serde(default = "default_bool_true")] + check_fuzzer_help: bool, +} + +#[async_trait] +impl Template for LibfuzzerMerge { + async fn run(&self, context: &RunContext) -> Result<()> { + let input_q_fut: OptionFuture<_> = self + .input_queue + .iter() + .map(|w| context.monitor_dir(w)) + .next() + .into(); + let input_q = input_q_fut.await.transpose()?; + + let libfuzzer_merge = crate::tasks::merge::libfuzzer_merge::Config { + target_exe: self.target_exe.clone(), + target_env: self.target_env.clone(), + target_options: self.target_options.clone(), + input_queue: input_q, + inputs: self + .inputs + .iter() + .enumerate() + .map(|(index, roi_pb)| { + context.to_monitored_sync_dir(format!("inputs_{index}"), roi_pb) + }) + .collect::>>()?, + unique_inputs: context + .to_monitored_sync_dir("unique_inputs", self.unique_inputs.clone())?, + preserve_existing_outputs: self.preserve_existing_outputs, + + check_fuzzer_help: self.check_fuzzer_help, + + common: CommonConfig { + task_id: uuid::Uuid::new_v4(), + ..context.common.clone() + }, + }; + + context + .spawn( + async move { crate::tasks::merge::libfuzzer_merge::spawn(libfuzzer_merge).await }, + ) + .await; + Ok(()) + } +} diff --git a/src/agent/onefuzz-task/src/local/libfuzzer_regression.rs b/src/agent/onefuzz-task/src/local/libfuzzer_regression.rs index da5af6d1b1..501d2385e2 100644 --- a/src/agent/onefuzz-task/src/local/libfuzzer_regression.rs +++ b/src/agent/onefuzz-task/src/local/libfuzzer_regression.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use std::path::PathBuf; +use std::{collections::HashMap, path::PathBuf}; use crate::{ local::common::{ @@ -13,11 +13,16 @@ use crate::{ tasks::{ config::CommonConfig, regression::libfuzzer::{Config, LibFuzzerRegressionTask}, + utils::default_bool_true, }, }; use anyhow::Result; +use async_trait::async_trait; use clap::{Arg, ArgAction, Command}; use flume::Sender; +use schemars::JsonSchema; + +use super::template::{RunContext, Template}; const REPORT_NAMES: &str = "report_names"; @@ -136,3 +141,87 @@ pub fn args(name: &'static str) -> Command { .about("execute a local-only libfuzzer regression task") .args(&build_shared_args(true)) } + +#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] +pub struct LibfuzzerRegression { + target_exe: PathBuf, + + #[serde(default)] + target_options: Vec, + + #[serde(default)] + target_env: HashMap, + + target_timeout: Option, + + crashes: PathBuf, + regression_reports: PathBuf, + report_list: Option>, + unique_reports: Option, + reports: Option, + no_repro: Option, + readonly_inputs: Option, + + #[serde(default = "default_bool_true")] + check_fuzzer_help: bool, + #[serde(default)] + check_retry_count: u64, + + #[serde(default)] + minimized_stack_depth: Option, +} + +#[async_trait] +impl Template for LibfuzzerRegression { + async fn run(&self, context: &RunContext) -> Result<()> { + let libfuzzer_regression = crate::tasks::regression::libfuzzer::Config { + target_exe: self.target_exe.clone(), + target_env: self.target_env.clone(), + target_options: self.target_options.clone(), + target_timeout: self.target_timeout, + crashes: context.to_monitored_sync_dir("crashes", self.crashes.clone())?, + regression_reports: context + .to_monitored_sync_dir("regression_reports", self.regression_reports.clone())?, + report_list: self.report_list.clone(), + + unique_reports: self + .unique_reports + .clone() + .map(|c| context.to_monitored_sync_dir("unique_reports", c)) + .transpose()?, + reports: self + .reports + .clone() + .map(|c| context.to_monitored_sync_dir("reports", c)) + .transpose()?, + no_repro: self + .no_repro + .clone() + .map(|c| context.to_monitored_sync_dir("no_repro", c)) + .transpose()?, + readonly_inputs: self + .readonly_inputs + .clone() + .map(|c| context.to_monitored_sync_dir("readonly_inputs", c)) + .transpose()?, + + check_fuzzer_help: self.check_fuzzer_help, + check_retry_count: self.check_retry_count, + minimized_stack_depth: self.minimized_stack_depth, + + common: CommonConfig { + task_id: uuid::Uuid::new_v4(), + ..context.common.clone() + }, + }; + context + .spawn(async move { + let regression = crate::tasks::regression::libfuzzer::LibFuzzerRegressionTask::new( + libfuzzer_regression, + ); + regression.run().await + }) + .await; + Ok(()) + } +} diff --git a/src/agent/onefuzz-task/src/local/libfuzzer_test_input.rs b/src/agent/onefuzz-task/src/local/libfuzzer_test_input.rs index 7a28e3e4a9..9c6f16094e 100644 --- a/src/agent/onefuzz-task/src/local/libfuzzer_test_input.rs +++ b/src/agent/onefuzz-task/src/local/libfuzzer_test_input.rs @@ -9,9 +9,14 @@ use crate::{ tasks::report::libfuzzer_report::{test_input, TestInputArgs}, }; use anyhow::Result; +use async_trait::async_trait; use clap::{Arg, Command}; use flume::Sender; -use std::path::PathBuf; +use onefuzz::machine_id::MachineIdentity; +use schemars::JsonSchema; +use std::{collections::HashMap, path::PathBuf}; + +use super::template::{RunContext, Template}; pub async fn run(args: &clap::ArgMatches, event_sender: Option>) -> Result<()> { let context = build_local_context(args, true, event_sender).await?; @@ -86,3 +91,53 @@ pub fn args(name: &'static str) -> Command { .about("test a libfuzzer application with a specific input") .args(&build_shared_args()) } + +#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] +pub struct LibfuzzerTestInput { + input: PathBuf, + target_exe: PathBuf, + target_options: Vec, + target_env: HashMap, + setup_dir: PathBuf, + extra_setup_dir: Option, + extra_output_dir: Option, + target_timeout: Option, + check_retry_count: u64, + minimized_stack_depth: Option, +} + +#[async_trait] +impl Template for LibfuzzerTestInput { + async fn run(&self, context: &RunContext) -> Result<()> { + let c = self.clone(); + let t = tokio::spawn(async move { + let libfuzzer_test_input = crate::tasks::report::libfuzzer_report::TestInputArgs { + input_url: None, + input: c.input.as_path(), + target_exe: c.target_exe.as_path(), + target_options: &c.target_options, + target_env: &c.target_env, + setup_dir: &c.setup_dir, + extra_output_dir: c.extra_output_dir.as_deref(), + extra_setup_dir: c.extra_setup_dir.as_deref(), + task_id: uuid::Uuid::new_v4(), + job_id: uuid::Uuid::new_v4(), + target_timeout: c.target_timeout, + check_retry_count: c.check_retry_count, + minimized_stack_depth: c.minimized_stack_depth, + machine_identity: MachineIdentity { + machine_id: uuid::Uuid::new_v4(), + machine_name: "local".to_string(), + scaleset_name: None, + }, + }; + + crate::tasks::report::libfuzzer_report::test_input(libfuzzer_test_input) + .await + .map(|_| ()) + }); + + context.add_handle(t).await; + Ok(()) + } +} diff --git a/src/agent/onefuzz-task/src/local/readme.md b/src/agent/onefuzz-task/src/local/readme.md new file mode 100644 index 0000000000..8c760d9c88 --- /dev/null +++ b/src/agent/onefuzz-task/src/local/readme.md @@ -0,0 +1,6 @@ +Example templates: `./example_templates` + +Updating schema: + +1. Run the test at the bottome of `template.rs` +1. Copy the output into `schema.json` diff --git a/src/agent/onefuzz-task/src/local/schema.json b/src/agent/onefuzz-task/src/local/schema.json index e21cd2817d..0a1f128e67 100644 --- a/src/agent/onefuzz-task/src/local/schema.json +++ b/src/agent/onefuzz-task/src/local/schema.json @@ -56,6 +56,12 @@ "default": true, "type": "boolean" }, + "crashdumps": { + "type": [ + "string", + "null" + ] + }, "crashes": { "type": "string" }, @@ -261,6 +267,204 @@ } } }, + { + "type": "object", + "required": [ + "target_env", + "target_exe", + "target_options", + "type" + ], + "properties": { + "check_asan_log": { + "default": false, + "type": "boolean" + }, + "check_debugger": { + "default": true, + "type": "boolean" + }, + "check_queue": { + "default": true, + "type": "boolean" + }, + "check_retry_count": { + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "crashes": { + "type": [ + "string", + "null" + ] + }, + "input_queue": { + "type": [ + "string", + "null" + ] + }, + "minimized_stack_depth": { + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint", + "minimum": 0.0 + }, + "no_repro": { + "type": [ + "string", + "null" + ] + }, + "reports": { + "type": [ + "string", + "null" + ] + }, + "target_env": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "target_exe": { + "type": "string" + }, + "target_options": { + "type": "array", + "items": { + "type": "string" + } + }, + "target_timeout": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "type": { + "type": "string", + "enum": [ + "CrashReport" + ] + }, + "unique_reports": { + "type": [ + "string", + "null" + ] + } + } + }, + { + "type": "object", + "required": [ + "crashes", + "generator_env", + "generator_exe", + "generator_options", + "readonly_inputs", + "rename_output", + "target_env", + "target_exe", + "target_options", + "type" + ], + "properties": { + "check_asan_log": { + "default": false, + "type": "boolean" + }, + "check_debugger": { + "default": true, + "type": "boolean" + }, + "check_retry_count": { + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "crashes": { + "type": "string" + }, + "ensemble_sync_delay": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "generator_env": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "generator_exe": { + "type": "string" + }, + "generator_options": { + "type": "array", + "items": { + "type": "string" + } + }, + "readonly_inputs": { + "type": "array", + "items": { + "type": "string" + } + }, + "rename_output": { + "type": "boolean" + }, + "target_env": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "target_exe": { + "type": "string" + }, + "target_options": { + "type": "array", + "items": { + "type": "string" + } + }, + "target_timeout": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "tools": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "Generator" + ] + } + } + }, { "type": "object", "required": [ @@ -343,7 +547,7 @@ "type": { "type": "string", "enum": [ - "Report" + "LibfuzzerCrashReport" ] }, "unique_reports": { @@ -353,8 +557,340 @@ ] } } + }, + { + "type": "object", + "required": [ + "inputs", + "preserve_existing_outputs", + "target_env", + "target_exe", + "target_options", + "type", + "unique_inputs" + ], + "properties": { + "check_fuzzer_help": { + "default": true, + "type": "boolean" + }, + "input_queue": { + "type": [ + "string", + "null" + ] + }, + "inputs": { + "type": "array", + "items": { + "type": "string" + } + }, + "preserve_existing_outputs": { + "type": "boolean" + }, + "target_env": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "target_exe": { + "type": "string" + }, + "target_options": { + "type": "array", + "items": { + "type": "string" + } + }, + "type": { + "type": "string", + "enum": [ + "LibfuzzerMerge" + ] + }, + "unique_inputs": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "crashes", + "regression_reports", + "target_exe", + "type" + ], + "properties": { + "check_fuzzer_help": { + "default": true, + "type": "boolean" + }, + "check_retry_count": { + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "crashes": { + "type": "string" + }, + "minimized_stack_depth": { + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint", + "minimum": 0.0 + }, + "no_repro": { + "type": [ + "string", + "null" + ] + }, + "readonly_inputs": { + "type": [ + "string", + "null" + ] + }, + "regression_reports": { + "type": "string" + }, + "report_list": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "reports": { + "type": [ + "string", + "null" + ] + }, + "target_env": { + "default": {}, + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "target_exe": { + "type": "string" + }, + "target_options": { + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "target_timeout": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "type": { + "type": "string", + "enum": [ + "LibfuzzerRegression" + ] + }, + "unique_reports": { + "type": [ + "string", + "null" + ] + } + } + }, + { + "type": "object", + "required": [ + "check_retry_count", + "input", + "setup_dir", + "target_env", + "target_exe", + "target_options", + "type" + ], + "properties": { + "check_retry_count": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "extra_output_dir": { + "type": [ + "string", + "null" + ] + }, + "extra_setup_dir": { + "type": [ + "string", + "null" + ] + }, + "input": { + "type": "string" + }, + "minimized_stack_depth": { + "type": [ + "integer", + "null" + ], + "format": "uint", + "minimum": 0.0 + }, + "setup_dir": { + "type": "string" + }, + "target_env": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "target_exe": { + "type": "string" + }, + "target_options": { + "type": "array", + "items": { + "type": "string" + } + }, + "target_timeout": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "type": { + "type": "string", + "enum": [ + "LibfuzzerTestInput" + ] + } + } + }, + { + "type": "object", + "required": [ + "check_asan_log", + "check_debugger", + "check_retry_count", + "input", + "job_id", + "setup_dir", + "target_env", + "target_exe", + "target_options", + "task_id", + "type" + ], + "properties": { + "check_asan_log": { + "type": "boolean" + }, + "check_debugger": { + "type": "boolean" + }, + "check_retry_count": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "extra_setup_dir": { + "type": [ + "string", + "null" + ] + }, + "input": { + "type": "string" + }, + "job_id": { + "type": "string", + "format": "uuid" + }, + "minimized_stack_depth": { + "type": [ + "integer", + "null" + ], + "format": "uint", + "minimum": 0.0 + }, + "setup_dir": { + "type": "string" + }, + "target_env": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "target_exe": { + "type": "string" + }, + "target_options": { + "type": "array", + "items": { + "type": "string" + } + }, + "target_timeout": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "task_id": { + "type": "string", + "format": "uuid" + }, + "type": { + "type": "string", + "enum": [ + "TestInput" + ] + } + } + }, + { + "description": "The radamsa task can be represented via a combination of the `Generator` and `Report` tasks. Please see `src/agent/onefuzz-task/src/local/example_templates/radamsa.yml` for an example template", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "Radamsa" + ] + } + } } ] } } -} \ No newline at end of file +} diff --git a/src/agent/onefuzz-task/src/local/template.rs b/src/agent/onefuzz-task/src/local/template.rs index ce0908fd58..b2e0c425ff 100644 --- a/src/agent/onefuzz-task/src/local/template.rs +++ b/src/agent/onefuzz-task/src/local/template.rs @@ -1,29 +1,25 @@ +use async_trait::async_trait; use flume::Sender; use onefuzz::{blob::BlobContainerUrl, syncdir::SyncedDir, utils::try_wait_all_join_handles}; use path_absolutize::Absolutize; use serde::Deserialize; -use std::{ - collections::HashMap, - path::{Path, PathBuf}, -}; +use std::path::{Path, PathBuf}; use storage_queue::QueueClient; use tokio::{sync::Mutex, task::JoinHandle}; use url::Url; use uuid::Uuid; -use crate::tasks::{ - config::CommonConfig, - fuzz::{ - self, - libfuzzer::{common::default_workers, generic::LibFuzzerFuzzTask}, - }, - report, +use crate::local::{ + coverage::Coverage, generic_analysis::Analysis, generic_crash_report::CrashReport, + generic_generator::Generator, libfuzzer::LibFuzzer, + libfuzzer_crash_report::LibfuzzerCrashReport, libfuzzer_merge::LibfuzzerMerge, + libfuzzer_regression::LibfuzzerRegression, libfuzzer_test_input::LibfuzzerTestInput, + test_input::TestInput, }; +use crate::tasks::config::CommonConfig; use super::common::{DirectoryMonitorQueue, SyncCountDirMonitor, UiEvent}; -use anyhow::Result; - -use futures::future::OptionFuture; +use anyhow::{Error, Result}; use schemars::JsonSchema; @@ -46,232 +42,73 @@ struct CommonProperties { pub create_job_dir: bool, } -#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] -struct LibFuzzer { - inputs: PathBuf, - readonly_inputs: Vec, - crashes: PathBuf, - crashdumps: PathBuf, - target_exe: PathBuf, - target_env: HashMap, - target_options: Vec, - target_workers: Option, - ensemble_sync_delay: Option, - #[serde(default = "default_bool_true")] - check_fuzzer_help: bool, - #[serde(default)] - expect_crash_on_failure: bool, -} - -#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] -struct Analysis { - analyzer_exe: String, - analyzer_options: Vec, - analyzer_env: HashMap, - target_exe: PathBuf, - target_options: Vec, - input_queue: Option, - crashes: Option, - analysis: PathBuf, - tools: PathBuf, - reports: Option, - unique_reports: Option, - no_repro: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] -struct Report { - target_exe: PathBuf, - target_env: HashMap, - // TODO: options are not yet used for crash reporting - target_options: Vec, - target_timeout: Option, - input_queue: Option, - crashes: Option, - reports: Option, - unique_reports: Option, - no_repro: Option, - #[serde(default = "default_bool_true")] - check_fuzzer_help: bool, - #[serde(default)] - check_retry_count: u64, - #[serde(default)] - minimized_stack_depth: Option, - #[serde(default = "default_bool_true")] - check_queue: bool, -} - -pub fn default_bool_true() -> bool { - true -} - -#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] -struct Coverage { - target_exe: PathBuf, - target_env: HashMap, - target_options: Vec, - target_timeout: Option, - module_allowlist: Option, - source_allowlist: Option, - input_queue: Option, - readonly_inputs: Vec, - coverage: PathBuf, -} - #[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] #[serde(tag = "type")] enum TaskConfig { LibFuzzer(LibFuzzer), Analysis(Analysis), Coverage(Coverage), - Report(Report), + CrashReport(CrashReport), + Generator(Generator), + LibfuzzerCrashReport(LibfuzzerCrashReport), + LibfuzzerMerge(LibfuzzerMerge), + LibfuzzerRegression(LibfuzzerRegression), + LibfuzzerTestInput(LibfuzzerTestInput), + TestInput(TestInput), + /// The radamsa task can be represented via a combination of the `Generator` and `Report` tasks. + /// Please see `src/agent/onefuzz-task/src/local/example_templates/radamsa.yml` for an example template + Radamsa, +} + +#[async_trait] +pub trait Template { + async fn run(&self, context: &RunContext) -> Result<()>; } impl TaskConfig { async fn launch(&self, context: RunContext) -> Result { match self { TaskConfig::LibFuzzer(config) => { - let ri: Result> = config - .readonly_inputs - .iter() - .enumerate() - .map(|(index, input)| { - context.to_sync_dir(format!("readonly_inputs_{index}"), input) - }) - .collect(); - - let libfuzzer_config = fuzz::libfuzzer::generic::Config { - inputs: context.to_monitored_sync_dir("inputs", &config.inputs)?, - readonly_inputs: Some(ri?), - crashes: context.to_monitored_sync_dir("crashes", &config.crashes)?, - crashdumps: Some( - context.to_monitored_sync_dir("crashdumps", &config.crashdumps)?, - ), - target_exe: config.target_exe.clone(), - target_env: config.target_env.clone(), - target_options: config.target_options.clone(), - target_workers: config.target_workers.unwrap_or(default_workers()), - ensemble_sync_delay: config.ensemble_sync_delay, - check_fuzzer_help: config.check_fuzzer_help, - expect_crash_on_failure: config.expect_crash_on_failure, - extra: (), - common: CommonConfig { - task_id: uuid::Uuid::new_v4(), - ..context.common.clone() - }, - }; - - context - .spawn(async move { - let fuzzer = LibFuzzerFuzzTask::new(libfuzzer_config)?; - fuzzer.run().await - }) - .await; + config.run(&context).await?; + } + TaskConfig::Analysis(config) => { + config.run(&context).await?; } - TaskConfig::Analysis(_analysis) => {} TaskConfig::Coverage(config) => { - let ri: Result> = config - .readonly_inputs - .iter() - .enumerate() - .map(|(index, input)| { - context.to_sync_dir(format!("readonly_inputs_{index}"), input) - }) - .collect(); - - let input_q = if let Some(w) = &config.input_queue { - Some(context.monitor_dir(w).await?) - } else { - None - }; - - let coverage_config = crate::tasks::coverage::generic::Config { - target_exe: config.target_exe.clone(), - target_env: config.target_env.clone(), - target_options: config.target_options.clone(), - target_timeout: None, - readonly_inputs: ri?, - input_queue: input_q, - common: CommonConfig { - task_id: uuid::Uuid::new_v4(), - ..context.common.clone() - }, - coverage_filter: None, - coverage: context.to_monitored_sync_dir("coverage", config.coverage.clone())?, - module_allowlist: config.module_allowlist.clone(), - source_allowlist: config.source_allowlist.clone(), - }; - - context - .spawn(async move { - let mut coverage = - crate::tasks::coverage::generic::CoverageTask::new(coverage_config); - coverage.run().await - }) - .await; + config.run(&context).await?; } - TaskConfig::Report(config) => { - let input_q_fut: OptionFuture<_> = config - .input_queue - .iter() - .map(|w| context.monitor_dir(w)) - .next() - .into(); - - let input_q = input_q_fut.await.transpose()?; - let report_config = report::libfuzzer_report::Config { - target_exe: config.target_exe.clone(), - target_env: config.target_env.clone(), - target_options: config.target_options.clone(), - target_timeout: config.target_timeout, - input_queue: input_q, - crashes: config - .crashes - .clone() - .map(|c| context.to_monitored_sync_dir("crashes", c)) - .transpose()?, - reports: config - .reports - .clone() - .map(|c| context.to_monitored_sync_dir("reports", c)) - .transpose()?, - unique_reports: config - .unique_reports - .clone() - .map(|c| context.to_monitored_sync_dir("unique_reports", c)) - .transpose()?, - no_repro: config - .no_repro - .clone() - .map(|c| context.to_monitored_sync_dir("no_repro", c)) - .transpose()?, - check_fuzzer_help: config.check_fuzzer_help, - check_retry_count: config.check_retry_count, - minimized_stack_depth: config.minimized_stack_depth, - check_queue: config.check_queue, - common: CommonConfig { - task_id: uuid::Uuid::new_v4(), - ..context.common.clone() - }, - }; - - context - .spawn(async move { - let mut report = report::libfuzzer_report::ReportTask::new(report_config); - report.managed_run().await - }) - .await; + TaskConfig::CrashReport(config) => { + config.run(&context).await?; + } + TaskConfig::Generator(config) => { + config.run(&context).await?; } + TaskConfig::LibfuzzerCrashReport(config) => { + config.run(&context).await?; + } + TaskConfig::LibfuzzerMerge(config) => { + config.run(&context).await?; + } + TaskConfig::LibfuzzerRegression(config) => { + config.run(&context).await?; + } + TaskConfig::LibfuzzerTestInput(config) => { + config.run(&context).await?; + } + TaskConfig::TestInput(config) => { + config.run(&context).await?; + } + TaskConfig::Radamsa => {} } Ok(context) } } -struct RunContext { +pub struct RunContext { monitor_queues: Mutex>, tasks_handle: Mutex>>>, - common: CommonConfig, + pub common: CommonConfig, event_sender: Option>, create_job_dir: bool, } @@ -287,14 +124,14 @@ impl RunContext { } } - async fn monitor_dir(&self, watch: impl AsRef) -> Result { + pub async fn monitor_dir(&self, watch: impl AsRef) -> Result { let monitor_q = DirectoryMonitorQueue::start_monitoring(watch).await?; let q_client = monitor_q.queue_client.clone(); self.monitor_queues.lock().await.push(monitor_q); Ok(q_client) } - fn to_monitored_sync_dir( + pub fn to_monitored_sync_dir( &self, name: impl AsRef, path: impl AsRef, @@ -307,7 +144,7 @@ impl RunContext { .monitor_count(&self.event_sender) } - fn to_sync_dir(&self, name: impl AsRef, path: impl AsRef) -> Result { + pub fn to_sync_dir(&self, name: impl AsRef, path: impl AsRef) -> Result { let path = path.as_ref(); let name = name.as_ref(); let current_dir = std::env::current_dir()?; @@ -335,6 +172,10 @@ impl RunContext { future: impl futures::Future> + std::marker::Send + 'static, ) { let handle = tokio::spawn(future); + self.add_handle(handle).await; + } + + pub async fn add_handle(&self, handle: JoinHandle>) { self.tasks_handle.lock().await.push(handle); } } @@ -392,6 +233,20 @@ mod test { #[test] fn test() { let schema = schemars::schema_for!(super::TaskGroup); - println!("{}", serde_json::to_string_pretty(&schema).unwrap()); + let schema_str = serde_json::to_string_pretty(&schema) + .unwrap() + .replace("\r\n", "\n"); + + let checked_in_schema = std::fs::read_to_string("src/local/schema.json") + .expect("Couldn't find checked-in schema.json") + .replace("\r\n", "\n"); + + println!("{}", schema_str); + + assert_eq!( + schema_str.replace('\n', ""), + checked_in_schema.replace('\n', ""), + "The checked-in local fuzzing schema did not match the generated schema." + ); } } diff --git a/src/agent/onefuzz-task/src/local/test_input.rs b/src/agent/onefuzz-task/src/local/test_input.rs index 715e1a5141..4077bd08f8 100644 --- a/src/agent/onefuzz-task/src/local/test_input.rs +++ b/src/agent/onefuzz-task/src/local/test_input.rs @@ -10,9 +10,15 @@ use crate::{ tasks::report::generic::{test_input, TestInputArgs}, }; use anyhow::Result; +use async_trait::async_trait; use clap::{Arg, ArgAction, Command}; use flume::Sender; -use std::path::PathBuf; +use onefuzz::machine_id::MachineIdentity; +use schemars::JsonSchema; +use std::{collections::HashMap, path::PathBuf}; +use uuid::Uuid; + +use super::template::{RunContext, Template}; pub async fn run(args: &clap::ArgMatches, event_sender: Option>) -> Result<()> { let context = build_local_context(args, false, event_sender).await?; @@ -89,3 +95,57 @@ pub fn args(name: &'static str) -> Command { .about("test an application with a specific input") .args(&build_shared_args()) } + +#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] +pub struct TestInput { + input: PathBuf, + target_exe: PathBuf, + target_options: Vec, + target_env: HashMap, + setup_dir: PathBuf, + extra_setup_dir: Option, + task_id: Uuid, + job_id: Uuid, + target_timeout: Option, + check_retry_count: u64, + check_asan_log: bool, + check_debugger: bool, + minimized_stack_depth: Option, +} + +#[async_trait] +impl Template for TestInput { + async fn run(&self, context: &RunContext) -> Result<()> { + let c = self.clone(); + let t = tokio::spawn(async move { + let libfuzzer_test_input = crate::tasks::report::generic::TestInputArgs { + input_url: None, + input: c.input.as_path(), + target_exe: c.target_exe.as_path(), + target_options: &c.target_options, + target_env: &c.target_env, + setup_dir: &c.setup_dir, + extra_setup_dir: c.extra_setup_dir.as_deref(), + task_id: uuid::Uuid::new_v4(), + job_id: uuid::Uuid::new_v4(), + target_timeout: c.target_timeout, + check_retry_count: c.check_retry_count, + check_asan_log: c.check_asan_log, + check_debugger: c.check_debugger, + minimized_stack_depth: c.minimized_stack_depth, + machine_identity: MachineIdentity { + machine_id: uuid::Uuid::new_v4(), + machine_name: "local".to_string(), + scaleset_name: None, + }, + }; + + crate::tasks::report::generic::test_input(libfuzzer_test_input) + .await + .map(|_| ()) + }); + + context.add_handle(t).await; + Ok(()) + } +} diff --git a/src/agent/onefuzz/src/input_tester.rs b/src/agent/onefuzz/src/input_tester.rs index dc0775ed28..331e466280 100644 --- a/src/agent/onefuzz/src/input_tester.rs +++ b/src/agent/onefuzz/src/input_tester.rs @@ -215,7 +215,9 @@ impl<'a> Tester<'a> { let triage = crate::triage::TriageCommand::new(cmd)?; // Share the new child ID with main thread. - let Ok(()) = sender.send(triage.pid()) else { bail!("unable to send PID") }; + let Ok(()) = sender.send(triage.pid()) else { + bail!("unable to send PID") + }; // The target run is blocking, and may hang. triage.run()