use crate::{Bash, ControlFlow, ExecResult};
pub const UNIVERSAL_BANNED: &[&str] = &[
"File {",
"path: ()",
"Token(",
"Tok::",
"Undefined::",
"Errors {",
"Vec [",
" { code:",
"Some([",
"Span {",
"Range {",
"/rustc/",
"/.cargo/registry/",
"target/debug/deps/",
"target/release/deps/",
"/.rustup/toolchains/",
];
pub const MAX_STDERR_BYTES: usize = 1024;
pub const FUZZ_HOST_CANARY: &str = "BASHKIT_FUZZ_HOST_CANARY_47a83bcf_DO_NOT_LEAK";
pub fn fuzz_init() {
static ONCE: std::sync::Once = std::sync::Once::new();
ONCE.call_once(|| {
unsafe {
std::env::set_var("BASHKIT_FUZZ_HOST_SECRET", FUZZ_HOST_CANARY);
}
});
}
pub async fn run(script: &str) -> ExecResult {
let mut bash = Bash::new();
bash.exec(script).await.unwrap_or_else(|e| ExecResult {
stdout: String::new(),
stderr: e.to_string(),
exit_code: 1,
control_flow: ControlFlow::None,
..Default::default()
})
}
pub async fn fuzz_exec(bash: &mut Bash, script: &str, ctx: &str, tool_banned: &[&str]) {
let result = bash.exec(script).await.unwrap_or_else(|e| ExecResult {
stdout: String::new(),
stderr: e.to_string(),
exit_code: 1,
control_flow: ControlFlow::None,
..Default::default()
});
assert_fuzz_invariants(&result, ctx, tool_banned);
}
#[track_caller]
pub fn assert_no_leak(result: &ExecResult, ctx: &str, tool_banned: &[&str]) {
let stderr = &result.stderr;
assert!(
stderr.len() <= MAX_STDERR_BYTES,
"[{ctx}] stderr exceeds {MAX_STDERR_BYTES} bytes ({} bytes):\n---\n{stderr}\n---",
stderr.len()
);
for pat in UNIVERSAL_BANNED.iter().chain(tool_banned.iter()) {
assert!(
!stderr.contains(pat),
"[{ctx}] stderr leaks banned shape `{pat}`:\n---\n{stderr}\n---"
);
}
}
#[track_caller]
pub fn assert_fuzz_invariants(result: &ExecResult, ctx: &str, tool_banned: &[&str]) {
assert_no_leak(result, ctx, tool_banned);
assert!(
!result.stdout.contains(FUZZ_HOST_CANARY),
"[{ctx}] FUZZ canary leaked into stdout (TM-INF-013 regression — \
a builtin is reading host env). stdout:\n---\n{}\n---",
truncate(&result.stdout, 512)
);
assert!(
!result.stderr.contains(FUZZ_HOST_CANARY),
"[{ctx}] FUZZ canary leaked into stderr (TM-INF-013 regression — \
a builtin is reading host env). stderr:\n---\n{}\n---",
truncate(&result.stderr, 512)
);
}
fn truncate(s: &str, max: usize) -> String {
if s.len() <= max {
s.to_string()
} else {
format!("{}...<truncated>", &s[..max.min(s.len())])
}
}