//! Cross-platform sandboxing for commands run on behalf of the agent.//!//! The public API is intentionally platform-neutral. Internally, macOS uses//! Seatbelt (`sandbox-exec`), Linux uses Bubblewrap (`bwrap`), and Windows uses//! Bubblewrap inside WSL. Restricted-network policies are enforced by an//! in-process HTTP/HTTPS proxy (the `http_proxy` crate) that this crate//! constructs and owns; callers only describe intent via [`SandboxPolicy`]. use std::{ collections::HashMap, fmt, path::{Path, PathBuf}, process::Output,}; use http_proxy::ProxyHandle;#[cfg(not(target_os = "windows"))]use http_proxy::{Allowlist, HostPattern, ProxyConfig, ProxyEvent, UpstreamProxy}; #[cfg(target_os = "linux")]mod linux_bubblewrap; #[cfg(target_os = "macos")]mod macos_seatbelt; #[cfg(target_os = "windows")]mod windows_wsl; mod util; #[cfg(target_os = "macos")]use util::canonicalize_allowing_missing_leaf;#[cfg(target_os = "linux")]use util::{CanonicalPathBuf, linux_fd_identity};pub use util::{ HostFilesystemLocation, HostFilesystemLocationDisplay, normalize_host_filesystem_locations, resolve_canonical,};#[cfg(target_os = "windows")]pub use windows_wsl::{ResolvedGrant, resolve_canonical_for_grant}; #[cfg(target_os = "windows")]pub(crate) const WSL_SANDBOX_UNAVAILABLE_PREFIX: &str = "Windows sandboxing via WSL is unavailable"; /// A path *inside the sandbox* — i.e. where a host location is exposed in the/// sandboxed process's view of the filesystem (for example, a bind-mount/// destination on Linux).////// Unlike [`HostFilesystemLocation`], this needs no hardening and is just a thin/// wrapper around a [`PathBuf`]. It only names a location within the sandbox's/// own namespace: the worst a tampered sandbox-side path can do is expose the/// (already-granted) host files at a *different* path inside the sandbox — it can/// never widen which host files are reachable. It is therefore fine to build one/// from an ordinary, even attacker-influenced, path.#[derive(Clone, Debug, Eq, PartialEq, Hash)]pub struct SandboxFilesystemLocation(PathBuf); impl SandboxFilesystemLocation { /// Name a location inside the sandbox's filesystem view. pub fn new(path: impl Into<PathBuf>) -> Self { Self(path.into()) } /// The in-sandbox path. Safe to read: this is not a trusted host identity. pub fn as_path(&self) -> &Path { &self.0 } /// Consume this wrapper, yielding the underlying in-sandbox path. pub fn into_path_buf(self) -> PathBuf { self.0 }} impl From<PathBuf> for SandboxFilesystemLocation { fn from(path: PathBuf) -> Self { Self(path) }} /// What a command is allowed to do, expressed as intent. This is the entire/// public configuration surface; how each policy is enforced (Seatbelt rules,/// Bubblewrap flags, a loopback proxy, …) is an implementation detail.#[derive(Clone, Debug, Eq, PartialEq)]pub struct SandboxPolicy { pub fs: SandboxFsPolicy, pub network: SandboxNetPolicy,} /// Filesystem policy for a sandboxed command.#[derive(Clone, Debug, Eq, PartialEq)]pub enum SandboxFsPolicy { /// Allow unrestricted filesystem writes except for protected paths, which /// remain readable but not writable. Unrestricted { protected_paths: Vec<HostFilesystemLocation>, }, /// Reads are allowed everywhere; writes are confined to these locations /// (and the standard ephemeral locations the platform provides). Each is a /// [`HostFilesystemLocation`] captured at validation time, never a bare path /// the enforcement layer would re-resolve. Restricted { writable_paths: Vec<HostFilesystemLocation>, protected_paths: Vec<HostFilesystemLocation>, },} /// Outbound-network policy for a sandboxed command.#[derive(Clone, Debug, Eq, PartialEq)]pub enum SandboxNetPolicy { /// Allow unrestricted outbound network access. Unrestricted, /// Block all outbound network access. Blocked, /// Allow outbound HTTP(S) only to these hostnames (exact hosts or /// leading-`*.` subdomain wildcards), enforced by an in-process proxy. Restricted { allowed_domains: Vec<String> },} /// Host paths that should remain readable but not writable, even when they fall/// under a writable subtree.////// The caller computes this list because the sandbox layer does not know which/// application-specific paths need stronger protection.pub type ProtectedPaths = Vec<HostFilesystemLocation>; /// Whether `path` (a host path) resides on a Windows drive reached through WSL's/// DrvFs, where sandbox-integrity guarantees are weaker than on the distro's/// native filesystem. Always `false` off Windows, where no such filesystem/// exists. This is a cheap structural check on the path shape; the authoritative/// per-grant classification is done in WSL by [`resolve_canonical_for_grant`].pub fn path_is_on_windows_drive(path: &Path) -> bool { #[cfg(target_os = "windows")] { windows_wsl::path_is_on_windows_drive(path) } #[cfg(not(target_os = "windows"))] { let _ = path; false }} impl SandboxPolicy { /// Combine two policy layers. Filesystem/network grants are unioned into the /// least-restrictive policy that satisfies both layers, while protected paths /// within restricted filesystem policies are unioned so every layer's /// protected subtrees remain protected. pub fn merge(self, other: SandboxPolicy) -> SandboxPolicy { SandboxPolicy { fs: self.fs.merge(other.fs), network: self.network.merge(other.network), } } /// Replace the protected paths in the filesystem policy, keeping writable /// paths and network policy unchanged. pub fn with_protected_paths(mut self, protected_paths: Vec<HostFilesystemLocation>) -> Self { match &mut self.fs { SandboxFsPolicy::Unrestricted { protected_paths: existing, } | SandboxFsPolicy::Restricted { protected_paths: existing, .. } => *existing = protected_paths, } self }} fn merge_locations( locations: Vec<HostFilesystemLocation>, other: Vec<HostFilesystemLocation>,) -> Vec<HostFilesystemLocation> { // Union the two sets and reduce to a minimal cover, so a location nested // under another (in either input) is dropped rather than bound redundantly. normalize_host_filesystem_locations(locations.into_iter().chain(other))} fn validate_writable_paths_do_not_overlap_protected_paths( writable_paths: &[HostFilesystemLocation], protected_paths: &[HostFilesystemLocation],) -> Result<(), SandboxError> { for writable_path in writable_paths { for protected_path in protected_paths { if writable_path_overlaps_protected_path(writable_path, protected_path) { return Err(SandboxError::InvalidRequest(format!( "writable sandbox path `{}` overlaps protected path `{}`", writable_path.untrusted_path_display(), protected_path.untrusted_path_display() ))); } } } Ok(())} #[cfg(target_os = "linux")]fn writable_path_overlaps_protected_path( writable_path: &HostFilesystemLocation, protected_path: &HostFilesystemLocation,) -> bool { linux_location_is_equal_or_descendant(writable_path, protected_path)} #[cfg(target_os = "macos")]fn writable_path_overlaps_protected_path( writable_path: &HostFilesystemLocation, protected_path: &HostFilesystemLocation,) -> bool { writable_path .macos_canonical_path() .starts_with(protected_path.macos_canonical_path())} #[cfg(not(any(target_os = "linux", target_os = "macos")))]fn writable_path_overlaps_protected_path( writable_path: &HostFilesystemLocation, protected_path: &HostFilesystemLocation,) -> bool { writable_path .untrusted_raw_path() .starts_with(protected_path.untrusted_raw_path())} #[cfg(target_os = "linux")]fn linux_location_is_equal_or_descendant( location: &HostFilesystemLocation, ancestor: &HostFilesystemLocation,) -> bool { use std::os::fd::{AsRawFd as _, FromRawFd as _, OwnedFd}; if location == ancestor { return true; } let Ok(mut current) = location.linux_dup_fd() else { log::warn!("failed to duplicate sandbox location fd while checking protected path overlap"); return false; }; loop { let parent = unsafe { let fd = libc::openat( current.as_raw_fd(), c"..".as_ptr(), libc::O_PATH | libc::O_DIRECTORY | libc::O_CLOEXEC, ); if fd < 0 { return false; } OwnedFd::from_raw_fd(fd) }; let Some(parent_identity) = linux_fd_identity(parent.as_raw_fd()) else { log::warn!("failed to fstat parent fd while checking protected path overlap"); return false; }; let Some(current_identity) = linux_fd_identity(current.as_raw_fd()) else { log::warn!("failed to fstat current fd while checking protected path overlap"); return false; }; let Some(ancestor_identity) = linux_fd_identity(ancestor.linux_fd().as_raw_fd()) else { log::warn!("failed to fstat protected fd while checking protected path overlap"); return false; }; if parent_identity == ancestor_identity { return true; } if parent_identity == current_identity { return false; } current = parent; }} impl SandboxFsPolicy { /// Unrestricted access dominates; otherwise the writable subtrees union. pub fn merge(self, other: SandboxFsPolicy) -> SandboxFsPolicy { match (self, other) { ( SandboxFsPolicy::Unrestricted { protected_paths: protected_a, }, SandboxFsPolicy::Unrestricted { protected_paths: protected_b, }, ) => SandboxFsPolicy::Unrestricted { protected_paths: merge_locations(protected_a, protected_b), }, ( SandboxFsPolicy::Unrestricted { protected_paths: protected_a, }, SandboxFsPolicy::Restricted { protected_paths: protected_b, .. }, ) | ( SandboxFsPolicy::Restricted { protected_paths: protected_a, .. }, SandboxFsPolicy::Unrestricted { protected_paths: protected_b, }, ) => SandboxFsPolicy::Unrestricted { protected_paths: merge_locations(protected_a, protected_b), }, ( SandboxFsPolicy::Restricted { writable_paths: writable_a, protected_paths: protected_a, }, SandboxFsPolicy::Restricted { writable_paths: writable_b, protected_paths: protected_b, }, ) => SandboxFsPolicy::Restricted { writable_paths: merge_locations(writable_a, writable_b), protected_paths: merge_locations(protected_a, protected_b), }, } }} impl SandboxNetPolicy { /// Unrestricted access dominates and `Blocked` is the identity; otherwise the /// allowed domains union. pub fn merge(self, other: SandboxNetPolicy) -> SandboxNetPolicy { match (self, other) { (SandboxNetPolicy::Unrestricted, _) | (_, SandboxNetPolicy::Unrestricted) => { SandboxNetPolicy::Unrestricted } (SandboxNetPolicy::Blocked, other) | (other, SandboxNetPolicy::Blocked) => other, ( SandboxNetPolicy::Restricted { allowed_domains: mut a, }, SandboxNetPolicy::Restricted { allowed_domains: b }, ) => { for domain in b { if !a.contains(&domain) { a.push(domain); } } SandboxNetPolicy::Restricted { allowed_domains: a } } } }} /// A command and its execution environment, before sandbox wrapping.#[derive(Clone, Debug, Default, Eq, PartialEq)]pub struct CommandAndArgs { /// Program to execute. pub program: String, /// Arguments passed to `program`. pub args: Vec<String>, /// Environment variables for the spawned process. pub env: HashMap<String, String>, /// Working directory for the spawned process. pub cwd: Option<PathBuf>,} /// A command transformed to run inside the platform sandbox. Plain data: spawn/// it however you like (PTY, `std::process`, …). The resources that must/// outlive the spawned process live in the [`Sandbox`] that produced this, not/// here — keep the `Sandbox` alive for the command's duration.#[derive(Clone, Debug, Default, Eq, PartialEq)]pub struct WrappedCommand { pub program: String, pub args: Vec<String>, pub env: HashMap<String, String>, pub cwd: Option<PathBuf>,} /// Errors returned by the sandbox abstraction.#[derive(Debug, Clone, Eq, PartialEq)]pub enum SandboxError { /// No sandbox implementation is available for this platform. UnsupportedPlatform, /// No usable Bubblewrap executable was found. BwrapNotFound, /// The only Bubblewrap executable found is setuid-root, which is refused. BwrapSetuidRejected, /// Bubblewrap was found but failed to create the sandbox. SandboxProbeFailed, /// The sandbox bridge executable path could not be resolved. BridgeExecutableUnavailable(String), /// Windows sandboxing through WSL is unavailable. WslUnavailable(String), /// The requested sandbox policy is not supported on this platform. UnsupportedPolicy(String), /// The sandbox request is invalid (e.g. a malformed allowed-domain). InvalidRequest(String), /// An I/O error occurred (e.g. spawning the network proxy). Io(String), /// Any other sandbox setup failure. Other(String),} impl fmt::Display for SandboxError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { SandboxError::UnsupportedPlatform => { write!(formatter, "sandboxing is not supported on this platform") } SandboxError::BwrapNotFound => { write!(formatter, "no usable `bwrap` binary was found on PATH") } SandboxError::BwrapSetuidRejected => write!( formatter, "the only available `bwrap` is setuid-root, which Zed refuses to run" ), SandboxError::SandboxProbeFailed => { write!( formatter, "`bwrap` is present but failed to create a sandbox" ) } SandboxError::BridgeExecutableUnavailable(message) => write!( formatter, "failed to resolve sandbox bridge executable: {message}" ), SandboxError::WslUnavailable(message) => write!(formatter, "{message}"), SandboxError::UnsupportedPolicy(message) => write!(formatter, "{message}"), SandboxError::InvalidRequest(message) => write!(formatter, "{message}"), SandboxError::Io(message) => write!(formatter, "{message}"), SandboxError::Other(message) => write!(formatter, "{message}"), } }} impl std::error::Error for SandboxError {} /// Resolved filesystem setup derived from [`SandboxFsPolicy`].struct FsSetup { allow_fs_write: bool, writable_paths: Vec<HostFilesystemLocation>, protected_paths: Vec<HostFilesystemLocation>,} /// Resolved network plan derived from [`SandboxNetPolicy`]. For the restricted/// case the allowlist is parsed up front; the proxy itself is spawned lazily on/// the first [`Sandbox::wrap`] so its upstream-proxy chaining can read the/// command's environment.enum NetSetup { Unrestricted, Blocked, // Restricted networking is rejected up front on Windows, so this variant is // never constructed there. #[cfg(not(target_os = "windows"))] Restricted { allowlist: Allowlist, },} /// A live sandbox: it owns the per-policy resources (the network proxy, and on/// macOS the temporary Seatbelt policy file) and produces sandboxed command/// invocations via [`Sandbox::wrap`] / [`Sandbox::execute`].////// Keep it alive for as long as commands wrapped by it are running; dropping it/// tears down the proxy.pub struct Sandbox { fs: FsSetup, network: NetSetup, /// In-process network proxy for the restricted-network case, spawned on the /// first `wrap`. Dropped on a background thread (the join blocks). proxy: Option<ProxyHandle>, /// Linux only: the host endpoint that hands the in-sandbox validator the /// captured `O_PATH` fds over a unix socket. Runs entirely in-process (a /// short-lived background thread, never a separate process) and is owned by /// this `Sandbox` — which is created per command — so it comes up when the /// command is wrapped and is torn down (thread stopped, socket removed) when /// the command finishes. Holds the fds, keeping their inodes pinned until /// then. Created lazily on the wrap that first needs it (a restricted-fs run /// with writable binds); a `Sandbox` normally wraps a single command. #[cfg(target_os = "linux")] validation_fd_sender: Option<linux_bubblewrap::ValidationFdSender>, /// Windows only: `(release channel, version)` of the Linux `zed` to /// provision inside WSL as the `--wsl-sandbox-helper` (version `latest` for /// dev builds). Set by the caller (which has the running release info); /// `None` falls back to exec'ing bwrap directly without in-sandbox bind /// validation. #[cfg(target_os = "windows")] wsl_zed_release: Option<(String, String)>, #[cfg(target_os = "macos")] seatbelt_config: Option<macos_seatbelt::SeatbeltConfigFile>,} impl Sandbox { /// Create a sandbox for `policy`, validating it for the current platform. /// /// This does not spawn the network proxy or probe `bwrap`; the proxy is /// created lazily on the first [`Sandbox::wrap`] (so it can chain through an /// upstream proxy named in the command's environment), and `bwrap`Lines 1–500 of 1481. Load more
//! Cross-platform sandboxing for commands run on behalf of the agent. //! //! The public API is intentionally platform-neutral. Internally, macOS uses //! Seatbelt (`sandbox-exec`), Linux uses Bubblewrap (`bwrap`), and Windows uses //! Bubblewrap inside WSL. Restricted-network policies are enforced by an //! in-process HTTP/HTTPS proxy (the `http_proxy` crate) that this crate //! constructs and owns; callers only describe intent via [`SandboxPolicy`]. use std::{ collections::HashMap, fmt, path::{Path, PathBuf}, process::Output, }; use http_proxy::ProxyHandle; #[cfg(not(target_os = "windows"))] use http_proxy::{Allowlist, HostPattern, ProxyConfig, ProxyEvent, UpstreamProxy}; #[cfg(target_os = "linux")] mod linux_bubblewrap; #[cfg(target_os = "macos")] mod macos_seatbelt; #[cfg(target_os = "windows")] mod windows_wsl; mod util; #[cfg(target_os = "macos")] use util::canonicalize_allowing_missing_leaf; #[cfg(target_os = "linux")] use util::{CanonicalPathBuf, linux_fd_identity}; pub use util::{ HostFilesystemLocation, HostFilesystemLocationDisplay, normalize_host_filesystem_locations, resolve_canonical, }; #[cfg(target_os = "windows")] pub use windows_wsl::{ResolvedGrant, resolve_canonical_for_grant}; #[cfg(target_os = "windows")] pub(crate) const WSL_SANDBOX_UNAVAILABLE_PREFIX: &str = "Windows sandboxing via WSL is unavailable"; /// A path *inside the sandbox* — i.e. where a host location is exposed in the /// sandboxed process's view of the filesystem (for example, a bind-mount /// destination on Linux). /// /// Unlike [`HostFilesystemLocation`], this needs no hardening and is just a thin /// wrapper around a [`PathBuf`]. It only names a location within the sandbox's /// own namespace: the worst a tampered sandbox-side path can do is expose the /// (already-granted) host files at a *different* path inside the sandbox — it can /// never widen which host files are reachable. It is therefore fine to build one /// from an ordinary, even attacker-influenced, path. #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct SandboxFilesystemLocation(PathBuf); impl SandboxFilesystemLocation { /// Name a location inside the sandbox's filesystem view. pub fn new(path: impl Into<PathBuf>) -> Self { Self(path.into()) } /// The in-sandbox path. Safe to read: this is not a trusted host identity. pub fn as_path(&self) -> &Path { &self.0 } /// Consume this wrapper, yielding the underlying in-sandbox path. pub fn into_path_buf(self) -> PathBuf { self.0 } } impl From<PathBuf> for SandboxFilesystemLocation { fn from(path: PathBuf) -> Self { Self(path) } } /// What a command is allowed to do, expressed as intent. This is the entire /// public configuration surface; how each policy is enforced (Seatbelt rules, /// Bubblewrap flags, a loopback proxy, …) is an implementation detail. #[derive(Clone, Debug, Eq, PartialEq)] pub struct SandboxPolicy { pub fs: SandboxFsPolicy, pub network: SandboxNetPolicy, } /// Filesystem policy for a sandboxed command. #[derive(Clone, Debug, Eq, PartialEq)] pub enum SandboxFsPolicy { /// Allow unrestricted filesystem writes except for protected paths, which /// remain readable but not writable. Unrestricted { protected_paths: Vec<HostFilesystemLocation>, }, /// Reads are allowed everywhere; writes are confined to these locations /// (and the standard ephemeral locations the platform provides). Each is a /// [`HostFilesystemLocation`] captured at validation time, never a bare path /// the enforcement layer would re-resolve. Restricted { writable_paths: Vec<HostFilesystemLocation>, protected_paths: Vec<HostFilesystemLocation>, }, } /// Outbound-network policy for a sandboxed command. #[derive(Clone, Debug, Eq, PartialEq)] pub enum SandboxNetPolicy { /// Allow unrestricted outbound network access. Unrestricted, /// Block all outbound network access. Blocked, /// Allow outbound HTTP(S) only to these hostnames (exact hosts or /// leading-`*.` subdomain wildcards), enforced by an in-process proxy. Restricted { allowed_domains: Vec<String> }, } /// Host paths that should remain readable but not writable, even when they fall /// under a writable subtree. /// /// The caller computes this list because the sandbox layer does not know which /// application-specific paths need stronger protection. pub type ProtectedPaths = Vec<HostFilesystemLocation>; /// Whether `path` (a host path) resides on a Windows drive reached through WSL's /// DrvFs, where sandbox-integrity guarantees are weaker than on the distro's /// native filesystem. Always `false` off Windows, where no such filesystem /// exists. This is a cheap structural check on the path shape; the authoritative /// per-grant classification is done in WSL by [`resolve_canonical_for_grant`]. pub fn path_is_on_windows_drive(path: &Path) -> bool { #[cfg(target_os = "windows")] { windows_wsl::path_is_on_windows_drive(path) } #[cfg(not(target_os = "windows"))] { let _ = path; false } } impl SandboxPolicy { /// Combine two policy layers. Filesystem/network grants are unioned into the /// least-restrictive policy that satisfies both layers, while protected paths /// within restricted filesystem policies are unioned so every layer's /// protected subtrees remain protected. pub fn merge(self, other: SandboxPolicy) -> SandboxPolicy { SandboxPolicy { fs: self.fs.merge(other.fs), network: self.network.merge(other.network), } } /// Replace the protected paths in the filesystem policy, keeping writable /// paths and network policy unchanged. pub fn with_protected_paths(mut self, protected_paths: Vec<HostFilesystemLocation>) -> Self { match &mut self.fs { SandboxFsPolicy::Unrestricted { protected_paths: existing, } | SandboxFsPolicy::Restricted { protected_paths: existing, .. } => *existing = protected_paths, } self } } fn merge_locations( locations: Vec<HostFilesystemLocation>, other: Vec<HostFilesystemLocation>, ) -> Vec<HostFilesystemLocation> { // Union the two sets and reduce to a minimal cover, so a location nested // under another (in either input) is dropped rather than bound redundantly. normalize_host_filesystem_locations(locations.into_iter().chain(other)) } fn validate_writable_paths_do_not_overlap_protected_paths( writable_paths: &[HostFilesystemLocation], protected_paths: &[HostFilesystemLocation], ) -> Result<(), SandboxError> { for writable_path in writable_paths { for protected_path in protected_paths { if writable_path_overlaps_protected_path(writable_path, protected_path) { return Err(SandboxError::InvalidRequest(format!( "writable sandbox path `{}` overlaps protected path `{}`", writable_path.untrusted_path_display(), protected_path.untrusted_path_display() ))); } } } Ok(()) } #[cfg(target_os = "linux")] fn writable_path_overlaps_protected_path( writable_path: &HostFilesystemLocation, protected_path: &HostFilesystemLocation, ) -> bool { linux_location_is_equal_or_descendant(writable_path, protected_path) } #[cfg(target_os = "macos")] fn writable_path_overlaps_protected_path( writable_path: &HostFilesystemLocation, protected_path: &HostFilesystemLocation, ) -> bool { writable_path .macos_canonical_path() .starts_with(protected_path.macos_canonical_path()) } #[cfg(not(any(target_os = "linux", target_os = "macos")))] fn writable_path_overlaps_protected_path( writable_path: &HostFilesystemLocation, protected_path: &HostFilesystemLocation, ) -> bool { writable_path .untrusted_raw_path() .starts_with(protected_path.untrusted_raw_path()) } #[cfg(target_os = "linux")] fn linux_location_is_equal_or_descendant( location: &HostFilesystemLocation, ancestor: &HostFilesystemLocation, ) -> bool { use std::os::fd::{AsRawFd as _, FromRawFd as _, OwnedFd}; if location == ancestor { return true; } let Ok(mut current) = location.linux_dup_fd() else { log::warn!("failed to duplicate sandbox location fd while checking protected path overlap"); return false; }; loop { let parent = unsafe { let fd = libc::openat( current.as_raw_fd(), c"..".as_ptr(), libc::O_PATH | libc::O_DIRECTORY | libc::O_CLOEXEC, ); if fd < 0 { return false; } OwnedFd::from_raw_fd(fd) }; let Some(parent_identity) = linux_fd_identity(parent.as_raw_fd()) else { log::warn!("failed to fstat parent fd while checking protected path overlap"); return false; }; let Some(current_identity) = linux_fd_identity(current.as_raw_fd()) else { log::warn!("failed to fstat current fd while checking protected path overlap"); return false; }; let Some(ancestor_identity) = linux_fd_identity(ancestor.linux_fd().as_raw_fd()) else { log::warn!("failed to fstat protected fd while checking protected path overlap"); return false; }; if parent_identity == ancestor_identity { return true; } if parent_identity == current_identity { return false; } current = parent; } } impl SandboxFsPolicy { /// Unrestricted access dominates; otherwise the writable subtrees union. pub fn merge(self, other: SandboxFsPolicy) -> SandboxFsPolicy { match (self, other) { ( SandboxFsPolicy::Unrestricted { protected_paths: protected_a, }, SandboxFsPolicy::Unrestricted { protected_paths: protected_b, }, ) => SandboxFsPolicy::Unrestricted { protected_paths: merge_locations(protected_a, protected_b), }, ( SandboxFsPolicy::Unrestricted { protected_paths: protected_a, }, SandboxFsPolicy::Restricted { protected_paths: protected_b, .. }, ) | ( SandboxFsPolicy::Restricted { protected_paths: protected_a, .. }, SandboxFsPolicy::Unrestricted { protected_paths: protected_b, }, ) => SandboxFsPolicy::Unrestricted { protected_paths: merge_locations(protected_a, protected_b), }, ( SandboxFsPolicy::Restricted { writable_paths: writable_a, protected_paths: protected_a, }, SandboxFsPolicy::Restricted { writable_paths: writable_b, protected_paths: protected_b, }, ) => SandboxFsPolicy::Restricted { writable_paths: merge_locations(writable_a, writable_b), protected_paths: merge_locations(protected_a, protected_b), }, } } } impl SandboxNetPolicy { /// Unrestricted access dominates and `Blocked` is the identity; otherwise the /// allowed domains union. pub fn merge(self, other: SandboxNetPolicy) -> SandboxNetPolicy { match (self, other) { (SandboxNetPolicy::Unrestricted, _) | (_, SandboxNetPolicy::Unrestricted) => { SandboxNetPolicy::Unrestricted } (SandboxNetPolicy::Blocked, other) | (other, SandboxNetPolicy::Blocked) => other, ( SandboxNetPolicy::Restricted { allowed_domains: mut a, }, SandboxNetPolicy::Restricted { allowed_domains: b }, ) => { for domain in b { if !a.contains(&domain) { a.push(domain); } } SandboxNetPolicy::Restricted { allowed_domains: a } } } } } /// A command and its execution environment, before sandbox wrapping. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct CommandAndArgs { /// Program to execute. pub program: String, /// Arguments passed to `program`. pub args: Vec<String>, /// Environment variables for the spawned process. pub env: HashMap<String, String>, /// Working directory for the spawned process. pub cwd: Option<PathBuf>, } /// A command transformed to run inside the platform sandbox. Plain data: spawn /// it however you like (PTY, `std::process`, …). The resources that must /// outlive the spawned process live in the [`Sandbox`] that produced this, not /// here — keep the `Sandbox` alive for the command's duration. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct WrappedCommand { pub program: String, pub args: Vec<String>, pub env: HashMap<String, String>, pub cwd: Option<PathBuf>, } /// Errors returned by the sandbox abstraction. #[derive(Debug, Clone, Eq, PartialEq)] pub enum SandboxError { /// No sandbox implementation is available for this platform. UnsupportedPlatform, /// No usable Bubblewrap executable was found. BwrapNotFound, /// The only Bubblewrap executable found is setuid-root, which is refused. BwrapSetuidRejected, /// Bubblewrap was found but failed to create the sandbox. SandboxProbeFailed, /// The sandbox bridge executable path could not be resolved. BridgeExecutableUnavailable(String), /// Windows sandboxing through WSL is unavailable. WslUnavailable(String), /// The requested sandbox policy is not supported on this platform. UnsupportedPolicy(String), /// The sandbox request is invalid (e.g. a malformed allowed-domain). InvalidRequest(String), /// An I/O error occurred (e.g. spawning the network proxy). Io(String), /// Any other sandbox setup failure. Other(String), } impl fmt::Display for SandboxError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { SandboxError::UnsupportedPlatform => { write!(formatter, "sandboxing is not supported on this platform") } SandboxError::BwrapNotFound => { write!(formatter, "no usable `bwrap` binary was found on PATH") } SandboxError::BwrapSetuidRejected => write!( formatter, "the only available `bwrap` is setuid-root, which Zed refuses to run" ), SandboxError::SandboxProbeFailed => { write!( formatter, "`bwrap` is present but failed to create a sandbox" ) } SandboxError::BridgeExecutableUnavailable(message) => write!( formatter, "failed to resolve sandbox bridge executable: {message}" ), SandboxError::WslUnavailable(message) => write!(formatter, "{message}"), SandboxError::UnsupportedPolicy(message) => write!(formatter, "{message}"), SandboxError::InvalidRequest(message) => write!(formatter, "{message}"), SandboxError::Io(message) => write!(formatter, "{message}"), SandboxError::Other(message) => write!(formatter, "{message}"), } } } impl std::error::Error for SandboxError {} /// Resolved filesystem setup derived from [`SandboxFsPolicy`]. struct FsSetup { allow_fs_write: bool, writable_paths: Vec<HostFilesystemLocation>, protected_paths: Vec<HostFilesystemLocation>, } /// Resolved network plan derived from [`SandboxNetPolicy`]. For the restricted /// case the allowlist is parsed up front; the proxy itself is spawned lazily on /// the first [`Sandbox::wrap`] so its upstream-proxy chaining can read the /// command's environment. enum NetSetup { Unrestricted, Blocked, // Restricted networking is rejected up front on Windows, so this variant is // never constructed there. #[cfg(not(target_os = "windows"))] Restricted { allowlist: Allowlist, }, } /// A live sandbox: it owns the per-policy resources (the network proxy, and on /// macOS the temporary Seatbelt policy file) and produces sandboxed command /// invocations via [`Sandbox::wrap`] / [`Sandbox::execute`]. /// /// Keep it alive for as long as commands wrapped by it are running; dropping it /// tears down the proxy. pub struct Sandbox { fs: FsSetup, network: NetSetup, /// In-process network proxy for the restricted-network case, spawned on the /// first `wrap`. Dropped on a background thread (the join blocks). proxy: Option<ProxyHandle>, /// Linux only: the host endpoint that hands the in-sandbox validator the /// captured `O_PATH` fds over a unix socket. Runs entirely in-process (a /// short-lived background thread, never a separate process) and is owned by /// this `Sandbox` — which is created per command — so it comes up when the /// command is wrapped and is torn down (thread stopped, socket removed) when /// the command finishes. Holds the fds, keeping their inodes pinned until /// then. Created lazily on the wrap that first needs it (a restricted-fs run /// with writable binds); a `Sandbox` normally wraps a single command. #[cfg(target_os = "linux")] validation_fd_sender: Option<linux_bubblewrap::ValidationFdSender>, /// Windows only: `(release channel, version)` of the Linux `zed` to /// provision inside WSL as the `--wsl-sandbox-helper` (version `latest` for /// dev builds). Set by the caller (which has the running release info); /// `None` falls back to exec'ing bwrap directly without in-sandbox bind /// validation. #[cfg(target_os = "windows")] wsl_zed_release: Option<(String, String)>, #[cfg(target_os = "macos")] seatbelt_config: Option<macos_seatbelt::SeatbeltConfigFile>, } impl Sandbox { /// Create a sandbox for `policy`, validating it for the current platform. /// /// This does not spawn the network proxy or probe `bwrap`; the proxy is /// created lazily on the first [`Sandbox::wrap`] (so it can chain through an /// upstream proxy named in the command's environment), and `bwrap`