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

321 lines
9.3 KiB
Rust

use kf_sandbox::probe::EnvironmentObservation;
use kf_sandbox::runtime::{RuntimeConfig, kernel_supported, preflight};
use kf_sandbox::spec::{ExecutionSpec, SpecPolicy};
fn config() -> RuntimeConfig {
serde_json::from_str(include_str!(
"../../../../deploy/sandbox.runtime.example.json"
))
.unwrap()
}
#[test]
fn example_has_distinct_identity_slots_and_applies_operator_task_ceiling() {
let validated = config().validate().unwrap();
assert_eq!(validated.task_identity(0), Some((100000, 100000)));
assert_eq!(validated.task_identity(1), Some((100001, 100001)));
assert_eq!(validated.task_identity(2), None);
assert_eq!(validated.task_identity(u32::MAX), None);
let mut request: ExecutionSpec =
serde_json::from_str(include_str!("fixtures/spec.valid.json")).unwrap();
request.limits.memory_bytes = 1024 * 1024 * 1024;
assert!(request.clone().validate(&SpecPolicy::default()).is_ok());
assert!(request.validate(&validated.spec_policy()).is_err());
let json = serde_json::to_string(validated.config()).unwrap();
assert_eq!(
serde_json::from_str::<RuntimeConfig>(&json).unwrap(),
*validated.config()
);
}
#[test]
fn roots_reject_traversal_aliases_system_locations_and_overlapping_storage() {
for path in [
"/",
"/var/lib/../kf-judge",
"/var//lib/kf-judge",
"/var/lib/kf-judge/",
"relative",
"/var/lib/a\\b",
"/etc/kf-judge",
] {
let mut policy = config();
policy.state_root = path.into();
assert!(policy.validate().is_err(), "{path}");
}
for path in [
"/var/lib/kf-judge/tasks",
"/var/lib/kf-judge/tasks/image",
"/var/lib/kf-judge/identities",
"/var/lib/kf-judge/identities/image",
"/var/lib/kf-judge",
"/srv/unrelated",
] {
let mut policy = config();
policy.image_root = path.into();
assert!(policy.validate().is_err(), "{path}");
}
for path in [
"/sys/fs/cgroup",
"/sys/fs/cgroup2/job",
"/tmp/cgroup",
"/sys/fs/cgroup/../job",
] {
let mut policy = config();
policy.delegated_cgroup_root = path.into();
assert!(policy.validate().is_err(), "{path}");
}
}
#[test]
fn identity_pools_reject_root_collision_shortage_and_integer_overflow() {
let invalid = [
{
let mut p = config();
p.identities.worker_uid = 0;
p
},
{
let mut p = config();
p.identities.worker_gid = u32::MAX;
p
},
{
let mut p = config();
p.identities.count = 0;
p
},
{
let mut p = config();
p.identities.count = 1;
p
},
{
let mut p = config();
p.identities.first_task_uid = 65534;
p
},
{
let mut p = config();
p.identities.worker_uid = 100001;
p
},
{
let mut p = config();
p.identities.first_task_gid = u32::MAX;
p
},
{
let mut p = config();
p.identities.first_task_uid = u32::MAX - 1;
p
},
{
let mut p = config();
p.max_concurrent_runs = 33;
p
},
];
for policy in invalid {
assert!(policy.validate().is_err());
}
}
#[test]
fn every_aggregate_reservation_must_cover_all_slots_and_supervision() {
let invalid = [
{
let mut p = config();
p.aggregate.memory_bytes = p.per_task_ceiling.memory_bytes * 2;
p
},
{
let mut p = config();
p.aggregate.processes = p.per_task_ceiling.processes * 2;
p
},
{
let mut p = config();
p.aggregate.workdir_bytes = p.per_task_ceiling.workdir_bytes * 2;
p
},
{
let mut p = config();
p.aggregate.cpu_quota_us = p.aggregate.task_cpu_quota_us * 2;
p
},
{
let mut p = config();
p.aggregate.supervisor_memory_bytes = 0;
p
},
{
let mut p = config();
p.aggregate.supervisor_processes = 0;
p
},
{
let mut p = config();
p.aggregate.supervisor_workdir_bytes = 0;
p
},
{
let mut p = config();
p.aggregate.supervisor_cpu_quota_us = 0;
p
},
];
for policy in invalid {
assert!(policy.validate().is_err());
}
let mut exact = config();
exact.aggregate.cpu_quota_us =
exact.aggregate.task_cpu_quota_us * 2 + exact.aggregate.supervisor_cpu_quota_us;
assert!(exact.validate().is_ok());
}
#[test]
fn overflow_huge_service_budgets_and_invalid_task_ceilings_are_rejected() {
let invalid = [
{
let mut p = config();
p.aggregate.task_cpu_quota_us = u64::MAX;
p.aggregate.cpu_quota_us = u64::MAX;
p
},
{
let mut p = config();
p.aggregate.supervisor_memory_bytes = u64::MAX;
p.aggregate.memory_bytes = u64::MAX;
p
},
{
let mut p = config();
p.aggregate.memory_bytes = 64 * 1024 * 1024 * 1024 + 1;
p
},
{
let mut p = config();
p.aggregate.processes = 8193;
p
},
{
let mut p = config();
p.aggregate.cpu_period_us = 0;
p
},
{
let mut p = config();
p.per_task_ceiling.processes = 0;
p
},
{
let mut p = config();
p.per_task_ceiling.memory_bytes = 3 * 1024 * 1024 * 1024;
p
},
];
for policy in invalid {
assert!(policy.validate().is_err());
}
}
#[test]
fn kernel_policy_handles_release_suffixes_future_versions_and_unknown_values() {
for (release, expected) in [
("6.6.0", Some(true)),
("6.5.99", Some(false)),
("5.19.0", Some(false)),
("7.2.9-1-cachyos", Some(true)),
("999.1.0", Some(true)),
("unknown", None),
("6.x.1", None),
("6", None),
] {
assert_eq!(kernel_supported(release), expected, "{release}");
}
}
#[test]
fn visible_interfaces_or_a_caller_isolation_flag_never_grant_execution() {
let observation = EnvironmentObservation {
os: "linux",
architecture: "x86_64",
kernel_release: Some("7.2.9-1-cachyos".into()),
namespace_entries: ["user", "mnt", "pid", "ipc", "uts", "net", "cgroup"]
.map(String::from)
.to_vec(),
cgroup_v2_visible: true,
cgroup_v2_controllers: ["cpu", "memory", "pids"].map(String::from).to_vec(),
current_process_seccomp_mode: Some(2),
isolation_verified: true,
};
let report = preflight(&config().validate().unwrap(), &observation);
assert!(!report.execution_allowed);
assert!(report.configuration_valid);
assert!(
report
.reasons
.iter()
.any(|reason| reason.code == "sandbox_not_implemented")
);
for subject in [
"cgroup_delegation",
"pidfd_wait",
"seccomp_filter",
"aggregate_budget_enforcement",
] {
assert!(report.reasons.iter().any(|reason| reason.code=="capability_not_verified" && reason.subject==subject));
}
}
#[test]
fn missing_or_unsupported_platform_capabilities_have_structured_reasons() {
let observation = EnvironmentObservation {
os: "windows",
architecture: "aarch64",
kernel_release: Some("6.5.0".into()),
namespace_entries: vec![],
cgroup_v2_visible: false,
cgroup_v2_controllers: vec![],
current_process_seccomp_mode: None,
isolation_verified: false,
};
let report = preflight(&config().validate().unwrap(), &observation);
for (code, subject) in [
("unsupported_platform", "os"),
("unsupported_platform", "architecture"),
("kernel_below_policy_minimum", "kernel"),
("namespace_not_visible", "net"),
("cgroup_v2_not_visible", "cgroup"),
("controller_not_visible", "memory"),
] {
assert!(
report
.reasons
.iter()
.any(|reason| reason.code == code && reason.subject == subject)
);
}
}
#[test]
fn configuration_cannot_enable_execution_or_bypass_required_fields() {
let valid = serde_json::to_value(config()).unwrap();
let mut invalid = valid.clone();
invalid["execution_enabled"] = serde_json::json!(true);
assert!(serde_json::from_value::<RuntimeConfig>(invalid).is_err());
for (pointer, value) in [
("/schema_version", serde_json::json!(2)),
("/image_kind", serde_json::json!("host")),
("/max_concurrent_runs", serde_json::json!(4294967296_u64)),
] {
let mut invalid = valid.clone();
*invalid.pointer_mut(pointer).unwrap() = value;
assert!(serde_json::from_value::<RuntimeConfig>(invalid).is_err());
}
let mut invalid = valid;
invalid.as_object_mut().unwrap().remove("aggregate");
assert!(serde_json::from_value::<RuntimeConfig>(invalid).is_err());
}