mirror of
https://github.com/openai/codex.git
synced 2026-11-13 22:47:20 +00:00
fix(permissions): fix symlinked writable roots in sandbox permissions (#15981)
## Summary - preserve logical symlink paths during permission normalization and config cwd handling - bind real targets for symlinked readable/writable roots in bwrap and remap carveouts and unreadable roots there - add regressions for symlinked carveouts and nested symlink escape masking ## Root cause Permission normalization canonicalized symlinked writable roots and cwd to their real targets too early. That drifted policy checks away from the logical paths the sandboxed process can actually address, while bwrap still needed the real targets for mounts. The mismatch caused shell and apply_patch failures on symlinked writable roots. ## Impact Fixes #15781. Also fixes #17079: - #17079 is the protected symlinked carveout side: bwrap now binds the real symlinked writable-root target and remaps carveouts before masking. Related to #15157: - #15157 is the broader permission-check side of this path-identity problem. This PR addresses the shared logical-vs-canonical normalization issue, but the reported Darwin prompt behavior should be validated separately before auto-closing it. This should also fix #14672, #14694, #14715, and #15725: - #14672, #14694, and #14715 are the same Linux symlinked-writable-root/bwrap family as #15781. - #15725 is the protected symlinked workspace path variant; the PR preserves the protected logical path in policy space while bwrap applies read-only or unreadable treatment to the resolved target so file-vs-directory bind mismatches do not abort sandbox setup. ## Notes - Added Linux-only regressions for symlinked writable ancestors and protected symlinked directory targets, including nested symlink escape masking without rebinding the escape target writable. --------- Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
1 parent
0a99943a94
commit
b114781495
11 files changed
+503
-111
No files matched your search
@@ -5,6 +5,7 @@ use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_absolute_path::canonicalize_preserving_symlinks;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -471,6 +472,9 @@ impl FileSystemSandboxPolicy {
|
||||
// so root-wide aliases do not create duplicate top-level masks.
|
||||
// Example: keep `/var/...` normalized under `/` instead of
|
||||
// materializing both `/var/...` and `/private/var/...`.
|
||||
// Nested symlink paths under a writable root stay logical so
|
||||
// downstream sandboxes can still bind the real target while
|
||||
// masking the user-visible symlink inode when needed.
|
||||
let preserve_raw_carveout_paths = root.as_path().parent().is_some();
|
||||
let raw_writable_roots: Vec<&AbsolutePathBuf> = writable_entries
|
||||
.iter()
|
||||
@@ -1081,14 +1085,17 @@ fn dedup_absolute_paths(
|
||||
fn normalize_effective_absolute_path(path: AbsolutePathBuf) -> AbsolutePathBuf {
|
||||
let raw_path = path.to_path_buf();
|
||||
for ancestor in raw_path.ancestors() {
|
||||
let Ok(canonical_ancestor) = ancestor.canonicalize() else {
|
||||
if std::fs::symlink_metadata(ancestor).is_err() {
|
||||
continue;
|
||||
}
|
||||
let Ok(normalized_ancestor) = canonicalize_preserving_symlinks(ancestor) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(suffix) = raw_path.strip_prefix(ancestor) else {
|
||||
continue;
|
||||
};
|
||||
if let Ok(normalized_path) =
|
||||
AbsolutePathBuf::from_absolute_path(canonical_ancestor.join(suffix))
|
||||
AbsolutePathBuf::from_absolute_path(normalized_ancestor.join(suffix))
|
||||
{
|
||||
return normalized_path;
|
||||
}
|
||||
@@ -1422,7 +1429,7 @@ mod tests {
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn effective_runtime_roots_canonicalize_symlinked_paths() {
|
||||
fn effective_runtime_roots_preserve_symlinked_paths() {
|
||||
let cwd = TempDir::new().expect("tempdir");
|
||||
let real_root = cwd.path().join("real");
|
||||
let link_root = cwd.path().join("link");
|
||||
@@ -1436,18 +1443,9 @@ mod tests {
|
||||
let link_root =
|
||||
AbsolutePathBuf::from_absolute_path(&link_root).expect("absolute symlinked root");
|
||||
let link_blocked = link_root.join("blocked");
|
||||
let expected_root = AbsolutePathBuf::from_absolute_path(
|
||||
real_root.canonicalize().expect("canonicalize real root"),
|
||||
)
|
||||
.expect("absolute canonical root");
|
||||
let expected_blocked = AbsolutePathBuf::from_absolute_path(
|
||||
blocked.canonicalize().expect("canonicalize blocked"),
|
||||
)
|
||||
.expect("absolute canonical blocked");
|
||||
let expected_codex = AbsolutePathBuf::from_absolute_path(
|
||||
codex_dir.canonicalize().expect("canonicalize .codex"),
|
||||
)
|
||||
.expect("absolute canonical .codex");
|
||||
let expected_root = link_root.clone();
|
||||
let expected_blocked = link_blocked.clone();
|
||||
let expected_codex = link_root.join(".codex");
|
||||
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
@@ -1482,7 +1480,7 @@ mod tests {
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn current_working_directory_special_path_canonicalizes_symlinked_cwd() {
|
||||
fn current_working_directory_special_path_preserves_symlinked_cwd() {
|
||||
let cwd = TempDir::new().expect("tempdir");
|
||||
let real_root = cwd.path().join("real");
|
||||
let link_root = cwd.path().join("link");
|
||||
@@ -1497,22 +1495,11 @@ mod tests {
|
||||
|
||||
let link_blocked =
|
||||
AbsolutePathBuf::from_absolute_path(link_root.join("blocked")).expect("link blocked");
|
||||
let expected_root = AbsolutePathBuf::from_absolute_path(
|
||||
real_root.canonicalize().expect("canonicalize real root"),
|
||||
)
|
||||
.expect("absolute canonical root");
|
||||
let expected_blocked = AbsolutePathBuf::from_absolute_path(
|
||||
blocked.canonicalize().expect("canonicalize blocked"),
|
||||
)
|
||||
.expect("absolute canonical blocked");
|
||||
let expected_agents = AbsolutePathBuf::from_absolute_path(
|
||||
agents_dir.canonicalize().expect("canonicalize .agents"),
|
||||
)
|
||||
.expect("absolute canonical .agents");
|
||||
let expected_codex = AbsolutePathBuf::from_absolute_path(
|
||||
codex_dir.canonicalize().expect("canonicalize .codex"),
|
||||
)
|
||||
.expect("absolute canonical .codex");
|
||||
let expected_root =
|
||||
AbsolutePathBuf::from_absolute_path(&link_root).expect("absolute symlinked root");
|
||||
let expected_blocked = link_blocked.clone();
|
||||
let expected_agents = expected_root.join(".agents");
|
||||
let expected_codex = expected_root.join(".codex");
|
||||
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
@@ -1617,11 +1604,8 @@ mod tests {
|
||||
let link_root =
|
||||
AbsolutePathBuf::from_absolute_path(&link_root).expect("absolute symlinked root");
|
||||
let link_private = link_root.join("linked-private");
|
||||
let expected_root = AbsolutePathBuf::from_absolute_path(
|
||||
real_root.canonicalize().expect("canonicalize real root"),
|
||||
)
|
||||
.expect("absolute canonical root");
|
||||
let expected_linked_private = expected_root.join("linked-private");
|
||||
let expected_root = link_root.clone();
|
||||
let expected_linked_private = link_private.clone();
|
||||
let unexpected_decoy =
|
||||
AbsolutePathBuf::from_absolute_path(decoy.canonicalize().expect("canonicalize decoy"))
|
||||
.expect("absolute canonical decoy");
|
||||
@@ -1667,11 +1651,8 @@ mod tests {
|
||||
let link_root =
|
||||
AbsolutePathBuf::from_absolute_path(&link_root).expect("absolute symlinked root");
|
||||
let link_private = link_root.join("linked-private");
|
||||
let expected_root = AbsolutePathBuf::from_absolute_path(
|
||||
real_root.canonicalize().expect("canonicalize real root"),
|
||||
)
|
||||
.expect("absolute canonical root");
|
||||
let expected_linked_private = expected_root.join("linked-private");
|
||||
let expected_root = link_root.clone();
|
||||
let expected_linked_private = link_private.clone();
|
||||
let unexpected_decoy =
|
||||
AbsolutePathBuf::from_absolute_path(decoy.canonicalize().expect("canonicalize decoy"))
|
||||
.expect("absolute canonical decoy");
|
||||
@@ -1737,12 +1718,12 @@ mod tests {
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn tmpdir_special_path_canonicalizes_symlinked_tmpdir() {
|
||||
fn tmpdir_special_path_preserves_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_special_path_preserves_symlinked_tmpdir")
|
||||
.output()
|
||||
.expect("run tmpdir subprocess test");
|
||||
|
||||
@@ -1767,20 +1748,10 @@ mod tests {
|
||||
|
||||
let link_blocked =
|
||||
AbsolutePathBuf::from_absolute_path(link_tmpdir.join("blocked")).expect("link blocked");
|
||||
let expected_root = AbsolutePathBuf::from_absolute_path(
|
||||
real_tmpdir
|
||||
.canonicalize()
|
||||
.expect("canonicalize real tmpdir"),
|
||||
)
|
||||
.expect("absolute canonical tmpdir");
|
||||
let expected_blocked = AbsolutePathBuf::from_absolute_path(
|
||||
blocked.canonicalize().expect("canonicalize blocked"),
|
||||
)
|
||||
.expect("absolute canonical blocked");
|
||||
let expected_codex = AbsolutePathBuf::from_absolute_path(
|
||||
codex_dir.canonicalize().expect("canonicalize .codex"),
|
||||
)
|
||||
.expect("absolute canonical .codex");
|
||||
let expected_root =
|
||||
AbsolutePathBuf::from_absolute_path(&link_tmpdir).expect("absolute symlinked tmpdir");
|
||||
let expected_blocked = link_blocked.clone();
|
||||
let expected_codex = expected_root.join(".codex");
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("TMPDIR", &link_tmpdir);
|
||||
@@ -1932,8 +1903,7 @@ mod tests {
|
||||
let cwd = TempDir::new().expect("tempdir");
|
||||
let docs = AbsolutePathBuf::resolve_path_against_base("docs", cwd.path());
|
||||
let expected_docs = AbsolutePathBuf::from_absolute_path(
|
||||
cwd.path()
|
||||
.canonicalize()
|
||||
canonicalize_preserving_symlinks(cwd.path())
|
||||
.expect("canonicalize cwd")
|
||||
.join("docs"),
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user