test(sandbox): verify complete native and compiler task workflows
This commit is contained in:
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user