diff --git a/pkg/sentry/fsimpl/devpts/line_discipline.go b/pkg/sentry/fsimpl/devpts/line_discipline.go index 60dd4476f..20d68f75e 100644 --- a/pkg/sentry/fsimpl/devpts/line_discipline.go +++ b/pkg/sentry/fsimpl/devpts/line_discipline.go @@ -106,6 +106,9 @@ type lineDiscipline struct { // handling certain special characters like backspace. column int + // numReplicas is the number of replica file descriptors. + numReplicas int + // masterWaiter is used to wait on the master end of the TTY. masterWaiter waiter.Queue @@ -269,6 +272,20 @@ func (l *lineDiscipline) outputQueueWrite(ctx context.Context, src usermem.IOSeq return 0, linuxerr.ErrWouldBlock } +// replicaOpen is called when a replica file descriptor is opened. +func (l *lineDiscipline) replicaOpen() { + l.termiosMu.Lock() + defer l.termiosMu.Unlock() + l.numReplicas++ +} + +// replicaClose is called when a replica file descriptor is closed. +func (l *lineDiscipline) replicaClose() { + l.termiosMu.Lock() + defer l.termiosMu.Unlock() + l.numReplicas-- +} + // transformer is a helper interface to make it easier to stateify queue. type transformer interface { // transform functions require queue's mutex to be held. diff --git a/pkg/sentry/fsimpl/devpts/queue.go b/pkg/sentry/fsimpl/devpts/queue.go index 01119a673..affb90601 100644 --- a/pkg/sentry/fsimpl/devpts/queue.go +++ b/pkg/sentry/fsimpl/devpts/queue.go @@ -110,6 +110,9 @@ func (q *queue) read(ctx context.Context, dst usermem.IOSequence, l *lineDiscipl defer q.mu.Unlock() if !q.readable { + if l.numReplicas == 0 { + return 0, false, false, linuxerr.EIO + } return 0, false, false, linuxerr.ErrWouldBlock } diff --git a/pkg/sentry/fsimpl/devpts/replica.go b/pkg/sentry/fsimpl/devpts/replica.go index cac44b778..6f9c29c6b 100644 --- a/pkg/sentry/fsimpl/devpts/replica.go +++ b/pkg/sentry/fsimpl/devpts/replica.go @@ -69,6 +69,7 @@ func (ri *replicaInode) Open(ctx context.Context, rp *vfs.ResolvingPath, d *kern // ignored silently. _ = t.ThreadGroup().SetControllingTTY(fd.inode.t.replicaKTTY, false /* steal */, fd.vfsfd.IsReadable()) } + ri.t.ld.replicaOpen() return &fd.vfsfd, nil } @@ -114,7 +115,9 @@ type replicaFileDescription struct { var _ vfs.FileDescriptionImpl = (*replicaFileDescription)(nil) // Release implements fs.FileOperations.Release. -func (rfd *replicaFileDescription) Release(ctx context.Context) {} +func (rfd *replicaFileDescription) Release(ctx context.Context) { + rfd.inode.t.ld.replicaClose() +} // EventRegister implements waiter.Waitable.EventRegister. func (rfd *replicaFileDescription) EventRegister(e *waiter.Entry) error { diff --git a/test/syscalls/linux/pty.cc b/test/syscalls/linux/pty.cc index 3d8ae1ca3..28abbea99 100644 --- a/test/syscalls/linux/pty.cc +++ b/test/syscalls/linux/pty.cc @@ -650,6 +650,29 @@ TEST_F(PtyTest, WriteReplicaToMaster) { EXPECT_EQ(memcmp(buf, kExpected, sizeof(kExpected)), 0); } +// Verifies that data enqueued into the replica is still readable by the master +// after the replica is closed. +TEST_F(PtyTest, WriteReplicaToMasterReadAfterReplicaClosed) { + // N.B. by default, the master reads nothing until the replica writes a + // newline, and the master gets a carriage return. + constexpr char kInput[] = "hello\n"; + constexpr char kExpected[] = "hello\r\n"; + + EXPECT_THAT(WriteFd(replica_.get(), kInput, sizeof(kInput) - 1), + SyscallSucceedsWithValue(sizeof(kInput) - 1)); + + // Close the replica. + replica_.reset(); + + char buf[sizeof(kExpected)] = {}; + ExpectReadable(master_, sizeof(buf) - 1, buf); + EXPECT_EQ(memcmp(buf, kExpected, sizeof(kExpected)), 0); + + // After all data has been read, the master should return EIO. + char c; + EXPECT_THAT(ReadFd(master_.get(), &c, 1), SyscallFailsWithErrno(EIO)); +} + TEST_F(PtyTest, WriteInvalidUTF8) { char c = 0xff; ASSERT_THAT(syscall(__NR_write, master_.get(), &c, sizeof(c)),