#[cfg(target_os = "linux")]
use std::path::Path;
#[cfg(target_os = "linux")]
const DEFAULT_RO_PATHS: &[&str] =
&["/usr", "/bin", "/sbin", "/lib", "/lib64", "/lib32", "/etc", "/proc", "/sys", "/run", "/opt"];
#[cfg(target_os = "linux")]
const DEFAULT_RW_PATHS: &[&str] = &["/tmp", "/dev", "/var/tmp"];
#[derive(Debug)]
enum SandboxMessage {
#[cfg(target_os = "linux")]
Info(String),
Warning(String),
}
#[derive(Debug, Default)]
pub struct SandboxReport {
messages: Vec<SandboxMessage>,
}
impl SandboxReport {
pub fn emit(self) {
for message in self.messages {
match message {
#[cfg(target_os = "linux")]
SandboxMessage::Info(message) => tracing::info!("{message}"),
SandboxMessage::Warning(message) => tracing::warn!("{message}"),
}
}
}
}
pub fn apply_if_requested() -> SandboxReport {
let mut report = SandboxReport::default();
if !crate::config::env_flag("WINX_SANDBOX") {
return report;
}
let workspace_root = match std::env::current_dir() {
Ok(dir) => dir,
Err(error) => {
report.messages.push(SandboxMessage::Warning(format!(
"WINX_SANDBOX: cannot determine the cwd ({error}); the workspace may not be writable under the sandbox - start winx from your project directory."
)));
std::path::PathBuf::from(".")
}
};
#[cfg(not(target_os = "linux"))]
{
let _ = &workspace_root;
report.messages.push(SandboxMessage::Warning(
"WINX_SANDBOX is set but Landlock is Linux-only; continuing UNSANDBOXED.".to_string(),
));
}
#[cfg(target_os = "linux")]
match apply_linux(&workspace_root) {
Ok(status) => report.messages.push(SandboxMessage::Info(format!(
"WINX_SANDBOX active (Landlock {status:?}); writes confined to the workspace + /tmp, home not readable. Extend via WINX_SANDBOX_RO_PATHS / WINX_SANDBOX_RW_PATHS."
))),
Err(error) => report.messages.push(SandboxMessage::Warning(format!(
"WINX_SANDBOX: could not apply Landlock ({error}); continuing UNSANDBOXED."
))),
}
report
}
#[cfg(target_os = "linux")]
fn apply_linux(workspace_root: &Path) -> anyhow::Result<landlock::RulesetStatus> {
use landlock::{Access, AccessFs, Ruleset, RulesetAttr, ABI};
let abi = ABI::V1;
let ro = AccessFs::from_read(abi);
let rw = AccessFs::from_all(abi);
let mut ruleset = Ruleset::default().handle_access(rw)?.create()?;
for path in DEFAULT_RO_PATHS {
ruleset = add_path(ruleset, Path::new(path), ro)?;
}
for path in env_paths("WINX_SANDBOX_RO_PATHS") {
ruleset = add_path(ruleset, &path, ro)?;
}
for path in DEFAULT_RW_PATHS {
ruleset = add_path(ruleset, Path::new(path), rw)?;
}
for path in env_paths("WINX_SANDBOX_RW_PATHS") {
ruleset = add_path(ruleset, &path, rw)?;
}
ruleset = add_path(ruleset, workspace_root, rw)?;
Ok(ruleset.restrict_self()?.ruleset)
}
#[cfg(target_os = "linux")]
fn add_path(
ruleset: landlock::RulesetCreated,
path: &Path,
access: landlock::BitFlags<landlock::AccessFs>,
) -> anyhow::Result<landlock::RulesetCreated> {
use landlock::{PathBeneath, PathFd, RulesetCreatedAttr};
match PathFd::new(path) {
Ok(fd) => Ok(ruleset.add_rule(PathBeneath::new(fd, access))?),
Err(_) => Ok(ruleset),
}
}
#[cfg(target_os = "linux")]
fn env_paths(var: &str) -> Vec<std::path::PathBuf> {
parse_path_list(&std::env::var(var).unwrap_or_default())
}
#[cfg(target_os = "linux")]
fn parse_path_list(value: &str) -> Vec<std::path::PathBuf> {
value.split(':').filter(|segment| !segment.is_empty()).map(std::path::PathBuf::from).collect()
}
#[cfg(all(test, target_os = "linux"))]
mod tests {
use super::parse_path_list;
use std::path::PathBuf;
#[test]
fn parse_path_list_splits_and_drops_empties() {
assert!(parse_path_list("").is_empty(), "expected empty list for an empty string");
assert_eq!(parse_path_list("/a:/b"), vec![PathBuf::from("/a"), PathBuf::from("/b")]);
assert_eq!(parse_path_list("/a::/b:"), vec![PathBuf::from("/a"), PathBuf::from("/b")]);
}
}