diff --git a/judge/crates/sandbox/examples/rlimit_probe.rs b/judge/crates/sandbox/examples/rlimit_probe.rs new file mode 100644 index 0000000..e75ad70 --- /dev/null +++ b/judge/crates/sandbox/examples/rlimit_probe.rs @@ -0,0 +1,114 @@ +//! Guarded fixed VM role applying the exact private production resource/reducer/filter code. +mod launcher { + pub mod entry { + pub use kf_sandbox::launcher::entry::ensure_single_threaded; + } +} +mod spec { + pub use kf_sandbox::spec::{ExecutionProfile, ValidatedSpec}; +} +#[allow(dead_code)] +#[path = "../src/isolation/identity.rs"] +pub(crate) mod identity; +#[allow(dead_code)] +#[path = "../src/isolation/rlimits.rs"] +mod rlimits; +#[allow(dead_code)] +#[path = "../src/isolation/seccomp.rs"] +pub(crate) mod seccomp; +mod isolation { + pub(crate) use crate::identity; +} +#[allow(dead_code)] +#[path = "../tests/support/mod.rs"] +mod support; +use std::os::unix::process::CommandExt; +fn main() { + support::require_vm(); + let args: Vec<_> = std::env::args().collect(); + assert_eq!(args.len(), 2); + let (profile, path, mode) = match args[1].as_str() { + "inherit" | "files" | "fds" | "stack" | "ceiling" => ( + spec::ExecutionProfile::Native, + "/kf/bin/rlimit-inherit-probe", + args[1].as_str(), + ), + "language-c" => ( + spec::ExecutionProfile::CompileC, + "/kf/bin/seccomp-language-probe", + "compile-c", + ), + "language-cpp" => ( + spec::ExecutionProfile::CompileCpp, + "/kf/bin/seccomp-language-probe", + "compile-cpp", + ), + "language-native" => ( + spec::ExecutionProfile::Native, + "/kf/bin/seccomp-language-probe", + "native", + ), + _ => panic!("fixed fixture mode"), + }; + let mut request = support::spec("rlimit-fixture", 64 << 20, 8).spec().clone(); + request.profile = profile; + request.limits = kf_sandbox::spec::ResourceLimits::for_profile(profile); + if profile != spec::ExecutionProfile::Native { + request.argv = vec![ + if profile == spec::ExecutionProfile::CompileC { + "/usr/bin/gcc" + } else { + "/usr/bin/g++" + } + .into(), + "/work/main".into(), + ]; + } + if !args[1].starts_with("language-") { + request.limits.file_bytes = 4096; + request.limits.stack_bytes = 262144; + request.limits.open_files = 16; + } + let spec = request + .validate(&kf_sandbox::spec::SpecPolicy::default()) + .unwrap(); + if args[1] == "ceiling" { + use rustix::process::{Resource, Rlimit}; + let before = rustix::process::getrlimit(Resource::Fsize); + rustix::process::setrlimit( + Resource::Nofile, + Rlimit { + current: Some(15), + maximum: Some(15), + }, + ) + .unwrap(); + let e = rlimits::RlimitPlan::from_spec(&spec) + .apply_current() + .unwrap_err(); + assert_eq!(e.kind(), std::io::ErrorKind::PermissionDenied); + assert_eq!( + rustix::process::getrlimit(Resource::Nofile).maximum, + Some(15) + ); + assert_eq!(rustix::process::getrlimit(Resource::Fsize), before); + println!( + "KF_RLIMIT_CEILING_PROOF inherited_hard15_retained=1 raise_refused=1 preflight_no_partial_mutation=1" + ); + return; + } + rlimits::RlimitPlan::from_spec(&spec) + .apply_current() + .unwrap(); + identity::reduce_current(100000, 100000).unwrap(); + seccomp::install(profile).unwrap(); + let mut command = std::process::Command::new(path); + command.arg(mode).env_clear(); + if let Some(path) = profile.compiler_search_path() { + command.env("PATH", path); + } + if args[1].starts_with("language-") { + command.env("TMPDIR", "/kf/seccomp-work"); + } + panic!("fixed fixture exec: {}", command.exec()); +} diff --git a/judge/crates/sandbox/src/isolation/mod.rs b/judge/crates/sandbox/src/isolation/mod.rs index b87e9fa..9332c0a 100644 --- a/judge/crates/sandbox/src/isolation/mod.rs +++ b/judge/crates/sandbox/src/isolation/mod.rs @@ -4,6 +4,7 @@ pub(crate) mod identity; pub(crate) mod inputs; pub mod namespaces; pub(crate) mod proc_dev; +pub(crate) mod rlimits; pub(crate) mod root; pub(crate) mod seccomp; // No public exec gate until resource supervision and lifecycle acceptance. diff --git a/judge/crates/sandbox/src/isolation/rlimits.rs b/judge/crates/sandbox/src/isolation/rlimits.rs new file mode 100644 index 0000000..70ab0e4 --- /dev/null +++ b/judge/crates/sandbox/src/isolation/rlimits.rs @@ -0,0 +1,136 @@ +//! Finite inherited per-process guardrails; task totals stay in cgroup/monitor. +//! Apply in the trusted single-threaded init before credentials/seccomp. Never +//! raise an inherited hard limit, use RLIMIT_CPU for aggregate CPU, or use +//! RLIMIT_AS/NPROC as substitutes for cgroup memory/pids. CORE is always zero. +use crate::spec::ValidatedSpec; +use rustix::process::{Resource, Rlimit}; +use serde::{Deserialize, Serialize}; +use std::io; +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RlimitPlan { + file_bytes: u64, + stack_bytes: u64, + open_files: u32, +} +fn denied(message: &'static str) -> io::Error { + io::Error::new(io::ErrorKind::PermissionDenied, message) +} +impl RlimitPlan { + pub(crate) fn from_spec(s: &ValidatedSpec) -> Self { + let l = &s.spec().limits; + Self { + file_bytes: l.file_bytes, + stack_bytes: l.stack_bytes, + open_files: l.open_files, + } + } + pub(crate) fn valid(&self) -> bool { + self.file_bytes > 0 + && self.file_bytes <= 64 << 20 + && self.stack_bytes > 0 + && self.stack_bytes <= 256 << 20 + && (3..=1024).contains(&self.open_files) + } + fn limits(self) -> [(Resource, u64); 4] { + [ + (Resource::Fsize, self.file_bytes), + (Resource::Nofile, self.open_files.into()), + (Resource::Stack, self.stack_bytes), + (Resource::Core, 0), + ] + } + pub(crate) fn apply_current(self) -> io::Result<()> { + crate::launcher::entry::ensure_single_threaded()?; + if !self.valid() || !rustix::process::geteuid().is_root() { + return Err(denied("rlimit initialization precondition")); + } + // Preflight every hard ceiling before the first irreversible mutation. + for (r, n) in self.limits() { + if rustix::process::getrlimit(r) + .maximum + .is_some_and(|old| old < n) + { + return Err(denied("rlimit would raise inherited hard ceiling")); + } + } + for (r, n) in self.limits() { + let expected = Rlimit { + current: Some(n), + maximum: Some(n), + }; + rustix::process::setrlimit(r, expected)?; + if rustix::process::getrlimit(r) != expected { + return Err(denied("rlimit kernel readback")); + } + } + Ok(()) + } + pub(crate) fn apply_init(self) -> io::Result<()> { + if rustix::process::getpid() != rustix::process::Pid::INIT { + return Err(denied("rlimit stage requires namespace init")); + } + self.apply_current() + } + pub(crate) fn verify_proc(self, text: &str) -> io::Result<()> { + if !self.valid() || text.len() > 4096 { + return Err(denied("rlimit readback size/plan")); + } + for (name, n, unit) in [ + ("Max file size", self.file_bytes, "bytes"), + ("Max open files", self.open_files.into(), "files"), + ("Max stack size", self.stack_bytes, "bytes"), + ("Max core file size", 0, "bytes"), + ] { + let mut lines = text.lines().filter_map(|line| line.strip_prefix(name)); + let line = lines + .next() + .ok_or_else(|| denied("missing rlimit readback"))?; + if lines.next().is_some() { + return Err(denied("duplicate rlimit readback")); + } + let parts: Vec<_> = line.split_whitespace().collect(); + if parts.len() != 3 + || parts[0].parse::().ok() != Some(n) + || parts[1].parse::().ok() != Some(n) + || parts[2] != unit + { + return Err(denied("rlimit readback mismatch")); + } + } + Ok(()) + } +} +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn readback_requires_both_finite_limits_correct_units_and_all_fields() { + let p = RlimitPlan { + file_bytes: 4096, + stack_bytes: 262144, + open_files: 16, + }; + let good = "Limit Soft Limit Hard Limit Units\nMax file size 4096 4096 bytes\nMax open files 16 16 files\nMax stack size 262144 262144 bytes\nMax core file size 0 0 bytes\n"; + p.verify_proc(good).unwrap(); + for bad in [ + good.replace("4096 4096", "4096 unlimited"), + good.replace("16 16 files", "16 32 files"), + good.replace("262144 262144 bytes", "262144 262144 files"), + good.replace("Max core file size 0 0 bytes\n", ""), + format!("{good}Max file size 4096 4096 bytes\n"), + ] { + assert!(p.verify_proc(&bad).is_err()); + } + for bad in [ + RlimitPlan { file_bytes: 0, ..p }, + RlimitPlan { + stack_bytes: 257 << 20, + ..p + }, + RlimitPlan { open_files: 2, ..p }, + ] { + assert!(!bad.valid()); + } + } +} diff --git a/judge/crates/sandbox/src/launcher/identity.rs b/judge/crates/sandbox/src/launcher/identity.rs index 19bb572..608ba66 100644 --- a/judge/crates/sandbox/src/launcher/identity.rs +++ b/judge/crates/sandbox/src/launcher/identity.rs @@ -179,6 +179,14 @@ impl IdentityReducedTarget { return Err(e); } }; + if let Err(e) = target.target.root.initialize_rlimits() { + let mut e = target + .target + .root + .refuse_stage(ExecutionStage::Preparation, Some(&e)); + release_after_cleanup(&mut lease, &mut e); + return Err(e); + } if let Err(e) = target.target.root.initialize_identity(lease.uid, lease.gid) { let mut e = target .target diff --git a/judge/crates/sandbox/src/launcher/namespaces.rs b/judge/crates/sandbox/src/launcher/namespaces.rs index 83586b6..5377e8f 100644 --- a/judge/crates/sandbox/src/launcher/namespaces.rs +++ b/judge/crates/sandbox/src/launcher/namespaces.rs @@ -189,6 +189,20 @@ pub fn namespace_init(expected_outer_parent: u32, initialize_root: bool) -> io:: NamespaceCommand::Cancel {} => return Ok(()), _ => return Err(denied("input command before preparation")), } + match channel.receive::()? { + NamespaceCommand::PrepareRlimits { plan } => { + let result = plan.apply_init(); + if let Err(e) = result { + let _ = channel.send(&NamespaceEvent::Failed { + errno: e.raw_os_error().and_then(|n| u16::try_from(n).ok()), + }); + return Err(e); + } + channel.send(&NamespaceEvent::RlimitsInitialized {})?; + } + NamespaceCommand::Cancel {} => return Ok(()), + _ => return Err(denied("rlimit command before inputs")), + } match channel.receive::()? { NamespaceCommand::PrepareIdentity { uid, gid } => { super::identity::initialize(&mut channel, uid, gid)?; @@ -225,6 +239,7 @@ pub struct NamespacedTarget { root_ready: bool, proc_dev_ready: bool, inputs_ready: bool, + rlimits_ready: bool, identity_ready: bool, seccomp_ready: bool, init_pid: u32, @@ -320,6 +335,7 @@ impl NamespacedTarget { root_ready: false, proc_dev_ready: false, inputs_ready: false, + rlimits_ready: false, identity_ready: false, seccomp_ready: false, }), @@ -396,8 +412,33 @@ impl NamespacedTarget { Ok(()) } + pub(crate) fn initialize_rlimits( + &mut self, + plan: crate::isolation::rlimits::RlimitPlan, + ) -> io::Result<()> { + if !self.inputs_ready || self.rlimits_ready || !plan.valid() { + return Err(denied("rlimit initialization order")); + } + self.initialize_filesystem( + NamespaceCommand::PrepareRlimits { plan }, + NamespaceEvent::RlimitsInitialized {}, + )?; + // Held, unreaped PID plus frozen exact-members cgroup prevents PID reuse + // and user mutation during the independent supervisor kernel readback. + use std::io::Read; + let mut bytes = String::new(); + std::fs::File::open(format!("/proc/{}/limits", self.init_pid))? + .take(4097) + .read_to_string(&mut bytes)?; + plan.verify_proc(&bytes)?; + self.rlimits_ready = true; + Ok(()) + } + pub(crate) fn initialize_identity(&mut self, uid: u32, gid: u32) -> io::Result<()> { - if !self.inputs_ready || self.identity_ready || !crate::isolation::identity::valid(uid, gid) + if !self.rlimits_ready + || self.identity_ready + || !crate::isolation::identity::valid(uid, gid) { return Err(denied("credential initialization order")); } diff --git a/judge/crates/sandbox/src/launcher/protocol.rs b/judge/crates/sandbox/src/launcher/protocol.rs index d98ab79..b6f544f 100644 --- a/judge/crates/sandbox/src/launcher/protocol.rs +++ b/judge/crates/sandbox/src/launcher/protocol.rs @@ -63,6 +63,9 @@ pub enum NamespaceCommand { executable: bool, }, FinishInputs {}, + PrepareRlimits { + plan: crate::isolation::rlimits::RlimitPlan, + }, PrepareIdentity { uid: u32, gid: u32, @@ -82,6 +85,7 @@ pub enum NamespaceEvent { InputsStarted {}, InputAttached {}, InputsInitialized {}, + RlimitsInitialized {}, IdentityInitialized {}, SeccompInitialized {}, Failed { errno: Option }, @@ -120,6 +124,7 @@ impl WireMessage for NamespaceCommand { && *bytes <= crate::artifacts::MAX_SNAPSHOT_BYTES } Self::PrepareIdentity { uid, gid } => crate::isolation::identity::valid(*uid, *gid), + Self::PrepareRlimits { plan } => plan.valid(), _ => true, } } diff --git a/judge/crates/sandbox/src/launcher/root.rs b/judge/crates/sandbox/src/launcher/root.rs index 4da5bb6..d5092c3 100644 --- a/judge/crates/sandbox/src/launcher/root.rs +++ b/judge/crates/sandbox/src/launcher/root.rs @@ -154,6 +154,15 @@ impl RootedTarget { self.lease.verify_root(self.bootstrap_pid()) } + pub(crate) fn initialize_rlimits(&mut self) -> io::Result<()> { + self.target + .initialize_rlimits(crate::isolation::rlimits::RlimitPlan::from_spec( + &self.request, + ))?; + self.lease.verify_root(self.init_pid())?; + self.lease.verify_root(self.bootstrap_pid()) + } + pub(crate) fn initialize_seccomp(&mut self) -> io::Result<()> { self.target .initialize_seccomp(self.request.spec().profile)?; diff --git a/judge/crates/sandbox/tests/fixtures/rlimit_deny.c b/judge/crates/sandbox/tests/fixtures/rlimit_deny.c new file mode 100644 index 0000000..cb578ad --- /dev/null +++ b/judge/crates/sandbox/tests/fixtures/rlimit_deny.c @@ -0,0 +1,31 @@ +/* VM-only kernel failure injector. It execs one fixed trusted test case. */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +int main(void) { + const char *vm=getenv("KF_SANDBOX_INDEPENDENT_VM"); + if(!vm||vm[0]!='1'||vm[1]||access("/kf/INDEPENDENT_VM",R_OK))return 2; + struct sock_filter filter[]={ + BPF_STMT(BPF_LD|BPF_W|BPF_ABS,offsetof(struct seccomp_data,arch)), + BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K,AUDIT_ARCH_X86_64,1,0), + BPF_STMT(BPF_RET|BPF_K,SECCOMP_RET_KILL_PROCESS), + BPF_STMT(BPF_LD|BPF_W|BPF_ABS,offsetof(struct seccomp_data,nr)), + BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K,__NR_prlimit64,0,3), + BPF_STMT(BPF_LD|BPF_W|BPF_ABS,offsetof(struct seccomp_data,args[2])), + BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K,0,1,0), + BPF_STMT(BPF_RET|BPF_K,SECCOMP_RET_ERRNO|EPERM), + BPF_STMT(BPF_RET|BPF_K,SECCOMP_RET_ALLOW) + }; + struct sock_fprog program={.len=sizeof filter/sizeof *filter,.filter=filter}; + if(prctl(PR_SET_NO_NEW_PRIVS,1,0,0,0)||prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&program))return 3; + setenv("KF_SANDBOX_DENY_RLIMIT","1",1); + execl("/kf/bin/rlimits-vm-tests","rlimits-vm-tests","--ignored","--test-threads=1","--exact","kernel_setter_failure_refuses_and_cleans_before_identity","--nocapture",(char*)0); + return 4; +} diff --git a/judge/crates/sandbox/tests/fixtures/rlimit_inherit_probe.c b/judge/crates/sandbox/tests/fixtures/rlimit_inherit_probe.c new file mode 100644 index 0000000..eba070c --- /dev/null +++ b/judge/crates/sandbox/tests/fixtures/rlimit_inherit_probe.c @@ -0,0 +1,48 @@ +/* Fixed resource-limit programs: guarded whole-capped independent VM only. */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +static void require(int yes,const char *why){if(!yes){fprintf(stderr,"rlimit: %s errno=%d\n",why,errno);exit(40);}} +static void verify(void){ + const int resources[]={RLIMIT_FSIZE,RLIMIT_NOFILE,RLIMIT_STACK,RLIMIT_CORE}; + const rlim_t limits[]={4096,16,262144,0}; + for(unsigned i=0;i<4;i++){struct rlimit r;require(getrlimit(resources[i],&r)==0&&r.rlim_cur==limits[i]&&r.rlim_max==limits[i],"finite soft/hard inheritance");} + require(getuid()==100000&&geteuid()==100000&&prctl(PR_GET_SECCOMP)==2,"reduced filtered identity"); +} +static void *thread(void *unused){(void)unused;verify();return NULL;} +static void recurse(unsigned n){volatile unsigned char bytes[8192];memset((void*)bytes,1,sizeof bytes);if(n)recurse(n-1);require(bytes[0]==1,"stack frame");} +int main(int argc,char **argv){ + require(argc==2,"fixed case");verify(); + if(!strcmp(argv[1],"inherited-exec")){puts("KF_RLIMIT_CHILD_EXEC_PROOF soft_hard=1");return 0;} + if(!strcmp(argv[1],"inherit")){ + pthread_t t;require(pthread_create(&t,NULL,thread,NULL)==0&&pthread_join(t,NULL)==0,"pthread inheritance"); + pid_t p=fork();require(p>=0,"fork inheritance"); + if(!p){verify();execl("/kf/bin/rlimit-inherit-probe","rlimit-inherit-probe","inherited-exec",(char*)0);_exit(41);} + int status;require(waitpid(p,&status,0)==p&&WIFEXITED(status)&&WEXITSTATUS(status)==0,"exec inheritance"); + struct rlimit raised={.rlim_cur=17,.rlim_max=17};errno=0;require(setrlimit(RLIMIT_NOFILE,&raised)==-1&&errno==EPERM,"no limit raising through filter"); + puts("KF_RLIMIT_PROOF mode=inherit fork=1 exec=1 pthread=1 four_limits=1 raise_denied=1");return 0; + } + if(!strcmp(argv[1],"files")){ + require(signal(SIGXFSZ,SIG_IGN)!=SIG_ERR,"SIGXFSZ ignore for EFBIG evidence"); + int fd=open("/kf/rlimit-work/limited-file",O_WRONLY|O_CREAT|O_EXCL,0600);require(fd>=0,"work file"); + char data[8192];memset(data,1,sizeof data); + require(write(fd,data,sizeof data)==4096,"finite file partial write");errno=0;require(write(fd,data,1)==-1&&errno==EFBIG,"file limit EFBIG");require(close(fd)==0,"file close"); + puts("KF_RLIMIT_PROOF mode=files finite4096=1 efbig=1");return 0; + } + if(!strcmp(argv[1],"fds")){ + int fds[20],n=0;for(;n<20;n++){fds[n]=open("/dev/null",O_RDONLY);if(fds[n]<0)break;require(fds[n]<16,"descriptor above NOFILE");} + require(n>0&&n<20&&errno==EMFILE,"FD limit EMFILE");for(int i=0;i Self { + support::require_vm(); + let p = PathBuf::from(format!("/kf/rlimits-publications-{}", std::process::id())); + fs::create_dir(&p).unwrap(); + fs::set_permissions(&p, fs::Permissions::from_mode(0o700)).unwrap(); + fs::write(p.join("program-001"), b"fixed-non-task-payload").unwrap(); + fs::write(p.join("input-001"), b"fixed-private-input").unwrap(); + Self(p) + } + 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 request(profile: ExecutionProfile) -> kf_sandbox::spec::ValidatedSpec { + let mut s = support::spec("rlimit-production", 64 << 20, 8) + .spec() + .clone(); + s.profile = profile; + s.limits = ResourceLimits::for_profile(profile); + if profile != ExecutionProfile::Native { + s.argv = vec![ + if profile == ExecutionProfile::CompileC { + "/usr/bin/gcc" + } else { + "/usr/bin/g++" + } + .into(), + "-c".into(), + "/work/main.cpp".into(), + ]; + s.outputs = vec!["main.o".into()]; + } + s.validate(&SpecPolicy::default()).unwrap() +} +fn prepare( + root: &support::Root, + p: &Publications, + s: &kf_sandbox::spec::ValidatedSpec, +) -> Result { + FilteredTarget::prepare(&root.validated(), s, &support::binary(), &p.root()) +} +fn limits(text: &str, name: &str) -> Vec { + text.lines() + .find_map(|l| l.strip_prefix(name)) + .unwrap() + .split_whitespace() + .map(String::from) + .collect() +} +#[test] +#[ignore = "independent capped VM: production pre-credential resource stage and all profiles"] +fn production_init_applies_and_reads_back_finite_limits_before_identity_and_seccomp() { + support::require_vm(); + let root = support::Root::new(); + let p = Publications::new(); + let before = support::children(); + for profile in [ + ExecutionProfile::Native, + ExecutionProfile::CompileC, + ExecutionProfile::CompileCpp, + ] { + let s = request(profile); + let target = prepare(&root, &p, &s).unwrap(); + let text = + fs::read_to_string(format!("/proc/{}/limits", target.root().init_pid())).unwrap(); + for (name, n, unit) in [ + ("Max file size", s.spec().limits.file_bytes, "bytes"), + ("Max stack size", s.spec().limits.stack_bytes, "bytes"), + ("Max open files", s.spec().limits.open_files.into(), "files"), + ("Max core file size", 0, "bytes"), + ] { + assert_eq!( + limits(&text, name), + vec![n.to_string(), n.to_string(), unit.into()] + ); + } + let status = + fs::read_to_string(format!("/proc/{}/status", target.root().init_pid())).unwrap(); + assert!(status.contains("NoNewPrivs:\t1\n") && status.contains("Seccomp:\t2\n")); + assert_ne!(target.identity().0, 0); + let e = target.refuse_remaining_isolation(); + assert_eq!(e.failure.stage, ExecutionStage::Exec); + assert!(e.cleanup_failure.is_none()); + assert_eq!(support::children(), before); + assert!(!root.path.join("kf-rlimit-production").exists()); + assert!(!Path::new("/var/lib/kf-judge/tasks/kf-rlimit-production").exists()); + assert_eq!( + fs::read_dir("/var/lib/kf-judge/identities") + .unwrap() + .count(), + 0 + ); + } + println!( + "KF_RLIMIT_PRODUCTION_PROOF all_profiles=1 finite_soft_hard=1 core0=1 independent_readback=1 identity_seccomp=1 no_exec_permit=1 cleanup=1" + ); +} +#[test] +#[ignore = "independent capped VM: exact production setter then real fork/exec/thread and file/FD/stack limits"] +fn real_inheritance_file_fd_and_stack_limits() { + support::require_vm(); + for mode in ["inherit", "files", "fds", "stack", "ceiling"] { + let output = Command::new("/kf/bin/rlimit-probe") + .arg(mode) + .env_clear() + .env("KF_SANDBOX_INDEPENDENT_VM", "1") + .output() + .unwrap(); + let stdout = String::from_utf8(output.stdout).unwrap(); + let stderr = String::from_utf8_lossy(&output.stderr); + if mode == "ceiling" { + assert!(output.status.success(), "{stdout} {stderr}"); + assert!(stdout.contains("KF_RLIMIT_CEILING_PROOF")); + print!("{stdout}"); + } else if mode == "stack" { + assert_eq!(output.status.signal(), Some(11), "{stdout} {stderr}"); + assert!(stdout.contains("KF_RLIMIT_STACK_READY")); + println!("KF_RLIMIT_PROOF mode=stack sigsegv=1 core0=1 bounded128frames=1"); + } else { + assert!(output.status.success(), "{mode} {stdout} {stderr}"); + assert!(stdout.contains(&format!("KF_RLIMIT_PROOF mode={mode}"))); + print!("{stdout}"); + } + } + assert_eq!( + fs::metadata("/kf/rlimit-work/limited-file").unwrap().len(), + 4096 + ); + fs::remove_file("/kf/rlimit-work/limited-file").unwrap(); + assert_eq!(fs::read_dir("/kf/rlimit-work").unwrap().count(), 0); +} +#[test] +#[ignore = "independent capped VM: actual default RLIMIT with GNU C/C++ and dynamic native threads"] +fn profile_defaults_support_actual_gnu_and_native_dynamic_threaded_io() { + support::require_vm(); + for (mode, profile) in [ + ("language-c", "compile-c"), + ("language-cpp", "compile-cpp"), + ("language-native", "native"), + ] { + let output = Command::new("/kf/bin/rlimit-probe") + .arg(mode) + .env_clear() + .env("KF_SANDBOX_INDEPENDENT_VM", "1") + .output() + .unwrap(); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!( + output.status.success(), + "{stdout} {}", + String::from_utf8_lossy(&output.stderr) + ); + assert!(stdout.contains(&format!("KF_SECCOMP_LANGUAGE_PROOF profile={profile}"))); + print!("{stdout}"); + } + println!( + "KF_RLIMIT_LANGUAGE_PROOF default_profiles=1 real_gnu=1 dynamic_pthread_binary_empty_exit7=1" + ); +} +#[test] +#[ignore = "independent capped VM: real prlimit64 setter kernel EPERM injector"] +fn kernel_setter_failure_refuses_and_cleans_before_identity() { + support::require_vm(); + assert_eq!(std::env::var("KF_SANDBOX_DENY_RLIMIT").as_deref(), Ok("1")); + let root = support::Root::new(); + let p = Publications::new(); + let before = support::children(); + let e = match prepare(&root, &p, &request(ExecutionProfile::Native)) { + Ok(_) => panic!("kernel RLIMIT failure accepted"), + Err(e) => e, + }; + assert_eq!(e.failure.stage, ExecutionStage::Preparation); + assert_eq!(e.failure.errno, Some(1)); + assert!(e.cleanup_failure.is_none()); + assert_eq!(support::children(), before); + assert!(!root.path.join("kf-rlimit-production").exists()); + assert!(!Path::new("/var/lib/kf-judge/tasks/kf-rlimit-production").exists()); + assert_eq!( + fs::read_dir("/var/lib/kf-judge/identities") + .unwrap() + .count(), + 0 + ); + println!( + "KF_RLIMIT_FAILURE_PROOF actual_kernel_eperm=1 pre_identity=1 ack_refused=1 owned_reaped=1 cg_root_lease_removed=1" + ); +}