summaryrefslogtreecommitdiffhomepage
path: root/test/syscalls/linux/pty.cc
diff options
context:
space:
mode:
Diffstat (limited to 'test/syscalls/linux/pty.cc')
-rw-r--r--test/syscalls/linux/pty.cc1673
1 files changed, 0 insertions, 1673 deletions
diff --git a/test/syscalls/linux/pty.cc b/test/syscalls/linux/pty.cc
deleted file mode 100644
index 2e4ab6ca8..000000000
--- a/test/syscalls/linux/pty.cc
+++ /dev/null
@@ -1,1673 +0,0 @@
-// Copyright 2018 The gVisor Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include <fcntl.h>
-#include <linux/capability.h>
-#include <linux/major.h>
-#include <poll.h>
-#include <sched.h>
-#include <signal.h>
-#include <sys/ioctl.h>
-#include <sys/mman.h>
-#include <sys/stat.h>
-#include <sys/sysmacros.h>
-#include <sys/types.h>
-#include <sys/wait.h>
-#include <termios.h>
-#include <unistd.h>
-
-#include <iostream>
-
-#include "gtest/gtest.h"
-#include "absl/base/macros.h"
-#include "absl/strings/str_cat.h"
-#include "absl/synchronization/notification.h"
-#include "absl/time/clock.h"
-#include "absl/time/time.h"
-#include "test/util/capability_util.h"
-#include "test/util/file_descriptor.h"
-#include "test/util/posix_error.h"
-#include "test/util/pty_util.h"
-#include "test/util/test_util.h"
-#include "test/util/thread_util.h"
-
-namespace gvisor {
-namespace testing {
-
-namespace {
-
-using ::testing::AnyOf;
-using ::testing::Contains;
-using ::testing::Eq;
-using ::testing::Not;
-using SubprocessCallback = std::function<void()>;
-
-// Tests Unix98 pseudoterminals.
-//
-// These tests assume that /dev/ptmx exists and is associated with a devpts
-// filesystem mounted at /dev/pts/. While a Linux distribution could
-// theoretically place those anywhere, glibc expects those locations, so they
-// are effectively fixed.
-
-// Minor device number for an unopened ptmx file.
-constexpr int kPtmxMinor = 2;
-
-// The timeout when polling for data from a pty. When data is written to one end
-// of a pty, Linux asynchronously makes it available to the other end, so we
-// have to wait.
-constexpr absl::Duration kTimeout = absl::Seconds(20);
-
-// The maximum line size in bytes returned per read from a pty file.
-constexpr int kMaxLineSize = 4096;
-
-constexpr char kMainPath[] = "/dev/ptmx";
-
-// glibc defines its own, different, version of struct termios. We care about
-// what the kernel does, not glibc.
-#define KERNEL_NCCS 19
-struct kernel_termios {
- tcflag_t c_iflag;
- tcflag_t c_oflag;
- tcflag_t c_cflag;
- tcflag_t c_lflag;
- cc_t c_line;
- cc_t c_cc[KERNEL_NCCS];
-};
-
-bool operator==(struct kernel_termios const& a,
- struct kernel_termios const& b) {
- return memcmp(&a, &b, sizeof(a)) == 0;
-}
-
-// Returns the termios-style control character for the passed character.
-//
-// e.g., for Ctrl-C, i.e., ^C, call ControlCharacter('C').
-//
-// Standard control characters are ASCII bytes 0 through 31.
-constexpr char ControlCharacter(char c) {
- // A is 1, B is 2, etc.
- return c - 'A' + 1;
-}
-
-// Returns the printable character the given control character represents.
-constexpr char FromControlCharacter(char c) { return c + 'A' - 1; }
-
-// Returns true if c is a control character.
-//
-// Standard control characters are ASCII bytes 0 through 31.
-constexpr bool IsControlCharacter(char c) { return c <= 31; }
-
-struct Field {
- const char* name;
- uint64_t mask;
- uint64_t value;
-};
-
-// ParseFields returns a string representation of value, using the names in
-// fields.
-std::string ParseFields(const Field* fields, size_t len, uint64_t value) {
- bool first = true;
- std::string s;
- for (size_t i = 0; i < len; i++) {
- const Field f = fields[i];
- if ((value & f.mask) == f.value) {
- if (!first) {
- s += "|";
- }
- s += f.name;
- first = false;
- value &= ~f.mask;
- }
- }
-
- if (value) {
- if (!first) {
- s += "|";
- }
- absl::StrAppend(&s, value);
- }
-
- return s;
-}
-
-const Field kIflagFields[] = {
- {"IGNBRK", IGNBRK, IGNBRK}, {"BRKINT", BRKINT, BRKINT},
- {"IGNPAR", IGNPAR, IGNPAR}, {"PARMRK", PARMRK, PARMRK},
- {"INPCK", INPCK, INPCK}, {"ISTRIP", ISTRIP, ISTRIP},
- {"INLCR", INLCR, INLCR}, {"IGNCR", IGNCR, IGNCR},
- {"ICRNL", ICRNL, ICRNL}, {"IUCLC", IUCLC, IUCLC},
- {"IXON", IXON, IXON}, {"IXANY", IXANY, IXANY},
- {"IXOFF", IXOFF, IXOFF}, {"IMAXBEL", IMAXBEL, IMAXBEL},
- {"IUTF8", IUTF8, IUTF8},
-};
-
-const Field kOflagFields[] = {
- {"OPOST", OPOST, OPOST}, {"OLCUC", OLCUC, OLCUC},
- {"ONLCR", ONLCR, ONLCR}, {"OCRNL", OCRNL, OCRNL},
- {"ONOCR", ONOCR, ONOCR}, {"ONLRET", ONLRET, ONLRET},
- {"OFILL", OFILL, OFILL}, {"OFDEL", OFDEL, OFDEL},
- {"NL0", NLDLY, NL0}, {"NL1", NLDLY, NL1},
- {"CR0", CRDLY, CR0}, {"CR1", CRDLY, CR1},
- {"CR2", CRDLY, CR2}, {"CR3", CRDLY, CR3},
- {"TAB0", TABDLY, TAB0}, {"TAB1", TABDLY, TAB1},
- {"TAB2", TABDLY, TAB2}, {"TAB3", TABDLY, TAB3},
- {"BS0", BSDLY, BS0}, {"BS1", BSDLY, BS1},
- {"FF0", FFDLY, FF0}, {"FF1", FFDLY, FF1},
- {"VT0", VTDLY, VT0}, {"VT1", VTDLY, VT1},
- {"XTABS", XTABS, XTABS},
-};
-
-#ifndef IBSHIFT
-// Shift from CBAUD to CIBAUD.
-#define IBSHIFT 16
-#endif
-
-const Field kCflagFields[] = {
- {"B0", CBAUD, B0},
- {"B50", CBAUD, B50},
- {"B75", CBAUD, B75},
- {"B110", CBAUD, B110},
- {"B134", CBAUD, B134},
- {"B150", CBAUD, B150},
- {"B200", CBAUD, B200},
- {"B300", CBAUD, B300},
- {"B600", CBAUD, B600},
- {"B1200", CBAUD, B1200},
- {"B1800", CBAUD, B1800},
- {"B2400", CBAUD, B2400},
- {"B4800", CBAUD, B4800},
- {"B9600", CBAUD, B9600},
- {"B19200", CBAUD, B19200},
- {"B38400", CBAUD, B38400},
- {"CS5", CSIZE, CS5},
- {"CS6", CSIZE, CS6},
- {"CS7", CSIZE, CS7},
- {"CS8", CSIZE, CS8},
- {"CSTOPB", CSTOPB, CSTOPB},
- {"CREAD", CREAD, CREAD},
- {"PARENB", PARENB, PARENB},
- {"PARODD", PARODD, PARODD},
- {"HUPCL", HUPCL, HUPCL},
- {"CLOCAL", CLOCAL, CLOCAL},
- {"B57600", CBAUD, B57600},
- {"B115200", CBAUD, B115200},
- {"B230400", CBAUD, B230400},
- {"B460800", CBAUD, B460800},
- {"B500000", CBAUD, B500000},
- {"B576000", CBAUD, B576000},
- {"B921600", CBAUD, B921600},
- {"B1000000", CBAUD, B1000000},
- {"B1152000", CBAUD, B1152000},
- {"B1500000", CBAUD, B1500000},
- {"B2000000", CBAUD, B2000000},
- {"B2500000", CBAUD, B2500000},
- {"B3000000", CBAUD, B3000000},
- {"B3500000", CBAUD, B3500000},
- {"B4000000", CBAUD, B4000000},
- {"CMSPAR", CMSPAR, CMSPAR},
- {"CRTSCTS", CRTSCTS, CRTSCTS},
- {"IB0", CIBAUD, B0 << IBSHIFT},
- {"IB50", CIBAUD, B50 << IBSHIFT},
- {"IB75", CIBAUD, B75 << IBSHIFT},
- {"IB110", CIBAUD, B110 << IBSHIFT},
- {"IB134", CIBAUD, B134 << IBSHIFT},
- {"IB150", CIBAUD, B150 << IBSHIFT},
- {"IB200", CIBAUD, B200 << IBSHIFT},
- {"IB300", CIBAUD, B300 << IBSHIFT},
- {"IB600", CIBAUD, B600 << IBSHIFT},
- {"IB1200", CIBAUD, B1200 << IBSHIFT},
- {"IB1800", CIBAUD, B1800 << IBSHIFT},
- {"IB2400", CIBAUD, B2400 << IBSHIFT},
- {"IB4800", CIBAUD, B4800 << IBSHIFT},
- {"IB9600", CIBAUD, B9600 << IBSHIFT},
- {"IB19200", CIBAUD, B19200 << IBSHIFT},
- {"IB38400", CIBAUD, B38400 << IBSHIFT},
- {"IB57600", CIBAUD, B57600 << IBSHIFT},
- {"IB115200", CIBAUD, B115200 << IBSHIFT},
- {"IB230400", CIBAUD, B230400 << IBSHIFT},
- {"IB460800", CIBAUD, B460800 << IBSHIFT},
- {"IB500000", CIBAUD, B500000 << IBSHIFT},
- {"IB576000", CIBAUD, B576000 << IBSHIFT},
- {"IB921600", CIBAUD, B921600 << IBSHIFT},
- {"IB1000000", CIBAUD, B1000000 << IBSHIFT},
- {"IB1152000", CIBAUD, B1152000 << IBSHIFT},
- {"IB1500000", CIBAUD, B1500000 << IBSHIFT},
- {"IB2000000", CIBAUD, B2000000 << IBSHIFT},
- {"IB2500000", CIBAUD, B2500000 << IBSHIFT},
- {"IB3000000", CIBAUD, B3000000 << IBSHIFT},
- {"IB3500000", CIBAUD, B3500000 << IBSHIFT},
- {"IB4000000", CIBAUD, B4000000 << IBSHIFT},
-};
-
-const Field kLflagFields[] = {
- {"ISIG", ISIG, ISIG}, {"ICANON", ICANON, ICANON},
- {"XCASE", XCASE, XCASE}, {"ECHO", ECHO, ECHO},
- {"ECHOE", ECHOE, ECHOE}, {"ECHOK", ECHOK, ECHOK},
- {"ECHONL", ECHONL, ECHONL}, {"NOFLSH", NOFLSH, NOFLSH},
- {"TOSTOP", TOSTOP, TOSTOP}, {"ECHOCTL", ECHOCTL, ECHOCTL},
- {"ECHOPRT", ECHOPRT, ECHOPRT}, {"ECHOKE", ECHOKE, ECHOKE},
- {"FLUSHO", FLUSHO, FLUSHO}, {"PENDIN", PENDIN, PENDIN},
- {"IEXTEN", IEXTEN, IEXTEN}, {"EXTPROC", EXTPROC, EXTPROC},
-};
-
-std::string FormatCC(char c) {
- if (isgraph(c)) {
- return std::string(1, c);
- } else if (c == ' ') {
- return " ";
- } else if (c == '\t') {
- return "\\t";
- } else if (c == '\r') {
- return "\\r";
- } else if (c == '\n') {
- return "\\n";
- } else if (c == '\0') {
- return "\\0";
- } else if (IsControlCharacter(c)) {
- return absl::StrCat("^", std::string(1, FromControlCharacter(c)));
- }
- return absl::StrCat("\\x", absl::Hex(c));
-}
-
-std::ostream& operator<<(std::ostream& os, struct kernel_termios const& a) {
- os << "{ c_iflag = "
- << ParseFields(kIflagFields, ABSL_ARRAYSIZE(kIflagFields), a.c_iflag);
- os << ", c_oflag = "
- << ParseFields(kOflagFields, ABSL_ARRAYSIZE(kOflagFields), a.c_oflag);
- os << ", c_cflag = "
- << ParseFields(kCflagFields, ABSL_ARRAYSIZE(kCflagFields), a.c_cflag);
- os << ", c_lflag = "
- << ParseFields(kLflagFields, ABSL_ARRAYSIZE(kLflagFields), a.c_lflag);
- os << ", c_line = " << a.c_line;
- os << ", c_cc = { [VINTR] = '" << FormatCC(a.c_cc[VINTR]);
- os << "', [VQUIT] = '" << FormatCC(a.c_cc[VQUIT]);
- os << "', [VERASE] = '" << FormatCC(a.c_cc[VERASE]);
- os << "', [VKILL] = '" << FormatCC(a.c_cc[VKILL]);
- os << "', [VEOF] = '" << FormatCC(a.c_cc[VEOF]);
- os << "', [VTIME] = '" << static_cast<int>(a.c_cc[VTIME]);
- os << "', [VMIN] = " << static_cast<int>(a.c_cc[VMIN]);
- os << ", [VSWTC] = '" << FormatCC(a.c_cc[VSWTC]);
- os << "', [VSTART] = '" << FormatCC(a.c_cc[VSTART]);
- os << "', [VSTOP] = '" << FormatCC(a.c_cc[VSTOP]);
- os << "', [VSUSP] = '" << FormatCC(a.c_cc[VSUSP]);
- os << "', [VEOL] = '" << FormatCC(a.c_cc[VEOL]);
- os << "', [VREPRINT] = '" << FormatCC(a.c_cc[VREPRINT]);
- os << "', [VDISCARD] = '" << FormatCC(a.c_cc[VDISCARD]);
- os << "', [VWERASE] = '" << FormatCC(a.c_cc[VWERASE]);
- os << "', [VLNEXT] = '" << FormatCC(a.c_cc[VLNEXT]);
- os << "', [VEOL2] = '" << FormatCC(a.c_cc[VEOL2]);
- os << "'}";
- return os;
-}
-
-// Return the default termios settings for a new terminal.
-struct kernel_termios DefaultTermios() {
- struct kernel_termios t = {};
- t.c_iflag = IXON | ICRNL;
- t.c_oflag = OPOST | ONLCR;
- t.c_cflag = B38400 | CSIZE | CS8 | CREAD;
- t.c_lflag = ISIG | ICANON | ECHO | ECHOE | ECHOK | ECHOCTL | ECHOKE | IEXTEN;
- t.c_line = 0;
- t.c_cc[VINTR] = ControlCharacter('C');
- t.c_cc[VQUIT] = ControlCharacter('\\');
- t.c_cc[VERASE] = '\x7f';
- t.c_cc[VKILL] = ControlCharacter('U');
- t.c_cc[VEOF] = ControlCharacter('D');
- t.c_cc[VTIME] = '\0';
- t.c_cc[VMIN] = 1;
- t.c_cc[VSWTC] = '\0';
- t.c_cc[VSTART] = ControlCharacter('Q');
- t.c_cc[VSTOP] = ControlCharacter('S');
- t.c_cc[VSUSP] = ControlCharacter('Z');
- t.c_cc[VEOL] = '\0';
- t.c_cc[VREPRINT] = ControlCharacter('R');
- t.c_cc[VDISCARD] = ControlCharacter('O');
- t.c_cc[VWERASE] = ControlCharacter('W');
- t.c_cc[VLNEXT] = ControlCharacter('V');
- t.c_cc[VEOL2] = '\0';
- return t;
-}
-
-// PollAndReadFd tries to read count bytes from buf within timeout.
-//
-// Returns a partial read if some bytes were read.
-//
-// fd must be non-blocking.
-PosixErrorOr<size_t> PollAndReadFd(int fd, void* buf, size_t count,
- absl::Duration timeout) {
- absl::Time end = absl::Now() + timeout;
-
- size_t completed = 0;
- absl::Duration remaining;
- while ((remaining = end - absl::Now()) > absl::ZeroDuration()) {
- struct pollfd pfd = {fd, POLLIN, 0};
- int ret = RetryEINTR(poll)(&pfd, 1, absl::ToInt64Milliseconds(remaining));
- if (ret < 0) {
- return PosixError(errno, "poll failed");
- } else if (ret == 0) {
- // Timed out.
- continue;
- } else if (ret != 1) {
- return PosixError(EINVAL, absl::StrCat("Bad poll ret ", ret));
- }
-
- ssize_t n =
- ReadFd(fd, static_cast<char*>(buf) + completed, count - completed);
- if (n < 0) {
- if (errno == EAGAIN) {
- // Linux sometimes returns EAGAIN from this read, despite the fact that
- // poll returned success. Let's just do what do as we are told and try
- // again.
- continue;
- }
- return PosixError(errno, "read failed");
- }
- completed += n;
- if (completed >= count) {
- return completed;
- }
- }
-
- if (completed) {
- return completed;
- }
- return PosixError(ETIMEDOUT, "Poll timed out");
-}
-
-TEST(PtyTrunc, Truncate) {
- // Opening PTYs with O_TRUNC shouldn't cause an error, but calls to
- // (f)truncate should.
- FileDescriptor main =
- ASSERT_NO_ERRNO_AND_VALUE(Open(kMainPath, O_RDWR | O_TRUNC));
- int n = ASSERT_NO_ERRNO_AND_VALUE(ReplicaID(main));
- std::string spath = absl::StrCat("/dev/pts/", n);
- FileDescriptor replica =
- ASSERT_NO_ERRNO_AND_VALUE(Open(spath, O_RDWR | O_NONBLOCK | O_TRUNC));
-
- EXPECT_THAT(truncate(kMainPath, 0), SyscallFailsWithErrno(EINVAL));
- EXPECT_THAT(truncate(spath.c_str(), 0), SyscallFailsWithErrno(EINVAL));
- EXPECT_THAT(ftruncate(main.get(), 0), SyscallFailsWithErrno(EINVAL));
- EXPECT_THAT(ftruncate(replica.get(), 0), SyscallFailsWithErrno(EINVAL));
-}
-
-TEST(BasicPtyTest, StatUnopenedMain) {
- struct stat s;
- ASSERT_THAT(stat(kMainPath, &s), SyscallSucceeds());
-
- EXPECT_EQ(s.st_rdev, makedev(TTYAUX_MAJOR, kPtmxMinor));
- EXPECT_EQ(s.st_size, 0);
- EXPECT_EQ(s.st_blocks, 0);
-
- // ptmx attached to a specific devpts mount uses block size 1024. See
- // fs/devpts/inode.c:devpts_fill_super.
- //
- // The global ptmx device uses the block size of the filesystem it is created
- // on (which is usually 4096 for disk filesystems).
- EXPECT_THAT(s.st_blksize, AnyOf(Eq(1024), Eq(4096)));
-}
-
-// Waits for count bytes to be readable from fd. Unlike poll, which can return
-// before all data is moved into a pty's read buffer, this function waits for
-// all count bytes to become readable.
-PosixErrorOr<int> WaitUntilReceived(int fd, int count) {
- int buffered = -1;
- absl::Duration remaining;
- absl::Time end = absl::Now() + kTimeout;
- while ((remaining = end - absl::Now()) > absl::ZeroDuration()) {
- if (ioctl(fd, FIONREAD, &buffered) < 0) {
- return PosixError(errno, "failed FIONREAD ioctl");
- }
- if (buffered >= count) {
- return buffered;
- }
- absl::SleepFor(absl::Milliseconds(500));
- }
- return PosixError(
- ETIMEDOUT,
- absl::StrFormat(
- "FIONREAD timed out, receiving only %d of %d expected bytes",
- buffered, count));
-}
-
-// Verifies that there is nothing left to read from fd.
-void ExpectFinished(const FileDescriptor& fd) {
- // Nothing more to read.
- char c;
- EXPECT_THAT(ReadFd(fd.get(), &c, 1), SyscallFailsWithErrno(EAGAIN));
-}
-
-// Verifies that we can read expected bytes from fd into buf.
-void ExpectReadable(const FileDescriptor& fd, int expected, char* buf) {
- size_t n = ASSERT_NO_ERRNO_AND_VALUE(
- PollAndReadFd(fd.get(), buf, expected, kTimeout));
- EXPECT_EQ(expected, n);
-}
-
-TEST(BasicPtyTest, OpenMainReplica) {
- FileDescriptor main = ASSERT_NO_ERRNO_AND_VALUE(Open("/dev/ptmx", O_RDWR));
- FileDescriptor replica = ASSERT_NO_ERRNO_AND_VALUE(OpenReplica(main));
-}
-
-// The replica entry in /dev/pts/ disappears when the main is closed, even if
-// the replica is still open.
-TEST(BasicPtyTest, ReplicaEntryGoneAfterMainClose) {
- FileDescriptor main = ASSERT_NO_ERRNO_AND_VALUE(Open("/dev/ptmx", O_RDWR));
- FileDescriptor replica = ASSERT_NO_ERRNO_AND_VALUE(OpenReplica(main));
-
- // Get pty index.
- int index = -1;
- ASSERT_THAT(ioctl(main.get(), TIOCGPTN, &index), SyscallSucceeds());
-
- std::string path = absl::StrCat("/dev/pts/", index);
-
- struct stat st;
- EXPECT_THAT(stat(path.c_str(), &st), SyscallSucceeds());
-
- main.reset();
-
- EXPECT_THAT(stat(path.c_str(), &st), SyscallFailsWithErrno(ENOENT));
-}
-
-TEST(BasicPtyTest, Getdents) {
- FileDescriptor main1 = ASSERT_NO_ERRNO_AND_VALUE(Open("/dev/ptmx", O_RDWR));
- int index1 = -1;
- ASSERT_THAT(ioctl(main1.get(), TIOCGPTN, &index1), SyscallSucceeds());
- FileDescriptor replica1 = ASSERT_NO_ERRNO_AND_VALUE(OpenReplica(main1));
-
- FileDescriptor main2 = ASSERT_NO_ERRNO_AND_VALUE(Open("/dev/ptmx", O_RDWR));
- int index2 = -1;
- ASSERT_THAT(ioctl(main2.get(), TIOCGPTN, &index2), SyscallSucceeds());
- FileDescriptor replica2 = ASSERT_NO_ERRNO_AND_VALUE(OpenReplica(main2));
-
- // The directory contains ptmx, index1, and index2. (Plus any additional PTYs
- // unrelated to this test.)
-
- std::vector<std::string> contents =
- ASSERT_NO_ERRNO_AND_VALUE(ListDir("/dev/pts/", true));
- EXPECT_THAT(contents, Contains(absl::StrCat(index1)));
- EXPECT_THAT(contents, Contains(absl::StrCat(index2)));
-
- main2.reset();
-
- // The directory contains ptmx and index1, but not index2 since the main is
- // closed. (Plus any additional PTYs unrelated to this test.)
-
- contents = ASSERT_NO_ERRNO_AND_VALUE(ListDir("/dev/pts/", true));
- EXPECT_THAT(contents, Contains(absl::StrCat(index1)));
- EXPECT_THAT(contents, Not(Contains(absl::StrCat(index2))));
-
- // N.B. devpts supports legacy "single-instance" mode and new "multi-instance"
- // mode. In legacy mode, devpts does not contain a "ptmx" device (the distro
- // must use mknod to create it somewhere, presumably /dev/ptmx).
- // Multi-instance mode does include a "ptmx" device tied to that mount.
- //
- // We don't check for the presence or absence of "ptmx", as distros vary in
- // their usage of the two modes.
-}
-
-class PtyTest : public ::testing::Test {
- protected:
- void SetUp() override {
- main_ = ASSERT_NO_ERRNO_AND_VALUE(Open("/dev/ptmx", O_RDWR | O_NONBLOCK));
- replica_ = ASSERT_NO_ERRNO_AND_VALUE(OpenReplica(main_));
- }
-
- void DisableCanonical() {
- struct kernel_termios t = {};
- EXPECT_THAT(ioctl(replica_.get(), TCGETS, &t), SyscallSucceeds());
- t.c_lflag &= ~ICANON;
- EXPECT_THAT(ioctl(replica_.get(), TCSETS, &t), SyscallSucceeds());
- }
-
- void EnableCanonical() {
- struct kernel_termios t = {};
- EXPECT_THAT(ioctl(replica_.get(), TCGETS, &t), SyscallSucceeds());
- t.c_lflag |= ICANON;
- EXPECT_THAT(ioctl(replica_.get(), TCSETS, &t), SyscallSucceeds());
- }
-
- // Main and replica ends of the PTY. Non-blocking.
- FileDescriptor main_;
- FileDescriptor replica_;
-};
-
-// Main to replica sanity test.
-TEST_F(PtyTest, WriteMainToReplica) {
- // N.B. by default, the replica reads nothing until the main writes a newline.
- constexpr char kBuf[] = "hello\n";
-
- EXPECT_THAT(WriteFd(main_.get(), kBuf, sizeof(kBuf) - 1),
- SyscallSucceedsWithValue(sizeof(kBuf) - 1));
-
- // Linux moves data from the main to the replica via async work scheduled via
- // tty_flip_buffer_push. Since it is asynchronous, the data may not be
- // available for reading immediately. Instead we must poll and assert that it
- // becomes available "soon".
-
- char buf[sizeof(kBuf)] = {};
- ExpectReadable(replica_, sizeof(buf) - 1, buf);
-
- EXPECT_EQ(memcmp(buf, kBuf, sizeof(kBuf)), 0);
-}
-
-// Replica to main sanity test.
-TEST_F(PtyTest, WriteReplicaToMain) {
- // N.B. by default, the main reads nothing until the replica writes a newline,
- // and the main 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));
-
- // Linux moves data from the main to the replica via async work scheduled via
- // tty_flip_buffer_push. Since it is asynchronous, the data may not be
- // available for reading immediately. Instead we must poll and assert that it
- // becomes available "soon".
-
- char buf[sizeof(kExpected)] = {};
- ExpectReadable(main_, sizeof(buf) - 1, buf);
-
- EXPECT_EQ(memcmp(buf, kExpected, sizeof(kExpected)), 0);
-}
-
-TEST_F(PtyTest, WriteInvalidUTF8) {
- char c = 0xff;
- ASSERT_THAT(syscall(__NR_write, main_.get(), &c, sizeof(c)),
- SyscallSucceedsWithValue(sizeof(c)));
-}
-
-// Both the main and replica report the standard default termios settings.
-//
-// Note that TCGETS on the main actually redirects to the replica (see comment
-// on MainTermiosUnchangable).
-TEST_F(PtyTest, DefaultTermios) {
- struct kernel_termios t = {};
- EXPECT_THAT(ioctl(replica_.get(), TCGETS, &t), SyscallSucceeds());
- EXPECT_EQ(t, DefaultTermios());
-
- EXPECT_THAT(ioctl(main_.get(), TCGETS, &t), SyscallSucceeds());
- EXPECT_EQ(t, DefaultTermios());
-}
-
-// Changing termios from the main actually affects the replica.
-//
-// TCSETS on the main actually redirects to the replica (see comment on
-// MainTermiosUnchangable).
-TEST_F(PtyTest, TermiosAffectsReplica) {
- struct kernel_termios main_termios = {};
- EXPECT_THAT(ioctl(main_.get(), TCGETS, &main_termios), SyscallSucceeds());
- main_termios.c_lflag ^= ICANON;
- EXPECT_THAT(ioctl(main_.get(), TCSETS, &main_termios), SyscallSucceeds());
-
- struct kernel_termios replica_termios = {};
- EXPECT_THAT(ioctl(replica_.get(), TCGETS, &replica_termios),
- SyscallSucceeds());
- EXPECT_EQ(main_termios, replica_termios);
-}
-
-// The main end of the pty has termios:
-//
-// struct kernel_termios t = {
-// .c_iflag = 0;
-// .c_oflag = 0;
-// .c_cflag = B38400 | CS8 | CREAD;
-// .c_lflag = 0;
-// .c_cc = /* same as DefaultTermios */
-// }
-//
-// (From drivers/tty/pty.c:unix98_pty_init)
-//
-// All termios control ioctls on the main actually redirect to the replica
-// (drivers/tty/tty_ioctl.c:tty_mode_ioctl), making it impossible to change the
-// main termios.
-//
-// Verify this by setting ICRNL (which rewrites input \r to \n) and verify that
-// it has no effect on the main.
-TEST_F(PtyTest, MainTermiosUnchangable) {
- struct kernel_termios main_termios = {};
- EXPECT_THAT(ioctl(main_.get(), TCGETS, &main_termios), SyscallSucceeds());
- main_termios.c_lflag |= ICRNL;
- EXPECT_THAT(ioctl(main_.get(), TCSETS, &main_termios), SyscallSucceeds());
-
- char c = '\r';
- ASSERT_THAT(WriteFd(replica_.get(), &c, 1), SyscallSucceedsWithValue(1));
-
- ExpectReadable(main_, 1, &c);
- EXPECT_EQ(c, '\r'); // ICRNL had no effect!
-
- ExpectFinished(main_);
-}
-
-// ICRNL rewrites input \r to \n.
-TEST_F(PtyTest, TermiosICRNL) {
- struct kernel_termios t = DefaultTermios();
- t.c_iflag |= ICRNL;
- t.c_lflag &= ~ICANON; // for byte-by-byte reading.
- ASSERT_THAT(ioctl(replica_.get(), TCSETS, &t), SyscallSucceeds());
-
- char c = '\r';
- ASSERT_THAT(WriteFd(main_.get(), &c, 1), SyscallSucceedsWithValue(1));
-
- ExpectReadable(replica_, 1, &c);
- EXPECT_EQ(c, '\n');
-
- ExpectFinished(replica_);
-}
-
-// ONLCR rewrites output \n to \r\n.
-TEST_F(PtyTest, TermiosONLCR) {
- struct kernel_termios t = DefaultTermios();
- t.c_oflag |= ONLCR;
- t.c_lflag &= ~ICANON; // for byte-by-byte reading.
- ASSERT_THAT(ioctl(replica_.get(), TCSETS, &t), SyscallSucceeds());
-
- char c = '\n';
- ASSERT_THAT(WriteFd(replica_.get(), &c, 1), SyscallSucceedsWithValue(1));
-
- // Extra byte for NUL for EXPECT_STREQ.
- char buf[3] = {};
- ExpectReadable(main_, 2, buf);
- EXPECT_STREQ(buf, "\r\n");
-
- ExpectFinished(replica_);
-}
-
-TEST_F(PtyTest, TermiosIGNCR) {
- struct kernel_termios t = DefaultTermios();
- t.c_iflag |= IGNCR;
- t.c_lflag &= ~ICANON; // for byte-by-byte reading.
- ASSERT_THAT(ioctl(replica_.get(), TCSETS, &t), SyscallSucceeds());
-
- char c = '\r';
- ASSERT_THAT(WriteFd(main_.get(), &c, 1), SyscallSucceedsWithValue(1));
-
- // Nothing to read.
- ASSERT_THAT(PollAndReadFd(replica_.get(), &c, 1, kTimeout),
- PosixErrorIs(ETIMEDOUT, ::testing::StrEq("Poll timed out")));
-}
-
-// Test that we can successfully poll for readable data from the replica.
-TEST_F(PtyTest, TermiosPollReplica) {
- struct kernel_termios t = DefaultTermios();
- t.c_iflag |= IGNCR;
- t.c_lflag &= ~ICANON; // for byte-by-byte reading.
- ASSERT_THAT(ioctl(replica_.get(), TCSETS, &t), SyscallSucceeds());
-
- absl::Notification notify;
- int sfd = replica_.get();
- ScopedThread th([sfd, &notify]() {
- notify.Notify();
-
- // Poll on the reader fd with POLLIN event.
- struct pollfd poll_fd = {sfd, POLLIN, 0};
- EXPECT_THAT(
- RetryEINTR(poll)(&poll_fd, 1, absl::ToInt64Milliseconds(kTimeout)),
- SyscallSucceedsWithValue(1));
-
- // Should trigger POLLIN event.
- EXPECT_EQ(poll_fd.revents & POLLIN, POLLIN);
- });
-
- notify.WaitForNotification();
- // Sleep ensures that poll begins waiting before we write to the FD.
- absl::SleepFor(absl::Seconds(1));
-
- char s[] = "foo\n";
- ASSERT_THAT(WriteFd(main_.get(), s, strlen(s) + 1), SyscallSucceeds());
-}
-
-// Test that we can successfully poll for readable data from the main.
-TEST_F(PtyTest, TermiosPollMain) {
- struct kernel_termios t = DefaultTermios();
- t.c_iflag |= IGNCR;
- t.c_lflag &= ~ICANON; // for byte-by-byte reading.
- ASSERT_THAT(ioctl(main_.get(), TCSETS, &t), SyscallSucceeds());
-
- absl::Notification notify;
- int mfd = main_.get();
- ScopedThread th([mfd, &notify]() {
- notify.Notify();
-
- // Poll on the reader fd with POLLIN event.
- struct pollfd poll_fd = {mfd, POLLIN, 0};
- EXPECT_THAT(
- RetryEINTR(poll)(&poll_fd, 1, absl::ToInt64Milliseconds(kTimeout)),
- SyscallSucceedsWithValue(1));
-
- // Should trigger POLLIN event.
- EXPECT_EQ(poll_fd.revents & POLLIN, POLLIN);
- });
-
- notify.WaitForNotification();
- // Sleep ensures that poll begins waiting before we write to the FD.
- absl::SleepFor(absl::Seconds(1));
-
- char s[] = "foo\n";
- ASSERT_THAT(WriteFd(replica_.get(), s, strlen(s) + 1), SyscallSucceeds());
-}
-
-TEST_F(PtyTest, TermiosINLCR) {
- struct kernel_termios t = DefaultTermios();
- t.c_iflag |= INLCR;
- t.c_lflag &= ~ICANON; // for byte-by-byte reading.
- ASSERT_THAT(ioctl(replica_.get(), TCSETS, &t), SyscallSucceeds());
-
- char c = '\n';
- ASSERT_THAT(WriteFd(main_.get(), &c, 1), SyscallSucceedsWithValue(1));
-
- ExpectReadable(replica_, 1, &c);
- EXPECT_EQ(c, '\r');
-
- ExpectFinished(replica_);
-}
-
-TEST_F(PtyTest, TermiosONOCR) {
- struct kernel_termios t = DefaultTermios();
- t.c_oflag |= ONOCR;
- t.c_lflag &= ~ICANON; // for byte-by-byte reading.
- ASSERT_THAT(ioctl(replica_.get(), TCSETS, &t), SyscallSucceeds());
-
- // The terminal is at column 0, so there should be no CR to read.
- char c = '\r';
- ASSERT_THAT(WriteFd(replica_.get(), &c, 1), SyscallSucceedsWithValue(1));
-
- // Nothing to read.
- ASSERT_THAT(PollAndReadFd(main_.get(), &c, 1, kTimeout),
- PosixErrorIs(ETIMEDOUT, ::testing::StrEq("Poll timed out")));
-
- // This time the column is greater than 0, so we should be able to read the CR
- // out of the other end.
- constexpr char kInput[] = "foo\r";
- constexpr int kInputSize = sizeof(kInput) - 1;
- ASSERT_THAT(WriteFd(replica_.get(), kInput, kInputSize),
- SyscallSucceedsWithValue(kInputSize));
-
- char buf[kInputSize] = {};
- ExpectReadable(main_, kInputSize, buf);
-
- EXPECT_EQ(memcmp(buf, kInput, kInputSize), 0);
-
- ExpectFinished(main_);
-
- // Terminal should be at column 0 again, so no CR can be read.
- ASSERT_THAT(WriteFd(replica_.get(), &c, 1), SyscallSucceedsWithValue(1));
-
- // Nothing to read.
- ASSERT_THAT(PollAndReadFd(main_.get(), &c, 1, kTimeout),
- PosixErrorIs(ETIMEDOUT, ::testing::StrEq("Poll timed out")));
-}
-
-TEST_F(PtyTest, TermiosOCRNL) {
- struct kernel_termios t = DefaultTermios();
- t.c_oflag |= OCRNL;
- t.c_lflag &= ~ICANON; // for byte-by-byte reading.
- ASSERT_THAT(ioctl(replica_.get(), TCSETS, &t), SyscallSucceeds());
-
- // The terminal is at column 0, so there should be no CR to read.
- char c = '\r';
- ASSERT_THAT(WriteFd(replica_.get(), &c, 1), SyscallSucceedsWithValue(1));
-
- ExpectReadable(main_, 1, &c);
- EXPECT_EQ(c, '\n');
-
- ExpectFinished(main_);
-}
-
-// Tests that VEOL is disabled when we start, and that we can set it to enable
-// it.
-TEST_F(PtyTest, VEOLTermination) {
- // Write a few bytes ending with '\0', and confirm that we can't read.
- constexpr char kInput[] = "hello";
- ASSERT_THAT(WriteFd(main_.get(), kInput, sizeof(kInput)),
- SyscallSucceedsWithValue(sizeof(kInput)));
- char buf[sizeof(kInput)] = {};
- ASSERT_THAT(PollAndReadFd(replica_.get(), buf, sizeof(kInput), kTimeout),
- PosixErrorIs(ETIMEDOUT, ::testing::StrEq("Poll timed out")));
-
- // Set the EOL character to '=' and write it.
- constexpr char delim = '=';
- struct kernel_termios t = DefaultTermios();
- t.c_cc[VEOL] = delim;
- ASSERT_THAT(ioctl(replica_.get(), TCSETS, &t), SyscallSucceeds());
- ASSERT_THAT(WriteFd(main_.get(), &delim, 1), SyscallSucceedsWithValue(1));
-
- // Now we can read, as sending EOL caused the line to become available.
- ExpectReadable(replica_, sizeof(kInput), buf);
- EXPECT_EQ(memcmp(buf, kInput, sizeof(kInput)), 0);
-
- ExpectReadable(replica_, 1, buf);
- EXPECT_EQ(buf[0], '=');
-
- ExpectFinished(replica_);
-}
-
-// Tests that we can write more than the 4096 character limit, then a
-// terminating character, then read out just the first 4095 bytes plus the
-// terminator.
-TEST_F(PtyTest, CanonBigWrite) {
- constexpr int kWriteLen = kMaxLineSize + 4;
- char input[kWriteLen];
- memset(input, 'M', kWriteLen - 1);
- input[kWriteLen - 1] = '\n';
- ASSERT_THAT(WriteFd(main_.get(), input, kWriteLen),
- SyscallSucceedsWithValue(kWriteLen));
-
- // We can read the line.
- char buf[kMaxLineSize] = {};
- ExpectReadable(replica_, kMaxLineSize, buf);
-
- ExpectFinished(replica_);
-}
-
-// Tests that data written in canonical mode can be read immediately once
-// switched to noncanonical mode.
-TEST_F(PtyTest, SwitchCanonToNoncanon) {
- // Write a few bytes without a terminating character, switch to noncanonical
- // mode, and read them.
- constexpr char kInput[] = "hello";
- ASSERT_THAT(WriteFd(main_.get(), kInput, sizeof(kInput)),
- SyscallSucceedsWithValue(sizeof(kInput)));
-
- // Nothing available yet.
- char buf[sizeof(kInput)] = {};
- ASSERT_THAT(PollAndReadFd(replica_.get(), buf, sizeof(kInput), kTimeout),
- PosixErrorIs(ETIMEDOUT, ::testing::StrEq("Poll timed out")));
-
- DisableCanonical();
-
- ExpectReadable(replica_, sizeof(kInput), buf);
- EXPECT_STREQ(buf, kInput);
-
- ExpectFinished(replica_);
-}
-
-TEST_F(PtyTest, SwitchCanonToNonCanonNewline) {
- // Write a few bytes with a terminating character.
- constexpr char kInput[] = "hello\n";
- ASSERT_THAT(WriteFd(main_.get(), kInput, sizeof(kInput)),
- SyscallSucceedsWithValue(sizeof(kInput)));
-
- DisableCanonical();
-
- // We can read the line.
- char buf[sizeof(kInput)] = {};
- ExpectReadable(replica_, sizeof(kInput), buf);
- EXPECT_STREQ(buf, kInput);
-
- ExpectFinished(replica_);
-}
-
-TEST_F(PtyTest, SwitchNoncanonToCanonNewlineBig) {
- DisableCanonical();
-
- // Write more than the maximum line size, then write a delimiter.
- constexpr int kWriteLen = 4100;
- char input[kWriteLen];
- memset(input, 'M', kWriteLen);
- ASSERT_THAT(WriteFd(main_.get(), input, kWriteLen),
- SyscallSucceedsWithValue(kWriteLen));
- // Wait for the input queue to fill.
- ASSERT_NO_ERRNO(WaitUntilReceived(replica_.get(), kMaxLineSize - 1));
- constexpr char delim = '\n';
- ASSERT_THAT(WriteFd(main_.get(), &delim, 1), SyscallSucceedsWithValue(1));
-
- EnableCanonical();
-
- // We can read the line.
- char buf[kMaxLineSize] = {};
- ExpectReadable(replica_, kMaxLineSize - 1, buf);
-
- // We can also read the remaining characters.
- ExpectReadable(replica_, 6, buf);
-
- ExpectFinished(replica_);
-}
-
-TEST_F(PtyTest, SwitchNoncanonToCanonNoNewline) {
- DisableCanonical();
-
- // Write a few bytes without a terminating character.
- // mode, and read them.
- constexpr char kInput[] = "hello";
- ASSERT_THAT(WriteFd(main_.get(), kInput, sizeof(kInput) - 1),
- SyscallSucceedsWithValue(sizeof(kInput) - 1));
-
- ASSERT_NO_ERRNO(WaitUntilReceived(replica_.get(), sizeof(kInput) - 1));
- EnableCanonical();
-
- // We can read the line.
- char buf[sizeof(kInput)] = {};
- ExpectReadable(replica_, sizeof(kInput) - 1, buf);
- EXPECT_STREQ(buf, kInput);
-
- ExpectFinished(replica_);
-}
-
-TEST_F(PtyTest, SwitchNoncanonToCanonNoNewlineBig) {
- DisableCanonical();
-
- // Write a few bytes without a terminating character.
- // mode, and read them.
- constexpr int kWriteLen = 4100;
- char input[kWriteLen];
- memset(input, 'M', kWriteLen);
- ASSERT_THAT(WriteFd(main_.get(), input, kWriteLen),
- SyscallSucceedsWithValue(kWriteLen));
-
- ASSERT_NO_ERRNO(WaitUntilReceived(replica_.get(), kMaxLineSize - 1));
- EnableCanonical();
-
- // We can read the line.
- char buf[kMaxLineSize] = {};
- ExpectReadable(replica_, kMaxLineSize - 1, buf);
-
- ExpectFinished(replica_);
-}
-
-// Tests that we can write over the 4095 noncanonical limit, then read out
-// everything.
-TEST_F(PtyTest, NoncanonBigWrite) {
- DisableCanonical();
-
- // Write well over the 4095 internal buffer limit.
- constexpr char kInput = 'M';
- constexpr int kInputSize = kMaxLineSize * 2;
- for (int i = 0; i < kInputSize; i++) {
- // This makes too many syscalls for save/restore.
- const DisableSave ds;
- ASSERT_THAT(WriteFd(main_.get(), &kInput, sizeof(kInput)),
- SyscallSucceedsWithValue(sizeof(kInput)));
- }
-
- // We should be able to read out everything. Sleep a bit so that Linux has a
- // chance to move data from the main to the replica.
- ASSERT_NO_ERRNO(WaitUntilReceived(replica_.get(), kMaxLineSize - 1));
- for (int i = 0; i < kInputSize; i++) {
- // This makes too many syscalls for save/restore.
- const DisableSave ds;
- char c;
- ExpectReadable(replica_, 1, &c);
- ASSERT_EQ(c, kInput);
- }
-
- ExpectFinished(replica_);
-}
-
-// ICANON doesn't make input available until a line delimiter is typed.
-//
-// Test newline.
-TEST_F(PtyTest, TermiosICANONNewline) {
- char input[3] = {'a', 'b', 'c'};
- ASSERT_THAT(WriteFd(main_.get(), input, sizeof(input)),
- SyscallSucceedsWithValue(sizeof(input)));
-
- // Extra bytes for newline (written later) and NUL for EXPECT_STREQ.
- char buf[5] = {};
-
- // Nothing available yet.
- ASSERT_THAT(PollAndReadFd(replica_.get(), buf, sizeof(input), kTimeout),
- PosixErrorIs(ETIMEDOUT, ::testing::StrEq("Poll timed out")));
-
- char delim = '\n';
- ASSERT_THAT(WriteFd(main_.get(), &delim, 1), SyscallSucceedsWithValue(1));
-
- // Now it is available.
- ASSERT_NO_ERRNO(WaitUntilReceived(replica_.get(), sizeof(input) + 1));
- ExpectReadable(replica_, sizeof(input) + 1, buf);
- EXPECT_STREQ(buf, "abc\n");
-
- ExpectFinished(replica_);
-}
-
-// ICANON doesn't make input available until a line delimiter is typed.
-//
-// Test EOF (^D).
-TEST_F(PtyTest, TermiosICANONEOF) {
- char input[3] = {'a', 'b', 'c'};
- ASSERT_THAT(WriteFd(main_.get(), input, sizeof(input)),
- SyscallSucceedsWithValue(sizeof(input)));
-
- // Extra byte for NUL for EXPECT_STREQ.
- char buf[4] = {};
-
- // Nothing available yet.
- ASSERT_THAT(PollAndReadFd(replica_.get(), buf, sizeof(input), kTimeout),
- PosixErrorIs(ETIMEDOUT, ::testing::StrEq("Poll timed out")));
- char delim = ControlCharacter('D');
- ASSERT_THAT(WriteFd(main_.get(), &delim, 1), SyscallSucceedsWithValue(1));
-
- // Now it is available. Note that ^D is not included.
- ExpectReadable(replica_, sizeof(input), buf);
- EXPECT_STREQ(buf, "abc");
-
- ExpectFinished(replica_);
-}
-
-// ICANON limits us to 4096 bytes including a terminating character. Anything
-// after and 4095th character is discarded (although still processed for
-// signals and echoing).
-TEST_F(PtyTest, CanonDiscard) {
- constexpr char kInput = 'M';
- constexpr int kInputSize = 4100;
- constexpr int kIter = 3;
-
- // A few times write more than the 4096 character maximum, then a newline.
- constexpr char delim = '\n';
- for (int i = 0; i < kIter; i++) {
- // This makes too many syscalls for save/restore.
- const DisableSave ds;
- for (int i = 0; i < kInputSize; i++) {
- ASSERT_THAT(WriteFd(main_.get(), &kInput, sizeof(kInput)),
- SyscallSucceedsWithValue(sizeof(kInput)));
- }
- ASSERT_THAT(WriteFd(main_.get(), &delim, 1), SyscallSucceedsWithValue(1));
- }
-
- // There should be multiple truncated lines available to read.
- for (int i = 0; i < kIter; i++) {
- char buf[kInputSize] = {};
- ExpectReadable(replica_, kMaxLineSize, buf);
- EXPECT_EQ(buf[kMaxLineSize - 1], delim);
- EXPECT_EQ(buf[kMaxLineSize - 2], kInput);
- }
-
- ExpectFinished(replica_);
-}
-
-TEST_F(PtyTest, CanonMultiline) {
- constexpr char kInput1[] = "GO\n";
- constexpr char kInput2[] = "BLUE\n";
-
- // Write both lines.
- ASSERT_THAT(WriteFd(main_.get(), kInput1, sizeof(kInput1) - 1),
- SyscallSucceedsWithValue(sizeof(kInput1) - 1));
- ASSERT_THAT(WriteFd(main_.get(), kInput2, sizeof(kInput2) - 1),
- SyscallSucceedsWithValue(sizeof(kInput2) - 1));
-
- // Get the first line.
- char line1[8] = {};
- ExpectReadable(replica_, sizeof(kInput1) - 1, line1);
- EXPECT_STREQ(line1, kInput1);
-
- // Get the second line.
- char line2[8] = {};
- ExpectReadable(replica_, sizeof(kInput2) - 1, line2);
- EXPECT_STREQ(line2, kInput2);
-
- ExpectFinished(replica_);
-}
-
-TEST_F(PtyTest, SwitchNoncanonToCanonMultiline) {
- DisableCanonical();
-
- constexpr char kInput1[] = "GO\n";
- constexpr char kInput2[] = "BLUE\n";
- constexpr char kExpected[] = "GO\nBLUE\n";
-
- // Write both lines.
- ASSERT_THAT(WriteFd(main_.get(), kInput1, sizeof(kInput1) - 1),
- SyscallSucceedsWithValue(sizeof(kInput1) - 1));
- ASSERT_THAT(WriteFd(main_.get(), kInput2, sizeof(kInput2) - 1),
- SyscallSucceedsWithValue(sizeof(kInput2) - 1));
-
- ASSERT_NO_ERRNO(
- WaitUntilReceived(replica_.get(), sizeof(kInput1) + sizeof(kInput2) - 2));
- EnableCanonical();
-
- // Get all together as one line.
- char line[9] = {};
- ExpectReadable(replica_, 8, line);
- EXPECT_STREQ(line, kExpected);
-
- ExpectFinished(replica_);
-}
-
-TEST_F(PtyTest, SwitchTwiceMultiline) {
- std::string kInputs[] = {"GO\n", "BLUE\n", "!"};
- std::string kExpected = "GO\nBLUE\n!";
-
- // Write each line.
- for (const std::string& input : kInputs) {
- ASSERT_THAT(WriteFd(main_.get(), input.c_str(), input.size()),
- SyscallSucceedsWithValue(input.size()));
- }
-
- DisableCanonical();
- // All written characters have to make it into the input queue before
- // canonical mode is re-enabled. If the final '!' character hasn't been
- // enqueued before canonical mode is re-enabled, it won't be readable.
- ASSERT_NO_ERRNO(WaitUntilReceived(replica_.get(), kExpected.size()));
- EnableCanonical();
-
- // Get all together as one line.
- char line[10] = {};
- ExpectReadable(replica_, 9, line);
- EXPECT_STREQ(line, kExpected.c_str());
-
- ExpectFinished(replica_);
-}
-
-TEST_F(PtyTest, QueueSize) {
- // Write the line.
- constexpr char kInput1[] = "GO\n";
- ASSERT_THAT(WriteFd(main_.get(), kInput1, sizeof(kInput1) - 1),
- SyscallSucceedsWithValue(sizeof(kInput1) - 1));
- ASSERT_NO_ERRNO(WaitUntilReceived(replica_.get(), sizeof(kInput1) - 1));
-
- // Ensure that writing more (beyond what is readable) does not impact the
- // readable size.
- char input[kMaxLineSize];
- memset(input, 'M', kMaxLineSize);
- ASSERT_THAT(WriteFd(main_.get(), input, kMaxLineSize),
- SyscallSucceedsWithValue(kMaxLineSize));
- int inputBufSize = ASSERT_NO_ERRNO_AND_VALUE(
- WaitUntilReceived(replica_.get(), sizeof(kInput1) - 1));
- EXPECT_EQ(inputBufSize, sizeof(kInput1) - 1);
-}
-
-TEST_F(PtyTest, PartialBadBuffer) {
- // Allocate 2 pages.
- void* addr = mmap(nullptr, 2 * kPageSize, PROT_READ | PROT_WRITE,
- MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
- ASSERT_NE(addr, MAP_FAILED);
- char* buf = reinterpret_cast<char*>(addr);
-
- // Guard the 2nd page for our read to run into.
- ASSERT_THAT(
- mprotect(reinterpret_cast<void*>(buf + kPageSize), kPageSize, PROT_NONE),
- SyscallSucceeds());
-
- // Leave only one free byte in the buffer.
- char* bad_buffer = buf + kPageSize - 1;
-
- // Write to the main.
- constexpr char kBuf[] = "hello\n";
- constexpr size_t size = sizeof(kBuf) - 1;
- EXPECT_THAT(WriteFd(main_.get(), kBuf, size), SyscallSucceedsWithValue(size));
-
- // Read from the replica into bad_buffer.
- ASSERT_NO_ERRNO(WaitUntilReceived(replica_.get(), size));
- EXPECT_THAT(ReadFd(replica_.get(), bad_buffer, size),
- SyscallFailsWithErrno(EFAULT));
-
- EXPECT_THAT(munmap(addr, 2 * kPageSize), SyscallSucceeds()) << addr;
-}
-
-TEST_F(PtyTest, SimpleEcho) {
- constexpr char kInput[] = "Mr. Eko";
- EXPECT_THAT(WriteFd(main_.get(), kInput, strlen(kInput)),
- SyscallSucceedsWithValue(strlen(kInput)));
-
- char buf[100] = {};
- ExpectReadable(main_, strlen(kInput), buf);
-
- EXPECT_STREQ(buf, kInput);
- ExpectFinished(main_);
-}
-
-TEST_F(PtyTest, GetWindowSize) {
- struct winsize ws;
- ASSERT_THAT(ioctl(replica_.get(), TIOCGWINSZ, &ws), SyscallSucceeds());
- EXPECT_EQ(ws.ws_row, 0);
- EXPECT_EQ(ws.ws_col, 0);
-}
-
-TEST_F(PtyTest, SetReplicaWindowSize) {
- constexpr uint16_t kRows = 343;
- constexpr uint16_t kCols = 2401;
- struct winsize ws = {.ws_row = kRows, .ws_col = kCols};
- ASSERT_THAT(ioctl(replica_.get(), TIOCSWINSZ, &ws), SyscallSucceeds());
-
- struct winsize retrieved_ws = {};
- ASSERT_THAT(ioctl(main_.get(), TIOCGWINSZ, &retrieved_ws), SyscallSucceeds());
- EXPECT_EQ(retrieved_ws.ws_row, kRows);
- EXPECT_EQ(retrieved_ws.ws_col, kCols);
-}
-
-TEST_F(PtyTest, SetMainWindowSize) {
- constexpr uint16_t kRows = 343;
- constexpr uint16_t kCols = 2401;
- struct winsize ws = {.ws_row = kRows, .ws_col = kCols};
- ASSERT_THAT(ioctl(main_.get(), TIOCSWINSZ, &ws), SyscallSucceeds());
-
- struct winsize retrieved_ws = {};
- ASSERT_THAT(ioctl(replica_.get(), TIOCGWINSZ, &retrieved_ws),
- SyscallSucceeds());
- EXPECT_EQ(retrieved_ws.ws_row, kRows);
- EXPECT_EQ(retrieved_ws.ws_col, kCols);
-}
-
-class JobControlTest : public ::testing::Test {
- protected:
- void SetUp() override {
- main_ = ASSERT_NO_ERRNO_AND_VALUE(Open("/dev/ptmx", O_RDWR | O_NONBLOCK));
- replica_ = ASSERT_NO_ERRNO_AND_VALUE(OpenReplica(main_));
-
- // Make this a session leader, which also drops the controlling terminal.
- // In the gVisor test environment, this test will be run as the session
- // leader already (as the sentry init process).
- if (!IsRunningOnGvisor()) {
- ASSERT_THAT(setsid(), SyscallSucceeds());
- }
- }
-
- PosixError RunInChild(SubprocessCallback childFunc) {
- pid_t child = fork();
- if (!child) {
- childFunc();
- _exit(0);
- }
- int wstatus;
- if (waitpid(child, &wstatus, 0) != child) {
- return PosixError(
- errno, absl::StrCat("child failed with wait status: ", wstatus));
- }
- return PosixError(wstatus, "process returned");
- }
-
- // Main and replica ends of the PTY. Non-blocking.
- FileDescriptor main_;
- FileDescriptor replica_;
-};
-
-TEST_F(JobControlTest, SetTTYMain) {
- auto res = RunInChild([=]() {
- TEST_PCHECK(setsid() >= 0);
- TEST_PCHECK(!ioctl(main_.get(), TIOCSCTTY, 0));
- });
- ASSERT_NO_ERRNO(res);
-}
-
-TEST_F(JobControlTest, SetTTY) {
- auto res = RunInChild([=]() {
- TEST_PCHECK(setsid() >= 0);
- TEST_PCHECK(ioctl(!replica_.get(), TIOCSCTTY, 0));
- });
- ASSERT_NO_ERRNO(res);
-}
-
-TEST_F(JobControlTest, SetTTYNonLeader) {
- // Fork a process that won't be the session leader.
- auto res =
- RunInChild([=]() { TEST_PCHECK(ioctl(replica_.get(), TIOCSCTTY, 0)); });
- ASSERT_NO_ERRNO(res);
-}
-
-TEST_F(JobControlTest, SetTTYBadArg) {
- SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));
- auto res = RunInChild([=]() {
- TEST_PCHECK(setsid() >= 0);
- TEST_PCHECK(!ioctl(replica_.get(), TIOCSCTTY, 1));
- });
- ASSERT_NO_ERRNO(res);
-}
-
-TEST_F(JobControlTest, SetTTYDifferentSession) {
- SKIP_IF(ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));
-
- auto res = RunInChild([=]() {
- TEST_PCHECK(setsid() >= 0);
- TEST_PCHECK(!ioctl(replica_.get(), TIOCSCTTY, 1));
-
- // Fork, join a new session, and try to steal the parent's controlling
- // terminal, which should fail.
- pid_t grandchild = fork();
- if (!grandchild) {
- TEST_PCHECK(setsid() >= 0);
- // We shouldn't be able to steal the terminal.
- TEST_PCHECK(ioctl(replica_.get(), TIOCSCTTY, 1));
- _exit(0);
- }
-
- int gcwstatus;
- TEST_PCHECK(waitpid(grandchild, &gcwstatus, 0) == grandchild);
- TEST_PCHECK(gcwstatus == 0);
- });
-}
-
-TEST_F(JobControlTest, ReleaseTTY) {
- ASSERT_THAT(ioctl(replica_.get(), TIOCSCTTY, 0), SyscallSucceeds());
-
- // Make sure we're ignoring SIGHUP, which will be sent to this process once we
- // disconnect they TTY.
- struct sigaction sa = {};
- sa.sa_handler = SIG_IGN;
- sa.sa_flags = 0;
- sigemptyset(&sa.sa_mask);
- struct sigaction old_sa;
- EXPECT_THAT(sigaction(SIGHUP, &sa, &old_sa), SyscallSucceeds());
- EXPECT_THAT(ioctl(replica_.get(), TIOCNOTTY), SyscallSucceeds());
- EXPECT_THAT(sigaction(SIGHUP, &old_sa, NULL), SyscallSucceeds());
-}
-
-TEST_F(JobControlTest, ReleaseUnsetTTY) {
- ASSERT_THAT(ioctl(replica_.get(), TIOCNOTTY), SyscallFailsWithErrno(ENOTTY));
-}
-
-TEST_F(JobControlTest, ReleaseWrongTTY) {
- auto res = RunInChild([=]() {
- TEST_PCHECK(setsid() >= 0);
- TEST_PCHECK(!ioctl(replica_.get(), TIOCSCTTY, 0));
- TEST_PCHECK(ioctl(main_.get(), TIOCNOTTY) < 0 && errno == ENOTTY);
- });
- ASSERT_NO_ERRNO(res);
-}
-
-TEST_F(JobControlTest, ReleaseTTYNonLeader) {
- auto ret = RunInChild([=]() {
- TEST_PCHECK(setsid() >= 0);
- TEST_PCHECK(!ioctl(replica_.get(), TIOCSCTTY, 0));
-
- pid_t grandchild = fork();
- if (!grandchild) {
- TEST_PCHECK(!ioctl(replica_.get(), TIOCNOTTY));
- _exit(0);
- }
-
- int wstatus;
- TEST_PCHECK(waitpid(grandchild, &wstatus, 0) == grandchild);
- TEST_PCHECK(wstatus == 0);
- });
- ASSERT_NO_ERRNO(ret);
-}
-
-TEST_F(JobControlTest, ReleaseTTYDifferentSession) {
- auto ret = RunInChild([=]() {
- TEST_PCHECK(setsid() >= 0);
-
- TEST_PCHECK(!ioctl(replica_.get(), TIOCSCTTY, 0));
-
- pid_t grandchild = fork();
- if (!grandchild) {
- // Join a new session, then try to disconnect.
- TEST_PCHECK(setsid() >= 0);
- TEST_PCHECK(ioctl(replica_.get(), TIOCNOTTY));
- _exit(0);
- }
-
- int wstatus;
- TEST_PCHECK(waitpid(grandchild, &wstatus, 0) == grandchild);
- TEST_PCHECK(wstatus == 0);
- });
- ASSERT_NO_ERRNO(ret);
-}
-
-// Used by the child process spawned in ReleaseTTYSignals to track received
-// signals.
-static int received;
-
-void sig_handler(int signum) { received |= signum; }
-
-// When the session leader releases its controlling terminal, the foreground
-// process group gets SIGHUP, then SIGCONT. This test:
-// - Spawns 2 threads
-// - Has thread 1 return 0 if it gets both SIGHUP and SIGCONT
-// - Has thread 2 leave the foreground process group, and return non-zero if it
-// receives any signals.
-// - Has the parent thread release its controlling terminal
-// - Checks that thread 1 got both signals
-// - Checks that thread 2 didn't get any signals.
-TEST_F(JobControlTest, ReleaseTTYSignals) {
- ASSERT_THAT(ioctl(replica_.get(), TIOCSCTTY, 0), SyscallSucceeds());
-
- received = 0;
- struct sigaction sa = {};
- sa.sa_handler = sig_handler;
- sa.sa_flags = 0;
- sigemptyset(&sa.sa_mask);
- sigaddset(&sa.sa_mask, SIGHUP);
- sigaddset(&sa.sa_mask, SIGCONT);
- sigprocmask(SIG_BLOCK, &sa.sa_mask, NULL);
-
- pid_t same_pgrp_child = fork();
- if (!same_pgrp_child) {
- // The child will wait for SIGHUP and SIGCONT, then return 0. It begins with
- // SIGHUP and SIGCONT blocked. We install signal handlers for those signals,
- // then use sigsuspend to wait for those specific signals.
- TEST_PCHECK(!sigaction(SIGHUP, &sa, NULL));
- TEST_PCHECK(!sigaction(SIGCONT, &sa, NULL));
- sigset_t mask;
- sigfillset(&mask);
- sigdelset(&mask, SIGHUP);
- sigdelset(&mask, SIGCONT);
- while (received != (SIGHUP | SIGCONT)) {
- sigsuspend(&mask);
- }
- _exit(0);
- }
-
- // We don't want to block these anymore.
- sigprocmask(SIG_UNBLOCK, &sa.sa_mask, NULL);
-
- // This child will return non-zero if either SIGHUP or SIGCONT are received.
- pid_t diff_pgrp_child = fork();
- if (!diff_pgrp_child) {
- TEST_PCHECK(!setpgid(0, 0));
- TEST_PCHECK(pause());
- _exit(1);
- }
-
- EXPECT_THAT(setpgid(diff_pgrp_child, diff_pgrp_child), SyscallSucceeds());
-
- // Make sure we're ignoring SIGHUP, which will be sent to this process once we
- // disconnect they TTY.
- struct sigaction sighup_sa = {};
- sighup_sa.sa_handler = SIG_IGN;
- sighup_sa.sa_flags = 0;
- sigemptyset(&sighup_sa.sa_mask);
- struct sigaction old_sa;
- EXPECT_THAT(sigaction(SIGHUP, &sighup_sa, &old_sa), SyscallSucceeds());
-
- // Release the controlling terminal, sending SIGHUP and SIGCONT to all other
- // processes in this process group.
- EXPECT_THAT(ioctl(replica_.get(), TIOCNOTTY), SyscallSucceeds());
-
- EXPECT_THAT(sigaction(SIGHUP, &old_sa, NULL), SyscallSucceeds());
-
- // The child in the same process group will get signaled.
- int wstatus;
- EXPECT_THAT(waitpid(same_pgrp_child, &wstatus, 0),
- SyscallSucceedsWithValue(same_pgrp_child));
- EXPECT_EQ(wstatus, 0);
-
- // The other child will not get signaled.
- EXPECT_THAT(waitpid(diff_pgrp_child, &wstatus, WNOHANG),
- SyscallSucceedsWithValue(0));
- EXPECT_THAT(kill(diff_pgrp_child, SIGKILL), SyscallSucceeds());
-}
-
-TEST_F(JobControlTest, GetForegroundProcessGroup) {
- auto res = RunInChild([=]() {
- pid_t pid, foreground_pgid;
- TEST_PCHECK(setsid() >= 0);
- TEST_PCHECK(!ioctl(replica_.get(), TIOCSCTTY, 1));
- TEST_PCHECK(!ioctl(replica_.get(), TIOCGPGRP, &foreground_pgid));
- TEST_PCHECK((pid = getpid()) >= 0);
- TEST_PCHECK(pid == foreground_pgid);
- });
- ASSERT_NO_ERRNO(res);
-}
-
-TEST_F(JobControlTest, GetForegroundProcessGroupNonControlling) {
- // At this point there's no controlling terminal, so TIOCGPGRP should fail.
- pid_t foreground_pgid;
- ASSERT_THAT(ioctl(replica_.get(), TIOCGPGRP, &foreground_pgid),
- SyscallFailsWithErrno(ENOTTY));
-}
-
-// This test:
-// - sets itself as the foreground process group
-// - creates a child process in a new process group
-// - sets that child as the foreground process group
-// - kills its child and sets itself as the foreground process group.
-TEST_F(JobControlTest, SetForegroundProcessGroup) {
- auto res = RunInChild([=]() {
- TEST_PCHECK(!ioctl(replica_.get(), TIOCSCTTY, 0));
-
- // Ignore SIGTTOU so that we don't stop ourself when calling tcsetpgrp.
- struct sigaction sa = {};
- sa.sa_handler = SIG_IGN;
- sa.sa_flags = 0;
- sigemptyset(&sa.sa_mask);
- sigaction(SIGTTOU, &sa, NULL);
-
- // Set ourself as the foreground process group.
- TEST_PCHECK(!tcsetpgrp(replica_.get(), getpgid(0)));
-
- // Create a new process that just waits to be signaled.
- pid_t grandchild = fork();
- if (!grandchild) {
- TEST_PCHECK(!pause());
- // We should never reach this.
- _exit(1);
- }
-
- // Make the child its own process group, then make it the controlling
- // process group of the terminal.
- TEST_PCHECK(!setpgid(grandchild, grandchild));
- TEST_PCHECK(!tcsetpgrp(replica_.get(), grandchild));
-
- // Sanity check - we're still the controlling session.
- TEST_PCHECK(getsid(0) == getsid(grandchild));
-
- // Signal the child, wait for it to exit, then retake the terminal.
- TEST_PCHECK(!kill(grandchild, SIGTERM));
- int wstatus;
- TEST_PCHECK(waitpid(grandchild, &wstatus, 0) == grandchild);
- TEST_PCHECK(WIFSIGNALED(wstatus));
- TEST_PCHECK(WTERMSIG(wstatus) == SIGTERM);
-
- // Set ourself as the foreground process.
- pid_t pgid;
- TEST_PCHECK(pgid = getpgid(0) == 0);
- TEST_PCHECK(!tcsetpgrp(replica_.get(), pgid));
- });
-}
-
-TEST_F(JobControlTest, SetForegroundProcessGroupWrongTTY) {
- pid_t pid = getpid();
- ASSERT_THAT(ioctl(replica_.get(), TIOCSPGRP, &pid),
- SyscallFailsWithErrno(ENOTTY));
-}
-
-TEST_F(JobControlTest, SetForegroundProcessGroupNegPgid) {
- auto ret = RunInChild([=]() {
- TEST_PCHECK(setsid() >= 0);
- TEST_PCHECK(!ioctl(replica_.get(), TIOCSCTTY, 0));
-
- pid_t pid = -1;
- TEST_PCHECK(ioctl(replica_.get(), TIOCSPGRP, &pid) && errno == EINVAL);
- });
- ASSERT_NO_ERRNO(ret);
-}
-
-TEST_F(JobControlTest, SetForegroundProcessGroupEmptyProcessGroup) {
- auto ret = RunInChild([=]() {
- TEST_PCHECK(!ioctl(replica_.get(), TIOCSCTTY, 0));
-
- // Create a new process, put it in a new process group, make that group the
- // foreground process group, then have the process wait.
- pid_t grandchild = fork();
- if (!grandchild) {
- TEST_PCHECK(!setpgid(0, 0));
- _exit(0);
- }
-
- // Wait for the child to exit.
- int wstatus;
- TEST_PCHECK(waitpid(grandchild, &wstatus, 0) == grandchild);
- // The child's process group doesn't exist anymore - this should fail.
- TEST_PCHECK(ioctl(replica_.get(), TIOCSPGRP, &grandchild) != 0 &&
- errno == ESRCH);
- });
-}
-
-TEST_F(JobControlTest, SetForegroundProcessGroupDifferentSession) {
- auto ret = RunInChild([=]() {
- TEST_PCHECK(setsid() >= 0);
- TEST_PCHECK(!ioctl(replica_.get(), TIOCSCTTY, 0));
-
- int sync_setsid[2];
- int sync_exit[2];
- TEST_PCHECK(pipe(sync_setsid) >= 0);
- TEST_PCHECK(pipe(sync_exit) >= 0);
-
- // Create a new process and put it in a new session.
- pid_t grandchild = fork();
- if (!grandchild) {
- TEST_PCHECK(setsid() >= 0);
- // Tell the parent we're in a new session.
- char c = 'c';
- TEST_PCHECK(WriteFd(sync_setsid[1], &c, 1) == 1);
- TEST_PCHECK(ReadFd(sync_exit[0], &c, 1) == 1);
- _exit(0);
- }
-
- // Wait for the child to tell us it's in a new session.
- char c = 'c';
- TEST_PCHECK(ReadFd(sync_setsid[0], &c, 1) == 1);
-
- // Child is in a new session, so we can't make it the foregroup process
- // group.
- TEST_PCHECK(ioctl(replica_.get(), TIOCSPGRP, &grandchild) &&
- errno == EPERM);
-
- TEST_PCHECK(WriteFd(sync_exit[1], &c, 1) == 1);
-
- int wstatus;
- TEST_PCHECK(waitpid(grandchild, &wstatus, 0) == grandchild);
- TEST_PCHECK(WIFEXITED(wstatus));
- TEST_PCHECK(!WEXITSTATUS(wstatus));
- });
- ASSERT_NO_ERRNO(ret);
-}
-
-// Verify that we don't hang when creating a new session from an orphaned
-// process group (b/139968068). Calling setsid() creates an orphaned process
-// group, as process groups that contain the session's leading process are
-// orphans.
-//
-// We create 2 sessions in this test. The init process in gVisor is considered
-// not to be an orphan (see sessions.go), so we have to create a session from
-// which to create a session. The latter session is being created from an
-// orphaned process group.
-TEST_F(JobControlTest, OrphanRegression) {
- pid_t session_2_leader = fork();
- if (!session_2_leader) {
- TEST_PCHECK(setsid() >= 0);
-
- pid_t session_3_leader = fork();
- if (!session_3_leader) {
- TEST_PCHECK(setsid() >= 0);
-
- _exit(0);
- }
-
- int wstatus;
- TEST_PCHECK(waitpid(session_3_leader, &wstatus, 0) == session_3_leader);
- TEST_PCHECK(wstatus == 0);
-
- _exit(0);
- }
-
- int wstatus;
- ASSERT_THAT(waitpid(session_2_leader, &wstatus, 0),
- SyscallSucceedsWithValue(session_2_leader));
- ASSERT_EQ(wstatus, 0);
-}
-
-} // namespace
-} // namespace testing
-} // namespace gvisor