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 1/6] 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), +} From 8433e24917ab7f6656e5d7dbd0baa872be9ef8e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= Date: Sun, 18 Aug 2024 01:38:01 +0200 Subject: [PATCH 2/6] filters: fuzzer for html escaping --- .github/workflows/rust.yml | 4 +- fuzz_parser/fuzz/Cargo.toml | 14 ---- {fuzz_parser => fuzzing}/Cargo.toml | 5 +- {fuzz_parser => fuzzing}/README.md | 10 ++- {fuzz_parser => fuzzing}/fuzz/.gitignore | 0 fuzzing/fuzz/Cargo.toml | 28 +++++++ .../fuzz/fuzz_targets/html.rs | 4 +- fuzzing/fuzz/fuzz_targets/parser.rs | 9 ++ fuzzing/fuzz/src/html/html.rs | 1 + fuzzing/fuzz/src/html/mod.rs | 44 ++++++++++ fuzzing/fuzz/src/lib.rs | 83 +++++++++++++++++++ .../src/lib.rs => fuzzing/fuzz/src/parser.rs | 10 ++- {fuzz_parser => fuzzing}/src/main.rs | 22 +++-- 13 files changed, 200 insertions(+), 34 deletions(-) delete mode 100644 fuzz_parser/fuzz/Cargo.toml rename {fuzz_parser => fuzzing}/Cargo.toml (64%) rename {fuzz_parser => fuzzing}/README.md (71%) rename {fuzz_parser => fuzzing}/fuzz/.gitignore (100%) create mode 100644 fuzzing/fuzz/Cargo.toml rename fuzz_parser/fuzz/src/main.rs => fuzzing/fuzz/fuzz_targets/html.rs (55%) create mode 100644 fuzzing/fuzz/fuzz_targets/parser.rs create mode 120000 fuzzing/fuzz/src/html/html.rs create mode 100644 fuzzing/fuzz/src/html/mod.rs create mode 100644 fuzzing/fuzz/src/lib.rs rename fuzz_parser/src/lib.rs => fuzzing/fuzz/src/parser.rs (84%) rename {fuzz_parser => fuzzing}/src/main.rs (60%) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index cd3cb366..4d95138f 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -37,7 +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 + fuzzing ] runs-on: ubuntu-latest steps: @@ -67,7 +67,7 @@ jobs: for PKG in \ rinja rinja_actix rinja_axum rinja_derive rinja_derive_standalone \ rinja_parser rinja_rocket rinja_warp testing examples/actix-web-app \ - fuzz_parser + fuzzing do cd "$PKG" echo "Testing: $PKG" diff --git a/fuzz_parser/fuzz/Cargo.toml b/fuzz_parser/fuzz/Cargo.toml deleted file mode 100644 index db87d662..00000000 --- a/fuzz_parser/fuzz/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[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/Cargo.toml b/fuzzing/Cargo.toml similarity index 64% rename from fuzz_parser/Cargo.toml rename to fuzzing/Cargo.toml index b3a96762..2e5722aa 100644 --- a/fuzz_parser/Cargo.toml +++ b/fuzzing/Cargo.toml @@ -1,14 +1,13 @@ [package] -name = "fuzz_parser" +name = "rinja_fuzzing" version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" publish = false [dependencies] -rinja_parser = { path = "../rinja_parser" } +fuzz = { path = "fuzz"} -arbitrary = { version = "1.3.2", features = ["derive"] } pretty-error-debug = "0.3.0" thiserror = "1.0.63" diff --git a/fuzz_parser/README.md b/fuzzing/README.md similarity index 71% rename from fuzz_parser/README.md rename to fuzzing/README.md index aca4f986..52750b44 100644 --- a/fuzz_parser/README.md +++ b/fuzzing/README.md @@ -1,4 +1,4 @@ -# Rinja Parser Fuzzer +# Rinja Fuzzing First install `cargo-fuzz` and rust-nightly (once): @@ -10,17 +10,19 @@ rustup install nightly Then execute in this folder: ```sh -RUST_BACKTRACE=1 nice cargo +nightly fuzz run fuzz +RUST_BACKTRACE=1 nice cargo +nightly fuzz run ``` +`fuzz_target` is one out of `html` or `parser`. + 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…`. +`fuzz/artifacts/parser/crash-b91ab…`. 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… +cargo run -- fuzz/artifacts/parser/crash-b91ab… ``` Find more information about fuzzing here: diff --git a/fuzz_parser/fuzz/.gitignore b/fuzzing/fuzz/.gitignore similarity index 100% rename from fuzz_parser/fuzz/.gitignore rename to fuzzing/fuzz/.gitignore diff --git a/fuzzing/fuzz/Cargo.toml b/fuzzing/fuzz/Cargo.toml new file mode 100644 index 00000000..de12f4a0 --- /dev/null +++ b/fuzzing/fuzz/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "fuzz" +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" +publish = false + +[package.metadata] +cargo-fuzz = true + +[dependencies] +rinja_parser = { path = "../../rinja_parser" } + +arbitrary = { version = "1.3.2", features = ["derive"] } +html-escape = "0.2.13" +libfuzzer-sys = "0.4.7" + +[[bin]] +name = "html" +path = "fuzz_targets/html.rs" +test = false +doc = false + +[[bin]] +name = "parser" +path = "fuzz_targets/parser.rs" +test = false +doc = false diff --git a/fuzz_parser/fuzz/src/main.rs b/fuzzing/fuzz/fuzz_targets/html.rs similarity index 55% rename from fuzz_parser/fuzz/src/main.rs rename to fuzzing/fuzz/fuzz_targets/html.rs index f55bcc66..66a012fc 100644 --- a/fuzz_parser/fuzz/src/main.rs +++ b/fuzzing/fuzz/fuzz_targets/html.rs @@ -1,7 +1,9 @@ #![no_main] +use fuzz::Scenario; + libfuzzer_sys::fuzz_target!(|data: &[u8]| { - if let Ok(scenario) = fuzz_parser::Scenario::new(data) { + if let Ok(scenario) = fuzz::html::Scenario::new(data) { let _ = scenario.run(); } }); diff --git a/fuzzing/fuzz/fuzz_targets/parser.rs b/fuzzing/fuzz/fuzz_targets/parser.rs new file mode 100644 index 00000000..9a4e7a18 --- /dev/null +++ b/fuzzing/fuzz/fuzz_targets/parser.rs @@ -0,0 +1,9 @@ +#![no_main] + +use fuzz::Scenario; + +libfuzzer_sys::fuzz_target!(|data: &[u8]| { + if let Ok(scenario) = fuzz::parser::Scenario::new(data) { + let _ = scenario.run(); + } +}); diff --git a/fuzzing/fuzz/src/html/html.rs b/fuzzing/fuzz/src/html/html.rs new file mode 120000 index 00000000..51d3fa4b --- /dev/null +++ b/fuzzing/fuzz/src/html/html.rs @@ -0,0 +1 @@ +../../../../rinja/src/html.rs \ No newline at end of file diff --git a/fuzzing/fuzz/src/html/mod.rs b/fuzzing/fuzz/src/html/mod.rs new file mode 100644 index 00000000..524af0d6 --- /dev/null +++ b/fuzzing/fuzz/src/html/mod.rs @@ -0,0 +1,44 @@ +mod html; + +use arbitrary::{Arbitrary, Unstructured}; +use html_escape::decode_html_entities_to_string; + +#[derive(Arbitrary, Debug)] +pub enum Scenario<'a> { + String(&'a str), + Char(char), +} + +impl<'a> super::Scenario<'a> for Scenario<'a> { + type NewError = arbitrary::Error; + + type RunError = std::convert::Infallible; + + fn new(data: &'a [u8]) -> Result { + Self::arbitrary_take_rest(Unstructured::new(data)) + } + + fn run(&self) -> Result<(), Self::RunError> { + match self { + &Scenario::String(src) => { + let mut dest = String::with_capacity(src.len()); + html::write_escaped_str(&mut dest, src).unwrap(); + + let mut unescaped = String::with_capacity(src.len()); + let unescaped = decode_html_entities_to_string(dest, &mut unescaped); + assert_eq!(src, unescaped); + } + &Scenario::Char(c) => { + let mut dest = String::with_capacity(6); + html::write_escaped_char(&mut dest, c).unwrap(); + + let mut src = [0; 4]; + let src = c.encode_utf8(&mut src); + let mut unescaped = String::with_capacity(4); + let unescaped = decode_html_entities_to_string(dest, &mut unescaped); + assert_eq!(src, unescaped); + } + } + Ok(()) + } +} diff --git a/fuzzing/fuzz/src/lib.rs b/fuzzing/fuzz/src/lib.rs new file mode 100644 index 00000000..40ee15ea --- /dev/null +++ b/fuzzing/fuzz/src/lib.rs @@ -0,0 +1,83 @@ +pub mod html; +pub mod parser; + +use std::error::Error; +use std::fmt; + +pub const TARGETS: &[( + &str, + for<'a> fn(&'a [u8]) -> Result, Box>, +)] = &[ + ("html", |data| NamedTarget::new::(data)), + ("parser", |data| NamedTarget::new::(data)), +]; + +pub trait Scenario<'a>: fmt::Debug + Sized { + type NewError: Error + Send + 'static; + type RunError: Error + Send + 'static; + + fn new(data: &'a [u8]) -> Result; + fn run(&self) -> Result<(), Self::RunError>; +} + +#[derive(Debug, Clone, Copy, Default)] +pub struct DisplayTargets; + +impl fmt::Display for DisplayTargets { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for (idx, &(name, _)) in TARGETS.iter().enumerate() { + match idx { + 0 => write!(f, "{name}")?, + _ => write!(f, "|{name}")?, + }; + } + Ok(()) + } +} + +pub struct NamedTarget<'a>(Box + 'a>); + +impl NamedTarget<'_> { + #[inline] + pub fn run(&self) -> Result<(), Box> { + self.0.run() + } +} + +impl fmt::Debug for NamedTarget<'_> { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.debug(f) + } +} + +impl<'a> NamedTarget<'a> { + #[inline] + fn new + 'a>(data: &'a [u8]) -> Result> { + match S::new(data) { + Ok(scenario) => Ok(Self(Box::new(scenario))), + Err(err) => Err(Box::new(err)), + } + } +} + +trait RunScenario<'a> { + fn debug(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result; + + fn run(&self) -> Result<(), Box>; +} + +impl<'a, T: Scenario<'a>> RunScenario<'a> for T { + #[inline] + fn debug(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.fmt(f) + } + + #[inline] + fn run(&self) -> Result<(), Box> { + match self.run() { + Ok(()) => Ok(()), + Err(err) => Err(Box::new(err)), + } + } +} diff --git a/fuzz_parser/src/lib.rs b/fuzzing/fuzz/src/parser.rs similarity index 84% rename from fuzz_parser/src/lib.rs rename to fuzzing/fuzz/src/parser.rs index d65bd513..f2f647cd 100644 --- a/fuzz_parser/src/lib.rs +++ b/fuzzing/fuzz/src/parser.rs @@ -7,8 +7,12 @@ pub struct Scenario<'a> { src: &'a str, } -impl<'a> Scenario<'a> { - pub fn new(data: &'a [u8]) -> Result { +impl<'a> super::Scenario<'a> for Scenario<'a> { + type NewError = arbitrary::Error; + + type RunError = rinja_parser::ParseError; + + fn new(data: &'a [u8]) -> Result { let mut data = Unstructured::new(data); let syntax = ArbitrarySyntax::arbitrary(&mut data)?; @@ -21,7 +25,7 @@ impl<'a> Scenario<'a> { Ok(Self { syntax, src }) } - pub fn run(&self) -> Result<(), rinja_parser::ParseError> { + fn run(&self) -> Result<(), Self::RunError> { let Scenario { syntax, src } = self; Ast::from_str(src, None, syntax)?; Ok(()) diff --git a/fuzz_parser/src/main.rs b/fuzzing/src/main.rs similarity index 60% rename from fuzz_parser/src/main.rs rename to fuzzing/src/main.rs index c909dc80..624eb7a4 100644 --- a/fuzz_parser/src/main.rs +++ b/fuzzing/src/main.rs @@ -3,18 +3,24 @@ use std::fs::OpenOptions; use std::io::Read; use std::path::{Path, PathBuf}; -use fuzz_parser::Scenario; +use fuzz::{DisplayTargets, TARGETS}; fn main() -> Result<(), Error> { let mut args = args_os().fuse(); let exe = args.next().map(PathBuf::from); + let name = args.next().and_then(|s| s.into_string().ok()); let path = args.next().map(PathBuf::from); let empty = args.next().map(|_| ()); - let (Some(path), None) = (path, empty) else { + let (Some(name), Some(path), None) = (name, path, empty) else { return Err(Error::Usage(exe)); }; + let scenario_builder = TARGETS + .iter() + .find_map(|&(scenario, func)| (scenario == name).then_some(func)) + .ok_or(Error::Target(name))?; + let mut data = Vec::new(); match OpenOptions::new().read(true).open(Path::new(&path)) { Ok(mut f) => { @@ -24,7 +30,7 @@ fn main() -> Result<(), Error> { Err(err) => return Err(Error::Open(err, path)), }; - let scenario = &Scenario::new(&data).map_err(Error::Build)?; + let scenario = scenario_builder(&data).map_err(Error::Build)?; eprintln!("{scenario:#?}"); scenario.run().map_err(Error::Run)?; println!("Success."); @@ -35,16 +41,18 @@ fn main() -> Result<(), Error> { #[derive(thiserror::Error, pretty_error_debug::Debug)] enum Error { #[error( - "wrong arguments supplied\nUsage: {} ", - .0.as_deref().unwrap_or(Path::new("fuzz_parser")).display(), + "wrong arguments supplied\nUsage: {} <{DisplayTargets}> ", + .0.as_deref().unwrap_or(Path::new("rinja_fuzzing")).display(), )] Usage(Option), + #[error("unknown fuzzing target {:?}\nImplemented targets: {DisplayTargets}", .0)] + Target(String), #[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), + Build(#[source] Box), #[error("could not run scenario")] - Run(#[source] rinja_parser::ParseError), + Run(#[source] Box), } From 79873f6a175730e8d6287d9e3883b76fb6841877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= Date: Sun, 18 Aug 2024 01:40:12 +0200 Subject: [PATCH 3/6] Make clippy happy --- fuzzing/fuzz/src/html/mod.rs | 9 +++++---- fuzzing/fuzz/src/lib.rs | 8 ++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/fuzzing/fuzz/src/html/mod.rs b/fuzzing/fuzz/src/html/mod.rs index 524af0d6..a9d87034 100644 --- a/fuzzing/fuzz/src/html/mod.rs +++ b/fuzzing/fuzz/src/html/mod.rs @@ -1,9 +1,10 @@ +#[allow(clippy::module_inception)] mod html; use arbitrary::{Arbitrary, Unstructured}; use html_escape::decode_html_entities_to_string; -#[derive(Arbitrary, Debug)] +#[derive(Arbitrary, Debug, Clone, Copy)] pub enum Scenario<'a> { String(&'a str), Char(char), @@ -19,8 +20,8 @@ impl<'a> super::Scenario<'a> for Scenario<'a> { } fn run(&self) -> Result<(), Self::RunError> { - match self { - &Scenario::String(src) => { + match *self { + Scenario::String(src) => { let mut dest = String::with_capacity(src.len()); html::write_escaped_str(&mut dest, src).unwrap(); @@ -28,7 +29,7 @@ impl<'a> super::Scenario<'a> for Scenario<'a> { let unescaped = decode_html_entities_to_string(dest, &mut unescaped); assert_eq!(src, unescaped); } - &Scenario::Char(c) => { + Scenario::Char(c) => { let mut dest = String::with_capacity(6); html::write_escaped_char(&mut dest, c).unwrap(); diff --git a/fuzzing/fuzz/src/lib.rs b/fuzzing/fuzz/src/lib.rs index 40ee15ea..999d535c 100644 --- a/fuzzing/fuzz/src/lib.rs +++ b/fuzzing/fuzz/src/lib.rs @@ -4,14 +4,14 @@ pub mod parser; use std::error::Error; use std::fmt; -pub const TARGETS: &[( - &str, - for<'a> fn(&'a [u8]) -> Result, Box>, -)] = &[ +pub const TARGETS: &[(&str, TargetBuilder)] = &[ ("html", |data| NamedTarget::new::(data)), ("parser", |data| NamedTarget::new::(data)), ]; +pub type TargetBuilder = + for<'a> fn(&'a [u8]) -> Result, Box>; + pub trait Scenario<'a>: fmt::Debug + Sized { type NewError: Error + Send + 'static; type RunError: Error + Send + 'static; From 1176c654bad7510ca2b67045f57c8ff06a154b43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= Date: Sun, 18 Aug 2024 02:08:07 +0200 Subject: [PATCH 4/6] fuzz: fuzz everything at once --- fuzzing/Cargo.toml | 3 +- fuzzing/README.md | 2 +- fuzzing/fuzz/Cargo.toml | 6 +++ fuzzing/fuzz/fuzz_targets/all.rs | 9 ++++ fuzzing/fuzz/src/all.rs | 70 ++++++++++++++++++++++++++++++++ fuzzing/fuzz/src/html/mod.rs | 4 +- fuzzing/fuzz/src/lib.rs | 15 +++---- fuzzing/fuzz/src/parser.rs | 4 +- fuzzing/src/main.rs | 2 +- 9 files changed, 97 insertions(+), 18 deletions(-) create mode 100644 fuzzing/fuzz/fuzz_targets/all.rs create mode 100644 fuzzing/fuzz/src/all.rs diff --git a/fuzzing/Cargo.toml b/fuzzing/Cargo.toml index 2e5722aa..d9731dd9 100644 --- a/fuzzing/Cargo.toml +++ b/fuzzing/Cargo.toml @@ -6,8 +6,9 @@ license = "MIT OR Apache-2.0" publish = false [dependencies] -fuzz = { path = "fuzz"} +fuzz = { path = "fuzz" } +arbitrary = "1.3.2" pretty-error-debug = "0.3.0" thiserror = "1.0.63" diff --git a/fuzzing/README.md b/fuzzing/README.md index 52750b44..8a90adf9 100644 --- a/fuzzing/README.md +++ b/fuzzing/README.md @@ -13,7 +13,7 @@ Then execute in this folder: RUST_BACKTRACE=1 nice cargo +nightly fuzz run ``` -`fuzz_target` is one out of `html` or `parser`. +`fuzz_target` is one out of `all`, `html` or `parser`. The execution won't stop, but continue until you kill it with ctrl+c. Or until it finds a panic. diff --git a/fuzzing/fuzz/Cargo.toml b/fuzzing/fuzz/Cargo.toml index de12f4a0..2cd0dba5 100644 --- a/fuzzing/fuzz/Cargo.toml +++ b/fuzzing/fuzz/Cargo.toml @@ -15,6 +15,12 @@ arbitrary = { version = "1.3.2", features = ["derive"] } html-escape = "0.2.13" libfuzzer-sys = "0.4.7" +[[bin]] +name = "all" +path = "fuzz_targets/all.rs" +test = false +doc = false + [[bin]] name = "html" path = "fuzz_targets/html.rs" diff --git a/fuzzing/fuzz/fuzz_targets/all.rs b/fuzzing/fuzz/fuzz_targets/all.rs new file mode 100644 index 00000000..20fd1655 --- /dev/null +++ b/fuzzing/fuzz/fuzz_targets/all.rs @@ -0,0 +1,9 @@ +#![no_main] + +use fuzz::Scenario; + +libfuzzer_sys::fuzz_target!(|data: &[u8]| { + if let Ok(scenario) = fuzz::all::Scenario::new(data) { + let _ = scenario.run(); + } +}); diff --git a/fuzzing/fuzz/src/all.rs b/fuzzing/fuzz/src/all.rs new file mode 100644 index 00000000..d7826198 --- /dev/null +++ b/fuzzing/fuzz/src/all.rs @@ -0,0 +1,70 @@ +macro_rules! this_file { + ($(crate :: $mod:ident :: $ty:ident;)*) => { + use std::fmt; + + use arbitrary::{Arbitrary, Unstructured}; + + #[derive(Debug)] + pub enum Scenario<'a> { + $($ty(crate::$mod::Scenario<'a>),)* + } + + impl<'a> super::Scenario<'a> for Scenario<'a> { + type RunError = RunError; + + fn new(data: &'a [u8]) -> Result { + let mut data = Unstructured::new(data); + let target = Arbitrary::arbitrary(&mut data)?; + let data = Arbitrary::arbitrary_take_rest(data)?; + + match target { + $(Target::$ty => Ok(Self::$ty(crate::$mod::Scenario::new(data)?)),)* + } + } + + fn run(&self) -> Result<(), Self::RunError> { + match self { + $(Self::$ty(scenario) => scenario.run().map_err(RunError::$ty),)* + } + } + } + + #[derive(Arbitrary)] + enum Target { + $($ty,)* + } + + pub enum RunError { + $($ty( as crate::Scenario<'static>>::RunError),)* + } + + impl fmt::Display for RunError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + $(Self::$ty(err) => err.fmt(f),)* + } + } + } + + impl fmt::Debug for RunError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + $(Self::$ty(err) => err.fmt(f),)* + } + } + } + + impl std::error::Error for RunError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + $(RunError::$ty(err) => Some(err),)* + } + } + } + }; +} + +this_file! { + crate::html::Html; + crate::parser::Parser; +} diff --git a/fuzzing/fuzz/src/html/mod.rs b/fuzzing/fuzz/src/html/mod.rs index a9d87034..88f52307 100644 --- a/fuzzing/fuzz/src/html/mod.rs +++ b/fuzzing/fuzz/src/html/mod.rs @@ -11,11 +11,9 @@ pub enum Scenario<'a> { } impl<'a> super::Scenario<'a> for Scenario<'a> { - type NewError = arbitrary::Error; - type RunError = std::convert::Infallible; - fn new(data: &'a [u8]) -> Result { + fn new(data: &'a [u8]) -> Result { Self::arbitrary_take_rest(Unstructured::new(data)) } diff --git a/fuzzing/fuzz/src/lib.rs b/fuzzing/fuzz/src/lib.rs index 999d535c..efcd1038 100644 --- a/fuzzing/fuzz/src/lib.rs +++ b/fuzzing/fuzz/src/lib.rs @@ -1,3 +1,4 @@ +pub mod all; pub mod html; pub mod parser; @@ -5,18 +6,17 @@ use std::error::Error; use std::fmt; pub const TARGETS: &[(&str, TargetBuilder)] = &[ + ("all", |data| NamedTarget::new::(data)), ("html", |data| NamedTarget::new::(data)), ("parser", |data| NamedTarget::new::(data)), ]; -pub type TargetBuilder = - for<'a> fn(&'a [u8]) -> Result, Box>; +pub type TargetBuilder = for<'a> fn(&'a [u8]) -> Result, arbitrary::Error>; pub trait Scenario<'a>: fmt::Debug + Sized { - type NewError: Error + Send + 'static; type RunError: Error + Send + 'static; - fn new(data: &'a [u8]) -> Result; + fn new(data: &'a [u8]) -> Result; fn run(&self) -> Result<(), Self::RunError>; } @@ -53,11 +53,8 @@ impl fmt::Debug for NamedTarget<'_> { impl<'a> NamedTarget<'a> { #[inline] - fn new + 'a>(data: &'a [u8]) -> Result> { - match S::new(data) { - Ok(scenario) => Ok(Self(Box::new(scenario))), - Err(err) => Err(Box::new(err)), - } + fn new + 'a>(data: &'a [u8]) -> Result { + Ok(Self(Box::new(S::new(data)?))) } } diff --git a/fuzzing/fuzz/src/parser.rs b/fuzzing/fuzz/src/parser.rs index f2f647cd..122fd16d 100644 --- a/fuzzing/fuzz/src/parser.rs +++ b/fuzzing/fuzz/src/parser.rs @@ -8,11 +8,9 @@ pub struct Scenario<'a> { } impl<'a> super::Scenario<'a> for Scenario<'a> { - type NewError = arbitrary::Error; - type RunError = rinja_parser::ParseError; - fn new(data: &'a [u8]) -> Result { + fn new(data: &'a [u8]) -> Result { let mut data = Unstructured::new(data); let syntax = ArbitrarySyntax::arbitrary(&mut data)?; diff --git a/fuzzing/src/main.rs b/fuzzing/src/main.rs index 624eb7a4..1432862d 100644 --- a/fuzzing/src/main.rs +++ b/fuzzing/src/main.rs @@ -52,7 +52,7 @@ enum Error { #[error("could not read opened input file {}", .1.display())] Read(#[source] std::io::Error, PathBuf), #[error("could not build scenario")] - Build(#[source] Box), + Build(#[source] arbitrary::Error), #[error("could not run scenario")] Run(#[source] Box), } From 33e80a2be4a9698697aa9df3a4828c5ffb9318a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= Date: Sun, 18 Aug 2024 05:37:35 +0200 Subject: [PATCH 5/6] fuzz: fuzz text filters --- fuzzing/README.md | 2 +- fuzzing/fuzz/Cargo.toml | 8 +++ fuzzing/fuzz/fuzz_targets/all.rs | 6 +- fuzzing/fuzz/fuzz_targets/filters.rs | 5 ++ fuzzing/fuzz/fuzz_targets/html.rs | 6 +- fuzzing/fuzz/fuzz_targets/parser.rs | 6 +- fuzzing/fuzz/src/all.rs | 1 + fuzzing/fuzz/src/filters.rs | 88 ++++++++++++++++++++++++++++ fuzzing/fuzz/src/lib.rs | 19 ++++++ 9 files changed, 125 insertions(+), 16 deletions(-) create mode 100644 fuzzing/fuzz/fuzz_targets/filters.rs create mode 100644 fuzzing/fuzz/src/filters.rs diff --git a/fuzzing/README.md b/fuzzing/README.md index 8a90adf9..b4b98d97 100644 --- a/fuzzing/README.md +++ b/fuzzing/README.md @@ -13,7 +13,7 @@ Then execute in this folder: RUST_BACKTRACE=1 nice cargo +nightly fuzz run ``` -`fuzz_target` is one out of `all`, `html` or `parser`. +`fuzz_target` is one out of `all`, `filters`, `html` or `parser`. The execution won't stop, but continue until you kill it with ctrl+c. Or until it finds a panic. diff --git a/fuzzing/fuzz/Cargo.toml b/fuzzing/fuzz/Cargo.toml index 2cd0dba5..ee54d506 100644 --- a/fuzzing/fuzz/Cargo.toml +++ b/fuzzing/fuzz/Cargo.toml @@ -9,11 +9,13 @@ publish = false cargo-fuzz = true [dependencies] +rinja = { path = "../../rinja" } rinja_parser = { path = "../../rinja_parser" } arbitrary = { version = "1.3.2", features = ["derive"] } html-escape = "0.2.13" libfuzzer-sys = "0.4.7" +thiserror = "1.0.63" [[bin]] name = "all" @@ -21,6 +23,12 @@ path = "fuzz_targets/all.rs" test = false doc = false +[[bin]] +name = "filters" +path = "fuzz_targets/filters.rs" +test = false +doc = false + [[bin]] name = "html" path = "fuzz_targets/html.rs" diff --git a/fuzzing/fuzz/fuzz_targets/all.rs b/fuzzing/fuzz/fuzz_targets/all.rs index 20fd1655..fc4c5868 100644 --- a/fuzzing/fuzz/fuzz_targets/all.rs +++ b/fuzzing/fuzz/fuzz_targets/all.rs @@ -1,9 +1,5 @@ #![no_main] -use fuzz::Scenario; - libfuzzer_sys::fuzz_target!(|data: &[u8]| { - if let Ok(scenario) = fuzz::all::Scenario::new(data) { - let _ = scenario.run(); - } + let _ = ::fuzz(data); }); diff --git a/fuzzing/fuzz/fuzz_targets/filters.rs b/fuzzing/fuzz/fuzz_targets/filters.rs new file mode 100644 index 00000000..99e6a8cd --- /dev/null +++ b/fuzzing/fuzz/fuzz_targets/filters.rs @@ -0,0 +1,5 @@ +#![no_main] + +libfuzzer_sys::fuzz_target!(|data: &[u8]| { + let _ = ::fuzz(data); +}); diff --git a/fuzzing/fuzz/fuzz_targets/html.rs b/fuzzing/fuzz/fuzz_targets/html.rs index 66a012fc..e66623b4 100644 --- a/fuzzing/fuzz/fuzz_targets/html.rs +++ b/fuzzing/fuzz/fuzz_targets/html.rs @@ -1,9 +1,5 @@ #![no_main] -use fuzz::Scenario; - libfuzzer_sys::fuzz_target!(|data: &[u8]| { - if let Ok(scenario) = fuzz::html::Scenario::new(data) { - let _ = scenario.run(); - } + let _ = ::fuzz(data); }); diff --git a/fuzzing/fuzz/fuzz_targets/parser.rs b/fuzzing/fuzz/fuzz_targets/parser.rs index 9a4e7a18..1c41dcee 100644 --- a/fuzzing/fuzz/fuzz_targets/parser.rs +++ b/fuzzing/fuzz/fuzz_targets/parser.rs @@ -1,9 +1,5 @@ #![no_main] -use fuzz::Scenario; - libfuzzer_sys::fuzz_target!(|data: &[u8]| { - if let Ok(scenario) = fuzz::parser::Scenario::new(data) { - let _ = scenario.run(); - } + let _ = ::fuzz(data); }); diff --git a/fuzzing/fuzz/src/all.rs b/fuzzing/fuzz/src/all.rs index d7826198..68708bb7 100644 --- a/fuzzing/fuzz/src/all.rs +++ b/fuzzing/fuzz/src/all.rs @@ -65,6 +65,7 @@ macro_rules! this_file { } this_file! { + crate::filters::Filters; crate::html::Html; crate::parser::Parser; } diff --git a/fuzzing/fuzz/src/filters.rs b/fuzzing/fuzz/src/filters.rs new file mode 100644 index 00000000..3ec37d62 --- /dev/null +++ b/fuzzing/fuzz/src/filters.rs @@ -0,0 +1,88 @@ +use arbitrary::{Arbitrary, Unstructured}; +use rinja::filters; + +#[derive(Arbitrary, Debug, Clone, Copy)] +pub enum Scenario<'a> { + Text(Text<'a>), +} + +impl<'a> super::Scenario<'a> for Scenario<'a> { + type RunError = rinja::Error; + + fn new(data: &'a [u8]) -> Result { + Self::arbitrary_take_rest(Unstructured::new(data)) + } + + fn run(&self) -> Result<(), Self::RunError> { + match *self { + Self::Text(text) => run_text(text), + } + } +} + +fn run_text(filter: Text<'_>) -> Result<(), rinja::Error> { + let Text { input, filter } = filter; + let _ = match filter { + TextFilter::Capitalize => filters::capitalize(input)?.to_string(), + TextFilter::Center(a) => filters::center(input, a)?.to_string(), + TextFilter::Indent(a) => filters::indent(input, a)?.to_string(), + TextFilter::Linebreaks => filters::linebreaks(input)?.to_string(), + TextFilter::LinebreaksBr => filters::linebreaksbr(input)?.to_string(), + TextFilter::Lowercase => filters::lowercase(input)?.to_string(), + TextFilter::ParagraphBreaks => filters::paragraphbreaks(input)?.to_string(), + TextFilter::Safe(e) => match e { + Escaper::Html => filters::safe(input, filters::Html)?.to_string(), + Escaper::Text => filters::safe(input, filters::Text)?.to_string(), + }, + TextFilter::Title => filters::title(input)?.to_string(), + TextFilter::Trim => filters::trim(input)?.to_string(), + TextFilter::Truncate(a) => filters::truncate(input, a)?.to_string(), + TextFilter::Uppercase => filters::uppercase(input)?.to_string(), + TextFilter::Urlencode => filters::urlencode(input)?.to_string(), + TextFilter::UrlencodeStrict => filters::urlencode_strict(input)?.to_string(), + }; + Ok(()) +} + +#[derive(Arbitrary, Debug, Clone, Copy)] +pub struct Text<'a> { + input: &'a str, + filter: TextFilter, +} + +#[derive(Arbitrary, Debug, Clone, Copy)] +enum TextFilter { + Capitalize, + Center(usize), + Indent(usize), + Linebreaks, + LinebreaksBr, + Lowercase, + ParagraphBreaks, + Safe(Escaper), + Title, + Trim, + Truncate(usize), + Uppercase, + Urlencode, + UrlencodeStrict, +} + +#[derive(Arbitrary, Debug, Clone, Copy)] +enum Escaper { + Html, + Text, +} + +// TODO: +// abs +// escape, +// filesizeformat +// fmt +// format +// into_f64 +// into_isize +// join +// json +// json_pretty +// wordcount diff --git a/fuzzing/fuzz/src/lib.rs b/fuzzing/fuzz/src/lib.rs index efcd1038..e68a1e2c 100644 --- a/fuzzing/fuzz/src/lib.rs +++ b/fuzzing/fuzz/src/lib.rs @@ -1,4 +1,5 @@ pub mod all; +pub mod filters; pub mod html; pub mod parser; @@ -7,6 +8,9 @@ use std::fmt; pub const TARGETS: &[(&str, TargetBuilder)] = &[ ("all", |data| NamedTarget::new::(data)), + ("filters", |data| { + NamedTarget::new::(data) + }), ("html", |data| NamedTarget::new::(data)), ("parser", |data| NamedTarget::new::(data)), ]; @@ -16,10 +20,25 @@ pub type TargetBuilder = for<'a> fn(&'a [u8]) -> Result, arbitra pub trait Scenario<'a>: fmt::Debug + Sized { type RunError: Error + Send + 'static; + fn fuzz(data: &'a [u8]) -> Result<(), FuzzError> { + Self::new(data) + .map_err(FuzzError::New)? + .run() + .map_err(FuzzError::Run) + } + fn new(data: &'a [u8]) -> Result; fn run(&self) -> Result<(), Self::RunError>; } +#[derive(Debug, thiserror::Error)] +pub enum FuzzError { + #[error("could not build scenario")] + New(#[source] arbitrary::Error), + #[error("could not run scenario")] + Run(#[source] RunError), +} + #[derive(Debug, Clone, Copy, Default)] pub struct DisplayTargets; From 22e666721786d169a2fb224c74571c553b892d5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= Date: Mon, 19 Aug 2024 13:37:15 +0200 Subject: [PATCH 6/6] Update after #141 was merged --- fuzzing/fuzz/src/parser.rs | 39 ++++++++++++++++---------------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/fuzzing/fuzz/src/parser.rs b/fuzzing/fuzz/src/parser.rs index 122fd16d..c609a3ed 100644 --- a/fuzzing/fuzz/src/parser.rs +++ b/fuzzing/fuzz/src/parser.rs @@ -1,5 +1,5 @@ use arbitrary::{Arbitrary, Unstructured}; -use rinja_parser::{Ast, Syntax}; +use rinja_parser::{Ast, Syntax, SyntaxBuilder}; #[derive(Debug, Default)] pub struct Scenario<'a> { @@ -13,10 +13,21 @@ impl<'a> super::Scenario<'a> for Scenario<'a> { 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 syntax = if let Some(syntax) = ; 6]>>::arbitrary(&mut data)? { + SyntaxBuilder { + name: "test", + block_start: syntax[0], + block_end: syntax[1], + expr_start: syntax[2], + expr_end: syntax[3], + comment_start: syntax[4], + comment_end: syntax[5], + } + .to_syntax() + .map_err(|_| arbitrary::Error::IncorrectFormat)? + } else { + Syntax::default() + }; let src = <&str>::arbitrary_take_rest(data)?; @@ -29,21 +40,3 @@ impl<'a> super::Scenario<'a> for Scenario<'a> { 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), - } - } -}