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::terminal_settings::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            // Path doesn't exist — skip it (e.g., /opt/homebrew on non-Homebrew systems)
 38            log::debug!(
 39                "Landlock: skipping nonexistent path {}: {e}",
 40                path.display()
 41            );
 42            Ok(ruleset)
 43        }
 44    }
 45}
 46
 47/// Apply a Landlock sandbox to the current process.
 48/// Must be called after fork(), before exec().
 49pub fn apply_sandbox(config: &SandboxConfig) -> Result<()> {
 50    // PR_SET_NO_NEW_PRIVS is required before landlock_restrict_self.
 51    // It prevents the process from gaining privileges via setuid binaries.
 52    let ret = unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) };
 53    if ret != 0 {
 54        return Err(Error::last_os_error());
 55    }
 56
 57    let ruleset_base = Ruleset::default()
 58        .handle_access(AccessFs::from_all(TARGET_ABI))
 59        .map_err(|e| Error::other(format!("landlock ruleset create: {e}")))?;
 60
 61    let ruleset_with_net = if !config.allow_network {
 62        ruleset_base
 63            .handle_access(AccessNet::from_all(TARGET_ABI))
 64            .map_err(|e| {
 65                Error::other(format!(
 66                    "landlock network restriction not supported (requires kernel 6.4+): {e}"
 67                ))
 68            })?
 69    } else {
 70        ruleset_base
 71    };
 72
 73    let mut ruleset = ruleset_with_net
 74        .create()
 75        .map_err(|e| Error::other(format!("landlock ruleset init: {e}")))?;
 76
 77    // System executable paths (read + execute)
 78    for path in &config.system_paths.executable {
 79        ruleset = add_path_rule(ruleset, path, fs_read_exec())
 80            .map_err(|e| Error::other(format!("landlock rule: {e}")))?;
 81    }
 82
 83    // System read-only paths
 84    for path in &config.system_paths.read_only {
 85        ruleset = add_path_rule(ruleset, path, fs_read())
 86            .map_err(|e| Error::other(format!("landlock rule: {e}")))?;
 87    }
 88
 89    // System read+write paths
 90    for path in &config.system_paths.read_write {
 91        ruleset = add_path_rule(ruleset, path, fs_all())
 92            .map_err(|e| Error::other(format!("landlock rule: {e}")))?;
 93    }
 94
 95    // Project directory: full access
 96    ruleset = add_path_rule(ruleset, &config.project_dir, fs_all())
 97        .map_err(|e| Error::other(format!("landlock project rule: {e}")))?;
 98
 99    // User-configured paths
100    for path in &config.additional_executable_paths {
101        ruleset = add_path_rule(ruleset, path, fs_read_exec())
102            .map_err(|e| Error::other(format!("landlock rule: {e}")))?;
103    }
104    for path in &config.additional_read_only_paths {
105        ruleset = add_path_rule(ruleset, path, fs_read())
106            .map_err(|e| Error::other(format!("landlock rule: {e}")))?;
107    }
108    for path in &config.additional_read_write_paths {
109        ruleset = add_path_rule(ruleset, path, fs_all())
110            .map_err(|e| Error::other(format!("landlock rule: {e}")))?;
111    }
112
113    // Shell config dotfiles: read-only
114    if let Ok(home) = std::env::var("HOME") {
115        let home = Path::new(&home);
116        for dotfile in &[
117            ".bashrc",
118            ".bash_profile",
119            ".bash_login",
120            ".profile",
121            ".zshrc",
122            ".zshenv",
123            ".zprofile",
124            ".zlogin",
125            ".zlogout",
126            ".inputrc",
127            ".terminfo",
128            ".gitconfig",
129        ] {
130            let path = home.join(dotfile);
131            if path.exists() {
132                ruleset = add_path_rule(ruleset, &path, fs_read())
133                    .map_err(|e| Error::other(format!("landlock dotfile rule: {e}")))?;
134            }
135        }
136        let config_dir = home.join(".config");
137        if config_dir.exists() {
138            ruleset = add_path_rule(ruleset, &config_dir, fs_read())
139                .map_err(|e| Error::other(format!("landlock .config rule: {e}")))?;
140        }
141        // /proc/self for bash process substitution
142        let proc_self = Path::new("/proc/self");
143        if proc_self.exists() {
144            ruleset = add_path_rule(ruleset, proc_self, fs_read())
145                .map_err(|e| Error::other(format!("landlock /proc/self rule: {e}")))?;
146        }
147    }
148
149    let status = ruleset
150        .restrict_self()
151        .map_err(|e| Error::other(format!("landlock restrict_self: {e}")))?;
152
153    match status.ruleset {
154        RulesetStatus::FullyEnforced => {
155            log::info!("Landlock sandbox fully enforced");
156        }
157        RulesetStatus::PartiallyEnforced => {
158            if !config.allow_network {
159                log::warn!(
160                    "Landlock sandbox partially enforced; \
161                     network restriction may not be enforced on this kernel"
162                );
163            } else {
164                log::warn!("Landlock sandbox partially enforced (older kernel ABI)");
165            }
166        }
167        RulesetStatus::NotEnforced => {
168            if !config.allow_network {
169                return Err(Error::other(
170                    "Landlock not supported on this kernel but network restriction was requested",
171                ));
172            }
173            log::warn!("Landlock not supported on this kernel; running unsandboxed");
174        }
175    }
176
177    Ok(())
178}