diff --git a/pkg/sentry/platform/kvm/kvm.go b/pkg/sentry/platform/kvm/kvm.go index 57f01e92e..1d92a462f 100644 --- a/pkg/sentry/platform/kvm/kvm.go +++ b/pkg/sentry/platform/kvm/kvm.go @@ -67,6 +67,8 @@ type KVM struct { // KVM never changes mm_structs. platform.UseHostProcessMemoryBarrier + platform.DoesOwnPageTables + // machine is the backing VM. machine *machine } diff --git a/pkg/sentry/platform/platform.go b/pkg/sentry/platform/platform.go index f91cf194f..3e32d97de 100644 --- a/pkg/sentry/platform/platform.go +++ b/pkg/sentry/platform/platform.go @@ -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 { diff --git a/pkg/sentry/platform/ptrace/ptrace.go b/pkg/sentry/platform/ptrace/ptrace.go index 3c045c414..985d480be 100644 --- a/pkg/sentry/platform/ptrace/ptrace.go +++ b/pkg/sentry/platform/ptrace/ptrace.go @@ -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. diff --git a/pkg/sentry/platform/systrap/systrap.go b/pkg/sentry/platform/systrap/systrap.go index 426b1c51a..261ed4e86 100644 --- a/pkg/sentry/platform/systrap/systrap.go +++ b/pkg/sentry/platform/systrap/systrap.go @@ -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. diff --git a/runsc/boot/BUILD b/runsc/boot/BUILD index edcbce418..a86ab645d 100644 --- a/runsc/boot/BUILD +++ b/runsc/boot/BUILD @@ -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", diff --git a/runsc/boot/filter/BUILD b/runsc/boot/filter/BUILD index 0f9aedc3f..b4fa0dd48 100644 --- a/runsc/boot/filter/BUILD +++ b/runsc/boot/filter/BUILD @@ -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", diff --git a/runsc/boot/filter/filter.go b/runsc/boot/filter/filter.go index b3a93fc38..dec0f8644 100644 --- a/runsc/boot/filter/filter.go +++ b/runsc/boot/filter/filter.go @@ -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()) diff --git a/runsc/boot/loader.go b/runsc/boot/loader.go index 1d974d1e4..e96176145 100644 --- a/runsc/boot/loader.go +++ b/runsc/boot/loader.go @@ -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 } diff --git a/runsc/boot/loader_test.go b/runsc/boot/loader_test.go index 81753ff6f..1fa050757 100644 --- a/runsc/boot/loader_test.go +++ b/runsc/boot/loader_test.go @@ -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) } diff --git a/runsc/boot/vfs.go b/runsc/boot/vfs.go index 19f598d84..d7ac1241e 100644 --- a/runsc/boot/vfs.go +++ b/runsc/boot/vfs.go @@ -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 +} diff --git a/runsc/cmd/boot.go b/runsc/cmd/boot.go index 7e0493878..6bf802dad 100644 --- a/runsc/cmd/boot.go +++ b/runsc/cmd/boot.go @@ -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) } diff --git a/runsc/cmd/chroot.go b/runsc/cmd/chroot.go index 1fe9c6435..fc452881d 100644 --- a/runsc/cmd/chroot.go +++ b/runsc/cmd/chroot.go @@ -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 +} diff --git a/runsc/config/config.go b/runsc/config/config.go index 2a4f15136..4f1ac4c9c 100644 --- a/runsc/config/config.go +++ b/runsc/config/config.go @@ -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 diff --git a/runsc/config/flags.go b/runsc/config/flags.go index 5366600b7..3e231e4aa 100644 --- a/runsc/config/flags.go +++ b/runsc/config/flags.go @@ -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.") diff --git a/runsc/container/container.go b/runsc/container/container.go index ea6d24a80..a2d6c91a3 100644 --- a/runsc/container/container.go +++ b/runsc/container/container.go @@ -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 +} diff --git a/runsc/container/hook.go b/runsc/container/hook.go index ce1c9e1de..bdea55e37 100644 --- a/runsc/container/hook.go +++ b/runsc/container/hook.go @@ -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 diff --git a/runsc/specutils/BUILD b/runsc/specutils/BUILD index 342581bf2..4d00e2d65 100644 --- a/runsc/specutils/BUILD +++ b/runsc/specutils/BUILD @@ -11,6 +11,7 @@ go_library( "cri.go", "fs.go", "namespace.go", + "nvidia.go", "specutils.go", ], visibility = ["//:sandbox"], diff --git a/runsc/specutils/nvidia.go b/runsc/specutils/nvidia.go new file mode 100644 index 000000000..7ffb5afb8 --- /dev/null +++ b/runsc/specutils/nvidia.go @@ -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 +}