use std::os::fd::OwnedFd;
use std::path::Path;
use crate::error::{ConfinementError, SandlockError};
use crate::protection::{Protection, ProtectionPolicy, ProtectionStatus};
use crate::sandbox::Sandbox;
use crate::sys::structs::{
LandlockNetPortAttr, LandlockPathBeneathAttr, LandlockRulesetAttr,
LANDLOCK_ACCESS_FS_EXECUTE, LANDLOCK_ACCESS_FS_IOCTL_DEV, LANDLOCK_ACCESS_FS_MAKE_BLOCK,
LANDLOCK_ACCESS_FS_MAKE_CHAR, LANDLOCK_ACCESS_FS_MAKE_DIR, LANDLOCK_ACCESS_FS_MAKE_FIFO,
LANDLOCK_ACCESS_FS_MAKE_REG, LANDLOCK_ACCESS_FS_MAKE_SOCK, LANDLOCK_ACCESS_FS_MAKE_SYM,
LANDLOCK_ACCESS_FS_READ_DIR, LANDLOCK_ACCESS_FS_READ_FILE, LANDLOCK_ACCESS_FS_REFER,
LANDLOCK_ACCESS_FS_REMOVE_DIR, LANDLOCK_ACCESS_FS_REMOVE_FILE, LANDLOCK_ACCESS_FS_TRUNCATE,
LANDLOCK_ACCESS_FS_WRITE_FILE, LANDLOCK_ACCESS_NET_BIND_TCP, LANDLOCK_ACCESS_NET_CONNECT_TCP,
LANDLOCK_CREATE_RULESET_VERSION, LANDLOCK_RULE_NET_PORT, LANDLOCK_RULE_PATH_BENEATH,
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, LANDLOCK_SCOPE_SIGNAL, SYS_LANDLOCK_CREATE_RULESET,
};
use crate::sys::syscall;
const READ_ACCESS: u64 =
LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR;
const ACCESS_FILE: u64 = LANDLOCK_ACCESS_FS_EXECUTE
| LANDLOCK_ACCESS_FS_WRITE_FILE
| LANDLOCK_ACCESS_FS_READ_FILE
| LANDLOCK_ACCESS_FS_TRUNCATE
| LANDLOCK_ACCESS_FS_IOCTL_DEV;
fn base_fs_access(abi: u32) -> u64 {
let mut mask: u64 = 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 {
mask |= LANDLOCK_ACCESS_FS_REFER;
}
if abi >= 3 {
mask |= LANDLOCK_ACCESS_FS_TRUNCATE;
}
if abi >= 5 {
mask |= LANDLOCK_ACCESS_FS_IOCTL_DEV;
}
mask
}
fn write_access(abi: u32) -> u64 {
let mut mask: u64 = READ_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;
if abi >= 2 {
mask |= LANDLOCK_ACCESS_FS_REFER;
}
if abi >= 3 {
mask |= LANDLOCK_ACCESS_FS_TRUNCATE;
}
if abi >= 5 {
mask |= LANDLOCK_ACCESS_FS_IOCTL_DEV;
}
mask
}
pub fn abi_version() -> Result<u32, ConfinementError> {
let ret = unsafe {
syscall::syscall3(
SYS_LANDLOCK_CREATE_RULESET,
0, 0, LANDLOCK_CREATE_RULESET_VERSION as u64,
)
};
match ret {
Ok(v) => Ok(v as u32),
Err(e) => {
let raw = e.raw_os_error().unwrap_or(0);
if raw == libc::ENOSYS || raw == libc::EOPNOTSUPP {
Err(ConfinementError::LandlockUnavailable(e.to_string()))
} else {
Err(ConfinementError::Landlock(format!(
"abi_version query failed: {}",
e
)))
}
}
}
}
fn add_path_rule(ruleset_fd: &OwnedFd, path: &Path, access: u64) -> Result<(), ConfinementError> {
use std::os::unix::fs::OpenOptionsExt;
let file = std::fs::OpenOptions::new()
.read(true)
.custom_flags(libc::O_PATH | libc::O_CLOEXEC)
.open(path)
.map_err(|e| {
ConfinementError::Landlock(format!("open path {:?} failed: {}", path, e))
})?;
let allowed_access = match file.metadata() {
Ok(m) if m.is_dir() => access,
_ => access & ACCESS_FILE,
};
use std::os::unix::io::AsRawFd;
let attr = LandlockPathBeneathAttr {
allowed_access,
parent_fd: file.as_raw_fd(),
};
syscall::landlock_add_rule(
ruleset_fd,
LANDLOCK_RULE_PATH_BENEATH,
&attr as *const _ as *const std::ffi::c_void,
0,
)
.map_err(|e| ConfinementError::Landlock(format!("add path rule for {:?}: {}", path, e)))?;
Ok(())
}
fn add_net_rule(ruleset_fd: &OwnedFd, port: u16, access: u64) -> Result<(), ConfinementError> {
let attr = LandlockNetPortAttr {
allowed_access: access,
port: port as u64,
};
syscall::landlock_add_rule(
ruleset_fd,
LANDLOCK_RULE_NET_PORT,
&attr as *const _ as *const std::ffi::c_void,
0,
)
.map_err(|e| ConfinementError::Landlock(format!("add net rule for port {}: {}", port, e)))?;
Ok(())
}
pub(crate) fn compute_scope_mask(abi: u32, pol: &ProtectionPolicy) -> u64 {
debug_assert!(
!matches!(
ProtectionStatus::resolve(Protection::SignalScope, abi, pol),
ProtectionStatus::Unavailable,
),
"compute_scope_mask called with SignalScope Unavailable; \
caller must filter via confine_inner's Protection::all() walk first"
);
debug_assert!(
!matches!(
ProtectionStatus::resolve(Protection::AbstractUnixSocketScope, abi, pol),
ProtectionStatus::Unavailable,
),
"compute_scope_mask called with AbstractUnixSocketScope Unavailable; \
caller must filter via confine_inner's Protection::all() walk first"
);
let mut mask: u64 = 0;
if ProtectionStatus::resolve(Protection::AbstractUnixSocketScope, abi, pol)
== ProtectionStatus::Active
{
mask |= LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET;
}
if ProtectionStatus::resolve(Protection::SignalScope, abi, pol) == ProtectionStatus::Active {
mask |= LANDLOCK_SCOPE_SIGNAL;
}
mask
}
pub fn compute_fs_mask(abi: u32, pol: &ProtectionPolicy) -> u64 {
let mut mask = base_fs_access(abi);
if matches!(
ProtectionStatus::resolve(Protection::FsRefer, abi, pol),
ProtectionStatus::Disabled | ProtectionStatus::Degraded
) {
mask &= !LANDLOCK_ACCESS_FS_REFER;
}
if matches!(
ProtectionStatus::resolve(Protection::FsTruncate, abi, pol),
ProtectionStatus::Disabled | ProtectionStatus::Degraded
) {
mask &= !LANDLOCK_ACCESS_FS_TRUNCATE;
}
if matches!(
ProtectionStatus::resolve(Protection::FsIoctlDev, abi, pol),
ProtectionStatus::Disabled | ProtectionStatus::Degraded
) {
mask &= !LANDLOCK_ACCESS_FS_IOCTL_DEV;
}
mask
}
pub fn compute_net_mask(
abi: u32,
pol: &ProtectionPolicy,
sandbox: &Sandbox,
handle_net: bool,
) -> (u64, bool) {
if !handle_net {
return (0, false);
}
if ProtectionStatus::resolve(Protection::NetTcp, abi, pol) != ProtectionStatus::Active {
return (0, false);
}
use crate::sandbox::Protocol;
let net_wildcard = !sandbox.net_deny.is_empty()
|| sandbox
.net_allow
.iter()
.any(|r| r.protocol == Protocol::Tcp && r.all_ports);
let mut mask = if net_wildcard {
LANDLOCK_ACCESS_NET_BIND_TCP
} else {
LANDLOCK_ACCESS_NET_BIND_TCP | LANDLOCK_ACCESS_NET_CONNECT_TCP
};
if !sandbox.net_deny_bind.is_empty() {
mask &= !LANDLOCK_ACCESS_NET_BIND_TCP;
}
(mask, net_wildcard)
}
pub const MIN_ABI: u32 = 6;
pub fn confine(policy: &Sandbox) -> Result<(), SandlockError> {
confine_inner(policy, true)
}
pub fn confine_filesystem(policy: &Sandbox) -> Result<(), SandlockError> {
confine_inner(policy, false)
}
fn confine_inner(policy: &Sandbox, handle_net: bool) -> Result<(), SandlockError> {
let abi = abi_version().map_err(|e| {
SandlockError::Runtime(crate::error::SandboxRuntimeError::Confinement(e))
})?;
let pol = &policy.protection_policy;
for protection in Protection::all() {
if ProtectionStatus::resolve(protection, abi, pol) == ProtectionStatus::Unavailable {
return Err(SandlockError::Runtime(
crate::error::SandboxRuntimeError::Confinement(
ConfinementError::ProtectionUnavailable {
protection,
required_abi: protection.min_abi(),
host_abi: abi,
},
),
));
}
}
let handled_access_fs = compute_fs_mask(abi, pol);
use crate::sandbox::Protocol;
let (handled_access_net, net_wildcard) = compute_net_mask(abi, pol, policy, handle_net);
let scoped = compute_scope_mask(abi, pol);
let attr = LandlockRulesetAttr {
handled_access_fs,
handled_access_net,
scoped,
};
let ruleset_fd = syscall::landlock_create_ruleset(&attr, std::mem::size_of::<LandlockRulesetAttr>(), 0)
.map_err(|e| {
SandlockError::Runtime(crate::error::SandboxRuntimeError::Confinement(
ConfinementError::Landlock(format!("create ruleset: {}", e)),
))
})?;
let chroot_root = policy.chroot.as_deref();
let fs_write_mask = write_access(abi) & handled_access_fs;
for path in &policy.fs_writable {
let host;
let rule_path = if let Some(root) = chroot_root {
host = root.join(path.strip_prefix("/").unwrap_or(path));
if !host.exists() { continue; }
host.as_path()
} else {
path.as_path()
};
add_path_rule(&ruleset_fd, rule_path, fs_write_mask).map_err(|e| {
SandlockError::Runtime(crate::error::SandboxRuntimeError::Confinement(e))
})?;
}
for path in &policy.fs_readable {
let host;
let rule_path = if let Some(root) = chroot_root {
host = root.join(path.strip_prefix("/").unwrap_or(path));
if !host.exists() { continue; }
host.as_path()
} else {
path.as_path()
};
add_path_rule(&ruleset_fd, rule_path, READ_ACCESS).map_err(|e| {
SandlockError::Runtime(crate::error::SandboxRuntimeError::Confinement(e))
})?;
}
if policy.gpu_devices.is_some() {
for path in &[
"/dev/nvidia0", "/dev/nvidia1", "/dev/nvidia2", "/dev/nvidia3",
"/dev/nvidiactl", "/dev/nvidia-uvm", "/dev/nvidia-uvm-tools",
"/dev/dri",
] {
let _ = add_path_rule(&ruleset_fd, std::path::Path::new(path), fs_write_mask);
}
for path in &[
"/proc/driver/nvidia",
"/sys/bus/pci/devices",
"/sys/module/nvidia",
] {
let _ = add_path_rule(&ruleset_fd, std::path::Path::new(path), READ_ACCESS);
}
}
let net_tcp_active =
ProtectionStatus::resolve(Protection::NetTcp, abi, pol) == ProtectionStatus::Active;
if handle_net && net_tcp_active {
for &port in &policy.net_allow_bind {
add_net_rule(&ruleset_fd, port, LANDLOCK_ACCESS_NET_BIND_TCP).map_err(|e| {
SandlockError::Runtime(crate::error::SandboxRuntimeError::Confinement(e))
})?;
}
}
if handle_net && net_tcp_active && !net_wildcard {
let mut connect_ports: std::collections::HashSet<u16> = std::collections::HashSet::new();
for rule in &policy.net_allow {
if rule.protocol != Protocol::Tcp {
continue;
}
for &p in &rule.ports {
connect_ports.insert(p);
}
}
for &p in &policy.http_ports {
connect_ports.insert(p);
}
for port in connect_ports {
add_net_rule(&ruleset_fd, port, LANDLOCK_ACCESS_NET_CONNECT_TCP).map_err(|e| {
SandlockError::Runtime(crate::error::SandboxRuntimeError::Confinement(e))
})?;
}
}
syscall::landlock_restrict_self(&ruleset_fd, 0).map_err(|e| {
SandlockError::Runtime(crate::error::SandboxRuntimeError::Confinement(
ConfinementError::Landlock(format!("restrict_self: {}", e)),
))
})?;
Ok(())
}
#[cfg(test)]
mod mask_contract_tests {
use super::*;
use crate::protection::ProtectionState;
use crate::Sandbox;
#[test]
fn scope_mask_strict_v6_sets_both_scope_bits() {
let pol = ProtectionPolicy::strict_all();
let mask = compute_scope_mask(6, &pol);
assert_eq!(
mask,
LANDLOCK_SCOPE_SIGNAL | LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET,
"strict_all on v6 host must request both v6 IPC scopes"
);
}
#[test]
fn scope_mask_disable_signal_clears_only_signal_bit() {
let mut pol = ProtectionPolicy::strict_all();
pol.set(Protection::SignalScope, ProtectionState::Disabled);
let mask = compute_scope_mask(6, &pol);
assert_eq!(mask & LANDLOCK_SCOPE_SIGNAL, 0, "SIGNAL must be cleared");
assert_eq!(
mask & LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET,
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET,
"ABSTRACT_UNIX_SOCKET must remain set"
);
}
#[test]
fn scope_mask_disable_abstract_unix_clears_only_abstract_bit() {
let mut pol = ProtectionPolicy::strict_all();
pol.set(Protection::AbstractUnixSocketScope, ProtectionState::Disabled);
let mask = compute_scope_mask(6, &pol);
assert_eq!(
mask & LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET,
0,
"ABSTRACT_UNIX_SOCKET must be cleared",
);
assert_eq!(
mask & LANDLOCK_SCOPE_SIGNAL,
LANDLOCK_SCOPE_SIGNAL,
"SIGNAL must remain set",
);
}
#[test]
fn scope_mask_disable_both_returns_zero() {
let mut pol = ProtectionPolicy::strict_all();
pol.set(Protection::SignalScope, ProtectionState::Disabled);
pol.set(Protection::AbstractUnixSocketScope, ProtectionState::Disabled);
assert_eq!(
compute_scope_mask(6, &pol),
0,
"both scopes disabled on a capable host must produce mask=0"
);
}
#[test]
fn scope_mask_allow_degraded_on_v5_host_returns_zero() {
let mut pol = ProtectionPolicy::strict_all();
pol.set(Protection::SignalScope, ProtectionState::Degradable);
pol.set(Protection::AbstractUnixSocketScope, ProtectionState::Degradable);
assert_eq!(
compute_scope_mask(5, &pol),
0,
"Degradable scopes on a v5 host must contribute no bits"
);
}
#[test]
fn fs_mask_strict_v6_includes_all_fs_protection_bits() {
let pol = ProtectionPolicy::strict_all();
let mask = compute_fs_mask(6, &pol);
for (bit, name) in [
(LANDLOCK_ACCESS_FS_REFER, "REFER"),
(LANDLOCK_ACCESS_FS_TRUNCATE, "TRUNCATE"),
(LANDLOCK_ACCESS_FS_IOCTL_DEV, "IOCTL_DEV"),
] {
assert_eq!(
mask & bit,
bit,
"{} bit must be set in the strict v6 fs mask",
name,
);
}
}
#[test]
fn fs_mask_disable_fs_refer_clears_only_refer_bit() {
let mut pol = ProtectionPolicy::strict_all();
pol.set(Protection::FsRefer, ProtectionState::Disabled);
let mask = compute_fs_mask(6, &pol);
assert_eq!(mask & LANDLOCK_ACCESS_FS_REFER, 0);
assert_eq!(mask & LANDLOCK_ACCESS_FS_TRUNCATE, LANDLOCK_ACCESS_FS_TRUNCATE);
assert_eq!(mask & LANDLOCK_ACCESS_FS_IOCTL_DEV, LANDLOCK_ACCESS_FS_IOCTL_DEV);
}
#[test]
fn fs_mask_disable_fs_truncate_clears_only_truncate_bit() {
let mut pol = ProtectionPolicy::strict_all();
pol.set(Protection::FsTruncate, ProtectionState::Disabled);
let mask = compute_fs_mask(6, &pol);
assert_eq!(mask & LANDLOCK_ACCESS_FS_TRUNCATE, 0);
assert_eq!(mask & LANDLOCK_ACCESS_FS_REFER, LANDLOCK_ACCESS_FS_REFER);
assert_eq!(mask & LANDLOCK_ACCESS_FS_IOCTL_DEV, LANDLOCK_ACCESS_FS_IOCTL_DEV);
}
#[test]
fn fs_mask_disable_fs_ioctl_dev_clears_only_ioctl_dev_bit() {
let mut pol = ProtectionPolicy::strict_all();
pol.set(Protection::FsIoctlDev, ProtectionState::Disabled);
let mask = compute_fs_mask(6, &pol);
assert_eq!(mask & LANDLOCK_ACCESS_FS_IOCTL_DEV, 0);
assert_eq!(mask & LANDLOCK_ACCESS_FS_REFER, LANDLOCK_ACCESS_FS_REFER);
assert_eq!(mask & LANDLOCK_ACCESS_FS_TRUNCATE, LANDLOCK_ACCESS_FS_TRUNCATE);
}
#[test]
fn fs_mask_degraded_protections_get_masked_off_on_low_abi_host() {
let mut pol = ProtectionPolicy::strict_all();
pol.set(Protection::FsIoctlDev, ProtectionState::Degradable);
let mask = compute_fs_mask(4, &pol);
assert_eq!(
mask & LANDLOCK_ACCESS_FS_IOCTL_DEV,
0,
"Degraded FsIoctlDev on a v4 host must NOT contribute the IOCTL_DEV bit",
);
}
fn empty_sandbox() -> Sandbox {
Sandbox::builder()
.build_unchecked()
.expect("minimal builder must produce a sandbox in unit tests")
}
#[test]
fn net_mask_handle_net_false_returns_zero_no_wildcard() {
let pol = ProtectionPolicy::strict_all();
let sb = empty_sandbox();
let (mask, wildcard) = compute_net_mask(6, &pol, &sb, false);
assert_eq!(mask, 0, "handle_net=false → mask is zero");
assert!(!wildcard, "handle_net=false → wildcard is false");
}
#[test]
fn net_mask_strict_no_wildcard_sets_bind_and_connect_bits() {
let pol = ProtectionPolicy::strict_all();
let sb = empty_sandbox();
let (mask, wildcard) = compute_net_mask(6, &pol, &sb, true);
assert_eq!(
mask,
LANDLOCK_ACCESS_NET_BIND_TCP | LANDLOCK_ACCESS_NET_CONNECT_TCP,
"strict NetTcp with no wildcard rule → both BIND_TCP and CONNECT_TCP",
);
assert!(!wildcard);
}
#[test]
fn net_mask_disable_net_tcp_returns_zero() {
let mut pol = ProtectionPolicy::strict_all();
pol.set(Protection::NetTcp, ProtectionState::Disabled);
let sb = empty_sandbox();
let (mask, wildcard) = compute_net_mask(6, &pol, &sb, true);
assert_eq!(
mask, 0,
"disabled NetTcp must produce mask=0 regardless of handle_net",
);
assert!(!wildcard);
}
#[test]
fn net_mask_degraded_net_tcp_on_v3_host_returns_zero() {
let mut pol = ProtectionPolicy::strict_all();
pol.set(Protection::NetTcp, ProtectionState::Degradable);
let sb = empty_sandbox();
let (mask, wildcard) = compute_net_mask(3, &pol, &sb, true);
assert_eq!(mask, 0);
assert!(!wildcard);
}
#[test]
fn net_mask_net_deny_forces_wildcard_dropping_connect_tcp() {
let pol = ProtectionPolicy::strict_all();
let sb = Sandbox::builder()
.net_deny("10.0.0.0/8")
.build()
.expect("net_deny sandbox builds");
let (mask, wildcard) = compute_net_mask(6, &pol, &sb, true);
assert_eq!(
mask,
LANDLOCK_ACCESS_NET_BIND_TCP,
"net_deny must drop CONNECT_TCP so all TCP connects reach the on-behalf path",
);
assert!(wildcard, "net_deny must set the wildcard flag");
}
#[test]
fn net_mask_net_deny_bind_drops_bind_tcp() {
let pol = ProtectionPolicy::strict_all();
let sb = Sandbox::builder()
.net_deny_bind("8080")
.build()
.expect("net_deny_bind sandbox builds");
let (mask, _wildcard) = compute_net_mask(6, &pol, &sb, true);
assert_eq!(
mask & LANDLOCK_ACCESS_NET_BIND_TCP,
0,
"net_deny_bind must drop BIND_TCP so all TCP binds reach the on-behalf path",
);
assert_ne!(
mask & LANDLOCK_ACCESS_NET_CONNECT_TCP,
0,
"net_deny_bind must not affect CONNECT_TCP handling",
);
}
}