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

Commit d2d57a8

Browse files
authored
Starting integration tests (#3438)
* Starting integration tests * Ready to test the test * Parametrize test * checkpoint * Test works * Run integration tests in pipeline * fmt * . * -p * Install clang * quotes not required in yaml? * Hopefully fixed windows? * Try without killondrop * lint * small test * another test * Reuse core name * Wrong step * bump tokio? * Try with rust * make build happy * Bump pete and small clean up * Clean up and make the test pass regularly * fix broken ci * Lower the poll timeout * Set the timeout in a nicer way * fix windows * fmt * Include and copy pdbs * Ignore if pdb is missing on linux * It takes too long for coverage to be generated * lint * Only warn on missing coverage since it's flaky * Fix windows build * Small clean up * Try lowering the poll delay * fix coverage * PR comments * . * Apparently make is missing? * Remove aggressive step skipping in CI
1 parent 6e2cb14 commit d2d57a8

File tree

13 files changed

+310
-21
lines changed

13 files changed

+310
-21
lines changed

.github/workflows/ci.yml

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,16 +79,24 @@ jobs:
7979
key: ${{env.ACTIONS_CACHE_KEY_DATE}} # additional key for cache-busting
8080
workspaces: src/agent
8181
- name: Linux Prereqs
82-
if: runner.os == 'Linux' && steps.cache-agent-artifacts.outputs.cache-hit != 'true'
82+
if: runner.os == 'Linux'
8383
run: |
8484
sudo apt-get -y update
85-
sudo apt-get -y install libssl-dev libunwind-dev build-essential pkg-config
85+
sudo apt-get -y install libssl-dev libunwind-dev build-essential pkg-config clang
86+
- name: Clone onefuzz-samples
87+
run: git clone https://github.com/microsoft/onefuzz-samples
88+
- name: Prepare for agent integration tests
89+
shell: bash
90+
working-directory: ./onefuzz-samples/examples/simple-libfuzzer
91+
run: |
92+
make
93+
mkdir -p ../../../src/agent/onefuzz-task/tests/targets/simple
94+
cp fuzz.exe ../../../src/agent/onefuzz-task/tests/targets/simple/fuzz.exe
95+
cp *.pdb ../../../src/agent/onefuzz-task/tests/targets/simple/ 2>/dev/null || :
8696
- name: Install Rust Prereqs
87-
if: steps.rust-build-cache.outputs.cache-hit != 'true' && steps.cache-agent-artifacts.outputs.cache-hit != 'true'
8897
shell: bash
8998
run: src/ci/rust-prereqs.sh
9099
- run: src/ci/agent.sh
91-
if: steps.cache-agent-artifacts.outputs.cache-hit != 'true'
92100
shell: bash
93101
- name: Upload coverage to Codecov
94102
uses: codecov/codecov-action@v3

src/agent/Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/agent/coverage/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ debugger = { path = "../debugger" }
2626

2727
[target.'cfg(target_os = "linux")'.dependencies]
2828
nix = "0.26"
29-
pete = "0.10"
29+
pete = "0.12"
3030
# For procfs, opt out of the `chrono` freature; it pulls in an old version
3131
# of `time`. We do not use the methods that the `chrono` feature enables.
3232
procfs = { version = "0.15.1", default-features = false, features = ["flate2"] }

src/agent/coverage/src/record/linux/debugger.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
use std::collections::BTreeMap;
55
use std::io::Read;
66
use std::process::{Child, Command};
7+
use std::time::Duration;
78

89
use anyhow::{bail, format_err, Result};
910
use debuggable_module::path::FilePath;
@@ -75,7 +76,11 @@ impl<'eh> Debugger<'eh> {
7576
// These calls should also be unnecessary no-ops, but we really want to avoid any dangling
7677
// or zombie child processes.
7778
let _ = child.kill();
78-
let _ = child.wait();
79+
80+
// We don't need to call child.wait() because of the following series of events:
81+
// 1. pete, our ptracing library, spawns the child process with ptrace flags
82+
// 2. rust stdlib set SIG_IGN as the SIGCHLD handler: https://github.com/rust-lang/rust/issues/110317
83+
// 3. linux kernel automatically reaps pids when the above 2 hold: https://github.com/torvalds/linux/blob/44149752e9987a9eac5ad78e6d3a20934b5e018d/kernel/signal.c#L2089-L2110
7984

8085
let output = Output {
8186
status,
@@ -198,8 +203,8 @@ impl DebuggerContext {
198203
pub fn new() -> Self {
199204
let breakpoints = Breakpoints::default();
200205
let images = None;
201-
let tracer = Ptracer::new();
202-
206+
let mut tracer = Ptracer::new();
207+
*tracer.poll_delay_mut() = Duration::from_millis(1);
203208
Self {
204209
breakpoints,
205210
images,

src/agent/onefuzz-task/Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ edition = "2021"
66
publish = false
77
license = "MIT"
88

9+
[lib]
10+
path = "src/lib.rs"
11+
name = "onefuzz_task_lib"
12+
13+
[[bin]]
14+
path = "src/main.rs"
15+
name = "onefuzz-task"
16+
917
[features]
1018
integration_test = []
1119

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

7886
[dev-dependencies]
7987
pretty_assertions = "1.4"
88+
tempfile = "3.8"

src/agent/onefuzz-task/src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#[macro_use]
2+
extern crate anyhow;
3+
#[macro_use]
4+
extern crate clap;
5+
#[macro_use]
6+
extern crate onefuzz_telemetry;
7+
8+
pub mod local;
9+
pub mod tasks;

src/agent/onefuzz-task/src/tasks/coverage/generic.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ impl CoverageTask {
161161
}
162162

163163
if seen_inputs {
164-
context.report_coverage_stats().await?;
164+
context.report_coverage_stats().await;
165165
context.save_and_sync_coverage().await?;
166166
}
167167

@@ -454,7 +454,7 @@ impl<'a> TaskContext<'a> {
454454
Ok(count)
455455
}
456456

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

460460
let coverage = RwLock::read(&self.coverage).await;
@@ -471,7 +471,6 @@ impl<'a> TaskContext<'a> {
471471
]),
472472
)
473473
.await;
474-
Ok(())
475474
}
476475

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

567566
self.record_input(input).await?;
568-
self.report_coverage_stats().await?;
567+
self.report_coverage_stats().await;
569568
self.save_and_sync_coverage().await?;
570569

571570
Ok(())

src/agent/onefuzz-task/src/tasks/fuzz/libfuzzer/common.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ where
272272
info!("config is: {:?}", self.config);
273273

274274
let fuzzer = L::from_config(&self.config).await?;
275-
let mut running = fuzzer.fuzz(crash_dir.path(), local_inputs, &inputs).await?;
275+
let mut running = fuzzer.fuzz(crash_dir.path(), local_inputs, &inputs)?;
276276

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

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
use std::{
2+
collections::HashSet,
3+
ffi::OsStr,
4+
path::{Path, PathBuf},
5+
};
6+
7+
use tokio::fs;
8+
9+
use anyhow::Result;
10+
use log::info;
11+
use onefuzz_task_lib::local::template;
12+
use std::time::Duration;
13+
use tokio::time::timeout;
14+
15+
macro_rules! libfuzzer_tests {
16+
($($name:ident: $value:expr,)*) => {
17+
$(
18+
#[tokio::test(flavor = "multi_thread")]
19+
#[cfg_attr(not(feature = "integration_test"), ignore)]
20+
async fn $name() {
21+
let _ = env_logger::builder().is_test(true).try_init();
22+
let (config, libfuzzer_target) = $value;
23+
test_libfuzzer_basic_template(PathBuf::from(config), PathBuf::from(libfuzzer_target)).await;
24+
}
25+
)*
26+
}
27+
}
28+
29+
// This is the format for adding other templates/targets for this macro
30+
// $TEST_NAME: ($RELATIVE_PATH_TO_TEMPLATE, $RELATIVE_PATH_TO_TARGET),
31+
// Make sure that you place the target binary in CI
32+
libfuzzer_tests! {
33+
libfuzzer_basic: ("./tests/templates/libfuzzer_basic.yml", "./tests/targets/simple/fuzz.exe"),
34+
}
35+
36+
async fn test_libfuzzer_basic_template(config: PathBuf, libfuzzer_target: PathBuf) {
37+
assert_exists_and_is_file(&config).await;
38+
assert_exists_and_is_file(&libfuzzer_target).await;
39+
40+
let test_layout = create_test_directory(&config, &libfuzzer_target)
41+
.await
42+
.expect("Failed to create test directory layout");
43+
44+
info!("Executed test from: {:?}", &test_layout.root);
45+
info!("Running template for 1 minute...");
46+
if let Ok(template_result) = timeout(
47+
Duration::from_secs(60),
48+
template::launch(&test_layout.config, None),
49+
)
50+
.await
51+
{
52+
// Something went wrong when running the template so lets print out the template to be helpful
53+
info!("Printing config as it was used in the test:");
54+
info!("{:?}", fs::read_to_string(&test_layout.config).await);
55+
template_result.unwrap();
56+
}
57+
58+
verify_test_layout_structure_did_not_change(&test_layout).await;
59+
assert_directory_is_not_empty(&test_layout.inputs).await;
60+
assert_directory_is_not_empty(&test_layout.crashes).await;
61+
verify_coverage_dir(&test_layout.coverage).await;
62+
63+
let _ = fs::remove_dir_all(&test_layout.root).await;
64+
}
65+
66+
async fn verify_test_layout_structure_did_not_change(test_layout: &TestLayout) {
67+
assert_exists_and_is_dir(&test_layout.root).await;
68+
assert_exists_and_is_file(&test_layout.config).await;
69+
assert_exists_and_is_file(&test_layout.target_exe).await;
70+
assert_exists_and_is_dir(&test_layout.crashdumps).await;
71+
assert_exists_and_is_dir(&test_layout.coverage).await;
72+
assert_exists_and_is_dir(&test_layout.crashes).await;
73+
assert_exists_and_is_dir(&test_layout.inputs).await;
74+
assert_exists_and_is_dir(&test_layout.regression_reports).await;
75+
}
76+
77+
async fn verify_coverage_dir(coverage: &Path) {
78+
warn_if_empty(coverage).await;
79+
}
80+
81+
async fn assert_exists_and_is_dir(dir: &Path) {
82+
assert!(dir.exists(), "Expected directory to exist. dir = {:?}", dir);
83+
assert!(
84+
dir.is_dir(),
85+
"Expected path to be a directory. dir = {:?}",
86+
dir
87+
);
88+
}
89+
90+
async fn warn_if_empty(dir: &Path) {
91+
if dir_is_empty(dir).await {
92+
println!("Expected directory to not be empty: {:?}", dir);
93+
}
94+
}
95+
96+
async fn assert_exists_and_is_file(file: &Path) {
97+
assert!(file.exists(), "Expected file to exist. file = {:?}", file);
98+
assert!(
99+
file.is_file(),
100+
"Expected path to be a file. file = {:?}",
101+
file
102+
);
103+
}
104+
105+
async fn dir_is_empty(dir: &Path) -> bool {
106+
fs::read_dir(dir)
107+
.await
108+
.unwrap_or_else(|_| panic!("Failed to list files in directory. dir = {:?}", dir))
109+
.next_entry()
110+
.await
111+
.unwrap_or_else(|_| {
112+
panic!(
113+
"Failed to get next file in directory listing. dir = {:?}",
114+
dir
115+
)
116+
})
117+
.is_some()
118+
}
119+
120+
async fn assert_directory_is_not_empty(dir: &Path) {
121+
assert!(
122+
dir_is_empty(dir).await,
123+
"Expected directory to not be empty. dir = {:?}",
124+
dir
125+
);
126+
}
127+
128+
async fn create_test_directory(config: &Path, target_exe: &Path) -> Result<TestLayout> {
129+
let mut test_directory = PathBuf::from(".").join(uuid::Uuid::new_v4().to_string());
130+
fs::create_dir_all(&test_directory).await?;
131+
test_directory = test_directory.canonicalize()?;
132+
133+
let mut inputs_directory = PathBuf::from(&test_directory).join("inputs");
134+
fs::create_dir(&inputs_directory).await?;
135+
inputs_directory = inputs_directory.canonicalize()?;
136+
137+
let mut crashes_directory = PathBuf::from(&test_directory).join("crashes");
138+
fs::create_dir(&crashes_directory).await?;
139+
crashes_directory = crashes_directory.canonicalize()?;
140+
141+
let mut crashdumps_directory = PathBuf::from(&test_directory).join("crashdumps");
142+
fs::create_dir(&crashdumps_directory).await?;
143+
crashdumps_directory = crashdumps_directory.canonicalize()?;
144+
145+
let mut coverage_directory = PathBuf::from(&test_directory).join("coverage");
146+
fs::create_dir(&coverage_directory).await?;
147+
coverage_directory = coverage_directory.canonicalize()?;
148+
149+
let mut regression_reports_directory =
150+
PathBuf::from(&test_directory).join("regression_reports");
151+
fs::create_dir(&regression_reports_directory).await?;
152+
regression_reports_directory = regression_reports_directory.canonicalize()?;
153+
154+
let mut target_in_test = PathBuf::from(&test_directory).join("fuzz.exe");
155+
fs::copy(target_exe, &target_in_test).await?;
156+
target_in_test = target_in_test.canonicalize()?;
157+
158+
let mut interesting_extensions = HashSet::new();
159+
interesting_extensions.insert(Some(OsStr::new("so")));
160+
interesting_extensions.insert(Some(OsStr::new("pdb")));
161+
let mut f = fs::read_dir(target_exe.parent().unwrap()).await?;
162+
while let Ok(Some(f)) = f.next_entry().await {
163+
if interesting_extensions.contains(&f.path().extension()) {
164+
fs::copy(f.path(), PathBuf::from(&test_directory).join(f.file_name())).await?;
165+
}
166+
}
167+
168+
let mut config_data = fs::read_to_string(config).await?;
169+
170+
config_data = config_data
171+
.replace("{TARGET_PATH}", target_in_test.to_str().unwrap())
172+
.replace("{INPUTS_PATH}", inputs_directory.to_str().unwrap())
173+
.replace("{CRASHES_PATH}", crashes_directory.to_str().unwrap())
174+
.replace("{CRASHDUMPS_PATH}", crashdumps_directory.to_str().unwrap())
175+
.replace("{COVERAGE_PATH}", coverage_directory.to_str().unwrap())
176+
.replace(
177+
"{REGRESSION_REPORTS_PATH}",
178+
regression_reports_directory.to_str().unwrap(),
179+
)
180+
.replace("{TEST_DIRECTORY}", test_directory.to_str().unwrap());
181+
182+
let mut config_in_test =
183+
PathBuf::from(&test_directory).join(config.file_name().unwrap_or_else(|| {
184+
panic!("Failed to get file name for config. config = {:?}", config)
185+
}));
186+
187+
fs::write(&config_in_test, &config_data).await?;
188+
config_in_test = config_in_test.canonicalize()?;
189+
190+
Ok(TestLayout {
191+
root: test_directory,
192+
config: config_in_test,
193+
target_exe: target_in_test,
194+
inputs: inputs_directory,
195+
crashes: crashes_directory,
196+
crashdumps: crashdumps_directory,
197+
coverage: coverage_directory,
198+
regression_reports: regression_reports_directory,
199+
})
200+
}
201+
202+
#[derive(Debug)]
203+
struct TestLayout {
204+
root: PathBuf,
205+
config: PathBuf,
206+
target_exe: PathBuf,
207+
inputs: PathBuf,
208+
crashes: PathBuf,
209+
crashdumps: PathBuf,
210+
coverage: PathBuf,
211+
regression_reports: PathBuf,
212+
}

0 commit comments

Comments
 (0)