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.cc1230
1 files changed, 1230 insertions, 0 deletions
diff --git a/test/syscalls/linux/pty.cc b/test/syscalls/linux/pty.cc
new file mode 100644
index 000000000..253aa26ba
--- /dev/null
+++ b/test/syscalls/linux/pty.cc
@@ -0,0 +1,1230 @@
+// Copyright 2018 Google LLC
+//
+// 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/major.h>
+#include <poll.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sys/types.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/file_descriptor.h"
+#include "test/util/posix_error.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;
+
+// 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;
+
+// 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 std::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) {
+ return PosixError(errno, "read failed");
+ }
+ completed += n;
+ if (completed >= count) {
+ return completed;
+ }
+ }
+
+ if (completed) {
+ return completed;
+ }
+ return PosixError(ETIMEDOUT, "Poll timed out");
+}
+
+// Opens the slave end of the passed master as R/W and nonblocking.
+PosixErrorOr<FileDescriptor> OpenSlave(const FileDescriptor& master) {
+ // Get pty index.
+ int n;
+ int ret = ioctl(master.get(), TIOCGPTN, &n);
+ if (ret < 0) {
+ return PosixError(errno, "ioctl(TIOCGPTN) failed");
+ }
+
+ // Unlock pts.
+ int unlock = 0;
+ ret = ioctl(master.get(), TIOCSPTLCK, &unlock);
+ if (ret < 0) {
+ return PosixError(errno, "ioctl(TIOSPTLCK) failed");
+ }
+
+ return Open(absl::StrCat("/dev/pts/", n), O_RDWR | O_NONBLOCK);
+}
+
+TEST(BasicPtyTest, StatUnopenedMaster) {
+ struct stat s;
+ ASSERT_THAT(stat("/dev/ptmx", &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, OpenMasterSlave) {
+ FileDescriptor master = ASSERT_NO_ERRNO_AND_VALUE(Open("/dev/ptmx", O_RDWR));
+ FileDescriptor slave = ASSERT_NO_ERRNO_AND_VALUE(OpenSlave(master));
+}
+
+// The slave entry in /dev/pts/ disappears when the master is closed, even if
+// the slave is still open.
+TEST(BasicPtyTest, SlaveEntryGoneAfterMasterClose) {
+ FileDescriptor master = ASSERT_NO_ERRNO_AND_VALUE(Open("/dev/ptmx", O_RDWR));
+ FileDescriptor slave = ASSERT_NO_ERRNO_AND_VALUE(OpenSlave(master));
+
+ // Get pty index.
+ int index = -1;
+ ASSERT_THAT(ioctl(master.get(), TIOCGPTN, &index), SyscallSucceeds());
+
+ std::string path = absl::StrCat("/dev/pts/", index);
+
+ struct stat st;
+ EXPECT_THAT(stat(path.c_str(), &st), SyscallSucceeds());
+
+ master.reset();
+
+ EXPECT_THAT(stat(path.c_str(), &st), SyscallFailsWithErrno(ENOENT));
+}
+
+TEST(BasicPtyTest, Getdents) {
+ FileDescriptor master1 = ASSERT_NO_ERRNO_AND_VALUE(Open("/dev/ptmx", O_RDWR));
+ int index1 = -1;
+ ASSERT_THAT(ioctl(master1.get(), TIOCGPTN, &index1), SyscallSucceeds());
+ FileDescriptor slave1 = ASSERT_NO_ERRNO_AND_VALUE(OpenSlave(master1));
+
+ FileDescriptor master2 = ASSERT_NO_ERRNO_AND_VALUE(Open("/dev/ptmx", O_RDWR));
+ int index2 = -1;
+ ASSERT_THAT(ioctl(master2.get(), TIOCGPTN, &index2), SyscallSucceeds());
+ FileDescriptor slave2 = ASSERT_NO_ERRNO_AND_VALUE(OpenSlave(master2));
+
+ // 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)));
+
+ master2.reset();
+
+ // The directory contains ptmx and index1, but not index2 since the master 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 {
+ master_ = ASSERT_NO_ERRNO_AND_VALUE(Open("/dev/ptmx", O_RDWR | O_NONBLOCK));
+ slave_ = ASSERT_NO_ERRNO_AND_VALUE(OpenSlave(master_));
+ }
+
+ void DisableCanonical() {
+ struct kernel_termios t = {};
+ EXPECT_THAT(ioctl(slave_.get(), TCGETS, &t), SyscallSucceeds());
+ t.c_lflag &= ~ICANON;
+ EXPECT_THAT(ioctl(slave_.get(), TCSETS, &t), SyscallSucceeds());
+ }
+
+ void EnableCanonical() {
+ struct kernel_termios t = {};
+ EXPECT_THAT(ioctl(slave_.get(), TCGETS, &t), SyscallSucceeds());
+ t.c_lflag |= ICANON;
+ EXPECT_THAT(ioctl(slave_.get(), TCSETS, &t), SyscallSucceeds());
+ }
+
+ // Master and slave ends of the PTY. Non-blocking.
+ FileDescriptor master_;
+ FileDescriptor slave_;
+};
+
+// Master to slave sanity test.
+TEST_F(PtyTest, WriteMasterToSlave) {
+ // N.B. by default, the slave reads nothing until the master writes a newline.
+ constexpr char kBuf[] = "hello\n";
+
+ EXPECT_THAT(WriteFd(master_.get(), kBuf, sizeof(kBuf) - 1),
+ SyscallSucceedsWithValue(sizeof(kBuf) - 1));
+
+ // Linux moves data from the master to the slave 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(slave_, sizeof(buf) - 1, buf);
+
+ EXPECT_EQ(memcmp(buf, kBuf, sizeof(kBuf)), 0);
+}
+
+// Slave to master sanity test.
+TEST_F(PtyTest, WriteSlaveToMaster) {
+ // N.B. by default, the master reads nothing until the slave writes a newline,
+ // and the master gets a carriage return.
+ constexpr char kInput[] = "hello\n";
+ constexpr char kExpected[] = "hello\r\n";
+
+ EXPECT_THAT(WriteFd(slave_.get(), kInput, sizeof(kInput) - 1),
+ SyscallSucceedsWithValue(sizeof(kInput) - 1));
+
+ // Linux moves data from the master to the slave 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(master_, sizeof(buf) - 1, buf);
+
+ EXPECT_EQ(memcmp(buf, kExpected, sizeof(kExpected)), 0);
+}
+
+// Both the master and slave report the standard default termios settings.
+//
+// Note that TCGETS on the master actually redirects to the slave (see comment
+// on MasterTermiosUnchangable).
+TEST_F(PtyTest, DefaultTermios) {
+ struct kernel_termios t = {};
+ EXPECT_THAT(ioctl(slave_.get(), TCGETS, &t), SyscallSucceeds());
+ EXPECT_EQ(t, DefaultTermios());
+
+ EXPECT_THAT(ioctl(master_.get(), TCGETS, &t), SyscallSucceeds());
+ EXPECT_EQ(t, DefaultTermios());
+}
+
+// Changing termios from the master actually affects the slave.
+//
+// TCSETS on the master actually redirects to the slave (see comment on
+// MasterTermiosUnchangable).
+TEST_F(PtyTest, TermiosAffectsSlave) {
+ struct kernel_termios master_termios = {};
+ EXPECT_THAT(ioctl(master_.get(), TCGETS, &master_termios), SyscallSucceeds());
+ master_termios.c_lflag ^= ICANON;
+ EXPECT_THAT(ioctl(master_.get(), TCSETS, &master_termios), SyscallSucceeds());
+
+ struct kernel_termios slave_termios = {};
+ EXPECT_THAT(ioctl(slave_.get(), TCGETS, &slave_termios), SyscallSucceeds());
+ EXPECT_EQ(master_termios, slave_termios);
+}
+
+// The master 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 master actually redirect to the slave
+// (drivers/tty/tty_ioctl.c:tty_mode_ioctl), making it impossible to change the
+// master termios.
+//
+// Verify this by setting ICRNL (which rewrites input \r to \n) and verify that
+// it has no effect on the master.
+TEST_F(PtyTest, MasterTermiosUnchangable) {
+ char c = '\r';
+ ASSERT_THAT(WriteFd(slave_.get(), &c, 1), SyscallSucceedsWithValue(1));
+
+ ExpectReadable(master_, 1, &c);
+ EXPECT_EQ(c, '\r'); // ICRNL had no effect!
+
+ ExpectFinished(master_);
+}
+
+// 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(slave_.get(), TCSETS, &t), SyscallSucceeds());
+
+ char c = '\r';
+ ASSERT_THAT(WriteFd(master_.get(), &c, 1), SyscallSucceedsWithValue(1));
+
+ ExpectReadable(slave_, 1, &c);
+ EXPECT_EQ(c, '\n');
+
+ ExpectFinished(slave_);
+}
+
+// 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(slave_.get(), TCSETS, &t), SyscallSucceeds());
+
+ char c = '\n';
+ ASSERT_THAT(WriteFd(slave_.get(), &c, 1), SyscallSucceedsWithValue(1));
+
+ // Extra byte for NUL for EXPECT_STREQ.
+ char buf[3] = {};
+ ExpectReadable(master_, 2, buf);
+ EXPECT_STREQ(buf, "\r\n");
+
+ ExpectFinished(slave_);
+}
+
+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(slave_.get(), TCSETS, &t), SyscallSucceeds());
+
+ char c = '\r';
+ ASSERT_THAT(WriteFd(master_.get(), &c, 1), SyscallSucceedsWithValue(1));
+
+ // Nothing to read.
+ ASSERT_THAT(PollAndReadFd(slave_.get(), &c, 1, kTimeout),
+ PosixErrorIs(ETIMEDOUT, ::testing::StrEq("Poll timed out")));
+}
+
+// Test that we can successfully poll for readable data from the slave.
+TEST_F(PtyTest, TermiosPollSlave) {
+ struct kernel_termios t = DefaultTermios();
+ t.c_iflag |= IGNCR;
+ t.c_lflag &= ~ICANON; // for byte-by-byte reading.
+ ASSERT_THAT(ioctl(slave_.get(), TCSETS, &t), SyscallSucceeds());
+
+ absl::Notification notify;
+ int sfd = slave_.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(master_.get(), s, strlen(s) + 1), SyscallSucceeds());
+}
+
+// Test that we can successfully poll for readable data from the master.
+TEST_F(PtyTest, TermiosPollMaster) {
+ struct kernel_termios t = DefaultTermios();
+ t.c_iflag |= IGNCR;
+ t.c_lflag &= ~ICANON; // for byte-by-byte reading.
+ ASSERT_THAT(ioctl(master_.get(), TCSETS, &t), SyscallSucceeds());
+
+ absl::Notification notify;
+ int mfd = master_.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(slave_.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(slave_.get(), TCSETS, &t), SyscallSucceeds());
+
+ char c = '\n';
+ ASSERT_THAT(WriteFd(master_.get(), &c, 1), SyscallSucceedsWithValue(1));
+
+ ExpectReadable(slave_, 1, &c);
+ EXPECT_EQ(c, '\r');
+
+ ExpectFinished(slave_);
+}
+
+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(slave_.get(), TCSETS, &t), SyscallSucceeds());
+
+ // The terminal is at column 0, so there should be no CR to read.
+ char c = '\r';
+ ASSERT_THAT(WriteFd(slave_.get(), &c, 1), SyscallSucceedsWithValue(1));
+
+ // Nothing to read.
+ ASSERT_THAT(PollAndReadFd(master_.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(slave_.get(), kInput, kInputSize),
+ SyscallSucceedsWithValue(kInputSize));
+
+ char buf[kInputSize] = {};
+ ExpectReadable(master_, kInputSize, buf);
+
+ EXPECT_EQ(memcmp(buf, kInput, kInputSize), 0);
+
+ ExpectFinished(master_);
+
+ // Terminal should be at column 0 again, so no CR can be read.
+ ASSERT_THAT(WriteFd(slave_.get(), &c, 1), SyscallSucceedsWithValue(1));
+
+ // Nothing to read.
+ ASSERT_THAT(PollAndReadFd(master_.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(slave_.get(), TCSETS, &t), SyscallSucceeds());
+
+ // The terminal is at column 0, so there should be no CR to read.
+ char c = '\r';
+ ASSERT_THAT(WriteFd(slave_.get(), &c, 1), SyscallSucceedsWithValue(1));
+
+ ExpectReadable(master_, 1, &c);
+ EXPECT_EQ(c, '\n');
+
+ ExpectFinished(master_);
+}
+
+// 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(master_.get(), kInput, sizeof(kInput)),
+ SyscallSucceedsWithValue(sizeof(kInput)));
+ char buf[sizeof(kInput)] = {};
+ ASSERT_THAT(PollAndReadFd(slave_.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(slave_.get(), TCSETS, &t), SyscallSucceeds());
+ ASSERT_THAT(WriteFd(master_.get(), &delim, 1), SyscallSucceedsWithValue(1));
+
+ // Now we can read, as sending EOL caused the line to become available.
+ ExpectReadable(slave_, sizeof(kInput), buf);
+ EXPECT_EQ(memcmp(buf, kInput, sizeof(kInput)), 0);
+
+ ExpectReadable(slave_, 1, buf);
+ EXPECT_EQ(buf[0], '=');
+
+ ExpectFinished(slave_);
+}
+
+// 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(master_.get(), input, kWriteLen),
+ SyscallSucceedsWithValue(kWriteLen));
+
+ // We can read the line.
+ char buf[kMaxLineSize] = {};
+ ExpectReadable(slave_, kMaxLineSize, buf);
+
+ ExpectFinished(slave_);
+}
+
+// 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(master_.get(), kInput, sizeof(kInput)),
+ SyscallSucceedsWithValue(sizeof(kInput)));
+
+ // Nothing available yet.
+ char buf[sizeof(kInput)] = {};
+ ASSERT_THAT(PollAndReadFd(slave_.get(), buf, sizeof(kInput), kTimeout),
+ PosixErrorIs(ETIMEDOUT, ::testing::StrEq("Poll timed out")));
+
+ DisableCanonical();
+
+ ExpectReadable(slave_, sizeof(kInput), buf);
+ EXPECT_STREQ(buf, kInput);
+
+ ExpectFinished(slave_);
+}
+
+TEST_F(PtyTest, SwitchCanonToNonCanonNewline) {
+ // Write a few bytes with a terminating character.
+ constexpr char kInput[] = "hello\n";
+ ASSERT_THAT(WriteFd(master_.get(), kInput, sizeof(kInput)),
+ SyscallSucceedsWithValue(sizeof(kInput)));
+
+ DisableCanonical();
+
+ // We can read the line.
+ char buf[sizeof(kInput)] = {};
+ ExpectReadable(slave_, sizeof(kInput), buf);
+ EXPECT_STREQ(buf, kInput);
+
+ ExpectFinished(slave_);
+}
+
+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(master_.get(), input, kWriteLen),
+ SyscallSucceedsWithValue(kWriteLen));
+ // Wait for the input queue to fill.
+ ASSERT_NO_ERRNO(WaitUntilReceived(slave_.get(), kMaxLineSize - 1));
+ constexpr char delim = '\n';
+ ASSERT_THAT(WriteFd(master_.get(), &delim, 1), SyscallSucceedsWithValue(1));
+
+ EnableCanonical();
+
+ // We can read the line.
+ char buf[kMaxLineSize] = {};
+ ExpectReadable(slave_, kMaxLineSize - 1, buf);
+
+ // We can also read the remaining characters.
+ ExpectReadable(slave_, 6, buf);
+
+ ExpectFinished(slave_);
+}
+
+TEST_F(PtyTest, SwitchNoncanonToCanonNoNewline) {
+ DisableCanonical();
+
+ // Write a few bytes without a terminating character.
+ // mode, and read them.
+ constexpr char kInput[] = "hello";
+ ASSERT_THAT(WriteFd(master_.get(), kInput, sizeof(kInput) - 1),
+ SyscallSucceedsWithValue(sizeof(kInput) - 1));
+
+ ASSERT_NO_ERRNO(WaitUntilReceived(slave_.get(), sizeof(kInput) - 1));
+ EnableCanonical();
+
+ // We can read the line.
+ char buf[sizeof(kInput)] = {};
+ ExpectReadable(slave_, sizeof(kInput) - 1, buf);
+ EXPECT_STREQ(buf, kInput);
+
+ ExpectFinished(slave_);
+}
+
+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(master_.get(), input, kWriteLen),
+ SyscallSucceedsWithValue(kWriteLen));
+
+ ASSERT_NO_ERRNO(WaitUntilReceived(slave_.get(), kMaxLineSize - 1));
+ EnableCanonical();
+
+ // We can read the line.
+ char buf[kMaxLineSize] = {};
+ ExpectReadable(slave_, kMaxLineSize - 1, buf);
+
+ ExpectFinished(slave_);
+}
+
+// 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(master_.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 master to the slave.
+ ASSERT_NO_ERRNO(WaitUntilReceived(slave_.get(), kMaxLineSize - 1));
+ for (int i = 0; i < kInputSize; i++) {
+ // This makes too many syscalls for save/restore.
+ const DisableSave ds;
+ char c;
+ ExpectReadable(slave_, 1, &c);
+ ASSERT_EQ(c, kInput);
+ }
+
+ ExpectFinished(slave_);
+}
+
+// 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(master_.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(slave_.get(), buf, sizeof(input), kTimeout),
+ PosixErrorIs(ETIMEDOUT, ::testing::StrEq("Poll timed out")));
+
+ char delim = '\n';
+ ASSERT_THAT(WriteFd(master_.get(), &delim, 1), SyscallSucceedsWithValue(1));
+
+ // Now it is available.
+ ASSERT_NO_ERRNO(WaitUntilReceived(slave_.get(), sizeof(input) + 1));
+ ExpectReadable(slave_, sizeof(input) + 1, buf);
+ EXPECT_STREQ(buf, "abc\n");
+
+ ExpectFinished(slave_);
+}
+
+// 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(master_.get(), input, sizeof(input)),
+ SyscallSucceedsWithValue(sizeof(input)));
+
+ // Extra byte for NUL for EXPECT_STREQ.
+ char buf[4] = {};
+
+ // Nothing available yet.
+ ASSERT_THAT(PollAndReadFd(slave_.get(), buf, sizeof(input), kTimeout),
+ PosixErrorIs(ETIMEDOUT, ::testing::StrEq("Poll timed out")));
+ char delim = ControlCharacter('D');
+ ASSERT_THAT(WriteFd(master_.get(), &delim, 1), SyscallSucceedsWithValue(1));
+
+ // Now it is available. Note that ^D is not included.
+ ExpectReadable(slave_, sizeof(input), buf);
+ EXPECT_STREQ(buf, "abc");
+
+ ExpectFinished(slave_);
+}
+
+// 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(master_.get(), &kInput, sizeof(kInput)),
+ SyscallSucceedsWithValue(sizeof(kInput)));
+ }
+ ASSERT_THAT(WriteFd(master_.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(slave_, kMaxLineSize, buf);
+ EXPECT_EQ(buf[kMaxLineSize - 1], delim);
+ EXPECT_EQ(buf[kMaxLineSize - 2], kInput);
+ }
+
+ ExpectFinished(slave_);
+}
+
+TEST_F(PtyTest, CanonMultiline) {
+ constexpr char kInput1[] = "GO\n";
+ constexpr char kInput2[] = "BLUE\n";
+
+ // Write both lines.
+ ASSERT_THAT(WriteFd(master_.get(), kInput1, sizeof(kInput1) - 1),
+ SyscallSucceedsWithValue(sizeof(kInput1) - 1));
+ ASSERT_THAT(WriteFd(master_.get(), kInput2, sizeof(kInput2) - 1),
+ SyscallSucceedsWithValue(sizeof(kInput2) - 1));
+
+ // Get the first line.
+ char line1[8] = {};
+ ExpectReadable(slave_, sizeof(kInput1) - 1, line1);
+ EXPECT_STREQ(line1, kInput1);
+
+ // Get the second line.
+ char line2[8] = {};
+ ExpectReadable(slave_, sizeof(kInput2) - 1, line2);
+ EXPECT_STREQ(line2, kInput2);
+
+ ExpectFinished(slave_);
+}
+
+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(master_.get(), kInput1, sizeof(kInput1) - 1),
+ SyscallSucceedsWithValue(sizeof(kInput1) - 1));
+ ASSERT_THAT(WriteFd(master_.get(), kInput2, sizeof(kInput2) - 1),
+ SyscallSucceedsWithValue(sizeof(kInput2) - 1));
+
+ ASSERT_NO_ERRNO(
+ WaitUntilReceived(slave_.get(), sizeof(kInput1) + sizeof(kInput2) - 2));
+ EnableCanonical();
+
+ // Get all together as one line.
+ char line[9] = {};
+ ExpectReadable(slave_, 8, line);
+ EXPECT_STREQ(line, kExpected);
+
+ ExpectFinished(slave_);
+}
+
+TEST_F(PtyTest, SwitchTwiceMultiline) {
+ std::string kInputs[] = {"GO\n", "BLUE\n", "!"};
+ std::string kExpected = "GO\nBLUE\n!";
+
+ // Write each line.
+ for (std::string input : kInputs) {
+ ASSERT_THAT(WriteFd(master_.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(slave_.get(), kExpected.size()));
+ EnableCanonical();
+
+ // Get all together as one line.
+ char line[10] = {};
+ ExpectReadable(slave_, 9, line);
+ EXPECT_STREQ(line, kExpected.c_str());
+
+ ExpectFinished(slave_);
+}
+
+TEST_F(PtyTest, QueueSize) {
+ // Write the line.
+ constexpr char kInput1[] = "GO\n";
+ ASSERT_THAT(WriteFd(master_.get(), kInput1, sizeof(kInput1) - 1),
+ SyscallSucceedsWithValue(sizeof(kInput1) - 1));
+ ASSERT_NO_ERRNO(WaitUntilReceived(slave_.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(master_.get(), input, kMaxLineSize),
+ SyscallSucceedsWithValue(kMaxLineSize));
+ int inputBufSize = ASSERT_NO_ERRNO_AND_VALUE(
+ WaitUntilReceived(slave_.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 master.
+ constexpr char kBuf[] = "hello\n";
+ constexpr size_t size = sizeof(kBuf) - 1;
+ EXPECT_THAT(WriteFd(master_.get(), kBuf, size),
+ SyscallSucceedsWithValue(size));
+
+ // Read from the slave into bad_buffer.
+ ASSERT_NO_ERRNO(WaitUntilReceived(slave_.get(), size));
+ EXPECT_THAT(ReadFd(slave_.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(master_.get(), kInput, strlen(kInput)),
+ SyscallSucceedsWithValue(strlen(kInput)));
+
+ char buf[100] = {};
+ ExpectReadable(master_, strlen(kInput), buf);
+
+ EXPECT_STREQ(buf, kInput);
+ ExpectFinished(master_);
+}
+
+TEST_F(PtyTest, GetWindowSize) {
+ struct winsize ws;
+ ASSERT_THAT(ioctl(slave_.get(), TIOCGWINSZ, &ws), SyscallSucceeds());
+ EXPECT_EQ(ws.ws_row, 0);
+ EXPECT_EQ(ws.ws_col, 0);
+}
+
+TEST_F(PtyTest, SetSlaveWindowSize) {
+ constexpr uint16_t kRows = 343;
+ constexpr uint16_t kCols = 2401;
+ struct winsize ws = {.ws_row = kRows, .ws_col = kCols};
+ ASSERT_THAT(ioctl(slave_.get(), TIOCSWINSZ, &ws), SyscallSucceeds());
+
+ struct winsize retrieved_ws = {};
+ ASSERT_THAT(ioctl(master_.get(), TIOCGWINSZ, &retrieved_ws),
+ SyscallSucceeds());
+ EXPECT_EQ(retrieved_ws.ws_row, kRows);
+ EXPECT_EQ(retrieved_ws.ws_col, kCols);
+}
+
+TEST_F(PtyTest, SetMasterWindowSize) {
+ constexpr uint16_t kRows = 343;
+ constexpr uint16_t kCols = 2401;
+ struct winsize ws = {.ws_row = kRows, .ws_col = kCols};
+ ASSERT_THAT(ioctl(master_.get(), TIOCSWINSZ, &ws), SyscallSucceeds());
+
+ struct winsize retrieved_ws = {};
+ ASSERT_THAT(ioctl(slave_.get(), TIOCGWINSZ, &retrieved_ws),
+ SyscallSucceeds());
+ EXPECT_EQ(retrieved_ws.ws_row, kRows);
+ EXPECT_EQ(retrieved_ws.ws_col, kCols);
+}
+
+} // namespace
+} // namespace testing
+} // namespace gvisor