Skip to content

Commit deee4fd

Browse files
committed
fix: preserve zsh-fork escalation fds in unified-exec PTYs
1 parent 4e77ea0 commit deee4fd

7 files changed

Lines changed: 517 additions & 20 deletions

File tree

codex-rs/core/src/tools/runtimes/shell/zsh_fork_backend.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,23 @@ mod imp {
4848
use crate::unified_exec::SpawnLifecycle;
4949
use codex_shell_escalation::EscalationSession;
5050

51+
const ESCALATE_SOCKET_ENV_VAR: &str = "CODEX_ESCALATE_SOCKET";
52+
5153
#[derive(Debug)]
5254
struct ZshForkSpawnLifecycle {
5355
escalation_session: EscalationSession,
5456
}
5557

5658
impl SpawnLifecycle for ZshForkSpawnLifecycle {
59+
fn inherited_fds(&self) -> Vec<i32> {
60+
self.escalation_session
61+
.env()
62+
.get(ESCALATE_SOCKET_ENV_VAR)
63+
.and_then(|fd| fd.parse().ok())
64+
.into_iter()
65+
.collect()
66+
}
67+
5768
fn after_spawn(&mut self) {
5869
self.escalation_session.close_client_socket();
5970
}

codex-rs/core/src/unified_exec/process.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ use super::UnifiedExecError;
2525
use super::head_tail_buffer::HeadTailBuffer;
2626

2727
pub(crate) trait SpawnLifecycle: std::fmt::Debug + Send + Sync {
28+
fn inherited_fds(&self) -> Vec<i32> {
29+
Vec::new()
30+
}
31+
2832
fn after_spawn(&mut self) {}
2933
}
3034

codex-rs/core/src/unified_exec/process_manager.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -535,14 +535,16 @@ impl UnifiedExecProcessManager {
535535
.command
536536
.split_first()
537537
.ok_or(UnifiedExecError::MissingCommandLine)?;
538+
let inherited_fds = spawn_lifecycle.inherited_fds();
538539

539540
let spawn_result = if tty {
540-
codex_utils_pty::pty::spawn_process(
541+
codex_utils_pty::pty::spawn_process_with_inherited_fds(
541542
program,
542543
args,
543544
env.cwd.as_path(),
544545
&env.env,
545546
&env.arg0,
547+
&inherited_fds,
546548
)
547549
.await
548550
} else {

codex-rs/shell-escalation/src/unix/escalate_client.rs

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::io;
2+
use std::os::fd::AsFd;
23
use std::os::fd::AsRawFd;
3-
use std::os::fd::FromRawFd as _;
44
use std::os::fd::OwnedFd;
55

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

31+
fn duplicate_fd_for_transfer(fd: impl AsFd, name: &str) -> anyhow::Result<OwnedFd> {
32+
fd.as_fd()
33+
.try_clone_to_owned()
34+
.with_context(|| format!("failed to duplicate {name} for escalation transfer"))
35+
}
36+
3137
pub async fn run_shell_escalation_execve_wrapper(
3238
file: String,
3339
argv: Vec<String>,
@@ -62,19 +68,26 @@ pub async fn run_shell_escalation_execve_wrapper(
6268
.context("failed to receive EscalateResponse")?;
6369
match message.action {
6470
EscalateAction::Escalate => {
65-
// TODO: maybe we should send ALL open FDs (except the escalate client)?
71+
// Duplicate stdio before transferring ownership to the server. The
72+
// wrapper must keep using its own stdin/stdout/stderr until the
73+
// escalated child takes over.
74+
let destination_fds = [
75+
io::stdin().as_raw_fd(),
76+
io::stdout().as_raw_fd(),
77+
io::stderr().as_raw_fd(),
78+
];
6679
let fds_to_send = [
67-
unsafe { OwnedFd::from_raw_fd(io::stdin().as_raw_fd()) },
68-
unsafe { OwnedFd::from_raw_fd(io::stdout().as_raw_fd()) },
69-
unsafe { OwnedFd::from_raw_fd(io::stderr().as_raw_fd()) },
80+
duplicate_fd_for_transfer(io::stdin(), "stdin")?,
81+
duplicate_fd_for_transfer(io::stdout(), "stdout")?,
82+
duplicate_fd_for_transfer(io::stderr(), "stderr")?,
7083
];
7184

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

7487
client
7588
.send_with_fds(
7689
SuperExecMessage {
77-
fds: fds_to_send.iter().map(AsRawFd::as_raw_fd).collect(),
90+
fds: destination_fds.into_iter().collect(),
7891
},
7992
&fds_to_send,
8093
)
@@ -115,3 +128,23 @@ pub async fn run_shell_escalation_execve_wrapper(
115128
}
116129
}
117130
}
131+
132+
#[cfg(test)]
133+
mod tests {
134+
use super::*;
135+
use std::os::fd::AsRawFd;
136+
use std::os::unix::net::UnixStream;
137+
138+
#[test]
139+
fn duplicate_fd_for_transfer_does_not_close_original() {
140+
let (left, _right) = UnixStream::pair().expect("socket pair");
141+
let original_fd = left.as_raw_fd();
142+
143+
let duplicate = duplicate_fd_for_transfer(&left, "test fd").expect("duplicate fd");
144+
assert_ne!(duplicate.as_raw_fd(), original_fd);
145+
146+
drop(duplicate);
147+
148+
assert_ne!(unsafe { libc::fcntl(original_fd, libc::F_GETFD) }, -1);
149+
}
150+
}

codex-rs/utils/pty/src/process.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
use core::fmt;
2+
use std::any::Any;
23
use std::io;
34
use std::sync::atomic::AtomicBool;
45
use std::sync::Arc;
56
use std::sync::Mutex as StdMutex;
67

7-
use portable_pty::MasterPty;
8-
use portable_pty::SlavePty;
98
use tokio::sync::broadcast;
109
use tokio::sync::mpsc;
1110
use tokio::sync::oneshot;
@@ -17,8 +16,8 @@ pub(crate) trait ChildTerminator: Send + Sync {
1716
}
1817

1918
pub struct PtyHandles {
20-
pub _slave: Option<Box<dyn SlavePty + Send>>,
21-
pub _master: Box<dyn MasterPty + Send>,
19+
pub _slave: Option<Box<dyn Any + Send>>,
20+
pub _master: Box<dyn Any + Send>,
2221
}
2322

2423
impl fmt::Debug for PtyHandles {

0 commit comments

Comments
 (0)