Skip to content

Commit cce0d19

Browse files
committed
fix: exec-server should drop oversized env vars when escalating
When trying to introduce an integration test for the `codex-shell-tool-mcp` in #7617, macOS CI hit serde decode errors in the escalation pipe when huge env vars inflated the `EscalateRequest` payload past the stream frame, corrupting JSON. (I'm pretty sure `$GITHUB_EVENT` was the offending env var.) This PR updates `exec-server` to filter out oversized env entries and skip reserved vars before serialization. It also updates the code to avoid attaching empty `SCM_RIGHTS` control messages so frames stay lean when no FDs are sent.
1 parent e91bb6b commit cce0d19

File tree

2 files changed

+112
-31
lines changed

2 files changed

+112
-31
lines changed

codex-rs/exec-server/src/posix/escalate_client.rs

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::collections::HashMap;
12
use std::io;
23
use std::os::fd::AsRawFd;
34
use std::os::fd::FromRawFd as _;
@@ -34,21 +35,15 @@ pub(crate) async fn run(file: String, argv: Vec<String>) -> anyhow::Result<i32>
3435
.send_with_fds(&HANDSHAKE_MESSAGE, &[server.into_inner().into()])
3536
.await
3637
.context("failed to send handshake datagram")?;
37-
let env = std::env::vars()
38-
.filter(|(k, _)| {
39-
!matches!(
40-
k.as_str(),
41-
ESCALATE_SOCKET_ENV_VAR | BASH_EXEC_WRAPPER_ENV_VAR
42-
)
43-
})
44-
.collect();
38+
let env = filter_env(std::env::vars());
39+
let request = EscalateRequest {
40+
file: file.clone().into(),
41+
argv: argv.clone(),
42+
workdir: std::env::current_dir()?,
43+
env,
44+
};
4545
client
46-
.send(EscalateRequest {
47-
file: file.clone().into(),
48-
argv: argv.clone(),
49-
workdir: std::env::current_dir()?,
50-
env,
51-
})
46+
.send(request)
5247
.await
5348
.context("failed to send EscalateRequest")?;
5449
let message = client.receive::<EscalateResponse>().await?;
@@ -107,3 +102,64 @@ pub(crate) async fn run(file: String, argv: Vec<String>) -> anyhow::Result<i32>
107102
}
108103
}
109104
}
105+
106+
fn filter_env<I>(env_iter: I) -> HashMap<String, String>
107+
where
108+
I: IntoIterator<Item = (String, String)>,
109+
{
110+
const MAX_ENV_ENTRY_LEN: i64 = 8_192;
111+
let mut env = HashMap::new();
112+
for (key, value) in env_iter {
113+
if matches!(
114+
key.as_str(),
115+
ESCALATE_SOCKET_ENV_VAR | BASH_EXEC_WRAPPER_ENV_VAR
116+
) {
117+
continue;
118+
}
119+
let entry_len = (key.len() + value.len()) as i64;
120+
if entry_len > MAX_ENV_ENTRY_LEN {
121+
tracing::debug!(key, entry_len, "skipping oversized environment variable");
122+
continue;
123+
}
124+
env.insert(key, value);
125+
}
126+
env
127+
}
128+
129+
#[cfg(test)]
130+
mod tests {
131+
use super::*;
132+
use pretty_assertions::assert_eq;
133+
134+
#[test]
135+
fn filter_env_drops_oversized_and_reserved_entries() {
136+
let oversized_value = "A".repeat(8_193);
137+
let env = vec![
138+
("KEEP".to_string(), "ok".to_string()),
139+
("DROP".to_string(), oversized_value),
140+
(
141+
ESCALATE_SOCKET_ENV_VAR.to_string(),
142+
"should_skip".to_string(),
143+
),
144+
(
145+
BASH_EXEC_WRAPPER_ENV_VAR.to_string(),
146+
"should_skip".to_string(),
147+
),
148+
];
149+
let filtered = filter_env(env);
150+
assert_eq!(Some(&"ok".to_string()), filtered.get("KEEP"));
151+
assert!(!filtered.contains_key("DROP"));
152+
assert!(!filtered.contains_key(ESCALATE_SOCKET_ENV_VAR));
153+
assert!(!filtered.contains_key(BASH_EXEC_WRAPPER_ENV_VAR));
154+
}
155+
156+
#[test]
157+
fn filter_env_keeps_entries_at_limit() {
158+
const KEY: &str = "KEEP";
159+
let value_len = 8_192 - KEY.len();
160+
let env = vec![(KEY.to_string(), "A".repeat(value_len))];
161+
let filtered = filter_env(env);
162+
assert_eq!(1, filtered.len());
163+
assert_eq!(value_len, filtered[KEY].len());
164+
}
165+
}

codex-rs/exec-server/src/posix/socket.rs

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -182,21 +182,26 @@ fn send_message_bytes(socket: &Socket, data: &[u8], fds: &[OwnedFd]) -> std::io:
182182
frame.extend_from_slice(&encode_length(data.len())?);
183183
frame.extend_from_slice(data);
184184

185-
let mut control = vec![0u8; control_space_for_fds(fds.len())];
186-
unsafe {
187-
let cmsg = control.as_mut_ptr().cast::<libc::cmsghdr>();
188-
(*cmsg).cmsg_len = libc::CMSG_LEN(size_of::<RawFd>() as c_uint * fds.len() as c_uint) as _;
189-
(*cmsg).cmsg_level = libc::SOL_SOCKET;
190-
(*cmsg).cmsg_type = libc::SCM_RIGHTS;
191-
let data_ptr = libc::CMSG_DATA(cmsg).cast::<RawFd>();
192-
for (i, fd) in fds.iter().enumerate() {
193-
data_ptr.add(i).write(fd.as_raw_fd());
194-
}
195-
}
196-
185+
let mut control;
197186
let payload = [IoSlice::new(&frame)];
198-
let msg = MsgHdr::new().with_buffers(&payload).with_control(&control);
199-
let mut sent = socket.sendmsg(&msg, 0)?;
187+
let mut sent = if fds.is_empty() {
188+
socket.send(&frame)?
189+
} else {
190+
control = vec![0u8; control_space_for_fds(fds.len())];
191+
unsafe {
192+
let cmsg = control.as_mut_ptr().cast::<libc::cmsghdr>();
193+
(*cmsg).cmsg_len =
194+
libc::CMSG_LEN(size_of::<RawFd>() as c_uint * fds.len() as c_uint) as _;
195+
(*cmsg).cmsg_level = libc::SOL_SOCKET;
196+
(*cmsg).cmsg_type = libc::SCM_RIGHTS;
197+
let data_ptr = libc::CMSG_DATA(cmsg).cast::<RawFd>();
198+
for (i, fd) in fds.iter().enumerate() {
199+
data_ptr.add(i).write(fd.as_raw_fd());
200+
}
201+
}
202+
let msg = MsgHdr::new().with_buffers(&payload).with_control(&control);
203+
socket.sendmsg(&msg, 0)?
204+
};
200205
while sent < frame.len() {
201206
let bytes = socket.send(&frame[sent..])?;
202207
if bytes == 0 {
@@ -236,8 +241,9 @@ fn send_datagram_bytes(socket: &Socket, data: &[u8], fds: &[OwnedFd]) -> std::io
236241
format!("too many fds: {}", fds.len()),
237242
));
238243
}
239-
let mut control = vec![0u8; control_space_for_fds(fds.len())];
240-
if !fds.is_empty() {
244+
245+
let control = if !fds.is_empty() {
246+
let mut control = vec![0u8; control_space_for_fds(fds.len())];
241247
unsafe {
242248
let cmsg = control.as_mut_ptr().cast::<libc::cmsghdr>();
243249
(*cmsg).cmsg_len =
@@ -249,7 +255,10 @@ fn send_datagram_bytes(socket: &Socket, data: &[u8], fds: &[OwnedFd]) -> std::io
249255
data_ptr.add(i).write(fd.as_raw_fd());
250256
}
251257
}
252-
}
258+
control
259+
} else {
260+
vec![]
261+
};
253262
let payload = [IoSlice::new(data)];
254263
let msg = MsgHdr::new().with_buffers(&payload).with_control(&control);
255264
let written = socket.sendmsg(&msg, 0)?;
@@ -433,6 +442,22 @@ mod tests {
433442
Ok(())
434443
}
435444

445+
#[tokio::test]
446+
async fn async_socket_round_trips_without_fds() -> std::io::Result<()> {
447+
let (server, client) = AsyncSocket::pair()?;
448+
let payload = TestPayload {
449+
id: 13,
450+
label: "no-fds".to_string(),
451+
};
452+
453+
let receive_task = tokio::spawn(async move { server.receive::<TestPayload>().await });
454+
client.send(payload.clone()).await?;
455+
456+
let received_payload = receive_task.await.unwrap()?;
457+
assert_eq!(payload, received_payload);
458+
Ok(())
459+
}
460+
436461
#[tokio::test]
437462
async fn async_datagram_sockets_round_trip_messages() -> std::io::Result<()> {
438463
let (server, client) = AsyncDatagramSocket::pair()?;

0 commit comments

Comments
 (0)