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
This commit is contained in:
Lucas Manning
2023-07-21 11:43:55 -07:00
committed by gVisor bot
parent f3e4a1fc3b
commit 19e04218b9
8 changed files with 334 additions and 4 deletions
+2
View File
@@ -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",
+2
View File
@@ -33,6 +33,8 @@ type accelFD struct {
vfs.FileDescriptionDefaultImpl
vfs.DentryMetadataFileDescriptionImpl
vfs.NoLockFD
hostFD int32
}
// Release implements vfs.FileDescriptionImpl.Release.
+65
View File
@@ -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",
})
}
+4
View File
@@ -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",
],
)
+164
View File
@@ -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
}
+62 -3
View File
@@ -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
}
+1
View File
@@ -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",
+34 -1
View File
@@ -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