Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Fuzz Testing Guide for Islet RMM

Note

This document regards an older version of Islet (tag ccav1.0-eac5).

Overview

Islet is built in Rust, which inherently ensures memory safety by design. To further enhance security, we employ tools like Miri to verify that unsafe code adheres to safety rules and Kani, a model checker, for formal verification. Beyond these measures, we also incorporate fuzz testing, a proven method for discovering vulnerabilities, into the Islet RMM development process. We utilize cargo fuzz, which leverages libFuzzer, to perform fuzz testing effectively.

Running Fuzz Tests

To execute fuzz tests, use the following command:

./scripts/fuzz.sh {fuzz test binary} {optional libFuzzer arguments}

If no fuzz test binary is provided as a command-line argument, a list of available binaries will be displayed. You can then select one from the list and run it.

Additional arguments can be supplied to configure libFuzzer itself (https://llvm.org/docs/LibFuzzer.html#options).

To collect coverage information, use the following command:

./scripts/fuzz-coverage.sh {fuzz test binary} {duration of fuzzing}

The coverage script for multiple fuzz binaries can be run successively to build a combined coverage report. The coverage report can be found at /code_coverage/ in HTML form.

The fuzz tests are intended to be run on aarch64 machines. However, they can also be run on x86_64 albeit much slower due to the overhead of QEMU userspace emulation.

Analyzing Fuzz Test Results

If a fuzzer encounteres a crash, the crashing input will be saved as /rmm/fuzz/crash-xxxx. A full backtrace will also be shown displaying the offending calls.

To reproduce the crash using the saved input, use the following command:

./scripts/fuzz.sh {fuzz test binary} {crash-xxxx}

Writing Additional Fuzz Test Harnesses

Fuzz test harnesses are located in /rmm/fuzz/fuzz_targets. Majority of the RMI command fuzzers are based on existing MIRI tests. Additional RMI fuzzers outside of miri coverage and RSI fuzzers are instead based on C-based ACS unit tests.

#![allow(unused)]
fn main() {
1    #![no_main]
2
3    use islet_rmm::rmi::{GRANULE_DELEGATE, GRANULE_UNDELEGATE, RTT_CREATE, RTT_DESTROY, SUCCESS};
4    use islet_rmm::test_utils::{mock, *};
5
6    use libfuzzer_sys::{arbitrary, fuzz_target};
}
  • We use a different entrypoint for fuzzing so we avoid using a main function as seen in line 1.
  • All the necessary RMI commands are imported in line 3.
  • Similar to miri, mock utilies are imported from test_utils as seen in line 4.
  • In line 6, fuzz_target imports the base libFuzzer fuzzing setup and arbitrary extends the former to allow structure-aware fuzzing.

Every fuzz test harness requires a fuzz_target! entrypoint where fuzz code is run.

#![allow(unused)]
fn main() {
      fuzz_target!(|data: &[u8]|) {
        /* Fuzz code */
      }
}

By default, rust-fuzz fuzzes a raw bytearray which is not suitable for Islet RMM where most data passed to commands is structured. We use arbitrary crate to perform structure-aware fuzzing, as demonstrated in the below snippet.

#![allow(unused)]
fn main() {
8     #[derive(Debug, arbitrary::Arbitrary)]
9     struct RTTCreateFuzz {
10        ipa: u64,
11        level: i64,
12    }
13
14    fuzz_target!(|data: RTTCreateFuzz|) {
}
#![allow(unused)]
fn main() {
15        let rd = realm_create();
16        let ipa = data.ipa as usize;
17        let level = data.level as usize;
18
19        let rtt = alloc_granule(IDX_RTT_LEVEL1);
20
21        let _ret = rmi::<GRANULE_DELEGATE>(&[rtt]);
22
23        let ret = rmi::<RTT_CREATE>(&[rd, rtt, ipa, level]);
}
  • In line 15, a realm is created using realm_create from the mock module.
  • In line 16-17, the fuzzed data generated by the fuzzer can be used as input for RMI commands.
  • In line 19, we allocate appropriate memory from the host for use by later RMI commands.
  • RMI commands can be invoked with rmi::<RMI_COMMAND>({arguments}) as seen in lines 21 and 23.
#![allow(unused)]
fn main() {
25        if ret[0] == SUCCESS {
26            let ret = rmi::<RTT_DESTROY>(&[rd, ipa, level]);
27            assert_eq!(ret[0], SUCCESS);
28        }
29
30        let _ret = rmi::<GRANULE_UNDELEGATE>(&[rtt]);
31
32        realm_destroy(rd);
}
  • RTT_CREATE is not expected to succeed for every input, but if it does succeed, the correct teardown of the same must be ensured as shown in lines 25-28.
  • Once the fuzzing work is done in the current iteration, the realm is destroyed with realm_destroy in line 32.
#![allow(unused)]
fn main() {
11    #[derive(Debug, arbitrary::Arbitrary)]
12    struct MeasurementExtendFuzz {
13        idx: u64,
14        size: u64,
15        values: [u64; 8],
16    }
17
18    fuzz_target!(|data: MeasurementExtendFuzz| {
19        let rd = mock::host::realm_setup();
20        let measurement_index = data.idx as usize;
21        let size = data.size as usize;
22        let values = &data.values;
23
24        let (rec1, run1) = (alloc_granule(IDX_REC1), alloc_granule(IDX_REC1_RUN));
25
26        let _ret = rmi::<REC_ENTER>(&[
27            rec1,
28            run1,
29            MEASUREMENT_EXTEND,
30            measurement_index,
31            size,
32            values[0] as usize,
33            values[1] as usize,
34            values[2] as usize,
35            values[3] as usize,
36            values[4] as usize,
37            values[5] as usize,
38            values[6] as usize,
39            values[7] as usize,
40        ]);
41
42        mock::host::realm_teardown(rd);
43    });
}
  • The above snippet demonstrates an example of RSI fuzzing. Much of the setup remains similar to RMI fuzzing.
  • The MEASUREMENT_EXTEND RSI command is run using REC_ENTER RMI command as seen in lines 26-40.

In normal contexts, REC_ENTER takes only two arguments but in fuzzing contexts, it can take a variable number of arguments. The third argument is the RSI call command and further arguments are passed as arguments to the RSI call. This is needed as realm code is not exercised in fuzzing.

The same method can also be used to simulate non-RSI realm exit scenarios by using the pseudo-call REC_ENTER_EXIT_CMD followed by the exit code and their arguments as shows in the below example.

#![allow(unused)]
fn main() {
35    let _ret = rmi::<REC_ENTER>(&[
36        rec1,
37        run1,
38        REC_ENTER_EXIT_CMD,
39        DataAbort,
40        esr as usize,
41        data.hpfar as usize,
42        data.far as usize,
43    ]);
}

To add fuzz-specific code, use the [cfg(fuzzing)] switch. In the below code, a hard-coded key is used for fuzzing attestation RSI calls.

#![allow(unused)]
fn main() {
178   #[cfg(fuzzing)]
179   let realm_attest_key = &RAK_PRIV_KEY;
180   #[cfg(not(fuzzing))]
181   let realm_attest_key = &realm_attest_key();
}

A new fuzz target can be added to Cargo.toml to compile and use the fuzzer as shown below.

[[bin]]
name = "rmi_rtt_create_fuzz"
path = "fuzz_targets/rmi_rtt_create_fuzz.rs"
test = false
doc = false
bench = false