From b62d558c2125f17c3b0ab4941c07e1fdbb842589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= Date: Sat, 17 Aug 2024 23:41:20 +0200 Subject: [PATCH] parser: add fuzzer --- .github/workflows/rust.yml | 6 +++-- fuzz_parser/Cargo.toml | 19 ++++++++++++++ fuzz_parser/README.md | 29 +++++++++++++++++++++ fuzz_parser/fuzz/.gitignore | 2 ++ fuzz_parser/fuzz/Cargo.toml | 14 ++++++++++ fuzz_parser/fuzz/src/main.rs | 7 +++++ fuzz_parser/src/lib.rs | 47 +++++++++++++++++++++++++++++++++ fuzz_parser/src/main.rs | 50 ++++++++++++++++++++++++++++++++++++ 8 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 fuzz_parser/Cargo.toml create mode 100644 fuzz_parser/README.md create mode 100644 fuzz_parser/fuzz/.gitignore create mode 100644 fuzz_parser/fuzz/Cargo.toml create mode 100644 fuzz_parser/fuzz/src/main.rs create mode 100644 fuzz_parser/src/lib.rs create mode 100644 fuzz_parser/src/main.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 42e9570c..cd3cb366 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -37,6 +37,7 @@ jobs: package: [ rinja, rinja_actix, rinja_axum, rinja_derive, rinja_derive_standalone, rinja_parser, rinja_rocket, rinja_warp, testing, examples/actix-web-app, + fuzz_parser ] runs-on: ubuntu-latest steps: @@ -65,7 +66,8 @@ jobs: set -eu for PKG in \ rinja rinja_actix rinja_axum rinja_derive rinja_derive_standalone \ - rinja_parser rinja_rocket rinja_warp testing examples/actix-web-app + rinja_parser rinja_rocket rinja_warp testing examples/actix-web-app \ + fuzz_parser do cd "$PKG" echo "Testing: $PKG" @@ -79,7 +81,7 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: - toolchain: "1.71.0" + toolchain: "1.71.1" - run: cargo check --lib -p rinja --all-features Audit: diff --git a/fuzz_parser/Cargo.toml b/fuzz_parser/Cargo.toml new file mode 100644 index 00000000..b3a96762 --- /dev/null +++ b/fuzz_parser/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "fuzz_parser" +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" +publish = false + +[dependencies] +rinja_parser = { path = "../rinja_parser" } + +arbitrary = { version = "1.3.2", features = ["derive"] } +pretty-error-debug = "0.3.0" +thiserror = "1.0.63" + +[workspace] +members = [".", "fuzz"] + +[profile.release] +debug = 1 diff --git a/fuzz_parser/README.md b/fuzz_parser/README.md new file mode 100644 index 00000000..aca4f986 --- /dev/null +++ b/fuzz_parser/README.md @@ -0,0 +1,29 @@ +# Rinja Parser Fuzzer + +First install `cargo-fuzz` and rust-nightly (once): + +```sh +cargo install cargo-fuzz +rustup install nightly +``` + +Then execute in this folder: + +```sh +RUST_BACKTRACE=1 nice cargo +nightly fuzz run fuzz +``` + +The execution won't stop, but continue until you kill it with ctrl+c. +Or until it finds a panic. +If the execution found a panic, then a file with the input scenario is written, e.g. +`fuzz/artifacts/fuzz/crash-4184…`. +To get more information about the failed scenario, run or debug this command with the given path: + +```sh +cargo run -- fuzz/artifacts/fuzz/crash-4184… +``` + +Find more information about fuzzing here: + +* `cargo fuzz help run` +* diff --git a/fuzz_parser/fuzz/.gitignore b/fuzz_parser/fuzz/.gitignore new file mode 100644 index 00000000..8fa697f9 --- /dev/null +++ b/fuzz_parser/fuzz/.gitignore @@ -0,0 +1,2 @@ +/artifacts/ +/corpus/ diff --git a/fuzz_parser/fuzz/Cargo.toml b/fuzz_parser/fuzz/Cargo.toml new file mode 100644 index 00000000..db87d662 --- /dev/null +++ b/fuzz_parser/fuzz/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "fuzz" +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" +publish = false + +[package.metadata] +cargo-fuzz = true + +[dependencies] +fuzz_parser = { path = ".." } + +libfuzzer-sys = "0.4.7" diff --git a/fuzz_parser/fuzz/src/main.rs b/fuzz_parser/fuzz/src/main.rs new file mode 100644 index 00000000..f55bcc66 --- /dev/null +++ b/fuzz_parser/fuzz/src/main.rs @@ -0,0 +1,7 @@ +#![no_main] + +libfuzzer_sys::fuzz_target!(|data: &[u8]| { + if let Ok(scenario) = fuzz_parser::Scenario::new(data) { + let _ = scenario.run(); + } +}); diff --git a/fuzz_parser/src/lib.rs b/fuzz_parser/src/lib.rs new file mode 100644 index 00000000..d65bd513 --- /dev/null +++ b/fuzz_parser/src/lib.rs @@ -0,0 +1,47 @@ +use arbitrary::{Arbitrary, Unstructured}; +use rinja_parser::{Ast, Syntax}; + +#[derive(Debug, Default)] +pub struct Scenario<'a> { + syntax: Syntax<'a>, + src: &'a str, +} + +impl<'a> Scenario<'a> { + pub fn new(data: &'a [u8]) -> Result { + let mut data = Unstructured::new(data); + + let syntax = ArbitrarySyntax::arbitrary(&mut data)?; + let _syntax = syntax.as_syntax(); + // FIXME: related issue: + let syntax = Syntax::default(); + + let src = <&str>::arbitrary_take_rest(data)?; + + Ok(Self { syntax, src }) + } + + pub fn run(&self) -> Result<(), rinja_parser::ParseError> { + let Scenario { syntax, src } = self; + Ast::from_str(src, None, syntax)?; + Ok(()) + } +} + +#[derive(Arbitrary, Default)] +struct ArbitrarySyntax<'a>(Option<[Option<&'a str>; 6]>); + +impl<'a> ArbitrarySyntax<'a> { + fn as_syntax(&self) -> Syntax<'a> { + let default = Syntax::default(); + let values = self.0.unwrap_or_default(); + Syntax { + block_start: values[0].unwrap_or(default.block_start), + block_end: values[1].unwrap_or(default.block_end), + expr_start: values[2].unwrap_or(default.expr_start), + expr_end: values[3].unwrap_or(default.expr_end), + comment_start: values[4].unwrap_or(default.comment_start), + comment_end: values[5].unwrap_or(default.comment_end), + } + } +} diff --git a/fuzz_parser/src/main.rs b/fuzz_parser/src/main.rs new file mode 100644 index 00000000..c909dc80 --- /dev/null +++ b/fuzz_parser/src/main.rs @@ -0,0 +1,50 @@ +use std::env::args_os; +use std::fs::OpenOptions; +use std::io::Read; +use std::path::{Path, PathBuf}; + +use fuzz_parser::Scenario; + +fn main() -> Result<(), Error> { + let mut args = args_os().fuse(); + let exe = args.next().map(PathBuf::from); + let path = args.next().map(PathBuf::from); + let empty = args.next().map(|_| ()); + + let (Some(path), None) = (path, empty) else { + return Err(Error::Usage(exe)); + }; + + let mut data = Vec::new(); + match OpenOptions::new().read(true).open(Path::new(&path)) { + Ok(mut f) => { + f.read_to_end(&mut data) + .map_err(|err| Error::Read(err, path))?; + } + Err(err) => return Err(Error::Open(err, path)), + }; + + let scenario = &Scenario::new(&data).map_err(Error::Build)?; + eprintln!("{scenario:#?}"); + scenario.run().map_err(Error::Run)?; + println!("Success."); + + Ok(()) +} + +#[derive(thiserror::Error, pretty_error_debug::Debug)] +enum Error { + #[error( + "wrong arguments supplied\nUsage: {} ", + .0.as_deref().unwrap_or(Path::new("fuzz_parser")).display(), + )] + Usage(Option), + #[error("could not open input file {}", .1.display())] + Open(#[source] std::io::Error, PathBuf), + #[error("could not read opened input file {}", .1.display())] + Read(#[source] std::io::Error, PathBuf), + #[error("could not build scenario")] + Build(#[source] arbitrary::Error), + #[error("could not run scenario")] + Run(#[source] rinja_parser::ParseError), +}