mirror of
https://github.com/openai/codex.git
synced 2026-11-12 22:42:36 +00:00
windows-sandbox: drive write roots from resolved permissions (#22923)
## Why This is the third PR in the Windows sandbox `SandboxPolicy` -> `PermissionProfile` migration stack. #22896 introduced `ResolvedWindowsSandboxPermissions`, and #22918 moved elevated runner IPC to carry `PermissionProfile`. This PR starts moving the remaining setup/spawn helpers away from asking legacy enum questions like “is this `WorkspaceWrite`?” and toward resolved runtime permission questions like “does this profile require write capability roots?” ## What changed - Added resolved-permissions helpers for network identity and write-capability detection. - Moved setup write-root gathering to operate on `ResolvedWindowsSandboxPermissions`, with the legacy `SandboxPolicy` wrapper left in place for existing call sites. - Updated identity setup, elevated capture setup, and world-writable audit denies to use resolved write roots. - Updated spawn preparation to carry resolved permissions in `SpawnContext` and use them for network blocking, setup write roots, elevated capability SID selection, and legacy capability roots. - Removed a now-unused legacy write-root helper. ## Verification - `cargo test -p codex-windows-sandbox` - `just fix -p codex-windows-sandbox` - Existing stack checks are green on #22896 and #22918; CI has started for this PR. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/22923). * #23715 * #23714 * #23167 * __->__ #22923
This commit is contained in:
1 parent
f48be015d6
commit
e1ec0eee5f
10 files changed
+361
-134
No files matched your search
@@ -18,6 +18,7 @@ use serial_test::serial;
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
struct EnvVarGuard {
|
struct EnvVarGuard {
|
||||||
@@ -46,13 +47,49 @@ impl Drop for EnvVarGuard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum TestCodexHome {
|
||||||
|
Persistent(PathBuf),
|
||||||
|
Temporary(TempDir),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestCodexHome {
|
||||||
|
fn path(&self) -> &Path {
|
||||||
|
match self {
|
||||||
|
Self::Persistent(path) => path.as_path(),
|
||||||
|
Self::Temporary(temp_dir) => temp_dir.path(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn codex_home_for_windows_sandbox_test(name: &str) -> anyhow::Result<TestCodexHome> {
|
||||||
|
if let Some(test_tmpdir) = std::env::var_os("TEST_TMPDIR") {
|
||||||
|
// The elevated backend provisions machine-local sandbox users. Bazel
|
||||||
|
// retries run in the same Windows VM, so keep CODEX_HOME stable within
|
||||||
|
// the test temp root and let setup reconcile its persisted ACL state.
|
||||||
|
let codex_home = PathBuf::from(test_tmpdir).join(name);
|
||||||
|
std::fs::create_dir_all(&codex_home)
|
||||||
|
.with_context(|| format!("create stable test CODEX_HOME {}", codex_home.display()))?;
|
||||||
|
return Ok(TestCodexHome::Persistent(codex_home));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(TestCodexHome::Temporary(TempDir::new()?))
|
||||||
|
}
|
||||||
|
|
||||||
fn stage_windows_sandbox_helpers() -> anyhow::Result<()> {
|
fn stage_windows_sandbox_helpers() -> anyhow::Result<()> {
|
||||||
let test_exe = std::env::current_exe().context("resolve current Windows test executable")?;
|
let test_exe = std::env::current_exe().context("resolve current Windows test executable")?;
|
||||||
let test_exe_dir = test_exe
|
let test_exe_dir = test_exe
|
||||||
.parent()
|
.parent()
|
||||||
.context("Windows test executable should have a parent directory")?;
|
.context("Windows test executable should have a parent directory")?;
|
||||||
let resources_dir = test_exe_dir.join("codex-resources");
|
let resources_dir = test_exe_dir.join("codex-resources");
|
||||||
std::fs::create_dir_all(&resources_dir)?;
|
match std::fs::create_dir_all(&resources_dir) {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(err)
|
||||||
|
if err.kind() == std::io::ErrorKind::PermissionDenied && resources_dir.is_dir() => {}
|
||||||
|
Err(err) => {
|
||||||
|
return Err(err)
|
||||||
|
.with_context(|| format!("create resources dir {}", resources_dir.display()));
|
||||||
|
}
|
||||||
|
}
|
||||||
for helper_name in ["codex-windows-sandbox-setup", "codex-command-runner"] {
|
for helper_name in ["codex-windows-sandbox-setup", "codex-command-runner"] {
|
||||||
let helper = codex_utils_cargo_bin::cargo_bin(helper_name)?;
|
let helper = codex_utils_cargo_bin::cargo_bin(helper_name)?;
|
||||||
let file_name = Path::new(helper_name).with_extension("exe");
|
let file_name = Path::new(helper_name).with_extension("exe");
|
||||||
@@ -64,8 +101,9 @@ fn stage_windows_sandbox_helpers() -> anyhow::Result<()> {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[serial(codex_home)]
|
#[serial(codex_home)]
|
||||||
async fn windows_restricted_token_rejects_exact_and_glob_deny_read_policy() -> anyhow::Result<()> {
|
async fn windows_restricted_token_rejects_exact_and_glob_deny_read_policy() -> anyhow::Result<()> {
|
||||||
let temp_home = TempDir::new()?;
|
let codex_home =
|
||||||
let _codex_home_guard = EnvVarGuard::set("CODEX_HOME", temp_home.path().as_os_str());
|
codex_home_for_windows_sandbox_test("windows-restricted-token-deny-read-codex-home")?;
|
||||||
|
let _codex_home_guard = EnvVarGuard::set("CODEX_HOME", codex_home.path().as_os_str());
|
||||||
let workspace = TempDir::new()?;
|
let workspace = TempDir::new()?;
|
||||||
let cwd = dunce::canonicalize(workspace.path())?.abs();
|
let cwd = dunce::canonicalize(workspace.path())?.abs();
|
||||||
let secret = cwd.join("secret.env");
|
let secret = cwd.join("secret.env");
|
||||||
@@ -144,8 +182,8 @@ async fn windows_restricted_token_rejects_exact_and_glob_deny_read_policy() -> a
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[serial(codex_home)]
|
#[serial(codex_home)]
|
||||||
async fn windows_elevated_enforces_exact_and_glob_deny_read_policy() -> anyhow::Result<()> {
|
async fn windows_elevated_enforces_exact_and_glob_deny_read_policy() -> anyhow::Result<()> {
|
||||||
let temp_home = TempDir::new()?;
|
let codex_home = codex_home_for_windows_sandbox_test("windows-elevated-deny-read-codex-home")?;
|
||||||
let _codex_home_guard = EnvVarGuard::set("CODEX_HOME", temp_home.path().as_os_str());
|
let _codex_home_guard = EnvVarGuard::set("CODEX_HOME", codex_home.path().as_os_str());
|
||||||
stage_windows_sandbox_helpers()?;
|
stage_windows_sandbox_helpers()?;
|
||||||
let workspace = TempDir::new()?;
|
let workspace = TempDir::new()?;
|
||||||
let cwd = dunce::canonicalize(workspace.path())?.abs();
|
let cwd = dunce::canonicalize(workspace.path())?.abs();
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ pub struct AllowDenyPaths {
|
|||||||
|
|
||||||
pub(crate) fn compute_allow_paths(
|
pub(crate) fn compute_allow_paths(
|
||||||
policy: &SandboxPolicy,
|
policy: &SandboxPolicy,
|
||||||
_policy_cwd: &Path,
|
policy_cwd: &Path,
|
||||||
command_cwd: &Path,
|
command_cwd: &Path,
|
||||||
env_map: &HashMap<String, String>,
|
env_map: &HashMap<String, String>,
|
||||||
) -> AllowDenyPaths {
|
) -> AllowDenyPaths {
|
||||||
let permissions =
|
let permissions =
|
||||||
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, command_cwd);
|
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, policy_cwd);
|
||||||
compute_allow_paths_for_permissions(&permissions, command_cwd, env_map)
|
compute_allow_paths_for_permissions(&permissions, command_cwd, env_map)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,6 +91,35 @@ mod tests {
|
|||||||
assert!(paths.deny.is_empty(), "no deny paths expected");
|
assert!(paths.deny.is_empty(), "no deny paths expected");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn uses_policy_cwd_for_legacy_workspace_root() {
|
||||||
|
let tmp = TempDir::new().expect("tempdir");
|
||||||
|
let policy_cwd = tmp.path().join("workspace");
|
||||||
|
let command_cwd = policy_cwd.join("subdir");
|
||||||
|
fs::create_dir_all(&command_cwd).expect("create command cwd");
|
||||||
|
|
||||||
|
let policy = SandboxPolicy::WorkspaceWrite {
|
||||||
|
writable_roots: vec![],
|
||||||
|
network_access: false,
|
||||||
|
exclude_tmpdir_env_var: true,
|
||||||
|
exclude_slash_tmp: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let paths = compute_allow_paths(&policy, &policy_cwd, &command_cwd, &HashMap::new());
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
paths
|
||||||
|
.allow
|
||||||
|
.contains(&dunce::canonicalize(&policy_cwd).unwrap())
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!paths
|
||||||
|
.allow
|
||||||
|
.contains(&dunce::canonicalize(&command_cwd).unwrap())
|
||||||
|
);
|
||||||
|
assert!(paths.deny.is_empty(), "no deny paths expected");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn excludes_tmp_env_vars_when_requested() {
|
fn excludes_tmp_env_vars_when_requested() {
|
||||||
let tmp = TempDir::new().expect("tempdir");
|
let tmp = TempDir::new().expect("tempdir");
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ use crate::logging::debug_log;
|
|||||||
use crate::logging::log_note;
|
use crate::logging::log_note;
|
||||||
use crate::path_normalization::canonical_path_key;
|
use crate::path_normalization::canonical_path_key;
|
||||||
use crate::policy::SandboxPolicy;
|
use crate::policy::SandboxPolicy;
|
||||||
use crate::setup::effective_write_roots_for_setup;
|
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
|
||||||
|
use crate::setup::effective_write_roots_for_permissions;
|
||||||
use crate::token::LocalSid;
|
use crate::token::LocalSid;
|
||||||
use crate::token::world_sid;
|
use crate::token::world_sid;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@@ -259,11 +260,18 @@ pub fn apply_capability_denies_for_world_writable(
|
|||||||
let cap_path = cap_sid_file(codex_home);
|
let cap_path = cap_sid_file(codex_home);
|
||||||
let caps = load_or_create_cap_sids(codex_home)?;
|
let caps = load_or_create_cap_sids(codex_home)?;
|
||||||
std::fs::write(&cap_path, serde_json::to_string(&caps)?)?;
|
std::fs::write(&cap_path, serde_json::to_string(&caps)?)?;
|
||||||
let (active_sids, workspace_roots): (Vec<LocalSid>, Vec<PathBuf>) = match sandbox_policy {
|
if matches!(
|
||||||
SandboxPolicy::WorkspaceWrite { .. } => {
|
sandbox_policy,
|
||||||
let roots = effective_write_roots_for_setup(
|
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
|
||||||
sandbox_policy,
|
) {
|
||||||
cwd,
|
return Ok(());
|
||||||
|
}
|
||||||
|
let permissions =
|
||||||
|
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(sandbox_policy, cwd);
|
||||||
|
let (active_sids, workspace_roots): (Vec<LocalSid>, Vec<PathBuf>) =
|
||||||
|
if permissions.uses_write_capabilities_for_cwd(cwd, env_map) {
|
||||||
|
let roots = effective_write_roots_for_permissions(
|
||||||
|
&permissions,
|
||||||
cwd,
|
cwd,
|
||||||
env_map,
|
env_map,
|
||||||
codex_home,
|
codex_home,
|
||||||
@@ -277,14 +285,9 @@ pub fn apply_capability_denies_for_world_writable(
|
|||||||
})
|
})
|
||||||
.collect::<Result<Vec<_>>>()?;
|
.collect::<Result<Vec<_>>>()?;
|
||||||
(active_sids, roots)
|
(active_sids, roots)
|
||||||
}
|
} else {
|
||||||
SandboxPolicy::ReadOnly { .. } => {
|
|
||||||
(vec![LocalSid::from_string(&caps.readonly)?], Vec::new())
|
(vec![LocalSid::from_string(&caps.readonly)?], Vec::new())
|
||||||
}
|
};
|
||||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
for path in flagged {
|
for path in flagged {
|
||||||
if workspace_roots
|
if workspace_roots
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@@ -39,10 +39,11 @@ mod windows_impl {
|
|||||||
use crate::logging::log_success;
|
use crate::logging::log_success;
|
||||||
use crate::policy::SandboxPolicy;
|
use crate::policy::SandboxPolicy;
|
||||||
use crate::policy::parse_policy;
|
use crate::policy::parse_policy;
|
||||||
|
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
|
||||||
use crate::runner_client::spawn_runner_transport;
|
use crate::runner_client::spawn_runner_transport;
|
||||||
use crate::sandbox_utils::ensure_codex_home_exists;
|
use crate::sandbox_utils::ensure_codex_home_exists;
|
||||||
use crate::sandbox_utils::inject_git_safe_directory;
|
use crate::sandbox_utils::inject_git_safe_directory;
|
||||||
use crate::setup::effective_write_roots_for_setup;
|
use crate::setup::effective_write_roots_for_permissions;
|
||||||
use crate::token::LocalSid;
|
use crate::token::LocalSid;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use codex_protocol::models::PermissionProfile;
|
use codex_protocol::models::PermissionProfile;
|
||||||
@@ -81,6 +82,10 @@ mod windows_impl {
|
|||||||
.map(AbsolutePathBuf::to_path_buf)
|
.map(AbsolutePathBuf::to_path_buf)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let policy = parse_policy(policy_json_or_preset)?;
|
let policy = parse_policy(policy_json_or_preset)?;
|
||||||
|
let permissions = ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(
|
||||||
|
&policy,
|
||||||
|
sandbox_policy_cwd,
|
||||||
|
);
|
||||||
normalize_null_device_env(&mut env_map);
|
normalize_null_device_env(&mut env_map);
|
||||||
ensure_non_interactive_pager(&mut env_map);
|
ensure_non_interactive_pager(&mut env_map);
|
||||||
inherit_path_env(&mut env_map);
|
inherit_path_env(&mut env_map);
|
||||||
@@ -112,32 +117,26 @@ mod windows_impl {
|
|||||||
anyhow::bail!("DangerFullAccess and ExternalSandbox are not supported for sandboxing")
|
anyhow::bail!("DangerFullAccess and ExternalSandbox are not supported for sandboxing")
|
||||||
}
|
}
|
||||||
let caps = load_or_create_cap_sids(codex_home)?;
|
let caps = load_or_create_cap_sids(codex_home)?;
|
||||||
let (sid_for_null, cap_sids) = match &policy {
|
let (sid_for_null, cap_sids) = if permissions.uses_write_capabilities_for_cwd(cwd, &env_map)
|
||||||
SandboxPolicy::ReadOnly { .. } => {
|
{
|
||||||
let sid = LocalSid::from_string(&caps.readonly)?;
|
let write_roots = effective_write_roots_for_permissions(
|
||||||
(sid, vec![caps.readonly])
|
&permissions,
|
||||||
}
|
cwd,
|
||||||
SandboxPolicy::WorkspaceWrite { .. } => {
|
&env_map,
|
||||||
let write_roots = effective_write_roots_for_setup(
|
codex_home,
|
||||||
&policy,
|
write_roots_override,
|
||||||
sandbox_policy_cwd,
|
);
|
||||||
cwd,
|
let cap_sids = write_roots
|
||||||
&env_map,
|
.iter()
|
||||||
codex_home,
|
.map(|root| workspace_write_cap_sid_for_root(codex_home, cwd, root))
|
||||||
write_roots_override,
|
.collect::<Result<Vec<_>>>()?;
|
||||||
);
|
if cap_sids.is_empty() {
|
||||||
let cap_sids = write_roots
|
anyhow::bail!("workspace-write sandbox has no writable root capability SIDs");
|
||||||
.iter()
|
|
||||||
.map(|root| workspace_write_cap_sid_for_root(codex_home, cwd, root))
|
|
||||||
.collect::<Result<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)
|
|
||||||
}
|
|
||||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
|
||||||
unreachable!("DangerFullAccess handled above")
|
|
||||||
}
|
}
|
||||||
|
(LocalSid::from_string(&cap_sids[0])?, cap_sids)
|
||||||
|
} else {
|
||||||
|
let sid = LocalSid::from_string(&caps.readonly)?;
|
||||||
|
(sid, vec![caps.readonly])
|
||||||
};
|
};
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
use crate::dpapi;
|
use crate::dpapi;
|
||||||
use crate::logging::debug_log;
|
use crate::logging::debug_log;
|
||||||
use crate::policy::SandboxPolicy;
|
use crate::policy::SandboxPolicy;
|
||||||
|
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
|
||||||
use crate::setup::SandboxNetworkIdentity;
|
use crate::setup::SandboxNetworkIdentity;
|
||||||
use crate::setup::SandboxUserRecord;
|
use crate::setup::SandboxUserRecord;
|
||||||
use crate::setup::SandboxUsersFile;
|
use crate::setup::SandboxUsersFile;
|
||||||
use crate::setup::SetupMarker;
|
use crate::setup::SetupMarker;
|
||||||
use crate::setup::gather_read_roots;
|
use crate::setup::gather_read_roots;
|
||||||
use crate::setup::gather_write_roots;
|
use crate::setup::gather_write_roots_for_permissions;
|
||||||
use crate::setup::offline_proxy_settings_from_env;
|
use crate::setup::offline_proxy_settings_from_env;
|
||||||
use crate::setup::run_elevated_setup;
|
use crate::setup::run_elevated_setup;
|
||||||
use crate::setup::run_setup_refresh_with_overrides;
|
use crate::setup::run_setup_refresh_with_overrides;
|
||||||
@@ -143,14 +144,16 @@ pub fn require_logon_sandbox_creds(
|
|||||||
deny_write_paths_override: &[PathBuf],
|
deny_write_paths_override: &[PathBuf],
|
||||||
proxy_enforced: bool,
|
proxy_enforced: bool,
|
||||||
) -> Result<SandboxCreds> {
|
) -> Result<SandboxCreds> {
|
||||||
|
let permissions =
|
||||||
|
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, policy_cwd);
|
||||||
let sandbox_dir = crate::setup::sandbox_dir(codex_home);
|
let sandbox_dir = crate::setup::sandbox_dir(codex_home);
|
||||||
let needed_read = read_roots_override
|
let needed_read = read_roots_override
|
||||||
.map(<[PathBuf]>::to_vec)
|
.map(<[PathBuf]>::to_vec)
|
||||||
.unwrap_or_else(|| gather_read_roots(command_cwd, policy, codex_home));
|
.unwrap_or_else(|| gather_read_roots(command_cwd, policy, codex_home));
|
||||||
let needed_write = write_roots_override
|
let needed_write = write_roots_override
|
||||||
.map(<[PathBuf]>::to_vec)
|
.map(<[PathBuf]>::to_vec)
|
||||||
.unwrap_or_else(|| gather_write_roots(policy, policy_cwd, command_cwd, env_map));
|
.unwrap_or_else(|| gather_write_roots_for_permissions(&permissions, command_cwd, env_map));
|
||||||
let network_identity = SandboxNetworkIdentity::from_policy(policy, proxy_enforced);
|
let network_identity = SandboxNetworkIdentity::from_permissions(&permissions, proxy_enforced);
|
||||||
let desired_offline_proxy_settings = offline_proxy_settings_from_env(env_map, network_identity);
|
let desired_offline_proxy_settings = offline_proxy_settings_from_env(env_map, network_identity);
|
||||||
// NOTE: Do not add CODEX_HOME/.sandbox to `needed_write`; it must remain non-writable by the
|
// NOTE: Do not add CODEX_HOME/.sandbox to `needed_write`; it must remain non-writable by the
|
||||||
// restricted capability token. The setup helper's `lock_sandbox_dir` is responsible for
|
// restricted capability token. The setup helper's `lock_sandbox_dir` is responsible for
|
||||||
|
|||||||
@@ -293,6 +293,7 @@ mod windows_impl {
|
|||||||
use super::process::create_process_as_user;
|
use super::process::create_process_as_user;
|
||||||
use super::sandbox_utils::ensure_codex_home_exists;
|
use super::sandbox_utils::ensure_codex_home_exists;
|
||||||
use super::spawn_prep::LegacyAclSids;
|
use super::spawn_prep::LegacyAclSids;
|
||||||
|
use super::spawn_prep::SpawnPrepOptions;
|
||||||
use super::spawn_prep::allow_null_device_for_workspace_write;
|
use super::spawn_prep::allow_null_device_for_workspace_write;
|
||||||
use super::spawn_prep::apply_legacy_session_acl_rules;
|
use super::spawn_prep::apply_legacy_session_acl_rules;
|
||||||
use super::spawn_prep::legacy_session_capability_roots;
|
use super::spawn_prep::legacy_session_capability_roots;
|
||||||
@@ -400,17 +401,20 @@ mod windows_impl {
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let common = prepare_legacy_spawn_context(
|
let common = prepare_legacy_spawn_context(
|
||||||
policy_json_or_preset,
|
policy_json_or_preset,
|
||||||
|
sandbox_policy_cwd,
|
||||||
codex_home,
|
codex_home,
|
||||||
cwd,
|
cwd,
|
||||||
&mut env_map,
|
&mut env_map,
|
||||||
&command,
|
&command,
|
||||||
/*inherit_path*/ false,
|
SpawnPrepOptions {
|
||||||
/*add_git_safe_directory*/ false,
|
inherit_path: false,
|
||||||
|
add_git_safe_directory: false,
|
||||||
|
},
|
||||||
)?;
|
)?;
|
||||||
let policy = common.policy;
|
let policy = common.policy;
|
||||||
let current_dir = common.current_dir;
|
let current_dir = common.current_dir;
|
||||||
let logs_base_dir = common.logs_base_dir.as_deref();
|
let logs_base_dir = common.logs_base_dir.as_deref();
|
||||||
let is_workspace_write = common.is_workspace_write;
|
let uses_write_capabilities = common.uses_write_capabilities;
|
||||||
if !policy.has_full_disk_read_access() {
|
if !policy.has_full_disk_read_access() {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"Restricted read-only access requires the elevated Windows sandbox backend"
|
"Restricted read-only access requires the elevated Windows sandbox backend"
|
||||||
@@ -429,7 +433,7 @@ mod windows_impl {
|
|||||||
codex_home,
|
codex_home,
|
||||||
);
|
);
|
||||||
let security = prepare_legacy_session_security(&policy, codex_home, cwd, capability_roots)?;
|
let security = prepare_legacy_session_security(&policy, codex_home, cwd, capability_roots)?;
|
||||||
allow_null_device_for_workspace_write(is_workspace_write);
|
allow_null_device_for_workspace_write(uses_write_capabilities);
|
||||||
apply_legacy_session_acl_rules(
|
apply_legacy_session_acl_rules(
|
||||||
&policy,
|
&policy,
|
||||||
sandbox_policy_cwd,
|
sandbox_policy_cwd,
|
||||||
@@ -618,7 +622,8 @@ mod windows_impl {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::policy::SandboxPolicy;
|
use crate::policy::SandboxPolicy;
|
||||||
use crate::spawn_prep::should_apply_network_block;
|
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
fn workspace_policy(network_access: bool) -> SandboxPolicy {
|
fn workspace_policy(network_access: bool) -> SandboxPolicy {
|
||||||
SandboxPolicy::WorkspaceWrite {
|
SandboxPolicy::WorkspaceWrite {
|
||||||
@@ -629,6 +634,11 @@ mod windows_impl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn should_apply_network_block(policy: &SandboxPolicy) -> bool {
|
||||||
|
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, Path::new("."))
|
||||||
|
.should_apply_network_block()
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn applies_network_block_when_access_is_disabled() {
|
fn applies_network_block_when_access_is_disabled() {
|
||||||
assert!(should_apply_network_block(&workspace_policy(
|
assert!(should_apply_network_block(&workspace_policy(
|
||||||
|
|||||||
@@ -56,16 +56,10 @@ pub fn token_mode_for_permission_profile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ResolvedWindowsSandboxPermissions {
|
impl ResolvedWindowsSandboxPermissions {
|
||||||
pub(crate) fn from_legacy_policy(policy: &SandboxPolicy) -> Self {
|
|
||||||
Self {
|
|
||||||
file_system: FileSystemSandboxPolicy::from(policy),
|
|
||||||
network: NetworkSandboxPolicy::from(policy),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn from_legacy_policy_for_cwd(policy: &SandboxPolicy, cwd: &Path) -> Self {
|
pub(crate) fn from_legacy_policy_for_cwd(policy: &SandboxPolicy, cwd: &Path) -> Self {
|
||||||
Self {
|
Self {
|
||||||
file_system: FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(policy, cwd),
|
file_system: FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(policy, cwd)
|
||||||
|
.materialize_project_roots_with_cwd(cwd),
|
||||||
network: NetworkSandboxPolicy::from(policy),
|
network: NetworkSandboxPolicy::from(policy),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,6 +88,18 @@ impl ResolvedWindowsSandboxPermissions {
|
|||||||
!self.network.is_enabled()
|
!self.network.is_enabled()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn network_policy(&self) -> NetworkSandboxPolicy {
|
||||||
|
self.network
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn uses_write_capabilities_for_cwd(
|
||||||
|
&self,
|
||||||
|
cwd: &Path,
|
||||||
|
env_map: &HashMap<String, String>,
|
||||||
|
) -> bool {
|
||||||
|
!self.writable_roots_for_cwd(cwd, env_map).is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn writable_roots_for_cwd(
|
pub(crate) fn writable_roots_for_cwd(
|
||||||
&self,
|
&self,
|
||||||
cwd: &Path,
|
cwd: &Path,
|
||||||
@@ -207,6 +213,34 @@ mod tests {
|
|||||||
assert_eq!(expected_roots, roots);
|
assert_eq!(expected_roots, roots);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn legacy_workspace_root_stays_bound_to_policy_cwd() {
|
||||||
|
let tmp = TempDir::new().expect("tempdir");
|
||||||
|
let policy_cwd = tmp.path().join("workspace");
|
||||||
|
let command_cwd = policy_cwd.join("subdir");
|
||||||
|
std::fs::create_dir_all(&command_cwd).expect("create command cwd");
|
||||||
|
|
||||||
|
let policy = SandboxPolicy::WorkspaceWrite {
|
||||||
|
writable_roots: Vec::new(),
|
||||||
|
network_access: false,
|
||||||
|
exclude_tmpdir_env_var: true,
|
||||||
|
exclude_slash_tmp: true,
|
||||||
|
};
|
||||||
|
let permissions =
|
||||||
|
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(&policy, &policy_cwd);
|
||||||
|
|
||||||
|
let roots = permissions
|
||||||
|
.writable_roots_for_cwd(&command_cwd, &HashMap::new())
|
||||||
|
.into_iter()
|
||||||
|
.map(|root| root.root)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
roots,
|
||||||
|
vec![dunce::canonicalize(&policy_cwd).expect("canonical policy cwd")]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn token_mode_for_profile_without_writable_roots_uses_readonly_capability() {
|
fn token_mode_for_profile_without_writable_roots_uses_readonly_capability() {
|
||||||
let tmp = TempDir::new().expect("tempdir");
|
let tmp = TempDir::new().expect("tempdir");
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use crate::logging::log_note;
|
|||||||
use crate::path_normalization::canonical_path_key;
|
use crate::path_normalization::canonical_path_key;
|
||||||
use crate::path_normalization::canonicalize_path;
|
use crate::path_normalization::canonicalize_path;
|
||||||
use crate::policy::SandboxPolicy;
|
use crate::policy::SandboxPolicy;
|
||||||
|
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
|
||||||
use crate::setup_error::SetupErrorCode;
|
use crate::setup_error::SetupErrorCode;
|
||||||
use crate::setup_error::SetupFailure;
|
use crate::setup_error::SetupFailure;
|
||||||
use crate::setup_error::clear_setup_error_report;
|
use crate::setup_error::clear_setup_error_report;
|
||||||
@@ -175,8 +176,12 @@ fn run_setup_refresh_inner(
|
|||||||
let (read_roots, write_roots) = build_payload_roots(&request, &overrides);
|
let (read_roots, write_roots) = build_payload_roots(&request, &overrides);
|
||||||
let deny_read_paths = build_payload_deny_read_paths(overrides.deny_read_paths);
|
let deny_read_paths = build_payload_deny_read_paths(overrides.deny_read_paths);
|
||||||
let deny_write_paths = build_payload_deny_write_paths(&request, overrides.deny_write_paths);
|
let deny_write_paths = build_payload_deny_write_paths(&request, overrides.deny_write_paths);
|
||||||
|
let permissions = ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(
|
||||||
|
request.policy,
|
||||||
|
request.policy_cwd,
|
||||||
|
);
|
||||||
let network_identity =
|
let network_identity =
|
||||||
SandboxNetworkIdentity::from_policy(request.policy, request.proxy_enforced);
|
SandboxNetworkIdentity::from_permissions(&permissions, request.proxy_enforced);
|
||||||
let offline_proxy_settings = offline_proxy_settings_from_env(request.env_map, network_identity);
|
let offline_proxy_settings = offline_proxy_settings_from_env(request.env_map, network_identity);
|
||||||
let payload = ElevationPayload {
|
let payload = ElevationPayload {
|
||||||
version: SETUP_VERSION,
|
version: SETUP_VERSION,
|
||||||
@@ -391,15 +396,16 @@ pub(crate) fn gather_read_roots(
|
|||||||
gather_legacy_full_read_roots(command_cwd, policy, codex_home)
|
gather_legacy_full_read_roots(command_cwd, policy, codex_home)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn gather_write_roots(
|
pub(crate) fn gather_write_roots_for_permissions(
|
||||||
policy: &SandboxPolicy,
|
permissions: &ResolvedWindowsSandboxPermissions,
|
||||||
policy_cwd: &Path,
|
|
||||||
command_cwd: &Path,
|
command_cwd: &Path,
|
||||||
env_map: &HashMap<String, String>,
|
env_map: &HashMap<String, String>,
|
||||||
) -> Vec<PathBuf> {
|
) -> Vec<PathBuf> {
|
||||||
let AllowDenyPaths { allow, .. } =
|
let roots = permissions
|
||||||
compute_allow_paths(policy, policy_cwd, command_cwd, env_map);
|
.writable_roots_for_cwd(command_cwd, env_map)
|
||||||
let roots: Vec<PathBuf> = allow.into_iter().collect();
|
.into_iter()
|
||||||
|
.map(|root| root.root)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
let mut dedup: HashSet<PathBuf> = HashSet::new();
|
let mut dedup: HashSet<PathBuf> = HashSet::new();
|
||||||
let mut out: Vec<PathBuf> = Vec::new();
|
let mut out: Vec<PathBuf> = Vec::new();
|
||||||
for r in canonical_existing(&roots) {
|
for r in canonical_existing(&roots) {
|
||||||
@@ -417,11 +423,29 @@ pub(crate) fn effective_write_roots_for_setup(
|
|||||||
env_map: &HashMap<String, String>,
|
env_map: &HashMap<String, String>,
|
||||||
codex_home: &Path,
|
codex_home: &Path,
|
||||||
write_roots_override: Option<&[PathBuf]>,
|
write_roots_override: Option<&[PathBuf]>,
|
||||||
|
) -> Vec<PathBuf> {
|
||||||
|
let permissions =
|
||||||
|
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, policy_cwd);
|
||||||
|
effective_write_roots_for_permissions(
|
||||||
|
&permissions,
|
||||||
|
command_cwd,
|
||||||
|
env_map,
|
||||||
|
codex_home,
|
||||||
|
write_roots_override,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn effective_write_roots_for_permissions(
|
||||||
|
permissions: &ResolvedWindowsSandboxPermissions,
|
||||||
|
command_cwd: &Path,
|
||||||
|
env_map: &HashMap<String, String>,
|
||||||
|
codex_home: &Path,
|
||||||
|
write_roots_override: Option<&[PathBuf]>,
|
||||||
) -> Vec<PathBuf> {
|
) -> Vec<PathBuf> {
|
||||||
let write_roots = if let Some(roots) = write_roots_override {
|
let write_roots = if let Some(roots) = write_roots_override {
|
||||||
canonical_existing(roots)
|
canonical_existing(roots)
|
||||||
} else {
|
} else {
|
||||||
gather_write_roots(policy, policy_cwd, command_cwd, env_map)
|
gather_write_roots_for_permissions(permissions, command_cwd, env_map)
|
||||||
};
|
};
|
||||||
let write_roots = expand_user_profile_root(write_roots);
|
let write_roots = expand_user_profile_root(write_roots);
|
||||||
let write_roots = filter_user_profile_root(write_roots);
|
let write_roots = filter_user_profile_root(write_roots);
|
||||||
@@ -465,8 +489,11 @@ pub(crate) enum SandboxNetworkIdentity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SandboxNetworkIdentity {
|
impl SandboxNetworkIdentity {
|
||||||
pub(crate) fn from_policy(policy: &SandboxPolicy, proxy_enforced: bool) -> Self {
|
pub(crate) fn from_permissions(
|
||||||
if proxy_enforced || !policy.has_full_network_access() {
|
permissions: &ResolvedWindowsSandboxPermissions,
|
||||||
|
proxy_enforced: bool,
|
||||||
|
) -> Self {
|
||||||
|
if proxy_enforced || !permissions.network_policy().is_enabled() {
|
||||||
Self::Offline
|
Self::Offline
|
||||||
} else {
|
} else {
|
||||||
Self::Online
|
Self::Online
|
||||||
@@ -737,8 +764,12 @@ pub fn run_elevated_setup(
|
|||||||
let (read_roots, write_roots) = build_payload_roots(&request, &overrides);
|
let (read_roots, write_roots) = build_payload_roots(&request, &overrides);
|
||||||
let deny_read_paths = build_payload_deny_read_paths(overrides.deny_read_paths);
|
let deny_read_paths = build_payload_deny_read_paths(overrides.deny_read_paths);
|
||||||
let deny_write_paths = build_payload_deny_write_paths(&request, overrides.deny_write_paths);
|
let deny_write_paths = build_payload_deny_write_paths(&request, overrides.deny_write_paths);
|
||||||
|
let permissions = ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(
|
||||||
|
request.policy,
|
||||||
|
request.policy_cwd,
|
||||||
|
);
|
||||||
let network_identity =
|
let network_identity =
|
||||||
SandboxNetworkIdentity::from_policy(request.policy, request.proxy_enforced);
|
SandboxNetworkIdentity::from_permissions(&permissions, request.proxy_enforced);
|
||||||
let offline_proxy_settings = offline_proxy_settings_from_env(request.env_map, network_identity);
|
let offline_proxy_settings = offline_proxy_settings_from_env(request.env_map, network_identity);
|
||||||
let payload = ElevationPayload {
|
let payload = ElevationPayload {
|
||||||
version: SETUP_VERSION,
|
version: SETUP_VERSION,
|
||||||
@@ -1487,6 +1518,37 @@ mod tests {
|
|||||||
assert!(!effective_write_roots.contains(&forbidden_sandbox));
|
assert!(!effective_write_roots.contains(&forbidden_sandbox));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn effective_write_roots_use_policy_cwd_for_legacy_workspace_root() {
|
||||||
|
let tmp = TempDir::new().expect("tempdir");
|
||||||
|
let codex_home = tmp.path().join("codex-home");
|
||||||
|
let policy_cwd = tmp.path().join("workspace");
|
||||||
|
let command_cwd = policy_cwd.join("subdir");
|
||||||
|
fs::create_dir_all(&codex_home).expect("create codex home");
|
||||||
|
fs::create_dir_all(&command_cwd).expect("create command cwd");
|
||||||
|
|
||||||
|
let policy = SandboxPolicy::WorkspaceWrite {
|
||||||
|
writable_roots: vec![],
|
||||||
|
network_access: false,
|
||||||
|
exclude_tmpdir_env_var: true,
|
||||||
|
exclude_slash_tmp: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let effective_write_roots = super::effective_write_roots_for_setup(
|
||||||
|
&policy,
|
||||||
|
&policy_cwd,
|
||||||
|
&command_cwd,
|
||||||
|
&HashMap::new(),
|
||||||
|
&codex_home,
|
||||||
|
/*write_roots_override*/ None,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
effective_write_roots,
|
||||||
|
vec![dunce::canonicalize(&policy_cwd).expect("canonical policy cwd")]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn payload_deny_write_paths_merge_explicit_and_protected_children() {
|
fn payload_deny_write_paths_merge_explicit_and_protected_children() {
|
||||||
let tmp = TempDir::new().expect("tempdir");
|
let tmp = TempDir::new().expect("tempdir");
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use crate::acl::add_deny_write_ace;
|
|||||||
use crate::acl::allow_null_device;
|
use crate::acl::allow_null_device;
|
||||||
use crate::allow::AllowDenyPaths;
|
use crate::allow::AllowDenyPaths;
|
||||||
use crate::allow::compute_allow_paths;
|
use crate::allow::compute_allow_paths;
|
||||||
|
use crate::allow::compute_allow_paths_for_permissions;
|
||||||
use crate::cap::load_or_create_cap_sids;
|
use crate::cap::load_or_create_cap_sids;
|
||||||
use crate::cap::workspace_write_cap_sid_for_root;
|
use crate::cap::workspace_write_cap_sid_for_root;
|
||||||
use crate::cap::workspace_write_root_contains_path;
|
use crate::cap::workspace_write_root_contains_path;
|
||||||
@@ -22,7 +23,7 @@ use crate::policy::parse_policy;
|
|||||||
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
|
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
|
||||||
use crate::sandbox_utils::ensure_codex_home_exists;
|
use crate::sandbox_utils::ensure_codex_home_exists;
|
||||||
use crate::sandbox_utils::inject_git_safe_directory;
|
use crate::sandbox_utils::inject_git_safe_directory;
|
||||||
use crate::setup::effective_write_roots_for_setup;
|
use crate::setup::effective_write_roots_for_permissions;
|
||||||
use crate::token::LocalSid;
|
use crate::token::LocalSid;
|
||||||
use crate::token::create_readonly_token_with_cap;
|
use crate::token::create_readonly_token_with_cap;
|
||||||
use crate::token::create_workspace_write_token_with_caps_from;
|
use crate::token::create_workspace_write_token_with_caps_from;
|
||||||
@@ -42,10 +43,11 @@ use windows_sys::Win32::Foundation::HANDLE;
|
|||||||
|
|
||||||
pub(crate) struct SpawnContext {
|
pub(crate) struct SpawnContext {
|
||||||
pub(crate) policy: SandboxPolicy,
|
pub(crate) policy: SandboxPolicy,
|
||||||
|
pub(crate) permissions: ResolvedWindowsSandboxPermissions,
|
||||||
pub(crate) current_dir: PathBuf,
|
pub(crate) current_dir: PathBuf,
|
||||||
pub(crate) sandbox_base: PathBuf,
|
pub(crate) sandbox_base: PathBuf,
|
||||||
pub(crate) logs_base_dir: Option<PathBuf>,
|
pub(crate) logs_base_dir: Option<PathBuf>,
|
||||||
pub(crate) is_workspace_write: bool,
|
pub(crate) uses_write_capabilities: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct ElevatedSpawnContext {
|
pub(crate) struct ElevatedSpawnContext {
|
||||||
@@ -54,6 +56,12 @@ pub(crate) struct ElevatedSpawnContext {
|
|||||||
pub(crate) cap_sids: Vec<String>,
|
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) struct LegacySessionSecurity {
|
||||||
pub(crate) h_token: HANDLE,
|
pub(crate) h_token: HANDLE,
|
||||||
pub(crate) readonly_sid: Option<LocalSid>,
|
pub(crate) readonly_sid: Option<LocalSid>,
|
||||||
@@ -73,18 +81,14 @@ pub(crate) struct LegacyAclSids<'a> {
|
|||||||
pub(crate) write_root_sids: &'a [RootCapabilitySid],
|
pub(crate) write_root_sids: &'a [RootCapabilitySid],
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn should_apply_network_block(policy: &SandboxPolicy) -> bool {
|
|
||||||
ResolvedWindowsSandboxPermissions::from_legacy_policy(policy).should_apply_network_block()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_spawn_context_common(
|
fn prepare_spawn_context_common(
|
||||||
policy_json_or_preset: &str,
|
policy_json_or_preset: &str,
|
||||||
|
policy_cwd: &Path,
|
||||||
codex_home: &Path,
|
codex_home: &Path,
|
||||||
cwd: &Path,
|
cwd: &Path,
|
||||||
env_map: &mut HashMap<String, String>,
|
env_map: &mut HashMap<String, String>,
|
||||||
command: &[String],
|
command: &[String],
|
||||||
inherit_path: bool,
|
options: SpawnPrepOptions,
|
||||||
add_git_safe_directory: bool,
|
|
||||||
) -> Result<SpawnContext> {
|
) -> Result<SpawnContext> {
|
||||||
let policy = parse_policy(policy_json_or_preset)?;
|
let policy = parse_policy(policy_json_or_preset)?;
|
||||||
if matches!(
|
if matches!(
|
||||||
@@ -96,10 +100,10 @@ fn prepare_spawn_context_common(
|
|||||||
|
|
||||||
normalize_null_device_env(env_map);
|
normalize_null_device_env(env_map);
|
||||||
ensure_non_interactive_pager(env_map);
|
ensure_non_interactive_pager(env_map);
|
||||||
if inherit_path {
|
if options.inherit_path {
|
||||||
inherit_path_env(env_map);
|
inherit_path_env(env_map);
|
||||||
}
|
}
|
||||||
if add_git_safe_directory {
|
if options.add_git_safe_directory {
|
||||||
inject_git_safe_directory(env_map, cwd);
|
inject_git_safe_directory(env_map, cwd);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,36 +113,39 @@ fn prepare_spawn_context_common(
|
|||||||
let logs_base_dir = Some(sandbox_base.clone());
|
let logs_base_dir = Some(sandbox_base.clone());
|
||||||
log_start(command, logs_base_dir.as_deref());
|
log_start(command, logs_base_dir.as_deref());
|
||||||
|
|
||||||
let is_workspace_write = matches!(&policy, SandboxPolicy::WorkspaceWrite { .. });
|
let permissions =
|
||||||
|
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(&policy, policy_cwd);
|
||||||
|
let uses_write_capabilities = permissions.uses_write_capabilities_for_cwd(cwd, env_map);
|
||||||
|
|
||||||
Ok(SpawnContext {
|
Ok(SpawnContext {
|
||||||
policy,
|
policy,
|
||||||
|
permissions,
|
||||||
current_dir: cwd.to_path_buf(),
|
current_dir: cwd.to_path_buf(),
|
||||||
sandbox_base,
|
sandbox_base,
|
||||||
logs_base_dir,
|
logs_base_dir,
|
||||||
is_workspace_write,
|
uses_write_capabilities,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn prepare_legacy_spawn_context(
|
pub(crate) fn prepare_legacy_spawn_context(
|
||||||
policy_json_or_preset: &str,
|
policy_json_or_preset: &str,
|
||||||
|
policy_cwd: &Path,
|
||||||
codex_home: &Path,
|
codex_home: &Path,
|
||||||
cwd: &Path,
|
cwd: &Path,
|
||||||
env_map: &mut HashMap<String, String>,
|
env_map: &mut HashMap<String, String>,
|
||||||
command: &[String],
|
command: &[String],
|
||||||
inherit_path: bool,
|
options: SpawnPrepOptions,
|
||||||
add_git_safe_directory: bool,
|
|
||||||
) -> Result<SpawnContext> {
|
) -> Result<SpawnContext> {
|
||||||
let common = prepare_spawn_context_common(
|
let common = prepare_spawn_context_common(
|
||||||
policy_json_or_preset,
|
policy_json_or_preset,
|
||||||
|
policy_cwd,
|
||||||
codex_home,
|
codex_home,
|
||||||
cwd,
|
cwd,
|
||||||
env_map,
|
env_map,
|
||||||
command,
|
command,
|
||||||
inherit_path,
|
options,
|
||||||
add_git_safe_directory,
|
|
||||||
)?;
|
)?;
|
||||||
if should_apply_network_block(&common.policy) {
|
if common.permissions.should_apply_network_block() {
|
||||||
apply_no_network_to_env(env_map)?;
|
apply_no_network_to_env(env_map)?;
|
||||||
}
|
}
|
||||||
Ok(common)
|
Ok(common)
|
||||||
@@ -195,14 +202,15 @@ pub(crate) fn legacy_session_capability_roots(
|
|||||||
env_map: &HashMap<String, String>,
|
env_map: &HashMap<String, String>,
|
||||||
codex_home: &Path,
|
codex_home: &Path,
|
||||||
) -> Vec<PathBuf> {
|
) -> Vec<PathBuf> {
|
||||||
let allow_paths = compute_allow_paths(policy, policy_cwd, current_dir, env_map)
|
let permissions =
|
||||||
|
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, policy_cwd);
|
||||||
|
let allow_paths = compute_allow_paths_for_permissions(&permissions, current_dir, env_map)
|
||||||
.allow
|
.allow
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
if matches!(policy, SandboxPolicy::WorkspaceWrite { .. }) {
|
if permissions.uses_write_capabilities_for_cwd(current_dir, env_map) {
|
||||||
effective_write_roots_for_setup(
|
effective_write_roots_for_permissions(
|
||||||
policy,
|
&permissions,
|
||||||
policy_cwd,
|
|
||||||
current_dir,
|
current_dir,
|
||||||
env_map,
|
env_map,
|
||||||
codex_home,
|
codex_home,
|
||||||
@@ -341,7 +349,7 @@ pub(crate) fn apply_legacy_session_acl_rules(
|
|||||||
if let Some(readonly_sid) = acl_sids.readonly_sid {
|
if let Some(readonly_sid) = acl_sids.readonly_sid {
|
||||||
allow_null_device(readonly_sid.as_ptr());
|
allow_null_device(readonly_sid.as_ptr());
|
||||||
}
|
}
|
||||||
if matches!(policy, SandboxPolicy::WorkspaceWrite { .. })
|
if !acl_sids.write_root_sids.is_empty()
|
||||||
&& let Some(workspace_sid) =
|
&& let Some(workspace_sid) =
|
||||||
matching_root_capability(current_dir, acl_sids.write_root_sids)
|
matching_root_capability(current_dir, acl_sids.write_root_sids)
|
||||||
{
|
{
|
||||||
@@ -371,32 +379,30 @@ pub(crate) fn prepare_elevated_spawn_context(
|
|||||||
) -> Result<ElevatedSpawnContext> {
|
) -> Result<ElevatedSpawnContext> {
|
||||||
let common = prepare_spawn_context_common(
|
let common = prepare_spawn_context_common(
|
||||||
policy_json_or_preset,
|
policy_json_or_preset,
|
||||||
|
sandbox_policy_cwd,
|
||||||
codex_home,
|
codex_home,
|
||||||
cwd,
|
cwd,
|
||||||
env_map,
|
env_map,
|
||||||
command,
|
command,
|
||||||
/*inherit_path*/ true,
|
SpawnPrepOptions {
|
||||||
/*add_git_safe_directory*/ true,
|
inherit_path: true,
|
||||||
|
add_git_safe_directory: true,
|
||||||
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let AllowDenyPaths { allow, deny } = compute_allow_paths(
|
let AllowDenyPaths { allow, deny } =
|
||||||
&common.policy,
|
compute_allow_paths_for_permissions(&common.permissions, &common.current_dir, env_map);
|
||||||
sandbox_policy_cwd,
|
|
||||||
&common.current_dir,
|
|
||||||
env_map,
|
|
||||||
);
|
|
||||||
let write_roots: Vec<PathBuf> = allow.into_iter().collect();
|
let write_roots: Vec<PathBuf> = allow.into_iter().collect();
|
||||||
let deny_write_paths: Vec<PathBuf> = deny.into_iter().collect();
|
let deny_write_paths: Vec<PathBuf> = deny.into_iter().collect();
|
||||||
let computed_write_roots_override = if common.is_workspace_write {
|
let computed_write_roots_override = if common.uses_write_capabilities {
|
||||||
Some(write_roots.as_slice())
|
Some(write_roots.as_slice())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
let write_roots_for_setup = write_roots_override.or(computed_write_roots_override);
|
let write_roots_for_setup = write_roots_override.or(computed_write_roots_override);
|
||||||
let effective_write_roots = if common.is_workspace_write {
|
let effective_write_roots = if common.uses_write_capabilities {
|
||||||
effective_write_roots_for_setup(
|
effective_write_roots_for_permissions(
|
||||||
&common.policy,
|
&common.permissions,
|
||||||
sandbox_policy_cwd,
|
|
||||||
&common.current_dir,
|
&common.current_dir,
|
||||||
env_map,
|
env_map,
|
||||||
codex_home,
|
codex_home,
|
||||||
@@ -405,7 +411,7 @@ pub(crate) fn prepare_elevated_spawn_context(
|
|||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
let setup_write_roots_override = if common.is_workspace_write {
|
let setup_write_roots_override = if common.uses_write_capabilities {
|
||||||
Some(effective_write_roots.as_slice())
|
Some(effective_write_roots.as_slice())
|
||||||
} else {
|
} else {
|
||||||
write_roots_override
|
write_roots_override
|
||||||
@@ -428,24 +434,20 @@ pub(crate) fn prepare_elevated_spawn_context(
|
|||||||
/*proxy_enforced*/ false,
|
/*proxy_enforced*/ false,
|
||||||
)?;
|
)?;
|
||||||
let caps = load_or_create_cap_sids(codex_home)?;
|
let caps = load_or_create_cap_sids(codex_home)?;
|
||||||
let (psid_to_use, cap_sids) = match &common.policy {
|
let (psid_to_use, cap_sids) = if common.uses_write_capabilities {
|
||||||
SandboxPolicy::ReadOnly { .. } => (
|
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)?,
|
LocalSid::from_string(&caps.readonly)?,
|
||||||
vec![caps.readonly.clone()],
|
vec![caps.readonly.clone()],
|
||||||
),
|
)
|
||||||
SandboxPolicy::WorkspaceWrite { .. } => {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
|
||||||
unreachable!("dangerous policies rejected before elevated session prep")
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -462,19 +464,26 @@ pub(crate) fn prepare_elevated_spawn_context(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::SandboxPolicy;
|
use super::SandboxPolicy;
|
||||||
|
use super::SpawnPrepOptions;
|
||||||
use super::deny_root_capabilities_for_path;
|
use super::deny_root_capabilities_for_path;
|
||||||
use super::legacy_session_capability_roots;
|
use super::legacy_session_capability_roots;
|
||||||
use super::prepare_legacy_spawn_context;
|
use super::prepare_legacy_spawn_context;
|
||||||
use super::prepare_spawn_context_common;
|
use super::prepare_spawn_context_common;
|
||||||
use super::root_capability_sids;
|
use super::root_capability_sids;
|
||||||
use super::should_apply_network_block;
|
|
||||||
use crate::cap::load_or_create_cap_sids;
|
use crate::cap::load_or_create_cap_sids;
|
||||||
use crate::cap::workspace_write_cap_sid_for_root;
|
use crate::cap::workspace_write_cap_sid_for_root;
|
||||||
|
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
|
||||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::path::Path;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
fn should_apply_network_block(policy: &SandboxPolicy) -> bool {
|
||||||
|
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, Path::new("."))
|
||||||
|
.should_apply_network_block()
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn no_network_env_rewrite_applies_for_workspace_write() {
|
fn no_network_env_rewrite_applies_for_workspace_write() {
|
||||||
assert!(should_apply_network_block(
|
assert!(should_apply_network_block(
|
||||||
@@ -502,12 +511,15 @@ mod tests {
|
|||||||
|
|
||||||
let _context = prepare_legacy_spawn_context(
|
let _context = prepare_legacy_spawn_context(
|
||||||
"workspace-write",
|
"workspace-write",
|
||||||
|
cwd.path(),
|
||||||
codex_home.path(),
|
codex_home.path(),
|
||||||
cwd.path(),
|
cwd.path(),
|
||||||
&mut env_map,
|
&mut env_map,
|
||||||
&["cmd.exe".to_string()],
|
&["cmd.exe".to_string()],
|
||||||
/*inherit_path*/ true,
|
SpawnPrepOptions {
|
||||||
/*add_git_safe_directory*/ false,
|
inherit_path: true,
|
||||||
|
add_git_safe_directory: false,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.expect("legacy env prep");
|
.expect("legacy env prep");
|
||||||
|
|
||||||
@@ -529,12 +541,15 @@ mod tests {
|
|||||||
|
|
||||||
let context = prepare_spawn_context_common(
|
let context = prepare_spawn_context_common(
|
||||||
"workspace-write",
|
"workspace-write",
|
||||||
|
cwd.path(),
|
||||||
codex_home.path(),
|
codex_home.path(),
|
||||||
cwd.path(),
|
cwd.path(),
|
||||||
&mut env_map,
|
&mut env_map,
|
||||||
&["cmd.exe".to_string()],
|
&["cmd.exe".to_string()],
|
||||||
/*inherit_path*/ true,
|
SpawnPrepOptions {
|
||||||
/*add_git_safe_directory*/ true,
|
inherit_path: true,
|
||||||
|
add_git_safe_directory: true,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.expect("preserve existing env prep");
|
.expect("preserve existing env prep");
|
||||||
assert_eq!(context.policy, SandboxPolicy::new_workspace_write_policy());
|
assert_eq!(context.policy, SandboxPolicy::new_workspace_write_policy());
|
||||||
@@ -546,6 +561,36 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn legacy_session_capability_roots_use_policy_cwd_for_workspace_root() {
|
||||||
|
let tmp = TempDir::new().expect("tempdir");
|
||||||
|
let codex_home = tmp.path().join("codex-home");
|
||||||
|
let policy_cwd = tmp.path().join("workspace");
|
||||||
|
let command_cwd = policy_cwd.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 policy = SandboxPolicy::WorkspaceWrite {
|
||||||
|
writable_roots: Vec::new(),
|
||||||
|
network_access: false,
|
||||||
|
exclude_tmpdir_env_var: true,
|
||||||
|
exclude_slash_tmp: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let roots = legacy_session_capability_roots(
|
||||||
|
&policy,
|
||||||
|
&policy_cwd,
|
||||||
|
&command_cwd,
|
||||||
|
&HashMap::new(),
|
||||||
|
&codex_home,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
roots,
|
||||||
|
vec![dunce::canonicalize(&policy_cwd).expect("canonical policy cwd")]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn root_capability_sids_only_include_active_roots() {
|
fn root_capability_sids_only_include_active_roots() {
|
||||||
let temp = TempDir::new().expect("tempdir");
|
let temp = TempDir::new().expect("tempdir");
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use crate::process::StdinMode;
|
|||||||
use crate::process::read_handle_loop;
|
use crate::process::read_handle_loop;
|
||||||
use crate::process::spawn_process_with_pipes;
|
use crate::process::spawn_process_with_pipes;
|
||||||
use crate::spawn_prep::LegacyAclSids;
|
use crate::spawn_prep::LegacyAclSids;
|
||||||
|
use crate::spawn_prep::SpawnPrepOptions;
|
||||||
use crate::spawn_prep::allow_null_device_for_workspace_write;
|
use crate::spawn_prep::allow_null_device_for_workspace_write;
|
||||||
use crate::spawn_prep::apply_legacy_session_acl_rules;
|
use crate::spawn_prep::apply_legacy_session_acl_rules;
|
||||||
use crate::spawn_prep::legacy_session_capability_roots;
|
use crate::spawn_prep::legacy_session_capability_roots;
|
||||||
@@ -283,12 +284,15 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy(
|
|||||||
) -> Result<SpawnedProcess> {
|
) -> Result<SpawnedProcess> {
|
||||||
let common = prepare_legacy_spawn_context(
|
let common = prepare_legacy_spawn_context(
|
||||||
policy_json_or_preset,
|
policy_json_or_preset,
|
||||||
|
sandbox_policy_cwd,
|
||||||
codex_home,
|
codex_home,
|
||||||
cwd,
|
cwd,
|
||||||
&mut env_map,
|
&mut env_map,
|
||||||
&command,
|
&command,
|
||||||
/*inherit_path*/ false,
|
SpawnPrepOptions {
|
||||||
/*add_git_safe_directory*/ false,
|
inherit_path: false,
|
||||||
|
add_git_safe_directory: false,
|
||||||
|
},
|
||||||
)?;
|
)?;
|
||||||
if !common.policy.has_full_disk_read_access() {
|
if !common.policy.has_full_disk_read_access() {
|
||||||
anyhow::bail!("Restricted read-only access requires the elevated Windows sandbox backend");
|
anyhow::bail!("Restricted read-only access requires the elevated Windows sandbox backend");
|
||||||
@@ -311,7 +315,7 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy(
|
|||||||
);
|
);
|
||||||
let security =
|
let security =
|
||||||
prepare_legacy_session_security(&common.policy, codex_home, cwd, capability_roots)?;
|
prepare_legacy_session_security(&common.policy, codex_home, cwd, capability_roots)?;
|
||||||
allow_null_device_for_workspace_write(common.is_workspace_write);
|
allow_null_device_for_workspace_write(common.uses_write_capabilities);
|
||||||
|
|
||||||
apply_legacy_session_acl_rules(
|
apply_legacy_session_acl_rules(
|
||||||
&common.policy,
|
&common.policy,
|
||||||
|
|||||||
Reference in new issue
Block a user