add a single-shot crash report utility (#703)

Adds `test-input` and `test-input-libfuzzer`, which print the CrashTestResult in json form.

While many of the existing tasks make sense running in a managed loop, crash report generation is something that having a single one-off is useful.

Example:
```
$ onefuzz-agent local test-input /tmp/fuzz.exe /tmp/crash.txt
{
  "crash_report": {
    "input_sha256": "a35b3ce1038750e9175a6dcd3f64c8d4e85720affb12cc11f5d0b6889274d06e",
    "executable": "/tmp/fuzz.exe",
    "crash_type": "SIGABRT",
    "crash_site": "0x7f0d9d4ad18b in gsignal+0xcb (/usr/lib/x86_64-linux-gnu/libc-2.31.so+0x4618b)",
    "call_stack": [
      "#0 0x7f0d9d4ad18b in gsignal+0xcb (/usr/lib/x86_64-linux-gnu/libc-2.31.so+0x4618b)",
      "#1 0x7f0d9d48c859 in abort+0x12b (/usr/lib/x86_64-linux-gnu/libc-2.31.so+0x25859)",
      "#2 0x7f0d9d4f73ee in <unknown> (/usr/lib/x86_64-linux-gnu/libc-2.31.so+0x903ee)",
      "#3 0x7f0d9d599b4a in __fortify_fail+0x2a (/usr/lib/x86_64-linux-gnu/libc-2.31.so+0x132b4a)",
      "#4 0x7f0d9d5983e6 in __chk_fail+0x16 (/usr/lib/x86_64-linux-gnu/libc-2.31.so+0x1313e6)",
      "#5 0x7f0d9d597e09 in __strncpy_chk+0x19 (/usr/lib/x86_64-linux-gnu/libc-2.31.so+0x130e09)",
      "#6 0x400a54 in from_file+0xa4 (/tmp/fuzz.exe+0xa54)",
      "#7 0x7f0d9d48e0b3 in __libc_start_main+0xf3 (/usr/lib/x86_64-linux-gnu/libc-2.31.so+0x270b3)",
      "#8 0x40077a in _start+0x2a (/tmp/fuzz.exe+0x77a)"
    ],
    "call_stack_sha256": "6906234fb235690cc2843a1a55f49ff68b424e54bec55f9b8258415d97b3e638",
    "task_id": "00000000-0000-0000-0000-000000000000",
    "job_id": "00000000-0000-0000-0000-000000000000"
  }
}
$
```
This commit is contained in:
bmc-msft
2021-03-22 13:46:33 -04:00
committed by GitHub
parent cf6c4e5632
commit 7be4f3bbc1
15 changed files with 207 additions and 27 deletions

View File

@ -7,7 +7,7 @@ use clap::{App, SubCommand};
use crate::local::{
common::add_common_config, generic_analysis, generic_crash_report, generic_generator,
libfuzzer, libfuzzer_coverage, libfuzzer_crash_report, libfuzzer_fuzz, libfuzzer_merge,
radamsa,
libfuzzer_test_input, radamsa, test_input,
};
const RADAMSA: &str = "radamsa";
@ -16,9 +16,11 @@ const LIBFUZZER_FUZZ: &str = "libfuzzer-fuzz";
const LIBFUZZER_CRASH_REPORT: &str = "libfuzzer-crash-report";
const LIBFUZZER_COVERAGE: &str = "libfuzzer-coverage";
const LIBFUZZER_MERGE: &str = "libfuzzer-merge";
const LIBFUZZER_TEST_INPUT: &str = "libfuzzer-test-input";
const GENERIC_CRASH_REPORT: &str = "generic-crash-report";
const GENERIC_GENERATOR: &str = "generic-generator";
const GENERIC_ANALYSIS: &str = "generic-analysis";
const GENERIC_TEST_INPUT: &str = "generic-test-input";
pub async fn run(args: &clap::ArgMatches<'_>) -> Result<()> {
match args.subcommand() {
@ -31,6 +33,8 @@ pub async fn run(args: &clap::ArgMatches<'_>) -> Result<()> {
(GENERIC_ANALYSIS, Some(sub)) => generic_analysis::run(sub).await,
(GENERIC_CRASH_REPORT, Some(sub)) => generic_crash_report::run(sub).await,
(GENERIC_GENERATOR, Some(sub)) => generic_generator::run(sub).await,
(GENERIC_TEST_INPUT, Some(sub)) => test_input::run(sub).await,
(LIBFUZZER_TEST_INPUT, Some(sub)) => libfuzzer_test_input::run(sub).await,
_ => {
anyhow::bail!("missing subcommand\nUSAGE: {}", args.usage());
}
@ -57,4 +61,8 @@ pub fn args(name: &str) -> App<'static, 'static> {
GENERIC_GENERATOR,
)))
.subcommand(add_common_config(generic_analysis::args(GENERIC_ANALYSIS)))
.subcommand(add_common_config(test_input::args(GENERIC_TEST_INPUT)))
.subcommand(add_common_config(libfuzzer_test_input::args(
LIBFUZZER_TEST_INPUT,
)))
}

View File

@ -32,6 +32,7 @@ pub const CHECK_ASAN_LOG: &str = "check_asan_log";
pub const TOOLS_DIR: &str = "tools_dir";
pub const RENAME_OUTPUT: &str = "rename_output";
pub const CHECK_FUZZER_HELP: &str = "check_fuzzer_help";
pub const DISABLE_CHECK_DEBUGGER: &str = "disable_check_debugger";
pub const TARGET_EXE: &str = "target_exe";
pub const TARGET_ENV: &str = "target_env";
@ -176,9 +177,19 @@ pub fn get_synced_dir(
})
}
pub fn build_common_config(args: &ArgMatches<'_>) -> Result<CommonConfig> {
// NOTE: generate_task_id is intended to change the default behavior for local
// fuzzing tasks from generating random task id to using UUID::nil(). This
// enables making the one-shot crash report generation, which isn't really a task,
// consistent across multiple runs.
pub fn build_common_config(args: &ArgMatches<'_>, generate_task_id: bool) -> Result<CommonConfig> {
let job_id = get_uuid("job_id", args).unwrap_or_else(|_| Uuid::nil());
let task_id = get_uuid("task_id", args).unwrap_or_else(|_| Uuid::new_v4());
let task_id = get_uuid("task_id", args).unwrap_or_else(|_| {
if generate_task_id {
Uuid::new_v4()
} else {
Uuid::nil()
}
});
let instance_id = get_uuid("instance_id", args).unwrap_or_else(|_| Uuid::nil());
let setup_dir = if args.is_present(SETUP_DIR) {

View File

@ -55,7 +55,7 @@ pub fn build_analysis_config(
}
pub async fn run(args: &clap::ArgMatches<'_>) -> Result<()> {
let common = build_common_config(args)?;
let common = build_common_config(args, true)?;
let config = build_analysis_config(args, None, common)?;
run_analysis(config).await
}

View File

@ -4,8 +4,9 @@
use crate::{
local::common::{
build_common_config, get_cmd_arg, get_cmd_env, get_cmd_exe, get_synced_dir, CmdType,
CHECK_ASAN_LOG, CHECK_RETRY_COUNT, CRASHES_DIR, DISABLE_CHECK_QUEUE, NO_REPRO_DIR,
REPORTS_DIR, TARGET_ENV, TARGET_EXE, TARGET_OPTIONS, TARGET_TIMEOUT, UNIQUE_REPORTS_DIR,
CHECK_ASAN_LOG, CHECK_RETRY_COUNT, CRASHES_DIR, DISABLE_CHECK_DEBUGGER,
DISABLE_CHECK_QUEUE, NO_REPRO_DIR, REPORTS_DIR, TARGET_ENV, TARGET_EXE, TARGET_OPTIONS,
TARGET_TIMEOUT, UNIQUE_REPORTS_DIR,
},
tasks::{
config::CommonConfig,
@ -46,7 +47,7 @@ pub fn build_report_config(
let check_retry_count = value_t!(args, CHECK_RETRY_COUNT, u64)?;
let check_queue = !args.is_present(DISABLE_CHECK_QUEUE);
let check_asan_log = args.is_present(CHECK_ASAN_LOG);
let check_debugger = !args.is_present("disable_check_debugger");
let check_debugger = !args.is_present(DISABLE_CHECK_DEBUGGER);
let config = Config {
target_exe,
@ -70,7 +71,7 @@ pub fn build_report_config(
}
pub async fn run(args: &clap::ArgMatches<'_>) -> Result<()> {
let common = build_common_config(args)?;
let common = build_common_config(args, true)?;
let config = build_report_config(args, None, common)?;
ReportTask::new(config).managed_run().await
}
@ -121,9 +122,9 @@ pub fn build_shared_args() -> Vec<Arg<'static, 'static>> {
Arg::with_name(CHECK_ASAN_LOG)
.takes_value(false)
.long(CHECK_ASAN_LOG),
Arg::with_name("disable_check_debugger")
Arg::with_name(DISABLE_CHECK_DEBUGGER)
.takes_value(false)
.long("disable_check_debugger"),
.long(DISABLE_CHECK_DEBUGGER),
]
}

View File

@ -4,9 +4,9 @@
use crate::{
local::common::{
build_common_config, get_cmd_arg, get_cmd_env, get_cmd_exe, get_synced_dir,
get_synced_dirs, CmdType, CHECK_ASAN_LOG, CHECK_RETRY_COUNT, CRASHES_DIR, GENERATOR_ENV,
GENERATOR_EXE, GENERATOR_OPTIONS, READONLY_INPUTS, RENAME_OUTPUT, TARGET_ENV, TARGET_EXE,
TARGET_OPTIONS, TARGET_TIMEOUT, TOOLS_DIR,
get_synced_dirs, CmdType, CHECK_ASAN_LOG, CHECK_RETRY_COUNT, CRASHES_DIR,
DISABLE_CHECK_DEBUGGER, GENERATOR_ENV, GENERATOR_EXE, GENERATOR_OPTIONS, READONLY_INPUTS,
RENAME_OUTPUT, TARGET_ENV, TARGET_EXE, TARGET_OPTIONS, TARGET_TIMEOUT, TOOLS_DIR,
},
tasks::{
config::CommonConfig,
@ -29,7 +29,7 @@ pub fn build_fuzz_config(args: &clap::ArgMatches<'_>, common: CommonConfig) -> R
let rename_output = args.is_present(RENAME_OUTPUT);
let check_asan_log = args.is_present(CHECK_ASAN_LOG);
let check_debugger = !args.is_present("disable_check_debugger");
let check_debugger = !args.is_present(DISABLE_CHECK_DEBUGGER);
let check_retry_count = value_t!(args, CHECK_RETRY_COUNT, u64)?;
let target_timeout = Some(value_t!(args, TARGET_TIMEOUT, u64)?);
@ -60,7 +60,7 @@ pub fn build_fuzz_config(args: &clap::ArgMatches<'_>, common: CommonConfig) -> R
}
pub async fn run(args: &clap::ArgMatches<'_>) -> Result<()> {
let common = build_common_config(args)?;
let common = build_common_config(args, true)?;
let config = build_fuzz_config(args, common)?;
GeneratorTask::new(config).run().await
}
@ -120,9 +120,9 @@ pub fn build_shared_args() -> Vec<Arg<'static, 'static>> {
.takes_value(true)
.long(TARGET_TIMEOUT)
.default_value("30"),
Arg::with_name("disable_check_debugger")
Arg::with_name(DISABLE_CHECK_DEBUGGER)
.takes_value(false)
.long("disable_check_debugger"),
.long(DISABLE_CHECK_DEBUGGER),
]
}

View File

@ -27,7 +27,7 @@ use tokio::task::spawn;
use uuid::Uuid;
pub async fn run(args: &clap::ArgMatches<'_>) -> Result<()> {
let common = build_common_config(args)?;
let common = build_common_config(args, true)?;
let fuzz_config = build_fuzz_config(args, common.clone())?;
let crash_dir = fuzz_config
.crashes

View File

@ -55,7 +55,7 @@ pub fn build_coverage_config(
}
pub async fn run(args: &clap::ArgMatches<'_>) -> Result<()> {
let common = build_common_config(args)?;
let common = build_common_config(args, true)?;
let config = build_coverage_config(args, false, None, common)?;
let mut task = CoverageTask::new(config);

View File

@ -63,7 +63,7 @@ pub fn build_report_config(
}
pub async fn run(args: &clap::ArgMatches<'_>) -> Result<()> {
let common = build_common_config(args)?;
let common = build_common_config(args, true)?;
let config = build_report_config(args, None, common)?;
ReportTask::new(config).managed_run().await
}

View File

@ -50,7 +50,7 @@ pub fn build_fuzz_config(args: &clap::ArgMatches<'_>, common: CommonConfig) -> R
}
pub async fn run(args: &clap::ArgMatches<'_>) -> Result<()> {
let common = build_common_config(args)?;
let common = build_common_config(args, true)?;
let config = build_fuzz_config(args, common)?;
LibFuzzerFuzzTask::new(config)?.run().await
}

View File

@ -45,7 +45,7 @@ pub fn build_merge_config(
}
pub async fn run(args: &clap::ArgMatches<'_>) -> Result<()> {
let common = build_common_config(args)?;
let common = build_common_config(args, true)?;
let config = build_merge_config(args, None, common)?;
spawn(std::sync::Arc::new(config)).await
}

View File

@ -0,0 +1,72 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use crate::{
local::common::{
build_common_config, get_cmd_arg, get_cmd_env, CmdType, CHECK_RETRY_COUNT, TARGET_ENV,
TARGET_EXE, TARGET_OPTIONS, TARGET_TIMEOUT,
},
tasks::report::libfuzzer_report::{test_input, TestInputArgs},
};
use anyhow::Result;
use clap::{App, Arg, SubCommand};
use std::path::PathBuf;
pub async fn run(args: &clap::ArgMatches<'_>) -> Result<()> {
let common = build_common_config(args, true)?;
let target_exe = value_t!(args, TARGET_EXE, PathBuf)?;
let target_env = get_cmd_env(CmdType::Target, args)?;
let target_options = get_cmd_arg(CmdType::Target, args);
let input = value_t!(args, "input", PathBuf)?;
let target_timeout = value_t!(args, TARGET_TIMEOUT, u64).ok();
let check_retry_count = value_t!(args, CHECK_RETRY_COUNT, u64)?;
let config = TestInputArgs {
target_exe: &target_exe.as_path(),
target_env: &target_env,
target_options: &target_options,
input_url: None,
input: input.as_path(),
job_id: common.job_id,
task_id: common.task_id,
target_timeout,
check_retry_count,
setup_dir: &common.setup_dir,
minimized_stack_depth: None,
};
let result = test_input(config).await?;
println!("{}", serde_json::to_string_pretty(&result)?);
Ok(())
}
pub fn build_shared_args() -> Vec<Arg<'static, 'static>> {
vec![
Arg::with_name(TARGET_EXE).takes_value(true).required(true),
Arg::with_name("input").takes_value(true).required(true),
Arg::with_name(TARGET_ENV)
.long(TARGET_ENV)
.takes_value(true)
.multiple(true),
Arg::with_name(TARGET_OPTIONS)
.default_value("{input}")
.long(TARGET_OPTIONS)
.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(CHECK_RETRY_COUNT)
.takes_value(true)
.long(CHECK_RETRY_COUNT)
.default_value("0"),
]
}
pub fn args(name: &'static str) -> App<'static, 'static> {
SubCommand::with_name(name)
.about("test a libfuzzer application with a specific input")
.args(&build_shared_args())
}

View File

@ -11,4 +11,6 @@ pub mod libfuzzer_coverage;
pub mod libfuzzer_crash_report;
pub mod libfuzzer_fuzz;
pub mod libfuzzer_merge;
pub mod libfuzzer_test_input;
pub mod radamsa;
pub mod test_input;

View File

@ -17,7 +17,7 @@ use tokio::task::spawn;
use uuid::Uuid;
pub async fn run(args: &clap::ArgMatches<'_>) -> Result<()> {
let common = build_common_config(args)?;
let common = build_common_config(args, true)?;
let fuzz_config = build_fuzz_config(args, common.clone())?;
let crash_dir = fuzz_config
.crashes

View File

@ -0,0 +1,82 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use crate::{
local::common::{
build_common_config, get_cmd_arg, get_cmd_env, CmdType, CHECK_ASAN_LOG, CHECK_RETRY_COUNT,
DISABLE_CHECK_DEBUGGER, TARGET_ENV, TARGET_EXE, TARGET_OPTIONS, TARGET_TIMEOUT,
},
tasks::report::generic::{test_input, TestInputArgs},
};
use anyhow::Result;
use clap::{App, Arg, SubCommand};
use std::path::PathBuf;
pub async fn run(args: &clap::ArgMatches<'_>) -> Result<()> {
let common = build_common_config(args, false)?;
let target_exe = value_t!(args, TARGET_EXE, PathBuf)?;
let target_env = get_cmd_env(CmdType::Target, args)?;
let target_options = get_cmd_arg(CmdType::Target, args);
let input = value_t!(args, "input", PathBuf)?;
let target_timeout = value_t!(args, TARGET_TIMEOUT, u64).ok();
let check_retry_count = value_t!(args, CHECK_RETRY_COUNT, u64)?;
let check_asan_log = args.is_present(CHECK_ASAN_LOG);
let check_debugger = !args.is_present(DISABLE_CHECK_DEBUGGER);
let config = TestInputArgs {
target_exe: &target_exe.as_path(),
target_env: &target_env,
target_options: &target_options,
input_url: None,
input: input.as_path(),
job_id: common.job_id,
task_id: common.task_id,
target_timeout,
check_retry_count,
setup_dir: &common.setup_dir,
minimized_stack_depth: None,
check_asan_log,
check_debugger,
};
let result = test_input(config).await?;
println!("{}", serde_json::to_string_pretty(&result)?);
Ok(())
}
pub fn build_shared_args() -> Vec<Arg<'static, 'static>> {
vec![
Arg::with_name(TARGET_EXE).takes_value(true).required(true),
Arg::with_name("input").takes_value(true).required(true),
Arg::with_name(TARGET_ENV)
.long(TARGET_ENV)
.takes_value(true)
.multiple(true),
Arg::with_name(TARGET_OPTIONS)
.default_value("{input}")
.long(TARGET_OPTIONS)
.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(CHECK_RETRY_COUNT)
.takes_value(true)
.long(CHECK_RETRY_COUNT)
.default_value("0"),
Arg::with_name(CHECK_ASAN_LOG)
.takes_value(false)
.long(CHECK_ASAN_LOG),
Arg::with_name(DISABLE_CHECK_DEBUGGER)
.takes_value(false)
.long("disable_check_debugger"),
]
}
pub fn args(name: &'static str) -> App<'static, 'static> {
SubCommand::with_name(name)
.about("test an application with a specific input")
.args(&build_shared_args())
}

View File

@ -32,24 +32,27 @@ pub struct CrashReport {
pub call_stack: Vec<String>,
pub call_stack_sha256: String,
#[serde(default)]
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub minimized_stack: Vec<String>,
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub minimized_stack_sha256: Option<String>,
#[serde(default)]
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub minimized_stack_function_names: Vec<String>,
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub minimized_stack_function_names_sha256: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub asan_log: Option<String>,
pub task_id: Uuid,
pub job_id: Uuid,
#[serde(skip_serializing_if = "Option::is_none")]
pub scariness_score: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scariness_description: Option<String>,
}
@ -62,6 +65,7 @@ pub struct NoCrash {
pub task_id: Uuid,
pub job_id: Uuid,
pub tries: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}