mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
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:
+71
-25
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user