mirror of
https://github.com/microsoft/onefuzz.git
synced 2025-06-16 11:58:09 +00:00
Add dotnet coverage task (#2062)
* checkpoint * some more progress * more progress * More progress * Now it's time to test it * It works locally 🎉 * Attempting clean build * fmt * temporarily stub out macos * missed a few * please be the last one * . * . * . * noop change to unstuck actions * . * . * Fix setup script * Some fixes * It works except for a race condition -- use a directory watcher to fix it * It works end to end! * Execute the commands using tokio's structs and timeout mechanism * It works.... for real this time * Undo timer changes * Cleanup * 🧹 * Fix import * . * PR comments * Fix clippy * Clippy whyyy * Only check dotnet path once * fmt * Fix a couple more comments
This commit is contained in:
@ -33,6 +33,7 @@ The current task types available are:
|
||||
* generic_crash_report: use a built-in debugging tool (debugapi or ptrace based)
|
||||
to rerun the crashing input, attempting to generate an informational report
|
||||
for each discovered crash
|
||||
* dotnet_coverage: same as `coverage` but for dotnet
|
||||
|
||||
Each type of task has a unique set of configuration options available, these
|
||||
include:
|
||||
|
@ -565,7 +565,8 @@ If webhook is set to have Event Grid message format then the payload will look a
|
||||
"generic_merge",
|
||||
"generic_generator",
|
||||
"generic_crash_report",
|
||||
"generic_regression"
|
||||
"generic_regression",
|
||||
"dotnet_coverage"
|
||||
],
|
||||
"title": "TaskType"
|
||||
},
|
||||
@ -1204,7 +1205,8 @@ If webhook is set to have Event Grid message format then the payload will look a
|
||||
"generic_merge",
|
||||
"generic_generator",
|
||||
"generic_crash_report",
|
||||
"generic_regression"
|
||||
"generic_regression",
|
||||
"dotnet_coverage"
|
||||
],
|
||||
"title": "TaskType"
|
||||
},
|
||||
@ -2444,7 +2446,8 @@ If webhook is set to have Event Grid message format then the payload will look a
|
||||
"generic_merge",
|
||||
"generic_generator",
|
||||
"generic_crash_report",
|
||||
"generic_regression"
|
||||
"generic_regression",
|
||||
"dotnet_coverage"
|
||||
],
|
||||
"title": "TaskType"
|
||||
},
|
||||
@ -3159,7 +3162,8 @@ If webhook is set to have Event Grid message format then the payload will look a
|
||||
"generic_merge",
|
||||
"generic_generator",
|
||||
"generic_crash_report",
|
||||
"generic_regression"
|
||||
"generic_regression",
|
||||
"dotnet_coverage"
|
||||
],
|
||||
"title": "TaskType"
|
||||
},
|
||||
@ -3665,7 +3669,8 @@ If webhook is set to have Event Grid message format then the payload will look a
|
||||
"generic_merge",
|
||||
"generic_generator",
|
||||
"generic_crash_report",
|
||||
"generic_regression"
|
||||
"generic_regression",
|
||||
"dotnet_coverage"
|
||||
],
|
||||
"title": "TaskType"
|
||||
},
|
||||
@ -4114,7 +4119,8 @@ If webhook is set to have Event Grid message format then the payload will look a
|
||||
"generic_merge",
|
||||
"generic_generator",
|
||||
"generic_crash_report",
|
||||
"generic_regression"
|
||||
"generic_regression",
|
||||
"dotnet_coverage"
|
||||
],
|
||||
"title": "TaskType"
|
||||
},
|
||||
@ -4551,7 +4557,8 @@ If webhook is set to have Event Grid message format then the payload will look a
|
||||
"generic_merge",
|
||||
"generic_generator",
|
||||
"generic_crash_report",
|
||||
"generic_regression"
|
||||
"generic_regression",
|
||||
"dotnet_coverage"
|
||||
],
|
||||
"title": "TaskType"
|
||||
},
|
||||
@ -4987,7 +4994,8 @@ If webhook is set to have Event Grid message format then the payload will look a
|
||||
"generic_merge",
|
||||
"generic_generator",
|
||||
"generic_crash_report",
|
||||
"generic_regression"
|
||||
"generic_regression",
|
||||
"dotnet_coverage"
|
||||
],
|
||||
"title": "TaskType"
|
||||
},
|
||||
@ -6725,7 +6733,8 @@ If webhook is set to have Event Grid message format then the payload will look a
|
||||
"generic_merge",
|
||||
"generic_generator",
|
||||
"generic_crash_report",
|
||||
"generic_regression"
|
||||
"generic_regression",
|
||||
"dotnet_coverage"
|
||||
],
|
||||
"title": "TaskType"
|
||||
},
|
||||
|
@ -70,7 +70,8 @@ public enum TaskType {
|
||||
GenericMerge,
|
||||
GenericGenerator,
|
||||
GenericCrashReport,
|
||||
GenericRegression
|
||||
GenericRegression,
|
||||
DotnetCoverage,
|
||||
}
|
||||
|
||||
public enum Os {
|
||||
|
@ -555,5 +555,42 @@ public static class Defs {
|
||||
ContainerPermission.Read | ContainerPermission.List
|
||||
),
|
||||
})
|
||||
} };
|
||||
},
|
||||
{ TaskType.DotnetCoverage ,
|
||||
new TaskDefinition(
|
||||
Features: new[] {
|
||||
TaskFeature.TargetExe,
|
||||
TaskFeature.TargetEnv,
|
||||
TaskFeature.TargetOptions,
|
||||
TaskFeature.TargetTimeout,
|
||||
TaskFeature.CoverageFilter,
|
||||
TaskFeature.TargetMustUseInput,
|
||||
},
|
||||
Vm: new VmDefinition(Compare: Compare.Equal, Value:1),
|
||||
Containers: new [] {
|
||||
new ContainerDefinition(
|
||||
Type:ContainerType.Setup,
|
||||
Compare: Compare.Equal,
|
||||
Value:1,
|
||||
Permissions: ContainerPermission.Read | ContainerPermission.List
|
||||
),
|
||||
new ContainerDefinition(
|
||||
Type:ContainerType.ReadonlyInputs,
|
||||
Compare: Compare.AtLeast,
|
||||
Value:1,
|
||||
Permissions: ContainerPermission.Read | ContainerPermission.List
|
||||
),
|
||||
new ContainerDefinition(
|
||||
Type:ContainerType.Coverage,
|
||||
Compare: Compare.Equal,
|
||||
Value:1,
|
||||
Permissions:
|
||||
ContainerPermission.List |
|
||||
ContainerPermission.Read |
|
||||
ContainerPermission.Write
|
||||
|
||||
)},
|
||||
MonitorQueue: ContainerType.ReadonlyInputs)
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
use crate::local::coverage;
|
||||
use crate::local::{
|
||||
common::add_common_config, generic_analysis, generic_crash_report, generic_generator,
|
||||
libfuzzer, libfuzzer_crash_report, libfuzzer_fuzz, libfuzzer_merge, libfuzzer_regression,
|
||||
libfuzzer_test_input, radamsa, test_input, tui::TerminalUi,
|
||||
};
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
use crate::local::{coverage, dotnet_coverage};
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{App, Arg, SubCommand};
|
||||
use crossterm::tty::IsTty;
|
||||
@ -23,6 +23,8 @@ enum Commands {
|
||||
Radamsa,
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
Coverage,
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
DotnetCoverage,
|
||||
LibfuzzerFuzz,
|
||||
LibfuzzerMerge,
|
||||
LibfuzzerCrashReport,
|
||||
@ -59,6 +61,8 @@ pub async fn run(args: clap::ArgMatches<'static>) -> Result<()> {
|
||||
match command {
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
Commands::Coverage => coverage::run(&sub_args, event_sender).await,
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
Commands::DotnetCoverage => dotnet_coverage::run(&sub_args, event_sender).await,
|
||||
Commands::Radamsa => radamsa::run(&sub_args, event_sender).await,
|
||||
Commands::LibfuzzerCrashReport => {
|
||||
libfuzzer_crash_report::run(&sub_args, event_sender).await
|
||||
@ -117,6 +121,8 @@ pub fn args(name: &str) -> App<'static, 'static> {
|
||||
let app = match subcommand {
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
Commands::Coverage => coverage::args(subcommand.into()),
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
Commands::DotnetCoverage => dotnet_coverage::args(subcommand.into()),
|
||||
Commands::Radamsa => radamsa::args(subcommand.into()),
|
||||
Commands::LibfuzzerCrashReport => libfuzzer_crash_report::args(subcommand.into()),
|
||||
Commands::LibfuzzerFuzz => libfuzzer_fuzz::args(subcommand.into()),
|
||||
|
@ -29,6 +29,8 @@ pub const CHECK_RETRY_COUNT: &str = "check_retry_count";
|
||||
pub const DISABLE_CHECK_QUEUE: &str = "disable_check_queue";
|
||||
pub const UNIQUE_REPORTS_DIR: &str = "unique_reports_dir";
|
||||
pub const COVERAGE_DIR: &str = "coverage_dir";
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
pub const DOTNET_COVERAGE_DIR: &str = "dotnet_coverage_dir";
|
||||
pub const READONLY_INPUTS: &str = "readonly_inputs_dir";
|
||||
pub const CHECK_ASAN_LOG: &str = "check_asan_log";
|
||||
pub const TOOLS_DIR: &str = "tools_dir";
|
||||
|
124
src/agent/onefuzz-task/src/local/dotnet_coverage.rs
Normal file
124
src/agent/onefuzz-task/src/local/dotnet_coverage.rs
Normal file
@ -0,0 +1,124 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use crate::{
|
||||
local::common::{
|
||||
build_local_context, get_cmd_arg, get_cmd_env, get_cmd_exe, get_synced_dir,
|
||||
get_synced_dirs, CmdType, SyncCountDirMonitor, UiEvent, COVERAGE_DIR, INPUTS_DIR,
|
||||
READONLY_INPUTS, TARGET_ENV, TARGET_EXE, TARGET_OPTIONS, TARGET_TIMEOUT,
|
||||
},
|
||||
tasks::{
|
||||
config::CommonConfig,
|
||||
coverage::dotnet::{Config, DotnetCoverageTask},
|
||||
},
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{App, Arg, SubCommand};
|
||||
use flume::Sender;
|
||||
use storage_queue::QueueClient;
|
||||
|
||||
pub fn build_shared_args(local_job: bool) -> Vec<Arg<'static, 'static>> {
|
||||
let mut args = vec![
|
||||
Arg::with_name(TARGET_EXE)
|
||||
.long(TARGET_EXE)
|
||||
.takes_value(true)
|
||||
.required(true),
|
||||
Arg::with_name(TARGET_ENV)
|
||||
.long(TARGET_ENV)
|
||||
.takes_value(true)
|
||||
.multiple(true),
|
||||
Arg::with_name(TARGET_OPTIONS)
|
||||
.long(TARGET_OPTIONS)
|
||||
.default_value("{input}")
|
||||
.takes_value(true)
|
||||
.value_delimiter(" ")
|
||||
.help("Use a quoted string with space separation to denote multiple arguments"),
|
||||
Arg::with_name(TARGET_TIMEOUT)
|
||||
.takes_value(true)
|
||||
.long(TARGET_TIMEOUT),
|
||||
Arg::with_name(COVERAGE_DIR)
|
||||
.takes_value(true)
|
||||
.required(!local_job)
|
||||
.long(COVERAGE_DIR),
|
||||
];
|
||||
if local_job {
|
||||
args.push(
|
||||
Arg::with_name(INPUTS_DIR)
|
||||
.long(INPUTS_DIR)
|
||||
.takes_value(true)
|
||||
.required(true),
|
||||
)
|
||||
} else {
|
||||
args.push(
|
||||
Arg::with_name(READONLY_INPUTS)
|
||||
.takes_value(true)
|
||||
.required(true)
|
||||
.long(READONLY_INPUTS)
|
||||
.multiple(true),
|
||||
)
|
||||
}
|
||||
args
|
||||
}
|
||||
|
||||
pub fn build_coverage_config(
|
||||
args: &clap::ArgMatches<'_>,
|
||||
local_job: bool,
|
||||
input_queue: Option<QueueClient>,
|
||||
common: CommonConfig,
|
||||
event_sender: Option<Sender<UiEvent>>,
|
||||
) -> Result<Config> {
|
||||
let target_exe = get_cmd_exe(CmdType::Target, args)?.into();
|
||||
let target_env = get_cmd_env(CmdType::Target, args)?;
|
||||
let target_options = get_cmd_arg(CmdType::Target, args);
|
||||
let target_timeout = value_t!(args, TARGET_TIMEOUT, u64).ok();
|
||||
|
||||
let readonly_inputs = if local_job {
|
||||
info!("Took inputs_dir");
|
||||
vec![
|
||||
get_synced_dir(INPUTS_DIR, common.job_id, common.task_id, args)?
|
||||
.monitor_count(&event_sender)?,
|
||||
]
|
||||
} else {
|
||||
get_synced_dirs(READONLY_INPUTS, common.job_id, common.task_id, args)?
|
||||
.into_iter()
|
||||
.map(|sd| sd.monitor_count(&event_sender))
|
||||
.collect::<Result<Vec<_>>>()?
|
||||
};
|
||||
|
||||
let coverage = get_synced_dir(COVERAGE_DIR, common.job_id, common.task_id, args)?
|
||||
.monitor_count(&event_sender)?;
|
||||
|
||||
let config = Config {
|
||||
target_exe,
|
||||
target_env,
|
||||
target_options,
|
||||
target_timeout,
|
||||
input_queue,
|
||||
readonly_inputs,
|
||||
coverage,
|
||||
common,
|
||||
};
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub async fn run(args: &clap::ArgMatches<'_>, event_sender: Option<Sender<UiEvent>>) -> Result<()> {
|
||||
let context = build_local_context(args, true, event_sender.clone())?;
|
||||
let config = build_coverage_config(
|
||||
args,
|
||||
false,
|
||||
None,
|
||||
context.common_config.clone(),
|
||||
event_sender,
|
||||
)?;
|
||||
|
||||
let mut task = DotnetCoverageTask::new(config);
|
||||
task.run().await
|
||||
}
|
||||
|
||||
pub fn args(name: &'static str) -> App<'static, 'static> {
|
||||
SubCommand::with_name(name)
|
||||
.about("execute a local-only coverage task")
|
||||
.args(&build_shared_args(false))
|
||||
}
|
@ -1,14 +1,6 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
use crate::{
|
||||
local::{
|
||||
common::COVERAGE_DIR,
|
||||
coverage::{build_coverage_config, build_shared_args as build_coverage_args},
|
||||
},
|
||||
tasks::coverage::generic::CoverageTask,
|
||||
};
|
||||
use crate::{
|
||||
local::{
|
||||
common::{
|
||||
@ -28,6 +20,17 @@ use crate::{
|
||||
report::libfuzzer_report::ReportTask,
|
||||
},
|
||||
};
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
use crate::{
|
||||
local::{
|
||||
common::{COVERAGE_DIR, DOTNET_COVERAGE_DIR},
|
||||
coverage,
|
||||
coverage::build_shared_args as build_coverage_args,
|
||||
dotnet_coverage,
|
||||
},
|
||||
tasks::coverage::dotnet::DotnetCoverageTask,
|
||||
tasks::coverage::generic::CoverageTask,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use clap::{App, SubCommand};
|
||||
use flume::Sender;
|
||||
@ -75,11 +78,33 @@ pub async fn run(args: &clap::ArgMatches<'_>, event_sender: Option<Sender<UiEven
|
||||
task_handles.push(crash_report_input_monitor.handle);
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
if args.is_present(DOTNET_COVERAGE_DIR) {
|
||||
let coverage_input_monitor =
|
||||
DirectoryMonitorQueue::start_monitoring(crash_dir.clone()).await?;
|
||||
let dotnet_coverage_config = dotnet_coverage::build_coverage_config(
|
||||
args,
|
||||
true,
|
||||
Some(coverage_input_monitor.queue_client),
|
||||
CommonConfig {
|
||||
task_id: Uuid::new_v4(),
|
||||
..context.common_config.clone()
|
||||
},
|
||||
event_sender.clone(),
|
||||
)?;
|
||||
|
||||
let mut dotnet_coverage = DotnetCoverageTask::new(dotnet_coverage_config);
|
||||
let dotnet_coverage_task = spawn(async move { dotnet_coverage.run().await });
|
||||
|
||||
task_handles.push(dotnet_coverage_task);
|
||||
task_handles.push(coverage_input_monitor.handle);
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
if args.is_present(COVERAGE_DIR) {
|
||||
let coverage_input_monitor =
|
||||
DirectoryMonitorQueue::start_monitoring(crash_dir.clone()).await?;
|
||||
let coverage_config = build_coverage_config(
|
||||
let coverage_config = coverage::build_coverage_config(
|
||||
args,
|
||||
true,
|
||||
Some(coverage_input_monitor.queue_client),
|
||||
|
@ -5,6 +5,8 @@ pub mod cmd;
|
||||
pub mod common;
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
pub mod coverage;
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
pub mod dotnet_coverage;
|
||||
pub mod generic_analysis;
|
||||
pub mod generic_crash_report;
|
||||
pub mod generic_generator;
|
||||
|
@ -83,6 +83,10 @@ pub enum Config {
|
||||
#[serde(alias = "coverage")]
|
||||
Coverage(coverage::generic::Config),
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
#[serde(alias = "dotnet_coverage")]
|
||||
DotnetCoverage(coverage::dotnet::Config),
|
||||
|
||||
#[serde(alias = "libfuzzer_fuzz")]
|
||||
LibFuzzerFuzz(fuzz::libfuzzer_fuzz::Config),
|
||||
|
||||
@ -130,6 +134,8 @@ impl Config {
|
||||
match self {
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
Config::Coverage(c) => &mut c.common,
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
Config::DotnetCoverage(c) => &mut c.common,
|
||||
Config::LibFuzzerFuzz(c) => &mut c.common,
|
||||
Config::LibFuzzerMerge(c) => &mut c.common,
|
||||
Config::LibFuzzerReport(c) => &mut c.common,
|
||||
@ -147,6 +153,8 @@ impl Config {
|
||||
match self {
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
Config::Coverage(c) => &c.common,
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
Config::DotnetCoverage(c) => &c.common,
|
||||
Config::LibFuzzerFuzz(c) => &c.common,
|
||||
Config::LibFuzzerMerge(c) => &c.common,
|
||||
Config::LibFuzzerReport(c) => &c.common,
|
||||
@ -164,6 +172,8 @@ impl Config {
|
||||
let event_type = match self {
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
Config::Coverage(_) => "coverage",
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
Config::DotnetCoverage(_) => "dotnet_coverage",
|
||||
Config::LibFuzzerFuzz(_) => "libfuzzer_fuzz",
|
||||
Config::LibFuzzerMerge(_) => "libfuzzer_merge",
|
||||
Config::LibFuzzerReport(_) => "libfuzzer_crash_report",
|
||||
@ -208,6 +218,12 @@ impl Config {
|
||||
match self {
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
Config::Coverage(config) => coverage::generic::CoverageTask::new(config).run().await,
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
Config::DotnetCoverage(config) => {
|
||||
coverage::dotnet::DotnetCoverageTask::new(config)
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
Config::LibFuzzerFuzz(config) => {
|
||||
fuzz::libfuzzer_fuzz::LibFuzzerFuzzTask::new(config)?
|
||||
.run()
|
||||
|
421
src/agent/onefuzz-task/src/tasks/coverage/dotnet.rs
Normal file
421
src/agent/onefuzz-task/src/tasks/coverage/dotnet.rs
Normal file
@ -0,0 +1,421 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use async_trait::async_trait;
|
||||
use onefuzz::{
|
||||
expand::{Expand, PlaceHolder},
|
||||
monitor::DirectoryMonitor,
|
||||
syncdir::SyncedDir,
|
||||
};
|
||||
use reqwest::Url;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
process::Stdio,
|
||||
time::Duration,
|
||||
};
|
||||
use std::{env, process::ExitStatus};
|
||||
use storage_queue::{Message, QueueClient};
|
||||
use tokio::{fs, process::Command, time::timeout};
|
||||
use tokio_stream::wrappers::ReadDirStream;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::tasks::{
|
||||
config::CommonConfig,
|
||||
coverage::COBERTURA_COVERAGE_FILE,
|
||||
generic::input_poller::{CallbackImpl, InputPoller, Processor},
|
||||
heartbeat::{HeartbeatSender, TaskHeartbeatClient},
|
||||
};
|
||||
|
||||
const MAX_COVERAGE_RECORDING_ATTEMPTS: usize = 2;
|
||||
const DEFAULT_TARGET_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Config {
|
||||
pub target_exe: PathBuf,
|
||||
pub target_env: HashMap<String, String>,
|
||||
pub target_options: Vec<String>,
|
||||
pub target_timeout: Option<u64>,
|
||||
|
||||
pub input_queue: Option<QueueClient>,
|
||||
pub readonly_inputs: Vec<SyncedDir>,
|
||||
pub coverage: SyncedDir,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub common: CommonConfig,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn timeout(&self) -> Duration {
|
||||
self.target_timeout
|
||||
.map(Duration::from_secs)
|
||||
.unwrap_or(DEFAULT_TARGET_TIMEOUT)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DotnetCoverageTask {
|
||||
config: Config,
|
||||
poller: InputPoller<Message>,
|
||||
}
|
||||
|
||||
impl DotnetCoverageTask {
|
||||
pub fn new(config: Config) -> Self {
|
||||
let poller = InputPoller::new("dotnet_coverage");
|
||||
Self { config, poller }
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) -> Result<()> {
|
||||
info!("starting dotnet_coverage task");
|
||||
self.config.coverage.init_pull().await?;
|
||||
|
||||
let dotnet_path = dotnet_path()?;
|
||||
let dotnet_coverage_path = dotnet_coverage_path()?;
|
||||
|
||||
let heartbeat = self.config.common.init_heartbeat(None).await?;
|
||||
let mut context = TaskContext::new(
|
||||
&self.config,
|
||||
heartbeat,
|
||||
dotnet_path,
|
||||
dotnet_coverage_path.clone(),
|
||||
);
|
||||
|
||||
if !context.uses_input() {
|
||||
bail!("input is not specified on the command line or arguments for the target");
|
||||
}
|
||||
|
||||
context.heartbeat.alive();
|
||||
|
||||
let coverage_local_path = self.config.coverage.local_path.canonicalize()?;
|
||||
let intermediate_files_path = intermediate_coverage_files_path(&coverage_local_path)?;
|
||||
fs::create_dir_all(&intermediate_files_path).await?;
|
||||
let timeout = self.config.timeout();
|
||||
let coverage_dir = self.config.coverage.clone();
|
||||
let dotnet_coverage_path = dotnet_coverage_path;
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = start_directory_monitor(
|
||||
&intermediate_files_path,
|
||||
&coverage_local_path,
|
||||
timeout,
|
||||
coverage_dir,
|
||||
&dotnet_coverage_path,
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("Directory monitor failed: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
for dir in &self.config.readonly_inputs {
|
||||
debug!("recording coverage for {}", dir.local_path.display());
|
||||
|
||||
dir.init_pull().await?;
|
||||
let dir_count = context.record_corpus(&dir.local_path).await?;
|
||||
|
||||
info!(
|
||||
"recorded coverage for {} inputs from {}",
|
||||
dir_count,
|
||||
dir.local_path.display()
|
||||
);
|
||||
|
||||
context.heartbeat.alive();
|
||||
}
|
||||
|
||||
context.heartbeat.alive();
|
||||
|
||||
if let Some(queue) = &self.config.input_queue {
|
||||
info!("polling queue for new coverage inputs");
|
||||
|
||||
let callback = CallbackImpl::new(queue.clone(), context)?;
|
||||
self.poller.run(callback).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_directory_monitor(
|
||||
intermediate_files_path: &PathBuf,
|
||||
coverage_local_path: &Path,
|
||||
timeout: Duration,
|
||||
coverage_dir: SyncedDir,
|
||||
dotnet_coverage_path: &Path,
|
||||
) -> Result<()> {
|
||||
info!(
|
||||
"Starting dotnet coverage intermediate file directory monitor on {}",
|
||||
intermediate_files_path.to_string_lossy()
|
||||
);
|
||||
let mut monitor = DirectoryMonitor::new(intermediate_files_path).await?;
|
||||
debug!("Started directory monitor, waiting for files");
|
||||
while (monitor.next_file().await?).is_some() {
|
||||
debug!("Found intermediate coverage file");
|
||||
save_and_sync_coverage(
|
||||
coverage_local_path,
|
||||
timeout,
|
||||
coverage_dir.clone(),
|
||||
dotnet_coverage_path,
|
||||
)
|
||||
.await?;
|
||||
info!("Updated and synced coverage");
|
||||
}
|
||||
info!("Shut down directory monitor");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct TaskContext<'a> {
|
||||
config: &'a Config,
|
||||
heartbeat: Option<TaskHeartbeatClient>,
|
||||
dotnet_path: PathBuf,
|
||||
dotnet_coverage_path: PathBuf,
|
||||
}
|
||||
|
||||
impl<'a> TaskContext<'a> {
|
||||
pub fn new(
|
||||
config: &'a Config,
|
||||
heartbeat: Option<TaskHeartbeatClient>,
|
||||
dotnet_path: PathBuf,
|
||||
dotnet_coverage_path: PathBuf,
|
||||
) -> Self {
|
||||
Self {
|
||||
config,
|
||||
heartbeat,
|
||||
dotnet_path,
|
||||
dotnet_coverage_path,
|
||||
}
|
||||
}
|
||||
async fn record_corpus(&mut self, dir: &Path) -> Result<usize> {
|
||||
use futures::stream::StreamExt;
|
||||
|
||||
let mut corpus = fs::read_dir(dir)
|
||||
.await
|
||||
.map(ReadDirStream::new)
|
||||
.with_context(|| format!("unable to read corpus directory: {}", dir.display()))?;
|
||||
|
||||
let mut count = 0;
|
||||
|
||||
while let Some(entry) = corpus.next().await {
|
||||
match entry {
|
||||
Ok(entry) => {
|
||||
if entry.file_type().await?.is_file() {
|
||||
self.record_input(&entry.path()).await?;
|
||||
count += 1;
|
||||
} else {
|
||||
warn!("skipping non-file dir entry: {}", entry.path().display());
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("{:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
async fn record_input(&mut self, input: &Path) -> Result<()> {
|
||||
debug!("recording coverage for {}", input.display());
|
||||
let attempts = MAX_COVERAGE_RECORDING_ATTEMPTS;
|
||||
|
||||
for attempt in 1..=attempts {
|
||||
let result = self.try_record_input(input).await;
|
||||
|
||||
if let Err(err) = &result {
|
||||
// Recording failed, check if we can retry.
|
||||
if attempt < attempts {
|
||||
// We will retry, but warn to capture the error if we succeed.
|
||||
warn!(
|
||||
"error recording coverage for input = {}: {:?}",
|
||||
input.display(),
|
||||
err
|
||||
);
|
||||
} else {
|
||||
// Final attempt, do not retry.
|
||||
return result.with_context(|| {
|
||||
format_err!(
|
||||
"failed to record coverage for input = {} after {} attempts",
|
||||
input.display(),
|
||||
attempts
|
||||
)
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// We successfully recorded the coverage for `input`, so stop.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn try_record_input(&self, input: &Path) -> Result<()> {
|
||||
let mut cmd = self.command_for_input(input).await?;
|
||||
let timeout = self.config.timeout();
|
||||
spawn_with_timeout(&mut cmd, timeout).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn command_for_input(&self, input: &Path) -> Result<Command> {
|
||||
let expand = Expand::new()
|
||||
.machine_id()
|
||||
.await?
|
||||
.input_path(input)
|
||||
.job_id(&self.config.common.job_id)
|
||||
.setup_dir(&self.config.common.setup_dir)
|
||||
.target_exe(&self.config.target_exe)
|
||||
.target_options(&self.config.target_options)
|
||||
.task_id(&self.config.common.task_id);
|
||||
|
||||
let dotnet_coverage_path = &self.dotnet_coverage_path;
|
||||
let dotnet_path = &self.dotnet_path;
|
||||
let id = Uuid::new_v4();
|
||||
let output_file_path =
|
||||
intermediate_coverage_files_path(self.config.coverage.local_path.as_path())?
|
||||
.join(format!("{}.cobertura.xml", id));
|
||||
|
||||
let target_options = expand.evaluate(&self.config.target_options)?;
|
||||
|
||||
let mut cmd = Command::new(dotnet_coverage_path);
|
||||
cmd.arg("collect")
|
||||
.args(["--output-format", "cobertura"])
|
||||
.args(["-o", &output_file_path.to_string_lossy()])
|
||||
.arg(format!(
|
||||
"{} {} -- {}",
|
||||
dotnet_path.to_string_lossy(),
|
||||
self.config.target_exe.canonicalize()?.to_string_lossy(),
|
||||
target_options.join(" ")
|
||||
));
|
||||
|
||||
info!("{:?}", &cmd);
|
||||
|
||||
for (k, v) in &self.config.target_env {
|
||||
cmd.env(k, expand.evaluate_value(v)?);
|
||||
}
|
||||
|
||||
cmd.env_remove("RUST_LOG");
|
||||
cmd.stdin(Stdio::null());
|
||||
cmd.stdout(Stdio::piped());
|
||||
cmd.stderr(Stdio::piped());
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
fn uses_input(&self) -> bool {
|
||||
let input = PlaceHolder::Input.get_string();
|
||||
|
||||
for entry in &self.config.target_options {
|
||||
if entry.contains(&input) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (k, v) in &self.config.target_env {
|
||||
if k == &input || v.contains(&input) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn save_and_sync_coverage(
|
||||
coverage_local_path: &Path,
|
||||
timeout: Duration,
|
||||
coverage_dir: SyncedDir,
|
||||
dotnet_coverage_path: &Path,
|
||||
) -> Result<()> {
|
||||
info!("Saving and syncing coverage");
|
||||
let mut cmd = command_for_merge(coverage_local_path, dotnet_coverage_path).await?;
|
||||
spawn_with_timeout(&mut cmd, timeout).await?;
|
||||
info!("Pushing coverage");
|
||||
coverage_dir.sync_push().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn command_for_merge(
|
||||
coverage_local_path: &Path,
|
||||
dotnet_coverage_path: &Path,
|
||||
) -> Result<Command> {
|
||||
let output_file = working_dir(coverage_local_path)?.join(COBERTURA_COVERAGE_FILE);
|
||||
|
||||
let mut cmd = Command::new(dotnet_coverage_path);
|
||||
cmd.arg("merge")
|
||||
.args(["--output-format", "cobertura"])
|
||||
.args(["-o", &output_file.to_string_lossy()])
|
||||
.arg("-r")
|
||||
.arg("--remove-input-files")
|
||||
.arg("*.cobertura.xml")
|
||||
.arg(COBERTURA_COVERAGE_FILE); // This lets us 'fold' any new coverage into the existing coverage file.
|
||||
|
||||
cmd.current_dir(working_dir(coverage_local_path)?);
|
||||
|
||||
info!("{:?}", &cmd);
|
||||
info!("From: {:?}", working_dir(coverage_local_path)?);
|
||||
|
||||
cmd.env_remove("RUST_LOG");
|
||||
cmd.stdin(Stdio::null());
|
||||
cmd.stdout(Stdio::piped());
|
||||
cmd.stderr(Stdio::piped());
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
fn working_dir(coverage_local_path: &Path) -> Result<PathBuf> {
|
||||
Ok(coverage_local_path.canonicalize()?)
|
||||
}
|
||||
|
||||
fn intermediate_coverage_files_path(coverage_local_path: &Path) -> Result<PathBuf> {
|
||||
Ok(working_dir(coverage_local_path)?.join("intermediate-coverage-files"))
|
||||
}
|
||||
|
||||
async fn spawn_with_timeout(
|
||||
cmd: &mut Command,
|
||||
timeout_after: Duration,
|
||||
) -> Result<ExitStatus, std::io::Error> {
|
||||
cmd.kill_on_drop(true);
|
||||
timeout(timeout_after, cmd.spawn()?.wait()).await?
|
||||
}
|
||||
|
||||
fn dotnet_coverage_path() -> Result<PathBuf> {
|
||||
let tools_dir = env::var("ONEFUZZ_TOOLS")?;
|
||||
#[cfg(target_os = "windows")]
|
||||
let dotnet_coverage_executable = "dotnet-coverage.exe";
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let dotnet_coverage_executable = "dotnet-coverage";
|
||||
let dotnet_coverage = Path::new(&tools_dir).join(dotnet_coverage_executable);
|
||||
|
||||
Ok(dotnet_coverage)
|
||||
}
|
||||
|
||||
fn dotnet_path() -> Result<PathBuf> {
|
||||
let dotnet_root_dir = env::var("DOTNET_ROOT")?;
|
||||
#[cfg(target_os = "windows")]
|
||||
let dotnet_executable = "dotnet.exe";
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let dotnet_executable = "dotnet";
|
||||
let dotnet = Path::new(&dotnet_root_dir).join(dotnet_executable); // The dotnet executable
|
||||
|
||||
Ok(dotnet)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'a> Processor for TaskContext<'a> {
|
||||
async fn process(&mut self, _url: Option<Url>, input: &Path) -> Result<()> {
|
||||
self.heartbeat.alive();
|
||||
|
||||
self.record_input(input).await?;
|
||||
let coverage_local_path = self.config.coverage.local_path.canonicalize()?;
|
||||
|
||||
save_and_sync_coverage(
|
||||
coverage_local_path.as_path(),
|
||||
self.config.timeout(),
|
||||
self.config.coverage.clone(),
|
||||
&self.dotnet_coverage_path,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -28,10 +28,11 @@ use crate::tasks::config::CommonConfig;
|
||||
use crate::tasks::generic::input_poller::{CallbackImpl, InputPoller, Processor};
|
||||
use crate::tasks::heartbeat::{HeartbeatSender, TaskHeartbeatClient};
|
||||
|
||||
use super::COBERTURA_COVERAGE_FILE;
|
||||
|
||||
const MAX_COVERAGE_RECORDING_ATTEMPTS: usize = 2;
|
||||
const COVERAGE_FILE: &str = "coverage.json";
|
||||
const SOURCE_COVERAGE_FILE: &str = "source-coverage.json";
|
||||
const COBERTURA_COVERAGE_FILE: &str = "cobertura-coverage.xml";
|
||||
const MODULE_CACHE_FILE: &str = "module-cache.json";
|
||||
|
||||
const DEFAULT_TARGET_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
@ -1,4 +1,7 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
const COBERTURA_COVERAGE_FILE: &str = "cobertura-coverage.xml";
|
||||
|
||||
pub mod dotnet;
|
||||
pub mod generic;
|
||||
|
@ -558,4 +558,40 @@ TASK_DEFINITIONS = {
|
||||
),
|
||||
],
|
||||
),
|
||||
TaskType.dotnet_coverage: TaskDefinition(
|
||||
features=[
|
||||
TaskFeature.target_exe,
|
||||
TaskFeature.target_env,
|
||||
TaskFeature.target_options,
|
||||
TaskFeature.target_timeout,
|
||||
TaskFeature.coverage_filter,
|
||||
TaskFeature.target_must_use_input,
|
||||
],
|
||||
vm=VmDefinition(compare=Compare.Equal, value=1),
|
||||
containers=[
|
||||
ContainerDefinition(
|
||||
type=ContainerType.setup,
|
||||
compare=Compare.Equal,
|
||||
value=1,
|
||||
permissions=[ContainerPermission.Read, ContainerPermission.List],
|
||||
),
|
||||
ContainerDefinition(
|
||||
type=ContainerType.readonly_inputs,
|
||||
compare=Compare.AtLeast,
|
||||
value=1,
|
||||
permissions=[ContainerPermission.Read, ContainerPermission.List],
|
||||
),
|
||||
ContainerDefinition(
|
||||
type=ContainerType.coverage,
|
||||
compare=Compare.Equal,
|
||||
value=1,
|
||||
permissions=[
|
||||
ContainerPermission.List,
|
||||
ContainerPermission.Read,
|
||||
ContainerPermission.Write,
|
||||
],
|
||||
),
|
||||
],
|
||||
monitor_queue=ContainerType.readonly_inputs,
|
||||
),
|
||||
}
|
||||
|
@ -165,6 +165,8 @@ class TaskType(Enum):
|
||||
generic_crash_report = "generic_crash_report"
|
||||
generic_regression = "generic_regression"
|
||||
|
||||
dotnet_coverage = "dotnet_coverage"
|
||||
|
||||
|
||||
class VmState(Enum):
|
||||
init = "init"
|
||||
|
@ -14,6 +14,7 @@ MANAGED_SETUP="/onefuzz/bin/managed.sh"
|
||||
SCALESET_SETUP="/onefuzz/bin/scaleset-setup.sh"
|
||||
DOTNET_VERSION="6.0.300"
|
||||
export DOTNET_ROOT=/onefuzz/tools/dotnet
|
||||
export DOTNET_CLI_HOME="$DOTNET_ROOT"
|
||||
export ONEFUZZ_ROOT=/onefuzz
|
||||
export LLVM_SYMBOLIZER_PATH=/onefuzz/bin/llvm-symbolizer
|
||||
|
||||
@ -128,23 +129,24 @@ if type apt > /dev/null 2> /dev/null; then
|
||||
|
||||
# Install dotnet
|
||||
until sudo apt install -y curl libicu-dev; do
|
||||
echo "apt failed, sleeping 10s then retrying"
|
||||
logger "apt failed, sleeping 10s then retrying"
|
||||
sleep 10
|
||||
done
|
||||
|
||||
echo "downloading dotnet install"
|
||||
curl --retry 10 -sSL https://dot.net/v1/dotnet-install.sh -o dotnet-install.sh > /dev/null
|
||||
logger "downloading dotnet install"
|
||||
curl --retry 10 -sSL https://dot.net/v1/dotnet-install.sh -o dotnet-install.sh 2>&1 | logger -s -i -t 'onefuzz-curl-dotnet-install'
|
||||
chmod +x dotnet-install.sh
|
||||
|
||||
echo "running dotnet install"
|
||||
. ./dotnet-install.sh --version "$DOTNET_VERSION" --install-dir "$DOTNET_ROOT" 2>&1 | logger -s -i -t 'onefuzz-dotnet-setup'
|
||||
logger "running dotnet install"
|
||||
/bin/bash ./dotnet-install.sh --version "$DOTNET_VERSION" --install-dir "$DOTNET_ROOT" 2>&1 | logger -s -i -t 'onefuzz-dotnet-setup'
|
||||
rm dotnet-install.sh
|
||||
|
||||
echo "install dotnet tools"
|
||||
logger "install dotnet tools"
|
||||
pushd "$DOTNET_ROOT"
|
||||
./dotnet tool install dotnet-dump --tool-path /onefuzz/tools
|
||||
./dotnet tool install dotnet-coverage --tool-path /onefuzz/tools
|
||||
./dotnet tool install dotnet-sos --tool-path /onefuzz/tools
|
||||
ls -lah 2>&1 | logger -s -i -t 'onefuzz-dotnet-tools'
|
||||
"$DOTNET_ROOT"/dotnet tool install dotnet-dump --version 6.0.328102 --tool-path /onefuzz/tools 2>&1 | logger -s -i -t 'onefuzz-dotnet-tools'
|
||||
"$DOTNET_ROOT"/dotnet tool install dotnet-coverage --version 17.3.6 --tool-path /onefuzz/tools 2>&1 | logger -s -i -t 'onefuzz-dotnet-tools'
|
||||
"$DOTNET_ROOT"/dotnet tool install dotnet-sos --version 6.0.328102 --tool-path /onefuzz/tools 2>&1 | logger -s -i -t 'onefuzz-dotnet-tools'
|
||||
popd
|
||||
fi
|
||||
|
||||
|
@ -181,9 +181,9 @@ function Install-Dotnet([string]$Version, [string]$InstallDir, [string]$ToolsDir
|
||||
log "Installing dotnet: done"
|
||||
log "Installing dotnet tools to ${ToolsDir}"
|
||||
Push-Location $InstallDir
|
||||
./dotnet.exe tool install dotnet-dump --tool-path $ToolsDir
|
||||
./dotnet.exe tool install dotnet-coverage --tool-path $ToolsDir
|
||||
./dotnet.exe tool install dotnet-sos --tool-path $ToolsDir
|
||||
./dotnet.exe tool install dotnet-dump --version 6.0.328102 --tool-path $ToolsDir
|
||||
./dotnet.exe tool install dotnet-coverage --version 17.3.6 --tool-path $ToolsDir
|
||||
./dotnet.exe tool install dotnet-sos --version 6.0.328102 --tool-path $ToolsDir
|
||||
Pop-Location
|
||||
log "Installing dotnet tools: done"
|
||||
}
|
||||
|
Reference in New Issue
Block a user