feat(sandbox): apply finite inherited resource limits before identity

This commit is contained in:
2026-10-08 01:37:06 +08:00
parent 4c7adda719
commit 62b08fea5b
10 changed files with 613 additions and 1 deletions
@@ -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());
}
@@ -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.
@@ -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::<u64>().ok() != Some(n)
|| parts[1].parse::<u64>().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());
}
}
}
@@ -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
@@ -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>()? {
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>()? {
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"));
}
@@ -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<u16> },
@@ -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,
}
}
@@ -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)?;
+31
View File
@@ -0,0 +1,31 @@
/* VM-only kernel failure injector. It execs one fixed trusted test case. */
#define _GNU_SOURCE
#include <linux/audit.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <errno.h>
#include <stddef.h>
#include <stdlib.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <unistd.h>
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;
}
@@ -0,0 +1,48 @@
/* Fixed resource-limit programs: guarded whole-capped independent VM only. */
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/resource.h>
#include <sys/wait.h>
#include <unistd.h>
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<n;i++)require(close(fds[i])==0,"FD close");
puts("KF_RLIMIT_PROOF mode=fds emfile=1 maxfd15=1");return 0;
}
if(!strcmp(argv[1],"stack")){puts("KF_RLIMIT_STACK_READY soft_hard262144=1 core0=1");fflush(stdout);recurse(128);return 42;}
return 43;
}
+219
View File
@@ -0,0 +1,219 @@
#![cfg(target_os = "linux")]
#[allow(dead_code)]
mod support;
use kf_sandbox::{
artifacts::ArtifactRoot,
launcher::seccomp::FilteredTarget,
outcome::ExecutionStage,
spec::{ExecutionProfile, ResourceLimits, SpecPolicy},
};
use std::os::unix::{fs::PermissionsExt, process::ExitStatusExt};
use std::{
fs,
path::{Path, PathBuf},
process::Command,
};
struct Publications(PathBuf);
impl Publications {
fn new() -> 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, kf_sandbox::launcher::blocked::InitializationFailure> {
FilteredTarget::prepare(&root.validated(), s, &support::binary(), &p.root())
}
fn limits(text: &str, name: &str) -> Vec<String> {
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"
);
}