mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
fix: support split carveouts in windows elevated sandbox (#14568)
## Summary - preserve legacy Windows elevated sandbox behavior for existing policies - add elevated-only support for split filesystem policies that can be represented as readable-root overrides, writable-root overrides, and extra deny-write carveouts - resolve those elevated filesystem overrides during sandbox transform and thread them through setup and policy refresh - keep failing closed for explicit unreadable (`none`) carveouts and reopened writable descendants under read-only carveouts - for explicit read-only-under-writable-root carveouts, materialize missing carveout directories during elevated setup before applying the deny-write ACL - document the elevated vs restricted-token support split in the core README ## Example Given a split filesystem policy like: ```toml ":root" = "read" ":cwd" = "write" "./docs" = "read" "C:/scratch" = "write" ``` the elevated backend now provisions the readable-root overrides, writable-root overrides, and extra deny-write carveouts during setup and refresh instead of collapsing back to the legacy workspace-only shape. If a read-only carveout under a writable root is missing at setup time, elevated setup creates that carveout as an empty directory before applying its deny-write ACE; otherwise the sandboxed command could create it later and bypass the carveout. This is only for explicit policy carveouts. Best-effort workspace protections like `.codex/` and `.agents/` still skip missing directories. A policy like: ```toml "/workspace" = "write" "/workspace/docs" = "read" "/workspace/docs/tmp" = "write" ``` still fails closed, because the elevated backend does not reopen writable descendants under read-only carveouts yet. --------- Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -278,7 +278,7 @@ async fn exec_full_buffer_capture_ignores_expiration() -> Result<()> {
|
||||
SandboxType::None,
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
&FileSystemSandboxPolicy::unrestricted(),
|
||||
/*windows_restricted_token_filesystem_overlay*/ None,
|
||||
/*windows_sandbox_filesystem_overrides*/ None,
|
||||
NetworkSandboxPolicy::Enabled,
|
||||
/*stdout_stream*/ None,
|
||||
/*after_spawn*/ None,
|
||||
@@ -318,7 +318,7 @@ async fn exec_full_buffer_capture_keeps_io_drain_timeout_when_descendant_holds_p
|
||||
SandboxType::None,
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
&FileSystemSandboxPolicy::unrestricted(),
|
||||
/*windows_restricted_token_filesystem_overlay*/ None,
|
||||
/*windows_sandbox_filesystem_overrides*/ None,
|
||||
NetworkSandboxPolicy::Enabled,
|
||||
/*stdout_stream*/ None,
|
||||
/*after_spawn*/ None,
|
||||
@@ -414,6 +414,22 @@ fn windows_restricted_token_runs_for_legacy_restricted_policies() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_proxy_enforcement_uses_elevated_backend() {
|
||||
assert!(!windows_sandbox_uses_elevated_backend(
|
||||
WindowsSandboxLevel::RestrictedToken,
|
||||
/*proxy_enforced*/ false,
|
||||
));
|
||||
assert!(windows_sandbox_uses_elevated_backend(
|
||||
WindowsSandboxLevel::RestrictedToken,
|
||||
/*proxy_enforced*/ true,
|
||||
));
|
||||
assert!(windows_sandbox_uses_elevated_backend(
|
||||
WindowsSandboxLevel::Elevated,
|
||||
/*proxy_enforced*/ false,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_restricted_token_rejects_network_only_restrictions() {
|
||||
let policy = SandboxPolicy::ExternalSandbox {
|
||||
@@ -481,6 +497,36 @@ fn windows_restricted_token_allows_legacy_workspace_write_policies() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_elevated_allows_legacy_restricted_read_policies() {
|
||||
let temp_dir = tempfile::TempDir::new().expect("tempdir");
|
||||
let docs = codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(
|
||||
temp_dir.path().join("docs"),
|
||||
)
|
||||
.expect("absolute docs");
|
||||
std::fs::create_dir_all(docs.as_path()).expect("create docs");
|
||||
let policy = SandboxPolicy::ReadOnly {
|
||||
access: codex_protocol::protocol::ReadOnlyAccess::Restricted {
|
||||
readable_roots: vec![docs],
|
||||
include_platform_defaults: false,
|
||||
},
|
||||
network_access: false,
|
||||
};
|
||||
let file_system_policy = FileSystemSandboxPolicy::from(&policy);
|
||||
|
||||
assert_eq!(
|
||||
unsupported_windows_restricted_token_sandbox_reason(
|
||||
SandboxType::WindowsRestrictedToken,
|
||||
&policy,
|
||||
&file_system_policy,
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
temp_dir.path(),
|
||||
WindowsSandboxLevel::Elevated,
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_restricted_token_rejects_split_only_filesystem_policies() {
|
||||
let temp_dir = tempfile::TempDir::new().expect("tempdir");
|
||||
@@ -618,7 +664,7 @@ fn windows_restricted_token_supports_full_read_split_write_read_carveouts() {
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
resolve_windows_restricted_token_filesystem_overlay(
|
||||
resolve_windows_restricted_token_filesystem_overrides(
|
||||
SandboxType::WindowsRestrictedToken,
|
||||
&policy,
|
||||
&file_system_policy,
|
||||
@@ -626,23 +672,63 @@ fn windows_restricted_token_supports_full_read_split_write_read_carveouts() {
|
||||
&cwd,
|
||||
WindowsSandboxLevel::RestrictedToken,
|
||||
),
|
||||
Ok(Some(WindowsRestrictedTokenFilesystemOverlay {
|
||||
Ok(Some(WindowsSandboxFilesystemOverrides {
|
||||
read_roots_override: None,
|
||||
write_roots_override: None,
|
||||
additional_deny_write_paths: expected_deny_write_paths,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_elevated_rejects_split_write_read_carveouts() {
|
||||
fn windows_elevated_supports_split_restricted_read_roots() {
|
||||
let temp_dir = tempfile::TempDir::new().expect("tempdir");
|
||||
let docs = temp_dir.path().join("docs");
|
||||
std::fs::create_dir_all(&docs).expect("create docs");
|
||||
let expected_docs = dunce::canonicalize(&docs).expect("canonical docs");
|
||||
let policy = SandboxPolicy::ReadOnly {
|
||||
access: codex_protocol::protocol::ReadOnlyAccess::FullAccess,
|
||||
network_access: false,
|
||||
};
|
||||
let file_system_policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
codex_protocol::permissions::FileSystemSandboxEntry {
|
||||
path: codex_protocol::permissions::FileSystemPath::Path {
|
||||
path: codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(&docs)
|
||||
.expect("absolute docs"),
|
||||
},
|
||||
access: codex_protocol::permissions::FileSystemAccessMode::Read,
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
resolve_windows_elevated_filesystem_overrides(
|
||||
SandboxType::WindowsRestrictedToken,
|
||||
&policy,
|
||||
&file_system_policy,
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
temp_dir.path(),
|
||||
/*use_windows_elevated_backend*/ true,
|
||||
),
|
||||
Ok(Some(WindowsSandboxFilesystemOverrides {
|
||||
read_roots_override: Some(vec![expected_docs]),
|
||||
write_roots_override: None,
|
||||
additional_deny_write_paths: vec![],
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_elevated_supports_split_write_read_carveouts() {
|
||||
let temp_dir = tempfile::TempDir::new().expect("tempdir");
|
||||
let docs = temp_dir.path().join("docs");
|
||||
std::fs::create_dir_all(&docs).expect("create docs");
|
||||
let expected_docs = dunce::canonicalize(&docs).expect("canonical docs");
|
||||
let policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![],
|
||||
read_only_access: codex_protocol::protocol::ReadOnlyAccess::FullAccess,
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: false,
|
||||
exclude_slash_tmp: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
};
|
||||
let file_system_policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
codex_protocol::permissions::FileSystemSandboxEntry {
|
||||
@@ -666,6 +752,60 @@ fn windows_elevated_rejects_split_write_read_carveouts() {
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
resolve_windows_elevated_filesystem_overrides(
|
||||
SandboxType::WindowsRestrictedToken,
|
||||
&policy,
|
||||
&file_system_policy,
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
temp_dir.path(),
|
||||
/*use_windows_elevated_backend*/ true,
|
||||
),
|
||||
Ok(Some(WindowsSandboxFilesystemOverrides {
|
||||
read_roots_override: None,
|
||||
write_roots_override: None,
|
||||
additional_deny_write_paths: vec![
|
||||
codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(expected_docs)
|
||||
.expect("absolute docs"),
|
||||
],
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_elevated_rejects_unreadable_split_carveouts() {
|
||||
let temp_dir = tempfile::TempDir::new().expect("tempdir");
|
||||
let blocked = temp_dir.path().join("blocked");
|
||||
std::fs::create_dir_all(&blocked).expect("create blocked");
|
||||
let policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![],
|
||||
read_only_access: codex_protocol::protocol::ReadOnlyAccess::FullAccess,
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
};
|
||||
let file_system_policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
codex_protocol::permissions::FileSystemSandboxEntry {
|
||||
path: codex_protocol::permissions::FileSystemPath::Special {
|
||||
value: codex_protocol::permissions::FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: codex_protocol::permissions::FileSystemAccessMode::Read,
|
||||
},
|
||||
codex_protocol::permissions::FileSystemSandboxEntry {
|
||||
path: codex_protocol::permissions::FileSystemPath::Special {
|
||||
value: codex_protocol::permissions::FileSystemSpecialPath::CurrentWorkingDirectory,
|
||||
},
|
||||
access: codex_protocol::permissions::FileSystemAccessMode::Write,
|
||||
},
|
||||
codex_protocol::permissions::FileSystemSandboxEntry {
|
||||
path: codex_protocol::permissions::FileSystemPath::Path {
|
||||
path: codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(&blocked)
|
||||
.expect("absolute blocked"),
|
||||
},
|
||||
access: codex_protocol::permissions::FileSystemAccessMode::None,
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
unsupported_windows_restricted_token_sandbox_reason(
|
||||
SandboxType::WindowsRestrictedToken,
|
||||
@@ -676,7 +816,65 @@ fn windows_elevated_rejects_split_write_read_carveouts() {
|
||||
WindowsSandboxLevel::Elevated,
|
||||
),
|
||||
Some(
|
||||
"windows elevated sandbox backend cannot enforce split filesystem permissions directly; refusing to run unsandboxed"
|
||||
"windows elevated sandbox cannot enforce unreadable split filesystem carveouts directly; refusing to run unsandboxed"
|
||||
.to_string()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_elevated_rejects_reopened_writable_descendants() {
|
||||
let temp_dir = tempfile::TempDir::new().expect("tempdir");
|
||||
let docs = temp_dir.path().join("docs");
|
||||
let nested = docs.join("nested");
|
||||
std::fs::create_dir_all(&nested).expect("create nested");
|
||||
let policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![],
|
||||
read_only_access: codex_protocol::protocol::ReadOnlyAccess::FullAccess,
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
};
|
||||
let file_system_policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
codex_protocol::permissions::FileSystemSandboxEntry {
|
||||
path: codex_protocol::permissions::FileSystemPath::Special {
|
||||
value: codex_protocol::permissions::FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: codex_protocol::permissions::FileSystemAccessMode::Read,
|
||||
},
|
||||
codex_protocol::permissions::FileSystemSandboxEntry {
|
||||
path: codex_protocol::permissions::FileSystemPath::Special {
|
||||
value: codex_protocol::permissions::FileSystemSpecialPath::CurrentWorkingDirectory,
|
||||
},
|
||||
access: codex_protocol::permissions::FileSystemAccessMode::Write,
|
||||
},
|
||||
codex_protocol::permissions::FileSystemSandboxEntry {
|
||||
path: codex_protocol::permissions::FileSystemPath::Path {
|
||||
path: codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(&docs)
|
||||
.expect("absolute docs"),
|
||||
},
|
||||
access: codex_protocol::permissions::FileSystemAccessMode::Read,
|
||||
},
|
||||
codex_protocol::permissions::FileSystemSandboxEntry {
|
||||
path: codex_protocol::permissions::FileSystemPath::Path {
|
||||
path: codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(&nested)
|
||||
.expect("absolute nested"),
|
||||
},
|
||||
access: codex_protocol::permissions::FileSystemAccessMode::Write,
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
unsupported_windows_restricted_token_sandbox_reason(
|
||||
SandboxType::WindowsRestrictedToken,
|
||||
&policy,
|
||||
&file_system_policy,
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
temp_dir.path(),
|
||||
WindowsSandboxLevel::Elevated,
|
||||
),
|
||||
Some(
|
||||
"windows elevated sandbox cannot reopen writable descendants under read-only carveouts directly; refusing to run unsandboxed"
|
||||
.to_string()
|
||||
)
|
||||
);
|
||||
@@ -744,7 +942,7 @@ async fn kill_child_process_group_kills_grandchildren_on_timeout() -> Result<()>
|
||||
SandboxType::None,
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
&FileSystemSandboxPolicy::from(&SandboxPolicy::new_read_only_policy()),
|
||||
None,
|
||||
/*windows_sandbox_filesystem_overrides*/ None,
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
/*stdout_stream*/ None,
|
||||
/*after_spawn*/ None,
|
||||
|
||||
Reference in New Issue
Block a user