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

@ -33,6 +33,7 @@ The current task types available are:
* generic_crash_report: use a built-in debugging tool (debugapi or ptrace based) * generic_crash_report: use a built-in debugging tool (debugapi or ptrace based)
to rerun the crashing input, attempting to generate an informational report to rerun the crashing input, attempting to generate an informational report
for each discovered crash 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 Each type of task has a unique set of configuration options available, these
include: include:

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_merge",
"generic_generator", "generic_generator",
"generic_crash_report", "generic_crash_report",
"generic_regression" "generic_regression",
"dotnet_coverage"
], ],
"title": "TaskType" "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_merge",
"generic_generator", "generic_generator",
"generic_crash_report", "generic_crash_report",
"generic_regression" "generic_regression",
"dotnet_coverage"
], ],
"title": "TaskType" "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_merge",
"generic_generator", "generic_generator",
"generic_crash_report", "generic_crash_report",
"generic_regression" "generic_regression",
"dotnet_coverage"
], ],
"title": "TaskType" "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_merge",
"generic_generator", "generic_generator",
"generic_crash_report", "generic_crash_report",
"generic_regression" "generic_regression",
"dotnet_coverage"
], ],
"title": "TaskType" "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_merge",
"generic_generator", "generic_generator",
"generic_crash_report", "generic_crash_report",
"generic_regression" "generic_regression",
"dotnet_coverage"
], ],
"title": "TaskType" "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_merge",
"generic_generator", "generic_generator",
"generic_crash_report", "generic_crash_report",
"generic_regression" "generic_regression",
"dotnet_coverage"
], ],
"title": "TaskType" "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_merge",
"generic_generator", "generic_generator",
"generic_crash_report", "generic_crash_report",
"generic_regression" "generic_regression",
"dotnet_coverage"
], ],
"title": "TaskType" "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_merge",
"generic_generator", "generic_generator",
"generic_crash_report", "generic_crash_report",
"generic_regression" "generic_regression",
"dotnet_coverage"
], ],
"title": "TaskType" "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_merge",
"generic_generator", "generic_generator",
"generic_crash_report", "generic_crash_report",
"generic_regression" "generic_regression",
"dotnet_coverage"
], ],
"title": "TaskType" "title": "TaskType"
}, },

View File

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

View File

@ -555,5 +555,42 @@ public static class Defs {
ContainerPermission.Read | ContainerPermission.List 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. // Copyright (c) Microsoft Corporation.
// Licensed under the MIT License. // Licensed under the MIT License.
#[cfg(any(target_os = "linux", target_os = "windows"))]
use crate::local::coverage;
use crate::local::{ use crate::local::{
common::add_common_config, generic_analysis, generic_crash_report, generic_generator, common::add_common_config, generic_analysis, generic_crash_report, generic_generator,
libfuzzer, libfuzzer_crash_report, libfuzzer_fuzz, libfuzzer_merge, libfuzzer_regression, libfuzzer, libfuzzer_crash_report, libfuzzer_fuzz, libfuzzer_merge, libfuzzer_regression,
libfuzzer_test_input, radamsa, test_input, tui::TerminalUi, 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 anyhow::{Context, Result};
use clap::{App, Arg, SubCommand}; use clap::{App, Arg, SubCommand};
use crossterm::tty::IsTty; use crossterm::tty::IsTty;
@ -23,6 +23,8 @@ enum Commands {
Radamsa, Radamsa,
#[cfg(any(target_os = "linux", target_os = "windows"))] #[cfg(any(target_os = "linux", target_os = "windows"))]
Coverage, Coverage,
#[cfg(any(target_os = "linux", target_os = "windows"))]
DotnetCoverage,
LibfuzzerFuzz, LibfuzzerFuzz,
LibfuzzerMerge, LibfuzzerMerge,
LibfuzzerCrashReport, LibfuzzerCrashReport,
@ -59,6 +61,8 @@ pub async fn run(args: clap::ArgMatches<'static>) -> Result<()> {
match command { match command {
#[cfg(any(target_os = "linux", target_os = "windows"))] #[cfg(any(target_os = "linux", target_os = "windows"))]
Commands::Coverage => coverage::run(&sub_args, event_sender).await, 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::Radamsa => radamsa::run(&sub_args, event_sender).await,
Commands::LibfuzzerCrashReport => { Commands::LibfuzzerCrashReport => {
libfuzzer_crash_report::run(&sub_args, event_sender).await 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 { let app = match subcommand {
#[cfg(any(target_os = "linux", target_os = "windows"))] #[cfg(any(target_os = "linux", target_os = "windows"))]
Commands::Coverage => coverage::args(subcommand.into()), 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::Radamsa => radamsa::args(subcommand.into()),
Commands::LibfuzzerCrashReport => libfuzzer_crash_report::args(subcommand.into()), Commands::LibfuzzerCrashReport => libfuzzer_crash_report::args(subcommand.into()),
Commands::LibfuzzerFuzz => libfuzzer_fuzz::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 DISABLE_CHECK_QUEUE: &str = "disable_check_queue";
pub const UNIQUE_REPORTS_DIR: &str = "unique_reports_dir"; pub const UNIQUE_REPORTS_DIR: &str = "unique_reports_dir";
pub const COVERAGE_DIR: &str = "coverage_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 READONLY_INPUTS: &str = "readonly_inputs_dir";
pub const CHECK_ASAN_LOG: &str = "check_asan_log"; pub const CHECK_ASAN_LOG: &str = "check_asan_log";
pub const TOOLS_DIR: &str = "tools_dir"; 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. // Copyright (c) Microsoft Corporation.
// Licensed under the MIT License. // 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::{ use crate::{
local::{ local::{
common::{ common::{
@ -28,6 +20,17 @@ use crate::{
report::libfuzzer_report::ReportTask, 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 anyhow::Result;
use clap::{App, SubCommand}; use clap::{App, SubCommand};
use flume::Sender; 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); 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"))] #[cfg(any(target_os = "linux", target_os = "windows"))]
if args.is_present(COVERAGE_DIR) { if args.is_present(COVERAGE_DIR) {
let coverage_input_monitor = let coverage_input_monitor =
DirectoryMonitorQueue::start_monitoring(crash_dir.clone()).await?; DirectoryMonitorQueue::start_monitoring(crash_dir.clone()).await?;
let coverage_config = build_coverage_config( let coverage_config = coverage::build_coverage_config(
args, args,
true, true,
Some(coverage_input_monitor.queue_client), Some(coverage_input_monitor.queue_client),

View File

@ -5,6 +5,8 @@ pub mod cmd;
pub mod common; pub mod common;
#[cfg(any(target_os = "linux", target_os = "windows"))] #[cfg(any(target_os = "linux", target_os = "windows"))]
pub mod coverage; pub mod coverage;
#[cfg(any(target_os = "linux", target_os = "windows"))]
pub mod dotnet_coverage;
pub mod generic_analysis; pub mod generic_analysis;
pub mod generic_crash_report; pub mod generic_crash_report;
pub mod generic_generator; pub mod generic_generator;

View File

@ -83,6 +83,10 @@ pub enum Config {
#[serde(alias = "coverage")] #[serde(alias = "coverage")]
Coverage(coverage::generic::Config), Coverage(coverage::generic::Config),
#[cfg(any(target_os = "linux", target_os = "windows"))]
#[serde(alias = "dotnet_coverage")]
DotnetCoverage(coverage::dotnet::Config),
#[serde(alias = "libfuzzer_fuzz")] #[serde(alias = "libfuzzer_fuzz")]
LibFuzzerFuzz(fuzz::libfuzzer_fuzz::Config), LibFuzzerFuzz(fuzz::libfuzzer_fuzz::Config),
@ -130,6 +134,8 @@ impl Config {
match self { match self {
#[cfg(any(target_os = "linux", target_os = "windows"))] #[cfg(any(target_os = "linux", target_os = "windows"))]
Config::Coverage(c) => &mut c.common, 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::LibFuzzerFuzz(c) => &mut c.common,
Config::LibFuzzerMerge(c) => &mut c.common, Config::LibFuzzerMerge(c) => &mut c.common,
Config::LibFuzzerReport(c) => &mut c.common, Config::LibFuzzerReport(c) => &mut c.common,
@ -147,6 +153,8 @@ impl Config {
match self { match self {
#[cfg(any(target_os = "linux", target_os = "windows"))] #[cfg(any(target_os = "linux", target_os = "windows"))]
Config::Coverage(c) => &c.common, Config::Coverage(c) => &c.common,
#[cfg(any(target_os = "linux", target_os = "windows"))]
Config::DotnetCoverage(c) => &c.common,
Config::LibFuzzerFuzz(c) => &c.common, Config::LibFuzzerFuzz(c) => &c.common,
Config::LibFuzzerMerge(c) => &c.common, Config::LibFuzzerMerge(c) => &c.common,
Config::LibFuzzerReport(c) => &c.common, Config::LibFuzzerReport(c) => &c.common,
@ -164,6 +172,8 @@ impl Config {
let event_type = match self { let event_type = match self {
#[cfg(any(target_os = "linux", target_os = "windows"))] #[cfg(any(target_os = "linux", target_os = "windows"))]
Config::Coverage(_) => "coverage", Config::Coverage(_) => "coverage",
#[cfg(any(target_os = "linux", target_os = "windows"))]
Config::DotnetCoverage(_) => "dotnet_coverage",
Config::LibFuzzerFuzz(_) => "libfuzzer_fuzz", Config::LibFuzzerFuzz(_) => "libfuzzer_fuzz",
Config::LibFuzzerMerge(_) => "libfuzzer_merge", Config::LibFuzzerMerge(_) => "libfuzzer_merge",
Config::LibFuzzerReport(_) => "libfuzzer_crash_report", Config::LibFuzzerReport(_) => "libfuzzer_crash_report",
@ -208,6 +218,12 @@ impl Config {
match self { match self {
#[cfg(any(target_os = "linux", target_os = "windows"))] #[cfg(any(target_os = "linux", target_os = "windows"))]
Config::Coverage(config) => coverage::generic::CoverageTask::new(config).run().await, 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) => { Config::LibFuzzerFuzz(config) => {
fuzz::libfuzzer_fuzz::LibFuzzerFuzzTask::new(config)? fuzz::libfuzzer_fuzz::LibFuzzerFuzzTask::new(config)?
.run() .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::generic::input_poller::{CallbackImpl, InputPoller, Processor};
use crate::tasks::heartbeat::{HeartbeatSender, TaskHeartbeatClient}; use crate::tasks::heartbeat::{HeartbeatSender, TaskHeartbeatClient};
use super::COBERTURA_COVERAGE_FILE;
const MAX_COVERAGE_RECORDING_ATTEMPTS: usize = 2; const MAX_COVERAGE_RECORDING_ATTEMPTS: usize = 2;
const COVERAGE_FILE: &str = "coverage.json"; const COVERAGE_FILE: &str = "coverage.json";
const SOURCE_COVERAGE_FILE: &str = "source-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 MODULE_CACHE_FILE: &str = "module-cache.json";
const DEFAULT_TARGET_TIMEOUT: Duration = Duration::from_secs(5); const DEFAULT_TARGET_TIMEOUT: Duration = Duration::from_secs(5);

View File

@ -1,4 +1,7 @@
// Copyright (c) Microsoft Corporation. // Copyright (c) Microsoft Corporation.
// Licensed under the MIT License. // Licensed under the MIT License.
const COBERTURA_COVERAGE_FILE: &str = "cobertura-coverage.xml";
pub mod dotnet;
pub mod generic; 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_crash_report = "generic_crash_report"
generic_regression = "generic_regression" generic_regression = "generic_regression"
dotnet_coverage = "dotnet_coverage"
class VmState(Enum): class VmState(Enum):
init = "init" init = "init"

View File

@ -14,6 +14,7 @@ MANAGED_SETUP="/onefuzz/bin/managed.sh"
SCALESET_SETUP="/onefuzz/bin/scaleset-setup.sh" SCALESET_SETUP="/onefuzz/bin/scaleset-setup.sh"
DOTNET_VERSION="6.0.300" DOTNET_VERSION="6.0.300"
export DOTNET_ROOT=/onefuzz/tools/dotnet export DOTNET_ROOT=/onefuzz/tools/dotnet
export DOTNET_CLI_HOME="$DOTNET_ROOT"
export ONEFUZZ_ROOT=/onefuzz export ONEFUZZ_ROOT=/onefuzz
export LLVM_SYMBOLIZER_PATH=/onefuzz/bin/llvm-symbolizer export LLVM_SYMBOLIZER_PATH=/onefuzz/bin/llvm-symbolizer
@ -128,23 +129,24 @@ if type apt > /dev/null 2> /dev/null; then
# Install dotnet # Install dotnet
until sudo apt install -y curl libicu-dev; do 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 sleep 10
done done
echo "downloading dotnet install" logger "downloading dotnet install"
curl --retry 10 -sSL https://dot.net/v1/dotnet-install.sh -o dotnet-install.sh > /dev/null 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 chmod +x dotnet-install.sh
echo "running dotnet install" logger "running dotnet install"
. ./dotnet-install.sh --version "$DOTNET_VERSION" --install-dir "$DOTNET_ROOT" 2>&1 | logger -s -i -t 'onefuzz-dotnet-setup' /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 rm dotnet-install.sh
echo "install dotnet tools" logger "install dotnet tools"
pushd "$DOTNET_ROOT" pushd "$DOTNET_ROOT"
./dotnet tool install dotnet-dump --tool-path /onefuzz/tools ls -lah 2>&1 | logger -s -i -t 'onefuzz-dotnet-tools'
./dotnet tool install dotnet-coverage --tool-path /onefuzz/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 tool install dotnet-sos --tool-path /onefuzz/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 popd
fi fi

View File

@ -181,9 +181,9 @@ function Install-Dotnet([string]$Version, [string]$InstallDir, [string]$ToolsDir
log "Installing dotnet: done" log "Installing dotnet: done"
log "Installing dotnet tools to ${ToolsDir}" log "Installing dotnet tools to ${ToolsDir}"
Push-Location $InstallDir Push-Location $InstallDir
./dotnet.exe tool install dotnet-dump --tool-path $ToolsDir ./dotnet.exe tool install dotnet-dump --version 6.0.328102 --tool-path $ToolsDir
./dotnet.exe tool install dotnet-coverage --tool-path $ToolsDir ./dotnet.exe tool install dotnet-coverage --version 17.3.6 --tool-path $ToolsDir
./dotnet.exe tool install dotnet-sos --tool-path $ToolsDir ./dotnet.exe tool install dotnet-sos --version 6.0.328102 --tool-path $ToolsDir
Pop-Location Pop-Location
log "Installing dotnet tools: done" log "Installing dotnet tools: done"
} }