diff options
author | Nicolas Lacasse <nlacasse@google.com> | 2018-10-01 22:05:41 -0700 |
---|---|---|
committer | Shentubot <shentubot@google.com> | 2018-10-01 22:06:56 -0700 |
commit | f1c01ed88666ea81d8f5cef7931153a9951a6e64 (patch) | |
tree | 796b9812ddda2d7b9866225dabb4b94b058c420b /pkg/sentry/control | |
parent | 0400e5459288592768af12ab71609c6df6afe3d7 (diff) |
runsc: Support job control signals in "exec -it".
Terminal support in runsc relies on host tty file descriptors that are imported
into the sandbox. Application tty ioctls are sent directly to the host fd.
However, those host tty ioctls are associated in the host kernel with a host
process (in this case runsc), and the host kernel intercepts job control
characters like ^C and send signals to the host process. Thus, typing ^C into a
"runsc exec" shell will send a SIGINT to the runsc process.
This change makes "runsc exec" handle all signals, and forward them into the
sandbox via the "ContainerSignal" urpc method. Since the "runsc exec" is
associated with a particular container process in the sandbox, the signal must
be associated with the same container process.
One big difficulty is that the signal should not necessarily be sent to the
sandbox process started by "exec", but instead must be sent to the foreground
process group for the tty. For example, we may exec "bash", and from bash call
"sleep 100". A ^C at this point should SIGINT sleep, not bash.
To handle this, tty files inside the sandbox must keep track of their
foreground process group, which is set/get via ioctls. When an incoming
ContainerSignal urpc comes in, we look up the foreground process group via the
tty file. Unfortunately, this means we have to expose and cache the tty file in
the Loader.
Note that "runsc exec" now handles signals properly, but "runs run" does not.
That will come in a later CL, as this one is complex enough already.
Example:
root@:/usr/local/apache2# sleep 100
^C
root@:/usr/local/apache2# sleep 100
^Z
[1]+ Stopped sleep 100
root@:/usr/local/apache2# fg
sleep 100
^C
root@:/usr/local/apache2#
PiperOrigin-RevId: 215334554
Change-Id: I53cdce39653027908510a5ba8d08c49f9cf24f39
Diffstat (limited to 'pkg/sentry/control')
-rw-r--r-- | pkg/sentry/control/proc.go | 78 |
1 files changed, 59 insertions, 19 deletions
diff --git a/pkg/sentry/control/proc.go b/pkg/sentry/control/proc.go index faf1168bb..0ba730c1e 100644 --- a/pkg/sentry/control/proc.go +++ b/pkg/sentry/control/proc.go @@ -78,7 +78,7 @@ type ExecArgs struct { Capabilities *auth.TaskCapabilities // StdioIsPty indicates that FDs 0, 1, and 2 are connected to a host - // pty fd. + // pty FD. StdioIsPty bool // FilePayload determines the files to give to the new process. @@ -90,7 +90,7 @@ type ExecArgs struct { // Exec runs a new task. func (proc *Proc) Exec(args *ExecArgs, waitStatus *uint32) error { - newTG, _, err := proc.execAsync(args) + newTG, _, _, err := proc.execAsync(args) if err != nil { return err } @@ -103,18 +103,27 @@ func (proc *Proc) Exec(args *ExecArgs, waitStatus *uint32) error { // ExecAsync runs a new task, but doesn't wait for it to finish. It is defined // as a function rather than a method to avoid exposing execAsync as an RPC. -func ExecAsync(proc *Proc, args *ExecArgs) (*kernel.ThreadGroup, kernel.ThreadID, error) { +func ExecAsync(proc *Proc, args *ExecArgs) (*kernel.ThreadGroup, kernel.ThreadID, *host.TTYFileOperations, error) { return proc.execAsync(args) } // execAsync runs a new task, but doesn't wait for it to finish. It returns the -// newly created thread group and its PID. -func (proc *Proc) execAsync(args *ExecArgs) (*kernel.ThreadGroup, kernel.ThreadID, error) { +// newly created thread group and its PID. If the stdio FDs are TTYs, then a +// TTYFileOperations that wraps the TTY is also returned. +func (proc *Proc) execAsync(args *ExecArgs) (*kernel.ThreadGroup, kernel.ThreadID, *host.TTYFileOperations, error) { // Import file descriptors. l := limits.NewLimitSet() fdm := proc.Kernel.NewFDMap() defer fdm.DecRef() + // No matter what happens, we should close all files in the FilePayload + // before returning. Any files that are imported will be duped. + defer func() { + for _, f := range args.FilePayload.Files { + f.Close() + } + }() + creds := auth.NewUserCredentials( args.KUID, args.KGID, @@ -150,31 +159,62 @@ func (proc *Proc) execAsync(args *ExecArgs) (*kernel.ThreadGroup, kernel.ThreadI paths := fs.GetPath(initArgs.Envv) f, err := proc.Kernel.RootMountNamespace().ResolveExecutablePath(ctx, initArgs.WorkingDirectory, initArgs.Argv[0], paths) if err != nil { - return nil, 0, fmt.Errorf("error finding executable %q in PATH %v: %v", initArgs.Argv[0], paths, err) + return nil, 0, nil, fmt.Errorf("error finding executable %q in PATH %v: %v", initArgs.Argv[0], paths, err) } initArgs.Filename = f } mounter := fs.FileOwnerFromContext(ctx) - for appFD, f := range args.FilePayload.Files { - enableIoctl := args.StdioIsPty && appFD <= 2 - // Import the given file FD. This dups the FD as well. - file, err := host.ImportFile(ctx, int(f.Fd()), mounter, enableIoctl) - if err != nil { - return nil, 0, err + var ttyFile *fs.File + for appFD, hostFile := range args.FilePayload.Files { + var appFile *fs.File + + if args.StdioIsPty && appFD < 3 { + // Import the file as a host TTY file. + if ttyFile == nil { + var err error + appFile, err = host.ImportFile(ctx, int(hostFile.Fd()), mounter, true /* isTTY */) + if err != nil { + return nil, 0, nil, err + } + defer appFile.DecRef() + + // Remember this in the TTY file, as we will + // use it for the other stdio FDs. + ttyFile = appFile + } else { + // Re-use the existing TTY file, as all three + // stdio FDs must point to the same fs.File in + // order to share TTY state, specifically the + // foreground process group id. + appFile = ttyFile + } + } else { + // Import the file as a regular host file. + var err error + appFile, err = host.ImportFile(ctx, int(hostFile.Fd()), mounter, false /* isTTY */) + if err != nil { + return nil, 0, nil, err + } + defer appFile.DecRef() } - defer file.DecRef() - - // We're done with this file. - f.Close() - if err := fdm.NewFDAt(kdefs.FD(appFD), file, kernel.FDFlags{}, l); err != nil { - return nil, 0, err + // Add the file to the FD map. + if err := fdm.NewFDAt(kdefs.FD(appFD), appFile, kernel.FDFlags{}, l); err != nil { + return nil, 0, nil, err } } - return proc.Kernel.CreateProcess(initArgs) + tg, tid, err := proc.Kernel.CreateProcess(initArgs) + if err != nil { + return nil, 0, nil, err + } + + if ttyFile == nil { + return tg, tid, nil, nil + } + return tg, tid, ttyFile.FileOperations.(*host.TTYFileOperations), nil } // PsArgs is the set of arguments to ps. |