Files
KF-OnlineJudge/judge/crates/sandbox/tests/spec_validation.rs
T

259 lines
8.5 KiB
Rust

use kf_sandbox::spec::{
ExecutionProfile, ExecutionSpec, InputArtifact, ResourceLimits, SpecPolicy, ValidationCode,
};
use serde_json::{Value, json};
fn fixture() -> Value {
serde_json::from_str(include_str!("fixtures/spec.valid.json")).unwrap()
}
fn spec() -> ExecutionSpec {
serde_json::from_value(fixture()).unwrap()
}
fn rejected(request: ExecutionSpec, field: &str, code: ValidationCode) {
let error = request.validate(&SpecPolicy::default()).unwrap_err();
assert_eq!(error.field, field);
assert_eq!(error.code, code);
}
#[test]
fn native_fixture_roundtrips_and_defaults_are_explicit() {
let request = spec().validate(&SpecPolicy::default()).unwrap();
assert_eq!(serde_json::to_value(request.spec()).unwrap(), fixture());
assert_eq!(request.spec().limits, ResourceLimits::default());
}
#[test]
fn compiler_profiles_require_fixed_executables_and_accept_profile_defaults() {
for (profile, executable) in [
(ExecutionProfile::CompileC, "/usr/bin/gcc"),
(ExecutionProfile::CompileCpp, "/usr/bin/g++"),
] {
let mut request = spec();
request.profile = profile;
request.argv = vec![
executable.into(),
"source/main.c".into(),
"-o".into(),
"main".into(),
];
request.inputs[0].path = "source/main.c".into();
request.outputs = vec!["main".into()];
request.limits = ResourceLimits::for_profile(profile);
request.validate(&SpecPolicy::default()).unwrap();
let mut invalid = spec();
invalid.profile = profile;
rejected(invalid, "argv[0]", ValidationCode::UnsupportedExecutable);
}
}
#[test]
fn json_rejects_unknown_profiles_versions_host_mounts_and_missing_limits() {
for (key, value) in [
("profile", json!("shell")),
("schema_version", json!(2)),
("mounts", json!([{"source": "/etc", "target": "/work"}])),
] {
let mut document = fixture();
document[key] = value;
assert!(
serde_json::from_value::<ExecutionSpec>(document).is_err(),
"{key}"
);
}
let mut document = fixture();
document["limits"]
.as_object_mut()
.unwrap()
.remove("memory_bytes");
assert!(serde_json::from_value::<ExecutionSpec>(document).is_err());
let mut document = fixture();
document["inputs"][0]["writable"] = json!(true);
assert!(serde_json::from_value::<ExecutionSpec>(document).is_err());
}
#[test]
fn argument_limits_reject_nul_empty_commands_and_excessive_size() {
for argv in [
vec![],
vec!["/work/main".into(), "a\0b".into()],
vec!["/work/main".into(), "x".repeat(4097)],
vec!["x".into(); 129],
std::iter::once("/work/main".into())
.chain(std::iter::repeat_n("x".repeat(4096), 8))
.collect(),
] {
let mut request = spec();
request.argv = argv;
rejected(request, "argv", ValidationCode::InvalidArgv);
}
let mut request = spec();
request.argv.push("参数 with spaces; $(not-a-shell)".into());
request.validate(&SpecPolicy::default()).unwrap();
}
#[test]
fn native_executable_must_be_a_bound_work_artifact() {
for executable in [
"/bin/sh",
"/work/../bin/sh",
"/work/missing",
"/work//main",
"main",
"",
] {
let mut request = spec();
request.argv[0] = executable.into();
rejected(request, "argv[0]", ValidationCode::UnsupportedExecutable);
}
}
#[test]
fn environment_does_not_accept_loader_overrides_or_arbitrary_values() {
for (key, value) in [
("LD_PRELOAD", "lib.so"),
("PATH", "/host/bin"),
("LANG", "a\0b"),
("TZ", "Europe/London"),
] {
let mut request = spec();
request.env.insert(key.into(), value.into());
rejected(request, "env", ValidationCode::InvalidEnvironment);
}
}
#[test]
fn paths_reject_traversal_absolute_windows_ambiguous_and_long_components() {
let mut invalid_paths = vec![
"".into(),
"/etc/passwd".into(),
"../secret".into(),
"a/../b".into(),
"a//b".into(),
"a/./b".into(),
"a/".into(),
"C:\\secret".into(),
"%2e%2e/secret".into(),
"a\0b".into(),
];
invalid_paths.push("x".repeat(256));
for path in invalid_paths {
let mut request = spec();
request.inputs[0].path = path;
rejected(request, "inputs.path", ValidationCode::UnsafePath);
}
let mut request = spec();
request.cwd = "../host".into();
rejected(request, "cwd", ValidationCode::UnsafePath);
}
#[test]
fn identifiers_cannot_be_host_paths_or_empty_and_artifact_counts_are_bounded() {
for value in ["", "../other", "/host/input", "a.b", "a\0b"] {
let mut request = spec();
request.run_id = value.into();
rejected(request, "run_id", ValidationCode::InvalidIdentifier);
let mut request = spec();
request.stdin_artifact = Some(value.into());
rejected(request, "stdin_artifact", ValidationCode::InvalidIdentifier);
}
let mut request = spec();
request.inputs = vec![request.inputs[0].clone(); 65];
rejected(request, "artifacts", ValidationCode::TooManyArtifacts);
let mut request = spec();
request.outputs = vec!["output".into(); 17];
rejected(request, "artifacts", ValidationCode::TooManyArtifacts);
}
#[test]
fn conflicting_bindings_and_exports_cannot_replace_read_only_inputs() {
for path in ["main", "main/child"] {
let mut request = spec();
request.inputs.push(InputArtifact {
artifact_id: "other".into(),
path: path.into(),
});
rejected(request, "inputs.path", ValidationCode::ConflictingPath);
}
let mut request = spec();
request.outputs.push("main".into());
rejected(request, "outputs", ValidationCode::ConflictingPath);
let mut request = spec();
request.outputs = vec!["out".into(), "out/file".into()];
rejected(request, "outputs", ValidationCode::ConflictingPath);
}
#[test]
fn cwd_can_contain_an_input_but_cannot_be_or_descend_from_an_input_file() {
let mut request = spec();
request.inputs[0].path = "bin/main".into();
request.argv[0] = "/work/bin/main".into();
request.cwd = "bin".into();
request.validate(&SpecPolicy::default()).unwrap();
for cwd in ["main", "main/subdirectory"] {
let mut request = spec();
request.cwd = cwd.into();
rejected(request, "inputs.path", ValidationCode::ConflictingPath);
}
}
#[test]
fn every_resource_rejects_zero_and_exceeding_its_policy_ceiling() {
for field in [
"cpu_time_us",
"wall_time_us",
"memory_bytes",
"stack_bytes",
"processes",
"open_files",
"stdout_bytes",
"stderr_bytes",
"file_bytes",
"workdir_bytes",
] {
let mut document = fixture();
document["limits"][field] = json!(0);
let request = serde_json::from_value(document).unwrap();
rejected(request, field, ValidationCode::InvalidLimit);
let mut document = fixture();
document["limits"][field] = json!(u32::MAX);
let request = serde_json::from_value(document).unwrap();
rejected(request, field, ValidationCode::LimitExceedsPolicy);
}
let mut document = fixture();
document["limits"]["memory_bytes"] = json!(u64::MAX);
rejected(
serde_json::from_value(document).unwrap(),
"memory_bytes",
ValidationCode::LimitExceedsPolicy,
);
let mut document = fixture();
document["limits"]["processes"] = json!(u64::MAX);
assert!(serde_json::from_value::<ExecutionSpec>(document).is_err());
}
#[test]
fn trusted_policy_can_tighten_limits_and_rejects_inconsistent_caps() {
let limits = ResourceLimits::default();
let policy = SpecPolicy::with_limits_ceiling(limits.clone()).unwrap();
let mut request = spec();
request.limits.cpu_time_us += 1;
assert_eq!(
request.validate(&policy).unwrap_err().code,
ValidationCode::LimitExceedsPolicy
);
let mut bad = limits;
bad.stack_bytes = bad.memory_bytes + 1;
assert_eq!(
SpecPolicy::with_limits_ceiling(bad).unwrap_err().code,
ValidationCode::InconsistentLimits
);
let mut request = spec();
request.limits.file_bytes = request.limits.workdir_bytes + 1;
rejected(request, "file_bytes", ValidationCode::LimitExceedsPolicy);
let mut request = spec();
request.limits.workdir_bytes = request.limits.file_bytes - 1;
rejected(request, "limits", ValidationCode::InconsistentLimits);
}