Merge pull request #8634 from blechschmidt:passfd

PiperOrigin-RevId: 516362077
This commit is contained in:
gVisor bot
2023-03-13 17:02:22 -07:00
14 changed files with 474 additions and 41 deletions
+61 -10
View File
@@ -18,6 +18,7 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os"
"sort" "sort"
"strings" "strings"
"text/tabwriter" "text/tabwriter"
@@ -44,6 +45,41 @@ type Proc struct {
Kernel *kernel.Kernel 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. // ExecArgs is the set of arguments to exec.
type ExecArgs struct { type ExecArgs struct {
// Filename is the filename to load. // Filename is the filename to load.
@@ -84,7 +120,7 @@ type ExecArgs struct {
StdioIsPty bool StdioIsPty bool
// FilePayload determines the files to give to the new process. // FilePayload determines the files to give to the new process.
urpc.FilePayload FilePayload
// ContainerID is the container for the process being executed. // ContainerID is the container for the process being executed.
ContainerID string ContainerID string
@@ -97,7 +133,7 @@ type ExecArgs struct {
} }
// String prints the arguments as a string. // String prints the arguments as a string.
func (args ExecArgs) String() string { func (args *ExecArgs) String() string {
if len(args.Argv) == 0 { if len(args.Argv) == 0 {
return args.Filename return args.Filename
} }
@@ -189,19 +225,15 @@ func (proc *Proc) execAsync(args *ExecArgs) (*kernel.ThreadGroup, kernel.ThreadI
} }
initArgs.Filename = resolved initArgs.Filename = resolved
fds, err := fd.NewFromFiles(args.Files) fdMap, err := args.createFDMap()
if err != nil { 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() { defer func() {
for _, fd := range fds { for _, hostFD := range fdMap {
_ = fd.Close() _ = 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) ttyFile, err := fdimport.Import(ctx, fdTable, args.StdioIsPty, args.KUID, args.KGID, fdMap)
if err != nil { if err != nil {
return nil, 0, nil, err return nil, 0, nil, err
@@ -404,3 +436,22 @@ func ContainerUsage(kr *kernel.Kernel) map[string]uint64 {
} }
return cusage 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
}
+37 -2
View File
@@ -101,6 +101,9 @@ type containerInfo struct {
// stdioFDs contains stdin, stdout, and stderr. // stdioFDs contains stdin, stdout, and stderr.
stdioFDs []*fd.FD 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 are the FDs that attach the sandbox to the gofers.
goferFDs []*fd.FD goferFDs []*fd.FD
@@ -186,6 +189,21 @@ type execProcess struct {
hostTTY *fd.FD 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() { func init() {
// Initialize the random number generator. // Initialize the random number generator.
mrand.Seed(gtime.Now().UnixNano()) mrand.Seed(gtime.Now().UnixNano())
@@ -211,6 +229,9 @@ type Args struct {
// StdioFDs is the stdio for the application. The Loader takes ownership of // StdioFDs is the stdio for the application. The Loader takes ownership of
// these FDs and may close them at any time. // these FDs and may close them at any time.
StdioFDs []int 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 // OverlayFilestoreFDs are the FDs to the regular files that will back the
// tmpfs upper mount in the overlay mounts. // tmpfs upper mount in the overlay mounts.
OverlayFilestoreFDs []int OverlayFilestoreFDs []int
@@ -287,6 +308,12 @@ func New(args Args) (*Loader, error) {
for _, overlayFD := range args.OverlayFilestoreFDs { for _, overlayFD := range args.OverlayFilestoreFDs {
info.overlayFilestoreFDs = append(info.overlayFilestoreFDs, fd.New(overlayFD)) 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. // Create kernel and platform.
p, err := createPlatform(args.Conf, args.Device) p, err := createPlatform(args.Conf, args.Device)
@@ -529,6 +556,9 @@ func (l *Loader) Destroy() {
for _, f := range l.root.stdioFDs { for _, f := range l.root.stdioFDs {
_ = f.Close() _ = f.Close()
} }
for _, f := range l.root.passFDs {
_ = f.host.Close()
}
for _, f := range l.root.goferFDs { for _, f := range l.root.goferFDs {
_ = f.Close() _ = 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) { 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. // Create the FD map, which will set stdin, stdout, and stderr.
ctx := info.procArgs.NewContext(l.k) 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 { if err != nil {
return nil, nil, fmt.Errorf("importing fds: %w", err) 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 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 { 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)) 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], 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) k := kernel.KernelFromContext(ctx)
fdTable := k.NewFDTable() fdTable := k.NewFDTable()
ttyFile, err := fdimport.Import(ctx, fdTable, console, auth.KUID(user.UID), auth.KGID(user.GID), fdMap) ttyFile, err := fdimport.Import(ctx, fdTable, console, auth.KUID(user.UID), auth.KGID(user.GID), fdMap)
+1 -2
View File
@@ -19,6 +19,7 @@ go_library(
"do.go", "do.go",
"events.go", "events.go",
"exec.go", "exec.go",
"fd_mapping.go",
"gofer.go", "gofer.go",
"help.go", "help.go",
"install.go", "install.go",
@@ -69,7 +70,6 @@ go_library(
"//pkg/state/statefile", "//pkg/state/statefile",
"//pkg/sync", "//pkg/sync",
"//pkg/unet", "//pkg/unet",
"//pkg/urpc",
"//runsc/boot", "//runsc/boot",
"//runsc/cmd/util", "//runsc/cmd/util",
"//runsc/config", "//runsc/config",
@@ -112,7 +112,6 @@ go_test(
"//pkg/sentry/control", "//pkg/sentry/control",
"//pkg/sentry/kernel/auth", "//pkg/sentry/kernel/auth",
"//pkg/test/testutil", "//pkg/test/testutil",
"//pkg/urpc",
"//runsc/cmd/util", "//runsc/cmd/util",
"//runsc/config", "//runsc/config",
"//runsc/container", "//runsc/container",
+5
View File
@@ -87,6 +87,9 @@ type Boot struct {
// provided in that order. // provided in that order.
stdioFDs intFlags 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 // applyCaps determines if capabilities defined in the spec should be applied
// to the process. // to the process.
applyCaps bool 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.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.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.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.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.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") 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"), Device: os.NewFile(uintptr(b.deviceFD), "platform device"),
GoferFDs: b.ioFDs.GetArray(), GoferFDs: b.ioFDs.GetArray(),
StdioFDs: b.stdioFDs.GetArray(), StdioFDs: b.stdioFDs.GetArray(),
PassFDs: b.passFDs.GetArray(),
OverlayFilestoreFDs: b.overlayFilestoreFDs.GetArray(), OverlayFilestoreFDs: b.overlayFilestoreFDs.GetArray(),
NumCPU: b.cpuNum, NumCPU: b.cpuNum,
TotalMem: b.totalMem, TotalMem: b.totalMem,
+44 -3
View File
@@ -32,7 +32,6 @@ import (
"gvisor.dev/gvisor/pkg/log" "gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/pkg/sentry/control" "gvisor.dev/gvisor/pkg/sentry/control"
"gvisor.dev/gvisor/pkg/sentry/kernel/auth" "gvisor.dev/gvisor/pkg/sentry/kernel/auth"
"gvisor.dev/gvisor/pkg/urpc"
"gvisor.dev/gvisor/runsc/cmd/util" "gvisor.dev/gvisor/runsc/cmd/util"
"gvisor.dev/gvisor/runsc/config" "gvisor.dev/gvisor/runsc/config"
"gvisor.dev/gvisor/runsc/console" "gvisor.dev/gvisor/runsc/console"
@@ -58,6 +57,10 @@ type Exec struct {
// file descriptor referencing the master end of the console's // file descriptor referencing the master end of the console's
// pseudoterminal. // pseudoterminal.
consoleSocket string consoleSocket string
// passFDs are user-supplied FDs from the host to be exposed to the
// sandboxed app.
passFDs intFlags
} }
// Name implements subcommands.Command.Name. // 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.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.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.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 // 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) 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 // containerd expects an actual process to represent the container being
// executed. If detach was specified, starts a child in non-detach mode, // 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 // 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, ExtraKGIDs: extraKGIDs,
Capabilities: caps, Capabilities: caps,
StdioIsPty: ex.consoleSocket != "" || console.IsPty(os.Stdin.Fd()), 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 }, nil
} }
@@ -379,7 +416,11 @@ func argsFromProcess(p *specs.Process, enableRaw bool) (*control.ExecArgs, error
ExtraKGIDs: extraKGIDs, ExtraKGIDs: extraKGIDs,
Capabilities: caps, Capabilities: caps,
StdioIsPty: p.Terminal, 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 }, nil
} }
+16 -9
View File
@@ -24,7 +24,6 @@ import (
"gvisor.dev/gvisor/pkg/abi/linux" "gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/sentry/control" "gvisor.dev/gvisor/pkg/sentry/control"
"gvisor.dev/gvisor/pkg/sentry/kernel/auth" "gvisor.dev/gvisor/pkg/sentry/kernel/auth"
"gvisor.dev/gvisor/pkg/urpc"
) )
func TestUser(t *testing.T) { func TestUser(t *testing.T) {
@@ -76,10 +75,14 @@ func TestCLIArgs(t *testing.T) {
expected: control.ExecArgs{ expected: control.ExecArgs{
Argv: []string{"ls", "/"}, Argv: []string{"ls", "/"},
WorkingDirectory: "/foo/bar", WorkingDirectory: "/foo/bar",
FilePayload: urpc.FilePayload{Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}}, FilePayload: control.NewFDMap(map[int]*os.File{
KUID: 0, 0: os.Stdin,
KGID: 0, 1: os.Stdout,
ExtraKGIDs: []auth.KGID{1, 2, 3}, 2: os.Stderr,
}),
KUID: 0,
KGID: 0,
ExtraKGIDs: []auth.KGID{1, 2, 3},
Capabilities: &auth.TaskCapabilities{ Capabilities: &auth.TaskCapabilities{
BoundingCaps: auth.CapabilitySetOf(linux.CAP_DAC_OVERRIDE), BoundingCaps: auth.CapabilitySetOf(linux.CAP_DAC_OVERRIDE),
EffectiveCaps: 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{ expected: control.ExecArgs{
Argv: []string{"ls", "/"}, Argv: []string{"ls", "/"},
WorkingDirectory: "/foo/bar", WorkingDirectory: "/foo/bar",
FilePayload: urpc.FilePayload{Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}}, FilePayload: control.NewFDMap(map[int]*os.File{
KUID: 0, 0: os.Stdin,
KGID: 0, 1: os.Stdout,
ExtraKGIDs: []auth.KGID{1, 2, 3}, 2: os.Stderr,
}),
KUID: 0,
KGID: 0,
ExtraKGIDs: []auth.KGID{1, 2, 3},
Capabilities: &auth.TaskCapabilities{ Capabilities: &auth.TaskCapabilities{
BoundingCaps: auth.CapabilitySetOf(linux.CAP_DAC_OVERRIDE), BoundingCaps: auth.CapabilitySetOf(linux.CAP_DAC_OVERRIDE),
EffectiveCaps: auth.CapabilitySetOf(linux.CAP_DAC_OVERRIDE), EffectiveCaps: auth.CapabilitySetOf(linux.CAP_DAC_OVERRIDE),
+72
View File
@@ -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
}
+28
View File
@@ -16,9 +16,11 @@ package cmd
import ( import (
"context" "context"
"os"
"github.com/google/subcommands" "github.com/google/subcommands"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/runsc/cmd/util" "gvisor.dev/gvisor/runsc/cmd/util"
"gvisor.dev/gvisor/runsc/config" "gvisor.dev/gvisor/runsc/config"
"gvisor.dev/gvisor/runsc/container" "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 indicates that runsc has to start a process and exit without waiting it.
detach bool detach bool
// passFDs are user-supplied FDs from the host to be exposed to the
// sandboxed app.
passFDs intFlags
} }
// Name implements subcommands.Command.Name. // Name implements subcommands.Command.Name.
@@ -54,6 +60,7 @@ func (*Run) Usage() string {
// SetFlags implements subcommands.Command.SetFlags. // SetFlags implements subcommands.Command.SetFlags.
func (r *Run) SetFlags(f *flag.FlagSet) { func (r *Run) SetFlags(f *flag.FlagSet) {
f.BoolVar(&r.detach, "detach", false, "detach from the container's process") 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) 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) 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{ runArgs := container.Args{
ID: id, ID: id,
Spec: spec, Spec: spec,
@@ -97,6 +124,7 @@ func (r *Run) Execute(_ context.Context, f *flag.FlagSet, args ...any) subcomman
PIDFile: r.pidFile, PIDFile: r.pidFile,
UserLog: r.userLog, UserLog: r.userLog,
Attached: !r.detach, Attached: !r.detach,
PassFiles: fdMap,
} }
ws, err := container.Run(conf, runArgs) ws, err := container.Run(conf, runArgs)
if err != nil { if err != nil {
-1
View File
@@ -83,7 +83,6 @@ go_test(
"//pkg/sync", "//pkg/sync",
"//pkg/test/testutil", "//pkg/test/testutil",
"//pkg/unet", "//pkg/unet",
"//pkg/urpc",
"//runsc/boot", "//runsc/boot",
"//runsc/cgroup", "//runsc/cgroup",
"//runsc/config", "//runsc/config",
+3 -4
View File
@@ -30,7 +30,6 @@ import (
"gvisor.dev/gvisor/pkg/sync" "gvisor.dev/gvisor/pkg/sync"
"gvisor.dev/gvisor/pkg/test/testutil" "gvisor.dev/gvisor/pkg/test/testutil"
"gvisor.dev/gvisor/pkg/unet" "gvisor.dev/gvisor/pkg/unet"
"gvisor.dev/gvisor/pkg/urpc"
) )
// socketPath creates a path inside bundleDir and ensures that the returned // 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. // our PID counts get messed up.
Argv: []string{"/bin/bash", "--noprofile", "--norc"}, Argv: []string{"/bin/bash", "--noprofile", "--norc"},
// Pass the pty replica as FD 0, 1, and 2. // Pass the pty replica as FD 0, 1, and 2.
FilePayload: urpc.FilePayload{ FilePayload: control.NewFDMap(map[int]*os.File{
Files: []*os.File{ptyReplica, ptyReplica, ptyReplica}, 0: ptyReplica, 1: ptyReplica, 2: ptyReplica,
}, }),
StdioIsPty: true, StdioIsPty: true,
} }
+5
View File
@@ -176,6 +176,10 @@ type Args struct {
// //
// It only applies for the init container. // It only applies for the init container.
Attached bool 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 // 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, Cgroup: containerCgroup,
Attached: args.Attached, Attached: args.Attached,
OverlayFilestoreFiles: overlayFilestoreFiles, OverlayFilestoreFiles: overlayFilestoreFiles,
PassFiles: args.PassFiles,
} }
sand, err := sandbox.New(conf, sandArgs) sand, err := sandbox.New(conf, sandArgs)
if err != nil { if err != nil {
+173 -7
View File
@@ -42,7 +42,6 @@ import (
"gvisor.dev/gvisor/pkg/sentry/platform" "gvisor.dev/gvisor/pkg/sentry/platform"
"gvisor.dev/gvisor/pkg/sync" "gvisor.dev/gvisor/pkg/sync"
"gvisor.dev/gvisor/pkg/test/testutil" "gvisor.dev/gvisor/pkg/test/testutil"
"gvisor.dev/gvisor/pkg/urpc"
"gvisor.dev/gvisor/runsc/cgroup" "gvisor.dev/gvisor/runsc/cgroup"
"gvisor.dev/gvisor/runsc/config" "gvisor.dev/gvisor/runsc/config"
"gvisor.dev/gvisor/runsc/flag" "gvisor.dev/gvisor/runsc/flag"
@@ -78,9 +77,11 @@ func executeCombinedOutput(conf *config.Config, cont *Container, name string, ar
defer r.Close() defer r.Close()
args := &control.ExecArgs{ args := &control.ExecArgs{
Filename: name, Filename: name,
Argv: append([]string{name}, arg...), Argv: append([]string{name}, arg...),
FilePayload: urpc.FilePayload{Files: []*os.File{os.Stdin, w, w}}, FilePayload: control.NewFDMap(map[int]*os.File{
0: os.Stdin, 1: w, 2: w,
}),
} }
ws, err := cont.executeSync(conf, args) ws, err := cont.executeSync(conf, args)
w.Close() w.Close()
@@ -853,9 +854,9 @@ func TestExec(t *testing.T) {
_, err = cont.executeSync(conf, &control.ExecArgs{ _, err = cont.executeSync(conf, &control.ExecArgs{
Argv: []string{"/nonexist"}, Argv: []string{"/nonexist"},
FilePayload: urpc.FilePayload{ FilePayload: control.NewFDMap(map[int]*os.File{
Files: []*os.File{os.NewFile(uintptr(fds[1]), "sock")}, 0: os.NewFile(uintptr(fds[1]), "sock"),
}, }),
}) })
want := "failed to load /nonexist" want := "failed to load /nonexist"
if err == nil || !strings.Contains(err.Error(), want) { if err == nil || !strings.Contains(err.Error(), want) {
@@ -2726,3 +2727,168 @@ func TestSandboxCommunicationUnshare(t *testing.T) {
t.Errorf("SignalContainer(): %v", err) 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)
}
}
+11
View File
@@ -106,6 +106,17 @@ func (f *Agency) Transfer(cmd *exec.Cmd, nextFD int) int {
return nextFD 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. // Close closes any files the agency has taken ownership over.
func (f *Agency) Close() { func (f *Agency) Close() {
for _, file := range f.closePending { for _, file := range f.closePending {
+18 -3
View File
@@ -241,6 +241,10 @@ type Args struct {
// SinkFiles is the an ordered array of files to be used by seccheck sinks // SinkFiles is the an ordered array of files to be used by seccheck sinks
// configured from the --pod-init-config file. // configured from the --pod-init-config file.
SinkFiles []*os.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 // 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) { 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) 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 return 0, err
} }
@@ -929,8 +943,9 @@ func (s *Sandbox) createSandboxProcess(conf *config.Config, args *Args, startSyn
cmd.Args = append(cmd.Args, "--attached") cmd.Args = append(cmd.Args, "--attached")
} }
// nextFD must not be used beyond this point. nextFD = donations.Transfer(cmd, nextFD)
_ = donations.Transfer(cmd, nextFD)
_ = donation.DonateAndTransferCustomFiles(cmd, nextFD, args.PassFiles)
// Add container ID as the last argument. // Add container ID as the last argument.
cmd.Args = append(cmd.Args, s.ID) cmd.Args = append(cmd.Args, s.ID)