Bind final-message output before running sandboxed tools (#58974)

## Why

`codex exec --output-last-message` wrote through a pathname after tools finished, allowing sandbox activity to redirect the final write by replacing that path with a symlink. The related risk is described in https://github.com/openai/codex/pull/25956.

## What changed

- Open the destination before starting the agent and retain its handle until the final write.
- Reject symlinks, Windows reparse points, and multiply linked files; verify that the pathname still identifies the bound file before truncating and after writing.
- Propagate output errors so the command fails when the destination cannot be opened or written safely.
- Preserve inherited Unix output streams, FIFOs, devices, and Windows `NUL` support, while avoiding duplicate final answers when human output targets stdout.

## Testing

Add regression coverage for sandbox-created symlinks across consecutive invocations, redirected stdout, delayed truncation, hard links, replaced paths, and Windows stream binding and filesystem identity checks.

GitOrigin-RevId: 4a31bd28a5eb2d0a9e200cb658eb7d19dc93e933
This commit is contained in:
viyatb-oai
2026-11-02 20:36:39 +00:00
committed by copyberry
parent 9c23d93d5b
commit 636cdcb7ec
17 changed files with 788 additions and 54 deletions
@@ -151,6 +151,7 @@ async fn file_system_operations_can_reject_symlinks_in_any_path_component(
symlink(&real, &directory_link)?;
let file_link = tmp_path.join("file-link");
symlink(&existing, &file_link)?;
assert!(codex_exec_server::BoundOutput::open(&directory_link.join("existing.txt")).is_err());
let no_follow_read = ReadFileOptions {
follow_symlinks: false,
@@ -103,6 +103,9 @@ async fn file_system_operations_can_reject_junctions_in_any_path_component(
std::fs::write(&removable, "keep")?;
let directory_junction = tmp.path().join("directory-junction");
create_directory_junction(&real, &directory_junction)?;
assert!(
codex_exec_server::BoundOutput::open(&directory_junction.join("existing.txt")).is_err()
);
let no_follow_read = ReadFileOptions {
follow_symlinks: false,