From 19e04218b98166664bdac4d3a9e5f93cb65e67a3 Mon Sep 17 00:00:00 2001 From: Lucas Manning Date: Fri, 21 Jul 2023 11:40:42 -0700 Subject: [PATCH] Add methods for generating PCI sysfs paths and registering accel devices. The TPU userspace driver needs access to specific PCI device information located in Linux sysfs. We mirror the sysfs paths the driver reads on the host in the Sentry sysfs. This way we can ensure we only expose the host device information that's strictly necessary for TPU to run. PiperOrigin-RevId: 550005271 --- pkg/sentry/devices/accel/BUILD | 2 + pkg/sentry/devices/accel/accel.go | 2 + pkg/sentry/devices/accel/device.go | 65 ++++++++++++ pkg/sentry/fsimpl/sys/BUILD | 4 + pkg/sentry/fsimpl/sys/pci.go | 164 +++++++++++++++++++++++++++++ pkg/sentry/fsimpl/sys/sys.go | 65 +++++++++++- runsc/boot/BUILD | 1 + runsc/boot/vfs.go | 35 +++++- 8 files changed, 334 insertions(+), 4 deletions(-) create mode 100644 pkg/sentry/devices/accel/device.go create mode 100644 pkg/sentry/fsimpl/sys/pci.go 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