Skip to content

Commit 5042b00

Browse files
committed
pty: Send SIGHUP to foreground process group when master is closed
When the PTY master file descriptor is closed, Linux sends SIGHUP to the foreground process group of the session that has this PTY as its controlling terminal. gVisor was missing this behavior entirely: closing the master end did not send any signal, causing child processes spawned via PTY to remain running indefinitely instead of being notified of the hangup. Fix by adding a TTY.Hangup() method that reuses the existing ReleaseControllingTTY() logic to send SIGHUP/SIGCONT and clear the controlling terminal. Call Hangup() for both masterKTTY and replicaKTTY in masterClose(). Signed-off-by: Tan Yifeng <yiftan@tencent.com>
1 parent 4eeb780 commit 5042b00

3 files changed

Lines changed: 71 additions & 0 deletions

File tree

pkg/sentry/fsimpl/devpts/devpts.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,12 @@ 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, send SIGHUP to the foreground process group
290+
// of the session that has this PTY as its controlling terminal.
291+
// This corresponds to Linux's pty_close() calling tty_vhangup().
292+
t.masterKTTY.Hangup(ctx)
293+
t.replicaKTTY.Hangup(ctx)
294+
289295
i.mu.Lock()
290296
defer i.mu.Unlock()
291297

pkg/sentry/kernel/tty.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,21 @@ 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 when the PTY master is
164+
// closed and corresponds to Linux's tty_vhangup_session().
165+
func (tty *TTY) Hangup(ctx context.Context) {
166+
tty.mu.Lock()
167+
tg := tty.tg
168+
tty.mu.Unlock()
169+
170+
if tg == nil {
171+
// This TTY is not a controlling terminal.
172+
return
173+
}
174+
175+
// Reuse the existing ReleaseControllingTTY logic which handles
176+
// sending SIGHUP/SIGCONT and clearing the controlling terminal.
177+
_ = tg.ReleaseControllingTTY(tty)
178+
}

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

@@ -2213,6 +2219,47 @@ TEST_F(PtyTest, SignalCharConsumedWhenISIGEnabled) {
22132219
EXPECT_EQ(buf, 'a');
22142220
}
22152221

2222+
// When the PTY master is closed, SIGHUP should be sent to the foreground
2223+
// process group of the session that has this PTY as its controlling terminal.
2224+
// This matches Linux pty_close() -> tty_vhangup() behavior.
2225+
TEST_F(JobControlTest, SIGHUPOnMasterClose) {
2226+
pid_t child = fork();
2227+
if (child == 0) {
2228+
// Close the inherited master fd so that the parent holds the only
2229+
// reference. pty_close/tty_vhangup fires only when the last master
2230+
// fd is closed.
2231+
close(master_.release());
2232+
2233+
// Create new session and set the replica as controlling terminal.
2234+
TEST_PCHECK(setsid() >= 0);
2235+
TEST_PCHECK(ioctl(replica_.get(), TIOCSCTTY, 0) >= 0);
2236+
2237+
// Install a SIGHUP handler that exits with a known status.
2238+
struct sigaction sa = {};
2239+
sa.sa_handler = [](int) { _exit(42); };
2240+
sigemptyset(&sa.sa_mask);
2241+
TEST_PCHECK(sigaction(SIGHUP, &sa, nullptr) >= 0);
2242+
2243+
// Sleep waiting for the signal. Use a timeout to avoid hanging the test.
2244+
sleep(10);
2245+
// If we get here, SIGHUP was not received.
2246+
_exit(1);
2247+
}
2248+
ASSERT_GT(child, 0);
2249+
2250+
// Give the child time to set up its session and controlling terminal.
2251+
absl::SleepFor(absl::Milliseconds(500));
2252+
2253+
// Close the master end. This should trigger SIGHUP to the child.
2254+
master_.reset();
2255+
2256+
// Wait for the child and verify it received SIGHUP (exited with 42).
2257+
int wstatus;
2258+
ASSERT_THAT(waitpid(child, &wstatus, 0), SyscallSucceedsWithValue(child));
2259+
ASSERT_TRUE(WIFEXITED(wstatus));
2260+
EXPECT_EQ(WEXITSTATUS(wstatus), 42);
2261+
}
2262+
22162263
} // namespace
22172264
} // namespace testing
22182265
} // namespace gvisor

0 commit comments

Comments
 (0)