mirror of
https://github.com/openai/codex.git
synced 2026-10-30 19:18:49 +00:00
## Why On macOS, process-group signals can be denied even when individual members can be signalled. Core execution cleanup used helpers that did not retry those signals against group members. ## What changed - Make `terminate_process_group` and `kill_process_group` use the existing member fallback on macOS, and simplify MCP and pipe callers to use the shared helpers. - Use the saved process-group ID when escalating cancellation after the termination grace period, so this path also uses the fallback. - Update the unsafe process-group ID test to exercise `terminate_process_group`. GitOrigin-RevId: 1bab28d3d53cd401b51ffe71d41fbece12aed66d
95 lines
3.2 KiB
Rust
95 lines
3.2 KiB
Rust
use std::io;
|
|
use std::process::Stdio;
|
|
use std::time::Duration;
|
|
|
|
use anyhow::Context;
|
|
use anyhow::Result;
|
|
use pretty_assertions::assert_eq;
|
|
use tokio::io::AsyncBufReadExt;
|
|
use tokio::io::BufReader;
|
|
use tokio::process::Command;
|
|
use tokio::time::timeout;
|
|
|
|
use super::signal_process_group_with_member_fallback;
|
|
use super::signal_process_id;
|
|
use super::terminate_process_group;
|
|
|
|
#[tokio::test]
|
|
async fn denied_group_signal_terminates_owned_descendants_and_preserves_escalation() -> Result<()> {
|
|
for leader_exited in [false, true] {
|
|
let mut wrapper = Command::new("/bin/sh")
|
|
.args([
|
|
"-c",
|
|
"trap '' TERM; /bin/sleep 30 & resistant=$!; trap - TERM; /bin/sleep 30 & sibling=$!; printf '%s %s\\n' \"$resistant\" \"$sibling\"; wait",
|
|
])
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::null())
|
|
.kill_on_drop(true)
|
|
.process_group(0)
|
|
.spawn()?;
|
|
let process_group_id =
|
|
wrapper.id().context("wrapper process has no process ID")? as libc::pid_t;
|
|
let stdout = wrapper.stdout.take().context("wrapper has no stdout")?;
|
|
let line = timeout(
|
|
Duration::from_secs(5),
|
|
BufReader::new(stdout).lines().next_line(),
|
|
)
|
|
.await??
|
|
.context("missing descendant IDs")?;
|
|
let (resistant_pid, sibling_pid) =
|
|
line.split_once(' ').context("invalid descendant IDs")?;
|
|
let resistant_pid = resistant_pid.parse::<libc::pid_t>()?;
|
|
let sibling_pid = sibling_pid.parse::<libc::pid_t>()?;
|
|
|
|
if leader_exited {
|
|
wrapper.kill().await?;
|
|
}
|
|
|
|
let mut denied_leader = false;
|
|
for signal in [libc::SIGTERM, libc::SIGKILL] {
|
|
assert!(signal_process_group_with_member_fallback(
|
|
process_group_id as u32,
|
|
signal,
|
|
|_, _| Err(io::Error::from_raw_os_error(libc::EPERM)),
|
|
|process_id, signal| {
|
|
if process_id == process_group_id {
|
|
denied_leader = true;
|
|
Err(io::Error::from_raw_os_error(libc::EPERM))
|
|
} else {
|
|
signal_process_id(process_id, signal)
|
|
}
|
|
},
|
|
)?);
|
|
if signal == libc::SIGTERM {
|
|
assert_eq!(denied_leader, !leader_exited);
|
|
assert!(signal_process_id(resistant_pid, /*signal*/ 0)?);
|
|
}
|
|
}
|
|
|
|
for process_id in [resistant_pid, sibling_pid] {
|
|
timeout(Duration::from_secs(5), async move {
|
|
while signal_process_id(process_id, /*signal*/ 0)? {
|
|
tokio::time::sleep(Duration::from_millis(20)).await;
|
|
}
|
|
Ok::<(), io::Error>(())
|
|
})
|
|
.await??;
|
|
}
|
|
|
|
if !leader_exited {
|
|
timeout(Duration::from_secs(5), wrapper.wait()).await??;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn denied_group_signal_rejects_unsafe_process_group_ids() {
|
|
for process_group_id in [0, u32::MAX] {
|
|
let error = terminate_process_group(process_group_id)
|
|
.expect_err("unsafe process group ID should be rejected");
|
|
assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
|
|
}
|
|
}
|