- Install nix
- If you are on a server,
sudo apt install xorg -y git submodule init && git submodule update --depth 1nix-shellor follow github.com/servo/servo documentation on how to install all the build dependencies and the github.com/AFLplusplus/AFLplusplus requirements- Note: From now on every command is meant to be run in the
nix-shell
- Note: From now on every command is meant to be run in the
rustup default stablecargo install cargo-aflcargo afl config --build --forceAFL_LLVM_CMPLOG=1 cargo afl build
To test that each target is working you may run the following command and observe the output. Where target_name is one of the files under src/bin without the .rs extension: for example eval_script.
cat path/to/test/input | ./target/debug/<target_name> Note that the command migth hang
or
AFL_DEBUG=1 RUST_BACKTRACE=full cargo afl fuzz -i in/random -o out/<target_name> target/debug/<target_name>
You must make sure that the target does not crash with all the inputs, and you may use these to check if your target is behaving correctly
The following commands set the performance settings required by AFl++. Note that when you restart the machine you must run them again.
echo core | sudo tee /proc/sys/kernel/core_patterncd /sys/devices/system/cpu && echo performance | sudo tee cpu*/cpufreq/scaling_governor
You may start a single core fuzzing instance with:
cargo afl fuzz -i in/random -o out target/debug/<target_name>
Note you may change the in/random folder to select different starting inputs
Note you may use just the in folder to select all starting inputs
Note that folder in/custom is not tracked by git and you may use it to test new inputs
To run multiple master and secondary instances on the same target you may use the run_fuzzer.sh script like this
./run_fuzzer.sh target/debug/<target_name> <in_folder> [true]
- #1 arg is the full path of the target
- #2 arg is the full path of the input folder. This is important since each target uses different inputs: scripts, strings, numbers
- #3 is optional and accepts only the value true to start two secondary fuzz instances. Note, this is a work in progress, and it might be worth it to have more secondary instances for better results
This script will create a tmux instance with a sub instance for each main and secondary instances, not the best but it is a simple example
For example, the all the targets may be started with the following commands. Note, that each instance (main, or secondary) needs a core for itself. So you are limited by the number of CPUs you have (in my case 16). You may list them with lscpu.
./run_fuzzer.sh ./target/debug/readable_stream_with_query_strategy ./in/strings
./run_fuzzer.sh ./target/debug/writable_stream ./in/strings
./run_fuzzer.sh ./target/debug/queueing_strategy ./in/numbers
./run_fuzzer.sh ./target/debug/readable_stream_byob_reader ./in/strings
./run_fuzzer.sh ./target/debug/readable_stream_default_controller ./in/strings
./run_fuzzer.sh ./target/debug/writable_stream_default_controller ./in/strings
./run_fuzzer.sh ./target/debug/transform_stream ./in/strings true
./run_fuzzer.sh ./target/debug/pipe_readable_stream ./in/strings
./run_fuzzer.sh ./target/debug/readable_stream ./in/strings
./run_fuzzer.sh ./target/debug/count_queuing_strategy ./in/numbers
./run_fuzzer.sh ./target/debug/eval_script ./in/scripts trueThe run_all.sh script contains these commands.
Note, that if you start and stop them manually you might have conflicts in the out/<target_name> folder and you might need to rename it or delete it.
The eval_script targets was found to be the only one yielding crashes so I wrote a wrapper just for this target that uses one main instance and 14 secondary.
Note that it was meant to run on a server with 16 cores and might not work on your local setup so I suggest you edit it before running it.
Note that some of the secondary might fail at start because of a timeout in reading the testcases but you can enter the tmux shell and restart the last command manually.
./run_eval_script_target.shIn folder ./out you will have a folder for each target named after it. For example, eval_script. You can use the following command to see the stats of the fuzzer.
cargo afl whatsup -s -m ./out/eval_script # or any other target
Or use ./check_status.sh
The output will look like
./out/readable_stream_byob_reader
Summary stats
=============
Fuzzers alive : 1
Total run time : 6 minutes, 1 seconds
Current average speed : 25 execs/sec
Pending per fuzzer : 1 faves, 107 total (on average)
Coverage reached : 2.98%
Crashes saved : 0
Time without finds : 38 seconds
The important things here are:
Fuzzers alive : 1how many fuzzers are running. Less than one means we are not fuzzing this target, maybe we stopped it or maybe there was some other errorCurrent average speed : 25 execs/sechow fast we are fuzzing. We would like to see something above 100Coverage reached : 2.98%coverage reached by the fuzzer. If this does not improve over time we need to rethink the targets or the inputs (or just wait more time)Crashes saved : 0how many crashes the fuzzer found. If this is above zero we need to go to folderout/<target_name>/<intance_name>/crashes/and start triaging the crash. For example, withcat path/to/crash/file | ./target/debug/<target_name>Timme without finds : 38 secondsthis tells the last time the fuzzer found a new path, if this grows too much the fuzzer will stop as it is not able to find new paths. Again here we need to rethink the targets and the inputs because in a way that they make it possible to explore new paths while fuzzing
To add a new target, it is enough to create a new file under ./src/bin/ with a new name and implement the target following the examples of the others. For example, the readable_stream target.
#[macro_use]
extern crate afl;
extern crate servo;
use std::sync::OnceLock;
use servo_js_fuzz::{ServoTest, run_script_on};
thread_local! {
static SERVO_LOCK: OnceLock<ServoTest> = OnceLock::new();
}
const SCRIPT_FORMAT: &str = r#"
function target(input) {
const readableStream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(input));
controller.close();
}
});
const reader = readableStream.getReader();
function readNext() {
reader.read().then(({ done, value }) => {
if (done) return;
readNext();
});
}
readNext();
}
target("%input%")
"#;
fn main() {
fuzz!(|data: &[u8]| {
if let Ok(input_data) = std::str::from_utf8(data) {
SERVO_LOCK.with(|cell| {
let servo_test = cell.get_or_init(|| ServoTest::new());
let script = SCRIPT_FORMAT.replace("%input%", input_data);
let _ = run_script_on(servo_test, &script);
});
}
});
}The fuzzer entry point is the fuzz! macro in the main function that takes a lambda as input with a u8 slice pointer. This slice pointer is the input generated by the fuzzer and can be combined with a script, like in this case, or used directly, like in the eval_script example.
Note, it is important to use a OnceLock in order to initialize the Servo instance only once and to have thread-safe access to it.
To add new inputs it is enough to place thme under one of the ./in folders. Note that adding them while the fuzzer is running will not add them to the queues you have to restart the fuzzer for it to see them.
- Run
./check_status.sh | grep Crashesscript that will runcargo afl whatsupon all the targets - If you see a crash you can find the file path with the following command
find ./out -type f -path '*/crashes/*' -not -name '*.txt'Folder triage contains scripts to help triaging and pocs creation
Note, these scripts must be run from the project root dir and not from inside the triage folder, while in the nix-shell
- AFL++ Overview https://aflplus.plus/
- AFL++ Fuzzing in depth https://aflplus.plus/docs/fuzzing_in_depth/
- Rust Fuzz Book https://rust-fuzz.github.io/book/afl.htm