diff --git a/pkg/sentry/control/proc.go b/pkg/sentry/control/proc.go index 36bd3e64b..50befb76f 100644 --- a/pkg/sentry/control/proc.go +++ b/pkg/sentry/control/proc.go @@ -45,27 +45,33 @@ type Proc struct { Kernel *kernel.Kernel } -// FilePayload aids to ensure that len(urpc.FilePayload.Files) == len(GuestFDs) -// when instantiated through the NewFDMap helper method. +// FilePayload aids to ensure that payload files and guest file descriptors are +// consistent when instantiated through the NewFilePayload helper method. type FilePayload struct { // FilePayload is the file payload that is transferred via RPC. urpc.FilePayload // GuestFDs are the file descriptors in the file descriptor map of the // executed application. They correspond 1:1 to the files in the - // urpc.FilePayload. + // urpc.FilePayload. If a program is executed from a host file descriptor, + // the file payload may contain one additional file. In that case, the file + // used for program execution is the last file in the Files array. GuestFDs []int } -// NewFDMap returns a FilePayload that maps file descriptors to files inside -// the executed process. -func NewFDMap(fdMap map[int]*os.File) FilePayload { - files := make([]*os.File, 0, len(fdMap)) +// NewFilePayload returns a FilePayload that maps file descriptors to files inside +// the executed process and provides a file for execution. +func NewFilePayload(fdMap map[int]*os.File, execFile *os.File) FilePayload { + fileCount := len(fdMap) + if execFile != nil { + fileCount += 1 + } + files := make([]*os.File, 0, fileCount) + guestFDs := make([]int, 0, len(fdMap)) // Make the map iteration order deterministic for the sake of testing. // Otherwise, the order is randomized and tests relying on the comparison // of equality will fail. - guestFDs := make([]int, 0, len(fdMap)) for key := range fdMap { guestFDs = append(guestFDs, key) } @@ -74,6 +80,11 @@ func NewFDMap(fdMap map[int]*os.File) FilePayload { for _, guestFD := range guestFDs { files = append(files, fdMap[guestFD]) } + + if execFile != nil { + files = append(files, execFile) + } + return FilePayload{ FilePayload: urpc.FilePayload{Files: files}, GuestFDs: guestFDs, @@ -219,13 +230,8 @@ func (proc *Proc) execAsync(args *ExecArgs) (*kernel.ThreadGroup, kernel.ThreadI initArgs.MountNamespace = proc.Kernel.GlobalInit().Leader().MountNamespace() initArgs.MountNamespace.IncRef() } - resolved, err := user.ResolveExecutablePath(ctx, &initArgs) - if err != nil { - return nil, 0, nil, err - } - initArgs.Filename = resolved - fdMap, err := args.createFDMap() + fdMap, execFD, err := args.unpackFiles() if err != nil { return nil, 0, nil, fmt.Errorf("creating fd map: %w", err) } @@ -234,6 +240,32 @@ func (proc *Proc) execAsync(args *ExecArgs) (*kernel.ThreadGroup, kernel.ThreadI _ = hostFD.Close() } }() + + if execFD != nil { + if initArgs.Filename != "" { + return nil, 0, nil, fmt.Errorf("process must either be started from a file or a filename, not both") + } + file, err := host.NewFD(ctx, proc.Kernel.HostMount(), execFD.FD(), &host.NewFDOptions{ + Readonly: true, + Savable: true, + VirtualOwner: true, + UID: args.KUID, + GID: args.KGID, + }) + if err != nil { + return nil, 0, nil, err + } + defer file.DecRef(ctx) + execFD.Release() + initArgs.File = file + } else { + resolved, err := user.ResolveExecutablePath(ctx, &initArgs) + if err != nil { + return nil, 0, nil, err + } + initArgs.Filename = resolved + } + ttyFile, err := fdimport.Import(ctx, fdTable, args.StdioIsPty, args.KUID, args.KGID, fdMap) if err != nil { return nil, 0, nil, err @@ -437,21 +469,35 @@ func ContainerUsage(kr *kernel.Kernel) map[string]uint64 { return cusage } -// createFDMap creates the file descriptor map from the unmarshalled ExecArgs. -func (args *ExecArgs) createFDMap() (map[int]*fd.FD, error) { - if len(args.Files) != len(args.GuestFDs) { - return nil, fmt.Errorf("length of payload files does not match length of file descriptor array") +// unpackFiles unpacks the file descriptor map and, if applicable, the file +// descriptor to be used for execution from the unmarshalled ExecArgs. +func (args *ExecArgs) unpackFiles() (map[int]*fd.FD, *fd.FD, error) { + var execFD *fd.FD + var err error + + // If there is one additional file, the last file is used for program + // execution. + if len(args.Files) == len(args.GuestFDs)+1 { + execFD, err = fd.NewFromFile(args.Files[len(args.Files)-1]) + if err != nil { + return nil, nil, fmt.Errorf("duplicating exec file: %w", err) + } + } else if len(args.Files) != len(args.GuestFDs) { + return nil, nil, fmt.Errorf("length of payload files does not match length of file descriptor array") } - fdMap := make(map[int]*fd.FD, len(args.Files)) - for i, file := range args.Files { - var appFD int - // GuestFDs are the indexes of our FD map. - appFD = args.GuestFDs[i] + + // GuestFDs are the indexes of our FD map. + fdMap := make(map[int]*fd.FD, len(args.GuestFDs)) + for i, appFD := range args.GuestFDs { + file := args.Files[i] + if appFD < 0 { + return nil, nil, fmt.Errorf("guest file descriptors must be 0 or greater") + } hostFD, err := fd.NewFromFile(file) if err != nil { - return nil, fmt.Errorf("duplicating payload files: %w", err) + return nil, nil, fmt.Errorf("duplicating payload files: %w", err) } fdMap[appFD] = hostFD } - return fdMap, nil + return fdMap, execFD, nil } diff --git a/pkg/sentry/fsimpl/host/host.go b/pkg/sentry/fsimpl/host/host.go index b32d2be8d..c7aea8303 100644 --- a/pkg/sentry/fsimpl/host/host.go +++ b/pkg/sentry/fsimpl/host/host.go @@ -144,6 +144,12 @@ type inode struct { // This field is initialized at creation time and is immutable. savable bool + // readonly is true if operations that can potentially change the host file + // are blocked. + // + // This field is initialized at creation time and is immutable. + readonly bool + // Event queue for blocking operations. queue waiter.Queue @@ -160,7 +166,7 @@ type inode struct { buf []byte } -func newInode(ctx context.Context, fs *filesystem, hostFD int, savable bool, fileType linux.FileMode, isTTY bool) (*inode, error) { +func newInode(ctx context.Context, fs *filesystem, hostFD int, savable bool, fileType linux.FileMode, isTTY bool, readonly bool) (*inode, error) { // Determine if hostFD is seekable. _, err := unix.Seek(hostFD, 0, linux.SEEK_CUR) seekable := !linuxerr.Equals(linuxerr.ESPIPE, err) @@ -179,6 +185,7 @@ func newInode(ctx context.Context, fs *filesystem, hostFD int, savable bool, fil seekable: seekable, isTTY: isTTY, savable: savable, + readonly: readonly, } i.InitRefs() i.CachedMappable.Init(hostFD) @@ -216,6 +223,10 @@ type NewFDOptions struct { VirtualOwner bool UID auth.KUID GID auth.KGID + + // If Readonly is true, we disallow operations that can potentially change + // the host file associated with the file descriptor. + Readonly bool } // NewFD returns a vfs.FileDescription representing the given host file @@ -226,6 +237,23 @@ func NewFD(ctx context.Context, mnt *vfs.Mount, hostFD int, opts *NewFDOptions) return nil, fmt.Errorf("can't import host FDs into filesystems of type %T", mnt.Filesystem().Impl()) } + if opts.Readonly { + if opts.IsTTY { + // This is not a technical limitation, but access checks for TTYs + // have not been implemented yet. + return nil, fmt.Errorf("readonly file descriptor may currently not be a TTY") + } + + flagsInt, err := unix.FcntlInt(uintptr(hostFD), unix.F_GETFL, 0) + if err != nil { + return nil, err + } + accessMode := uint32(flagsInt) & unix.O_ACCMODE + if accessMode != unix.O_RDONLY { + return nil, fmt.Errorf("readonly file descriptor may only be opened as O_RDONLY on the host") + } + } + // Retrieve metadata. var stat unix.Stat_t if err := unix.Fstat(hostFD, &stat); err != nil { @@ -243,7 +271,7 @@ func NewFD(ctx context.Context, mnt *vfs.Mount, hostFD int, opts *NewFDOptions) } fileType := linux.FileMode(stat.Mode).FileType() - i, err := newInode(ctx, fs, hostFD, opts.Savable, fileType, opts.IsTTY) + i, err := newInode(ctx, fs, hostFD, opts.Savable, fileType, opts.IsTTY, opts.Readonly) if err != nil { return nil, err } @@ -501,6 +529,10 @@ func (i *inode) stat(stat *unix.Stat_t) error { // // +checklocksignore func (i *inode) SetStat(ctx context.Context, fs *vfs.Filesystem, creds *auth.Credentials, opts vfs.SetStatOptions) error { + if i.readonly { + return linuxerr.EPERM + } + s := &opts.Stat m := s.Mask @@ -712,6 +744,9 @@ func (f *fileDescription) Release(context.Context) { // Allocate implements vfs.FileDescriptionImpl.Allocate. func (f *fileDescription) Allocate(ctx context.Context, mode, offset, length uint64) error { + if f.inode.readonly { + return linuxerr.EPERM + } return unix.Fallocate(f.inode.hostFD, uint32(mode), int64(offset), int64(length)) } @@ -834,6 +869,9 @@ func (f *fileDescription) Write(ctx context.Context, src usermem.IOSequence, opt } func (f *fileDescription) writeToHostFD(ctx context.Context, src usermem.IOSequence, offset int64, flags uint32) (int64, error) { + if f.inode.readonly { + return 0, linuxerr.EPERM + } hostFD := f.inode.hostFD // TODO(gvisor.dev/issue/2601): Support select pwritev2 flags. if flags != 0 { @@ -918,6 +956,9 @@ func (f *fileDescription) Seek(_ context.Context, offset int64, whence int32) (i // Sync implements vfs.FileDescriptionImpl.Sync. func (f *fileDescription) Sync(ctx context.Context) error { + if f.inode.readonly { + return linuxerr.EPERM + } // TODO(gvisor.dev/issue/1897): Currently, we always sync everything. return unix.Fsync(f.inode.hostFD) } diff --git a/runsc/boot/loader.go b/runsc/boot/loader.go index 6afc87404..f78dfcb7b 100644 --- a/runsc/boot/loader.go +++ b/runsc/boot/loader.go @@ -104,6 +104,9 @@ type containerInfo struct { // passFDs are mappings of user-supplied host to guest file descriptors. passFDs []fdMapping + // execFD is the host file descriptor used for program execution. + execFD *fd.FD + // goferFDs are the FDs that attach the sandbox to the gofers. goferFDs []*fd.FD @@ -232,6 +235,8 @@ type Args struct { // PassFDs are user-supplied FD mappings from host to guest descriptors. // The Loader takes ownership of these FDs and may close them at any time. PassFDs []FDMapping + // ExecFD is the host file descriptor used for program execution. + ExecFD int // OverlayFilestoreFDs are the FDs to the regular files that will back the // tmpfs upper mount in the overlay mounts. OverlayFilestoreFDs []int @@ -308,6 +313,11 @@ func New(args Args) (*Loader, error) { for _, overlayFD := range args.OverlayFilestoreFDs { info.overlayFilestoreFDs = append(info.overlayFilestoreFDs, fd.New(overlayFD)) } + + if args.ExecFD >= 0 { + info.execFD = fd.New(args.ExecFD) + } + for _, customFD := range args.PassFDs { info.passFDs = append(info.passFDs, fdMapping{ host: fd.New(customFD.Host), @@ -860,6 +870,26 @@ func (l *Loader) createContainerProcess(root bool, cid string, info *containerIn // ours either way. info.procArgs.FDTable = fdTable + if info.execFD != nil { + if info.procArgs.Filename != "" { + return nil, nil, fmt.Errorf("process must either be started from a file or a filename, not both") + } + file, err := host.NewFD(ctx, l.k.HostMount(), info.execFD.FD(), &host.NewFDOptions{ + Readonly: true, + Savable: true, + VirtualOwner: true, + UID: auth.KUID(info.spec.Process.User.UID), + GID: auth.KGID(info.spec.Process.User.GID), + }) + if err != nil { + return nil, nil, err + } + defer file.DecRef(ctx) + info.execFD.Release() + + info.procArgs.File = file + } + // Gofer FDs must be ordered and the first FD is always the rootfs. if len(info.goferFDs) < 1 { return nil, nil, fmt.Errorf("rootfs gofer FD not found") @@ -1430,6 +1460,9 @@ func createFDTable(ctx context.Context, console bool, stdioFDs []*fd.FD, passFDs // Create the entries for the host files that were passed to our app. for _, customFD := range passFDs { + if customFD.guest < 0 { + return nil, nil, fmt.Errorf("guest file descriptors must be 0 or greater") + } fdMap[customFD.guest] = customFD.host } diff --git a/runsc/boot/vfs.go b/runsc/boot/vfs.go index 4c59859ca..c0a8c38f8 100644 --- a/runsc/boot/vfs.go +++ b/runsc/boot/vfs.go @@ -179,6 +179,10 @@ func setupContainerVFS(ctx context.Context, conf *config.Config, mntr *container } procArgs.MountNamespace = mns + // We are executing a file directly. Do not resolve the executable path. + if procArgs.File != nil { + return nil + } // Resolve the executable path from working dir and environment. resolved, err := user.ResolveExecutablePath(ctx, procArgs) if err != nil { diff --git a/runsc/cmd/boot.go b/runsc/cmd/boot.go index 140ee5b64..da53104a0 100644 --- a/runsc/cmd/boot.go +++ b/runsc/cmd/boot.go @@ -89,6 +89,9 @@ type Boot struct { // passFDs are mappings of user-supplied host to guest file descriptors. passFDs fdMappings + // execFD is the host file descriptor used for program execution. + execFD int + // applyCaps determines if capabilities defined in the spec should be applied // to the process. applyCaps bool @@ -172,6 +175,7 @@ func (b *Boot) SetFlags(f *flag.FlagSet) { f.Var(&b.ioFDs, "io-fds", "list of FDs to connect gofer clients. They must follow this order: root first, then mounts as defined in the spec") f.Var(&b.stdioFDs, "stdio-fds", "list of FDs containing sandbox stdin, stdout, and stderr in that order") f.Var(&b.passFDs, "pass-fd", "mapping of host to guest FDs. They must be in M:N format. M is the host and N the guest descriptor.") + f.IntVar(&b.execFD, "exec-fd", -1, "host file descriptor used for program execution.") f.Var(&b.overlayFilestoreFDs, "overlay-filestore-fds", "FDs to the regular files that will back the tmpfs upper mount in the overlay mounts.") f.IntVar(&b.userLogFD, "user-log-fd", 0, "file descriptor to write user logs to. 0 means no logging.") f.IntVar(&b.startSyncFD, "start-sync-fd", -1, "required FD to used to synchronize sandbox startup") @@ -390,6 +394,7 @@ func (b *Boot) Execute(_ context.Context, f *flag.FlagSet, args ...any) subcomma GoferFDs: b.ioFDs.GetArray(), StdioFDs: b.stdioFDs.GetArray(), PassFDs: b.passFDs.GetArray(), + ExecFD: b.execFD, OverlayFilestoreFDs: b.overlayFilestoreFDs.GetArray(), NumCPU: b.cpuNum, TotalMem: b.totalMem, diff --git a/runsc/cmd/exec.go b/runsc/cmd/exec.go index 6da5e96a5..de268ae0a 100644 --- a/runsc/cmd/exec.go +++ b/runsc/cmd/exec.go @@ -61,6 +61,9 @@ type Exec struct { // passFDs are user-supplied FDs from the host to be exposed to the // sandboxed app. passFDs fdMappings + + // execFD is the host file descriptor used for program execution. + execFD int } // Name implements subcommands.Command.Name. @@ -105,6 +108,7 @@ func (ex *Exec) SetFlags(f *flag.FlagSet) { f.StringVar(&ex.internalPidFile, "internal-pid-file", "", "filename that the container-internal 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") f.Var(&ex.passFDs, "pass-fd", "file descriptor passed to the container in M:N format, where M is the host and N is the guest descriptor (can be supplied multiple times)") + f.IntVar(&ex.execFD, "exec-fd", -1, "host file descriptor used for program execution") } // Execute implements subcommands.Command.Execute. It starts a process in an @@ -160,6 +164,11 @@ func (ex *Exec) Execute(_ context.Context, f *flag.FlagSet, args ...any) subcomm fdMap[mapping.Guest] = file } + var execFile *os.File + if ex.execFD >= 0 { + execFile = os.NewFile(uintptr(ex.execFD), "exec-fd") + } + // Close the underlying file descriptors after we have passed them. defer func() { for _, file := range fdMap { @@ -168,9 +177,13 @@ func (ex *Exec) Execute(_ context.Context, f *flag.FlagSet, args ...any) subcomm log.Debugf("Failed to close FD %d", fd) } } + + if execFile != nil && execFile.Close() != nil { + log.Debugf("Failed to close exec FD") + } }() - e.FilePayload = control.NewFDMap(fdMap) + e.FilePayload = control.NewFilePayload(fdMap, execFile) // containerd expects an actual process to represent the container being // executed. If detach was specified, starts a child in non-detach mode, @@ -362,11 +375,11 @@ func (ex *Exec) argsFromCLI(argv []string, enableRaw bool) (*control.ExecArgs, e ExtraKGIDs: extraKGIDs, Capabilities: caps, StdioIsPty: ex.consoleSocket != "" || console.IsPty(os.Stdin.Fd()), - FilePayload: control.NewFDMap(map[int]*os.File{ + FilePayload: control.NewFilePayload(map[int]*os.File{ 0: os.Stdin, 1: os.Stdout, 2: os.Stderr, - }), + }, nil), }, nil } @@ -415,11 +428,11 @@ func argsFromProcess(p *specs.Process, enableRaw bool) (*control.ExecArgs, error ExtraKGIDs: extraKGIDs, Capabilities: caps, StdioIsPty: p.Terminal, - FilePayload: control.NewFDMap(map[int]*os.File{ + FilePayload: control.NewFilePayload(map[int]*os.File{ 0: os.Stdin, 1: os.Stdout, 2: os.Stderr, - }), + }, nil), }, nil } diff --git a/runsc/cmd/exec_test.go b/runsc/cmd/exec_test.go index 4c858911e..20c804d97 100644 --- a/runsc/cmd/exec_test.go +++ b/runsc/cmd/exec_test.go @@ -75,11 +75,11 @@ func TestCLIArgs(t *testing.T) { expected: control.ExecArgs{ Argv: []string{"ls", "/"}, WorkingDirectory: "/foo/bar", - FilePayload: control.NewFDMap(map[int]*os.File{ + FilePayload: control.NewFilePayload(map[int]*os.File{ 0: os.Stdin, 1: os.Stdout, 2: os.Stderr, - }), + }, nil), KUID: 0, KGID: 0, ExtraKGIDs: []auth.KGID{1, 2, 3}, @@ -132,11 +132,11 @@ func TestJSONArgs(t *testing.T) { expected: control.ExecArgs{ Argv: []string{"ls", "/"}, WorkingDirectory: "/foo/bar", - FilePayload: control.NewFDMap(map[int]*os.File{ + FilePayload: control.NewFilePayload(map[int]*os.File{ 0: os.Stdin, 1: os.Stdout, 2: os.Stderr, - }), + }, nil), KUID: 0, KGID: 0, ExtraKGIDs: []auth.KGID{1, 2, 3}, diff --git a/runsc/cmd/run.go b/runsc/cmd/run.go index 1e6dbe90c..ba4fa1aea 100644 --- a/runsc/cmd/run.go +++ b/runsc/cmd/run.go @@ -39,6 +39,9 @@ type Run struct { // passFDs are user-supplied FDs from the host to be exposed to the // sandboxed app. passFDs fdMappings + + // execFD is the host file descriptor used for program execution. + execFD int } // Name implements subcommands.Command.Name. @@ -61,6 +64,7 @@ func (*Run) Usage() string { func (r *Run) SetFlags(f *flag.FlagSet) { f.BoolVar(&r.detach, "detach", false, "detach from the container's process") f.Var(&r.passFDs, "pass-fd", "file descriptor passed to the container in M:N format, where M is the host and N is the guest descriptor (can be supplied multiple times)") + f.IntVar(&r.execFD, "exec-fd", -1, "host file descriptor used for program execution") r.Create.SetFlags(f) } @@ -106,6 +110,11 @@ func (r *Run) Execute(_ context.Context, f *flag.FlagSet, args ...any) subcomman fdMap[mapping.Guest] = file } + var execFile *os.File + if r.execFD >= 0 { + execFile = os.NewFile(uintptr(r.execFD), "exec-fd") + } + // Close the underlying file descriptors after we have passed them. defer func() { for _, file := range fdMap { @@ -114,6 +123,10 @@ func (r *Run) Execute(_ context.Context, f *flag.FlagSet, args ...any) subcomman log.Debugf("Failed to close FD %d", fd) } } + + if execFile != nil && execFile.Close() != nil { + log.Debugf("Failed to close exec FD") + } }() runArgs := container.Args{ @@ -125,6 +138,7 @@ func (r *Run) Execute(_ context.Context, f *flag.FlagSet, args ...any) subcomman UserLog: r.userLog, Attached: !r.detach, PassFiles: fdMap, + ExecFile: execFile, } ws, err := container.Run(conf, runArgs) if err != nil { diff --git a/runsc/container/console_test.go b/runsc/container/console_test.go index 0c98c9364..c668bed38 100644 --- a/runsc/container/console_test.go +++ b/runsc/container/console_test.go @@ -281,9 +281,9 @@ func TestJobControlSignalExec(t *testing.T) { // our PID counts get messed up. Argv: []string{"/bin/bash", "--noprofile", "--norc"}, // Pass the pty replica as FD 0, 1, and 2. - FilePayload: control.NewFDMap(map[int]*os.File{ + FilePayload: control.NewFilePayload(map[int]*os.File{ 0: ptyReplica, 1: ptyReplica, 2: ptyReplica, - }), + }, nil), StdioIsPty: true, } diff --git a/runsc/container/container.go b/runsc/container/container.go index baf1d6748..d727b77d3 100644 --- a/runsc/container/container.go +++ b/runsc/container/container.go @@ -180,6 +180,9 @@ type Args struct { // PassFiles are user-supplied files from the host to be exposed to the // sandboxed app. PassFiles map[int]*os.File + + // ExecFile is the host file used for program execution. + ExecFile *os.File } // New creates the container in a new Sandbox process, unless the metadata @@ -301,6 +304,7 @@ func New(conf *config.Config, args Args) (*Container, error) { Attached: args.Attached, OverlayFilestoreFiles: overlayFilestoreFiles, PassFiles: args.PassFiles, + ExecFile: args.ExecFile, } sand, err := sandbox.New(conf, sandArgs) if err != nil { diff --git a/runsc/container/container_test.go b/runsc/container/container_test.go index 26cc3186f..1a3e34a46 100644 --- a/runsc/container/container_test.go +++ b/runsc/container/container_test.go @@ -69,19 +69,26 @@ func execute(conf *config.Config, cont *Container, name string, arg ...string) ( return cont.executeSync(conf, args) } -func executeCombinedOutput(conf *config.Config, cont *Container, name string, arg ...string) ([]byte, error) { +// executeCombinedOutput executes a process in the container and captures +// stdout and stderr. If execFile is supplied, a host file will be executed. +// Otherwise, the name argument is used to resolve the executable in the guest. +func executeCombinedOutput(conf *config.Config, cont *Container, execFile *os.File, name string, arg ...string) ([]byte, error) { r, w, err := os.Pipe() if err != nil { return nil, err } defer r.Close() + // Unset the filename when we execute via FD. + if execFile != nil { + name = "" + } args := &control.ExecArgs{ Filename: name, Argv: append([]string{name}, arg...), - FilePayload: control.NewFDMap(map[int]*os.File{ + FilePayload: control.NewFilePayload(map[int]*os.File{ 0: os.Stdin, 1: w, 2: w, - }), + }, execFile), } ws, err := cont.executeSync(conf, args) w.Close() @@ -176,7 +183,7 @@ func blockUntilWaitable(pid int) error { // execPS executes `ps` inside the container and return the processes. func execPS(conf *config.Config, c *Container) ([]*control.Process, error) { - out, err := executeCombinedOutput(conf, c, "/bin/ps", "-e") + out, err := executeCombinedOutput(conf, c, nil, "/bin/ps", "-e") if err != nil { return nil, err } @@ -854,9 +861,9 @@ func TestExec(t *testing.T) { _, err = cont.executeSync(conf, &control.ExecArgs{ Argv: []string{"/nonexist"}, - FilePayload: control.NewFDMap(map[int]*os.File{ + FilePayload: control.NewFilePayload(map[int]*os.File{ 0: os.NewFile(uintptr(fds[1]), "sock"), - }), + }, nil), }) want := "failed to load /nonexist" if err == nil || !strings.Contains(err.Error(), want) { @@ -1625,7 +1632,7 @@ func TestReadonlyRoot(t *testing.T) { } // Read mounts to check that root is readonly. - out, err := executeCombinedOutput(conf, c, "/bin/sh", "-c", "mount | grep ' / ' | grep -o -e '(.*)'") + out, err := executeCombinedOutput(conf, c, nil, "/bin/sh", "-c", "mount | grep ' / ' | grep -o -e '(.*)'") if err != nil { t.Fatalf("exec failed: %v", err) } @@ -1684,7 +1691,7 @@ func TestReadonlyMount(t *testing.T) { // Read mounts to check that volume is readonly. cmd := fmt.Sprintf("mount | grep ' %s ' | grep -o -e '(.*)'", dir) - out, err := executeCombinedOutput(conf, c, "/bin/sh", "-c", cmd) + out, err := executeCombinedOutput(conf, c, nil, "/bin/sh", "-c", cmd) if err != nil { t.Fatalf("exec failed, err: %v", err) } @@ -2505,7 +2512,7 @@ func TestRlimitsExec(t *testing.T) { t.Fatalf("error starting container: %v", err) } - got, err := executeCombinedOutput(conf, cont, "/bin/sh", "-c", "ulimit -n") + got, err := executeCombinedOutput(conf, cont, nil, "/bin/sh", "-c", "ulimit -n") if err != nil { t.Fatal(err) } @@ -2869,10 +2876,10 @@ func TestFDPassingExec(t *testing.T) { cmd := fmt.Sprintf("cat /proc/self/fd/%d > /proc/self/fd/%d", int(guestRead.Fd()), int(guestWrite.Fd())) execArgs := &control.ExecArgs{ Argv: []string{"/bin/bash", "-c", cmd}, - FilePayload: control.NewFDMap(map[int]*os.File{ + FilePayload: control.NewFilePayload(map[int]*os.File{ int(guestRead.Fd()): guestRead, int(guestWrite.Fd()): guestWrite, - }), + }, nil), } if _, err = cont.Execute(conf, execArgs); err != nil { @@ -2892,3 +2899,136 @@ func TestFDPassingExec(t *testing.T) { t.Errorf("got message %q, want %q", got, msg) } } + +// findInPath finds a filename in the PATH environment variable. +func findInPath(filename string) string { + for _, dir := range strings.Split(os.Getenv("PATH"), ":") { + fullPath := filepath.Join(dir, filename) + if _, err := os.Stat(fullPath); err == nil { + return fullPath + } + } + return "" +} + +// TestExecFDRun checks that an executable from the host can be started inside +// a container. +func TestExecFDRun(t *testing.T) { + // In the guest, read from the host and write the result back to the host. + conf := testutil.TestConfig(t) + // Note that we do not supply the name or path of the echo binary here. + // Thus, the guest does not know the binary path or name either. + // argv[0] inside echo is "can be anything". + spec := testutil.NewSpecWithArgs("can be anything", "hello world") + + _, bundleDir, cleanup, err := testutil.SetupContainer(spec, conf) + if err != nil { + t.Fatalf("error setting up container: %v", err) + } + defer cleanup() + + // Find the echo binary on the host. + echoPath := findInPath("echo") + if echoPath == "" { + t.Fatalf("failed to find echo executable in PATH") + } + + // Open the echo binary as a file. + echoFile, err := os.Open(echoPath) + if err != nil { + t.Fatalf("opening echo binary: %v", err) + } + defer echoFile.Close() + + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("creating pipe: %v", err) + } + defer r.Close() + + args := Args{ + ID: testutil.RandomContainerID(), + Spec: spec, + BundleDir: bundleDir, + PassFiles: map[int]*os.File{ + 0: os.Stdin, 1: w, 2: w, + }, + ExecFile: echoFile, + } + + cont, err := New(conf, args) + if err != nil { + t.Fatalf("Creating container: %v", err) + } + defer cont.Destroy() + + if err := cont.Start(conf); err != nil { + t.Fatalf("starting container: %v", err) + } + + w.Close() + + got, err := io.ReadAll(r) + if err != nil { + t.Errorf("reading container output: %v", err) + } + if want := "hello world\n"; string(got) != want { + t.Errorf("got message %q, want %q", got, want) + } +} + +// TestExecFDExec checks that an executable from the host can be started from a +// file descriptor inside an already running container. +func TestExecFDExec(t *testing.T) { + conf := testutil.TestConfig(t) + + // We just sleep here because we want to test execution in an already + // running container. + spec := testutil.NewSpecWithArgs("bash", "-c", "sleep infinity") + + _, bundleDir, cleanup, err := testutil.SetupContainer(spec, conf) + if err != nil { + t.Fatalf("error setting up container: %v", err) + } + defer cleanup() + + args := Args{ + ID: testutil.RandomContainerID(), + Spec: spec, + BundleDir: bundleDir, + } + + cont, err := New(conf, args) + if err != nil { + t.Fatalf("Creating container: %v", err) + } + defer cont.Destroy() + + if err := cont.Start(conf); err != nil { + t.Fatalf("starting container: %v", err) + } + + // Find the echo binary on the host. + echoPath := findInPath("echo") + if echoPath == "" { + t.Fatalf("failed to find echo executable in PATH") + } + + // Open the echo binary as a file. + echoFile, err := os.Open(echoPath) + if err != nil { + t.Fatalf("opening echo binary: %v", err) + } + defer echoFile.Close() + + // Note that we do not supply the name or path of the echo binary here. + // Thus, the guest does not know the binary path or name either. + // argv[0] inside echo is "can be anything". + got, err := executeCombinedOutput(conf, cont, echoFile, "can be anything", "hello world") + if err != nil { + t.Fatal(err) + } + if want := "hello world\n"; string(got) != want { + t.Errorf("echo result, got: %q, want: %q", got, want) + } +} diff --git a/runsc/container/metric_server_test.go b/runsc/container/metric_server_test.go index 01f7dc92a..d3d86c116 100644 --- a/runsc/container/metric_server_test.go +++ b/runsc/container/metric_server_test.go @@ -184,7 +184,7 @@ func TestContainerMetrics(t *testing.T) { } t.Logf("After container start, fs_opens=%d (snapshotted at %v)", postStartOpens, postStartTimestamp) // The touch operation may fail from permission errors, but the metric should still be incremented. - shOutput, err := executeCombinedOutput(te.sleepConf, cont, "/bin/bash", "-c", fmt.Sprintf("for i in $(seq 1 %d); do touch /tmp/$i || true; done", targetOpens)) + shOutput, err := executeCombinedOutput(te.sleepConf, cont, nil, "/bin/bash", "-c", fmt.Sprintf("for i in $(seq 1 %d); do touch /tmp/$i || true; done", targetOpens)) if err != nil { t.Fatalf("Exec failed: %v; output: %v", err, shOutput) } @@ -290,7 +290,7 @@ func TestContainerMetricsRobustAgainstRestarts(t *testing.T) { if err := cont.Start(te.sleepConf); err != nil { t.Fatalf("Cannot start container: %v", err) } - shOutput, err := executeCombinedOutput(te.sleepConf, cont, "/bin/bash", "-c", fmt.Sprintf("for i in $(seq 1 %d); do touch /tmp/$i || true; done", targetOpens)) + shOutput, err := executeCombinedOutput(te.sleepConf, cont, nil, "/bin/bash", "-c", fmt.Sprintf("for i in $(seq 1 %d); do touch /tmp/$i || true; done", targetOpens)) if err != nil { t.Fatalf("Exec failed: %v; output: %v", err, shOutput) } @@ -327,7 +327,7 @@ func TestContainerMetricsRobustAgainstRestarts(t *testing.T) { // Do a bunch of touches again. The metric server is down during this time. // This verifies that metric value modifications does not depend on the metric server being up. - shOutput, err = executeCombinedOutput(te.sleepConf, cont, "/bin/bash", "-c", fmt.Sprintf("for i in $(seq 1 %d); do touch /tmp/$i || true; done", targetOpens)) + shOutput, err = executeCombinedOutput(te.sleepConf, cont, nil, "/bin/bash", "-c", fmt.Sprintf("for i in $(seq 1 %d); do touch /tmp/$i || true; done", targetOpens)) if err != nil { t.Fatalf("Exec failed: %v; output: %v", err, shOutput) } diff --git a/runsc/container/multi_container_test.go b/runsc/container/multi_container_test.go index 2c9be0efa..74a4b9b07 100644 --- a/runsc/container/multi_container_test.go +++ b/runsc/container/multi_container_test.go @@ -2205,7 +2205,7 @@ func TestMultiContainerShm(t *testing.T) { } // Check that file can be found in the other container. - out, err := executeCombinedOutput(conf, containers[1], "/bin/cat", output) + out, err := executeCombinedOutput(conf, containers[1], nil, "/bin/cat", output) if err != nil { t.Fatalf("exec failed: %v", err) } diff --git a/runsc/sandbox/sandbox.go b/runsc/sandbox/sandbox.go index a3d1490f6..2b7bc940d 100644 --- a/runsc/sandbox/sandbox.go +++ b/runsc/sandbox/sandbox.go @@ -245,6 +245,9 @@ type Args struct { // PassFiles are user-supplied files from the host to be exposed to the // sandboxed app. PassFiles map[int]*os.File + + // ExecFile is the file from the host used for program execution. + ExecFile *os.File } // New creates the sandbox process. The caller must call Destroy() on the @@ -959,6 +962,10 @@ func (s *Sandbox) createSandboxProcess(conf *config.Config, args *Args, startSyn cmd.Args = append(cmd.Args, "--attached") } + if args.ExecFile != nil { + donations.Donate("exec-fd", args.ExecFile) + } + nextFD = donations.Transfer(cmd, nextFD) _ = donation.DonateAndTransferCustomFiles(cmd, nextFD, args.PassFiles)