use super::{CommandSpec, SandboxPolicy};
use std::ffi::CString;
use std::path::Path;
pub fn is_available() -> bool {
#[cfg(target_os = "linux")]
{
unsafe {
let result = libc::syscall(
libc::SYS_landlock_create_ruleset,
std::ptr::null::<libc::c_void>(),
0usize,
LANDLOCK_CREATE_RULESET_VERSION,
);
result >= 0
}
}
#[cfg(not(target_os = "linux"))]
{
false
}
}
#[cfg(target_os = "linux")]
pub fn get_abi_version() -> Option<i32> {
unsafe {
let result = libc::syscall(
libc::SYS_landlock_create_ruleset,
std::ptr::null::<libc::c_void>(),
0usize,
LANDLOCK_CREATE_RULESET_VERSION,
);
if result >= 0 {
i32::try_from(result).ok()
} else {
None
}
}
}
#[cfg(target_os = "linux")]
const LANDLOCK_CREATE_RULESET_VERSION: u32 = 1 << 0;
#[cfg(target_os = "linux")]
const LANDLOCK_ACCESS_FS_EXECUTE: u64 = 1 << 0;
#[cfg(target_os = "linux")]
const LANDLOCK_ACCESS_FS_WRITE_FILE: u64 = 1 << 1;
#[cfg(target_os = "linux")]
const LANDLOCK_ACCESS_FS_READ_FILE: u64 = 1 << 2;
#[cfg(target_os = "linux")]
const LANDLOCK_ACCESS_FS_READ_DIR: u64 = 1 << 3;
#[cfg(target_os = "linux")]
const LANDLOCK_ACCESS_FS_REMOVE_DIR: u64 = 1 << 4;
#[cfg(target_os = "linux")]
const LANDLOCK_ACCESS_FS_REMOVE_FILE: u64 = 1 << 5;
#[cfg(target_os = "linux")]
const LANDLOCK_ACCESS_FS_MAKE_CHAR: u64 = 1 << 6;
#[cfg(target_os = "linux")]
const LANDLOCK_ACCESS_FS_MAKE_DIR: u64 = 1 << 7;
#[cfg(target_os = "linux")]
const LANDLOCK_ACCESS_FS_MAKE_REG: u64 = 1 << 8;
#[cfg(target_os = "linux")]
const LANDLOCK_ACCESS_FS_MAKE_SOCK: u64 = 1 << 9;
#[cfg(target_os = "linux")]
const LANDLOCK_ACCESS_FS_MAKE_FIFO: u64 = 1 << 10;
#[cfg(target_os = "linux")]
const LANDLOCK_ACCESS_FS_MAKE_BLOCK: u64 = 1 << 11;
#[cfg(target_os = "linux")]
const LANDLOCK_ACCESS_FS_MAKE_SYM: u64 = 1 << 12;
#[cfg(target_os = "linux")]
const LANDLOCK_ACCESS_FS_REFER: u64 = 1 << 13;
#[cfg(target_os = "linux")]
const LANDLOCK_ACCESS_FS_TRUNCATE: u64 = 1 << 14;
#[cfg(target_os = "linux")]
const LANDLOCK_ACCESS_FS_READ: u64 = LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR;
#[cfg(target_os = "linux")]
const LANDLOCK_ACCESS_FS_WRITE: u64 = LANDLOCK_ACCESS_FS_WRITE_FILE
| LANDLOCK_ACCESS_FS_REMOVE_DIR
| LANDLOCK_ACCESS_FS_REMOVE_FILE
| LANDLOCK_ACCESS_FS_MAKE_DIR
| LANDLOCK_ACCESS_FS_MAKE_REG
| LANDLOCK_ACCESS_FS_MAKE_SYM
| LANDLOCK_ACCESS_FS_TRUNCATE;
#[cfg(target_os = "linux")]
#[repr(C)]
struct LandlockRulesetAttr {
handled_access_fs: u64,
}
#[cfg(target_os = "linux")]
#[repr(C)]
struct LandlockPathBeneathAttr {
allowed_access: u64,
parent_fd: i32,
}
#[cfg(target_os = "linux")]
const LANDLOCK_RULE_PATH_BENEATH: u32 = 1;
#[cfg(target_os = "linux")]
pub struct LandlockSandbox {
ruleset_fd: i32,
policy: SandboxPolicy,
}
#[cfg(target_os = "linux")]
impl LandlockSandbox {
pub fn from_policy(policy: &SandboxPolicy) -> std::io::Result<Self> {
let handled_access =
LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_READ | LANDLOCK_ACCESS_FS_WRITE;
let attr = LandlockRulesetAttr {
handled_access_fs: handled_access,
};
let ruleset_fd = unsafe {
libc::syscall(
libc::SYS_landlock_create_ruleset,
&raw const attr,
std::mem::size_of::<LandlockRulesetAttr>(),
0u32,
)
};
if ruleset_fd < 0 {
return Err(std::io::Error::last_os_error());
}
let ruleset_fd = i32::try_from(ruleset_fd).map_err(|_| {
std::io::Error::other("Failed to create Landlock ruleset: file descriptor out of range")
})?;
Ok(Self {
ruleset_fd,
policy: policy.clone(),
})
}
pub fn allow_read(&self, path: &Path) -> std::io::Result<()> {
self.add_rule(path, LANDLOCK_ACCESS_FS_READ | LANDLOCK_ACCESS_FS_EXECUTE)
}
pub fn allow_write(&self, path: &Path) -> std::io::Result<()> {
self.add_rule(
path,
LANDLOCK_ACCESS_FS_READ | LANDLOCK_ACCESS_FS_WRITE | LANDLOCK_ACCESS_FS_EXECUTE,
)
}
fn add_rule(&self, path: &Path, access: u64) -> std::io::Result<()> {
let path_cstr = CString::new(path.to_string_lossy().as_bytes())
.map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid path"))?;
let fd = unsafe { libc::open(path_cstr.as_ptr(), libc::O_PATH | libc::O_CLOEXEC) };
if fd < 0 {
return Ok(());
}
let attr = LandlockPathBeneathAttr {
allowed_access: access,
parent_fd: fd,
};
let result = unsafe {
libc::syscall(
libc::SYS_landlock_add_rule,
self.ruleset_fd,
LANDLOCK_RULE_PATH_BENEATH,
&raw const attr,
0u32,
)
};
unsafe {
libc::close(fd);
}
if result < 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
}
pub fn apply(&self) -> std::io::Result<()> {
let result = unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) };
if result < 0 {
return Err(std::io::Error::last_os_error());
}
let result =
unsafe { libc::syscall(libc::SYS_landlock_restrict_self, self.ruleset_fd, 0u32) };
if result < 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
}
}
#[cfg(target_os = "linux")]
impl Drop for LandlockSandbox {
fn drop(&mut self) {
unsafe {
libc::close(self.ruleset_fd);
}
}
}
#[cfg(target_os = "linux")]
pub fn create_landlock_wrapper(
spec: &CommandSpec,
_writable_paths: &[std::path::PathBuf],
_readable_paths: &[std::path::PathBuf],
) -> Vec<String> {
let mut cmd = vec![spec.program.clone()];
cmd.extend(spec.args.clone());
cmd
}
#[cfg(target_os = "linux")]
pub fn detect_denial(exit_code: i32, stderr: &str) -> bool {
if exit_code == 0 {
return false;
}
stderr.contains("Permission denied")
|| stderr.contains("Operation not permitted")
|| stderr.contains("EACCES")
|| stderr.contains("EPERM")
}
#[cfg(not(target_os = "linux"))]
pub fn get_abi_version() -> Option<i32> {
None
}
#[cfg(not(target_os = "linux"))]
pub fn detect_denial(_exit_code: i32, _stderr: &str) -> bool {
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_available() {
let _ = is_available();
}
#[test]
#[cfg(target_os = "linux")]
fn test_get_abi_version() {
let _ = get_abi_version();
}
#[test]
fn test_detect_denial() {
#[cfg(target_os = "linux")]
{
assert!(detect_denial(1, "Permission denied"));
assert!(detect_denial(1, "Operation not permitted"));
assert!(!detect_denial(0, "Success"));
}
}
}