sandbox_exec.rs

  1//! Sandbox executor: the `--sandbox-exec` entry point.
  2//!
  3//! When Zed is invoked with `--sandbox-exec <json-config> -- <shell> [args...]`,
  4//! this module takes over. It:
  5//! 1. Parses the sandbox config from the JSON argument
  6//! 2. Filters environment variables to the allowed set
  7//! 3. Applies the OS-level sandbox (Seatbelt on macOS, Landlock on Linux)
  8//! 4. Execs the real shell (never returns)
  9//!
 10//! This approach avoids modifying the alacritty fork — alacritty spawns the
 11//! Zed binary as the "shell", and the Zed binary applies the sandbox before
 12//! exec-ing the real shell. Since both Seatbelt and Landlock sandboxes are
 13//! inherited by child processes, the real shell and everything it spawns
 14//! are sandboxed.
 15//!
 16//! Note: passing JSON directly via a CLI argument is safe because
 17//! `std::process::Command::arg()` passes arguments to `execve` without
 18//! shell interpretation, so no quoting issues arise.
 19
 20use crate::{ResolvedSystemPaths, SandboxConfig};
 21use std::os::unix::process::CommandExt;
 22use std::process::Command;
 23
 24/// Serializable sandbox config for passing between processes via a JSON CLI arg.
 25#[derive(serde::Serialize, serde::Deserialize)]
 26pub struct SandboxExecConfig {
 27    pub project_dir: String,
 28    pub executable_paths: Vec<String>,
 29    pub read_only_paths: Vec<String>,
 30    pub read_write_paths: Vec<String>,
 31    pub additional_executable_paths: Vec<String>,
 32    pub additional_read_only_paths: Vec<String>,
 33    pub additional_read_write_paths: Vec<String>,
 34    pub allow_network: bool,
 35    pub allowed_env_vars: Vec<String>,
 36    /// Optional fingerprint UUID for session tracking (macOS).
 37    #[serde(default)]
 38    pub fingerprint_uuid: Option<String>,
 39    /// Whether this is a tracking-only config (no filesystem restrictions).
 40    #[serde(default)]
 41    pub tracking_only: bool,
 42    /// Optional cgroup path for Linux process tracking.
 43    #[serde(default)]
 44    pub cgroup_path: Option<String>,
 45}
 46
 47impl SandboxExecConfig {
 48    /// Convert from the resolved `SandboxConfig` to the serializable form.
 49    pub fn from_sandbox_config(config: &SandboxConfig) -> Self {
 50        Self {
 51            project_dir: config.project_dir.to_string_lossy().into_owned(),
 52            executable_paths: config
 53                .system_paths
 54                .executable
 55                .iter()
 56                .map(|p| p.to_string_lossy().into_owned())
 57                .collect(),
 58            read_only_paths: config
 59                .system_paths
 60                .read_only
 61                .iter()
 62                .map(|p| p.to_string_lossy().into_owned())
 63                .collect(),
 64            read_write_paths: config
 65                .system_paths
 66                .read_write
 67                .iter()
 68                .map(|p| p.to_string_lossy().into_owned())
 69                .collect(),
 70            additional_executable_paths: config
 71                .additional_executable_paths
 72                .iter()
 73                .map(|p| p.to_string_lossy().into_owned())
 74                .collect(),
 75            additional_read_only_paths: config
 76                .additional_read_only_paths
 77                .iter()
 78                .map(|p| p.to_string_lossy().into_owned())
 79                .collect(),
 80            additional_read_write_paths: config
 81                .additional_read_write_paths
 82                .iter()
 83                .map(|p| p.to_string_lossy().into_owned())
 84                .collect(),
 85            allow_network: config.allow_network,
 86            allowed_env_vars: config.allowed_env_vars.clone(),
 87            fingerprint_uuid: None,
 88            tracking_only: false,
 89            cgroup_path: None,
 90        }
 91    }
 92
 93    /// Convert back to a `SandboxConfig` for the sandbox implementation functions.
 94    pub fn to_sandbox_config(&self) -> SandboxConfig {
 95        use std::path::PathBuf;
 96
 97        SandboxConfig {
 98            project_dir: PathBuf::from(&self.project_dir),
 99            system_paths: ResolvedSystemPaths {
100                executable: self.executable_paths.iter().map(PathBuf::from).collect(),
101                read_only: self.read_only_paths.iter().map(PathBuf::from).collect(),
102                read_write: self.read_write_paths.iter().map(PathBuf::from).collect(),
103            },
104            additional_executable_paths: self
105                .additional_executable_paths
106                .iter()
107                .map(PathBuf::from)
108                .collect(),
109            additional_read_only_paths: self
110                .additional_read_only_paths
111                .iter()
112                .map(PathBuf::from)
113                .collect(),
114            additional_read_write_paths: self
115                .additional_read_write_paths
116                .iter()
117                .map(PathBuf::from)
118                .collect(),
119            allow_network: self.allow_network,
120            allowed_env_vars: self.allowed_env_vars.clone(),
121        }
122    }
123
124    /// Serialize the config to a JSON string for passing via CLI arg.
125    pub fn to_json(&self) -> String {
126        serde_json::to_string(self).expect("SandboxExecConfig is non-cyclic")
127    }
128
129    /// Deserialize a config from a JSON string.
130    pub fn from_json(json: &str) -> Result<Self, String> {
131        serde_json::from_str(json).map_err(|e| format!("invalid sandbox config JSON: {e}"))
132    }
133}
134
135/// Main entry point for `zed --sandbox-exec <json-config> [-- shell args...]`.
136///
137/// This function never returns — it applies the sandbox and execs the real shell.
138/// The `shell_args` are the remaining positional arguments after `--`.
139pub fn sandbox_exec_main(config_json: &str, shell_args: &[String]) -> ! {
140    let config = match SandboxExecConfig::from_json(config_json) {
141        Ok(c) => c,
142        Err(e) => {
143            eprintln!("zed --sandbox-exec: failed to parse config: {e}");
144            std::process::exit(1);
145        }
146    };
147
148    if shell_args.is_empty() {
149        eprintln!("zed --sandbox-exec: no shell command specified");
150        std::process::exit(1);
151    }
152
153    let mut sandbox_config = config.to_sandbox_config();
154    sandbox_config.canonicalize_paths();
155
156    // Step 1: Collect allowed environment variables.
157    let zed_vars = [
158        "ZED_TERM",
159        "TERM_PROGRAM",
160        "TERM",
161        "COLORTERM",
162        "TERM_PROGRAM_VERSION",
163    ];
164    let allowed: std::collections::HashSet<&str> =
165        config.allowed_env_vars.iter().map(|s| s.as_str()).collect();
166
167    let filtered_env: Vec<(String, String)> = std::env::vars()
168        .filter(|(key, _)| allowed.contains(key.as_str()) || zed_vars.contains(&key.as_str()))
169        .collect();
170
171    // Step 2: Apply the OS-level sandbox.
172    #[cfg(target_os = "macos")]
173    {
174        if config.tracking_only {
175            if let Some(ref uuid_str) = config.fingerprint_uuid {
176                let fingerprint =
177                    match crate::sandbox_macos::SessionFingerprint::from_uuid_str(uuid_str) {
178                        Ok(fp) => fp,
179                        Err(e) => {
180                            eprintln!("zed --sandbox-exec: invalid fingerprint UUID: {e}");
181                            std::process::exit(1);
182                        }
183                    };
184                if let Err(e) = crate::sandbox_macos::apply_fingerprint_only(&fingerprint) {
185                    eprintln!("zed --sandbox-exec: failed to apply fingerprint profile: {e}");
186                    std::process::exit(1);
187                }
188            }
189        } else {
190            let result = match config.fingerprint_uuid.as_ref() {
191                Some(uuid_str) => {
192                    match crate::sandbox_macos::SessionFingerprint::from_uuid_str(uuid_str) {
193                        Ok(fingerprint) => crate::sandbox_macos::apply_sandbox_with_fingerprint(
194                            &sandbox_config,
195                            &fingerprint,
196                        ),
197                        Err(e) => {
198                            eprintln!("zed --sandbox-exec: invalid fingerprint UUID: {e}");
199                            std::process::exit(1);
200                        }
201                    }
202                }
203                None => crate::sandbox_macos::apply_sandbox(&sandbox_config),
204            };
205            if let Err(e) = result {
206                eprintln!("zed --sandbox-exec: failed to apply macOS sandbox: {e}");
207                std::process::exit(1);
208            }
209        }
210    }
211
212    #[cfg(target_os = "linux")]
213    {
214        // Move into the session cgroup for process tracking
215        if let Some(ref cgroup_path) = config.cgroup_path {
216            let session = crate::cgroup::CgroupSession::from_path(cgroup_path);
217            let pid = unsafe { libc::getpid() };
218            if let Err(e) = session.add_process(pid) {
219                eprintln!("zed --sandbox-exec: failed to join cgroup: {e}");
220                std::process::exit(1);
221            }
222        }
223
224        // Apply Landlock restrictions (only if not tracking-only)
225        if !config.tracking_only {
226            if let Err(e) = crate::sandbox_linux::apply_sandbox(&sandbox_config) {
227                eprintln!("zed --sandbox-exec: failed to apply Linux sandbox: {e}");
228                std::process::exit(1);
229            }
230        }
231    }
232
233    // Step 3: Exec the real shell.
234    let program = &shell_args[0];
235    let args = &shell_args[1..];
236    let err = Command::new(program)
237        .args(args)
238        .env_clear()
239        .envs(filtered_env)
240        .exec();
241
242    eprintln!("zed --sandbox-exec: failed to exec {program}: {err}");
243    std::process::exit(1);
244}