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:
Teo Voinea
2022-07-06 16:13:45 -04:00
committed by GitHub
parent bc48069a94
commit f37224e8bb
18 changed files with 725 additions and 37 deletions

View File

@ -13,7 +13,7 @@ fuzz jobs from a few virtual machines to thousands of cores.
## Features
* **Composable fuzzing workflows**: Open source allows users to onboard their own
fuzzers, [swap instrumentation](docs/custom-analysis.md), and manage seed inputs.
fuzzers, [swap instrumentation](docs/custom-analysis.md), and manage seed inputs.
* **Built-in ensemble fuzzing**: By default, fuzzers work as a team to share strengths,
swapping inputs of interest between fuzzing technologies.
* **Programmatic triage and result de-duplication**: It provides unique flaw cases that

View File

@ -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:
@ -70,4 +71,4 @@ implementation level details on the types of tasks available.
## Environment Variables
* `ONEFUZZ_TARGET_SETUP_PATH`: An environment variable set prior to launching target-specific setup scripts that defines the path to the setup container.
* `ONEFUZZ_TARGET_SETUP_PATH`: An environment variable set prior to launching target-specific setup scripts that defines the path to the setup container.

View File

@ -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"
},

View File

@ -70,7 +70,8 @@ public enum TaskType {
GenericMerge,
GenericGenerator,
GenericCrashReport,
GenericRegression
GenericRegression,
DotnetCoverage,
}
public enum Os {

View File

@ -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)
},
};
}

View File

@ -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()),

View File

@ -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";

View 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))
}

View File

@ -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),

View File

@ -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;

View File

@ -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()

View 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(())
}
}

View File

@ -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);

View File

@ -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;

View File

@ -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,
),
}

View File

@ -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"

View File

@ -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

View File

@ -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"
}