use std::io;
use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, OwnedFd, RawFd};
use std::os::unix::process::CommandExt;
use std::path::{Path, PathBuf};
use std::process::Command;
use seccompiler::{
BpfProgram, SeccompAction, SeccompCmpArgLen, SeccompCmpOp, SeccompCondition, SeccompFilter,
SeccompRule, TargetArch,
};
use super::{
policy_allows_capability, policy_allows_network, process_sandbox_presets, sandbox_rejection,
PrepareOutcome, SandboxBackend,
};
use crate::orchestration::{CapabilityPolicy, ProcessSandboxPreset, SandboxProfile};
use crate::value::VmError;
pub(super) struct Backend;
#[path = "linux_descriptors.rs"]
mod descriptors;
use crate::process_sandbox::DeviceMountFinalization;
pub use descriptors::DescriptorTransfer;
#[path = "linux_bwrap.rs"]
pub(super) mod bwrap;
pub(crate) use bwrap::sealed_filter as sealed_launch_bytes;
#[path = "linux_filesystem.rs"]
mod filesystem;
#[path = "linux_landlock.rs"]
mod landlock_support;
#[path = "linux_nesting.rs"]
mod nesting;
#[path = "linux_self_confinement.rs"]
mod self_confinement;
use filesystem::filesystem_profile;
pub(crate) use filesystem::installed_write_grants;
use landlock_support::{AccessFs, FsAccess, LandlockSupport, ABI};
use nesting::nested_seccomp_filter_rule;
impl SandboxBackend for Backend {
fn name() -> &'static str {
"linux"
}
fn filesystem_mechanism() -> super::SandboxMechanism {
if landlock_available() {
super::SandboxMechanism::LinuxLandlock
} else {
super::SandboxMechanism::LinuxBubblewrap
}
}
fn mechanism_limits() -> Vec<super::enforcement::MechanismLimit> {
landlock_support::host_support()
.map(LandlockSupport::limits)
.unwrap_or_default()
}
fn confine_current_process(
policy: &CapabilityPolicy,
) -> Result<super::SandboxMechanism, VmError> {
self_confinement::confine(policy)?;
Ok(super::SandboxMechanism::LinuxLandlock)
}
fn available() -> bool {
true
}
fn filesystem_available() -> bool {
landlock_available() || bwrap::available()
}
fn prepare_std_command(
program: &str,
args: &[String],
command: &mut Command,
policy: &CapabilityPolicy,
profile: SandboxProfile,
) -> Result<PrepareOutcome, VmError> {
if !landlock_available() {
return bwrap::prepare(program, args, policy, profile);
}
let mut prep = profile_setup(program, policy, profile)?;
if let Some(launcher) = resolve_netns_launcher(policy)? {
return Ok(namespaced_outcome(launcher, program, args, &mut prep));
}
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> {
if !landlock_available() {
return bwrap::prepare(program, args, policy, profile);
}
let mut prep = profile_setup(program, policy, profile)?;
if let Some(launcher) = resolve_netns_launcher(policy)? {
return Ok(namespaced_outcome(launcher, program, args, &mut prep));
}
unsafe {
command.pre_exec(move || apply_profile(&prep));
}
Ok(PrepareOutcome::Direct)
}
}
struct ProcessProfile {
landlock: Option<LandlockProfile>,
seccomp: BpfProgram,
}
struct LandlockProfile {
ruleset: OwnedFd,
}
struct FilesystemProfile {
rules: Vec<FilesystemRule>,
symlinks: std::collections::BTreeMap<PathBuf, PathBuf>,
handled_access_fs: FsAccess,
read_deny_roots: Vec<PathBuf>,
unheld_read_denials: Vec<filesystem::UnheldReadDenial>,
}
struct FilesystemRule {
file: std::fs::File,
path: PathBuf,
allowed_access: FsAccess,
}
enum ProcessFilesystemScope {
Host,
PrivatePidNamespace,
}
impl LandlockProfile {
fn take_ruleset(self) -> OwnedFd {
self.ruleset
}
}
pub struct TransferableConfinement {
ruleset: Option<OwnedFd>,
seccomp: BpfProgram,
}
const SECCOMP_INSTRUCTION_BYTES: usize = 8;
impl TransferableConfinement {
pub fn ruleset_fd(&self) -> Option<RawFd> {
self.ruleset.as_ref().map(AsRawFd::as_raw_fd)
}
pub fn seccomp_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(self.seccomp.len() * SECCOMP_INSTRUCTION_BYTES);
for instruction in &self.seccomp {
bytes.extend_from_slice(&instruction.code.to_le_bytes());
bytes.push(instruction.jt);
bytes.push(instruction.jf);
bytes.extend_from_slice(&instruction.k.to_le_bytes());
}
bytes
}
pub fn from_parts(ruleset_fd: Option<RawFd>, seccomp: &[u8]) -> io::Result<Self> {
if !seccomp.len().is_multiple_of(SECCOMP_INSTRUCTION_BYTES) {
return Err(io::Error::other(
"transferred seccomp program is not a whole number of instructions",
));
}
let program = seccomp
.chunks_exact(SECCOMP_INSTRUCTION_BYTES)
.map(|chunk| seccompiler::sock_filter {
code: u16::from_le_bytes([chunk[0], chunk[1]]),
jt: chunk[2],
jf: chunk[3],
k: u32::from_le_bytes([chunk[4], chunk[5], chunk[6], chunk[7]]),
})
.collect();
Ok(Self {
ruleset: ruleset_fd.map(|fd| unsafe { OwnedFd::from_raw_fd(fd) }),
seccomp: program,
})
}
pub fn enter(&self) -> io::Result<()> {
if let Some(ruleset) = self.ruleset.as_ref() {
landlock_support::enter_ruleset(ruleset.as_raw_fd())?;
confirm_filesystem_boundary_holds()?;
}
seccompiler::apply_filter(&self.seccomp)
.map_err(|err| io::Error::other(format!("failed to install the seccomp filter: {err}")))
}
pub fn into_ruleset_fd(self) -> Option<RawFd> {
self.ruleset.map(IntoRawFd::into_raw_fd)
}
}
fn confirm_filesystem_boundary_holds() -> io::Result<()> {
let root = unsafe { libc::open(c"/".as_ptr(), libc::O_RDONLY | libc::O_DIRECTORY) };
if root < 0 {
return Ok(());
}
unsafe {
libc::close(root);
}
Err(io::Error::other(
"the Landlock ruleset was entered but the filesystem boundary is not holding: the root \
directory is still readable, so this process is not confined",
))
}
pub fn transferable_confinement(program: &str) -> Result<Option<TransferableConfinement>, VmError> {
let Some((policy, profile)) = super::active_sandbox_policy() else {
return Ok(None);
};
super::ensure_spawn_enforceable::<Backend>(&policy)?;
let resolved = crate::stdlib::process::resolve_program_path_for_spawn(program);
let prepared = profile_setup(&resolved, &policy, profile)?;
Ok(Some(TransferableConfinement {
ruleset: prepared.landlock.map(LandlockProfile::take_ruleset),
seccomp: prepared.seccomp,
}))
}
fn profile_setup(
program: &str,
policy: &CapabilityPolicy,
profile: SandboxProfile,
) -> Result<ProcessProfile, VmError> {
let support = host_landlock(profile)?;
let observable: Vec<&str> = support
.limits()
.into_iter()
.filter(|limit| limit.changes_child_behavior())
.map(|limit| limit.description())
.collect();
if !observable.is_empty() {
super::warn_once(
"landlock_abi_limits",
&format!(
"Linux Landlock ABI {} on this host confines child processes, but {}",
support.abi(),
observable.join(", and ")
),
);
}
profile_setup_for(program, policy, support)
}
fn host_landlock(profile: SandboxProfile) -> Result<LandlockSupport, VmError> {
landlock_support::host_support().ok_or_else(|| {
super::SandboxMechanismUnavailable::new(
super::SandboxMechanism::LinuxLandlock,
super::SandboxMechanismAvailability::AbsentOnHost,
profile,
)
.into_error()
})
}
fn profile_setup_for(
program: &str,
policy: &CapabilityPolicy,
support: LandlockSupport,
) -> Result<ProcessProfile, VmError> {
if policy.process_network_proxy.is_some() {
return Err(sandbox_rejection(
"managed child-process egress requires a proxy-only Linux network namespace; this build cannot enforce that boundary"
.to_string(),
));
}
resolve_netns_launcher(policy)?;
let landlock = landlock_profile_for(program, policy, support)?;
Ok(ProcessProfile {
landlock: Some(landlock),
seccomp: compile_seccomp_program(policy, support.bounds_truncation())?,
})
}
fn unix_socket_local_ipc_grant(policy: &CapabilityPolicy) -> bool {
!policy.process_sandbox.unix_socket_roots.is_empty() && !policy_allows_network(policy)
}
fn apply_profile(profile: &ProcessProfile) -> io::Result<()> {
if let Some(landlock) = &profile.landlock {
landlock_support::enter_ruleset(landlock.ruleset.as_raw_fd())?;
}
seccompiler::apply_filter(&profile.seccomp)
.map_err(|err| io::Error::other(format!("failed to install the seccomp filter: {err}")))?;
Ok(())
}
fn landlock_profile_for(
program: &str,
policy: &CapabilityPolicy,
support: LandlockSupport,
) -> Result<LandlockProfile, VmError> {
let scope_abstract_unix = unix_socket_local_ipc_grant(policy);
if scope_abstract_unix && !support.scopes_abstract_unix_sockets() {
return Err(sandbox_rejection(format!(
"serve-only Unix-domain sockets need Landlock ABI {} to contain abstract sockets; this host reports ABI {}",
ABI::V6,
support.abi()
)));
}
let filesystem = filesystem_profile(
program,
policy,
support.handled_access(),
ProcessFilesystemScope::Host,
)?;
let ruleset = support
.populated_ruleset(
scope_abstract_unix,
filesystem
.rules
.iter()
.map(|rule| (&rule.file, rule.allowed_access)),
)
.map_err(|error| {
sandbox_rejection(format!(
"failed to build the Linux Landlock ruleset: {error}"
))
})?;
Ok(LandlockProfile { ruleset })
}
fn system_read_roots() -> Vec<PathBuf> {
[
"/bin",
"/lib",
"/lib64",
"/usr",
"/etc",
"/nix/store",
"/System",
]
.into_iter()
.map(PathBuf::from)
.collect()
}
fn network_name_service_read_roots(policy: &CapabilityPolicy) -> Vec<PathBuf> {
if !policy_allows_network(policy) {
return Vec::new();
}
[
"/etc/resolv.conf",
"/etc/hosts",
"/etc/nsswitch.conf",
"/etc/gai.conf",
"/etc/host.conf",
]
.into_iter()
.map(PathBuf::from)
.collect()
}
fn developer_toolchain_system_read_roots(policy: &CapabilityPolicy) -> Vec<PathBuf> {
if process_sandbox_presets(policy).contains(&ProcessSandboxPreset::DeveloperToolchains) {
vec![PathBuf::from("/opt")]
} else {
Vec::new()
}
}
fn standard_device_rules() -> Vec<(PathBuf, FsAccess)> {
[
("/dev/null", AccessFs::ReadFile | AccessFs::WriteFile),
("/dev/zero", AccessFs::ReadFile.into()),
("/dev/random", AccessFs::ReadFile.into()),
("/dev/urandom", AccessFs::ReadFile.into()),
]
.into_iter()
.map(|(path, access)| (PathBuf::from(path), access))
.collect()
}
fn expand_around_denied(root: &Path, denied: &[PathBuf]) -> Result<Vec<PathBuf>, VmError> {
let inside: Vec<&PathBuf> = denied
.iter()
.filter(|deny| deny.starts_with(root) && deny.as_path() != root)
.collect();
if denied.iter().any(|deny| root.starts_with(deny)) {
return Ok(Vec::new());
}
if inside.is_empty() {
return Ok(vec![root.to_path_buf()]);
}
let mut granted: Vec<PathBuf> = Vec::new();
for deny in &inside {
let relative = deny.strip_prefix(root).map_err(|_| {
sandbox_rejection(format!(
"cannot subtract '{}' from sandbox root '{}'",
deny.display(),
root.display()
))
})?;
let mut cursor = root.to_path_buf();
for component in relative.components() {
let entries = match std::fs::read_dir(&cursor) {
Ok(entries) => entries,
Err(error)
if matches!(
error.kind(),
io::ErrorKind::NotFound | io::ErrorKind::PermissionDenied
) =>
{
break;
}
Err(error) => {
return Err(sandbox_rejection(format!(
"cannot enumerate '{}' to exclude denied path '{}': {error}; refusing \
the spawn rather than granting a root that would expose it",
cursor.display(),
deny.display()
)));
}
};
let step = cursor.join(component.as_os_str());
for entry in entries.flatten() {
let sibling = entry.path();
if sibling == step {
continue;
}
if denied
.iter()
.any(|d| sibling.starts_with(d) || d.starts_with(&sibling))
{
continue;
}
if !granted.contains(&sibling) {
granted.push(sibling);
}
}
cursor = step;
if granted.len() > MAX_DENY_EXPANSION_RULES {
return Err(sandbox_rejection(format!(
"excluding denied paths from sandbox root '{}' produced {} rules, over the \
{} cap; the directory that expanded was '{}' while excluding '{}'. Refusing \
the spawn rather than granting the root unsubtracted.",
root.display(),
granted.len(),
MAX_DENY_EXPANSION_RULES,
cursor.display(),
deny.display(),
)));
}
}
}
Ok(granted)
}
const MAX_DENY_EXPANSION_RULES: usize = 4096;
fn push_rule(
profile: &mut FilesystemProfile,
path: PathBuf,
allowed_access: FsAccess,
optional: bool,
) -> Result<(), VmError> {
let mut pending = vec![(path, optional)];
let mut observed = std::collections::BTreeSet::new();
while let Some((path, optional)) = pending.pop() {
for ancestor in path.ancestors() {
if let Ok(target) = std::fs::read_link(ancestor) {
profile.symlinks.insert(ancestor.to_path_buf(), target);
}
}
let path = super::normalize_for_policy(&path);
if !observed.insert(path.clone()) {
continue;
}
if observed.len() > MAX_DENY_EXPANSION_RULES {
return Err(sandbox_rejection(
"credential subtraction exceeded its grant expansion ceiling".into(),
));
}
if allowed_access.intersects(filesystem::LANDLOCK_WRITE_ACCESS) {
if filesystem::keep_writable_grant(profile, &path) {
push_rule_exact(profile, path, allowed_access, optional)?;
}
continue;
}
let expanded = expand_around_denied(&path, &profile.read_deny_roots)?;
if expanded.len() == 1 && expanded[0] == path {
push_rule_exact(profile, path, allowed_access, optional)?;
} else {
pending.extend(expanded.into_iter().map(|path| (path, true)));
}
}
Ok(())
}
fn push_rule_exact(
profile: &mut FilesystemProfile,
path: PathBuf,
allowed_access: FsAccess,
optional: bool,
) -> Result<(), VmError> {
let file = match std::fs::File::open(&path) {
Ok(file) => file,
Err(error)
if optional
&& matches!(
error.kind(),
io::ErrorKind::NotFound | io::ErrorKind::PermissionDenied
) =>
{
if error.kind() == io::ErrorKind::PermissionDenied {
super::warn_once(
&format!("sandbox_unreadable_root:{}", path.display()),
&format!(
"sandbox root '{}' exists but could not be opened ({error}); it was NOT \
granted. The child runs with less reach, not more.",
path.display()
),
);
}
return Ok(());
}
Err(error) => {
return Err(sandbox_rejection(format!(
"failed to open sandbox path '{}': {error}",
path.display()
)));
}
};
let opened_path =
std::fs::read_link(format!("/proc/self/fd/{}", file.as_raw_fd())).map_err(|error| {
sandbox_rejection(format!(
"cannot identify pinned sandbox grant {}: {error}",
path.display()
))
})?;
if opened_path != path {
return Err(sandbox_rejection(format!(
"sandbox grant {} changed during preparation",
path.display()
)));
}
let is_dir = file
.metadata()
.map(|metadata| metadata.is_dir())
.unwrap_or(false);
let allowed_access = if is_dir {
allowed_access
} else {
allowed_access & !DIRECTORY_ONLY_ACCESS_FS
};
profile.rules.push(FilesystemRule {
file,
path,
allowed_access: allowed_access & profile.handled_access_fs,
});
Ok(())
}
fn proc_runtime_reads_are_contained() -> bool {
std::fs::read_to_string("/proc/sys/kernel/yama/ptrace_scope")
.ok()
.is_some_and(|value| yama_scope_contains_process_reads(&value))
}
fn yama_scope_contains_process_reads(value: &str) -> bool {
value.trim().parse::<u8>().is_ok_and(|scope| scope >= 1)
}
fn compile_seccomp_program(
policy: &CapabilityPolicy,
filesystem_bounds_truncation: bool,
) -> Result<BpfProgram, VmError> {
let mut rules: std::collections::BTreeMap<libc::c_long, Vec<SeccompRule>> =
allowed_syscalls(policy)
.iter()
.map(|syscall| (*syscall, Vec::new()))
.collect();
if !filesystem_bounds_truncation {
landlock_support::filter_path_truncation(&mut rules)?;
}
if unix_socket_local_ipc_grant(policy) {
let unix_domain_only = SeccompRule::new(vec![SeccompCondition::new(
0,
SeccompCmpArgLen::Dword,
SeccompCmpOp::Eq,
libc::AF_UNIX as u64,
)
.map_err(|err| {
sandbox_rejection(format!("failed to build the Unix-domain condition: {err}"))
})?])
.map_err(|err| sandbox_rejection(format!("failed to build the socket rule: {err}")))?;
rules.insert(libc::SYS_socket, vec![unix_domain_only]);
for syscall in [
libc::SYS_bind,
libc::SYS_listen,
libc::SYS_accept,
libc::SYS_accept4,
libc::SYS_getsockname,
] {
rules.entry(syscall).or_default();
}
}
rules.insert(libc::SYS_seccomp, vec![nested_seccomp_filter_rule()?]);
let rules: std::collections::BTreeMap<libc::c_long, Vec<SeccompRule>> = rules;
let filter = SeccompFilter::new(
rules,
SeccompAction::Errno(libc::EPERM as u32),
SeccompAction::Allow,
target_arch(),
)
.map_err(|err| sandbox_rejection(format!("failed to build the seccomp filter: {err}")))?;
BpfProgram::try_from(filter)
.map_err(|err| sandbox_rejection(format!("failed to compile the seccomp filter: {err}")))
}
const fn target_arch() -> TargetArch {
#[cfg(target_arch = "x86_64")]
{
TargetArch::x86_64
}
#[cfg(target_arch = "aarch64")]
{
TargetArch::aarch64
}
#[cfg(target_arch = "riscv64")]
{
TargetArch::riscv64
}
#[cfg(not(any(
target_arch = "x86_64",
target_arch = "aarch64",
target_arch = "riscv64"
)))]
{
compile_error!(
"the Linux sandbox backend has no seccomp target architecture for this platform; \
seccompiler supports x86_64, aarch64, and riscv64"
)
}
}
#[cfg(target_arch = "x86_64")]
const SYS_FCHMODAT2: libc::c_long = libc::SYS_fchmodat2;
#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
const SYS_FCHMODAT2: libc::c_long = 452;
fn allowed_syscalls(policy: &CapabilityPolicy) -> Vec<libc::c_long> {
let mut syscalls = vec![
libc::SYS_brk,
libc::SYS_capget,
libc::SYS_chdir,
libc::SYS_clock_getres,
libc::SYS_clock_gettime,
libc::SYS_clock_nanosleep,
libc::SYS_clone,
libc::SYS_clone3,
libc::SYS_close,
libc::SYS_close_range,
libc::SYS_copy_file_range,
libc::SYS_dup,
libc::SYS_dup3,
libc::SYS_epoll_create1,
libc::SYS_epoll_ctl,
libc::SYS_epoll_pwait,
libc::SYS_epoll_pwait2,
libc::SYS_eventfd2,
libc::SYS_execve,
libc::SYS_execveat,
libc::SYS_exit,
libc::SYS_exit_group,
libc::SYS_faccessat,
libc::SYS_faccessat2,
libc::SYS_fallocate,
libc::SYS_fchdir,
libc::SYS_fchmod,
libc::SYS_fchmodat,
SYS_FCHMODAT2,
libc::SYS_fchown,
libc::SYS_fchownat,
libc::SYS_fcntl,
libc::SYS_fdatasync,
libc::SYS_fgetxattr,
libc::SYS_flistxattr,
libc::SYS_flock,
libc::SYS_fremovexattr,
libc::SYS_fsetxattr,
libc::SYS_fstat,
libc::SYS_fstatfs,
libc::SYS_fsync,
libc::SYS_ftruncate,
libc::SYS_futex,
libc::SYS_futex_waitv,
libc::SYS_get_robust_list,
libc::SYS_getcpu,
libc::SYS_getcwd,
libc::SYS_getdents64,
libc::SYS_getegid,
libc::SYS_geteuid,
libc::SYS_getgid,
libc::SYS_getgroups,
libc::SYS_getitimer,
libc::SYS_getpeername,
libc::SYS_getpgid,
libc::SYS_getpid,
libc::SYS_getppid,
libc::SYS_getpriority,
libc::SYS_getrandom,
libc::SYS_getresgid,
libc::SYS_getresuid,
libc::SYS_getrusage,
libc::SYS_getsid,
libc::SYS_getsockname,
libc::SYS_getsockopt,
libc::SYS_gettid,
libc::SYS_gettimeofday,
libc::SYS_getuid,
libc::SYS_getxattr,
libc::SYS_inotify_add_watch,
libc::SYS_inotify_init1,
libc::SYS_inotify_rm_watch,
libc::SYS_ioctl,
libc::SYS_kill,
libc::SYS_linkat,
libc::SYS_listxattr,
libc::SYS_llistxattr,
libc::SYS_lremovexattr,
libc::SYS_lseek,
libc::SYS_lsetxattr,
libc::SYS_lgetxattr,
libc::SYS_madvise,
libc::SYS_membarrier,
libc::SYS_memfd_create,
libc::SYS_mincore,
libc::SYS_mkdirat,
libc::SYS_mknodat,
libc::SYS_mlock,
libc::SYS_mlock2,
libc::SYS_mmap,
libc::SYS_mprotect,
libc::SYS_mremap,
libc::SYS_msync,
libc::SYS_munlock,
libc::SYS_munmap,
libc::SYS_nanosleep,
libc::SYS_newfstatat,
libc::SYS_openat,
libc::SYS_openat2,
libc::SYS_pipe2,
libc::SYS_pidfd_open,
libc::SYS_pidfd_send_signal,
libc::SYS_ppoll,
libc::SYS_prctl,
libc::SYS_pread64,
libc::SYS_preadv,
libc::SYS_preadv2,
libc::SYS_prlimit64,
libc::SYS_pselect6,
libc::SYS_pwrite64,
libc::SYS_pwritev,
libc::SYS_pwritev2,
libc::SYS_read,
libc::SYS_readahead,
libc::SYS_readlinkat,
libc::SYS_readv,
libc::SYS_recvfrom,
libc::SYS_recvmmsg,
libc::SYS_recvmsg,
libc::SYS_removexattr,
libc::SYS_renameat2,
libc::SYS_restart_syscall,
libc::SYS_rseq,
libc::SYS_rt_sigaction,
libc::SYS_rt_sigpending,
libc::SYS_rt_sigprocmask,
libc::SYS_rt_sigqueueinfo,
libc::SYS_rt_sigreturn,
libc::SYS_rt_sigsuspend,
libc::SYS_rt_sigtimedwait,
libc::SYS_sched_get_priority_max,
libc::SYS_sched_get_priority_min,
libc::SYS_sched_getaffinity,
libc::SYS_sched_getparam,
libc::SYS_sched_getscheduler,
libc::SYS_sched_rr_get_interval,
libc::SYS_sched_setaffinity,
libc::SYS_sched_setparam,
libc::SYS_sched_setscheduler,
libc::SYS_sched_yield,
libc::SYS_sendmmsg,
libc::SYS_sendmsg,
libc::SYS_sendto,
libc::SYS_set_robust_list,
libc::SYS_set_tid_address,
libc::SYS_setitimer,
libc::SYS_setpgid,
libc::SYS_setsockopt,
libc::SYS_setpriority,
libc::SYS_setsid,
libc::SYS_setxattr,
libc::SYS_shutdown,
libc::SYS_sigaltstack,
libc::SYS_signalfd4,
libc::SYS_socketpair,
libc::SYS_splice,
libc::SYS_statfs,
libc::SYS_statx,
libc::SYS_symlinkat,
libc::SYS_sync,
libc::SYS_tee,
libc::SYS_tgkill,
libc::SYS_timer_create,
libc::SYS_timer_delete,
libc::SYS_timer_getoverrun,
libc::SYS_timer_gettime,
libc::SYS_timer_settime,
libc::SYS_timerfd_create,
libc::SYS_timerfd_gettime,
libc::SYS_timerfd_settime,
libc::SYS_times,
libc::SYS_tkill,
libc::SYS_umask,
libc::SYS_uname,
libc::SYS_unlinkat,
libc::SYS_utimensat,
libc::SYS_vmsplice,
libc::SYS_wait4,
libc::SYS_waitid,
libc::SYS_write,
libc::SYS_writev,
libc::SYS_landlock_create_ruleset,
libc::SYS_landlock_add_rule,
libc::SYS_landlock_restrict_self,
];
#[cfg(target_arch = "x86_64")]
syscalls.extend([
libc::SYS_access,
libc::SYS_alarm,
libc::SYS_arch_prctl,
libc::SYS_chmod,
libc::SYS_chown,
libc::SYS_creat,
libc::SYS_dup2,
libc::SYS_epoll_create,
libc::SYS_epoll_wait,
libc::SYS_eventfd,
libc::SYS_fadvise64,
libc::SYS_fork,
libc::SYS_futimesat,
libc::SYS_getdents,
libc::SYS_getpgrp,
libc::SYS_getrlimit,
libc::SYS_link,
libc::SYS_lchown,
libc::SYS_lstat,
libc::SYS_mkdir,
libc::SYS_open,
libc::SYS_pause,
libc::SYS_pipe,
libc::SYS_poll,
libc::SYS_readlink,
libc::SYS_rename,
libc::SYS_renameat,
libc::SYS_rmdir,
libc::SYS_select,
libc::SYS_sendfile,
libc::SYS_setrlimit,
libc::SYS_stat,
libc::SYS_symlink,
libc::SYS_sync_file_range,
libc::SYS_time,
libc::SYS_truncate,
libc::SYS_unlink,
libc::SYS_utime,
libc::SYS_vfork,
]);
if policy_allows_network(policy) || namespaced_loopback_grant(policy) {
syscalls.extend([
libc::SYS_accept,
libc::SYS_accept4,
libc::SYS_bind,
libc::SYS_connect,
libc::SYS_listen,
libc::SYS_socket,
]);
} else {
}
syscalls.sort_unstable();
syscalls.dedup();
syscalls
}
fn read_only_access() -> FsAccess {
AccessFs::from_read(ABI::V1)
}
fn workspace_access(policy: &CapabilityPolicy) -> FsAccess {
let read_access = read_only_access();
let write_access = AccessFs::from_write(ABI::V3);
if !policy.capabilities_are_restricted() || policy.process_sandbox.allow_child_workspace_write {
return read_access | write_access;
}
let mut access = FsAccess::empty();
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 |= AccessFs::RemoveDir | AccessFs::RemoveFile;
}
if access.is_empty() {
read_access
} else {
access
}
}
pub(super) fn landlock_available() -> bool {
landlock_support::host_support().is_some()
}
const DIRECTORY_ONLY_ACCESS_FS: FsAccess = landlock_support::make_bitflags!(AccessFs::{
ReadDir
| RemoveDir
| RemoveFile
| MakeChar
| MakeDir
| MakeReg
| MakeSock
| MakeFifo
| MakeBlock
| MakeSym
| Refer
});
#[path = "netns.rs"]
mod netns;
pub use super::command_for::command_for_reexec;
pub use netns::decode_seccomp_hex;
pub use netns::keep_ruleset_across_exec;
pub(crate) use netns::keep_ruleset_across_exec_tokio;
pub use netns::ReexecConfinement;
pub(super) use netns::{launcher_argv_with_ruleset, resolve_netns_launcher};
use netns::{namespaced_loopback_grant, namespaced_outcome};
#[cfg(test)]
#[path = "linux_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "linux_path_grant_tests.rs"]
mod path_grant_tests;
#[cfg(test)]
#[path = "netns_tests.rs"]
mod netns_tests;
#[cfg(test)]
#[path = "linux_socket_root_tests.rs"]
mod socket_root_tests;
#[cfg(test)]
#[path = "linux_credential_denylist_tests.rs"]
mod credential_denylist_tests;
#[cfg(test)]
#[path = "linux_writable_denial_tests.rs"]
mod writable_denial_tests;