Add runsc flags --nvproxy and --nvproxy-docker.

The --nvproxy flag allows container GPU usage to be specified via device nodes
and mounts provided in the runtime spec, as when using Kubernetes with GKE's
Nvidia GPU device plugin
(https://github.com/GoogleCloudPlatform/container-engine-accelerators/tree/master/cmd/nvidia_gpu).

The --nvproxy-docker flag additionally allows container GPU usage to be
specified via the NVIDIA_VISIBLE_DEVICES container environment variable, as
when using `docker --gpus`. This does not require the Nvidia Container Toolkit
(or the Nvidia Container Runtime [Hook], which are part of the Toolkit), but
does require libnvidia-container, which is typically installed as a dependency
of the Nvidia Container Toolkit.

Updates #14

PiperOrigin-RevId: 535002602
This commit is contained in:
Jamie Liu
2023-05-24 15:32:17 -07:00
committed by gVisor bot
parent d51a90101a
commit e672476d06
18 changed files with 410 additions and 35 deletions
+2
View File
@@ -67,6 +67,8 @@ type KVM struct {
// KVM never changes mm_structs.
platform.UseHostProcessMemoryBarrier
platform.DoesOwnPageTables
// machine is the backing VM.
machine *machine
}
+24
View File
@@ -58,6 +58,14 @@ type Platform interface {
// is supported.
HaveGlobalMemoryBarrier() bool
// OwnsPageTables returns true if the Platform implementation manages any
// page tables directly (rather than via host mmap(2) etc.) As of this
// writing, this property is relevant because the AddressSpace interface
// does not support specification of memory type (cacheability), such that
// host FDs specifying memory types (e.g. device drivers) can only set them
// correctly in host-managed page tables.
OwnsPageTables() bool
// MapUnit returns the alignment used for optional mappings into this
// platform's AddressSpaces. Higher values indicate lower per-page costs
// for AddressSpace.MapFile. As a special case, a MapUnit of 0 indicates
@@ -167,6 +175,22 @@ func (UseHostProcessMemoryBarrier) GlobalMemoryBarrier() error {
return hostmm.GlobalMemoryBarrier()
}
// DoesOwnPageTables implements Platform.OwnsPageTables in the positive.
type DoesOwnPageTables struct{}
// OwnsPageTables implements Platform.OwnsPageTables.
func (DoesOwnPageTables) OwnsPageTables() bool {
return true
}
// DoesNotOwnPageTables implements Platform.OwnsPageTables in the negative.
type DoesNotOwnPageTables struct{}
// OwnsPageTables implements Platform.OwnsPageTables.
func (DoesNotOwnPageTables) OwnsPageTables() bool {
return false
}
// MemoryManager represents an abstraction above the platform address space
// which manages memory mappings and their contents.
type MemoryManager interface {
+1
View File
@@ -207,6 +207,7 @@ type PTrace struct {
platform.MMapMinAddr
platform.NoCPUPreemptionDetection
platform.UseHostGlobalMemoryBarrier
platform.DoesNotOwnPageTables
}
// New returns a new ptrace-based implementation of the platform interface.
+1
View File
@@ -296,6 +296,7 @@ func (c *context) PrepareSleep() {
type Systrap struct {
platform.NoCPUPreemptionDetection
platform.UseHostGlobalMemoryBarrier
platform.DoesNotOwnPageTables
// memoryFile is used to create a stub sysmsg stack
// which is shared with the Sentry.
+2
View File
@@ -51,6 +51,7 @@ go_library(
"//pkg/sentry/arch:registers_go_proto",
"//pkg/sentry/control",
"//pkg/sentry/devices/memdev",
"//pkg/sentry/devices/nvproxy",
"//pkg/sentry/devices/ttydev",
"//pkg/sentry/devices/tundev",
"//pkg/sentry/fdimport",
@@ -142,6 +143,7 @@ go_test(
"//pkg/cpuid",
"//pkg/fspath",
"//pkg/log",
"//pkg/sentry/kernel/auth",
"//pkg/sentry/seccheck",
"//pkg/sentry/vfs",
"//pkg/sync",
+1
View File
@@ -28,6 +28,7 @@ go_library(
"//pkg/abi/linux",
"//pkg/log",
"//pkg/seccomp",
"//pkg/sentry/devices/nvproxy",
"//pkg/sentry/platform",
"//pkg/sentry/socket/hostinet",
"//pkg/tcpip/link/fdbased",
+6
View File
@@ -20,6 +20,7 @@ package filter
import (
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/pkg/seccomp"
"gvisor.dev/gvisor/pkg/sentry/devices/nvproxy"
"gvisor.dev/gvisor/pkg/sentry/platform"
)
@@ -30,6 +31,7 @@ type Options struct {
HostNetworkRawSockets bool
HostFilesystem bool
ProfileEnable bool
NVProxy bool
ControllerFD int
}
@@ -58,6 +60,10 @@ func Install(opt Options) error {
Report("host filesystem enabled: syscall filters less restrictive!")
s.Merge(hostFilesystemFilters())
}
if opt.NVProxy {
Report("Nvidia GPU driver proxy enabled: syscall filters less restrictive!")
s.Merge(nvproxy.Filters())
}
s.Merge(opt.Platform.SyscallFilters())
+28 -14
View File
@@ -118,6 +118,9 @@ type containerInfo struct {
// overlaid. The first entry is for rootfs and the following entries are for
// bind mounts in spec.Mounts (in the same order).
overlayMediums []OverlayMedium
// nvidiaUVMDevMajor is the device major number used for nvidia-uvm.
nvidiaUVMDevMajor uint32
}
// Loader keeps state needed to start the kernel and run the container.
@@ -158,6 +161,9 @@ type Loader struct {
// /sys/devices/virtual/dmi/id/product_name.
productName string
// nvidiaUVMDevMajor is the device major number used for nvidia-uvm.
nvidiaUVMDevMajor uint32
// mu guards processes and porForwardProxies.
mu sync.Mutex
@@ -293,10 +299,15 @@ func New(args Args) (*Loader, error) {
kernel.IOUringEnabled = args.Conf.IOUring
info := containerInfo{
conf: args.Conf,
spec: args.Spec,
overlayMediums: args.OverlayMediums,
}
// Make host FDs stable between invocations. Host FDs must map to the exact
// same number when the sandbox is restored. Otherwise the wrong FD will be
// used.
info := containerInfo{overlayMediums: args.OverlayMediums}
newfd := startingStdioFD
for _, stdioFD := range args.StdioFDs {
@@ -339,6 +350,9 @@ func New(args Args) (*Loader, error) {
if err != nil {
return nil, fmt.Errorf("creating platform: %w", err)
}
if args.Conf.NVProxy && p.OwnsPageTables() {
return nil, fmt.Errorf("--nvproxy is incompatible with platform %s: owns page tables", args.Conf.Platform)
}
k := &kernel.Kernel{
Platform: p,
}
@@ -423,7 +437,7 @@ func New(args Args) (*Loader, error) {
return nil, fmt.Errorf("initializing kernel: %w", err)
}
if err := registerFilesystems(k); err != nil {
if err := registerFilesystems(k, &info); err != nil {
return nil, fmt.Errorf("registering filesystems: %w", err)
}
@@ -456,9 +470,6 @@ func New(args Args) (*Loader, error) {
return nil, fmt.Errorf("creating pod mount hints: %w", err)
}
info.conf = args.Conf
info.spec = args.Spec
// Set up host mount that will be used for imported fds.
hostFilesystem, err := host.NewFilesystem(k.VFS())
if err != nil {
@@ -475,14 +486,15 @@ func New(args Args) (*Loader, error) {
eid := execID{cid: args.ID}
l := &Loader{
k: k,
watchdog: dog,
sandboxID: args.ID,
processes: map[execID]*execProcess{eid: {}},
mountHints: mountHints,
root: info,
stopProfiling: stopProfiling,
productName: args.ProductName,
k: k,
watchdog: dog,
sandboxID: args.ID,
processes: map[execID]*execProcess{eid: {}},
mountHints: mountHints,
root: info,
stopProfiling: stopProfiling,
productName: args.ProductName,
nvidiaUVMDevMajor: info.nvidiaUVMDevMajor,
}
// We don't care about child signals; some platforms can generate a
@@ -627,6 +639,7 @@ func (l *Loader) installSeccompFilters() error {
HostNetworkRawSockets: hostnet && l.root.conf.EnableRaw,
HostFilesystem: l.root.conf.DirectFS,
ProfileEnable: l.root.conf.ProfileEnable,
NVProxy: l.root.conf.NVProxy,
ControllerFD: l.ctrl.srv.FD(),
}
if err := filter.Install(opts); err != nil {
@@ -822,6 +835,7 @@ func (l *Loader) startSubcontainer(spec *specs.Spec, conf *config.Config, cid st
goferFDs: goferFDs,
overlayFilestoreFDs: overlayFilestoreFDs,
overlayMediums: overlayMediums,
nvidiaUVMDevMajor: l.nvidiaUVMDevMajor,
}
info.procArgs, err = createProcessArgs(cid, spec, creds, l.k, pidns)
if err != nil {
@@ -914,7 +928,7 @@ func (l *Loader) createContainerProcess(root bool, cid string, info *containerIn
return nil, nil, err
}
}
if err := setupContainerVFS(ctx, info.conf, mntr, &info.procArgs); err != nil {
if err := setupContainerVFS(ctx, info, mntr, &info.procArgs); err != nil {
return nil, nil, err
}
+3 -1
View File
@@ -29,6 +29,7 @@ import (
"gvisor.dev/gvisor/pkg/cpuid"
"gvisor.dev/gvisor/pkg/fspath"
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
"gvisor.dev/gvisor/pkg/sentry/seccheck"
"gvisor.dev/gvisor/pkg/sentry/vfs"
"gvisor.dev/gvisor/pkg/sync"
@@ -480,7 +481,8 @@ func TestCreateMountNamespace(t *testing.T) {
}
ctx := l.k.SupervisorContext()
mns, err := mntr.mountAll(l.root.conf, &l.root.procArgs)
creds := auth.NewRootCredentials(l.root.procArgs.Credentials.UserNamespace)
mns, err := mntr.mountAll(ctx, creds, l.root.conf, &l.root.procArgs)
if err != nil {
t.Fatalf("mountAll: %v", err)
}
+126 -17
View File
@@ -31,6 +31,7 @@ import (
"gvisor.dev/gvisor/pkg/fspath"
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/pkg/sentry/devices/memdev"
"gvisor.dev/gvisor/pkg/sentry/devices/nvproxy"
"gvisor.dev/gvisor/pkg/sentry/devices/ttydev"
"gvisor.dev/gvisor/pkg/sentry/devices/tundev"
"gvisor.dev/gvisor/pkg/sentry/fsimpl/cgroupfs"
@@ -79,7 +80,7 @@ func selfOverlayFilestoreName(sandboxID string) string {
// tmpfs has some extra supported options that we must pass through.
var tmpfsAllowedData = []string{"mode", "size", "uid", "gid"}
func registerFilesystems(k *kernel.Kernel) error {
func registerFilesystems(k *kernel.Kernel, info *containerInfo) error {
ctx := k.SupervisorContext()
creds := auth.NewRootCredentials(k.RootUserNamespace())
vfsObj := k.VFS()
@@ -126,7 +127,7 @@ func registerFilesystems(k *kernel.Kernel) error {
AllowUserList: true,
})
// Setup files in devtmpfs.
// Register devices.
if err := memdev.Register(vfsObj); err != nil {
return fmt.Errorf("registering memdev: %w", err)
}
@@ -139,11 +140,11 @@ func registerFilesystems(k *kernel.Kernel) error {
return fmt.Errorf("registering tundev: %v", err)
}
}
if err := fuse.Register(vfsObj); err != nil {
return fmt.Errorf("registering fusedev: %w", err)
}
// Setup files in devtmpfs.
a, err := devtmpfs.NewAccessor(ctx, vfsObj, creds, devtmpfs.Name)
if err != nil {
return fmt.Errorf("creating devtmpfs accessor: %w", err)
@@ -164,21 +165,42 @@ func registerFilesystems(k *kernel.Kernel) error {
return fmt.Errorf("creating tundev devtmpfs files: %v", err)
}
}
if err := fuse.CreateDevtmpfsFile(ctx, a); err != nil {
return fmt.Errorf("creating fusedev devtmpfs files: %w", err)
}
if err := nvproxyRegisterDevicesAndCreateFiles(ctx, info, k, vfsObj, a); err != nil {
return err
}
return nil
}
func setupContainerVFS(ctx context.Context, conf *config.Config, mntr *containerMounter, procArgs *kernel.CreateProcessArgs) error {
mns, err := mntr.mountAll(conf, procArgs)
func setupContainerVFS(ctx context.Context, info *containerInfo, mntr *containerMounter, procArgs *kernel.CreateProcessArgs) error {
// Create context with root credentials to mount the filesystem (the current
// user may not be privileged enough).
rootCreds := auth.NewRootCredentials(procArgs.Credentials.UserNamespace)
rootProcArgs := *procArgs
rootProcArgs.WorkingDirectory = "/"
rootProcArgs.Credentials = rootCreds
rootProcArgs.Umask = 0022
rootProcArgs.MaxSymlinkTraversals = linux.MaxSymlinkTraversals
rootCtx := rootProcArgs.NewContext(mntr.k)
mns, err := mntr.mountAll(rootCtx, rootCreds, info.conf, &rootProcArgs)
if err != nil {
return fmt.Errorf("failed to setupFS: %w", err)
}
procArgs.MountNamespace = mns
mnsRoot := mns.Root()
mnsRoot.IncRef()
defer mnsRoot.DecRef(rootCtx)
if err := createDeviceFiles(rootCtx, rootCreds, info, mntr.k.VFS(), mnsRoot); err != nil {
return fmt.Errorf("failed to create device files: %w", err)
}
// We are executing a file directly. Do not resolve the executable path.
if procArgs.File != nil {
return nil
@@ -396,19 +418,9 @@ func (c *containerMounter) getMountAccessType(conf *config.Config, mount *specs.
return conf.FileAccessMounts
}
func (c *containerMounter) mountAll(conf *config.Config, procArgs *kernel.CreateProcessArgs) (*vfs.MountNamespace, error) {
func (c *containerMounter) mountAll(rootCtx context.Context, rootCreds *auth.Credentials, conf *config.Config, rootProcArgs *kernel.CreateProcessArgs) (*vfs.MountNamespace, error) {
log.Infof("Configuring container's file system")
// Create context with root credentials to mount the filesystem (the current
// user may not be privileged enough).
rootCreds := auth.NewRootCredentials(procArgs.Credentials.UserNamespace)
rootProcArgs := *procArgs
rootProcArgs.WorkingDirectory = "/"
rootProcArgs.Credentials = rootCreds
rootProcArgs.Umask = 0022
rootProcArgs.MaxSymlinkTraversals = linux.MaxSymlinkTraversals
rootCtx := rootProcArgs.NewContext(c.k)
mns, err := c.createMountNamespace(rootCtx, conf, rootCreds)
if err != nil {
return nil, fmt.Errorf("creating mount namespace: %w", err)
@@ -1026,3 +1038,100 @@ func (c *containerMounter) configureRestore(ctx context.Context) (context.Contex
}
return context.WithValue(ctx, gofer.CtxRestoreServerFDMap, fdmap), nil
}
func createDeviceFiles(ctx context.Context, creds *auth.Credentials, info *containerInfo, vfsObj *vfs.VirtualFilesystem, root vfs.VirtualDentry) error {
if info.spec.Linux == nil {
return nil
}
for _, dev := range info.spec.Linux.Devices {
pop := vfs.PathOperation{
Root: root,
Start: root,
Path: fspath.Parse(dev.Path),
}
opts := vfs.MknodOptions{
Mode: linux.FileMode(dev.FileMode.Perm()),
}
// See https://github.com/opencontainers/runtime-spec/blob/main/config-linux.md#devices.
switch dev.Type {
case "b":
opts.Mode |= linux.S_IFBLK
opts.DevMajor = uint32(dev.Major)
opts.DevMinor = uint32(dev.Minor)
case "c", "u":
opts.Mode |= linux.S_IFCHR
opts.DevMajor = uint32(dev.Major)
opts.DevMinor = uint32(dev.Minor)
case "p":
opts.Mode |= linux.S_IFIFO
default:
return fmt.Errorf("specified device at %q has invalid type %q", dev.Path, dev.Type)
}
if dev.Path == "/dev/nvidia-uvm" && info.nvidiaUVMDevMajor != 0 && opts.DevMajor != info.nvidiaUVMDevMajor {
// nvidia-uvm's major device number is dynamically assigned, so the
// number that it has on the host may differ from the number that
// it has in sentry VFS; switch from the former to the latter.
log.Infof("Switching /dev/nvidia-uvm device major number from %d to %d", dev.Major, info.nvidiaUVMDevMajor)
opts.DevMajor = info.nvidiaUVMDevMajor
}
if err := vfsObj.MkdirAllAt(ctx, path.Dir(dev.Path), root, creds, &vfs.MkdirOptions{
Mode: 0o755,
}); err != nil {
return fmt.Errorf("failed to create ancestor directories of %q: %w", dev.Path, err)
}
// EEXIST is silently ignored; compare
// opencontainers/runc:libcontainer/rootfs_linux.go:createDeviceNode().
created := true
if err := vfsObj.MknodAt(ctx, creds, &pop, &opts); err != nil && !linuxerr.Equals(linuxerr.EEXIST, err) {
if linuxerr.Equals(linuxerr.EEXIST, err) {
created = false
} else {
return fmt.Errorf("failed to create device file at %q: %w", dev.Path, err)
}
}
if created && (dev.UID != nil || dev.GID != nil) {
var opts vfs.SetStatOptions
if dev.UID != nil {
opts.Stat.Mask |= linux.STATX_UID
opts.Stat.UID = *dev.UID
}
if dev.GID != nil {
opts.Stat.Mask |= linux.STATX_GID
opts.Stat.GID = *dev.GID
}
if err := vfsObj.SetStatAt(ctx, creds, &pop, &opts); err != nil {
return fmt.Errorf("failed to set UID/GID for device file %q: %w", dev.Path, err)
}
}
}
return nil
}
func nvproxyRegisterDevicesAndCreateFiles(ctx context.Context, info *containerInfo, k *kernel.Kernel, vfsObj *vfs.VirtualFilesystem, a *devtmpfs.Accessor) error {
if !info.conf.NVProxy {
return nil
}
uvmDevMajor, err := k.VFS().GetDynamicCharDevMajor()
if err != nil {
return fmt.Errorf("reserving device major number for nvidia-uvm: %w", err)
}
if err := nvproxy.Register(vfsObj, uvmDevMajor); err != nil {
return fmt.Errorf("registering nvproxy driver: %w", err)
}
info.nvidiaUVMDevMajor = uvmDevMajor
if specutils.HaveNvidiaVisibleDevices(info.spec, info.conf) {
if err := nvproxy.CreateDriverDevtmpfsFiles(ctx, a, uvmDevMajor); err != nil {
return fmt.Errorf("creating nvproxy devtmpfs files: %w", err)
}
nvd, err := specutils.NvidiaVisibleDevices(info.spec, info.conf)
if err != nil {
return fmt.Errorf("getting NVIDIA_VISIBLE_DEVICES: %w", err)
}
for _, d := range nvd {
if err := nvproxy.CreateIndexDevtmpfsFile(ctx, a, d); err != nil {
return fmt.Errorf("creating nvproxy devtmpfs file for device %d: %w", d, err)
}
}
}
return nil
}
+1 -1
View File
@@ -242,7 +242,7 @@ func (b *Boot) Execute(_ context.Context, f *flag.FlagSet, args ...any) subcomma
}
if b.setUpRoot {
if err := setUpChroot(b.pidns); err != nil {
if err := setUpChroot(b.pidns, conf); err != nil {
util.Fatalf("error setting up chroot: %v", err)
}
+39 -2
View File
@@ -15,12 +15,15 @@
package cmd
import (
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/runsc/config"
"gvisor.dev/gvisor/runsc/specutils"
)
@@ -78,7 +81,7 @@ func copyFile(dst, src string) error {
// setUpChroot creates an empty directory with runsc mounted at /runsc and proc
// mounted at /proc.
func setUpChroot(pidns bool) error {
func setUpChroot(pidns bool, conf *config.Config) error {
// We are a new mount namespace, so we can use /tmp as a directory to
// construct a new root.
chroot := os.TempDir()
@@ -92,7 +95,7 @@ func setUpChroot(pidns bool) error {
}
if err := specutils.SafeMount("runsc-root", chroot, "tmpfs", unix.MS_NOSUID|unix.MS_NODEV|unix.MS_NOEXEC, "", "/proc"); err != nil {
return fmt.Errorf("error mounting tmpfs in choot: %v", err)
return fmt.Errorf("error mounting tmpfs in chroot: %v", err)
}
if err := os.Mkdir(filepath.Join(chroot, "etc"), 0755); err != nil {
@@ -114,9 +117,43 @@ func setUpChroot(pidns bool) error {
}
}
if err := nvproxyUpdateChroot(chroot, conf); err != nil {
return fmt.Errorf("error configuring chroot for Nvidia GPUs: %w", err)
}
if err := specutils.SafeMount("", chroot, "", unix.MS_REMOUNT|unix.MS_RDONLY|unix.MS_BIND, "", "/proc"); err != nil {
return fmt.Errorf("error remounting chroot in read-only: %v", err)
}
return pivotRoot(chroot)
}
func nvproxyUpdateChroot(chroot string, conf *config.Config) error {
if !conf.NVProxy {
return nil
}
if err := os.Mkdir(filepath.Join(chroot, "dev"), 0755); err != nil && !errors.Is(err, os.ErrExist) {
return fmt.Errorf("error creating /dev in chroot: %w", err)
}
if err := mountInChroot(chroot, "/dev/nvidiactl", "/dev/nvidiactl", "bind", unix.MS_BIND); err != nil {
return fmt.Errorf("error mounting /dev/nvidiactl in chroot: %w", err)
}
if err := mountInChroot(chroot, "/dev/nvidia-uvm", "/dev/nvidia-uvm", "bind", unix.MS_BIND); err != nil {
return fmt.Errorf("error mounting /dev/nvidia-uvm in chroot: %w", err)
}
// We must bind-mount all available GPUs because in the Kubernetes case,
// the set of usable GPUs isn't known until time of subcontainer creation.
paths, err := filepath.Glob("/dev/nvidia*")
if err != nil {
return fmt.Errorf("enumerating Nvidia device files: %w", err)
}
re := regexp.MustCompile(`^/dev/nvidia\d+$`)
for _, path := range paths {
if re.MatchString(path) {
if err := mountInChroot(chroot, path, path, "bind", unix.MS_BIND); err != nil {
return fmt.Errorf("error mounting %q in chroot: %v", path, err)
}
}
}
return nil
}
+8
View File
@@ -282,6 +282,14 @@ type Config struct {
// exists, but is mostly idle. Not supported in rootless mode.
DirectFS bool `flag:"directfs"`
// NVProxy enables support for Nvidia GPUs.
NVProxy bool `flag:"nvproxy"`
// NVProxyDocker exposes GPUs to containers based on the
// NVIDIA_VISIBLE_DEVICES container environment variable, as requested by
// containers or set by `docker --gpus`.
NVProxyDocker bool `flag:"nvproxy-docker"`
// TestOnlyAllowRunAsCurrentUserWithoutChroot should only be used in
// tests. It allows runsc to start the sandbox process as the current
// user, and without chrooting the sandbox process. This can be
+4
View File
@@ -115,6 +115,10 @@ func RegisterFlags(flagSet *flag.FlagSet) {
flagSet.Bool("buffer-pooling", true, "enable allocation of buffers from a shared pool instead of the heap.")
flagSet.Bool("EXPERIMENTAL-afxdp", false, "EXPERIMENTAL. Use an AF_XDP socket to receive packets.")
// Flags that control sandbox runtime behavior: accelerator related.
flagSet.Bool("nvproxy", false, "EXPERIMENTAL: enable support for Nvidia GPUs")
flagSet.Bool("nvproxy-docker", false, "Expose GPUs to containers based on NVIDIA_VISIBLE_DEVICES, as requested by the container or set by `docker --gpus`. Allows containers to self-serve GPU access and thus disabled by default for security. libnvidia-container must be installed on the host. No effect unless --nvproxy is enabled.")
// Test flags, not to be used outside tests, ever.
flagSet.Bool("TESTONLY-unsafe-nonroot", false, "TEST ONLY; do not ever use! This skips many security measures that isolate the host from the sandbox.")
flagSet.String("TESTONLY-test-name-env", "", "TEST ONLY; do not ever use! Used for automated tests to improve logging.")
+76
View File
@@ -297,6 +297,10 @@ func New(conf *config.Config, args Args) (*Container, error) {
}
c.OverlayMediums = overlayMediums
if err := runInCgroup(containerCgroup, func() error {
if err := nvproxyUpdateAppRootFilesystem(args.Spec, conf); err != nil {
return err
}
ioFiles, specFile, err := c.createGoferProcess(args.Spec, conf, args.BundleDir, args.Attached)
if err != nil {
return fmt.Errorf("cannot create gofer process: %w", err)
@@ -1661,3 +1665,75 @@ func logIDMappings(mappings []specs.LinuxIDMapping, idType string) {
log.Debugf("\tContainer ID: %d, Host ID: %d, Range Length: %d", m.ContainerID, m.HostID, m.Size)
}
}
func nvproxyUpdateAppRootFilesystem(spec *specs.Spec, conf *config.Config) error {
if !specutils.HaveNvidiaVisibleDevices(spec, conf) {
return nil
}
// Delegate to nvidia-container-cli from libnvidia-container. This
// essentially replicates
// nvidia-container-toolkit:cmd/nvidia-container-runtime-hook, i.e. the
// binary that executeHook() is hard-coded to skip, with differences noted
// inline. We do this rather than move the prestart hook because the
// "runtime environment" in which prestart hooks execute is vaguely
// defined, such that nvidia-container-runtime-hook and existing runsc
// hooks differ in their expected environment.
//
// Note that nvidia-container-cli will set up files in /dev and /proc which
// are useless, since they will be hidden by sentry devtmpfs and procfs
// respectively (and some device files will have the wrong device numbers
// from the application's perspective since nvproxy may register device
// numbers in sentry VFS that differ from those on the host, e.g. for
// nvidia-uvm). These files are separately created during sandbox VFS
// construction. For this reason, we don't need to parse
// NVIDIA_VISIBLE_DEVICES or pass --device to nvidia-container-cli.
if spec.Root == nil {
return fmt.Errorf("spec missing root filesystem")
}
// Locate binaries. For security reasons, unlike
// nvidia-container-runtime-hook, we don't add the container's filesystem
// to the search path. We also don't support
// /etc/nvidia-container-runtime/config.toml to avoid importing a TOML
// parser.
cliPath, err := exec.LookPath("nvidia-container-cli")
if err != nil {
return fmt.Errorf("failed to locate nvidia-container-cli in PATH: %w", err)
}
ldconfigPath, err := exec.LookPath("ldconfig")
if err != nil {
return fmt.Errorf("failed to locate ldconfig in PATH: %w", err)
}
// nvidia-container-cli does not create this directory.
if err := os.MkdirAll(path.Join(spec.Root.Path, "proc", "driver", "nvidia"), 0555); err != nil {
return fmt.Errorf("failed to create /proc/driver/nvidia in app filesystem: %w", err)
}
argv := []string{
cliPath,
"--load-kmods",
"configure",
fmt.Sprintf("--ldconfig=@%s", ldconfigPath),
"--no-cgroups", // runsc doesn't configure device cgroups yet
"--utility",
"--compute",
fmt.Sprintf("--pid=%d", os.Getpid()),
spec.Root.Path,
}
log.Debugf("Executing %q", argv)
var stdout, stderr strings.Builder
cmd := exec.Cmd{
Path: argv[0],
Args: argv,
Env: os.Environ(),
Stdout: &stdout,
Stderr: &stderr,
}
if err := cmd.Run(); err != nil {
return fmt.Errorf("nvidia-container-cli failed, err: %v\nstdout: %s\nstderr: %s", err, stdout.String(), stderr.String())
}
return nil
}
+9
View File
@@ -69,6 +69,15 @@ func executeHook(h specs.Hook, s specs.State) error {
return fmt.Errorf("path for hook is not absolute: %q", h.Path)
}
// Don't invoke nvidia-container-runtime-hook at prestart, which may be
// configured by e.g. Docker's --gpus flag, since
// nvidia-container-runtime-hook doesn't understand gVisor's bifurcation
// between sentry and application filesystems.
if strings.HasSuffix(h.Path, "/nvidia-container-runtime-hook") {
log.Infof("Skipping nvidia-container-runtime-hook")
return nil
}
b, err := json.Marshal(s)
if err != nil {
return err
+1
View File
@@ -11,6 +11,7 @@ go_library(
"cri.go",
"fs.go",
"namespace.go",
"nvidia.go",
"specutils.go",
],
visibility = ["//:sandbox"],
+78
View File
@@ -0,0 +1,78 @@
// 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 specutils
import (
"fmt"
"path/filepath"
"regexp"
"strconv"
"strings"
specs "github.com/opencontainers/runtime-spec/specs-go"
"gvisor.dev/gvisor/runsc/config"
)
const nvdEnvVar = "NVIDIA_VISIBLE_DEVICES"
// HaveNvidiaVisibleDevices returns true if the NVIDIA_VISIBLE_DEVICES
// environment variable for the specified container enables Nvidia GPU usage.
func HaveNvidiaVisibleDevices(spec *specs.Spec, conf *config.Config) bool {
if !conf.NVProxy || !conf.NVProxyDocker || spec.Process == nil {
return false
}
nvd, _ := EnvVar(spec.Process.Env, nvdEnvVar)
return nvd != "" && nvd != "void"
}
// NvidiaVisibleDevices returns the Nvidia GPU device minor numbers enabled by
// the NVIDIA_VISIBLE_DEVICES environment variable for the specified container.
func NvidiaVisibleDevices(spec *specs.Spec, conf *config.Config) ([]uint32, error) {
if !conf.NVProxy || !conf.NVProxyDocker || spec.Process == nil {
return nil, nil
}
nvd, _ := EnvVar(spec.Process.Env, nvdEnvVar)
if nvd == "" || nvd == "void" || nvd == "none" {
return nil, nil
}
var devMinors []uint32
if nvd == "all" {
paths, err := filepath.Glob("/dev/nvidia*")
if err != nil {
return nil, fmt.Errorf("enumerating Nvidia device files: %w", err)
}
re := regexp.MustCompile(`^/dev/nvidia(\d+)$`)
for _, path := range paths {
if ms := re.FindStringSubmatch(path); ms != nil {
index, err := strconv.ParseUint(ms[1], 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid host device file %q: %w", path, err)
}
devMinors = append(devMinors, uint32(index))
}
}
return devMinors, nil
}
// Expect nvd to be a list of indices; UUIDs aren't supported
// yet.
for _, indexStr := range strings.Split(nvd, ",") {
index, err := strconv.ParseUint(indexStr, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid %q in NVIDIA_VISIBLE_DEVICES %q: %w", indexStr, nvd, err)
}
devMinors = append(devMinors, uint32(index))
}
return devMinors, nil
}