Merge nucleic/sleek-thistle-egret-fyej into dev
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
target
|
||||
corpus
|
||||
artifacts
|
||||
coverage
|
||||
@@ -0,0 +1,46 @@
|
||||
[package]
|
||||
name = "brush-fuzz"
|
||||
description = "Fuzz tests for brush"
|
||||
publish = false
|
||||
version = "0.2.2"
|
||||
authors.workspace = true
|
||||
categories.workspace = true
|
||||
edition.workspace = true
|
||||
keywords.workspace = true
|
||||
license.workspace = true
|
||||
readme.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[package.metadata]
|
||||
cargo-fuzz = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.102"
|
||||
assert_cmd = "2.2.0"
|
||||
libfuzzer-sys = "0.4"
|
||||
tokio = { version = "1.52.1", features = ["rt"] }
|
||||
|
||||
[dependencies.brush-core]
|
||||
path = "../brush-core"
|
||||
|
||||
[dependencies.brush-parser]
|
||||
path = "../brush-parser"
|
||||
features = ["arbitrary"]
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_parse"
|
||||
path = "fuzz_targets/fuzz_parse.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_arithmetic"
|
||||
path = "fuzz_targets/fuzz_arithmetic.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
@@ -0,0 +1,97 @@
|
||||
#![no_main]
|
||||
#![allow(missing_docs)]
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::Result;
|
||||
use brush_parser::ast;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
static TOKIO_RT: LazyLock<tokio::runtime::Runtime> =
|
||||
LazyLock::new(|| tokio::runtime::Runtime::new().unwrap());
|
||||
|
||||
static SHELL_TEMPLATE: LazyLock<brush_core::Shell> = LazyLock::new(|| {
|
||||
TOKIO_RT
|
||||
.block_on(
|
||||
brush_core::Shell::builder()
|
||||
.profile(brush_core::ProfileLoadBehavior::Skip)
|
||||
.rc(brush_core::RcLoadBehavior::Skip)
|
||||
.build(),
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
fn eval_arithmetic(mut shell: brush_core::Shell, input: &ast::ArithmeticExpr) -> Result<()> {
|
||||
const DEFAULT_TIMEOUT_IN_SECONDS: u64 = 15;
|
||||
|
||||
//
|
||||
// Turn it back into a string so we can pass it in on the command-line.
|
||||
//
|
||||
let input_str = input.to_string();
|
||||
|
||||
//
|
||||
// Instantiate a brush shell with defaults, then try to evaluate the expression.
|
||||
//
|
||||
let parsed_expr = brush_parser::arithmetic::parse(input_str.as_str()).ok();
|
||||
let our_eval_result = if let Some(parsed_expr) = parsed_expr {
|
||||
shell.eval_arithmetic(&parsed_expr).ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
//
|
||||
// Now run it under 'bash'
|
||||
//
|
||||
let mut oracle_cmd = std::process::Command::new("bash");
|
||||
oracle_cmd
|
||||
.arg("--noprofile")
|
||||
.arg("--norc")
|
||||
.arg("-O")
|
||||
.arg("extglob")
|
||||
.arg("-t");
|
||||
|
||||
let mut oracle_cmd = assert_cmd::Command::from_std(oracle_cmd);
|
||||
|
||||
oracle_cmd.timeout(std::time::Duration::from_secs(DEFAULT_TIMEOUT_IN_SECONDS));
|
||||
|
||||
let input = std::format!("echo \"$(( {input_str} ))\"\n");
|
||||
oracle_cmd.write_stdin(input.as_bytes());
|
||||
|
||||
let oracle_result = oracle_cmd.output()?;
|
||||
let oracle_eval_result = if oracle_result.status.success() {
|
||||
let oracle_output = std::str::from_utf8(&oracle_result.stdout)?;
|
||||
oracle_output.trim().parse::<i64>().ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
//
|
||||
// Compare results.
|
||||
//
|
||||
if our_eval_result != oracle_eval_result {
|
||||
Err(anyhow::anyhow!(
|
||||
"Mismatched eval results: {oracle_eval_result:?} from oracle vs. {our_eval_result:?} from our test (expr: '{input_str}', oracle result: {oracle_result:?})"
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fuzz_target!(|input: ast::ArithmeticExpr| {
|
||||
let s = input.to_string();
|
||||
let s = s.trim();
|
||||
|
||||
// For now, intentionally ignore known problematic cases without actually running them.
|
||||
if s.contains("+ 0")
|
||||
|| s.is_empty()
|
||||
|| s.contains(|c: char| c.is_ascii_control() || !c.is_ascii())
|
||||
|| s.contains("$[")
|
||||
// old deprecated form of arithmetic expansion
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let shell = SHELL_TEMPLATE.clone();
|
||||
eval_arithmetic(shell, &input).unwrap();
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
#![no_main]
|
||||
#![allow(missing_docs)]
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use anyhow::Result;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
static TOKIO_RT: LazyLock<tokio::runtime::Runtime> =
|
||||
LazyLock::new(|| tokio::runtime::Runtime::new().unwrap());
|
||||
|
||||
static SHELL_TEMPLATE: LazyLock<brush_core::Shell> = LazyLock::new(|| {
|
||||
TOKIO_RT
|
||||
.block_on(
|
||||
brush_core::Shell::builder()
|
||||
.profile(brush_core::ProfileLoadBehavior::Skip)
|
||||
.rc(brush_core::RcLoadBehavior::Skip)
|
||||
.build(),
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
#[expect(clippy::unused_async)]
|
||||
async fn parse_async(shell: brush_core::Shell, input: String) -> Result<()> {
|
||||
const DEFAULT_TIMEOUT_IN_SECONDS: u64 = 15;
|
||||
|
||||
//
|
||||
// Instantiate a brush shell with defaults, then try to parse the input.
|
||||
//
|
||||
let our_parse_result = shell.parse_string(input.clone());
|
||||
|
||||
//
|
||||
// Now run it under 'bash -n -t' as a crude way to see if it's at least valid syntax.
|
||||
//
|
||||
let mut oracle_cmd = std::process::Command::new("bash");
|
||||
oracle_cmd
|
||||
.arg("--noprofile")
|
||||
.arg("--norc")
|
||||
.arg("-O")
|
||||
.arg("extglob")
|
||||
.arg("-n")
|
||||
.arg("-t");
|
||||
|
||||
let mut oracle_cmd = assert_cmd::Command::from_std(oracle_cmd);
|
||||
|
||||
oracle_cmd.timeout(std::time::Duration::from_secs(DEFAULT_TIMEOUT_IN_SECONDS));
|
||||
|
||||
let mut input = input;
|
||||
input.push('\n');
|
||||
oracle_cmd.write_stdin(input.as_bytes());
|
||||
|
||||
let oracle_result = oracle_cmd.output()?;
|
||||
|
||||
//
|
||||
// Compare results.
|
||||
//
|
||||
if our_parse_result.is_ok() != oracle_result.status.success() {
|
||||
Err(anyhow::anyhow!(
|
||||
"Mismatched parse results: {oracle_result:?} vs {our_parse_result:?}"
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fuzz_target!(|input: String| {
|
||||
// Ignore known problematic cases without actually running them.
|
||||
if input.is_empty()
|
||||
|| input.contains(|c: char| c.is_ascii_control() || !c.is_ascii()) // non-ascii chars (or control sequences)
|
||||
|| input.contains('!') // history expansions
|
||||
|| (input.contains('[') && !input.contains(']')) // ???
|
||||
|| input.contains("<<") // weirdness with here docs
|
||||
|| input.ends_with('\\') // unterminated trailing escape char?
|
||||
|| input.contains("|&")
|
||||
// unimplemented bash-ism
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let shell = SHELL_TEMPLATE.clone();
|
||||
TOKIO_RT.block_on(parse_async(shell, input)).unwrap();
|
||||
});
|
||||
Reference in New Issue
Block a user