feat(sandbox): install profile seccomp with ABI and inheritance checks
This commit is contained in:
Generated
+11
@@ -385,7 +385,9 @@ version = "0.0.1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"kf-contracts",
|
"kf-contracts",
|
||||||
|
"libc",
|
||||||
"rustix",
|
"rustix",
|
||||||
|
"seccompiler",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
@@ -556,6 +558,15 @@ version = "1.0.23"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "seccompiler"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4ae55de56877481d112a559bbc12667635fdaf5e005712fd4e2b2fa50ffc884"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.229"
|
version = "1.0.229"
|
||||||
|
|||||||
@@ -352,3 +352,27 @@ setuid-root 文件无法恢复权限,跨 UID 信号/ptrace/文件访问拒绝
|
|||||||
注入真实 setgroups EPERM,验证失败回滚;它不构成生产 seccomp 策略。fixture 仅可在
|
注入真实 setgroups EPERM,验证失败回滚;它不构成生产 seccomp 策略。fixture 仅可在
|
||||||
规定独立 VM 使用,没有向 CLI 增加用户执行命令。后续 seccomp、全任务树/stdio/计量
|
规定独立 VM 使用,没有向 CLI 增加用户执行命令。后续 seccomp、全任务树/stdio/计量
|
||||||
和运行后清理仍未验收,`run` 继续拒绝。
|
和运行后清理仍未验收,`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 和完整运行安全验收仍待实现。
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ serde.workspace = true
|
|||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
rustix = { version = "1.1.5", features = ["net", "process", "fs", "thread", "mount", "system", "stdio"] }
|
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]
|
[lints]
|
||||||
workspace = true
|
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 mod namespaces;
|
||||||
pub(crate) mod proc_dev;
|
pub(crate) mod proc_dev;
|
||||||
pub(crate) mod root;
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -201,15 +201,27 @@ impl IdentityReducedTarget {
|
|||||||
pub fn stdin(&self) -> Option<&crate::artifacts::SnapshotFile> {
|
pub fn stdin(&self) -> Option<&crate::artifacts::SnapshotFile> {
|
||||||
self.target.as_ref().expect("owned target").stdin()
|
self.target.as_ref().expect("owned target").stdin()
|
||||||
}
|
}
|
||||||
pub fn refuse_remaining_isolation(mut self) -> InitializationFailure {
|
pub(crate) fn initialize_seccomp(&mut self) -> io::Result<()> {
|
||||||
let target = self.target.take().expect("owned target");
|
self.target
|
||||||
let mut result = target
|
.as_mut()
|
||||||
|
.expect("owned target")
|
||||||
.target
|
.target
|
||||||
.root
|
.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);
|
release_after_cleanup(&mut self.lease, &mut result);
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
pub fn refuse_remaining_isolation(self) -> InitializationFailure {
|
||||||
|
self.refuse_at(ExecutionStage::Seccomp, None)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
impl Drop for IdentityReducedTarget {
|
impl Drop for IdentityReducedTarget {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
|
|||||||
@@ -22,3 +22,5 @@ pub mod proc_dev;
|
|||||||
pub mod protocol;
|
pub mod protocol;
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub mod root;
|
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>()? {
|
match channel.receive::<NamespaceCommand>()? {
|
||||||
NamespaceCommand::PrepareIdentity { uid, gid } => {
|
NamespaceCommand::PrepareIdentity { uid, gid } => {
|
||||||
super::identity::initialize(&mut channel, 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(()),
|
NamespaceCommand::Cancel {} => return Ok(()),
|
||||||
_ => return Err(denied("identity command before inputs")),
|
_ => return Err(denied("identity command before inputs")),
|
||||||
@@ -219,6 +226,7 @@ pub struct NamespacedTarget {
|
|||||||
proc_dev_ready: bool,
|
proc_dev_ready: bool,
|
||||||
inputs_ready: bool,
|
inputs_ready: bool,
|
||||||
identity_ready: bool,
|
identity_ready: bool,
|
||||||
|
seccomp_ready: bool,
|
||||||
init_pid: u32,
|
init_pid: u32,
|
||||||
ids: NamespaceIds,
|
ids: NamespaceIds,
|
||||||
}
|
}
|
||||||
@@ -313,6 +321,7 @@ impl NamespacedTarget {
|
|||||||
proc_dev_ready: false,
|
proc_dev_ready: false,
|
||||||
inputs_ready: false,
|
inputs_ready: false,
|
||||||
identity_ready: false,
|
identity_ready: false,
|
||||||
|
seccomp_ready: false,
|
||||||
}),
|
}),
|
||||||
Err(error) => Err(prepared.refuse_at(ExecutionStage::Namespaces, Some(&error))),
|
Err(error) => Err(prepared.refuse_at(ExecutionStage::Namespaces, Some(&error))),
|
||||||
}
|
}
|
||||||
@@ -405,6 +414,30 @@ impl NamespacedTarget {
|
|||||||
Ok(())
|
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<()> {
|
fn expect_event(&mut self, expected: NamespaceEvent) -> io::Result<()> {
|
||||||
match self.control.receive::<NamespaceEvent>()? {
|
match self.control.receive::<NamespaceEvent>()? {
|
||||||
event if event == expected => Ok(()),
|
event if event == expected => Ok(()),
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ pub enum NamespaceCommand {
|
|||||||
uid: u32,
|
uid: u32,
|
||||||
gid: u32,
|
gid: u32,
|
||||||
},
|
},
|
||||||
|
PrepareSeccomp {
|
||||||
|
profile: crate::spec::ExecutionProfile,
|
||||||
|
},
|
||||||
Cancel {},
|
Cancel {},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +83,7 @@ pub enum NamespaceEvent {
|
|||||||
InputAttached {},
|
InputAttached {},
|
||||||
InputsInitialized {},
|
InputsInitialized {},
|
||||||
IdentityInitialized {},
|
IdentityInitialized {},
|
||||||
|
SeccompInitialized {},
|
||||||
Failed { errno: Option<u16> },
|
Failed { errno: Option<u16> },
|
||||||
Cancelled {},
|
Cancelled {},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,6 +154,13 @@ impl RootedTarget {
|
|||||||
self.lease.verify_root(self.bootstrap_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)?;
|
||||||
|
self.lease.verify_root(self.init_pid())?;
|
||||||
|
self.lease.verify_root(self.bootstrap_pid())
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn refuse_stage(
|
pub(crate) fn refuse_stage(
|
||||||
mut self,
|
mut self,
|
||||||
stage: ExecutionStage,
|
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 {})
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user