feat(sandbox): define runtime policy and preflight refusals

This commit is contained in:
2026-10-06 03:58:19 +08:00
parent ee1f43a4cb
commit 1f45266517
8 changed files with 892 additions and 2 deletions
+40
View File
@@ -0,0 +1,40 @@
{
"schema_version": 1,
"state_root": "/var/lib/kf-judge",
"tasks_root": "/var/lib/kf-judge/tasks",
"image_root": "/var/lib/kf-judge/images/gnu-cxx-x86_64-v1",
"image_kind": "linux_gnu_cxx_x86_64_v1",
"delegated_cgroup_root": "/sys/fs/cgroup/kf-judge.service",
"identities": {
"worker_uid": 1000,
"worker_gid": 1000,
"first_task_uid": 100000,
"first_task_gid": 100000,
"count": 2
},
"max_concurrent_runs": 2,
"per_task_ceiling": {
"cpu_time_us": 10000000,
"wall_time_us": 30000000,
"memory_bytes": 536870912,
"stack_bytes": 16777216,
"processes": 32,
"open_files": 128,
"stdout_bytes": 1048576,
"stderr_bytes": 1048576,
"file_bytes": 67108864,
"workdir_bytes": 268435456
},
"aggregate": {
"memory_bytes": 1610612736,
"supervisor_memory_bytes": 268435456,
"processes": 128,
"supervisor_processes": 32,
"workdir_bytes": 1073741824,
"supervisor_workdir_bytes": 67108864,
"cpu_period_us": 100000,
"cpu_quota_us": 250000,
"task_cpu_quota_us": 100000,
"supervisor_cpu_quota_us": 50000
}
}
+36 -1
View File
@@ -5,7 +5,7 @@ Cargo workspace 分为五个 crate:
| Crate | 职责 | 当前能力 |
| --- | --- | --- |
| `kf-contracts` | 跨服务通知和执行契约 | 最小 v1 通知、共享 JSON 样例检查 |
| `kf-sandbox` | Linux 隔离、限制、计量与清理 | `probe` 只读观察;`run` 拒绝执行 |
| `kf-sandbox` | Linux 隔离、限制、计量与清理 | 规格/配置检查与 `probe` 只读观察;`run` 拒绝执行 |
| `kf-engine` | 语言适配、测试点编排、比较与确定性汇总 | 预留库边界,无模拟判定 |
| `kf-worker` | 拉取、租约、工件准备与本地编排 | `--check` 入口,无消费循环 |
| `kf-coordinator` | 节点登记、租约、状态与结果写入权 | HTTP 存活检查,无任务端点 |
@@ -77,3 +77,38 @@ FD、栈和其他信号仍按原退出状态记录,缺少专门证据时不猜
超限输出须有对应证据;缺失计量、无说明的截断或不完整读取报告基础设施失败。
工件只能导出规格已申请的路径,单文件与已导出总量均受限;这些字段检查不能替代实际文件安全检查。
准备失败和清理失败分别保留,真实进程树终止、资源回收与 VM 验收仍待后续阶段实现。
## 可信运行配置与启动前检查
```bash
cd judge
cargo run --locked -p kf-sandbox -- check-runtime --config ../deploy/sandbox.runtime.example.json
```
首轮支持策略为 Linux x86_64、内核至少 6.6;这是项目保守支持范围,内核版本不能代替逐项能力验证。
所需接口包括 user/mount/PID/IPC/UTS/network/cgroup namespace、pidfd 等待、身份降权、seccomp filter,
以及实际委派子树中的 CPU/memory/pids 控制器、`cgroup.kill`、`memory.peak`、内存/进程事件和交换空间限制。
初始镜像类型固定为 `linux_gnu_cxx_x86_64_v1`:受控只读目录镜像、GNU C/C++ 编译器、动态加载器和
可信 `manifest.json`;镜像内容、权限和只读挂载验证仍未实现。
配置由可信部署方提供,最多 64 KiB;不能由执行请求覆盖。`runtime.rs` 校验受控 state/tasks/image
目录布局、独立 cgroup 子树、普通 Worker 身份和独立任务 UID/GID 槽位;任务身份从 100000 起,
不与配置中的 Worker 重叠。槽位仅定义映射,真正分配前须核查系统账号占用,直到旧任务树为空且
清理完成后才可复用,服务重启也须恢复租用状态。
样例最多并发 2 个任务,每任务上限 512 MiB 内存、32 个进程和 256 MiB 工作空间;
整体内存预算 1.5 GiB、进程预算 128、工作空间预算 1 GiB,分别给监督器保留 256 MiB、32 个进程和 64 MiB。
CPU 带宽在同一 100 ms 周期中,每任务配额 100 ms、监督器预留 50 ms、父级配额 250 ms。
校验采用检查过的整数运算,整体预算至少覆盖「任务上限 × 并发数 + 监督器预留」,拒绝溢出或不足。
CPU 带宽和 CPU 总时间分别配置;预算目前只完成政策校验,真正应用和核验由后续 cgroup/监督器实现。
工作空间的 tmpfs 占用仍计入任务内存;镜像存储与工件长期保留由部署/工件提供方单独管理。
`check-runtime` 只读取当前环境和配置路径,输出结构化拒绝,退出码 2;配置有效时明确
`configuration_valid: true`、`execution_allowed: false`。目录/接口可见与不可见分别记录,
配置归属、路径真实包含关系、镜像完整性、身份池、helper、委派、预算实际生效、pidfd、namespace、
降权、seccomp 和清理能力均保留未验证状态。它不创建目录、改账号、启动服务或运行用户程序。
即使观察值里的 `isolation_verified` 被误设为 true,也不能解除未实现的执行拒绝。
接口语义参见 [cgroup v2](https://docs.kernel.org/admin-guide/cgroup-v2.html)、
[pidfd_open](https://man7.org/linux/man-pages/man2/pidfd_open.2.html) 和
[seccomp filter](https://docs.kernel.org/userspace-api/seccomp_filter.html)。
+2 -1
View File
@@ -1,8 +1,9 @@
//! Linux execution boundary. Current implementation only provides read-only observations.
//! Linux execution boundary: policy/contract checks and read-only observations.
//!
//! JS-01 defines execution contracts; JS-02..04 add the launcher, isolation and limits.
//! Until that work is verified, the CLI cannot execute any user program.
pub mod outcome;
pub mod probe;
pub mod runtime;
pub mod spec;
+40
View File
@@ -21,6 +21,11 @@ enum Command {
#[arg(long)]
spec: PathBuf,
},
/// Check trusted runtime policy and report unavailable/unverified capabilities.
CheckRuntime {
#[arg(long)]
config: PathBuf,
},
/// Reserved execution entrypoint; refuses execution until isolation is implemented.
Run,
}
@@ -44,6 +49,7 @@ fn main() -> ExitCode {
ExitCode::from(2)
}
Command::Validate { spec } => validate_spec(spec),
Command::CheckRuntime { config } => check_runtime(config),
}
}
@@ -91,3 +97,37 @@ fn spec_error(code: &str, details: Option<serde_json::Value>) -> ExitCode {
eprintln!("{}", serde_json::json!({"error": code, "details": details}));
ExitCode::from(2)
}
fn check_runtime(path: PathBuf) -> ExitCode {
let Ok(file) = File::open(path) else {
return spec_error("cannot_read_runtime_config", None);
};
let mut bytes = Vec::new();
if file
.take(kf_sandbox::runtime::MAX_CONFIG_BYTES + 1)
.read_to_end(&mut bytes)
.is_err()
{
return spec_error("cannot_read_runtime_config", None);
}
if bytes.len() as u64 > kf_sandbox::runtime::MAX_CONFIG_BYTES {
return spec_error("runtime_config_too_large", None);
}
let request: kf_sandbox::runtime::RuntimeConfig = match serde_json::from_slice(&bytes) {
Ok(request) => request,
Err(error) => {
return spec_error(
"invalid_runtime_config_json",
Some(serde_json::json!({"line":error.line(), "column":error.column()})),
);
}
};
match request.validate() {
Ok(config) => {
let report = kf_sandbox::runtime::preflight(&config, &kf_sandbox::probe::observe());
eprintln!("{}", serde_json::json!(report));
ExitCode::from(2)
}
Err(error) => spec_error("invalid_runtime_config", Some(serde_json::json!(error))),
}
}
+6
View File
@@ -4,6 +4,8 @@ use std::fs;
#[derive(Debug, Serialize)]
pub struct EnvironmentObservation {
pub os: &'static str,
pub architecture: &'static str,
pub kernel_release: Option<String>,
pub namespace_entries: Vec<String>,
pub cgroup_v2_visible: bool,
pub cgroup_v2_controllers: Vec<String>,
@@ -40,6 +42,10 @@ pub fn observe() -> EnvironmentObservation {
});
EnvironmentObservation {
os: std::env::consts::OS,
architecture: std::env::consts::ARCH,
kernel_release: fs::read_to_string("/proc/sys/kernel/osrelease")
.ok()
.map(|release| release.trim().to_owned()),
namespace_entries,
cgroup_v2_visible,
cgroup_v2_controllers,
+367
View File
@@ -0,0 +1,367 @@
//! Trusted operator configuration and read-only refusal diagnostics.
//! Validation does not provision directories, reserve identities, apply a
//! resource budget or authenticate the configuration file's ownership.
use crate::probe::EnvironmentObservation;
use crate::spec::{ResourceLimits, SpecPolicy};
use kf_contracts::SchemaVersion;
use serde::{Deserialize, Serialize};
use std::path::Path;
/// Initial support policy, not a claim that version alone proves capability.
pub const MIN_KERNEL: (u16, u16) = (6, 6);
pub const MAX_CONFIG_BYTES: u64 = 65_536;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum ImageKind {
#[serde(rename = "linux_gnu_cxx_x86_64_v1")]
LinuxGnuCxxX86_64V1,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct IdentityPool {
pub worker_uid: u32,
pub worker_gid: u32,
pub first_task_uid: u32,
pub first_task_gid: u32,
pub count: u32,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AggregateBudget {
pub memory_bytes: u64,
pub supervisor_memory_bytes: u64,
pub processes: u32,
pub supervisor_processes: u32,
pub workdir_bytes: u64,
pub supervisor_workdir_bytes: u64,
pub cpu_period_us: u64,
pub cpu_quota_us: u64,
pub task_cpu_quota_us: u64,
pub supervisor_cpu_quota_us: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RuntimeConfig {
pub schema_version: SchemaVersion,
pub state_root: String,
pub tasks_root: String,
pub image_root: String,
pub image_kind: ImageKind,
pub delegated_cgroup_root: String,
pub identities: IdentityPool,
pub max_concurrent_runs: u32,
pub per_task_ceiling: ResourceLimits,
pub aggregate: AggregateBudget,
}
#[derive(Debug, PartialEq, Eq, Serialize)]
pub struct ConfigError {
pub field: &'static str,
pub code: &'static str,
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "{}: {}", self.field, self.code)
}
}
impl std::error::Error for ConfigError {}
fn invalid(field: &'static str, code: &'static str) -> ConfigError {
ConfigError { field, code }
}
/// Immutable structural policy; deliberately not an execution permit.
#[derive(Debug)]
pub struct ValidatedRuntimeConfig(RuntimeConfig);
impl ValidatedRuntimeConfig {
pub fn config(&self) -> &RuntimeConfig {
&self.0
}
pub fn spec_policy(&self) -> SpecPolicy {
SpecPolicy::with_limits_ceiling(self.0.per_task_ceiling.clone())
.expect("validated immutable limits ceiling")
}
/// Slot mapping only; the supervisor must lease slots until the old task
/// tree is empty and cleanup is complete, including after restart.
pub fn task_identity(&self, slot: u32) -> Option<(u32, u32)> {
(slot < self.0.max_concurrent_runs).then(|| {
(
self.0.identities.first_task_uid + slot,
self.0.identities.first_task_gid + slot,
)
})
}
}
fn canonical_absolute(value: &str) -> bool {
value.starts_with('/')
&& value.len() <= 4096
&& value[1..].split('/').all(|part| {
!part.is_empty()
&& part != "."
&& part != ".."
&& part.len() <= 255
&& part
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.'))
})
}
fn inside(path: &str, root: &str) -> bool {
path.strip_prefix(root)
.is_some_and(|tail| tail.starts_with('/'))
}
fn fits(each: u64, slots: u32, reserve: u64, total: u64) -> bool {
each.checked_mul(u64::from(slots))
.and_then(|sum| sum.checked_add(reserve))
.is_some_and(|required| required <= total)
}
impl RuntimeConfig {
pub fn validate(self) -> Result<ValidatedRuntimeConfig, ConfigError> {
for (field, value) in [
("state_root", &self.state_root),
("tasks_root", &self.tasks_root),
("image_root", &self.image_root),
("delegated_cgroup_root", &self.delegated_cgroup_root),
] {
if !canonical_absolute(value) {
return Err(invalid(field, "unsafe_path"));
}
}
if !(inside(&self.state_root, "/var/lib") || inside(&self.state_root, "/srv"))
|| !inside(&self.tasks_root, &self.state_root)
|| !inside(&self.image_root, &self.state_root)
|| self.tasks_root == self.image_root
|| inside(&self.tasks_root, &self.image_root)
|| inside(&self.image_root, &self.tasks_root)
{
return Err(invalid("roots", "invalid_layout"));
}
if !inside(&self.delegated_cgroup_root, "/sys/fs/cgroup") {
return Err(invalid("delegated_cgroup_root", "not_delegated_subtree"));
}
if !(1..=32).contains(&self.max_concurrent_runs) {
return Err(invalid("max_concurrent_runs", "out_of_range"));
}
let ids = &self.identities;
if ids.worker_uid < 1000
|| ids.worker_gid < 1000
|| ids.worker_uid == u32::MAX
|| ids.worker_gid == u32::MAX
|| ids.first_task_uid < 100_000
|| ids.first_task_gid < 100_000
|| ids.count < self.max_concurrent_runs
|| ids.count > 32
{
return Err(invalid("identities", "invalid_pool"));
}
for (first, worker) in [
(ids.first_task_uid, ids.worker_uid),
(ids.first_task_gid, ids.worker_gid),
] {
let Some(end) = first.checked_add(ids.count - 1) else {
return Err(invalid("identities", "integer_overflow"));
};
if end == u32::MAX || (first..=end).contains(&worker) {
return Err(invalid("identities", "overlapping_or_reserved_identity"));
}
}
SpecPolicy::with_limits_ceiling(self.per_task_ceiling.clone())
.map_err(|error| invalid(error.field, "invalid_task_ceiling"))?;
let budget = &self.aggregate;
if budget.supervisor_memory_bytes < 64 * 1024 * 1024
|| budget.supervisor_processes < 8
|| budget.supervisor_workdir_bytes < 1024 * 1024
|| !(1000..=1_000_000).contains(&budget.cpu_period_us)
|| budget.task_cpu_quota_us < 1000
|| budget.supervisor_cpu_quota_us < 1000
{
return Err(invalid("aggregate", "invalid_reservation"));
}
for (field, each, reserve, total) in [
(
"aggregate.memory_bytes",
self.per_task_ceiling.memory_bytes,
budget.supervisor_memory_bytes,
budget.memory_bytes,
),
(
"aggregate.processes",
u64::from(self.per_task_ceiling.processes),
u64::from(budget.supervisor_processes),
u64::from(budget.processes),
),
(
"aggregate.workdir_bytes",
self.per_task_ceiling.workdir_bytes,
budget.supervisor_workdir_bytes,
budget.workdir_bytes,
),
(
"aggregate.cpu_quota_us",
budget.task_cpu_quota_us,
budget.supervisor_cpu_quota_us,
budget.cpu_quota_us,
),
] {
if !fits(each, self.max_concurrent_runs, reserve, total) {
return Err(invalid(field, "insufficient_or_overflowing_budget"));
}
}
// Conservative first-deployment ceilings, not unlimited host settings.
if budget.memory_bytes > 64 * 1024 * 1024 * 1024
|| budget.workdir_bytes > 16 * 1024 * 1024 * 1024
|| budget.processes > 8192
|| budget.cpu_quota_us > budget.cpu_period_us * 64
{
return Err(invalid("aggregate", "exceeds_service_ceiling"));
}
Ok(ValidatedRuntimeConfig(self))
}
}
#[derive(Debug, Serialize)]
pub struct RefusalReason {
pub code: &'static str,
pub subject: &'static str,
}
#[derive(Debug, Serialize)]
pub struct RuntimePreflight {
pub error: &'static str,
pub configuration_valid: bool,
pub execution_allowed: bool,
pub reasons: Vec<RefusalReason>,
}
pub fn kernel_supported(release: &str) -> Option<bool> {
let mut parts = release.split('.');
let major = parts.next()?.parse::<u16>().ok()?;
let minor = parts.next()?.parse::<u16>().ok()?;
Some((major, minor) >= MIN_KERNEL)
}
/// Read-only observations. Every privileged capability remains unverified;
/// even a hypothetical complete filesystem does not authorize execution.
pub fn preflight(
config: &ValidatedRuntimeConfig,
observation: &EnvironmentObservation,
) -> RuntimePreflight {
let mut reasons = Vec::new();
let mut refuse = |code, subject| reasons.push(RefusalReason { code, subject });
if observation.os != "linux" {
refuse("unsupported_platform", "os");
}
if observation.architecture != "x86_64" {
refuse("unsupported_platform", "architecture");
}
match observation
.kernel_release
.as_deref()
.and_then(kernel_supported)
{
Some(true) => {}
Some(false) => refuse("kernel_below_policy_minimum", "kernel"),
None => refuse("kernel_unobservable", "kernel"),
}
for namespace in ["user", "mnt", "pid", "ipc", "uts", "net", "cgroup"] {
if !observation
.namespace_entries
.iter()
.any(|entry| entry == namespace)
{
refuse("namespace_not_visible", namespace);
}
}
if !observation.cgroup_v2_visible {
refuse("cgroup_v2_not_visible", "cgroup");
}
for controller in ["cpu", "memory", "pids"] {
if !observation
.cgroup_v2_controllers
.iter()
.any(|entry| entry == controller)
{
refuse("controller_not_visible", controller);
}
}
let policy = config.config();
for (subject, root) in [
("state_root", &policy.state_root),
("tasks_root", &policy.tasks_root),
("image_root", &policy.image_root),
("delegated_cgroup_root", &policy.delegated_cgroup_root),
] {
if !Path::new(root).is_dir() {
refuse("directory_not_visible", subject);
}
}
for interface in [
"cgroup.type",
"cgroup.subtree_control",
"cgroup.kill",
"cgroup.events",
"cpu.max",
"cpu.stat",
"memory.max",
"memory.swap.max",
"memory.peak",
"memory.events",
"memory.oom.group",
"pids.max",
"pids.events",
] {
if !Path::new(&policy.delegated_cgroup_root)
.join(interface)
.is_file()
{
refuse("cgroup_interface_not_visible", interface);
}
}
// A controlled directory image includes a GNU C/C++ toolchain, runtime
// loader and immutable manifest. Content/ownership checks come later.
for (subject, relative) in [
("image_manifest", "manifest.json"),
("gcc", "usr/bin/gcc"),
("g++", "usr/bin/g++"),
("dynamic_loader", "lib64/ld-linux-x86-64.so.2"),
] {
if !Path::new(&policy.image_root).join(relative).is_file() {
refuse("image_file_not_visible", subject);
}
}
for subject in [
"configuration_ownership",
"directory_containment",
"image_integrity",
"identity_pool_ownership",
"helper_privileges",
"cgroup_delegation",
"aggregate_budget_enforcement",
"pidfd_wait",
"namespace_setup",
"identity_drop",
"seccomp_filter",
"cleanup",
] {
refuse("capability_not_verified", subject);
}
refuse("sandbox_not_implemented", "isolated_execution");
RuntimePreflight {
error: "runtime_not_ready",
configuration_valid: true,
execution_allowed: false,
reasons,
}
}
+83
View File
@@ -0,0 +1,83 @@
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use std::process::Command;
use std::sync::atomic::{AtomicU32, Ordering};
const EXAMPLE: &str = include_str!("../../../../deploy/sandbox.runtime.example.json");
static SEQUENCE: AtomicU32 = AtomicU32::new(0);
struct ConfigFile(PathBuf);
impl ConfigFile {
fn new(contents: &[u8]) -> Self {
let path = std::env::temp_dir().join(format!(
"kf-runtime-{}-{}.json",
std::process::id(),
SEQUENCE.fetch_add(1, Ordering::Relaxed)
));
let mut file = fs::OpenOptions::new()
.create_new(true)
.write(true)
.open(&path)
.unwrap();
file.write_all(contents).unwrap();
Self(path)
}
fn check(&self) -> serde_json::Value {
let output = Command::new(env!("CARGO_BIN_EXE_kf-sandbox"))
.args(["check-runtime", "--config"])
.arg(&self.0)
.output()
.unwrap();
assert_eq!(output.status.code(), Some(2));
assert!(output.stdout.is_empty());
serde_json::from_slice(&output.stderr).unwrap()
}
}
impl Drop for ConfigFile {
fn drop(&mut self) {
let _ = fs::remove_file(&self.0);
}
}
#[test]
fn runtime_cli_reads_example_but_reports_unverified_capabilities_and_refuses() {
let report = ConfigFile::new(EXAMPLE.as_bytes()).check();
assert_eq!(report["configuration_valid"], true);
assert_eq!(report["execution_allowed"], false);
assert_eq!(report["error"], "runtime_not_ready");
assert!(
report["reasons"]
.as_array()
.unwrap()
.iter()
.any(|reason| reason["code"] == "sandbox_not_implemented")
);
}
#[test]
fn runtime_config_size_boundary_and_invalid_json_are_structured_without_echoing_content() {
let mut exact = EXAMPLE.as_bytes().to_vec();
exact.resize(kf_sandbox::runtime::MAX_CONFIG_BYTES as usize, b' ');
assert_eq!(ConfigFile::new(&exact).check()["configuration_valid"], true);
exact.push(b' ');
assert_eq!(
ConfigFile::new(&exact).check()["error"],
"runtime_config_too_large"
);
let report = ConfigFile::new(br#"{"secret":"PRIVATE-CONFIG", "broken":}"#).check();
assert_eq!(report["error"], "invalid_runtime_config_json");
assert!(!report.to_string().contains("PRIVATE-CONFIG"));
}
#[test]
fn runtime_cli_rejects_invalid_policy_and_missing_configuration() {
let mut invalid: serde_json::Value = serde_json::from_str(EXAMPLE).unwrap();
invalid["identities"]["worker_uid"] = serde_json::json!(0);
let report = ConfigFile::new(serde_json::to_string(&invalid).unwrap().as_bytes()).check();
assert_eq!(report["error"], "invalid_runtime_config");
assert_eq!(report["details"]["field"], "identities");
let missing = ConfigFile::new(EXAMPLE.as_bytes());
fs::remove_file(&missing.0).unwrap();
assert_eq!(missing.check()["error"], "cannot_read_runtime_config");
}
@@ -0,0 +1,318 @@
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",
"/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());
}