mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
Add --pass-fd flag to runsc run and exec
This commit implements file descriptor passing from the host to the guest. It implements a --pass-fd flag that can be specified multiple times with FD numbers from the host that will be inserted into the file descriptor table of the guest.
This commit is contained in:
+61
-10
@@ -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
|
||||
}
|
||||
|
||||
+37
-2
@@ -100,6 +100,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
|
||||
|
||||
@@ -185,6 +188,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())
|
||||
@@ -210,6 +228,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
|
||||
@@ -283,6 +304,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)
|
||||
@@ -525,6 +552,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()
|
||||
}
|
||||
@@ -815,7 +845,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)
|
||||
}
|
||||
@@ -1375,7 +1405,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))
|
||||
}
|
||||
@@ -1385,6 +1415,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)
|
||||
|
||||
+1
-2
@@ -16,6 +16,7 @@ go_library(
|
||||
"do.go",
|
||||
"events.go",
|
||||
"exec.go",
|
||||
"fd_mapping.go",
|
||||
"gofer.go",
|
||||
"help.go",
|
||||
"install.go",
|
||||
@@ -62,7 +63,6 @@ go_library(
|
||||
"//pkg/state/statefile",
|
||||
"//pkg/sync",
|
||||
"//pkg/unet",
|
||||
"//pkg/urpc",
|
||||
"//runsc/boot",
|
||||
"//runsc/cmd/util",
|
||||
"//runsc/config",
|
||||
@@ -105,7 +105,6 @@ go_test(
|
||||
"//pkg/sentry/control",
|
||||
"//pkg/sentry/kernel/auth",
|
||||
"//pkg/test/testutil",
|
||||
"//pkg/urpc",
|
||||
"//runsc/cmd/util",
|
||||
"//runsc/config",
|
||||
"//runsc/container",
|
||||
|
||||
@@ -66,6 +66,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
|
||||
@@ -148,6 +151,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, "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.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")
|
||||
@@ -348,6 +352,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,
|
||||
|
||||
+43
-3
@@ -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 fdMappings
|
||||
}
|
||||
|
||||
// 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 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)")
|
||||
}
|
||||
|
||||
// Execute implements subcommands.Command.Execute. It starts a process in an
|
||||
@@ -140,6 +144,34 @@ 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.
|
||||
for _, mapping := range ex.passFDs {
|
||||
file := os.NewFile(uintptr(mapping.Host), "")
|
||||
if file == nil {
|
||||
util.Fatalf("failed to create file from file descriptor %d", mapping.Host)
|
||||
}
|
||||
fdMap[mapping.Guest] = 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)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
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 +362,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 +415,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
|
||||
}
|
||||
|
||||
|
||||
+16
-9
@@ -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),
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
// 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 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 {
|
||||
// Split returns a slice of length 1 if its first argument does not
|
||||
// contain the separator. An additional length check is not necessary.
|
||||
// In case no separator is used and the argument is a valid integer, we
|
||||
// assume that host FD and guest FD should be identical.
|
||||
fd, err := strconv.Atoi(split[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid flag value: must be an integer or a mapping of format M:N")
|
||||
}
|
||||
*i = append(*i, boot.FDMapping{
|
||||
Host: fd,
|
||||
Guest: fd,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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 fdMappings
|
||||
}
|
||||
|
||||
// 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 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)")
|
||||
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 _, mapping := range r.passFDs {
|
||||
file := os.NewFile(uintptr(mapping.Host), "")
|
||||
if file == nil {
|
||||
return util.Errorf("Failed to create file from file descriptor %d", mapping.Host)
|
||||
}
|
||||
fdMap[mapping.Guest] = 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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -292,6 +296,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 {
|
||||
|
||||
@@ -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()
|
||||
@@ -851,9 +852,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) {
|
||||
@@ -2724,3 +2725,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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("--pass-fd=%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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -927,8 +941,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)
|
||||
|
||||
Reference in New Issue
Block a user