mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
Add an efaproxy driver plug-in for AWS Elastic Fabric Adapter (EFA) devices
bound to the host `efa` driver. EFA's driver-private uverbs ABI carries no
userspace buffer pointers on CQ/QP CREATE: queue memory is kernel-allocated
and handed back via mmap keys in the CREATE response, which the core already
forwards to the host FD verbatim. PrepareCreateDMA is therefore a no-op; the
plug-in exists to register the "efa" driver name and to model EFA's MR_QUERY
driver-namespace method (used by libfabric to fetch RDMA read/write
interconnect IDs).
Model the standard AH object's destroy method (UVERBS_OBJECT_AH): EFA (SRD)
creates address handles via the legacy write path and destroys them through
the modern object, which the core must allow.
Extend the virtual RDMA sysfs for libfabric/hwloc topology discovery:
- device/driver symlink and /sys/bus/pci/drivers/<driver> tree, which
libfabric's EFA provider resolves during device discovery.
- Mirror each PCI node's raw config space ("config" file). hwloc (used by
aws-ofi-nccl to build the NCCL topology) reads it to recover the PCI
bridge bus-number registers; without it hwloc cannot reconstruct the
bridge hierarchy and aws-ofi-nccl aborts its topology write.
- Serve the per-port lid_mask_count attribute alongside the other port
attributes libibverbs reads from sysfs; ib_core creates it on every
port.
The sysfs integration tests cover the new driver tree and config mirroring.
Advertise a >= 5.12 kernel release in RDMA-enabled sandboxes so aws-ofi-nccl
enables dmabuf-based GPU memory registration (ibv_reg_dmabuf_mr), which the
proxy handles via the existing REG_DMABUF_MR path. Sandboxes without RDMA
keep the current 4.19 release to limit the bump's blast radius; a later
change can make the newer release the default.
Co-authored-by: Alessio Ricci Toniolo <[email protected]>
428 lines
16 KiB
Go
428 lines
16 KiB
Go
// 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 sys
|
|
|
|
import (
|
|
"fmt"
|
|
"path"
|
|
"strings"
|
|
|
|
"gvisor.dev/gvisor/pkg/abi/linux"
|
|
"gvisor.dev/gvisor/pkg/context"
|
|
"gvisor.dev/gvisor/pkg/rdma"
|
|
"gvisor.dev/gvisor/pkg/sentry/fsimpl/kernfs"
|
|
"gvisor.dev/gvisor/pkg/sentry/kernel"
|
|
"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
|
|
)
|
|
|
|
// This file builds the RDMA sysfs surface from a rdma.Snapshot.
|
|
//
|
|
// The layout is path-identical to the host: one canonical subtree under
|
|
// /sys/devices/pci... holds every attribute, with the device-class
|
|
// directories (infiniband, infiniband_verbs, net) as real directories under
|
|
// their PCI leaf, exactly as the kernel lays them out. /sys/class/* and
|
|
// /sys/bus/pci/devices contain only symlinks into the canonical subtree,
|
|
// with kernel-identical relative targets. Fidelity matters because
|
|
// consumers resolve these links with realpath() and then navigate the
|
|
// resolved paths: libibverbs discovers devices via
|
|
// /sys/class/infiniband_verbs, and NCCL's topology discovery realpath()s
|
|
// /sys/class/infiniband/<dev>/device, walks the PCI hierarchy upward
|
|
// reading bridge attributes, and locates GPUs via
|
|
// "/sys/class/pci_bus/<bus>/../../<bdf>".
|
|
//
|
|
// Most attributes are static snapshots. Per-port dynamic state (link state,
|
|
// rate, and above all the RoCE GID table, which repopulates whenever the
|
|
// paired netdev's netns or addresses change) is served by reading host sysfs
|
|
// at access time; those host paths are bind-mounted into the sandbox's chroot.
|
|
|
|
// rdmaSysfsDirs is the output of newRDMASysfs: subtrees for GetFilesystem
|
|
// to graft into the overall /sys hierarchy.
|
|
type rdmaSysfsDirs struct {
|
|
// devices maps the pciXXXX:YY root-complex names to their subtrees,
|
|
// to be added under /sys/devices.
|
|
devices map[string]kernfs.Inode
|
|
// class maps class-directory names (infiniband, infiniband_verbs,
|
|
// net, pci_bus) to symlink-farm directories for /sys/class.
|
|
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/<driver> directory of bound-device back-symlinks.
|
|
busPCIDrivers map[string]kernfs.Inode
|
|
// node is the /sys/devices/system/node subtree, or nil.
|
|
node kernfs.Inode
|
|
}
|
|
|
|
// rdmaDirTree is the intermediate mutable representation, keyed by entry name.
|
|
type rdmaDirTree struct {
|
|
children map[string]*rdmaDirTree
|
|
files map[string]string // static file contents
|
|
hostFiles map[string]string // file name -> host path read at access time
|
|
symlinks map[string]string // link name -> relative target
|
|
}
|
|
|
|
func newRDMADirTree() *rdmaDirTree {
|
|
return &rdmaDirTree{
|
|
children: map[string]*rdmaDirTree{},
|
|
files: map[string]string{},
|
|
hostFiles: map[string]string{},
|
|
symlinks: map[string]string{},
|
|
}
|
|
}
|
|
|
|
// get returns the subtree at relPath (slash-separated), creating
|
|
// intermediate directories.
|
|
func (t *rdmaDirTree) get(relPath string) *rdmaDirTree {
|
|
cur := t
|
|
for _, part := range strings.Split(relPath, "/") {
|
|
if part == "" {
|
|
continue
|
|
}
|
|
next, ok := cur.children[part]
|
|
if !ok {
|
|
next = newRDMADirTree()
|
|
cur.children[part] = next
|
|
}
|
|
cur = next
|
|
}
|
|
return cur
|
|
}
|
|
|
|
// newRDMASysfs builds the RDMA sysfs subtrees from snap.
|
|
func (fs *filesystem) newRDMASysfs(ctx context.Context, creds *auth.Credentials, snap *rdma.Snapshot) (*rdmaSysfsDirs, error) {
|
|
root := newRDMADirTree()
|
|
classIB := map[string]string{} // ibdev -> symlink target
|
|
classUverbs := map[string]string{} // uverbsN -> symlink target
|
|
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
|
|
// subsystem to /sys/bus/pci; without it a device reads as "not a PCI
|
|
// device" and topology discovery aborts. The link points into our own
|
|
// synthesized /sys/bus/pci, so it is not a host escape hatch. The
|
|
// bus/<bus> entry each device sits on is created for pci_bus below.
|
|
cores := kernel.KernelFromContext(ctx).ApplicationCores()
|
|
for _, n := range snap.PCINodes {
|
|
if err := checkRelPath(n.Path); err != nil {
|
|
return nil, err
|
|
}
|
|
d := root.get(n.Path)
|
|
for name, val := range n.Attrs {
|
|
if rdma.SafeName(name) {
|
|
d.files[name] = val
|
|
}
|
|
}
|
|
// CPU-affinity attributes (from pciAttrNames) need to be rewritten.
|
|
// gVisor presents just 1 NUMA node (over CPUs 0..cores-1) to the
|
|
// application. A "-1" numa_node (device on no node) is preserved.
|
|
if v, ok := d.files["numa_node"]; ok && strings.TrimSpace(v) != "-1" {
|
|
d.files["numa_node"] = "0\n"
|
|
}
|
|
if _, ok := d.files["local_cpus"]; ok {
|
|
d.files["local_cpus"] = fullCPUMask(cores) + "\n"
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
|
|
// 2. Per-device class subtrees under their PCI leaves.
|
|
for i := range snap.Devices {
|
|
dev := &snap.Devices[i]
|
|
if err := checkRelPath(dev.LeafPCI); err != nil {
|
|
return nil, err
|
|
}
|
|
if !rdma.SafeName(dev.IBDev) || !rdma.SafeName(dev.Uverbs) {
|
|
return nil, fmt.Errorf("unsafe RDMA device names %q/%q", dev.IBDev, dev.Uverbs)
|
|
}
|
|
// From <leaf>/<classdir>/<name>, the PCI function directory is
|
|
// three levels up plus its own name.
|
|
deviceLink := "../../../" + path.Base(dev.LeafPCI)
|
|
|
|
// infiniband/<ibdev>/
|
|
ibRel := dev.LeafPCI + "/infiniband/" + dev.IBDev
|
|
ib := root.get(ibRel)
|
|
for name, val := range dev.IBAttrs {
|
|
if rdma.SafeName(name) {
|
|
ib.files[name] = val
|
|
}
|
|
}
|
|
ib.symlinks["device"] = deviceLink
|
|
fs.addPorts(ib, dev)
|
|
classIB[dev.IBDev] = "../../" + ibRel
|
|
|
|
// infiniband_verbs/<uverbsN>/
|
|
uvRel := dev.LeafPCI + "/infiniband_verbs/" + dev.Uverbs
|
|
uv := root.get(uvRel)
|
|
// IBDev is the trimmed device name (it is also a path component); the
|
|
// sysfs "ibdev" file holds that name followed by a newline. dev and
|
|
// abi_version were snapshotted verbatim, so they already carry the
|
|
// host's trailing newline.
|
|
uv.files["ibdev"] = dev.IBDev + "\n"
|
|
uv.files["abi_version"] = dev.ABIVersion
|
|
// The RDMA proxy registers the uverbs device at the host's fixed
|
|
// IB uverbs major (IB_UVERBS_MAJOR), so this host "major:minor"
|
|
// already matches the guest /dev/infiniband node and needs no rewrite.
|
|
uv.files["dev"] = dev.Dev
|
|
uv.symlinks["device"] = deviceLink
|
|
classUverbs[dev.Uverbs] = "../../" + uvRel
|
|
|
|
// net/<name>/
|
|
for _, nd := range dev.NetDevs {
|
|
if !rdma.SafeName(nd.Name) {
|
|
continue
|
|
}
|
|
netRel := dev.LeafPCI + "/net/" + nd.Name
|
|
nt := root.get(netRel)
|
|
for name, val := range nd.Attrs {
|
|
if rdma.SafeName(name) {
|
|
nt.files[name] = val
|
|
}
|
|
}
|
|
nt.symlinks["device"] = deviceLink
|
|
classNet[nd.Name] = "../../" + netRel
|
|
}
|
|
}
|
|
// pci_bus and subsystem entries are created for every BDF node in the
|
|
// loop above; GPU leaves are already in snap.PCINodes, so they are
|
|
// covered without special handling here.
|
|
|
|
// 3. Convert the tree and assemble the outputs.
|
|
out := &rdmaSysfsDirs{
|
|
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 {
|
|
return nil, fmt.Errorf("RDMA snapshot contains no devices/ paths")
|
|
}
|
|
for name, sub := range devicesTree.children {
|
|
out.devices[name] = fs.buildRDMADir(ctx, creds, sub)
|
|
}
|
|
|
|
out.class["infiniband"] = fs.newDir(ctx, creds, defaultSysDirMode, fs.symlinkFarm(ctx, creds, classIB))
|
|
uverbsEntries := fs.symlinkFarm(ctx, creds, classUverbs)
|
|
if snap.VerbsABIVersion != "" {
|
|
uverbsEntries["abi_version"] = fs.newStaticFile(ctx, creds, defaultSysMode, snap.VerbsABIVersion)
|
|
}
|
|
out.class["infiniband_verbs"] = fs.newDir(ctx, creds, defaultSysDirMode, uverbsEntries)
|
|
if len(classNet) > 0 {
|
|
out.class["net"] = fs.newDir(ctx, creds, defaultSysDirMode, fs.symlinkFarm(ctx, creds, classNet))
|
|
}
|
|
if len(classPCIBus) > 0 {
|
|
out.class["pci_bus"] = fs.newDir(ctx, creds, defaultSysDirMode, fs.symlinkFarm(ctx, creds, classPCIBus))
|
|
}
|
|
|
|
for _, n := range snap.PCINodes {
|
|
base := path.Base(n.Path)
|
|
if rdma.IsBDF(base) {
|
|
out.busPCIDevices[base] = kernfs.NewStaticSymlink(ctx, creds, linux.UNNAMED_MAJOR, fs.devMinor, fs.NextIno(), "../../../"+n.Path)
|
|
}
|
|
}
|
|
|
|
// /sys/bus/pci/drivers/<driver>/<bdf> 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)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// addPorts populates <ibdev>/ports/<n>/ with static attributes and
|
|
// live host-backed files (GIDs, GID attributes, state, counters).
|
|
func (fs *filesystem) addPorts(ib *rdmaDirTree, dev *rdma.Device) {
|
|
for num, port := range dev.Ports {
|
|
if !rdma.SafeName(num) {
|
|
continue
|
|
}
|
|
hostBase := path.Join("/sys/class/infiniband", dev.IBDev, "ports", num)
|
|
p := ib.get("ports/" + num)
|
|
for name, val := range port.StaticAttrs {
|
|
if rdma.SafeName(name) {
|
|
p.files[name] = val
|
|
}
|
|
}
|
|
for _, name := range port.LiveAttrs {
|
|
if rdma.SafeName(name) {
|
|
p.hostFiles[name] = path.Join(hostBase, name)
|
|
}
|
|
}
|
|
gids := p.get("gids")
|
|
types := p.get("gid_attrs/types")
|
|
ndevs := p.get("gid_attrs/ndevs")
|
|
for _, idx := range port.GIDNames {
|
|
if !rdma.SafeName(idx) {
|
|
continue
|
|
}
|
|
gids.hostFiles[idx] = path.Join(hostBase, "gids", idx)
|
|
types.hostFiles[idx] = path.Join(hostBase, "gid_attrs/types", idx)
|
|
ndevs.hostFiles[idx] = path.Join(hostBase, "gid_attrs/ndevs", idx)
|
|
}
|
|
for _, name := range port.CounterNames {
|
|
if rdma.SafeName(name) {
|
|
p.get("counters").hostFiles[name] = path.Join(hostBase, "counters", name)
|
|
}
|
|
}
|
|
for _, name := range port.HWCounterNames {
|
|
if rdma.SafeName(name) {
|
|
p.get("hw_counters").hostFiles[name] = path.Join(hostBase, "hw_counters", name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// addPCIBus creates <parent-of-leaf>/pci_bus/<bus> (the kernel places a
|
|
// bus directory inside the bridge that creates the bus) and records the
|
|
// /sys/class/pci_bus symlink. NCCL resolves GPU and NIC positions via
|
|
// "/sys/class/pci_bus/<bus>/../../<bdf>".
|
|
//
|
|
// Precondtion: rdma.IsBDF(path.Base(leaf)) == true
|
|
func (fs *filesystem) addPCIBus(root *rdmaDirTree, leaf string, classPCIBus map[string]string) {
|
|
base := path.Base(leaf)
|
|
i := strings.LastIndex(base, ":")
|
|
if i < 0 {
|
|
panic(fmt.Sprintf("RDMA leaf %q is not a PCI BDF", leaf))
|
|
}
|
|
bus := base[:i] // "0000:0c" of "0000:0c:00.0"
|
|
parent := path.Dir(leaf)
|
|
root.get(parent + "/pci_bus/" + bus)
|
|
classPCIBus[bus] = "../../" + parent + "/pci_bus/" + bus
|
|
}
|
|
|
|
// buildNUMA synthesizes the /sys/devices/system/node subtree. The sentry's
|
|
// memory model has a single NUMA node. So advertise exactly one node holding
|
|
// every sandbox CPU, matching the single-socket /proc/cpuinfo view.
|
|
func (fs *filesystem) buildNUMA(ctx context.Context, creds *auth.Credentials, numa *rdma.NUMA, cores uint) kernfs.Inode {
|
|
entries := map[string]kernfs.Inode{}
|
|
// The aggregate files (online, possible, has_cpu, ...) are all
|
|
// node-range lists; the single node 0 covers them all.
|
|
for name := range numa.Aggregate {
|
|
if rdma.SafeName(name) {
|
|
entries[name] = fs.newStaticFile(ctx, creds, defaultSysMode, "0\n")
|
|
}
|
|
}
|
|
entries["node0"] = fs.newDir(ctx, creds, defaultSysDirMode, map[string]kernfs.Inode{
|
|
"cpumap": fs.newStaticFile(ctx, creds, defaultSysMode, fullCPUMask(cores)+"\n"),
|
|
"cpulist": fs.newStaticFile(ctx, creds, defaultSysMode, cpuListString(cores)),
|
|
// The only node's distance to itself (LOCAL_DISTANCE).
|
|
"distance": fs.newStaticFile(ctx, creds, defaultSysMode, "10\n"),
|
|
})
|
|
return fs.newDir(ctx, creds, defaultSysDirMode, entries)
|
|
}
|
|
|
|
// cpuListString formats CPUs 0..cores-1 in the kernel's cpulist format
|
|
// (lib/bitmap.c:bitmap_print_to_pagebuf(list=true)).
|
|
func cpuListString(cores uint) string {
|
|
if cores == 1 {
|
|
return "0\n"
|
|
}
|
|
return fmt.Sprintf("0-%d\n", cores-1)
|
|
}
|
|
|
|
// buildRDMADir converts an rdmaDirTree into kernfs inodes.
|
|
func (fs *filesystem) buildRDMADir(ctx context.Context, creds *auth.Credentials, t *rdmaDirTree) kernfs.Inode {
|
|
entries := map[string]kernfs.Inode{}
|
|
for name, val := range t.files {
|
|
entries[name] = fs.newStaticFile(ctx, creds, defaultSysMode, val)
|
|
}
|
|
for name, hostPath := range t.hostFiles {
|
|
// Read from host sysfs at file-read time via openat(-1, ...,
|
|
// O_RDONLY|O_NOFOLLOW) (sys.hostFile.Generate); the rdmaproxy seccomp
|
|
// filter allows that openat.
|
|
entries[name] = fs.newHostFile(ctx, creds, defaultSysMode, hostPath)
|
|
}
|
|
for name, target := range t.symlinks {
|
|
entries[name] = kernfs.NewStaticSymlink(ctx, creds, linux.UNNAMED_MAJOR, fs.devMinor, fs.NextIno(), target)
|
|
}
|
|
for name, child := range t.children {
|
|
entries[name] = fs.buildRDMADir(ctx, creds, child)
|
|
}
|
|
return fs.newDir(ctx, creds, defaultSysDirMode, entries)
|
|
}
|
|
|
|
func (fs *filesystem) symlinkFarm(ctx context.Context, creds *auth.Credentials, targets map[string]string) map[string]kernfs.Inode {
|
|
entries := map[string]kernfs.Inode{}
|
|
for name, target := range targets {
|
|
if rdma.SafeName(name) {
|
|
entries[name] = kernfs.NewStaticSymlink(ctx, creds, linux.UNNAMED_MAJOR, fs.devMinor, fs.NextIno(), target)
|
|
}
|
|
}
|
|
return entries
|
|
}
|
|
|
|
// checkRelPath validates a snapshot-provided sysfs-relative path: rooted
|
|
// under devices/, with every component a safe name (see SafeName).
|
|
func checkRelPath(relPath string) error {
|
|
if !strings.HasPrefix(relPath, "devices/") {
|
|
return fmt.Errorf("RDMA sysfs path %q is not under devices/", relPath)
|
|
}
|
|
for _, part := range strings.Split(relPath, "/") {
|
|
if !rdma.SafeName(part) {
|
|
return fmt.Errorf("RDMA sysfs path %q contains unsafe component %q", relPath, part)
|
|
}
|
|
}
|
|
return nil
|
|
}
|