Skip to content

Commit 5d02d03

Browse files
authored
Add fuzzing target to fuzz return type and parameters (#301)
* Adds fuzzing target to fuzz the ParameterValue and ReturnType of both guest and host function calls. Rename existing target to host_print. Move fuzz directory to root directory. Signed-off-by: Ludvig Liljenberg <[email protected]> * Fix leaky stack discovered by fuzzing host functions Signed-off-by: Ludvig Liljenberg <[email protected]> --------- Signed-off-by: Ludvig Liljenberg <[email protected]>
1 parent 8efebb6 commit 5d02d03

File tree

23 files changed

+252
-116
lines changed

23 files changed

+252
-116
lines changed

.github/workflows/Fuzzing.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ permissions:
1010
contents: read
1111

1212
jobs:
13-
1413
fuzzing:
1514
uses: ./.github/workflows/dep_fuzzing.yml
1615
with:
16+
targets: '["host_print", "guest_call", "host_call"]' # Pass as a JSON array
1717
max_total_time: 18000 # 5 hours in seconds
1818
secrets: inherit

.github/workflows/ValidatePullRequest.yml

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ jobs:
5454
- docs-pr
5555
uses: ./.github/workflows/dep_fuzzing.yml
5656
with:
57+
targets: '["host_print", "guest_call", "host_call"]' # Pass as a JSON array
5758
max_total_time: 300 # 5 minutes in seconds
5859
docs_only: ${{needs.docs-pr.outputs.docs-only}}
5960
secrets: inherit

.github/workflows/dep_fuzzing.yml

+9-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ on:
77
description: Maximum total time for the fuzz run in seconds
88
required: true
99
type: number
10+
targets:
11+
description: Fuzz targets to run
12+
required: true
13+
type: string
1014
docs_only:
1115
description: Skip fuzzing if docs only
1216
required: false
@@ -21,6 +25,9 @@ jobs:
2125
fuzz:
2226
if: ${{ inputs.docs_only == 'false' }}
2327
runs-on: [ self-hosted, Linux, X64, "1ES.Pool=hld-kvm-amd" ]
28+
strategy:
29+
matrix:
30+
target: ${{ fromJson(inputs.targets) }}
2431
steps:
2532
- name: Checkout code
2633
uses: actions/checkout@v4
@@ -44,12 +51,12 @@ jobs:
4451
run: cargo install cargo-fuzz
4552

4653
- name: Run Fuzzing
47-
run: cargo +nightly fuzz run --release fuzz_target_1 -- -max_total_time=300
54+
run: just fuzz-timed ${{ matrix.target }} ${{ inputs.max_total_time }}
4855
working-directory: src/hyperlight_host
4956

5057
- name: Upload Crash Artifacts
5158
if: failure() # This ensures artifacts are only uploaded on failure
5259
uses: actions/upload-artifact@v4
5360
with:
5461
name: fuzz-crash-artifacts
55-
path: src/hyperlight_host/fuzz/artifacts/
62+
path: fuzz/artifacts/

Cargo.lock

+24-9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ members = [
1111
"src/hyperlight_host",
1212
"src/hyperlight_guest_capi",
1313
"src/hyperlight_testing",
14-
"src/hyperlight_host/fuzz",
14+
"fuzz",
1515
]
1616
# Because hyperlight-guest has custom linker flags,
1717
# we exclude it from the default-members list

Justfile

+7-4
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,11 @@ bench target=default-target features="":
190190
cargo bench --profile={{ if target == "debug" { "dev" } else { target } }} {{ if features =="" {''} else { "--features " + features } }} -- --verbose
191191

192192
# FUZZING
193-
fuzz:
194-
cd src/hyperlight_host && cargo +nightly fuzz run fuzz_target_1
195193

196-
fuzz-timed:
197-
cd src/hyperlight_host && cargo +nightly fuzz run fuzz_target_1 -- -max_total_time=300
194+
# Fuzzes the given target
195+
fuzz fuzz-target:
196+
cargo +nightly fuzz run {{ fuzz-target }} --release
197+
198+
# Fuzzes the given target. Stops after `max_time` seconds
199+
fuzz-timed fuzz-target max_time:
200+
cargo +nightly fuzz run {{ fuzz-target }} --release -- -max_total_time={{ max_time }}
File renamed without changes.

fuzz/Cargo.toml

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
[package]
2+
name = "hyperlight-fuzz"
3+
version = "0.0.0"
4+
publish = false
5+
edition = { workspace = true }
6+
7+
[package.metadata]
8+
cargo-fuzz = true
9+
10+
[dependencies]
11+
libfuzzer-sys = "0.4"
12+
hyperlight-testing = { workspace = true }
13+
hyperlight-host = { workspace = true, default-features = true, features = ["fuzzing"]}
14+
15+
[[bin]]
16+
name = "host_print"
17+
path = "fuzz_targets/host_print.rs"
18+
test = false
19+
doc = false
20+
bench = false
21+
22+
[[bin]]
23+
name = "guest_call"
24+
path = "fuzz_targets/guest_call.rs"
25+
test = false
26+
doc = false
27+
bench = false
28+
29+
[[bin]]
30+
name = "host_call"
31+
path = "fuzz_targets/host_call.rs"
32+
test = false
33+
doc = false
34+
bench = false

src/hyperlight_host/fuzz/README.md fuzz/README.md

+5-9
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,19 @@ This directory contains the fuzzing infrastructure for Hyperlight. We use `cargo
44

55
You can run the fuzzers with:
66
```sh
7-
cargo +nightly-2023-11-28-x86_64-unknown-linux-gnu fuzz run --release <fuzzer_name>
7+
just fuzz <fuzz_target>
88
```
9-
10-
> Note: Because nightly toolchains are not stable, we pin the nightly version to `2023-11-28`. To install this toolchain, run:
11-
> ```sh
12-
> rustup toolchain install nightly-2023-11-28-x86_64-unknown-linux-gnu
13-
> ```
9+
which evaluates to the following command `cargo +nightly fuzz run host_print --release`. We use the release profile to make sure the release-optimized guest is used. The default fuzz profile which is release+debugsymbols would cause our debug guests to be loaded, since we currently determine which test guest to load based on whether debug symbols are present.
1410

1511
As per Microsoft's Offensive Research & Security Engineering (MORSE) team, all host exposed functions that receive or interact with guest data must be continuously fuzzed for, at least, 500 million fuzz test cases without any crashes. Because `cargo-fuzz` doesn't support setting a maximum number of iterations; instead, we use the `--max_total_time` flag to set a maximum time to run the fuzzer. We have a GitHub action (acting like a CRON job) that runs the fuzzers for 24 hours every week.
1612

17-
Currently, we only fuzz the `PrintOutput` function. We plan to add more fuzzers in the future.
13+
Currently, we fuzz the parameters and return type to a hardcoded `PrintOutput` guest function, and the `HostPrint` host function. We plan to add more fuzzers in the future.
1814

1915
## On Failure
2016

2117
If you encounter a failure, you can re-run an entire seed (i.e., group of inputs) with:
2218
```sh
23-
cargo +nightly-2023-11-28-x86_64-unknown-linux-gnu fuzz run --release <fuzzer_name> -- -seed=<seed-number>
19+
cargo +nightly fuzz run <fuzzer_target> -- -seed=<seed-number>
2420
```
2521

2622
The seed number can be seed in a specific run, like:
@@ -29,5 +25,5 @@ The seed number can be seed in a specific run, like:
2925
Or, if repro-ing a failure from CI, you can download the artifact from the fuzzing run, and run it like:
3026

3127
```sh
32-
cargo +nightly-2023-11-28-x86_64-unknown-linux-gnu fuzz run --release -O <fuzzer_name> <fuzzer-input (e.g., fuzz/artifacts/fuzz_target_1/crash-93c522e64ee822034972ccf7026d3a8f20d5267c>
28+
cargo +nightly fuzz run -O <fuzzer_target> <fuzzer-input (e.g., fuzz/artifacts/fuzz_target_1/crash-93c522e64ee822034972ccf7026d3a8f20d5267c>
3329
```
File renamed without changes.

src/hyperlight_host/fuzz/fuzz_targets/fuzz_target_1.rs fuzz/fuzz_targets/guest_call.rs

+23-23
Original file line numberDiff line numberDiff line change
@@ -16,35 +16,35 @@ limitations under the License.
1616

1717
#![no_main]
1818

19-
use hyperlight_host::func::{ParameterValue, ReturnType, ReturnValue};
19+
use std::sync::{Mutex, OnceLock};
20+
21+
use hyperlight_host::func::{ParameterValue, ReturnType};
2022
use hyperlight_host::sandbox::uninitialized::GuestBinary;
2123
use hyperlight_host::sandbox_state::sandbox::EvolvableSandbox;
2224
use hyperlight_host::sandbox_state::transition::Noop;
2325
use hyperlight_host::{MultiUseSandbox, UninitializedSandbox};
2426
use hyperlight_testing::simple_guest_as_string;
2527
use libfuzzer_sys::fuzz_target;
26-
27-
fuzz_target!(|data: &[u8]| {
28-
let u_sbox = UninitializedSandbox::new(
29-
GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
30-
None,
31-
None,
32-
None,
33-
)
34-
.unwrap();
35-
36-
let mu_sbox: MultiUseSandbox = u_sbox.evolve(Noop::default()).unwrap();
37-
38-
let msg = String::from_utf8_lossy(data).to_string();
39-
let len = msg.len() as i32;
40-
let mut ctx = mu_sbox.new_call_context();
41-
let result = ctx
42-
.call(
43-
"PrintOutput",
44-
ReturnType::Int,
45-
Some(vec![ParameterValue::String(msg.clone())]),
28+
static SANDBOX: OnceLock<Mutex<MultiUseSandbox>> = OnceLock::new();
29+
30+
// This fuzz target tests all combinations of ReturnType and Parameters for `call_guest_function_by_name`.
31+
// For fuzzing efficiency, we create one Sandbox and reuse it for all fuzzing iterations.
32+
fuzz_target!(
33+
init: {
34+
let u_sbox = UninitializedSandbox::new(
35+
GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
36+
None,
37+
None,
38+
None,
4639
)
4740
.unwrap();
4841

49-
assert_eq!(result, ReturnValue::Int(len));
50-
});
42+
let mu_sbox: MultiUseSandbox = u_sbox.evolve(Noop::default()).unwrap();
43+
SANDBOX.set(Mutex::new(mu_sbox)).unwrap();
44+
},
45+
46+
|data: (ReturnType, Option<Vec<ParameterValue>>)| {
47+
let mut sandbox = SANDBOX.get().unwrap().lock().unwrap();
48+
let _ = sandbox.call_guest_function_by_name("PrintOutput", data.0, data.1);
49+
}
50+
);

fuzz/fuzz_targets/host_call.rs

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
Copyright 2024 The Hyperlight Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
#![no_main]
18+
19+
use std::sync::{Mutex, OnceLock};
20+
21+
use hyperlight_host::func::{ParameterValue, ReturnType};
22+
use hyperlight_host::sandbox::uninitialized::GuestBinary;
23+
use hyperlight_host::sandbox::SandboxConfiguration;
24+
use hyperlight_host::sandbox_state::sandbox::EvolvableSandbox;
25+
use hyperlight_host::sandbox_state::transition::Noop;
26+
use hyperlight_host::{HyperlightError, MultiUseSandbox, UninitializedSandbox};
27+
use hyperlight_testing::simple_guest_as_string;
28+
use libfuzzer_sys::fuzz_target;
29+
static SANDBOX: OnceLock<Mutex<MultiUseSandbox>> = OnceLock::new();
30+
31+
// This fuzz target tests all combinations of ReturnType and Parameters for `call_guest_function_by_name`.
32+
// For fuzzing efficiency, we create one Sandbox and reuse it for all fuzzing iterations.
33+
fuzz_target!(
34+
init: {
35+
let u_sbox = UninitializedSandbox::new(
36+
GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
37+
None,
38+
None,
39+
None,
40+
)
41+
.unwrap();
42+
43+
let mu_sbox: MultiUseSandbox = u_sbox.evolve(Noop::default()).unwrap();
44+
SANDBOX.set(Mutex::new(mu_sbox)).unwrap();
45+
},
46+
47+
|data: (String, ReturnType, Vec<ParameterValue>)| {
48+
let (host_func_name, host_func_return, mut host_func_params) = data;
49+
let mut sandbox = SANDBOX.get().unwrap().lock().unwrap();
50+
host_func_params.insert(0, ParameterValue::String(host_func_name));
51+
match sandbox.call_guest_function_by_name("FuzzHostFunc", host_func_return, Some(host_func_params)) {
52+
Err(HyperlightError::GuestAborted(_, message)) if !message.contains("Host Function Not Found") => {
53+
// We don't allow GuestAborted errors, except for the "Host Function Not Found" case
54+
panic!("Guest Aborted: {}", message);
55+
}
56+
_ => {}
57+
}
58+
}
59+
);

fuzz/fuzz_targets/host_print.rs

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#![no_main]
2+
3+
use std::sync::{Mutex, OnceLock};
4+
5+
use hyperlight_host::func::{ParameterValue, ReturnType, ReturnValue};
6+
use hyperlight_host::sandbox::uninitialized::GuestBinary;
7+
use hyperlight_host::sandbox_state::sandbox::EvolvableSandbox;
8+
use hyperlight_host::sandbox_state::transition::Noop;
9+
use hyperlight_host::{MultiUseSandbox, UninitializedSandbox};
10+
use hyperlight_testing::simple_guest_as_string;
11+
use libfuzzer_sys::{fuzz_target, Corpus};
12+
13+
static SANDBOX: OnceLock<Mutex<MultiUseSandbox>> = OnceLock::new();
14+
15+
// This fuzz target is used to test the HostPrint host function. We generate
16+
// an arbitrary ParameterValue::String, which is passed to the guest, which passes
17+
// it without modification to the host function.
18+
// For fuzzing efficiency, we create one Sandbox and reuse it for all fuzzing iterations.
19+
fuzz_target!(
20+
init: {
21+
let u_sbox = UninitializedSandbox::new(
22+
GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
23+
None,
24+
None,
25+
None,
26+
)
27+
.unwrap();
28+
29+
let mu_sbox: MultiUseSandbox = u_sbox.evolve(Noop::default()).unwrap();
30+
SANDBOX.set(Mutex::new(mu_sbox)).unwrap();
31+
},
32+
33+
|data: ParameterValue| -> Corpus {
34+
// only interested in String types
35+
if !matches!(data, ParameterValue::String(_)) {
36+
return Corpus::Reject;
37+
}
38+
39+
let mut sandbox = SANDBOX.get().unwrap().lock().unwrap();
40+
let res = sandbox.call_guest_function_by_name(
41+
"PrintOutput",
42+
ReturnType::Int,
43+
Some(vec![data.clone()]),
44+
);
45+
match res {
46+
Ok(ReturnValue::Int(len)) => assert!(len >= 0),
47+
_ => panic!("Unexpected return value: {:?}", res),
48+
}
49+
50+
Corpus::Keep
51+
});

rust-toolchain.toml

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,2 @@
11
[toolchain]
2-
channel = "1.81.0"
3-
# if you update this, don't forget to change the pinned version
4-
# of nightly we use in the fuzzing workflow.
2+
channel = "1.81.0"

0 commit comments

Comments
 (0)