Codex Handbook
windows-sandbox-rs/src/spawn_prep.rs 716 lines
use crate::acl::add_allow_ace;use crate::acl::add_deny_write_ace;use crate::acl::allow_null_device;use crate::allow::AllowDenyPaths;use crate::allow::compute_allow_paths_for_permissions;use crate::cap::load_or_create_cap_sids;use crate::cap::workspace_write_cap_sid_for_root;use crate::cap::workspace_write_root_contains_path;use crate::cap::workspace_write_root_overlaps_path;use crate::cap::workspace_write_root_specificity;use crate::deny_read_state::sync_persistent_deny_read_acls;use crate::env::apply_no_network_to_env;use crate::env::ensure_non_interactive_pager;use crate::env::inherit_path_env;use crate::env::normalize_null_device_env;use crate::identity::SandboxCreds;use crate::identity::require_logon_sandbox_creds;use crate::logging::log_start;use crate::path_normalization::canonicalize_path;use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;use crate::sandbox_utils::ensure_codex_home_exists;use crate::sandbox_utils::inject_git_safe_directory;use crate::setup::effective_write_roots_for_permissions;use crate::token::LocalSid;use crate::token::create_readonly_token_with_cap;use crate::token::create_workspace_write_token_with_caps_from;use crate::token::get_current_token_for_restriction;use crate::token::get_logon_sid_bytes;use crate::workspace_acl::is_command_cwd_root;use crate::workspace_acl::protect_workspace_agents_dir;use crate::workspace_acl::protect_workspace_codex_dir;use anyhow::Context;use anyhow::Result;use codex_protocol::models::PermissionProfile;use codex_utils_absolute_path::AbsolutePathBuf;use std::collections::HashMap;use std::ffi::c_void;use std::path::Path;use std::path::PathBuf;use windows_sys::Win32::Foundation::CloseHandle;use windows_sys::Win32::Foundation::HANDLE;pub(crate) struct SpawnContext {    pub(crate) permissions: ResolvedWindowsSandboxPermissions,    pub(crate) current_dir: PathBuf,    pub(crate) logs_base_dir: Option<PathBuf>,    pub(crate) uses_write_capabilities: bool,}pub(crate) struct ElevatedSpawnContext {    pub(crate) sandbox_base: PathBuf,    pub(crate) logs_base_dir: Option<PathBuf>,    pub(crate) sandbox_creds: SandboxCreds,    pub(crate) cap_sids: Vec<String>,}#[derive(Debug, Clone, Copy)]pub(crate) struct SpawnPrepOptions {    pub(crate) inherit_path: bool,    pub(crate) add_git_safe_directory: bool,}pub(crate) struct LegacySessionSecurity {    pub(crate) h_token: HANDLE,    pub(crate) readonly_sid: Option<LocalSid>,    pub(crate) readonly_sid_str: Option<String>,    pub(crate) write_root_sids: Vec<RootCapabilitySid>,}pub(crate) struct RootCapabilitySid {    pub(crate) root: PathBuf,    pub(crate) sid: LocalSid,    pub(crate) sid_str: String,}pub(crate) struct LegacyAclSids<'a> {    pub(crate) readonly_sid: Option<&'a LocalSid>,    pub(crate) readonly_sid_str: Option<&'a str>,    pub(crate) write_root_sids: &'a [RootCapabilitySid],}fn prepare_spawn_context_common(    permission_profile: &PermissionProfile,    workspace_roots: &[AbsolutePathBuf],    codex_home: &Path,    cwd: &Path,    env_map: &mut HashMap<String, String>,    command: &[String],    options: SpawnPrepOptions,) -> Result<SpawnContext> {    let permissions =        ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(            permission_profile,            workspace_roots,        )?;    normalize_null_device_env(env_map);    ensure_non_interactive_pager(env_map);    if options.inherit_path {        inherit_path_env(env_map);    }    if options.add_git_safe_directory {        inject_git_safe_directory(env_map, cwd);    }    ensure_codex_home_exists(codex_home)?;    let sandbox_base = codex_home.join(".sandbox");    std::fs::create_dir_all(&sandbox_base)?;    let logs_base_dir = Some(sandbox_base);    log_start(command, logs_base_dir.as_deref());    let uses_write_capabilities = permissions.uses_write_capabilities_for_cwd(cwd, env_map);    Ok(SpawnContext {        permissions,        current_dir: cwd.to_path_buf(),        logs_base_dir,        uses_write_capabilities,    })}pub(crate) fn prepare_legacy_spawn_context(    permission_profile: &PermissionProfile,    workspace_roots: &[AbsolutePathBuf],    codex_home: &Path,    cwd: &Path,    env_map: &mut HashMap<String, String>,    command: &[String],    options: SpawnPrepOptions,) -> Result<SpawnContext> {    let common = prepare_spawn_context_common(        permission_profile,        workspace_roots,        codex_home,        cwd,        env_map,        command,        options,    )?;    if common.permissions.should_apply_network_block() {        apply_no_network_to_env(env_map)?;    }    Ok(common)}pub(crate) fn prepare_legacy_session_security(    uses_write_capabilities: bool,    codex_home: &Path,    cwd: &Path,    capability_roots: impl IntoIterator<Item = PathBuf>,) -> Result<LegacySessionSecurity> {    let caps = load_or_create_cap_sids(codex_home)?;    let (h_token, readonly_sid, readonly_sid_str, write_root_sids) = unsafe {        if uses_write_capabilities {            let write_root_sids = root_capability_sids(codex_home, cwd, capability_roots)?;            if write_root_sids.is_empty() {                anyhow::bail!("workspace-write sandbox has no writable root capability SIDs");            }            let base = get_current_token_for_restriction()?;            let cap_ptrs: Vec<*mut c_void> = write_root_sids                .iter()                .map(|root| root.sid.as_ptr())                .collect();            let h_token = create_workspace_write_token_with_caps_from(base, cap_ptrs.as_slice());            CloseHandle(base);            let h_token = h_token?;            (h_token, None, None, write_root_sids)        } else {            let psid = LocalSid::from_string(&caps.readonly)?;            let (h_token, _psid) = create_readonly_token_with_cap(psid.as_ptr())?;            (h_token, Some(psid), Some(caps.readonly), Vec::new())        }    };    Ok(LegacySessionSecurity {        h_token,        readonly_sid,        readonly_sid_str,        write_root_sids,    })}pub(crate) fn legacy_session_capability_roots(    permissions: &ResolvedWindowsSandboxPermissions,    current_dir: &Path,    env_map: &HashMap<String, String>,    codex_home: &Path,) -> Vec<PathBuf> {    let allow_paths = compute_allow_paths_for_permissions(permissions, current_dir, env_map)        .allow        .into_iter()        .collect::<Vec<_>>();    if permissions.uses_write_capabilities_for_cwd(current_dir, env_map) {        effective_write_roots_for_permissions(            permissions,            current_dir,            env_map,            codex_home,            Some(allow_paths.as_slice()),        )    } else {        allow_paths    }}pub(crate) fn root_capability_sids(    codex_home: &Path,    cwd: &Path,    allow_paths: impl IntoIterator<Item = PathBuf>,) -> Result<Vec<RootCapabilitySid>> {    let mut roots: Vec<PathBuf> = allow_paths.into_iter().collect();    roots.sort_by_key(|root| canonicalize_path(root.as_path()));    roots.dedup_by(|a, b| canonicalize_path(a.as_path()) == canonicalize_path(b.as_path()));    let mut out = Vec::with_capacity(roots.len());    for root in roots {        let sid_str = workspace_write_cap_sid_for_root(codex_home, cwd, &root)?;        let sid = LocalSid::from_string(&sid_str)?;        out.push(RootCapabilitySid { root, sid, sid_str });    }    Ok(out)}fn matching_root_capability<'a>(    path: &Path,    root_sids: &'a [RootCapabilitySid],) -> Option<&'a RootCapabilitySid> {    root_sids        .iter()        .filter(|root_sid| workspace_write_root_contains_path(&root_sid.root, path))        .max_by_key(|root_sid| workspace_write_root_specificity(&root_sid.root))}fn deny_root_capabilities_for_path<'a>(    path: &Path,    root_sids: &'a [RootCapabilitySid],) -> Vec<&'a RootCapabilitySid> {    let matching_root_sids = root_sids        .iter()        .filter(|root_sid| workspace_write_root_overlaps_path(&root_sid.root, path))        .collect::<Vec<_>>();    if matching_root_sids.is_empty() {        root_sids.iter().collect()    } else {        matching_root_sids    }}pub(crate) fn allow_null_device_for_workspace_write(is_workspace_write: bool) {    if !is_workspace_write {        return;    }    unsafe {        if let Ok(base) = get_current_token_for_restriction() {            if let Ok(bytes) = get_logon_sid_bytes(base) {                let mut tmp = bytes;                let psid = tmp.as_mut_ptr() as *mut c_void;                allow_null_device(psid);            }            CloseHandle(base);        }    }}#[allow(clippy::too_many_arguments)]pub(crate) fn apply_legacy_session_acl_rules(    permissions: &ResolvedWindowsSandboxPermissions,    codex_home: &Path,    current_dir: &Path,    env_map: &HashMap<String, String>,    additional_deny_read_paths: &[PathBuf],    additional_deny_write_paths: &[PathBuf],    acl_sids: LegacyAclSids<'_>,) -> Result<()> {    let AllowDenyPaths { allow, mut deny } =        compute_allow_paths_for_permissions(permissions, current_dir, env_map);    unsafe {        for path in additional_deny_write_paths {            // Explicit carveouts must exist before the command starts so the            // sandbox cannot create them under a writable parent first.            if !path.exists() {                std::fs::create_dir_all(path)                    .with_context(|| format!("create deny-write path {}", path.display()))?;            }            deny.insert(path.clone());        }        if let Some(readonly_sid) = acl_sids.readonly_sid {            for p in &allow {                let _ = add_allow_ace(p, readonly_sid.as_ptr());            }        } else {            for p in &allow {                let Some(root_sid) = matching_root_capability(p, acl_sids.write_root_sids) else {                    continue;                };                let _ = add_allow_ace(p, root_sid.sid.as_ptr());            }        }        for p in &deny {            for root_sid in deny_root_capabilities_for_path(p, acl_sids.write_root_sids) {                let _ = add_deny_write_ace(p, root_sid.sid.as_ptr());            }        }        if !additional_deny_read_paths.is_empty() {            if let Some(readonly_sid) = acl_sids.readonly_sid {                let Some(readonly_sid_str) = acl_sids.readonly_sid_str else {                    anyhow::bail!("readonly capability SID string missing");                };                sync_persistent_deny_read_acls(                    codex_home,                    readonly_sid_str,                    additional_deny_read_paths,                    readonly_sid.as_ptr(),                )?;            } else {                for root_sid in acl_sids.write_root_sids {                    sync_persistent_deny_read_acls(                        codex_home,                        &root_sid.sid_str,                        additional_deny_read_paths,                        root_sid.sid.as_ptr(),                    )?;                }            }        }        for root_sid in acl_sids.write_root_sids {            allow_null_device(root_sid.sid.as_ptr());        }        if let Some(readonly_sid) = acl_sids.readonly_sid {            allow_null_device(readonly_sid.as_ptr());        }        if !acl_sids.write_root_sids.is_empty()            && let Some(workspace_sid) =                matching_root_capability(current_dir, acl_sids.write_root_sids)        {            let canonical_cwd = canonicalize_path(current_dir);            if is_command_cwd_root(&workspace_sid.root, &canonical_cwd) {                let _ = protect_workspace_codex_dir(current_dir, workspace_sid.sid.as_ptr());                let _ = protect_workspace_agents_dir(current_dir, workspace_sid.sid.as_ptr());            }        }    }    Ok(())}#[allow(clippy::too_many_arguments)]pub(crate) fn prepare_elevated_spawn_context_for_permissions(    permissions: ResolvedWindowsSandboxPermissions,    codex_home: &Path,    cwd: &Path,    env_map: &mut HashMap<String, String>,    command: &[String],    read_roots_override: Option<&[PathBuf]>,    read_roots_include_platform_defaults: bool,    write_roots_override: Option<&[PathBuf]>,    deny_read_paths_override: &[PathBuf],    deny_write_paths_override: &[PathBuf],    proxy_enforced: bool,) -> Result<ElevatedSpawnContext> {    normalize_null_device_env(env_map);    ensure_non_interactive_pager(env_map);    inherit_path_env(env_map);    inject_git_safe_directory(env_map, cwd);    // Use a temp-based log dir that the sandbox user can write.    let sandbox_base = codex_home.join(".sandbox");    ensure_codex_home_exists(&sandbox_base)?;    let logs_base_dir = Some(sandbox_base.clone());    log_start(command, logs_base_dir.as_deref());    let uses_write_capabilities = permissions.uses_write_capabilities_for_cwd(cwd, env_map);    let AllowDenyPaths { allow, deny } =        compute_allow_paths_for_permissions(&permissions, cwd, env_map);    let write_roots: Vec<PathBuf> = allow.into_iter().collect();    let deny_write_paths: Vec<PathBuf> = deny.into_iter().collect();    let computed_write_roots_override = if uses_write_capabilities {        Some(write_roots.as_slice())    } else {        None    };    let write_roots_for_setup = write_roots_override.or(computed_write_roots_override);    let effective_write_roots = if uses_write_capabilities {        effective_write_roots_for_permissions(            &permissions,            cwd,            env_map,            codex_home,            write_roots_for_setup,        )    } else {        Vec::new()    };    let setup_write_roots_override = if uses_write_capabilities {        Some(effective_write_roots.as_slice())    } else {        write_roots_override    };    let sandbox_creds = require_logon_sandbox_creds(        &permissions,        cwd,        env_map,        codex_home,        read_roots_override,        read_roots_include_platform_defaults,        setup_write_roots_override,        deny_read_paths_override,        if deny_write_paths_override.is_empty() {            &deny_write_paths        } else {            deny_write_paths_override        },        proxy_enforced,    )?;    let caps = load_or_create_cap_sids(codex_home)?;    let (psid_to_use, cap_sids) = if uses_write_capabilities {        let cap_sids = root_capability_sids(codex_home, cwd, effective_write_roots)?            .into_iter()            .map(|root_sid| root_sid.sid_str)            .collect::<Vec<_>>();        if cap_sids.is_empty() {            anyhow::bail!("workspace-write sandbox has no writable root capability SIDs");        }        (LocalSid::from_string(&cap_sids[0])?, cap_sids)    } else {        (            LocalSid::from_string(&caps.readonly)?,            vec![caps.readonly.clone()],        )    };    unsafe {        allow_null_device(psid_to_use.as_ptr());    }    Ok(ElevatedSpawnContext {        sandbox_base,        logs_base_dir,        sandbox_creds,        cap_sids,    })}#[cfg(test)]mod tests {    use super::SpawnPrepOptions;    use super::deny_root_capabilities_for_path;    use super::legacy_session_capability_roots;    use super::prepare_legacy_spawn_context;    use super::prepare_spawn_context_common;    use super::root_capability_sids;    use crate::cap::load_or_create_cap_sids;    use crate::cap::workspace_write_cap_sid_for_root;    use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;    use codex_protocol::models::PermissionProfile;    use codex_protocol::permissions::NetworkSandboxPolicy;    use codex_utils_absolute_path::AbsolutePathBuf;    use pretty_assertions::assert_eq;    use std::collections::HashMap;    use std::path::Path;    use tempfile::TempDir;    fn workspace_profile(        network_policy: NetworkSandboxPolicy,        writable_roots: &[AbsolutePathBuf],        exclude_tmpdir_env_var: bool,        exclude_slash_tmp: bool,    ) -> PermissionProfile {        PermissionProfile::workspace_write_with(            writable_roots,            network_policy,            exclude_tmpdir_env_var,            exclude_slash_tmp,        )    }    fn workspace_roots_for(root: &Path) -> Vec<AbsolutePathBuf> {        vec![AbsolutePathBuf::from_absolute_path(root).expect("absolute workspace root")]    }    fn should_apply_network_block(permission_profile: &PermissionProfile) -> bool {        ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(            permission_profile,            &[],        )        .expect("managed permission profile")        .should_apply_network_block()    }    #[test]    fn no_network_env_rewrite_applies_for_workspace_write() {        assert!(should_apply_network_block(            &PermissionProfile::workspace_write()        ));    }    #[test]    fn no_network_env_rewrite_skips_when_network_access_is_allowed() {        assert!(!should_apply_network_block(&workspace_profile(            NetworkSandboxPolicy::Enabled,            &[],            /*exclude_tmpdir_env_var*/ false,            /*exclude_slash_tmp*/ false,        )));    }    #[test]    fn legacy_spawn_env_applies_offline_network_rewrite() {        let codex_home = TempDir::new().expect("tempdir");        let cwd = TempDir::new().expect("tempdir");        let mut env_map = HashMap::new();        let workspace_roots = workspace_roots_for(cwd.path());        let _context = prepare_legacy_spawn_context(            &PermissionProfile::workspace_write(),            workspace_roots.as_slice(),            codex_home.path(),            cwd.path(),            &mut env_map,            &["cmd.exe".to_string()],            SpawnPrepOptions {                inherit_path: true,                add_git_safe_directory: false,            },        )        .expect("legacy env prep");        assert_eq!(env_map.get("SBX_NONET_ACTIVE"), Some(&"1".to_string()));        assert_eq!(            env_map.get("HTTP_PROXY"),            Some(&"http://127.0.0.1:9".to_string())        );    }    #[test]    fn common_spawn_env_keeps_network_env_unchanged() {        let codex_home = TempDir::new().expect("tempdir");        let cwd = TempDir::new().expect("tempdir");        let mut env_map = HashMap::from([(            "HTTP_PROXY".to_string(),            "http://user.proxy:8080".to_string(),        )]);        let workspace_roots = workspace_roots_for(cwd.path());        let context = prepare_spawn_context_common(            &PermissionProfile::workspace_write(),            workspace_roots.as_slice(),            codex_home.path(),            cwd.path(),            &mut env_map,            &["cmd.exe".to_string()],            SpawnPrepOptions {                inherit_path: true,                add_git_safe_directory: true,            },        )        .expect("preserve existing env prep");        assert!(context.uses_write_capabilities);        assert_eq!(env_map.get("SBX_NONET_ACTIVE"), None);        assert_eq!(            env_map.get("HTTP_PROXY"),            Some(&"http://user.proxy:8080".to_string())        );    }    #[test]    fn legacy_session_capability_roots_use_runtime_workspace_roots_for_workspace_root() {        let tmp = TempDir::new().expect("tempdir");        let codex_home = tmp.path().join("codex-home");        let workspace_root = tmp.path().join("workspace");        let command_cwd = workspace_root.join("subdir");        std::fs::create_dir_all(&codex_home).expect("create codex home");        std::fs::create_dir_all(&command_cwd).expect("create command cwd");        let permission_profile = workspace_profile(            NetworkSandboxPolicy::Restricted,            &[],            /*exclude_tmpdir_env_var*/ true,            /*exclude_slash_tmp*/ true,        );        let workspace_roots = workspace_roots_for(workspace_root.as_path());        let permissions =            ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(                &permission_profile,                workspace_roots.as_slice(),            )            .expect("managed permission profile");        let roots = legacy_session_capability_roots(            &permissions,            &command_cwd,            &HashMap::new(),            &codex_home,        );        assert_eq!(            roots,            vec![dunce::canonicalize(&workspace_root).expect("canonical workspace root")]        );    }    #[test]    fn root_capability_sids_only_include_active_roots() {        let temp = TempDir::new().expect("tempdir");        let codex_home = temp.path().join("codex-home");        let workspace = temp.path().join("workspace");        let active_root = temp.path().join("active-root");        let stale_root = temp.path().join("stale-root");        std::fs::create_dir_all(&codex_home).expect("create codex home");        std::fs::create_dir_all(&workspace).expect("create workspace");        std::fs::create_dir_all(&active_root).expect("create active root");        std::fs::create_dir_all(&stale_root).expect("create stale root");        let stale_sid = workspace_write_cap_sid_for_root(&codex_home, &workspace, &stale_root)            .expect("stale sid");        let active_sid = workspace_write_cap_sid_for_root(&codex_home, &workspace, &active_root)            .expect("active sid");        let workspace_sid = workspace_write_cap_sid_for_root(&codex_home, &workspace, &workspace)            .expect("workspace sid");        let caps = load_or_create_cap_sids(&codex_home).expect("load caps");        let sid_strs = root_capability_sids(            &codex_home,            &workspace,            vec![workspace.clone(), active_root],        )        .expect("root capabilities")        .into_iter()        .map(|root_sid| root_sid.sid_str)        .collect::<Vec<_>>();        assert_eq!(sid_strs.len(), 2);        assert!(sid_strs.contains(&workspace_sid));        assert!(sid_strs.contains(&active_sid));        assert!(!sid_strs.contains(&stale_sid));        assert!(!sid_strs.contains(&caps.workspace));    }    #[test]    fn legacy_deny_path_includes_nested_active_root_sid() {        let temp = TempDir::new().expect("tempdir");        let codex_home = temp.path().join("codex-home");        let workspace = temp.path().join("workspace");        let protected_dir = workspace.join(".codex");        let nested_root = protected_dir.join("nested-root");        let unrelated_root = temp.path().join("unrelated-root");        std::fs::create_dir_all(&codex_home).expect("create codex home");        std::fs::create_dir_all(&workspace).expect("create workspace");        std::fs::create_dir_all(&nested_root).expect("create nested root");        std::fs::create_dir_all(&unrelated_root).expect("create unrelated root");        let workspace_sid = workspace_write_cap_sid_for_root(&codex_home, &workspace, &workspace)            .expect("workspace sid");        let nested_sid = workspace_write_cap_sid_for_root(&codex_home, &workspace, &nested_root)            .expect("nested sid");        let unrelated_sid =            workspace_write_cap_sid_for_root(&codex_home, &workspace, &unrelated_root)                .expect("unrelated sid");        let root_sids = root_capability_sids(            &codex_home,            &workspace,            vec![workspace.clone(), nested_root, unrelated_root],        )        .expect("root capabilities");        let deny_sid_strs = deny_root_capabilities_for_path(&protected_dir, &root_sids)            .into_iter()            .map(|root_sid| root_sid.sid_str.clone())            .collect::<Vec<_>>();        assert_eq!(deny_sid_strs, vec![workspace_sid, nested_sid]);        assert!(!deny_sid_strs.contains(&unrelated_sid));    }    #[test]    fn legacy_capability_roots_use_effective_write_roots() {        let temp = TempDir::new().expect("tempdir");        let codex_home = temp.path().join("codex-home");        let workspace = temp.path().join("workspace");        let active_root = temp.path().join("active-root");        let sandbox_root = codex_home.join(".sandbox");        std::fs::create_dir_all(&codex_home).expect("create codex home");        std::fs::create_dir_all(&workspace).expect("create workspace");        std::fs::create_dir_all(&active_root).expect("create active root");        std::fs::create_dir_all(&sandbox_root).expect("create sandbox root");        let writable_roots = vec![            AbsolutePathBuf::try_from(active_root.as_path()).expect("active root"),            AbsolutePathBuf::try_from(codex_home.as_path()).expect("codex home"),            AbsolutePathBuf::try_from(sandbox_root.as_path()).expect("sandbox root"),        ];        let permission_profile = workspace_profile(            NetworkSandboxPolicy::Restricted,            &writable_roots,            /*exclude_tmpdir_env_var*/ true,            /*exclude_slash_tmp*/ true,        );        let workspace_roots = workspace_roots_for(workspace.as_path());        let permissions =            ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(                &permission_profile,                workspace_roots.as_slice(),            )            .expect("managed permission profile");        let roots =            legacy_session_capability_roots(&permissions, &workspace, &HashMap::new(), &codex_home);        assert!(roots.contains(&dunce::canonicalize(&workspace).expect("workspace")));        assert!(roots.contains(&dunce::canonicalize(&active_root).expect("active root")));        assert!(!roots.contains(&dunce::canonicalize(&codex_home).expect("codex home")));        assert!(!roots.contains(&dunce::canonicalize(&sandbox_root).expect("sandbox root")));    }}