tests: cover sandbox link write behavior (#21819)

## Why

[PR #1705](https://github.com/openai/codex/pull/1705) moved
`apply_patch` execution under the configured sandbox and called out the
need for integration coverage. We already covered textual `../` escapes,
but did not have coverage for link aliases that live inside a writable
workspace while pointing at, or aliasing, files visible outside it.

This PR locks in the current sandbox boundary without changing
production write semantics. Symlink escapes into a read-only outside
root should fail and leave the outside file unchanged. Existing hard
links are characterized separately: if a user-created hard link already
exists inside the writable root, sandboxed writes preserve normal
hard-link semantics rather than replacing the link and silently breaking
that relationship.

## What Changed

- Added
`apply_patch_cli_does_not_write_through_symlink_escape_outside_workspace`
to verify `apply_patch` cannot update a symlink that targets a file
outside the writable workspace.
- Added `apply_patch_cli_preserves_existing_hard_link_outside_workspace`
to verify `apply_patch` intentionally writes through an existing hard
link and does not unlink or replace it.
- Added `file_system_sandboxed_write_preserves_existing_hard_link` to
verify sandboxed `fs/writeFile` preserves an existing hard link and
writes the shared inode.

## Testing

- `cargo test -p codex-exec-server file_system_sandboxed_write`
- `cargo test -p codex-core
apply_patch_cli_does_not_write_through_symlink_escape_outside_workspace`
- `cargo test -p codex-core
apply_patch_cli_preserves_existing_hard_link_outside_workspace`
- `just fix -p codex-exec-server -p codex-core`
- `just fix -p codex-core`



---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/21819).
* #21845
* __->__ #21819
This commit is contained in:
Michael Bolin
2026-05-09 08:28:15 -07:00
committed by GitHub
Unverified
parent fca81eeb5b
commit 0c70698e24
3 changed files with 276 additions and 1 deletions
+48
View File
@@ -2,6 +2,7 @@
mod common;
use std::os::unix::fs::MetadataExt;
#[cfg(target_os = "linux")]
use std::os::unix::fs::PermissionsExt;
use std::os::unix::fs::symlink;
@@ -732,6 +733,53 @@ async fn file_system_sandboxed_write_rejects_symlink_escape(use_remote: bool) ->
Ok(())
}
#[test_case(false ; "local")]
#[test_case(true ; "remote")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn file_system_sandboxed_write_preserves_existing_hard_link(use_remote: bool) -> Result<()> {
let context = create_file_system_context(use_remote).await?;
let file_system = context.file_system;
let tmp = TempDir::new()?;
let allowed_dir = tmp.path().join("allowed");
let outside_dir = tmp.path().join("outside");
std::fs::create_dir_all(&allowed_dir)?;
std::fs::create_dir_all(&outside_dir)?;
let outside_file = outside_dir.join("outside.txt");
let hard_link = allowed_dir.join("hard-link.txt");
std::fs::write(&outside_file, "outside\n")?;
std::fs::hard_link(&outside_file, &hard_link)?;
let sandbox = workspace_write_sandbox(allowed_dir);
file_system
.write_file(
&absolute_path(hard_link.clone()),
b"updated through existing hard link\n".to_vec(),
Some(&sandbox),
)
.await
.with_context(|| format!("mode={use_remote}"))?;
assert_eq!(
std::fs::read_to_string(&outside_file)?,
"updated through existing hard link\n"
);
assert_eq!(
std::fs::read_to_string(&hard_link)?,
"updated through existing hard link\n"
);
let outside_metadata = std::fs::metadata(&outside_file)?;
let link_metadata = std::fs::metadata(&hard_link)?;
assert_eq!(
(link_metadata.dev(), link_metadata.ino()),
(outside_metadata.dev(), outside_metadata.ino())
);
Ok(())
}
#[test_case(false ; "local")]
#[test_case(true ; "remote")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]