From 2e2c89338bfb484ba86ea1dff622335dd339dcab Mon Sep 17 00:00:00 2001 From: Ludvig Liljenberg Date: Fri, 28 Feb 2025 09:57:03 -0800 Subject: [PATCH] Adds fuzzing target to fuzz the ParameterValue and ReturnType. Rename existing target to host_print. Move fuzz directory to root directory. Signed-off-by: Ludvig Liljenberg --- .github/workflows/Fuzzing.yml | 2 +- .github/workflows/ValidatePullRequest.yml | 1 + .github/workflows/dep_fuzzing.yml | 11 +++- Cargo.lock | 33 ++++++++---- Cargo.toml | 2 +- Justfile | 11 ++-- {src/hyperlight_host/fuzz => fuzz}/.gitignore | 0 fuzz/Cargo.toml | 27 ++++++++++ {src/hyperlight_host/fuzz => fuzz}/README.md | 14 ++--- .../fuzz => fuzz}/doc-assets/image.png | Bin .../fuzz_targets/guest_call.rs | 46 ++++++++-------- fuzz/fuzz_targets/host_print.rs | 51 ++++++++++++++++++ rust-toolchain.toml | 4 +- src/hyperlight_common/Cargo.toml | 2 + .../src/flatbuffer_wrappers/function_types.rs | 2 + src/hyperlight_common/src/lib.rs | 3 +- src/hyperlight_host/Cargo.toml | 1 + src/hyperlight_host/fuzz/Cargo.toml | 20 ------- 18 files changed, 157 insertions(+), 73 deletions(-) rename {src/hyperlight_host/fuzz => fuzz}/.gitignore (100%) create mode 100644 fuzz/Cargo.toml rename {src/hyperlight_host/fuzz => fuzz}/README.md (61%) rename {src/hyperlight_host/fuzz => fuzz}/doc-assets/image.png (100%) rename src/hyperlight_host/fuzz/fuzz_targets/fuzz_target_1.rs => fuzz/fuzz_targets/guest_call.rs (50%) create mode 100644 fuzz/fuzz_targets/host_print.rs delete mode 100644 src/hyperlight_host/fuzz/Cargo.toml diff --git a/.github/workflows/Fuzzing.yml b/.github/workflows/Fuzzing.yml index c1cde4463..c73e68338 100644 --- a/.github/workflows/Fuzzing.yml +++ b/.github/workflows/Fuzzing.yml @@ -10,9 +10,9 @@ permissions: contents: read jobs: - fuzzing: uses: ./.github/workflows/dep_fuzzing.yml with: + targets: '["host_print", "guest_call"]' # Pass as a JSON array max_total_time: 18000 # 5 hours in seconds secrets: inherit \ No newline at end of file diff --git a/.github/workflows/ValidatePullRequest.yml b/.github/workflows/ValidatePullRequest.yml index 0380813fa..8a77d552f 100644 --- a/.github/workflows/ValidatePullRequest.yml +++ b/.github/workflows/ValidatePullRequest.yml @@ -54,6 +54,7 @@ jobs: - docs-pr uses: ./.github/workflows/dep_fuzzing.yml with: + targets: '["host_print", "guest_call"]' # Pass as a JSON array max_total_time: 300 # 5 minutes in seconds docs_only: ${{needs.docs-pr.outputs.docs-only}} secrets: inherit diff --git a/.github/workflows/dep_fuzzing.yml b/.github/workflows/dep_fuzzing.yml index 742754501..d9f5a7562 100644 --- a/.github/workflows/dep_fuzzing.yml +++ b/.github/workflows/dep_fuzzing.yml @@ -7,6 +7,10 @@ on: description: Maximum total time for the fuzz run in seconds required: true type: number + targets: + description: Fuzz targets to run + required: true + type: string docs_only: description: Skip fuzzing if docs only required: false @@ -21,6 +25,9 @@ jobs: fuzz: if: ${{ inputs.docs_only == 'false' }} runs-on: [ self-hosted, Linux, X64, "1ES.Pool=hld-kvm-amd" ] + strategy: + matrix: + target: ${{ fromJson(inputs.targets) }} steps: - name: Checkout code uses: actions/checkout@v4 @@ -44,7 +51,7 @@ jobs: run: cargo install cargo-fuzz - name: Run Fuzzing - run: cargo +nightly fuzz run --release fuzz_target_1 -- -max_total_time=300 + run: just fuzz-timed ${{ matrix.target }} ${{ inputs.max_total_time }} working-directory: src/hyperlight_host - name: Upload Crash Artifacts @@ -52,4 +59,4 @@ jobs: uses: actions/upload-artifact@v4 with: name: fuzz-crash-artifacts - path: src/hyperlight_host/fuzz/artifacts/ + path: fuzz/artifacts/ diff --git a/Cargo.lock b/Cargo.lock index a9a778cd0..68ba78612 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,6 +107,9 @@ name = "arbitrary" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] [[package]] name = "async-stream" @@ -589,6 +592,17 @@ dependencies = [ "typenum", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "digest" version = "0.10.7" @@ -1092,6 +1106,7 @@ name = "hyperlight-common" version = "0.2.0" dependencies = [ "anyhow", + "arbitrary", "flatbuffers", "hyperlight-testing", "log", @@ -1099,6 +1114,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "hyperlight-fuzz" +version = "0.0.0" +dependencies = [ + "hyperlight-host", + "hyperlight-testing", + "libfuzzer-sys", +] + [[package]] name = "hyperlight-guest" version = "0.2.0" @@ -1209,15 +1233,6 @@ dependencies = [ "log", ] -[[package]] -name = "hyperlight_host-fuzz" -version = "0.0.0" -dependencies = [ - "hyperlight-host", - "hyperlight-testing", - "libfuzzer-sys", -] - [[package]] name = "iana-time-zone" version = "0.1.61" diff --git a/Cargo.toml b/Cargo.toml index dae97c452..3fa824ad8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ members = [ "src/hyperlight_host", "src/hyperlight_guest_capi", "src/hyperlight_testing", - "src/hyperlight_host/fuzz", + "fuzz", ] # Because hyperlight-guest has custom linker flags, # we exclude it from the default-members list diff --git a/Justfile b/Justfile index c91c08780..ed43a2d58 100644 --- a/Justfile +++ b/Justfile @@ -190,8 +190,11 @@ bench target=default-target features="": cargo bench --profile={{ if target == "debug" { "dev" } else { target } }} {{ if features =="" {''} else { "--features " + features } }} -- --verbose # FUZZING -fuzz: - cd src/hyperlight_host && cargo +nightly fuzz run fuzz_target_1 -fuzz-timed: - cd src/hyperlight_host && cargo +nightly fuzz run fuzz_target_1 -- -max_total_time=300 +# Fuzzes the given target +fuzz fuzz-target: + cargo +nightly fuzz run {{ fuzz-target }} --release + +# Fuzzes the given target. Stops after `max_time` seconds +fuzz-timed fuzz-target max_time: + cargo +nightly fuzz run {{ fuzz-target }} --release -- -max_total_time={{ max_time }} diff --git a/src/hyperlight_host/fuzz/.gitignore b/fuzz/.gitignore similarity index 100% rename from src/hyperlight_host/fuzz/.gitignore rename to fuzz/.gitignore diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 000000000..e35089618 --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "hyperlight-fuzz" +version = "0.0.0" +publish = false +edition = { workspace = true } + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +hyperlight-testing = { workspace = true } +hyperlight-host = { workspace = true, default-features = true, features = ["fuzzing"]} + +[[bin]] +name = "host_print" +path = "fuzz_targets/host_print.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "guest_call" +path = "fuzz_targets/guest_call.rs" +test = false +doc = false +bench = false diff --git a/src/hyperlight_host/fuzz/README.md b/fuzz/README.md similarity index 61% rename from src/hyperlight_host/fuzz/README.md rename to fuzz/README.md index 8e7fcf2bb..f18bc3b8c 100644 --- a/src/hyperlight_host/fuzz/README.md +++ b/fuzz/README.md @@ -4,23 +4,19 @@ This directory contains the fuzzing infrastructure for Hyperlight. We use `cargo You can run the fuzzers with: ```sh -cargo +nightly-2023-11-28-x86_64-unknown-linux-gnu fuzz run --release +just fuzz ``` - -> Note: Because nightly toolchains are not stable, we pin the nightly version to `2023-11-28`. To install this toolchain, run: -> ```sh -> rustup toolchain install nightly-2023-11-28-x86_64-unknown-linux-gnu -> ``` +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. 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. -Currently, we only fuzz the `PrintOutput` function. We plan to add more fuzzers in the future. +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. ## On Failure If you encounter a failure, you can re-run an entire seed (i.e., group of inputs) with: ```sh -cargo +nightly-2023-11-28-x86_64-unknown-linux-gnu fuzz run --release -- -seed= +cargo +nightly fuzz run -- -seed= ``` 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: Or, if repro-ing a failure from CI, you can download the artifact from the fuzzing run, and run it like: ```sh -cargo +nightly-2023-11-28-x86_64-unknown-linux-gnu fuzz run --release -O +cargo +nightly fuzz run -O ``` \ No newline at end of file diff --git a/src/hyperlight_host/fuzz/doc-assets/image.png b/fuzz/doc-assets/image.png similarity index 100% rename from src/hyperlight_host/fuzz/doc-assets/image.png rename to fuzz/doc-assets/image.png diff --git a/src/hyperlight_host/fuzz/fuzz_targets/fuzz_target_1.rs b/fuzz/fuzz_targets/guest_call.rs similarity index 50% rename from src/hyperlight_host/fuzz/fuzz_targets/fuzz_target_1.rs rename to fuzz/fuzz_targets/guest_call.rs index e500c6276..12142a660 100644 --- a/src/hyperlight_host/fuzz/fuzz_targets/fuzz_target_1.rs +++ b/fuzz/fuzz_targets/guest_call.rs @@ -16,35 +16,35 @@ limitations under the License. #![no_main] -use hyperlight_host::func::{ParameterValue, ReturnType, ReturnValue}; +use std::sync::{Mutex, OnceLock}; + +use hyperlight_host::func::{ParameterValue, ReturnType}; use hyperlight_host::sandbox::uninitialized::GuestBinary; use hyperlight_host::sandbox_state::sandbox::EvolvableSandbox; use hyperlight_host::sandbox_state::transition::Noop; use hyperlight_host::{MultiUseSandbox, UninitializedSandbox}; use hyperlight_testing::simple_guest_as_string; use libfuzzer_sys::fuzz_target; - -fuzz_target!(|data: &[u8]| { - let u_sbox = UninitializedSandbox::new( - GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), - None, - None, - None, - ) - .unwrap(); - - let mu_sbox: MultiUseSandbox = u_sbox.evolve(Noop::default()).unwrap(); - - let msg = String::from_utf8_lossy(data).to_string(); - let len = msg.len() as i32; - let mut ctx = mu_sbox.new_call_context(); - let result = ctx - .call( - "PrintOutput", - ReturnType::Int, - Some(vec![ParameterValue::String(msg.clone())]), +static SANDBOX: OnceLock> = OnceLock::new(); + +// This fuzz target tests all combinations of ReturnType and Parameters for `call_guest_function_by_name`. +// For fuzzing efficiency, we create one Sandbox and reuse it for all fuzzing iterations. +fuzz_target!( + init: { + let u_sbox = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + None, + None, + None, ) .unwrap(); - assert_eq!(result, ReturnValue::Int(len)); -}); + let mu_sbox: MultiUseSandbox = u_sbox.evolve(Noop::default()).unwrap(); + SANDBOX.set(Mutex::new(mu_sbox)).unwrap(); + }, + + |data: (ReturnType, Option>)| { + let mut sandbox = SANDBOX.get().unwrap().lock().unwrap(); + let _ = sandbox.call_guest_function_by_name("PrintOutput", data.0, data.1); + } +); diff --git a/fuzz/fuzz_targets/host_print.rs b/fuzz/fuzz_targets/host_print.rs new file mode 100644 index 000000000..c44124d2f --- /dev/null +++ b/fuzz/fuzz_targets/host_print.rs @@ -0,0 +1,51 @@ +#![no_main] + +use std::sync::{Mutex, OnceLock}; + +use hyperlight_host::func::{ParameterValue, ReturnType, ReturnValue}; +use hyperlight_host::sandbox::uninitialized::GuestBinary; +use hyperlight_host::sandbox_state::sandbox::EvolvableSandbox; +use hyperlight_host::sandbox_state::transition::Noop; +use hyperlight_host::{MultiUseSandbox, UninitializedSandbox}; +use hyperlight_testing::simple_guest_as_string; +use libfuzzer_sys::{fuzz_target, Corpus}; + +static SANDBOX: OnceLock> = OnceLock::new(); + +// This fuzz target is used to test the HostPrint host function. We generate +// an arbitrary ParameterValue::String, which is passed to the guest, which passes +// it without modification to the host function. +// For fuzzing efficiency, we create one Sandbox and reuse it for all fuzzing iterations. +fuzz_target!( + init: { + let u_sbox = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + None, + None, + None, + ) + .unwrap(); + + let mu_sbox: MultiUseSandbox = u_sbox.evolve(Noop::default()).unwrap(); + SANDBOX.set(Mutex::new(mu_sbox)).unwrap(); + }, + + |data: ParameterValue| -> Corpus { + // only interested in String types + if !matches!(data, ParameterValue::String(_)) { + return Corpus::Reject; + } + + let mut sandbox = SANDBOX.get().unwrap().lock().unwrap(); + let res = sandbox.call_guest_function_by_name( + "PrintOutput", + ReturnType::Int, + Some(vec![data.clone()]), + ); + match res { + Ok(ReturnValue::Int(len)) => assert!(len >= 0), + _ => panic!("Unexpected return value: {:?}", res), + } + + Corpus::Keep +}); diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 11ac5e51b..3cf2110e7 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,2 @@ [toolchain] -channel = "1.81.0" -# if you update this, don't forget to change the pinned version -# of nightly we use in the fuzzing workflow. \ No newline at end of file +channel = "1.81.0" \ No newline at end of file diff --git a/src/hyperlight_common/Cargo.toml b/src/hyperlight_common/Cargo.toml index 3fb04a1a5..7a0801c25 100644 --- a/src/hyperlight_common/Cargo.toml +++ b/src/hyperlight_common/Cargo.toml @@ -20,9 +20,11 @@ anyhow = { version = "1.0.96", default-features = false } log = "0.4.26" tracing = { version = "0.1.41", optional = true } strum = {version = "0.27", default-features = false, features = ["derive"]} +arbitrary = {version = "1.4.1", optional = true, features = ["derive"]} [features] default = ["tracing"] +fuzzing = ["dep:arbitrary"] [dev-dependencies] hyperlight-testing = { workspace = true } diff --git a/src/hyperlight_common/src/flatbuffer_wrappers/function_types.rs b/src/hyperlight_common/src/flatbuffer_wrappers/function_types.rs index 6eda6b267..42a26c81c 100644 --- a/src/hyperlight_common/src/flatbuffer_wrappers/function_types.rs +++ b/src/hyperlight_common/src/flatbuffer_wrappers/function_types.rs @@ -32,6 +32,7 @@ use crate::flatbuffers::hyperlight::generated::{ }; /// Supported parameter types with values for function calling. +#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))] #[derive(Debug, Clone, PartialEq)] pub enum ParameterValue { /// i32 @@ -104,6 +105,7 @@ pub enum ReturnValue { } /// Supported return types from function calling. +#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))] #[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] #[repr(C)] pub enum ReturnType { diff --git a/src/hyperlight_common/src/lib.rs b/src/hyperlight_common/src/lib.rs index a291508ba..e2e75cc9e 100644 --- a/src/hyperlight_common/src/lib.rs +++ b/src/hyperlight_common/src/lib.rs @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -#![no_std] +// We use Arbitrary during fuzzing, which requires std +#![cfg_attr(not(feature = "fuzzing"), no_std)] extern crate alloc; diff --git a/src/hyperlight_host/Cargo.toml b/src/hyperlight_host/Cargo.toml index c0592a2b1..057cae9b6 100644 --- a/src/hyperlight_host/Cargo.toml +++ b/src/hyperlight_host/Cargo.toml @@ -132,6 +132,7 @@ mshv3 = ["dep:mshv-bindings3", "dep:mshv-ioctls3"] inprocess = [] # This enables easy debug in the guest gdb = ["dep:gdbstub", "dep:gdbstub_arch"] +fuzzing = ["hyperlight-common/fuzzing"] [[bench]] name = "benchmarks" diff --git a/src/hyperlight_host/fuzz/Cargo.toml b/src/hyperlight_host/fuzz/Cargo.toml deleted file mode 100644 index aab002916..000000000 --- a/src/hyperlight_host/fuzz/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "hyperlight_host-fuzz" -version = "0.0.0" -publish = false -edition = "2021" - -[package.metadata] -cargo-fuzz = true - -[dependencies] -libfuzzer-sys = "0.4" -hyperlight-testing = { workspace = true } -hyperlight-host = { workspace = true, default-features = true } - -[[bin]] -name = "fuzz_target_1" -path = "fuzz_targets/fuzz_target_1.rs" -test = false -doc = false -bench = false