Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 deletions src/tools/bash/read_guard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,12 @@ fn inspect_text(text: &str, cr_mode: CrMode) -> ReadVerdict {
.push(ReadCandidate::Expand(word.text.clone()));
continue;
}
push_static(&mut verdict.candidates, word, index >= leading);
push_static(
&mut verdict.candidates,
word,
index >= leading,
index >= leading && !echo,
);
collect_relative_targets(&mut verdict.relative_words, word, index >= leading);
}
}
Expand Down Expand Up @@ -240,7 +245,12 @@ fn join_base(base: &str, path: &str) -> String {
}
}

fn push_static(candidates: &mut Vec<ReadCandidate>, word: &Word, equals_rhs_is_candidate: bool) {
fn push_static(
candidates: &mut Vec<ReadCandidate>,
word: &Word,
equals_rhs_is_candidate: bool,
parent_home_is_candidate: bool,
) {
let mut produced = Vec::new();
if word.text.starts_with('/') {
// A quoted operator or bracket is part of the actual filename. Inspect
Expand All @@ -264,6 +274,18 @@ fn push_static(candidates: &mut Vec<ReadCandidate>, word: &Word, equals_rhs_is_c
if equals_rhs_is_candidate && let Some(value) = equals_right_hand_side(&word.text) {
produced.push(ReadCandidate::Literal(value.to_string()));
}
// Issue #628: a bare `..` or a `~`-prefixed `/`-less word names the
// parent or home directory, which the existing literal read proof
// rejects. It carries no `/`, so it was never a literal-path candidate
// before. The right-hand side of a non-assignment `NAME=value`
// (`make PREFIX=~`, `cat --file=..`) is the same. A leading assignment
// and an `echo` display segment are excluded by the caller.
if parent_home_is_candidate && is_parent_or_home_literal(&word.text) {
produced.push(ReadCandidate::Literal(word.text.clone()));
}
if parent_home_is_candidate && let Some(value) = parent_home_right_hand_side(&word.text) {
produced.push(ReadCandidate::Literal(value.to_string()));
}
produced.extend(
super::absolute_path_candidates(&word.text)
.map(|path| ReadCandidate::Expand(path.to_owned())),
Expand All @@ -273,6 +295,21 @@ fn push_static(candidates: &mut Vec<ReadCandidate>, word: &Word, equals_rhs_is_c
candidates.extend(produced);
}

/// Whether a static word names the parent directory (`..` exactly) or a
/// `~`-prefixed home spelling without a directory separator (Issue #628). A
/// word with a `/` keeps its existing literal-path handling.
fn is_parent_or_home_literal(word: &str) -> bool {
word == ".." || (word.starts_with('~') && !word.contains('/'))
}

/// The right-hand side of a `NAME=value` word when it is a parent/home literal
/// (`make PREFIX=~`, `cat --file=..`); see [`is_parent_or_home_literal`]
/// (Issue #628).
fn parent_home_right_hand_side(word: &str) -> Option<&str> {
let (_, value) = word.split_once('=')?;
is_parent_or_home_literal(value).then_some(value)
}

/// The right-hand side of a `NAME=value` word when it is a static path spelling.
/// An empty or absolute right side yields `None`: the embedded absolute-path scan
/// already inspects an absolute one (Issue #603 design 3).
Expand Down
20 changes: 19 additions & 1 deletion src/tools/bash_write_guard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,10 @@ pub(super) fn confinement_rejection(
});
}
if target.path == "/dev/null"
&& matches!(target.operation.as_str(), "output redirection" | "tee")
&& matches!(
target.operation.as_str(),
"output redirection" | "tee" | "dd"
)
{
continue;
}
Expand Down Expand Up @@ -504,6 +507,21 @@ fn collect_program_targets(program: &str, arguments: &[&str], targets: &mut Vec<
operation: program.to_string(),
}));
}
// Issue #628: `dd` names its output file with `of=`, which the write
// table did not recognize. Every non-empty `of=` argument is a write
// destination, exactly like a redirection or `tee` operand.
"dd" => {
targets.extend(
arguments
.iter()
.filter_map(|argument| argument.strip_prefix("of="))
.filter(|value| !value.is_empty())
.map(|value| WriteTarget {
path: value.to_string(),
operation: program.to_string(),
}),
);
}
"chmod" | "chown" => {
targets.extend(operands.into_iter().skip(1).map(|path| WriteTarget {
path: path.to_string(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
{"id": "equals-parent-escape", "command": "dd if=../outside/secret", "R": true, "A": false, "W": false, "P": false, "S": false, "V": true, "operation": "path reference"}
{"id": "allow-quoted-space-inside", "command": "cat \"docs/my notes.md\"", "R": false, "A": false, "W": false, "P": false, "S": false, "V": true}
{"id": "allow-escaped-space-inside", "command": "cat docs/my\\ notes.md", "R": false, "A": false, "W": false, "P": false, "S": false, "V": false}
{"id": "allow-dd-nonpath", "command": "dd if=in.bin of=out.bin", "R": false, "A": false, "W": false, "P": false, "S": false, "V": true}
{"id": "allow-dd-nonpath", "command": "dd if=in.bin of=out.bin", "R": false, "A": true, "W": false, "P": false, "S": false, "V": false}
{"id": "allow-grep-file-nonpath", "command": "grep --file=patterns.txt src", "R": false, "A": false, "W": false, "P": false, "S": false, "V": true}
{"id": "allow-features", "command": "cargo test --features=gui", "R": false, "A": false, "W": false, "P": false, "S": false, "V": true}
{"id": "allow-target-dir", "command": "cargo build --target-dir=target/h05", "R": false, "A": false, "W": false, "P": false, "S": false, "V": true}
Expand Down Expand Up @@ -101,3 +101,29 @@
{"id": "allow-crlf-git", "command": "git status\r\ngit diff\r\n", "R": false, "A": false, "W": false, "P": false, "S": false, "V": true}
{"id": "allow-crlf-cd-cat", "command": "cd sub\r\ncat f\r\n", "R": false, "A": false, "W": false, "P": false, "S": false, "V": false}
{"id": "allow-crlf-echo-out", "command": "echo x > out.txt\r\n", "R": false, "A": true, "W": false, "P": false, "S": false, "V": false}
{"id": "parent-dir-read", "command": "ls ..", "R": true, "A": false, "W": false, "P": false, "S": false, "V": true, "operation": "path reference"}
{"id": "parent-dir-option-read", "command": "ls -la ..", "R": true, "A": false, "W": false, "P": false, "S": false, "V": true, "operation": "path reference"}
{"id": "parent-dir-quoted-read", "command": "ls '..'", "R": true, "A": false, "W": false, "P": false, "S": false, "V": true, "operation": "path reference"}
{"id": "parent-dir-cat-read", "command": "cat ..", "R": true, "A": false, "W": false, "P": false, "S": false, "V": true, "operation": "path reference"}
{"id": "home-read", "command": "ls ~", "R": true, "A": false, "W": false, "P": false, "S": false, "V": true, "operation": "path reference"}
{"id": "home-user-read", "command": "ls ~root", "R": true, "A": false, "W": false, "P": false, "S": false, "V": true, "operation": "path reference"}
{"id": "home-plus-read", "command": "ls ~+", "R": true, "A": false, "W": false, "P": false, "S": false, "V": true, "operation": "path reference"}
{"id": "home-minus-read", "command": "ls ~-", "R": true, "A": false, "W": false, "P": false, "S": false, "V": true, "operation": "path reference"}
{"id": "equals-home-arg", "command": "make PREFIX=~", "R": true, "A": false, "W": false, "P": false, "S": false, "V": true, "operation": "path reference"}
{"id": "equals-parent-arg", "command": "cat --file=..", "R": true, "A": false, "W": false, "P": false, "S": false, "V": true, "operation": "path reference"}
{"id": "option-c-parent-tar", "command": "tar -C .. -cf - x", "R": true, "A": false, "W": false, "P": false, "S": false, "V": true, "operation": "path reference"}
{"id": "option-c-parent-git", "command": "git -C .. status", "R": true, "A": false, "W": false, "P": false, "S": false, "V": true, "operation": "path reference"}
{"id": "dd-out-symlink", "command": "dd if=in of=lf", "R": true, "A": true, "W": true, "P": false, "S": false, "V": false, "operation": "dd"}
{"id": "dd-out-parent", "command": "dd if=in of=../outside/x", "R": true, "A": true, "W": true, "P": false, "S": false, "V": false, "operation": "dd"}
{"id": "dd-out-home", "command": "dd if=in of=~/f", "R": true, "A": true, "W": true, "P": false, "S": false, "V": false, "operation": "dd"}
{"id": "dd-out-devnull", "command": "dd if=in of=/dev/null", "R": false, "A": true, "W": false, "P": false, "S": false, "V": false}
{"id": "dd-protected-path", "command": "dd if=in of=tests/spec.rs", "R": false, "A": true, "W": false, "P": true, "S": false, "V": false}
{"id": "dd-no-output", "command": "dd if=in", "R": false, "A": false, "W": false, "P": false, "S": false, "V": true}
{"id": "dd-multi-output", "command": "dd if=in of=a of=b", "R": false, "A": true, "W": false, "P": false, "S": false, "V": false}
{"id": "dd-redirect-write", "command": "dd if=in > out", "R": false, "A": true, "W": false, "P": false, "S": false, "V": false}
{"id": "echo-home-unchanged", "command": "echo ~", "R": false, "A": false, "W": false, "P": false, "S": false, "V": true}
{"id": "echo-parent-unchanged", "command": "echo ..", "R": false, "A": false, "W": false, "P": false, "S": false, "V": true}
{"id": "assignment-home-unchanged", "command": "X=~ cargo test", "R": false, "A": false, "W": false, "P": false, "S": false, "V": true}
{"id": "git-range-unchanged", "command": "git diff main..feature", "R": false, "A": false, "W": false, "P": false, "S": false, "V": true}
{"id": "git-tilde-unchanged", "command": "git log HEAD~1", "R": false, "A": false, "W": false, "P": false, "S": false, "V": true}
{"id": "ls-dot-unchanged", "command": "ls .", "R": false, "A": false, "W": false, "P": false, "S": false, "V": true}
Loading
Loading