Files
KF-OnlineJudge/judge/crates/sandbox/src/acceptance.rs
T

233 lines
8.5 KiB
Rust

//! Fixed independent-VM acceptance entry; never present in default builds.
//! No caller-selected command, filesystem, artifact directory or configuration.
use crate::{
artifacts::ArtifactRoot,
launcher::run::{self, MonitorMode},
runtime::RuntimeConfig,
spec::{ExecutionSpec, SpecPolicy},
};
use std::{fs, io, io::Read, os::unix::fs::PermissionsExt, path::Path};
mod normal;
pub(crate) fn verify_guest(require_env: bool) -> io::Result<()> {
if (require_env && std::env::var("KF_SANDBOX_INDEPENDENT_VM").as_deref() != Ok("1"))
|| fs::read_to_string("/kf/INDEPENDENT_VM")?.trim() != "isolated-test-guest"
|| !fs::read_to_string("/sys/class/dmi/id/sys_vendor")?.contains("QEMU")
|| ["/mnt/hgfs", "/home/kronecker", "/root/.ssh"]
.iter()
.any(|p| Path::new(p).exists())
{
return Err(io::Error::other("independent VM required"));
}
let mounts = fs::read_to_string("/proc/self/mountinfo")?;
if ["hgfs", "9p", "virtiofs"]
.iter()
.any(|s| mounts.contains(s))
|| fs::read_dir("/sys/class/net")?
.map(|e| e.map(|e| e.file_name()))
.collect::<Result<Vec<_>, _>>()?
!= [std::ffi::OsString::from("lo")]
{
return Err(io::Error::other("guest shares or network"));
}
Ok(())
}
struct Fixture {
cg: String,
publications: String,
}
impl Fixture {
fn new(program: &str) -> io::Result<Self> {
let cg = format!("/sys/fs/cgroup/kf-acceptance-{}", std::process::id());
let publications = format!("/kf/execution-publications-{}", std::process::id());
fs::create_dir(&cg)?;
fs::create_dir(&publications)?;
fs::set_permissions(&cg, fs::Permissions::from_mode(0o700))?;
fs::set_permissions(&publications, fs::Permissions::from_mode(0o700))?;
let fixture = Self { cg, publications };
fs::copy(program, format!("{}/program-001", fixture.publications))?;
fs::write(
format!("{}/input-001", fixture.publications),
vec![0xa5; 65537],
)?;
Ok(fixture)
}
fn root(&self) -> io::Result<ArtifactRoot> {
ArtifactRoot::from_directory(rustix::fs::open(
&self.publications,
rustix::fs::OFlags::PATH | rustix::fs::OFlags::DIRECTORY | rustix::fs::OFlags::CLOEXEC,
rustix::fs::Mode::empty(),
)?)
}
fn config(&self) -> io::Result<crate::runtime::ValidatedRuntimeConfig> {
let mut config: RuntimeConfig = serde_json::from_str(include_str!(
"../../../../deploy/sandbox.runtime.example.json"
))?;
config.delegated_cgroup_root = self.cg.clone();
config.validate().map_err(io::Error::other)
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _ = fs::remove_dir(&self.cg);
let _ = fs::remove_dir_all(&self.publications);
}
}
fn verify_snapshot(file: &crate::artifacts::SnapshotFile, pattern: bool) -> io::Result<()> {
let seals = rustix::fs::fcntl_get_seals(file)?;
if !seals.contains(
rustix::fs::SealFlags::WRITE
| rustix::fs::SealFlags::SHRINK
| rustix::fs::SealFlags::GROW
| rustix::fs::SealFlags::SEAL
| rustix::fs::SealFlags::EXEC,
) || rustix::fs::fcntl_getfl(file)?.intersection(rustix::fs::OFlags::ACCMODE)
!= rustix::fs::OFlags::RDONLY
{
return Err(io::Error::other("immutable snapshot readback"));
}
if pattern {
let mut reader = file.reader()?;
let mut offset = 0;
let mut buf = [0_u8; 4096];
loop {
let n = reader.read(&mut buf)?;
if n == 0 {
break;
}
for (i, b) in buf[..n].iter().enumerate() {
if *b != (offset + i) as u8 {
return Err(io::Error::other("binary snapshot contents"));
}
}
offset += n;
}
if offset as u64 != file.bytes() {
return Err(io::Error::other("binary snapshot bytes"));
}
}
Ok(())
}
fn prefix(file: &crate::artifacts::SnapshotFile) -> io::Result<String> {
let mut b = Vec::new();
file.reader()?.take(64).read_to_end(&mut b)?;
Ok(b.iter().map(|b| format!("{b:02x}")).collect())
}
pub fn run_case(case: &str) -> io::Result<serde_json::Value> {
verify_guest(true)?;
if case.starts_with("normal-") {
return normal::run_case(case);
}
let rollback = case.strip_prefix("cancel-").filter(|s| {
matches!(
*s,
"blocked"
| "namespace"
| "root"
| "proc"
| "inputs"
| "identity"
| "seccomp"
| "io"
| "exec"
)
});
let failing = case.strip_prefix("fail-").filter(|s| {
matches!(
*s,
"namespace" | "root" | "proc" | "inputs" | "rlimits" | "identity" | "seccomp" | "io"
)
});
let mode = match case {
"binary" | "stdout" | "stderr" | "wall" | "leftover" | "early" | "disk" | "export" => {
MonitorMode::Normal
}
"cancel" => MonitorMode::CancelAfter(100000),
"monitor" => MonitorMode::FailAfter(100000),
_ if rollback.is_some() || failing.is_some() => MonitorMode::Normal,
_ => return Err(io::Error::other("unknown fixed case")),
};
let id = format!("execution-{case}");
let fixture = Fixture::new("/kf/bin/execution-fixture")?;
let root = fixture.root()?;
let config = fixture.config()?;
let mut request: ExecutionSpec =
serde_json::from_str(include_str!("../tests/fixtures/spec.valid.json"))?;
request.run_id = id;
request.argv.push(case.into());
request.limits.cpu_time_us = 2_000_000;
request.limits.wall_time_us = 8_000_000;
if matches!(case, "stdout" | "stderr") {
request.limits.stdout_bytes = 4096;
request.limits.stderr_bytes = 4096;
}
if matches!(case, "wall") {
request.limits.wall_time_us = 200000;
}
if case == "binary" {
request.outputs = vec!["result.bin".into()];
}
if case == "disk" {
request.limits.file_bytes = 131072;
request.limits.workdir_bytes = 262144;
request.limits.stdout_bytes = 4096;
request.limits.stderr_bytes = 4096;
}
if case == "export" {
request.outputs = vec!["valid.bin".into(), "escape".into()];
}
if case == "fail-io" {
request.limits.file_bytes = 65536;
request.limits.stdout_bytes = 131072;
request.limits.stderr_bytes = 131072;
request.stdin_artifact = None;
}
if case == "fail-inputs" {
fs::remove_file(format!("{}/program-001", fixture.publications))?;
}
let spec = request
.validate(&SpecPolicy::default())
.map_err(io::Error::other)?;
if let Some(stage) = rollback {
return crate::launcher::rollback::cancel_stage(
stage,
&config,
&spec,
&std::env::current_exe()?,
&root,
);
}
if let Some(stage) = failing {
return crate::launcher::rollback::fail_stage(
stage,
&config,
&spec,
&std::env::current_exe()?,
&root,
);
}
let completed =
run::execute(&config, &spec, &std::env::current_exe()?, &root, mode).map_err(|e| {
io::Error::other(format!(
"initialization {:?} {:?} cleanup {:?}",
e.failure.stage, e.failure.errno, e.cleanup_failure
))
})?;
describe(case, &spec, &completed, case == "binary")
}
fn describe(
case: &str,
spec: &crate::spec::ValidatedSpec,
completed: &run::Completed,
pattern: bool,
) -> io::Result<serde_json::Value> {
verify_snapshot(&completed.stdout, pattern)?;
verify_snapshot(&completed.stderr, pattern)?;
for o in &completed.outputs {
verify_snapshot(&o.file, pattern)?;
}
let outcome = completed.report.classify(spec).map_err(io::Error::other)?;
let outputs=completed.outputs.iter().map(|o|Ok(serde_json::json!({"path":o.path,"bytes":o.file.bytes(),"prefix_hex":prefix(&o.file)?}))).collect::<io::Result<Vec<_>>>()?;
let result = serde_json::json!({"case":case,"immutable_snapshots_verified":true,"complete_binary_content_verified":pattern,"report":completed.report,"outcome":outcome,"proof":completed.proof,"stdout_prefix_hex":prefix(&completed.stdout)?,"stderr_prefix_hex":prefix(&completed.stderr)?,"outputs":outputs});
Ok(result)
}