core: stop stealing the tty foreground group from embedding processes - #168
Merged
Conversation
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
force-pushed
the
fix-tty-foreground-grab
branch
from
July 27, 2026 22:40
22f7284 to
6948d44
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)
Tests
New integration tests in test_tty.rs run each case inside a fresh session whose controlling terminal is a private pty:
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