mirror of
https://github.com/openai/codex.git
synced 2026-03-27 18:46:34 +03:00
Compare commits
1 Commits
pr15976
...
dev/cc/rea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ea5174996 |
@@ -571,13 +571,14 @@ fn permissions_profiles_require_default_permissions() -> std::io::Result<()> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permissions_profiles_reject_writes_outside_workspace_root() -> std::io::Result<()> {
|
||||
fn permissions_profiles_allow_writes_outside_workspace_root_with_read_only_legacy_fallback()
|
||||
-> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?;
|
||||
let external_write_path = if cfg!(windows) { r"C:\temp" } else { "/tmp" };
|
||||
|
||||
let err = Config::load_from_base_config_with_overrides(
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml {
|
||||
default_permissions: Some("workspace".to_string()),
|
||||
permissions: Some(PermissionsToml {
|
||||
@@ -601,14 +602,45 @@ fn permissions_profiles_reject_writes_outside_workspace_root() -> std::io::Resul
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.path().to_path_buf(),
|
||||
)
|
||||
.expect_err("writes outside the workspace root should be rejected");
|
||||
)?;
|
||||
|
||||
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
|
||||
assert_eq!(
|
||||
config.permissions.sandbox_policy.get(),
|
||||
&SandboxPolicy::ReadOnly {
|
||||
access: ReadOnlyAccess::Restricted {
|
||||
include_platform_defaults: false,
|
||||
readable_roots: Vec::new(),
|
||||
},
|
||||
network_access: false,
|
||||
}
|
||||
);
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("filesystem writes outside the workspace root"),
|
||||
"{err}"
|
||||
config
|
||||
.permissions
|
||||
.file_system_sandbox_policy
|
||||
.can_write_path_with_cwd(Path::new(external_write_path), cwd.path())
|
||||
);
|
||||
assert!(
|
||||
config
|
||||
.permissions
|
||||
.file_system_sandbox_policy
|
||||
.needs_direct_runtime_enforcement(
|
||||
config.permissions.network_sandbox_policy,
|
||||
cwd.path(),
|
||||
)
|
||||
);
|
||||
assert!(
|
||||
config
|
||||
.permissions
|
||||
.file_system_sandbox_policy
|
||||
.can_write_path_with_cwd(codex_home.path().join("memories").as_path(), cwd.path())
|
||||
);
|
||||
assert_eq!(config.approvals_reviewer, ApprovalsReviewer::User);
|
||||
|
||||
let warnings = &config.startup_warnings;
|
||||
assert!(
|
||||
warnings.is_empty(),
|
||||
"external writes should no longer fail config load: {warnings:?}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -719,6 +751,132 @@ fn permissions_profiles_allow_unknown_special_paths() -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permissions_profiles_allow_tmpdir_write_with_read_only_legacy_fallback() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?;
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml {
|
||||
default_permissions: Some("workspace".to_string()),
|
||||
permissions: Some(PermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
":tmpdir".to_string(),
|
||||
FilesystemPermissionToml::Access(FileSystemAccessMode::Write),
|
||||
)]),
|
||||
}),
|
||||
network: None,
|
||||
},
|
||||
)]),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
ConfigOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.path().to_path_buf(),
|
||||
)?;
|
||||
|
||||
let memories_root = codex_home.path().join("memories").abs();
|
||||
assert!(
|
||||
config
|
||||
.permissions
|
||||
.file_system_sandbox_policy
|
||||
.entries
|
||||
.iter()
|
||||
.any(|entry| {
|
||||
entry.access == FileSystemAccessMode::Write
|
||||
&& entry.path
|
||||
== FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Tmpdir,
|
||||
}
|
||||
})
|
||||
);
|
||||
assert!(
|
||||
config
|
||||
.permissions
|
||||
.file_system_sandbox_policy
|
||||
.can_write_path_with_cwd(memories_root.as_path(), cwd.path())
|
||||
);
|
||||
assert_eq!(
|
||||
config.permissions.sandbox_policy.get(),
|
||||
&SandboxPolicy::ReadOnly {
|
||||
access: ReadOnlyAccess::Restricted {
|
||||
include_platform_defaults: false,
|
||||
readable_roots: Vec::new(),
|
||||
},
|
||||
network_access: false,
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permissions_profiles_allow_slash_tmp_write_with_read_only_legacy_fallback() -> std::io::Result<()>
|
||||
{
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?;
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml {
|
||||
default_permissions: Some("workspace".to_string()),
|
||||
permissions: Some(PermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
"/tmp".to_string(),
|
||||
FilesystemPermissionToml::Access(FileSystemAccessMode::Write),
|
||||
)]),
|
||||
}),
|
||||
network: None,
|
||||
},
|
||||
)]),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
ConfigOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.path().to_path_buf(),
|
||||
)?;
|
||||
|
||||
assert!(
|
||||
config
|
||||
.permissions
|
||||
.file_system_sandbox_policy
|
||||
.entries
|
||||
.iter()
|
||||
.any(|entry| {
|
||||
entry.access == FileSystemAccessMode::Write
|
||||
&& entry.path
|
||||
== FileSystemPath::Path {
|
||||
path: test_absolute_path("/tmp"),
|
||||
}
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
config.permissions.sandbox_policy.get(),
|
||||
&SandboxPolicy::ReadOnly {
|
||||
access: ReadOnlyAccess::Restricted {
|
||||
include_platform_defaults: false,
|
||||
readable_roots: Vec::new(),
|
||||
},
|
||||
network_access: false,
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permissions_profiles_allow_unknown_special_paths_with_nested_entries() -> std::io::Result<()> {
|
||||
let config = load_workspace_permission_profile(PermissionProfileToml {
|
||||
|
||||
@@ -2191,17 +2191,18 @@ impl Config {
|
||||
default_permissions,
|
||||
&mut startup_warnings,
|
||||
)?;
|
||||
let mut sandbox_policy = file_system_sandbox_policy
|
||||
.to_legacy_sandbox_policy(network_sandbox_policy, resolved_cwd.as_path())?;
|
||||
if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) {
|
||||
if !file_system_sandbox_policy
|
||||
.get_writable_roots_with_cwd(resolved_cwd.as_path())
|
||||
.is_empty()
|
||||
{
|
||||
file_system_sandbox_policy = file_system_sandbox_policy
|
||||
.with_additional_writable_roots(
|
||||
resolved_cwd.as_path(),
|
||||
&additional_writable_roots,
|
||||
);
|
||||
sandbox_policy = file_system_sandbox_policy
|
||||
.to_legacy_sandbox_policy(network_sandbox_policy, resolved_cwd.as_path())?;
|
||||
}
|
||||
let sandbox_policy = file_system_sandbox_policy
|
||||
.to_legacy_sandbox_policy(network_sandbox_policy, resolved_cwd.as_path())?;
|
||||
(
|
||||
configured_network_proxy_config,
|
||||
sandbox_policy,
|
||||
|
||||
@@ -83,7 +83,20 @@ pub enum FileSystemSpecialPath {
|
||||
#[ts(optional)]
|
||||
subpath: Option<PathBuf>,
|
||||
},
|
||||
/// Config-facing `:tmpdir` special path.
|
||||
///
|
||||
/// This is the broader restricted-policy temp bundle used by filesystem
|
||||
/// permission profiles. On macOS it expands to `$TMPDIR`, `/tmp`, and
|
||||
/// `/tmp`'s canonical target so callers can grant write access to the tmp
|
||||
/// namespace without depending on a single alias.
|
||||
Tmpdir,
|
||||
/// Legacy `$TMPDIR`-only special path used when bridging old
|
||||
/// `SandboxPolicy::WorkspaceWrite` semantics into split filesystem policy.
|
||||
///
|
||||
/// Keep this narrower than [`FileSystemSpecialPath::Tmpdir`] so existing
|
||||
/// `exclude_tmpdir_env_var = false` behavior does not silently widen to the
|
||||
/// full temp bundle.
|
||||
TmpdirEnvVar,
|
||||
SlashTmp,
|
||||
/// WARNING: `:special_path` tokens are part of config compatibility.
|
||||
/// Do not make older runtimes reject newly introduced tokens.
|
||||
@@ -615,50 +628,57 @@ impl FileSystemSandboxPolicy {
|
||||
FileSystemSpecialPath::CurrentWorkingDirectory => {
|
||||
if entry.access.can_write() {
|
||||
workspace_root_writable = true;
|
||||
} else if entry.access.can_read()
|
||||
&& let Some(path) = resolve_file_system_special_path(
|
||||
} else if entry.access.can_read() {
|
||||
readable_roots.extend(resolve_file_system_special_paths(
|
||||
value,
|
||||
cwd_absolute.as_ref(),
|
||||
)
|
||||
{
|
||||
readable_roots.push(path);
|
||||
));
|
||||
}
|
||||
}
|
||||
FileSystemSpecialPath::ProjectRoots { subpath } => {
|
||||
if subpath.is_none() && entry.access.can_write() {
|
||||
workspace_root_writable = true;
|
||||
} else if let Some(path) =
|
||||
resolve_file_system_special_path(value, cwd_absolute.as_ref())
|
||||
{
|
||||
} else {
|
||||
let resolved_paths = resolve_file_system_special_paths(
|
||||
value,
|
||||
cwd_absolute.as_ref(),
|
||||
);
|
||||
if entry.access.can_write() {
|
||||
writable_roots.push(path);
|
||||
writable_roots.extend(resolved_paths);
|
||||
} else if entry.access.can_read() {
|
||||
readable_roots.push(path);
|
||||
readable_roots.extend(resolved_paths);
|
||||
}
|
||||
}
|
||||
}
|
||||
FileSystemSpecialPath::Tmpdir => {
|
||||
if entry.access.can_write() {
|
||||
tmpdir_writable = true;
|
||||
} else if entry.access.can_read()
|
||||
&& let Some(path) = resolve_file_system_special_path(
|
||||
slash_tmp_writable = true;
|
||||
} else if entry.access.can_read() {
|
||||
readable_roots.extend(resolve_file_system_special_paths(
|
||||
value,
|
||||
cwd_absolute.as_ref(),
|
||||
)
|
||||
{
|
||||
readable_roots.push(path);
|
||||
));
|
||||
}
|
||||
}
|
||||
FileSystemSpecialPath::TmpdirEnvVar => {
|
||||
if entry.access.can_write() {
|
||||
tmpdir_writable = true;
|
||||
} else if entry.access.can_read() {
|
||||
readable_roots.extend(resolve_file_system_special_paths(
|
||||
value,
|
||||
cwd_absolute.as_ref(),
|
||||
));
|
||||
}
|
||||
}
|
||||
FileSystemSpecialPath::SlashTmp => {
|
||||
if entry.access.can_write() {
|
||||
slash_tmp_writable = true;
|
||||
} else if entry.access.can_read()
|
||||
&& let Some(path) = resolve_file_system_special_path(
|
||||
} else if entry.access.can_read() {
|
||||
readable_roots.extend(resolve_file_system_special_paths(
|
||||
value,
|
||||
cwd_absolute.as_ref(),
|
||||
)
|
||||
{
|
||||
readable_roots.push(path);
|
||||
));
|
||||
}
|
||||
}
|
||||
FileSystemSpecialPath::Unknown { .. } => {}
|
||||
@@ -699,11 +719,6 @@ impl FileSystemSandboxPolicy {
|
||||
exclude_tmpdir_env_var: !tmpdir_writable,
|
||||
exclude_slash_tmp: !slash_tmp_writable,
|
||||
}
|
||||
} else if !writable_roots.is_empty() || tmpdir_writable || slash_tmp_writable {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"permissions profile requests filesystem writes outside the workspace root, which is not supported until the runtime enforces FileSystemSandboxPolicy directly",
|
||||
));
|
||||
} else {
|
||||
SandboxPolicy::ReadOnly {
|
||||
access: read_only_access,
|
||||
@@ -718,13 +733,13 @@ impl FileSystemSandboxPolicy {
|
||||
let cwd_absolute = AbsolutePathBuf::from_absolute_path(cwd).ok();
|
||||
self.entries
|
||||
.iter()
|
||||
.filter_map(|entry| {
|
||||
resolve_entry_path(&entry.path, cwd_absolute.as_ref()).map(|path| {
|
||||
ResolvedFileSystemEntry {
|
||||
.flat_map(|entry| {
|
||||
resolve_entry_paths(&entry.path, cwd_absolute.as_ref())
|
||||
.into_iter()
|
||||
.map(|path| ResolvedFileSystemEntry {
|
||||
path,
|
||||
access: entry.access,
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -846,7 +861,7 @@ impl From<&SandboxPolicy> for FileSystemSandboxPolicy {
|
||||
if !exclude_tmpdir_env_var {
|
||||
entries.push(FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Tmpdir,
|
||||
value: FileSystemSpecialPath::TmpdirEnvVar,
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
});
|
||||
@@ -869,21 +884,21 @@ impl From<&SandboxPolicy> for FileSystemSandboxPolicy {
|
||||
fn resolve_file_system_path(
|
||||
path: &FileSystemPath,
|
||||
cwd: Option<&AbsolutePathBuf>,
|
||||
) -> Option<AbsolutePathBuf> {
|
||||
) -> Vec<AbsolutePathBuf> {
|
||||
match path {
|
||||
FileSystemPath::Path { path } => Some(path.clone()),
|
||||
FileSystemPath::Special { value } => resolve_file_system_special_path(value, cwd),
|
||||
FileSystemPath::Path { path } => vec![path.clone()],
|
||||
FileSystemPath::Special { value } => resolve_file_system_special_paths(value, cwd),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_entry_path(
|
||||
fn resolve_entry_paths(
|
||||
path: &FileSystemPath,
|
||||
cwd: Option<&AbsolutePathBuf>,
|
||||
) -> Option<AbsolutePathBuf> {
|
||||
) -> Vec<AbsolutePathBuf> {
|
||||
match path {
|
||||
FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
} => cwd.map(absolute_root_path_for_cwd),
|
||||
} => cwd.map(absolute_root_path_for_cwd).into_iter().collect(),
|
||||
_ => resolve_file_system_path(path, cwd),
|
||||
}
|
||||
}
|
||||
@@ -928,6 +943,7 @@ fn special_paths_share_target(left: &FileSystemSpecialPath, right: &FileSystemSp
|
||||
FileSystemSpecialPath::CurrentWorkingDirectory,
|
||||
)
|
||||
| (FileSystemSpecialPath::Tmpdir, FileSystemSpecialPath::Tmpdir)
|
||||
| (FileSystemSpecialPath::TmpdirEnvVar, FileSystemSpecialPath::TmpdirEnvVar)
|
||||
| (FileSystemSpecialPath::SlashTmp, FileSystemSpecialPath::SlashTmp) => true,
|
||||
(
|
||||
FileSystemSpecialPath::CurrentWorkingDirectory,
|
||||
@@ -964,11 +980,13 @@ fn special_path_matches_absolute_path(
|
||||
value: &FileSystemSpecialPath,
|
||||
path: &AbsolutePathBuf,
|
||||
) -> bool {
|
||||
match value {
|
||||
FileSystemSpecialPath::Root => path.as_path().parent().is_none(),
|
||||
FileSystemSpecialPath::SlashTmp => path.as_path() == Path::new("/tmp"),
|
||||
_ => false,
|
||||
if matches!(value, FileSystemSpecialPath::Root) {
|
||||
return path.as_path().parent().is_none();
|
||||
}
|
||||
|
||||
resolve_cwd_independent_special_paths(value)
|
||||
.into_iter()
|
||||
.any(|candidate| candidate == *path)
|
||||
}
|
||||
|
||||
/// Orders resolved entries so the most specific path wins first, then applies
|
||||
@@ -988,45 +1006,82 @@ fn absolute_root_path_for_cwd(cwd: &AbsolutePathBuf) -> AbsolutePathBuf {
|
||||
.unwrap_or_else(|err| panic!("cwd root must be an absolute path: {err}"))
|
||||
}
|
||||
|
||||
fn resolve_file_system_special_path(
|
||||
fn resolve_file_system_special_paths(
|
||||
value: &FileSystemSpecialPath,
|
||||
cwd: Option<&AbsolutePathBuf>,
|
||||
) -> Option<AbsolutePathBuf> {
|
||||
) -> Vec<AbsolutePathBuf> {
|
||||
match value {
|
||||
FileSystemSpecialPath::Root
|
||||
| FileSystemSpecialPath::Minimal
|
||||
| FileSystemSpecialPath::Unknown { .. } => None,
|
||||
| FileSystemSpecialPath::Unknown { .. } => Vec::new(),
|
||||
FileSystemSpecialPath::CurrentWorkingDirectory => {
|
||||
let cwd = cwd?;
|
||||
Some(cwd.clone())
|
||||
let Some(cwd) = cwd else {
|
||||
return Vec::new();
|
||||
};
|
||||
vec![cwd.clone()]
|
||||
}
|
||||
FileSystemSpecialPath::ProjectRoots { subpath } => {
|
||||
let cwd = cwd?;
|
||||
let Some(cwd) = cwd else {
|
||||
return Vec::new();
|
||||
};
|
||||
match subpath.as_ref() {
|
||||
Some(subpath) => {
|
||||
AbsolutePathBuf::resolve_path_against_base(subpath, cwd.as_path()).ok()
|
||||
}
|
||||
None => Some(cwd.clone()),
|
||||
Some(subpath) => AbsolutePathBuf::resolve_path_against_base(subpath, cwd.as_path())
|
||||
.into_iter()
|
||||
.collect(),
|
||||
None => vec![cwd.clone()],
|
||||
}
|
||||
}
|
||||
FileSystemSpecialPath::Tmpdir => {
|
||||
let tmpdir = std::env::var_os("TMPDIR")?;
|
||||
if tmpdir.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let tmpdir = AbsolutePathBuf::from_absolute_path(PathBuf::from(tmpdir)).ok()?;
|
||||
Some(tmpdir)
|
||||
}
|
||||
}
|
||||
FileSystemSpecialPath::SlashTmp => {
|
||||
#[allow(clippy::expect_used)]
|
||||
let slash_tmp = AbsolutePathBuf::from_absolute_path("/tmp").expect("/tmp is absolute");
|
||||
if !slash_tmp.as_path().is_dir() {
|
||||
return None;
|
||||
}
|
||||
Some(slash_tmp)
|
||||
FileSystemSpecialPath::Tmpdir => resolve_tmpdir_paths(),
|
||||
FileSystemSpecialPath::TmpdirEnvVar => resolve_tmpdir_env_var_path().into_iter().collect(),
|
||||
FileSystemSpecialPath::SlashTmp => resolve_slash_tmp_path().into_iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_cwd_independent_special_paths(value: &FileSystemSpecialPath) -> Vec<AbsolutePathBuf> {
|
||||
match value {
|
||||
FileSystemSpecialPath::Root => Vec::new(),
|
||||
FileSystemSpecialPath::Minimal
|
||||
| FileSystemSpecialPath::CurrentWorkingDirectory
|
||||
| FileSystemSpecialPath::ProjectRoots { .. }
|
||||
| FileSystemSpecialPath::Unknown { .. } => Vec::new(),
|
||||
FileSystemSpecialPath::Tmpdir => resolve_tmpdir_paths(),
|
||||
FileSystemSpecialPath::TmpdirEnvVar => resolve_tmpdir_env_var_path().into_iter().collect(),
|
||||
FileSystemSpecialPath::SlashTmp => resolve_slash_tmp_path().into_iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_tmpdir_paths() -> Vec<AbsolutePathBuf> {
|
||||
let mut paths = Vec::new();
|
||||
if let Some(tmpdir) = resolve_tmpdir_env_var_path() {
|
||||
paths.push(tmpdir);
|
||||
}
|
||||
if let Some(slash_tmp) = resolve_slash_tmp_path() {
|
||||
paths.push(slash_tmp.clone());
|
||||
if let Ok(realpath) = slash_tmp.as_path().canonicalize()
|
||||
&& let Ok(realpath) = AbsolutePathBuf::from_absolute_path(realpath)
|
||||
{
|
||||
paths.push(realpath);
|
||||
}
|
||||
}
|
||||
dedup_absolute_paths(paths, /*normalize_effective_paths*/ false)
|
||||
}
|
||||
|
||||
fn resolve_tmpdir_env_var_path() -> Option<AbsolutePathBuf> {
|
||||
let tmpdir = std::env::var_os("TMPDIR")?;
|
||||
if tmpdir.is_empty() {
|
||||
None
|
||||
} else {
|
||||
AbsolutePathBuf::from_absolute_path(PathBuf::from(tmpdir)).ok()
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_slash_tmp_path() -> Option<AbsolutePathBuf> {
|
||||
#[allow(clippy::expect_used)]
|
||||
let slash_tmp = AbsolutePathBuf::from_absolute_path("/tmp").expect("/tmp is absolute");
|
||||
if !slash_tmp.as_path().is_dir() {
|
||||
return None;
|
||||
}
|
||||
Some(slash_tmp)
|
||||
}
|
||||
|
||||
fn dedup_absolute_paths(
|
||||
@@ -1538,12 +1593,14 @@ mod tests {
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn tmpdir_special_path_canonicalizes_symlinked_tmpdir() {
|
||||
fn tmpdir_env_var_special_path_canonicalizes_symlinked_tmpdir() {
|
||||
if std::env::var_os(SYMLINKED_TMPDIR_TEST_ENV).is_none() {
|
||||
let output = std::process::Command::new(std::env::current_exe().expect("test binary"))
|
||||
.env(SYMLINKED_TMPDIR_TEST_ENV, "1")
|
||||
.arg("--exact")
|
||||
.arg("permissions::tests::tmpdir_special_path_canonicalizes_symlinked_tmpdir")
|
||||
.arg(
|
||||
"permissions::tests::tmpdir_env_var_special_path_canonicalizes_symlinked_tmpdir",
|
||||
)
|
||||
.output()
|
||||
.expect("run tmpdir subprocess test");
|
||||
|
||||
@@ -1590,7 +1647,7 @@ mod tests {
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Tmpdir,
|
||||
value: FileSystemSpecialPath::TmpdirEnvVar,
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
@@ -1620,6 +1677,143 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn tmpdir_special_path_includes_tmpdir_env_and_tmp_namespace() {
|
||||
if std::env::var_os(SYMLINKED_TMPDIR_TEST_ENV).is_none() {
|
||||
let output = std::process::Command::new(std::env::current_exe().expect("test binary"))
|
||||
.env(SYMLINKED_TMPDIR_TEST_ENV, "1")
|
||||
.arg("--exact")
|
||||
.arg(
|
||||
"permissions::tests::tmpdir_special_path_includes_tmpdir_env_and_tmp_namespace",
|
||||
)
|
||||
.output()
|
||||
.expect("run tmpdir subprocess test");
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"tmpdir subprocess test failed\nstdout:\n{}\nstderr:\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let cwd = TempDir::new().expect("tempdir");
|
||||
let real_tmpdir = cwd.path().join("real-tmpdir");
|
||||
let link_tmpdir = cwd.path().join("link-tmpdir");
|
||||
fs::create_dir_all(&real_tmpdir).expect("create real tmpdir");
|
||||
symlink_dir(&real_tmpdir, &link_tmpdir).expect("create symlinked tmpdir");
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("TMPDIR", &link_tmpdir);
|
||||
}
|
||||
|
||||
let slash_tmp =
|
||||
AbsolutePathBuf::from_absolute_path("/tmp").expect("/tmp should be absolute");
|
||||
let slash_tmp_realpath = AbsolutePathBuf::from_absolute_path(
|
||||
slash_tmp
|
||||
.as_path()
|
||||
.canonicalize()
|
||||
.expect("canonicalize /tmp"),
|
||||
)
|
||||
.expect("absolute canonical /tmp");
|
||||
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Tmpdir,
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
}]);
|
||||
|
||||
assert!(policy.can_write_path_with_cwd(link_tmpdir.as_path(), cwd.path()));
|
||||
assert!(policy.can_write_path_with_cwd(Path::new("/tmp/codex-protocol"), cwd.path()));
|
||||
assert!(
|
||||
policy.can_write_path_with_cwd(
|
||||
slash_tmp_realpath
|
||||
.join("codex-protocol")
|
||||
.expect("valid tmp child")
|
||||
.as_path(),
|
||||
cwd.path(),
|
||||
)
|
||||
);
|
||||
|
||||
let writable_roots = policy.get_writable_roots_with_cwd(cwd.path());
|
||||
assert!(writable_roots.iter().any(|root| {
|
||||
root.root
|
||||
== AbsolutePathBuf::from_absolute_path(
|
||||
real_tmpdir.canonicalize().expect("canonicalize tmpdir"),
|
||||
)
|
||||
.expect("absolute canonical tmpdir")
|
||||
}));
|
||||
assert!(
|
||||
writable_roots
|
||||
.iter()
|
||||
.any(|root| root.root == slash_tmp_realpath)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tmpdir_write_without_workspace_root_falls_back_to_read_only_legacy_policy()
|
||||
-> std::io::Result<()> {
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Tmpdir,
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
}]);
|
||||
|
||||
let sandbox_policy = policy.to_legacy_sandbox_policy(
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
Path::new("/tmp/workspace"),
|
||||
)?;
|
||||
|
||||
assert_eq!(
|
||||
sandbox_policy,
|
||||
SandboxPolicy::ReadOnly {
|
||||
access: ReadOnlyAccess::Restricted {
|
||||
include_platform_defaults: false,
|
||||
readable_roots: Vec::new(),
|
||||
},
|
||||
network_access: false,
|
||||
}
|
||||
);
|
||||
assert!(policy.needs_direct_runtime_enforcement(
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
Path::new("/tmp/workspace"),
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_workspace_write_uses_tmpdir_env_var_special_path() {
|
||||
let file_system_policy = FileSystemSandboxPolicy::from(&SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: Vec::new(),
|
||||
read_only_access: ReadOnlyAccess::Restricted {
|
||||
include_platform_defaults: false,
|
||||
readable_roots: Vec::new(),
|
||||
},
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: false,
|
||||
exclude_slash_tmp: true,
|
||||
});
|
||||
|
||||
assert!(file_system_policy.entries.iter().any(|entry| {
|
||||
entry.access == FileSystemAccessMode::Write
|
||||
&& entry.path
|
||||
== FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::TmpdirEnvVar,
|
||||
}
|
||||
}));
|
||||
assert!(!file_system_policy.entries.iter().any(|entry| {
|
||||
entry.access == FileSystemAccessMode::Write
|
||||
&& entry.path
|
||||
== FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Tmpdir,
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_access_with_cwd_uses_most_specific_entry() {
|
||||
let cwd = TempDir::new().expect("tempdir");
|
||||
|
||||
@@ -4148,7 +4148,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_system_policy_rejects_legacy_bridge_for_non_workspace_writes() {
|
||||
fn file_system_policy_falls_back_to_read_only_legacy_bridge_for_non_workspace_writes() {
|
||||
let cwd = if cfg!(windows) {
|
||||
Path::new(r"C:\workspace")
|
||||
} else {
|
||||
@@ -4166,14 +4166,19 @@ mod tests {
|
||||
access: FileSystemAccessMode::Write,
|
||||
}]);
|
||||
|
||||
let err = policy
|
||||
let legacy_policy = policy
|
||||
.to_legacy_sandbox_policy(NetworkSandboxPolicy::Restricted, cwd)
|
||||
.expect_err("non-workspace writes should be rejected");
|
||||
.expect("non-workspace writes should fall back to read-only");
|
||||
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("filesystem writes outside the workspace root"),
|
||||
"{err}"
|
||||
assert_eq!(
|
||||
legacy_policy,
|
||||
SandboxPolicy::ReadOnly {
|
||||
access: ReadOnlyAccess::Restricted {
|
||||
include_platform_defaults: false,
|
||||
readable_roots: Vec::new(),
|
||||
},
|
||||
network_access: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -90,12 +90,6 @@
|
||||
(allow file-read* file-test-existence file-write-data file-ioctl
|
||||
(literal "/dev/dtracehelper"))
|
||||
|
||||
; Scratch space so tools can create temp files.
|
||||
(allow file-read* file-test-existence file-write* (subpath "/tmp"))
|
||||
(allow file-read* file-write* (subpath "/private/tmp"))
|
||||
(allow file-read* file-write* (subpath "/var/tmp"))
|
||||
(allow file-read* file-write* (subpath "/private/var/tmp"))
|
||||
|
||||
; Allow reading standard config directories.
|
||||
(allow file-read* (subpath "/etc"))
|
||||
(allow file-read* (subpath "/private/etc"))
|
||||
|
||||
@@ -64,6 +64,32 @@ fn base_policy_allows_node_cpu_sysctls() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restricted_platform_defaults_do_not_include_tmp_write_rules() {
|
||||
let file_system_policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Minimal,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
}]);
|
||||
|
||||
let args = create_seatbelt_command_args_for_policies_with_extensions(
|
||||
vec!["/bin/true".to_string()],
|
||||
&file_system_policy,
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
Path::new("/"),
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let policy = seatbelt_policy_arg(&args);
|
||||
assert!(!policy.contains("(subpath \"/tmp\")"));
|
||||
assert!(!policy.contains("(subpath \"/private/tmp\")"));
|
||||
assert!(!policy.contains("(subpath \"/var/tmp\")"));
|
||||
assert!(!policy.contains("(subpath \"/private/var/tmp\")"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_seatbelt_args_routes_network_through_proxy_ports() {
|
||||
let policy = dynamic_network_policy(
|
||||
|
||||
Reference in New Issue
Block a user