diff --git a/pkg/sentry/control/proc.go b/pkg/sentry/control/proc.go index 89bc60f8f..36bd3e64b 100644 --- a/pkg/sentry/control/proc.go +++ b/pkg/sentry/control/proc.go @@ -18,6 +18,7 @@ import ( "bytes" "encoding/json" "fmt" + "os" "sort" "strings" "text/tabwriter" @@ -44,6 +45,41 @@ type Proc struct { Kernel *kernel.Kernel } +// FilePayload aids to ensure that len(urpc.FilePayload.Files) == len(GuestFDs) +// when instantiated through the NewFDMap 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. + 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)) + + // 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) + } + sort.Ints(guestFDs) + + for _, guestFD := range guestFDs { + files = append(files, fdMap[guestFD]) + } + return FilePayload{ + FilePayload: urpc.FilePayload{Files: files}, + GuestFDs: guestFDs, + } +} + // ExecArgs is the set of arguments to exec. type ExecArgs struct { // Filename is the filename to load. @@ -84,7 +120,7 @@ type ExecArgs struct { StdioIsPty bool // FilePayload determines the files to give to the new process. - urpc.FilePayload + FilePayload // ContainerID is the container for the process being executed. ContainerID string @@ -97,7 +133,7 @@ type ExecArgs struct { } // String prints the arguments as a string. -func (args ExecArgs) String() string { +func (args *ExecArgs) String() string { if len(args.Argv) == 0 { return args.Filename } @@ -189,19 +225,15 @@ func (proc *Proc) execAsync(args *ExecArgs) (*kernel.ThreadGroup, kernel.ThreadI } initArgs.Filename = resolved - fds, err := fd.NewFromFiles(args.Files) + fdMap, err := args.createFDMap() if err != nil { - return nil, 0, nil, fmt.Errorf("duplicating payload files: %w", err) + return nil, 0, nil, fmt.Errorf("creating fd map: %w", err) } defer func() { - for _, fd := range fds { - _ = fd.Close() + for _, hostFD := range fdMap { + _ = hostFD.Close() } }() - fdMap := make(map[int]*fd.FD, len(fds)) - for appFD, hostFD := range fds { - fdMap[appFD] = hostFD - } ttyFile, err := fdimport.Import(ctx, fdTable, args.StdioIsPty, args.KUID, args.KGID, fdMap) if err != nil { return nil, 0, nil, err @@ -404,3 +436,22 @@ 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") + } + 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] + hostFD, err := fd.NewFromFile(file) + if err != nil { + return nil, fmt.Errorf("duplicating payload files: %w", err) + } + fdMap[appFD] = hostFD + } + return fdMap, nil +} diff --git a/runsc/boot/loader.go b/runsc/boot/loader.go index c93953b8d..1e2649957 100644 --- a/runsc/boot/loader.go +++ b/runsc/boot/loader.go @@ -101,6 +101,9 @@ type containerInfo struct { // stdioFDs contains stdin, stdout, and stderr. stdioFDs []*fd.FD + // passFDs are mappings of user-supplied host to guest file descriptors. + passFDs []fdMapping + // goferFDs are the FDs that attach the sandbox to the gofers. goferFDs []*fd.FD @@ -186,6 +189,21 @@ type execProcess struct { hostTTY *fd.FD } +// fdMapping maps guest to host file descriptors. Guest file descriptors are +// exposed to the application inside the sandbox through the FD table. +type fdMapping struct { + guest int + host *fd.FD +} + +// FDMapping is a helper type to represent a mapping from guest to host file +// descriptors. In contrast to the unexported fdMapping type, it does not imply +// file ownership. +type FDMapping struct { + Guest int + Host int +} + func init() { // Initialize the random number generator. mrand.Seed(gtime.Now().UnixNano()) @@ -211,6 +229,9 @@ type Args struct { // StdioFDs is the stdio for the application. The Loader takes ownership of // these FDs and may close them at any time. StdioFDs []int + // 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 // OverlayFilestoreFDs are the FDs to the regular files that will back the // tmpfs upper mount in the overlay mounts. OverlayFilestoreFDs []int @@ -287,6 +308,12 @@ func New(args Args) (*Loader, error) { for _, overlayFD := range args.OverlayFilestoreFDs { info.overlayFilestoreFDs = append(info.overlayFilestoreFDs, fd.New(overlayFD)) } + for _, customFD := range args.PassFDs { + info.passFDs = append(info.passFDs, fdMapping{ + host: fd.New(customFD.Host), + guest: customFD.Guest, + }) + } // Create kernel and platform. p, err := createPlatform(args.Conf, args.Device) @@ -529,6 +556,9 @@ func (l *Loader) Destroy() { for _, f := range l.root.stdioFDs { _ = f.Close() } + for _, f := range l.root.passFDs { + _ = f.host.Close() + } for _, f := range l.root.goferFDs { _ = f.Close() } @@ -825,7 +855,7 @@ func (l *Loader) startSubcontainer(spec *specs.Spec, conf *config.Config, cid st func (l *Loader) createContainerProcess(root bool, cid string, info *containerInfo) (*kernel.ThreadGroup, *host.TTYFileDescription, error) { // Create the FD map, which will set stdin, stdout, and stderr. ctx := info.procArgs.NewContext(l.k) - fdTable, ttyFile, err := createFDTable(ctx, info.spec.Process.Terminal, info.stdioFDs, info.spec.Process.User) + fdTable, ttyFile, err := createFDTable(ctx, info.spec.Process.Terminal, info.stdioFDs, info.passFDs, info.spec.Process.User) if err != nil { return nil, nil, fmt.Errorf("importing fds: %w", err) } @@ -1391,7 +1421,7 @@ func (l *Loader) ttyFromIDLocked(key execID) (*host.TTYFileDescription, error) { return ep.tty, nil } -func createFDTable(ctx context.Context, console bool, stdioFDs []*fd.FD, user specs.User) (*kernel.FDTable, *host.TTYFileDescription, error) { +func createFDTable(ctx context.Context, console bool, stdioFDs []*fd.FD, passFDs []fdMapping, user specs.User) (*kernel.FDTable, *host.TTYFileDescription, error) { if len(stdioFDs) != 3 { return nil, nil, fmt.Errorf("stdioFDs should contain exactly 3 FDs (stdin, stdout, and stderr), but %d FDs received", len(stdioFDs)) } @@ -1401,6 +1431,11 @@ func createFDTable(ctx context.Context, console bool, stdioFDs []*fd.FD, user sp 2: stdioFDs[2], } + // Create the entries for the host files that were passed to our app. + for _, customFD := range passFDs { + fdMap[customFD.guest] = customFD.host + } + k := kernel.KernelFromContext(ctx) fdTable := k.NewFDTable() ttyFile, err := fdimport.Import(ctx, fdTable, console, auth.KUID(user.UID), auth.KGID(user.GID), fdMap) diff --git a/runsc/cmd/BUILD b/runsc/cmd/BUILD index bf2011b87..96e81a4ae 100644 --- a/runsc/cmd/BUILD +++ b/runsc/cmd/BUILD @@ -19,6 +19,7 @@ go_library( "do.go", "events.go", "exec.go", + "fd_mapping.go", "gofer.go", "help.go", "install.go", @@ -69,7 +70,6 @@ go_library( "//pkg/state/statefile", "//pkg/sync", "//pkg/unet", - "//pkg/urpc", "//runsc/boot", "//runsc/cmd/util", "//runsc/config", @@ -112,7 +112,6 @@ go_test( "//pkg/sentry/control", "//pkg/sentry/kernel/auth", "//pkg/test/testutil", - "//pkg/urpc", "//runsc/cmd/util", "//runsc/config", "//runsc/container", diff --git a/runsc/cmd/boot.go b/runsc/cmd/boot.go index 6dfdff5b4..496400398 100644 --- a/runsc/cmd/boot.go +++ b/runsc/cmd/boot.go @@ -87,6 +87,9 @@ type Boot struct { // provided in that order. stdioFDs intFlags + // passFDs are mappings of user-supplied host to guest file descriptors. + passFDs fdMappings + // applyCaps determines if capabilities defined in the spec should be applied // to the process. applyCaps bool @@ -169,6 +172,7 @@ func (b *Boot) SetFlags(f *flag.FlagSet) { f.IntVar(&b.deviceFD, "device-fd", -1, "FD for the platform device file") 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, "custom-fds", "mapping of host to guest FDs. They must be in M:N format. M is the host and N the guest descriptor.") 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") @@ -392,6 +396,7 @@ func (b *Boot) Execute(_ context.Context, f *flag.FlagSet, args ...any) subcomma Device: os.NewFile(uintptr(b.deviceFD), "platform device"), GoferFDs: b.ioFDs.GetArray(), StdioFDs: b.stdioFDs.GetArray(), + PassFDs: b.passFDs.GetArray(), OverlayFilestoreFDs: b.overlayFilestoreFDs.GetArray(), NumCPU: b.cpuNum, TotalMem: b.totalMem, diff --git a/runsc/cmd/exec.go b/runsc/cmd/exec.go index bd871d29c..6d32edf98 100644 --- a/runsc/cmd/exec.go +++ b/runsc/cmd/exec.go @@ -32,7 +32,6 @@ import ( "gvisor.dev/gvisor/pkg/log" "gvisor.dev/gvisor/pkg/sentry/control" "gvisor.dev/gvisor/pkg/sentry/kernel/auth" - "gvisor.dev/gvisor/pkg/urpc" "gvisor.dev/gvisor/runsc/cmd/util" "gvisor.dev/gvisor/runsc/config" "gvisor.dev/gvisor/runsc/console" @@ -58,6 +57,10 @@ type Exec struct { // file descriptor referencing the master end of the console's // pseudoterminal. consoleSocket string + + // passFDs are user-supplied FDs from the host to be exposed to the + // sandboxed app. + passFDs intFlags } // Name implements subcommands.Command.Name. @@ -101,6 +104,7 @@ func (ex *Exec) SetFlags(f *flag.FlagSet) { f.StringVar(&ex.pidFile, "pid-file", "", "filename that the container pid will be written to") 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 descriptors passed to the container. Can be supplied multiple times.") } // Execute implements subcommands.Command.Execute. It starts a process in an @@ -140,6 +144,35 @@ func (ex *Exec) Execute(_ context.Context, f *flag.FlagSet, args ...any) subcomm log.Infof("Using exec capabilities from container: %+v", e.Capabilities) } + // Create the file descriptor map for the process in the container. + fdMap := map[int]*os.File{ + 0: os.Stdin, + 1: os.Stdout, + 2: os.Stderr, + } + + // Add custom file descriptors to the map. + var files []*os.File + for _, fd := range ex.passFDs { + file := os.NewFile(uintptr(fd), "") + if file == nil { + util.Fatalf("failed to create file from file descriptor %d", fd) + } + fdMap[int(file.Fd())] = file + } + + // Close the underlying file descriptors after we have passed them. + defer func() { + for _, file := range files { + fd := file.Fd() + if file.Close() != nil { + log.Debugf("Failed to close FD %d", fd) + } + } + }() + + e.FilePayload = control.NewFDMap(fdMap) + // containerd expects an actual process to represent the container being // executed. If detach was specified, starts a child in non-detach mode, // write the child's PID to the pid file. So when the container returns, the @@ -330,7 +363,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: urpc.FilePayload{[]*os.File{os.Stdin, os.Stdout, os.Stderr}}, + FilePayload: control.NewFDMap(map[int]*os.File{ + 0: os.Stdin, + 1: os.Stdout, + 2: os.Stderr, + }), }, nil } @@ -379,7 +416,11 @@ func argsFromProcess(p *specs.Process, enableRaw bool) (*control.ExecArgs, error ExtraKGIDs: extraKGIDs, Capabilities: caps, StdioIsPty: p.Terminal, - FilePayload: urpc.FilePayload{Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}}, + FilePayload: control.NewFDMap(map[int]*os.File{ + 0: os.Stdin, + 1: os.Stdout, + 2: os.Stderr, + }), }, nil } diff --git a/runsc/cmd/exec_test.go b/runsc/cmd/exec_test.go index a1e980d08..4c858911e 100644 --- a/runsc/cmd/exec_test.go +++ b/runsc/cmd/exec_test.go @@ -24,7 +24,6 @@ import ( "gvisor.dev/gvisor/pkg/abi/linux" "gvisor.dev/gvisor/pkg/sentry/control" "gvisor.dev/gvisor/pkg/sentry/kernel/auth" - "gvisor.dev/gvisor/pkg/urpc" ) func TestUser(t *testing.T) { @@ -76,10 +75,14 @@ func TestCLIArgs(t *testing.T) { expected: control.ExecArgs{ Argv: []string{"ls", "/"}, WorkingDirectory: "/foo/bar", - FilePayload: urpc.FilePayload{Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}}, - KUID: 0, - KGID: 0, - ExtraKGIDs: []auth.KGID{1, 2, 3}, + FilePayload: control.NewFDMap(map[int]*os.File{ + 0: os.Stdin, + 1: os.Stdout, + 2: os.Stderr, + }), + KUID: 0, + KGID: 0, + ExtraKGIDs: []auth.KGID{1, 2, 3}, Capabilities: &auth.TaskCapabilities{ BoundingCaps: auth.CapabilitySetOf(linux.CAP_DAC_OVERRIDE), EffectiveCaps: auth.CapabilitySetOf(linux.CAP_DAC_OVERRIDE), @@ -129,10 +132,14 @@ func TestJSONArgs(t *testing.T) { expected: control.ExecArgs{ Argv: []string{"ls", "/"}, WorkingDirectory: "/foo/bar", - FilePayload: urpc.FilePayload{Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}}, - KUID: 0, - KGID: 0, - ExtraKGIDs: []auth.KGID{1, 2, 3}, + FilePayload: control.NewFDMap(map[int]*os.File{ + 0: os.Stdin, + 1: os.Stdout, + 2: os.Stderr, + }), + KUID: 0, + KGID: 0, + ExtraKGIDs: []auth.KGID{1, 2, 3}, Capabilities: &auth.TaskCapabilities{ BoundingCaps: auth.CapabilitySetOf(linux.CAP_DAC_OVERRIDE), EffectiveCaps: auth.CapabilitySetOf(linux.CAP_DAC_OVERRIDE), diff --git a/runsc/cmd/fd_mapping.go b/runsc/cmd/fd_mapping.go new file mode 100644 index 000000000..48a76f63e --- /dev/null +++ b/runsc/cmd/fd_mapping.go @@ -0,0 +1,72 @@ +// Copyright 2023 The gVisor Authors. +// +// 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 cmd + +import ( + "fmt" + "strconv" + "strings" + + "gvisor.dev/gvisor/runsc/boot" +) + +// fdMappings can be used with flags that appear multiple times. +type fdMappings []boot.FDMapping + +// String implements flag.Value. +func (i *fdMappings) String() string { + return fmt.Sprintf("%v", *i) +} + +// Get implements flag.Value. +func (i *fdMappings) Get() any { + return i +} + +// GetArray returns an array of mappings. +func (i *fdMappings) GetArray() []boot.FDMapping { + return *i +} + +// Set implements flag.Value and appends a mapping from the command line to the +// mappings array. +func (i *fdMappings) Set(s string) error { + split := strings.Split(s, ":") + if len(split) != 2 { + return fmt.Errorf("invalid flag value: must be of format M:N") + } + + fdHost, err := strconv.Atoi(split[0]) + if err != nil { + return fmt.Errorf("invalid flag host value: %v", err) + } + if fdHost < 0 { + return fmt.Errorf("flag host value must be >= 0: %d", fdHost) + } + + fdGuest, err := strconv.Atoi(split[1]) + if err != nil { + return fmt.Errorf("invalid flag guest value: %v", err) + } + if fdGuest < 0 { + return fmt.Errorf("flag guest value must be >= 0: %d", fdGuest) + } + + *i = append(*i, boot.FDMapping{ + Host: fdHost, + Guest: fdGuest, + }) + return nil +} diff --git a/runsc/cmd/run.go b/runsc/cmd/run.go index bbeb610b9..48eaf9f00 100644 --- a/runsc/cmd/run.go +++ b/runsc/cmd/run.go @@ -16,9 +16,11 @@ package cmd import ( "context" + "os" "github.com/google/subcommands" "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/log" "gvisor.dev/gvisor/runsc/cmd/util" "gvisor.dev/gvisor/runsc/config" "gvisor.dev/gvisor/runsc/container" @@ -33,6 +35,10 @@ type Run struct { // detach indicates that runsc has to start a process and exit without waiting it. detach bool + + // passFDs are user-supplied FDs from the host to be exposed to the + // sandboxed app. + passFDs intFlags } // Name implements subcommands.Command.Name. @@ -54,6 +60,7 @@ func (*Run) Usage() string { // SetFlags implements subcommands.Command.SetFlags. 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 descriptors passed to the container. Can be supplied multiple times.") r.Create.SetFlags(f) } @@ -89,6 +96,26 @@ func (r *Run) Execute(_ context.Context, f *flag.FlagSet, args ...any) subcomman } specutils.LogSpecDebug(spec, conf.OCISeccomp) + // Create files from file descriptors. + fdMap := make(map[int]*os.File) + for _, fd := range r.passFDs { + file := os.NewFile(uintptr(fd), "") + if file == nil { + return util.Errorf("Failed to create file from file descriptor %d", fd) + } + fdMap[fd] = file + } + + // Close the underlying file descriptors after we have passed them. + defer func() { + for _, file := range fdMap { + fd := file.Fd() + if file.Close() != nil { + log.Debugf("Failed to close FD %d", fd) + } + } + }() + runArgs := container.Args{ ID: id, Spec: spec, @@ -97,6 +124,7 @@ func (r *Run) Execute(_ context.Context, f *flag.FlagSet, args ...any) subcomman PIDFile: r.pidFile, UserLog: r.userLog, Attached: !r.detach, + PassFiles: fdMap, } ws, err := container.Run(conf, runArgs) if err != nil { diff --git a/runsc/container/BUILD b/runsc/container/BUILD index 8788651b9..673ddf116 100644 --- a/runsc/container/BUILD +++ b/runsc/container/BUILD @@ -83,7 +83,6 @@ go_test( "//pkg/sync", "//pkg/test/testutil", "//pkg/unet", - "//pkg/urpc", "//runsc/boot", "//runsc/cgroup", "//runsc/config", diff --git a/runsc/container/console_test.go b/runsc/container/console_test.go index d3a08eed2..0c98c9364 100644 --- a/runsc/container/console_test.go +++ b/runsc/container/console_test.go @@ -30,7 +30,6 @@ import ( "gvisor.dev/gvisor/pkg/sync" "gvisor.dev/gvisor/pkg/test/testutil" "gvisor.dev/gvisor/pkg/unet" - "gvisor.dev/gvisor/pkg/urpc" ) // socketPath creates a path inside bundleDir and ensures that the returned @@ -282,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: urpc.FilePayload{ - Files: []*os.File{ptyReplica, ptyReplica, ptyReplica}, - }, + FilePayload: control.NewFDMap(map[int]*os.File{ + 0: ptyReplica, 1: ptyReplica, 2: ptyReplica, + }), StdioIsPty: true, } diff --git a/runsc/container/container.go b/runsc/container/container.go index 304c14347..a82f2255b 100644 --- a/runsc/container/container.go +++ b/runsc/container/container.go @@ -176,6 +176,10 @@ type Args struct { // // It only applies for the init container. Attached bool + + // PassFiles are user-supplied files from the host to be exposed to the + // sandboxed app. + PassFiles map[int]*os.File } // New creates the container in a new Sandbox process, unless the metadata @@ -296,6 +300,7 @@ func New(conf *config.Config, args Args) (*Container, error) { Cgroup: containerCgroup, Attached: args.Attached, OverlayFilestoreFiles: overlayFilestoreFiles, + PassFiles: args.PassFiles, } sand, err := sandbox.New(conf, sandArgs) if err != nil { diff --git a/runsc/container/container_test.go b/runsc/container/container_test.go index f940ca272..26cc3186f 100644 --- a/runsc/container/container_test.go +++ b/runsc/container/container_test.go @@ -42,7 +42,6 @@ import ( "gvisor.dev/gvisor/pkg/sentry/platform" "gvisor.dev/gvisor/pkg/sync" "gvisor.dev/gvisor/pkg/test/testutil" - "gvisor.dev/gvisor/pkg/urpc" "gvisor.dev/gvisor/runsc/cgroup" "gvisor.dev/gvisor/runsc/config" "gvisor.dev/gvisor/runsc/flag" @@ -78,9 +77,11 @@ func executeCombinedOutput(conf *config.Config, cont *Container, name string, ar defer r.Close() args := &control.ExecArgs{ - Filename: name, - Argv: append([]string{name}, arg...), - FilePayload: urpc.FilePayload{Files: []*os.File{os.Stdin, w, w}}, + Filename: name, + Argv: append([]string{name}, arg...), + FilePayload: control.NewFDMap(map[int]*os.File{ + 0: os.Stdin, 1: w, 2: w, + }), } ws, err := cont.executeSync(conf, args) w.Close() @@ -853,9 +854,9 @@ func TestExec(t *testing.T) { _, err = cont.executeSync(conf, &control.ExecArgs{ Argv: []string{"/nonexist"}, - FilePayload: urpc.FilePayload{ - Files: []*os.File{os.NewFile(uintptr(fds[1]), "sock")}, - }, + FilePayload: control.NewFDMap(map[int]*os.File{ + 0: os.NewFile(uintptr(fds[1]), "sock"), + }), }) want := "failed to load /nonexist" if err == nil || !strings.Contains(err.Error(), want) { @@ -2726,3 +2727,168 @@ func TestSandboxCommunicationUnshare(t *testing.T) { t.Errorf("SignalContainer(): %v", err) } } + +// writeAndReadFromPipe writes the bytes to the write end of the pipe, then +// reads from the read end and returns the result. +func writeAndReadFromPipe(write, read *os.File, msg string) (string, error) { + // Write the message to be read by the guest. + if _, err := io.StringWriter(write).WriteString(msg); err != nil { + return "", fmt.Errorf("failed to write message to pipe: %w", err) + } + write.Close() + + // Read and return the message. + response, err := io.ReadAll(read) + if err != nil { + return "", fmt.Errorf("failed to read from pipe: %w", err) + } + read.Close() + + return string(response), nil +} + +func createPipes() (*os.File, *os.File, *os.File, *os.File, func(), error) { + // This is the first pipe which the host writes to and the guest reads + // from. + guestRead, hostWrite, err := os.Pipe() + if err != nil { + return nil, nil, nil, nil, nil, err + } + + // This is the second pipe which the guest writes to and the host reads + // from. + hostRead, guestWrite, err := os.Pipe() + if err != nil { + guestRead.Close() + hostWrite.Close() + return nil, nil, nil, nil, nil, err + } + + cleanup := func() { + guestRead.Close() + hostWrite.Close() + hostRead.Close() + guestWrite.Close() + } + + return guestRead, hostWrite, hostRead, guestWrite, cleanup, nil +} + +// TestFDPassingRun checks that file descriptors passed into a new container +// work as expected. +func TestFDPassingRun(t *testing.T) { + guestRead, hostWrite, hostRead, guestWrite, cleanup, err := createPipes() + if err != nil { + t.Fatalf("error creating pipes: %v", err) + } + defer cleanup() + + // In the guest, read from the host and write the result back to the host. + conf := testutil.TestConfig(t) + cmd := fmt.Sprintf("cat /proc/self/fd/%d > /proc/self/fd/%d", int(guestRead.Fd()), int(guestWrite.Fd())) + spec := testutil.NewSpecWithArgs("bash", "-c", cmd) + + _, 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, + PassFiles: map[int]*os.File{ + int(guestRead.Fd()): guestRead, + int(guestWrite.Fd()): guestWrite, + }, + } + + 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) + } + + // We close guestWrite here because it has been passed into the container. + // If we do not close it, we will never see an EOF. + guestWrite.Close() + + msg := "hello" + got, err := writeAndReadFromPipe(hostWrite, hostRead, msg) + if err != nil { + t.Fatal(err) + } + if got != msg { + t.Errorf("got message %q, want %q", got, msg) + } +} + +// TestFDPassingExec checks that file descriptors passed into an already +// running container work as expected. +func TestFDPassingExec(t *testing.T) { + guestRead, hostWrite, hostRead, guestWrite, cleanup, err := createPipes() + if err != nil { + t.Fatalf("error creating pipes: %v", err) + } + defer cleanup() + + conf := testutil.TestConfig(t) + + // We just sleep here because we want to test file descriptor passing + // inside a process executed inside 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) + } + + // Prepare executing a command in the running container. + 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{ + int(guestRead.Fd()): guestRead, + int(guestWrite.Fd()): guestWrite, + }), + } + + if _, err = cont.Execute(conf, execArgs); err != nil { + t.Fatalf("Failed to execute command: %v", err) + } + + // We close guestWrite here because it has been passed into the container. + // If we do not close it, we will never see an EOF. + guestWrite.Close() + + msg := "hello" + got, err := writeAndReadFromPipe(hostWrite, hostRead, msg) + if err != nil { + t.Fatal(err) + } + if got != msg { + t.Errorf("got message %q, want %q", got, msg) + } +} diff --git a/runsc/donation/donation.go b/runsc/donation/donation.go index cb5632282..cdf0340d5 100644 --- a/runsc/donation/donation.go +++ b/runsc/donation/donation.go @@ -106,6 +106,17 @@ func (f *Agency) Transfer(cmd *exec.Cmd, nextFD int) int { return nextFD } +// DonateAndTransferCustomFiles sets up the flags for passing file descriptors from the +// host to the sandbox. Making use of the agency is not necessary, +func DonateAndTransferCustomFiles(cmd *exec.Cmd, nextFD int, files map[int]*os.File) int { + for fd, file := range files { + cmd.Args = append(cmd.Args, fmt.Sprintf("--custom-fds=%d:%d", nextFD, fd)) + cmd.ExtraFiles = append(cmd.ExtraFiles, file) + nextFD++ + } + return nextFD +} + // Close closes any files the agency has taken ownership over. func (f *Agency) Close() { for _, file := range f.closePending { diff --git a/runsc/sandbox/sandbox.go b/runsc/sandbox/sandbox.go index bc41813ce..dbb5689d0 100644 --- a/runsc/sandbox/sandbox.go +++ b/runsc/sandbox/sandbox.go @@ -241,6 +241,10 @@ type Args struct { // SinkFiles is the an ordered array of files to be used by seccheck sinks // configured from the --pod-init-config file. SinkFiles []*os.File + + // PassFiles are user-supplied files from the host to be exposed to the + // sandboxed app. + PassFiles map[int]*os.File } // New creates the sandbox process. The caller must call Destroy() on the @@ -528,7 +532,17 @@ func (s *Sandbox) NewCGroup() (cgroup.Cgroup, error) { func (s *Sandbox) Execute(conf *config.Config, args *control.ExecArgs) (int32, error) { log.Debugf("Executing new process in container %q in sandbox %q", args.ContainerID, s.ID) - if err := s.configureStdios(conf, args.Files); err != nil { + // Stdios are those files which have an FD <= 2 in the process. We do not + // want the ownership of other files to be changed by configureStdios. + var stdios []*os.File + for i, fd := range args.GuestFDs { + if fd > 2 || i >= len(args.Files) { + continue + } + stdios = append(stdios, args.Files[i]) + } + + if err := s.configureStdios(conf, stdios); err != nil { return 0, err } @@ -929,8 +943,9 @@ func (s *Sandbox) createSandboxProcess(conf *config.Config, args *Args, startSyn cmd.Args = append(cmd.Args, "--attached") } - // nextFD must not be used beyond this point. - _ = donations.Transfer(cmd, nextFD) + nextFD = donations.Transfer(cmd, nextFD) + + _ = donation.DonateAndTransferCustomFiles(cmd, nextFD, args.PassFiles) // Add container ID as the last argument. cmd.Args = append(cmd.Args, s.ID)