Merge pull request #9950 from thecodingwizard:master

PiperOrigin-RevId: 605691085
This commit is contained in:
gVisor bot
2024-02-09 11:47:53 -08:00
2 changed files with 191 additions and 14 deletions
@@ -16,6 +16,7 @@ package devpts
import (
"bytes"
"unicode"
"unicode/utf8"
"gvisor.dev/gvisor/pkg/abi/linux"
@@ -459,6 +460,94 @@ func (*inputQueueTransformer) transform(l *lineDiscipline, q *queue, buf []byte)
l.terminal.replicaKTTY.SignalForegroundProcessGroup(kernel.SignalInfoPriv(linux.SIGTSTP))
case l.termios.ControlCharacters[linux.VQUIT]: // ctrl-\
l.terminal.replicaKTTY.SignalForegroundProcessGroup(kernel.SignalInfoPriv(linux.SIGQUIT))
// In canonical mode, some characters need to be handled specially; for example, backspace.
// This roughly aligns with n_tty.c:n_tty_receive_char_canon and n_tty.c:eraser
// cBytes[0] == ControlCharacters[linux.VKILL] is also handled by n_tty.c:eraser, but this isn't implemented
case l.termios.ControlCharacters[linux.VWERASE]:
if !l.termios.LEnabled(linux.IEXTEN) {
break
}
fallthrough
case l.termios.ControlCharacters[linux.VERASE]:
if !l.termios.LEnabled(linux.ICANON) {
break
}
c := cBytes[0]
killType := linux.VERASE
if c == l.termios.ControlCharacters[linux.VWERASE] {
killType = linux.VWERASE
}
seenAlphanumeric := false
for len(q.readBuf) > 0 {
// Erase a character. If IUTF8 is enabled, erase an entire multibyte unicode character.
var toErase byte
cnt := 0
isContinuationByte := true
for ; cnt < len(q.readBuf) && isContinuationByte; cnt++ {
toErase = q.readBuf[len(q.readBuf)-cnt-1]
isContinuationByte = l.termios.IEnabled(linux.IUTF8) && (toErase&0xc0) == 0x80
}
if isContinuationByte {
// Do not partially erase a multibyte unicode character.
break
}
// VWERASE will continue erasing characters until we encounter the first non-alphanumeric character
// that follows some alphanumeric character. We consider "_" to be alphanumeric.
if killType == linux.VWERASE {
if unicode.IsLetter(rune(toErase)) || unicode.IsDigit(rune(toErase)) || toErase == '_' {
seenAlphanumeric = true
} else if seenAlphanumeric {
break
}
}
q.readBuf = q.readBuf[:len(q.readBuf)-cnt]
if l.termios.LEnabled(linux.ECHO) {
if l.termios.LEnabled(linux.ECHOPRT) {
// Not implemented
} else if killType == linux.VERASE && !l.termios.LEnabled(linux.ECHOE) {
// Not implemented
} else if toErase == '\t' {
// Not implemented
} else {
const unicodeDelete byte = 0x7f
isCtrl := toErase < 0x20 || toErase == unicodeDelete
echoctl := l.termios.LEnabled(linux.ECHOCTL)
charsToDelete := 1
if isCtrl {
// echoctl controls how we echo control characters, which also determines how we delete them.
if echoctl {
// echoctl echoes control characters as ^X, so we need to erase two characters.
charsToDelete = 2
} else {
// if echoctl is disabled, we don't echo control characters so we don't have to erase anything.
charsToDelete = 0
}
}
for i := 0; i < charsToDelete; i++ {
// Linux's kernel does character deletion with this sequence
// of bytes, presumably because some older terminals don't erase
// characters with \b, so we need to "erase" the old character
// by writing a space over it.
l.outQueue.writeBytes([]byte{'\b', ' ', '\b'}, l)
}
}
}
// VERASE only erases a single character
if killType == linux.VERASE {
break
}
}
buf = buf[1:]
ret += 1
notifyEcho = true
continue
}
// In canonical mode, we discard non-terminating characters
+102 -14
View File
@@ -18,6 +18,7 @@
#include <poll.h>
#include <sched.h>
#include <signal.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/poll.h>
@@ -31,6 +32,7 @@
#include <csignal>
#include <iostream>
#include <string>
#include "gmock/gmock.h"
#include "gtest/gtest.h"
@@ -98,8 +100,8 @@ struct kernel_termios {
cc_t c_cc[KERNEL_NCCS];
};
bool operator==(struct kernel_termios const& a,
struct kernel_termios const& b) {
bool operator==(struct kernel_termios const &a,
struct kernel_termios const &b) {
return memcmp(&a, &b, sizeof(a)) == 0;
}
@@ -122,14 +124,14 @@ constexpr char FromControlCharacter(char c) { return c + 'A' - 1; }
constexpr bool IsControlCharacter(char c) { return c <= 31; }
struct Field {
const char* name;
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) {
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++) {
@@ -293,7 +295,7 @@ std::string FormatCC(char c) {
return absl::StrCat("\\x", absl::Hex(c));
}
std::ostream& operator<<(std::ostream& os, struct kernel_termios const& a) {
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 = "
@@ -357,7 +359,7 @@ struct kernel_termios DefaultTermios() {
// 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,
PosixErrorOr<size_t> PollAndReadFd(int fd, void *buf, size_t count,
absl::Duration timeout) {
absl::Time end = absl::Now() + timeout;
@@ -376,7 +378,7 @@ PosixErrorOr<size_t> PollAndReadFd(int fd, void* buf, size_t count,
}
ssize_t n =
ReadFd(fd, static_cast<char*>(buf) + completed, count - completed);
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
@@ -471,14 +473,14 @@ PosixErrorOr<int> WaitUntilReceived(int fd, int count) {
}
// Verifies that there is nothing left to read from fd.
void ExpectFinished(const FileDescriptor& 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) {
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);
@@ -657,6 +659,28 @@ class PtyTest : public ::testing::Test {
EXPECT_THAT(ioctl(replica_.get(), TCSETS, &t), SyscallSucceeds());
}
// Writes master_input to the master file descriptor and verifies that
// the replica and the echo output match what is expected.
void TestCanonicalIO(const char *master_input,
const char *expected_replica_output,
const char *expected_echo_output) {
ASSERT_THAT(WriteFd(master_.get(), master_input, strlen(master_input)),
SyscallSucceedsWithValue(strlen(master_input)));
std::string buf(strlen(expected_replica_output), '\0');
ASSERT_NO_ERRNO(
WaitUntilReceived(replica_.get(), strlen(expected_replica_output)));
ExpectReadable(replica_, strlen(expected_replica_output), &buf[0]);
EXPECT_STREQ(buf.c_str(), expected_replica_output);
std::string echo_buf(strlen(expected_echo_output), '\0');
ExpectReadable(master_, strlen(expected_echo_output), &echo_buf[0]);
EXPECT_STREQ(echo_buf.c_str(), expected_echo_output);
ExpectFinished(master_);
ExpectFinished(replica_);
}
// Master and replica ends of the PTY. Non-blocking.
FileDescriptor master_;
FileDescriptor replica_;
@@ -1074,6 +1098,70 @@ TEST_F(PtyTest, VEOLTermination) {
ExpectFinished(replica_);
}
// Tests that sending "backspace" to the master fd will be handled properly
// in canonical mode
TEST_F(PtyTest, CanonInputBackspace) {
constexpr char kInput[] = "gvisor\x7f\n";
constexpr char kExpectedOutput[] = "gviso\n";
constexpr char kEchoExpectedOutput[] = "gvisor\b \b\r\n";
TestCanonicalIO(kInput, kExpectedOutput, kEchoExpectedOutput);
}
// one backspace should delete the entire multibyte character when IUTF8 is set
TEST_F(PtyTest, CanonInputBackspaceMultibyteCharacterWithIUTF8) {
struct kernel_termios t = DefaultTermios();
t.c_iflag |= IUTF8;
ASSERT_THAT(ioctl(replica_.get(), TCSETS, &t), SyscallSucceeds());
constexpr char kInput[] = "gviso\xc6\xa9\x7f\n";
constexpr char kExpectedOutput[] = "gviso\n";
constexpr char kEchoExpectedOutput[] = "gviso\xc6\xa9\b \b\r\n";
TestCanonicalIO(kInput, kExpectedOutput, kEchoExpectedOutput);
}
// one backspace should only delete one byte in a multibyte character
// if IUTF8 is not set
TEST_F(PtyTest, CanonInputBackspaceMultibyteCharacterWithoutIUTF8) {
constexpr char kInput[] = "gviso\xc6\xa9\x7f\n";
constexpr char kExpectedOutput[] = "gviso\xc6\n";
constexpr char kEchoExpectedOutput[] = "gviso\xc6\xa9\b \b\r\n";
TestCanonicalIO(kInput, kExpectedOutput, kEchoExpectedOutput);
}
// backspace should not partially delete a multibyte character when IUTF8 is set
TEST_F(PtyTest, CanonInputBackspaceMultibyteCharacterPartialWithIUTF8) {
struct kernel_termios t = DefaultTermios();
t.c_iflag |= IUTF8;
ASSERT_THAT(ioctl(replica_.get(), TCSETS, &t), SyscallSucceeds());
constexpr char kInput[] = "\x80\x7f\n";
constexpr char kExpectedOutput[] = "\x80\n";
constexpr char kEchoExpectedOutput[] = "\x80\r\n";
TestCanonicalIO(kInput, kExpectedOutput, kEchoExpectedOutput);
}
// backspace can partially delete a multibyte character when IUTF8 is not set
TEST_F(PtyTest, CanonInputBackspaceMultibyteCharacterPartialWithoutIUTF8) {
constexpr char kInput[] = "\x80\x7f\n";
constexpr char kExpectedOutput[] = "\n";
constexpr char kEchoExpectedOutput[] = "\x80\b \b\r\n";
TestCanonicalIO(kInput, kExpectedOutput, kEchoExpectedOutput);
}
// ^W, \x17, should erase a word
TEST_F(PtyTest, CanonInputWordErase) {
constexpr char kInput[] = "hello hi\x17\n";
constexpr char kExpectedOutput[] = "hello \n";
constexpr char kEchoExpectedOutput[] = "hello hi\b \b\b \b\r\n";
TestCanonicalIO(kInput, kExpectedOutput, kEchoExpectedOutput);
}
// 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.
@@ -1363,7 +1451,7 @@ TEST_F(PtyTest, SwitchTwiceMultiline) {
std::string kExpected = "GO\nBLUE\n!";
// Write each line.
for (const std::string& input : kInputs) {
for (const std::string &input : kInputs) {
ASSERT_THAT(WriteFd(master_.get(), input.c_str(), input.size()),
SyscallSucceedsWithValue(input.size()));
}
@@ -1403,18 +1491,18 @@ TEST_F(PtyTest, QueueSize) {
TEST_F(PtyTest, PartialBadBuffer) {
// Allocate 2 pages.
void* addr = mmap(nullptr, 2 * kPageSize, PROT_READ | PROT_WRITE,
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);
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),
mprotect(reinterpret_cast<void *>(buf + kPageSize), kPageSize, PROT_NONE),
SyscallSucceeds());
// Leave only one free byte in the buffer.
char* bad_buffer = buf + kPageSize - 1;
char *bad_buffer = buf + kPageSize - 1;
// Write to the master.
constexpr char kBuf[] = "hello\n";