use std::collections::HashMap;
use std::sync::LazyLock;
use serde::Deserialize;
use crate::verdict::{SafetyLevel, Verdict};
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum Shape {
Command,
ExecPath,
DataPath,
Opaque,
OptionString,
}
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum PathRole {
Read,
Write,
Exec,
}
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
#[serde(rename_all = "kebab-case")]
enum Sep {
#[default]
Equals,
Space,
}
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
struct PathFlagEntry {
flag: String,
role: PathRole,
#[serde(default)]
sep: Sep,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct Entry {
shape: Shape,
#[serde(default)]
allowed: Vec<String>,
#[serde(default)]
path_flags: Vec<PathFlagEntry>,
#[allow(dead_code)] because: String,
#[allow(dead_code)] #[serde(default)]
measured: Option<bool>,
}
#[derive(Deserialize, Debug)]
struct Table {
env: HashMap<String, Entry>,
}
#[derive(Clone)]
pub(crate) struct Rule {
pub(crate) shape: Shape,
allowed: Vec<String>,
path_flags: Vec<PathFlagEntry>,
}
struct Compiled {
exact: HashMap<String, Rule>,
globs: Vec<(String, String, Rule)>,
}
static TABLE: LazyLock<Compiled> = LazyLock::new(|| {
let src = include_str!("../envvars.toml");
let parsed: Table = toml::from_str(src).expect("embedded envvars.toml must parse");
let mut exact = HashMap::new();
let mut globs = Vec::new();
for (name, entry) in parsed.env {
match name.split_once('*') {
Some((pre, suf)) => {
assert!(!suf.contains('*'), "envvars.toml: `{name}` has more than one `*`");
globs.push((pre.to_string(), suf.to_string(),
Rule { shape: entry.shape, allowed: entry.allowed, path_flags: entry.path_flags }));
}
None => {
exact.insert(name, Rule { shape: entry.shape, allowed: entry.allowed, path_flags: entry.path_flags });
}
}
}
Compiled { exact, globs }
});
fn rule_of(name: &str) -> Option<&'static Rule> {
if let Some(r) = TABLE.exact.get(name) {
return Some(r);
}
TABLE.globs.iter().find_map(|(pre, suf, rule)| {
(name.len() >= pre.len() + suf.len() && name.starts_with(pre.as_str()) && name.ends_with(suf.as_str()))
.then_some(rule)
})
}
#[cfg(test)]
pub(crate) fn shape_of(name: &str) -> Option<Shape> {
rule_of(name).map(|r| r.shape)
}
pub(crate) fn assignment_verdict(name: &str, value: &str) -> Verdict {
let name = name.strip_suffix('+').unwrap_or(name);
let Some(rule) = rule_of(name) else {
return Verdict::Allowed(SafetyLevel::Inert);
};
if value.is_empty() {
return Verdict::Allowed(SafetyLevel::Inert);
}
match rule.shape {
Shape::Command => crate::command_verdict(value),
Shape::ExecPath => exec_path_verdict(value),
Shape::DataPath => data_path_verdict(value),
Shape::Opaque => Verdict::Denied,
Shape::OptionString => option_string_verdict(value, &rule.allowed, &rule.path_flags),
}
}
fn option_string_verdict(value: &str, allowed: &[String], path_flags: &[PathFlagEntry]) -> Verdict {
let raw: Vec<&str> = value.split_whitespace().collect();
let mut verdict = Verdict::Allowed(SafetyLevel::Inert);
let mut i = 0;
while i < raw.len() {
if let Some((role, path, used)) = path_flag_match(raw[i], raw.get(i + 1).copied(), path_flags)
{
if path.is_empty() {
return Verdict::Denied;
}
verdict = verdict.combine(gate_path(role, path));
i += used;
continue;
}
let joined;
let unit = if raw[i].len() <= 2
&& raw[i].starts_with('-')
&& i + 1 < raw.len()
&& !raw[i + 1].starts_with('-')
{
joined = format!("{}{}", raw[i], raw[i + 1]);
i += 2;
joined.as_str()
} else {
i += 1;
raw[i - 1]
};
if !allowed.iter().any(|a| flag_matches(unit, a)) {
return Verdict::Denied;
}
}
verdict
}
fn path_flag_match<'a>(
tok: &'a str,
next: Option<&'a str>,
path_flags: &[PathFlagEntry],
) -> Option<(PathRole, &'a str, usize)> {
path_flags.iter().find_map(|pf| match pf.sep {
Sep::Equals => tok
.split_once('=')
.filter(|(f, _)| *f == pf.flag)
.map(|(_, v)| (pf.role, v, 1)),
Sep::Space => (tok == pf.flag).then_some(match next {
Some(v) if !v.starts_with('-') => (pf.role, v, 2),
_ => (pf.role, "", 1),
}),
})
}
fn gate_path(role: PathRole, path: &str) -> Verdict {
match role {
PathRole::Read => data_path_verdict(path),
PathRole::Write => crate::engine::resolve::write_target_verdict(path),
PathRole::Exec => exec_path_verdict(path),
}
}
fn flag_matches(token: &str, entry: &str) -> bool {
match entry.strip_suffix('*') {
Some(prefix) => token.starts_with(prefix),
None => token == entry || token.strip_prefix(entry).is_some_and(|r| r.starts_with('=')),
}
}
fn exec_path_verdict(value: &str) -> Verdict {
worst_element(value, crate::engine::resolve::execute_file_verdict)
}
fn data_path_verdict(value: &str) -> Verdict {
worst_element(value, crate::engine::resolve::read_content_verdict)
}
fn worst_element(value: &str, judge: fn(&str) -> Verdict) -> Verdict {
value
.split(':')
.filter(|s| !s.is_empty())
.map(judge)
.fold(Verdict::Allowed(SafetyLevel::Inert), Verdict::combine)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_table_parses_and_covers_the_measured_vectors() {
for name in [
"LD_PRELOAD", "DYLD_INSERT_LIBRARIES", "BASH_ENV", "PYTHONPATH", "PHPRC",
"PHP_INI_SCAN_DIR", "RUSTC_WRAPPER", "GIT_SSH_COMMAND", "GIT_DIR",
"DOTNET_STARTUP_HOOKS", "PERL5LIB", "RUBYLIB",
"CLASSPATH", "JAVA_TOOL_OPTIONS", "_JAVA_OPTIONS", "JDK_JAVA_OPTIONS", "GOFLAGS",
"LUA_INIT", "JULIA_LOAD_PATH", "JULIA_DEPOT_PATH", "R_PROFILE_USER",
] {
assert!(shape_of(name).is_some(), "envvars.toml is missing a measured vector: {name}");
}
}
#[test]
fn an_unlisted_name_is_inert() {
assert!(shape_of("PROJECT").is_none());
assert!(shape_of("NODE_ENV").is_none());
assert!(shape_of("RUSTUP_TOOLCHAIN").is_none());
for n in ["PROJECT", "NODE_ENV", "RUSTUP_TOOLCHAIN", "WRITE", "FOO"] {
assert_eq!(assignment_verdict(n, "anything"), Verdict::Allowed(SafetyLevel::Inert));
}
}
#[test]
fn a_name_glob_matches_the_variable_segment() {
assert_eq!(shape_of("CARGO_TARGET_X86_64_APPLE_DARWIN_RUNNER"), Some(Shape::Command));
assert_eq!(shape_of("CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER"), Some(Shape::Command));
assert_eq!(shape_of("GIT_CONFIG_KEY_0"), Some(Shape::Opaque));
assert_eq!(shape_of("GIT_CONFIG_VALUE_17"), Some(Shape::Opaque));
assert_eq!(shape_of("CARGO_TARGET_DIR"), None);
assert_eq!(shape_of("CARGO_TERM_COLOR"), None);
}
#[test]
fn a_glob_needs_a_real_middle_segment() {
assert_eq!(shape_of("CARGO_TARGET__RUNNER"), Some(Shape::Command), "empty middle is still a match");
assert_eq!(shape_of("CARGO_TARGET_RUNNER"), None, "prefix and suffix must not overlap");
}
#[test]
fn an_empty_value_is_inert_even_for_a_listed_name() {
for n in ["GIT_PAGER", "LD_PRELOAD", "GIT_DIR"] {
assert_eq!(assignment_verdict(n, ""), Verdict::Allowed(SafetyLevel::Inert));
}
}
}
#[cfg(test)]
mod integration_tests {
#[test]
fn every_measured_injection_vector_denies() {
for cmd in [
"LD_PRELOAD=/tmp/evil.so ls",
"LD_AUDIT=/tmp/evil.so ls",
"DYLD_INSERT_LIBRARIES=/tmp/evil.dylib cat notes.txt",
"BASH_ENV=/tmp/evil.sh bash -c ls",
"PYTHONPATH=/tmp/inj python3 --version",
"PHPRC=/tmp/evil/php.ini php --version",
"PHP_INI_SCAN_DIR=/tmp/evil php --version",
"RUSTC_WRAPPER=/tmp/evil cargo build",
"CARGO_BUILD_RUSTC_WRAPPER=/tmp/evil cargo build",
"DOTNET_STARTUP_HOOKS=/tmp/evil.dll dotnet --info",
"GIT_SSH_COMMAND='sh -c evil' git status",
"GIT_DIR=/tmp/evil git log",
"PERL5LIB=/tmp/inj perl --version",
"RUBYLIB=/tmp/inj ruby --version",
"CLASSPATH=/tmp/evil java Run",
"JAVA_TOOL_OPTIONS='-javaagent:/tmp/e.jar' java -version",
"_JAVA_OPTIONS='-javaagent:/tmp/e.jar' java -version",
"JDK_JAVA_OPTIONS='-javaagent:/tmp/e.jar' java -version",
"JAVA_TOOL_OPTIONS='-XX:OnOutOfMemoryError=/tmp/e.sh' java -version",
"JAVA_TOOL_OPTIONS='-XX:OnError=/tmp/e.sh' java -version",
"GOFLAGS='-toolexec=/tmp/evil' go list ./...",
"GOFLAGS='-overlay=/tmp/o.json' go list ./...",
"GOFLAGS='-modfile=/tmp/alt.mod' go list ./...",
"LUA_INIT='os.execute(\"id\")' ls",
"LUA_INIT=@/tmp/init.lua ls",
"JULIA_LOAD_PATH=/tmp/evil ls",
"JULIA_DEPOT_PATH=/tmp/evil ls",
"R_PROFILE_USER=/tmp/evil.R ls",
] {
assert!(!crate::is_safe_command(cmd), "a measured injection vector was allowed: {cmd}");
}
}
#[test]
fn an_unlisted_assignment_changes_nothing() {
for cmd in [
"FOO=bar ls",
"RUSTUP_TOOLCHAIN=stable cargo test",
"RACK_ENV=test bundle install",
"RAILS_ENV=test bundle exec rspec",
"NODE_ENV=production npm ci --ignore-scripts",
"PROJECT=safe-chains QUERY=x ls",
"TZ=UTC date",
] {
assert!(crate::is_safe_command(cmd), "an unlisted assignment caused a denial: {cmd}");
}
}
#[test]
fn a_listed_name_with_a_benign_value_still_allows() {
for cmd in [
"GIT_PAGER=cat git log",
"GIT_PAGER= git log", "GIT_DIR=.git git log",
"PYTHONPATH=./lib ls",
"PERL5LIB=./lib ls",
"LD_PRELOAD= ls",
] {
assert!(crate::is_safe_command(cmd), "a benign value was denied: {cmd}");
}
}
#[test]
fn a_path_list_is_judged_by_its_worst_element() {
assert!(crate::is_safe_command("PYTHONPATH=./a:./b ls"));
assert!(!crate::is_safe_command("PYTHONPATH=./a:/tmp/evil ls"));
assert!(!crate::is_safe_command("PYTHONPATH=/tmp/evil:./b ls"));
}
#[test]
fn an_env_twin_of_a_denied_flag_also_denies() {
for (flag_form, env_form) in [
("cargo test --config target.x86_64-apple-darwin.runner=/tmp/evil",
"CARGO_TARGET_X86_64_APPLE_DARWIN_RUNNER=/tmp/evil cargo test"),
("cargo build --config build.rustc-wrapper=/tmp/evil",
"RUSTC_WRAPPER=/tmp/evil cargo build"),
("git -c core.pager='sh -c evil' log",
"GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.pager GIT_CONFIG_VALUE_0='sh -c evil' git log"),
] {
assert!(!crate::is_safe_command(flag_form), "sanity: the flag form should deny: {flag_form}");
assert!(!crate::is_safe_command(env_form), "the env twin was allowed: {env_form}");
}
}
#[test]
fn a_twin_with_a_benign_value_still_allows() {
assert!(crate::is_safe_command("CARGO_TARGET_X86_64_APPLE_DARWIN_RUNNER=echo cargo test"));
assert!(crate::is_safe_command("CARGO_TERM_COLOR=always cargo test"));
assert!(crate::is_safe_command("CARGO_TARGET_DIR=./target cargo build"));
}
#[test]
fn an_option_string_denies_a_flag_that_is_not_allowlisted() {
for cmd in [
"RUSTFLAGS='-C linker=/tmp/evil' cargo build",
"CARGO_BUILD_RUSTFLAGS='-Clinker=/tmp/evil' cargo build",
"NODE_OPTIONS='--require /tmp/inj.js' node app.js",
"NODE_OPTIONS='--import file:///tmp/inj.mjs' node app.js",
"RUBYOPT='-r/tmp/inj' ruby app.rb",
"PERL5OPT='-I/tmp -MInject' perl app.pl",
"PERL5OPT='-I/tmp -d:Inj' perl app.pl",
"NODE_OPTIONS='--inspect=0.0.0.0:9229' node app.js",
"NODE_OPTIONS='--allow-child-process' node app.js",
"JAVA_TOOL_OPTIONS='-javaagent:/tmp/e.jar' java -version",
"JAVA_TOOL_OPTIONS='-agentlib:jdwp=transport=dt_socket' java -version",
"JAVA_TOOL_OPTIONS='-agentpath:/tmp/e.dylib' java -version",
"JAVA_TOOL_OPTIONS='-Xbootclasspath/a:/tmp/e.jar' java -version",
"JAVA_TOOL_OPTIONS='-cp /tmp/evil' java -version",
"JAVA_TOOL_OPTIONS='--module-path /tmp/evil' java -version",
"JAVA_TOOL_OPTIONS='-XX:OnError=id' java -version",
"JAVA_TOOL_OPTIONS='-XX:OnOutOfMemoryError=id' java -version",
"JAVA_TOOL_OPTIONS='-XX:VMOptionsFile=/tmp/o' java -version",
"JAVA_TOOL_OPTIONS='-Djava.rmi.server.codebase=http://x/' java -version",
"_JAVA_OPTIONS='-javaagent:/tmp/e.jar' java -version",
"JDK_JAVA_OPTIONS='-XX:OnError=id' java -version",
"GOFLAGS='-toolexec=/tmp/evil' ls",
"GOFLAGS='-exec=/tmp/evil' ls",
"GOFLAGS='-overlay=/tmp/o.json' ls",
"GOFLAGS='-ldflags=-linkmode=external' ls",
"GOFLAGS='-gcflags=-N' ls",
] {
assert!(!crate::is_safe_command(cmd), "an option-string vector was allowed: {cmd}");
}
}
#[test]
fn an_option_string_allows_the_researched_inert_flags() {
for cmd in [
"RUSTFLAGS='-D warnings' cargo build",
"RUSTFLAGS='-C opt-level=3' cargo build",
"RUSTFLAGS='-Copt-level=3 -Cdebuginfo=0' cargo build",
"RUSTDOCFLAGS='-D warnings' cargo doc",
"NODE_OPTIONS='--max-old-space-size=4096' node app.js",
"NODE_OPTIONS='--enable-source-maps' node app.js",
"RUBYOPT='-w' ruby app.rb",
"PERL5OPT='-w -T' ls",
"JAVA_TOOL_OPTIONS='-Xmx2g' java -version",
"JAVA_TOOL_OPTIONS='-Xms512m -Xmx4g -XX:+UseG1GC' java -version",
"JAVA_TOOL_OPTIONS='-XX:MaxGCPauseMillis=200' java -version",
"JAVA_TOOL_OPTIONS='-verbose:gc' java -version",
"_JAVA_OPTIONS='-Xmx2g' java -version",
"JDK_JAVA_OPTIONS='-Xss4m' java -version",
"GOFLAGS='-mod=readonly -v' ls",
"GOFLAGS='-count=1' ls",
"GOFLAGS='-tags=integration -race' ls",
] {
assert!(crate::is_safe_command(cmd), "an inert flag string was denied: {cmd}");
}
}
#[test]
fn the_jvm_option_variables_classify_identically() {
let names = ["JAVA_TOOL_OPTIONS", "_JAVA_OPTIONS", "JDK_JAVA_OPTIONS"];
let rules: Vec<_> = names
.iter()
.map(|n| super::rule_of(n).unwrap_or_else(|| panic!("{n} is missing from envvars.toml")))
.collect();
for (name, rule) in names.iter().zip(&rules) {
assert_eq!(rule.shape, rules[0].shape, "{name} disagrees on shape");
assert_eq!(rule.allowed, rules[0].allowed, "{name} has a divergent allowlist");
if *name == "JDK_JAVA_OPTIONS" {
for pf in &rules[0].path_flags {
assert!(
rule.path_flags.contains(pf),
"JDK_JAVA_OPTIONS dropped the path flag {}, so the two spellings disagree",
pf.flag,
);
}
} else {
assert_eq!(rule.path_flags, rules[0].path_flags, "{name} has divergent path flags");
}
}
assert!(!rules[0].allowed.is_empty(), "an empty allowlist would deny every JVM tuning flag");
}
#[test]
fn a_listed_flag_does_not_admit_a_longer_flag_that_merely_starts_the_same_way() {
for cmd in [
"GOFLAGS='-modfile=/tmp/evil.mod' go list ./...",
"GOFLAGS='-vet=off' go vet ./...",
"GOFLAGS='-nolocalimports' go list ./...",
"GOFLAGS='-runtime=x' go list ./...",
"RUSTDOCFLAGS='--cfgevil' cargo doc",
] {
assert!(!crate::is_safe_command(cmd), "a longer flag was admitted by prefix: {cmd}");
}
for cmd in [
"GOFLAGS='-mod=vendor' go list ./...",
"GOFLAGS='-v -x' go list ./...",
"GOFLAGS=-trimpath go list ./...",
] {
assert!(crate::is_safe_command(cmd), "a listed flag was denied: {cmd}");
}
}
#[test]
fn a_declared_path_flag_is_gated_by_its_locus() {
for cmd in [
"RUSTFLAGS='-Cincremental=/etc/cron.d' cargo build",
"CARGO_BUILD_RUSTFLAGS='-Cincremental=/etc/x' cargo build",
"JAVA_TOOL_OPTIONS='-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/etc/x' java -version",
"RUSTFLAGS='-Cincremental=' cargo build",
] {
assert!(!crate::is_safe_command(cmd), "an out-of-worktree path was allowed: {cmd}");
}
for cmd in [
"RUSTFLAGS='-Cincremental=./target/inc' cargo build",
"RUSTFLAGS='-Copt-level=3 -Cincremental=./target/inc' cargo build",
"JAVA_TOOL_OPTIONS='-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./dump.hprof' java -version",
] {
assert!(crate::is_safe_command(cmd), "an in-worktree path was denied: {cmd}");
}
}
#[test]
fn every_spelling_of_an_assignment_agrees() {
const BAD: &str = "/tmp/evil";
let mut checked = 0;
let names: Vec<String> = super::TABLE
.exact
.keys()
.cloned()
.chain(super::TABLE.globs.iter().map(|(pre, suf, _)| format!("{pre}X{suf}")))
.collect();
for name in names {
if crate::is_safe_command(&format!("{name}={BAD} ls")) {
continue;
}
checked += 1;
for spelling in [
format!("env {name}={BAD} ls"),
format!("export {name}={BAD}"),
format!("export {name}={BAD} && ls"),
format!("declare -x {name}={BAD}"),
format!("typeset -x {name}={BAD}"),
format!("export {name}+={BAD}"),
format!("declare -x {name}+={BAD}"),
format!("env {name}+={BAD} ls"),
format!("mise set {name}={BAD}"),
] {
assert!(
!crate::is_safe_command(&spelling),
"`{name}={BAD}` denies as a prefix but `{spelling}` was allowed — one spelling \
of the assignment escapes classification",
);
}
}
assert!(checked > 20, "only {checked} variables exercised; the table was not walked");
}
#[test]
fn the_alternate_spellings_carry_the_same_level() {
for (prefix, alternate) in [
("PYTHONPATH=./lib true", "export PYTHONPATH=./lib"),
("RUSTFLAGS='-Cincremental=./x' true", "export RUSTFLAGS='-Cincremental=./x'"),
("FOO=bar true", "export FOO=bar"),
] {
assert_eq!(
crate::command_verdict(prefix),
crate::command_verdict(alternate),
"`{prefix}` and `{alternate}` disagree on level",
);
}
}
#[test]
fn an_assignments_level_reaches_the_commands_verdict() {
use crate::verdict::{SafetyLevel, Verdict};
assert_eq!(
crate::command_verdict("RUSTFLAGS='-Cincremental=./x' echo hi"),
crate::command_verdict("touch ./x"),
"an env-spelled worktree write must classify as that write does",
);
assert_eq!(
crate::command_verdict("RUSTFLAGS='-Cincremental=./x' echo hi"),
Verdict::Allowed(SafetyLevel::SafeWrite),
);
for cmd in ["RUSTFLAGS='-Copt-level=3' echo hi", "FOO=bar echo hi", "echo hi"] {
assert_eq!(
crate::command_verdict(cmd),
Verdict::Allowed(SafetyLevel::Inert),
"an inert assignment escalated: {cmd}",
);
}
}
#[test]
fn a_classpath_flag_agrees_with_the_classpath_variable() {
for (env_form, flag_form) in [
("CLASSPATH=./lib mvn test", "JDK_JAVA_OPTIONS='-cp ./lib' mvn test"),
("CLASSPATH=/tmp/evil mvn test", "JDK_JAVA_OPTIONS='-cp /tmp/evil' mvn test"),
("CLASSPATH=./lib gradle build", "JDK_JAVA_OPTIONS='-classpath ./lib' gradle build"),
("CLASSPATH=~/.m2 mvn test", "JDK_JAVA_OPTIONS='--class-path ~/.m2' mvn test"),
] {
assert_eq!(
crate::is_safe_command(env_form),
crate::is_safe_command(flag_form),
"the two spellings of one capability disagree: `{env_form}` vs `{flag_form}`",
);
}
assert!(crate::is_safe_command("JDK_JAVA_OPTIONS='-cp ./lib' mvn test"), "worktree classpath");
assert!(!crate::is_safe_command("JDK_JAVA_OPTIONS='-cp /tmp/evil' mvn test"), "foreign classpath");
assert!(!crate::is_safe_command("JDK_JAVA_OPTIONS='-cp ./lib:/tmp/evil' mvn test"));
assert!(!crate::is_safe_command("JDK_JAVA_OPTIONS='-cp' mvn test"));
for cmd in [
"JDK_JAVA_OPTIONS='-cp -XX:OnError=/tmp/e.sh' mvn test",
"JDK_JAVA_OPTIONS='-cp -javaagent:/tmp/e.jar' mvn test",
"JDK_JAVA_OPTIONS='-cp -Xmx4g' mvn test",
] {
assert!(!crate::is_safe_command(cmd), "a path flag swallowed a flag as its value: {cmd}");
}
for cmd in ["JAVA_TOOL_OPTIONS='-cp ./lib' mvn test", "_JAVA_OPTIONS='-cp ./lib' mvn test"] {
assert!(!crate::is_safe_command(cmd), "a launcher-only flag was admitted to the VM pair: {cmd}");
}
}
#[test]
fn an_undeclared_path_flag_still_denies() {
for cmd in [
"RUSTFLAGS='-Cprofile-use=./x.profdata' cargo build",
"JAVA_TOOL_OPTIONS='-XX:VMOptionsFile=./opts' java -version",
"JAVA_TOOL_OPTIONS='-XX:CompilerDirectivesFile=./d.json' java -version",
"GOFLAGS='-overlay=./o.json' go list ./...",
"RUSTFLAGS='-Cincremental' cargo build",
"JAVA_TOOL_OPTIONS='-XX:HeapDumpPath' java -version",
] {
assert!(!crate::is_safe_command(cmd), "an undeclared path flag was allowed: {cmd}");
}
}
#[test]
fn no_path_flag_is_also_listed_as_an_ungated_flag() {
let mut checked = 0;
for rule in super::TABLE.exact.values().chain(super::TABLE.globs.iter().map(|(_, _, r)| r)) {
for pf in &rule.path_flags {
checked += 1;
assert!(
!rule.allowed.iter().any(|a| super::flag_matches(&pf.flag, a)),
"{} is declared as a path flag AND matched by `allowed`, so its value escapes \
the locus gate",
pf.flag,
);
}
}
assert!(checked > 0, "no path flags seen; the table was not walked");
}
#[test]
fn option_string_fields_appear_only_on_option_string_entries() {
for rule in super::TABLE.exact.values().chain(super::TABLE.globs.iter().map(|(_, _, r)| r)) {
if rule.shape == super::Shape::OptionString {
assert!(
!rule.allowed.is_empty(),
"an option-string entry with an empty `allowed` denies every value; say so with \
a different shape instead"
);
continue;
}
assert!(rule.allowed.is_empty(), "`allowed` is set on a non-option-string entry");
assert!(rule.path_flags.is_empty(), "`path_flags` is set on a non-option-string entry");
}
}
#[test]
fn every_option_string_entry_is_a_nonempty_flag() {
for rule in super::TABLE.exact.values().chain(super::TABLE.globs.iter().map(|(_, _, r)| r)) {
if rule.shape != super::Shape::OptionString {
continue;
}
for entry in &rule.allowed {
let core = entry.strip_suffix('*').unwrap_or(entry);
assert!(!core.is_empty(), "a bare `*` entry admits every token of the option string");
assert!(
core.starts_with('-'),
"`{entry}` does not start with `-`, so it matches bare value tokens, not a flag"
);
}
for pf in &rule.path_flags {
assert!(pf.flag.starts_with('-'), "path flag `{}` is not a flag", pf.flag);
}
}
}
#[test]
fn every_option_string_entry_obeys_its_own_star() {
let mut checked = 0;
for rule in super::TABLE.exact.values().chain(super::TABLE.globs.iter().map(|(_, _, r)| r)) {
if rule.shape != super::Shape::OptionString {
continue;
}
for entry in &rule.allowed {
checked += 1;
match entry.strip_suffix('*') {
Some(prefix) => assert!(
super::flag_matches(&format!("{prefix}zz"), entry),
"{entry} is starred but rejects its own extension"
),
None => {
assert!(super::flag_matches(entry, entry), "{entry} rejects itself");
assert!(
!super::flag_matches(&format!("{entry}zz"), entry),
"{entry} admits the unrelated flag {entry}zz — star it only if the \
value glues on with no delimiter"
);
}
}
}
}
assert!(checked > 50, "only {checked} entries seen; the table was not walked");
}
#[test]
fn lua_init_denies_in_both_of_its_forms() {
assert!(!crate::is_safe_command("LUA_INIT='os.execute(\"id\")' ls"));
assert!(!crate::is_safe_command("LUA_INIT=@./init.lua ls"));
}
#[test]
fn julia_paths_follow_the_executor_locus() {
assert!(crate::is_safe_command("JULIA_LOAD_PATH=./deps ls"));
assert!(!crate::is_safe_command("JULIA_LOAD_PATH=/tmp/evil ls"));
assert!(!crate::is_safe_command("JULIA_DEPOT_PATH=/tmp/evil ls"));
}
#[test]
fn a_split_short_flag_is_judged_as_one_unit() {
assert!(!crate::is_safe_command("RUSTFLAGS='-C linker=/tmp/evil' cargo build"));
assert!(!crate::is_safe_command("RUSTFLAGS='-Clinker=/tmp/evil' cargo build"));
assert!(crate::is_safe_command("RUSTFLAGS='-C opt-level=3' cargo build"));
assert!(crate::is_safe_command("RUSTFLAGS='-Copt-level=3' cargo build"));
}
#[test]
fn loading_from_temp_denies_even_though_reading_it_allows() {
assert!(crate::is_safe_command("cat /tmp/evil.so"), "reading /tmp is ordinary");
assert!(!crate::is_safe_command("LD_PRELOAD=/tmp/evil.so ls"), "loading it must not be");
}
}