sandbox_linux.rs

  1//! Linux Landlock sandbox implementation.
  2//!
  3//! Uses the Landlock LSM to restrict filesystem access for the current process.
  4//! Must be called after fork(), before exec().
  5
  6use landlock::{
  7    ABI, Access, AccessFs, AccessNet, PathBeneath, PathFd, Ruleset, RulesetAttr,
  8    RulesetCreatedAttr, RulesetStatus,
  9};
 10use std::io::{Error, Result};
 11use std::path::Path;
 12
 13use crate::SandboxConfig;
 14
 15const TARGET_ABI: ABI = ABI::V5;
 16
 17fn fs_read() -> AccessFs {
 18    AccessFs::ReadFile | AccessFs::ReadDir
 19}
 20
 21fn fs_read_exec() -> AccessFs {
 22    fs_read() | AccessFs::Execute
 23}
 24
 25fn fs_all() -> AccessFs {
 26    AccessFs::from_all(TARGET_ABI)
 27}
 28
 29fn add_path_rule(
 30    ruleset: landlock::RulesetCreated,
 31    path: &Path,
 32    access: AccessFs,
 33) -> std::result::Result<landlock::RulesetCreated, landlock::RulesetError> {
 34    match PathFd::new(path) {
 35        Ok(fd) => ruleset.add_rule(PathBeneath::new(fd, access)),
 36        Err(e) => {
 37            log::debug!(
 38                "Landlock: skipping nonexistent path {}: {e}",
 39                path.display()
 40            );
 41            Ok(ruleset)
 42        }
 43    }
 44}
 45
 46/// Apply a Landlock sandbox to the current process.
 47/// Must be called after fork(), before exec().
 48pub fn apply_sandbox(config: &SandboxConfig) -> Result<()> {
 49    let ret = unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) };
 50    if ret != 0 {
 51        return Err(Error::last_os_error());
 52    }
 53
 54    let ruleset_base = Ruleset::default()
 55        .handle_access(AccessFs::from_all(TARGET_ABI))
 56        .map_err(|e| Error::other(format!("landlock ruleset create: {e}")))?;
 57
 58    let ruleset_with_net = if !config.allow_network {
 59        ruleset_base
 60            .handle_access(AccessNet::from_all(TARGET_ABI))
 61            .map_err(|e| {
 62                Error::other(format!(
 63                    "landlock network restriction not supported (requires kernel 6.4+): {e}"
 64                ))
 65            })?
 66    } else {
 67        ruleset_base
 68    };
 69
 70    let mut ruleset = ruleset_with_net
 71        .create()
 72        .map_err(|e| Error::other(format!("landlock ruleset init: {e}")))?;
 73
 74    for path in &config.system_paths.executable {
 75        ruleset = add_path_rule(ruleset, path, fs_read_exec())
 76            .map_err(|e| Error::other(format!("landlock rule: {e}")))?;
 77    }
 78
 79    for path in &config.system_paths.read_only {
 80        ruleset = add_path_rule(ruleset, path, fs_read())
 81            .map_err(|e| Error::other(format!("landlock rule: {e}")))?;
 82    }
 83
 84    for path in &config.system_paths.read_write {
 85        ruleset = add_path_rule(ruleset, path, fs_all())
 86            .map_err(|e| Error::other(format!("landlock rule: {e}")))?;
 87    }
 88
 89    ruleset = add_path_rule(ruleset, &config.project_dir, fs_all())
 90        .map_err(|e| Error::other(format!("landlock project rule: {e}")))?;
 91
 92    for path in &config.additional_executable_paths {
 93        ruleset = add_path_rule(ruleset, path, fs_read_exec())
 94            .map_err(|e| Error::other(format!("landlock rule: {e}")))?;
 95    }
 96    for path in &config.additional_read_only_paths {
 97        ruleset = add_path_rule(ruleset, path, fs_read())
 98            .map_err(|e| Error::other(format!("landlock rule: {e}")))?;
 99    }
100    for path in &config.additional_read_write_paths {
101        ruleset = add_path_rule(ruleset, path, fs_all())
102            .map_err(|e| Error::other(format!("landlock rule: {e}")))?;
103    }
104
105    if let Ok(home) = std::env::var("HOME") {
106        let home = Path::new(&home);
107        for dotfile in SandboxConfig::READ_ONLY_DOTFILES {
108            let path = home.join(dotfile);
109            if path.exists() {
110                ruleset = add_path_rule(ruleset, &path, fs_read())
111                    .map_err(|e| Error::other(format!("landlock dotfile rule: {e}")))?;
112            }
113        }
114        for dotfile in SandboxConfig::READ_WRITE_DOTFILES {
115            let path = home.join(dotfile);
116            if path.exists() {
117                ruleset = add_path_rule(ruleset, &path, fs_all())
118                    .map_err(|e| Error::other(format!("landlock dotfile rule: {e}")))?;
119            }
120        }
121        let config_dir = home.join(".config");
122        if config_dir.exists() {
123            ruleset = add_path_rule(ruleset, &config_dir, fs_read())
124                .map_err(|e| Error::other(format!("landlock .config rule: {e}")))?;
125        }
126        let proc_self = Path::new("/proc/self");
127        if proc_self.exists() {
128            ruleset = add_path_rule(ruleset, proc_self, fs_all())
129                .map_err(|e| Error::other(format!("landlock /proc/self rule: {e}")))?;
130        }
131    }
132
133    let status = ruleset
134        .restrict_self()
135        .map_err(|e| Error::other(format!("landlock restrict_self: {e}")))?;
136
137    match status.ruleset {
138        RulesetStatus::FullyEnforced => {
139            log::info!("Landlock sandbox fully enforced");
140        }
141        RulesetStatus::PartiallyEnforced => {
142            return Err(Error::other(
143                "Landlock sandbox only partially enforced on this kernel. \
144                 The sandbox cannot guarantee the requested restrictions. \
145                 Upgrade to kernel 6.4+ for full enforcement, or disable sandboxing.",
146            ));
147        }
148        RulesetStatus::NotEnforced => {
149            return Err(Error::other(
150                "Landlock is not supported on this kernel (requires 5.13+). \
151                 The terminal cannot be sandboxed. \
152                 Upgrade your kernel or disable sandboxing.",
153            ));
154        }
155    }
156
157    Ok(())
158}