[RFC PATCH 10/11] selftests/kvm: gmem_provider iommufd tests

From: David Woodhouse

Date: Thu Jul 16 2026 - 11:46:38 EST


From: David Woodhouse <dwmw@xxxxxxxxxxxx>

Three selftests that exercise the guest_memfd provider ABI from the
iommufd side, going progressively closer to a real assigned-device
setup:

gmem_provider_iommufd_test
Dual-map. A KVM memslot and an iommufd IOAS are both backed by the
same provider dma-buf. The guest writes a magic pattern into the
slot; the host mmap of the same provider fd sees the guest's write;
the IOAS also holds a mapping of the same backing. Proves KVM and
iommufd converge on the same provider phys via two different fds
(the KVM guest_memfd fd and the dma-buf fd).

gmem_provider_vfio_test
A real vfio-pci device is bound to iommufd and attached to an IOAS
that has the provider region mapped. Confirms the attach path
accepts a dma-buf-backed IOAS without complaint; does not issue
device DMA. Requires an unused PCI device already bound to
vfio-pci and exposed as /dev/vfio/devices/vfio0 (overridable via
GMEM_VFIO_CDEV).

gmem_provider_nvme_dma_test
Full round trip. Binds an NVMe controller to vfio-pci, attaches
it to an IOAS with the provider region mapped at IOVA_DST,
initialises NVMe admin queues (also in the IOAS via anonymous
pages), submits Identify Controller with PRP1=IOVA_DST, and
verifies the 4 KiB response via the host mmap of the same
provider fd (VID/SSVID/MN check). Then revokes the target range
with GMEM_PROVIDER_SET_PRESENT and re-submits Identify to the same
IOVA: iommufd's iopt_revoke_notify tears down the mapping and the
device DMA either faults at the IOMMU (visible as IO_PAGE_FAULT in
dmesg on AMD-Vi / equivalent on VT-d, SMMUv3) or completes with an
NVMe error; either way the provider region is not overwritten.
Restores and confirms fresh mappings succeed.

Requires an EBS/NVMe scratch device on the test host bound to vfio-pci;
the tests SKIP cleanly if no such device is available.

Signed-off-by: David Woodhouse (Kiro) <dwmw@xxxxxxxxxxxx>
---
tools/testing/selftests/kvm/Makefile.kvm | 9 +-
.../kvm/x86/gmem_provider_iommufd_test.c | 150 +++++++
.../kvm/x86/gmem_provider_nvme_dma_test.c | 365 ++++++++++++++++++
.../kvm/x86/gmem_provider_vfio_test.c | 134 +++++++
4 files changed, 655 insertions(+), 3 deletions(-)
create mode 100644 tools/testing/selftests/kvm/x86/gmem_provider_iommufd_test.c
create mode 100644 tools/testing/selftests/kvm/x86/gmem_provider_nvme_dma_test.c
create mode 100644 tools/testing/selftests/kvm/x86/gmem_provider_vfio_test.c

diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm
index e6e9ac9da45a..7f46843e7428 100644
--- a/tools/testing/selftests/kvm/Makefile.kvm
+++ b/tools/testing/selftests/kvm/Makefile.kvm
@@ -77,6 +77,12 @@ TEST_GEN_PROGS_x86 += x86/evmcs_smm_controls_test
TEST_GEN_PROGS_x86 += x86/exit_on_emulation_failure_test
TEST_GEN_PROGS_x86 += x86/fastops_test
TEST_GEN_PROGS_x86 += x86/fix_hypercall_test
+TEST_GEN_PROGS_x86 += x86/gmem_provider_test
+TEST_GEN_PROGS_x86 += x86/gmem_provider_hugepage_test
+TEST_GEN_PROGS_x86 += x86/gmem_provider_revoke_test
+TEST_GEN_PROGS_x86 += x86/gmem_provider_iommufd_test
+TEST_GEN_PROGS_x86 += x86/gmem_provider_vfio_test
+TEST_GEN_PROGS_x86 += x86/gmem_provider_nvme_dma_test
TEST_GEN_PROGS_x86 += x86/hwcr_msr_test
TEST_GEN_PROGS_x86 += x86/hyperv_clock
TEST_GEN_PROGS_x86 += x86/hyperv_cpuid
@@ -161,9 +167,6 @@ TEST_GEN_PROGS_x86 += rseq_test
TEST_GEN_PROGS_x86 += steal_time
TEST_GEN_PROGS_x86 += system_counter_offset_test
TEST_GEN_PROGS_x86 += pre_fault_memory_test
-TEST_GEN_PROGS_x86 += x86/gmem_provider_test
-TEST_GEN_PROGS_x86 += x86/gmem_provider_hugepage_test
-TEST_GEN_PROGS_x86 += x86/gmem_provider_revoke_test

# Compiled outputs used by test targets
TEST_GEN_PROGS_EXTENDED_x86 += x86/nx_huge_pages_test
diff --git a/tools/testing/selftests/kvm/x86/gmem_provider_iommufd_test.c b/tools/testing/selftests/kvm/x86/gmem_provider_iommufd_test.c
new file mode 100644
index 000000000000..0b2fc097b5a9
--- /dev/null
+++ b/tools/testing/selftests/kvm/x86/gmem_provider_iommufd_test.c
@@ -0,0 +1,150 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * gmem_provider_iommufd_test - map the SAME provider-backed region into KVM
+ * (as a guest_memfd memslot) AND into an iommufd IOAS (via a dma-buf exported
+ * from the provider fd). Prove both APIs accept the mapping and that a guest
+ * write is visible via the host mmap.
+ *
+ * The iommufd side exercises exactly the provider->iommufd path we just wired:
+ * GET_DMABUF on the provider fd -> IOMMU_IOAS_MAP_FILE, which walks
+ * iopt_map_dmabuf -> sym_..._iommufd_map -> gmem_provider_dma_buf_iommufd_map.
+ *
+ * The test opens the provider with GMEM_PROVIDER_FLAG_MMAP_CAPABLE at SETUP time; requires iommufd
+ * available at /dev/iommu. Actual IOMMU page-table programming happens once
+ * a device is attached; here we validate the plumbing (map succeeds; unmap
+ * returns the same byte count) without needing a passthrough device.
+ */
+#include <fcntl.h>
+#include <errno.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+
+#include <linux/iommufd.h>
+
+#include "test_util.h"
+#include "kvm_util.h"
+#include "processor.h"
+
+/* Mirrors samples/kvm/gmem_provider.h */
+struct gmem_provider_setup {
+ __s32 kvm_fd;
+ __u32 flags;
+ __u64 size;
+};
+#define GMEM_PROVIDER_SETUP _IOW('G', 1, struct gmem_provider_setup)
+#define GMEM_PROVIDER_FLAG_MMAP_CAPABLE (1u << 0)
+#define GMEM_PROVIDER_GET_DMABUF _IO('G', 3)
+
+#define DATA_SLOT 10
+#define DATA_GPA (1ULL << 32)
+#define DATA_SIZE 0x200000ULL /* 2 MiB */
+#define IOVA_BASE (1ULL << 34) /* arbitrary */
+#define MAGIC 0xa5a5a5a5a5a5a5a5ULL
+
+static void guest_code(void)
+{
+ *(volatile uint64_t *)DATA_GPA = MAGIC;
+ GUEST_DONE();
+}
+
+int main(void)
+{
+ struct vm_shape shape = {
+ .mode = VM_MODE_DEFAULT,
+ .type = KVM_X86_SW_PROTECTED_VM,
+ };
+ struct gmem_provider_setup setup = { .flags = GMEM_PROVIDER_FLAG_MMAP_CAPABLE };
+ struct iommu_ioas_alloc alloc = {};
+ struct iommu_ioas_map_file map = {};
+ struct kvm_vcpu *vcpu;
+ struct kvm_vm *vm;
+ struct ucall uc;
+ int gmem_ctl, gmem_fd, dmabuf_fd, iommufd, r;
+ void *hva;
+
+ TEST_REQUIRE(kvm_check_cap(KVM_CAP_VM_TYPES) & BIT(KVM_X86_SW_PROTECTED_VM));
+
+ gmem_ctl = open("/dev/gmem_provider", O_RDWR);
+ __TEST_REQUIRE(gmem_ctl >= 0,
+ "gmem_provider not loaded (/dev/gmem_provider absent)");
+ iommufd = open("/dev/iommu", O_RDWR);
+ __TEST_REQUIRE(iommufd >= 0, "iommufd unavailable (/dev/iommu absent)");
+
+ /*
+ * 1) Provider fd bound to KVM VM.
+ */
+ vm = vm_create_shape_with_one_vcpu(shape, &vcpu, guest_code);
+ setup.kvm_fd = vm->fd;
+ setup.size = DATA_SIZE;
+ gmem_fd = ioctl(gmem_ctl, GMEM_PROVIDER_SETUP, &setup);
+ TEST_ASSERT(gmem_fd >= 0, "GMEM_PROVIDER_SETUP failed errno=%d", errno);
+
+ /*
+ * 2) KVM side: host mmap + guest_memfd memslot (gmem-only when mmap-capable).
+ */
+ hva = mmap(NULL, DATA_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, gmem_fd, 0);
+ TEST_ASSERT(hva != MAP_FAILED,
+ "provider mmap failed errno=%d", errno);
+ r = __vm_set_user_memory_region2(vm, DATA_SLOT, KVM_MEM_GUEST_MEMFD,
+ DATA_GPA, DATA_SIZE, hva, gmem_fd, 0);
+ TEST_ASSERT(!r, "KVM_SET_USER_MEMORY_REGION2 failed r=%d errno=%d", r, errno);
+ virt_map(vm, DATA_GPA, DATA_GPA, 1);
+
+ /*
+ * 3) iommufd side: allocate IOAS, get a dma-buf from the SAME provider fd,
+ * and map it into the IOAS via IOMMU_IOAS_MAP_FILE. This drives
+ * iopt_map_dmabuf -> gmem_provider_dma_buf_iommufd_map.
+ */
+ alloc.size = sizeof(alloc);
+ r = ioctl(iommufd, IOMMU_IOAS_ALLOC, &alloc);
+ TEST_ASSERT(!r, "IOMMU_IOAS_ALLOC failed errno=%d", errno);
+ pr_info("iommufd: allocated ioas id=%u\n", alloc.out_ioas_id);
+
+ dmabuf_fd = ioctl(gmem_fd, GMEM_PROVIDER_GET_DMABUF);
+ TEST_ASSERT(dmabuf_fd >= 0, "GMEM_PROVIDER_GET_DMABUF failed errno=%d", errno);
+ pr_info("provider: exported dma-buf fd=%d\n", dmabuf_fd);
+
+ map.size = sizeof(map);
+ map.flags = IOMMU_IOAS_MAP_FIXED_IOVA |
+ IOMMU_IOAS_MAP_READABLE | IOMMU_IOAS_MAP_WRITEABLE;
+ map.ioas_id = alloc.out_ioas_id;
+ map.fd = dmabuf_fd;
+ map.start = 0;
+ map.length = DATA_SIZE;
+ map.iova = IOVA_BASE;
+ r = ioctl(iommufd, IOMMU_IOAS_MAP_FILE, &map);
+ TEST_ASSERT(!r,
+ "IOMMU_IOAS_MAP_FILE(dma-buf) failed r=%d errno=%d\n"
+ " (gmem_provider_dma_buf_iommufd_map path)",
+ r, errno);
+ pr_info("iommufd: mapped provider dma-buf @ IOVA 0x%llx (0x%llx bytes)\n",
+ (unsigned long long)map.iova, (unsigned long long)map.length);
+
+ /*
+ * 4) Run the guest. It writes MAGIC via the KVM/NPT path; we then read
+ * via the host mmap to confirm the dual-mapping resolves to the same
+ * physical backing.
+ */
+ vcpu_run(vcpu);
+ TEST_ASSERT(get_ucall(vcpu, &uc) == UCALL_DONE,
+ "guest exit: %s", exit_reason_str(vcpu->run->exit_reason));
+
+ TEST_ASSERT(*(volatile uint64_t *)hva == MAGIC,
+ "host mmap read 0x%llx, want MAGIC (guest+host+iommufd disagree)",
+ (unsigned long long)*(volatile uint64_t *)hva);
+ pr_info("dual-map ok: guest wrote MAGIC, host mmap sees 0x%llx, "
+ "iommufd IOAS holds the same backing\n",
+ (unsigned long long)*(volatile uint64_t *)hva);
+
+ kvm_vm_free(vm);
+ close(dmabuf_fd);
+ close(iommufd);
+ munmap(hva, DATA_SIZE);
+ close(gmem_fd);
+ close(gmem_ctl);
+ return 0;
+}
diff --git a/tools/testing/selftests/kvm/x86/gmem_provider_nvme_dma_test.c b/tools/testing/selftests/kvm/x86/gmem_provider_nvme_dma_test.c
new file mode 100644
index 000000000000..8c8a4b2e8ac9
--- /dev/null
+++ b/tools/testing/selftests/kvm/x86/gmem_provider_nvme_dma_test.c
@@ -0,0 +1,365 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * gmem_provider_nvme_dma_test - real DMA into provider memory via a
+ * passthrough NVMe controller.
+ *
+ * Sends an Identify Controller (opc=0x06, CNS=1) admin command to a
+ * vfio-pci-bound NVMe controller, with PRP1 pointing at an IOVA that maps
+ * to the gmem provider's backing. Success = the provider region contains
+ * the 4 KiB Identify Controller data structure (verified via VID field
+ * matching the PCI vendor read from config space).
+ *
+ * Requires the gmem_provider module loaded, iommufd available, and an NVMe
+ * controller bound to vfio-pci at /dev/vfio/devices/vfio0 (overridable via
+ * GMEM_VFIO_CDEV). DESTROYS ANY IN-FLIGHT STATE on that controller.
+ */
+#include <fcntl.h>
+#include <errno.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+
+#include <linux/iommufd.h>
+#include <linux/vfio.h>
+#include <linux/kvm.h>
+
+#include "test_util.h"
+
+struct gmem_provider_setup {
+ __s32 kvm_fd;
+ __u32 flags;
+ __u64 size;
+};
+#define GMEM_PROVIDER_SETUP _IOW('G', 1, struct gmem_provider_setup)
+#define GMEM_PROVIDER_FLAG_MMAP_CAPABLE (1u << 0)
+#define GMEM_PROVIDER_GET_DMABUF _IO('G', 3)
+
+struct gmem_provider_present {
+ __u64 offset;
+ __u64 len;
+ __u32 present;
+ __u32 pad;
+};
+#define GMEM_PROVIDER_SET_PRESENT _IOW('G', 2, struct gmem_provider_present)
+
+#define PROVIDER_SIZE 0x200000ULL /* 2 MiB provider region */
+#define IOVA_DST 0x40000000ULL /* target for Identify Controller data */
+#define IOVA_SQ (IOVA_DST + PROVIDER_SIZE)
+#define IOVA_CQ (IOVA_SQ + 0x1000)
+
+/* NVMe controller regs (BAR0). */
+#define NVME_REG_CAP 0x00
+#define NVME_REG_VS 0x08
+#define NVME_REG_CC 0x14
+#define NVME_REG_CSTS 0x1C
+#define NVME_REG_AQA 0x24
+#define NVME_REG_ASQ 0x28
+#define NVME_REG_ACQ 0x30
+#define NVME_SQ0TDBL 0x1000
+
+#define NVME_CC_EN (1U << 0)
+#define NVME_CSTS_RDY (1U << 0)
+
+static uint32_t rd32(void *b, uint32_t o) { return *(volatile uint32_t *)((char *)b + o); }
+static uint64_t rd64(void *b, uint32_t o) { return *(volatile uint64_t *)((char *)b + o); }
+static void wr32(void *b, uint32_t o, uint32_t v) { *(volatile uint32_t *)((char *)b + o) = v; }
+static void wr64(void *b, uint32_t o, uint64_t v) { *(volatile uint64_t *)((char *)b + o) = v; }
+
+static int ioas_map_user(int iommufd_fd, uint32_t ioas_id,
+ void *va, uint64_t iova, uint64_t len)
+{
+ struct iommu_ioas_map m = {
+ .size = sizeof(m),
+ .flags = IOMMU_IOAS_MAP_FIXED_IOVA |
+ IOMMU_IOAS_MAP_READABLE |
+ IOMMU_IOAS_MAP_WRITEABLE,
+ .ioas_id = ioas_id,
+ .user_va = (uintptr_t)va,
+ .length = len,
+ .iova = iova,
+ };
+ return ioctl(iommufd_fd, IOMMU_IOAS_MAP, &m);
+}
+
+int main(void)
+{
+ struct gmem_provider_setup setup = { .flags = GMEM_PROVIDER_FLAG_MMAP_CAPABLE };
+ struct iommu_ioas_alloc alloc = {};
+ struct iommu_ioas_map_file mapf = {};
+ struct vfio_device_bind_iommufd bind = {};
+ struct vfio_device_attach_iommufd_pt att = {};
+ struct vfio_region_info reg = {};
+ const char *cdev_path;
+ int gmem_ctl, gmem_fd, dmabuf_fd, iommufd_fd, vfio_fd;
+ void *provider_hva, *sq_buf, *cq_buf, *bar;
+ uint16_t pci_cmd;
+ uint64_t cap, cfg_off;
+ uint32_t doorbell_stride, aqa, cc, csts;
+ int i;
+
+ gmem_ctl = open("/dev/gmem_provider", O_RDWR);
+ __TEST_REQUIRE(gmem_ctl >= 0, "gmem_provider module not loaded");
+ iommufd_fd = open("/dev/iommu", O_RDWR);
+ __TEST_REQUIRE(iommufd_fd >= 0, "iommufd unavailable");
+ cdev_path = getenv("GMEM_VFIO_CDEV");
+ if (!cdev_path)
+ cdev_path = "/dev/vfio/devices/vfio0";
+ vfio_fd = open(cdev_path, O_RDWR);
+ __TEST_REQUIRE(vfio_fd >= 0, "vfio cdev absent: %s", cdev_path);
+
+ /* Provider fd (bare KVM VM to satisfy SETUP). */
+ {
+ int kvm = open("/dev/kvm", O_RDWR);
+ int vm;
+ TEST_ASSERT(kvm >= 0, "open /dev/kvm errno=%d", errno);
+ vm = ioctl(kvm, KVM_CREATE_VM, 0);
+ TEST_ASSERT(vm >= 0, "KVM_CREATE_VM errno=%d", errno);
+ setup.kvm_fd = vm;
+ close(kvm);
+ }
+ setup.size = PROVIDER_SIZE;
+ gmem_fd = ioctl(gmem_ctl, GMEM_PROVIDER_SETUP, &setup);
+ TEST_ASSERT(gmem_fd >= 0, "SETUP errno=%d", errno);
+
+ provider_hva = mmap(NULL, PROVIDER_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED,
+ gmem_fd, 0);
+ TEST_ASSERT(provider_hva != MAP_FAILED, "provider mmap errno=%d", errno);
+ memset(provider_hva, 0xcc, 0x1000); /* poison first page */
+
+ /* IOAS + provider dma-buf. */
+ alloc.size = sizeof(alloc);
+ TEST_ASSERT(!ioctl(iommufd_fd, IOMMU_IOAS_ALLOC, &alloc), "IOAS_ALLOC");
+ dmabuf_fd = ioctl(gmem_fd, GMEM_PROVIDER_GET_DMABUF);
+ TEST_ASSERT(dmabuf_fd >= 0, "GET_DMABUF");
+ mapf.size = sizeof(mapf);
+ mapf.flags = IOMMU_IOAS_MAP_FIXED_IOVA | IOMMU_IOAS_MAP_READABLE |
+ IOMMU_IOAS_MAP_WRITEABLE;
+ mapf.ioas_id = alloc.out_ioas_id;
+ mapf.fd = dmabuf_fd;
+ mapf.length = PROVIDER_SIZE;
+ mapf.iova = IOVA_DST;
+ TEST_ASSERT(!ioctl(iommufd_fd, IOMMU_IOAS_MAP_FILE, &mapf), "IOAS_MAP_FILE");
+
+ /* Admin SQ/CQ backing pages (userspace anon, IOMMU-mapped). */
+ sq_buf = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ TEST_ASSERT(sq_buf != MAP_FAILED, "sq mmap");
+ memset(sq_buf, 0, 0x1000);
+ cq_buf = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ TEST_ASSERT(cq_buf != MAP_FAILED, "cq mmap");
+ memset(cq_buf, 0, 0x1000);
+ TEST_ASSERT(!ioas_map_user(iommufd_fd, alloc.out_ioas_id, sq_buf, IOVA_SQ, 0x1000),
+ "IOAS_MAP(sq) errno=%d", errno);
+ TEST_ASSERT(!ioas_map_user(iommufd_fd, alloc.out_ioas_id, cq_buf, IOVA_CQ, 0x1000),
+ "IOAS_MAP(cq) errno=%d", errno);
+
+ /* Bind + attach. */
+ bind.argsz = sizeof(bind);
+ bind.iommufd = iommufd_fd;
+ TEST_ASSERT(!ioctl(vfio_fd, VFIO_DEVICE_BIND_IOMMUFD, &bind), "BIND");
+ att.argsz = sizeof(att);
+ att.pt_id = alloc.out_ioas_id;
+ TEST_ASSERT(!ioctl(vfio_fd, VFIO_DEVICE_ATTACH_IOMMUFD_PT, &att), "ATTACH");
+ pr_info("vfio device attached, pt_id=%u\n", att.pt_id);
+
+ /* mmap BAR0. */
+ reg.argsz = sizeof(reg);
+ reg.index = VFIO_PCI_BAR0_REGION_INDEX;
+ TEST_ASSERT(!ioctl(vfio_fd, VFIO_DEVICE_GET_REGION_INFO, &reg), "GET_REGION_INFO");
+ __TEST_REQUIRE(reg.flags & VFIO_REGION_INFO_FLAG_MMAP, "BAR0 not mmap-capable");
+ bar = mmap(NULL, reg.size, PROT_READ | PROT_WRITE, MAP_SHARED, vfio_fd, reg.offset);
+ TEST_ASSERT(bar != MAP_FAILED, "BAR0 mmap");
+ pr_info("BAR0 mapped size=0x%llx\n", (unsigned long long)reg.size);
+
+ /* Enable Bus Master. */
+ reg.argsz = sizeof(reg);
+ reg.index = VFIO_PCI_CONFIG_REGION_INDEX;
+ TEST_ASSERT(!ioctl(vfio_fd, VFIO_DEVICE_GET_REGION_INFO, &reg), "CFG region info");
+ cfg_off = reg.offset;
+ TEST_ASSERT(pread(vfio_fd, &pci_cmd, 2, cfg_off + 0x04) == 2, "cfg CMD read");
+ pci_cmd |= 0x0006;
+ TEST_ASSERT(pwrite(vfio_fd, &pci_cmd, 2, cfg_off + 0x04) == 2, "cfg CMD write");
+
+ cap = rd64(bar, NVME_REG_CAP);
+ doorbell_stride = 4U << ((cap >> 32) & 0xf);
+ pr_info("NVMe CAP=0x%016llx VS=0x%08x MQES=%u DSTRD=%u\n",
+ (unsigned long long)cap, rd32(bar, NVME_REG_VS),
+ (uint32_t)((cap & 0xffff) + 1), doorbell_stride);
+
+ /* Disable + wait !RDY. */
+ wr32(bar, NVME_REG_CC, 0);
+ for (i = 0; i < 5000 && (rd32(bar, NVME_REG_CSTS) & NVME_CSTS_RDY); i++)
+ usleep(1000);
+
+ /* Admin queues: 64 entries each. */
+ wr64(bar, NVME_REG_ASQ, IOVA_SQ);
+ wr64(bar, NVME_REG_ACQ, IOVA_CQ);
+ aqa = (63U << 16) | 63U; /* ACQS-1 | ASQS-1 */
+ wr32(bar, NVME_REG_AQA, aqa);
+ cc = (6U << 16) | (4U << 20) | NVME_CC_EN; /* IOSQES=6 IOCQES=4 EN */
+ wr32(bar, NVME_REG_CC, cc);
+
+ for (i = 0; i < 10000; i++) {
+ csts = rd32(bar, NVME_REG_CSTS);
+ if (csts & NVME_CSTS_RDY)
+ break;
+ usleep(1000);
+ if (csts & 0x02) { /* CFS */
+ TEST_FAIL("NVMe controller fatal status CSTS=0x%x", csts);
+ }
+ }
+ TEST_ASSERT(csts & NVME_CSTS_RDY, "controller never reached RDY, CSTS=0x%x", csts);
+ pr_info("NVMe ready. CSTS=0x%x\n", csts);
+
+ /* Identify Controller SQE at slot 0. */
+ {
+ uint32_t *s = (uint32_t *)sq_buf;
+ memset(s, 0, 64);
+ s[0] = 0x00010006; /* CID=1 << 16 | OPC=Identify */
+ s[1] = 0; /* NSID */
+ /* PRP1 (offset 24 in SQE = dword 6) */
+ s[6] = (uint32_t)IOVA_DST;
+ s[7] = (uint32_t)(IOVA_DST >> 32);
+ s[8] = 0;
+ s[9] = 0; /* PRP2 */
+ s[10] = 0x01; /* CDW10: CNS = Identify Controller */
+ }
+
+ /* Ring SQ0 tail doorbell. */
+ wr32(bar, NVME_SQ0TDBL, 1);
+ pr_info("Identify Controller submitted (SQ0 tail=1)\n");
+
+ /* Poll CQ0 slot 0 for a valid completion (phase tag = 1 on first pass). */
+ {
+ volatile uint32_t *c = (volatile uint32_t *)cq_buf;
+ uint32_t status;
+ for (i = 0; i < 5000; i++) {
+ if (c[3] & 0x10000) /* P bit */
+ break;
+ usleep(1000);
+ }
+ TEST_ASSERT(c[3] & 0x10000, "completion phase bit never set");
+ status = (c[3] >> 17) & 0x7fff;
+ pr_info("CQE: DW0=0x%08x DW1=0x%08x DW2=0x%08x DW3=0x%08x status=0x%04x\n",
+ c[0], c[1], c[2], c[3], status);
+ TEST_ASSERT(status == 0, "Identify Controller status non-zero: 0x%x", status);
+ fprintf(stderr, "MARKER: about to write CQ doorbell\n"); fflush(stderr);
+ /* CQ0 head doorbell = 1 */
+ wr32(bar, NVME_SQ0TDBL + doorbell_stride, 1);
+ fprintf(stderr, "MARKER: about to read provider\n"); fflush(stderr);
+ }
+
+ /* Provider region now holds Identify Controller data. VID/SSVID at bytes 0..3
+ * must equal the device's PCI vendor + subsystem vendor (both are Amazon here). */
+ {
+ uint16_t vid = ((uint16_t *)provider_hva)[0];
+ uint16_t ssvid = ((uint16_t *)provider_hva)[1];
+ uint8_t first = ((uint8_t *)provider_hva)[0];
+ uint8_t poison_still = ((uint8_t *)provider_hva)[8];
+ char mn[41] = {};
+ memcpy(mn, (char *)provider_hva + 24, 40);
+ fprintf(stderr, "MARKER: read done; byte0=0x%02x byte8=0x%02x\n",
+ first, poison_still);
+ fflush(stderr);
+ pr_info("Identify Controller in provider: VID=0x%04x SSVID=0x%04x MN='%s'\n",
+ vid, ssvid, mn);
+ fflush(stdout);
+ TEST_ASSERT(vid == 0x1d0f,
+ "unexpected VID 0x%04x in provider region -- Identify DMA landed elsewhere?\n"
+ " poison=0x%02x byte0=0x%02x (poison intact=DMA didn't land here)",
+ vid, poison_still, first);
+ pr_info("NVMe -> provider DMA VERIFIED\n");
+ }
+
+ /*
+ * Phase 2: revoke the target page in the provider and verify that the
+ * next DMA to the same IOVA fails, without touching provider memory.
+ *
+ * SET_PRESENT(present=0) marks the range absent in the provider bitmap
+ * and fans out dma_buf_invalidate_mappings() to every exported dma-buf.
+ * iommufd's iopt_revoke_notify (registered as .invalidate_mappings on
+ * the dynamic attach) tears down the IOMMU mapping for that range and
+ * marks the pages as revoked, so a subsequent device DMA to the same
+ * IOVA lands with no IOMMU translation and either faults at the IOMMU
+ * or completes with an NVMe error. Either way the provider region
+ * must not be overwritten.
+ */
+ {
+ struct gmem_provider_present rev = {
+ .offset = 0, .len = 0x1000, .present = 0,
+ };
+ volatile uint32_t *c = (volatile uint32_t *)cq_buf;
+ uint32_t *s = (uint32_t *)sq_buf;
+ uint32_t status;
+ int ret;
+
+ /* Re-seed the target page so we can detect *any* write to it. */
+ memset(provider_hva, 0xa5, 0x1000);
+
+ ret = ioctl(gmem_fd, GMEM_PROVIDER_SET_PRESENT, &rev);
+ TEST_ASSERT(ret == 0, "SET_PRESENT(revoke) errno=%d", errno);
+ pr_info("Revoked provider offset=0 len=0x1000; dma-buf mappings invalidated\n");
+
+ /* Issue a second Identify Controller aimed at the revoked IOVA.
+ * Use a new CID and place the SQE in slot 1; the SQ ring will
+ * wrap for larger runs but we only ever use slots 0 and 1.
+ */
+ memset(&s[16], 0, 64);
+ s[16] = 0x00020006; /* CID=2 << 16 | OPC=Identify */
+ s[16 + 6] = (uint32_t)IOVA_DST;
+ s[16 + 7] = (uint32_t)(IOVA_DST >> 32);
+ s[16 + 10] = 0x01; /* CNS = Identify Controller */
+
+ wr32(bar, NVME_SQ0TDBL, 2);
+ pr_info("Identify Controller (revoked target) submitted (SQ0 tail=2)\n");
+
+ /* Poll for CQE at slot 1 with phase bit inverted from first pass. */
+ for (i = 0; i < 5000; i++) {
+ if ((c[4 + 3] & 0x10000)) /* CQE 1 P bit */
+ break;
+ usleep(1000);
+ }
+ TEST_ASSERT(c[4 + 3] & 0x10000, "revoke-phase CQE never landed");
+ status = (c[4 + 3] >> 17) & 0x7fff;
+ pr_info("post-revoke CQE: DW0=0x%08x DW3=0x%08x status=0x%04x\n",
+ c[4 + 0], c[4 + 3], status);
+ wr32(bar, NVME_SQ0TDBL + doorbell_stride, 2);
+
+ /* Two acceptable outcomes here: (a) NVMe reports a non-zero
+ * status (typical when the IOMMU fault takes the transaction
+ * down), or (b) NVMe reports success but the target page was
+ * not overwritten because the IOMMU rerouted / dropped the
+ * write. Either proves the revocation took effect on the DMA
+ * side. A silent success that overwrites provider memory
+ * would be a bug in our revoke plumbing.
+ */
+ {
+ uint8_t b0 = ((uint8_t *)provider_hva)[0];
+ pr_info("post-revoke provider byte0=0x%02x (seeded 0xa5)\n", b0);
+ TEST_ASSERT(b0 == 0xa5 || status != 0,
+ "revoke leaked: status=0 and provider byte0=0x%02x != 0xa5",
+ b0);
+ pr_info("Revoked-page DMA is not observable in provider memory\n");
+ }
+
+ /* Restore and confirm a fresh IOMMU_IOAS_MAP_FILE succeeds. */
+ rev.present = 1;
+ ret = ioctl(gmem_fd, GMEM_PROVIDER_SET_PRESENT, &rev);
+ TEST_ASSERT(ret == 0, "SET_PRESENT(restore) errno=%d", errno);
+ pr_info("Restore + revoke round trip complete\n");
+ }
+
+ munmap(bar, reg.size);
+ close(vfio_fd);
+ close(dmabuf_fd);
+ close(iommufd_fd);
+ munmap(provider_hva, PROVIDER_SIZE);
+ close(gmem_fd);
+ close(gmem_ctl);
+ return 0;
+}
diff --git a/tools/testing/selftests/kvm/x86/gmem_provider_vfio_test.c b/tools/testing/selftests/kvm/x86/gmem_provider_vfio_test.c
new file mode 100644
index 000000000000..97a66a295b14
--- /dev/null
+++ b/tools/testing/selftests/kvm/x86/gmem_provider_vfio_test.c
@@ -0,0 +1,134 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * gmem_provider_vfio_test - real passthrough device attached to an iommufd
+ * IOAS holding the provider dma-buf.
+ *
+ * Steps:
+ * 1. SETUP provider (KVM VM shape not required for this test).
+ * 2. iommufd IOAS + GET_DMABUF + IOMMU_IOAS_MAP_FILE - IOAS holds the
+ * provider region.
+ * 3. Open a vfio-pci cdev (default /dev/vfio/devices/vfio0, overridable
+ * via GMEM_VFIO_CDEV env), VFIO_DEVICE_BIND_IOMMUFD, then
+ * VFIO_DEVICE_ATTACH_IOMMUFD_PT to the IOAS.
+ *
+ * On success the kernel has allocated (or reused) an iommu_domain for the
+ * device covering the provider region: subsequent DMA by the device to the
+ * mapped IOVAs would land in provider memory. Actual DMA descriptor
+ * programming is device-specific and left as a follow-on.
+ *
+ * Skips if either the provider module or a bound vfio-pci cdev is absent.
+ */
+#include <fcntl.h>
+#include <errno.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+
+#include <linux/iommufd.h>
+#include <linux/vfio.h>
+#include <linux/kvm.h>
+
+#include "test_util.h"
+
+struct gmem_provider_setup {
+ __s32 kvm_fd;
+ __u32 flags;
+ __u64 size;
+};
+#define GMEM_PROVIDER_SETUP _IOW('G', 1, struct gmem_provider_setup)
+#define GMEM_PROVIDER_FLAG_MMAP_CAPABLE (1u << 0)
+#define GMEM_PROVIDER_GET_DMABUF _IO('G', 3)
+
+#define DATA_SIZE 0x200000ULL
+#define IOVA_BASE (1ULL << 34)
+
+int main(void)
+{
+ struct gmem_provider_setup setup = { .flags = GMEM_PROVIDER_FLAG_MMAP_CAPABLE };
+ struct iommu_ioas_alloc alloc = {};
+ struct iommu_ioas_map_file map = {};
+ struct vfio_device_bind_iommufd bind = {};
+ struct vfio_device_attach_iommufd_pt att = {};
+ const char *cdev_path;
+ int gmem_ctl, gmem_fd, dmabuf_fd, iommufd_fd, vfio_fd, r;
+
+ gmem_ctl = open("/dev/gmem_provider", O_RDWR);
+ __TEST_REQUIRE(gmem_ctl >= 0, "gmem_provider module not loaded");
+ iommufd_fd = open("/dev/iommu", O_RDWR);
+ __TEST_REQUIRE(iommufd_fd >= 0, "iommufd unavailable (/dev/iommu absent)");
+
+ cdev_path = getenv("GMEM_VFIO_CDEV");
+ if (!cdev_path)
+ cdev_path = "/dev/vfio/devices/vfio0";
+ vfio_fd = open(cdev_path, O_RDWR);
+ __TEST_REQUIRE(vfio_fd >= 0,
+ "no vfio-pci cdev at %s (bind a device first)", cdev_path);
+ pr_info("using vfio cdev: %s (fd=%d)\n", cdev_path, vfio_fd);
+
+ /* Provider fd (KVM binding not needed here, but SETUP still requires a
+ * valid kvm_fd; pass /dev/kvm's own fd -- the SETUP path just fgets it
+ * to grab a KVM ref, and /dev/kvm's file has ->private_data==NULL, so
+ * the "kvm not attached" check must be relaxed OR we create a VM.
+ * For robustness, create a bare KVM VM. */
+ {
+ int kvm = open("/dev/kvm", O_RDWR);
+ int vm;
+ TEST_ASSERT(kvm >= 0, "open /dev/kvm errno=%d", errno);
+ vm = ioctl(kvm, KVM_CREATE_VM, 0);
+ TEST_ASSERT(vm >= 0, "KVM_CREATE_VM errno=%d", errno);
+ setup.kvm_fd = vm;
+ close(kvm);
+ }
+ setup.size = DATA_SIZE;
+ gmem_fd = ioctl(gmem_ctl, GMEM_PROVIDER_SETUP, &setup);
+ TEST_ASSERT(gmem_fd >= 0, "GMEM_PROVIDER_SETUP errno=%d", errno);
+
+ /* IOAS + provider dma-buf */
+ alloc.size = sizeof(alloc);
+ r = ioctl(iommufd_fd, IOMMU_IOAS_ALLOC, &alloc);
+ TEST_ASSERT(!r, "IOMMU_IOAS_ALLOC errno=%d", errno);
+
+ dmabuf_fd = ioctl(gmem_fd, GMEM_PROVIDER_GET_DMABUF);
+ TEST_ASSERT(dmabuf_fd >= 0, "GET_DMABUF errno=%d", errno);
+
+ map.size = sizeof(map);
+ map.flags = IOMMU_IOAS_MAP_FIXED_IOVA |
+ IOMMU_IOAS_MAP_READABLE | IOMMU_IOAS_MAP_WRITEABLE;
+ map.ioas_id = alloc.out_ioas_id;
+ map.fd = dmabuf_fd;
+ map.length = DATA_SIZE;
+ map.iova = IOVA_BASE;
+ r = ioctl(iommufd_fd, IOMMU_IOAS_MAP_FILE, &map);
+ TEST_ASSERT(!r, "IOMMU_IOAS_MAP_FILE errno=%d", errno);
+ pr_info("IOAS %u holds provider dma-buf @ IOVA 0x%llx (0x%llx bytes)\n",
+ alloc.out_ioas_id,
+ (unsigned long long)map.iova, (unsigned long long)map.length);
+
+ /* Bind VFIO cdev to iommufd */
+ bind.argsz = sizeof(bind);
+ bind.iommufd = iommufd_fd;
+ r = ioctl(vfio_fd, VFIO_DEVICE_BIND_IOMMUFD, &bind);
+ TEST_ASSERT(!r, "VFIO_DEVICE_BIND_IOMMUFD errno=%d", errno);
+ pr_info("VFIO device bound: devid=%u\n", bind.out_devid);
+
+ /* Attach the device to our IOAS */
+ att.argsz = sizeof(att);
+ att.pt_id = alloc.out_ioas_id;
+ r = ioctl(vfio_fd, VFIO_DEVICE_ATTACH_IOMMUFD_PT, &att);
+ TEST_ASSERT(!r,
+ "VFIO_DEVICE_ATTACH_IOMMUFD_PT errno=%d\n"
+ " (real device -> IOAS attach failed)", errno);
+ pr_info("VFIO device attached to IOAS -> pt_id=%u (kernel-selected)\n",
+ att.pt_id);
+ pr_info("real passthrough device now has IOMMU domain covering provider region\n");
+
+ close(vfio_fd);
+ close(dmabuf_fd);
+ close(iommufd_fd);
+ close(gmem_fd);
+ close(gmem_ctl);
+ return 0;
+}
--
2.54.0