use std::io;
use std::os::fd::AsRawFd;
use std::os::unix::process::CommandExt;
use std::path::PathBuf;
use std::process::Command;
use super::{
policy_allows_capability, policy_allows_network, process_sandbox_roots, sandbox_rejection,
warn_once, PrepareOutcome, SandboxBackend, SandboxFallback,
};
use crate::orchestration::{CapabilityPolicy, SandboxProfile};
use crate::value::VmError;
pub(super) struct Backend;
impl SandboxBackend for Backend {
fn name() -> &'static str {
"linux"
}
fn available() -> bool {
true
}
fn prepare_std_command(
_program: &str,
_args: &[String],
command: &mut Command,
policy: &CapabilityPolicy,
profile: SandboxProfile,
) -> Result<PrepareOutcome, VmError> {
let prep = profile_setup(policy, profile)?;
unsafe {
command.pre_exec(move || apply_profile(&prep));
}
Ok(PrepareOutcome::Direct)
}
fn prepare_tokio_command(
_program: &str,
_args: &[String],
command: &mut tokio::process::Command,
policy: &CapabilityPolicy,
profile: SandboxProfile,
) -> Result<PrepareOutcome, VmError> {
let prep = profile_setup(policy, profile)?;
unsafe {
command.pre_exec(move || apply_profile(&prep));
}
Ok(PrepareOutcome::Direct)
}
}
struct ProcessProfile {
landlock: Option<LandlockProfile>,
denied_syscalls: Vec<libc::c_long>,
}
struct LandlockProfile {
ruleset_fd: libc::c_int,
rules: Vec<LandlockRule>,
handled_access_fs: u64,
}
struct LandlockRule {
file: std::fs::File,
allowed_access: u64,
}
impl Drop for LandlockProfile {
fn drop(&mut self) {
unsafe {
libc::close(self.ruleset_fd);
}
}
}
fn profile_setup(
policy: &CapabilityPolicy,
profile: SandboxProfile,
) -> Result<ProcessProfile, VmError> {
Ok(ProcessProfile {
landlock: landlock_profile(policy, profile)?,
denied_syscalls: denied_syscalls(policy),
})
}
fn apply_profile(profile: &ProcessProfile) -> io::Result<()> {
install_seccomp_filter(&profile.denied_syscalls)?;
if let Some(landlock) = &profile.landlock {
install_landlock_ruleset(landlock)?;
}
Ok(())
}
fn landlock_profile(
policy: &CapabilityPolicy,
profile: SandboxProfile,
) -> Result<Option<LandlockProfile>, VmError> {
let abi = landlock_abi_version();
if abi == 0 {
return match super::effective_fallback(profile) {
SandboxFallback::Enforce => Err(sandbox_rejection(
"Linux Landlock is not available; OsHardened profile requires it (set HARN_HANDLER_SANDBOX=warn or off, or pick the worktree profile, to run without filesystem isolation)".to_string(),
)),
SandboxFallback::Warn => {
warn_once(
"handler_sandbox_linux_landlock_unavailable",
"Linux Landlock is not available; process filesystem isolation is disabled",
);
Ok(None)
}
SandboxFallback::Off => Ok(None),
};
}
let handled_access_fs = landlock_handled_access(abi);
let ruleset_attr = LandlockRulesetAttr { handled_access_fs };
let ruleset_fd = unsafe {
libc::syscall(
libc::SYS_landlock_create_ruleset,
&ruleset_attr as *const LandlockRulesetAttr,
std::mem::size_of::<LandlockRulesetAttr>(),
0,
) as libc::c_int
};
if ruleset_fd < 0 {
return Err(sandbox_rejection(format!(
"failed to create Linux Landlock ruleset: {}",
io::Error::last_os_error()
)));
}
let mut profile = LandlockProfile {
ruleset_fd,
rules: Vec::new(),
handled_access_fs,
};
for path in system_read_roots() {
push_rule(
&mut profile,
path,
LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR | LANDLOCK_ACCESS_FS_EXECUTE,
true,
)?;
}
let workspace_access = workspace_access(policy);
for root in process_sandbox_roots(policy) {
push_rule(&mut profile, root, workspace_access, false)?;
}
Ok(Some(profile))
}
fn system_read_roots() -> Vec<PathBuf> {
[
"/bin",
"/lib",
"/lib64",
"/usr",
"/etc",
"/nix/store",
"/System",
]
.into_iter()
.map(PathBuf::from)
.collect()
}
fn push_rule(
profile: &mut LandlockProfile,
path: PathBuf,
allowed_access: u64,
optional: bool,
) -> Result<(), VmError> {
let path = super::normalize_for_policy(&path);
let file = match std::fs::File::open(&path) {
Ok(file) => file,
Err(error) if optional && error.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(error) => {
return Err(sandbox_rejection(format!(
"failed to open sandbox path '{}': {error}",
path.display()
)));
}
};
profile.rules.push(LandlockRule {
file,
allowed_access: allowed_access & profile.handled_access_fs,
});
Ok(())
}
fn install_landlock_ruleset(profile: &LandlockProfile) -> io::Result<()> {
for rule in &profile.rules {
let path_beneath = LandlockPathBeneathAttr {
allowed_access: rule.allowed_access,
parent_fd: rule.file.as_raw_fd(),
};
let result = unsafe {
libc::syscall(
libc::SYS_landlock_add_rule,
profile.ruleset_fd,
LANDLOCK_RULE_PATH_BENEATH,
&path_beneath as *const LandlockPathBeneathAttr,
0,
)
};
if result < 0 {
return Err(io::Error::last_os_error());
}
}
unsafe {
if libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0 {
return Err(io::Error::last_os_error());
}
let result = libc::syscall(libc::SYS_landlock_restrict_self, profile.ruleset_fd, 0);
if result < 0 {
return Err(io::Error::last_os_error());
}
}
Ok(())
}
fn install_seccomp_filter(denied_syscalls: &[libc::c_long]) -> io::Result<()> {
unsafe {
if libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0 {
return Err(io::Error::last_os_error());
}
}
let mut filter = Vec::with_capacity(denied_syscalls.len() * 2 + 1);
filter.push(bpf_stmt(
(libc::BPF_LD | libc::BPF_W | libc::BPF_ABS) as u16,
0,
));
for syscall in denied_syscalls {
filter.push(bpf_jump(
(libc::BPF_JMP | libc::BPF_JEQ | libc::BPF_K) as u16,
*syscall as u32,
0,
1,
));
filter.push(bpf_stmt(
(libc::BPF_RET | libc::BPF_K) as u16,
libc::SECCOMP_RET_ERRNO | libc::EPERM as u32,
));
}
filter.push(bpf_stmt(
(libc::BPF_RET | libc::BPF_K) as u16,
libc::SECCOMP_RET_ALLOW,
));
let mut program = libc::sock_fprog {
len: filter.len() as u16,
filter: filter.as_mut_ptr(),
};
unsafe {
if libc::prctl(
libc::PR_SET_SECCOMP,
libc::SECCOMP_MODE_FILTER,
&mut program as *mut libc::sock_fprog,
0,
0,
) != 0
{
return Err(io::Error::last_os_error());
}
}
Ok(())
}
fn bpf_stmt(code: u16, k: u32) -> libc::sock_filter {
libc::sock_filter {
code,
jt: 0,
jf: 0,
k,
}
}
fn bpf_jump(code: u16, k: u32, jt: u8, jf: u8) -> libc::sock_filter {
libc::sock_filter { code, jt, jf, k }
}
fn denied_syscalls(policy: &CapabilityPolicy) -> Vec<libc::c_long> {
let mut syscalls = vec![
libc::SYS_bpf,
libc::SYS_delete_module,
libc::SYS_fanotify_init,
libc::SYS_finit_module,
libc::SYS_init_module,
libc::SYS_kexec_file_load,
libc::SYS_kexec_load,
libc::SYS_mount,
libc::SYS_open_by_handle_at,
libc::SYS_perf_event_open,
libc::SYS_process_vm_readv,
libc::SYS_process_vm_writev,
libc::SYS_ptrace,
libc::SYS_reboot,
libc::SYS_swapon,
libc::SYS_swapoff,
libc::SYS_umount2,
libc::SYS_userfaultfd,
];
if !policy_allows_network(policy) {
syscalls.extend([
libc::SYS_accept,
libc::SYS_accept4,
libc::SYS_bind,
libc::SYS_connect,
libc::SYS_listen,
libc::SYS_recvfrom,
libc::SYS_recvmsg,
libc::SYS_sendmsg,
libc::SYS_sendto,
libc::SYS_socket,
libc::SYS_socketpair,
]);
}
syscalls.sort_unstable();
syscalls.dedup();
syscalls
}
fn workspace_access(policy: &CapabilityPolicy) -> u64 {
let read_access =
LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR | LANDLOCK_ACCESS_FS_EXECUTE;
let write_access = LANDLOCK_ACCESS_FS_WRITE_FILE
| LANDLOCK_ACCESS_FS_REMOVE_DIR
| LANDLOCK_ACCESS_FS_REMOVE_FILE
| LANDLOCK_ACCESS_FS_MAKE_CHAR
| LANDLOCK_ACCESS_FS_MAKE_DIR
| LANDLOCK_ACCESS_FS_MAKE_REG
| LANDLOCK_ACCESS_FS_MAKE_SOCK
| LANDLOCK_ACCESS_FS_MAKE_FIFO
| LANDLOCK_ACCESS_FS_MAKE_BLOCK
| LANDLOCK_ACCESS_FS_MAKE_SYM
| LANDLOCK_ACCESS_FS_REFER
| LANDLOCK_ACCESS_FS_TRUNCATE;
if policy.capabilities.is_empty() {
return read_access | write_access;
}
let mut access = 0;
if policy_allows_capability(policy, "workspace", &["read_text", "list", "exists"]) {
access |= read_access;
}
if policy_allows_capability(policy, "workspace", &["write_text"]) {
access |= write_access;
}
if policy_allows_capability(policy, "workspace", &["delete"]) {
access |= LANDLOCK_ACCESS_FS_REMOVE_DIR | LANDLOCK_ACCESS_FS_REMOVE_FILE;
}
if access == 0 {
read_access
} else {
access
}
}
fn landlock_abi_version() -> u32 {
let result = unsafe {
libc::syscall(
libc::SYS_landlock_create_ruleset,
std::ptr::null::<libc::c_void>(),
0,
LANDLOCK_CREATE_RULESET_VERSION,
)
};
if result <= 0 {
0
} else {
result as u32
}
}
fn landlock_handled_access(abi: u32) -> u64 {
let mut access = LANDLOCK_ACCESS_FS_EXECUTE
| LANDLOCK_ACCESS_FS_WRITE_FILE
| LANDLOCK_ACCESS_FS_READ_FILE
| LANDLOCK_ACCESS_FS_READ_DIR
| LANDLOCK_ACCESS_FS_REMOVE_DIR
| LANDLOCK_ACCESS_FS_REMOVE_FILE
| LANDLOCK_ACCESS_FS_MAKE_CHAR
| LANDLOCK_ACCESS_FS_MAKE_DIR
| LANDLOCK_ACCESS_FS_MAKE_REG
| LANDLOCK_ACCESS_FS_MAKE_SOCK
| LANDLOCK_ACCESS_FS_MAKE_FIFO
| LANDLOCK_ACCESS_FS_MAKE_BLOCK
| LANDLOCK_ACCESS_FS_MAKE_SYM;
if abi >= 2 {
access |= LANDLOCK_ACCESS_FS_REFER;
}
if abi >= 3 {
access |= LANDLOCK_ACCESS_FS_TRUNCATE;
}
access
}
#[repr(C)]
struct LandlockRulesetAttr {
handled_access_fs: u64,
}
#[repr(C)]
struct LandlockPathBeneathAttr {
allowed_access: u64,
parent_fd: libc::c_int,
}
const LANDLOCK_CREATE_RULESET_VERSION: u32 = 1 << 0;
const LANDLOCK_RULE_PATH_BENEATH: libc::c_int = 1;
const LANDLOCK_ACCESS_FS_EXECUTE: u64 = 1 << 0;
const LANDLOCK_ACCESS_FS_WRITE_FILE: u64 = 1 << 1;
const LANDLOCK_ACCESS_FS_READ_FILE: u64 = 1 << 2;
const LANDLOCK_ACCESS_FS_READ_DIR: u64 = 1 << 3;
const LANDLOCK_ACCESS_FS_REMOVE_DIR: u64 = 1 << 4;
const LANDLOCK_ACCESS_FS_REMOVE_FILE: u64 = 1 << 5;
const LANDLOCK_ACCESS_FS_MAKE_CHAR: u64 = 1 << 6;
const LANDLOCK_ACCESS_FS_MAKE_DIR: u64 = 1 << 7;
const LANDLOCK_ACCESS_FS_MAKE_REG: u64 = 1 << 8;
const LANDLOCK_ACCESS_FS_MAKE_SOCK: u64 = 1 << 9;
const LANDLOCK_ACCESS_FS_MAKE_FIFO: u64 = 1 << 10;
const LANDLOCK_ACCESS_FS_MAKE_BLOCK: u64 = 1 << 11;
const LANDLOCK_ACCESS_FS_MAKE_SYM: u64 = 1 << 12;
const LANDLOCK_ACCESS_FS_REFER: u64 = 1 << 13;
const LANDLOCK_ACCESS_FS_TRUNCATE: u64 = 1 << 14;