Skip to content

Commit

Permalink
Implement /dev/tty for sentry-internal ttys.
Browse files Browse the repository at this point in the history
The /dev/tty acts as a replica for the current thread group's controlling
terminal.

In a follow-up, I will make /dev/tty work for donated host ttys.

Updates #10925

PiperOrigin-RevId: 681629892
  • Loading branch information
nlacasse authored and gvisor-bot committed Oct 2, 2024
1 parent d3ce23c commit 72193f1
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 34 deletions.
2 changes: 1 addition & 1 deletion pkg/sentry/control/proc.go
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ func ttyName(tty *kernel.TTY) string {
if tty == nil {
return "?"
}
return fmt.Sprintf("pts/%d", tty.Index)
return fmt.Sprintf("pts/%d", tty.Index())
}

// ContainerUsage retrieves per-container CPU usage.
Expand Down
1 change: 1 addition & 0 deletions pkg/sentry/devices/ttydev/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ go_library(
"//pkg/abi/linux",
"//pkg/context",
"//pkg/errors/linuxerr",
"//pkg/sentry/kernel",
"//pkg/sentry/vfs",
],
)
16 changes: 14 additions & 2 deletions pkg/sentry/devices/ttydev/ttydev.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.

// Package ttydev implements an unopenable vfs.Device for /dev/tty.
// Package ttydev implements a vfs.Device for /dev/tty.
package ttydev

import (
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/context"
"gvisor.dev/gvisor/pkg/errors/linuxerr"
"gvisor.dev/gvisor/pkg/sentry/kernel"
"gvisor.dev/gvisor/pkg/sentry/vfs"
)

Expand All @@ -35,7 +36,18 @@ type ttyDevice struct{}

// Open implements vfs.Device.Open.
func (ttyDevice) Open(ctx context.Context, mnt *vfs.Mount, vfsd *vfs.Dentry, opts vfs.OpenOptions) (*vfs.FileDescription, error) {
return nil, linuxerr.EIO
t := kernel.TaskFromContext(ctx)
if t == nil {
return nil, linuxerr.ENXIO
}
tty := t.ThreadGroup().TTY()
if tty == nil {
return nil, linuxerr.ENXIO
}
// Opening /dev/tty does not set the controlling terminal. See Linux
// tty_open().
opts.Flags |= linux.O_NOCTTY
return tty.Open(ctx, mnt, vfsd, opts)
}

// Register registers all devices implemented by this package in vfsObj.
Expand Down
9 changes: 8 additions & 1 deletion pkg/sentry/fsimpl/devpts/devpts.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"gvisor.dev/gvisor/pkg/context"
"gvisor.dev/gvisor/pkg/errors/linuxerr"
"gvisor.dev/gvisor/pkg/sentry/fsimpl/kernfs"
"gvisor.dev/gvisor/pkg/sentry/kernel"
"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
"gvisor.dev/gvisor/pkg/sentry/vfs"
)
Expand Down Expand Up @@ -265,7 +266,13 @@ func (i *rootInode) allocateTerminal(ctx context.Context, creds *auth.Credential
}

// Create the new terminal and replica.
t := newTerminal(idx)
t := &Terminal{
n: idx,
root: i,
}
t.masterKTTY = kernel.NewTTY(idx, t)
t.replicaKTTY = kernel.NewTTY(idx, t)
t.ld = newLineDiscipline(linux.DefaultReplicaTermios, t)
replica := &replicaInode{
root: i,
t: t,
Expand Down
21 changes: 1 addition & 20 deletions pkg/sentry/fsimpl/devpts/replica.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,26 +53,7 @@ var _ kernfs.Inode = (*replicaInode)(nil)

// Open implements kernfs.Inode.Open.
func (ri *replicaInode) Open(ctx context.Context, rp *vfs.ResolvingPath, d *kernfs.Dentry, opts vfs.OpenOptions) (*vfs.FileDescription, error) {
t := kernel.TaskFromContext(ctx)
if t == nil {
panic("open must be called from a task goroutine")
}
fd := &replicaFileDescription{
inode: ri,
}
fd.LockFD.Init(&ri.locks)
if err := fd.vfsfd.Init(fd, opts.Flags, rp.Mount(), d.VFSDentry(), &vfs.FileDescriptionOptions{}); err != nil {
return nil, err
}
if opts.Flags&linux.O_NOCTTY == 0 {
// Opening a replica sets the process' controlling TTY when
// possible. An error indicates it cannot be set, and is
// ignored silently.
_ = t.ThreadGroup().SetControllingTTY(fd.inode.t.replicaKTTY, false /* steal */, fd.vfsfd.IsReadable())
}
ri.t.ld.replicaOpen()
return &fd.vfsfd, nil

return ri.t.Open(ctx, rp.Mount(), d.VFSDentry(), opts)
}

// Valid implements kernfs.Inode.Valid.
Expand Down
44 changes: 36 additions & 8 deletions pkg/sentry/fsimpl/devpts/terminal.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@ package devpts

import (
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/context"
"gvisor.dev/gvisor/pkg/errors/linuxerr"
"gvisor.dev/gvisor/pkg/sentry/kernel"
"gvisor.dev/gvisor/pkg/sentry/vfs"
)

// Terminal is a pseudoterminal.
//
// Terminal implements kernel.TTYOperations.
//
// +stateify savable
type Terminal struct {
// n is the terminal index. It is immutable.
Expand All @@ -29,6 +34,9 @@ type Terminal struct {
// ld is the line discipline of the terminal. It is immutable.
ld *lineDiscipline

// root is the rootInode for this devpts mount. It is immutable.
root *rootInode

// masterKTTY contains the controlling process of the master end of
// this terminal. This field is immutable.
masterKTTY *kernel.TTY
Expand All @@ -38,13 +46,33 @@ type Terminal struct {
replicaKTTY *kernel.TTY
}

func newTerminal(n uint32) *Terminal {
t := &Terminal{
n: n,
masterKTTY: &kernel.TTY{Index: n},
replicaKTTY: &kernel.TTY{Index: n},
}
t.ld = newLineDiscipline(linux.DefaultReplicaTermios, t)
var _ kernel.TTYOperations = (*Terminal)(nil)

return t
// Open implements kernel.TTYOperations.Open.
func (t *Terminal) Open(ctx context.Context, mnt *vfs.Mount, vfsd *vfs.Dentry, opts vfs.OpenOptions) (*vfs.FileDescription, error) {
tsk := kernel.TaskFromContext(ctx)
if tsk == nil {
return nil, linuxerr.EIO
}
t.root.mu.Lock()
ri, ok := t.root.replicas[t.replicaKTTY.Index()]
t.root.mu.Unlock()
if !ok {
return nil, linuxerr.EIO
}
fd := &replicaFileDescription{
inode: ri,
}
fd.LockFD.Init(&ri.locks)
if err := fd.vfsfd.Init(fd, opts.Flags, mnt, vfsd, &vfs.FileDescriptionOptions{}); err != nil {
return nil, err
}
if opts.Flags&linux.O_NOCTTY == 0 {
// Opening a replica sets the process' controlling TTY when
// possible. An error indicates it cannot be set, and is
// ignored silently. See Linux tty_open().
_ = tsk.ThreadGroup().SetControllingTTY(t.replicaKTTY, false /* steal */, fd.vfsfd.IsReadable())
}
ri.t.ld.replicaOpen()
return &fd.vfsfd, nil
}
29 changes: 27 additions & 2 deletions pkg/sentry/kernel/tty.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,49 @@ package kernel

import (
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/context"
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/pkg/sentry/vfs"
"gvisor.dev/gvisor/pkg/sync"
)

// TTYOperations handle tty operations. It is analogous to (a small subset) of
// Linux's struct tty_operations and exists to avoid a circular dependency.
type TTYOperations interface {
// Open opens the tty.
Open(ctx context.Context, mnt *vfs.Mount, vfsd *vfs.Dentry, opts vfs.OpenOptions) (*vfs.FileDescription, error)
}

// TTY defines the relationship between a thread group and its controlling
// terminal.
//
// +stateify savable
type TTY struct {
// Index is the terminal index. It is immutable.
Index uint32
// index is the terminal index. It is immutable.
index uint32

// TTYOperations holds operations on the tty. It is immutable.
TTYOperations

mu sync.Mutex `state:"nosave"`

// tg is protected by mu.
tg *ThreadGroup
}

// NewTTY constructs a new TTY.
func NewTTY(index uint32, ttyOps TTYOperations) *TTY {
return &TTY{
TTYOperations: ttyOps,
index: index,
}
}

// Index returns the tty's index.
func (tty *TTY) Index() uint32 {
return tty.index
}

// TTY returns the thread group's controlling terminal. If nil, there is no
// controlling terminal.
func (tg *ThreadGroup) TTY() *TTY {
Expand Down
2 changes: 2 additions & 0 deletions test/syscalls/linux/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -1604,7 +1604,9 @@ cc_binary(
"//test/util:cleanup",
"//test/util:file_descriptor",
"//test/util:fs_util",
"//test/util:logging",
"//test/util:mount_util",
"//test/util:multiprocess_util",
"//test/util:posix_error",
"//test/util:pty_util",
"//test/util:signal_util",
Expand Down
50 changes: 50 additions & 0 deletions test/syscalls/linux/pty.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

#include <errno.h>
#include <fcntl.h>
#include <linux/capability.h>
#include <linux/major.h>
Expand Down Expand Up @@ -45,7 +46,9 @@
#include "test/util/file_descriptor.h"
#include "test/util/fs_util.h"
#include "test/util/linux_capability_util.h"
#include "test/util/logging.h"
#include "test/util/mount_util.h"
#include "test/util/multiprocess_util.h"
#include "test/util/posix_error.h"
#include "test/util/pty_util.h"
#include "test/util/signal_util.h"
Expand Down Expand Up @@ -638,6 +641,53 @@ TEST(BasicPtyTest, SetMode) {
EXPECT_EQ(st.st_mode, 0620 | S_IFCHR);
}

TEST(BasicPtyTest, OpenDevTTY) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));

// Run in forked process so we don't pollute the controlling terminal of the
// main test process, which other tests expect to be unset.
const auto rest = [&] {
// We must be session leader to set controlling terminal,
// which will be opened by /dev/tty.
setsid();

FileDescriptor master =
TEST_CHECK_NO_ERRNO_AND_VALUE(Open("/dev/ptmx", O_RDWR));

// Opening the replica without O_NOCTTY will set controlling terminal.
FileDescriptor replica =
TEST_CHECK_NO_ERRNO_AND_VALUE(OpenReplica(master, O_RDWR | O_NONBLOCK));

// Terminal now available at /dev/tty.
FileDescriptor devtty =
TEST_CHECK_NO_ERRNO_AND_VALUE(Open("/dev/tty", O_RDWR | O_NONBLOCK));
};

EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0));
}

TEST(BasicPtyTest, OpenDevTTYNoCTTY) {
// Become session leader, otherwise we will never have hope of setting a
// controlling terminal. This test verifies that we fail to open /dev/tty for
// other (expected) reasons.
setsid();

// No controlling terminal, so can't open /dev/tty.
EXPECT_THAT(open("/dev/tty", O_RDWR | O_NONBLOCK),
SyscallFailsWithErrno(ENXIO));

// Open a master, but still no terminal available.
FileDescriptor master = ASSERT_NO_ERRNO_AND_VALUE(Open("/dev/ptmx", O_RDWR));
EXPECT_THAT(open("/dev/tty", O_RDWR | O_NONBLOCK),
SyscallFailsWithErrno(ENXIO));

// Open terminal with NOCTTY. Still can't open /dev/tty.
FileDescriptor replica_noctty = ASSERT_NO_ERRNO_AND_VALUE(
OpenReplica(master, O_RDWR | O_NONBLOCK | O_NOCTTY));
EXPECT_THAT(open("/dev/tty", O_RDWR | O_NONBLOCK),
SyscallFailsWithErrno(ENXIO));
}

class PtyTest : public ::testing::Test {
protected:
void SetUp() override {
Expand Down

0 comments on commit 72193f1

Please sign in to comment.