runsc: Terminal support for "docker exec -ti".

This CL adds terminal support for "docker exec".  We previously only supported
consoles for the container process, but not exec processes.

The SYS_IOCTL syscall was added to the default seccomp filter list, but only
for ioctls that get/set winsize and termios structs. We need to allow these
ioctl for all containers because it's possible to run "exec -ti" on a
container that was started without an attached console, after the filters
have been installed.

Note that control-character signals are still not properly supported.

Tested with:
	$ docker run --runtime=runsc -it alpine
In another terminial:
	$ docker exec -it <containerid> /bin/sh

PiperOrigin-RevId: 210185456
Change-Id: I6d2401e53a7697bb988c120a8961505c335f96d9
This commit is contained in:
Nicolas Lacasse
2018-08-24 17:43:21 -07:00
committed by Shentubot
parent c48708a041
commit 106de2182d
17 changed files with 151 additions and 56 deletions
+5 -1
View File
@@ -21,8 +21,12 @@ const (
TCGETS = 0x00005401
TCSETS = 0x00005402
TCSETSW = 0x00005403
TIOCINQ = 0x0000541b
TIOCGPGRP = 0x0000540f
TIOCSPGRP = 0x00005410
TIOCOUTQ = 0x00005411
TIOCGWINSZ = 0x00005413
TIOCSWINSZ = 0x00005414
TIOCINQ = 0x0000541b
FIONREAD = TIOCINQ
FIONBIO = 0x00005421
TIOCGPTN = 0x80045430
+8
View File
@@ -26,6 +26,14 @@ const (
disabledChar = 0
)
// Winsize is struct winsize, defined in uapi/asm-generic/termios.h.
type Winsize struct {
Row uint16
Col uint16
Xpixel uint16
Ypixel uint16
}
// Termios is struct termios, defined in uapi/asm-generic/termbits.h.
type Termios struct {
InputFlags uint32
+7 -10
View File
@@ -19,7 +19,6 @@ import (
"encoding/json"
"fmt"
"sort"
"syscall"
"text/tabwriter"
"time"
@@ -73,6 +72,10 @@ type ExecArgs struct {
// Capabilities is the list of capabilities to give to the process.
Capabilities *auth.TaskCapabilities
// StdioIsPty indicates that FDs 0, 1, and 2 are connected to a host
// pty fd.
StdioIsPty bool
// FilePayload determines the files to give to the new process.
urpc.FilePayload
}
@@ -108,17 +111,11 @@ func (proc *Proc) Exec(args *ExecArgs, waitStatus *uint32) error {
mounter := fs.FileOwnerFromContext(ctx)
for appFD, f := range args.FilePayload.Files {
// Copy the underlying FD.
newFD, err := syscall.Dup(int(f.Fd()))
if err != nil {
return err
}
f.Close()
enableIoctl := args.StdioIsPty && appFD <= 2
// Install the given file as an FD.
file, err := host.NewFile(ctx, newFD, mounter)
// Import the given file FD. This dups the FD as well.
file, err := host.ImportFile(ctx, int(f.Fd()), mounter, enableIoctl)
if err != nil {
syscall.Close(newFD)
return err
}
defer file.DecRef()
-1
View File
@@ -48,7 +48,6 @@ go_library(
"//pkg/unet",
"//pkg/waiter",
"//pkg/waiter/fdnotifier",
"@org_golang_x_sys//unix:go_default_library",
],
)
+9 -10
View File
@@ -18,7 +18,6 @@ import (
"fmt"
"syscall"
"golang.org/x/sys/unix"
"gvisor.googlesource.com/gvisor/pkg/abi/linux"
"gvisor.googlesource.com/gvisor/pkg/fd"
"gvisor.googlesource.com/gvisor/pkg/log"
@@ -296,7 +295,7 @@ func (f *fileOperations) Ioctl(ctx context.Context, io usermem.IO, args arch.Sys
fd := f.iops.fileState.FD()
ioctl := args[1].Uint64()
switch ioctl {
case unix.TCGETS:
case linux.TCGETS:
termios, err := ioctlGetTermios(fd)
if err != nil {
return 0, err
@@ -306,7 +305,7 @@ func (f *fileOperations) Ioctl(ctx context.Context, io usermem.IO, args arch.Sys
})
return 0, err
case unix.TCSETS, unix.TCSETSW:
case linux.TCSETS, linux.TCSETSW:
var termios linux.Termios
if _, err := usermem.CopyObjectIn(ctx, io, args[2].Pointer(), &termios, usermem.IOOpts{
AddressSpaceActive: true,
@@ -316,7 +315,7 @@ func (f *fileOperations) Ioctl(ctx context.Context, io usermem.IO, args arch.Sys
err := ioctlSetTermios(fd, ioctl, &termios)
return 0, err
case unix.TIOCGPGRP:
case linux.TIOCGPGRP:
// Args: pid_t *argp
// When successful, equivalent to *argp = tcgetpgrp(fd).
// Get the process group ID of the foreground process group on
@@ -332,7 +331,7 @@ func (f *fileOperations) Ioctl(ctx context.Context, io usermem.IO, args arch.Sys
})
return 0, err
case unix.TIOCSPGRP:
case linux.TIOCSPGRP:
// Args: const pid_t *argp
// Equivalent to tcsetpgrp(fd, *argp).
// Set the foreground process group ID of this terminal.
@@ -343,10 +342,10 @@ func (f *fileOperations) Ioctl(ctx context.Context, io usermem.IO, args arch.Sys
log.Warningf("Ignoring application ioctl(TIOCSPGRP) call")
return 0, nil
case unix.TIOCGWINSZ:
case linux.TIOCGWINSZ:
// Args: struct winsize *argp
// Get window size.
winsize, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ)
winsize, err := ioctlGetWinsize(fd)
if err != nil {
return 0, err
}
@@ -355,16 +354,16 @@ func (f *fileOperations) Ioctl(ctx context.Context, io usermem.IO, args arch.Sys
})
return 0, err
case unix.TIOCSWINSZ:
case linux.TIOCSWINSZ:
// Args: const struct winsize *argp
// Set window size.
var winsize unix.Winsize
var winsize linux.Winsize
if _, err := usermem.CopyObjectIn(ctx, io, args[2].Pointer(), &winsize, usermem.IOOpts{
AddressSpaceActive: true,
}); err != nil {
return 0, err
}
err := unix.IoctlSetWinsize(fd, unix.TIOCSWINSZ, &winsize)
err := ioctlSetWinsize(fd, &winsize)
return 0, err
default:
+18 -1
View File
@@ -23,7 +23,7 @@ import (
func ioctlGetTermios(fd int) (*linux.Termios, error) {
var t linux.Termios
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), syscall.TCGETS, uintptr(unsafe.Pointer(&t)))
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), linux.TCGETS, uintptr(unsafe.Pointer(&t)))
if errno != 0 {
return nil, errno
}
@@ -37,3 +37,20 @@ func ioctlSetTermios(fd int, req uint64, t *linux.Termios) error {
}
return nil
}
func ioctlGetWinsize(fd int) (*linux.Winsize, error) {
var w linux.Winsize
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), linux.TIOCGWINSZ, uintptr(unsafe.Pointer(&w)))
if errno != 0 {
return nil, errno
}
return &w, nil
}
func ioctlSetWinsize(fd int, w *linux.Winsize) error {
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), linux.TIOCSWINSZ, uintptr(unsafe.Pointer(w)))
if errno != 0 {
return errno
}
return nil
}
+1 -1
View File
@@ -227,7 +227,7 @@ func (cm *containerManager) Start(args *StartArgs, _ *struct{}) error {
// Execute runs a command on a created or running sandbox.
func (cm *containerManager) Execute(e *control.ExecArgs, waitStatus *uint32) error {
log.Debugf("containerManager.Execute")
log.Debugf("containerManager.Execute: %+v", *e)
proc := control.Proc{Kernel: cm.l.k}
if err := proc.Exec(e, waitStatus); err != nil {
return fmt.Errorf("error executing: %+v: %v", e, err)
+1
View File
@@ -18,6 +18,7 @@ go_library(
"//runsc/boot:__subpackages__",
],
deps = [
"//pkg/abi/linux",
"//pkg/log",
"//pkg/seccomp",
"//pkg/sentry/platform",
+30 -8
View File
@@ -18,6 +18,7 @@ import (
"syscall"
"golang.org/x/sys/unix"
"gvisor.googlesource.com/gvisor/pkg/abi/linux"
"gvisor.googlesource.com/gvisor/pkg/seccomp"
)
@@ -78,15 +79,36 @@ var allowedSyscalls = seccomp.SyscallRules{
syscall.SYS_TGKILL: {},
syscall.SYS_WRITE: {},
syscall.SYS_WRITEV: {},
}
// TODO: Ioctl is needed in order to support tty consoles.
// Once filters support argument-checking, we should only allow ioctl
// with tty-related arguments.
func consoleFilters() seccomp.SyscallRules {
return seccomp.SyscallRules{
syscall.SYS_IOCTL: {},
}
// SYS_IOCTL is needed for terminal support, but we only allow
// setting/getting termios and winsize.
syscall.SYS_IOCTL: []seccomp.Rule{
{
seccomp.AllowAny{}, /* fd */
seccomp.AllowValue(linux.TCGETS),
seccomp.AllowAny{}, /* termios struct */
},
{
seccomp.AllowAny{}, /* fd */
seccomp.AllowValue(linux.TCSETS),
seccomp.AllowAny{}, /* termios struct */
},
{
seccomp.AllowAny{}, /* fd */
seccomp.AllowValue(linux.TCSETSW),
seccomp.AllowAny{}, /* termios struct */
},
{
seccomp.AllowAny{}, /* fd */
seccomp.AllowValue(linux.TIOCSWINSZ),
seccomp.AllowAny{}, /* winsize struct */
},
{
seccomp.AllowAny{}, /* fd */
seccomp.AllowValue(linux.TIOCGWINSZ),
seccomp.AllowAny{}, /* winsize struct */
},
},
}
// whitelistFSFilters returns syscalls made by whitelistFS. Using WhitelistFS
+1 -5
View File
@@ -28,7 +28,7 @@ import (
)
// Install installs seccomp filters for based on the given platform.
func Install(p platform.Platform, whitelistFS, console, hostNetwork bool) error {
func Install(p platform.Platform, whitelistFS, hostNetwork bool) error {
s := allowedSyscalls
// Set of additional filters used by -race and -msan. Returns empty
@@ -39,10 +39,6 @@ func Install(p platform.Platform, whitelistFS, console, hostNetwork bool) error
Report("direct file access allows unrestricted file access!")
s.Merge(whitelistFSFilters())
}
if console {
Report("console is enabled: syscall filters less restrictive!")
s.Merge(consoleFilters())
}
if hostNetwork {
Report("host networking enabled: syscall filters less restrictive!")
s.Merge(hostInetFilters())
+1 -1
View File
@@ -340,7 +340,7 @@ func (l *Loader) run() error {
} else {
whitelistFS := l.conf.FileAccess == FileAccessDirect
hostNet := l.conf.Network == NetworkHost
if err := filter.Install(l.k.Platform, whitelistFS, l.console, hostNet); err != nil {
if err := filter.Install(l.k.Platform, whitelistFS, hostNet); err != nil {
return fmt.Errorf("Failed to install seccomp filters: %v", err)
}
}
+1
View File
@@ -38,6 +38,7 @@ go_library(
"//pkg/sentry/kernel/auth",
"//pkg/urpc",
"//runsc/boot",
"//runsc/console",
"//runsc/container",
"//runsc/fsgofer",
"//runsc/specutils",
+36 -3
View File
@@ -35,6 +35,7 @@ import (
"gvisor.googlesource.com/gvisor/pkg/sentry/kernel/auth"
"gvisor.googlesource.com/gvisor/pkg/urpc"
"gvisor.googlesource.com/gvisor/runsc/boot"
"gvisor.googlesource.com/gvisor/runsc/console"
"gvisor.googlesource.com/gvisor/runsc/container"
"gvisor.googlesource.com/gvisor/runsc/specutils"
)
@@ -50,6 +51,11 @@ type Exec struct {
detach bool
processPath string
pidFile string
// consoleSocket is the path to an AF_UNIX socket which will receive a
// file descriptor referencing the master end of the console's
// pseudoterminal.
consoleSocket string
}
// Name implements subcommands.Command.Name.
@@ -91,6 +97,7 @@ func (ex *Exec) SetFlags(f *flag.FlagSet) {
f.BoolVar(&ex.detach, "detach", false, "detach from the container's process")
f.StringVar(&ex.processPath, "process", "", "path to the process.json")
f.StringVar(&ex.pidFile, "pid-file", "", "filename that the container pid will be written to")
f.StringVar(&ex.consoleSocket, "console-socket", "", "path to an AF_UNIX socket which will receive a file descriptor referencing the master end of the console's pseudoterminal")
}
// Execute implements subcommands.Command.Execute. It starts a process in an
@@ -178,11 +185,35 @@ func (ex *Exec) execAndWait(waitStatus *syscall.WaitStatus) subcommands.ExitStat
args = append(args, a)
}
}
cmd := exec.Command(binPath, args...)
// Exec stdio defaults to current process stdio.
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// If the console control socket file is provided, then create a new
// pty master/slave pair and set the tty on the sandbox process.
if ex.consoleSocket != "" {
// Create a new tty pair and send the master on the provided
// socket.
tty, err := console.NewWithSocket(ex.consoleSocket)
if err != nil {
Fatalf("error setting up console with socket %q: %v", ex.consoleSocket, err)
}
defer tty.Close()
// Set stdio to the new tty slave.
cmd.Stdin = tty
cmd.Stdout = tty
cmd.Stderr = tty
cmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true,
Setctty: true,
Ctty: int(tty.Fd()),
}
}
if err := cmd.Start(); err != nil {
Fatalf("failure to start child exec process, err: %v", err)
}
@@ -252,11 +283,12 @@ func (ex *Exec) argsFromCLI(argv []string) (*control.ExecArgs, error) {
return &control.ExecArgs{
Argv: argv,
WorkingDirectory: ex.cwd,
FilePayload: urpc.FilePayload{Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}},
KUID: ex.user.kuid,
KGID: ex.user.kgid,
ExtraKGIDs: extraKGIDs,
Capabilities: caps,
StdioIsPty: ex.consoleSocket != "",
FilePayload: urpc.FilePayload{[]*os.File{os.Stdin, os.Stdout, os.Stderr}},
}, nil
}
@@ -292,11 +324,12 @@ func argsFromProcess(p *specs.Process) (*control.ExecArgs, error) {
Argv: p.Args,
Envv: p.Env,
WorkingDirectory: p.Cwd,
FilePayload: urpc.FilePayload{Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}},
KUID: auth.KUID(p.User.UID),
KGID: auth.KGID(p.User.GID),
ExtraKGIDs: extraKGIDs,
Capabilities: caps,
StdioIsPty: p.Terminal,
FilePayload: urpc.FilePayload{Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}},
}, nil
}
+16
View File
@@ -0,0 +1,16 @@
package(licenses = ["notice"]) # Apache 2.0
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "console",
srcs = ["console.go"],
importpath = "gvisor.googlesource.com/gvisor/runsc/console",
visibility = [
"//runsc:__subpackages__",
],
deps = [
"@com_github_kr_pty//:go_default_library",
"@org_golang_x_sys//unix:go_default_library",
],
)
@@ -12,7 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package sandbox
// Package console contains utilities for working with pty consols in runsc.
package console
import (
"fmt"
@@ -23,9 +24,9 @@ import (
"golang.org/x/sys/unix"
)
// setupConsole creates pty master/slave pair, sends the master FD over the
// given socket, and returns the slave.
func setupConsole(socketPath string) (*os.File, error) {
// NewWithSocket creates pty master/slave pair, sends the master FD over the given
// socket, and returns the slave.
func NewWithSocket(socketPath string) (*os.File, error) {
// Create a new pty master and slave.
ptyMaster, ptySlave, err := pty.Open()
if err != nil {
+1 -2
View File
@@ -5,7 +5,6 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "sandbox",
srcs = [
"console.go",
"namespace.go",
"network.go",
"sandbox.go",
@@ -21,9 +20,9 @@ go_library(
"//pkg/sentry/control",
"//pkg/urpc",
"//runsc/boot",
"//runsc/console",
"//runsc/fsgofer",
"//runsc/specutils",
"@com_github_kr_pty//:go_default_library",
"@com_github_opencontainers_runtime-spec//specs-go:go_default_library",
"@com_github_vishvananda_netlink//:go_default_library",
"@org_golang_x_sys//unix:go_default_library",
+11 -9
View File
@@ -31,6 +31,7 @@ import (
"gvisor.googlesource.com/gvisor/pkg/sentry/control"
"gvisor.googlesource.com/gvisor/pkg/urpc"
"gvisor.googlesource.com/gvisor/runsc/boot"
"gvisor.googlesource.com/gvisor/runsc/console"
"gvisor.googlesource.com/gvisor/runsc/fsgofer"
"gvisor.googlesource.com/gvisor/runsc/specutils"
)
@@ -392,7 +393,7 @@ func (s *Sandbox) createSandboxProcess(spec *specs.Spec, conf *boot.Config, bund
"boot",
"--bundle", bundleDir,
"--controller-fd="+strconv.Itoa(nextFD),
fmt.Sprintf("--console=%t", consoleEnabled))
"--console="+strconv.FormatBool(consoleEnabled))
nextFD++
controllerFile := os.NewFile(uintptr(fd), "control_server_socket")
@@ -407,14 +408,19 @@ func (s *Sandbox) createSandboxProcess(spec *specs.Spec, conf *boot.Config, bund
nextFD++
}
// Sandbox stdio defaults to current process stdio.
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// If the console control socket file is provided, then create a new
// pty master/slave pair and set the tty on the sandbox process.
if consoleEnabled {
// setupConsole will send the master on the socket, and return
// the slave.
tty, err := setupConsole(consoleSocket)
// console.NewWithSocket will send the master on the socket,
// and return the slave.
tty, err := console.NewWithSocket(consoleSocket)
if err != nil {
return fmt.Errorf("error setting up control socket %q: %v", consoleSocket, err)
return fmt.Errorf("error setting up console with socket %q: %v", consoleSocket, err)
}
defer tty.Close()
@@ -423,10 +429,6 @@ func (s *Sandbox) createSandboxProcess(spec *specs.Spec, conf *boot.Config, bund
cmd.Stderr = tty
cmd.SysProcAttr.Setctty = true
cmd.SysProcAttr.Ctty = int(tty.Fd())
} else {
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
// Detach from this session, otherwise cmd will get SIGHUP and SIGCONT