feat(sandbox): apply finite inherited resource limits before identity
This commit is contained in:
@@ -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)?;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user