diff --git a/judge/crates/sandbox/src/acceptance.rs b/judge/crates/sandbox/src/acceptance.rs index 3e45715..8b27a6d 100644 --- a/judge/crates/sandbox/src/acceptance.rs +++ b/judge/crates/sandbox/src/acceptance.rs @@ -7,6 +7,7 @@ use crate::{ spec::{ExecutionSpec, SpecPolicy}, }; use std::{fs, io, io::Read, os::unix::fs::PermissionsExt, path::Path}; +pub(crate) mod isolation; mod normal; mod resources; pub(crate) fn verify_guest(require_env: bool) -> io::Result<()> { @@ -121,6 +122,9 @@ pub fn run_case(case: &str) -> io::Result { if case.starts_with("resource-") { return resources::run_case(case); } + if case.starts_with("isolation-") { + return isolation::run_case(case); + } let rollback = case.strip_prefix("cancel-").filter(|s| { matches!( *s, diff --git a/judge/crates/sandbox/src/acceptance/isolation.rs b/judge/crates/sandbox/src/acceptance/isolation.rs new file mode 100644 index 0000000..a766d8d --- /dev/null +++ b/judge/crates/sandbox/src/acceptance/isolation.rs @@ -0,0 +1,86 @@ +//! Fixed complete-boundary probes inside the independent, bounded VM only. +use super::{Fixture, describe}; +use crate::{ + launcher::run::{self, MonitorMode}, + spec::{ExecutionSpec, InputArtifact, SpecPolicy}, +}; +use std::{fs, io, os::unix::fs::symlink}; + +pub(super) fn run_case(case: &str) -> io::Result { + if !matches!(case, "isolation-files" | "isolation-inherited-fd") { + return Err(io::Error::other("unknown isolation case")); + } + let fixture = Fixture::new("/kf/bin/isolation-probe")?; + let config = fixture.config()?; + let image = &config.config().image_root; + fs::write(format!("{image}/image-sentinel"), b"readonly-image")?; + fs::write("/kf/outside-sentinel", b"outside-private-root")?; + symlink("../../kf/outside-sentinel", format!("{image}/escape-image"))?; + fs::write( + format!("{}/data-001", fixture.publications), + b"protected-input", + )?; + fs::write( + format!("{}/stdin-001", fixture.publications), + b"private-stdin", + )?; + let mut request: ExecutionSpec = + serde_json::from_str(include_str!("../../tests/fixtures/spec.valid.json"))?; + request.run_id = case.into(); + request.argv.push("files".into()); + request.inputs.push(InputArtifact { + artifact_id: "data-001".into(), + path: "source/input".into(), + }); + request.stdin_artifact = Some("stdin-001".into()); + request.limits.cpu_time_us = 2_000_000; + request.limits.wall_time_us = 8_000_000; + let spec = request + .validate(&SpecPolicy::default()) + .map_err(io::Error::other)?; + let root = fixture.root()?; + // The fresh privileged exec role must refuse, rather than inherit a caller + // directory capability even when it was accidentally left without CLOEXEC. + let inherited = if case == "isolation-inherited-fd" { + let fd = rustix::fs::open( + "/kf", + rustix::fs::OFlags::RDONLY | rustix::fs::OFlags::DIRECTORY, + rustix::fs::Mode::empty(), + )?; + rustix::io::fcntl_setfd(&fd, rustix::io::FdFlags::empty())?; + Some(fd) + } else { + None + }; + let result = run::execute( + &config, + &spec, + &std::env::current_exe()?, + &root, + MonitorMode::Normal, + ); + drop(inherited); + fs::remove_file(format!("{image}/escape-image"))?; + if fs::read("/kf/outside-sentinel")? != b"outside-private-root" + || fs::read(format!("{image}/image-sentinel"))? != b"readonly-image" + || fs::read(format!("{}/data-001", fixture.publications))? != b"protected-input" + { + return Err(io::Error::other("outside or input sentinel changed")); + } + if case == "isolation-inherited-fd" { + let failure = result + .err() + .ok_or_else(|| io::Error::other("inherited directory not refused"))?; + if failure.cleanup_failure.is_some() { + return Err(io::Error::other("inherited descriptor cleanup failed")); + } + return Ok( + serde_json::json!({"case":case,"refused_before_exec":true,"failure":failure.failure,"cleanup_failure":failure.cleanup_failure,"outside_sentinels_unchanged":true}), + ); + } + let completed = + result.map_err(|e| io::Error::other(format!("isolation preparation: {:?}", e.failure)))?; + let mut value = describe(case, &spec, &completed, false)?; + value["outside_sentinels_unchanged"] = true.into(); + Ok(value) +} diff --git a/judge/crates/sandbox/tests/fixtures/isolation_probe.c b/judge/crates/sandbox/tests/fixtures/isolation_probe.c new file mode 100644 index 0000000..6607240 --- /dev/null +++ b/judge/crates/sandbox/tests/fixtures/isolation_probe.c @@ -0,0 +1,70 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +static void check(int yes, const char *why) { + if (!yes) { fprintf(stderr,"ISOLATION_FAIL %s errno=%d\n",why,errno); exit(1); } +} +static void absent(const char *path) { + int fd=open(path,O_RDONLY|O_CLOEXEC); + check(fd<0&&(errno==ENOENT||errno==EACCES||errno==EPERM),path); +} +static void files(void) { + check(getpid()==2 && getuid()>=100000 && geteuid()==getuid(),"namespace uid"); + check(prctl(PR_GET_NO_NEW_PRIVS,0,0,0,0)==1 && prctl(PR_GET_SECCOMP)==2,"NNP/filter"); + char cwd[128];check(getcwd(cwd,sizeof cwd)&&!strcmp(cwd,"/work"),"initial cwd"); + check(!getenv("KF_SANDBOX_INDEPENDENT_VM")&&!getenv("HOME")&&!getenv("LD_PRELOAD"),"cleared environment"); + const char *hidden[]={"/kf/outside-sentinel","/var/lib/kf-judge/tasks","/sys","/.old-root","/escape-image","/proc/sys","/proc/sysrq-trigger","/proc/kcore","/proc/keys","/proc/meminfo","/proc/cpuinfo","/proc/1/root","/proc/1/fd/3","/proc/1/environ","/proc/self/root/../../kf/outside-sentinel","/proc/self/cwd/../../kf/outside-sentinel","/work/stdin-001","/work/input-001"}; + for(size_t i=0;i