diff options
Diffstat (limited to 'pkg/sentry/fs/tty')
-rw-r--r-- | pkg/sentry/fs/tty/BUILD | 63 | ||||
-rw-r--r-- | pkg/sentry/fs/tty/dir.go | 398 | ||||
-rw-r--r-- | pkg/sentry/fs/tty/fs.go | 95 | ||||
-rw-r--r-- | pkg/sentry/fs/tty/inode.go | 143 | ||||
-rw-r--r-- | pkg/sentry/fs/tty/line_discipline.go | 342 | ||||
-rw-r--r-- | pkg/sentry/fs/tty/master.go | 173 | ||||
-rw-r--r-- | pkg/sentry/fs/tty/slave.go | 151 | ||||
-rw-r--r-- | pkg/sentry/fs/tty/terminal.go | 44 | ||||
-rw-r--r-- | pkg/sentry/fs/tty/tty_test.go | 56 |
9 files changed, 1465 insertions, 0 deletions
diff --git a/pkg/sentry/fs/tty/BUILD b/pkg/sentry/fs/tty/BUILD new file mode 100644 index 000000000..90b350410 --- /dev/null +++ b/pkg/sentry/fs/tty/BUILD @@ -0,0 +1,63 @@ +package(licenses = ["notice"]) # Apache 2.0 + +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") +load("//tools/go_stateify:defs.bzl", "go_stateify") + +go_stateify( + name = "tty_state", + srcs = [ + "dir.go", + "fs.go", + "inode.go", + "line_discipline.go", + "master.go", + "slave.go", + "terminal.go", + ], + out = "tty_state.go", + package = "tty", +) + +go_library( + name = "tty", + srcs = [ + "dir.go", + "fs.go", + "inode.go", + "line_discipline.go", + "master.go", + "slave.go", + "terminal.go", + "tty_state.go", + ], + importpath = "gvisor.googlesource.com/gvisor/pkg/sentry/fs/tty", + visibility = ["//pkg/sentry:internal"], + deps = [ + "//pkg/abi/linux", + "//pkg/refs", + "//pkg/sentry/arch", + "//pkg/sentry/context", + "//pkg/sentry/device", + "//pkg/sentry/fs", + "//pkg/sentry/fs/fsutil", + "//pkg/sentry/kernel/auth", + "//pkg/sentry/kernel/time", + "//pkg/sentry/usermem", + "//pkg/state", + "//pkg/syserror", + "//pkg/tcpip/transport/unix", + "//pkg/waiter", + ], +) + +go_test( + name = "tty_test", + size = "small", + srcs = ["tty_test.go"], + embed = [":tty"], + deps = [ + "//pkg/abi/linux", + "//pkg/sentry/context/contexttest", + "//pkg/sentry/usermem", + ], +) diff --git a/pkg/sentry/fs/tty/dir.go b/pkg/sentry/fs/tty/dir.go new file mode 100644 index 000000000..2c5b2aed6 --- /dev/null +++ b/pkg/sentry/fs/tty/dir.go @@ -0,0 +1,398 @@ +// Copyright 2018 Google Inc. +// +// 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. + +// Package tty provide pseudoterminals via a devpts filesystem. +package tty + +import ( + "fmt" + "math" + "strconv" + "sync" + + "gvisor.googlesource.com/gvisor/pkg/abi/linux" + "gvisor.googlesource.com/gvisor/pkg/sentry/context" + "gvisor.googlesource.com/gvisor/pkg/sentry/fs" + "gvisor.googlesource.com/gvisor/pkg/sentry/fs/fsutil" + "gvisor.googlesource.com/gvisor/pkg/sentry/kernel/auth" + "gvisor.googlesource.com/gvisor/pkg/sentry/usermem" + "gvisor.googlesource.com/gvisor/pkg/syserror" + "gvisor.googlesource.com/gvisor/pkg/tcpip/transport/unix" + "gvisor.googlesource.com/gvisor/pkg/waiter" +) + +// dirInodeOperations is the root of a devpts mount. +// +// This indirectly manages all terminals within the mount. +// +// New Terminals are created by masterInodeOperations.GetFile, which registers +// the slave Inode in the this directory for discovery via Lookup/Readdir. The +// slave inode is unregistered when the master file is Released, as the slave +// is no longer discoverable at that point. +// +// References on the underlying Terminal are held by masterFileOperations and +// slaveInodeOperations. +// +// masterInodeOperations and slaveInodeOperations hold a pointer to +// dirInodeOperations, which is reference counted by the refcount their +// corresponding Dirents hold on their parent (this directory). +// +// dirInodeOperations implements fs.InodeOperations. +type dirInodeOperations struct { + fsutil.DeprecatedFileOperations + fsutil.InodeNotSocket + fsutil.InodeNotRenameable + fsutil.InodeNotSymlink + fsutil.InodeNoExtendedAttributes + fsutil.NoMappable + fsutil.NoopWriteOut + + // msrc is the super block this directory is on. + // + // TODO: Plumb this through instead of storing it here. + msrc *fs.MountSource + + // mu protects the fields below. + mu sync.Mutex `state:"nosave"` + + // attr contains the UnstableAttrs. + attr fsutil.InMemoryAttributes + + // master is the master PTY inode. + master *fs.Inode + + // slaves contains the slave inodes reachable from the directory. + // + // A new slave is added by allocateTerminal and is removed by + // masterFileOperations.Release. + // + // A reference is held on every slave in the map. + slaves map[uint32]*fs.Inode + + // dentryMap is a SortedDentryMap used to implement Readdir containing + // the master and all entries in slaves. + dentryMap *fs.SortedDentryMap + + // next is the next pty index to use. + // + // TODO: reuse indices when ptys are closed. + next uint32 +} + +var _ fs.InodeOperations = (*dirInodeOperations)(nil) + +// newDir creates a new dir with a ptmx file and no terminals. +func newDir(ctx context.Context, m *fs.MountSource) *fs.Inode { + d := &dirInodeOperations{ + attr: fsutil.InMemoryAttributes{ + Unstable: fs.WithCurrentTime(ctx, fs.UnstableAttr{ + Owner: fs.RootOwner, + Perms: fs.FilePermsFromMode(0555), + }), + }, + msrc: m, + slaves: make(map[uint32]*fs.Inode), + dentryMap: fs.NewSortedDentryMap(nil), + } + // Linux devpts uses a default mode of 0000 for ptmx which can be + // changed with the ptmxmode mount option. However, that default is not + // useful here (since we'd *always* need the mount option, so it is + // accessible by default). + d.master = newMasterInode(ctx, d, fs.RootOwner, fs.FilePermsFromMode(0666)) + d.dentryMap.Add("ptmx", fs.DentAttr{ + Type: d.master.StableAttr.Type, + InodeID: d.master.StableAttr.InodeID, + }) + + return fs.NewInode(d, m, fs.StableAttr{ + DeviceID: ptsDevice.DeviceID(), + // N.B. Linux always uses inode id 1 for the directory. See + // fs/devpts/inode.c:devpts_fill_super. + // + // TODO: Since ptsDevice must be shared between + // different mounts, we must not assign fixed numbers. + InodeID: ptsDevice.NextIno(), + BlockSize: usermem.PageSize, + Type: fs.Directory, + }) +} + +// Release implements fs.InodeOperations.Release. +func (d *dirInodeOperations) Release(ctx context.Context) { + d.master.DecRef() + if len(d.slaves) != 0 { + panic(fmt.Sprintf("devpts directory still contains active terminals: %+v", d)) + } +} + +// Lookup implements fs.InodeOperations.Lookup. +func (d *dirInodeOperations) Lookup(ctx context.Context, dir *fs.Inode, name string) (*fs.Dirent, error) { + d.mu.Lock() + defer d.mu.Unlock() + + // Master? + if name == "ptmx" { + d.master.IncRef() + return fs.NewDirent(d.master, name), nil + } + + // Slave number? + n, err := strconv.ParseUint(name, 10, 32) + if err != nil { + // Not found. + return nil, syserror.ENOENT + } + + s, ok := d.slaves[uint32(n)] + if !ok { + return nil, syserror.ENOENT + } + + s.IncRef() + return fs.NewDirent(s, name), nil +} + +// Create implements fs.InodeOperations.Create. +// +// Creation is never allowed. +func (d *dirInodeOperations) Create(ctx context.Context, dir *fs.Inode, name string, flags fs.FileFlags, perm fs.FilePermissions) (*fs.File, error) { + return nil, syserror.EACCES +} + +// CreateDirectory implements fs.InodeOperations.CreateDirectory. +// +// Creation is never allowed. +func (d *dirInodeOperations) CreateDirectory(ctx context.Context, dir *fs.Inode, name string, perm fs.FilePermissions) error { + return syserror.EACCES +} + +// CreateLink implements fs.InodeOperations.CreateLink. +// +// Creation is never allowed. +func (d *dirInodeOperations) CreateLink(ctx context.Context, dir *fs.Inode, oldname, newname string) error { + return syserror.EACCES +} + +// CreateHardLink implements fs.InodeOperations.CreateHardLink. +// +// Creation is never allowed. +func (d *dirInodeOperations) CreateHardLink(ctx context.Context, dir *fs.Inode, target *fs.Inode, name string) error { + return syserror.EACCES +} + +// CreateFifo implements fs.InodeOperations.CreateFifo. +// +// Creation is never allowed. +func (d *dirInodeOperations) CreateFifo(ctx context.Context, dir *fs.Inode, name string, perm fs.FilePermissions) error { + return syserror.EACCES +} + +// Remove implements fs.InodeOperations.Remove. +// +// Removal is never allowed. +func (d *dirInodeOperations) Remove(ctx context.Context, dir *fs.Inode, name string) error { + return syserror.EPERM +} + +// RemoveDirectory implements fs.InodeOperations.RemoveDirectory. +// +// Removal is never allowed. +func (d *dirInodeOperations) RemoveDirectory(ctx context.Context, dir *fs.Inode, name string) error { + return syserror.EPERM +} + +// Bind implements fs.InodeOperations.Bind. +func (d *dirInodeOperations) Bind(ctx context.Context, dir *fs.Inode, name string, data unix.BoundEndpoint, perm fs.FilePermissions) error { + return syserror.EPERM +} + +// GetFile implements fs.InodeOperations.GetFile. +func (d *dirInodeOperations) GetFile(ctx context.Context, dirent *fs.Dirent, flags fs.FileFlags) (*fs.File, error) { + return fs.NewFile(ctx, dirent, flags, &dirFileOperations{di: d}), nil +} + +// UnstableAttr implements fs.InodeOperations.UnstableAttr. +func (d *dirInodeOperations) UnstableAttr(ctx context.Context, inode *fs.Inode) (fs.UnstableAttr, error) { + d.mu.Lock() + defer d.mu.Unlock() + return d.attr.Unstable, nil +} + +// Check implements fs.InodeOperations.Check. +func (d *dirInodeOperations) Check(ctx context.Context, inode *fs.Inode, p fs.PermMask) bool { + return fs.ContextCanAccessFile(ctx, inode, p) +} + +// SetPermissions implements fs.InodeOperations.SetPermissions. +func (d *dirInodeOperations) SetPermissions(ctx context.Context, inode *fs.Inode, p fs.FilePermissions) bool { + d.mu.Lock() + defer d.mu.Unlock() + return d.attr.SetPermissions(ctx, p) +} + +// SetOwner implements fs.InodeOperations.SetOwner. +func (d *dirInodeOperations) SetOwner(ctx context.Context, inode *fs.Inode, owner fs.FileOwner) error { + d.mu.Lock() + defer d.mu.Unlock() + return d.attr.SetOwner(ctx, owner) +} + +// SetTimestamps implements fs.InodeOperations.SetTimestamps. +func (d *dirInodeOperations) SetTimestamps(ctx context.Context, inode *fs.Inode, ts fs.TimeSpec) error { + d.mu.Lock() + defer d.mu.Unlock() + return d.attr.SetTimestamps(ctx, ts) +} + +// Truncate implements fs.InodeOperations.Truncate. +func (d *dirInodeOperations) Truncate(ctx context.Context, inode *fs.Inode, size int64) error { + return syserror.EINVAL +} + +// AddLink implements fs.InodeOperations.AddLink. +func (d *dirInodeOperations) AddLink() {} + +// DropLink implements fs.InodeOperations.DropLink. +func (d *dirInodeOperations) DropLink() {} + +// NotifyStatusChange implements fs.InodeOperations.NotifyStatusChange. +func (d *dirInodeOperations) NotifyStatusChange(ctx context.Context) { + d.mu.Lock() + defer d.mu.Unlock() + + d.attr.TouchStatusChangeTime(ctx) +} + +// IsVirtual implements fs.InodeOperations.IsVirtual. +func (d *dirInodeOperations) IsVirtual() bool { + return true +} + +// StatFS implements fs.InodeOperations.StatFS. +func (d *dirInodeOperations) StatFS(ctx context.Context) (fs.Info, error) { + return fs.Info{ + Type: linux.DEVPTS_SUPER_MAGIC, + }, nil +} + +// allocateTerminal creates a new Terminal and installs a pts node for it. +// +// The caller must call DecRef when done with the returned Terminal. +func (d *dirInodeOperations) allocateTerminal(ctx context.Context) (*Terminal, error) { + d.mu.Lock() + defer d.mu.Unlock() + + n := d.next + if n == math.MaxUint32 { + return nil, syserror.ENOMEM + } + + if _, ok := d.slaves[n]; ok { + panic(fmt.Sprintf("pty index collision; index %d already exists", n)) + } + + t := newTerminal(ctx, d, n) + d.next++ + + // The reference returned by newTerminal is returned to the caller. + // Take another for the slave inode. + t.IncRef() + + // Create a pts node. The owner is based on the context that opens + // ptmx. + creds := auth.CredentialsFromContext(ctx) + uid, gid := creds.EffectiveKUID, creds.EffectiveKGID + slave := newSlaveInode(ctx, d, t, fs.FileOwner{uid, gid}, fs.FilePermsFromMode(0666)) + + d.slaves[n] = slave + d.dentryMap.Add(strconv.FormatUint(uint64(n), 10), fs.DentAttr{ + Type: slave.StableAttr.Type, + InodeID: slave.StableAttr.InodeID, + }) + + return t, nil +} + +// masterClose is called when the master end of t is closed. +func (d *dirInodeOperations) masterClose(t *Terminal) { + d.mu.Lock() + defer d.mu.Unlock() + + // The slave end disappears from the directory when the master end is + // closed, even if the slave end is open elsewhere. + // + // N.B. since we're using a backdoor method to remove a directory entry + // we won't properly fire inotify events like Linux would. + s, ok := d.slaves[t.n] + if !ok { + panic(fmt.Sprintf("Terminal %+v doesn't exist in %+v?", t, d)) + } + + s.DecRef() + delete(d.slaves, t.n) + d.dentryMap.Remove(strconv.FormatUint(uint64(t.n), 10)) +} + +// dirFileOperations are the fs.FileOperations for the directory. +// +// This is nearly identical to fsutil.DirFileOperations, except that it takes +// df.di.mu in IterateDir. +type dirFileOperations struct { + waiter.AlwaysReady `state:"nosave"` + fsutil.NoopRelease `state:"nosave"` + fsutil.GenericSeek `state:"nosave"` + fsutil.NoFsync `state:"nosave"` + fsutil.NoopFlush `state:"nosave"` + fsutil.NoMMap `state:"nosave"` + fsutil.NoIoctl `state:"nosave"` + + // di is the inode operations. + di *dirInodeOperations + + // dirCursor contains the name of the last directory entry that was + // serialized. + dirCursor string +} + +var _ fs.FileOperations = (*dirFileOperations)(nil) + +// IterateDir implements DirIterator.IterateDir. +func (df *dirFileOperations) IterateDir(ctx context.Context, dirCtx *fs.DirCtx, offset int) (int, error) { + df.di.mu.Lock() + defer df.di.mu.Unlock() + + n, err := fs.GenericReaddir(dirCtx, df.di.dentryMap) + return offset + n, err +} + +// Readdir implements FileOperations.Readdir. +func (df *dirFileOperations) Readdir(ctx context.Context, file *fs.File, serializer fs.DentrySerializer) (int64, error) { + root := fs.RootFromContext(ctx) + defer root.DecRef() + dirCtx := &fs.DirCtx{ + Serializer: serializer, + DirCursor: &df.dirCursor, + } + return fs.DirentReaddir(ctx, file.Dirent, df, root, dirCtx, file.Offset()) +} + +// Read implements FileOperations.Read +func (df *dirFileOperations) Read(context.Context, *fs.File, usermem.IOSequence, int64) (int64, error) { + return 0, syserror.EISDIR +} + +// Write implements FileOperations.Write. +func (df *dirFileOperations) Write(context.Context, *fs.File, usermem.IOSequence, int64) (int64, error) { + return 0, syserror.EISDIR +} diff --git a/pkg/sentry/fs/tty/fs.go b/pkg/sentry/fs/tty/fs.go new file mode 100644 index 000000000..f5e7a3162 --- /dev/null +++ b/pkg/sentry/fs/tty/fs.go @@ -0,0 +1,95 @@ +// Copyright 2018 Google Inc. +// +// 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. + +package tty + +import ( + "gvisor.googlesource.com/gvisor/pkg/sentry/context" + "gvisor.googlesource.com/gvisor/pkg/sentry/device" + "gvisor.googlesource.com/gvisor/pkg/sentry/fs" + "gvisor.googlesource.com/gvisor/pkg/syserror" +) + +// ptsDevice is the pseudo-filesystem device. +var ptsDevice = device.NewAnonDevice() + +// filesystem is a devpts filesystem. +// +// This devpts is always in the new "multi-instance" mode. i.e., it contains a +// ptmx device tied to this mount. +type filesystem struct{} + +func init() { + fs.RegisterFilesystem(&filesystem{}) +} + +// Name matches drivers/devpts/indoe.c:devpts_fs_type.name. +func (*filesystem) Name() string { + return "devpts" +} + +// AllowUserMount allows users to mount(2) this file system. +func (*filesystem) AllowUserMount() bool { + // TODO: Users may mount this once the terminals are in a + // usable state. + return false +} + +// Flags returns that there is nothing special about this file system. +func (*filesystem) Flags() fs.FilesystemFlags { + return 0 +} + +// MountSource returns a devpts root that can be positioned in the vfs. +func (f *filesystem) Mount(ctx context.Context, device string, flags fs.MountSourceFlags, data string) (*fs.Inode, error) { + // device is always ignored. + + // No options are supported. + if data != "" { + return nil, syserror.EINVAL + } + + return newDir(ctx, fs.NewMountSource(&superOperations{}, f, flags)), nil +} + +// superOperations implements fs.MountSourceOperations, preventing caching. +type superOperations struct{} + +// Revalidate implements fs.DirentOperations.Revalidate. +// +// It always returns true, forcing a Lookup for all entries. +// +// Slave entries are dropped from dir when their master is closed, so an +// existing slave Dirent in the tree is not sufficient to guarantee that it +// still exists on the filesystem. +func (superOperations) Revalidate(*fs.Dirent) bool { + return true +} + +// Keep implements fs.DirentOperations.Keep. +// +// Keep returns false because Revalidate would force a lookup on cached entries +// anyways. +func (superOperations) Keep(*fs.Dirent) bool { + return false +} + +// ResetInodeMappings implements MountSourceOperations.ResetInodeMappings. +func (superOperations) ResetInodeMappings() {} + +// SaveInodeMapping implements MountSourceOperations.SaveInodeMapping. +func (superOperations) SaveInodeMapping(*fs.Inode, string) {} + +// Destroy implements MountSourceOperations.Destroy. +func (superOperations) Destroy() {} diff --git a/pkg/sentry/fs/tty/inode.go b/pkg/sentry/fs/tty/inode.go new file mode 100644 index 000000000..04b9a7727 --- /dev/null +++ b/pkg/sentry/fs/tty/inode.go @@ -0,0 +1,143 @@ +// Copyright 2018 Google Inc. +// +// 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. + +package tty + +import ( + "sync" + + "gvisor.googlesource.com/gvisor/pkg/abi/linux" + "gvisor.googlesource.com/gvisor/pkg/sentry/context" + "gvisor.googlesource.com/gvisor/pkg/sentry/fs" + "gvisor.googlesource.com/gvisor/pkg/sentry/fs/fsutil" + ktime "gvisor.googlesource.com/gvisor/pkg/sentry/kernel/time" + "gvisor.googlesource.com/gvisor/pkg/syserror" +) + +// inodeOperations are the base fs.InodeOperations for master and slave Inodes. +// +// inodeOperations does not implement: +// +// * fs.InodeOperations.Release +// * fs.InodeOperations.GetFile +type inodeOperations struct { + fsutil.DeprecatedFileOperations `state:"nosave"` + fsutil.InodeNoExtendedAttributes `state:"nosave"` + fsutil.InodeNotDirectory `state:"nosave"` + fsutil.InodeNotRenameable `state:"nosave"` + fsutil.InodeNotSocket `state:"nosave"` + fsutil.InodeNotSymlink `state:"nosave"` + fsutil.NoMappable `state:"nosave"` + fsutil.NoopWriteOut `state:"nosave"` + + // mu protects the fields below. + mu sync.Mutex `state:"nosave"` + + // uattr is the inode's UnstableAttr. + uattr fs.UnstableAttr +} + +// UnstableAttr implements fs.InodeOperations.UnstableAttr. +func (i *inodeOperations) UnstableAttr(ctx context.Context, inode *fs.Inode) (fs.UnstableAttr, error) { + i.mu.Lock() + defer i.mu.Unlock() + return i.uattr, nil +} + +// Check implements fs.InodeOperations.Check. +func (i *inodeOperations) Check(ctx context.Context, inode *fs.Inode, p fs.PermMask) bool { + return fs.ContextCanAccessFile(ctx, inode, p) +} + +// SetPermissions implements fs.InodeOperations.SetPermissions +func (i *inodeOperations) SetPermissions(ctx context.Context, inode *fs.Inode, p fs.FilePermissions) bool { + i.mu.Lock() + defer i.mu.Unlock() + i.uattr.Perms = p + i.uattr.StatusChangeTime = ktime.NowFromContext(ctx) + return true +} + +// SetOwner implements fs.InodeOperations.SetOwner. +func (i *inodeOperations) SetOwner(ctx context.Context, inode *fs.Inode, owner fs.FileOwner) error { + i.mu.Lock() + defer i.mu.Unlock() + if owner.UID.Ok() { + i.uattr.Owner.UID = owner.UID + } + if owner.GID.Ok() { + i.uattr.Owner.GID = owner.GID + } + return nil +} + +// SetTimestamps implements fs.InodeOperations.SetTimestamps. +func (i *inodeOperations) SetTimestamps(ctx context.Context, inode *fs.Inode, ts fs.TimeSpec) error { + if ts.ATimeOmit && ts.MTimeOmit { + return nil + } + + i.mu.Lock() + defer i.mu.Unlock() + + now := ktime.NowFromContext(ctx) + if !ts.ATimeOmit { + if ts.ATime.IsZero() { + i.uattr.AccessTime = now + } else { + i.uattr.AccessTime = ts.ATime + } + } + if !ts.MTimeOmit { + if ts.MTime.IsZero() { + i.uattr.ModificationTime = now + } else { + i.uattr.ModificationTime = ts.MTime + } + } + i.uattr.StatusChangeTime = now + return nil +} + +// Truncate implements fs.InodeOperations.Truncate. +func (i *inodeOperations) Truncate(ctx context.Context, inode *fs.Inode, size int64) error { + return syserror.EINVAL +} + +// AddLink implements fs.InodeOperations.AddLink. +func (i *inodeOperations) AddLink() { +} + +// DropLink implements fs.InodeOperations.DropLink. +func (i *inodeOperations) DropLink() { +} + +// NotifyStatusChange implements fs.InodeOperations.NotifyStatusChange. +func (i *inodeOperations) NotifyStatusChange(ctx context.Context) { + i.mu.Lock() + defer i.mu.Unlock() + i.uattr.StatusChangeTime = ktime.NowFromContext(ctx) +} + +// IsVirtual implements fs.InodeOperations.IsVirtual. +func (i *inodeOperations) IsVirtual() bool { + return true +} + +// StatFS implements fs.InodeOperations.StatFS. +func (i *inodeOperations) StatFS(ctx context.Context) (fs.Info, error) { + return fs.Info{ + Type: linux.DEVPTS_SUPER_MAGIC, + }, nil +} diff --git a/pkg/sentry/fs/tty/line_discipline.go b/pkg/sentry/fs/tty/line_discipline.go new file mode 100644 index 000000000..fde4e7941 --- /dev/null +++ b/pkg/sentry/fs/tty/line_discipline.go @@ -0,0 +1,342 @@ +// Copyright 2018 Google Inc. +// +// 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. + +package tty + +import ( + "bytes" + "sync" + "unicode/utf8" + + "gvisor.googlesource.com/gvisor/pkg/abi/linux" + "gvisor.googlesource.com/gvisor/pkg/sentry/arch" + "gvisor.googlesource.com/gvisor/pkg/sentry/context" + "gvisor.googlesource.com/gvisor/pkg/sentry/usermem" + "gvisor.googlesource.com/gvisor/pkg/syserror" + "gvisor.googlesource.com/gvisor/pkg/waiter" +) + +const ( + spacesPerTab = 8 +) + +// 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 +// pages are good resources for how to affect the line discipline: +// +// * termios(3) +// * tty_ioctl(4) +// +// This file corresponds most closely to drivers/tty/n_tty.c. +// +// lineDiscipline has a simple structure but supports a multitude of options +// (see the above man pages). It consists of two queues of bytes: one from the +// terminal master to slave (the input queue) and one from slave to master (the +// output queue). When bytes are written to one end of the pty, the line +// discipline reads the bytes, modifies them or takes special action if +// required, and enqueues them to be read by the other end of the pty: +// +// input from terminal +-------------+ input to process (e.g. bash) +// +------------------------>| input queue |---------------------------+ +// | +-------------+ | +// | | +// | v +// masterFD slaveFD +// ^ | +// | | +// | output to terminal +--------------+ output from process | +// +------------------------| output queue |<--------------------------+ +// +--------------+ +// +// Lock order: +// inMu +// outMu +// termiosMu +type lineDiscipline struct { + // inMu protects inQueue. + inMu sync.Mutex `state:"nosave"` + + // inQueue is the input queue of the terminal. + inQueue queue + + // outMu protects outQueue. + outMu sync.Mutex `state:"nosave"` + + // outQueue is the output queue of the terminal. + outQueue queue + + // termiosMu protects termios. + termiosMu sync.Mutex `state:"nosave"` + + // termios is the terminal configuration used by the lineDiscipline. + termios linux.KernelTermios + + // column is the location in a row of the cursor. This is important for + // handling certain special characters like backspace. + column int +} + +// getTermios gets the linux.Termios for the tty. +func (l *lineDiscipline) getTermios(ctx context.Context, io usermem.IO, args arch.SyscallArguments) (uintptr, error) { + l.termiosMu.Lock() + defer l.termiosMu.Unlock() + // We must copy a Termios struct, not KernelTermios. + t := l.termios.ToTermios() + _, err := usermem.CopyObjectOut(ctx, io, args[2].Pointer(), t, usermem.IOOpts{ + AddressSpaceActive: true, + }) + return 0, err +} + +// setTermios sets a linux.Termios for the tty. +func (l *lineDiscipline) setTermios(ctx context.Context, io usermem.IO, args arch.SyscallArguments) (uintptr, error) { + l.termiosMu.Lock() + defer l.termiosMu.Unlock() + // 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) + return 0, err +} + +func (l *lineDiscipline) masterReadiness() waiter.EventMask { + l.inMu.Lock() + defer l.inMu.Unlock() + l.outMu.Lock() + defer l.outMu.Unlock() + return l.inQueue.writeReadiness() | l.outQueue.readReadiness() +} + +func (l *lineDiscipline) slaveReadiness() waiter.EventMask { + l.inMu.Lock() + 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 +} + +// writeReadiness returns whether q is ready to be written to. +func (q *queue) writeReadiness() waiter.EventMask { + return waiter.EventOut +} + +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) +} + +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) +} + +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) +} + +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) +} + +// 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)) + + // 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 { + return 0, syserror.ErrWouldBlock + } + return int64(n), err +} + +// 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). +// +// Precondition: q's lock must be held. +func (l *lineDiscipline) queueWrite(ctx context.Context, src usermem.IOSequence, q *queue, output bool) (int64, error) { + // TODO: Use CopyInTo/safemem to avoid extra copying. + // Get the bytes to write from user-space. + b := make([]byte, src.NumBytes()) + n, err := src.CopyIn(ctx, b) + if err != nil { + return 0, err + } + b = 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 { + return 0, syserror.ErrWouldBlock + } + + // 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() + + // Enqueue buf at the end of the queue. + buf.WriteTo(&q.buf) + return int64(n), err +} + +// transformOutput does ouput processing for one end of the pty. See +// drivers/tty/n_tty.c:do_output_char for an analagous kernel function. +// +// Precondition: l.termiosMu must be held. +func (l *lineDiscipline) transformOutput(buf []byte) *bytes.Buffer { + if !l.termios.OEnabled(linux.OPOST) { + return bytes.NewBuffer(buf) + } + + var ret bytes.Buffer + for len(buf) > 0 { + c := l.removeRune(&buf) + switch c { + case '\n': + if l.termios.OEnabled(linux.ONLRET) { + l.column = 0 + } + if l.termios.OEnabled(linux.ONLCR) { + ret.Write([]byte{'\r', '\n'}) + continue + } + case '\r': + if l.termios.OEnabled(linux.ONOCR) && l.column == 0 { + continue + } + if l.termios.OEnabled(linux.OCRNL) { + c = '\n' + if l.termios.OEnabled(linux.ONLRET) { + l.column = 0 + } + break + } + l.column = 0 + case '\t': + spaces := spacesPerTab - l.column%spacesPerTab + if l.termios.OutputFlags&linux.TABDLY == linux.XTABS { + l.column += spaces + ret.Write(bytes.Repeat([]byte{' '}, 8)) + continue + } + l.column += spaces + case '\b': + if l.column > 0 { + l.column-- + } + default: + l.column++ + } + ret.WriteRune(c) + } + 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 +// 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) + switch c { + case '\r': + if l.termios.IEnabled(linux.IGNCR) { + continue + } + if l.termios.IEnabled(linux.ICRNL) { + c = '\n' + } + case '\n': + if l.termios.IEnabled(linux.INLCR) { + c = '\r' + } + } + ret.WriteRune(c) + } + return &ret +} + +// 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 { + 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) + } else { + c = rune((*b)[0]) + size = 1 + } + *b = (*b)[size:] + return c +} diff --git a/pkg/sentry/fs/tty/master.go b/pkg/sentry/fs/tty/master.go new file mode 100644 index 000000000..3c47ee517 --- /dev/null +++ b/pkg/sentry/fs/tty/master.go @@ -0,0 +1,173 @@ +// Copyright 2018 Google Inc. +// +// 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. + +package tty + +import ( + "gvisor.googlesource.com/gvisor/pkg/abi/linux" + "gvisor.googlesource.com/gvisor/pkg/sentry/arch" + "gvisor.googlesource.com/gvisor/pkg/sentry/context" + "gvisor.googlesource.com/gvisor/pkg/sentry/fs" + "gvisor.googlesource.com/gvisor/pkg/sentry/fs/fsutil" + "gvisor.googlesource.com/gvisor/pkg/sentry/usermem" + "gvisor.googlesource.com/gvisor/pkg/syserror" + "gvisor.googlesource.com/gvisor/pkg/waiter" +) + +// masterInodeOperations are the fs.InodeOperations for the master end of the +// Terminal (ptmx file). +type masterInodeOperations struct { + inodeOperations + + // d is the containing dir. + d *dirInodeOperations +} + +var _ fs.InodeOperations = (*masterInodeOperations)(nil) + +// newMasterInode creates an Inode for the master end of a terminal. +func newMasterInode(ctx context.Context, d *dirInodeOperations, owner fs.FileOwner, p fs.FilePermissions) *fs.Inode { + iops := &masterInodeOperations{ + inodeOperations: inodeOperations{ + uattr: fs.WithCurrentTime(ctx, fs.UnstableAttr{ + Owner: owner, + Perms: p, + Links: 1, + // Size and Blocks are always 0. + }), + }, + d: d, + } + + return fs.NewInode(iops, d.msrc, fs.StableAttr{ + DeviceID: ptsDevice.DeviceID(), + // N.B. Linux always uses inode id 2 for ptmx. See + // fs/devpts/inode.c:mknod_ptmx. + // + // TODO: Since ptsDevice must be shared between + // different mounts, we must not assign fixed numbers. + InodeID: ptsDevice.NextIno(), + Type: fs.CharacterDevice, + // See fs/devpts/inode.c:devpts_fill_super. + BlockSize: 1024, + // The PTY master effectively has two different major/minor + // device numbers. + // + // This one is returned by stat for both opened and unopened + // instances of this inode. + // + // When the inode is opened (GetFile), a new device number is + // allocated based on major UNIX98_PTY_MASTER_MAJOR and the tty + // index as minor number. However, this device number is only + // accessible via ioctl(TIOCGDEV) and /proc/TID/stat. + DeviceFileMajor: linux.TTYAUX_MAJOR, + DeviceFileMinor: linux.PTMX_MINOR, + }) +} + +// Release implements fs.InodeOperations.Release. +func (mi *masterInodeOperations) Release(ctx context.Context) { +} + +// GetFile implements fs.InodeOperations.GetFile. +// +// It allocates a new terminal. +func (mi *masterInodeOperations) GetFile(ctx context.Context, d *fs.Dirent, flags fs.FileFlags) (*fs.File, error) { + t, err := mi.d.allocateTerminal(ctx) + if err != nil { + return nil, err + } + + return fs.NewFile(ctx, d, flags, &masterFileOperations{ + d: mi.d, + t: t, + }), nil +} + +// masterFileOperations are the fs.FileOperations for the master end of a terminal. +type masterFileOperations struct { + fsutil.PipeSeek `state:"nosave"` + fsutil.NotDirReaddir `state:"nosave"` + fsutil.NoFsync `state:"nosave"` + fsutil.NoopFlush `state:"nosave"` + fsutil.NoMMap `state:"nosave"` + + // d is the containing dir. + d *dirInodeOperations + + // t is the connected Terminal. + t *Terminal +} + +var _ fs.FileOperations = (*masterFileOperations)(nil) + +// Release implements fs.FileOperations.Release. +func (mf *masterFileOperations) Release() { + mf.d.masterClose(mf.t) + mf.t.DecRef() +} + +// EventRegister implements waiter.Waitable.EventRegister. +func (mf *masterFileOperations) EventRegister(e *waiter.Entry, mask waiter.EventMask) { + mf.t.ld.inQueue.EventRegister(e, mask) + mf.t.ld.outQueue.EventRegister(e, mask) +} + +// EventUnregister implements waiter.Waitable.EventUnregister. +func (mf *masterFileOperations) EventUnregister(e *waiter.Entry) { + mf.t.ld.inQueue.EventUnregister(e) + mf.t.ld.outQueue.EventUnregister(e) +} + +// Readiness implements waiter.Waitable.Readiness. +func (mf *masterFileOperations) Readiness(mask waiter.EventMask) waiter.EventMask { + return mf.t.ld.masterReadiness() +} + +// Read implements fs.FileOperations.Read. +func (mf *masterFileOperations) Read(ctx context.Context, _ *fs.File, dst usermem.IOSequence, _ int64) (int64, error) { + return mf.t.ld.outputQueueRead(ctx, dst) +} + +// Write implements fs.FileOperations.Write. +func (mf *masterFileOperations) Write(ctx context.Context, _ *fs.File, src usermem.IOSequence, _ int64) (int64, error) { + return mf.t.ld.inputQueueWrite(ctx, src) +} + +// 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.TCGETS: + // N.B. TCGETS on the master actually returns the configuration + // of the slave end. + return mf.t.ld.getTermios(ctx, io, args) + case linux.TCSETS: + // N.B. TCSETS on the master actually affects the configuration + // of the slave end. + return mf.t.ld.setTermios(ctx, io, args) + case linux.TCSETSW: + // TODO: This should drain the output queue first. + return mf.t.ld.setTermios(ctx, io, args) + case linux.TIOCGPTN: + _, err := usermem.CopyObjectOut(ctx, io, args[2].Pointer(), uint32(mf.t.n), usermem.IOOpts{ + AddressSpaceActive: true, + }) + return 0, err + case linux.TIOCSPTLCK: + // TODO: Implement pty locking. For now just pretend we do. + return 0, nil + default: + return 0, syserror.ENOTTY + } +} diff --git a/pkg/sentry/fs/tty/slave.go b/pkg/sentry/fs/tty/slave.go new file mode 100644 index 000000000..9178071a4 --- /dev/null +++ b/pkg/sentry/fs/tty/slave.go @@ -0,0 +1,151 @@ +// Copyright 2018 Google Inc. +// +// 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. + +package tty + +import ( + "gvisor.googlesource.com/gvisor/pkg/abi/linux" + "gvisor.googlesource.com/gvisor/pkg/sentry/arch" + "gvisor.googlesource.com/gvisor/pkg/sentry/context" + "gvisor.googlesource.com/gvisor/pkg/sentry/fs" + "gvisor.googlesource.com/gvisor/pkg/sentry/fs/fsutil" + "gvisor.googlesource.com/gvisor/pkg/sentry/usermem" + "gvisor.googlesource.com/gvisor/pkg/syserror" + "gvisor.googlesource.com/gvisor/pkg/waiter" +) + +// slaveInodeOperations are the fs.InodeOperations for the slave end of the +// Terminal (pts file). +type slaveInodeOperations struct { + inodeOperations + + // d is the containing dir. + d *dirInodeOperations + + // t is the connected Terminal. + t *Terminal +} + +var _ fs.InodeOperations = (*slaveInodeOperations)(nil) + +// newSlaveInode creates an fs.Inode for the slave end of a terminal. +// +// newSlaveInode takes ownership of t. +func newSlaveInode(ctx context.Context, d *dirInodeOperations, t *Terminal, owner fs.FileOwner, p fs.FilePermissions) *fs.Inode { + iops := &slaveInodeOperations{ + inodeOperations: inodeOperations{ + uattr: fs.WithCurrentTime(ctx, fs.UnstableAttr{ + Owner: owner, + Perms: p, + Links: 1, + // Size and Blocks are always 0. + }), + }, + d: d, + t: t, + } + + return fs.NewInode(iops, d.msrc, fs.StableAttr{ + DeviceID: ptsDevice.DeviceID(), + // N.B. Linux always uses inode id = tty index + 3. See + // fs/devpts/inode.c:devpts_pty_new. + // + // TODO: Since ptsDevice must be shared between + // different mounts, we must not assign fixed numbers. + InodeID: ptsDevice.NextIno(), + Type: fs.CharacterDevice, + // See fs/devpts/inode.c:devpts_fill_super. + BlockSize: 1024, + DeviceFileMajor: linux.UNIX98_PTY_SLAVE_MAJOR, + DeviceFileMinor: t.n, + }) +} + +// Release implements fs.InodeOperations.Release. +func (si *slaveInodeOperations) Release(ctx context.Context) { + si.t.DecRef() +} + +// GetFile implements fs.InodeOperations.GetFile. +// +// This may race with destruction of the terminal. If the terminal is gone, it +// returns ENOENT. +func (si *slaveInodeOperations) GetFile(ctx context.Context, d *fs.Dirent, flags fs.FileFlags) (*fs.File, error) { + return fs.NewFile(ctx, d, flags, &slaveFileOperations{si: si}), nil +} + +// slaveFileOperations are the fs.FileOperations for the slave end of a terminal. +type slaveFileOperations struct { + fsutil.PipeSeek `state:"nosave"` + fsutil.NotDirReaddir `state:"nosave"` + fsutil.NoFsync `state:"nosave"` + fsutil.NoopFlush `state:"nosave"` + fsutil.NoMMap `state:"nosave"` + + // si is the inode operations. + si *slaveInodeOperations +} + +var _ fs.FileOperations = (*slaveFileOperations)(nil) + +// Release implements fs.FileOperations.Release. +func (sf *slaveFileOperations) Release() { +} + +// EventRegister implements waiter.Waitable.EventRegister. +func (sf *slaveFileOperations) EventRegister(e *waiter.Entry, mask waiter.EventMask) { + sf.si.t.ld.outQueue.EventRegister(e, mask) + sf.si.t.ld.inQueue.EventRegister(e, mask) +} + +// EventUnregister implements waiter.Waitable.EventUnregister. +func (sf *slaveFileOperations) EventUnregister(e *waiter.Entry) { + sf.si.t.ld.outQueue.EventUnregister(e) + sf.si.t.ld.inQueue.EventUnregister(e) +} + +// Readiness implements waiter.Waitable.Readiness. +func (sf *slaveFileOperations) Readiness(mask waiter.EventMask) waiter.EventMask { + return sf.si.t.ld.slaveReadiness() +} + +// Read implements fs.FileOperations.Read. +func (sf *slaveFileOperations) Read(ctx context.Context, _ *fs.File, dst usermem.IOSequence, _ int64) (int64, error) { + return sf.si.t.ld.inputQueueRead(ctx, dst) +} + +// Write implements fs.FileOperations.Write. +func (sf *slaveFileOperations) Write(ctx context.Context, _ *fs.File, src usermem.IOSequence, _ int64) (int64, error) { + return sf.si.t.ld.outputQueueWrite(ctx, src) +} + +// 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.TCGETS: + return sf.si.t.ld.getTermios(ctx, io, args) + case linux.TCSETS: + return sf.si.t.ld.setTermios(ctx, io, args) + case linux.TCSETSW: + // TODO: This should drain the output queue first. + return sf.si.t.ld.setTermios(ctx, io, args) + case linux.TIOCGPTN: + _, err := usermem.CopyObjectOut(ctx, io, args[2].Pointer(), uint32(sf.si.t.n), usermem.IOOpts{ + AddressSpaceActive: true, + }) + return 0, err + default: + return 0, syserror.ENOTTY + } +} diff --git a/pkg/sentry/fs/tty/terminal.go b/pkg/sentry/fs/tty/terminal.go new file mode 100644 index 000000000..6ae713a32 --- /dev/null +++ b/pkg/sentry/fs/tty/terminal.go @@ -0,0 +1,44 @@ +// Copyright 2018 Google Inc. +// +// 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. + +package tty + +import ( + "gvisor.googlesource.com/gvisor/pkg/abi/linux" + "gvisor.googlesource.com/gvisor/pkg/refs" + "gvisor.googlesource.com/gvisor/pkg/sentry/context" +) + +// Terminal is a pseudoterminal. +type Terminal struct { + refs.AtomicRefCount + + // n is the terminal index. + n uint32 + + // d is the containing directory. + d *dirInodeOperations + + // ld is the line discipline of the terminal. + ld lineDiscipline +} + +func newTerminal(ctx context.Context, d *dirInodeOperations, n uint32) *Terminal { + termios := linux.DefaultSlaveTermios + return &Terminal{ + d: d, + n: n, + ld: lineDiscipline{termios: termios}, + } +} diff --git a/pkg/sentry/fs/tty/tty_test.go b/pkg/sentry/fs/tty/tty_test.go new file mode 100644 index 000000000..0c7560ed7 --- /dev/null +++ b/pkg/sentry/fs/tty/tty_test.go @@ -0,0 +1,56 @@ +// Copyright 2018 Google Inc. +// +// 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. + +package tty + +import ( + "testing" + + "gvisor.googlesource.com/gvisor/pkg/abi/linux" + "gvisor.googlesource.com/gvisor/pkg/sentry/context/contexttest" + "gvisor.googlesource.com/gvisor/pkg/sentry/usermem" +) + +func TestSimpleMasterToSlave(t *testing.T) { + ld := lineDiscipline{termios: linux.DefaultSlaveTermios} + ctx := contexttest.Context(t) + inBytes := []byte("hello, tty\n") + src := usermem.BytesIOSequence(inBytes) + outBytes := make([]byte, 32) + dst := usermem.BytesIOSequence(outBytes) + + // Write to the input queue. + nw, err := ld.inputQueueWrite(ctx, src) + if err != nil { + t.Fatalf("error writing to input queue: %v", err) + } + if nw != int64(len(inBytes)) { + t.Fatalf("wrote wrong length: got %d, want %d", nw, len(inBytes)) + } + + // Read from the input queue. + nr, err := ld.inputQueueRead(ctx, dst) + if err != nil { + t.Fatalf("error reading from input queue: %v", err) + } + if nr != int64(len(inBytes)) { + t.Fatalf("read wrong length: got %d, want %d", nr, len(inBytes)) + } + + outStr := string(outBytes[:nr]) + inStr := string(inBytes) + if outStr != inStr { + t.Fatalf("written and read strings do not match: got %q, want %q", outStr, inStr) + } +} |