Add support for execution via host file descriptor

This commit adds support for program execution via a host file
descriptor. To make use of this feature, the host file descriptor must
be provided to the --exec-fd argument. For example,

    exec 3</usr/bin/echo
    runsc exec --exec-fd=3 mycontainer hello world

will run the host's echo binary inside gVisor. In this case, "hello" is
supplied to echo as argv[0]. As a result, the output of the above
command is "world".

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