Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions codex-rs/core/src/tools/runtimes/shell/zsh_fork_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,23 @@ mod imp {
use crate::unified_exec::SpawnLifecycle;
use codex_shell_escalation::EscalationSession;

const ESCALATE_SOCKET_ENV_VAR: &str = "CODEX_ESCALATE_SOCKET";

#[derive(Debug)]
struct ZshForkSpawnLifecycle {
escalation_session: EscalationSession,
}

impl SpawnLifecycle for ZshForkSpawnLifecycle {
fn inherited_fds(&self) -> Vec<i32> {
self.escalation_session
.env()
.get(ESCALATE_SOCKET_ENV_VAR)
.and_then(|fd| fd.parse().ok())
.into_iter()
.collect()
}

fn after_spawn(&mut self) {
self.escalation_session.close_client_socket();
}
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/core/src/unified_exec/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ use super::UnifiedExecError;
use super::head_tail_buffer::HeadTailBuffer;

pub(crate) trait SpawnLifecycle: std::fmt::Debug + Send + Sync {
fn inherited_fds(&self) -> Vec<i32> {
Vec::new()
}

fn after_spawn(&mut self) {}
}

Expand Down
7 changes: 5 additions & 2 deletions codex-rs/core/src/unified_exec/process_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -535,23 +535,26 @@ impl UnifiedExecProcessManager {
.command
.split_first()
.ok_or(UnifiedExecError::MissingCommandLine)?;
let inherited_fds = spawn_lifecycle.inherited_fds();

let spawn_result = if tty {
codex_utils_pty::pty::spawn_process(
codex_utils_pty::pty::spawn_process_with_inherited_fds(
program,
args,
env.cwd.as_path(),
&env.env,
&env.arg0,
&inherited_fds,
)
.await
} else {
codex_utils_pty::pipe::spawn_process_no_stdin(
codex_utils_pty::pipe::spawn_process_no_stdin_with_inherited_fds(
program,
args,
env.cwd.as_path(),
&env.env,
&env.arg0,
&inherited_fds,
)
.await
};
Expand Down
45 changes: 39 additions & 6 deletions codex-rs/shell-escalation/src/unix/escalate_client.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::io;
use std::os::fd::AsFd;
use std::os::fd::AsRawFd;
use std::os::fd::FromRawFd as _;
use std::os::fd::OwnedFd;

use anyhow::Context as _;
Expand Down Expand Up @@ -28,6 +28,12 @@ fn get_escalate_client() -> anyhow::Result<AsyncDatagramSocket> {
Ok(unsafe { AsyncDatagramSocket::from_raw_fd(client_fd) }?)
}

fn duplicate_fd_for_transfer(fd: impl AsFd, name: &str) -> anyhow::Result<OwnedFd> {
fd.as_fd()
.try_clone_to_owned()
.with_context(|| format!("failed to duplicate {name} for escalation transfer"))
}

pub async fn run_shell_escalation_execve_wrapper(
file: String,
argv: Vec<String>,
Expand Down Expand Up @@ -62,19 +68,26 @@ pub async fn run_shell_escalation_execve_wrapper(
.context("failed to receive EscalateResponse")?;
match message.action {
EscalateAction::Escalate => {
// TODO: maybe we should send ALL open FDs (except the escalate client)?
// Duplicate stdio before transferring ownership to the server. The
// wrapper must keep using its own stdin/stdout/stderr until the
// escalated child takes over.
let destination_fds = [
io::stdin().as_raw_fd(),
io::stdout().as_raw_fd(),
io::stderr().as_raw_fd(),
];
let fds_to_send = [
unsafe { OwnedFd::from_raw_fd(io::stdin().as_raw_fd()) },
unsafe { OwnedFd::from_raw_fd(io::stdout().as_raw_fd()) },
unsafe { OwnedFd::from_raw_fd(io::stderr().as_raw_fd()) },
duplicate_fd_for_transfer(io::stdin(), "stdin")?,
duplicate_fd_for_transfer(io::stdout(), "stdout")?,
duplicate_fd_for_transfer(io::stderr(), "stderr")?,
];

// TODO: also forward signals over the super-exec socket

client
.send_with_fds(
SuperExecMessage {
fds: fds_to_send.iter().map(AsRawFd::as_raw_fd).collect(),
fds: destination_fds.into_iter().collect(),
},
&fds_to_send,
)
Expand Down Expand Up @@ -115,3 +128,23 @@ pub async fn run_shell_escalation_execve_wrapper(
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::os::fd::AsRawFd;
use std::os::unix::net::UnixStream;

#[test]
fn duplicate_fd_for_transfer_does_not_close_original() {
let (left, _right) = UnixStream::pair().expect("socket pair");
let original_fd = left.as_raw_fd();

let duplicate = duplicate_fd_for_transfer(&left, "test fd").expect("duplicate fd");
assert_ne!(duplicate.as_raw_fd(), original_fd);

drop(duplicate);

assert_ne!(unsafe { libc::fcntl(original_fd, libc::F_GETFD) }, -1);
}
}
33 changes: 31 additions & 2 deletions codex-rs/utils/pty/src/pipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,15 @@ async fn spawn_process_with_stdin_mode(
env: &HashMap<String, String>,
arg0: &Option<String>,
stdin_mode: PipeStdinMode,
inherited_fds: &[i32],
) -> Result<SpawnedProcess> {
if program.is_empty() {
anyhow::bail!("missing program for pipe spawn");
}

#[cfg(not(unix))]
let _ = inherited_fds;

let mut command = Command::new(program);
#[cfg(unix)]
if let Some(arg0) = arg0 {
Expand All @@ -116,11 +120,14 @@ async fn spawn_process_with_stdin_mode(
#[cfg(target_os = "linux")]
let parent_pid = unsafe { libc::getpid() };
#[cfg(unix)]
let inherited_fds = inherited_fds.to_vec();
#[cfg(unix)]
unsafe {
command.pre_exec(move || {
crate::process_group::detach_from_tty()?;
#[cfg(target_os = "linux")]
crate::process_group::set_parent_death_signal(parent_pid)?;
crate::pty::close_random_fds_except(&inherited_fds);
Ok(())
});
}
Expand Down Expand Up @@ -253,7 +260,7 @@ pub async fn spawn_process(
env: &HashMap<String, String>,
arg0: &Option<String>,
) -> Result<SpawnedProcess> {
spawn_process_with_stdin_mode(program, args, cwd, env, arg0, PipeStdinMode::Piped).await
spawn_process_with_stdin_mode(program, args, cwd, env, arg0, PipeStdinMode::Piped, &[]).await
}

/// Spawn a process using regular pipes, but close stdin immediately.
Expand All @@ -264,5 +271,27 @@ pub async fn spawn_process_no_stdin(
env: &HashMap<String, String>,
arg0: &Option<String>,
) -> Result<SpawnedProcess> {
spawn_process_with_stdin_mode(program, args, cwd, env, arg0, PipeStdinMode::Null).await
spawn_process_no_stdin_with_inherited_fds(program, args, cwd, env, arg0, &[]).await
}

/// Spawn a process using regular pipes, close stdin immediately, and preserve
/// selected inherited file descriptors across exec on Unix.
pub async fn spawn_process_no_stdin_with_inherited_fds(
program: &str,
args: &[String],
cwd: &Path,
env: &HashMap<String, String>,
arg0: &Option<String>,
inherited_fds: &[i32],
) -> Result<SpawnedProcess> {
spawn_process_with_stdin_mode(
program,
args,
cwd,
env,
arg0,
PipeStdinMode::Null,
inherited_fds,
)
.await
}
10 changes: 6 additions & 4 deletions codex-rs/utils/pty/src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use std::sync::Mutex as StdMutex;

use portable_pty::MasterPty;
use portable_pty::SlavePty;
use tokio::sync::broadcast;
use tokio::sync::mpsc;
use tokio::sync::oneshot;
Expand All @@ -16,9 +14,13 @@ pub(crate) trait ChildTerminator: Send + Sync {
fn kill(&mut self) -> io::Result<()>;
}

pub(crate) trait PtyHandleKeepAlive: Send {}

impl<T: Send> PtyHandleKeepAlive for T {}

pub struct PtyHandles {
pub _slave: Option<Box<dyn SlavePty + Send>>,
pub _master: Box<dyn MasterPty + Send>,
pub(crate) _slave: Option<Box<dyn PtyHandleKeepAlive>>,
pub(crate) _master: Box<dyn PtyHandleKeepAlive>,
}

impl fmt::Debug for PtyHandles {
Expand Down
Loading
Loading