#[derive(Debug, Clone)]
pub struct VerdictInputs {
pub dereference: bool,
}
#[derive(Debug, Clone)]
pub struct Verdict {
pub safe: bool,
pub reasons: Vec<String>,
pub caveats: Vec<String>,
}
impl Verdict {
pub fn render(&self) -> String {
let mut out = String::new();
if self.safe {
out.push_str("TOCTOU status: SAFE\n");
} else {
out.push_str("TOCTOU status: NOT SAFE\n");
for reason in &self.reasons {
out.push_str(&format!(" Reason: {}\n", reason));
}
}
for caveat in &self.caveats {
out.push_str(&format!(" Note: {}\n", caveat));
}
out
}
}
pub fn toctou_verdict(inputs: &VerdictInputs) -> Verdict {
let linux_build = cfg!(target_os = "linux");
let safe = !inputs.dereference && linux_build;
let mut reasons = Vec::new();
if inputs.dereference {
reasons.push(
"--dereference/-L follows symlinks by request, so a swapped link is followed \
— not hardened under privilege asymmetry"
.to_string(),
);
}
if !linux_build {
reasons
.push("the TOCTOU-hardened path is Linux-only; this build does not use it".to_string());
}
let caveats = vec![
"Hardening assumes the directory named on the command line (and the path components \
above it) are not modifiable by a less-privileged actor; it protects everything at or \
below the named root. Also assumes fs.protected_hardlinks=1 (Linux default)."
.to_string(),
];
Verdict {
safe,
reasons,
caveats,
}
}
#[derive(Debug)]
pub enum LinterAction {
Exit { output: String, code: i32 },
Proceed,
}
pub fn run_linter(
dereference: bool,
toctou_check: bool,
require_toctou_safe: bool,
) -> LinterAction {
if !toctou_check && !require_toctou_safe {
return LinterAction::Proceed;
}
let inputs = VerdictInputs { dereference };
let verdict = toctou_verdict(&inputs);
if toctou_check {
let code = if verdict.safe { 0 } else { 1 };
return LinterAction::Exit {
output: verdict.render(),
code,
};
}
if !verdict.safe {
let mut msg = "Refusing to run: invocation is not TOCTOU-safe.\n".to_string();
for reason in &verdict.reasons {
msg.push_str(&format!(" Reason: {}\n", reason));
}
return LinterAction::Exit {
output: msg,
code: 1,
};
}
LinterAction::Proceed
}
pub fn enforce_or_exit(dereference: bool, toctou_check: bool, require_toctou_safe: bool) {
match run_linter(dereference, toctou_check, require_toctou_safe) {
LinterAction::Exit { output, code } => {
print!("{}", output);
std::process::exit(code);
}
LinterAction::Proceed => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_dereference_is_safe() {
let v = toctou_verdict(&VerdictInputs { dereference: false });
if cfg!(target_os = "linux") {
assert!(v.safe, "no-dereference on Linux should be safe");
assert!(v.reasons.is_empty(), "no reasons expected for safe verdict");
} else {
assert!(!v.safe);
}
assert!(
!v.caveats.is_empty(),
"caveats must be present even when safe"
);
}
#[test]
fn dereference_is_not_safe() {
let v = toctou_verdict(&VerdictInputs { dereference: true });
assert!(!v.safe, "dereference must make the verdict not-safe");
assert!(
!v.reasons.is_empty(),
"at least one reason must be present when not safe"
);
assert!(
v.reasons
.iter()
.any(|r| r.contains("dereference") || r.contains("-L")),
"reason must mention dereference/-L, got: {:?}",
v.reasons
);
}
#[test]
fn caveats_always_present() {
for deref in [false, true] {
let v = toctou_verdict(&VerdictInputs { dereference: deref });
assert!(
!v.caveats.is_empty(),
"caveats must be present regardless of verdict (deref={})",
deref
);
assert!(
v.caveats
.iter()
.any(|c| c.contains("named on the command line")),
"trusted-boundary caveat must be present, got: {:?}",
v.caveats
);
}
}
#[test]
fn render_safe_contains_safe() {
let v = toctou_verdict(&VerdictInputs { dereference: false });
let rendered = v.render();
if cfg!(target_os = "linux") {
assert!(
rendered.contains("SAFE"),
"rendered output must contain SAFE: {rendered}"
);
}
}
#[test]
fn render_not_safe_contains_not_safe() {
let v = toctou_verdict(&VerdictInputs { dereference: true });
let rendered = v.render();
assert!(
rendered.contains("NOT SAFE"),
"rendered output must contain NOT SAFE: {rendered}"
);
}
}