feat(sandbox): install profile seccomp with ABI and inheritance checks

This commit is contained in:
2026-10-07 19:16:20 +08:00
parent 46714bd9ea
commit 9bb27f2e02
15 changed files with 826 additions and 5 deletions
+11
View File
@@ -385,7 +385,9 @@ version = "0.0.1"
dependencies = [
"clap",
"kf-contracts",
"libc",
"rustix",
"seccompiler",
"serde",
"serde_json",
]
@@ -556,6 +558,15 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "seccompiler"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4ae55de56877481d112a559bbc12667635fdaf5e005712fd4e2b2fa50ffc884"
dependencies = [
"libc",
]
[[package]]
name = "serde"
version = "1.0.229"
+24
View File
@@ -352,3 +352,27 @@ setuid-root 文件无法恢复权限,跨 UID 信号/ptrace/文件访问拒绝
注入真实 setgroups EPERM,验证失败回滚;它不构成生产 seccomp 策略。fixture 仅可在
规定独立 VM 使用,没有向 CLI 增加用户执行命令。后续 seccomp、全任务树/stdio/计量
和运行后清理仍未验收,`run` 继续拒绝。
### 生产 seccomp 初始化检查点
`launcher/seccomp.rs` 的 `FilteredTarget` 在已降权的固定 PID 1 安装策略,父监督器
冻结后读回 `Seccomp=2`、`NoNewPrivs=1` 和增加一个过滤器;内核错误按 seccomp 阶段
拒绝,并先清空/回收任务再释放身份租约。这个类型仍在 exec 阶段拒绝,CLI `run` 未启用。
`isolation/seccomp.rs` 固定支持 Linux x86_64,按 native、compile_c、compile_cpp
建立默认 EPERM 的 syscall 白名单;兼容 i386 ABI 终止整个调用进程,x32/未知编号
拒绝。clone 只允许明确的普通 fork 或 pthread flags;编译 profile 另允许 vfork 和
编译输出权限设置。clone3 的指针参数不可由经典 BPF 检查,因此返回 ENOSYS,交由
glibc 回退到受检查的 clone。过滤器由固定 Rust 数据生成,经 seccompiler 0.5.0 的
安全 API 安装;请求不能传入 BPF。libc 仅提供 ABI 常量,依赖均限制在沙箱 crate。
允许的语言操作仍受独立根目录、只读输入、任务身份和 cgroup 限额约束;seccomp
不能检查路径字符串。控制通道 FD 3 的 recvmsg/sendmsg 和设置 I/O 超时仅用于可信
初始化,FD 为 CLOEXEC;最终用户 stdio/FD 替换仍须另行验收。通用信号发送、网络
创建、ptrace、namespace 重建和权限修改没有语言例外。pthread 取消/主动发送信号、
用户自建 namespace、clone3 专用功能和非 x86_64 程序当前不受支持。
`seccomp_vm.rs` 及固定 VM fixture 验证集成 init 安装、冻结读回、失败清理,以及
fork/exec/pthread 的真实过滤器继承、i386 SIGSYS、x32 EPERM 和 clone3 ENOSYS。
这些 fixture 仅在无共享挂载/无凭据、整体资源受限的独立 VM 中运行;普通 BPF
回归不是内核隔离验收。进程树监督、最终 stdio 和完整运行安全验收仍待实现。
+5
View File
@@ -12,6 +12,11 @@ serde.workspace = true
serde_json.workspace = true
rustix = { version = "1.1.5", features = ["net", "process", "fs", "thread", "mount", "system", "stdio"] }
# Fixed BPF compilation/installation through safe APIs; libc supplies syscall
# numbers only. Both dependencies stay inside the OS sandbox boundary.
seccompiler = { version = "=0.5.0", default-features = false }
libc = "=0.2.190"
[lints]
workspace = true
@@ -0,0 +1,40 @@
//! VM-only fixed fixture, using the exact private production policy/reducer.
mod launcher {
pub mod entry {
pub use kf_sandbox::launcher::entry::ensure_single_threaded;
}
}
mod spec {
pub use kf_sandbox::spec::ExecutionProfile;
}
#[allow(dead_code)]
#[path = "../src/isolation/identity.rs"]
pub(crate) mod identity;
mod isolation {
pub(crate) use crate::identity;
}
#[allow(dead_code)]
#[path = "../src/isolation/seccomp.rs"]
mod policy;
#[path = "../tests/support/mod.rs"]
#[allow(dead_code)]
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 = match args[1].as_str() {
"native" => spec::ExecutionProfile::Native,
"compile-c" => spec::ExecutionProfile::CompileC,
"compile-cpp" => spec::ExecutionProfile::CompileCpp,
_ => panic!("fixed fixture profile"),
};
isolation::identity::reduce_current(100000, 100000).unwrap();
policy::install(profile).unwrap();
let e = std::process::Command::new("/kf/bin/seccomp-inherit-probe")
.arg(&args[1])
.env_clear()
.exec();
panic!("fixed fixture exec: {e}");
}
@@ -5,3 +5,4 @@ pub(crate) mod inputs;
pub mod namespaces;
pub(crate) mod proc_dev;
pub(crate) mod root;
pub(crate) mod seccomp;
@@ -0,0 +1,447 @@
//! Fixed x86_64 syscall policy, not request-supplied BPF or a user execution permit.
use crate::spec::ExecutionProfile;
use seccompiler::{
BpfProgram, SeccompAction, SeccompCmpArgLen::Qword, SeccompCmpOp, SeccompCondition,
SeccompFilter, SeccompRule, TargetArch, sock_filter,
};
use std::{collections::BTreeMap, io};
const AUDIT_X86_64: u32 = 0xc000_003e;
const MAX_INSTRUCTIONS: usize = 2048;
fn denied(message: &'static str) -> io::Error {
io::Error::new(io::ErrorKind::PermissionDenied, message)
}
fn condition(arg: u8, op: SeccompCmpOp, value: u64) -> io::Result<SeccompCondition> {
SeccompCondition::new(arg, Qword, op, value).map_err(io::Error::other)
}
fn rules(conditions: Vec<Vec<SeccompCondition>>) -> io::Result<Vec<SeccompRule>> {
conditions
.into_iter()
.map(|c| SeccompRule::new(c).map_err(io::Error::other))
.collect()
}
pub(crate) fn compile(profile: ExecutionProfile) -> io::Result<BpfProgram> {
if !cfg!(target_arch = "x86_64") {
return Err(denied("unsupported seccomp architecture"));
}
let mut allowed = BTreeMap::new();
// Ordinary glibc, C/C++ I/O, loader, memory, clocks, synchronization and
// child waiting. Paths/FDs still require the independent filesystem and
// descriptor boundaries; seccomp cannot inspect pointed-to path strings.
for nr in [
libc::SYS_read,
libc::SYS_write,
libc::SYS_readv,
libc::SYS_writev,
libc::SYS_pread64,
libc::SYS_pwrite64,
libc::SYS_preadv,
libc::SYS_pwritev,
libc::SYS_open,
libc::SYS_openat,
libc::SYS_close,
libc::SYS_close_range,
libc::SYS_lseek,
libc::SYS_stat,
libc::SYS_lstat,
libc::SYS_fstat,
libc::SYS_newfstatat,
libc::SYS_statx,
libc::SYS_access,
libc::SYS_faccessat,
libc::SYS_faccessat2,
libc::SYS_getdents64,
libc::SYS_readlink,
libc::SYS_readlinkat,
libc::SYS_getcwd,
libc::SYS_chdir,
libc::SYS_fchdir,
libc::SYS_dup,
libc::SYS_dup2,
libc::SYS_dup3,
libc::SYS_fcntl,
libc::SYS_pipe,
libc::SYS_pipe2,
libc::SYS_poll,
libc::SYS_ppoll,
libc::SYS_select,
libc::SYS_pselect6,
libc::SYS_epoll_create1,
libc::SYS_epoll_ctl,
libc::SYS_epoll_wait,
libc::SYS_epoll_pwait,
libc::SYS_eventfd2,
libc::SYS_mmap,
libc::SYS_mprotect,
libc::SYS_munmap,
libc::SYS_mremap,
libc::SYS_madvise,
libc::SYS_brk,
libc::SYS_msync,
libc::SYS_mincore,
libc::SYS_futex,
libc::SYS_set_tid_address,
libc::SYS_set_robust_list,
libc::SYS_get_robust_list,
libc::SYS_rseq,
libc::SYS_rt_sigaction,
libc::SYS_rt_sigprocmask,
libc::SYS_rt_sigreturn,
libc::SYS_sigaltstack,
libc::SYS_rt_sigsuspend,
libc::SYS_rt_sigtimedwait,
libc::SYS_restart_syscall,
libc::SYS_clock_gettime,
libc::SYS_clock_getres,
libc::SYS_clock_nanosleep,
libc::SYS_nanosleep,
libc::SYS_gettimeofday,
libc::SYS_time,
libc::SYS_sched_yield,
libc::SYS_sched_getaffinity,
libc::SYS_getrandom,
libc::SYS_uname,
libc::SYS_getpid,
libc::SYS_getppid,
libc::SYS_gettid,
libc::SYS_getuid,
libc::SYS_geteuid,
libc::SYS_getgid,
libc::SYS_getegid,
libc::SYS_getresuid,
libc::SYS_getresgid,
libc::SYS_getgroups,
libc::SYS_capget,
libc::SYS_getrusage,
libc::SYS_times,
libc::SYS_getrlimit,
libc::SYS_sysinfo,
libc::SYS_umask,
libc::SYS_fsync,
libc::SYS_fdatasync,
libc::SYS_ftruncate,
libc::SYS_truncate,
libc::SYS_fallocate,
libc::SYS_mkdir,
libc::SYS_mkdirat,
libc::SYS_rmdir,
libc::SYS_unlink,
libc::SYS_unlinkat,
libc::SYS_rename,
libc::SYS_renameat,
libc::SYS_renameat2,
libc::SYS_utimensat,
libc::SYS_statfs,
libc::SYS_fstatfs,
libc::SYS_fork,
libc::SYS_wait4,
libc::SYS_waitid,
libc::SYS_execve,
libc::SYS_exit,
libc::SYS_exit_group,
] {
allowed.insert(nr, vec![]);
}
// clone is the inspectable glibc fallback. Unknown/high bits, new
// namespaces, ptrace, pidfd and parent reassignment never match.
let child_flags = (libc::CLONE_CHILD_SETTID | libc::CLONE_CHILD_CLEARTID) as u64;
let thread_required = (libc::CLONE_VM
| libc::CLONE_FS
| libc::CLONE_FILES
| libc::CLONE_SIGHAND
| libc::CLONE_THREAD) as u64;
let thread_optional =
child_flags | (libc::CLONE_SYSVSEM | libc::CLONE_SETTLS | libc::CLONE_PARENT_SETTID) as u64;
let mut clones = vec![
vec![condition(
0,
SeccompCmpOp::MaskedEq(!child_flags),
libc::SIGCHLD as u64,
)?],
vec![condition(
0,
SeccompCmpOp::MaskedEq(!thread_optional),
thread_required,
)?],
];
if profile != ExecutionProfile::Native {
clones.push(vec![condition(
0,
SeccompCmpOp::Eq,
(libc::CLONE_VM | libc::CLONE_VFORK | libc::SIGCHLD) as u64,
)?]);
for nr in [
libc::SYS_vfork,
libc::SYS_chmod,
libc::SYS_fchmod,
libc::SYS_fchmodat,
] {
allowed.insert(nr, vec![]);
}
}
allowed.insert(libc::SYS_clone, rules(clones)?);
// TLS setup is needed by dynamic/static glibc, but user code cannot mutate
// GS or use arbitrary arch_prctl extensions.
allowed.insert(
libc::SYS_arch_prctl,
rules(vec![vec![condition(
0,
SeccompCmpOp::Eq,
0x1002, // ARCH_SET_FS
)?]])?,
);
// Read-only process information; mutating privilege, dumpability and
// seccomp settings is not part of the language contract.
allowed.insert(
libc::SYS_prctl,
rules(
[
libc::PR_GET_NO_NEW_PRIVS,
libc::PR_GET_SECCOMP,
libc::PR_GET_SECUREBITS,
libc::PR_GET_KEEPCAPS,
libc::PR_GET_PDEATHSIG,
]
.into_iter()
.map(|op| Ok(vec![condition(0, SeccompCmpOp::Eq, op as u64)?]))
.collect::<io::Result<_>>()?,
)?,
);
// glibc reads its own limit through prlimit64(pid=0,new_limit=NULL).
allowed.insert(
libc::SYS_prlimit64,
rules(vec![vec![
condition(0, SeccompCmpOp::Eq, 0)?,
condition(2, SeccompCmpOp::Eq, 0)?,
]])?,
);
// Initialization transport only. The FD is fixed, authenticated and
// CLOEXEC; the final execution stage must additionally close/replace stdio.
for nr in [libc::SYS_recvmsg, libc::SYS_sendmsg] {
allowed.insert(nr, rules(vec![vec![condition(0, SeccompCmpOp::Eq, 3)?]])?);
}
// UnixStream::write uses send(MSG_NOSIGNAL), encoded as sendto with no
// destination on x86_64. It is not a network address exception.
allowed.insert(
libc::SYS_sendto,
rules(vec![vec![
condition(0, SeccompCmpOp::Eq, 3)?,
condition(4, SeccompCmpOp::Eq, 0)?,
condition(5, SeccompCmpOp::Eq, 0)?,
]])?,
);
allowed.insert(
libc::SYS_shutdown,
rules(vec![vec![condition(0, SeccompCmpOp::Eq, 3)?]])?,
);
allowed.insert(
libc::SYS_setsockopt,
rules(
[libc::SO_RCVTIMEO, libc::SO_SNDTIMEO]
.into_iter()
.map(|opt| {
Ok(vec![
condition(0, SeccompCmpOp::Eq, 3)?,
condition(1, SeccompCmpOp::Eq, libc::SOL_SOCKET as u64)?,
condition(2, SeccompCmpOp::Eq, opt as u64)?,
])
})
.collect::<io::Result<_>>()?,
)?,
);
let base: BpfProgram = SeccompFilter::new(
allowed,
SeccompAction::Errno(libc::EPERM as u32),
SeccompAction::Allow,
TargetArch::x86_64,
)
.map_err(io::Error::other)?
.try_into()
.map_err(io::Error::other)?;
// clone3 has pointer arguments that classic BPF cannot safely inspect.
// Return ENOSYS to select glibc's argument-checked clone fallback. Guard
// the ABI before this exception; the generated base also checks the ABI.
let mut program = vec![
sock_filter {
code: 0x20,
jt: 0,
jf: 0,
k: 4,
},
sock_filter {
code: 0x15,
jt: 1,
jf: 0,
k: AUDIT_X86_64,
},
sock_filter {
code: 0x06,
jt: 0,
jf: 0,
k: libc::SECCOMP_RET_KILL_PROCESS,
},
sock_filter {
code: 0x20,
jt: 0,
jf: 0,
k: 0,
},
sock_filter {
code: 0x15,
jt: 0,
jf: 1,
k: libc::SYS_clone3 as u32,
},
sock_filter {
code: 0x06,
jt: 0,
jf: 0,
k: libc::SECCOMP_RET_ERRNO | libc::ENOSYS as u32,
},
];
program.extend(base);
if program.len() > MAX_INSTRUCTIONS {
return Err(denied("seccomp instruction budget"));
}
Ok(program)
}
pub(crate) fn filter_count(status: &str) -> io::Result<u32> {
let field = |name| {
status
.lines()
.find_map(|line| line.strip_prefix(name))
.and_then(|s| s.trim().parse::<u32>().ok())
};
if field("Seccomp:") != Some(2) || field("NoNewPrivs:") != Some(1) {
return Err(denied("seccomp status readback"));
}
field("Seccomp_filters:").ok_or_else(|| denied("missing seccomp filter count"))
}
pub(crate) fn install(profile: ExecutionProfile) -> io::Result<()> {
crate::launcher::entry::ensure_single_threaded()?;
let uid = rustix::process::getuid().as_raw();
let gid = rustix::process::getgid().as_raw();
if !crate::isolation::identity::valid(uid, gid) {
return Err(denied("seccomp task identity precondition"));
}
crate::isolation::identity::verify_status(
&std::fs::read_to_string("/proc/self/status")?,
uid,
gid,
)?;
let program = compile(profile)?;
seccompiler::apply_filter_all_threads(&program).map_err(|e| match e {
seccompiler::Error::Prctl(e) | seccompiler::Error::Seccomp(e) => e,
other => io::Error::other(other),
})?;
filter_count(&std::fs::read_to_string("/proc/self/status")?)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
// Evaluate generated classic BPF against synthetic seccomp_data; this is
// an ordinary policy regression, not evidence of kernel installation.
fn evaluate(p: &[sock_filter], arch: u32, nr: u32, args: [u64; 6]) -> u32 {
let mut data = [0_u8; 64];
data[..4].copy_from_slice(&nr.to_le_bytes());
data[4..8].copy_from_slice(&arch.to_le_bytes());
for (i, arg) in args.into_iter().enumerate() {
data[16 + i * 8..24 + i * 8].copy_from_slice(&arg.to_le_bytes());
}
let (mut pc, mut a) = (0_usize, 0_u32);
for _ in 0..MAX_INSTRUCTIONS {
let ins = &p[pc];
pc += 1;
match ins.code {
0x20 => {
a = u32::from_le_bytes(
data[ins.k as usize..ins.k as usize + 4].try_into().unwrap(),
)
}
0x54 => a &= ins.k,
0x05 => pc += ins.k as usize,
0x15 => pc += if a == ins.k { ins.jt } else { ins.jf } as usize,
0x25 => pc += if a > ins.k { ins.jt } else { ins.jf } as usize,
0x35 => pc += if a >= ins.k { ins.jt } else { ins.jf } as usize,
0x06 => return ins.k,
_ => panic!("unexpected BPF opcode"),
}
}
panic!("BPF budget exceeded");
}
#[test]
fn abi_unknown_calls_and_clone_flags_fail_closed() {
for profile in [
ExecutionProfile::Native,
ExecutionProfile::CompileC,
ExecutionProfile::CompileCpp,
] {
let p = compile(profile).unwrap();
let deny = libc::SECCOMP_RET_ERRNO | libc::EPERM as u32;
assert_eq!(
evaluate(&p, 0x40000003, 20, [0; 6]),
libc::SECCOMP_RET_KILL_PROCESS
);
assert_eq!(
evaluate(&p, 0x40000003, libc::SYS_clone3 as u32, [0; 6]),
libc::SECCOMP_RET_KILL_PROCESS
);
for nr in [0x40000000 | libc::SYS_getuid as u32, u32::MAX, 1023] {
assert_eq!(evaluate(&p, AUDIT_X86_64, nr, [0; 6]), deny);
}
assert_eq!(
evaluate(&p, AUDIT_X86_64, libc::SYS_clone3 as u32, [0; 6]),
libc::SECCOMP_RET_ERRNO | libc::ENOSYS as u32
);
for flags in [
libc::CLONE_NEWUSER as u64,
libc::CLONE_NEWPID as u64,
libc::CLONE_PARENT as u64,
1_u64 << 32,
libc::CLONE_PTRACE as u64,
] {
let mut args = [0; 6];
args[0] = flags | libc::SIGCHLD as u64;
assert_eq!(
evaluate(&p, AUDIT_X86_64, libc::SYS_clone as u32, args),
deny
);
}
}
}
#[test]
fn compile_profile_exception_does_not_widen_native() {
let flags = (libc::CLONE_VM | libc::CLONE_VFORK | libc::SIGCHLD) as u64;
for profile in [
ExecutionProfile::Native,
ExecutionProfile::CompileC,
ExecutionProfile::CompileCpp,
] {
let p = compile(profile).unwrap();
let expected = if profile == ExecutionProfile::Native {
libc::SECCOMP_RET_ERRNO | libc::EPERM as u32
} else {
libc::SECCOMP_RET_ALLOW
};
assert_eq!(
evaluate(
&p,
AUDIT_X86_64,
libc::SYS_clone as u32,
[flags, 0, 0, 0, 0, 0]
),
expected
);
assert_eq!(
evaluate(&p, AUDIT_X86_64, libc::SYS_chmod as u32, [0; 6]),
expected
);
}
}
}
+16 -4
View File
@@ -201,15 +201,27 @@ impl IdentityReducedTarget {
pub fn stdin(&self) -> Option<&crate::artifacts::SnapshotFile> {
self.target.as_ref().expect("owned target").stdin()
}
pub fn refuse_remaining_isolation(mut self) -> InitializationFailure {
let target = self.target.take().expect("owned target");
let mut result = target
pub(crate) fn initialize_seccomp(&mut self) -> io::Result<()> {
self.target
.as_mut()
.expect("owned target")
.target
.root
.refuse_stage(ExecutionStage::Seccomp, None);
.initialize_seccomp()
}
pub(crate) fn refuse_at(
mut self,
stage: ExecutionStage,
error: Option<&io::Error>,
) -> InitializationFailure {
let target = self.target.take().expect("owned target");
let mut result = target.target.root.refuse_stage(stage, error);
release_after_cleanup(&mut self.lease, &mut result);
result
}
pub fn refuse_remaining_isolation(self) -> InitializationFailure {
self.refuse_at(ExecutionStage::Seccomp, None)
}
}
impl Drop for IdentityReducedTarget {
fn drop(&mut self) {
+2
View File
@@ -22,3 +22,5 @@ pub mod proc_dev;
pub mod protocol;
#[cfg(target_os = "linux")]
pub mod root;
#[cfg(target_os = "linux")]
pub mod seccomp;
@@ -192,7 +192,14 @@ pub fn namespace_init(expected_outer_parent: u32, initialize_root: bool) -> io::
match channel.receive::<NamespaceCommand>()? {
NamespaceCommand::PrepareIdentity { uid, gid } => {
super::identity::initialize(&mut channel, uid, gid)?;
let _ = channel.receive::<NamespaceCommand>()?;
match channel.receive::<NamespaceCommand>()? {
NamespaceCommand::PrepareSeccomp { profile } => {
super::seccomp::initialize(&mut channel, profile)?;
let _ = channel.receive::<NamespaceCommand>()?;
}
NamespaceCommand::Cancel {} => return Ok(()),
_ => return Err(denied("seccomp command before identity")),
}
}
NamespaceCommand::Cancel {} => return Ok(()),
_ => return Err(denied("identity command before inputs")),
@@ -219,6 +226,7 @@ pub struct NamespacedTarget {
proc_dev_ready: bool,
inputs_ready: bool,
identity_ready: bool,
seccomp_ready: bool,
init_pid: u32,
ids: NamespaceIds,
}
@@ -313,6 +321,7 @@ impl NamespacedTarget {
proc_dev_ready: false,
inputs_ready: false,
identity_ready: false,
seccomp_ready: false,
}),
Err(error) => Err(prepared.refuse_at(ExecutionStage::Namespaces, Some(&error))),
}
@@ -405,6 +414,30 @@ impl NamespacedTarget {
Ok(())
}
pub(crate) fn initialize_seccomp(
&mut self,
profile: crate::spec::ExecutionProfile,
) -> io::Result<()> {
if !self.identity_ready || self.seccomp_ready {
return Err(denied("seccomp initialization order"));
}
let status = std::fs::read_to_string(format!("/proc/{}/status", self.init_pid))?;
let before = status_value(&status, "Seccomp_filters:")?;
self.initialize_filesystem(
NamespaceCommand::PrepareSeccomp { profile },
NamespaceEvent::SeccompInitialized {},
)?;
let after = crate::isolation::seccomp::filter_count(&std::fs::read_to_string(format!(
"/proc/{}/status",
self.init_pid
))?)?;
if before.checked_add(1) != Some(after) {
return Err(denied("seccomp filter count mismatch"));
}
self.seccomp_ready = true;
Ok(())
}
fn expect_event(&mut self, expected: NamespaceEvent) -> io::Result<()> {
match self.control.receive::<NamespaceEvent>()? {
event if event == expected => Ok(()),
@@ -67,6 +67,9 @@ pub enum NamespaceCommand {
uid: u32,
gid: u32,
},
PrepareSeccomp {
profile: crate::spec::ExecutionProfile,
},
Cancel {},
}
@@ -80,6 +83,7 @@ pub enum NamespaceEvent {
InputAttached {},
InputsInitialized {},
IdentityInitialized {},
SeccompInitialized {},
Failed { errno: Option<u16> },
Cancelled {},
}
@@ -154,6 +154,13 @@ impl RootedTarget {
self.lease.verify_root(self.bootstrap_pid())
}
pub(crate) fn initialize_seccomp(&mut self) -> io::Result<()> {
self.target
.initialize_seccomp(self.request.spec().profile)?;
self.lease.verify_root(self.init_pid())?;
self.lease.verify_root(self.bootstrap_pid())
}
pub(crate) fn refuse_stage(
mut self,
stage: ExecutionStage,
@@ -0,0 +1,51 @@
//! Seccomp initialization checkpoint; the missing user supervisor still refuses exec.
use super::{blocked::InitializationFailure, identity::IdentityReducedTarget};
use crate::{
artifacts::ArtifactRoot, outcome::ExecutionStage, runtime::ValidatedRuntimeConfig,
spec::ValidatedSpec,
};
use std::{io, path::Path};
pub struct FilteredTarget {
target: IdentityReducedTarget,
}
impl FilteredTarget {
pub fn prepare(
config: &ValidatedRuntimeConfig,
spec: &ValidatedSpec,
executable: &Path,
artifacts: &ArtifactRoot,
) -> Result<Self, InitializationFailure> {
let mut target = IdentityReducedTarget::prepare(config, spec, executable, artifacts)?;
if let Err(e) = target.initialize_seccomp() {
return Err(target.refuse_at(ExecutionStage::Seccomp, Some(&e)));
}
Ok(Self { target })
}
pub fn root(&self) -> &super::root::RootedTarget {
self.target.root()
}
pub fn identity(&self) -> (u32, u32) {
self.target.identity()
}
pub fn stdin(&self) -> Option<&crate::artifacts::SnapshotFile> {
self.target.stdin()
}
pub fn refuse_remaining_isolation(self) -> InitializationFailure {
self.target.refuse_at(ExecutionStage::Exec, None)
}
}
pub(crate) fn initialize(
channel: &mut super::protocol::ControlChannel,
profile: crate::spec::ExecutionProfile,
) -> io::Result<()> {
let result = crate::isolation::seccomp::install(profile);
if let Err(e) = result {
let _ = channel.send(&super::protocol::NamespaceEvent::Failed {
errno: e.raw_os_error().and_then(|n| u16::try_from(n).ok()),
});
return Err(e);
}
channel.send(&super::protocol::NamespaceEvent::SeccompInitialized {})
}
+29
View File
@@ -0,0 +1,29 @@
/* 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_seccomp,0,1),
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_SECCOMP","1",1);
execl("/kf/bin/seccomp-vm-tests","seccomp-vm-tests","--ignored","--test-threads=1","--exact","kernel_seccomp_failure_refuses_and_cleans","--nocapture",(char*)0);
return 4;
}
@@ -0,0 +1,39 @@
/* Fixed independent-VM probe; never installed as a user execution endpoint. */
#define _GNU_SOURCE
#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <unistd.h>
static void require(int yes,const char *why){if(!yes){fprintf(stderr,"seccomp probe: %s errno=%d\n",why,errno);exit(1);}}
static void filtered(void){
require(prctl(PR_GET_SECCOMP)==2,"mode");
require(prctl(PR_GET_NO_NEW_PRIVS,0,0,0,0)==1,"nnp");
FILE *f=fopen("/proc/self/status","r");require(f!=NULL,"status");
char line[256];unsigned count=0;
while(fgets(line,sizeof line,f))if(sscanf(line,"Seccomp_filters: %u",&count)==1)break;
fclose(f);require(count==1,"one production filter");
errno=0;require(syscall(0x40000000|SYS_getuid)==-1&&errno==EPERM,"x32 rejected");
errno=0;require(syscall(SYS_clone3,NULL,0)==-1&&errno==ENOSYS,"clone3 fallback");
errno=0;require(syscall(1023)==-1&&errno==EPERM,"unknown rejected");
}
static void *thread(void *p){filtered();return p;}
int main(int argc,char **argv){
require(argc==2,"fixed args");filtered();
if(!strcmp(argv[1],"--exec-child")){puts("KF_SECCOMP_EXEC_INHERIT_PROOF");return 0;}
pthread_t t;require(pthread_create(&t,NULL,thread,(void*)17)==0,"pthread create");
void *result=NULL;require(pthread_join(t,&result)==0&&result==(void*)17,"pthread join");
pid_t p=fork();require(p>=0,"fork");
if(!p){filtered();execl("/kf/bin/seccomp-inherit-probe","seccomp-inherit-probe","--exec-child",(char*)0);_exit(3);}
int status=0;require(waitpid(p,&status,0)==p&&WIFEXITED(status)&&WEXITSTATUS(status)==0,"fork exec wait");
p=fork();require(p>=0,"i386 fork");
if(!p){long value;__asm__ volatile("int $0x80":"=a"(value):"0"(20):"memory");_exit(value?4:5);}
require(waitpid(p,&status,0)==p&&WIFSIGNALED(status)&&WTERMSIG(status)==SIGSYS,"i386 killed");
printf("KF_SECCOMP_INHERIT_PROOF profile=%s mode=2 nnp=1 filters=1 fork_exec_thread=1 x32_eperm=1 i386_sigsys=1 clone3_enosys=1 unknown_eperm=1\n",argv[1]);
return 0;
}
+116
View File
@@ -0,0 +1,116 @@
#![cfg(target_os = "linux")]
mod support;
use kf_sandbox::{
artifacts::ArtifactRoot,
launcher::seccomp::FilteredTarget,
outcome::ExecutionStage,
spec::{ExecutionProfile, SpecPolicy},
};
use std::{fs, os::unix::fs::PermissionsExt, path::Path, process::Command};
fn check(profile: ExecutionProfile, name: &str) {
let root = support::Root::new();
let before = support::children();
let path = format!("/kf/seccomp-publications-{}", std::process::id());
fs::create_dir(&path).unwrap();
fs::set_permissions(&path, fs::Permissions::from_mode(0o700)).unwrap();
fs::write(format!("{path}/program-001"), b"fixed-non-task-payload").unwrap();
fs::write(format!("{path}/input-001"), b"private-stdin").unwrap();
let artifacts = ArtifactRoot::from_directory(
rustix::fs::open(
&path,
rustix::fs::OFlags::PATH | rustix::fs::OFlags::DIRECTORY | rustix::fs::OFlags::CLOEXEC,
rustix::fs::Mode::empty(),
)
.unwrap(),
)
.unwrap();
let mut spec = support::spec("seccomp-fixture", 64 << 20, 8).spec().clone();
spec.run_id = format!("seccomp-{name}");
spec.profile = profile;
if profile != ExecutionProfile::Native {
spec.argv = vec![
if profile == ExecutionProfile::CompileC {
"/usr/bin/gcc"
} else {
"/usr/bin/g++"
}
.into(),
"/work/main".into(),
];
}
let result = FilteredTarget::prepare(
&root.validated(),
&spec.validate(&SpecPolicy::default()).unwrap(),
&support::binary(),
&artifacts,
);
if std::env::var("KF_SANDBOX_DENY_SECCOMP").as_deref() == Ok("1") {
let Err(e) = result else {
panic!("kernel policy failure ignored")
};
assert_eq!(e.failure.stage, ExecutionStage::Seccomp);
assert_eq!(e.failure.errno, Some(1));
assert!(e.cleanup_failure.is_none());
println!("KF_SECCOMP_FAILURE_PROOF real_eperm=1 no_ack=1 target_reaped=1 lease_released=1");
} else {
let target = result.unwrap();
let status =
fs::read_to_string(format!("/proc/{}/status", target.root().init_pid())).unwrap();
assert!(status.contains("Seccomp:\t2\n"));
assert!(status.contains("Seccomp_filters:\t1\n"));
assert!(status.contains("NoNewPrivs:\t1\n"));
assert_eq!(target.identity(), (100000, 100000));
assert!(target.root().snapshot_outputs().unwrap().is_empty());
let e = target.refuse_remaining_isolation();
assert_eq!(e.failure.stage, ExecutionStage::Exec);
assert!(e.cleanup_failure.is_none());
let output = Command::new("/kf/bin/seccomp-probe")
.arg(name)
.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)
);
print!("{}", String::from_utf8_lossy(&output.stdout));
println!(
"KF_SECCOMP_INIT_PROOF profile={name} frozen=1 filter_count=1 identity=1 run_refused=1"
);
}
assert_eq!(support::children(), before);
assert_eq!(
fs::read_dir("/var/lib/kf-judge/identities")
.unwrap()
.count(),
0
);
assert!(!Path::new(&format!("/var/lib/kf-judge/tasks/kf-seccomp-{name}")).exists());
assert!(!root.path.join(format!("kf-seccomp-{name}")).exists());
fs::remove_dir_all(path).unwrap();
}
#[test]
#[ignore = "requires independent capped VM"]
fn native_policy_installs_and_inherits() {
check(ExecutionProfile::Native, "native");
}
#[test]
#[ignore = "requires independent capped VM"]
fn compile_c_policy_installs_and_inherits() {
check(ExecutionProfile::CompileC, "compile-c");
}
#[test]
#[ignore = "requires independent capped VM"]
fn compile_cpp_policy_installs_and_inherits() {
check(ExecutionProfile::CompileCpp, "compile-cpp");
}
#[test]
#[ignore = "requires independent capped VM with kernel seccomp failure injector"]
fn kernel_seccomp_failure_refuses_and_cleans() {
assert_eq!(std::env::var("KF_SANDBOX_DENY_SECCOMP").as_deref(), Ok("1"));
check(ExecutionProfile::Native, "native");
}