Skip to content

Probing tools: what the runtimes and the kernel actually do - #64

Draft
safl wants to merge 8 commits into
mainfrom
probes
Draft

Probing tools: what the runtimes and the kernel actually do#64
safl wants to merge 8 commits into
mainfrom
probes

Conversation

@safl

@safl safl commented Aug 25, 2026

Copy link
Copy Markdown
Owner

The probes under tools/ report what a runtime or the kernel actually does, as
opposed to tests/, which assert what uPCIe requires. They print and exit zero;
they do not fail when the answer is inconvenient.

They were carried along in #59 and #63 because that is where
they were needed. They belong on their own: they touch tools/ and one
subdir('tools') line, and depend on nothing either of those branches adds.

Draft because the questions they answer are still moving. The kernel refuses
IOMMU_IOAS_MAP_FILE on a GPU-exported dma-buf today, and if that changes then
what upcie_probe_vram_ioas_{cuda,hip} reports changes with it.

Worth a careful look

The flavour contract in probe_dmabuf.h. A flavour defines three functions,
includes the header, and calls probe_run(). The functions are prototyped in
the header rather than described only in prose, so a flavour missing one, or
defining it with another signature, is diagnosed at the prototype instead of at
the point of use or at link time.

Verification

upcie_probe_vfio_cdev, upcie_probe_vfio_share and upcie_probe_vfio_delegate
build on Linux with no accelerator toolchain present, and every commit builds on
its own. clang-format is clean across the branch.

The CUDA and HIP flavours are gated on their toolchains and were not compiled on
the machine this was assembled on, so they are unverified here; the contract in
probe_dmabuf.h was instead checked with a stub flavour, both complete and with
a function withheld, to confirm the prototypes catch it. What the probes report
was measured earlier on an NVIDIA RTX A6000 and an AMD Radeon RX 7800 XT, and
those findings are recorded in tools/README.md.

safl added 8 commits August 25, 2026 15:50
uPCIe translates VRAM addresses by exporting a device range as a dma-buf and
reading its scatter list, and the per-chunk form of that has been assumed to
work on both vendors. It does not: on ROCm the range arguments to
hipMemGetHandleForAddressRange are accepted and discarded, and what comes back
describes the whole underlying buffer object at the base of the allocation
regardless of the offset asked for. CUDA honours the request exactly. Since
trusting the first segment address then puts the DMA somewhere other than
where the caller pointed, and does so quietly, the difference needs to be
measured and written down rather than inferred.

This adds a probing tool per flavour and the findings from running both, so
the memory work can be designed against what the runtimes do. Probes report
rather than assert, which is why they sit apart from tests/.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Simon A. F. Lund <os@safl.dk>
Whether a registration can require whole granules turns on what a runtime
reports for an allocation that does not end on a granule boundary, and on
whether the export agrees with it. It does not: both report the size
unrounded, then CUDA exports exactly that while ROCm exports the larger object
it actually allocated. A LUT fill that trusts the reported size to bound the
export therefore fails on one vendor and overshoots on the other.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Simon A. F. Lund <os@safl.dk>
…ce's MMIO

VFIO_DEVICE_FEATURE_DMA_BUF exports a slice of a BAR, and test_dmamem_vfio_bar
already reports that the descriptor carries no CPU mapping. That matters
because cuMemHostRegister() with CU_MEMHOSTREGISTER_IOMEMORY, which is how the
GPU-initiated NVMe path reaches a doorbell, takes a host address. A process
holding only the descriptor has none to give it.

The remaining route was for the runtime to import the descriptor itself, and
the answer decides whether MMIO can be delegated to a process that never holds
the device fd. It cannot, though not for the reason the question anticipated:
CUDA refuses CU_EXTERNAL_MEMORY_HANDLE_TYPE_DMABUF_FD with NOT_SUPPORTED for
any exporter, including a dma-buf it exported itself, and HIP has no dma-buf
handle type to ask with. The probe carries that control with it, since a
runtime that will not import its own descriptor says nothing about vfio.

It reads rather than writes and defaults to offset 0, so it touches CAP rather
than a doorbell, and prints the host's read of the same register beside the
device's. The findings are in tools/README.md next to the probe.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Simon A. F. Lund <os@safl.dk>
Passing a vfio device fd over SCM_RIGHTS and mapping BAR0 in the receiver is
already known to work, and the GPU-initiated NVMe path is already known to ring
doorbells through cuMemHostRegister(CU_MEMHOSTREGISTER_IOMEMORY). What was not
established is that the second works on a mapping produced by the first, which
is the whole of the case for delegating a controller to another process. It
does, as root, and the SM reads what the primary reads.

Two processes over a named socket rather than a fork with a setuid in it, since
the question is what an unprivileged process can do and a process that dropped
privilege is not in that state: it keeps the supplementary groups it started
with unless they are cleared, and a uid change clears the dumpable flag. As an
unprivileged process the secondary gets the descriptor, maps BAR0 and reads the
register, then fails the registration with CUDA_ERROR_NOT_PERMITTED. The
standalone mode is the control for that result, opening the device itself with
no delegation at all, and it is refused in the same place, so the requirement
belongs to the calling process rather than to the descriptor.

The read comes from a kernel rather than from cuMemcpyDtoH(), since the copy
engine is not what rings a doorbell. It reads CAP rather than writing a
doorbell, so running it disturbs nothing.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Simon A. F. Lund <os@safl.dk>
Under uio_pci_generic a controller consumes physical addresses and reaches a
GPU allocation through its dma-buf scatter list. Under vfio-pci it consumes
IOVAs, so the allocation has to be mapped with IOMMU_IOAS_MAP_FILE, and
iommufd.h records that as of 6.19 that call takes only dma-bufs exported by
vfio-pci. That comment is load-bearing for anything wanting a controller and a
GPU under the same IOMMU, it describes a kernel we have moved off, and nothing
here asks the kernel we run.

A memfd is mapped first as a control, since a MAP_FILE that refuses everything
would say nothing about GPU memory in particular.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Simon A. F. Lund <os@safl.dk>
The privilege boundary the probe found was measured with CUDA only, while the
design it informs speaks about GPU submission without naming a vendor. HIP has
hipHostRegisterIoMemory, so the same question can be put to ROCm.

The device-side read goes through hipMemcpyDtoH rather than an SM, since this
project has no ROCm kernel-compilation path. That is weaker than the CUDA
flavour and it is enough for the question being asked, which is whether the
registration is accepted at all.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Simon A. F. Lund <os@safl.dk>
…the primary

Two questions the design turns on were being answered by assumption. Which side
of a delegation is charged for pinned pages decides whether an unprivileged
secondary needs its RLIMIT_MEMLOCK raised, and the design withholds the iommufd
partly on the strength of an answer nobody measured. What a secondary still has
once the primary exits decides whether a restart is a supported handover or
fatal, and the earlier note on it rested on a premise that was wrong.

Both are vfio questions rather than GPU ones, so this probe needs no runtime and
builds everywhere. It maps once from each side so a lowered limit on either
shows which one it bounds.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Simon A. F. Lund <os@safl.dk>
These were written in the xNVMe tree while working out how a controller could
be shared between processes, which put them a repository away from the nine
probes that ask the same kind of question, and from the vfio and iommufd
wrappers they exercise. They belong beside upcie_vfio_delegate_probe, which
asks about the accounting and lifetime of the same delegation.

Moved verbatim, keeping their original attribution. The share probe gains a
_GNU_SOURCE definition because the build here is -std=c11, under which syscall
and ftruncate are not declared; it still includes no uPCIe header, since what
it asks about is raw kernel behaviour.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Simon A. F. Lund <os@safl.dk>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant