diff options
Diffstat (limited to 'pkg')
-rw-r--r-- | pkg/abi/linux/tty.go | 28 | ||||
-rw-r--r-- | pkg/sentry/fs/tty/line_discipline.go | 354 | ||||
-rw-r--r-- | pkg/sentry/fs/tty/master.go | 3 | ||||
-rw-r--r-- | pkg/sentry/fs/tty/slave.go | 3 |
4 files changed, 298 insertions, 90 deletions
diff --git a/pkg/abi/linux/tty.go b/pkg/abi/linux/tty.go index f9e641af9..84b6ccc87 100644 --- a/pkg/abi/linux/tty.go +++ b/pkg/abi/linux/tty.go @@ -14,9 +14,16 @@ package linux +import ( + "unicode/utf8" +) + const ( // NumControlCharacters is the number of control characters in Termios. NumControlCharacters = 19 + // disabledChar is used to indicate that a control character is + // disabled. + disabledChar = 0 ) // Termios is struct termios, defined in uapi/asm-generic/termbits.h. @@ -86,6 +93,27 @@ func (t *KernelTermios) FromTermios(term Termios) { t.ControlCharacters = term.ControlCharacters } +// IsTerminating returns whether c is a line terminating character. +func (t *KernelTermios) IsTerminating(c rune) bool { + if t.IsEOF(c) { + return true + } + switch byte(c) { + case disabledChar: + return false + case '\n', t.ControlCharacters[VEOL]: + return true + case t.ControlCharacters[VEOL2]: + return t.LEnabled(IEXTEN) + } + return false +} + +// IsEOF returns whether c is the EOF character. +func (t *KernelTermios) IsEOF(c rune) bool { + return utf8.RuneLen(c) == 1 && byte(c) == t.ControlCharacters[VEOF] && t.ControlCharacters[VEOF] != disabledChar +} + // Input flags. const ( IGNBRK = 0000001 diff --git a/pkg/sentry/fs/tty/line_discipline.go b/pkg/sentry/fs/tty/line_discipline.go index a3aa95ece..bdc4f5b92 100644 --- a/pkg/sentry/fs/tty/line_discipline.go +++ b/pkg/sentry/fs/tty/line_discipline.go @@ -28,9 +28,90 @@ import ( ) const ( + // canonMaxBytes is the number of bytes that fit into a single line of + // terminal input in canonical mode. This corresponds to N_TTY_BUF_SIZE + // in include/linux/tty.h. + canonMaxBytes = 4096 + + // nonCanonMaxBytes is the maximum number of bytes that can be read at + // a time in noncanonical mode. + nonCanonMaxBytes = canonMaxBytes - 1 + spacesPerTab = 8 ) +// queue represents one of the input or output queues between a pty master and +// slave. Bytes written to a queue are added to the read buffer until it is +// full, at which point they are written to the wait buffer. Bytes are +// processed (i.e. undergo termios transformations) as they are added to the +// read buffer. The read buffer is readable when its length is nonzero and +// readable is true. +type queue struct { + waiter.Queue `state:"nosave"` + + // readBuf is buffer of data ready to be read when readable is true. + // This data has been processed. + readBuf bytes.Buffer `state:".([]byte)"` + + // waitBuf contains data that can't fit into readBuf. It is put here + // until it can be loaded into the read buffer. waitBuf contains data + // that hasn't been processed. + waitBuf bytes.Buffer `state:".([]byte)"` + + // readable indicates whether the read buffer can be read from. In + // canonical mode, there can be an unterminated line in the read buffer, + // so readable must be checked. + readable bool +} + +// saveReadBuf is invoked by stateify. +func (q *queue) saveReadBuf() []byte { + return append([]byte(nil), q.readBuf.Bytes()...) +} + +// loadReadBuf is invoked by stateify. +func (q *queue) loadReadBuf(b []byte) { + q.readBuf.Write(b) +} + +// saveWaitBuf is invoked by stateify. +func (q *queue) saveWaitBuf() []byte { + return append([]byte(nil), q.waitBuf.Bytes()...) +} + +// loadWaitBuf is invoked by stateify. +func (q *queue) loadWaitBuf(b []byte) { + q.waitBuf.Write(b) +} + +// readReadiness returns whether q is ready to be read from. +func (q *queue) readReadiness(t *linux.KernelTermios) waiter.EventMask { + if q.readBuf.Len() > 0 && q.readable { + return waiter.EventIn + } + return waiter.EventMask(0) +} + +// writeReadiness returns whether q is ready to be written to. +func (q *queue) writeReadiness(t *linux.KernelTermios) waiter.EventMask { + // Like Linux, we don't impose a maximum size on what can be enqueued. + return waiter.EventOut +} + +// readableSize writes the number of readable bytes to userspace. +func (q *queue) readableSize(ctx context.Context, io usermem.IO, args arch.SyscallArguments) error { + var size int32 + if q.readable { + size = int32(q.readBuf.Len()) + } + + _, err := usermem.CopyObjectOut(ctx, io, args[2].Pointer(), size, usermem.IOOpts{ + AddressSpaceActive: true, + }) + return err + +} + // lineDiscipline dictates how input and output are handled between the // pseudoterminal (pty) master and slave. It can be configured to alter I/O, // modify control characters (e.g. Ctrl-C for SIGINT), etc. The following man @@ -50,7 +131,7 @@ const ( // // input from terminal +-------------+ input to process (e.g. bash) // +------------------------>| input queue |---------------------------+ -// | +-------------+ | +// | (inputQueueWrite) +-------------+ (inputQueueRead) | // | | // | v // masterFD slaveFD @@ -58,7 +139,7 @@ const ( // | | // | output to terminal +--------------+ output from process | // +------------------------| output queue |<--------------------------+ -// +--------------+ +// (outputQueueRead) +--------------+ (outputQueueWrite) // // Lock order: // inMu @@ -102,14 +183,25 @@ func (l *lineDiscipline) getTermios(ctx context.Context, io usermem.IO, args arc // setTermios sets a linux.Termios for the tty. func (l *lineDiscipline) setTermios(ctx context.Context, io usermem.IO, args arch.SyscallArguments) (uintptr, error) { + l.inMu.Lock() + defer l.inMu.Unlock() l.termiosMu.Lock() defer l.termiosMu.Unlock() + oldCanonEnabled := l.termios.LEnabled(linux.ICANON) // We must copy a Termios struct, not KernelTermios. var t linux.Termios _, err := usermem.CopyObjectIn(ctx, io, args[2].Pointer(), &t, usermem.IOOpts{ AddressSpaceActive: true, }) l.termios.FromTermios(t) + + // If canonical mode is turned off, move bytes from inQueue's wait + // buffer to its read buffer. Anything already in the read buffer is + // now readable. + if oldCanonEnabled && !l.termios.LEnabled(linux.ICANON) { + l.pushWaitBuf(&l.inQueue, transformInput) + } + return 0, err } @@ -118,7 +210,9 @@ func (l *lineDiscipline) masterReadiness() waiter.EventMask { defer l.inMu.Unlock() l.outMu.Lock() defer l.outMu.Unlock() - return l.inQueue.writeReadiness() | l.outQueue.readReadiness() + // We don't have to lock a termios because the default master termios + // is immutable. + return l.inQueue.writeReadiness(&linux.MasterTermios) | l.outQueue.readReadiness(&linux.MasterTermios) } func (l *lineDiscipline) slaveReadiness() waiter.EventMask { @@ -126,93 +220,97 @@ func (l *lineDiscipline) slaveReadiness() waiter.EventMask { defer l.inMu.Unlock() l.outMu.Lock() defer l.outMu.Unlock() - return l.outQueue.writeReadiness() | l.inQueue.readReadiness() -} - -// queue represents one of the input or output queues between a pty master and -// slave. -type queue struct { - waiter.Queue `state:"nosave"` - buf bytes.Buffer `state:".([]byte)"` -} - -// saveBuf is invoked by stateify. -func (q *queue) saveBuf() []byte { - return append([]byte(nil), q.buf.Bytes()...) -} - -// loadBuf is invoked by stateify. -func (q *queue) loadBuf(b []byte) { - q.buf.Write(b) -} - -// readReadiness returns whether q is ready to be read from. -// -// Preconditions: q's mutex must be held. -func (q *queue) readReadiness() waiter.EventMask { - ready := waiter.EventMask(0) - if q.buf.Len() > 0 { - ready |= waiter.EventIn - } - return ready + l.termiosMu.Lock() + defer l.termiosMu.Unlock() + return l.outQueue.writeReadiness(&l.termios) | l.inQueue.readReadiness(&l.termios) } -// writeReadiness returns whether q is ready to be written to. -func (q *queue) writeReadiness() waiter.EventMask { - return waiter.EventOut +func (l *lineDiscipline) inputQueueReadSize(ctx context.Context, io usermem.IO, args arch.SyscallArguments) error { + l.inMu.Lock() + defer l.inMu.Unlock() + return l.inQueue.readableSize(ctx, io, args) } func (l *lineDiscipline) inputQueueRead(ctx context.Context, dst usermem.IOSequence) (int64, error) { l.inMu.Lock() defer l.inMu.Unlock() - return l.queueRead(ctx, dst, &l.inQueue) + return l.queueRead(ctx, dst, &l.inQueue, transformInput) } func (l *lineDiscipline) inputQueueWrite(ctx context.Context, src usermem.IOSequence) (int64, error) { l.inMu.Lock() defer l.inMu.Unlock() - return l.queueWrite(ctx, src, &l.inQueue, false) + return l.queueWrite(ctx, src, &l.inQueue, transformInput) +} + +func (l *lineDiscipline) outputQueueReadSize(ctx context.Context, io usermem.IO, args arch.SyscallArguments) error { + l.outMu.Lock() + defer l.outMu.Unlock() + return l.outQueue.readableSize(ctx, io, args) } func (l *lineDiscipline) outputQueueRead(ctx context.Context, dst usermem.IOSequence) (int64, error) { l.outMu.Lock() defer l.outMu.Unlock() - return l.queueRead(ctx, dst, &l.outQueue) + return l.queueRead(ctx, dst, &l.outQueue, transformOutput) } func (l *lineDiscipline) outputQueueWrite(ctx context.Context, src usermem.IOSequence) (int64, error) { l.outMu.Lock() defer l.outMu.Unlock() - return l.queueWrite(ctx, src, &l.outQueue, true) + return l.queueWrite(ctx, src, &l.outQueue, transformOutput) } // queueRead reads from q to userspace. // // Preconditions: q's lock must be held. -func (l *lineDiscipline) queueRead(ctx context.Context, dst usermem.IOSequence, q *queue) (int64, error) { - // Copy bytes out to user-space. queueRead doesn't have to do any - // processing or other extra work -- that's all taken care of when - // writing to a queue. - n, err := q.buf.WriteTo(dst.Writer(ctx)) +func (l *lineDiscipline) queueRead(ctx context.Context, dst usermem.IOSequence, q *queue, f transform) (int64, error) { + if !q.readable { + return 0, syserror.ErrWouldBlock + } + + // Read out from the read buffer. + n := canonMaxBytes + if n > int(dst.NumBytes()) { + n = int(dst.NumBytes()) + } + if n > q.readBuf.Len() { + n = q.readBuf.Len() + } + n, err := dst.Writer(ctx).Write(q.readBuf.Bytes()[:n]) + if err != nil { + return 0, err + } + // Discard bytes read out. + q.readBuf.Next(n) + + // If we read everything, this queue is no longer readable. + if q.readBuf.Len() == 0 { + q.readable = false + } + + // Move data from the queue's wait buffer to its read buffer. + l.termiosMu.Lock() + defer l.termiosMu.Unlock() + l.pushWaitBuf(q, f) // If state changed, notify any waiters. If nothing was available to // read, let the caller know we could block. if n > 0 { q.Notify(waiter.EventOut) - } else if err == nil { + } else { return 0, syserror.ErrWouldBlock } - return int64(n), err + return int64(n), nil } -// queueWrite writes to q from userspace. `output` is whether the queue being -// written to should be subject to output processing (i.e. whether it is the -// output queue). +// queueWrite writes to q from userspace. f is the function used to perform +// processing on data being written and write it to the read buffer. // // Precondition: q's lock must be held. -func (l *lineDiscipline) queueWrite(ctx context.Context, src usermem.IOSequence, q *queue, output bool) (int64, error) { +func (l *lineDiscipline) queueWrite(ctx context.Context, src usermem.IOSequence, q *queue, f transform) (int64, error) { // TODO: Use CopyInTo/safemem to avoid extra copying. - // Get the bytes to write from user-space. + // Copy in the bytes to write from user-space. b := make([]byte, src.NumBytes()) n, err := src.CopyIn(ctx, b) if err != nil { @@ -220,49 +318,69 @@ func (l *lineDiscipline) queueWrite(ctx context.Context, src usermem.IOSequence, } b = b[:n] + // Write as much as possible to the read buffer. + l.termiosMu.Lock() + defer l.termiosMu.Unlock() + n = f(l, q, b) + + // Write remaining data to the wait buffer. + nWaiting, _ := q.waitBuf.Write(b[n:]) + // If state changed, notify any waiters. If we were unable to write // anything, let the caller know we could block. if n > 0 { q.Notify(waiter.EventIn) - } else { + } else if nWaiting == 0 { return 0, syserror.ErrWouldBlock } + return int64(n + nWaiting), nil +} - // Optionally perform line discipline transformations depending on - // whether we're writing to the input queue or output queue. - var buf *bytes.Buffer - l.termiosMu.Lock() - if output { - buf = l.transformOutput(b) - } else { - buf = l.transformInput(b) - } - l.termiosMu.Unlock() +// pushWaitBuf fills the queue's read buffer with data from the wait buffer. +// +// Precondition: l.inMu and l.termiosMu must be held. +func (l *lineDiscipline) pushWaitBuf(q *queue, f transform) { + // Remove bytes from the wait buffer and move them to the read buffer. + n := f(l, q, q.waitBuf.Bytes()) + q.waitBuf.Next(n) - // Enqueue buf at the end of the queue. - buf.WriteTo(&q.buf) - return int64(n), err + // If state changed, notify any waiters. + if n > 0 { + q.Notify(waiter.EventIn) + } } +// transform functions require the passed in lineDiscipline's mutex to be held. +type transform func(*lineDiscipline, *queue, []byte) int + // transformOutput does output processing for one end of the pty. See // drivers/tty/n_tty.c:do_output_char for an analogous kernel function. // -// Precondition: l.termiosMu must be held. -func (l *lineDiscipline) transformOutput(buf []byte) *bytes.Buffer { +// Precondition: l.termiosMu and q's mutex must be held. +func transformOutput(l *lineDiscipline, q *queue, buf []byte) int { + // transformOutput is effectively always in noncanonical mode, as the + // master termios never has ICANON set. + if !l.termios.OEnabled(linux.OPOST) { - return bytes.NewBuffer(buf) + n, _ := q.readBuf.Write(buf) + if q.readBuf.Len() > 0 { + q.readable = true + } + return n } - var ret bytes.Buffer + var ret int for len(buf) > 0 { - c := l.removeRune(&buf) + c, size := l.peekRune(buf) + ret += size + buf = buf[size:] switch c { case '\n': if l.termios.OEnabled(linux.ONLRET) { l.column = 0 } if l.termios.OEnabled(linux.ONLCR) { - ret.Write([]byte{'\r', '\n'}) + q.readBuf.Write([]byte{'\r', '\n'}) continue } case '\r': @@ -281,7 +399,7 @@ func (l *lineDiscipline) transformOutput(buf []byte) *bytes.Buffer { spaces := spacesPerTab - l.column%spacesPerTab if l.termios.OutputFlags&linux.TABDLY == linux.XTABS { l.column += spaces - ret.Write(bytes.Repeat([]byte{' '}, 8)) + q.readBuf.Write(bytes.Repeat([]byte{' '}, spacesPerTab)) continue } l.column += spaces @@ -292,24 +410,40 @@ func (l *lineDiscipline) transformOutput(buf []byte) *bytes.Buffer { default: l.column++ } - ret.WriteRune(c) + q.readBuf.WriteRune(c) + } + if q.readBuf.Len() > 0 { + q.readable = true } - return &ret + return ret } -// transformInput does input processing for one end of the pty. Characters -// read are transformed according to flags set in the termios struct. See +// transformInput does input processing for one end of the pty. Characters read +// are transformed according to flags set in the termios struct. See // drivers/tty/n_tty.c:n_tty_receive_char_special for an analogous kernel // function. // -// Precondition: l.termiosMu must be held. -func (l *lineDiscipline) transformInput(buf []byte) *bytes.Buffer { - var ret bytes.Buffer - for len(buf) > 0 { - c := l.removeRune(&buf) +// Precondition: l.termiosMu and q's mutex must be held. +func transformInput(l *lineDiscipline, q *queue, buf []byte) int { + // If there's a line waiting to be read in canonical mode, don't write + // anything else to the read buffer. + if l.termios.LEnabled(linux.ICANON) && q.readable { + return 0 + } + + maxBytes := nonCanonMaxBytes + if l.termios.LEnabled(linux.ICANON) { + maxBytes = canonMaxBytes + } + + var ret int + for len(buf) > 0 && q.readBuf.Len() < canonMaxBytes { + c, size := l.peekRune(buf) switch c { case '\r': if l.termios.IEnabled(linux.IGNCR) { + buf = buf[size:] + ret += size continue } if l.termios.IEnabled(linux.ICRNL) { @@ -320,23 +454,63 @@ func (l *lineDiscipline) transformInput(buf []byte) *bytes.Buffer { c = '\r' } } - ret.WriteRune(c) + + // In canonical mode, we discard non-terminating characters + // after the first 4095. + if l.shouldDiscard(q, c) { + buf = buf[size:] + ret += size + continue + } + + // Stop if the buffer would be overfilled. + if q.readBuf.Len()+size > maxBytes { + break + } + buf = buf[size:] + ret += size + + // If we get EOF, make the buffer available for reading. + if l.termios.LEnabled(linux.ICANON) && l.termios.IsEOF(c) { + q.readable = true + break + } + + q.readBuf.WriteRune(c) + + // If we finish a line, make it available for reading. + if l.termios.LEnabled(linux.ICANON) && l.termios.IsTerminating(c) { + q.readable = true + break + } + } + + // In noncanonical mode, everything is readable. + if !l.termios.LEnabled(linux.ICANON) && q.readBuf.Len() > 0 { + q.readable = true } - return &ret + + return ret +} + +// shouldDiscard returns whether c should be discarded. In canonical mode, if +// too many bytes are enqueued, we keep reading input and discarding it until +// we find a terminating character. Signal/echo processing still occurs. +func (l *lineDiscipline) shouldDiscard(q *queue, c rune) bool { + return l.termios.LEnabled(linux.ICANON) && q.readBuf.Len()+utf8.RuneLen(c) >= canonMaxBytes && !l.termios.IsTerminating(c) } -// removeRune removes and returns the first rune from the byte array. The -// buffer's length is updated accordingly. -func (l *lineDiscipline) removeRune(b *[]byte) rune { +// peekRune returns the first rune from the byte array depending on whether +// UTF8 is enabled. +func (l *lineDiscipline) peekRune(b []byte) (rune, int) { var c rune var size int // If UTF-8 support is enabled, runes might be multiple bytes. if l.termios.IEnabled(linux.IUTF8) { - c, size = utf8.DecodeRune(*b) + c, size = utf8.DecodeRune(b) } else { - c = rune((*b)[0]) + c = rune(b[0]) size = 1 } - *b = (*b)[size:] - return c + return c, size } diff --git a/pkg/sentry/fs/tty/master.go b/pkg/sentry/fs/tty/master.go index 3c47ee517..74cdbe874 100644 --- a/pkg/sentry/fs/tty/master.go +++ b/pkg/sentry/fs/tty/master.go @@ -148,6 +148,9 @@ func (mf *masterFileOperations) Write(ctx context.Context, _ *fs.File, src userm // Ioctl implements fs.FileOperations.Ioctl. func (mf *masterFileOperations) Ioctl(ctx context.Context, io usermem.IO, args arch.SyscallArguments) (uintptr, error) { switch args[1].Uint() { + case linux.FIONREAD: // linux.FIONREAD == linux.TIOCINQ + // Get the number of bytes in the output queue read buffer. + return 0, mf.t.ld.outputQueueReadSize(ctx, io, args) case linux.TCGETS: // N.B. TCGETS on the master actually returns the configuration // of the slave end. diff --git a/pkg/sentry/fs/tty/slave.go b/pkg/sentry/fs/tty/slave.go index 9178071a4..f5eec726e 100644 --- a/pkg/sentry/fs/tty/slave.go +++ b/pkg/sentry/fs/tty/slave.go @@ -133,6 +133,9 @@ func (sf *slaveFileOperations) Write(ctx context.Context, _ *fs.File, src userme // Ioctl implements fs.FileOperations.Ioctl. func (sf *slaveFileOperations) Ioctl(ctx context.Context, io usermem.IO, args arch.SyscallArguments) (uintptr, error) { switch args[1].Uint() { + case linux.FIONREAD: // linux.FIONREAD == linux.TIOCINQ + // Get the number of bytes in the input queue read buffer. + return 0, sf.si.t.ld.inputQueueReadSize(ctx, io, args) case linux.TCGETS: return sf.si.t.ld.getTermios(ctx, io, args) case linux.TCSETS: |