diff --git a/pkg/sentry/devices/accel/BUILD b/pkg/sentry/devices/accel/BUILD index 89c006d06..13a983d96 100644 --- a/pkg/sentry/devices/accel/BUILD +++ b/pkg/sentry/devices/accel/BUILD @@ -6,6 +6,7 @@ go_library( name = "accel", srcs = [ "accel.go", + "device.go", "seccomp_filters.go", ], visibility = ["//pkg/sentry:internal"], @@ -16,6 +17,7 @@ go_library( "//pkg/errors/linuxerr", "//pkg/seccomp", "//pkg/sentry/arch", + "//pkg/sentry/fsimpl/devtmpfs", "//pkg/sentry/vfs", "//pkg/usermem", "//pkg/waiter", diff --git a/pkg/sentry/devices/accel/accel.go b/pkg/sentry/devices/accel/accel.go index fef24a23f..97868dc63 100644 --- a/pkg/sentry/devices/accel/accel.go +++ b/pkg/sentry/devices/accel/accel.go @@ -33,6 +33,8 @@ type accelFD struct { vfs.FileDescriptionDefaultImpl vfs.DentryMetadataFileDescriptionImpl vfs.NoLockFD + + hostFD int32 } // Release implements vfs.FileDescriptionImpl.Release. diff --git a/pkg/sentry/devices/accel/device.go b/pkg/sentry/devices/accel/device.go new file mode 100644 index 000000000..56fbe44de --- /dev/null +++ b/pkg/sentry/devices/accel/device.go @@ -0,0 +1,65 @@ +// 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 accel + +import ( + "fmt" + + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/context" + "gvisor.dev/gvisor/pkg/sentry/fsimpl/devtmpfs" + "gvisor.dev/gvisor/pkg/sentry/vfs" +) + +// accelDevice implements vfs.Device for /dev/accel[0-9]+. +// +// +stateify savable +type accelDevice struct { + minor uint32 +} + +func (dev *accelDevice) Open(ctx context.Context, mnt *vfs.Mount, vfsd *vfs.Dentry, opts vfs.OpenOptions) (*vfs.FileDescription, error) { + hostPath := fmt.Sprintf("/dev/accel%d", dev.minor) + hostFD, err := unix.Openat(-1, hostPath, int((opts.Flags&unix.O_ACCMODE)|unix.O_NOFOLLOW), 0) + if err != nil { + ctx.Warningf("accelDevice: failed to open host %s: %v", hostPath, err) + return nil, err + } + fd := &accelFD{ + hostFD: int32(hostFD), + } + if err := fd.vfsfd.Init(fd, opts.Flags, mnt, vfsd, &vfs.FileDescriptionOptions{ + UseDentryMetadata: true, + }); err != nil { + unix.Close(hostFD) + return nil, err + } + return &fd.vfsfd, nil +} + +// CreateDevtmpfsFile creates a /dev/accel[0-9]+ device file. +func CreateDevtmpfsFile(ctx context.Context, dev *devtmpfs.Accessor, num uint32) error { + return dev.CreateDeviceFile(ctx, fmt.Sprintf("accel%d", num), vfs.CharDevice, linux.ACCEL_MAJOR, num, 0666) +} + +// Register registers all devices implemented by this package in vfsObj. +func Register(vfsObj *vfs.VirtualFilesystem, minor uint32) error { + return vfsObj.RegisterDevice(vfs.CharDevice, linux.ACCEL_MAJOR, minor, &accelDevice{ + minor: minor, + }, &vfs.RegisterDeviceOptions{ + GroupName: "accel", + }) +} diff --git a/pkg/sentry/fsimpl/sys/BUILD b/pkg/sentry/fsimpl/sys/BUILD index 2419fe325..fa57122ec 100644 --- a/pkg/sentry/fsimpl/sys/BUILD +++ b/pkg/sentry/fsimpl/sys/BUILD @@ -22,6 +22,7 @@ go_library( "dir_refs.go", "kcov.go", "net.go", + "pci.go", "sys.go", ], visibility = ["//pkg/sentry:internal"], @@ -31,9 +32,11 @@ go_library( "//pkg/context", "//pkg/coverage", "//pkg/errors/linuxerr", + "//pkg/fsutil", "//pkg/log", "//pkg/refs", "//pkg/sentry/arch", + "//pkg/sentry/fsimpl/host", "//pkg/sentry/fsimpl/kernfs", "//pkg/sentry/inet", "//pkg/sentry/kernel", @@ -41,6 +44,7 @@ go_library( "//pkg/sentry/memmap", "//pkg/sentry/vfs", "//pkg/usermem", + "@org_golang_x_sys//unix:go_default_library", ], ) diff --git a/pkg/sentry/fsimpl/sys/pci.go b/pkg/sentry/fsimpl/sys/pci.go new file mode 100644 index 000000000..8a49b0169 --- /dev/null +++ b/pkg/sentry/fsimpl/sys/pci.go @@ -0,0 +1,164 @@ +// 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 sys + +import ( + "fmt" + "path" + regex "regexp" + + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/context" + "gvisor.dev/gvisor/pkg/fsutil" + "gvisor.dev/gvisor/pkg/sentry/fsimpl/kernfs" + "gvisor.dev/gvisor/pkg/sentry/kernel/auth" +) + +const ( + pciMainBusDevicePath = "/sys/devices/pci0000:00" + // Size of the buffer that host file content will be read into. All relevant + // host files are smaller than this. + hostFileBufSize = 0x1000 +) + +var ( + // Matches PCI device addresses in the main domain. + pciDeviceRegex = regex.MustCompile(`0000:([a-fA-F0-9]{2}|[a-fA-F0-9]{4}):[a-fA-F0-9]{2}\.[a-fA-F0-9]{1,2}`) + // Matches the directories for the main bus (i.e. pci000:00), accel, and + // individual devices (e.g. 00:00:04.0) + sysDevicesDirRegex = regex.MustCompile(`pci0000:00|accel|(0000:([a-fA-F0-9]{2}|[a-fA-F0-9]{4}):[a-fA-F0-9]{2}\.[a-fA-F0-9]{1,2})`) + // Files allowlisted for host passthrough. These files are read-only. + sysDevicesFiles = map[string]any{ + "vendor": nil, "device": nil, "subsystem_vendor": nil, "subsystem_device": nil, + "revision": nil, "class": nil, "numa_node": nil, "iommu_group": nil, + "resource": nil, "pci_address": nil, "dev": nil, "driver_version": nil, + "reset_count": nil, "write_open_count": nil, "status": nil, + "is_device_owned": nil, "device_owner": nil, "framework_version": nil, + "user_mem_ranges": nil, "interrupt_counts": nil, "chip_model": nil, + "bar_offsets": nil, "bar_sizes": nil, "resource0": nil, "resource1": nil, + "resource2": nil, "resource3": nil, "resource4": nil, "resource5": nil, + } +) + +// Create /sys/class/accel/accel# symlinks. +func (fs *filesystem) newAccelDir(ctx context.Context, creds *auth.Credentials) (map[string]kernfs.Inode, error) { + accelDirs := map[string]kernfs.Inode{} + pciDents, err := hostDirEntries(pciMainBusDevicePath) + if err != nil { + return nil, err + } + for _, pciDent := range pciDents { + accelDents, err := hostDirEntries(path.Join(pciMainBusDevicePath, pciDent, "accel")) + if err != nil { + return nil, err + } + if len(accelDents) != 1 { + return nil, fmt.Errorf("path %q should only have one entry", path.Join(pciMainBusDevicePath, pciDent, "accel")) + } + accelDirs[accelDents[0]] = kernfs.NewStaticSymlink(ctx, creds, linux.UNNAMED_MAJOR, fs.devMinor, fs.NextIno(), fmt.Sprintf("../../devices/pci0000:00/%s/accel/%s", pciDent, accelDents[0])) + } + + return accelDirs, nil +} + +// Create /sys/bus/pci/devices symlinks. +func (fs *filesystem) newPCIDevicesDir(ctx context.Context, creds *auth.Credentials) (map[string]kernfs.Inode, error) { + pciDevicesDir := map[string]kernfs.Inode{} + pciDents, err := hostDirEntries(pciMainBusDevicePath) + if err != nil { + return nil, err + } + for _, pciDent := range pciDents { + pciDevicesDir[pciDent] = kernfs.NewStaticSymlink(ctx, creds, linux.UNNAMED_MAJOR, fs.devMinor, fs.NextIno(), fmt.Sprintf("../../../devices/pci0000:00/%s", pciDent)) + } + + return pciDevicesDir, nil +} + +// Recursively build out sysfs directories according to the allowlisted files, +// directories, and symlinks defined in this package. +func (fs *filesystem) mirrorPCIBusDeviceDir(ctx context.Context, creds *auth.Credentials, dir string) (map[string]kernfs.Inode, error) { + subs := map[string]kernfs.Inode{} + dents, err := hostDirEntries(dir) + if err != nil { + return nil, err + } + for _, dent := range dents { + dentPath := path.Join(dir, dent) + dentMode, err := hostFileMode(dentPath) + if err != nil { + return nil, err + } + switch dentMode { + case unix.S_IFDIR: + if match := sysDevicesDirRegex.MatchString(dent); !match { + continue + } + contents, err := fs.mirrorPCIBusDeviceDir(ctx, creds, dentPath) + if err != nil { + return nil, err + } + subs[dent] = fs.newDir(ctx, creds, defaultSysMode, contents) + case unix.S_IFREG: + if _, ok := sysDevicesFiles[dent]; ok { + subs[dent] = fs.newHostFile(ctx, creds, defaultSysMode, dentPath) + } + case unix.S_IFLNK: + // Both the device and PCI address entries are links to the original PCI + // device directory that's at the same place earlier in the dir tree. + if match := pciDeviceRegex.MatchString(dent); !(match || dent == "device") { + continue + } + pciDeviceName := pciDeviceRegex.FindString(dir) + if pciDeviceName == "" { + return nil, fmt.Errorf("could not populate sysfs pci symlink %s", dir) + } + linkContent := fmt.Sprintf("../../../%s", pciDeviceName) + subs[dent] = kernfs.NewStaticSymlink(ctx, creds, linux.UNNAMED_MAJOR, fs.devMinor, fs.NextIno(), linkContent) + } + } + return subs, nil +} + +func hostFileMode(path string) (uint32, error) { + fd, err := unix.Openat(-1, path, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_PATH, 0) + if err != nil { + return 0, err + } + stat := unix.Stat_t{} + if err := unix.Fstat(fd, &stat); err != nil { + return 0, err + } + return stat.Mode & unix.S_IFMT, nil +} + +func hostDirEntries(path string) ([]string, error) { + fd, err := unix.Openat(-1, path, unix.O_RDONLY|unix.O_NOFOLLOW, 0) + if err != nil { + return nil, err + } + var buf [hostFileBufSize]byte + n, err := unix.Getdents(fd, buf[:]) + if err != nil { + return nil, err + } + var dents []string + fsutil.ParseDirents(buf[:n], func(_ uint64, _ int64, _ uint8, name string, _ uint16) bool { + dents = append(dents, name) + return true + }) + return dents, nil +} diff --git a/pkg/sentry/fsimpl/sys/sys.go b/pkg/sentry/fsimpl/sys/sys.go index 6bebcf736..d9e24e157 100644 --- a/pkg/sentry/fsimpl/sys/sys.go +++ b/pkg/sentry/fsimpl/sys/sys.go @@ -20,6 +20,7 @@ import ( "fmt" "strconv" + "golang.org/x/sys/unix" "gvisor.dev/gvisor/pkg/abi/linux" "gvisor.dev/gvisor/pkg/context" "gvisor.dev/gvisor/pkg/coverage" @@ -51,6 +52,9 @@ type FilesystemType struct{} type InternalData struct { // ProductName is the value to be set to devices/virtual/dmi/id/product_name. ProductName string + // EnableAccelSysfs is whether to populate sysfs paths used by hardware + // accelerators. + EnableAccelSysfs bool } // filesystem implements vfs.FilesystemImpl. @@ -117,10 +121,35 @@ func (fsType FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.Virt } productName := "" + var busSub map[string]kernfs.Inode if opts.InternalData != nil { - data := opts.InternalData.(*InternalData) - productName = data.ProductName + idata := opts.InternalData.(*InternalData) + productName = idata.ProductName + if idata.EnableAccelSysfs { + pciMainBusSub, err := fs.mirrorPCIBusDeviceDir(ctx, creds, pciMainBusDevicePath) + if err != nil { + return nil, nil, err + } + devicesSub["pci0000:00"] = fs.newDir(ctx, creds, defaultSysDirMode, pciMainBusSub) + + accelSub, err := fs.newAccelDir(ctx, creds) + if err != nil { + return nil, nil, err + } + classSub["accel"] = fs.newDir(ctx, creds, defaultSysDirMode, accelSub) + + pciDevicesSub, err := fs.newPCIDevicesDir(ctx, creds) + if err != nil { + return nil, nil, err + } + busSub = map[string]kernfs.Inode{ + "pci": fs.newDir(ctx, creds, defaultSysDirMode, map[string]kernfs.Inode{ + "devices": fs.newDir(ctx, creds, defaultSysDirMode, pciDevicesSub), + }), + } + } } + if len(productName) > 0 { log.Debugf("Setting product_name: %q", productName) classSub["dmi"] = fs.newDir(ctx, creds, defaultSysDirMode, map[string]kernfs.Inode{ @@ -136,7 +165,7 @@ func (fsType FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.Virt } root := fs.newDir(ctx, creds, defaultSysDirMode, map[string]kernfs.Inode{ "block": fs.newDir(ctx, creds, defaultSysDirMode, nil), - "bus": fs.newDir(ctx, creds, defaultSysDirMode, nil), + "bus": fs.newDir(ctx, creds, defaultSysDirMode, busSub), "class": fs.newDir(ctx, creds, defaultSysDirMode, classSub), "dev": fs.newDir(ctx, creds, defaultSysDirMode, nil), "devices": fs.newDir(ctx, creds, defaultSysDirMode, devicesSub), @@ -287,3 +316,33 @@ func (fs *filesystem) newStaticFile(ctx context.Context, creds *auth.Credentials s.Init(ctx, creds, linux.UNNAMED_MAJOR, fs.devMinor, fs.NextIno(), s, mode) return s } + +// hostFile is an inode whose contents are generated by reading from the +// host. +// +// +stateify savable +type hostFile struct { + kernfs.DynamicBytesFile + hostPath string +} + +func (hf *hostFile) Generate(ctx context.Context, buf *bytes.Buffer) error { + fd, err := unix.Openat(-1, hf.hostPath, unix.O_RDONLY|unix.O_NOFOLLOW, 0) + if err != nil { + return err + } + var data [hostFileBufSize]byte + n, err := unix.Read(fd, data[:]) + if err != nil { + return err + } + unix.Close(fd) + buf.Write(data[:n]) + return nil +} + +func (fs *filesystem) newHostFile(ctx context.Context, creds *auth.Credentials, mode linux.FileMode, hostPath string) kernfs.Inode { + hf := &hostFile{hostPath: hostPath} + hf.Init(ctx, creds, linux.UNNAMED_MAJOR, fs.devMinor, fs.NextIno(), hf, mode) + return hf +} diff --git a/runsc/boot/BUILD b/runsc/boot/BUILD index a86ab645d..647be0726 100644 --- a/runsc/boot/BUILD +++ b/runsc/boot/BUILD @@ -50,6 +50,7 @@ go_library( "//pkg/sentry/arch", "//pkg/sentry/arch:registers_go_proto", "//pkg/sentry/control", + "//pkg/sentry/devices/accel", "//pkg/sentry/devices/memdev", "//pkg/sentry/devices/nvproxy", "//pkg/sentry/devices/ttydev", diff --git a/runsc/boot/vfs.go b/runsc/boot/vfs.go index ca325c39f..59824363a 100644 --- a/runsc/boot/vfs.go +++ b/runsc/boot/vfs.go @@ -18,6 +18,7 @@ import ( "fmt" "path" "path/filepath" + "regexp" "sort" "strconv" "strings" @@ -30,6 +31,7 @@ import ( "gvisor.dev/gvisor/pkg/fd" "gvisor.dev/gvisor/pkg/fspath" "gvisor.dev/gvisor/pkg/log" + "gvisor.dev/gvisor/pkg/sentry/devices/accel" "gvisor.dev/gvisor/pkg/sentry/devices/memdev" "gvisor.dev/gvisor/pkg/sentry/devices/nvproxy" "gvisor.dev/gvisor/pkg/sentry/devices/ttydev" @@ -173,6 +175,10 @@ func registerFilesystems(k *kernel.Kernel, info *containerInfo) error { return err } + if err := tpuProxyRegisterDevicesAndCreateFiles(ctx, info, k, vfsObj, a); err != nil { + return err + } + return nil } @@ -784,9 +790,11 @@ func (c *containerMounter) getMountNameAndOptions(conf *config.Config, m *mountI fsName = sys.Name case sys.Name: + sysData := &sys.InternalData{EnableAccelSysfs: conf.TPUProxy} if len(c.productName) > 0 { - internalData = &sys.InternalData{ProductName: c.productName} + sysData.ProductName = c.productName } + internalData = sysData case tmpfs.Name: var err error @@ -1113,6 +1121,31 @@ func createDeviceFiles(ctx context.Context, creds *auth.Credentials, info *conta return nil } +func tpuProxyRegisterDevicesAndCreateFiles(ctx context.Context, info *containerInfo, k *kernel.Kernel, vfsObj *vfs.VirtualFilesystem, a *devtmpfs.Accessor) error { + if !info.conf.TPUProxy { + return nil + } + // At this point /dev/accel just contains the TPU devices have been mounted + // into the sandbox chroot. Enumerate all of them and create sentry devices. + paths, err := filepath.Glob("/dev/accel*") + if err != nil { + return fmt.Errorf("enumerating accel device files: %w", err) + } + for _, path := range paths { + accelDeviceRegex := regexp.MustCompile(`^/dev/accel(\d+)$`) + if ms := accelDeviceRegex.FindStringSubmatch(path); ms != nil { + deviceNum, _ := strconv.ParseUint(ms[1], 10, 32) + if err := accel.Register(vfsObj, uint32(deviceNum)); err != nil { + return fmt.Errorf("registering accel driver: %w", err) + } + if err := accel.CreateDevtmpfsFile(ctx, a, uint32(deviceNum)); err != nil { + return fmt.Errorf("creating accel device file %q: %w", deviceNum, err) + } + } + } + return nil +} + func nvproxyRegisterDevicesAndCreateFiles(ctx context.Context, info *containerInfo, k *kernel.Kernel, vfsObj *vfs.VirtualFilesystem, a *devtmpfs.Accessor) error { if !specutils.GPUFunctionalityRequested(info.spec, info.conf) { return nil