diff --git a/codex-rs/protocol/src/permissions.rs b/codex-rs/protocol/src/permissions.rs index 977fb80f40..ce48b08f15 100644 --- a/codex-rs/protocol/src/permissions.rs +++ b/codex-rs/protocol/src/permissions.rs @@ -462,10 +462,15 @@ impl FileSystemSandboxPolicy { pub fn has_denied_read_restrictions(&self) -> bool { matches!(self.kind, FileSystemSandboxKind::Restricted) - && self - .entries - .iter() - .any(|entry| entry.access == FileSystemAccessMode::Deny) + && self.entries.iter().any(|entry| { + entry.access == FileSystemAccessMode::Deny + && !matches!( + &entry.path, + FileSystemPath::Special { + value: FileSystemSpecialPath::SlashTmp, + } if !cfg!(unix) + ) + }) } pub fn from_legacy_sandbox_policy_preserving_deny_entries( @@ -546,6 +551,7 @@ impl FileSystemSandboxPolicy { FileSystemPath::GlobPattern { .. } => true, FileSystemPath::Special { value } => match value { FileSystemSpecialPath::Root => entry.access == FileSystemAccessMode::Deny, + FileSystemSpecialPath::SlashTmp if !cfg!(unix) => false, FileSystemSpecialPath::Minimal | FileSystemSpecialPath::Unknown { .. } => { false } @@ -1249,7 +1255,7 @@ impl FileSystemSandboxPolicy { } else if unbridgeable_root_write || !writable_roots.is_empty() || tmpdir_writable - || slash_tmp_writable + || (cfg!(unix) && slash_tmp_writable) { return Err(io::Error::new( io::ErrorKind::InvalidInput, @@ -1523,6 +1529,9 @@ fn resolve_file_system_special_path( } } FileSystemSpecialPath::SlashTmp => { + if !cfg!(unix) { + return None; + } #[allow(clippy::expect_used)] let slash_tmp = AbsolutePathBuf::from_absolute_path("/tmp").expect("/tmp is absolute"); if !slash_tmp.as_path().is_dir() { @@ -1959,6 +1968,86 @@ mod tests { Ok(()) } + #[cfg(windows)] + #[test] + fn workspace_write_ignores_slash_tmp_special_path_but_preserves_literal_path() { + let cwd = TempDir::new().expect("tempdir"); + let slash_tmp = AbsolutePathBuf::from_absolute_path("/tmp").expect("absolute tmp path"); + let slash_tmp_only_policy = FileSystemSandboxPolicy::restricted(vec![ + FileSystemSandboxEntry::new( + FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + FileSystemAccessMode::Read, + ), + FileSystemSandboxEntry::new( + FileSystemPath::Special { + value: FileSystemSpecialPath::SlashTmp, + }, + FileSystemAccessMode::Write, + ), + ]); + assert_eq!( + slash_tmp_only_policy + .to_legacy_sandbox_policy(NetworkSandboxPolicy::Restricted, cwd.path()) + .expect("legacy sandbox policy"), + SandboxPolicy::ReadOnly { + network_access: false, + } + ); + assert!( + !slash_tmp_only_policy + .needs_direct_runtime_enforcement(NetworkSandboxPolicy::Restricted, cwd.path()) + ); + + for access in [FileSystemAccessMode::Read, FileSystemAccessMode::Deny] { + let policy = FileSystemSandboxPolicy::restricted(vec![ + FileSystemSandboxEntry::new( + FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + FileSystemAccessMode::Write, + ), + FileSystemSandboxEntry::new( + FileSystemPath::Special { + value: FileSystemSpecialPath::SlashTmp, + }, + access, + ), + ]); + assert!(policy.has_full_disk_write_access()); + assert!(policy.has_full_disk_read_access()); + } + + let legacy_policy = SandboxPolicy::new_workspace_write_policy(); + assert_eq!( + FileSystemSandboxPolicy::from(&legacy_policy) + .to_legacy_sandbox_policy(NetworkSandboxPolicy::Restricted, cwd.path()) + .expect("legacy workspace-write policy"), + legacy_policy + ); + assert_eq!( + FileSystemSandboxPolicy::from(&legacy_policy) + .get_writable_roots_with_cwd(cwd.path()) + .into_iter() + .map(|root| root.root) + .collect::>(), + legacy_policy + .get_writable_roots_with_cwd(cwd.path()) + .into_iter() + .map(|root| normalize_effective_absolute_path(root.root)) + .collect::>() + ); + assert!( + FileSystemSandboxPolicy::workspace_write( + std::slice::from_ref(&slash_tmp), + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ false, + ) + .can_write_path_with_cwd(slash_tmp.as_path(), cwd.path()) + ); + } + #[cfg(unix)] #[test] fn writable_roots_proactively_protect_missing_dot_codex() { diff --git a/codex-rs/sandboxing/src/policy_transforms.rs b/codex-rs/sandboxing/src/policy_transforms.rs index 8f5648cd4e..59a89349c7 100644 --- a/codex-rs/sandboxing/src/policy_transforms.rs +++ b/codex-rs/sandboxing/src/policy_transforms.rs @@ -244,7 +244,14 @@ fn granted_file_system_entry_within_request( granted_entry: &FileSystemSandboxEntry, cwd: &Path, ) -> bool { - if !granted_entry.access.can_read() { + if !granted_entry.access.can_read() + || matches!( + &granted_entry.path, + FileSystemPath::Special { + value: FileSystemSpecialPath::SlashTmp, + } if !cfg!(unix) + ) + { return false; } @@ -398,10 +405,14 @@ fn resolve_permission_path(path: &FileSystemPath, cwd: &Path) -> Option AbsolutePathBuf::from_absolute_path("/tmp") - .ok() - .filter(|path| path.as_path().is_dir()), - FileSystemSpecialPath::Minimal | FileSystemSpecialPath::Unknown { .. } => None, + FileSystemSpecialPath::SlashTmp if cfg!(unix) => { + AbsolutePathBuf::from_absolute_path("/tmp") + .ok() + .filter(|path| path.as_path().is_dir()) + } + FileSystemSpecialPath::SlashTmp + | FileSystemSpecialPath::Minimal + | FileSystemSpecialPath::Unknown { .. } => None, }, } } diff --git a/codex-rs/sandboxing/src/policy_transforms_tests.rs b/codex-rs/sandboxing/src/policy_transforms_tests.rs index 2689c41961..5973f9c756 100644 --- a/codex-rs/sandboxing/src/policy_transforms_tests.rs +++ b/codex-rs/sandboxing/src/policy_transforms_tests.rs @@ -294,6 +294,41 @@ fn intersect_permission_profiles_drops_explicit_empty_reads_without_grant() { ); } +#[cfg(windows)] +#[test] +fn intersect_permission_profiles_rejects_symbolic_slash_tmp_grants() { + let cwd = TempDir::new().expect("tempdir"); + let slash_tmp = AbsolutePathBuf::from_absolute_path("/tmp").expect("absolute tmp path"); + let requested = PermissionProfile { + file_system: Some(FileSystemPermissions::from_read_write_roots( + /*read*/ None, + Some(vec![slash_tmp]), + )), + ..Default::default() + }; + let granted = PermissionProfile { + file_system: Some(FileSystemPermissions { + entries: vec![FileSystemSandboxEntry::new( + FileSystemPath::Special { + value: FileSystemSpecialPath::SlashTmp, + }, + FileSystemAccessMode::Write, + )], + glob_scan_max_depth: None, + }), + ..Default::default() + }; + + assert_eq!( + intersect_permission_profiles(requested, granted.clone(), cwd.path()), + PermissionProfile::default() + ); + assert_eq!( + intersect_permission_profiles(granted.clone(), granted, cwd.path()), + PermissionProfile::default() + ); +} + #[test] fn intersect_permission_profiles_accepts_child_path_granted_for_requested_cwd() { let temp_dir = TempDir::new().expect("create temp dir");