use std::path::{Path, PathBuf};
use std::process::Stdio;
use tokio::process::Command;
use crate::error::{Error, Result};
use crate::policy::{Act, Effect, Policy};
use crate::sandbox::{self, RunSpec, Sandbox, SandboxConfig};
use crate::state::{PolicyEvent, Store};
#[derive(Debug, Clone)]
pub enum Verification {
FileContains(String),
FileEquals(String),
CompilesRust,
RustTestPasses {
test_src: String,
},
WorkspaceFileContains {
file: PathBuf,
needle: String,
},
EachCompilesRust(Vec<PathBuf>),
WorkspaceTestPasses {
files: Vec<PathBuf>,
test_src: String,
},
}
pub struct ExecGuard<'a> {
policy: &'a Policy,
trace: Option<(&'a Store, i64, u32)>,
sandbox: Option<SandboxConfig>,
}
impl<'a> ExecGuard<'a> {
pub fn new(policy: &'a Policy) -> Self {
Self {
policy,
trace: None,
sandbox: Some(SandboxConfig::default()),
}
}
pub fn tracing(mut self, store: &'a Store, run_id: i64, step: u32) -> Self {
self.trace = Some((store, run_id, step));
self
}
pub fn sandboxed(mut self, config: SandboxConfig) -> Self {
self.sandbox = Some(config);
self
}
pub fn no_sandbox(mut self) -> Self {
self.sandbox = None;
self
}
fn permissive() -> ExecGuard<'static> {
static PERMISSIVE: std::sync::OnceLock<Policy> = std::sync::OnceLock::new();
ExecGuard {
policy: PERMISSIVE.get_or_init(Policy::permissive),
trace: None,
sandbox: Some(SandboxConfig::default()),
}
}
fn check(&self, program: &str, argv: &[String]) -> Result<()> {
let verdict = self.policy.check(Act::Exec, program);
let full = format!("{program} {}", argv.join(" "));
if let Some((store, run_id, step)) = self.trace {
let mut ev = if verdict.effect == Effect::Allow {
PolicyEvent::decision(step, "exec", &full, "allow", "policy")
} else {
PolicyEvent::refusal(step, "exec", &full)
};
ev.rule = verdict.rule.clone();
ev.layer = verdict.layer.clone();
let _ = store.record_event(run_id, &ev);
}
if verdict.effect == Effect::Allow {
Ok(())
} else {
Err(Error::Refused {
act: "exec".into(),
target: program.to_string(),
rule: verdict.rule,
layer: verdict.layer,
})
}
}
fn record_gate_failure(&self, phase: &str) {
if let Some((store, run_id, step)) = self.trace {
let _ = store.record_sandbox_event(&crate::state::SandboxEvent::gate_phase_failed(
run_id, step, phase,
));
}
}
async fn exec(&self, argv: &[String], workdir: &Path) -> Result<bool> {
match &self.sandbox {
Some(cfg) => {
let sb = sandbox::select(cfg);
let backend = sb.backend();
if let Some((store, run_id, step)) = self.trace {
let _ = store.record_sandbox_event(&crate::state::SandboxEvent::create(
run_id,
step,
backend.as_str(),
));
let _ = store.record_sandbox_event(&crate::state::SandboxEvent::exec(
run_id,
step,
backend.as_str(),
&argv.join(" "),
));
}
let outcome = sb
.run(RunSpec {
argv,
workdir,
limits: &cfg.limits,
allow_network: cfg.allow_network,
})
.await?;
if let Some((store, run_id, step)) = self.trace {
if let Some(cap) = outcome.cap_hit {
let _ = store.record_sandbox_event(&crate::state::SandboxEvent::cap_hit(
run_id,
step,
cap.as_str(),
));
}
let _ = store.record_sandbox_event(&crate::state::SandboxEvent::destroy(
run_id, step,
));
}
Ok(outcome.success())
}
None => {
let status = Command::new(&argv[0])
.args(&argv[1..])
.current_dir(workdir)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.await?;
Ok(status.success())
}
}
}
}
pub const TEST_BINARY: &str = "<test-binary>";
impl Verification {
pub async fn passes(&self, path: &Path, contents: &str) -> Result<bool> {
self.passes_guarded(path, contents, &ExecGuard::permissive()).await
}
pub async fn passes_guarded(
&self,
_path: &Path,
contents: &str,
guard: &ExecGuard<'_>,
) -> Result<bool> {
match self {
Verification::FileContains(needle) => Ok(contents.contains(needle)),
Verification::FileEquals(expected) => Ok(contents == expected),
Verification::CompilesRust => compile_source(contents, None, guard).await,
Verification::RustTestPasses { test_src } => {
compile_source(contents, Some(test_src), guard).await
}
Verification::EachCompilesRust(_)
| Verification::WorkspaceTestPasses { .. }
| Verification::WorkspaceFileContains { .. } => Err(Error::Config(
"multi-file verification requires a workspace root".into(),
)),
}
}
pub async fn passes_in(&self, root: &Path) -> Result<bool> {
self.passes_in_guarded(root, &ExecGuard::permissive()).await
}
pub async fn passes_in_guarded(&self, root: &Path, guard: &ExecGuard<'_>) -> Result<bool> {
match self {
Verification::WorkspaceFileContains { file, needle } => {
let src = tokio::fs::read_to_string(root.join(file)).await.unwrap_or_default();
Ok(src.contains(needle))
}
Verification::EachCompilesRust(files) => {
for f in files {
let src = tokio::fs::read_to_string(root.join(f)).await.unwrap_or_default();
if !compile_source(&src, None, guard).await? {
return Ok(false);
}
}
Ok(true)
}
Verification::WorkspaceTestPasses { files, test_src } => {
let mut combined = String::new();
for f in files {
let src = tokio::fs::read_to_string(root.join(f)).await.unwrap_or_default();
combined.push_str(&src);
combined.push('\n');
}
compile_source(&combined, Some(test_src), guard).await
}
_ => Err(Error::Config(
"single-file verification used in workspace mode".into(),
)),
}
}
pub fn describe(&self) -> String {
match self {
Verification::FileContains(needle) => {
format!("the file must contain exactly this text: {needle:?}")
}
Verification::FileEquals(expected) => {
format!("the file's entire contents must equal exactly: {expected:?}")
}
Verification::CompilesRust => {
"the file must compile as Rust (rustc --crate-type lib)".to_string()
}
Verification::RustTestPasses { test_src } => {
format!("the file must compile and pass this test:\n{test_src}")
}
Verification::WorkspaceFileContains { file, needle } => {
format!("the file {file:?} must contain exactly this text: {needle:?}")
}
Verification::EachCompilesRust(files) => {
format!("each of these files must compile as Rust: {files:?}")
}
Verification::WorkspaceTestPasses { files, test_src } => format!(
"these files {files:?} must together compile and pass this test:\n{test_src}"
),
}
}
}
fn subject_lib_args(subject: &Path, rlib: &Path) -> Vec<String> {
["--edition", "2021", "--crate-type", "lib", "--crate-name", SUBJECT_CRATE]
.iter()
.map(|s| s.to_string())
.chain([subject.display().to_string(), "-o".into(), rlib.display().to_string()])
.collect()
}
fn test_build_args(combined: &Path, bin: &Path) -> Vec<String> {
["--edition", "2021", "--test"]
.iter()
.map(|s| s.to_string())
.chain([combined.display().to_string(), "-o".into(), bin.display().to_string()])
.collect()
}
fn probe_args(dir: &Path, probe: &Path, rlib: &Path) -> Vec<String> {
["--edition", "2021", "--crate-type", "lib", "--emit", "metadata", "--extern"]
.iter()
.map(|s| s.to_string())
.chain([
format!("{SUBJECT_CRATE}={}", rlib.display()),
"--out-dir".into(),
dir.display().to_string(),
probe.display().to_string(),
])
.collect()
}
const SUBJECT_CRATE: &str = "subject";
const PROBE_ITEM: &str = "pub fn __io_harness_probe() {}\n";
const PROBE_CRATE: &str = "extern crate subject;\n\
pub fn __io_harness_check() { subject::__io_harness_probe() }\n";
const CRITERION_OPEN: &str = "\n#[cfg(test)]
mod __io_harness_criterion {
#[allow(unused_imports)]
use ::core::{assert, assert_eq, assert_ne, debug_assert, debug_assert_eq, debug_assert_ne,
matches, panic, todo, unimplemented, unreachable, write, writeln, format_args};
#[allow(unused_imports)]
use ::std::{dbg, eprint, eprintln, format, print, println, vec};
#[allow(unused_imports)] use super::*;
";
const CRITERION_CLOSE: &str = "\n}\n";
async fn compile_source(
source: &str,
test_src: Option<&str>,
guard: &ExecGuard<'_>,
) -> Result<bool> {
let dir = tempfile::tempdir()?;
match test_src {
None => {
let subject = dir.path().join("subject.rs");
tokio::fs::write(&subject, format!("{source}\n{PROBE_ITEM}")).await?;
let rlib = dir.path().join("libsubject.rlib");
let args = subject_lib_args(&subject, &rlib);
guard.check("rustc", &args)?;
let argv = std::iter::once("rustc".to_string())
.chain(args.iter().cloned())
.collect::<Vec<_>>();
if !guard.exec(&argv, dir.path()).await? {
guard.record_gate_failure("subject-compile");
return Ok(false);
}
let probe = dir.path().join("probe.rs");
tokio::fs::write(&probe, PROBE_CRATE).await?;
let args = probe_args(dir.path(), &probe, &rlib);
guard.check("rustc", &args)?;
let argv = std::iter::once("rustc".to_string())
.chain(args.iter().cloned())
.collect::<Vec<_>>();
let passed = guard.exec(&argv, dir.path()).await?;
if !passed {
guard.record_gate_failure("subject-emptied");
}
Ok(passed)
}
Some(test) => {
let subject = dir.path().join("subject.rs");
tokio::fs::write(&subject, format!("{source}\n{PROBE_ITEM}")).await?;
let rlib = dir.path().join("libsubject.rlib");
let args = subject_lib_args(&subject, &rlib);
guard.check("rustc", &args)?;
let subject_argv = std::iter::once("rustc".to_string())
.chain(args.iter().cloned())
.collect::<Vec<_>>();
if !guard.exec(&subject_argv, dir.path()).await? {
guard.record_gate_failure("subject-compile");
return Ok(false);
}
let probe = dir.path().join("probe.rs");
tokio::fs::write(&probe, PROBE_CRATE).await?;
let args = probe_args(dir.path(), &probe, &rlib);
guard.check("rustc", &args)?;
let probe_argv = std::iter::once("rustc".to_string())
.chain(args.iter().cloned())
.collect::<Vec<_>>();
if !guard.exec(&probe_argv, dir.path()).await? {
guard.record_gate_failure("subject-emptied");
return Ok(false);
}
let combined = dir.path().join("combined.rs");
tokio::fs::write(
&combined,
format!("{source}\n{CRITERION_OPEN}{test}{CRITERION_CLOSE}"),
)
.await?;
let bin = dir.path().join("t");
let args = test_build_args(&combined, &bin);
guard.check("rustc", &args)?;
let build_argv = std::iter::once("rustc".to_string())
.chain(args.iter().cloned())
.collect::<Vec<_>>();
if !guard.exec(&build_argv, dir.path()).await? {
guard.record_gate_failure("criterion-compile");
return Ok(false);
}
guard.check(TEST_BINARY, &[bin.display().to_string()])?;
let passed = guard.exec(&[bin.display().to_string()], dir.path()).await?;
if !passed {
guard.record_gate_failure("test-run");
}
Ok(passed)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
async fn passes(v: &Verification, contents: &str) -> bool {
v.passes(&PathBuf::from("unused"), contents).await.unwrap()
}
#[tokio::test]
async fn contains_passes_and_fails() {
let v = Verification::FileContains("fn hello".into());
assert!(passes(&v, "pub fn hello() {}").await);
assert!(!passes(&v, "pub fn world() {}").await);
}
#[tokio::test]
async fn equals_is_exact() {
let v = Verification::FileEquals("a".into());
assert!(passes(&v, "a").await);
assert!(!passes(&v, "a ").await);
}
#[tokio::test]
async fn compiles_rust_rejects_stub_accepts_real() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("hello.rs");
tokio::fs::write(&file, "fn hello").await.unwrap();
assert!(!Verification::CompilesRust.passes(&file, "fn hello").await.unwrap());
let good = "pub fn hello() -> u32 { 42 }\n";
tokio::fs::write(&file, good).await.unwrap();
assert!(Verification::CompilesRust.passes(&file, good).await.unwrap());
}
#[tokio::test]
async fn rust_test_passes_only_when_test_passes() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("hello.rs");
let good = "pub fn hello() -> u32 { 42 }\n";
tokio::fs::write(&file, good).await.unwrap();
let ok = Verification::RustTestPasses {
test_src: "#[test] fn t() { assert_eq!(hello(), 42); }".into(),
};
assert!(ok.passes(&file, good).await.unwrap());
let bad = Verification::RustTestPasses {
test_src: "#[test] fn t() { assert_eq!(hello(), 41); }".into(),
};
assert!(!bad.passes(&file, good).await.unwrap());
}
#[tokio::test]
async fn a_command_absent_from_the_allow_list_is_refused_not_failed() {
let policy = Policy::default().layer("locked").deny_exec("rustc");
let guard = ExecGuard::new(&policy);
let good = "pub fn hello() -> u32 { 42 }\n";
let refused = Verification::CompilesRust
.passes_guarded(Path::new("x.rs"), good, &guard)
.await;
assert!(
matches!(refused, Err(Error::Refused { ref target, .. }) if target == "rustc"),
"expected a typed refusal, got {refused:?}"
);
let allowed = Policy::default();
assert!(Verification::CompilesRust
.passes_guarded(Path::new("x.rs"), good, &ExecGuard::new(&allowed))
.await
.unwrap());
}
#[tokio::test]
async fn denying_the_test_binary_type_checks_without_running_it() {
let policy = Policy::default().layer("no-exec").deny_exec(TEST_BINARY);
let v = Verification::RustTestPasses {
test_src: "#[test] fn t() { assert_eq!(hello(), 42); }".into(),
};
let out = v
.passes_guarded(
Path::new("x.rs"),
"pub fn hello() -> u32 { 42 }\n",
&ExecGuard::new(&policy),
)
.await;
assert!(
matches!(out, Err(Error::Refused { ref target, .. }) if target == TEST_BINARY),
"expected the run of the test binary to be refused, got {out:?}"
);
}
#[tokio::test]
async fn a_verification_with_no_policy_still_spawns_as_0_3_0_did() {
assert!(Verification::CompilesRust
.passes(Path::new("x.rs"), "pub fn hello() -> u32 { 42 }\n")
.await
.unwrap());
}
#[tokio::test]
async fn each_compiles_rust_fails_if_any_file_fails() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
std::fs::write(root.join("a.rs"), "pub fn a() -> u32 { 1 }\n").unwrap();
std::fs::write(root.join("b.rs"), "pub fn b() -> u32 { 2 }\n").unwrap();
let v = Verification::EachCompilesRust(vec!["a.rs".into(), "b.rs".into()]);
assert!(v.passes_in(root).await.unwrap());
std::fs::write(root.join("b.rs"), "pub fn b").unwrap();
assert!(!v.passes_in(root).await.unwrap());
}
#[tokio::test]
async fn workspace_test_passes_only_when_files_work_together() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
std::fs::write(root.join("a.rs"), "pub fn a() -> u32 { 40 }\n").unwrap();
std::fs::write(root.join("b.rs"), "pub fn b() -> u32 { 2 }\n").unwrap();
let v = Verification::WorkspaceTestPasses {
files: vec!["a.rs".into(), "b.rs".into()],
test_src: "#[test] fn t() { assert_eq!(a() + b(), 42); }".into(),
};
assert!(v.passes_in(root).await.unwrap());
std::fs::write(root.join("b.rs"), "pub fn b() -> u32 { 99 }\n").unwrap();
assert!(!v.passes_in(root).await.unwrap());
}
#[tokio::test]
async fn multi_file_variant_errors_in_single_file_mode() {
let v = Verification::EachCompilesRust(vec!["a.rs".into()]);
assert!(v.passes(&PathBuf::from("unused"), "").await.is_err());
}
}