zeroclaw/src/security/landlock.rs
2026-02-18 14:42:39 +08:00

261 lines
8.8 KiB
Rust

//! Landlock sandbox (Linux kernel 5.13+ LSM)
//!
//! Landlock provides unprivileged sandboxing through the Linux kernel.
//! This module uses the pure-Rust `landlock` crate for filesystem access control.
#[cfg(all(feature = "sandbox-landlock", target_os = "linux"))]
use landlock::{AccessFs, PathBeneath, PathFd, Ruleset, RulesetAttr, RulesetCreatedAttr};
use crate::security::traits::Sandbox;
use std::path::Path;
/// Landlock sandbox backend for Linux
#[cfg(all(feature = "sandbox-landlock", target_os = "linux"))]
#[derive(Debug)]
pub struct LandlockSandbox {
workspace_dir: Option<std::path::PathBuf>,
}
#[cfg(all(feature = "sandbox-landlock", target_os = "linux"))]
impl LandlockSandbox {
/// Create a new Landlock sandbox with the given workspace directory
pub fn new() -> std::io::Result<Self> {
Self::with_workspace(None)
}
/// Create a Landlock sandbox with a specific workspace directory
pub fn with_workspace(workspace_dir: Option<std::path::PathBuf>) -> std::io::Result<Self> {
// Test if Landlock is available by trying to create a minimal ruleset
let test_ruleset = Ruleset::default()
.handle_access(AccessFs::ReadFile | AccessFs::WriteFile)
.and_then(|ruleset| ruleset.create());
match test_ruleset {
Ok(_) => Ok(Self { workspace_dir }),
Err(e) => {
tracing::debug!("Landlock not available: {}", e);
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"Landlock not available",
))
}
}
}
/// Probe if Landlock is available (for auto-detection)
pub fn probe() -> std::io::Result<Self> {
Self::new()
}
/// Apply Landlock restrictions to the current process
fn apply_restrictions(&self) -> std::io::Result<()> {
let mut ruleset = Ruleset::default()
.handle_access(
AccessFs::ReadFile
| AccessFs::WriteFile
| AccessFs::ReadDir
| AccessFs::RemoveDir
| AccessFs::RemoveFile
| AccessFs::MakeChar
| AccessFs::MakeSock
| AccessFs::MakeFifo
| AccessFs::MakeBlock
| AccessFs::MakeReg
| AccessFs::MakeSym,
)
.and_then(|ruleset| ruleset.create())
.map_err(|e| std::io::Error::other(e.to_string()))?;
// Allow workspace directory (read/write)
if let Some(ref workspace) = self.workspace_dir {
if workspace.exists() {
let workspace_fd =
PathFd::new(workspace).map_err(|e| std::io::Error::other(e.to_string()))?;
ruleset = ruleset
.add_rule(PathBeneath::new(
workspace_fd,
AccessFs::ReadFile | AccessFs::WriteFile | AccessFs::ReadDir,
))
.map_err(|e| std::io::Error::other(e.to_string()))?;
}
}
// Allow /tmp for general operations
let tmp_fd =
PathFd::new(Path::new("/tmp")).map_err(|e| std::io::Error::other(e.to_string()))?;
ruleset = ruleset
.add_rule(PathBeneath::new(
tmp_fd,
AccessFs::ReadFile | AccessFs::WriteFile,
))
.map_err(|e| std::io::Error::other(e.to_string()))?;
// Allow /usr and /bin for executing commands
let usr_fd =
PathFd::new(Path::new("/usr")).map_err(|e| std::io::Error::other(e.to_string()))?;
ruleset = ruleset
.add_rule(PathBeneath::new(
usr_fd,
AccessFs::ReadFile | AccessFs::ReadDir,
))
.map_err(|e| std::io::Error::other(e.to_string()))?;
let bin_fd =
PathFd::new(Path::new("/bin")).map_err(|e| std::io::Error::other(e.to_string()))?;
ruleset = ruleset
.add_rule(PathBeneath::new(
bin_fd,
AccessFs::ReadFile | AccessFs::ReadDir,
))
.map_err(|e| std::io::Error::other(e.to_string()))?;
// Apply the ruleset
match ruleset.restrict_self() {
Ok(_) => {
tracing::debug!("Landlock restrictions applied successfully");
Ok(())
}
Err(e) => {
tracing::warn!("Failed to apply Landlock restrictions: {}", e);
Err(std::io::Error::other(e.to_string()))
}
}
}
}
#[cfg(all(feature = "sandbox-landlock", target_os = "linux"))]
impl Sandbox for LandlockSandbox {
fn wrap_command(&self, _cmd: &mut std::process::Command) -> std::io::Result<()> {
// Apply Landlock restrictions before executing the command
// Note: This affects the current process, not the child process
// Child processes inherit the Landlock restrictions
self.apply_restrictions()
}
fn is_available(&self) -> bool {
// Try to create a minimal ruleset to verify availability
Ruleset::default()
.handle_access(AccessFs::ReadFile)
.and_then(|ruleset| ruleset.create())
.is_ok()
}
fn name(&self) -> &str {
"landlock"
}
fn description(&self) -> &str {
"Linux kernel LSM sandboxing (filesystem access control)"
}
}
// Stub implementations for non-Linux or when feature is disabled
#[cfg(not(all(feature = "sandbox-landlock", target_os = "linux")))]
pub struct LandlockSandbox;
#[cfg(not(all(feature = "sandbox-landlock", target_os = "linux")))]
impl LandlockSandbox {
pub fn new() -> std::io::Result<Self> {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"Landlock is only supported on Linux with the sandbox-landlock feature",
))
}
pub fn with_workspace(_workspace_dir: Option<std::path::PathBuf>) -> std::io::Result<Self> {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"Landlock is only supported on Linux",
))
}
pub fn probe() -> std::io::Result<Self> {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"Landlock is only supported on Linux",
))
}
}
#[cfg(not(all(feature = "sandbox-landlock", target_os = "linux")))]
impl Sandbox for LandlockSandbox {
fn wrap_command(&self, _cmd: &mut std::process::Command) -> std::io::Result<()> {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"Landlock is only supported on Linux",
))
}
fn is_available(&self) -> bool {
false
}
fn name(&self) -> &str {
"landlock"
}
fn description(&self) -> &str {
"Linux kernel LSM sandboxing (not available on this platform)"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(all(feature = "sandbox-landlock", target_os = "linux"))]
#[test]
fn landlock_sandbox_name() {
if let Ok(sandbox) = LandlockSandbox::new() {
assert_eq!(sandbox.name(), "landlock");
}
}
#[cfg(not(all(feature = "sandbox-landlock", target_os = "linux")))]
#[test]
fn landlock_not_available_on_non_linux() {
assert!(!LandlockSandbox.is_available());
assert_eq!(LandlockSandbox.name(), "landlock");
}
#[test]
fn landlock_with_none_workspace() {
// Should work even without a workspace directory
let result = LandlockSandbox::with_workspace(None);
// Result depends on platform and feature flag
match result {
Ok(sandbox) => assert!(sandbox.is_available()),
Err(_) => assert!(!cfg!(all(
feature = "sandbox-landlock",
target_os = "linux"
))),
}
}
// ── §1.1 Landlock stub tests ──────────────────────────────
#[cfg(not(all(feature = "sandbox-landlock", target_os = "linux")))]
#[test]
fn landlock_stub_wrap_command_returns_unsupported() {
let sandbox = LandlockSandbox;
let mut cmd = std::process::Command::new("echo");
let result = sandbox.wrap_command(&mut cmd);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::Unsupported);
}
#[cfg(not(all(feature = "sandbox-landlock", target_os = "linux")))]
#[test]
fn landlock_stub_new_returns_unsupported() {
let result = LandlockSandbox::new();
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::Unsupported);
}
#[cfg(not(all(feature = "sandbox-landlock", target_os = "linux")))]
#[test]
fn landlock_stub_probe_returns_unsupported() {
let result = LandlockSandbox::probe();
assert!(result.is_err());
}
}