Skip to content

core: stop stealing the tty foreground group from embedding processes - #168

Merged
congwang-mk merged 2 commits into
mainfrom
fix-tty-foreground-grab
Jul 27, 2026
Merged

core: stop stealing the tty foreground group from embedding processes#168
congwang-mk merged 2 commits into
mainfrom
fix-tty-foreground-grab

Conversation

@congwang-mk

Copy link
Copy Markdown
Contributor

Fixes the second problem reported in #164 (the first, the empty sandlock list, is addressed by the control-socket work in #153).

Problem

The confined child made itself the terminal's foreground process group whenever stdin was a tty, regardless of how the sandbox was spawned. For the captured run()/spawn() paths used by the SDKs, this silently demoted the embedding process (a Python REPL in the report) to a background job: its next stdin read raised SIGTTIN and the shell reported it as Stopped, and nothing ever returned the foreground group after the child exited.

Reproduced on main with a pty harness: after Sandbox.run() returns, tcgetpgrp(0) points at the dead sandbox child's group instead of the caller's, on both the success and failure paths.

Fix (two commits)

  1. Captured runs never touch the terminal. The parent threads the stdio intent into the child setup (ChildSpawnArgs.foreground, true only when every stream is inherited), and the tcsetpgrp grab is gated on it.
  2. Interactive runs give the terminal back. The runtime records that the child took the foreground; whoever reaps the child (wait() or Drop) restores it to the calling process. The restore only fires while the child's group still owns the terminal, and blocks SIGTTOU around tcsetpgrp since the caller is a background group at that moment. This matters for API users embedding run_interactive(): the CLI's parent shell hides the leak, but an embedding process lives on the same terminal afterward.

Tests

New integration tests in test_tty.rs run each case inside a fresh session whose controlling terminal is a private pty:

  • captured run leaves the tty foreground with the caller (failed with "foreground stolen" before commit 1)
  • interactive run returns the foreground after exit (failed before commit 2)
  • interactive child owns the foreground while alive, and the caller gets it back after kill() + wait() (also covers the kill path)

Full Rust workspace suite and all 386 Python tests pass. CLI behavior verified end to end: a non-job-control script that runs "sandlock run -i ... -- /bin/true" and then reads stdin gets stopped with a main-built binary and reads normally with this branch.

🤖 Generated with Claude Code

The confined child made itself the terminal's foreground process group
whenever stdin was a tty, regardless of how the sandbox was spawned. The
grab exists for interactive runs, where the child genuinely owns the
terminal, but it also fired on the captured run()/spawn() paths used by
the SDKs. Any embedding process on a job-control terminal (a Python REPL
being the reported case) was silently demoted to a background job: its
next stdin read raised SIGTTIN and the shell reported it as stopped, and
nothing ever returned the foreground group after the child exited. This
is the second problem reported in #164.

Thread the stdio spec's intent down to confine_child and take the
foreground only when every stream is inherited, which is exactly the
interactive case.

Signed-off-by: Cong Wang <cwang@multikernel.io>
… runs

An interactive child takes the terminal's foreground process group so it
can read from the tty, but nothing gave the foreground back when the
child exited. That is fine for the CLI, whose parent shell restores
itself when sandlock exits, but an API user embedding run_interactive()
keeps living on the same terminal afterward: it stays a background
group, and its next tty read stops it with SIGTTIN, the same symptom
issue #164 reported for captured runs.

Track the handover in the runtime and, wherever the child is reaped
(wait() or Drop), move the foreground back to this process. The restore
only fires while the child's group still owns the terminal, and blocks
SIGTTOU around tcsetpgrp since the caller is a background group at that
point.

Signed-off-by: Cong Wang <cwang@multikernel.io>
@congwang-mk
congwang-mk force-pushed the fix-tty-foreground-grab branch from 22f7284 to 6948d44 Compare July 27, 2026 22:40
@congwang-mk
congwang-mk merged commit b1b1d23 into main Jul 27, 2026
13 checks passed
@congwang-mk
congwang-mk deleted the fix-tty-foreground-grab branch July 27, 2026 22:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant