From 9fe0462626720ca58b700e7ee852e4d492825e42 Mon Sep 17 00:00:00 2001 From: Skylar Graika Date: Thu, 22 Jan 2026 17:40:37 -0800 Subject: [PATCH 1/4] core: prevent shell_snapshot subprocess from reading stdin --- codex-rs/core/src/shell_snapshot.rs | 87 +++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/codex-rs/core/src/shell_snapshot.rs b/codex-rs/core/src/shell_snapshot.rs index 44e732df613..f04b9e7d04c 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; @@ -180,6 +181,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(|| { @@ -453,6 +455,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")); @@ -531,6 +589,35 @@ 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<()> { From 0796384362f53b6882151245ff8135778d39eee0 Mon Sep 17 00:00:00 2001 From: Skylar Graika Date: Thu, 22 Jan 2026 17:43:34 -0800 Subject: [PATCH 2/4] fmt --- codex-rs/core/src/shell_snapshot.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/codex-rs/core/src/shell_snapshot.rs b/codex-rs/core/src/shell_snapshot.rs index f04b9e7d04c..21550bef833 100644 --- a/codex-rs/core/src/shell_snapshot.rs +++ b/codex-rs/core/src/shell_snapshot.rs @@ -605,7 +605,10 @@ mod tests { }; let home_display = home.display(); - let script = format!("HOME=\"{home_display}\"; export HOME; {}", bash_snapshot_script()); + 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")?; From 69eef1794ae24b2019182368ed60f72cc76cea9c Mon Sep 17 00:00:00 2001 From: Skylar Graika Date: Fri, 23 Jan 2026 06:43:02 -0800 Subject: [PATCH 3/4] ci: skip Bazel workflow on fork PRs --- .github/workflows/bazel.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/bazel.yml b/.github/workflows/bazel.yml index 4f97d2de023..9663d4e8bd6 100644 --- a/.github/workflows/bazel.yml +++ b/.github/workflows/bazel.yml @@ -17,6 +17,9 @@ concurrency: cancel-in-progress: ${{ github.ref_name != 'main' }} jobs: test: + # This workflow relies on `secrets.BUILDBUDDY_API_KEY`, which is not available for PRs + # from forks. Skip the Bazel jobs in that case rather than failing with UNAUTHENTICATED. + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} strategy: fail-fast: false matrix: From ac486981c9ecce60091a126827a83a35e1b007a1 Mon Sep 17 00:00:00 2001 From: Skylar Graika Date: Tue, 27 Jan 2026 08:23:24 -0800 Subject: [PATCH 4/4] Revert "ci: skip Bazel workflow on fork PRs" This reverts commit 69eef1794ae24b2019182368ed60f72cc76cea9c. --- .github/workflows/bazel.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/bazel.yml b/.github/workflows/bazel.yml index 9663d4e8bd6..4f97d2de023 100644 --- a/.github/workflows/bazel.yml +++ b/.github/workflows/bazel.yml @@ -17,9 +17,6 @@ concurrency: cancel-in-progress: ${{ github.ref_name != 'main' }} jobs: test: - # This workflow relies on `secrets.BUILDBUDDY_API_KEY`, which is not available for PRs - # from forks. Skip the Bazel jobs in that case rather than failing with UNAUTHENTICATED. - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} strategy: fail-fast: false matrix: