Skip to content

Commit b773847

Browse files
committed
pty: Send SIGHUP to foreground process group on master close
When the PTY master is closed, send SIGHUPto the foreground process group of the session that has the slave as its controlling terminal. This matches Linux's pty_close() -> tty_vhangup(tty->link) behavior (drivers/tty/pty.c). Without this, child processes spawned via PTY (e.g. by expect/telnet) are never notified of the hangup and remain running indefinitely. Signed-off-by: Tan Yifeng <yiftan@tencent.com>
1 parent 58b37ef commit b773847

3 files changed

Lines changed: 70 additions & 0 deletions

File tree

pkg/sentry/fsimpl/devpts/devpts.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,10 @@ func (i *rootInode) allocateTerminal(ctx context.Context, creds *auth.Credential
286286

287287
// masterClose is called when the master end of t is closed.
288288
func (i *rootInode) masterClose(ctx context.Context, t *Terminal) {
289+
// When the master is closed, hang up the slave (replica) side.
290+
// This corresponds to Linux's pty_close() calling tty_vhangup(tty->link).
291+
t.replicaKTTY.Hangup(ctx)
292+
289293
i.mu.Lock()
290294
defer i.mu.Unlock()
291295

pkg/sentry/kernel/tty.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,22 @@ func (tty *TTY) CheckChange(ctx context.Context, sig linux.Signal) error {
158158
_ = pg.SendSignal(SignalInfoPriv(sig))
159159
return linuxerr.ERESTARTSYS
160160
}
161+
162+
// Hangup releases the controlling terminal and sends SIGHUP/SIGCONT to
163+
// the foreground process group. This is called on the replica (slave) TTY
164+
// when the PTY master is closed, corresponding to Linux's pty_close()
165+
// calling tty_vhangup(tty->link).
166+
func (tty *TTY) Hangup(ctx context.Context) {
167+
tty.mu.Lock()
168+
tg := tty.tg
169+
tty.mu.Unlock()
170+
171+
if tg == nil {
172+
// This TTY is not a controlling terminal.
173+
return
174+
}
175+
176+
// Reuse the existing ReleaseControllingTTY logic which handles
177+
// sending SIGHUP/SIGCONT and clearing the controlling terminal.
178+
_ = tg.ReleaseControllingTTY(tty)
179+
}

test/syscalls/linux/pty.cc

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,12 @@ TEST(BasicPtyTest, OpenDevTTY) {
425425
// which will be opened by /dev/tty.
426426
setsid();
427427

428+
// Ignore SIGHUP: when the master fd is closed during cleanup, the
429+
// kernel sends SIGHUP to the foreground process group of the
430+
// controlling terminal session. Since this child *is* the session leader,
431+
// it would be killed by the default SIGHUP disposition before _exit(0).
432+
TEST_PCHECK(signal(SIGHUP, SIG_IGN) != SIG_ERR);
433+
428434
FileDescriptor master =
429435
TEST_CHECK_NO_ERRNO_AND_VALUE(Open("/dev/ptmx", O_RDWR));
430436

@@ -2449,6 +2455,47 @@ TEST_F(PtyTest, SignalCharConsumedWhenISIGEnabled) {
24492455
EXPECT_EQ(buf, 'a');
24502456
}
24512457

2458+
// When the PTY master is closed, SIGHUP should be sent to the foreground
2459+
// process group of the session that has this PTY as its controlling terminal.
2460+
// This matches Linux pty_close() -> tty_vhangup() behavior.
2461+
TEST_F(JobControlTest, SIGHUPOnMasterClose) {
2462+
pid_t child = fork();
2463+
if (child == 0) {
2464+
// Close the inherited master fd so that the parent holds the only
2465+
// reference. pty_close/tty_vhangup fires only when the last master
2466+
// fd is closed.
2467+
close(master_.release());
2468+
2469+
// Create new session and set the replica as controlling terminal.
2470+
TEST_PCHECK(setsid() >= 0);
2471+
TEST_PCHECK(ioctl(replica_.get(), TIOCSCTTY, 0) >= 0);
2472+
2473+
// Install a SIGHUP handler that exits with a known status.
2474+
struct sigaction sa = {};
2475+
sa.sa_handler = [](int) { _exit(42); };
2476+
sigemptyset(&sa.sa_mask);
2477+
TEST_PCHECK(sigaction(SIGHUP, &sa, nullptr) >= 0);
2478+
2479+
// Sleep waiting for the signal. Use a timeout to avoid hanging the test.
2480+
sleep(10);
2481+
// If we get here, SIGHUP was not received.
2482+
_exit(1);
2483+
}
2484+
ASSERT_GT(child, 0);
2485+
2486+
// Give the child time to set up its session and controlling terminal.
2487+
absl::SleepFor(absl::Milliseconds(500));
2488+
2489+
// Close the master end. This should trigger SIGHUP to the child.
2490+
master_.reset();
2491+
2492+
// Wait for the child and verify it received SIGHUP (exited with 42).
2493+
int wstatus;
2494+
ASSERT_THAT(waitpid(child, &wstatus, 0), SyscallSucceedsWithValue(child));
2495+
ASSERT_TRUE(WIFEXITED(wstatus));
2496+
EXPECT_EQ(WEXITSTATUS(wstatus), 42);
2497+
}
2498+
24522499
} // namespace
24532500
} // namespace testing
24542501
} // namespace gvisor

0 commit comments

Comments
 (0)