fxrs-process 0.0.7

Process, PTY, and terminal lifecycle services for fxrs
Documentation
#[cfg(target_os = "macos")]
use std::path::Path;

use fx_core::{SandboxMode, ToolContext, ToolError};

pub(crate) const MACOS_SANDBOX_EXEC: &str = "/usr/bin/sandbox-exec";

pub(crate) fn profile(context: &ToolContext) -> Result<Option<String>, ToolError> {
    match context.sandbox {
        SandboxMode::None => Ok(None),
        SandboxMode::Os => platform_profile(context).map(Some),
    }
}

#[cfg(target_os = "macos")]
fn platform_profile(context: &ToolContext) -> Result<String, ToolError> {
    if !Path::new(MACOS_SANDBOX_EXEC).is_file() {
        return Err(ToolError::Execution(
            "operating system sandbox is unavailable on this host".into(),
        ));
    }
    let workspace = context.workspace_root.canonicalize().map_err(|error| {
        ToolError::Execution(format!("workspace is unavailable for sandboxing: {error}"))
    })?;
    let mut profile = String::from("(version 1)\n(deny default)\n(allow file-read*)\n");
    write_subpath(&mut profile, "file-write*", &workspace);
    for root in &context.additional_roots {
        let root = root.canonicalize().map_err(|error| {
            ToolError::Execution(format!(
                "additional sandbox root {} is unavailable: {error}",
                root.display()
            ))
        })?;
        write_subpath(&mut profile, "file-write*", &root);
    }
    for path in ["/tmp", "/private/tmp", "/dev"] {
        write_subpath(&mut profile, "file-write*", Path::new(path));
    }
    profile.push_str(
        "(allow process-exec)\n\
         (allow process-fork)\n\
         (allow sysctl-read)\n\
         (allow mach-lookup)\n\
         (allow network-outbound)\n\
         (allow signal)\n\
         (allow iokit-open)\n",
    );
    Ok(profile)
}

#[cfg(not(target_os = "macos"))]
fn platform_profile(_context: &ToolContext) -> Result<String, ToolError> {
    Err(ToolError::Execution(
        "operating system sandbox is unavailable on this host".into(),
    ))
}

#[cfg(target_os = "macos")]
fn write_subpath(output: &mut String, operation: &str, path: &Path) {
    output.push_str("(allow ");
    output.push_str(operation);
    output.push_str(" (subpath \"");
    for character in path.to_string_lossy().chars() {
        match character {
            '\\' => output.push_str("\\\\"),
            '"' => output.push_str("\\\""),
            '\n' => output.push_str("\\n"),
            '\r' => output.push_str("\\r"),
            '\t' => output.push_str("\\t"),
            character => output.push(character),
        }
    }
    output.push_str("\"))\n");
}

pub(crate) fn denied(stderr: &str) -> bool {
    stderr.contains("Sandbox: deny")
        || stderr.contains("sandbox-exec: sandbox_apply")
        || stderr.contains("Operation not permitted")
        || stderr.contains("operation not permitted")
}

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

    #[test]
    fn detects_only_sandbox_style_stderr() {
        assert!(denied("Sandbox: deny(1) file-write-create /outside"));
        assert!(denied("open: Operation not permitted"));
        assert!(denied("zsh: operation not permitted: /outside"));
        assert!(!denied("ordinary command failure"));
    }

    #[cfg(target_os = "macos")]
    #[test]
    fn profile_allows_workspace_writes_but_not_inbound_network() {
        let workspace = std::env::current_dir().unwrap();
        let mut context = ToolContext::new(workspace.clone());
        context.sandbox = SandboxMode::Os;
        let profile = profile(&context).unwrap().unwrap();
        assert!(profile.contains(&workspace.display().to_string()));
        assert!(profile.contains("network-outbound"));
        assert!(!profile.contains("network-inbound"));
    }
}