254 lines
9.8 KiB
Rust
254 lines
9.8 KiB
Rust
#![cfg(target_os = "linux")]
|
|
mod support;
|
|
use kf_sandbox::artifacts::ArtifactRoot;
|
|
use kf_sandbox::launcher::identity::IdentityReducedTarget;
|
|
use kf_sandbox::outcome::ExecutionStage;
|
|
use std::os::unix::fs::PermissionsExt;
|
|
use std::{
|
|
fs,
|
|
path::{Path, PathBuf},
|
|
process::Command,
|
|
};
|
|
use support::{Root, binary, children};
|
|
const LEASES: &str = "/var/lib/kf-judge/identities";
|
|
struct Publications(PathBuf);
|
|
impl Publications {
|
|
fn new() -> Self {
|
|
support::require_vm();
|
|
let path = PathBuf::from(format!("/kf/identity-publications-{}", std::process::id()));
|
|
fs::create_dir(&path).unwrap();
|
|
fs::set_permissions(&path, fs::Permissions::from_mode(0o700)).unwrap();
|
|
fs::write(path.join("program-001"), b"fixed-non-task-payload").unwrap();
|
|
fs::write(path.join("input-001"), b"private-stdin").unwrap();
|
|
Self(path)
|
|
}
|
|
fn root(&self) -> ArtifactRoot {
|
|
ArtifactRoot::from_directory(
|
|
rustix::fs::open(
|
|
&self.0,
|
|
rustix::fs::OFlags::PATH
|
|
| rustix::fs::OFlags::DIRECTORY
|
|
| rustix::fs::OFlags::CLOEXEC,
|
|
rustix::fs::Mode::empty(),
|
|
)
|
|
.unwrap(),
|
|
)
|
|
.unwrap()
|
|
}
|
|
}
|
|
impl Drop for Publications {
|
|
fn drop(&mut self) {
|
|
fs::remove_dir_all(&self.0).unwrap();
|
|
}
|
|
}
|
|
fn prepare(
|
|
root: &Root,
|
|
publications: &Publications,
|
|
id: &str,
|
|
) -> Result<IdentityReducedTarget, kf_sandbox::launcher::blocked::InitializationFailure> {
|
|
IdentityReducedTarget::prepare(
|
|
&root.validated(),
|
|
&support::spec(id, 64 << 20, 8),
|
|
&binary(),
|
|
&publications.root(),
|
|
)
|
|
}
|
|
fn leases() -> Vec<String> {
|
|
let mut result: Vec<_> = fs::read_dir(LEASES)
|
|
.unwrap()
|
|
.map(|e| e.unwrap().file_name().into_string().unwrap())
|
|
.collect();
|
|
result.sort();
|
|
result
|
|
}
|
|
fn verify_ids(target: &IdentityReducedTarget) {
|
|
let (uid, gid) = target.identity();
|
|
let status = fs::read_to_string(format!("/proc/{}/status", target.root().init_pid())).unwrap();
|
|
for (name, id) in [("Uid:", uid), ("Gid:", gid)] {
|
|
let line = status.lines().find(|l| l.starts_with(name)).unwrap();
|
|
assert_eq!(
|
|
line.split_whitespace()
|
|
.skip(1)
|
|
.map(|s| s.parse::<u32>().unwrap())
|
|
.collect::<Vec<_>>(),
|
|
vec![id; 4]
|
|
);
|
|
}
|
|
for name in ["CapInh:", "CapPrm:", "CapEff:", "CapBnd:", "CapAmb:"] {
|
|
assert_eq!(
|
|
u64::from_str_radix(
|
|
status
|
|
.lines()
|
|
.find(|l| l.starts_with(name))
|
|
.unwrap()
|
|
.split_whitespace()
|
|
.nth(1)
|
|
.unwrap(),
|
|
16
|
|
)
|
|
.unwrap(),
|
|
0
|
|
);
|
|
}
|
|
assert_eq!(
|
|
status
|
|
.lines()
|
|
.find(|l| l.starts_with("Groups:"))
|
|
.unwrap()
|
|
.split_whitespace()
|
|
.count(),
|
|
1
|
|
);
|
|
assert!(status.contains("NoNewPrivs:\t1\n"));
|
|
assert!(target.root().snapshot_outputs().unwrap().is_empty());
|
|
}
|
|
#[test]
|
|
#[ignore = "requires independent capped VM and fixed credential inheritance fixture"]
|
|
fn production_init_reduces_credentials_and_leases_distinct_ids_until_cleanup() {
|
|
let root = Root::new();
|
|
let publications = Publications::new();
|
|
let before = children();
|
|
let one = prepare(&root, &publications, "identity-one").unwrap();
|
|
let two = prepare(&root, &publications, "identity-two").unwrap();
|
|
assert_eq!(one.identity(), (100000, 100000));
|
|
assert_eq!(two.identity(), (100001, 100001));
|
|
verify_ids(&one);
|
|
verify_ids(&two);
|
|
assert_eq!(leases(), ["uid-100000", "uid-100001"]);
|
|
let Err(busy) = prepare(&root, &publications, "identity-busy") else {
|
|
panic!("live identity reused");
|
|
};
|
|
assert_eq!(busy.failure.stage, ExecutionStage::Identity);
|
|
assert_eq!(busy.failure.errno, Some(11));
|
|
assert!(busy.cleanup_failure.is_none());
|
|
let output = Command::new("/kf/bin/identity-probe")
|
|
.args(["fixed-probe", &two.root().init_pid().to_string()])
|
|
.env_clear()
|
|
.env("KF_SANDBOX_INDEPENDENT_VM", "1")
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
output.status.success(),
|
|
"{:?} {:?}",
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
let proof = String::from_utf8(output.stdout).unwrap();
|
|
assert!(proof.contains("KF_IDENTITY_REDUCER_PROOF"));
|
|
print!("{proof}");
|
|
let result = one.refuse_remaining_isolation();
|
|
assert_eq!(result.failure.stage, ExecutionStage::Seccomp);
|
|
assert!(result.cleanup_failure.is_none());
|
|
let reuse = prepare(&root, &publications, "identity-reuse").unwrap();
|
|
assert_eq!(reuse.identity(), (100000, 100000));
|
|
verify_ids(&reuse);
|
|
drop(reuse);
|
|
drop(two);
|
|
assert!(leases().is_empty());
|
|
assert_eq!(children(), before);
|
|
for id in [
|
|
"identity-one",
|
|
"identity-two",
|
|
"identity-busy",
|
|
"identity-reuse",
|
|
] {
|
|
assert!(!root.path.join(format!("kf-{id}")).exists());
|
|
assert!(!Path::new(&format!("/var/lib/kf-judge/tasks/kf-{id}")).exists());
|
|
}
|
|
println!(
|
|
"KF_IDENTITY_PROOF all_four_ids=1 groups=0 five_cap_sets=0 nnp=1 distinct_leases=1 exhaustion_refused=1 release_after_cleanup=1 safe_reuse=1 drop_cleanup=1 fixed_fixture_not_user_exec=1"
|
|
);
|
|
}
|
|
#[test]
|
|
#[ignore = "requires independent capped VM"]
|
|
fn foreign_markers_and_unsafe_lease_roots_are_not_adopted() {
|
|
let root = Root::new();
|
|
let publications = Publications::new();
|
|
let before = children();
|
|
fs::create_dir_all(LEASES).unwrap();
|
|
fs::set_permissions(LEASES, fs::Permissions::from_mode(0o700)).unwrap();
|
|
let foreign = Path::new(LEASES).join("uid-100000");
|
|
fs::create_dir(&foreign).unwrap();
|
|
fs::set_permissions(&foreign, fs::Permissions::from_mode(0o700)).unwrap();
|
|
fs::write(foreign.join("foreign"), b"must retain").unwrap();
|
|
let target = prepare(&root, &publications, "identity-foreign").unwrap();
|
|
assert_eq!(target.identity(), (100001, 100001));
|
|
drop(target);
|
|
assert_eq!(fs::read(foreign.join("foreign")).unwrap(), b"must retain");
|
|
fs::remove_file(foreign.join("foreign")).unwrap();
|
|
fs::remove_dir(&foreign).unwrap();
|
|
fs::set_permissions(LEASES, fs::Permissions::from_mode(0o777)).unwrap();
|
|
let Err(error) = prepare(&root, &publications, "identity-unsafe") else {
|
|
panic!("writable lease root accepted");
|
|
};
|
|
assert_eq!(error.failure.stage, ExecutionStage::Identity);
|
|
assert!(error.cleanup_failure.is_none());
|
|
assert!(leases().is_empty());
|
|
fs::set_permissions(LEASES, fs::Permissions::from_mode(0o700)).unwrap();
|
|
fs::remove_file(publications.0.join("program-001")).unwrap();
|
|
let Err(error) = prepare(&root, &publications, "identity-bad-input") else {
|
|
panic!("missing input accepted");
|
|
};
|
|
assert_eq!(error.failure.stage, ExecutionStage::Preparation);
|
|
assert!(error.cleanup_failure.is_none());
|
|
assert!(leases().is_empty());
|
|
assert_eq!(children(), before);
|
|
println!(
|
|
"KF_IDENTITY_LEASE_POLICY_PROOF foreign_retained=1 writable_root_refused=1 failed_preparation_slot_released=1 no_target_leftover=1"
|
|
);
|
|
}
|
|
#[test]
|
|
#[ignore = "requires independent capped VM; cleanup failure injection"]
|
|
fn incomplete_target_cleanup_keeps_identity_reserved() {
|
|
let root = Root::new();
|
|
let publications = Publications::new();
|
|
let before = children();
|
|
let target = prepare(&root, &publications, "identity-cleanup-failure").unwrap();
|
|
let task = Path::new("/var/lib/kf-judge/tasks/kf-identity-cleanup-failure");
|
|
fs::write(task.join("foreign"), b"keep").unwrap();
|
|
let error = target.refuse_remaining_isolation();
|
|
assert_eq!(error.failure.stage, ExecutionStage::Seccomp);
|
|
assert!(error.cleanup_failure.is_some());
|
|
assert_eq!(leases(), ["uid-100000"]);
|
|
assert!(!root.path.join("kf-identity-cleanup-failure").exists());
|
|
assert_eq!(children(), before);
|
|
let next = prepare(&root, &publications, "identity-after-failed-cleanup").unwrap();
|
|
assert_eq!(next.identity(), (100001, 100001));
|
|
drop(next);
|
|
assert_eq!(fs::read(task.join("foreign")).unwrap(), b"keep");
|
|
// VM operator restores the test baseline only after proving both task
|
|
// cgroups/children gone. Fresh-process restart recovery is tested separately.
|
|
fs::remove_file(task.join("foreign")).unwrap();
|
|
fs::remove_dir(task).unwrap();
|
|
fs::remove_file(Path::new(LEASES).join("uid-100000/owner.json")).unwrap();
|
|
fs::remove_dir(Path::new(LEASES).join("uid-100000")).unwrap();
|
|
assert!(leases().is_empty());
|
|
println!(
|
|
"KF_IDENTITY_CLEANUP_PROOF cleanup_error_separate=1 failed_slot_reserved=1 alternate_slot_only=1 foreign_not_deleted=1 test_baseline_operator_restore=1"
|
|
);
|
|
}
|
|
#[test]
|
|
#[ignore = "requires independent capped VM with kernel setgroups failure injector"]
|
|
fn kernel_credential_failure_refuses_and_releases_own_lease() {
|
|
let root = Root::new();
|
|
assert_eq!(
|
|
std::env::var("KF_SANDBOX_DENY_SETGROUPS").as_deref(),
|
|
Ok("1")
|
|
);
|
|
let publications = Publications::new();
|
|
let before = children();
|
|
let Err(error) = prepare(&root, &publications, "identity-kernel-denied") else {
|
|
panic!("credential error ignored");
|
|
};
|
|
assert_eq!(error.failure.stage, ExecutionStage::Identity);
|
|
assert_eq!(error.failure.errno, Some(1));
|
|
assert!(error.cleanup_failure.is_none());
|
|
assert!(leases().is_empty());
|
|
assert_eq!(children(), before);
|
|
assert!(!root.path.join("kf-identity-kernel-denied").exists());
|
|
assert!(!Path::new("/var/lib/kf-judge/tasks/kf-identity-kernel-denied").exists());
|
|
println!(
|
|
"KF_IDENTITY_FAILURE_PROOF real_kernel_eperm=1 identity_ack_refused=1 slot_released_after_teardown=1 child_reaped=1 task_cleanup=1"
|
|
);
|
|
}
|