test(sandbox): verify complete native and compiler task workflows

This commit is contained in:
2026-10-08 07:12:11 +08:00
parent f9789ea4c4
commit 057c02f39d
5 changed files with 308 additions and 30 deletions
+51 -30
View File
@@ -7,6 +7,7 @@ use crate::{
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"
@@ -34,6 +35,37 @@ 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);
@@ -82,6 +114,9 @@ fn prefix(file: &crate::artifacts::SnapshotFile) -> io::Result<String> {
}
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,
@@ -112,31 +147,9 @@ pub fn run_case(case: &str) -> io::Result<serde_json::Value> {
_ => return Err(io::Error::other("unknown fixed case")),
};
let id = format!("execution-{case}");
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 = Fixture { cg, publications };
fs::copy(
"/kf/bin/execution-fixture",
format!("{}/program-001", fixture.publications),
)?;
fs::write(
format!("{}/input-001", fixture.publications),
vec![0xa5; 65537],
)?;
let root = ArtifactRoot::from_directory(rustix::fs::open(
&fixture.publications,
rustix::fs::OFlags::PATH | rustix::fs::OFlags::DIRECTORY | rustix::fs::OFlags::CLOEXEC,
rustix::fs::Mode::empty(),
)?)?;
let mut config: RuntimeConfig = serde_json::from_str(include_str!(
"../../../../deploy/sandbox.runtime.example.json"
))?;
config.delegated_cgroup_root = fixture.cg.clone();
let config = config.validate().map_err(io::Error::other)?;
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;
@@ -199,13 +212,21 @@ pub fn run_case(case: &str) -> io::Result<serde_json::Value> {
e.failure.stage, e.failure.errno, e.cleanup_failure
))
})?;
verify_snapshot(&completed.stdout, case == "binary")?;
verify_snapshot(&completed.stderr, case == "binary")?;
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, case == "binary")?;
verify_snapshot(&o.file, pattern)?;
}
let outcome = completed.report.classify(&spec).map_err(io::Error::other)?;
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":case=="binary","report":completed.report,"outcome":outcome,"proof":completed.proof,"stdout_prefix_hex":prefix(&completed.stdout)?,"stderr_prefix_hex":prefix(&completed.stderr)?,"outputs":outputs});
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)
}
@@ -0,0 +1,128 @@
//! Fixed language fixtures using the same private root execution pipeline.
use super::{Fixture, describe};
use crate::{
launcher::run::{self, MonitorMode},
outcome::ExecutionOutcome,
spec::{ExecutionProfile, ExecutionSpec, InputArtifact, ResourceLimits, SpecPolicy},
};
use std::{fs, io, io::Read};
pub(super) fn run_case(case: &str) -> io::Result<serde_json::Value> {
let (program, language, compile) = match case {
"normal-static" => ("/kf/bin/normal-static", "C", false),
"normal-c" | "normal-empty" | "normal-nonzero" => ("/kf/bin/normal-c", "C", false),
"normal-cpp" => ("/kf/bin/normal-cpp", "C++", false),
"normal-compile-c" => ("/kf/bin/normal-c", "C", true),
"normal-compile-cpp" => ("/kf/bin/normal-cpp", "C++", true),
_ => return Err(io::Error::other("unknown normal case")),
};
let fixture = Fixture::new(program)?;
let config = fixture.config()?;
// Software-emulated GCC consumes much more CPU than the native target.
// Only this fixed VM fixture gets a larger, still finite compiler budget.
let mut compiler_config = config.config().clone();
compiler_config.per_task_ceiling.cpu_time_us = 30_000_000;
compiler_config.per_task_ceiling.wall_time_us = 60_000_000;
let compiler_config = compiler_config.validate().map_err(io::Error::other)?;
let root = fixture.root()?;
let binary = std::env::current_exe()?;
let mut request: ExecutionSpec =
serde_json::from_str(include_str!("../../tests/fixtures/spec.valid.json"))?;
let mut compilation = None;
if compile {
let cpp = language == "C++";
let source_name = if cpp { "main.cpp" } else { "main.c" };
let source = if cpp {
include_str!("../../tests/fixtures/normal.cpp")
} else {
include_str!("../../tests/fixtures/normal.c")
};
fs::write(format!("{}/source-001", fixture.publications), source)?;
let mut compiler = request.clone();
compiler.run_id = format!("{case}-build");
compiler.profile = if cpp {
ExecutionProfile::CompileCpp
} else {
ExecutionProfile::CompileC
};
compiler.limits = ResourceLimits::for_profile(compiler.profile);
compiler.limits.cpu_time_us = 30_000_000;
compiler.limits.wall_time_us = 60_000_000;
compiler.argv = vec![
if cpp { "/usr/bin/g++" } else { "/usr/bin/gcc" }.into(),
"-O0".into(),
"-Wall".into(),
"-Wextra".into(),
"-Werror".into(),
format!("/work/{source_name}"),
"-o".into(),
"/work/main.bin".into(),
];
compiler.inputs = vec![InputArtifact {
artifact_id: "source-001".into(),
path: source_name.into(),
}];
compiler.outputs = vec!["main.bin".into()];
compiler.stdin_artifact = None;
let spec = compiler
.validate(&SpecPolicy::default())
.map_err(io::Error::other)?;
let completed = run::execute(&compiler_config, &spec, &binary, &root, MonitorMode::Normal)
.map_err(|e| io::Error::other(format!("compiler initialization: {:?}", e.failure)))?;
let result = describe(case, &spec, &completed, false)?;
if completed.report.classify(&spec).map_err(io::Error::other)?
!= (ExecutionOutcome::Exited { code: 0 })
|| completed.outputs.len() != 1
{
return Err(io::Error::other(format!(
"sandbox compilation failed: {result}"
)));
}
let output = &completed.outputs[0].file;
let mut magic = [0_u8; 4];
output.reader()?.read_exact(&mut magic)?;
if magic != *b"\x7fELF" || output.bytes() < 4096 {
return Err(io::Error::other("compiler output is not ELF"));
}
// Publish the sealed export only after full compiler teardown. The
// next task snapshots it anew as its single approved executable input.
let mut destination = fs::File::create(format!("{}/program-001", fixture.publications))?;
let copied = io::copy(&mut output.reader()?, &mut destination)?;
if copied != output.bytes() {
return Err(io::Error::other("compiler publication length"));
}
compilation = Some(result);
}
request.run_id = format!("{case}-run");
let mode = match case {
"normal-empty" => "empty",
"normal-nonzero" => "nonzero",
_ => "normal",
};
request.argv.push(mode.into());
if mode == "empty" {
request.stdin_artifact = None;
}
let spec = request
.validate(&SpecPolicy::default())
.map_err(io::Error::other)?;
let completed = run::execute(&config, &spec, &binary, &root, MonitorMode::Normal)
.map_err(|e| io::Error::other(format!("normal initialization: {:?}", e.failure)))?;
let expected = format!(
"{language} bytes={}\n",
if mode == "empty" { 0 } else { 65537 }
);
let mut bytes = Vec::new();
completed
.stdout
.reader()?
.take(64)
.read_to_end(&mut bytes)?;
if bytes != expected.as_bytes() || completed.stdout.bytes() != bytes.len() as u64 {
return Err(io::Error::other("normal complete stdout content"));
}
let mut result = describe(case, &spec, &completed, false)?;
result["complete_stdout_content_verified"] = true.into();
result["compilation"] = compilation.into();
Ok(result)
}
+16
View File
@@ -0,0 +1,16 @@
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char **argv) {
if (argc != 2) return 100;
unsigned char bytes[4096];
size_t count = 0;
ssize_t n;
while ((n = read(0, bytes, sizeof bytes)) > 0) {
for (ssize_t i = 0; i < n; i++) if (bytes[i] != 0xa5) return 101;
count += (size_t)n;
}
if (n < 0 || count != (strcmp(argv[1], "empty") ? 65537U : 0U)) return 102;
printf("C bytes=%zu\n", count);
return !strcmp(argv[1], "nonzero") ? 23 : 0;
}
+14
View File
@@ -0,0 +1,14 @@
#include <iostream>
#include <string>
int main(int argc, char **argv) {
if (argc != 2 || std::string(argv[1]) != "normal") return 100;
std::size_t count = 0;
char byte;
while (std::cin.get(byte)) {
if (static_cast<unsigned char>(byte) != 0xa5) return 101;
++count;
}
if (!std::cin.eof() || count != 65537) return 102;
std::cout << "C++ bytes=" << count << '\n';
return 0;
}
+99
View File
@@ -0,0 +1,99 @@
#![cfg(all(target_os = "linux", feature = "vm-acceptance"))]
#[allow(dead_code)]
mod support;
use std::{fs, process::Command};
fn check_run(v: &serde_json::Value, code: u8) {
assert_eq!(
v["outcome"],
serde_json::json!({"kind":"exited", "code":code}),
"{v}"
);
assert_eq!(v["report"]["cleanup"], "complete", "{v}");
assert_eq!(v["report"]["errors"], serde_json::json!([]));
assert_eq!(v["report"]["limits"], serde_json::json!([]));
assert_eq!(v["immutable_snapshots_verified"], true);
assert_eq!(
v["proof"]["order"],
serde_json::json!([
"verified_isolation",
"exec_gate",
"cgroup_empty",
"owned_reaped",
"final_accounting",
"final_export",
"resource_removal"
])
);
for metric in [
"cpu_time_us",
"wall_time_us",
"memory_peak_bytes",
"memory_events",
] {
assert_eq!(v["report"]["usage"][metric]["state"], "measured", "{v}");
}
}
fn run(case: &str, code: u8) -> serde_json::Value {
support::require_vm();
let output = Command::new(support::binary())
.args(["vm-case", case])
.env_clear()
.env("KF_SANDBOX_INDEPENDENT_VM", "1")
.output()
.unwrap();
assert!(
output.status.success(),
"{case}: {} {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let v: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
check_run(&v, code);
for dir in ["tasks", "identities", "captures"] {
assert_eq!(
fs::read_dir(format!("/var/lib/kf-judge/{dir}"))
.unwrap()
.count(),
0
);
}
println!("KF_NORMAL_PROOF {v}");
v
}
#[test]
#[ignore = "independent capped VM: static and dynamic C/C++ in actual private root"]
fn static_and_dynamic_language_programs() {
for case in ["normal-static", "normal-c", "normal-cpp"] {
assert_eq!(run(case, 0)["complete_stdout_content_verified"], true);
}
}
#[test]
#[ignore = "independent capped VM: sandbox compiler exported ELF runs as a new task"]
fn sandbox_compilation_then_artifact_execution() {
for case in ["normal-compile-c", "normal-compile-cpp"] {
let v = run(case, 0);
check_run(&v["compilation"], 0);
assert_eq!(v["compilation"]["outputs"][0]["path"], "main.bin");
assert!(
v["compilation"]["outputs"][0]["prefix_hex"]
.as_str()
.unwrap()
.starts_with("7f454c46")
);
assert_ne!(v["compilation"]["report"]["run_id"], v["report"]["run_id"]);
}
}
#[test]
#[ignore = "independent capped VM: empty stdin EOF, binary capture/export and nonzero exit"]
fn empty_binary_and_nonzero_are_real_results() {
assert_eq!(
run("normal-empty", 0)["report"]["stdout"]["retained_bytes"],
10
);
run("normal-nonzero", 23);
let v = run("binary", 7);
assert_eq!(v["complete_binary_content_verified"], true);
assert_eq!(v["report"]["stdout"]["retained_bytes"], 65537);
assert_eq!(v["outputs"][0]["bytes"], 32769);
}