Convert peripheral tool collections into Arc-backed tools so all-features builds compile cleanly. Gate the Landlock Path import behind the Linux sandbox cfg to remove the remaining warning during validation.
270 lines
9.3 KiB
Rust
270 lines
9.3 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;
|
|
#[cfg(all(feature = "sandbox-landlock", target_os = "linux"))]
|
|
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<()> {
|
|
// `restrict_self()` affects the current process and all descendants.
|
|
// Applying it here would permanently tighten the parent agent runtime
|
|
// on every command invocation and eventually degrade execution.
|
|
//
|
|
// Until we can apply restrictions in the child pre-exec path, fail
|
|
// closed instead of mutating the long-lived parent process.
|
|
let _ = &self.workspace_dir;
|
|
Err(std::io::Error::new(
|
|
std::io::ErrorKind::Unsupported,
|
|
"Landlock per-command wrapping is not yet supported safely; use firejail, bubblewrap, or docker backend",
|
|
))
|
|
}
|
|
|
|
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());
|
|
}
|
|
}
|