diff --git a/CURRENT_VERSION b/CURRENT_VERSION index 3ecb398163..e5c15102d9 100644 --- a/CURRENT_VERSION +++ b/CURRENT_VERSION @@ -1 +1 @@ -8.9.0 \ No newline at end of file +8.9.0 diff --git a/src/agent/Cargo.lock b/src/agent/Cargo.lock index eb35241201..d71771494c 100644 --- a/src/agent/Cargo.lock +++ b/src/agent/Cargo.lock @@ -342,6 +342,21 @@ dependencies = [ "shlex", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -1776,6 +1791,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "libm" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" + [[package]] name = "libproc" version = "0.12.0" @@ -2080,6 +2101,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2245,6 +2267,7 @@ dependencies = [ "coverage", "crossterm 0.27.0", "debuggable-module", + "dunce", "env_logger", "flexi_logger", "flume", @@ -2260,6 +2283,7 @@ dependencies = [ "onefuzz-telemetry", "path-absolutize", "pretty_assertions", + "proptest", "ratatui", "regex", "reqwest", @@ -2592,6 +2616,26 @@ dependencies = [ "rustix 0.36.15", ] +[[package]] +name = "proptest" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c003ac8c77cb07bb74f5f198bce836a689bcd5a42574612bf14d17bfd08c20e" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.3.3", + "lazy_static", + "num-traits", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + [[package]] name = "queue-file" version = "1.4.10" @@ -2602,6 +2646,12 @@ dependencies = [ "snafu", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quick-xml" version = "0.30.0" @@ -2701,6 +2751,15 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "range-collections" version = "0.2.4" @@ -2911,6 +2970,18 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.15" @@ -3676,6 +3747,12 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-bidi" version = "0.3.13" @@ -3797,6 +3874,15 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "waker-fn" version = "1.1.0" diff --git a/src/agent/onefuzz-task/Cargo.toml b/src/agent/onefuzz-task/Cargo.toml index 4b3e8e8c43..8d12c1cacb 100644 --- a/src/agent/onefuzz-task/Cargo.toml +++ b/src/agent/onefuzz-task/Cargo.toml @@ -85,4 +85,6 @@ schemars = { version = "0.8.12", features = ["uuid1"] } [dev-dependencies] pretty_assertions = "1.4" +proptest = "1.3.1" tempfile = "3.8" +dunce = "1.0" diff --git a/src/agent/onefuzz-task/src/config_test_utils.rs b/src/agent/onefuzz-task/src/config_test_utils.rs new file mode 100644 index 0000000000..1b36ce50bf --- /dev/null +++ b/src/agent/onefuzz-task/src/config_test_utils.rs @@ -0,0 +1,487 @@ +use onefuzz::expand::PlaceHolder; + +// Moving this trait method into the GetExpand trait, and returning `Vec<(PlaceHolder, Box)>` instead, +// would let us use define a default implementation for `get_expand()` while also coupling the expand values we +// test with those we give to the expander. +// It seems to me like a non-trivial (and perhaps bad) design change though. +pub trait GetExpandFields { + fn get_expand_fields(&self) -> Vec<(PlaceHolder, String)>; +} + +macro_rules! config_test { + ($t:ty) => { + proptest! { + #[test] + fn test_get_expand_values_match_config( + config in any::<$t>(), + ) { + let expand = config.get_expand(); + let params = config.get_expand_fields(); + + for (param, expected) in params.iter() { + let evaluated = expand.evaluate_value(param.get_string()).unwrap(); + assert_eq!(evaluated, *expected, "placeholder {} did not match expected value", param.get_string()); + } + } + } + } +} + +pub mod arbitraries { + use std::path::PathBuf; + + use onefuzz::{blob::BlobContainerUrl, machine_id::MachineIdentity, syncdir::SyncedDir}; + use onefuzz_telemetry::{InstanceTelemetryKey, MicrosoftTelemetryKey}; + use proptest::{option, prelude::*}; + use reqwest::Url; + use uuid::Uuid; + + use crate::tasks::{analysis, config::CommonConfig, coverage, fuzz, merge, report}; + + prop_compose! { + fn arb_uuid()( + uuid in "[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}" + ) -> Uuid { + Uuid::parse_str(&uuid).unwrap() + } + } + + prop_compose! { + fn arb_instance_telemetry_key()( + uuid in arb_uuid() + ) -> InstanceTelemetryKey { + InstanceTelemetryKey::new(uuid) + } + } + + prop_compose! { + fn arb_microsoft_telemetry_key()( + uuid in arb_uuid() + ) -> MicrosoftTelemetryKey { + MicrosoftTelemetryKey::new(uuid) + } + } + + prop_compose! { + fn arb_url()( + // Don't use this for any url that isn't just being used for a string comparison (as for the config tests) + // basically all that matters here is that we generate a parsable url + url in r"https?://(www\.)?[a-zA-Z0-9]{1,64}\.com" + ) -> Url { + match Url::parse(&url) { + Ok(url) => url, + Err(err) => panic!("invalid url generated ({}): {}", err, url), + } + } + } + + prop_compose! { + // Todo: consider a better way to generate a path + fn arb_pathbuf()( + path in "src" + ) -> PathBuf { + PathBuf::from(path) + } + } + + prop_compose! { + fn arb_machine_identity()( + machine_id in arb_uuid(), + machine_name in ".*", + scaleset_name in ".*", + ) -> MachineIdentity { + MachineIdentity { + machine_id, + machine_name, + scaleset_name: Some(scaleset_name), + } + } + } + + fn arb_blob_container_url() -> impl Strategy { + prop_oneof![ + arb_url().prop_map(BlobContainerUrl::BlobContainer), + arb_pathbuf().prop_map(BlobContainerUrl::Path), + ] + } + + prop_compose! { + fn arb_synced_dir()( + local_path in arb_pathbuf(), + remote_path in option::of(arb_blob_container_url()), + ) -> SyncedDir { + SyncedDir { + local_path, + remote_path, + } + } + } + + prop_compose! { + fn arb_string_vec_no_vars()( + // I don't know how to figure out the expected value of the target options if they could contain variables (e.g. {machine_id}) + // This should be fine since this isn't used to test nested expansion + options in prop::collection::vec("[^{}]*", 10), + ) -> Vec { + options + } + } + + prop_compose! { + fn arb_common_config()( + job_id in arb_uuid(), + task_id in arb_uuid(), + instance_id in arb_uuid(), + heartbeat_queue in option::of(arb_url()), + job_result_queue in option::of(arb_url()), + instance_telemetry_key in option::of(arb_instance_telemetry_key()), // consider implementing Arbitrary for these types for a canonical way to generate them + microsoft_telemetry_key in option::of(arb_microsoft_telemetry_key()), // We can probably derive Arbitrary if it's implemented for the composing types like Url + logs in option::of(arb_url()), + setup_dir in arb_pathbuf(), + extra_setup_dir in option::of(arb_pathbuf()), + extra_output in option::of(arb_synced_dir()), + min_available_memory_mb in any::(), + machine_identity in arb_machine_identity(), + tags in prop::collection::hash_map(".*", ".*", 3), + from_agent_to_task_endpoint in ".*", + from_task_to_agent_endpoint in ".*", + ) -> CommonConfig { + CommonConfig { + job_id, + task_id, + instance_id, + heartbeat_queue, + job_result_queue, + instance_telemetry_key, + microsoft_telemetry_key, + logs, + setup_dir, + extra_setup_dir, + extra_output, + min_available_memory_mb, + machine_identity, + tags, + from_agent_to_task_endpoint, + from_task_to_agent_endpoint, + } + } + } + + impl Arbitrary for CommonConfig { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + arb_common_config().boxed() + } + } + + prop_compose! { + fn arb_analysis_config()( + analyzer_exe in Just("src/lib.rs".to_string()), + analyzer_options in arb_string_vec_no_vars(), + analyzer_env in prop::collection::hash_map(".*", ".*", 10), + target_exe in arb_pathbuf(), + target_options in arb_string_vec_no_vars(), + input_queue in Just(None), + crashes in option::of(arb_synced_dir()), + analysis in arb_synced_dir(), + tools in option::of(arb_synced_dir()), + reports in option::of(arb_synced_dir()), + unique_reports in option::of(arb_synced_dir()), + no_repro in option::of(arb_synced_dir()), + common in arb_common_config(), + ) -> analysis::generic::Config { + analysis::generic::Config { + analyzer_exe, + analyzer_options, + analyzer_env, + target_exe, + target_options, + input_queue, + crashes, + analysis, + tools, + reports, + unique_reports, + no_repro, + common, + } + } + } + + impl Arbitrary for analysis::generic::Config { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + arb_analysis_config().boxed() + } + } + + prop_compose! { + fn arb_merge_config()( + supervisor_exe in Just("src/lib.rs".to_string()), + supervisor_options in arb_string_vec_no_vars(), + supervisor_env in prop::collection::hash_map(".*", ".*", 10), + supervisor_input_marker in ".*", + target_exe in arb_pathbuf(), + target_options in arb_string_vec_no_vars(), + target_options_merge in any::(), + tools in arb_synced_dir(), + input_queue in arb_url(), + inputs in arb_synced_dir(), + unique_inputs in arb_synced_dir(), + common in arb_common_config(), + ) -> merge::generic::Config { + merge::generic::Config { + supervisor_exe, + supervisor_options, + supervisor_env, + supervisor_input_marker, + target_exe, + target_options, + target_options_merge, + tools, + input_queue, + inputs, + unique_inputs, + common, + } + } + } + + impl Arbitrary for merge::generic::Config { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + arb_merge_config().boxed() + } + } + + prop_compose! { + fn arb_coverage_config()( + target_exe in arb_pathbuf(), + target_env in prop::collection::hash_map(".*", ".*", 10), + target_options in arb_string_vec_no_vars(), + target_timeout in option::of(any::()), + coverage_filter in option::of(".*"), + module_allowlist in option::of(".*"), + source_allowlist in option::of(".*"), + input_queue in Just(None), + readonly_inputs in prop::collection::vec(arb_synced_dir(), 10), + coverage in arb_synced_dir(), + common in arb_common_config(), + ) -> coverage::generic::Config { + coverage::generic::Config { + target_exe, + target_env, + target_options, + target_timeout, + coverage_filter, + module_allowlist, + source_allowlist, + input_queue, + readonly_inputs, + coverage, + common, + } + } + } + + impl Arbitrary for coverage::generic::Config { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + arb_coverage_config().boxed() + } + } + + prop_compose! { + fn arb_dotnet_coverage_config()( + target_exe in arb_pathbuf(), + target_env in prop::collection::hash_map(".*", ".*", 10), + target_options in arb_string_vec_no_vars(), + target_timeout in option::of(any::()), + input_queue in Just(None), + readonly_inputs in prop::collection::vec(arb_synced_dir(), 10), + coverage in arb_synced_dir(), + tools in arb_synced_dir(), + common in arb_common_config(), + ) -> coverage::dotnet::Config { + coverage::dotnet::Config { + target_exe, + target_env, + target_options, + target_timeout, + input_queue, + readonly_inputs, + coverage, + tools, + common, + } + } + } + + impl Arbitrary for coverage::dotnet::Config { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + arb_dotnet_coverage_config().boxed() + } + } + + prop_compose! { + fn arb_dotnet_report_config()( + target_exe in arb_pathbuf(), + target_env in prop::collection::hash_map(".*", ".*", 10), + target_options in arb_string_vec_no_vars(), + target_timeout in option::of(any::()), + input_queue in Just(None), + crashes in option::of(arb_synced_dir()), + reports in option::of(arb_synced_dir()), + unique_reports in option::of(arb_synced_dir()), + no_repro in option::of(arb_synced_dir()), + tools in arb_synced_dir(), + check_fuzzer_help in any::(), + check_retry_count in any::(), + minimized_stack_depth in option::of(any::()), + check_queue in any::(), + common in arb_common_config(), + ) -> report::dotnet::generic::Config { + report::dotnet::generic::Config { + target_exe, + target_env, + target_options, + target_timeout, + input_queue, + crashes, + reports, + unique_reports, + no_repro, + tools, + check_fuzzer_help, + check_retry_count, + minimized_stack_depth, + check_queue, + common, + } + } + } + + impl Arbitrary for report::dotnet::generic::Config { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + arb_dotnet_report_config().boxed() + } + } + + prop_compose! { + fn arb_generator_fuzz_config()( + generator_exe in Just("src/lib.rs".to_string()), + generator_env in prop::collection::hash_map(".*", ".*", 10), + generator_options in arb_string_vec_no_vars(), + readonly_inputs in prop::collection::vec(arb_synced_dir(), 10), + crashes in arb_synced_dir(), + tools in option::of(arb_synced_dir()), + target_exe in arb_pathbuf(), + target_env in prop::collection::hash_map(".*", ".*", 10), + target_options in arb_string_vec_no_vars(), + target_timeout in option::of(any::()), + check_asan_log in any::(), + check_debugger in any::(), + check_retry_count in any::(), + rename_output in any::(), + ensemble_sync_delay in option::of(any::()), + common in arb_common_config(), + ) -> fuzz::generator::Config { + fuzz::generator::Config { + generator_exe, + generator_env, + generator_options, + readonly_inputs, + crashes, + tools, + target_exe, + target_env, + target_options, + target_timeout, + check_asan_log, + check_debugger, + check_retry_count, + rename_output, + ensemble_sync_delay, + common, + } + } + } + + impl Arbitrary for fuzz::generator::Config { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + arb_generator_fuzz_config().boxed() + } + } + + prop_compose! { + fn arb_supervisor_config()( + inputs in arb_synced_dir(), + crashes in arb_synced_dir(), + crashdumps in option::of(arb_synced_dir()), + supervisor_exe in Just("src/lib.rs".to_string()), + supervisor_env in prop::collection::hash_map(".*", ".*", 0), + supervisor_options in arb_string_vec_no_vars(), + supervisor_input_marker in option::of(".*"), + target_exe in option::of(arb_pathbuf()), + target_options in option::of(arb_string_vec_no_vars()), + tools in option::of(arb_synced_dir()), + wait_for_files in Just(None), + stats_file in Just(None), + stats_format in Just(None), + ensemble_sync_delay in Just(None), + reports in option::of(arb_synced_dir()), + unique_reports in Just(None), + no_repro in Just(None), + coverage in option::of(arb_synced_dir()), + common in arb_common_config(), + ) -> fuzz::supervisor::SupervisorConfig { + fuzz::supervisor::SupervisorConfig { + inputs, + crashes, + crashdumps, + supervisor_exe, + supervisor_env, + supervisor_options, + supervisor_input_marker, + target_exe, + target_options, + tools, + wait_for_files, + stats_file, + stats_format, + ensemble_sync_delay, + reports, + unique_reports, + no_repro, + coverage, + common, + } + } + } + + impl Arbitrary for fuzz::supervisor::SupervisorConfig { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + arb_supervisor_config().boxed() + } + } +} diff --git a/src/agent/onefuzz-task/src/lib.rs b/src/agent/onefuzz-task/src/lib.rs index 997eea549d..cd5a14073d 100644 --- a/src/agent/onefuzz-task/src/lib.rs +++ b/src/agent/onefuzz-task/src/lib.rs @@ -5,5 +5,8 @@ extern crate clap; #[macro_use] extern crate onefuzz_telemetry; +#[cfg(test)] +#[macro_use] +pub mod config_test_utils; pub mod local; pub mod tasks; diff --git a/src/agent/onefuzz-task/src/local/common.rs b/src/agent/onefuzz-task/src/local/common.rs index 17940d799f..16c1e326fe 100644 --- a/src/agent/onefuzz-task/src/local/common.rs +++ b/src/agent/onefuzz-task/src/local/common.rs @@ -231,22 +231,12 @@ pub async fn build_local_context( task_id, instance_id, setup_dir, - extra_setup_dir: None, - extra_output: None, machine_identity: MachineIdentity { machine_id: Uuid::nil(), machine_name: "local".to_string(), scaleset_name: None, }, - instance_telemetry_key: None, - heartbeat_queue: None, - job_result_queue: None, - microsoft_telemetry_key: None, - logs: None, - min_available_memory_mb: 0, - tags: Default::default(), - from_agent_to_task_endpoint: "/".to_string(), - from_task_to_agent_endpoint: "/".to_string(), + ..Default::default() }; let current_dir = current_dir()?; diff --git a/src/agent/onefuzz-task/src/local/template.rs b/src/agent/onefuzz-task/src/local/template.rs index 3393edd89a..7b3838e5cd 100644 --- a/src/agent/onefuzz-task/src/local/template.rs +++ b/src/agent/onefuzz-task/src/local/template.rs @@ -195,14 +195,8 @@ pub async fn launch( let task_group: TaskGroup = serde_yaml::from_value(value)?; let common = CommonConfig { - task_id: Uuid::nil(), job_id: Uuid::new_v4(), instance_id: Uuid::new_v4(), - heartbeat_queue: None, - job_result_queue: None, - instance_telemetry_key: None, - microsoft_telemetry_key: None, - logs: None, setup_dir: task_group.common.setup_dir.unwrap_or_default(), extra_setup_dir: task_group.common.extra_setup_dir, min_available_memory_mb: crate::tasks::config::default_min_available_memory_mb(), @@ -211,10 +205,7 @@ pub async fn launch( machine_name: "local".to_string(), scaleset_name: None, }, - tags: Default::default(), - from_agent_to_task_endpoint: "/".to_string(), - from_task_to_agent_endpoint: "/".to_string(), - extra_output: None, + ..Default::default() }; let mut context = RunContext::new(common, event_sender); diff --git a/src/agent/onefuzz-task/src/tasks/analysis/generic.rs b/src/agent/onefuzz-task/src/tasks/analysis/generic.rs index 05c6c3d169..0ef97f236f 100644 --- a/src/agent/onefuzz-task/src/tasks/analysis/generic.rs +++ b/src/agent/onefuzz-task/src/tasks/analysis/generic.rs @@ -47,6 +47,33 @@ pub struct Config { pub common: CommonConfig, } +impl Config { + pub fn get_expand(&self) -> Expand<'_> { + self.common + .get_expand() + .analyzer_exe(&self.analyzer_exe) + .analyzer_options(&self.analyzer_options) + .target_exe(&self.target_exe) + .target_options(&self.target_options) + .output_dir(&self.analysis.local_path) + .set_optional(self.tools.clone().map(|t| t.local_path), Expand::tools_dir) + .set_optional_ref(&self.reports, |expand, reports| { + expand.reports_dir(reports.local_path.as_path()) + }) + .set_optional_ref(&self.crashes, |expand, crashes| { + expand + .set_optional_ref( + &crashes.remote_path.clone().and_then(|u| u.account()), + |expand, account| expand.crashes_account(account), + ) + .set_optional_ref( + &crashes.remote_path.clone().and_then(|u| u.container()), + |expand, container| expand.crashes_container(container), + ) + }) + } +} + pub async fn run(config: Config) -> Result<()> { let task_dir = config .analysis @@ -206,45 +233,11 @@ pub async fn run_tool( let target_exe = try_resolve_setup_relative_path(&config.common.setup_dir, &config.target_exe).await?; - let expand = Expand::new(&config.common.machine_identity) - .machine_id() - .input_path(&input) + let expand = config + .get_expand() + .input_path(&input) // Only this one is dynamic, the other two should probably be a part of the config .target_exe(&target_exe) - .target_options(&config.target_options) - .analyzer_exe(&config.analyzer_exe) - .analyzer_options(&config.analyzer_options) - .output_dir(&config.analysis.local_path) - .setup_dir(&config.common.setup_dir) - .set_optional( - config.tools.clone().map(|t| t.local_path), - Expand::tools_dir, - ) - .set_optional_ref(&config.common.extra_setup_dir, Expand::extra_setup_dir) - .set_optional_ref(&config.common.extra_output, |expand, value| { - expand.extra_output_dir(value.local_path.as_path()) - }) - .job_id(&config.common.job_id) - .task_id(&config.common.task_id) - .set_optional_ref(&config.common.microsoft_telemetry_key, |tester, key| { - tester.microsoft_telemetry_key(key) - }) - .set_optional_ref(&config.common.instance_telemetry_key, |tester, key| { - tester.instance_telemetry_key(key) - }) - .set_optional_ref(reports_dir, |tester, reports_dir| { - tester.reports_dir(reports_dir) - }) - .set_optional_ref(&config.crashes, |tester, crashes| { - tester - .set_optional_ref( - &crashes.remote_path.clone().and_then(|u| u.account()), - |tester, account| tester.crashes_account(account), - ) - .set_optional_ref( - &crashes.remote_path.clone().and_then(|u| u.container()), - |tester, container| tester.crashes_container(container), - ) - }); + .set_optional_ref(reports_dir, Expand::reports_dir); let analyzer_path = expand.evaluate_value(&config.analyzer_exe)?; @@ -273,3 +266,75 @@ pub async fn run_tool( .with_context(|| format!("analyzer failed to run: {analyzer_path}"))?; Ok(()) } + +#[cfg(test)] +mod tests { + use onefuzz::expand::PlaceHolder; + use proptest::prelude::*; + + use crate::config_test_utils::GetExpandFields; + + use super::Config; + + impl GetExpandFields for Config { + fn get_expand_fields(&self) -> Vec<(PlaceHolder, String)> { + let mut params = self.common.get_expand_fields(); + params.push(( + PlaceHolder::AnalyzerExe, + dunce::canonicalize(&self.analyzer_exe) + .unwrap() + .to_string_lossy() + .to_string(), + )); + params.push(( + PlaceHolder::AnalyzerOptions, + self.analyzer_options.join(" "), + )); + params.push(( + PlaceHolder::TargetExe, + dunce::canonicalize(&self.target_exe) + .unwrap() + .to_string_lossy() + .to_string(), + )); + params.push((PlaceHolder::TargetOptions, self.target_options.join(" "))); + params.push(( + PlaceHolder::OutputDir, + dunce::canonicalize(&self.analysis.local_path) + .unwrap() + .to_string_lossy() + .to_string(), + )); + if let Some(tools) = &self.tools { + params.push(( + PlaceHolder::ToolsDir, + dunce::canonicalize(&tools.local_path) + .unwrap() + .to_string_lossy() + .to_string(), + )); + } + if let Some(reports) = &self.reports { + params.push(( + PlaceHolder::ReportsDir, + dunce::canonicalize(&reports.local_path) + .unwrap() + .to_string_lossy() + .to_string(), + )); + } + if let Some(crashes) = &self.crashes { + if let Some(account) = crashes.remote_path.clone().and_then(|u| u.account()) { + params.push((PlaceHolder::CrashesAccount, account)); + } + if let Some(container) = crashes.remote_path.clone().and_then(|u| u.container()) { + params.push((PlaceHolder::CrashesContainer, container)); + } + } + + params + } + } + + config_test!(Config); +} diff --git a/src/agent/onefuzz-task/src/tasks/config.rs b/src/agent/onefuzz-task/src/tasks/config.rs index e29e0fd60d..c259b413f8 100644 --- a/src/agent/onefuzz-task/src/tasks/config.rs +++ b/src/agent/onefuzz-task/src/tasks/config.rs @@ -11,6 +11,7 @@ use crate::tasks::{ }; use anyhow::{Context, Result}; use onefuzz::{ + expand::Expand, machine_id::MachineIdentity, syncdir::{SyncOperation, SyncedDir}, }; @@ -123,6 +124,54 @@ impl CommonConfig { None => Ok(None), } } + + pub fn get_expand(&self) -> Expand<'_> { + Expand::new(&self.machine_identity) + .machine_id() + .job_id(&self.job_id) + .task_id(&self.task_id) + .setup_dir(&self.setup_dir) + .set_optional_ref(&self.instance_telemetry_key, Expand::instance_telemetry_key) + .set_optional_ref( + &self.microsoft_telemetry_key, + Expand::microsoft_telemetry_key, + ) + .set_optional_ref(&self.extra_setup_dir, Expand::extra_setup_dir) + .set_optional_ref(&self.extra_output, |expand, extra_output| { + expand.extra_output_dir(extra_output.local_path.as_path()) + }) + } +} + +impl Default for CommonConfig { + /// Returns an instance with Default:default() values for all fields besides: + /// - `machine_identity`: with a generated id, "test" for machine name, and None for scaleset name + /// - `from_agent_to_task_endpoint`: with a value of "/" + /// - `from_task_to_agent_endpoint`: with a value of "/" + fn default() -> Self { + Self { + job_id: Default::default(), + task_id: Default::default(), + instance_id: Default::default(), + heartbeat_queue: Default::default(), + job_result_queue: Default::default(), + instance_telemetry_key: Default::default(), + microsoft_telemetry_key: Default::default(), + logs: Default::default(), + setup_dir: Default::default(), + extra_setup_dir: Default::default(), + extra_output: Default::default(), + min_available_memory_mb: Default::default(), + machine_identity: MachineIdentity { + machine_id: uuid::Uuid::new_v4(), + machine_name: "test".to_string(), + scaleset_name: None, + }, + tags: Default::default(), + from_agent_to_task_endpoint: "/".to_string(), + from_task_to_agent_endpoint: "/".to_string(), + } + } } #[derive(Debug, Deserialize)] @@ -364,3 +413,61 @@ impl Config { Ok(()) } } + +#[cfg(test)] +mod tests { + use onefuzz::expand::PlaceHolder; + use proptest::prelude::*; + + use crate::config_test_utils::GetExpandFields; + + use super::CommonConfig; + + impl GetExpandFields for CommonConfig { + fn get_expand_fields(&self) -> Vec<(PlaceHolder, String)> { + let mut params = vec![ + ( + PlaceHolder::MachineId, + self.machine_identity.machine_id.to_string(), + ), + (PlaceHolder::JobId, self.job_id.to_string()), + (PlaceHolder::TaskId, self.task_id.to_string()), + ( + PlaceHolder::SetupDir, + dunce::canonicalize(&self.setup_dir) + .unwrap() + .to_string_lossy() + .to_string(), + ), + ]; + if let Some(key) = &self.instance_telemetry_key { + params.push((PlaceHolder::InstanceTelemetryKey, key.to_string())); + } + if let Some(key) = &self.microsoft_telemetry_key { + params.push((PlaceHolder::MicrosoftTelemetryKey, key.clone().to_string())); + } + if let Some(dir) = &self.extra_setup_dir { + params.push(( + PlaceHolder::ExtraSetupDir, + dunce::canonicalize(dir) + .unwrap() + .to_string_lossy() + .to_string(), + )); + } + if let Some(dir) = &self.extra_output { + params.push(( + PlaceHolder::ExtraOutputDir, + dunce::canonicalize(&dir.local_path) + .unwrap() + .to_string_lossy() + .to_string(), + )); + } + + params + } + } + + config_test!(CommonConfig); +} diff --git a/src/agent/onefuzz-task/src/tasks/coverage/dotnet.rs b/src/agent/onefuzz-task/src/tasks/coverage/dotnet.rs index 93afd1dfd7..8eed445a3d 100644 --- a/src/agent/onefuzz-task/src/tasks/coverage/dotnet.rs +++ b/src/agent/onefuzz-task/src/tasks/coverage/dotnet.rs @@ -56,6 +56,16 @@ impl Config { } } +impl Config { + pub fn get_expand(&self) -> Expand<'_> { + self.common + .get_expand() + .target_options(&self.target_options) + .coverage_dir(&self.coverage.local_path) + .tools_dir(self.tools.local_path.to_string_lossy().into_owned()) + } +} + pub struct DotnetCoverageTask { config: Config, poller: InputPoller, @@ -293,18 +303,11 @@ impl<'a> TaskContext<'a> { async fn command_for_input(&self, input: &Path) -> Result { let target_exe = self.target_exe().await?; - let expand = Expand::new(&self.config.common.machine_identity) - .machine_id() + let expand = self + .config + .get_expand() .input_path(input) - .job_id(&self.config.common.job_id) - .setup_dir(&self.config.common.setup_dir) - .set_optional_ref(&self.config.common.extra_setup_dir, Expand::extra_setup_dir) - .set_optional_ref(&self.config.common.extra_output, |expand, value| { - expand.extra_output_dir(value.local_path.as_path()) - }) - .target_exe(&target_exe) - .target_options(&self.config.target_options) - .task_id(&self.config.common.task_id); + .target_exe(&target_exe); let dotnet_coverage_path = &self.dotnet_coverage_path; let dotnet_path = &self.dotnet_path; @@ -458,3 +461,38 @@ impl<'a> Processor for TaskContext<'a> { Ok(()) } } + +#[cfg(test)] +mod tests { + use onefuzz::expand::PlaceHolder; + use proptest::prelude::*; + + use crate::config_test_utils::GetExpandFields; + + use super::Config; + + impl GetExpandFields for Config { + fn get_expand_fields(&self) -> Vec<(PlaceHolder, String)> { + let mut params = self.common.get_expand_fields(); + params.push((PlaceHolder::TargetOptions, self.target_options.join(" "))); + params.push(( + PlaceHolder::CoverageDir, + dunce::canonicalize(&self.coverage.local_path) + .unwrap() + .to_string_lossy() + .to_string(), + )); + params.push(( + PlaceHolder::ToolsDir, + dunce::canonicalize(&self.tools.local_path) + .unwrap() + .to_string_lossy() + .to_string(), + )); + + params + } + } + + config_test!(Config); +} diff --git a/src/agent/onefuzz-task/src/tasks/coverage/generic.rs b/src/agent/onefuzz-task/src/tasks/coverage/generic.rs index 704188293b..2ebc748010 100644 --- a/src/agent/onefuzz-task/src/tasks/coverage/generic.rs +++ b/src/agent/onefuzz-task/src/tasks/coverage/generic.rs @@ -80,6 +80,13 @@ impl Config { .map(Duration::from_secs) .unwrap_or(DEFAULT_TARGET_TIMEOUT) } + + pub fn get_expand(&self) -> Expand<'_> { + self.common + .get_expand() + .target_options(&self.target_options) + .coverage_dir(&self.coverage.local_path) + } } pub struct CoverageTask { @@ -348,18 +355,11 @@ impl<'a> TaskContext<'a> { try_resolve_setup_relative_path(&self.config.common.setup_dir, &self.config.target_exe) .await?; - let expand = Expand::new(&self.config.common.machine_identity) - .machine_id() - .input_path(input) - .job_id(&self.config.common.job_id) - .setup_dir(&self.config.common.setup_dir) - .set_optional_ref(&self.config.common.extra_setup_dir, Expand::extra_setup_dir) - .set_optional_ref(&self.config.common.extra_output, |expand, value| { - expand.extra_output_dir(value.local_path.as_path()) - }) + let expand = self + .config + .get_expand() .target_exe(&target_exe) - .target_options(&self.config.target_options) - .task_id(&self.config.common.task_id); + .input_path(input); let mut cmd = Command::new(&target_exe); @@ -603,3 +603,31 @@ impl CoverageStats { stats } } + +#[cfg(test)] +mod tests { + use onefuzz::expand::PlaceHolder; + use proptest::prelude::*; + + use crate::config_test_utils::GetExpandFields; + + use super::Config; + + impl GetExpandFields for Config { + fn get_expand_fields(&self) -> Vec<(PlaceHolder, String)> { + let mut params = self.common.get_expand_fields(); + params.push((PlaceHolder::TargetOptions, self.target_options.join(" "))); + params.push(( + PlaceHolder::CoverageDir, + dunce::canonicalize(&self.coverage.local_path) + .unwrap() + .to_string_lossy() + .to_string(), + )); + + params + } + } + + config_test!(Config); +} diff --git a/src/agent/onefuzz-task/src/tasks/fuzz/generator.rs b/src/agent/onefuzz-task/src/tasks/fuzz/generator.rs index bd7511cac2..8e27c4fb0b 100644 --- a/src/agent/onefuzz-task/src/tasks/fuzz/generator.rs +++ b/src/agent/onefuzz-task/src/tasks/fuzz/generator.rs @@ -51,6 +51,21 @@ pub struct Config { pub common: CommonConfig, } +impl Config { + pub fn get_expand(&self) -> Expand<'_> { + self.common + .get_expand() + .generator_exe(&self.generator_exe) + .generator_options(&self.generator_options) + .crashes(&self.crashes.local_path) + .target_exe(&self.target_exe) + .target_options(&self.target_options) + .set_optional_ref(&self.tools, |expand, tools| { + expand.tools_dir(&tools.local_path) + }) + } +} + pub struct GeneratorTask { config: Config, } @@ -169,29 +184,11 @@ impl GeneratorTask { ) -> Result<()> { utils::reset_tmp_dir(&output_dir).await?; let (mut generator, generator_path) = { - let expand = Expand::new(&self.config.common.machine_identity) - .machine_id() - .setup_dir(&self.config.common.setup_dir) - .set_optional_ref(&self.config.common.extra_setup_dir, Expand::extra_setup_dir) - .set_optional_ref(&self.config.common.extra_output, |expand, value| { - expand.extra_output_dir(value.local_path.as_path()) - }) + let expand = self + .config + .get_expand() .generated_inputs(&output_dir) - .input_corpus(&corpus_dir) - .generator_exe(&self.config.generator_exe) - .generator_options(&self.config.generator_options) - .job_id(&self.config.common.job_id) - .task_id(&self.config.common.task_id) - .set_optional_ref( - &self.config.common.microsoft_telemetry_key, - |tester, key| tester.microsoft_telemetry_key(key), - ) - .set_optional_ref(&self.config.common.instance_telemetry_key, |tester, key| { - tester.instance_telemetry_key(key) - }) - .set_optional_ref(&self.config.tools, |expand, tools| { - expand.tools_dir(&tools.local_path) - }); + .input_corpus(&corpus_dir); let generator_path = expand.evaluate_value(&self.config.generator_exe)?; @@ -225,13 +222,63 @@ impl GeneratorTask { } } +#[cfg(test)] mod tests { - #[tokio::test] + use onefuzz::expand::PlaceHolder; + use proptest::prelude::*; + + use crate::config_test_utils::GetExpandFields; + + use super::Config; + + impl GetExpandFields for Config { + fn get_expand_fields(&self) -> Vec<(PlaceHolder, String)> { + let mut params = self.common.get_expand_fields(); + params.push(( + PlaceHolder::GeneratorExe, + dunce::canonicalize(&self.generator_exe) + .unwrap() + .to_string_lossy() + .to_string(), + )); + params.push(( + PlaceHolder::GeneratorOptions, + self.generator_options.join(" "), + )); + params.push(( + PlaceHolder::Crashes, + dunce::canonicalize(&self.crashes.local_path) + .unwrap() + .to_string_lossy() + .to_string(), + )); + params.push(( + PlaceHolder::TargetExe, + dunce::canonicalize(&self.target_exe) + .unwrap() + .to_string_lossy() + .to_string(), + )); + params.push((PlaceHolder::TargetOptions, self.target_options.join(" "))); + if let Some(dir) = &self.tools { + params.push(( + PlaceHolder::ToolsDir, + dunce::canonicalize(&dir.local_path) + .unwrap() + .to_string_lossy() + .to_string(), + )); + } + + params + } + } + + config_test!(Config); + #[cfg(target_os = "linux")] - #[ignore] - async fn test_radamsa_linux() -> anyhow::Result<()> { - use super::{Config, GeneratorTask}; - use crate::tasks::config::CommonConfig; + mod linux { + use super::super::{Config, GeneratorTask}; use onefuzz::blob::BlobContainerUrl; use onefuzz::syncdir::SyncedDir; use reqwest::Url; @@ -239,95 +286,78 @@ mod tests { use std::env; use tempfile::tempdir; - let crashes_temp = tempfile::tempdir()?; - let crashes: &std::path::Path = crashes_temp.path(); - - let inputs_temp = tempfile::tempdir()?; - let inputs: &std::path::Path = inputs_temp.path(); - let input_file = inputs.join("seed.txt"); - tokio::fs::write(input_file, "test").await?; - - let generator_options: Vec = vec![ - "-o", - "{generated_inputs}/input-%n-%s", - "-n", - "100", - "-r", - "{input_corpus}", - ] - .iter() - .map(|p| p.to_string()) - .collect(); - - let radamsa_path = env::var("ONEFUZZ_TEST_RADAMSA_LINUX")?; - let radamsa_as_path = std::path::Path::new(&radamsa_path); - let radamsa_dir = radamsa_as_path.parent().unwrap(); - - let readonly_inputs_local = tempfile::tempdir().unwrap().path().into(); - let crashes_local = tempfile::tempdir().unwrap().path().into(); - let tools_local = tempfile::tempdir().unwrap().path().into(); - let config = Config { - generator_exe: String::from("{tools_dir}/radamsa"), - generator_options, - readonly_inputs: vec![SyncedDir { - local_path: readonly_inputs_local, - remote_path: Some(BlobContainerUrl::parse( - Url::from_directory_path(inputs).unwrap(), - )?), - }], - crashes: SyncedDir { - local_path: crashes_local, - remote_path: Some(BlobContainerUrl::parse( - Url::from_directory_path(crashes).unwrap(), - )?), - }, - tools: Some(SyncedDir { - local_path: tools_local, - remote_path: Some(BlobContainerUrl::parse( - Url::from_directory_path(radamsa_dir).unwrap(), - )?), - }), - target_exe: Default::default(), - target_env: Default::default(), - target_options: Default::default(), - target_timeout: None, - check_asan_log: false, - check_debugger: false, - rename_output: false, - ensemble_sync_delay: None, - generator_env: HashMap::default(), - check_retry_count: 0, - common: CommonConfig { - job_id: Default::default(), - task_id: Default::default(), - instance_id: Default::default(), - heartbeat_queue: Default::default(), - job_result_queue: Default::default(), - instance_telemetry_key: Default::default(), - microsoft_telemetry_key: Default::default(), - logs: Default::default(), - setup_dir: Default::default(), - extra_setup_dir: Default::default(), - extra_output: Default::default(), - min_available_memory_mb: Default::default(), - machine_identity: onefuzz::machine_id::MachineIdentity { - machine_id: uuid::Uuid::new_v4(), - machine_name: "test".to_string(), - scaleset_name: None, + #[tokio::test] + #[ignore] + async fn test_radamsa_linux() -> anyhow::Result<()> { + let crashes_temp = tempfile::tempdir()?; + let crashes: &std::path::Path = crashes_temp.path(); + + let inputs_temp = tempfile::tempdir()?; + let inputs: &std::path::Path = inputs_temp.path(); + let input_file = inputs.join("seed.txt"); + tokio::fs::write(input_file, "test").await?; + + let generator_options: Vec = vec![ + "-o", + "{generated_inputs}/input-%n-%s", + "-n", + "100", + "-r", + "{input_corpus}", + ] + .iter() + .map(|p| p.to_string()) + .collect(); + + let radamsa_path = env::var("ONEFUZZ_TEST_RADAMSA_LINUX")?; + let radamsa_as_path = std::path::Path::new(&radamsa_path); + let radamsa_dir = radamsa_as_path.parent().unwrap(); + + let readonly_inputs_local = tempfile::tempdir().unwrap().path().into(); + let crashes_local = tempfile::tempdir().unwrap().path().into(); + let tools_local = tempfile::tempdir().unwrap().path().into(); + let config = Config { + generator_exe: String::from("{tools_dir}/radamsa"), + generator_options, + readonly_inputs: vec![SyncedDir { + local_path: readonly_inputs_local, + remote_path: Some(BlobContainerUrl::parse( + Url::from_directory_path(inputs).unwrap(), + )?), + }], + crashes: SyncedDir { + local_path: crashes_local, + remote_path: Some(BlobContainerUrl::parse( + Url::from_directory_path(crashes).unwrap(), + )?), }, - tags: Default::default(), - from_agent_to_task_endpoint: "/".to_string(), - from_task_to_agent_endpoint: "/".to_string(), - }, - }; - let task = GeneratorTask::new(config); + tools: Some(SyncedDir { + local_path: tools_local, + remote_path: Some(BlobContainerUrl::parse( + Url::from_directory_path(radamsa_dir).unwrap(), + )?), + }), + target_exe: Default::default(), + target_env: Default::default(), + target_options: Default::default(), + target_timeout: None, + check_asan_log: false, + check_debugger: false, + rename_output: false, + ensemble_sync_delay: None, + generator_env: HashMap::default(), + check_retry_count: 0, + common: Default::default(), + }; + let task = GeneratorTask::new(config); - let generated_inputs = tempdir()?; - task.generate_inputs(inputs.to_path_buf(), generated_inputs.path()) - .await?; + let generated_inputs = tempdir()?; + task.generate_inputs(inputs.to_path_buf(), generated_inputs.path()) + .await?; - let count = std::fs::read_dir(generated_inputs.path())?.count(); - assert_eq!(count, 100, "No inputs generated"); - Ok(()) + let count = std::fs::read_dir(generated_inputs.path())?.count(); + assert_eq!(count, 100, "No inputs generated"); + Ok(()) + } } } diff --git a/src/agent/onefuzz-task/src/tasks/fuzz/supervisor.rs b/src/agent/onefuzz-task/src/tasks/fuzz/supervisor.rs index 3f00e20b8d..a7b7ee6087 100644 --- a/src/agent/onefuzz-task/src/tasks/fuzz/supervisor.rs +++ b/src/agent/onefuzz-task/src/tasks/fuzz/supervisor.rs @@ -61,6 +61,43 @@ pub struct SupervisorConfig { pub common: CommonConfig, } +impl SupervisorConfig { + pub fn get_expand(&self) -> Expand<'_> { + self.common + .get_expand() + .input_corpus(&self.inputs.local_path) + .supervisor_exe(&self.supervisor_exe) + .supervisor_options(&self.supervisor_options) + .set_optional_ref(&self.target_exe, Expand::target_exe) + .set_optional_ref(&self.supervisor_input_marker, |expand, input_marker| { + expand.input_marker(input_marker) + }) + .set_optional_ref(&self.target_options, |expand, target_options| { + expand.target_options(target_options) + }) + .set_optional_ref(&self.tools, |expand, tools| { + expand.tools_dir(&tools.local_path) + }) + .set_optional_ref(&self.coverage, |expand, coverage| { + expand.coverage_dir(&coverage.local_path) + }) + .set_optional_ref(&self.crashdumps, |expand, crashdumps| { + expand.crashdumps(&crashdumps.local_path) + }) + .set_optional_ref(&self.reports, |expand, reports| { + expand.reports_dir(&reports.local_path) + }) + .set_optional_ref( + &self.crashes.remote_path.clone().and_then(|u| u.account()), + |expand, account| expand.crashes_account(account), + ) + .set_optional_ref( + &self.crashes.remote_path.clone().and_then(|u| u.container()), + |expand, container| expand.crashes_container(container), + ) + } +} + const HEARTBEAT_PERIOD: Duration = Duration::from_secs(60); pub async fn spawn(config: SupervisorConfig) -> Result<(), Error> { @@ -252,57 +289,19 @@ async fn start_supervisor( None }; - let expand = Expand::new(&config.common.machine_identity) - .machine_id() - .supervisor_exe(&config.supervisor_exe) - .supervisor_options(&config.supervisor_options) + let expand = config + .get_expand() .runtime_dir(&runtime_dir) .crashes(&crashes.local_path) + .input_corpus(&inputs.local_path) // Why isn't this value in the config? It's not super clear to me from looking at the calling code. + .reports_dir(reports_dir) .set_optional_ref(&crashdumps, |expand, crashdumps| { + // And this one too... expand.crashdumps(&crashdumps.local_path) }) - .input_corpus(&inputs.local_path) - .reports_dir(reports_dir) - .setup_dir(&config.common.setup_dir) - .set_optional_ref(&config.common.extra_setup_dir, Expand::extra_setup_dir) - .set_optional_ref(&config.common.extra_output, |expand, value| { - expand.extra_output_dir(value.local_path.as_path()) - }) - .job_id(&config.common.job_id) - .task_id(&config.common.task_id) - .set_optional_ref(&config.tools, |expand, tools| { - expand.tools_dir(&tools.local_path) - }) - .set_optional_ref(&config.coverage, |expand, coverage| { - expand.coverage_dir(&coverage.local_path) - }) .set_optional_ref(&target_exe, |expand, target_exe| { expand.target_exe(target_exe) - }) - .set_optional_ref(&config.supervisor_input_marker, |expand, input_marker| { - expand.input_marker(input_marker) - }) - .set_optional_ref(&config.target_options, |expand, target_options| { - expand.target_options(target_options) - }) - .set_optional_ref(&config.common.microsoft_telemetry_key, |expand, key| { - expand.microsoft_telemetry_key(key) - }) - .set_optional_ref(&config.common.instance_telemetry_key, |expand, key| { - expand.instance_telemetry_key(key) - }) - .set_optional_ref( - &config.crashes.remote_path.clone().and_then(|u| u.account()), - |expand, account| expand.crashes_account(account), - ) - .set_optional_ref( - &config - .crashes - .remote_path - .clone() - .and_then(|u| u.container()), - |expand, container| expand.crashes_container(container), - ); + }); let supervisor_path = expand.evaluate_value(&config.supervisor_exe)?; let mut cmd = Command::new(supervisor_path); @@ -328,177 +327,250 @@ async fn start_supervisor( } #[cfg(test)] -#[cfg(target_os = "linux")] mod tests { - use super::*; - use crate::tasks::stats::afl::read_stats; - use onefuzz::blob::BlobContainerUrl; - use onefuzz::machine_id::MachineIdentity; - use onefuzz::process::monitor_process; - use onefuzz_telemetry::EventData; - use reqwest::Url; - use std::collections::HashMap; - use std::env; - use std::time::Instant; - - const MAX_FUZZ_TIME_SECONDS: u64 = 120; - - async fn has_stats(path: &PathBuf) -> bool { - if let Ok(stats) = read_stats(path).await { - for entry in stats { - if matches!(entry, EventData::ExecsSecond(x) if x > 0.0) { - return true; - } + use onefuzz::expand::PlaceHolder; + use proptest::prelude::*; + + use crate::config_test_utils::GetExpandFields; + + use super::SupervisorConfig; + + impl GetExpandFields for SupervisorConfig { + fn get_expand_fields(&self) -> Vec<(PlaceHolder, String)> { + let mut params = self.common.get_expand_fields(); + params.push(( + PlaceHolder::InputCorpus, + dunce::canonicalize(&self.inputs.local_path) + .unwrap() + .to_string_lossy() + .to_string(), + )); + params.push(( + PlaceHolder::SupervisorExe, + dunce::canonicalize(&self.supervisor_exe) + .unwrap() + .to_string_lossy() + .to_string(), + )); + params.push(( + PlaceHolder::SupervisorOptions, + self.supervisor_options.join(" "), + )); + if let Some(target_exe) = &self.target_exe { + params.push(( + PlaceHolder::TargetExe, + dunce::canonicalize(target_exe) + .unwrap() + .to_string_lossy() + .to_string(), + )); } - false - } else { - false + if let Some(input_marker) = &self.supervisor_input_marker { + params.push((PlaceHolder::Input, input_marker.clone())); + } + if let Some(target_options) = &self.target_options { + params.push((PlaceHolder::TargetOptions, target_options.join(" "))); + } + if let Some(tools) = &self.tools { + params.push(( + PlaceHolder::ToolsDir, + dunce::canonicalize(&tools.local_path) + .unwrap() + .to_string_lossy() + .to_string(), + )); + } + if let Some(coverage) = &self.coverage { + params.push(( + PlaceHolder::CoverageDir, + dunce::canonicalize(&coverage.local_path) + .unwrap() + .to_string_lossy() + .to_string(), + )); + } + if let Some(crashdumps) = &self.crashdumps { + params.push(( + PlaceHolder::Crashdumps, + dunce::canonicalize(&crashdumps.local_path) + .unwrap() + .to_string_lossy() + .to_string(), + )); + } + if let Some(reports) = &self.reports { + params.push(( + PlaceHolder::ReportsDir, + dunce::canonicalize(&reports.local_path) + .unwrap() + .to_string_lossy() + .to_string(), + )); + } + if let Some(account) = &self.crashes.remote_path.clone().and_then(|u| u.account()) { + params.push((PlaceHolder::CrashesAccount, account.clone())); + } + if let Some(container) = &self.crashes.remote_path.clone().and_then(|u| u.container()) { + params.push((PlaceHolder::CrashesContainer, container.clone())); + } + + params } } - #[tokio::test] - #[cfg_attr(not(feature = "integration_test"), ignore)] - async fn test_fuzzer_linux() { - let runtime_dir = tempfile::tempdir().unwrap(); + config_test!(SupervisorConfig); + + #[cfg(target_os = "linux")] + mod linux { + use super::super::*; + use crate::tasks::stats::afl::read_stats; + use onefuzz::blob::BlobContainerUrl; + use onefuzz::process::monitor_process; + use onefuzz_telemetry::EventData; + use reqwest::Url; + use std::collections::HashMap; + use std::env; + use std::time::Instant; + + const MAX_FUZZ_TIME_SECONDS: u64 = 120; + + async fn has_stats(path: &PathBuf) -> bool { + if let Ok(stats) = read_stats(path).await { + for entry in stats { + if matches!(entry, EventData::ExecsSecond(x) if x > 0.0) { + return true; + } + } + false + } else { + false + } + } - let supervisor_exe = if let Ok(x) = env::var("ONEFUZZ_TEST_AFL_LINUX_FUZZER") { - x - } else { - warn!("Unable to test AFL integration"); - return; - }; + #[tokio::test] + #[cfg_attr(not(feature = "integration_test"), ignore)] + async fn test_fuzzer_linux() { + let runtime_dir = tempfile::tempdir().unwrap(); - let target_exe = if let Ok(x) = env::var("ONEFUZZ_TEST_AFL_LINUX_TEST_BINARY") { - Some(x.into()) - } else { - warn!("Unable to test AFL integration"); - return; - }; + let supervisor_exe = if let Ok(x) = env::var("ONEFUZZ_TEST_AFL_LINUX_FUZZER") { + x + } else { + warn!("Unable to test AFL integration"); + return; + }; - let reports_dir_temp = tempfile::tempdir().unwrap(); - let reports_dir = reports_dir_temp.path().into(); + let target_exe = if let Ok(x) = env::var("ONEFUZZ_TEST_AFL_LINUX_TEST_BINARY") { + Some(x.into()) + } else { + warn!("Unable to test AFL integration"); + return; + }; - let fault_dir_temp = tempfile::tempdir().unwrap(); - let crashes_local = tempfile::tempdir().unwrap().path().into(); - let crashes = SyncedDir { - local_path: crashes_local, - remote_path: Some( - BlobContainerUrl::parse(Url::from_directory_path(fault_dir_temp).unwrap()).unwrap(), - ), - }; + let reports_dir_temp = tempfile::tempdir().unwrap(); + let reports_dir = reports_dir_temp.path().into(); + + let fault_dir_temp = tempfile::tempdir().unwrap(); + let crashes_local = tempfile::tempdir().unwrap().path().into(); + let crashes = SyncedDir { + local_path: crashes_local, + remote_path: Some( + BlobContainerUrl::parse(Url::from_directory_path(fault_dir_temp).unwrap()) + .unwrap(), + ), + }; - let crashdumps_dir_temp = tempfile::tempdir().unwrap(); - let crashdumps_local = tempfile::tempdir().unwrap().path().into(); - let crashdumps = SyncedDir { - local_path: crashdumps_local, - remote_path: Some( - BlobContainerUrl::parse(Url::from_directory_path(crashdumps_dir_temp).unwrap()) - .unwrap(), - ), - }; + let crashdumps_dir_temp = tempfile::tempdir().unwrap(); + let crashdumps_local = tempfile::tempdir().unwrap().path().into(); + let crashdumps = SyncedDir { + local_path: crashdumps_local, + remote_path: Some( + BlobContainerUrl::parse(Url::from_directory_path(crashdumps_dir_temp).unwrap()) + .unwrap(), + ), + }; - let corpus_dir_local = tempfile::tempdir().unwrap().path().into(); - let corpus_dir_temp = tempfile::tempdir().unwrap(); - let corpus_dir = SyncedDir { - local_path: corpus_dir_local, - remote_path: Some( - BlobContainerUrl::parse(Url::from_directory_path(corpus_dir_temp).unwrap()) - .unwrap(), - ), - }; - let seed_file_name = corpus_dir.local_path.join("seed.txt"); - tokio::fs::write(seed_file_name, "xyz").await.unwrap(); - - let target_options = Some(vec!["{input}".to_owned()]); - let supervisor_env = HashMap::new(); - let supervisor_options: Vec<_> = vec![ - "-d", - "-i", - "{input_corpus}", - "-o", - "{crashes}", - "--", - "{target_exe}", - "{target_options}", - ] - .iter() - .map(|p| p.to_string()) - .collect(); - - // AFL input marker - let supervisor_input_marker = Some("@@".to_owned()); - - let config = SupervisorConfig { - supervisor_exe, - supervisor_env, - supervisor_options, - supervisor_input_marker, - target_exe, - target_options, - inputs: corpus_dir.clone(), - crashes: crashes.clone(), - crashdumps: Some(crashdumps.clone()), - tools: None, - wait_for_files: None, - stats_file: None, - stats_format: None, - ensemble_sync_delay: None, - reports: None, - unique_reports: None, - no_repro: None, - coverage: None, - common: CommonConfig { - job_id: Default::default(), - task_id: Default::default(), - instance_id: Default::default(), - heartbeat_queue: Default::default(), - job_result_queue: Default::default(), - instance_telemetry_key: Default::default(), - microsoft_telemetry_key: Default::default(), - logs: Default::default(), - setup_dir: Default::default(), - extra_setup_dir: Default::default(), - extra_output: Default::default(), - min_available_memory_mb: Default::default(), - machine_identity: MachineIdentity { - machine_id: uuid::Uuid::new_v4(), - machine_name: "test".to_string(), - scaleset_name: None, - }, - tags: Default::default(), - from_agent_to_task_endpoint: "/".to_string(), - from_task_to_agent_endpoint: "/".to_string(), - }, - }; + let corpus_dir_local = tempfile::tempdir().unwrap().path().into(); + let corpus_dir_temp = tempfile::tempdir().unwrap(); + let corpus_dir = SyncedDir { + local_path: corpus_dir_local, + remote_path: Some( + BlobContainerUrl::parse(Url::from_directory_path(corpus_dir_temp).unwrap()) + .unwrap(), + ), + }; + let seed_file_name = corpus_dir.local_path.join("seed.txt"); + tokio::fs::write(seed_file_name, "xyz").await.unwrap(); + + let target_options = Some(vec!["{input}".to_owned()]); + let supervisor_env = HashMap::new(); + let supervisor_options: Vec<_> = vec![ + "-d", + "-i", + "{input_corpus}", + "-o", + "{crashes}", + "--", + "{target_exe}", + "{target_options}", + ] + .iter() + .map(|p| p.to_string()) + .collect(); + + // AFL input marker + let supervisor_input_marker = Some("@@".to_owned()); + + let config = SupervisorConfig { + supervisor_exe, + supervisor_env, + supervisor_options, + supervisor_input_marker, + target_exe, + target_options, + inputs: corpus_dir.clone(), + crashes: crashes.clone(), + crashdumps: Some(crashdumps.clone()), + tools: None, + wait_for_files: None, + stats_file: None, + stats_format: None, + ensemble_sync_delay: None, + reports: None, + unique_reports: None, + no_repro: None, + coverage: None, + common: Default::default(), + }; - let process = start_supervisor( - runtime_dir, - &config, - &crashes, - Some(&crashdumps), - &corpus_dir, - reports_dir, - ) - .await - .unwrap(); - - let notify = Notify::new(); - let _fuzzing_monitor = - monitor_process(process, "supervisor".to_string(), false, Some(¬ify)); - let stat_output = crashes.local_path.join("fuzzer_stats"); - let start = Instant::now(); - loop { - if has_stats(&stat_output).await { - break; - } + let process = start_supervisor( + runtime_dir, + &config, + &crashes, + Some(&crashdumps), + &corpus_dir, + reports_dir, + ) + .await + .unwrap(); + + let notify = Notify::new(); + let _fuzzing_monitor = + monitor_process(process, "supervisor".to_string(), false, Some(¬ify)); + let stat_output = crashes.local_path.join("fuzzer_stats"); + let start = Instant::now(); + loop { + if has_stats(&stat_output).await { + break; + } - if start.elapsed().as_secs() > MAX_FUZZ_TIME_SECONDS { - panic!( - "afl did not generate stats in {} seconds", - MAX_FUZZ_TIME_SECONDS - ); + if start.elapsed().as_secs() > MAX_FUZZ_TIME_SECONDS { + panic!( + "afl did not generate stats in {} seconds", + MAX_FUZZ_TIME_SECONDS + ); + } + tokio::time::sleep(std::time::Duration::from_secs(1)).await; } - tokio::time::sleep(std::time::Duration::from_secs(1)).await; } } } diff --git a/src/agent/onefuzz-task/src/tasks/merge/generic.rs b/src/agent/onefuzz-task/src/tasks/merge/generic.rs index 3b6a2094d8..aea191d136 100644 --- a/src/agent/onefuzz-task/src/tasks/merge/generic.rs +++ b/src/agent/onefuzz-task/src/tasks/merge/generic.rs @@ -40,6 +40,20 @@ pub struct Config { pub common: CommonConfig, } +impl Config { + pub fn get_expand(&self) -> Expand<'_> { + self.common + .get_expand() + .input_marker(&self.supervisor_input_marker) + .input_corpus(&self.unique_inputs.local_path) + .target_exe(&self.target_exe) + .target_options(&self.target_options) + .supervisor_exe(&self.supervisor_exe) + .supervisor_options(&self.supervisor_options) + .tools_dir(self.tools.local_path.to_string_lossy().into_owned()) + } +} + pub async fn spawn(config: &Config) -> Result<()> { config.tools.init_pull().await?; set_executable(&config.tools.local_path).await?; @@ -129,29 +143,10 @@ async fn merge(config: &Config, output_dir: impl AsRef) -> Result<()> { let target_exe = try_resolve_setup_relative_path(&config.common.setup_dir, &config.target_exe).await?; - let expand = Expand::new(&config.common.machine_identity) - .machine_id() - .input_marker(&config.supervisor_input_marker) - .input_corpus(&config.unique_inputs.local_path) - .target_options(&config.target_options) - .supervisor_exe(&config.supervisor_exe) - .supervisor_options(&config.supervisor_options) + let expand = config + .get_expand() .generated_inputs(output_dir) - .target_exe(&target_exe) - .setup_dir(&config.common.setup_dir) - .set_optional_ref(&config.common.extra_setup_dir, Expand::extra_setup_dir) - .set_optional_ref(&config.common.extra_output, |expand, value| { - expand.extra_output_dir(value.local_path.as_path()) - }) - .tools_dir(&config.tools.local_path) - .job_id(&config.common.job_id) - .task_id(&config.common.task_id) - .set_optional_ref(&config.common.microsoft_telemetry_key, |tester, key| { - tester.microsoft_telemetry_key(key) - }) - .set_optional_ref(&config.common.instance_telemetry_key, |tester, key| { - tester.instance_telemetry_key(key) - }); + .target_exe(&target_exe); let supervisor_path = expand.evaluate_value(&config.supervisor_exe)?; @@ -181,3 +176,57 @@ async fn merge(config: &Config, output_dir: impl AsRef) -> Result<()> { cmd.spawn()?.wait_with_output().await?; Ok(()) } + +#[cfg(test)] +mod tests { + use onefuzz::expand::PlaceHolder; + use proptest::prelude::*; + + use crate::config_test_utils::GetExpandFields; + + use super::Config; + + impl GetExpandFields for Config { + fn get_expand_fields(&self) -> Vec<(PlaceHolder, String)> { + let mut params = self.common.get_expand_fields(); + params.push((PlaceHolder::Input, self.supervisor_input_marker.clone())); + params.push(( + PlaceHolder::InputCorpus, + dunce::canonicalize(&self.unique_inputs.local_path) + .unwrap() + .to_string_lossy() + .to_string(), + )); + params.push(( + PlaceHolder::TargetExe, + dunce::canonicalize(&self.target_exe) + .unwrap() + .to_string_lossy() + .to_string(), + )); + params.push((PlaceHolder::TargetOptions, self.target_options.join(" "))); + params.push(( + PlaceHolder::SupervisorExe, + dunce::canonicalize(&self.supervisor_exe) + .unwrap() + .to_string_lossy() + .to_string(), + )); + params.push(( + PlaceHolder::SupervisorOptions, + self.supervisor_options.join(" "), + )); + params.push(( + PlaceHolder::ToolsDir, + dunce::canonicalize(&self.tools.local_path) + .unwrap() + .to_string_lossy() + .to_string(), + )); + + params + } + } + + config_test!(Config); +} diff --git a/src/agent/onefuzz-task/src/tasks/report/dotnet/generic.rs b/src/agent/onefuzz-task/src/tasks/report/dotnet/generic.rs index b8659845de..036b20d028 100644 --- a/src/agent/onefuzz-task/src/tasks/report/dotnet/generic.rs +++ b/src/agent/onefuzz-task/src/tasks/report/dotnet/generic.rs @@ -59,6 +59,32 @@ pub struct Config { pub common: CommonConfig, } +impl Config { + pub fn get_expand(&self) -> Expand<'_> { + let tools_dir = self.tools.local_path.to_string_lossy().into_owned(); + + self.common + .get_expand() + .target_exe(&self.target_exe) + .target_options(&self.target_options) + .tools_dir(tools_dir) + .set_optional_ref(&self.reports, |expand, reports| { + expand.reports_dir(reports.local_path.as_path()) + }) + .set_optional_ref(&self.crashes, |expand, crashes| { + expand + .set_optional_ref( + &crashes.remote_path.clone().and_then(|u| u.account()), + |expand, account| expand.crashes_account(account), + ) + .set_optional_ref( + &crashes.remote_path.clone().and_then(|u| u.container()), + |expand, container| expand.crashes_container(container), + ) + }) + } +} + pub struct DotnetCrashReportTask { config: Arc, pub poller: InputPoller, @@ -130,12 +156,10 @@ impl AsanProcessor { } async fn target_exe(&self) -> Result { - let tools_dir = self.config.tools.local_path.to_string_lossy().into_owned(); - // Try to expand `target_exe` with support for `{tools_dir}`. // // Allows using `LibFuzzerDotnetLoader.exe` from a shared tools container. - let expand = Expand::new(&self.config.common.machine_identity).tools_dir(tools_dir); + let expand = self.config.get_expand(); let expanded = expand.evaluate_value(self.config.target_exe.to_string_lossy())?; let expanded_path = Path::new(&expanded); @@ -183,13 +207,7 @@ impl AsanProcessor { let mut args = vec![target_exe]; args.extend(self.config.target_options.clone()); - let expand = Expand::new(&self.config.common.machine_identity) - .input_path(input) - .setup_dir(&self.config.common.setup_dir) - .set_optional_ref(&self.config.common.extra_setup_dir, Expand::extra_setup_dir) - .set_optional_ref(&self.config.common.extra_output, |expand, value| { - expand.extra_output_dir(value.local_path.as_path()) - }); + let expand = self.config.get_expand(); let expanded_args = expand.evaluate(&args)?; @@ -278,3 +296,55 @@ impl Processor for AsanProcessor { Ok(()) } } + +#[cfg(test)] +mod tests { + use onefuzz::expand::PlaceHolder; + use proptest::prelude::*; + + use crate::config_test_utils::GetExpandFields; + + use super::Config; + + impl GetExpandFields for Config { + fn get_expand_fields(&self) -> Vec<(PlaceHolder, String)> { + let mut params = self.common.get_expand_fields(); + params.push(( + PlaceHolder::TargetExe, + dunce::canonicalize(&self.target_exe) + .unwrap() + .to_string_lossy() + .to_string(), + )); + params.push((PlaceHolder::TargetOptions, self.target_options.join(" "))); + params.push(( + PlaceHolder::ToolsDir, + dunce::canonicalize(&self.tools.local_path) + .unwrap() + .to_string_lossy() + .to_string(), + )); + if let Some(reports) = &self.reports { + params.push(( + PlaceHolder::ReportsDir, + dunce::canonicalize(&reports.local_path) + .unwrap() + .to_string_lossy() + .to_string(), + )); + } + if let Some(crashes) = &self.crashes { + if let Some(account) = crashes.remote_path.clone().and_then(|u| u.account()) { + params.push((PlaceHolder::CrashesAccount, account)); + } + if let Some(container) = crashes.remote_path.clone().and_then(|u| u.container()) { + params.push((PlaceHolder::CrashesContainer, container)); + } + } + + params + } + } + + config_test!(Config); +}