core-agent-ide/codex-rs/shell-escalation
Michael Bolin ef37d313c6
fix: preserve zsh-fork escalation fds across unified-exec spawn paths (#13644)
## Why

`zsh-fork` sessions launched through unified-exec need the escalation
socket to survive the wrapper -> server -> child handoff so later
intercepted `exec()` calls can still reach the escalation server.

The inherited-fd spawn path also needs to avoid closing Rust's internal
exec-error pipe, and the shell-escalation handoff needs to tolerate the
receive-side case where a transferred fd is installed into the same
stdio slot it will be mapped onto.

## What Changed

- Added `SpawnLifecycle::inherited_fds()` in
`codex-rs/core/src/unified_exec/process.rs` and threaded inherited fds
through `codex-rs/core/src/unified_exec/process_manager.rs` so
unified-exec can preserve required descriptors across both PTY and
no-stdin pipe spawn paths.
- Updated `codex-rs/core/src/tools/runtimes/shell/zsh_fork_backend.rs`
to expose the escalation socket fd through the spawn lifecycle.
- Added inherited-fd-aware spawn helpers in
`codex-rs/utils/pty/src/pty.rs` and `codex-rs/utils/pty/src/pipe.rs`,
including Unix pre-exec fd pruning that preserves requested inherited
fds while leaving `FD_CLOEXEC` descriptors alone. The pruning helper is
now named `close_inherited_fds_except()` to better describe that
behavior.
- Updated `codex-rs/shell-escalation/src/unix/escalate_client.rs` to
duplicate local stdio before transfer and send destination stdio numbers
in `SuperExecMessage`, so the wrapper keeps using its own
`stdin`/`stdout`/`stderr` until the escalated child takes over.
- Updated `codex-rs/shell-escalation/src/unix/escalate_server.rs` so the
server accepts the overlap case where a received fd reuses the same
stdio descriptor number that the child setup will target with `dup2`.
- Added comments around the PTY stdio wiring and the overlap regression
helper to make the fd handoff and controlling-terminal setup easier to
follow.

## Verification

- `cargo test -p codex-utils-pty`
- covers preserved-fd PTY spawn behavior, PTY resize, Python REPL
continuity, exec-failure reporting, and the no-stdin pipe path
- `cargo test -p codex-shell-escalation`
- covers duplicated-fd transfer on the client side and verifies the
overlap case by passing a pipe-backed stdin payload through the
server-side `dup2` path

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/13644).
* #14624
* __->__ #13644
2026-03-13 20:25:31 +00:00
..
src fix: preserve zsh-fork escalation fds across unified-exec spawn paths (#13644) 2026-03-13 20:25:31 +00:00
BUILD.bazel refactor: normalize unix module layout for exec-server and shell-escalation (#12556) 2026-02-23 09:28:17 -08:00
Cargo.toml feat: include sandbox config with escalation request (#12839) 2026-02-26 12:00:18 -08:00
README.md fix: use https://git.savannah.gnu.org/git/bash instead of https://github.com/bolinfest/bash (#13057) 2026-03-02 09:09:54 -08:00

codex-shell-escalation

This crate contains the Unix shell-escalation protocol implementation and the codex-execve-wrapper executable.

codex-execve-wrapper receives the arguments to an intercepted execve(2) call and delegates the decision to the shell-escalation protocol over a shared file descriptor (specified by the CODEX_ESCALATE_SOCKET environment variable). The server on the other side replies with one of:

  • Run: codex-execve-wrapper should invoke execve(2) on itself to run the original command within the sandboxed shell.
  • Escalate: forward the file descriptors of the current process so the command can be run faithfully outside the sandbox. When the process completes, the server forwards the exit code back to codex-execve-wrapper.
  • Deny: the server has declared the proposed command to be forbidden, so codex-execve-wrapper prints an error to stderr and exits with 1.

Patched Bash

We carry a small patch to execute_cmd.c (see patches/bash-exec-wrapper.patch) that adds support for EXEC_WRAPPER. The original commit message is “add support for BASH_EXEC_WRAPPER” and the patch applies cleanly to a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b from https://github.com/bminor/bash. To rebuild manually:

git clone https://git.savannah.gnu.org/git/bash
git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
git apply /path/to/patches/bash-exec-wrapper.patch
./configure --without-bash-malloc
make -j"$(nproc)"