diff --git a/g3doc/user_guide/rdma.md b/g3doc/user_guide/rdma.md index e42abaff7ad..0f41db7860f 100644 --- a/g3doc/user_guide/rdma.md +++ b/g3doc/user_guide/rdma.md @@ -27,8 +27,9 @@ RDMA, allowing the NIC to transfer data directly to and from GPU memory. RDMA support is under active development. The following limitations apply: -* **Mellanox NICs only.** Only Mellanox ConnectX (`mlx5`) adapters are - currently supported. Support for additional vendors is planned. +* **Mellanox and AWS EFA NICs only.** Only Mellanox ConnectX (`mlx5`) + adapters and AWS Elastic Fabric Adapters (`efa`) are currently supported. + Support for additional vendors is planned. * **Host kernel 5.12 or newer.** `rdmaproxy` proxies the modern `RDMA_VERBS_IOCTL` interface only; the legacy `write(2)` command interface @@ -39,6 +40,11 @@ RDMA support is under active development. The following limitations apply: through the dma-buf mechanism, which is the modern default. The legacy `nvidia-peermem` kernel-module path is not supported. +* **EFA needs a dma-buf-capable NCCL plugin.** Because GPUDirect works only + through dma-buf (above), AWS EFA requires `aws-ofi-nccl` v1.19.2 or newer: + earlier releases hard-disable dma-buf on EFA device generations 1-3 and + register GPU memory by virtual address instead, which is not supported. + * **Single-container sandboxes only.** The RDMA devices must be declared in the OCI spec of the sandbox's root container. Deployments where the devices appear only in a sub-container's spec — such as a Kubernetes pod where the diff --git a/pkg/abi/ib/ib.go b/pkg/abi/ib/ib.go index 7edf249b40a..3e3057bf7eb 100644 --- a/pkg/abi/ib/ib.go +++ b/pkg/abi/ib/ib.go @@ -81,6 +81,7 @@ const ( UVERBS_OBJECT_PD = 1 UVERBS_OBJECT_CQ = 3 UVERBS_OBJECT_QP = 4 + UVERBS_OBJECT_AH = 6 UVERBS_OBJECT_MR = 7 UVERBS_OBJECT_ASYNC_EVENT = 16 ) @@ -106,6 +107,11 @@ const ( UVERBS_METHOD_QP_CREATE = 0 UVERBS_METHOD_QP_DESTROY = 1 + // enum uverbs_methods_ah. AH creation uses the legacy write path + // (IB_USER_VERBS_CMD_CREATE_AH via INVOKE_WRITE); only destroy has a + // modern object method. + UVERBS_METHOD_AH_DESTROY = 0 + // enum uverbs_methods_mr. UVERBS_METHOD_MR_DESTROY = 1 UVERBS_METHOD_REG_DMABUF_MR = 4 @@ -160,6 +166,9 @@ const ( // UVERBS_ATTR_DESTROY_PD_HANDLE is the handle attribute ID for uverbs_attrs_destroy_pd_cmd_attr_ids. const UVERBS_ATTR_DESTROY_PD_HANDLE = 0 +// enum uverbs_attrs_ah_destroy_ids. +const UVERBS_ATTR_DESTROY_AH_HANDLE = 0 + // enum uverbs_attrs_reg_mr_cmd_attr_ids. const ( UVERBS_ATTR_REG_MR_HANDLE = 0 @@ -274,6 +283,21 @@ const ( MLX5_IB_ATTR_UAR_OBJ_DESTROY_HANDLE = 0x1000 ) +// EFA driver-namespace method/attr IDs from include/uapi/rdma/efa-abi.h. EFA +// extends the standard UVERBS_OBJECT_MR with a query method returning the +// interconnect IDs an RDMA-read/write source MR must advertise to peers. +const ( + // enum efa_mr_methods. + EFA_IB_METHOD_MR_QUERY = 0x1000 + + // enum efa_query_mr_attrs. + EFA_IB_ATTR_QUERY_MR_HANDLE = 0x1000 + EFA_IB_ATTR_QUERY_MR_RESP_IC_ID_VALIDITY = 0x1001 + EFA_IB_ATTR_QUERY_MR_RESP_RECV_IC_ID = 0x1002 + EFA_IB_ATTR_QUERY_MR_RESP_RDMA_READ_IC_ID = 0x1003 + EFA_IB_ATTR_QUERY_MR_RESP_RDMA_RECV_IC_ID = 0x1004 +) + // Legacy write(2)-path command numbers (enum ib_uverbs_write_cmds, // include/uapi/rdma/ib_user_verbs.h), as carried by the INVOKE_WRITE // WRITE_CMD attribute. diff --git a/pkg/rdma/collect.go b/pkg/rdma/collect.go index 997324e41e1..ec0c3aabb7d 100644 --- a/pkg/rdma/collect.go +++ b/pkg/rdma/collect.go @@ -55,7 +55,7 @@ var ibAttrNames = []string{ // table repopulates when netdevs move namespaces and acquire addresses; // link state and rate can change on retrain). var portLiveAttrNames = []string{ - "state", "phys_state", "rate", "lid", "sm_lid", "sm_sl", + "state", "phys_state", "rate", "lid", "lid_mask_count", "sm_lid", "sm_sl", } // Per-port attributes that are fixed for the sandbox lifetime. @@ -182,13 +182,18 @@ func Collect(sysRoot string, uverbs []UverbsSpec) (*Snapshot, error) { } } - // Materialize every PCI node with its static attributes. + // Materialize every PCI node with its static attributes and config space. for p := range pciPaths { attrs, err := readAttrs(path.Join(sysRoot, p), pciAttrNames) if err != nil { return nil, fmt.Errorf("PCI node %q: %w", p, err) } - s.PCINodes = append(s.PCINodes, PCINode{Path: p, Attrs: attrs}) + // config is best-effort: root complexes and some bridges lack it. + config, err := os.ReadFile(path.Join(sysRoot, p, "config")) + if err != nil && !errors.Is(err, fs.ErrNotExist) { + return nil, fmt.Errorf("reading PCI config of %q: %w", p, err) + } + s.PCINodes = append(s.PCINodes, PCINode{Path: p, Attrs: attrs, Config: config}) } sort.Slice(s.PCINodes, func(i, j int) bool { return s.PCINodes[i].Path < s.PCINodes[j].Path }) diff --git a/pkg/rdma/snapshot.go b/pkg/rdma/snapshot.go index 193304a9d01..79b62c4a0eb 100644 --- a/pkg/rdma/snapshot.go +++ b/pkg/rdma/snapshot.go @@ -40,6 +40,11 @@ type PCINode struct { // Attrs maps attribute file name to contents (verbatim, including any // trailing newline). Attrs map[string]string `json:"attrs"` + // Config is the raw PCI config space ("config" file), or nil if absent. + // hwloc (used by aws-ofi-nccl for NCCL topology) reads it to recover the + // PCI-bridge bus-number registers and PCIe link attributes; without it + // hwloc cannot reconstruct the bridge hierarchy. + Config []byte `json:"config,omitempty"` } // Port is the per-IB-port state. Attributes split into static (immutable diff --git a/pkg/sentry/devices/rdmaproxy/efaproxy/BUILD b/pkg/sentry/devices/rdmaproxy/efaproxy/BUILD new file mode 100644 index 00000000000..87cd4fd0827 --- /dev/null +++ b/pkg/sentry/devices/rdmaproxy/efaproxy/BUILD @@ -0,0 +1,22 @@ +load("//tools:defs.bzl", "go_library") + +package( + default_applicable_licenses = ["//:license"], + licenses = ["notice"], +) + +go_library( + name = "efaproxy", + srcs = [ + "efaproxy.go", + ], + visibility = [ + "//pkg/sentry:internal", + "//runsc:__subpackages__", + ], + deps = [ + "//pkg/abi/ib", + "//pkg/sentry/devices/rdmaproxy", + "//pkg/sentry/kernel", + ], +) diff --git a/pkg/sentry/devices/rdmaproxy/efaproxy/efaproxy.go b/pkg/sentry/devices/rdmaproxy/efaproxy/efaproxy.go new file mode 100644 index 00000000000..3babf5d2251 --- /dev/null +++ b/pkg/sentry/devices/rdmaproxy/efaproxy/efaproxy.go @@ -0,0 +1,71 @@ +// Copyright 2026 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 efaproxy implements the rdmaproxy.Driver plug-in for AWS Elastic +// Fabric Adapter (EFA) devices bound to the host `efa` kernel driver. +// +// To make this driver available, Init() must be called. +package efaproxy + +import ( + "gvisor.dev/gvisor/pkg/abi/ib" + "gvisor.dev/gvisor/pkg/sentry/devices/rdmaproxy" + "gvisor.dev/gvisor/pkg/sentry/kernel" +) + +// driverName matches the DRIVER= field of +// /sys/class/infiniband//device/uevent for EFA adapters. runsc looks +// this up via rdmaproxy.LookupDriver and attaches the resulting driver to the +// corresponding uverbs device. +const driverName = "efa" + +// efaDriver is the rdmaproxy.Driver implementation for EFA adapters. +type efaDriver struct{} + +// Name implements rdmaproxy.Driver.Name. +func (efaDriver) Name() string { return driverName } + +// PrepareCreateDMA implements rdmaproxy.Driver.PrepareCreateDMA. EFA CQ/QP +// CREATE command structs (efa_ibv_create_cq / efa_ibv_create_qp) carry no +// userspace buffer pointers. The host kernel allocates the work-queue and +// doorbell memory and hands it back through mmap keys in the CREATE *response* +// (q_mmap_key, rq_mmap_key, sq_db_mmap_key, ...); userspace then mmap()s the +// uverbs FD at those offsets, which the rdmaproxy core already forwards to the +// host FD verbatim. There is thus no app memory to mirror or rewrite at CREATE +// time, so PrepareCreateDMA is a no-op. +func (efaDriver) PrepareCreateDMA(t *kernel.Task, uhwIn []byte) (*rdmaproxy.PinnedDMABufs, error) { + return nil, nil +} + +// Schemas implements rdmaproxy.Driver.Schemas. EFA extends the standard MR +// object with a query method returning the interconnect IDs a source MR must +// advertise for RDMA read/write; libfabric's EFA provider issues it during +// endpoint setup. All attributes are handles or fixed scalars, so no address +// translation is needed. +func (efaDriver) Schemas() map[uint32]*rdmaproxy.MethodSchema { + return map[uint32]*rdmaproxy.MethodSchema{ + rdmaproxy.SchemaKey(ib.UVERBS_OBJECT_MR, ib.EFA_IB_METHOD_MR_QUERY): { + Attrs: map[uint16]rdmaproxy.AttrType{ + ib.EFA_IB_ATTR_QUERY_MR_HANDLE: rdmaproxy.AttrIdr, + ib.EFA_IB_ATTR_QUERY_MR_RESP_IC_ID_VALIDITY: rdmaproxy.AttrPtrOut, + ib.EFA_IB_ATTR_QUERY_MR_RESP_RECV_IC_ID: rdmaproxy.AttrPtrOut, + ib.EFA_IB_ATTR_QUERY_MR_RESP_RDMA_READ_IC_ID: rdmaproxy.AttrPtrOut, + ib.EFA_IB_ATTR_QUERY_MR_RESP_RDMA_RECV_IC_ID: rdmaproxy.AttrPtrOut, + }, + }, + } +} + +// Init registers the EFA driver plug-in with the rdmaproxy core. +func Init() { rdmaproxy.RegisterDriver(efaDriver{}) } diff --git a/pkg/sentry/devices/rdmaproxy/schema.go b/pkg/sentry/devices/rdmaproxy/schema.go index 37c795a1d29..b1e1a36ae84 100644 --- a/pkg/sentry/devices/rdmaproxy/schema.go +++ b/pkg/sentry/devices/rdmaproxy/schema.go @@ -200,6 +200,13 @@ func buildSchemas() map[uint32]*MethodSchema { ib.UVERBS_ATTR_DESTROY_PD_HANDLE: AttrIdr, }, }, + // AH creation rides the legacy write path (DmaInvokeWrite); only + // destroy has a modern object method. + SchemaKey(ib.UVERBS_OBJECT_AH, ib.UVERBS_METHOD_AH_DESTROY): { + Attrs: map[uint16]AttrType{ + ib.UVERBS_ATTR_DESTROY_AH_HANDLE: AttrIdr, + }, + }, SchemaKey(ib.UVERBS_OBJECT_MR, ib.UVERBS_METHOD_REG_MR): { Dma: DmaMRReg, HandleAttr: ib.UVERBS_ATTR_REG_MR_HANDLE, Attrs: map[uint16]AttrType{ diff --git a/pkg/sentry/fsimpl/proc/tasks_files.go b/pkg/sentry/fsimpl/proc/tasks_files.go index 6fe79fcfcac..08528155ec2 100644 --- a/pkg/sentry/fsimpl/proc/tasks_files.go +++ b/pkg/sentry/fsimpl/proc/tasks_files.go @@ -27,6 +27,7 @@ import ( "gvisor.dev/gvisor/pkg/sentry/fsimpl/kernfs" "gvisor.dev/gvisor/pkg/sentry/kernel" "gvisor.dev/gvisor/pkg/sentry/kernel/auth" + "gvisor.dev/gvisor/pkg/sentry/kernel/version" "gvisor.dev/gvisor/pkg/sentry/ktime" "gvisor.dev/gvisor/pkg/sentry/usage" "gvisor.dev/gvisor/pkg/sentry/vfs" @@ -394,8 +395,7 @@ func (*versionData) Generate(ctx context.Context, buf *bytes.Buffer) error { // FIXME(mpratt): Using Version from the init task SyscallTable // disregards the different version a task may have (e.g., in a uts // namespace). - ver := kernelVersion(ctx) - fmt.Fprintf(buf, "%s version %s %s\n", ver.Sysname, ver.Release, ver.Version) + fmt.Fprintf(buf, "%s version %s %s\n", version.LinuxSysname, version.LinuxRelease(), version.LinuxVersion) return nil } @@ -442,23 +442,10 @@ var _ dynamicInode = (*cmdLineData)(nil) // Generate implements vfs.DynamicByteSource.Generate. func (*cmdLineData) Generate(ctx context.Context, buf *bytes.Buffer) error { - fmt.Fprintf(buf, "BOOT_IMAGE=/vmlinuz-%s-gvisor quiet\n", kernelVersion(ctx).Release) + fmt.Fprintf(buf, "BOOT_IMAGE=/vmlinuz-%s quiet\n", version.LinuxRelease()) return nil } -// kernelVersion returns the kernel version. -func kernelVersion(ctx context.Context) kernel.Version { - k := kernel.KernelFromContext(ctx) - init := k.GlobalInit() - if init == nil { - // Attempted to read before the init Task is created. This can - // only occur during startup, which should never need to read - // this file. - panic("Attempted to read version before initial Task is available") - } - return init.Leader().SyscallTable().Version -} - // devicesData backs /proc/devices. // // +stateify savable diff --git a/pkg/sentry/fsimpl/proc/tasks_sys.go b/pkg/sentry/fsimpl/proc/tasks_sys.go index 91f500f28f0..0f6b97b8cae 100644 --- a/pkg/sentry/fsimpl/proc/tasks_sys.go +++ b/pkg/sentry/fsimpl/proc/tasks_sys.go @@ -74,7 +74,7 @@ func (fs *filesystem) newSysDir(ctx context.Context, root *auth.Credentials, k * "keys": fs.newStaticDir(ctx, root, map[string]kernfs.Inode{ "maxkeys": fs.newMaxKeySizeFile(ctx, k, root), }), - "osrelease": fs.newInode(ctx, root, 0444, newStaticFile(version.LinuxRelease)), + "osrelease": fs.newInode(ctx, root, 0444, newStaticFile(version.LinuxRelease())), "ostype": fs.newInode(ctx, root, 0444, newStaticFile(version.LinuxSysname)), "version": fs.newInode(ctx, root, 0444, newStaticFile(version.LinuxVersion)), }), diff --git a/pkg/sentry/fsimpl/sys/rdma.go b/pkg/sentry/fsimpl/sys/rdma.go index 1310a9c16fc..3c705352fe8 100644 --- a/pkg/sentry/fsimpl/sys/rdma.go +++ b/pkg/sentry/fsimpl/sys/rdma.go @@ -58,6 +58,9 @@ type rdmaSysfsDirs struct { class map[string]kernfs.Inode // busPCIDevices contains the /sys/bus/pci/devices symlinks. busPCIDevices map[string]kernfs.Inode + // busPCIDrivers maps a kernel driver name to its + // /sys/bus/pci/drivers/ directory of bound-device back-symlinks. + busPCIDrivers map[string]kernfs.Inode // node is the /sys/devices/system/node subtree, or nil. node kernfs.Inode } @@ -105,6 +108,26 @@ func (fs *filesystem) newRDMASysfs(ctx context.Context, creds *auth.Credentials, classNet := map[string]string{} // netdev -> symlink target classPCIBus := map[string]string{} // bus ("0000:0c") -> symlink target + // driverByLeaf maps a leaf PCI function path to its kernel driver name + // (from the DRIVER= line of the leaf's uevent). Used to synthesize the + // device/driver symlink and /sys/bus/pci/drivers tree that libfabric's + // EFA provider resolves during device discovery. + driverByLeaf := map[string]string{} + for i := range snap.Devices { + leaf := snap.Devices[i].LeafPCI + for _, n := range snap.PCINodes { + if n.Path != leaf { + continue + } + for _, line := range strings.Split(n.Attrs["uevent"], "\n") { + if drv, ok := strings.CutPrefix(line, "DRIVER="); ok && rdma.SafeName(drv) { + driverByLeaf[leaf] = drv + } + } + } + } + classPCIDrivers := map[string][]string{} // driver -> leaf PCI paths bound to it + // 1. The canonical PCI hierarchy with per-level static attributes, plus // the "subsystem" symlink every PCI device carries. NCCL and other // consumers classify a directory as a PCI device by following @@ -135,12 +158,21 @@ func (fs *filesystem) newRDMASysfs(ctx context.Context, creds *auth.Credentials, if _, ok := d.files["local_cpulist"]; ok { d.files["local_cpulist"] = cpuListString(cores) } + // Raw PCI config space (binary). hwloc reads it to rebuild the PCI + // bridge hierarchy; without it aws-ofi-nccl's NCCL topology write fails. + if n.Config != nil { + d.files["config"] = string(n.Config) + } // Root complexes (pciXXXX:YY) carry no subsystem link and sit on // no parent bus; only function directories (BDFs) do. if rdma.IsBDF(path.Base(n.Path)) { // depth of n.Path below /sys == number of "../" to reach /sys. depth := strings.Count(n.Path, "/") + 1 d.symlinks["subsystem"] = strings.Repeat("../", depth) + "bus/pci" + if drv, ok := driverByLeaf[n.Path]; ok { + d.symlinks["driver"] = strings.Repeat("../", depth) + "bus/pci/drivers/" + drv + classPCIDrivers[drv] = append(classPCIDrivers[drv], n.Path) + } fs.addPCIBus(root, n.Path, classPCIBus) } } @@ -211,6 +243,7 @@ func (fs *filesystem) newRDMASysfs(ctx context.Context, creds *auth.Credentials, devices: map[string]kernfs.Inode{}, class: map[string]kernfs.Inode{}, busPCIDevices: map[string]kernfs.Inode{}, + busPCIDrivers: map[string]kernfs.Inode{}, } devicesTree, ok := root.children["devices"] if !ok { @@ -240,6 +273,17 @@ func (fs *filesystem) newRDMASysfs(ctx context.Context, creds *auth.Credentials, } } + // /sys/bus/pci/drivers// back-symlinks (the inverse of the + // device/driver links added above). libfabric's EFA provider realpath's + // the driver dir to confirm the bound driver during discovery. + for drv, leaves := range classPCIDrivers { + entries := map[string]kernfs.Inode{} + for _, leaf := range leaves { + entries[path.Base(leaf)] = kernfs.NewStaticSymlink(ctx, creds, linux.UNNAMED_MAJOR, fs.devMinor, fs.NextIno(), "../../../../"+leaf) + } + out.busPCIDrivers[drv] = fs.newDir(ctx, creds, defaultSysDirMode, entries) + } + if snap.NUMA != nil { out.node = fs.buildNUMA(ctx, creds, snap.NUMA, cores) } diff --git a/pkg/sentry/fsimpl/sys/sys.go b/pkg/sentry/fsimpl/sys/sys.go index 03b358ef05e..f0438680811 100644 --- a/pkg/sentry/fsimpl/sys/sys.go +++ b/pkg/sentry/fsimpl/sys/sys.go @@ -137,6 +137,7 @@ func (fsType FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.Virt productName := "" busSub := make(map[string]kernfs.Inode) // /sys/bus pciDevices := make(map[string]kernfs.Inode) // /sys/bus/pci/devices + pciDrivers := make(map[string]kernfs.Inode) // /sys/bus/pci/drivers kernelSub := kernelDir(ctx, fs, creds) // /sys/kernel if opts.InternalData != nil { idata := opts.InternalData.(*InternalData) @@ -204,15 +205,23 @@ func (fsType FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.Virt for name, sub := range rdmaDirs.busPCIDevices { pciDevices[name] = sub } + for name, sub := range rdmaDirs.busPCIDrivers { + pciDrivers[name] = sub + } if rdmaDirs.node != nil { systemSub["node"] = rdmaDirs.node } } } - if len(pciDevices) > 0 { - busSub["pci"] = fs.newDir(ctx, creds, defaultSysDirMode, map[string]kernfs.Inode{ - "devices": fs.newDir(ctx, creds, defaultSysDirMode, pciDevices), - }) + if len(pciDevices) > 0 || len(pciDrivers) > 0 { + pciSub := map[string]kernfs.Inode{} + if len(pciDevices) > 0 { + pciSub["devices"] = fs.newDir(ctx, creds, defaultSysDirMode, pciDevices) + } + if len(pciDrivers) > 0 { + pciSub["drivers"] = fs.newDir(ctx, creds, defaultSysDirMode, pciDrivers) + } + busSub["pci"] = fs.newDir(ctx, creds, defaultSysDirMode, pciSub) } devicesSub["system"] = fs.newDir(ctx, creds, defaultSysDirMode, systemSub) diff --git a/pkg/sentry/fsimpl/sys/sys_integration_test.go b/pkg/sentry/fsimpl/sys/sys_integration_test.go index b286a4c5564..691c1414d22 100644 --- a/pkg/sentry/fsimpl/sys/sys_integration_test.go +++ b/pkg/sentry/fsimpl/sys/sys_integration_test.go @@ -323,6 +323,10 @@ func TestEnableTPUProxyPathsV5(t *testing.T) { } } +// rdmaTestPCIConfig is a fake raw PCI config space blob; deliberately not +// valid UTF-8 to check binary-faithful mirroring. +var rdmaTestPCIConfig = []byte{0x86, 0x80, 0x0d, 0x02, 0x00, 0x00, 0x10, 0x00} + // newRDMATestSnapshot returns a snapshot with a ConnectX NIC behind a bridge // on domain 0000 and a GPU on an extended (VMD-style, 5-hex-digit) domain. func newRDMATestSnapshot() *rdma.Snapshot { @@ -331,10 +335,10 @@ func newRDMATestSnapshot() *rdma.Snapshot { VerbsABIVersion: "6\n", PCINodes: []rdma.PCINode{ {Path: "devices/pci0000:07"}, - {Path: "devices/pci0000:07/0000:07:01.0", Attrs: map[string]string{"class": "0x060400\n"}}, + {Path: "devices/pci0000:07/0000:07:01.0", Attrs: map[string]string{"class": "0x060400\n"}, Config: rdmaTestPCIConfig}, {Path: nicLeaf, Attrs: map[string]string{ "vendor": "0x15b3\n", - "uevent": "PCI_SLOT_NAME=0000:0c:00.0\n", + "uevent": "DRIVER=mlx5_core\nPCI_SLOT_NAME=0000:0c:00.0\n", // Host-view CPU affinity: must be rewritten to the sandbox's // single-node view. "numa_node": "1\n", @@ -399,12 +403,14 @@ func TestRDMASysfs(t *testing.T) { "pci_bus": linux.DT_DIR, "subsystem": linux.DT_LNK, "class": linux.DT_REG, + "config": linux.DT_REG, }, nicLeaf: { "infiniband": linux.DT_DIR, "infiniband_verbs": linux.DT_DIR, "net": linux.DT_DIR, "subsystem": linux.DT_LNK, + "driver": linux.DT_LNK, "vendor": linux.DT_REG, "uevent": linux.DT_REG, "numa_node": linux.DT_REG, @@ -455,6 +461,8 @@ func TestRDMASysfs(t *testing.T) { "0000:0c:00.0": linux.DT_LNK, "10000:e0:06.0": linux.DT_LNK, }, + "/bus/pci/drivers": {"mlx5_core": linux.DT_DIR}, + "/bus/pci/drivers/mlx5_core": {"0000:0c:00.0": linux.DT_LNK}, // The two host nodes collapse into a single sandbox node. "/devices/system/node": {"online": linux.DT_REG, "possible": linux.DT_REG, "node0": linux.DT_DIR}, "/devices/system/node/node0": { @@ -486,6 +494,13 @@ func TestRDMASysfs(t *testing.T) { nicLeaf + "/numa_node": "0\n", nicLeaf + "/local_cpulist": wantCPUList, "/devices/pci10000:e0/10000:e0:06.0/numa_node": "-1\n", + // Raw PCI config space is mirrored byte-faithfully. + "/devices/pci0000:07/0000:07:01.0/config": string(rdmaTestPCIConfig), + // The device/driver symlink and the /sys/bus/pci/drivers back-symlink + // resolve to the same PCI function (libfabric realpath's the former + // during EFA discovery). + nicLeaf + "/driver/0000:0c:00.0/vendor": "0x15b3\n", + "/bus/pci/drivers/mlx5_core/0000:0c:00.0/vendor": "0x15b3\n", } { got, err := readTestFile(s, p) if err != nil { diff --git a/pkg/sentry/kernel/BUILD b/pkg/sentry/kernel/BUILD index 5bd28c0533a..a14753a7a2a 100644 --- a/pkg/sentry/kernel/BUILD +++ b/pkg/sentry/kernel/BUILD @@ -341,7 +341,6 @@ go_library( "user_counters_mutex.go", "uts_namespace.go", "vdso.go", - "version.go", ], imports = [ "gvisor.dev/gvisor/pkg/bpf", diff --git a/pkg/sentry/kernel/syscalls.go b/pkg/sentry/kernel/syscalls.go index ea80ddc49cd..b7840ca6e67 100644 --- a/pkg/sentry/kernel/syscalls.go +++ b/pkg/sentry/kernel/syscalls.go @@ -285,9 +285,6 @@ type SyscallTable struct { // Arch is the architecture that this syscall table targets. Arch arch.Arch - // The OS version that this syscall table implements. - Version Version - // AuditNumber is a numeric constant that represents the syscall table. If // non-zero, auditNumber must be one of the AUDIT_ARCH_* values defined by // linux/audit.h. diff --git a/pkg/sentry/kernel/version.go b/pkg/sentry/kernel/version.go deleted file mode 100644 index 678ef9e2725..00000000000 --- a/pkg/sentry/kernel/version.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2018 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 kernel - -// Version defines the application-visible system version. -type Version struct { - // Operating system name (e.g. "Linux"). - Sysname string - - // Operating system release (e.g. "4.4-amd64"). - Release string - - // Operating system version. On Linux this takes the shape - // "#VERSION CONFIG_FLAGS TIMESTAMP" - // where: - // - VERSION is a sequence counter incremented on every successful build - // - CONFIG_FLAGS is a space-separated list of major enabled kernel features - // (e.g. "SMP" and "PREEMPT") - // - TIMESTAMP is the build timestamp as returned by `date` - Version string -} diff --git a/pkg/sentry/kernel/version/version.go b/pkg/sentry/kernel/version/version.go index 1052811a28b..94e40341b44 100644 --- a/pkg/sentry/kernel/version/version.go +++ b/pkg/sentry/kernel/version/version.go @@ -19,17 +19,41 @@ const ( // LinuxSysname is the OS name advertised by gVisor. LinuxSysname = "Linux" - // LinuxRelease is the Linux release version number advertised by gVisor. + // defaultRelease is the default Linux release version number advertised + // by gVisor. // // Must be high enough to satisfy the NT_GNU_ABI_TAG minimum-kernel check // performed by glibc's dynamic linker; otherwise dlopen() rejects modern // shared libraries (e.g. libQt6Core.so.6 requires >= 4.11.0) with a // misleading ENOENT. 4.19 is the final LTS of the Linux 4.x series, - // which keeps us on a 4.x base consistent with the syscall table ABI - // while providing headroom for typical modern userspace. The "-gvisor" - // suffix follows the distro-kernel convention (e.g. "-generic", "-azure"). - LinuxRelease = "4.19.0-gvisor" + // which keeps us on a 4.x base. The "-gvisor" suffix follows the + // distro-kernel convention (e.g. "-generic", "-azure"). + defaultRelease = "4.19.0-gvisor" + + // rdmaRelease is the release advertised instead when RDMA support is + // enabled. It must be >= 5.12 so RDMA userspace enables dmabuf-based + // memory registration for GPUDirect: aws-ofi-nccl (the EFA NCCL + // transport) gates ibv_reg_dmabuf_mr() on the uname(2) release, and the + // RDMA dmabuf uverbs ioctls landed upstream in Linux 5.12. Without this + // the EFA provider falls back to registering raw CUDA VAs, which the + // sandbox cannot pin. Advertised only in RDMA sandboxes to limit the + // bump's blast radius; a later change can make it the default. + rdmaRelease = "5.15.0-gvisor" // LinuxVersion is the version info advertised by gVisor. LinuxVersion = "#1 SMP Sun Jan 10 15:06:54 PST 2016" ) + +// release is the advertised release; see UseRDMARelease. +var release = defaultRelease + +// LinuxRelease returns the Linux release version number advertised by gVisor. +func LinuxRelease() string { + return release +} + +// UseRDMARelease switches the advertised release to rdmaRelease. It must only +// be called during early boot. +func UseRDMARelease() { + release = rdmaRelease +} diff --git a/pkg/sentry/syscalls/linux/linux64.go b/pkg/sentry/syscalls/linux/linux64.go index f4a82dbd45c..da61ec6fa91 100644 --- a/pkg/sentry/syscalls/linux/linux64.go +++ b/pkg/sentry/syscalls/linux/linux64.go @@ -22,24 +22,14 @@ import ( "gvisor.dev/gvisor/pkg/hostarch" "gvisor.dev/gvisor/pkg/sentry/arch" "gvisor.dev/gvisor/pkg/sentry/kernel" - "gvisor.dev/gvisor/pkg/sentry/kernel/version" "gvisor.dev/gvisor/pkg/sentry/syscalls" ) // AMD64 is a table of Linux amd64 syscall API with the corresponding syscall // numbers from Linux 4.4. var AMD64 = &kernel.SyscallTable{ - OS: abi.Linux, - Arch: arch.AMD64, - Version: kernel.Version{ - // Version 4.4 is chosen as a stable, longterm version of Linux, which - // guides the interface provided by this syscall table. The build - // version is that for a clean build with default kernel config, at 5 - // minutes after v4.4 was tagged. - Sysname: version.LinuxSysname, - Release: version.LinuxRelease, - Version: version.LinuxVersion, - }, + OS: abi.Linux, + Arch: arch.AMD64, AuditNumber: linux.AUDIT_ARCH_X86_64, Table: map[uintptr]kernel.Syscall{ 0: syscalls.SupportedPoint("read", Read, PointRead), @@ -413,13 +403,8 @@ var AMD64 = &kernel.SyscallTable{ // ARM64 is a table of Linux arm64 syscall API with the corresponding syscall // numbers from Linux 4.4. var ARM64 = &kernel.SyscallTable{ - OS: abi.Linux, - Arch: arch.ARM64, - Version: kernel.Version{ - Sysname: version.LinuxSysname, - Release: version.LinuxRelease, - Version: version.LinuxVersion, - }, + OS: abi.Linux, + Arch: arch.ARM64, AuditNumber: linux.AUDIT_ARCH_AARCH64, Table: map[uintptr]kernel.Syscall{ 0: syscalls.PartiallySupported("io_setup", IoSetup, "Generally supported with exceptions. User ring optimizations are not implemented.", []string{"gvisor.dev/issue/204"}), diff --git a/pkg/sentry/syscalls/linux/sys_utsname.go b/pkg/sentry/syscalls/linux/sys_utsname.go index 40eb6912f57..32608ebe695 100644 --- a/pkg/sentry/syscalls/linux/sys_utsname.go +++ b/pkg/sentry/syscalls/linux/sys_utsname.go @@ -19,20 +19,19 @@ import ( "gvisor.dev/gvisor/pkg/errors/linuxerr" "gvisor.dev/gvisor/pkg/sentry/arch" "gvisor.dev/gvisor/pkg/sentry/kernel" + "gvisor.dev/gvisor/pkg/sentry/kernel/version" ) // Uname implements linux syscall uname. func Uname(t *kernel.Task, sysno uintptr, args arch.SyscallArguments) (uintptr, *kernel.SyscallControl, error) { - version := t.SyscallTable().Version - uts := t.UTSNamespace() // Fill in structure fields. var u linux.UtsName - copy(u.Sysname[:], version.Sysname) + copy(u.Sysname[:], version.LinuxSysname) copy(u.Nodename[:], uts.HostName()) - copy(u.Release[:], version.Release) - copy(u.Version[:], version.Version) + copy(u.Release[:], version.LinuxRelease()) + copy(u.Version[:], version.LinuxVersion) // build tag above. switch t.SyscallTable().Arch { case arch.AMD64: diff --git a/runsc/boot/BUILD b/runsc/boot/BUILD index a5fdff2596f..7d1c29d3775 100644 --- a/runsc/boot/BUILD +++ b/runsc/boot/BUILD @@ -68,6 +68,7 @@ go_library( "//pkg/sentry/devices/nvproxy/nvconf", "//pkg/sentry/devices/rdmaproxy", "//pkg/sentry/devices/rdmaproxy/cxproxy", + "//pkg/sentry/devices/rdmaproxy/efaproxy", "//pkg/sentry/devices/tpuproxy", "//pkg/sentry/devices/tpuproxy/vfio", "//pkg/sentry/devices/ttydev", @@ -94,6 +95,7 @@ go_library( "//pkg/sentry/kernel/auth", "//pkg/sentry/limits", "//pkg/sentry/loader", + "//pkg/sentry/kernel/version", "//pkg/sentry/pgalloc", "//pkg/sentry/platform", "//pkg/sentry/platform/platforms", @@ -103,6 +105,7 @@ go_library( "//pkg/sentry/seccheck/sinks/remote", "//pkg/sentry/socket/hostinet", "//pkg/sentry/socket/netfilter", + "//pkg/sentry/syscalls/linux", "//pkg/sentry/socket/netlink", "//pkg/sentry/socket/netlink/netfilter", "//pkg/sentry/socket/netlink/route", diff --git a/runsc/boot/loader.go b/runsc/boot/loader.go index 6588a4e621f..01487a8aac6 100644 --- a/runsc/boot/loader.go +++ b/runsc/boot/loader.go @@ -44,6 +44,7 @@ import ( "gvisor.dev/gvisor/pkg/sentry/devices/nvproxy" "gvisor.dev/gvisor/pkg/sentry/devices/nvproxy/nvconf" "gvisor.dev/gvisor/pkg/sentry/devices/rdmaproxy/cxproxy" + "gvisor.dev/gvisor/pkg/sentry/devices/rdmaproxy/efaproxy" "gvisor.dev/gvisor/pkg/sentry/fdimport" cgroup2fs "gvisor.dev/gvisor/pkg/sentry/fsimpl/cgroup2fs" "gvisor.dev/gvisor/pkg/sentry/fsimpl/host" @@ -52,6 +53,7 @@ import ( "gvisor.dev/gvisor/pkg/sentry/inet" "gvisor.dev/gvisor/pkg/sentry/kernel" "gvisor.dev/gvisor/pkg/sentry/kernel/auth" + "gvisor.dev/gvisor/pkg/sentry/kernel/version" "gvisor.dev/gvisor/pkg/sentry/loader" "gvisor.dev/gvisor/pkg/sentry/pgalloc" "gvisor.dev/gvisor/pkg/sentry/platform" @@ -552,6 +554,8 @@ func New(args Args) (*Loader, error) { } if specutils.RDMAEnabled(args.Spec, args.Conf) { cxproxy.Init() + efaproxy.Init() + version.UseRDMARelease() } eid := execID{cid: args.ID}