diff --git a/pkg/sentry/fsimpl/devpts/line_discipline.go b/pkg/sentry/fsimpl/devpts/line_discipline.go index 714554c8c..9f4a7c41a 100644 --- a/pkg/sentry/fsimpl/devpts/line_discipline.go +++ b/pkg/sentry/fsimpl/devpts/line_discipline.go @@ -180,9 +180,14 @@ func (l *lineDiscipline) setWindowSize(t *kernel.Task, args arch.SyscallArgument } func (l *lineDiscipline) masterReadiness() waiter.EventMask { - // We don't have to lock a termios because the default master termios - // is immutable. - return l.inQueue.writeReadiness(&linux.MasterTermios) | l.outQueue.readReadiness(&linux.MasterTermios) + // The master termios is immutable so termiosMu is not needed. + res := l.inQueue.writeReadiness(&linux.MasterTermios) | l.outQueue.readReadiness(&linux.MasterTermios) + l.termiosMu.RLock() + if l.numReplicas == 0 { + res |= waiter.EventHUp + } + l.termiosMu.RUnlock() + return res } func (l *lineDiscipline) replicaReadiness() waiter.EventMask { @@ -285,8 +290,12 @@ func (l *lineDiscipline) replicaOpen() { // replicaClose is called when a replica file descriptor is closed. func (l *lineDiscipline) replicaClose() { l.termiosMu.Lock() - defer l.termiosMu.Unlock() l.numReplicas-- + notify := l.numReplicas == 0 + l.termiosMu.Unlock() + if notify { + l.masterWaiter.Notify(waiter.EventHUp) + } } // transformer is a helper interface to make it easier to stateify queue. diff --git a/test/syscalls/linux/pty.cc b/test/syscalls/linux/pty.cc index 1a75e587c..b425d3836 100644 --- a/test/syscalls/linux/pty.cc +++ b/test/syscalls/linux/pty.cc @@ -20,6 +20,8 @@ #include #include #include +#include +#include #include #include #include @@ -660,6 +662,49 @@ class PtyTest : public ::testing::Test { FileDescriptor replica_; }; +// NOTE(gvisor.dev/issue/9951): Regression test. +TEST_F(PtyTest, ReplicaCloseNotify) { + // Open a second replica. + FileDescriptor replica2_ = ASSERT_NO_ERRNO_AND_VALUE(OpenReplica(master_)); + fd_set read_set; + FD_ZERO(&read_set); + FD_SET(master_.get(), &read_set); + int max_fd = master_.get() + 1; + + // Set timeout to 0.2 seconds. + struct timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 200000; + + // Ensure that there are no readable events. + FD_ZERO(&read_set); + FD_SET(master_.get(), &read_set); + EXPECT_THAT(select(max_fd, &read_set, NULL, NULL, &tv), + SyscallSucceedsWithValue(0)); + + // Close the second replica and no readable event should occur. + replica2_.reset(); + FD_ZERO(&read_set); + FD_SET(master_.get(), &read_set); + EXPECT_THAT(select(max_fd, &read_set, NULL, NULL, &tv), + SyscallSucceedsWithValue(0)); + + // Close the last remaining replica and a readable event should occur. + replica_.reset(); + FD_ZERO(&read_set); + FD_SET(master_.get(), &read_set); + EXPECT_THAT(select(max_fd, &read_set, NULL, NULL, &tv), + SyscallSucceedsWithValue(1)); + EXPECT_TRUE(FD_ISSET(master_.get(), &read_set)); + + // Check that the right events are occurring. + struct pollfd pfd; + pfd.fd = master_.get(); + pfd.events = POLLIN | POLLOUT | POLLRDHUP | POLLRDNORM | POLLWRNORM; + EXPECT_THAT(poll(&pfd, 1, 1), SyscallSucceedsWithValue(1)); + EXPECT_EQ(POLLHUP | POLLOUT | POLLWRNORM, pfd.revents); +} + // Master to replica sanity test. TEST_F(PtyTest, WriteMasterToReplica) { // N.B. by default, the replica reads nothing until the master writes a