use crate::capability::{AccessMode, CapabilitySet, NetworkMode, SignalMode};
use crate::error::{NonoError, Result};
use crate::sandbox::SupportInfo;
use landlock::{
Access, AccessFs, AccessNet, BitFlags, CompatLevel, Compatible, NetPort, PathBeneath, PathFd,
Ruleset, RulesetAttr, RulesetCreatedAttr, Scope, ABI,
};
use std::path::Path;
use tracing::{debug, info, warn};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DetectedAbi {
pub abi: ABI,
}
impl DetectedAbi {
#[must_use]
pub fn new(abi: ABI) -> Self {
Self { abi }
}
#[must_use]
pub fn has_refer(&self) -> bool {
AccessFs::from_all(self.abi).contains(AccessFs::Refer)
}
#[must_use]
pub fn has_truncate(&self) -> bool {
AccessFs::from_all(self.abi).contains(AccessFs::Truncate)
}
#[must_use]
pub fn has_network(&self) -> bool {
!AccessNet::from_all(self.abi).is_empty()
}
#[must_use]
pub fn has_ioctl_dev(&self) -> bool {
AccessFs::from_all(self.abi).contains(AccessFs::IoctlDev)
}
#[must_use]
pub fn has_scoping(&self) -> bool {
!Scope::from_all(self.abi).is_empty()
}
#[must_use]
pub fn version_string(&self) -> &'static str {
match self.abi {
ABI::V1 => "V1",
ABI::V2 => "V2",
ABI::V3 => "V3",
ABI::V4 => "V4",
ABI::V5 => "V5",
ABI::V6 => "V6",
_ => "unknown",
}
}
#[must_use]
pub fn feature_names(&self) -> Vec<String> {
let mut features = vec!["Basic filesystem access control".to_string()];
if self.has_refer() {
features.push("File rename across directories (Refer)".to_string());
}
if self.has_truncate() {
features.push("File truncation (Truncate)".to_string());
}
if self.has_network() {
features.push("TCP network filtering".to_string());
}
if self.has_ioctl_dev() {
features.push("Device ioctl filtering".to_string());
}
if self.has_scoping() {
features.push("Process scoping".to_string());
}
features
}
}
impl std::fmt::Display for DetectedAbi {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Landlock {}", self.version_string())
}
}
const ABI_PROBE_ORDER: [ABI; 6] = [ABI::V6, ABI::V5, ABI::V4, ABI::V3, ABI::V2, ABI::V1];
pub fn detect_abi() -> Result<DetectedAbi> {
let mut last_error = None;
for &abi in &ABI_PROBE_ORDER {
match probe_abi_candidate(abi) {
Ok(()) => return Ok(DetectedAbi::new(abi)),
Err(err) => {
debug!("ABI {:?} probe failed: {}", abi, err);
last_error = Some(format!("ABI {:?}: {}", abi, err));
}
}
}
Err(NonoError::SandboxInit(format!(
"No supported Landlock ABI detected{}",
last_error
.as_ref()
.map(|e| format!(" (last error: {})", e))
.unwrap_or_default()
)))
}
fn probe_abi_candidate(abi: ABI) -> std::result::Result<(), String> {
let mut ruleset = Ruleset::default().set_compatibility(CompatLevel::HardRequirement);
ruleset = ruleset
.handle_access(AccessFs::from_all(abi))
.map_err(|e| format!("filesystem access probe failed: {}", e))?;
let handled_net = AccessNet::from_all(abi);
if !handled_net.is_empty() {
ruleset = ruleset
.handle_access(handled_net)
.map_err(|e| format!("network access probe failed: {}", e))?;
}
let scopes = Scope::from_all(abi);
if !scopes.is_empty() {
ruleset = ruleset
.scope(scopes)
.map_err(|e| format!("scope probe failed: {}", e))?;
}
ruleset
.create()
.map_err(|e| format!("ruleset creation probe failed: {}", e))?;
Ok(())
}
static WSL2_DETECTED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
#[must_use]
pub fn is_wsl2() -> bool {
*WSL2_DETECTED.get_or_init(detect_wsl2)
}
fn detect_wsl2() -> bool {
if std::path::Path::new("/proc/sys/fs/binfmt_misc/WSLInterop").exists() {
info!("WSL2 detected via /proc/sys/fs/binfmt_misc/WSLInterop");
return true;
}
if let Ok(version) = std::fs::read_to_string("/proc/version") {
if version.contains("microsoft") || version.contains("WSL") {
info!("WSL2 detected via /proc/version kernel string");
return true;
}
}
if std::env::var_os("WSL_DISTRO_NAME").is_some() {
warn!(
"WSL_DISTRO_NAME is set but no kernel-controlled WSL2 indicators found; \
ignoring env var to prevent security downgrade"
);
}
false
}
pub fn is_supported() -> bool {
detect_abi().is_ok()
}
pub fn support_info() -> SupportInfo {
match detect_abi() {
Ok(detected) => {
let features = detected.feature_names();
SupportInfo {
is_supported: true,
platform: "linux",
details: format!(
"Landlock available ({}, features: {})",
detected,
features.join(", ")
),
}
}
Err(_) => SupportInfo {
is_supported: false,
platform: "linux",
details: "Landlock not available. Requires Linux kernel 5.13+ with Landlock enabled."
.to_string(),
},
}
}
struct LandlockAccess {
effective: BitFlags<AccessFs>,
dropped: BitFlags<AccessFs>,
}
fn access_to_landlock(access: AccessMode, abi: ABI) -> LandlockAccess {
let available = AccessFs::from_all(abi);
let desired = match access {
AccessMode::Read => AccessFs::ReadFile | AccessFs::ReadDir | AccessFs::Execute,
AccessMode::Write => {
AccessFs::WriteFile
| AccessFs::MakeChar
| AccessFs::MakeDir
| AccessFs::MakeReg
| AccessFs::MakeSock
| AccessFs::MakeFifo
| AccessFs::MakeBlock
| AccessFs::MakeSym
| AccessFs::RemoveFile
| AccessFs::RemoveDir
| AccessFs::Refer
| AccessFs::Truncate
}
AccessMode::ReadWrite => {
let read = access_to_landlock(AccessMode::Read, abi);
let write = access_to_landlock(AccessMode::Write, abi);
return LandlockAccess {
effective: read.effective | write.effective,
dropped: read.dropped | write.dropped,
};
}
};
LandlockAccess {
effective: desired & available,
dropped: desired & !available,
}
}
#[cfg(test)]
#[must_use]
fn can_use_seccomp_network_block_fallback(caps: &CapabilitySet) -> bool {
matches!(
seccomp_network_fallback_mode(caps),
SeccompNetFallback::BlockAll
)
}
fn is_device_path(path: &Path) -> bool {
use std::os::unix::fs::FileTypeExt;
std::fs::metadata(path)
.map(|m| {
let ft = m.file_type();
ft.is_char_device() || ft.is_block_device()
})
.unwrap_or(false)
}
fn is_device_directory(path: &Path) -> bool {
path.starts_with("/dev") && path.is_dir()
}
fn requested_scopes(caps: &CapabilitySet, abi: &DetectedAbi) -> Result<BitFlags<Scope>> {
match caps.signal_mode() {
SignalMode::AllowAll => Ok(BitFlags::EMPTY),
SignalMode::Isolated => {
if abi.has_scoping() {
Ok(Scope::Signal.into())
} else {
Ok(BitFlags::EMPTY)
}
}
SignalMode::AllowSameSandbox => {
if !abi.has_scoping() {
return Err(NonoError::SandboxInit(
"SignalMode::AllowSameSandbox requires Landlock ABI V6+ \
(LANDLOCK_SCOPE_SIGNAL), but this kernel does not support process scoping."
.to_string(),
));
}
Ok(Scope::Signal.into())
}
}
}
pub fn apply(caps: &CapabilitySet) -> Result<SeccompNetFallback> {
let detected = detect_abi()?;
apply_with_abi(caps, &detected)
}
pub fn apply_with_abi(caps: &CapabilitySet, abi: &DetectedAbi) -> Result<SeccompNetFallback> {
let target_abi = abi.abi;
info!("Using Landlock ABI {:?}", target_abi);
let scopes = requested_scopes(caps, abi)?;
let handled_fs = AccessFs::from_all(target_abi);
debug!("Handling filesystem access: {:?}", handled_fs);
let ruleset_builder = Ruleset::default()
.set_compatibility(CompatLevel::HardRequirement)
.handle_access(handled_fs)
.map_err(|e| NonoError::SandboxInit(format!("Failed to handle fs access: {}", e)))?
.set_compatibility(CompatLevel::BestEffort);
let needs_network_handling = !matches!(caps.network_mode(), NetworkMode::AllowAll)
|| !caps.tcp_connect_ports().is_empty()
|| !caps.tcp_bind_ports().is_empty();
let mut seccomp_net_fallback = SeccompNetFallback::None;
let ruleset_builder = if needs_network_handling {
let handled_net = AccessNet::from_all(target_abi);
if !handled_net.is_empty() {
debug!("Handling network access: {:?}", handled_net);
ruleset_builder
.set_compatibility(CompatLevel::HardRequirement)
.handle_access(handled_net)
.map_err(|e| {
NonoError::SandboxInit(format!(
"Network filtering requested but unsupported by this kernel: {}",
e
))
})?
.set_compatibility(CompatLevel::BestEffort)
} else {
let fallback = seccomp_network_fallback_mode(caps);
match &fallback {
SeccompNetFallback::BlockAll => {
warn!(
"Landlock ABI {:?} lacks TCP network filtering; \
using seccomp full-network-block fallback",
target_abi
);
seccomp_net_fallback = fallback;
ruleset_builder
}
SeccompNetFallback::ProxyOnly {
proxy_port,
bind_ports,
} => {
warn!(
"Landlock ABI {:?} lacks TCP network filtering; \
using seccomp proxy-only fallback (port={}, bind_ports={:?})",
target_abi, proxy_port, bind_ports
);
seccomp_net_fallback = fallback;
ruleset_builder
}
SeccompNetFallback::None => {
return Err(NonoError::SandboxInit(
"Network filtering requested but kernel Landlock ABI doesn't support it \
(requires V4+). On this kernel, only full --block-net or --proxy-only \
fallback via seccomp is supported."
.to_string(),
));
}
}
}
} else {
ruleset_builder
};
let ruleset_builder = if scopes.is_empty() {
ruleset_builder
} else {
debug!("Handling Landlock scopes: {:?}", scopes);
ruleset_builder
.set_compatibility(CompatLevel::HardRequirement)
.scope(scopes)
.map_err(|e| {
NonoError::SandboxInit(format!(
"Signal scoping requested but unsupported by this kernel: {}",
e
))
})?
.set_compatibility(CompatLevel::BestEffort)
};
if matches!(caps.signal_mode(), SignalMode::Isolated) && abi.has_scoping() {
debug!(
"SignalMode::Isolated is approximated on Linux with same-sandbox signal scoping: \
Landlock can restrict signals to the same sandbox, but not to self only"
);
} else if matches!(caps.signal_mode(), SignalMode::Isolated) {
debug!(
"SignalMode::Isolated is not enforceable on this kernel: \
Landlock ABI V6+ is required for signal scoping"
);
}
let mut ruleset = ruleset_builder
.create()
.map_err(|e| NonoError::SandboxInit(format!("Failed to create ruleset: {}", e)))?;
if matches!(seccomp_net_fallback, SeccompNetFallback::None) {
if let NetworkMode::ProxyOnly { port, bind_ports } = caps.network_mode() {
debug!("Adding ProxyOnly TCP connect rule for port {}", port);
ruleset = ruleset
.add_rule(NetPort::new(*port, AccessNet::ConnectTcp))
.map_err(|e| {
NonoError::SandboxInit(format!(
"Cannot add TCP connect rule for proxy port {}: {}",
port, e
))
})?;
for bp in bind_ports {
debug!("Adding ProxyOnly TCP bind rule for port {}", bp);
ruleset = ruleset
.add_rule(NetPort::new(*bp, AccessNet::BindTcp))
.map_err(|e| {
NonoError::SandboxInit(format!(
"Cannot add TCP bind rule for port {}: {}",
bp, e
))
})?;
}
}
for port in caps.tcp_connect_ports() {
debug!("Adding TCP connect rule for port {}", port);
ruleset = ruleset
.add_rule(NetPort::new(*port, AccessNet::ConnectTcp))
.map_err(|e| {
NonoError::SandboxInit(format!(
"Cannot add TCP connect rule for port {}: {}",
port, e
))
})?;
}
for port in caps.tcp_bind_ports() {
debug!("Adding TCP bind rule for port {}", port);
ruleset = ruleset
.add_rule(NetPort::new(*port, AccessNet::BindTcp))
.map_err(|e| {
NonoError::SandboxInit(format!(
"Cannot add TCP bind rule for port {}: {}",
port, e
))
})?;
}
if !matches!(caps.network_mode(), NetworkMode::AllowAll) {
for port in caps.localhost_ports() {
debug!("Adding localhost TCP connect rule for port {}", port);
ruleset = ruleset
.add_rule(NetPort::new(*port, AccessNet::ConnectTcp))
.map_err(|e| {
NonoError::SandboxInit(format!(
"Cannot add TCP connect rule for localhost port {}: {}",
port, e
))
})?;
debug!("Adding localhost TCP bind rule for port {}", port);
ruleset = ruleset
.add_rule(NetPort::new(*port, AccessNet::BindTcp))
.map_err(|e| {
NonoError::SandboxInit(format!(
"Cannot add TCP bind rule for localhost port {}: {}",
port, e
))
})?;
}
}
}
let ioctl_dev_available = AccessFs::from_all(target_abi).contains(AccessFs::IoctlDev);
for cap in caps.fs_capabilities() {
let result = access_to_landlock(cap.access, target_abi);
let mut access = result.effective;
if !result.dropped.is_empty() {
debug!(
"Landlock ABI {:?} does not support {:?} for path {} (requested for {:?})",
target_abi,
result.dropped,
cap.resolved.display(),
cap.access
);
}
if ioctl_dev_available
&& matches!(cap.access, AccessMode::Write | AccessMode::ReadWrite)
&& (is_device_path(&cap.resolved) || is_device_directory(&cap.resolved))
{
access |= AccessFs::IoctlDev;
debug!(
"Adding IoctlDev for device path: {}",
cap.resolved.display()
);
}
debug!(
"Adding rule: {} with access {:?}",
cap.resolved.display(),
access
);
let path_fd = PathFd::new(&cap.resolved)?;
ruleset = ruleset
.add_rule(PathBeneath::new(path_fd, access))
.map_err(|e| {
NonoError::SandboxInit(format!(
"Cannot add Landlock rule for {}: {} (filesystem may not support Landlock)",
cap.resolved.display(),
e
))
})?;
}
let status = ruleset
.restrict_self()
.map_err(|e| NonoError::SandboxInit(format!("Failed to restrict self: {}", e)))?;
match status.ruleset {
landlock::RulesetStatus::FullyEnforced => {
info!("Landlock sandbox fully enforced");
}
landlock::RulesetStatus::PartiallyEnforced => {
debug!("Landlock sandbox enforced in best-effort mode (partially enforced)");
}
landlock::RulesetStatus::NotEnforced => {
return Err(NonoError::SandboxInit(
"Landlock sandbox was not enforced".to_string(),
));
}
}
if matches!(seccomp_net_fallback, SeccompNetFallback::BlockAll) {
install_seccomp_block_network().map_err(|e| {
NonoError::SandboxInit(format!(
"Failed to install seccomp network block fallback: {}",
e
))
})?;
info!("Seccomp network block fallback enforced");
}
Ok(seccomp_net_fallback)
}
#[repr(C)]
#[derive(Debug, Clone)]
pub struct SeccompNotif {
pub id: u64,
pub pid: u32,
pub flags: u32,
pub data: SeccompData,
}
#[repr(C)]
#[derive(Debug, Clone, Default)]
pub struct SeccompData {
pub nr: i32,
pub arch: u32,
pub instruction_pointer: u64,
pub args: [u64; 6],
}
#[repr(C)]
#[derive(Debug)]
struct SeccompNotifResp {
id: u64,
val: i64,
error: i32,
flags: u32,
}
#[repr(C)]
#[derive(Debug)]
struct SeccompNotifAddfd {
id: u64,
flags: u32,
srcfd: u32,
newfd: u32,
newfd_flags: u32,
}
const SECCOMP_SET_MODE_FILTER: libc::c_uint = 1;
const SECCOMP_FILTER_FLAG_NEW_LISTENER: libc::c_uint = 1 << 3;
const SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV: libc::c_uint = 1 << 4;
const SECCOMP_IOCTL_NOTIF_RECV: libc::c_ulong = 0xc0502100;
const SECCOMP_IOCTL_NOTIF_SEND: libc::c_ulong = 0xc0182101;
const SECCOMP_IOCTL_NOTIF_ID_VALID: libc::c_ulong = 0x40082102;
const SECCOMP_IOCTL_NOTIF_ADDFD: libc::c_ulong = 0x40182103;
const SECCOMP_ADDFD_FLAG_SEND: u32 = 1 << 1;
const SECCOMP_USER_NOTIF_FLAG_CONTINUE: u32 = 1;
const BPF_LD: u16 = 0x00;
const BPF_W: u16 = 0x00;
const BPF_ABS: u16 = 0x20;
const BPF_JMP: u16 = 0x05;
const BPF_JEQ: u16 = 0x10;
const BPF_K: u16 = 0x00;
const BPF_RET: u16 = 0x06;
const SECCOMP_RET_ERRNO: u32 = 0x0005_0000;
const SECCOMP_RET_USER_NOTIF: u32 = 0x7fc0_0000;
const SECCOMP_RET_ALLOW: u32 = 0x7fff_0000;
#[cfg(target_arch = "x86_64")]
pub const SYS_OPENAT: i32 = 257;
#[cfg(target_arch = "x86_64")]
pub const SYS_OPENAT2: i32 = 437;
#[cfg(target_arch = "aarch64")]
pub const SYS_OPENAT: i32 = 56;
#[cfg(target_arch = "aarch64")]
pub const SYS_OPENAT2: i32 = 437;
#[cfg(target_os = "linux")]
const SYS_SOCKET: i32 = libc::SYS_socket as i32;
#[cfg(target_os = "linux")]
const SYS_SOCKETPAIR: i32 = libc::SYS_socketpair as i32;
#[cfg(target_os = "linux")]
const SYS_IO_URING_SETUP: i32 = libc::SYS_io_uring_setup as i32;
#[cfg(target_os = "linux")]
pub const SYS_CONNECT: i32 = libc::SYS_connect as i32;
#[cfg(target_os = "linux")]
pub const SYS_BIND: i32 = libc::SYS_bind as i32;
#[repr(C)]
#[derive(Debug, Clone, Default)]
pub struct OpenHow {
pub flags: u64,
pub mode: u64,
pub resolve: u64,
}
#[must_use]
pub fn classify_access_from_flags(flags: i32) -> crate::AccessMode {
match flags & libc::O_ACCMODE {
libc::O_RDONLY => crate::AccessMode::Read,
libc::O_WRONLY => crate::AccessMode::Write,
_ => crate::AccessMode::ReadWrite,
}
}
const OPENAT2_HOW_SIZE_MAX: usize = 4096;
#[must_use]
pub fn validate_openat2_size(how_size: usize) -> bool {
let min_size = std::mem::size_of::<OpenHow>();
how_size >= min_size && how_size <= OPENAT2_HOW_SIZE_MAX
}
const SECCOMP_DATA_NR_OFFSET: u32 = 0;
const SECCOMP_DATA_ARG0_OFFSET: u32 = 16;
#[repr(C)]
#[derive(Debug, Clone, Copy)]
struct SockFilterInsn {
code: u16,
jt: u8,
jf: u8,
k: u32,
}
#[repr(C)]
struct SockFprog {
len: u16,
filter: *const SockFilterInsn,
}
pub fn install_seccomp_notify() -> Result<std::os::fd::OwnedFd> {
use std::os::fd::FromRawFd;
let filter = [
SockFilterInsn {
code: BPF_LD | BPF_W | BPF_ABS,
jt: 0,
jf: 0,
k: SECCOMP_DATA_NR_OFFSET,
},
SockFilterInsn {
code: BPF_JMP | BPF_JEQ | BPF_K,
jt: 2, jf: 0,
k: SYS_OPENAT as u32,
},
SockFilterInsn {
code: BPF_JMP | BPF_JEQ | BPF_K,
jt: 1, jf: 0,
k: SYS_OPENAT2 as u32,
},
SockFilterInsn {
code: BPF_RET | BPF_K,
jt: 0,
jf: 0,
k: SECCOMP_RET_ALLOW,
},
SockFilterInsn {
code: BPF_RET | BPF_K,
jt: 0,
jf: 0,
k: SECCOMP_RET_USER_NOTIF,
},
];
let prog = SockFprog {
len: filter.len() as u16,
filter: filter.as_ptr(),
};
let ret = unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) };
if ret != 0 {
return Err(NonoError::SandboxInit(format!(
"prctl(PR_SET_NO_NEW_PRIVS) failed: {}",
std::io::Error::last_os_error()
)));
}
let flags = SECCOMP_FILTER_FLAG_NEW_LISTENER | SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV;
let ret = unsafe {
libc::syscall(
libc::SYS_seccomp,
SECCOMP_SET_MODE_FILTER,
flags,
&prog as *const SockFprog,
)
};
let notify_fd = if ret < 0 {
let flags = SECCOMP_FILTER_FLAG_NEW_LISTENER;
let ret = unsafe {
libc::syscall(
libc::SYS_seccomp,
SECCOMP_SET_MODE_FILTER,
flags,
&prog as *const SockFprog,
)
};
if ret < 0 {
return Err(NonoError::SandboxInit(format!(
"seccomp(SECCOMP_SET_MODE_FILTER) failed: {}. \
Requires kernel >= 5.0 with SECCOMP_FILTER_FLAG_NEW_LISTENER.",
std::io::Error::last_os_error()
)));
}
ret as i32
} else {
ret as i32
};
Ok(unsafe { std::os::fd::OwnedFd::from_raw_fd(notify_fd) })
}
fn build_seccomp_block_network_filter() -> [SockFilterInsn; 10] {
let errno_ret = SECCOMP_RET_ERRNO | (libc::EPERM as u32);
[
SockFilterInsn {
code: BPF_LD | BPF_W | BPF_ABS,
jt: 0,
jf: 0,
k: SECCOMP_DATA_NR_OFFSET,
},
SockFilterInsn {
code: BPF_JMP | BPF_JEQ | BPF_K,
jt: 4,
jf: 0,
k: SYS_SOCKET as u32,
},
SockFilterInsn {
code: BPF_JMP | BPF_JEQ | BPF_K,
jt: 3,
jf: 0,
k: SYS_SOCKETPAIR as u32,
},
SockFilterInsn {
code: BPF_JMP | BPF_JEQ | BPF_K,
jt: 1,
jf: 0,
k: SYS_IO_URING_SETUP as u32,
},
SockFilterInsn {
code: BPF_RET | BPF_K,
jt: 0,
jf: 0,
k: SECCOMP_RET_ALLOW,
},
SockFilterInsn {
code: BPF_RET | BPF_K,
jt: 0,
jf: 0,
k: errno_ret,
},
SockFilterInsn {
code: BPF_LD | BPF_W | BPF_ABS,
jt: 0,
jf: 0,
k: SECCOMP_DATA_ARG0_OFFSET,
},
SockFilterInsn {
code: BPF_JMP | BPF_JEQ | BPF_K,
jt: 1,
jf: 0,
k: libc::AF_UNIX as u32,
},
SockFilterInsn {
code: BPF_RET | BPF_K,
jt: 0,
jf: 0,
k: errno_ret,
},
SockFilterInsn {
code: BPF_RET | BPF_K,
jt: 0,
jf: 0,
k: SECCOMP_RET_ALLOW,
},
]
}
pub fn install_seccomp_block_network() -> Result<()> {
let filter = build_seccomp_block_network_filter();
let prog = SockFprog {
len: filter.len() as u16,
filter: filter.as_ptr(),
};
let ret = unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) };
if ret != 0 {
return Err(NonoError::SandboxInit(format!(
"prctl(PR_SET_NO_NEW_PRIVS) failed: {}",
std::io::Error::last_os_error()
)));
}
let ret = unsafe {
libc::syscall(
libc::SYS_seccomp,
SECCOMP_SET_MODE_FILTER,
0,
&prog as *const SockFprog,
)
};
if ret < 0 {
return Err(NonoError::SandboxInit(format!(
"seccomp(SECCOMP_SET_MODE_FILTER) for network block failed: {}",
std::io::Error::last_os_error()
)));
}
Ok(())
}
pub fn probe_seccomp_block_network_support() -> Result<bool> {
let pid = unsafe { libc::fork() };
if pid < 0 {
return Err(NonoError::SandboxInit(format!(
"fork() failed during seccomp network fallback probe: {}",
std::io::Error::last_os_error()
)));
}
if pid == 0 {
let exit_code = if install_seccomp_block_network().is_ok() {
0
} else {
1
};
unsafe { libc::_exit(exit_code) };
}
let mut status = 0;
let waited = unsafe { libc::waitpid(pid, &mut status, 0) };
if waited < 0 {
return Err(NonoError::SandboxInit(format!(
"waitpid() failed during seccomp network fallback probe: {}",
std::io::Error::last_os_error()
)));
}
Ok(libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 0)
}
pub fn recv_notif(notify_fd: std::os::fd::RawFd) -> Result<SeccompNotif> {
let mut notif = SeccompNotif {
id: 0,
pid: 0,
flags: 0,
data: SeccompData::default(),
};
let ret = unsafe {
libc::ioctl(
notify_fd,
SECCOMP_IOCTL_NOTIF_RECV,
&mut notif as *mut SeccompNotif,
)
};
if ret < 0 {
return Err(NonoError::SandboxInit(format!(
"SECCOMP_IOCTL_NOTIF_RECV failed: {}",
std::io::Error::last_os_error()
)));
}
Ok(notif)
}
pub fn read_notif_path(pid: u32, addr: u64) -> Result<std::path::PathBuf> {
use std::io::Read;
let mem_path = format!("/proc/{}/mem", pid);
let mut file = std::fs::File::open(&mem_path)
.map_err(|e| NonoError::SandboxInit(format!("Failed to open {}: {}", mem_path, e)))?;
std::io::Seek::seek(&mut file, std::io::SeekFrom::Start(addr))
.map_err(|e| NonoError::SandboxInit(format!("Failed to seek in {}: {}", mem_path, e)))?;
let mut buf = vec![0u8; 4096];
let n = file.read(&mut buf).map_err(|e| {
NonoError::SandboxInit(format!("Failed to read path from {}: {}", mem_path, e))
})?;
let end = buf[..n].iter().position(|&b| b == 0).unwrap_or(n);
if end == 0 || end >= 4096 {
return Err(NonoError::SandboxInit(
"Invalid path in seccomp notification (empty or too long)".to_string(),
));
}
let path_str = std::str::from_utf8(&buf[..end]).map_err(|_| {
NonoError::SandboxInit("Path in seccomp notification is not valid UTF-8".to_string())
})?;
Ok(std::path::PathBuf::from(path_str))
}
pub fn resolve_notif_path(
pid: u32,
dirfd: u64,
raw_path: &std::path::Path,
) -> Result<std::path::PathBuf> {
if raw_path.is_absolute() {
return Ok(raw_path.to_path_buf());
}
#[allow(clippy::unnecessary_cast)]
let at_fdcwd_u64 = libc::AT_FDCWD as i32 as u32 as u64;
#[allow(clippy::unnecessary_cast)]
let at_fdcwd_u64_extended = libc::AT_FDCWD as i64 as u64;
let base_dir = if dirfd == at_fdcwd_u64 || dirfd == at_fdcwd_u64_extended {
let cwd_link = format!("/proc/{}/cwd", pid);
std::fs::read_link(&cwd_link).map_err(|e| {
NonoError::SandboxInit(format!(
"Failed to read {} for dirfd-relative path resolution: {}",
cwd_link, e
))
})?
} else {
let fd_link = format!("/proc/{}/fd/{}", pid, dirfd);
std::fs::read_link(&fd_link).map_err(|e| {
NonoError::SandboxInit(format!(
"Failed to read {} for dirfd-relative path resolution: {}",
fd_link, e
))
})?
};
Ok(base_dir.join(raw_path))
}
pub fn read_open_how(pid: u32, addr: u64) -> Result<OpenHow> {
use std::io::Read;
let mem_path = format!("/proc/{}/mem", pid);
let mut file = std::fs::File::open(&mem_path)
.map_err(|e| NonoError::SandboxInit(format!("Failed to open {}: {}", mem_path, e)))?;
std::io::Seek::seek(&mut file, std::io::SeekFrom::Start(addr))
.map_err(|e| NonoError::SandboxInit(format!("Failed to seek in {}: {}", mem_path, e)))?;
let mut buf = [0u8; std::mem::size_of::<OpenHow>()];
file.read_exact(&mut buf).map_err(|e| {
NonoError::SandboxInit(format!("Failed to read open_how from {}: {}", mem_path, e))
})?;
let open_how: OpenHow = unsafe { std::ptr::read_unaligned(buf.as_ptr().cast()) };
Ok(open_how)
}
pub fn notif_id_valid(notify_fd: std::os::fd::RawFd, notif_id: u64) -> Result<bool> {
let ret = unsafe {
libc::ioctl(
notify_fd,
SECCOMP_IOCTL_NOTIF_ID_VALID,
¬if_id as *const u64,
)
};
if ret < 0 {
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(libc::ENOENT) {
return Ok(false);
}
return Err(NonoError::SandboxInit(format!(
"SECCOMP_IOCTL_NOTIF_ID_VALID failed: {}",
err
)));
}
Ok(true)
}
pub fn inject_fd(
notify_fd: std::os::fd::RawFd,
notif_id: u64,
fd: std::os::fd::RawFd,
) -> Result<()> {
let addfd = SeccompNotifAddfd {
id: notif_id,
flags: SECCOMP_ADDFD_FLAG_SEND,
srcfd: fd as u32,
newfd: 0, newfd_flags: libc::O_CLOEXEC as u32, };
let ret = unsafe {
libc::ioctl(
notify_fd,
SECCOMP_IOCTL_NOTIF_ADDFD,
&addfd as *const SeccompNotifAddfd,
)
};
if ret < 0 {
return Err(NonoError::SandboxInit(format!(
"SECCOMP_IOCTL_NOTIF_ADDFD failed: {}. Requires kernel >= 5.14.",
std::io::Error::last_os_error()
)));
}
Ok(())
}
pub fn respond_notif_errno(notify_fd: std::os::fd::RawFd, notif_id: u64, errno: i32) -> Result<()> {
let resp = SeccompNotifResp {
id: notif_id,
val: 0,
error: -errno,
flags: 0,
};
let ret = unsafe {
libc::ioctl(
notify_fd,
SECCOMP_IOCTL_NOTIF_SEND,
&resp as *const SeccompNotifResp,
)
};
if ret < 0 {
return Err(NonoError::SandboxInit(format!(
"SECCOMP_IOCTL_NOTIF_SEND failed: {}",
std::io::Error::last_os_error()
)));
}
Ok(())
}
pub fn continue_notif(notify_fd: std::os::fd::RawFd, notif_id: u64) -> Result<()> {
let resp = SeccompNotifResp {
id: notif_id,
val: 0,
error: 0,
flags: SECCOMP_USER_NOTIF_FLAG_CONTINUE,
};
let ret = unsafe {
libc::ioctl(
notify_fd,
SECCOMP_IOCTL_NOTIF_SEND,
&resp as *const SeccompNotifResp,
)
};
if ret < 0 {
return Err(NonoError::SandboxInit(format!(
"SECCOMP_IOCTL_NOTIF_SEND (continue) failed: {}",
std::io::Error::last_os_error()
)));
}
Ok(())
}
pub fn deny_notif(notify_fd: std::os::fd::RawFd, notif_id: u64) -> Result<()> {
respond_notif_errno(notify_fd, notif_id, libc::EPERM)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnixSocketKind {
Pathname,
Abstract,
Unnamed,
}
#[must_use]
pub fn classify_af_unix(addrlen: u64, sun_path_first_byte: Option<u8>) -> UnixSocketKind {
if addrlen <= 2 {
return UnixSocketKind::Unnamed;
}
match sun_path_first_byte {
Some(0) => UnixSocketKind::Abstract,
Some(_) => UnixSocketKind::Pathname,
None => UnixSocketKind::Unnamed,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SockaddrInfo {
pub family: u16,
pub port: u16,
pub is_loopback: bool,
pub unix_kind: Option<UnixSocketKind>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SeccompNetFallback {
None,
BlockAll,
ProxyOnly {
proxy_port: u16,
bind_ports: Vec<u16>,
},
}
#[must_use]
pub fn seccomp_network_fallback_mode(caps: &CapabilitySet) -> SeccompNetFallback {
match caps.network_mode() {
NetworkMode::Blocked => {
if caps.tcp_connect_ports().is_empty()
&& caps.tcp_bind_ports().is_empty()
&& caps.localhost_ports().is_empty()
{
SeccompNetFallback::BlockAll
} else {
SeccompNetFallback::None
}
}
NetworkMode::ProxyOnly { port, bind_ports } => SeccompNetFallback::ProxyOnly {
proxy_port: *port,
bind_ports: bind_ports.clone(),
},
NetworkMode::AllowAll => SeccompNetFallback::None,
}
}
fn build_seccomp_proxy_filter(_has_bind_ports: bool) -> Vec<SockFilterInsn> {
let errno_ret = SECCOMP_RET_ERRNO | (libc::EACCES as u32);
let bind_action = SECCOMP_RET_USER_NOTIF;
vec![
SockFilterInsn {
code: BPF_LD | BPF_W | BPF_ABS,
jt: 0,
jf: 0,
k: SECCOMP_DATA_NR_OFFSET,
},
SockFilterInsn {
code: BPF_JMP | BPF_JEQ | BPF_K,
jt: 6,
jf: 0,
k: SYS_SOCKET as u32,
},
SockFilterInsn {
code: BPF_JMP | BPF_JEQ | BPF_K,
jt: 13,
jf: 0,
k: SYS_CONNECT as u32,
},
SockFilterInsn {
code: BPF_JMP | BPF_JEQ | BPF_K,
jt: 13,
jf: 0,
k: SYS_BIND as u32,
},
SockFilterInsn {
code: BPF_JMP | BPF_JEQ | BPF_K,
jt: 8,
jf: 0,
k: SYS_SOCKETPAIR as u32,
},
SockFilterInsn {
code: BPF_JMP | BPF_JEQ | BPF_K,
jt: 1,
jf: 0,
k: SYS_IO_URING_SETUP as u32,
},
SockFilterInsn {
code: BPF_RET | BPF_K,
jt: 0,
jf: 0,
k: SECCOMP_RET_ALLOW,
},
SockFilterInsn {
code: BPF_RET | BPF_K,
jt: 0,
jf: 0,
k: errno_ret,
},
SockFilterInsn {
code: BPF_LD | BPF_W | BPF_ABS,
jt: 0,
jf: 0,
k: SECCOMP_DATA_ARG0_OFFSET,
},
SockFilterInsn {
code: BPF_JMP | BPF_JEQ | BPF_K,
jt: 8,
jf: 0,
k: libc::AF_UNIX as u32,
},
SockFilterInsn {
code: BPF_JMP | BPF_JEQ | BPF_K,
jt: 7,
jf: 0,
k: libc::AF_INET as u32,
},
SockFilterInsn {
code: BPF_JMP | BPF_JEQ | BPF_K,
jt: 6,
jf: 0,
k: libc::AF_INET6 as u32,
},
SockFilterInsn {
code: BPF_RET | BPF_K,
jt: 0,
jf: 0,
k: errno_ret,
},
SockFilterInsn {
code: BPF_LD | BPF_W | BPF_ABS,
jt: 0,
jf: 0,
k: SECCOMP_DATA_ARG0_OFFSET,
},
SockFilterInsn {
code: BPF_JMP | BPF_JEQ | BPF_K,
jt: 3,
jf: 0,
k: libc::AF_UNIX as u32,
},
SockFilterInsn {
code: BPF_RET | BPF_K,
jt: 0,
jf: 0,
k: errno_ret,
},
SockFilterInsn {
code: BPF_RET | BPF_K,
jt: 0,
jf: 0,
k: SECCOMP_RET_USER_NOTIF,
},
SockFilterInsn {
code: BPF_RET | BPF_K,
jt: 0,
jf: 0,
k: bind_action,
},
SockFilterInsn {
code: BPF_RET | BPF_K,
jt: 0,
jf: 0,
k: SECCOMP_RET_ALLOW,
},
]
}
pub fn install_seccomp_proxy_filter(has_bind_ports: bool) -> Result<std::os::fd::OwnedFd> {
use std::os::fd::FromRawFd;
let filter = build_seccomp_proxy_filter(has_bind_ports);
let prog = SockFprog {
len: filter.len() as u16,
filter: filter.as_ptr(),
};
let ret = unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) };
if ret != 0 {
return Err(NonoError::SandboxInit(format!(
"prctl(PR_SET_NO_NEW_PRIVS) failed: {}",
std::io::Error::last_os_error()
)));
}
let flags = SECCOMP_FILTER_FLAG_NEW_LISTENER | SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV;
let ret = unsafe {
libc::syscall(
libc::SYS_seccomp,
SECCOMP_SET_MODE_FILTER,
flags,
&prog as *const SockFprog,
)
};
let notify_fd = if ret < 0 {
let flags = SECCOMP_FILTER_FLAG_NEW_LISTENER;
let ret = unsafe {
libc::syscall(
libc::SYS_seccomp,
SECCOMP_SET_MODE_FILTER,
flags,
&prog as *const SockFprog,
)
};
if ret < 0 {
return Err(NonoError::SandboxInit(format!(
"seccomp(SECCOMP_SET_MODE_FILTER) for proxy filter failed: {}. \
Requires kernel >= 5.0 with SECCOMP_FILTER_FLAG_NEW_LISTENER.",
std::io::Error::last_os_error()
)));
}
ret as i32
} else {
ret as i32
};
Ok(unsafe { std::os::fd::OwnedFd::from_raw_fd(notify_fd) })
}
pub fn read_notif_sockaddr(pid: u32, addr_ptr: u64, addrlen: u64) -> Result<SockaddrInfo> {
use std::io::Read;
if addrlen < 2 {
return Err(NonoError::SandboxInit(
"sockaddr too small to contain sa_family".to_string(),
));
}
let mem_path = format!("/proc/{}/mem", pid);
let mut file = std::fs::File::open(&mem_path)
.map_err(|e| NonoError::SandboxInit(format!("Failed to open {}: {}", mem_path, e)))?;
std::io::Seek::seek(&mut file, std::io::SeekFrom::Start(addr_ptr))
.map_err(|e| NonoError::SandboxInit(format!("Failed to seek in {}: {}", mem_path, e)))?;
let read_len = std::cmp::min(addrlen as usize, 28);
let mut buf = [0u8; 28];
let n = file.read(&mut buf[..read_len]).map_err(|e| {
NonoError::SandboxInit(format!("Failed to read sockaddr from {}: {}", mem_path, e))
})?;
if n < 2 {
return Err(NonoError::SandboxInit(
"Short read for sockaddr sa_family".to_string(),
));
}
let family = u16::from_ne_bytes([buf[0], buf[1]]);
match family as i32 {
libc::AF_INET => {
if n < 8 {
return Err(NonoError::SandboxInit(
"sockaddr_in too small for port + addr".to_string(),
));
}
let port = u16::from_be_bytes([buf[2], buf[3]]);
let addr = [buf[4], buf[5], buf[6], buf[7]];
let is_loopback = addr[0] == 127;
Ok(SockaddrInfo {
family,
port,
is_loopback,
unix_kind: None,
})
}
libc::AF_INET6 => {
if n < 24 {
return Err(NonoError::SandboxInit(
"sockaddr_in6 too small for port + addr".to_string(),
));
}
let port = u16::from_be_bytes([buf[2], buf[3]]);
let mut addr = [0u8; 16];
addr.copy_from_slice(&buf[8..24]);
let is_loopback = addr == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1];
let is_v4_mapped_loopback =
addr[..12] == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff] && addr[12] == 127;
Ok(SockaddrInfo {
family,
port,
is_loopback: is_loopback || is_v4_mapped_loopback,
unix_kind: None,
})
}
libc::AF_UNIX => {
let sun_path_first_byte = if n >= 3 { Some(buf[2]) } else { None };
Ok(SockaddrInfo {
family,
port: 0,
is_loopback: true, unix_kind: Some(classify_af_unix(addrlen, sun_path_first_byte)),
})
}
_ => Ok(SockaddrInfo {
family,
port: 0,
is_loopback: false,
unix_kind: None,
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_supported() {
let _ = is_supported();
}
#[test]
fn test_support_info() {
let info = support_info();
assert!(!info.details.is_empty());
}
#[test]
fn test_access_conversion_v3() {
let abi = ABI::V3;
let read = access_to_landlock(AccessMode::Read, abi);
assert!(read.effective.contains(AccessFs::ReadFile));
assert!(!read.effective.contains(AccessFs::WriteFile));
assert!(read.dropped.is_empty());
let write = access_to_landlock(AccessMode::Write, abi);
assert!(write.effective.contains(AccessFs::WriteFile));
assert!(!write.effective.contains(AccessFs::ReadFile));
assert!(write.effective.contains(AccessFs::RemoveFile));
assert!(write.effective.contains(AccessFs::RemoveDir));
assert!(write.effective.contains(AccessFs::Refer));
assert!(write.effective.contains(AccessFs::Truncate));
assert!(!write.effective.contains(AccessFs::IoctlDev));
assert!(write.dropped.is_empty());
let rw = access_to_landlock(AccessMode::ReadWrite, abi);
assert!(rw.effective.contains(AccessFs::ReadFile));
assert!(rw.effective.contains(AccessFs::WriteFile));
assert!(rw.effective.contains(AccessFs::RemoveFile));
assert!(rw.effective.contains(AccessFs::RemoveDir));
assert!(rw.effective.contains(AccessFs::Refer));
assert!(rw.effective.contains(AccessFs::Truncate));
assert!(rw.dropped.is_empty());
}
#[test]
fn test_access_conversion_v1_drops_refer_and_truncate() {
let abi = ABI::V1;
let write = access_to_landlock(AccessMode::Write, abi);
assert!(write.effective.contains(AccessFs::WriteFile));
assert!(!write.effective.contains(AccessFs::Refer));
assert!(!write.effective.contains(AccessFs::Truncate));
assert!(!write.effective.contains(AccessFs::IoctlDev));
assert!(write.effective.contains(AccessFs::RemoveFile));
assert!(write.effective.contains(AccessFs::RemoveDir));
assert!(write.dropped.contains(AccessFs::Refer));
assert!(write.dropped.contains(AccessFs::Truncate));
}
#[test]
fn test_access_conversion_v2_has_refer_but_not_truncate() {
let abi = ABI::V2;
let write = access_to_landlock(AccessMode::Write, abi);
assert!(write.effective.contains(AccessFs::WriteFile));
assert!(write.effective.contains(AccessFs::Refer));
assert!(!write.effective.contains(AccessFs::Truncate));
assert!(!write.effective.contains(AccessFs::IoctlDev));
assert!(write.dropped.contains(AccessFs::Truncate));
assert!(!write.dropped.contains(AccessFs::Refer));
}
#[test]
fn test_access_conversion_v5_excludes_ioctl_dev_from_generic_flags() {
let abi = ABI::V5;
let write = access_to_landlock(AccessMode::Write, abi);
assert!(!write.effective.contains(AccessFs::IoctlDev));
let rw = access_to_landlock(AccessMode::ReadWrite, abi);
assert!(!rw.effective.contains(AccessFs::IoctlDev));
let read = access_to_landlock(AccessMode::Read, abi);
assert!(!read.effective.contains(AccessFs::IoctlDev));
}
#[test]
fn test_is_device_path_dev_null() {
assert!(is_device_path(Path::new("/dev/null")));
}
#[test]
fn test_is_device_path_regular_file() {
assert!(!is_device_path(Path::new("/etc/hosts")));
}
#[test]
fn test_is_device_path_nonexistent() {
assert!(!is_device_path(Path::new("/nonexistent/path/12345")));
}
#[test]
fn test_is_device_directory_dev_pts() {
if Path::new("/dev/pts").exists() {
assert!(is_device_directory(Path::new("/dev/pts")));
}
}
#[test]
fn test_is_device_directory_not_dev() {
assert!(!is_device_directory(Path::new("/tmp")));
}
#[test]
fn test_detected_abi_feature_methods() {
let v1 = DetectedAbi::new(ABI::V1);
assert!(!v1.has_refer());
assert!(!v1.has_truncate());
assert!(!v1.has_network());
assert!(!v1.has_ioctl_dev());
assert!(!v1.has_scoping());
let v2 = DetectedAbi::new(ABI::V2);
assert!(v2.has_refer());
assert!(!v2.has_truncate());
let v3 = DetectedAbi::new(ABI::V3);
assert!(v3.has_refer());
assert!(v3.has_truncate());
assert!(!v3.has_network());
let v4 = DetectedAbi::new(ABI::V4);
assert!(v4.has_network());
assert!(!v4.has_ioctl_dev());
let v5 = DetectedAbi::new(ABI::V5);
assert!(v5.has_ioctl_dev());
assert!(!v5.has_scoping());
let v6 = DetectedAbi::new(ABI::V6);
assert!(v6.has_scoping());
}
#[test]
fn test_requested_scopes_allow_all_is_empty() {
let caps = CapabilitySet::new().set_signal_mode(SignalMode::AllowAll);
let scopes = requested_scopes(&caps, &DetectedAbi::new(ABI::V6));
assert!(matches!(scopes, Ok(actual) if actual.is_empty()));
}
#[test]
fn test_requested_scopes_isolated_uses_signal_scope_on_v6() {
let caps = CapabilitySet::new().set_signal_mode(SignalMode::Isolated);
let scopes = requested_scopes(&caps, &DetectedAbi::new(ABI::V6));
assert!(matches!(scopes, Ok(actual) if actual == BitFlags::from(Scope::Signal)));
}
#[test]
fn test_requested_scopes_isolated_is_empty_without_v6() {
let caps = CapabilitySet::new().set_signal_mode(SignalMode::Isolated);
let scopes = requested_scopes(&caps, &DetectedAbi::new(ABI::V5));
assert!(matches!(scopes, Ok(actual) if actual.is_empty()));
}
#[test]
fn test_requested_scopes_allow_same_sandbox_requires_v6() {
let caps = CapabilitySet::new().set_signal_mode(SignalMode::AllowSameSandbox);
let scopes = requested_scopes(&caps, &DetectedAbi::new(ABI::V5));
assert!(
matches!(scopes, Err(NonoError::SandboxInit(message)) if message.contains("Landlock ABI V6+"))
);
}
#[test]
fn test_requested_scopes_allow_same_sandbox_uses_signal_scope() {
let caps = CapabilitySet::new().set_signal_mode(SignalMode::AllowSameSandbox);
let scopes = requested_scopes(&caps, &DetectedAbi::new(ABI::V6));
assert!(matches!(scopes, Ok(actual) if actual == BitFlags::from(Scope::Signal)));
}
#[cfg(target_os = "linux")]
#[test]
fn test_signal_scope_blocks_external_kill_on_v6() {
struct ChildCleanup {
sandbox_pid: Option<libc::pid_t>,
target_pid: Option<libc::pid_t>,
}
impl Drop for ChildCleanup {
fn drop(&mut self) {
if let Some(pid) = self.sandbox_pid.take() {
unsafe {
libc::kill(pid, libc::SIGKILL);
libc::waitpid(pid, std::ptr::null_mut(), 0);
}
}
if let Some(pid) = self.target_pid.take() {
unsafe {
libc::kill(pid, libc::SIGKILL);
libc::waitpid(pid, std::ptr::null_mut(), 0);
}
}
}
}
let detected = match detect_abi() {
Ok(detected) => detected,
Err(_) => return,
};
if !detected.has_scoping() {
return;
}
let mut report_pipe = [0; 2];
let pipe_result = unsafe { libc::pipe(report_pipe.as_mut_ptr()) };
assert_eq!(pipe_result, 0, "pipe() failed");
let target_pid = unsafe { libc::fork() };
assert!(target_pid >= 0, "fork() for target failed");
let mut cleanup = ChildCleanup {
sandbox_pid: None,
target_pid: Some(target_pid),
};
if target_pid == 0 {
unsafe {
libc::close(report_pipe[0]);
libc::close(report_pipe[1]);
libc::signal(libc::SIGUSR1, libc::SIG_IGN);
libc::pause();
libc::_exit(0);
}
}
let sandbox_pid = unsafe { libc::fork() };
assert!(sandbox_pid >= 0, "fork() for sandbox failed");
cleanup.sandbox_pid = Some(sandbox_pid);
if sandbox_pid == 0 {
let mut payload = [0_u8; 2];
unsafe {
libc::close(report_pipe[0]);
}
let caps = CapabilitySet::new().set_signal_mode(SignalMode::AllowSameSandbox);
match apply_with_abi(&caps, &detected) {
Ok(_) => {
let kill_result = unsafe { libc::kill(target_pid, libc::SIGUSR1) };
let errno = std::io::Error::last_os_error()
.raw_os_error()
.unwrap_or(255);
payload[0] = if kill_result == -1 { 1 } else { 0 };
payload[1] = u8::try_from(errno).unwrap_or(u8::MAX);
}
Err(_) => {
payload[0] = 2;
payload[1] = 0;
}
}
let write_len = payload.len();
let wrote = unsafe {
libc::write(
report_pipe[1],
payload.as_ptr().cast::<libc::c_void>(),
write_len,
)
};
let exit_code = if wrote == isize::try_from(write_len).unwrap_or(-1) {
0
} else {
3
};
unsafe {
libc::close(report_pipe[1]);
libc::_exit(exit_code);
}
}
unsafe {
libc::close(report_pipe[1]);
}
let mut sandbox_status = 0;
let waited_sandbox = unsafe { libc::waitpid(sandbox_pid, &mut sandbox_status, 0) };
assert_eq!(waited_sandbox, sandbox_pid, "waitpid() for sandbox failed");
assert!(
libc::WIFEXITED(sandbox_status),
"sandbox child did not exit normally"
);
cleanup.sandbox_pid = None;
assert_eq!(
libc::WEXITSTATUS(sandbox_status),
0,
"sandbox child returned failure"
);
let mut payload = [0_u8; 2];
let read_len = payload.len();
let read_result = unsafe {
libc::read(
report_pipe[0],
payload.as_mut_ptr().cast::<libc::c_void>(),
read_len,
)
};
unsafe {
libc::close(report_pipe[0]);
}
assert_eq!(
read_result,
isize::try_from(read_len).unwrap_or(-1),
"failed to read sandbox report"
);
assert_eq!(payload[0], 1, "sandboxed kill unexpectedly succeeded");
assert_eq!(
i32::from(payload[1]),
libc::EPERM,
"kill should fail with EPERM"
);
let target_wait = unsafe { libc::waitpid(target_pid, std::ptr::null_mut(), libc::WNOHANG) };
assert_eq!(target_wait, 0, "external target should still be running");
unsafe {
libc::kill(target_pid, libc::SIGKILL);
libc::waitpid(target_pid, std::ptr::null_mut(), 0);
}
cleanup.target_pid = None;
}
#[test]
fn test_detected_abi_version_string() {
assert_eq!(DetectedAbi::new(ABI::V1).version_string(), "V1");
assert_eq!(DetectedAbi::new(ABI::V4).version_string(), "V4");
assert_eq!(DetectedAbi::new(ABI::V6).version_string(), "V6");
}
#[test]
fn test_detected_abi_display() {
let d = DetectedAbi::new(ABI::V4);
assert_eq!(format!("{}", d), "Landlock V4");
}
#[test]
fn test_detected_abi_feature_names() {
let v1 = DetectedAbi::new(ABI::V1);
let names = v1.feature_names();
assert_eq!(names.len(), 1);
assert_eq!(names[0], "Basic filesystem access control");
let v4 = DetectedAbi::new(ABI::V4);
let names = v4.feature_names();
assert!(names.iter().any(|n| n.starts_with("TCP network filtering")));
assert!(names
.iter()
.any(|n| n == "File rename across directories (Refer)"));
assert!(names.iter().any(|n| n == "File truncation (Truncate)"));
}
#[test]
fn test_detect_abi_returns_ok_on_supported_system() {
let _ = detect_abi();
}
#[test]
fn test_seccomp_notif_struct_sizes() {
use std::mem;
assert_eq!(mem::size_of::<SeccompData>(), 64);
assert_eq!(mem::size_of::<SeccompNotif>(), 80);
assert_eq!(mem::size_of::<SeccompNotifResp>(), 24);
assert_eq!(mem::size_of::<SeccompNotifAddfd>(), 24);
}
#[test]
fn test_bpf_filter_instruction_count() {
let filter = [
SockFilterInsn {
code: BPF_LD | BPF_W | BPF_ABS,
jt: 0,
jf: 0,
k: SECCOMP_DATA_NR_OFFSET,
},
SockFilterInsn {
code: BPF_JMP | BPF_JEQ | BPF_K,
jt: 2,
jf: 0,
k: SYS_OPENAT as u32,
},
SockFilterInsn {
code: BPF_JMP | BPF_JEQ | BPF_K,
jt: 1,
jf: 0,
k: SYS_OPENAT2 as u32,
},
SockFilterInsn {
code: BPF_RET | BPF_K,
jt: 0,
jf: 0,
k: SECCOMP_RET_ALLOW,
},
SockFilterInsn {
code: BPF_RET | BPF_K,
jt: 0,
jf: 0,
k: SECCOMP_RET_USER_NOTIF,
},
];
assert_eq!(filter.len(), 5);
}
#[test]
fn test_build_seccomp_block_network_filter() {
let filter = build_seccomp_block_network_filter();
assert_eq!(filter.len(), 10);
assert_eq!(filter[0].k, SECCOMP_DATA_NR_OFFSET);
assert_eq!(filter[1].k, SYS_SOCKET as u32);
assert_eq!(filter[1].jt, 4);
assert_eq!(filter[2].k, SYS_SOCKETPAIR as u32);
assert_eq!(filter[2].jt, 3);
assert_eq!(filter[3].k, SYS_IO_URING_SETUP as u32);
assert_eq!(filter[3].jt, 1);
assert_eq!(filter[4].k, SECCOMP_RET_ALLOW);
assert_eq!(filter[5].k, SECCOMP_RET_ERRNO | (libc::EPERM as u32));
assert_eq!(filter[6].k, SECCOMP_DATA_ARG0_OFFSET);
assert_eq!(filter[7].k, libc::AF_UNIX as u32);
assert_eq!(filter[7].jt, 1);
assert_eq!(filter[8].k, SECCOMP_RET_ERRNO | (libc::EPERM as u32));
assert_eq!(filter[9].k, SECCOMP_RET_ALLOW);
}
#[test]
fn test_open_how_struct_size() {
use std::mem;
assert_eq!(mem::size_of::<OpenHow>(), 24);
}
#[test]
fn test_syscall_numbers_distinct() {
assert_ne!(SYS_OPENAT, SYS_OPENAT2);
}
#[test]
fn test_syscall_numbers_match_seccomp_data_nr_type() {
let _: i32 = SYS_OPENAT;
let _: i32 = SYS_OPENAT2;
}
#[test]
fn test_classify_access_rdonly() {
let access = classify_access_from_flags(libc::O_RDONLY);
assert!(matches!(access, crate::AccessMode::Read));
}
#[test]
fn test_classify_access_wronly() {
let access = classify_access_from_flags(libc::O_WRONLY);
assert!(matches!(access, crate::AccessMode::Write));
}
#[test]
fn test_classify_access_rdwr() {
let access = classify_access_from_flags(libc::O_RDWR);
assert!(matches!(access, crate::AccessMode::ReadWrite));
}
#[test]
fn test_classify_access_with_extra_flags() {
let flags = libc::O_RDONLY | libc::O_CREAT | libc::O_TRUNC;
let access = classify_access_from_flags(flags);
assert!(matches!(access, crate::AccessMode::Read));
let flags = libc::O_WRONLY | libc::O_APPEND;
let access = classify_access_from_flags(flags);
assert!(matches!(access, crate::AccessMode::Write));
let flags = libc::O_RDWR | libc::O_CLOEXEC;
let access = classify_access_from_flags(flags);
assert!(matches!(access, crate::AccessMode::ReadWrite));
}
#[test]
fn test_classify_access_pointer_as_flags_gives_readwrite() {
let fake_pointer = 0x7fff_1234_5678_i64 as i32; let access = classify_access_from_flags(fake_pointer);
let _ = access; }
#[test]
fn test_validate_openat2_size_rejects_zero() {
assert!(!validate_openat2_size(0));
}
#[test]
fn test_validate_openat2_size_rejects_undersized() {
assert!(!validate_openat2_size(1));
assert!(!validate_openat2_size(8));
assert!(!validate_openat2_size(16));
assert!(!validate_openat2_size(23));
}
#[test]
fn test_validate_openat2_size_accepts_exact() {
let exact_size = std::mem::size_of::<OpenHow>();
assert_eq!(exact_size, 24);
assert!(validate_openat2_size(exact_size));
}
#[test]
fn test_validate_openat2_size_accepts_larger() {
assert!(validate_openat2_size(32));
assert!(validate_openat2_size(64));
assert!(validate_openat2_size(128));
}
#[test]
fn test_validate_openat2_size_rejects_unreasonably_large() {
assert!(!validate_openat2_size(4097));
assert!(!validate_openat2_size(usize::MAX));
}
#[test]
fn test_resolve_notif_path_absolute_unchanged() {
let abs_path = std::path::PathBuf::from("/usr/lib/libc.so.6");
let result = resolve_notif_path(1, 42, &abs_path);
let path = match result {
Ok(p) => p,
Err(e) => panic!("unexpected error: {e}"),
};
assert_eq!(path, abs_path);
}
#[test]
fn test_resolve_notif_path_absolute_with_at_fdcwd() {
let abs_path = std::path::PathBuf::from("/etc/passwd");
let at_fdcwd = libc::AT_FDCWD as i64 as u64;
let path = match resolve_notif_path(1, at_fdcwd, &abs_path) {
Ok(p) => p,
Err(e) => panic!("unexpected error: {e}"),
};
assert_eq!(path, abs_path);
}
#[test]
fn test_resolve_notif_path_relative_with_invalid_pid_fails() {
let rel_path = std::path::PathBuf::from("relative/path.so");
let at_fdcwd = libc::AT_FDCWD as i64 as u64;
let result = resolve_notif_path(u32::MAX, at_fdcwd, &rel_path);
assert!(result.is_err());
}
#[test]
fn test_resolve_notif_path_relative_with_invalid_fd_fails() {
let rel_path = std::path::PathBuf::from("some_lib.so");
let result = resolve_notif_path(u32::MAX, 999, &rel_path);
assert!(result.is_err());
}
#[test]
fn test_resolve_notif_path_at_fdcwd_both_representations() {
let abs_path = std::path::PathBuf::from("/absolute");
#[allow(clippy::unnecessary_cast)]
let at_fdcwd_32 = libc::AT_FDCWD as i32 as u32 as u64; let at_fdcwd_64 = libc::AT_FDCWD as i64 as u64;
let path_32 = match resolve_notif_path(1, at_fdcwd_32, &abs_path) {
Ok(p) => p,
Err(e) => panic!("unexpected error for 32-bit AT_FDCWD: {e}"),
};
assert_eq!(path_32, abs_path);
let path_64 = match resolve_notif_path(1, at_fdcwd_64, &abs_path) {
Ok(p) => p,
Err(e) => panic!("unexpected error for 64-bit AT_FDCWD: {e}"),
};
assert_eq!(path_64, abs_path);
}
#[test]
fn test_seccomp_network_fallback_mode_blocked() {
let caps = CapabilitySet::new().block_network();
assert_eq!(
seccomp_network_fallback_mode(&caps),
SeccompNetFallback::BlockAll
);
}
#[test]
fn test_seccomp_network_fallback_mode_blocked_with_ports_is_none() {
let mut caps = CapabilitySet::new().block_network();
caps.add_localhost_port(3000);
assert_eq!(
seccomp_network_fallback_mode(&caps),
SeccompNetFallback::None
);
}
#[test]
fn test_seccomp_network_fallback_mode_proxy_only() {
let caps = CapabilitySet::new().proxy_only(8080);
assert_eq!(
seccomp_network_fallback_mode(&caps),
SeccompNetFallback::ProxyOnly {
proxy_port: 8080,
bind_ports: vec![],
}
);
}
#[test]
fn test_seccomp_network_fallback_mode_proxy_only_with_bind() {
let caps = CapabilitySet::new().proxy_only_with_bind(8080, vec![3000, 3001]);
assert_eq!(
seccomp_network_fallback_mode(&caps),
SeccompNetFallback::ProxyOnly {
proxy_port: 8080,
bind_ports: vec![3000, 3001],
}
);
}
#[test]
fn test_seccomp_network_fallback_mode_allow_all() {
let caps = CapabilitySet::new();
assert_eq!(
seccomp_network_fallback_mode(&caps),
SeccompNetFallback::None
);
}
#[test]
fn test_legacy_can_use_seccomp_block_fallback() {
assert!(can_use_seccomp_network_block_fallback(
&CapabilitySet::new().block_network()
));
let with_proxy = CapabilitySet::new().proxy_only(8080);
assert!(!can_use_seccomp_network_block_fallback(&with_proxy));
let mut with_localhost = CapabilitySet::new().block_network();
with_localhost.add_localhost_port(3000);
assert!(!can_use_seccomp_network_block_fallback(&with_localhost));
}
#[test]
fn test_build_seccomp_proxy_filter_with_bind() {
let filter = build_seccomp_proxy_filter(true);
assert_eq!(filter.len(), 19);
assert_eq!(filter[0].code, BPF_LD | BPF_W | BPF_ABS);
assert_eq!(filter[0].k, SECCOMP_DATA_NR_OFFSET);
assert_eq!(filter[16].code, BPF_RET | BPF_K);
assert_eq!(filter[16].k, SECCOMP_RET_USER_NOTIF);
assert_eq!(filter[17].code, BPF_RET | BPF_K);
assert_eq!(filter[17].k, SECCOMP_RET_USER_NOTIF);
}
#[test]
fn test_build_seccomp_proxy_filter_without_bind() {
let filter = build_seccomp_proxy_filter(false);
assert_eq!(filter.len(), 19);
assert_eq!(filter[17].code, BPF_RET | BPF_K);
assert_eq!(
filter[17].k, SECCOMP_RET_USER_NOTIF,
"bind must route to USER_NOTIF regardless of has_bind_ports so \
the supervisor can permit AF_UNIX pathname bind (#685)"
);
}
#[test]
fn test_sockaddr_info_ipv4_loopback() {
let info = SockaddrInfo {
family: libc::AF_INET as u16,
port: 8080,
is_loopback: true,
unix_kind: None,
};
assert!(info.is_loopback);
assert_eq!(info.port, 8080);
}
#[test]
fn test_sockaddr_info_ipv6_loopback() {
let info = SockaddrInfo {
family: libc::AF_INET6 as u16,
port: 443,
is_loopback: true,
unix_kind: None,
};
assert!(info.is_loopback);
}
#[test]
fn test_sockaddr_info_non_loopback() {
let info = SockaddrInfo {
family: libc::AF_INET as u16,
port: 80,
is_loopback: false,
unix_kind: None,
};
assert!(!info.is_loopback);
}
#[test]
fn test_sockaddr_info_unix_is_loopback() {
let info = SockaddrInfo {
family: libc::AF_UNIX as u16,
port: 0,
is_loopback: true,
unix_kind: Some(UnixSocketKind::Pathname),
};
assert!(info.is_loopback);
assert_eq!(info.port, 0);
}
#[test]
fn test_classify_af_unix_pathname() {
assert_eq!(
classify_af_unix(14, Some(b'/')),
UnixSocketKind::Pathname,
"non-null first byte => pathname"
);
}
#[test]
fn test_classify_af_unix_abstract() {
assert_eq!(
classify_af_unix(6, Some(0)),
UnixSocketKind::Abstract,
"null first byte => abstract namespace"
);
}
#[test]
fn test_classify_af_unix_unnamed() {
assert_eq!(
classify_af_unix(2, None),
UnixSocketKind::Unnamed,
"addrlen <= 2 => unnamed"
);
}
#[test]
fn test_classify_af_unix_fails_closed_on_short_read() {
assert_eq!(
classify_af_unix(10, None),
UnixSocketKind::Unnamed,
"missing sun_path byte => fail-closed to unnamed"
);
}
#[cfg(target_os = "linux")]
#[test]
fn test_seccomp_proxy_filter_allows_proxy_port_blocks_others() {
use std::io::Read;
let listener = match std::net::TcpListener::bind("127.0.0.1:0") {
Ok(l) => l,
Err(_) => return, };
let proxy_port = match listener.local_addr() {
Ok(addr) => addr.port(),
Err(_) => return,
};
let mut report_pipe = [0i32; 2];
let pipe_result = unsafe { libc::pipe(report_pipe.as_mut_ptr()) };
assert_eq!(pipe_result, 0, "pipe() failed");
let pid = unsafe { libc::fork() };
assert!(pid >= 0, "fork() failed");
if pid == 0 {
unsafe { libc::close(report_pipe[0]) };
drop(listener);
let result = install_seccomp_proxy_filter(false);
if result.is_err() {
let payload: [u8; 3] = [2, 2, 2]; unsafe {
libc::write(report_pipe[1], payload.as_ptr().cast(), payload.len());
libc::close(report_pipe[1]);
libc::_exit(0);
}
}
let notify_fd = result.expect("install_seccomp_proxy_filter failed");
let notify_raw = {
use std::os::fd::AsRawFd;
notify_fd.as_raw_fd()
};
let proxy_port_copy = proxy_port;
let handler = std::thread::spawn(move || {
for _ in 0..2 {
let notif = match recv_notif(notify_raw) {
Ok(n) => n,
Err(_) => break,
};
let info = match read_notif_sockaddr(
notif.pid,
notif.data.args[1],
notif.data.args[2],
) {
Ok(i) => i,
Err(_) => {
let _ = deny_notif(notify_raw, notif.id);
continue;
}
};
if info.is_loopback && info.port == proxy_port_copy {
let _ = continue_notif(notify_raw, notif.id);
} else {
let _ = respond_notif_errno(notify_raw, notif.id, libc::EACCES);
}
}
});
let sock1 = unsafe { libc::socket(libc::AF_INET, libc::SOCK_STREAM, 0) };
let mut addr1: libc::sockaddr_in = unsafe { std::mem::zeroed() };
addr1.sin_family = libc::AF_INET as u16;
addr1.sin_port = proxy_port.to_be();
addr1.sin_addr.s_addr = u32::from_be_bytes([127, 0, 0, 1]).to_be();
let connect1 = unsafe {
libc::connect(
sock1,
(&addr1 as *const libc::sockaddr_in).cast(),
std::mem::size_of::<libc::sockaddr_in>() as u32,
)
};
let errno1 = if connect1 < 0 {
std::io::Error::last_os_error().raw_os_error().unwrap_or(-1)
} else {
0
};
unsafe { libc::close(sock1) };
let sock2 = unsafe { libc::socket(libc::AF_INET, libc::SOCK_STREAM, 0) };
let mut addr2: libc::sockaddr_in = unsafe { std::mem::zeroed() };
addr2.sin_family = libc::AF_INET as u16;
addr2.sin_port = (proxy_port.wrapping_add(1)).to_be();
addr2.sin_addr.s_addr = u32::from_be_bytes([127, 0, 0, 1]).to_be();
let connect2 = unsafe {
libc::connect(
sock2,
(&addr2 as *const libc::sockaddr_in).cast(),
std::mem::size_of::<libc::sockaddr_in>() as u32,
)
};
let errno2 = if connect2 < 0 {
std::io::Error::last_os_error().raw_os_error().unwrap_or(-1)
} else {
0
};
unsafe { libc::close(sock2) };
handler.join().ok();
let payload: [u8; 3] = [
if connect1 == 0 { 0 } else { 1 },
errno1 as u8,
errno2 as u8,
];
unsafe {
libc::write(report_pipe[1], payload.as_ptr().cast(), payload.len());
libc::close(report_pipe[1]);
libc::_exit(0);
}
}
unsafe { libc::close(report_pipe[1]) };
let mut status = 0;
unsafe { libc::waitpid(pid, &mut status, 0) };
let mut buf = [0u8; 3];
let mut pipe_read = unsafe {
use std::os::fd::FromRawFd;
std::fs::File::from_raw_fd(report_pipe[0])
};
let n = pipe_read.read(&mut buf).expect("read from pipe failed");
if n == 3 && buf[0] == 2 && buf[1] == 2 && buf[2] == 2 {
return;
}
assert_eq!(n, 3, "expected 3 bytes from child, got {n}");
assert_eq!(
buf[0], 0,
"connect to proxy port should succeed, got result={} errno={}",
buf[0], buf[1]
);
assert_eq!(
buf[2],
libc::EACCES as u8,
"connect to non-proxy port should get EACCES, got errno={}",
buf[2]
);
}
#[cfg(target_os = "linux")]
#[test]
fn test_seccomp_proxy_filter_allows_af_unix_bind_without_bind_ports() {
use std::io::Read;
let sock_path = format!("/tmp/nono-integ-af-unix-bind-{}.sock", std::process::id());
let _ = std::fs::remove_file(&sock_path);
let mut report_pipe = [0i32; 2];
assert_eq!(unsafe { libc::pipe(report_pipe.as_mut_ptr()) }, 0, "pipe()");
let pid = unsafe { libc::fork() };
assert!(pid >= 0, "fork() failed");
if pid == 0 {
unsafe { libc::close(report_pipe[0]) };
let notify_fd = match install_seccomp_proxy_filter(false) {
Ok(fd) => fd,
Err(_) => {
let sentinel: [u8; 2] = [2, 2];
unsafe {
libc::write(report_pipe[1], sentinel.as_ptr().cast(), sentinel.len());
libc::close(report_pipe[1]);
libc::_exit(0);
}
}
};
let notify_raw = {
use std::os::fd::AsRawFd;
notify_fd.as_raw_fd()
};
let handler = std::thread::spawn(move || {
for _ in 0..1 {
let notif = match recv_notif(notify_raw) {
Ok(n) => n,
Err(_) => break,
};
let info = match read_notif_sockaddr(
notif.pid,
notif.data.args[1],
notif.data.args[2],
) {
Ok(i) => i,
Err(_) => {
let _ = deny_notif(notify_raw, notif.id);
continue;
}
};
let is_pathname_unix = info.family == libc::AF_UNIX as u16
&& matches!(info.unix_kind, Some(UnixSocketKind::Pathname));
if is_pathname_unix {
let _ = continue_notif(notify_raw, notif.id);
} else {
let _ = respond_notif_errno(notify_raw, notif.id, libc::EACCES);
}
}
});
let sock = unsafe { libc::socket(libc::AF_UNIX, libc::SOCK_STREAM, 0) };
let mut addr: libc::sockaddr_un = unsafe { std::mem::zeroed() };
addr.sun_family = libc::AF_UNIX as u16;
let bytes = sock_path.as_bytes();
assert!(bytes.len() < addr.sun_path.len(), "test path too long");
for (i, &b) in bytes.iter().enumerate() {
addr.sun_path[i] = b as libc::c_char;
}
let addrlen = (std::mem::size_of::<u16>() + bytes.len() + 1) as libc::socklen_t;
let rc =
unsafe { libc::bind(sock, (&addr as *const libc::sockaddr_un).cast(), addrlen) };
let errno = if rc < 0 {
std::io::Error::last_os_error().raw_os_error().unwrap_or(-1)
} else {
0
};
unsafe { libc::close(sock) };
let _ = handler.join();
let _ = std::fs::remove_file(&sock_path);
let payload: [u8; 2] = [
if rc < 0 { 1 } else { 0 },
errno.unsigned_abs().min(255) as u8,
];
unsafe {
libc::write(report_pipe[1], payload.as_ptr().cast(), payload.len());
libc::close(report_pipe[1]);
libc::_exit(0);
}
}
unsafe { libc::close(report_pipe[1]) };
let mut status: libc::c_int = 0;
unsafe { libc::waitpid(pid, &mut status, 0) };
use std::os::fd::FromRawFd;
let mut pipe_read = unsafe { std::fs::File::from_raw_fd(report_pipe[0]) };
let mut buf = [0u8; 2];
let n = pipe_read.read(&mut buf).expect("read from pipe");
if n == 2 && buf[0] == 2 && buf[1] == 2 {
return;
}
assert_eq!(n, 2, "expected 2 bytes from child, got {n}");
assert_eq!(
buf[0], 0,
"AF_UNIX bind must succeed under proxy filter with \
has_bind_ports=false (errno={})",
buf[1]
);
}
#[cfg(target_os = "linux")]
#[test]
fn test_proxy_only_with_landlock_v4_returns_no_fallback() {
let detected = match detect_abi() {
Ok(d) => d,
Err(_) => return,
};
if !detected.has_network() {
return;
}
let pid = unsafe { libc::fork() };
assert!(pid >= 0, "fork() failed");
if pid == 0 {
let caps = CapabilitySet::new().proxy_only(8080);
let exit_code = match apply_with_abi(&caps, &detected) {
Ok(SeccompNetFallback::None) => 0,
Ok(SeccompNetFallback::BlockAll) => 1,
Ok(SeccompNetFallback::ProxyOnly { .. }) => 2,
Err(_) => 3,
};
unsafe { libc::_exit(exit_code) };
}
let mut status = 0;
unsafe { libc::waitpid(pid, &mut status, 0) };
assert!(libc::WIFEXITED(status));
assert_eq!(
libc::WEXITSTATUS(status),
0,
"Expected SeccompNetFallback::None on V4+ kernel, got exit code {}",
libc::WEXITSTATUS(status)
);
}
#[cfg(target_os = "linux")]
#[test]
fn test_proxy_only_without_landlock_net_returns_proxy_fallback() {
let detected = match detect_abi() {
Ok(d) => d,
Err(_) => return,
};
if detected.has_network() {
return;
}
let pid = unsafe { libc::fork() };
assert!(pid >= 0, "fork() failed");
if pid == 0 {
let caps = CapabilitySet::new().proxy_only(8080);
let exit_code = match apply_with_abi(&caps, &detected) {
Ok(SeccompNetFallback::ProxyOnly {
proxy_port: 8080, ..
}) => 0,
Ok(SeccompNetFallback::ProxyOnly { .. }) => 1, Ok(SeccompNetFallback::None) => 2,
Ok(SeccompNetFallback::BlockAll) => 3,
Err(_) => 4,
};
unsafe { libc::_exit(exit_code) };
}
let mut status = 0;
unsafe { libc::waitpid(pid, &mut status, 0) };
assert!(libc::WIFEXITED(status));
assert_eq!(
libc::WEXITSTATUS(status),
0,
"Expected SeccompNetFallback::ProxyOnly on pre-V4 kernel, got exit code {}",
libc::WEXITSTATUS(status)
);
}
#[cfg(target_os = "linux")]
#[test]
fn test_seccomp_proxy_filter_blocks_bind_without_bind_ports() {
let mut report_pipe = [0i32; 2];
let pipe_result = unsafe { libc::pipe(report_pipe.as_mut_ptr()) };
assert_eq!(pipe_result, 0, "pipe() failed");
let pid = unsafe { libc::fork() };
assert!(pid >= 0, "fork() failed");
if pid == 0 {
unsafe { libc::close(report_pipe[0]) };
let notify_fd = match install_seccomp_proxy_filter(false) {
Ok(fd) => fd,
Err(_) => {
let skip: u8 = 255;
unsafe {
libc::write(report_pipe[1], &skip as *const u8 as _, 1);
libc::close(report_pipe[1]);
libc::_exit(0);
}
}
};
let notify_raw = {
use std::os::fd::AsRawFd;
notify_fd.as_raw_fd()
};
let handler = std::thread::spawn(move || {
for _ in 0..1 {
let notif = match recv_notif(notify_raw) {
Ok(n) => n,
Err(_) => break,
};
let _ = respond_notif_errno(notify_raw, notif.id, libc::EACCES);
}
});
let sock = unsafe { libc::socket(libc::AF_INET, libc::SOCK_STREAM, 0) };
let mut addr: libc::sockaddr_in = unsafe { std::mem::zeroed() };
addr.sin_family = libc::AF_INET as u16;
addr.sin_port = 0;
addr.sin_addr.s_addr = u32::from_be_bytes([127, 0, 0, 1]).to_be();
let bind_result = unsafe {
libc::bind(
sock,
(&addr as *const libc::sockaddr_in).cast(),
std::mem::size_of::<libc::sockaddr_in>() as u32,
)
};
let errno = if bind_result < 0 {
std::io::Error::last_os_error().raw_os_error().unwrap_or(-1) as u8
} else {
0
};
unsafe { libc::close(sock) };
let _ = handler.join();
unsafe {
libc::write(report_pipe[1], &errno as *const u8 as _, 1);
libc::close(report_pipe[1]);
libc::_exit(0);
}
}
unsafe { libc::close(report_pipe[1]) };
let mut status = 0;
unsafe { libc::waitpid(pid, &mut status, 0) };
let mut buf = [0u8; 1];
let mut pipe_read = unsafe {
use std::os::fd::FromRawFd;
std::fs::File::from_raw_fd(report_pipe[0])
};
use std::io::Read as _;
let n = pipe_read.read(&mut buf).expect("read from pipe");
if n == 1 && buf[0] == 255 {
return; }
assert_eq!(n, 1);
assert_eq!(
buf[0],
libc::EACCES as u8,
"AF_INET bind() must still receive EACCES (from supervisor, not filter) \
when has_bind_ports=false, got errno={}",
buf[0]
);
}
#[test]
fn test_is_wsl2_does_not_panic() {
let _ = is_wsl2();
}
#[test]
fn test_is_wsl2_consistent() {
let first = is_wsl2();
let second = is_wsl2();
assert_eq!(first, second, "is_wsl2() must return consistent results");
}
#[test]
fn test_detect_wsl2_matches_indicators() {
let has_interop = std::path::Path::new("/proc/sys/fs/binfmt_misc/WSLInterop").exists();
let has_kernel_string = std::fs::read_to_string("/proc/version")
.map(|v| v.contains("microsoft") || v.contains("WSL"))
.unwrap_or(false);
if has_interop || has_kernel_string {
assert!(
is_wsl2(),
"Kernel-controlled WSL2 indicators present but is_wsl2() returned false"
);
}
}
#[test]
fn test_wsl2_landlock_available() {
if is_wsl2() || is_supported() {
assert!(
is_supported(),
"Landlock must be available when WSL2 or native Linux"
);
}
}
}