Files
codex/codex-rs/exec-server/src/shell_snapshot_file.rs
T
jif c98e263fb5 Replay exec-server shell snapshots through unnamed files (#48078)
## Why

Large shell snapshots need a replay path that does not add their state to the child process environment.

## What changed

- Keep captured state cached in memory and give each Bash or Zsh launch an independent, read-only file descriptor to source. Unlink the file before writing shell state, create it under the sandbox-protected daemon directory, and close the carrier descriptor before restoring functions.
- Probe descriptor-path access inside the capture sandbox. Retain environment replay for `sh` and when descriptor paths are unavailable; use normal shell startup if preparing the replay file fails.
- Group options and aliases during capture so sourcing preserves their parsing behavior.

## Testing

Add coverage for concurrent replays with 480 KiB payloads, pipe and TTY execution, stdin preservation, descriptor cleanup, sandbox protection, blocked descriptor-path fallback, and alias/option parsing equivalence.

GitOrigin-RevId: 8103d5c37ce4d1885628a4efba3551549f12bea8
2026-09-25 12:23:15 +00:00

49 lines
2.0 KiB
Rust

//! Per-launch snapshot transport, created under the sandbox-protected daemon root.
//! No named file ever contains shell state, and the cache owns bytes, not descriptors.
//! The caller keeps the reader alive through spawn.
use codex_shell_command::shell_detect::ShellType;
use std::fs::File;
use std::io;
use std::io::Write;
use std::os::fd::AsRawFd;
use std::os::unix::fs::MetadataExt;
pub(super) fn materialize(shell: ShellType, state: &str) -> io::Result<File> {
// Mode 0700 alone cannot stop a same-user sandboxed command from replacing
// the pathname or retaining a writable handle before unlink. Reuse the root
// that every filesystem-restricted sandbox hides, independent of CODEX_HOME.
let root = codex_uds::prepare_shared_daemon_socket_directory()?;
let directory = tempfile::tempdir_in(root)?;
let writer = tempfile::NamedTempFile::new_in(directory.path())?;
// Open independently, rather than dup: writing must not advance the reader.
let reader = File::open(writer.path())?;
let reader = File::from(rustix::io::fcntl_dupfd_cloexec(&reader, /*min*/ 10)?);
let (mut writer, path) = writer.into_parts();
path.close()?;
directory.close()?;
if reader.metadata()?.nlink() != 0 {
return Err(io::Error::other("shell snapshot transport is still linked"));
}
let fd = reader.as_raw_fd();
// Source has opened its own reader before this runs. Close the carrier before
// restoring functions that may shadow exec/unset, without borrowing stdin.
let close = match shell {
ShellType::Bash => format!("\\exec {fd}<&-\n"),
ShellType::Zsh => format!(
"__codex_snapshot_fd={fd}\nexec {{__codex_snapshot_fd}}<&-\nunset __codex_snapshot_fd\n"
),
ShellType::Sh | ShellType::PowerShell | ShellType::Cmd => {
return Err(io::Error::other("unsupported snapshot shell"));
}
};
writer.write_all(close.as_bytes())?;
writer.write_all(state.as_bytes())?;
Ok(reader)
}
#[cfg(test)]
#[path = "shell_snapshot_file_tests.rs"]
mod tests;