parser: add fuzzer

This commit is contained in:
René Kijewski 2024-08-17 23:41:20 +02:00
parent 241c60143e
commit b62d558c21
8 changed files with 172 additions and 2 deletions

View File

@ -37,6 +37,7 @@ jobs:
package: [ package: [
rinja, rinja_actix, rinja_axum, rinja_derive, rinja_derive_standalone, 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
] ]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -65,7 +66,8 @@ jobs:
set -eu set -eu
for PKG in \ for PKG in \
rinja rinja_actix rinja_axum rinja_derive rinja_derive_standalone \ 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 do
cd "$PKG" cd "$PKG"
echo "Testing: $PKG" echo "Testing: $PKG"
@ -79,7 +81,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master - uses: dtolnay/rust-toolchain@master
with: with:
toolchain: "1.71.0" toolchain: "1.71.1"
- run: cargo check --lib -p rinja --all-features - run: cargo check --lib -p rinja --all-features
Audit: Audit:

19
fuzz_parser/Cargo.toml Normal file
View File

@ -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

29
fuzz_parser/README.md Normal file
View File

@ -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`
* <https://rust-fuzz.github.io/book/cargo-fuzz.html>

2
fuzz_parser/fuzz/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/artifacts/
/corpus/

View File

@ -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"

View File

@ -0,0 +1,7 @@
#![no_main]
libfuzzer_sys::fuzz_target!(|data: &[u8]| {
if let Ok(scenario) = fuzz_parser::Scenario::new(data) {
let _ = scenario.run();
}
});

47
fuzz_parser/src/lib.rs Normal file
View File

@ -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<Self, arbitrary::Error> {
let mut data = Unstructured::new(data);
let syntax = ArbitrarySyntax::arbitrary(&mut data)?;
let _syntax = syntax.as_syntax();
// FIXME: related issue: <https://github.com/rinja-rs/rinja/issues/138>
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),
}
}
}

50
fuzz_parser/src/main.rs Normal file
View File

@ -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: {} <path>",
.0.as_deref().unwrap_or(Path::new("fuzz_parser")).display(),
)]
Usage(Option<PathBuf>),
#[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),
}