use crate::jailer::error::IsolationError;
use crate::jailer::sandbox::PathAccess;
use boxlite_shared::errors::BoxliteError;
use landlock::{
ABI, Access, AccessFs, AccessNet, CompatLevel, Compatible, PathBeneath, PathFd, Ruleset,
RulesetAttr, RulesetCreatedAttr, RulesetError,
};
use std::os::fd::{IntoRawFd, RawFd};
const TARGET_ABI: ABI = ABI::V5;
const SYSTEM_READ_PATHS: &[&str] = &[
"/usr", "/lib", "/lib64", "/bin", "/sbin", "/etc", "/proc", "/dev",
];
const SYSTEM_WRITE_PATHS: &[&str] = &["/tmp"];
pub fn build_landlock_ruleset(
paths: &[PathAccess],
network_enabled: bool,
) -> Result<Option<RawFd>, BoxliteError> {
let mut ruleset = Ruleset::default()
.set_compatibility(CompatLevel::BestEffort)
.handle_access(AccessFs::from_all(TARGET_ABI))
.map_err(|e| map_landlock_error("handle filesystem access", e))?;
if !network_enabled {
ruleset = ruleset
.handle_access(AccessNet::from_all(TARGET_ABI))
.map_err(|e| map_landlock_error("handle network access", e))?;
}
let mut ruleset_created = ruleset
.create()
.map_err(|e| map_landlock_error("create ruleset", e))?
.set_compatibility(CompatLevel::BestEffort);
let read_access = AccessFs::from_read(TARGET_ABI);
for path in SYSTEM_READ_PATHS {
if let Ok(path_fd) = PathFd::new(path) {
ruleset_created = ruleset_created
.add_rule(PathBeneath::new(path_fd, read_access))
.map_err(|e| map_landlock_error(&format!("add rule for {path}"), e))?;
}
}
let all_access = AccessFs::from_all(TARGET_ABI);
for path in SYSTEM_WRITE_PATHS {
if let Ok(path_fd) = PathFd::new(path) {
ruleset_created = ruleset_created
.add_rule(PathBeneath::new(path_fd, all_access))
.map_err(|e| map_landlock_error(&format!("add rule for {path}"), e))?;
}
}
for pa in paths {
let real_path = pa.path.canonicalize().unwrap_or_else(|_| pa.path.clone());
let path_fd = match PathFd::new(&real_path) {
Ok(fd) => fd,
Err(_) => {
continue;
}
};
let access = if pa.writable {
AccessFs::from_all(TARGET_ABI)
} else {
AccessFs::from_read(TARGET_ABI)
};
ruleset_created = ruleset_created
.add_rule(PathBeneath::new(path_fd, access))
.map_err(|e| map_landlock_error(&format!("add rule for {}", real_path.display()), e))?;
}
let owned_fd: Option<std::os::fd::OwnedFd> = ruleset_created.into();
match owned_fd {
Some(fd) => {
let raw_fd = fd.into_raw_fd();
Ok(Some(raw_fd))
}
None => {
Ok(None)
}
}
}
pub unsafe fn restrict_self_raw(ruleset_fd: RawFd) -> i32 {
let ret = unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) };
if ret != 0 {
let errno = unsafe { *libc::__errno_location() };
unsafe { libc::close(ruleset_fd) };
return errno;
}
let ret = unsafe {
libc::syscall(
libc::SYS_landlock_restrict_self,
ruleset_fd as libc::c_long,
0i64,
)
};
let errno = if ret != 0 {
unsafe { *libc::__errno_location() }
} else {
0
};
unsafe { libc::close(ruleset_fd) };
errno
}
pub fn is_landlock_available() -> bool {
Ruleset::default()
.handle_access(AccessFs::Execute)
.and_then(|r| r.create())
.is_ok()
}
fn map_landlock_error(context: &str, err: RulesetError) -> BoxliteError {
BoxliteError::from(crate::jailer::error::JailerError::Isolation(
IsolationError::Landlock(format!("{context}: {err}")),
))
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_is_landlock_available() {
let available = is_landlock_available();
println!("Landlock available: {available}");
}
#[test]
fn test_build_ruleset_empty_paths() {
let result = build_landlock_ruleset(&[], false);
assert!(result.is_ok());
}
#[test]
fn test_build_ruleset_with_paths() {
let paths = vec![
PathAccess {
path: PathBuf::from("/tmp"),
writable: true,
},
PathAccess {
path: PathBuf::from("/usr"),
writable: false,
},
];
let result = build_landlock_ruleset(&paths, false);
assert!(result.is_ok());
}
#[test]
fn test_build_ruleset_nonexistent_path_skipped() {
let paths = vec![PathAccess {
path: PathBuf::from("/this/path/does/not/exist"),
writable: false,
}];
let result = build_landlock_ruleset(&paths, false);
assert!(
result.is_ok(),
"Nonexistent paths should be silently skipped"
);
}
#[test]
fn test_build_ruleset_network_disabled_denies_all() {
let result = build_landlock_ruleset(&[], false);
assert!(result.is_ok());
}
#[test]
fn test_build_ruleset_network_enabled_permits_all() {
let result = build_landlock_ruleset(&[], true);
assert!(result.is_ok());
}
#[test]
fn test_landlock_enforcement_e2e() {
let tmp = tempfile::tempdir().expect("create tempdir");
let allowed_file = tmp.path().join("allowed.txt");
std::fs::write(&allowed_file, b"hello").expect("write allowed file");
let paths = vec![PathAccess {
path: tmp.path().to_path_buf(),
writable: true,
}];
let result = build_landlock_ruleset(&paths, false);
let Ok(Some(fd)) = result else {
println!("Landlock not available, skipping enforcement test");
return;
};
let allowed_path = allowed_file.clone();
let handle = std::thread::spawn(move || {
let errno = unsafe { restrict_self_raw(fd) };
assert_eq!(errno, 0, "restrict_self_raw failed with errno {errno}");
let content = std::fs::read_to_string(&allowed_path);
assert!(
content.is_ok(),
"Should be able to read allowed file, got: {:?}",
content.err()
);
assert_eq!(content.unwrap(), "hello");
let write_result =
std::fs::write(allowed_path.parent().unwrap().join("new.txt"), b"world");
assert!(
write_result.is_ok(),
"Should be able to write to allowed writable dir, got: {:?}",
write_result.err()
);
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
let denied = std::fs::read_dir(&home);
if !home.starts_with("/tmp") && !home.starts_with("/usr") {
assert!(
denied.is_err(),
"Reading home dir ({home}) should be denied by Landlock, but succeeded"
);
let err = denied.unwrap_err();
assert_eq!(
err.kind(),
std::io::ErrorKind::PermissionDenied,
"Expected EACCES, got: {err}"
);
}
});
handle.join().expect("enforcement thread panicked");
}
fn home_test_dir(name: &str) -> PathBuf {
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
let dir = PathBuf::from(home).join(".boxlite-test").join(name);
std::fs::create_dir_all(&dir).expect("create home test dir");
dir
}
fn cleanup_home_test_dir(name: &str) {
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
let dir = PathBuf::from(home).join(".boxlite-test").join(name);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn test_landlock_readonly_denies_write() {
let test_id = format!("ro-test-{}", std::process::id());
let ro_dir = home_test_dir(&format!("{test_id}/ro"));
let rw_dir = home_test_dir(&format!("{test_id}/rw"));
let ro_file = ro_dir.join("readonly.txt");
std::fs::write(&ro_file, b"read only").expect("write ro file");
let paths = vec![
PathAccess {
path: ro_dir.clone(),
writable: false,
},
PathAccess {
path: rw_dir.clone(),
writable: true,
},
];
let result = build_landlock_ruleset(&paths, false);
let Ok(Some(fd)) = result else {
println!("Landlock not available, skipping readonly test");
cleanup_home_test_dir(&test_id);
return;
};
let ro = ro_dir.clone();
let rw = rw_dir.clone();
let handle = std::thread::spawn(move || {
let errno = unsafe { restrict_self_raw(fd) };
assert_eq!(errno, 0, "restrict_self_raw failed");
let content = std::fs::read_to_string(&ro_file);
assert!(
content.is_ok(),
"Should read from ro dir: {:?}",
content.err()
);
assert_eq!(content.unwrap(), "read only");
let write_result = std::fs::write(ro.join("new.txt"), b"denied");
assert!(
write_result.is_err(),
"Writing to read-only dir should be denied"
);
assert_eq!(
write_result.unwrap_err().kind(),
std::io::ErrorKind::PermissionDenied,
"Expected EACCES for write to read-only dir"
);
let write_ok = std::fs::write(rw.join("allowed.txt"), b"ok");
assert!(
write_ok.is_ok(),
"Should write to rw dir: {:?}",
write_ok.err()
);
});
handle.join().expect("readonly test thread panicked");
cleanup_home_test_dir(&test_id);
}
#[test]
fn test_landlock_multiple_paths_enforcement() {
let test_id = format!("multi-test-{}", std::process::id());
let dir_a = home_test_dir(&format!("{test_id}/a"));
let dir_b = home_test_dir(&format!("{test_id}/b"));
let dir_c = home_test_dir(&format!("{test_id}/c"));
std::fs::write(dir_a.join("a.txt"), b"alpha").unwrap();
std::fs::write(dir_b.join("b.txt"), b"beta").unwrap();
let paths = vec![
PathAccess {
path: dir_a.clone(),
writable: false,
},
PathAccess {
path: dir_b.clone(),
writable: true,
},
];
let result = build_landlock_ruleset(&paths, false);
let Ok(Some(fd)) = result else {
println!("Landlock not available, skipping multi-path test");
cleanup_home_test_dir(&test_id);
return;
};
let a = dir_a.clone();
let b = dir_b.clone();
let c = dir_c.clone();
let handle = std::thread::spawn(move || {
let errno = unsafe { restrict_self_raw(fd) };
assert_eq!(errno, 0);
assert!(std::fs::read_to_string(a.join("a.txt")).is_ok());
assert!(std::fs::write(a.join("x.txt"), b"denied").is_err());
assert!(std::fs::read_to_string(b.join("b.txt")).is_ok());
assert!(std::fs::write(b.join("y.txt"), b"ok").is_ok());
let denied = std::fs::read_dir(&c);
assert!(denied.is_err(), "dir_c should be denied (not in ruleset)");
assert_eq!(
denied.unwrap_err().kind(),
std::io::ErrorKind::PermissionDenied
);
});
handle.join().expect("multi-path test thread panicked");
cleanup_home_test_dir(&test_id);
}
#[test]
fn test_landlock_network_deny() {
use std::net::TcpStream;
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind listener");
let port = listener.local_addr().unwrap().port();
let pre_check = TcpStream::connect(("127.0.0.1", port));
assert!(pre_check.is_ok(), "Pre-Landlock connect should work");
drop(pre_check);
let paths = vec![];
let result = build_landlock_ruleset(&paths, false); let Ok(Some(fd)) = result else {
println!("Landlock not available, skipping network test");
return;
};
let handle = std::thread::spawn(move || {
let errno = unsafe { restrict_self_raw(fd) };
assert_eq!(errno, 0);
let result = TcpStream::connect(("127.0.0.1", port));
match result {
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
}
Ok(_) => {
println!(
"TCP connect succeeded — kernel likely < 6.7 (no Landlock network support). \
This is expected graceful degradation."
);
}
Err(e) => {
panic!("Unexpected error kind: {e}");
}
}
});
handle.join().expect("network deny test thread panicked");
drop(listener);
}
}