mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
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:
@@ -67,6 +67,8 @@ type KVM struct {
|
||||
// KVM never changes mm_structs.
|
||||
platform.UseHostProcessMemoryBarrier
|
||||
|
||||
platform.DoesOwnPageTables
|
||||
|
||||
// machine is the backing VM.
|
||||
machine *machine
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,6 +11,7 @@ go_library(
|
||||
"cri.go",
|
||||
"fs.go",
|
||||
"namespace.go",
|
||||
"nvidia.go",
|
||||
"specutils.go",
|
||||
],
|
||||
visibility = ["//:sandbox"],
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user