//! Linux sandbox integration built on Bubblewrap (`bwrap`) for filesystem and
//! network confinement.
//!
//! We can use `--bind` and `--ro-bind` (read-only) to bind host filesystem
//! paths to paths in the sandbox. If networking is restricted, we also set
//! `--unshare-net` to disable *all* network access.
//!
//! When restricting network access, we:
//! - set `--unshare-net` - any requests to `example.com` will fail
//!   - requests to `localhost` will succeed, but it will be an isolated localhost
//!     from the host system.
//! - create a unix socket, and mount it in the sandbox with `--bind`
//! - run a bridge process inside the sandbox that:
//!   - listens on `localhost:<port>` and forwards reads/writes to the socket
//!   - then, runs the untrusted command
//! - on the zed side, we listen to the socket and forward reads/writes to the
//!   internal HTTP proxy
//!
//! If networking is fully blocked or fully allowed, we don't bother with the
//! proxy/socket at all (and simply set/unset `--unshare-net`).
//!
//! This design for networking avoids needing seccomp, a fork/exec dance, and
//! eliminates a race condition involving BPF user notifications.

use anyhow::{Context as _, Result, anyhow, bail};
use std::ffi::{OsStr, OsString};
use std::io::{Read, Write};
use std::net::{Ipv4Addr, Shutdown, TcpListener, TcpStream};
use std::os::fd::{AsRawFd as _, FromRawFd as _, OwnedFd, RawFd};
use std::os::unix::fs::MetadataExt as _;
use std::os::unix::net::{UnixListener, UnixStream};
use std::os::unix::process::{CommandExt as _, ExitStatusExt as _};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::thread;

/// Re-exec marker for the in-sandbox launcher: it runs inside the sandbox before
/// the real command to (a) validate that bwrap bound the writable grants to the
/// inodes we captured (the bind-source TOCTOU backstop) and (b) run the
/// restricted-network HTTP bridge. See `README.md` for the design.
const LAUNCHER_FLAG: &str = "--zed-linux-sandbox-launcher";
/// Re-exec marker for the WSL-side helper. This runs *inside WSL* (a Linux
/// process) and does what `Sandbox::wrap` + the validation-fd sender do
/// in-process on native Linux: capture the writable binds' `O_PATH` fds, stand
/// up the validation socket, assemble the bwrap invocation, and spawn it. The
/// capture must happen WSL-side because a Windows process holds no Linux fds.
/// See `README.md`. Shared with the Windows side via `crate::WSL_SANDBOX_HELPER_FLAG`.
const WSL_HELPER_FLAG: &str = crate::WSL_SANDBOX_HELPER_FLAG;
const WSL_RESOLVE_FLAG: &str = crate::WSL_SANDBOX_RESOLVE_FLAG;
/// Sentinel argv token meaning "this optional field is absent".
const LAUNCHER_NONE: &str = "-";
const PROXY_SOCKET_SANDBOX_PATH_PREFIX: &str = "/tmp/zed-sandbox";
const VALIDATION_SOCKET_SANDBOX_PATH_PREFIX: &str = "/tmp/zed-sandbox-validate";
const SANDBOX_SETUP_FAILED_EXIT_CODE: i32 = 126;
const PUMP_BUFFER_SIZE: usize = 64 * 1024;
/// Upper bound on writable binds validated in a single `SCM_RIGHTS` message,
/// kept comfortably below the kernel's per-message fd limit (`SCM_MAX_FD`, 253).
/// Exceeding it fails closed rather than silently validating a subset.
const MAX_VALIDATED_BINDS: usize = 200;

/// Network-access setting for a sandboxed command.
///
/// Mirrors [`crate::macos_seatbelt::NetworkAccess`] so Linux and macOS expose
/// the same public shape.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum NetworkAccess {
    /// All outbound network blocked (own network namespace).
    #[default]
    None,
    /// Outbound HTTP(S) is available through a loopback proxy port inside the
    /// sandbox. The port is bridged to a host-side Unix socket proxy that
    /// enforces the hostname allowlist.
    LocalhostPort(u16),
    /// All outbound network allowed.
    All,
}

/// Per-command relaxations of the default Bubblewrap (Linux) sandbox.
///
/// Mirrors [`crate::macos_seatbelt::SandboxPermissions`].
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct SandboxPermissions {
    /// Network access policy for the command.
    pub network: NetworkAccess,
    /// Allow unrestricted filesystem writes.
    pub allow_fs_write: bool,
}

/// The outcome of preparing a Linux sandbox.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LauncherStatus {
    /// No usable `bwrap` binary was found on `PATH` (or bundled).
    BwrapNotFound,
    /// The only `bwrap` found is setuid-root, which we refuse to execute.
    SetuidRejected,
    /// `bwrap` is present but failed to set up the sandbox with our arguments.
    SandboxProbeFailed,
}

impl LauncherStatus {
    /// A human-readable explanation suitable for diagnostics.
    pub fn describe(self) -> &'static str {
        match self {
            LauncherStatus::BwrapNotFound => "no usable `bwrap` binary was found on PATH",
            LauncherStatus::SetuidRejected => {
                "the only available `bwrap` is setuid-root, which Zed refuses to run"
            }
            LauncherStatus::SandboxProbeFailed => {
                "`bwrap` is present but failed to create a sandbox (unprivileged user \
                 namespaces may be disabled)"
            }
        }
    }
}

/// Where a `bwrap` lookup ended up.
enum BwrapLocation {
    /// A usable, non-setuid `bwrap` binary.
    Found(PathBuf),
    /// `bwrap` exists but every candidate is setuid-root (which we won't run).
    OnlySetuid,
    /// No `bwrap` binary was found at all.
    NotFound,
}

fn locate_bwrap() -> BwrapLocation {
    let mut saw_setuid = false;
    for candidate in candidate_bwrap_paths() {
        if !candidate.is_file() {
            continue;
        }
        if is_setuid_root(&candidate) {
            saw_setuid = true;
            continue;
        }
        return BwrapLocation::Found(candidate);
    }
    if saw_setuid {
        BwrapLocation::OnlySetuid
    } else {
        BwrapLocation::NotFound
    }
}

fn candidate_bwrap_paths() -> Vec<PathBuf> {
    let mut candidates = Vec::new();
    if let Some(system) = system_bwrap_path() {
        candidates.push(system);
    }
    if let Some(bundled) = bundled_bwrap_path() {
        candidates.push(bundled);
    }
    candidates
}

fn system_bwrap_path() -> Option<PathBuf> {
    let path = std::env::var_os("PATH")?;
    std::env::split_paths(&path)
        .map(|directory| directory.join("bwrap"))
        .find(|candidate| candidate.is_file())
}

fn bundled_bwrap_path() -> Option<PathBuf> {
    None
}

fn is_setuid_root(path: &Path) -> bool {
    match std::fs::metadata(path) {
        Ok(metadata) => (metadata.mode() & libc::S_ISUID != 0) && metadata.uid() == 0,
        Err(_) => false,
    }
}

#[allow(
    clippy::disallowed_methods,
    reason = "the probe is a short-lived background operation that must block on bwrap"
)]
fn probe_bwrap(bwrap: &Path, bwrap_args: &[String]) -> bool {
    // Capture stderr (rather than discarding it) so a failed probe can report
    // *why* bwrap refused — the difference between "user namespaces disabled",
    // "chdir target missing", "no permission", etc. — instead of a bare
    // `SandboxProbeFailed`.
    let output = Command::new(bwrap)
        .args(bwrap_args)
        .arg("--")
        .arg("true")
        .stdin(Stdio::null())
        .stdout(Stdio::null())
        .stderr(Stdio::piped())
        .output();

    match output {
        Ok(output) if output.status.success() => true,
        Ok(output) => {
            log::warn!(
                "[sandbox] bwrap probe failed ({}). command: {} {}\nbwrap stderr: {}",
                output.status,
                bwrap.display(),
                bwrap_args.join(" "),
                String::from_utf8_lossy(&output.stderr).trim()
            );
            false
        }
        Err(error) => {
            log::warn!(
                "[sandbox] bwrap probe could not be spawned: {error}. command: {} {}",
                bwrap.display(),
                bwrap_args.join(" ")
            );
            false
        }
    }
}

/// Build the `bwrap` argument list (everything after the `bwrap` program and
/// before the trailing `-- <command>`) for the given policy.
///
/// `proxy_socket_path` is the host pathname Unix socket used for
/// [`NetworkAccess::LocalhostPort`]. It is bind-mounted to a unique path inside
/// the sandbox where the bridge connects to it.
pub fn build_bwrap_args(
    writable_directories: &[&Path],
    protected_paths: &[&Path],
    permissions: SandboxPermissions,
    cwd: Option<&Path>,
    proxy_socket_path: Option<&Path>,
) -> Vec<String> {
    let proxy_socket_sandbox_path = proxy_socket_path
        .filter(|_| matches!(permissions.network, NetworkAccess::LocalhostPort(_)))
        .map(|_| unique_proxy_socket_sandbox_path());
    build_bwrap_args_with_sandbox_paths(
        writable_directories,
        protected_paths,
        permissions,
        cwd,
        proxy_socket_path,
        proxy_socket_sandbox_path.as_deref(),
        None,
        None,
    )
}

#[allow(
    clippy::too_many_arguments,
    reason = "a flat arg list mirrors the bwrap flags this assembles"
)]
fn build_bwrap_args_with_sandbox_paths(
    writable_directories: &[&Path],
    protected_paths: &[&Path],
    permissions: SandboxPermissions,
    cwd: Option<&Path>,
    proxy_socket_path: Option<&Path>,
    proxy_socket_sandbox_path: Option<&Path>,
    validation_socket_path: Option<&Path>,
    validation_socket_sandbox_path: Option<&Path>,
) -> Vec<String> {
    let mut args = Vec::new();

    let root_bind = if permissions.allow_fs_write {
        "--bind"
    } else {
        "--ro-bind"
    };
    push_bind(&mut args, root_bind, "/", "/");

    args.push("--dev".to_string());
    args.push("/dev".to_string());
    args.push("--proc".to_string());
    args.push("/proc".to_string());

    if !permissions.allow_fs_write {
        args.push("--tmpfs".to_string());
        args.push("/tmp".to_string());

        for directory in writable_directories {
            // Bind each writable directory at its *exact* path, **verbatim** —
            // never re-`canonicalize`d here. The path was already resolved once,
            // at capture time, to the pinned inode's current path (`readlink` of
            // the `O_PATH` fd); re-resolving it now would reopen the
            // time-of-check-to-time-of-use gap a malicious swap exploits. We must
            // also never widen to an existing ancestor, so a path that doesn't
            // exist is skipped rather than falling back to a parent. Whatever
            // inode bwrap actually binds is verified after the mounts by the
            // in-sandbox launcher (see `validate_binds`), which fails closed on a
            // mismatch.
            if !directory.exists() {
                continue;
            }
            let path = directory.to_string_lossy().into_owned();
            push_bind(&mut args, "--bind", &path, &path);
        }
    }

    // Protect requested paths by re-binding them read-only *over* any broader rw
    // bind (order matters: later binds win). Unlike Seatbelt, bwrap can't deny
    // writes while allowing content reads with a policy rule, so a protected path
    // is read-only — its contents stay readable but can't be written. A
    // not-yet-existing path can't be bound, so it's skipped.
    for protected_path in protected_paths {
        if !protected_path.exists() {
            continue;
        }
        let path = protected_path.to_string_lossy().into_owned();
        push_bind(&mut args, "--ro-bind", &path, &path);
    }

    for flag in [
        "--unshare-user",
        "--unshare-ipc",
        "--unshare-uts",
        "--unshare-pid",
        "--unshare-cgroup-try",
        "--die-with-parent",
    ] {
        args.push(flag.to_string());
    }

    match permissions.network {
        NetworkAccess::None => args.push("--unshare-net".to_string()),
        NetworkAccess::LocalhostPort(_) => {
            args.push("--unshare-net".to_string());
            if let Some((proxy_socket_path, proxy_socket_sandbox_path)) =
                proxy_socket_path.zip(proxy_socket_sandbox_path)
            {
                let source = proxy_socket_path.to_string_lossy().into_owned();
                let destination = proxy_socket_sandbox_path.to_string_lossy().into_owned();
                push_bind(&mut args, "--bind", &source, &destination);
            }
        }
        NetworkAccess::All => {}
    }

    // The validation socket is filesystem-based, so it works regardless of the
    // network policy (an `--unshare-net`'d sandbox can't reach an abstract
    // socket, but a bind-mounted one is fine). Bind it after the `/tmp` tmpfs so
    // it isn't shadowed by the overlay.
    if let Some((source, destination)) = validation_socket_path.zip(validation_socket_sandbox_path)
    {
        let source = source.to_string_lossy().into_owned();
        let destination = destination.to_string_lossy().into_owned();
        push_bind(&mut args, "--bind", &source, &destination);
    }

    if let Some(cwd) = cwd {
        args.push("--chdir".to_string());
        args.push(cwd.to_string_lossy().into_owned());
    }

    args
}

/// A unique destination path for the proxy socket bind mount *inside* the
/// sandbox. Each sandboxed command runs in its own mount namespace, so the path
/// only needs to be unique enough to avoid colliding with anything a previous
/// command in the same namespace lineage may have created; combining the pid
/// with a monotonic counter is sufficient and keeps the path predictable for
/// diagnostics.
fn unique_proxy_socket_sandbox_path() -> PathBuf {
    static COUNTER: AtomicU64 = AtomicU64::new(0);
    let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
    PathBuf::from(format!(
        "{PROXY_SOCKET_SANDBOX_PATH_PREFIX}-{}-{counter}.sock",
        std::process::id()
    ))
}

/// In-sandbox bind destination for the validation socket. Each sandboxed command
/// runs in its own mount namespace, so this only needs to avoid colliding with
/// other binds in the same namespace.
fn unique_validation_socket_sandbox_path() -> PathBuf {
    static COUNTER: AtomicU64 = AtomicU64::new(0);
    let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
    PathBuf::from(format!(
        "{VALIDATION_SOCKET_SANDBOX_PATH_PREFIX}-{}-{counter}.sock",
        std::process::id()
    ))
}

/// Create a private, owner-only directory for the validation listener socket and
/// return `(directory, socket path)`. The socket is a host path (it lives
/// outside the sandbox's `/tmp` tmpfs) and is bind-mounted in at
/// [`unique_validation_socket_sandbox_path`].
///
/// The socket carries the captured `O_PATH` descriptors over `SCM_RIGHTS`, so
/// connect access must be confined to us. Rather than bind in shared `/tmp` and
/// `chmod` after (which leaves a window where another user could connect before
/// the `chmod` lands), we create a `0700` directory up front: `create` is atomic
/// and fails if the path already exists, and `mode(0o700)` makes it
/// owner-only from creation, so no other user can even traverse to the socket.
fn unique_validation_socket_host_path() -> std::io::Result<(PathBuf, PathBuf)> {
    use std::os::unix::fs::DirBuilderExt as _;
    static COUNTER: AtomicU64 = AtomicU64::new(0);
    let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
    let dir = std::env::temp_dir().join(format!(
        "zed-sandbox-validate-{}-{counter}",
        std::process::id()
    ));
    // Clear a stale directory left by a previous run that reused this pid; this
    // only succeeds for a directory we own, so it can't be used to displace
    // another user's squatted path (in which case `create` below fails closed).
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::DirBuilder::new().mode(0o700).create(&dir)?;
    let socket_path = dir.join("validate.sock");
    Ok((dir, socket_path))
}

/// Host endpoint that hands the in-sandbox validator the captured `O_PATH`
/// descriptors for the writable binds.
///
/// Runs in-process: `spawn` starts a short-lived background **thread** (never a
/// separate process) that listens on an owner-only `AF_UNIX` socket and serves
/// the descriptors to **exactly one** client (the in-sandbox validator) via
/// `SCM_RIGHTS`, then closes and removes the socket. The validator connects
/// once, after bwrap has mounted the binds; `SCM_RIGHTS` keeps the descriptors
/// alive in flight, so once the single send completes there is nothing left to
/// serve. Holding the descriptors until that send keeps the captured inodes
/// pinned (so they can't be recycled) up to the moment they're handed off.
///
/// It is owned by the per-command [`Sandbox`](crate::Sandbox); `Drop` wakes a
/// still-blocked accept and removes the socket and its private directory (a
/// no-op if the thread already tore them down). The socket is bind-mounted into
/// the sandbox at [`Self::sandbox_socket_path`].
pub(crate) struct ValidationFdSender {
    host_socket_dir: PathBuf,
    host_socket_path: PathBuf,
    sandbox_socket_path: PathBuf,
    shutdown: Arc<AtomicBool>,
}

impl ValidationFdSender {
    /// Start serving `fds` (one per writable bind, in bind order). The caller
    /// must pass the same order to the launcher so each fd lines up with its
    /// bind-destination path.
    pub(crate) fn spawn(fds: Vec<OwnedFd>) -> std::io::Result<Self> {
        use std::os::unix::fs::PermissionsExt as _;

        if fds.len() > MAX_VALIDATED_BINDS {
            return Err(std::io::Error::other(format!(
                "too many writable binds to validate ({} > {MAX_VALIDATED_BINDS})",
                fds.len()
            )));
        }
        let (host_socket_dir, host_socket_path) = unique_validation_socket_host_path()?;
        let listener = UnixListener::bind(&host_socket_path)?;
        // The enclosing directory is already `0700`, so this is defense in depth:
        // connect permission on an `AF_UNIX` socket is governed by write
        // permission on the socket file, so keep it owner-only too.
        std::fs::set_permissions(&host_socket_path, std::fs::Permissions::from_mode(0o600))?;
        let sandbox_socket_path = unique_validation_socket_sandbox_path();
        let shutdown = Arc::new(AtomicBool::new(false));

        // We use std threading APIs here because this code is run from both the
        // Linux zed binary and also the WSL helper, which does not have a GPUI
        // runtime.
        thread::Builder::new()
            .name("zed-sandbox-validation".to_string())
            .spawn({
                let shutdown = shutdown.clone();
                let host_socket_path = host_socket_path.clone();
                move || {
                    // `fds` is held until the single send below, keeping the
                    // pinned inodes alive until they're handed to the validator.
                    let raw_fds: Vec<RawFd> = fds.iter().map(|fd| fd.as_raw_fd()).collect();
                    // Serve exactly one client: the in-sandbox validator connects
                    // once. A second connection (e.g. the wake from `Drop`) is
                    // distinguished by the `shutdown` flag and never served.
                    match listener.accept() {
                        Ok((stream, _)) => {
                            if !shutdown.load(Ordering::SeqCst) {
                                if let Err(error) = send_fds(&stream, &raw_fds) {
                                    log::warn!("[sandbox] failed to send validation fds: {error}");
                                }
                            }
                        }
                        Err(error) => {
                            if !shutdown.load(Ordering::SeqCst) {
                                log::warn!("[sandbox] sandbox validation accept failed: {error}");
                            }
                        }
                    }
                    drop(fds);
                    // Close the listener and remove the socket so it can't be
                    // connected to again. The directory is removed by `Drop`.
                    drop(listener);
                    let _ = std::fs::remove_file(&host_socket_path);
                }
            })?;

        Ok(Self {
            host_socket_dir,
            host_socket_path,
            sandbox_socket_path,
            shutdown,
        })
    }

    pub(crate) fn host_socket_path(&self) -> &Path {
        &self.host_socket_path
    }

    pub(crate) fn sandbox_socket_path(&self) -> &Path {
        &self.sandbox_socket_path
    }
}

impl Drop for ValidationFdSender {
    fn drop(&mut self) {
        // If the validator never connected (e.g. bwrap failed before the
        // launcher ran), the thread is still blocked in `accept`. Flag shutdown
        // and poke the socket so the accept returns and the thread exits without
        // serving the descriptors, then remove the socket and its private
        // directory (a no-op if the thread already removed the socket after its
        // single send).
        self.shutdown.store(true, Ordering::SeqCst);
        let _ = UnixStream::connect(&self.host_socket_path);
        let _ = std::fs::remove_file(&self.host_socket_path);
        let _ = std::fs::remove_dir_all(&self.host_socket_dir);
    }
}

fn push_bind(args: &mut Vec<String>, flag: &str, source: &str, destination: &str) {
    args.push(flag.to_string());
    args.push(source.to_string());
    args.push(destination.to_string());
}

fn resolve_bwrap() -> std::result::Result<PathBuf, LauncherStatus> {
    match locate_bwrap() {
        BwrapLocation::Found(path) => Ok(path),
        BwrapLocation::OnlySetuid => Err(LauncherStatus::SetuidRejected),
        BwrapLocation::NotFound => Err(LauncherStatus::BwrapNotFound),
    }
}

fn prepare_sandbox(
    permissions: SandboxPermissions,
) -> std::result::Result<(PathBuf, Vec<String>), LauncherStatus> {
    let bwrap = match resolve_bwrap() {
        Ok(bwrap) => bwrap,
        Err(status) => {
            log::warn!("[sandbox] cannot create sandbox: {}", status.describe());
            return Err(status);
        }
    };
    // The probe only answers "can a sandbox be created on this host at all", so
    // it runs a bare, representative sandbox: no writable grants, no protected
    // Git dirs, no proxy socket, and no `--chdir` into a command's working
    // directory. None of those affect createability, and binding them would make
    // the probe depend on per-command layout (e.g. a worktree under the `/tmp`
    // tmpfs that `--chdir` then can't reach).
    let bwrap_args = build_bwrap_args(&[], &[], permissions, None, None);
    if !probe_bwrap(&bwrap, &bwrap_args) {
        return Err(LauncherStatus::SandboxProbeFailed);
    }
    Ok((bwrap, bwrap_args))
}

/// Check whether an OS sandbox can be created on this host for this policy.
pub fn check_can_create_sandbox(
    permissions: SandboxPermissions,
) -> std::result::Result<(), LauncherStatus> {
    prepare_sandbox(permissions).map(|_| ())
}

/// The host (Zed-side) socket paths for the in-sandbox bind validator.
#[derive(Clone, Copy)]
pub struct ValidationSocket<'a> {
    /// Host pathname of the listener the validator connects back to.
    pub host_socket_path: &'a Path,
    /// In-sandbox path the host socket is bind-mounted to (where the validator
    /// actually connects).
    pub sandbox_socket_path: &'a Path,
}

/// Build the final command line that runs `program` inside Bubblewrap.
///
/// `bridge_program` should be the current Zed executable; it is re-exec'd inside
/// the sandbox as the launcher whenever bind validation and/or the
/// restricted-network bridge are needed, running before the real command.
///
/// The host `proxy_socket_path` and `validation_socket` are each bind-mounted to
/// a per-invocation path inside the sandbox, and those in-sandbox paths are
/// handed to the launcher.
#[allow(
    clippy::too_many_arguments,
    reason = "assembling a bwrap command line is inherently parameter-heavy"
)]
pub fn wrap_invocation(
    bridge_program: &str,
    permissions: SandboxPermissions,
    writable_dirs: &[&Path],
    protected_paths: &[&Path],
    cwd: Option<&Path>,
    program: &str,
    args: &[String],
    proxy_socket_path: Option<&Path>,
    validation_socket: Option<ValidationSocket<'_>>,
) -> Result<(String, Vec<String>)> {
    if matches!(permissions.network, NetworkAccess::LocalhostPort(_)) && proxy_socket_path.is_none()
    {
        bail!("restricted Linux network access requires a proxy Unix socket path");
    }
    if writable_dirs.len() > MAX_VALIDATED_BINDS {
        bail!(
            "too many writable binds to validate ({} > {MAX_VALIDATED_BINDS})",
            writable_dirs.len()
        );
    }

    // Every writable path must already exist: `bwrap` can't bind a nonexistent
    // source, and the command can't create it either (its parent is read-only
    // inside the sandbox). Callers hand us captured canonical paths whose
    // inodes were pinned at policy-construction time, so a missing path here
    // means the location vanished since capture. Never create it with the
    // agent's ambient permissions — recreating from path text would bind a
    // fresh, unapproved object in place of the pinned one (and historically
    // this materialized bogus paths, e.g. a granted *file* path springing into
    // existence as a directory). Running anyway would give the command silently
    // less access than it believes it has — so fail closed with a clear error.
    if !permissions.allow_fs_write {
        for directory in writable_dirs {
            if !directory.exists() {
                bail!(
                    "failed to provide writable sandbox path {}: it no longer exists",
                    directory.display()
                );
            }
        }
    }

    let bwrap = resolve_bwrap().map_err(|status| anyhow!(status.describe()))?;
    let proxy_socket_sandbox_path = match permissions.network {
        NetworkAccess::LocalhostPort(_) => Some(unique_proxy_socket_sandbox_path()),
        NetworkAccess::None | NetworkAccess::All => None,
    };
    let mut bwrap_args = build_bwrap_args_with_sandbox_paths(
        writable_dirs,
        protected_paths,
        permissions,
        cwd,
        proxy_socket_path,
        proxy_socket_sandbox_path.as_deref(),
        validation_socket.map(|socket| socket.host_socket_path),
        validation_socket.map(|socket| socket.sandbox_socket_path),
    );
    bwrap_args.push("--".to_string());

    let bridge = match permissions.network {
        NetworkAccess::LocalhostPort(port) => {
            let proxy_socket_sandbox_path = proxy_socket_sandbox_path
                .as_ref()
                .context("missing in-sandbox proxy socket path")?;
            Some((proxy_socket_sandbox_path.clone(), port))
        }
        NetworkAccess::None | NetworkAccess::All => None,
    };

    // Always route through the in-sandbox launcher, even when there are no
    // writable binds to validate and no restricted-network bridge: the launcher
    // is where the seccomp filter is installed on the untrusted command (see
    // `exec_command` / `run_bridge`). Absent fields are passed as the `-`
    // sentinel, and `run_launcher` then just installs the filter and `exec`s.
    bwrap_args.push(bridge_program.to_string());
    bwrap_args.push(LAUNCHER_FLAG.to_string());
    // Field 1: validation socket (in-sandbox path) or sentinel.
    bwrap_args.push(match validation_socket {
        Some(socket) => socket.sandbox_socket_path.to_string_lossy().into_owned(),
        None => LAUNCHER_NONE.to_string(),
    });
    // Fields 2-3: bridge socket (in-sandbox path) + port, or sentinels.
    match &bridge {
        Some((socket, port)) => {
            bwrap_args.push(socket.to_string_lossy().into_owned());
            bwrap_args.push(port.to_string());
        }
        None => {
            bwrap_args.push(LAUNCHER_NONE.to_string());
            bwrap_args.push(LAUNCHER_NONE.to_string());
        }
    }
    // Field 4: the writable bind-destination paths to validate (count, then
    // the paths), in the same order the host sends their fds.
    let validation_paths: &[&Path] = if validation_socket.is_some() {
        writable_dirs
    } else {
        &[]
    };
    bwrap_args.push(validation_paths.len().to_string());
    for path in validation_paths {
        bwrap_args.push(path.to_string_lossy().into_owned());
    }
    bwrap_args.push("--".to_string());

    bwrap_args.push(program.to_string());
    bwrap_args.extend(args.iter().cloned());

    let bwrap = bwrap
        .to_str()
        .with_context(|| format!("bwrap path contains invalid UTF-8: {}", bwrap.display()))?;
    Ok((bwrap.to_string(), bwrap_args))
}

/// Handle a possible re-exec of this binary as the in-sandbox launcher (bind
/// validator + network bridge). Does not return if it was invoked as one.
pub fn run_launcher_if_invoked() {
    let Some(invocation) = parse_launcher_args(std::env::args_os()) else {
        return;
    };
    let invocation = match invocation {
        Ok(invocation) => invocation,
        Err(error) => {
            eprintln!("zed: malformed sandbox launcher invocation: {error:#}");
            std::process::exit(127);
        }
    };
    run_launcher(invocation);
}

/// A decoded in-sandbox launcher invocation (the `--zed-linux-sandbox-launcher`
/// re-exec). All fields are produced by the trusted host side and parsed before
/// any untrusted command runs.
struct LauncherInvocation {
    /// In-sandbox path of the validation socket, if bind validation is required.
    validation_socket: Option<PathBuf>,
    /// Writable bind-destination paths to validate, in the order the host sends
    /// their fds. Empty when validation isn't required.
    validation_paths: Vec<PathBuf>,
    /// `(in-sandbox proxy socket path, loopback port)` if the restricted-network
    /// bridge is required.
    bridge: Option<(PathBuf, u16)>,
    program: OsString,
    args: Vec<OsString>,
}

fn parse_launcher_args(
    args: impl IntoIterator<Item = OsString>,
) -> Option<Result<LauncherInvocation>> {
    let mut args = args.into_iter();
    args.next()?;
    if args.next()?.to_str() != Some(LAUNCHER_FLAG) {
        return None;
    }
    Some(decode_launcher_args(args))
}

/// Parse an optional field encoded as either a real value or the `-` sentinel.
fn optional_field(value: OsString) -> Option<OsString> {
    if value == LAUNCHER_NONE {
        None
    } else {
        Some(value)
    }
}

fn decode_launcher_args(mut args: impl Iterator<Item = OsString>) -> Result<LauncherInvocation> {
    let validation_socket =
        optional_field(args.next().context("missing validation socket field")?).map(PathBuf::from);
    let bridge_socket =
        optional_field(args.next().context("missing bridge socket field")?).map(PathBuf::from);
    let bridge_port = optional_field(args.next().context("missing bridge port field")?)
        .map(|value| {
            value
                .to_str()
                .context("bridge port is not valid UTF-8")?
                .parse::<u16>()
                .context("invalid bridge port")
        })
        .transpose()?;
    let bridge = match (bridge_socket, bridge_port) {
        (Some(socket), Some(port)) => Some((socket, port)),
        (None, None) => None,
        _ => bail!("bridge socket and port must be set together"),
    };

    let path_count = args
        .next()
        .context("missing validation path count")?
        .to_str()
        .context("validation path count is not valid UTF-8")?
        .parse::<usize>()
        .context("invalid validation path count")?;
    if path_count > MAX_VALIDATED_BINDS {
        bail!("validation path count {path_count} exceeds {MAX_VALIDATED_BINDS}");
    }
    let mut validation_paths = Vec::with_capacity(path_count);
    for _ in 0..path_count {
        validation_paths.push(PathBuf::from(
            args.next().context("missing validation path")?,
        ));
    }

    let separator = args.next().context("missing launcher argument separator")?;
    if separator != "--" {
        bail!("missing launcher argument separator");
    }
    let program = args.next().context("missing program to run")?;
    let args = args.collect();
    Ok(LauncherInvocation {
        validation_socket,
        validation_paths,
        bridge,
        program,
        args,
    })
}

/// The in-sandbox launcher entry point. Runs after bwrap's mounts and before the
/// real command: it verifies the writable binds weren't redirected, optionally
/// starts the restricted-network bridge, then runs the command. Never returns.
fn run_launcher(invocation: LauncherInvocation) -> ! {
    if let Some(socket) = &invocation.validation_socket {
        if let Err(error) = validate_binds(socket, &invocation.validation_paths) {
            // Fail closed: a redirected (or unverifiable) writable bind means the
            // command must not run at all.
            eprintln!("zed: sandbox bind validation failed: {error:#}");
            std::process::exit(SANDBOX_SETUP_FAILED_EXIT_CODE);
        }
    }

    match invocation.bridge {
        Some((socket_path, port)) => {
            run_bridge(socket_path, port, &invocation.program, &invocation.args)
        }
        // No bridge to keep alive, so `exec` the command directly rather than
        // lingering as a parent process.
        None => exec_command(&invocation.program, &invocation.args),
    }
}

/// Verify each writable bind resolves, inside the sandbox, to the exact inode the
/// host captured. Receives the captured `O_PATH` fds over `socket_path` via
/// `SCM_RIGHTS` (in the same order as `paths`), then compares `fstat(received
/// fd)` against `lstat(mounted path)`. Both stats run in this process inside the
/// sandbox, so the comparison needs no cross-namespace assumption about device
/// numbers. Any mismatch — or any failure to obtain the expected number of fds —
/// is an error (the caller fails closed).
fn validate_binds(socket_path: &Path, paths: &[PathBuf]) -> Result<()> {
    let stream = UnixStream::connect(socket_path)
        .with_context(|| format!("connecting to validation socket {}", socket_path.display()))?;
    let fds = recv_fds(&stream).context("receiving validation descriptors")?;
    if fds.len() != paths.len() {
        bail!(
            "expected {} validation descriptor(s), received {}",
            paths.len(),
            fds.len()
        );
    }
    for (fd, path) in fds.iter().zip(paths) {
        let expected = fd_dev_ino(fd.as_raw_fd())
            .with_context(|| format!("fstat of captured descriptor for {}", path.display()))?;
        let mounted = lstat_dev_ino(path)
            .with_context(|| format!("lstat of mounted bind {}", path.display()))?;
        if expected != mounted {
            bail!(
                "writable bind {} was redirected (captured inode {:?}, mounted inode {:?})",
                path.display(),
                expected,
                mounted
            );
        }
    }
    Ok(())
}

/// Build the seccomp-BPF program installed on the untrusted command before it
/// runs. This is the syscall-level half of preventing session-IPC-socket
/// sandbox escapes: a read-only bind mount does not stop `connect()` to a unix
/// socket, so instead we stop the command from ever *obtaining* a non-IP socket.
///
/// The program (default action: allow):
/// - `socket()` is denied (`EPERM`) unless the family is `AF_INET`/`AF_INET6`/
///   `AF_NETLINK` — so no `AF_UNIX` (session IPC) or `AF_VSOCK` (VM host) sockets.
/// - `socketpair()` is allowed only for `AF_UNIX` (a process-local pair that
///   cannot reach anything outside the sandbox).
/// - `io_uring_*` is denied, so its ring ops can't create/connect sockets
///   without going through the filtered syscalls.
/// - `ptrace`/`process_vm_*` are denied.
///
/// `connect`/`recvmsg`/`sendmsg`/`bind`/`listen`/`accept` stay allowed: with no
/// way to create a forbidden socket (and — by fd hygiene — no forbidden fd
/// inherited), there is nothing dangerous for them to act on, and blocking
/// `connect` would break legitimate loopback/proxy use. Foreign-architecture
/// syscalls are killed by seccompiler's arch check, closing the 32-bit-ABI
/// (`socketcall`) bypass.
///
/// Returns `None` on an architecture seccompiler can't target (we ship on
/// x86_64/aarch64, so that is not a configuration we run in practice).
fn build_command_seccomp_program() -> Result<Option<seccompiler::BpfProgram>> {
    use seccompiler::{
        BpfProgram, SeccompAction, SeccompCmpArgLen, SeccompCmpOp, SeccompCondition, SeccompFilter,
        SeccompRule, TargetArch,
    };
    use std::collections::BTreeMap;

    let Ok(target_arch) = TargetArch::try_from(std::env::consts::ARCH) else {
        return Ok(None);
    };

    // `socket(domain, ...)`: deny unless `domain` (arg0, an `int` — compare the
    // low 32 bits) is an allowed IP/netlink family. The rule matches when the
    // family is none of the allowed ones, and a matched rule takes the deny
    // action; an allowed family matches no rule and falls through to `Allow`.
    let socket_deny = SeccompRule::new(vec![
        SeccompCondition::new(
            0,
            SeccompCmpArgLen::Dword,
            SeccompCmpOp::Ne,
            libc::AF_INET as u64,
        )?,
        SeccompCondition::new(
            0,
            SeccompCmpArgLen::Dword,
            SeccompCmpOp::Ne,
            libc::AF_INET6 as u64,
        )?,
        SeccompCondition::new(
            0,
            SeccompCmpArgLen::Dword,
            SeccompCmpOp::Ne,
            libc::AF_NETLINK as u64,
        )?,
    ])?;
    // `socketpair(domain, ...)`: allow only `AF_UNIX`.
    let socketpair_deny = SeccompRule::new(vec![SeccompCondition::new(
        0,
        SeccompCmpArgLen::Dword,
        SeccompCmpOp::Ne,
        libc::AF_UNIX as u64,
    )?])?;

    let mut rules: BTreeMap<i64, Vec<SeccompRule>> = BTreeMap::new();
    rules.insert(libc::SYS_socket, vec![socket_deny]);
    rules.insert(libc::SYS_socketpair, vec![socketpair_deny]);
    // Unconditional denials (an empty rule chain always takes the match action).
    for syscall in [
        libc::SYS_io_uring_setup,
        libc::SYS_io_uring_enter,
        libc::SYS_io_uring_register,
        libc::SYS_ptrace,
        libc::SYS_process_vm_readv,
        libc::SYS_process_vm_writev,
    ] {
        rules.insert(syscall, Vec::new());
    }

    let filter = SeccompFilter::new(
        rules,
        SeccompAction::Allow,
        SeccompAction::Errno(libc::EPERM as u32),
        target_arch,
    )
    .context("building sandbox command seccomp filter")?;
    let program =
        BpfProgram::try_from(filter).context("compiling sandbox command seccomp filter")?;
    Ok(Some(program))
}

/// Install [`build_command_seccomp_program`] on the calling thread, which is
/// about to become (or `exec` into) the untrusted command; the filter survives
/// `exec`. `apply_filter` also sets `PR_SET_NO_NEW_PRIVS`. On an unsupported
/// architecture there is no filter to install — log and proceed rather than
/// break the sandbox on a platform we don't ship.
fn install_command_seccomp_filter() -> Result<()> {
    match build_command_seccomp_program()? {
        Some(program) => seccompiler::apply_filter(&program)
            .context("installing sandbox command seccomp filter")?,
        None => log::warn!(
            "[sandbox] seccomp is unavailable on {}; the unix-socket syscall guard \
             was not installed",
            std::env::consts::ARCH
        ),
    }
    Ok(())
}

/// Replace this process with the sandboxed command. Only returns (after logging)
/// if `exec` itself fails.
fn exec_command(program: &OsStr, args: &[OsString]) -> ! {
    // Lock down socket/io_uring/ptrace syscalls right before handing control to
    // the untrusted command; the filter survives `exec`.
    if let Err(error) = install_command_seccomp_filter() {
        eprintln!("zed: failed to install sandbox seccomp filter: {error:#}");
        std::process::exit(SANDBOX_SETUP_FAILED_EXIT_CODE);
    }
    let error = Command::new(program).args(args).exec();
    eprintln!("zed: failed to exec sandboxed command: {error}");
    std::process::exit(SANDBOX_SETUP_FAILED_EXIT_CODE);
}

#[allow(
    clippy::disallowed_methods,
    reason = "the bridge is an in-sandbox process that must synchronously spawn and wait for the command"
)]
fn run_bridge(socket_path: PathBuf, port: u16, program: &OsStr, program_args: &[OsString]) -> ! {
    let listener = match TcpListener::bind((Ipv4Addr::LOCALHOST, port)) {
        Ok(listener) => listener,
        Err(error) => {
            eprintln!("zed: failed to bind sandbox proxy bridge: {error}");
            std::process::exit(SANDBOX_SETUP_FAILED_EXIT_CODE);
        }
    };

    if let Err(error) = thread::Builder::new()
        .name("zed-sandbox-bridge".to_string())
        .stack_size(128 * 1024)
        .spawn(move || run_bridge_listener(listener, socket_path))
    {
        eprintln!("zed: failed to spawn sandbox proxy bridge: {error}");
        std::process::exit(SANDBOX_SETUP_FAILED_EXIT_CODE);
    }

    // The command runs under the syscall filter, installed in the child via
    // `pre_exec` — *this* bridge process must NOT be filtered, since it keeps
    // using `AF_UNIX` to reach the host proxy for every request the command
    // makes. Build the program before the fork; the child only applies it.
    let seccomp_program = match build_command_seccomp_program() {
        Ok(program) => program,
        Err(error) => {
            eprintln!("zed: failed to build sandbox seccomp filter: {error:#}");
            std::process::exit(SANDBOX_SETUP_FAILED_EXIT_CODE);
        }
    };
    let mut command = Command::new(program);
    command.args(program_args);
    // SAFETY: the closure runs in the forked child after `fork` and before
    // `exec`. It only calls `seccompiler::apply_filter` (a `prctl` on a program
    // built before the fork) — async-signal-safe and allocation-free.
    unsafe {
        command.pre_exec(move || {
            if let Some(program) = &seccomp_program {
                seccompiler::apply_filter(program)
                    .map_err(|error| std::io::Error::other(format!("seccomp: {error}")))?;
            }
            Ok(())
        });
    }
    let mut child = match command.spawn() {
        Ok(child) => child,
        Err(error) => {
            eprintln!("zed: failed to spawn sandboxed command: {error}");
            std::process::exit(SANDBOX_SETUP_FAILED_EXIT_CODE);
        }
    };

    match child.wait() {
        Ok(status) => {
            if let Some(code) = status.code() {
                std::process::exit(code);
            }
            let signal = status.signal().unwrap_or(1);
            std::process::exit(128 + signal);
        }
        Err(error) => {
            eprintln!("zed: failed to wait for sandboxed command: {error}");
            std::process::exit(SANDBOX_SETUP_FAILED_EXIT_CODE);
        }
    }
}

/// Send `fds` over `stream` in a single message carrying one byte of payload and
/// the descriptors as `SCM_RIGHTS` ancillary data.
fn send_fds(stream: &UnixStream, fds: &[RawFd]) -> std::io::Result<()> {
    use nix::sys::socket::{ControlMessage, MsgFlags, sendmsg};
    let payload = [0u8; 1];
    let iov = [std::io::IoSlice::new(&payload)];
    let control = [ControlMessage::ScmRights(fds)];
    sendmsg::<()>(
        stream.as_raw_fd(),
        &iov,
        &control,
        MsgFlags::MSG_NOSIGNAL,
        None,
    )
    .map_err(std::io::Error::from)?;
    Ok(())
}

/// Receive the descriptors sent via `SCM_RIGHTS` on `stream`. They arrive with
/// `O_CLOEXEC` (via `MSG_CMSG_CLOEXEC`) so they don't leak into the command
/// that's `exec`'d after validation. The ancillary buffer is sized for the
/// protocol maximum ([`MAX_VALIDATED_BINDS`]); a sender exceeding it trips
/// `MSG_CTRUNC` and a mismatched count is rejected by the caller.
fn recv_fds(stream: &UnixStream) -> std::io::Result<Vec<OwnedFd>> {
    use nix::sys::socket::{ControlMessageOwned, MsgFlags, recvmsg};
    let mut payload = [0u8; 1];
    let mut iov = [std::io::IoSliceMut::new(&mut payload)];
    let mut control = nix::cmsg_space!([RawFd; MAX_VALIDATED_BINDS]);
    let message = recvmsg::<()>(
        stream.as_raw_fd(),
        &mut iov,
        Some(&mut control),
        MsgFlags::MSG_CMSG_CLOEXEC,
    )
    .map_err(std::io::Error::from)?;
    if message.flags.contains(MsgFlags::MSG_CTRUNC) {
        return Err(std::io::Error::other(
            "validation descriptors were truncated in transit",
        ));
    }

    let mut fds = Vec::new();
    for control_message in message.cmsgs().map_err(std::io::Error::from)? {
        if let ControlMessageOwned::ScmRights(raw_fds) = control_message {
            for raw in raw_fds {
                // SAFETY: the kernel installs each `SCM_RIGHTS` descriptor as a
                // freshly-allocated, open fd in *our* table for this message
                // (with `O_CLOEXEC` via `MSG_CMSG_CLOEXEC`), so it's valid and
                // can't alias an fd we already hold. `nix`'s `ScmRights` does not
                // take ownership of received fds (closing them is the caller's
                // responsibility), so wrapping each exactly once here makes us
                // their sole owner. Both hold for any peer, which is why this
                // (and `recv_fds`) is sound as a safe fn rather than needing an
                // `unsafe fn` precondition on the caller.
                fds.push(unsafe { OwnedFd::from_raw_fd(raw) });
            }
        }
    }
    Ok(fds)
}

/// `(device, inode)` of the object an already-open descriptor refers to.
fn fd_dev_ino(fd: RawFd) -> std::io::Result<(u64, u64)> {
    let stat = nix::sys::stat::fstat(fd).map_err(std::io::Error::from)?;
    Ok((stat.st_dev as u64, stat.st_ino as u64))
}

/// `(device, inode)` of `path` without following a final symlink.
fn lstat_dev_ino(path: &Path) -> std::io::Result<(u64, u64)> {
    // `symlink_metadata` is `lstat(2)` (it does not follow a final symlink).
    let metadata = std::fs::symlink_metadata(path)?;
    Ok((metadata.dev(), metadata.ino()))
}

/// A decoded WSL-helper invocation (`--wsl-sandbox-helper`). All fields are
/// produced by the trusted Windows side and parsed before any untrusted command
/// runs.
struct WslHelperInvocation {
    /// Absolute path of `bwrap` inside WSL (located by the Windows-side probe).
    bwrap_path: PathBuf,
    /// Pre-built bwrap *options* — everything before the trailing `-- cmd` —
    /// assembled on the Windows side (`windows_wsl::build_bwrap_args`): root
    /// bind, `/tmp` tmpfs, writable binds, Windows-interop blocking, `--setenv`s,
    /// `--chdir`, namespace flags, etc.
    base_args: Vec<OsString>,
    /// The writable bind destinations to validate — exactly the ones `base_args`
    /// binds read-write. Each is captured here (WSL-side) and checked in-sandbox.
    writable_paths: Vec<PathBuf>,
    program: OsString,
    args: Vec<OsString>,
}

/// Handle a possible re-exec of this binary as the WSL-side sandbox helper. Does
/// not return if it was invoked as one.
pub fn run_wsl_helper_if_invoked() {
    let args: Vec<OsString> = std::env::args_os().collect();
    if args.get(1).and_then(|arg| arg.to_str()) == Some(WSL_RESOLVE_FLAG) {
        let Some(path) = args.get(2) else {
            eprintln!("zed: malformed WSL canonical-path resolver invocation");
            std::process::exit(127);
        };
        if args.len() != 3 {
            eprintln!("zed: malformed WSL canonical-path resolver invocation");
            std::process::exit(127);
        }
        match crate::util::CanonicalPathBuf::resolve(PathBuf::from(path.as_os_str())) {
            Ok(canonical) => {
                let Some(canonical) = canonical.path().to_str() else {
                    eprintln!("zed: canonical WSL sandbox grant is not valid UTF-8");
                    std::process::exit(SANDBOX_SETUP_FAILED_EXIT_CODE);
                };
                let Ok(distro) = std::env::var("WSL_DISTRO_NAME") else {
                    eprintln!("zed: WSL_DISTRO_NAME is unavailable while resolving sandbox grant");
                    std::process::exit(SANDBOX_SETUP_FAILED_EXIT_CODE);
                };
                // Classify the backing filesystem of the *canonical* target (the
                // path that will actually be bound), so the Windows side can
                // persist whether this grant lives on a Windows-hosted (DrvFs)
                // filesystem, whose sandbox-integrity guarantees are weaker.
                let fs_class = if path_is_on_windows_fs(Path::new(canonical)) {
                    "windows-fs"
                } else {
                    "native-fs"
                };
                println!("{distro}");
                println!("{canonical}");
                println!("{fs_class}");
                std::process::exit(0);
            }
            Err(error) => {
                eprintln!("zed: could not resolve WSL sandbox grant: {error}");
                std::process::exit(SANDBOX_SETUP_FAILED_EXIT_CODE);
            }
        }
    }

    let Some(invocation) = parse_wsl_helper_args(args) else {
        return;
    };
    let invocation = match invocation {
        Ok(invocation) => invocation,
        Err(error) => {
            eprintln!("zed: malformed WSL sandbox helper invocation: {error:#}");
            std::process::exit(127);
        }
    };
    run_wsl_helper(invocation);
}

/// Whether `path` resides on a Windows-hosted filesystem exposed to WSL via
/// DrvFs. Inside WSL the Windows drives (`/mnt/<letter>`) are served by either
/// 9p or virtiofs, whereas the distro's own filesystems are native (ext4/btrfs/
/// overlay/tmpfs). We use the backing filesystem type rather than a `/mnt/`
/// path prefix so a reconfigured automount root, or a symlink that lands on a
/// Windows drive, is still classified correctly. Fails safe: if the type can't
/// be determined we report `true` (weaker guarantees ⇒ warn) rather than
/// silently treating it as native.
fn path_is_on_windows_fs(path: &Path) -> bool {
    // `statfs.f_type` magics for the DrvFs transports. virtiofs is FUSE-backed.
    const V9FS_MAGIC: u64 = 0x0102_1997;
    const FUSE_SUPER_MAGIC: u64 = 0x6573_5546;
    match nix::sys::statfs::statfs(path) {
        Ok(stat) => {
            // `f_type` is `i64` on glibc but `u64` on musl, so widen it
            // losslessly instead of comparing against a fixed-width constant.
            u64::try_from(stat.filesystem_type().0)
                .is_ok_and(|fs_type| fs_type == V9FS_MAGIC || fs_type == FUSE_SUPER_MAGIC)
        }
        Err(_) => true,
    }
}

fn parse_wsl_helper_args(
    args: impl IntoIterator<Item = OsString>,
) -> Option<Result<WslHelperInvocation>> {
    let mut args = args.into_iter();
    args.next()?;
    if args.next()?.to_str() != Some(WSL_HELPER_FLAG) {
        return None;
    }
    Some(decode_wsl_helper_args(args))
}

fn decode_wsl_helper_args(mut args: impl Iterator<Item = OsString>) -> Result<WslHelperInvocation> {
    let bwrap_path = PathBuf::from(args.next().context("missing bwrap path")?);
    let base_count = parse_count(
        args.next().context("missing base-arg count")?,
        "base-arg count",
    )?;
    let mut base_args = Vec::with_capacity(base_count);
    for _ in 0..base_count {
        base_args.push(args.next().context("missing base arg")?);
    }
    let writable_count = parse_count(
        args.next().context("missing writable count")?,
        "writable count",
    )?;
    if writable_count > MAX_VALIDATED_BINDS {
        bail!("writable count {writable_count} exceeds {MAX_VALIDATED_BINDS}");
    }
    let mut writable_paths = Vec::with_capacity(writable_count);
    for _ in 0..writable_count {
        writable_paths.push(PathBuf::from(args.next().context("missing writable path")?));
    }
    let separator = args.next().context("missing argument separator")?;
    if separator != "--" {
        bail!("missing argument separator");
    }
    let program = args.next().context("missing program to run")?;
    let args = args.collect();
    Ok(WslHelperInvocation {
        bwrap_path,
        base_args,
        writable_paths,
        program,
        args,
    })
}

fn parse_count(value: OsString, what: &str) -> Result<usize> {
    value
        .to_str()
        .with_context(|| format!("{what} is not valid UTF-8"))?
        .parse::<usize>()
        .with_context(|| format!("invalid {what}"))
}

/// The WSL-side helper entry point. Mirrors the native-Linux host side: capture
/// the writable binds' inodes (here, inside WSL), stand up the validation socket,
/// finish assembling the bwrap invocation (validation socket bind + in-sandbox
/// validator re-exec), spawn bwrap, and propagate its exit. Never returns.
#[allow(
    clippy::disallowed_methods,
    reason = "the WSL helper is a dedicated per-command process that must spawn and wait for bwrap"
)]
fn run_wsl_helper(invocation: WslHelperInvocation) -> ! {
    let mut fds = Vec::with_capacity(invocation.writable_paths.len());
    for path in &invocation.writable_paths {
        let fd = crate::util::CanonicalPathBuf::from_canonical(path.clone())
            .and_then(|canonical| canonical.dup_fd());
        match fd {
            Ok(fd) => fds.push(fd),
            Err(error) => {
                eprintln!(
                    "zed: WSL sandbox helper could not verify canonical writable bind {}: {error}",
                    path.display()
                );
                std::process::exit(SANDBOX_SETUP_FAILED_EXIT_CODE);
            }
        }
    }

    let validation = if fds.is_empty() {
        None
    } else {
        match ValidationFdSender::spawn(fds) {
            Ok(sender) => Some(sender),
            Err(error) => {
                eprintln!("zed: WSL sandbox helper could not start the bind validator: {error}");
                std::process::exit(SANDBOX_SETUP_FAILED_EXIT_CODE);
            }
        }
    };

    let current_exe = match std::env::current_exe() {
        Ok(path) => path,
        Err(error) => {
            eprintln!("zed: WSL sandbox helper could not resolve its own path: {error}");
            std::process::exit(SANDBOX_SETUP_FAILED_EXIT_CODE);
        }
    };

    let mut args = invocation.base_args.clone();
    // Always re-exec ourselves as the in-sandbox launcher, even when there is
    // nothing to validate: the launcher is where the seccomp filter is installed
    // on the untrusted command (see `exec_command`). When there are writable
    // binds, also bind the validation socket so the launcher can verify them.
    // WSL has no restricted-network bridge, so both bridge fields are the absent
    // sentinel.
    if let Some(sender) = &validation {
        // Bind the validation socket in, after the base args' tmpfs and writable
        // binds so it isn't shadowed.
        args.push(OsString::from("--bind"));
        args.push(sender.host_socket_path().as_os_str().to_os_string());
        args.push(sender.sandbox_socket_path().as_os_str().to_os_string());
    }
    args.push(OsString::from("--"));
    args.push(current_exe.into_os_string());
    args.push(OsString::from(LAUNCHER_FLAG));
    // Field 1: validation socket (in-sandbox path) or sentinel.
    match &validation {
        Some(sender) => args.push(sender.sandbox_socket_path().as_os_str().to_os_string()),
        None => args.push(OsString::from(LAUNCHER_NONE)),
    }
    // Fields 2-3: bridge socket + port (WSL has no bridge).
    args.push(OsString::from(LAUNCHER_NONE));
    args.push(OsString::from(LAUNCHER_NONE));
    // Field 4: writable bind-destination paths to validate (count, then paths);
    // empty when there is nothing to validate.
    let validation_paths: &[PathBuf] = if validation.is_some() {
        &invocation.writable_paths
    } else {
        &[]
    };
    args.push(OsString::from(validation_paths.len().to_string()));
    for path in validation_paths {
        args.push(path.clone().into_os_string());
    }
    args.push(OsString::from("--"));
    args.push(invocation.program.clone());
    args.extend(invocation.args.iter().cloned());

    let mut child = match Command::new(&invocation.bwrap_path).args(&args).spawn() {
        Ok(child) => child,
        Err(error) => {
            eprintln!("zed: WSL sandbox helper could not spawn bwrap: {error}");
            std::process::exit(SANDBOX_SETUP_FAILED_EXIT_CODE);
        }
    };

    let status = child.wait();
    // Hold the validator open until bwrap (and thus the in-sandbox validator that
    // connects to it during startup) has finished.
    drop(validation);
    match status {
        Ok(status) => {
            if let Some(code) = status.code() {
                std::process::exit(code);
            }
            let signal = status.signal().unwrap_or(1);
            std::process::exit(128 + signal);
        }
        Err(error) => {
            eprintln!("zed: WSL sandbox helper failed waiting for bwrap: {error}");
            std::process::exit(SANDBOX_SETUP_FAILED_EXIT_CODE);
        }
    }
}

fn run_bridge_listener(listener: TcpListener, socket_path: PathBuf) {
    for stream in listener.incoming() {
        match stream {
            Ok(stream) => {
                let socket_path = socket_path.clone();
                if let Err(error) = thread::Builder::new()
                    .name("zed-sandbox-bridge-conn".to_string())
                    .stack_size(128 * 1024)
                    .spawn(move || forward_bridge_connection(stream, socket_path))
                {
                    eprintln!("zed: failed to spawn sandbox bridge connection thread: {error}");
                }
            }
            Err(error) => eprintln!("zed: sandbox bridge accept failed: {error}"),
        }
    }
}

fn forward_bridge_connection(tcp_stream: TcpStream, socket_path: PathBuf) {
    let unix_stream = match UnixStream::connect(&socket_path) {
        Ok(stream) => stream,
        Err(error) => {
            eprintln!(
                "zed: sandbox bridge failed to connect to proxy socket {}: {error}",
                socket_path.display()
            );
            return;
        }
    };
    copy_bidirectional(tcp_stream, unix_stream);
}

fn copy_bidirectional(tcp_stream: TcpStream, unix_stream: UnixStream) {
    let tcp_read = match tcp_stream.try_clone() {
        Ok(stream) => stream,
        Err(error) => {
            eprintln!("zed: sandbox bridge failed to clone TCP stream: {error}");
            return;
        }
    };
    let unix_read = match unix_stream.try_clone() {
        Ok(stream) => stream,
        Err(error) => {
            eprintln!("zed: sandbox bridge failed to clone Unix stream: {error}");
            return;
        }
    };

    let tcp_write = tcp_stream;
    let unix_write = unix_stream;
    let to_proxy = match thread::Builder::new()
        .name("zed-sandbox-bridge-out".to_string())
        .stack_size(128 * 1024)
        .spawn(move || copy_one_way(tcp_read, unix_write))
    {
        Ok(handle) => handle,
        Err(error) => {
            eprintln!("zed: failed to spawn sandbox bridge pump thread: {error}");
            return;
        }
    };
    copy_one_way(unix_read, tcp_write);
    if to_proxy.join().is_err() {
        eprintln!("zed: sandbox bridge pump thread panicked");
    }
}

trait BridgeStream: Read + Write {
    fn shutdown(&self, how: Shutdown) -> std::io::Result<()>;
}

impl BridgeStream for TcpStream {
    fn shutdown(&self, how: Shutdown) -> std::io::Result<()> {
        TcpStream::shutdown(self, how)
    }
}

impl BridgeStream for UnixStream {
    fn shutdown(&self, how: Shutdown) -> std::io::Result<()> {
        UnixStream::shutdown(self, how)
    }
}

fn copy_one_way(mut from: impl Read, mut to: impl BridgeStream) {
    let mut buffer = vec![0; PUMP_BUFFER_SIZE];
    loop {
        let count = match from.read(&mut buffer) {
            Ok(0) => break,
            Ok(count) => count,
            Err(_) => break,
        };
        if to.write_all(&buffer[..count]).is_err() {
            break;
        }
    }
    let _ = to.shutdown(Shutdown::Write);
}

#[cfg(test)]
mod tests {
    use super::*;

    fn launcher_argv(program: &str, args: Vec<&str>) -> Vec<OsString> {
        std::iter::once(program)
            .chain(args)
            .map(OsString::from)
            .collect()
    }

    #[test]
    fn test_build_bwrap_args_binds_exact_path_never_widens_to_ancestor() {
        // A requested writable path that doesn't exist must NOT be bound, and in
        // particular must never cause an existing ancestor (here the tempdir) to
        // be bound read-write — that was a sandbox-escape (scope widening).
        let dir = tempfile::tempdir().unwrap();
        let existing = dir.path().canonicalize().unwrap();
        let missing = existing.join("does-not-exist").join("nested");

        let args = build_bwrap_args(
            &[missing.as_path()],
            &[],
            SandboxPermissions::default(),
            None,
            None,
        );

        let existing_str = existing.to_string_lossy().into_owned();
        assert!(
            !windows_contains(&args, &["--bind", &existing_str, &existing_str]),
            "a missing writable path must not widen the bind to an existing ancestor: {args:?}"
        );
        let missing_str = missing.to_string_lossy().into_owned();
        assert!(
            !windows_contains(&args, &["--bind", &missing_str, &missing_str]),
            "a missing writable path must not be bound: {args:?}"
        );
    }

    #[test]
    fn test_build_bwrap_args_default_binds_writable_dirs_read_write() {
        let writable = tempfile::tempdir().unwrap();
        let args = build_bwrap_args(
            &[writable.path()],
            &[],
            SandboxPermissions::default(),
            Some(writable.path()),
            None,
        );

        assert!(windows_contains(&args, &["--ro-bind", "/", "/"]));
        // The writable dir is bound verbatim at the exact path given (never
        // re-canonicalized, which would reopen the bind-source TOCTOU gap).
        let writable_str = writable.path().to_string_lossy().into_owned();
        assert!(windows_contains(
            &args,
            &["--bind", &writable_str, &writable_str]
        ));
        assert!(windows_contains(&args, &["--tmpfs", "/tmp"]));
        assert!(args.iter().any(|arg| arg == "--chdir"));
        assert!(args.iter().any(|arg| arg == "--unshare-net"));
    }

    #[test]
    fn test_build_bwrap_args_network_namespace_follows_permission() {
        let denied = build_bwrap_args(&[], &[], SandboxPermissions::default(), None, None);
        assert!(denied.iter().any(|arg| arg == "--unshare-net"));

        let allowed = build_bwrap_args(
            &[],
            &[],
            SandboxPermissions {
                network: NetworkAccess::All,
                allow_fs_write: false,
            },
            None,
            None,
        );
        assert!(!allowed.iter().any(|arg| arg == "--unshare-net"));

        let socket = PathBuf::from("/tmp/zed-proxy.sock");
        let restricted = build_bwrap_args(
            &[],
            &[],
            SandboxPermissions {
                network: NetworkAccess::LocalhostPort(8080),
                allow_fs_write: false,
            },
            None,
            Some(socket.as_path()),
        );
        assert!(restricted.iter().any(|arg| arg == "--unshare-net"));
        let sandbox_destination = proxy_socket_bind_destination(&restricted)
            .expect("restricted run should bind the proxy socket into the sandbox");
        assert!(sandbox_destination.starts_with(PROXY_SOCKET_SANDBOX_PATH_PREFIX));
    }

    #[test]
    fn test_build_bwrap_args_allow_fs_write_binds_root_read_write() {
        let permissions = SandboxPermissions {
            network: NetworkAccess::None,
            allow_fs_write: true,
        };
        let protected = Path::new("/tmp");
        let args = build_bwrap_args(&[], &[protected], permissions, None, None);
        assert!(windows_contains(&args, &["--bind", "/", "/"]));
        assert!(!windows_contains(&args, &["--ro-bind", "/", "/"]));
        assert!(windows_contains(&args, &["--ro-bind", "/tmp", "/tmp"]));
        assert!(!windows_contains(&args, &["--tmpfs", "/tmp"]));
    }

    #[test]
    fn test_launcher_args_round_trip_bridge_and_validation() {
        let bridge_socket = "/tmp/zed-sandbox-1234-0.sock";
        let validate_socket = "/tmp/zed-sandbox-validate-1234-0.sock";
        let argv = launcher_argv(
            "/path/to/zed",
            vec![
                LAUNCHER_FLAG,
                validate_socket,
                bridge_socket,
                "8080",
                "2",
                "/work/a",
                "/work/b",
                "--",
                "/bin/sh",
                "-c",
                "echo hi there",
            ],
        );

        let decoded = parse_launcher_args(argv)
            .expect("should be recognized as launcher invocation")
            .expect("should decode successfully");

        assert_eq!(
            decoded.validation_socket,
            Some(PathBuf::from(validate_socket))
        );
        assert_eq!(
            decoded.validation_paths,
            vec![PathBuf::from("/work/a"), PathBuf::from("/work/b")]
        );
        assert_eq!(
            decoded.bridge,
            Some((PathBuf::from(bridge_socket), 8080u16))
        );
        assert_eq!(decoded.program, OsString::from("/bin/sh"));
        assert_eq!(
            decoded.args,
            vec![OsString::from("-c"), OsString::from("echo hi there")]
        );
    }

    #[test]
    fn test_wsl_helper_args_round_trip() {
        let argv = launcher_argv(
            "/path/to/zed",
            vec![
                WSL_HELPER_FLAG,
                "/usr/bin/bwrap",
                // base bwrap options (3 tokens)
                "3",
                "--ro-bind",
                "/",
                "/",
                // writable binds to validate (1)
                "1",
                "/work/a",
                "--",
                "/bin/sh",
                "-c",
                "echo hi",
            ],
        );

        let decoded = parse_wsl_helper_args(argv)
            .expect("should be recognized as a WSL helper invocation")
            .expect("should decode successfully");

        assert_eq!(decoded.bwrap_path, PathBuf::from("/usr/bin/bwrap"));
        assert_eq!(
            decoded.base_args,
            vec![
                OsString::from("--ro-bind"),
                OsString::from("/"),
                OsString::from("/")
            ]
        );
        assert_eq!(decoded.writable_paths, vec![PathBuf::from("/work/a")]);
        assert_eq!(decoded.program, OsString::from("/bin/sh"));
        assert_eq!(
            decoded.args,
            vec![OsString::from("-c"), OsString::from("echo hi")]
        );
    }

    #[test]
    fn test_launcher_args_round_trip_no_bridge() {
        let validate_socket = "/tmp/zed-sandbox-validate-1234-0.sock";
        let argv = launcher_argv(
            "/path/to/zed",
            vec![
                LAUNCHER_FLAG,
                validate_socket,
                LAUNCHER_NONE,
                LAUNCHER_NONE,
                "1",
                "/work/a",
                "--",
                "/bin/true",
            ],
        );

        let decoded = parse_launcher_args(argv)
            .expect("should be recognized as launcher invocation")
            .expect("should decode successfully");

        assert_eq!(
            decoded.validation_socket,
            Some(PathBuf::from(validate_socket))
        );
        assert_eq!(decoded.validation_paths, vec![PathBuf::from("/work/a")]);
        assert_eq!(decoded.bridge, None);
        assert_eq!(decoded.program, OsString::from("/bin/true"));
        assert!(decoded.args.is_empty());
    }

    #[test]
    fn test_wrap_invocation_uses_bridge_for_restricted_network() {
        let socket = PathBuf::from("/tmp/zed-proxy.sock");
        let permissions = SandboxPermissions {
            network: NetworkAccess::LocalhostPort(8080),
            allow_fs_write: false,
        };
        let args = build_wrapped_args_for_test(
            "/path/to/zed",
            permissions,
            "/bin/sh",
            &["-c".to_string(), "echo hi".to_string()],
            Some(socket.as_path()),
        );

        // The bind destination inside the sandbox and the path handed to the
        // launcher's bridge fields must be the same unique path. With no writable
        // binds, the validation field is the `-` sentinel and the path count is 0.
        let sandbox_destination = proxy_socket_bind_destination(&args)
            .expect("restricted run should bind the proxy socket into the sandbox");
        assert!(sandbox_destination.starts_with(PROXY_SOCKET_SANDBOX_PATH_PREFIX));
        assert!(windows_contains(
            &args,
            &[
                "/path/to/zed",
                LAUNCHER_FLAG,
                LAUNCHER_NONE,
                &sandbox_destination,
                "8080",
                "0",
                "--",
            ]
        ));
    }

    /// Reconstruct the argv `wrap_invocation` would produce for the bridge-only
    /// (no writable binds, no validation socket) case, without needing a real
    /// `bwrap` on the test host.
    fn build_wrapped_args_for_test(
        bridge_program: &str,
        permissions: SandboxPermissions,
        program: &str,
        program_args: &[String],
        proxy_socket_path: Option<&Path>,
    ) -> Vec<String> {
        let proxy_socket_sandbox_path = match permissions.network {
            NetworkAccess::LocalhostPort(_) => Some(unique_proxy_socket_sandbox_path()),
            NetworkAccess::None | NetworkAccess::All => None,
        };
        let mut bwrap_args = build_bwrap_args_with_sandbox_paths(
            &[],
            &[],
            permissions,
            None,
            proxy_socket_path,
            proxy_socket_sandbox_path.as_deref(),
            None,
            None,
        );
        bwrap_args.push("--".to_string());
        if let NetworkAccess::LocalhostPort(port) = permissions.network {
            let proxy_socket_sandbox_path =
                proxy_socket_sandbox_path.expect("restricted network needs a sandbox socket path");
            bwrap_args.push(bridge_program.to_string());
            bwrap_args.push(LAUNCHER_FLAG.to_string());
            bwrap_args.push(LAUNCHER_NONE.to_string());
            bwrap_args.push(proxy_socket_sandbox_path.to_string_lossy().into_owned());
            bwrap_args.push(port.to_string());
            bwrap_args.push("0".to_string());
            bwrap_args.push("--".to_string());
        }
        bwrap_args.push(program.to_string());
        bwrap_args.extend(program_args.iter().cloned());
        bwrap_args
    }

    /// Returns the in-sandbox destination of the proxy socket `--bind`, if any.
    fn proxy_socket_bind_destination(args: &[String]) -> Option<String> {
        args.windows(3).find_map(|window| {
            if window[0] == "--bind" && window[1] == "/tmp/zed-proxy.sock" {
                Some(window[2].clone())
            } else {
                None
            }
        })
    }

    fn windows_contains(haystack: &[String], needle: &[&str]) -> bool {
        haystack
            .windows(needle.len())
            .any(|window| window.iter().map(String::as_str).eq(needle.iter().copied()))
    }

    /// Open an `O_PATH` descriptor to `path`, mirroring how the policy layer
    /// captures a `HostFilesystemLocation`.
    fn open_o_path(path: &Path) -> OwnedFd {
        use std::os::unix::fs::OpenOptionsExt as _;
        let file = std::fs::OpenOptions::new()
            .read(true)
            .custom_flags(libc::O_PATH | libc::O_CLOEXEC)
            .open(path)
            .expect("open O_PATH");
        OwnedFd::from(file)
    }

    // End-to-end check of the bind validator's core, without a real sandbox:
    // the server hands over the captured fd via SCM_RIGHTS and `validate_binds`
    // compares it against the path it's told was mounted. A matching path passes;
    // a *different* directory (as a redirected bind would produce) is rejected,
    // proving the validator fails closed rather than no-ops.
    //
    // Each `ValidationFdSender` serves a single client and then tears down, so
    // the two cases use separate senders.
    #[test]
    fn test_validate_binds_accepts_match_and_rejects_redirect() {
        let captured = tempfile::tempdir().unwrap();
        let other = tempfile::tempdir().unwrap();

        // The mounted path is the captured inode -> validation passes.
        let sender = ValidationFdSender::spawn(vec![open_o_path(captured.path())])
            .expect("spawn validation fd sender");
        validate_binds(sender.host_socket_path(), &[captured.path().to_path_buf()])
            .expect("matching bind must validate");
        drop(sender);

        // The mounted path is a *different* inode (a redirected bind) -> rejected.
        let sender = ValidationFdSender::spawn(vec![open_o_path(captured.path())])
            .expect("spawn validation fd sender");
        let error = validate_binds(sender.host_socket_path(), &[other.path().to_path_buf()])
            .expect_err("a redirected bind must be rejected");
        assert!(
            error.to_string().contains("redirected"),
            "unexpected error: {error:#}"
        );
    }

    // A wrong fd count from the server (here: zero fds for one path) must fail
    // closed too — the validator never assumes an unvalidated bind is fine.
    #[test]
    fn test_validate_binds_rejects_missing_descriptors() {
        let captured = tempfile::tempdir().unwrap();
        let sender = ValidationFdSender::spawn(Vec::new()).expect("spawn validation fd sender");
        let error = validate_binds(sender.host_socket_path(), &[captured.path().to_path_buf()])
            .expect_err("missing descriptors must be rejected");
        assert!(
            error.to_string().contains("descriptor"),
            "unexpected error: {error:#}"
        );
    }

    // The filter compiles to a non-empty BPF program on architectures
    // seccompiler can target (the ones we ship). On others it's `None`, which is
    // acceptable — no filter is installed there.
    #[test]
    fn test_command_seccomp_program_builds() {
        let program = build_command_seccomp_program().expect("build seccomp program");
        if let Some(program) = program {
            assert!(!program.is_empty(), "seccomp program must not be empty");
        }
    }

    // Actually enforce the filter: in a child process, apply it and confirm that
    // `socket(AF_UNIX)` is denied while `socket(AF_INET)` and
    // `socketpair(AF_UNIX)` still work — the exact guarantee that closes the
    // unix-socket sandbox escape.
    #[test]
    fn test_command_seccomp_filter_blocks_unix_but_allows_ip() {
        // Build in the parent (this allocates); the child only applies the
        // prebuilt program and makes raw syscalls.
        let Some(program) = build_command_seccomp_program().expect("build seccomp program") else {
            return; // unsupported arch: nothing to enforce
        };

        // SAFETY: after `fork`, the child calls only async-signal-safe
        // libc/`prctl` (via `apply_filter` on a program built before the fork)
        // and `_exit`; it never returns to Rust or allocates.
        let pid = unsafe { libc::fork() };
        assert!(pid >= 0, "fork failed");
        if pid == 0 {
            if seccompiler::apply_filter(&program).is_err() {
                unsafe { libc::_exit(10) };
            }
            // AF_UNIX socket creation must be denied.
            if unsafe { libc::socket(libc::AF_UNIX, libc::SOCK_STREAM, 0) } >= 0 {
                unsafe { libc::_exit(11) };
            }
            // AF_INET socket creation must still work.
            let inet = unsafe { libc::socket(libc::AF_INET, libc::SOCK_STREAM, 0) };
            if inet < 0 {
                unsafe { libc::_exit(12) };
            }
            // AF_UNIX socketpair (process-local) must still work.
            let mut fds = [0i32; 2];
            if unsafe { libc::socketpair(libc::AF_UNIX, libc::SOCK_STREAM, 0, fds.as_mut_ptr()) }
                != 0
            {
                unsafe { libc::_exit(13) };
            }
            unsafe { libc::_exit(0) };
        }

        let mut status = 0i32;
        let waited = unsafe { libc::waitpid(pid, &mut status, 0) };
        assert_eq!(waited, pid, "waitpid failed");
        assert!(
            libc::WIFEXITED(status),
            "child did not exit normally: {status:#x}"
        );
        let code = libc::WEXITSTATUS(status);
        assert_eq!(
            code, 0,
            "child reported a seccomp mismatch (exit {code}): 10=apply failed, \
             11=AF_UNIX allowed, 12=AF_INET blocked, 13=socketpair blocked"
        );
    }

    // A requested writable path that doesn't exist must fail the whole
    // invocation — never be created with the agent's ambient permissions, and
    // never run the command with silently less write access than the agent
    // asked for. This check runs before `resolve_bwrap`, so the test needs no
    // real `bwrap`.
    #[test]
    fn test_wrap_invocation_fails_when_writable_path_cannot_be_provided() {
        let file = tempfile::NamedTempFile::new().unwrap();
        let unbindable = file.path().join("subdir");
        let result = wrap_invocation(
            "/proc/self/exe",
            SandboxPermissions {
                network: NetworkAccess::None,
                allow_fs_write: false,
            },
            &[unbindable.as_path()],
            &[],
            None,
            "/bin/true",
            &[],
            None,
            None,
        );
        let error = result.expect_err("must fail closed when a writable path can't be provided");
        assert!(
            error.to_string().contains("writable sandbox path"),
            "unexpected error: {error:#}"
        );
        assert!(
            !unbindable.exists(),
            "wrap_invocation must never create a missing writable path"
        );
    }
}
