Skip to content
This repository has been archived by the owner on Nov 1, 2023. It is now read-only.

Starting integration tests #3438

Merged
merged 46 commits into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
ffe5ff6
Starting integration tests
tevoinea Aug 21, 2023
a3a2f7b
Ready to test the test
tevoinea Aug 21, 2023
e3f71f9
Parametrize test
tevoinea Aug 22, 2023
e7cb584
checkpoint
tevoinea Aug 22, 2023
2239d35
Test works
tevoinea Aug 22, 2023
e179c9e
Run integration tests in pipeline
tevoinea Aug 22, 2023
bda8ccf
fmt
tevoinea Aug 22, 2023
d9a5f0c
.
tevoinea Aug 22, 2023
2025d82
-p
tevoinea Aug 22, 2023
cf6843a
Install clang
tevoinea Aug 22, 2023
33f8710
quotes not required in yaml?
tevoinea Aug 22, 2023
9a48645
Hopefully fixed windows?
tevoinea Aug 23, 2023
35a9801
Merge branch 'main' of https://github.com/microsoft/onefuzz into tevo…
tevoinea Aug 23, 2023
82fad9a
Try without killondrop
tevoinea Aug 23, 2023
e488f07
lint
tevoinea Aug 23, 2023
e8fb29a
small test
tevoinea Aug 23, 2023
d7a0391
another test
tevoinea Aug 23, 2023
732904b
Reuse core name
tevoinea Aug 23, 2023
fb5b9d5
Wrong step
tevoinea Aug 23, 2023
d7eb838
bump tokio?
tevoinea Aug 24, 2023
8c98c13
Try with rust
tevoinea Aug 28, 2023
621436f
make build happy
tevoinea Aug 28, 2023
983d3a2
Bump pete and small clean up
tevoinea Sep 5, 2023
b5a89bd
Clean up and make the test pass regularly
tevoinea Sep 5, 2023
7b64031
fix broken ci
tevoinea Sep 5, 2023
4718d60
Lower the poll timeout
tevoinea Sep 5, 2023
c95f79e
Set the timeout in a nicer way
tevoinea Sep 5, 2023
910bbe4
fix windows
tevoinea Sep 5, 2023
c1fc9f2
fmt
tevoinea Sep 5, 2023
4d43b74
Include and copy pdbs
tevoinea Sep 5, 2023
2be8468
Ignore if pdb is missing on linux
tevoinea Sep 5, 2023
ae250a1
It takes too long for coverage to be generated
tevoinea Sep 5, 2023
1cd58f4
lint
tevoinea Sep 5, 2023
4f59b27
Only warn on missing coverage since it's flaky
tevoinea Sep 5, 2023
8ebe892
Fix windows build
tevoinea Sep 6, 2023
082a51e
Merge branch 'main' into tevoinea/LocalTaskIntegrationTests
tevoinea Sep 6, 2023
096efcd
Small clean up
tevoinea Sep 6, 2023
54a4ed9
Merge branch 'tevoinea/LocalTaskIntegrationTests' of https://github.c…
tevoinea Sep 6, 2023
80b843e
Try lowering the poll delay
tevoinea Sep 6, 2023
0e678d2
fix coverage
tevoinea Sep 7, 2023
0da29a9
PR comments
tevoinea Sep 7, 2023
f3f7281
.
tevoinea Sep 7, 2023
cbab650
Merge branch 'main' into tevoinea/LocalTaskIntegrationTests
tevoinea Sep 7, 2023
ba05f02
Apparently make is missing?
tevoinea Sep 7, 2023
4b6c410
Merge branch 'tevoinea/LocalTaskIntegrationTests' of https://github.c…
tevoinea Sep 7, 2023
9e9c548
Remove aggressive step skipping in CI
tevoinea Sep 7, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +79,24 @@ jobs:
key: ${{env.ACTIONS_CACHE_KEY_DATE}} # additional key for cache-busting
workspaces: src/agent
- name: Linux Prereqs
if: runner.os == 'Linux' && steps.cache-agent-artifacts.outputs.cache-hit != 'true'
if: runner.os == 'Linux'
run: |
sudo apt-get -y update
sudo apt-get -y install libssl-dev libunwind-dev build-essential pkg-config
sudo apt-get -y install libssl-dev libunwind-dev build-essential pkg-config clang
- name: Clone onefuzz-samples
run: git clone https://github.com/microsoft/onefuzz-samples
- name: Prepare for agent integration tests
shell: bash
working-directory: ./onefuzz-samples/examples/simple-libfuzzer
run: |
make
mkdir -p ../../../src/agent/onefuzz-task/tests/targets/simple
cp fuzz.exe ../../../src/agent/onefuzz-task/tests/targets/simple/fuzz.exe
cp *.pdb ../../../src/agent/onefuzz-task/tests/targets/simple/ 2>/dev/null || :
- name: Install Rust Prereqs
if: steps.rust-build-cache.outputs.cache-hit != 'true' && steps.cache-agent-artifacts.outputs.cache-hit != 'true'
shell: bash
run: src/ci/rust-prereqs.sh
- run: src/ci/agent.sh
if: steps.cache-agent-artifacts.outputs.cache-hit != 'true'
shell: bash
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
Expand Down
4 changes: 2 additions & 2 deletions src/agent/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/agent/coverage/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ debugger = { path = "../debugger" }

[target.'cfg(target_os = "linux")'.dependencies]
nix = "0.26"
pete = "0.10"
pete = "0.12"
# For procfs, opt out of the `chrono` freature; it pulls in an old version
# of `time`. We do not use the methods that the `chrono` feature enables.
procfs = { version = "0.15.1", default-features = false, features = ["flate2"] }
Expand Down
11 changes: 8 additions & 3 deletions src/agent/coverage/src/record/linux/debugger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use std::collections::BTreeMap;
use std::io::Read;
use std::process::{Child, Command};
use std::time::Duration;

use anyhow::{bail, format_err, Result};
use debuggable_module::path::FilePath;
Expand Down Expand Up @@ -75,7 +76,11 @@ impl<'eh> Debugger<'eh> {
// These calls should also be unnecessary no-ops, but we really want to avoid any dangling
// or zombie child processes.
let _ = child.kill();
let _ = child.wait();

// We don't need to call child.wait() because of the following series of events:
// 1. pete, our ptracing library, spawns the child process with ptrace flags
// 2. rust stdlib set SIG_IGN as the SIGCHLD handler: https://github.com/rust-lang/rust/issues/110317
// 3. linux kernel automatically reaps pids when the above 2 hold: https://github.com/torvalds/linux/blob/44149752e9987a9eac5ad78e6d3a20934b5e018d/kernel/signal.c#L2089-L2110

let output = Output {
status,
Expand Down Expand Up @@ -198,8 +203,8 @@ impl DebuggerContext {
pub fn new() -> Self {
let breakpoints = Breakpoints::default();
let images = None;
let tracer = Ptracer::new();

let mut tracer = Ptracer::new();
*tracer.poll_delay_mut() = Duration::from_millis(1);
Self {
breakpoints,
images,
Expand Down
9 changes: 9 additions & 0 deletions src/agent/onefuzz-task/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ edition = "2021"
publish = false
license = "MIT"

[lib]
path = "src/lib.rs"
name = "onefuzz_task_lib"

[[bin]]
path = "src/main.rs"
name = "onefuzz-task"

[features]
integration_test = []

Expand Down Expand Up @@ -77,3 +85,4 @@ schemars = { version = "0.8.12", features = ["uuid1"] }

[dev-dependencies]
pretty_assertions = "1.4"
tempfile = "3.8"
9 changes: 9 additions & 0 deletions src/agent/onefuzz-task/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#[macro_use]
extern crate anyhow;
#[macro_use]
extern crate clap;
#[macro_use]
extern crate onefuzz_telemetry;

pub mod local;
pub mod tasks;
7 changes: 3 additions & 4 deletions src/agent/onefuzz-task/src/tasks/coverage/generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ impl CoverageTask {
}

if seen_inputs {
context.report_coverage_stats().await?;
context.report_coverage_stats().await;
context.save_and_sync_coverage().await?;
}

Expand Down Expand Up @@ -454,7 +454,7 @@ impl<'a> TaskContext<'a> {
Ok(count)
}

pub async fn report_coverage_stats(&self) -> Result<()> {
pub async fn report_coverage_stats(&self) {
use EventData::*;

let coverage = RwLock::read(&self.coverage).await;
Expand All @@ -471,7 +471,6 @@ impl<'a> TaskContext<'a> {
]),
)
.await;
Ok(())
}

pub async fn save_coverage(
Expand Down Expand Up @@ -565,7 +564,7 @@ impl<'a> Processor for TaskContext<'a> {
self.heartbeat.alive();

self.record_input(input).await?;
self.report_coverage_stats().await?;
self.report_coverage_stats().await;
self.save_and_sync_coverage().await?;

Ok(())
Expand Down
2 changes: 1 addition & 1 deletion src/agent/onefuzz-task/src/tasks/fuzz/libfuzzer/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ where
info!("config is: {:?}", self.config);

let fuzzer = L::from_config(&self.config).await?;
let mut running = fuzzer.fuzz(crash_dir.path(), local_inputs, &inputs).await?;
let mut running = fuzzer.fuzz(crash_dir.path(), local_inputs, &inputs)?;
tevoinea marked this conversation as resolved.
Show resolved Hide resolved

info!("child is: {:?}", running);

Expand Down
212 changes: 212 additions & 0 deletions src/agent/onefuzz-task/tests/template_integration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
use std::{
collections::HashSet,
ffi::OsStr,
path::{Path, PathBuf},
};

use tokio::fs;

use anyhow::Result;
use log::info;
use onefuzz_task_lib::local::template;
use std::time::Duration;
use tokio::time::timeout;

macro_rules! libfuzzer_tests {
($($name:ident: $value:expr,)*) => {
$(
#[tokio::test(flavor = "multi_thread")]
#[cfg_attr(not(feature = "integration_test"), ignore)]
async fn $name() {
let _ = env_logger::builder().is_test(true).try_init();
let (config, libfuzzer_target) = $value;
test_libfuzzer_basic_template(PathBuf::from(config), PathBuf::from(libfuzzer_target)).await;
}
)*
}
}

// This is the format for adding other templates/targets for this macro
// $TEST_NAME: ($RELATIVE_PATH_TO_TEMPLATE, $RELATIVE_PATH_TO_TARGET),
// Make sure that you place the target binary in CI
libfuzzer_tests! {
libfuzzer_basic: ("./tests/templates/libfuzzer_basic.yml", "./tests/targets/simple/fuzz.exe"),
}

async fn test_libfuzzer_basic_template(config: PathBuf, libfuzzer_target: PathBuf) {
assert_exists_and_is_file(&config).await;
assert_exists_and_is_file(&libfuzzer_target).await;

let test_layout = create_test_directory(&config, &libfuzzer_target)
.await
.expect("Failed to create test directory layout");

info!("Executed test from: {:?}", &test_layout.root);
info!("Running template for 1 minute...");
if let Ok(template_result) = timeout(
Duration::from_secs(60),
template::launch(&test_layout.config, None),
)
.await
{
// Something went wrong when running the template so lets print out the template to be helpful
info!("Printing config as it was used in the test:");
info!("{:?}", fs::read_to_string(&test_layout.config).await);
template_result.unwrap();
}

verify_test_layout_structure_did_not_change(&test_layout).await;
assert_directory_is_not_empty(&test_layout.inputs).await;
assert_directory_is_not_empty(&test_layout.crashes).await;
verify_coverage_dir(&test_layout.coverage).await;

let _ = fs::remove_dir_all(&test_layout.root).await;
}

async fn verify_test_layout_structure_did_not_change(test_layout: &TestLayout) {
assert_exists_and_is_dir(&test_layout.root).await;
assert_exists_and_is_file(&test_layout.config).await;
assert_exists_and_is_file(&test_layout.target_exe).await;
assert_exists_and_is_dir(&test_layout.crashdumps).await;
assert_exists_and_is_dir(&test_layout.coverage).await;
assert_exists_and_is_dir(&test_layout.crashes).await;
assert_exists_and_is_dir(&test_layout.inputs).await;
assert_exists_and_is_dir(&test_layout.regression_reports).await;
}

async fn verify_coverage_dir(coverage: &Path) {
warn_if_empty(coverage).await;
}

async fn assert_exists_and_is_dir(dir: &Path) {
assert!(dir.exists(), "Expected directory to exist. dir = {:?}", dir);
assert!(
dir.is_dir(),
"Expected path to be a directory. dir = {:?}",
dir
);
}

async fn warn_if_empty(dir: &Path) {
if dir_is_empty(dir).await {
println!("Expected directory to not be empty: {:?}", dir);
}
}

async fn assert_exists_and_is_file(file: &Path) {
assert!(file.exists(), "Expected file to exist. file = {:?}", file);
assert!(
file.is_file(),
"Expected path to be a file. file = {:?}",
file
);
}

async fn dir_is_empty(dir: &Path) -> bool {
fs::read_dir(dir)
.await
.unwrap_or_else(|_| panic!("Failed to list files in directory. dir = {:?}", dir))
.next_entry()
.await
.unwrap_or_else(|_| {
panic!(
"Failed to get next file in directory listing. dir = {:?}",
dir
)
})
.is_some()
}

async fn assert_directory_is_not_empty(dir: &Path) {
assert!(
dir_is_empty(dir).await,
"Expected directory to not be empty. dir = {:?}",
dir
);
}

async fn create_test_directory(config: &Path, target_exe: &Path) -> Result<TestLayout> {
let mut test_directory = PathBuf::from(".").join(uuid::Uuid::new_v4().to_string());
tevoinea marked this conversation as resolved.
Show resolved Hide resolved
fs::create_dir_all(&test_directory).await?;
test_directory = test_directory.canonicalize()?;

let mut inputs_directory = PathBuf::from(&test_directory).join("inputs");
fs::create_dir(&inputs_directory).await?;
inputs_directory = inputs_directory.canonicalize()?;

let mut crashes_directory = PathBuf::from(&test_directory).join("crashes");
fs::create_dir(&crashes_directory).await?;
crashes_directory = crashes_directory.canonicalize()?;

let mut crashdumps_directory = PathBuf::from(&test_directory).join("crashdumps");
fs::create_dir(&crashdumps_directory).await?;
crashdumps_directory = crashdumps_directory.canonicalize()?;

let mut coverage_directory = PathBuf::from(&test_directory).join("coverage");
fs::create_dir(&coverage_directory).await?;
coverage_directory = coverage_directory.canonicalize()?;

let mut regression_reports_directory =
PathBuf::from(&test_directory).join("regression_reports");
fs::create_dir(&regression_reports_directory).await?;
regression_reports_directory = regression_reports_directory.canonicalize()?;

let mut target_in_test = PathBuf::from(&test_directory).join("fuzz.exe");
fs::copy(target_exe, &target_in_test).await?;
target_in_test = target_in_test.canonicalize()?;

let mut interesting_extensions = HashSet::new();
interesting_extensions.insert(Some(OsStr::new("so")));
interesting_extensions.insert(Some(OsStr::new("pdb")));
let mut f = fs::read_dir(target_exe.parent().unwrap()).await?;
while let Ok(Some(f)) = f.next_entry().await {
if interesting_extensions.contains(&f.path().extension()) {
fs::copy(f.path(), PathBuf::from(&test_directory).join(f.file_name())).await?;
}
}

let mut config_data = fs::read_to_string(config).await?;

config_data = config_data
.replace("{TARGET_PATH}", target_in_test.to_str().unwrap())
.replace("{INPUTS_PATH}", inputs_directory.to_str().unwrap())
.replace("{CRASHES_PATH}", crashes_directory.to_str().unwrap())
.replace("{CRASHDUMPS_PATH}", crashdumps_directory.to_str().unwrap())
.replace("{COVERAGE_PATH}", coverage_directory.to_str().unwrap())
.replace(
"{REGRESSION_REPORTS_PATH}",
regression_reports_directory.to_str().unwrap(),
)
.replace("{TEST_DIRECTORY}", test_directory.to_str().unwrap());

let mut config_in_test =
PathBuf::from(&test_directory).join(config.file_name().unwrap_or_else(|| {
panic!("Failed to get file name for config. config = {:?}", config)
}));

fs::write(&config_in_test, &config_data).await?;
config_in_test = config_in_test.canonicalize()?;

Ok(TestLayout {
root: test_directory,
config: config_in_test,
target_exe: target_in_test,
inputs: inputs_directory,
crashes: crashes_directory,
crashdumps: crashdumps_directory,
coverage: coverage_directory,
regression_reports: regression_reports_directory,
})
}

#[derive(Debug)]
struct TestLayout {
root: PathBuf,
config: PathBuf,
target_exe: PathBuf,
inputs: PathBuf,
crashes: PathBuf,
crashdumps: PathBuf,
coverage: PathBuf,
regression_reports: PathBuf,
}
Loading