From 9008a0eff915042e46b1ecb2a1b32e3b2b5225a9 Mon Sep 17 00:00:00 2001 From: Skylar Graika Date: Fri, 30 Jan 2026 13:47:10 -0800 Subject: [PATCH] core: prevent shell_snapshot from inheriting stdin (#9735) Fixes #9559. When `shell_snapshot` runs, it may execute user startup files (e.g. `.bashrc`). If those files read from stdin (or if stdin is an interactive TTY under job control), the snapshot subprocess can block or receive `SIGTTIN` (as reported over SSH). This change explicitly sets `stdin` to `Stdio::null()` for the snapshot subprocess, so it can't read from the terminal. Regression test added that would hang/timeout without this change. Tests: `ulimit -n 4096 && cargo test -p codex-core`. cc @dongdongbh @etraut-openai --------- Co-authored-by: Skylar Graika --- codex-rs/core/src/shell_snapshot.rs | 90 +++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/codex-rs/core/src/shell_snapshot.rs b/codex-rs/core/src/shell_snapshot.rs index d6d738508a..7ba1b67caf 100644 --- a/codex-rs/core/src/shell_snapshot.rs +++ b/codex-rs/core/src/shell_snapshot.rs @@ -1,6 +1,7 @@ use std::io::ErrorKind; use std::path::Path; use std::path::PathBuf; +use std::process::Stdio; use std::sync::Arc; use std::time::Duration; use std::time::SystemTime; @@ -187,6 +188,7 @@ async fn run_script_with_timeout( // returns a ref of handler. let mut handler = Command::new(&args[0]); handler.args(&args[1..]); + handler.stdin(Stdio::null()); #[cfg(unix)] unsafe { handler.pre_exec(|| { @@ -477,6 +479,62 @@ mod tests { use tempfile::tempdir; + #[cfg(unix)] + struct BlockingStdinPipe { + original: i32, + write_end: i32, + } + + #[cfg(unix)] + impl BlockingStdinPipe { + fn install() -> Result { + let mut fds = [0i32; 2]; + if unsafe { libc::pipe(fds.as_mut_ptr()) } == -1 { + return Err(std::io::Error::last_os_error()).context("create stdin pipe"); + } + + let original = unsafe { libc::dup(libc::STDIN_FILENO) }; + if original == -1 { + let err = std::io::Error::last_os_error(); + unsafe { + libc::close(fds[0]); + libc::close(fds[1]); + } + return Err(err).context("dup stdin"); + } + + if unsafe { libc::dup2(fds[0], libc::STDIN_FILENO) } == -1 { + let err = std::io::Error::last_os_error(); + unsafe { + libc::close(fds[0]); + libc::close(fds[1]); + libc::close(original); + } + return Err(err).context("replace stdin"); + } + + unsafe { + libc::close(fds[0]); + } + + Ok(Self { + original, + write_end: fds[1], + }) + } + } + + #[cfg(unix)] + impl Drop for BlockingStdinPipe { + fn drop(&mut self) { + unsafe { + libc::dup2(self.original, libc::STDIN_FILENO); + libc::close(self.original); + libc::close(self.write_end); + } + } + } + #[cfg(not(target_os = "windows"))] fn assert_posix_snapshot_sections(snapshot: &str) { assert!(snapshot.contains("# Snapshot file")); @@ -557,6 +615,38 @@ mod tests { Ok(()) } + #[cfg(unix)] + #[tokio::test] + async fn snapshot_shell_does_not_inherit_stdin() -> Result<()> { + let _stdin_guard = BlockingStdinPipe::install()?; + + let dir = tempdir()?; + let home = dir.path(); + fs::write(home.join(".bashrc"), "read -r ignored\n").await?; + + let shell = Shell { + shell_type: ShellType::Bash, + shell_path: PathBuf::from("/bin/bash"), + shell_snapshot: crate::shell::empty_shell_snapshot_receiver(), + }; + + let home_display = home.display(); + let script = format!( + "HOME=\"{home_display}\"; export HOME; {}", + bash_snapshot_script() + ); + let output = run_script_with_timeout(&shell, &script, Duration::from_millis(500), true) + .await + .context("run snapshot command")?; + + assert!( + output.contains("# Snapshot file"), + "expected snapshot marker in output; output={output:?}" + ); + + Ok(()) + } + #[cfg(target_os = "linux")] #[tokio::test] async fn timed_out_snapshot_shell_is_terminated() -> Result<()> {