Skip to content

Commit 8f986e0

Browse files
authored
fix(executor): restore tty foreground pgrp for interactive sessions (#42)
When the sandboxed child was placed in its own process group (#39), it was no longer the tty foreground group, causing SIGTTIN on any tty read and hanging interactive shells. Add TtyForegroundGuard: hands the tty foreground to the child pgrp via tcsetpgrp after spawn (SIGTTOU suppressed during handoff) and restores the original foreground on drop.
1 parent 92e9ad1 commit 8f986e0

1 file changed

Lines changed: 48 additions & 0 deletions

File tree

src/sandbox/executor.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,53 @@ fn signal_pgroup(pgid: i32, sig: libc::c_int) {
266266
}
267267
}
268268

269+
/// RAII guard that restores the terminal's foreground process group on drop.
270+
/// Required for interactive sessions: when the child runs in its own pgrp,
271+
/// it must own the tty foreground or any tty read raises SIGTTIN and the
272+
/// shell hangs. The parent must reclaim the foreground on exit so the
273+
/// user's shell does not end up stopped.
274+
struct TtyForegroundGuard {
275+
tty_fd: libc::c_int,
276+
original_pgrp: libc::pid_t,
277+
}
278+
279+
impl TtyForegroundGuard {
280+
/// If stdin is a tty, hand its foreground pgrp to `child_pgid` and
281+
/// return a guard that restores the previous foreground on drop.
282+
fn install(child_pgid: i32) -> Option<Self> {
283+
let tty_fd = libc::STDIN_FILENO;
284+
if unsafe { libc::isatty(tty_fd) } != 1 {
285+
return None;
286+
}
287+
let original_pgrp = unsafe { libc::tcgetpgrp(tty_fd) };
288+
if original_pgrp < 0 {
289+
return None;
290+
}
291+
// tcsetpgrp from a non-foreground pgrp would normally raise SIGTTOU
292+
// on the caller; ignore it briefly so the handoff succeeds.
293+
let prev = unsafe { libc::signal(libc::SIGTTOU, libc::SIG_IGN) };
294+
let rc = unsafe { libc::tcsetpgrp(tty_fd, child_pgid) };
295+
unsafe { libc::signal(libc::SIGTTOU, prev) };
296+
if rc != 0 {
297+
return None;
298+
}
299+
Some(Self {
300+
tty_fd,
301+
original_pgrp,
302+
})
303+
}
304+
}
305+
306+
impl Drop for TtyForegroundGuard {
307+
fn drop(&mut self) {
308+
let prev = unsafe { libc::signal(libc::SIGTTOU, libc::SIG_IGN) };
309+
unsafe {
310+
libc::tcsetpgrp(self.tty_fd, self.original_pgrp);
311+
libc::signal(libc::SIGTTOU, prev);
312+
}
313+
}
314+
}
315+
269316
/// Spawn `cmd` in its own process group and wait for it, forwarding
270317
/// SIGINT/SIGTERM/SIGHUP to the sandboxed subtree with a SIGTERM →
271318
/// grace-period → SIGKILL escalation.
@@ -277,6 +324,7 @@ fn spawn_with_signal_forwarding(mut cmd: Command) -> io::Result<ExitStatus> {
277324
let mut child = cmd.spawn()?;
278325
let pgid = child.id() as i32;
279326
let mut kill_guard = PgidKillGuard::new(pgid);
327+
let _tty_guard = TtyForegroundGuard::install(pgid);
280328

281329
let mut signals = Signals::new([SIGINT, SIGTERM, SIGHUP])?;
282330
let signal_handle = signals.handle();

0 commit comments

Comments
 (0)