From: Fred Griffoul <griffoul@gmail.com>
To: "Paolo Bonzini" <pbonzini@redhat.com>,
"Sean Christopherson" <seanjc@google.com>,
"Marc Zyngier" <maz@kernel.org>,
"Oliver Upton" <oupton@kernel.org>,
"Sumit Semwal" <sumit.semwal@linaro.org>,
"Christian König" <christian.koenig@amd.com>,
"Jason Gunthorpe" <jgg@ziepe.ca>,
"Kevin Tian" <kevin.tian@intel.com>
Cc: David Woodhouse <dwmw2@infradead.org>,
Ackerley Tng <ackerleytng@google.com>,
Joey Gouly <joey.gouly@arm.com>,
Suzuki K Poulose <suzuki.poulose@arm.com>,
Zenghui Yu <yuzenghui@huawei.com>,
Steffen Eiden <seiden@linux.ibm.com>,
Catalin Marinas <catalin.marinas@arm.com>,
Will Deacon <will@kernel.org>, Thomas Gleixner <tglx@kernel.org>,
Ingo Molnar <mingo@redhat.com>, Borislav Petkov <bp@alien8.de>,
Dave Hansen <dave.hansen@linux.intel.com>,
"H . Peter Anvin" <hpa@zytor.com>, Joerg Roedel <joro@8bytes.org>,
Robin Murphy <robin.murphy@arm.com>,
Alex Williamson <alex@shazbot.org>, Shuah Khan <shuah@kernel.org>,
Steven Rostedt <rostedt@goodmis.org>,
Masami Hiramatsu <mhiramat@kernel.org>,
Mathieu Desnoyers <mathieu.desnoyers@efficios.com>,
linux-kernel@vger.kernel.org, kvm@vger.kernel.org,
kvmarm@lists.linux.dev, linux-arm-kernel@lists.infradead.org,
iommu@lists.linux.dev, linux-media@vger.kernel.org,
dri-devel@lists.freedesktop.org, linaro-mm-sig@lists.linaro.org,
linux-kselftest@vger.kernel.org,
linux-trace-kernel@vger.kernel.org, x86@kernel.org
Subject: [RFC PATCH 6/6] samples/kvm, selftests/kvm: Exercise dma-buf backing
Date: Mon, 5 Oct 2026 09:55:52 +0000 [thread overview]
Message-ID: <20261005095552.52748-7-griffoul@gmail.com> (raw)
In-Reply-To: <20261005095552.52748-1-griffoul@gmail.com>
From: Fred Griffoul <fgriffo@amazon.co.uk>
Turn the sample into a dma-buf exporter, and move every supported test
to dma-buf-backed guest_memfd. Doing both in one commit avoids a state
where the sample and the tests use different interfaces.
The sample owns one root region and creates a child descriptor for each
VM. It supports moving, donating and reclaiming pages, absent pages,
read-only ranges, a scratch page and per-child ioctl allowlists.
The sample's get_phys() reports the run at the requested offset. It
uses bitmap searches to find where presence or read-only state changes.
An absent page returns -ENOENT. In scratch mode, an absent page of a
root child is reported as the read-only scratch page instead. Scratch
mode applies only to root children, because a mode change invalidates
only them. Every change of ownership sends a ranged invalidation.
Each test passes one dma-buf fd to both guest_memfd and iommufd. New
tests cover:
- two VMMs, checking what the guest, the device and the host see;
- SET_PRESENT and SET_READONLY racing with one KVM_CREATE_GUEST_MEMFD,
so that lockdep checks attachment setup;
- a 2 MiB-aligned region in which every other page is replaced by the
scratch page. The guest reads zeros and its own data, and KVM maps
the region with 4 KiB pages. An untouched aligned region still maps
at 2 MiB.
The sample sizes the CMA root before children overlap, and publishes
each child only after its ownership and allowlist are set. Confidential
VMs are out of scope; their test is an explicit skip.
Signed-off-by: Fred Griffoul <fgriffo@amazon.co.uk>
---
samples/kvm/gmem_provider.c | 1287 ++++++++++-------
samples/kvm/gmem_provider.h | 144 +-
tools/testing/selftests/kvm/Makefile.kvm | 3 +-
.../kvm/gmem_provider_nvme_dma_test.c | 28 +-
.../testing/selftests/kvm/include/kvm_util.h | 25 +-
.../testing/selftests/kvm/x86/gmem_poc_test.c | 824 +++++++++++
.../kvm/x86/gmem_provider_hugepage_test.c | 30 +-
.../kvm/x86/gmem_provider_iommufd_test.c | 42 +-
.../kvm/x86/gmem_provider_readonly_test.c | 172 +++
.../kvm/x86/gmem_provider_revoke_test.c | 34 +-
.../selftests/kvm/x86/gmem_provider_test.c | 190 +--
.../kvm/x86/gmem_provider_vfio_test.c | 45 +-
12 files changed, 2091 insertions(+), 733 deletions(-)
create mode 100644 tools/testing/selftests/kvm/x86/gmem_poc_test.c
create mode 100644 tools/testing/selftests/kvm/x86/gmem_provider_readonly_test.c
diff --git a/samples/kvm/gmem_provider.c b/samples/kvm/gmem_provider.c
index b6824fe5d228..2cde5bcb71ca 100644
--- a/samples/kvm/gmem_provider.c
+++ b/samples/kvm/gmem_provider.c
@@ -1,43 +1,46 @@
// SPDX-License-Identifier: GPL-2.0
/*
- * gmem_provider - sample external guest_memfd provider.
+ * gmem_provider - sample owner of page-less memory, shared as a dma-buf.
*
- * Demonstrates the KVM guest_memfd provider ABI with two backing modes:
+ * A toy owner of physical memory: one root region carved into children, one
+ * child per VM, ranges that MOVE between children or are DONATEd to the root
+ * and RECLAIMed, a shared scratch frame for revoked pages, and a per-fd
+ * ioctl allowlist fixed by the creator. The provider owns the frames and is
+ * the single authority for who may map them.
*
- * - External (page-less): loaded with addr=/len=, backs guest memory with a
- * fixed physical range that has no struct page -- e.g. memory carved out of
- * the kernel with mem= on the command line. This is the case the provider
- * ABI exists for; get_pfn() returns bare PFNs KVM treats as non-refcounted.
+ * It is a dma-buf exporter and nothing else. A child fd hands out a dma-buf
+ * for the child's memory; a VMM gives that dma-buf to KVM_CREATE_GUEST_MEMFD
+ * for the guest and to iommufd for its devices. An ownership change here is
+ * one ranged invalidation of the child's dma-bufs; every importer drops the
+ * changed view and re-reads the layout. The owner knows nothing of KVM, or which
+ * importer is a guest and which is a device.
*
- * - CMA fallback (page-backed): when addr=/len= are not given, allocates a
- * physically contiguous region via alloc_contig_pages() sized by the setup
- * ioctl. Easier to run (no mem= boot param), and still exercises 2M/1G
- * mappings. Not preserved across kexec/live update.
- *
- * On SEV-SNP hosts, bind() resets the range's RMP entries to 4K shared so a
- * (new) SNP VM can re-encrypt it, which is what allows re-binding the range to
- * a fresh VM across a live update.
+ * Backing:
+ * - External (page-less): loaded with addr=/len=, a fixed physical range with
+ * no struct page, e.g. carved out with memmap= on the command line.
+ * - CMA fallback: without addr=/len=, alloc_contig_pages() sized by SETUP.
*
* Usage:
- * # page-less external range:
- * insmod gmem_provider.ko addr=0x5D40000000 len=0x1000000
- * # or CMA fallback (no params); size comes from the ioctl
- * insmod gmem_provider.ko
- * fd = open("/dev/gmem_provider"); ioctl(fd, GMEM_PROVIDER_SETUP, {kvm_fd, size});
- * pass the returned fd + KVM_MEM_GUEST_MEMFD to KVM_SET_USER_MEMORY_REGION2.
+ * insmod gmem_provider.ko addr=0x180000000 len=0x42000000
+ * ctl = open("/dev/gmem_provider");
+ * child = ioctl(ctl, GMEM_PROVIDER_NEW_CHILD, {offset, len, allow}); # control
+ * dmabuf = ioctl(child, GMEM_PROVIDER_GET_DMABUF); # VMM
+ * gmem = ioctl(vm, KVM_CREATE_GUEST_MEMFD, {size, USE_DMABUF, dmabuf});
+ * KVM_SET_USER_MEMORY_REGION2(..., KVM_MEM_GUEST_MEMFD, gmem);
+ * IOMMU_IOAS_MAP_FILE(ioas, dmabuf, ...);
+ *
+ * The one rule: change ownership state, drop the provider lock, then revoke.
*/
#include <linux/anon_inodes.h>
-#include <linux/bitmap.h>
#include <linux/dma-buf.h>
-#include <linux/dma-buf-mapping.h>
#include <linux/dma-resv.h>
+#include <linux/bitmap.h>
#include <linux/file.h>
#include <linux/fs.h>
#include <linux/gfp.h>
#include <linux/highmem.h>
#include <linux/io.h>
-#include <linux/kvm_host.h>
#include <linux/miscdevice.h>
#include <linux/mm.h>
#include <linux/module.h>
@@ -50,7 +53,6 @@
#include "gmem_provider.h"
-MODULE_IMPORT_NS("DMA_BUF");
static unsigned long long addr;
module_param(addr, ullong, 0444);
@@ -63,444 +65,294 @@ MODULE_PARM_DESC(len, "size in bytes of the external backing region (optional)")
struct gmem_info {
/*
- * MUST be first: file->private_data points here. is_kvm_gmem_file()
- * on the KVM side proves the reinterpretation is safe.
- */
- struct kvm_gmem_backing backing;
-
- /*
- * Protects everything below (except the immutable base_pfn/npages/
- * cma_pages fields set at setup). Ordering: info->lock is a leaf;
- * do not acquire other locks under it. KVM's slots_lock is already
- * held on the .bind/.unbind paths so info->lock is only needed to
- * serialise those against ioctl(SET_PRESENT).
+ * Protects everything below except the immutable fields set at setup
+ * and the dma-buf list. Ordering: gmem_root.lock, then
+ * info->dmabufs_lock, then a dma-buf reservation. info->lock is a
+ * leaf: it is dropped before any invalidation.
*/
struct mutex lock;
- /* Kept locally: kvm_gmem_ops does not carry a kvm pointer. */
- struct kvm *kvm;
-
- /*
- * Single active memslot binding. Used at release time to zap any
- * still-active guest mappings before the memory disappears. NULL
- * when no memslot is bound (or the last one has been unbound).
- */
- struct kvm_memory_slot *bound_slot;
- bool mmap_capable; /* -> KVM_MEMSLOT_GMEM_ONLY at bind */
+ bool mmap_capable; /* child and dma-buf may be mmap()ed */
unsigned long base_pfn;
unsigned long npages;
- struct page *cma_pages; /* non-NULL if CMA-allocated */
- gfn_t base_gfn; /* recorded at bind, for revoke */
- pgoff_t pgoff; /* provider offset (pages) of the slot */
+ struct page *cma_pages; /* non-NULL if CMA-allocated (SETUP path only) */
+
+ /*
+ * Toy descriptor tree. A child created by NEW_CHILD is a sub-range
+ * of the root region: @root_index is its first page within the root,
+ * @owned marks which of its pages are currently granted to it (a page
+ * MOVEd out or DONATEd is not owned; a page revoked by SET_PRESENT is
+ * owned but absent). @allow is the ioctl allowlist. SETUP-created
+ * providers have no root and own everything.
+ */
+ unsigned long root_index;
+ unsigned long *owned; /* NULL for SETUP-created providers */
+ u32 allow;
+ struct list_head root_link; /* gmem_root.children */
unsigned long *absent; /* bitmap of currently-revoked pages */
- struct list_head dmabufs; /* struct gmem_dmabuf entries */
- struct mutex dmabufs_lock;
+ unsigned long *readonly; /* bitmap of pages the guest may not write */
+ struct address_space *mapping; /* our file's: host windows live here */
+ struct list_head dmabufs; /* exported dma-bufs (struct gmem_dmabuf) */
+ struct mutex dmabufs_lock; /* protects @dmabufs */
};
static struct gmem_info *to_gmem_info(struct file *file)
{
- return container_of(file->private_data, struct gmem_info, backing);
+ return file->private_data;
}
-/* Map a backing PFN for CPU access: page-backed via kmap, page-less via memremap. */
-static void *gmem_map_pfn(kvm_pfn_t pfn)
-{
- if (pfn_valid(pfn))
- return kmap_local_pfn(pfn);
- return memremap(PFN_PHYS(pfn), PAGE_SIZE, MEMREMAP_WB);
-}
+/*
+ * The root of the toy descriptor tree: one backing region owned by the
+ * control device. Children carve sub-ranges out of it. All ownership
+ * transitions (NEW_CHILD, MOVE, DONATE, RECLAIM) run under root.lock, which
+ * is taken before any child's info->lock.
+ */
+static struct gmem_root {
+ struct mutex lock; /* protects the fields below */
+ unsigned long base_pfn;
+ unsigned long npages;
+ struct page *cma_pages;
+ unsigned long *owned; /* pages some child currently owns */
+ unsigned long *donated; /* pages parked at the root */
+ struct list_head children;
+ bool scratch_enabled;
+} gmem_root;
-static void gmem_unmap_pfn(kvm_pfn_t pfn, void *vaddr)
-{
- if (!vaddr)
- return;
- if (pfn_valid(pfn))
- kunmap_local(vaddr);
- else
- memunmap(vaddr);
-}
+/*
+ * One zeroed scratch frame for the whole module. When scratch mode is on,
+ * a revoked page of a root child reports this frame read-only to every
+ * importer, so a device that cannot tolerate an IOMMU fault lands here.
+ */
+static struct page *gmem_scratch_page;
-/* Largest order KVM may map at @gfn, snapped to 4K/2M/1G. */
-static int gmem_max_order(struct gmem_info *info, gfn_t gfn, unsigned long index)
+static inline unsigned long gmem_scratch_pfn(void)
{
- unsigned long pfn = info->base_pfn + index;
- unsigned long remaining = info->npages - index;
- unsigned int pud_order = PUD_SHIFT - PAGE_SHIFT;
- unsigned int pmd_order = PMD_SHIFT - PAGE_SHIFT;
- unsigned long absent_next;
-
- /*
- * A hugepage may not span any revoked page. Clamp by the distance to
- * the next absent bit; scanning is cheap because @absent is a plain
- * bitmap and the caller already checked test_bit(index).
- */
- if (info->absent) {
- absent_next = find_next_bit(info->absent, info->npages,
- index + 1);
- remaining = min(remaining, absent_next - index);
- }
-
- if (IS_ALIGNED(pfn, 1UL << pud_order) &&
- IS_ALIGNED(gfn, 1UL << pud_order) &&
- remaining >= (1UL << pud_order))
- return pud_order;
-
- if (IS_ALIGNED(pfn, 1UL << pmd_order) &&
- IS_ALIGNED(gfn, 1UL << pmd_order) &&
- remaining >= (1UL << pmd_order))
- return pmd_order;
-
- return 0;
+ return page_to_pfn(gmem_scratch_page);
}
-static int gmem_get_pfn(struct file *file, struct kvm *kvm,
- struct kvm_memory_slot *slot, gfn_t gfn,
- kvm_pfn_t *pfn, struct page **page, int *max_order,
- bool *writable)
+/*
+ * Is page @index of @info currently reachable by the guest and devices?
+ * Callable with or without info->lock: every change to the bitmaps is
+ * followed by an invalidation of the range, so a lock-free answer is either
+ * current or about to be superseded.
+ */
+static inline bool gmem_page_present(struct gmem_info *info, unsigned long index)
{
- struct gmem_info *info = to_gmem_info(file);
- pgoff_t index = gfn - slot->base_gfn + slot->gmem.pgoff;
-
- if (index >= info->npages)
- return -EINVAL;
-
- /* Revoked (absent) page: behave like not-present so the fault fails. */
+ if (info->owned && !test_bit(index, info->owned))
+ return false;
if (info->absent && test_bit(index, info->absent))
- return -EFAULT;
-
- *pfn = info->base_pfn + index;
- if (max_order)
- *max_order = gmem_max_order(info, gfn, index);
- return 0;
+ return false;
+ return true;
}
-static int gmem_populate(struct file *file, struct kvm *kvm,
- struct kvm_memory_slot *slot, gfn_t gfn,
- kvm_pfn_t *pfn, struct page *src_page, int order)
-{
- struct gmem_info *info = to_gmem_info(file);
- pgoff_t index = gfn - slot->base_gfn + slot->gmem.pgoff;
-
- if (index >= info->npages)
- return -EINVAL;
-
- *pfn = info->base_pfn + index;
+static void gmem_release(struct file *file);
+static int gmem_mmap(struct file *file, struct vm_area_struct *vma);
+static long gmem_fd_ioctl(struct file *file, unsigned int cmd, unsigned long arg);
- if (src_page) {
- void *dst, *src;
- /* Map dst first: memremap() may sleep, kmap_local_page() must not. */
- dst = gmem_map_pfn(*pfn);
- if (!dst)
- return -ENOMEM;
- src = kmap_local_page(src_page);
- memcpy(dst, src, PAGE_SIZE);
- kunmap_local(src);
- gmem_unmap_pfn(*pfn, dst);
- }
- return 0;
-}
-
-static int gmem_bind(struct file *file, struct kvm *kvm,
- struct kvm_memory_slot *slot, loff_t offset)
+static void gmem_release(struct file *file)
{
struct gmem_info *info = to_gmem_info(file);
- struct kvm *old_kvm = NULL;
- unsigned long start = offset >> PAGE_SHIFT;
-
- if (offset < 0 || !PAGE_ALIGNED(offset) ||
- start + slot->npages > info->npages)
- return -EINVAL;
/*
- * An mmap-capable backing hands the VMM a host mapping onto pages
- * that a hardware-encrypted VM (SEV, SEV-ES, SEV-SNP, TDX) will mark
- * private in the RMP/EPT; the mmap can only ever fault on those
- * pages. Refuse rather than hand the VMM a useless (and misleading)
- * shared view. SW_PROTECTED_VM has no hardware encryption and is
- * fine.
+ * Every exported dma-buf holds a reference on this file, so none can
+ * be alive here: no device and no guest_memfd still maps our frames.
*/
-#ifdef CONFIG_X86
- if (info->mmap_capable &&
- (kvm->arch.vm_type == KVM_X86_SEV_VM ||
- kvm->arch.vm_type == KVM_X86_SEV_ES_VM ||
- kvm->arch.vm_type == KVM_X86_SNP_VM ||
- kvm->arch.vm_type == KVM_X86_TDX_VM))
- return -EACCES;
-#endif
+ WARN_ON(!list_empty(&info->dmabufs));
- /* Record the binding so the revoke ioctl can translate offset -> gfn. */
- mutex_lock(&info->lock);
- info->base_gfn = slot->base_gfn;
- info->pgoff = start;
- info->bound_slot = slot;
- mutex_unlock(&info->lock);
-
-#if IS_ENABLED(CONFIG_AMD_MEM_ENCRYPT)
/*
- * Reset the RMP for the range to 4K shared so a (new) SEV-SNP VM can
- * transition it to private and re-encrypt it. PSMASH any 2M entries
- * first. Harmless on non-SNP hosts, where these return -ENODEV.
+ * A child returns its carved range to the root. Pages it still owned
+ * become free again; pages it had DONATEd stay parked at the root
+ * (still in gmem_root.donated) until RECLAIM or module exit; pages
+ * MOVEd out belong to another child and are not ours to free.
*/
- {
+ if (info->owned) {
unsigned long i;
- unsigned long first_pmd_pfn = ALIGN(info->base_pfn + start,
- PTRS_PER_PMD);
-
- for (i = first_pmd_pfn - (info->base_pfn + start);
- i + PTRS_PER_PMD <= slot->npages;
- i += PTRS_PER_PMD)
- psmash(info->base_pfn + start + i);
- for (i = 0; i < slot->npages; i++) {
- unsigned long pfn = info->base_pfn + start + i;
- int ret = rmp_make_shared(pfn, PG_LEVEL_4K);
-
- if (ret && ret != -ENODEV)
- pr_info_once("gmem_provider: rmp_make_shared(0x%lx) = %d\n",
- pfn, ret);
- }
+ mutex_lock(&gmem_root.lock);
+ list_del(&info->root_link);
+ for_each_set_bit(i, info->owned, info->npages)
+ __clear_bit(info->root_index + i, gmem_root.owned);
+ mutex_unlock(&gmem_root.lock);
+ kvfree(info->owned);
}
-#endif
-
- /*
- * Claim (or, on re-bind, transfer) VM ownership; pins the VM.
- * kvm_put_kvm(old_kvm) MUST run outside info->lock: if the put
- * drops the last ref, kvm_destroy_vm() runs inline and calls back
- * into our gmem_unbind() (via kvm_gmem_unbind() on each memslot),
- * which needs info->lock -- taking it here would self-deadlock.
- */
- mutex_lock(&info->lock);
- if (info->kvm != kvm) {
- old_kvm = info->kvm;
- kvm_get_kvm(kvm);
- info->kvm = kvm;
- }
- mutex_unlock(&info->lock);
- if (old_kvm)
- kvm_put_kvm(old_kvm);
-
- /*
- * Record the file on the slot (KVM's outer bind no longer does this
- * for us) and mark the slot gmem-only if this backing serves host
- * accesses through its own mmap.
- */
- WRITE_ONCE(slot->gmem.file, file);
- slot->gmem.pgoff = start;
- if (info->mmap_capable)
- slot->flags |= KVM_MEMSLOT_GMEM_ONLY;
-
- return 0;
+ if (info->cma_pages)
+ free_contig_range(info->base_pfn, info->npages);
+ kvfree(info->absent);
+ kvfree(info->readonly);
+ kfree(info);
+ module_put(THIS_MODULE);
}
-static void gmem_unbind(struct file *file, struct kvm *kvm,
- struct kvm_memory_slot *slot)
+static vm_fault_t gmem_vm_fault(struct vm_fault *vmf)
{
- struct gmem_info *info = to_gmem_info(file);
-
- mutex_lock(&info->lock);
- if (info->bound_slot == slot)
- info->bound_slot = NULL;
- mutex_unlock(&info->lock);
-
-#if IS_ENABLED(CONFIG_AMD_MEM_ENCRYPT)
- {
- unsigned long start = slot->gmem.pgoff;
- unsigned long first_pmd_pfn = ALIGN(info->base_pfn + start,
- PTRS_PER_PMD);
- unsigned long i;
-
- /*
- * Symmetric with gmem_bind(): PSMASH any 2M RMP entries first
- * so rmp_make_shared(PG_LEVEL_4K) can succeed on SNP hosts.
- * Harmless on non-SNP: psmash() returns -ENODEV.
- */
- for (i = first_pmd_pfn - (info->base_pfn + start);
- i + PTRS_PER_PMD <= slot->npages;
- i += PTRS_PER_PMD)
- psmash(info->base_pfn + start + i);
+ /* The VMA's file is ours or a dma-buf's; the child rides in vm_private_data. */
+ struct gmem_info *info = vmf->vma->vm_private_data;
+ unsigned long index = vmf->pgoff;
- for (i = 0; i < slot->npages; i++) {
- unsigned long pfn = info->base_pfn + start + i;
-
- rmp_make_shared(pfn, PG_LEVEL_4K);
- }
- }
-#endif
+ if (index >= info->npages || !gmem_page_present(info, index))
+ return VM_FAULT_SIGBUS;
+ return vmf_insert_pfn(vmf->vma, vmf->address, info->base_pfn + index);
}
-static void gmem_release(struct file *file);
-static int gmem_mmap(struct file *file, struct vm_area_struct *vma);
-static long gmem_fd_ioctl(struct file *file, unsigned int cmd, unsigned long arg);
-
-static const struct kvm_gmem_ops gmem_ops = {
- .bind = gmem_bind,
- .unbind = gmem_unbind,
- .get_pfn = gmem_get_pfn,
- .populate = gmem_populate,
- .release = gmem_release,
- .mmap = gmem_mmap,
- .ioctl = gmem_fd_ioctl,
+static const struct vm_operations_struct gmem_vm_ops = {
+ .fault = gmem_vm_fault,
};
-static void gmem_release(struct file *file)
-{
- struct gmem_info *info = to_gmem_info(file);
- struct kvm_memory_slot *slot;
-
- /*
- * If a memslot is still bound at close time, KVM has not yet had a
- * chance to call ops->unbind. Zap the guest mappings for the range
- * and clear slot->gmem.file so the eventual unbind is a no-op. This
- * matches native gmem's kvm_gmem_release() and prevents the guest
- * from continuing to hit backing memory after we free it below.
- */
- mutex_lock(&info->lock);
- slot = info->bound_slot;
- if (slot && info->kvm) {
- kvm_gmem_invalidate_range(info->kvm, slot->base_gfn,
- slot->base_gfn + slot->npages);
- WRITE_ONCE(slot->gmem.file, NULL);
- info->bound_slot = NULL;
- }
- mutex_unlock(&info->lock);
-
- if (info->kvm)
- kvm_put_kvm(info->kvm);
- if (info->cma_pages)
- free_contig_range(info->base_pfn, info->npages);
- kvfree(info->absent);
- kfree(info);
- module_put(THIS_MODULE);
-}
-
static int gmem_mmap(struct file *file, struct vm_area_struct *vma)
{
struct gmem_info *info = to_gmem_info(file);
unsigned long npages = vma_pages(vma);
- /*
- * gmem_bind() refuses to bind an mmap-capable fd to a coco VM. A fd
- * that was created without GMEM_PROVIDER_FLAG_MMAP_CAPABLE has no
- * such gate at bind time, so its mmap must not succeed at any point.
- */
+ /* A fd created without GMEM_PROVIDER_FLAG_MMAP_CAPABLE is never host-mappable. */
if (!info->mmap_capable)
return -EPERM;
if (vma->vm_pgoff + npages > info->npages)
return -EINVAL;
- /* Page-less backing: map raw PFNs, not folios. */
+ /*
+ * Page-less backing: raw PFNs, inserted on fault, never at mmap()
+ * time, so that a window torn down on a revoke comes back by itself
+ * once the page is the child's again.
+ */
vm_flags_set(vma, VM_PFNMAP | VM_IO | VM_DONTEXPAND | VM_DONTDUMP);
- return remap_pfn_range(vma, vma->vm_start, info->base_pfn + vma->vm_pgoff,
- npages << PAGE_SHIFT, vma->vm_page_prot);
+ vma->vm_private_data = info;
+ vma->vm_ops = &gmem_vm_ops;
+ return 0;
}
/*
- * Dynamic dma-buf exporter over the provider's backing.
- *
- * Follows the same shape as drivers/vfio/pci/vfio_pci_dmabuf.c: a per-dmabuf
- * priv holding a phys_vec, a revocable dynamic attach, and a "private
- * interconnect" symbol iommufd looks up to fetch phys directly (instead of
- * mapping through the DMA API).
+ * The dma-buf a child exports.
*
- * A revoke on the provider (SET_PRESENT present=0) fans out to every exported
- * dma-buf via dma_buf_invalidate_mappings(), so iommufd (which registered a
- * revocable importer) tears down the IOMMU mapping alongside KVM's NPT zap.
+ * A child may hand out several dma-bufs over its life (one per holder of the
+ * fd who asks); each covers the whole child and pins this file until it is
+ * released. Importers must accept ranged invalidation: every change to the
+ * child's pages is sent to them, and they re-read get_phys().
*/
struct gmem_dmabuf {
struct dma_buf *dmabuf;
struct gmem_info *info;
struct file *provider_file; /* holds info alive */
struct list_head list; /* info->dmabufs */
- struct phys_vec phys; /* single contiguous range */
- struct kref kref;
- struct completion comp;
- bool revoked;
};
static int gmem_dma_buf_attach(struct dma_buf *dmabuf,
struct dma_buf_attachment *attach)
{
- struct gmem_dmabuf *priv = dmabuf->priv;
-
- if (!attach->peer2peer)
- return -EOPNOTSUPP;
- if (priv->revoked)
- return -ENODEV;
- if (!dma_buf_attach_revocable(attach))
+ /* Only importers that can be told to re-read may attach. */
+ if (!attach->peer2peer || !dma_buf_attach_revocable(attach))
return -EOPNOTSUPP;
return 0;
}
-static void gmem_dma_buf_done(struct kref *kref)
-{
- struct gmem_dmabuf *priv = container_of(kref, struct gmem_dmabuf, kref);
-
- complete(&priv->comp);
-}
-
static struct sg_table *gmem_dma_buf_map(struct dma_buf_attachment *attach,
enum dma_data_direction dir)
{
- struct gmem_dmabuf *priv = attach->dmabuf->priv;
- struct sg_table *sgt;
-
- dma_resv_assert_held(priv->dmabuf->resv);
- if (priv->revoked)
- return ERR_PTR(-ENODEV);
-
- /* RAM, not P2P MMIO: no p2pdma_provider. */
- sgt = dma_buf_phys_vec_to_sgt(attach, NULL, &priv->phys, 1,
- priv->phys.len, dir);
- if (IS_ERR(sgt))
- return sgt;
-
- kref_get(&priv->kref);
- return sgt;
+ /* DMA-API importers are not served; use get_phys() (iommufd does). */
+ return ERR_PTR(-EOPNOTSUPP);
}
static void gmem_dma_buf_unmap(struct dma_buf_attachment *attach,
- struct sg_table *sgt,
- enum dma_data_direction dir)
+ struct sg_table *sgt, enum dma_data_direction dir)
{
- struct gmem_dmabuf *priv = attach->dmabuf->priv;
+}
+
+/* A host window through the dma-buf: the same rules as a window on the child. */
+static int gmem_dma_buf_mmap(struct dma_buf *dmabuf, struct vm_area_struct *vma)
+{
+ struct gmem_dmabuf *priv = dmabuf->priv;
- dma_resv_assert_held(priv->dmabuf->resv);
- dma_buf_free_sgt(attach, sgt, dir);
- kref_put(&priv->kref, gmem_dma_buf_done);
+ return gmem_mmap(priv->provider_file, vma);
}
static void gmem_dma_buf_release(struct dma_buf *dmabuf)
{
struct gmem_dmabuf *priv = dmabuf->priv;
+ struct gmem_info *info = priv->info;
- if (priv->info) {
- mutex_lock(&priv->info->dmabufs_lock);
- list_del_init(&priv->list);
- mutex_unlock(&priv->info->dmabufs_lock);
- }
- if (priv->provider_file)
- fput(priv->provider_file);
+ mutex_lock(&info->dmabufs_lock);
+ list_del(&priv->list);
+ mutex_unlock(&info->dmabufs_lock);
+ fput(priv->provider_file);
kfree(priv);
}
-/* Report this flat sample region through the generic dma-buf operation. */
+/* Return the first bit whose value differs from @index. */
+static unsigned long gmem_bitmap_next_change(const unsigned long *bitmap,
+ unsigned long nbits,
+ unsigned long index)
+{
+ if (!bitmap)
+ return nbits;
+ if (test_bit(index, bitmap))
+ return find_next_zero_bit(bitmap, nbits, index + 1);
+ return find_next_bit(bitmap, nbits, index + 1);
+}
+
+/* Find a run with one present/read-only disposition, without a per-bit scan. */
+static unsigned long gmem_disposition_end(struct gmem_info *info,
+ unsigned long index)
+{
+ bool present = gmem_page_present(info, index);
+ bool readonly = info->readonly && test_bit(index, info->readonly);
+ unsigned long pos = index;
+
+ for (;;) {
+ unsigned long next = info->npages;
+
+ next = min(next, gmem_bitmap_next_change(info->owned,
+ info->npages, pos));
+ next = min(next, gmem_bitmap_next_change(info->absent,
+ info->npages, pos));
+ next = min(next, gmem_bitmap_next_change(info->readonly,
+ info->npages, pos));
+ if (next >= info->npages ||
+ gmem_page_present(info, next) != present ||
+ (!!(info->readonly && test_bit(next, info->readonly))) != readonly)
+ return next;
+ pos = next;
+ }
+}
+
+/*
+ * Describe the run at @offset from the child's ownership bitmaps. A present
+ * page extends to the next page whose presence or read-only state differs,
+ * found with bitmap searches. An absent page is not backed, unless scratch
+ * mode substitutes the shared read-only scratch frame for that one page.
+ * Scratch mode covers only children of the root: SET_SCRATCH invalidates
+ * those, so a SETUP provider must never report the scratch frame.
+ */
static int gmem_dma_buf_get_phys(struct dma_buf_attachment *attach,
u64 offset, u64 len,
struct phys_vec *phys, u32 *attr)
{
struct gmem_dmabuf *priv = attach->dmabuf->priv;
+ struct gmem_info *info = priv->info;
+ unsigned long index = offset >> PAGE_SHIFT;
+ unsigned long end, run_end;
- dma_resv_assert_held(attach->dmabuf->resv);
- if (priv->revoked)
- return -ENODEV;
+ if (!PAGE_ALIGNED(offset) || !PAGE_ALIGNED(len))
+ return -EINVAL;
+ end = (offset + len) >> PAGE_SHIFT;
+
+ if (!gmem_page_present(info, index)) {
+ if (!info->owned || !READ_ONCE(gmem_root.scratch_enabled))
+ return -ENOENT;
+ phys->paddr = PFN_PHYS(gmem_scratch_pfn());
+ phys->len = PAGE_SIZE;
+ *attr = DMA_BUF_PHYS_ATTR_RAM | DMA_BUF_PHYS_ATTR_READONLY;
+ return 0;
+ }
- phys->paddr = priv->phys.paddr + offset;
- phys->len = len;
+ run_end = min(gmem_disposition_end(info, index), end);
+ phys->paddr = PFN_PHYS(info->base_pfn + index);
+ phys->len = (u64)(run_end - index) << PAGE_SHIFT;
*attr = DMA_BUF_PHYS_ATTR_RAM;
+ if (info->readonly && test_bit(index, info->readonly))
+ *attr |= DMA_BUF_PHYS_ATTR_READONLY;
return 0;
}
@@ -510,23 +362,9 @@ static const struct dma_buf_ops gmem_dma_buf_ops = {
.unmap_dma_buf = gmem_dma_buf_unmap,
.release = gmem_dma_buf_release,
.get_phys = gmem_dma_buf_get_phys,
+ .mmap = gmem_dma_buf_mmap,
};
-/* Called with info->dmabufs_lock held on the revoke path. */
-static void gmem_dma_buf_revoke_all(struct gmem_info *info)
-{
- struct gmem_dmabuf *priv;
-
- list_for_each_entry(priv, &info->dmabufs, list) {
- dma_resv_lock(priv->dmabuf->resv, NULL);
- if (!priv->revoked) {
- priv->revoked = true;
- dma_buf_invalidate_mappings(priv->dmabuf);
- }
- dma_resv_unlock(priv->dmabuf->resv);
- }
-}
-
static int gmem_provider_get_dmabuf(struct file *file)
{
struct gmem_info *info = to_gmem_info(file);
@@ -537,27 +375,19 @@ static int gmem_provider_get_dmabuf(struct file *file)
priv = kzalloc(sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
-
priv->info = info;
priv->provider_file = get_file(file);
- priv->phys.paddr = (u64)info->base_pfn << PAGE_SHIFT;
- priv->phys.len = (u64)info->npages << PAGE_SHIFT;
- kref_init(&priv->kref);
- init_completion(&priv->comp);
- INIT_LIST_HEAD(&priv->list);
-
exp_info.ops = &gmem_dma_buf_ops;
- exp_info.size = priv->phys.len;
+ exp_info.size = (u64)info->npages << PAGE_SHIFT;
exp_info.flags = O_RDWR;
exp_info.priv = priv;
-
priv->dmabuf = dma_buf_export(&exp_info);
if (IS_ERR(priv->dmabuf)) {
fd = PTR_ERR(priv->dmabuf);
+ fput(priv->provider_file);
kfree(priv);
return fd;
}
-
mutex_lock(&info->dmabufs_lock);
list_add(&priv->list, &info->dmabufs);
mutex_unlock(&info->dmabufs_lock);
@@ -568,14 +398,132 @@ static int gmem_provider_get_dmabuf(struct file *file)
return fd;
}
+/*
+ * Tell every importer of @info that [start, end) changed. The caller has
+ * already updated the bitmaps and dropped info->lock: an importer re-reads
+ * get_phys() from inside this.
+ */
+static void gmem_revoke_range(struct gmem_info *info,
+ unsigned long start, unsigned long end)
+{
+ struct gmem_dmabuf *priv;
+
+ lockdep_assert_not_held(&info->lock);
+ mutex_lock(&info->dmabufs_lock);
+ list_for_each_entry(priv, &info->dmabufs, list) {
+ dma_resv_lock(priv->dmabuf->resv, NULL);
+ dma_buf_invalidate_mappings_range(priv->dmabuf,
+ (u64)start << PAGE_SHIFT,
+ (u64)(end - start) << PAGE_SHIFT);
+ dma_resv_unlock(priv->dmabuf->resv);
+ }
+ mutex_unlock(&info->dmabufs_lock);
+}
+
+/*
+ * The child lost the frames behind [start, end): host windows over them go
+ * too. Not for a read-only flip or a scratch toggle, where the VMM may be
+ * the page's writer; the VMM re-maps after a grant.
+ */
+static void gmem_unmap_host(struct gmem_info *info, unsigned long start,
+ unsigned long end)
+{
+ loff_t off = (loff_t)start << PAGE_SHIFT, len = (loff_t)(end - start) << PAGE_SHIFT;
+ struct gmem_dmabuf *priv;
+
+ if (end <= start)
+ return;
+ unmap_mapping_range(info->mapping, off, len, 1);
+ /* Windows through a dma-buf live on the dma-buf's own file. */
+ mutex_lock(&info->dmabufs_lock);
+ list_for_each_entry(priv, &info->dmabufs, list)
+ unmap_mapping_range(priv->dmabuf->file->f_mapping, off, len, 1);
+ mutex_unlock(&info->dmabufs_lock);
+}
+
+/* Which allowlist bit gates each child-fd ioctl. 0 = not gated. */
+static u32 gmem_ioctl_allow_bit(unsigned int cmd)
+{
+ switch (cmd) {
+ case GMEM_PROVIDER_SET_PRESENT: return GMEM_ALLOW_SET_PRESENT;
+ case GMEM_PROVIDER_SET_READONLY: return GMEM_ALLOW_SET_READONLY;
+ case GMEM_PROVIDER_GET_DMABUF: return GMEM_ALLOW_GET_DMABUF;
+ case GMEM_PROVIDER_GET_STATS: return GMEM_ALLOW_GET_STATS;
+ default: return 0;
+ }
+}
+
+static long gmem_get_stats(struct gmem_info *info, void __user *uarg)
+{
+ struct gmem_provider_stats st = {};
+
+ mutex_lock(&info->lock);
+ st.region_offset = (u64)info->root_index << PAGE_SHIFT;
+ st.region_len = (u64)info->npages << PAGE_SHIFT;
+ st.owned_pages = info->owned ?
+ bitmap_weight(info->owned, info->npages) : info->npages;
+ st.absent_pages = info->absent ?
+ bitmap_weight(info->absent, info->npages) : 0;
+ st.readonly_pages = info->readonly ?
+ bitmap_weight(info->readonly, info->npages) : 0;
+ st.allow = info->allow;
+ mutex_unlock(&info->lock);
+
+ return copy_to_user(uarg, &st, sizeof(st)) ? -EFAULT : 0;
+}
+
static long gmem_fd_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct gmem_info *info = to_gmem_info(file);
struct gmem_provider_present p;
unsigned long start_index, end_index;
+ u32 need = gmem_ioctl_allow_bit(cmd);
+
+ /*
+ * The allowlist is fixed by the creator at NEW_CHILD and checked here
+ * before dispatch, so it also governs ioctls added later. A SETUP
+ * provider has no root and allows everything, as before.
+ */
+ if (need && !(info->allow & need))
+ return -EPERM;
if (cmd == GMEM_PROVIDER_GET_DMABUF)
return gmem_provider_get_dmabuf(file);
+ if (cmd == GMEM_PROVIDER_GET_STATS)
+ return gmem_get_stats(info, (void __user *)arg);
+
+ if (cmd == GMEM_PROVIDER_SET_READONLY) {
+ struct gmem_provider_readonly r;
+
+ if (copy_from_user(&r, (void __user *)arg, sizeof(r)))
+ return -EFAULT;
+ if (!r.len || !PAGE_ALIGNED(r.offset) || !PAGE_ALIGNED(r.len) ||
+ r.pad)
+ return -EINVAL;
+ start_index = r.offset >> PAGE_SHIFT;
+ end_index = start_index + (r.len >> PAGE_SHIFT);
+ if (end_index > info->npages || end_index < start_index)
+ return -EINVAL;
+
+ /*
+ * Flip the bits, then invalidate the range so importers drop
+ * their mappings and the next access re-reads get_phys() with
+ * the new permission. Making a range read-only must
+ * tear down writable mappings; making it writable again is
+ * also invalidated so a stale read-only mapping does not keep
+ * exiting.
+ */
+ mutex_lock(&info->lock);
+ if (r.readonly)
+ bitmap_set(info->readonly, start_index,
+ end_index - start_index);
+ else
+ bitmap_clear(info->readonly, start_index,
+ end_index - start_index);
+ mutex_unlock(&info->lock);
+ gmem_revoke_range(info, start_index, end_index);
+ return 0;
+ }
if (cmd != GMEM_PROVIDER_SET_PRESENT)
return -ENOTTY;
@@ -589,152 +537,495 @@ static long gmem_fd_ioctl(struct file *file, unsigned int cmd, unsigned long arg
if (end_index > info->npages || end_index < start_index)
return -EINVAL;
- if (p.present) {
- /* Restore: next guest fault calls get_pfn() and re-maps. */
- mutex_lock(&info->lock);
+ mutex_lock(&info->lock);
+ if (p.present)
bitmap_clear(info->absent, start_index, end_index - start_index);
- mutex_unlock(&info->lock);
- } else {
- unsigned long clamped_start;
-
- /* Revoke: mark absent, then zap the guest NPT/EPT for the range. */
- mutex_lock(&info->lock);
+ else
bitmap_set(info->absent, start_index, end_index - start_index);
+ /*
+ * Both directions invalidate: on revoke so the guest and devices stop
+ * using the pages, on restore so importers that were shown a hole or
+ * the scratch frame re-read and map the real frames again.
+ */
+ mutex_unlock(&info->lock);
+ gmem_revoke_range(info, start_index, end_index);
+ if (!p.present)
+ gmem_unmap_host(info, start_index, end_index);
+ return 0;
+}
- /*
- * Translate provider offset -> guest gfn. Skip any part of
- * the range that falls outside the currently bound slot; a
- * naive subtraction would underflow.
- */
- clamped_start = max_t(unsigned long, start_index, info->pgoff);
- if (info->kvm && end_index > clamped_start)
- kvm_gmem_invalidate_range(info->kvm,
- info->base_gfn + clamped_start - info->pgoff,
- info->base_gfn + end_index - info->pgoff);
- mutex_unlock(&info->lock);
-
- /*
- * Fan out to iommufd (and any other dma-buf importer): mark the
- * exported dma-buf(s) revoked and invalidate any active mappings.
- * The provider stays ignorant of scratch-page policy; that lives
- * in the importer.
- */
- mutex_lock(&info->dmabufs_lock);
- gmem_dma_buf_revoke_all(info);
- mutex_unlock(&info->dmabufs_lock);
- }
+static int gmem_fops_release(struct inode *inode, struct file *file)
+{
+ gmem_release(file);
return 0;
}
-static long gmem_ctl_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
+/* A child file owns its controls, host windows and dma-buf exports. */
+static const struct file_operations gmem_provider_fops = {
+ .owner = THIS_MODULE,
+ .release = gmem_fops_release,
+ .mmap = gmem_mmap,
+ .unlocked_ioctl = gmem_fd_ioctl,
+ .compat_ioctl = gmem_fd_ioctl,
+};
+
+/*
+ * Allocate a child over [base_pfn, +npages) and return its fd. Shared by
+ * SETUP (standalone region) and NEW_CHILD (a root sub-range). The fd owns the
+ * module pin on success.
+ */
+static int gmem_new_provider_fd(unsigned long base_pfn,
+ unsigned long npages, struct page *cma_pages,
+ u32 flags, unsigned long root_index,
+ unsigned long *owned, u32 allow,
+ struct gmem_info **out)
{
- struct gmem_provider_setup setup;
+ struct file *file;
struct gmem_info *info;
- struct file *kvm_file;
- struct page *pages = NULL;
- struct kvm *kvm;
- unsigned long npages;
int fd, ret;
- if (cmd != GMEM_PROVIDER_SETUP)
- return -ENOTTY;
+ info = kzalloc_obj(*info);
+ if (!info)
+ return -ENOMEM;
+ info->base_pfn = base_pfn;
+ info->npages = npages;
+ info->cma_pages = cma_pages;
+ info->root_index = root_index;
+ info->owned = owned;
+ info->allow = allow;
+ info->absent = kvzalloc_objs(unsigned long, BITS_TO_LONGS(npages));
+ info->readonly = kvzalloc_objs(unsigned long, BITS_TO_LONGS(npages));
+ if (!info->absent || !info->readonly) {
+ ret = -ENOMEM;
+ goto err_free;
+ }
+ INIT_LIST_HEAD(&info->dmabufs);
+ INIT_LIST_HEAD(&info->root_link);
+ mutex_init(&info->dmabufs_lock);
+ mutex_init(&info->lock);
- if (copy_from_user(&setup, (void __user *)arg, sizeof(setup)))
- return -EFAULT;
- if (setup.flags & ~GMEM_PROVIDER_FLAG_MMAP_CAPABLE)
- return -EINVAL;
+ /* Pin THIS_MODULE while the provider fd is alive (release drops it). */
+ if (!try_module_get(THIS_MODULE)) {
+ ret = -ENODEV;
+ goto err_free;
+ }
+ info->mmap_capable = !!(flags & GMEM_PROVIDER_FLAG_MMAP_CAPABLE);
- kvm_file = fget(setup.kvm_fd);
- if (!kvm_file)
- return -EBADF;
- if (!file_is_kvm(kvm_file)) {
- fput(kvm_file);
- return -EINVAL;
+ /*
+ * A private inode, not the shared anon one: host windows are torn
+ * down by file range on revoke, which must not touch other files.
+ */
+ fd = get_unused_fd_flags(O_CLOEXEC);
+ if (fd < 0) {
+ ret = fd;
+ module_put(THIS_MODULE);
+ goto err_free;
}
- kvm = kvm_file->private_data;
- if (!kvm) {
- fput(kvm_file);
- return -EINVAL;
+ file = anon_inode_create_getfile("[gmem-provider]", &gmem_provider_fops,
+ info, O_RDWR, NULL);
+ if (IS_ERR(file)) {
+ put_unused_fd(fd);
+ ret = PTR_ERR(file);
+ module_put(THIS_MODULE);
+ goto err_free;
}
- kvm_get_kvm(kvm);
- fput(kvm_file);
+ info->mapping = file->f_mapping;
+ if (owned)
+ list_add(&info->root_link, &gmem_root.children);
+ fd_install(fd, file);
+ if (out)
+ *out = info;
+ return fd;
- info = kzalloc(sizeof(*info), GFP_KERNEL);
- if (!info) {
- ret = -ENOMEM;
- goto err_put_kvm;
- }
+err_free:
+ kvfree(info->absent);
+ kvfree(info->readonly);
+ kfree(info);
+ return ret;
+}
+
+static long gmem_ctl_setup(void __user *uarg)
+{
+ struct gmem_provider_setup setup;
+ struct page *pages = NULL;
+ unsigned long base_pfn, npages;
+ int fd;
+
+ if (copy_from_user(&setup, uarg, sizeof(setup)))
+ return -EFAULT;
+ if (setup.flags & ~GMEM_PROVIDER_FLAG_MMAP_CAPABLE)
+ return -EINVAL;
+ /* kvm_fd is legacy and ignored: binding happens in KVM_CREATE_GUEST_MEMFD. */
if (addr && len) {
/* External page-less range from module params. */
- info->base_pfn = addr >> PAGE_SHIFT;
- info->npages = len >> PAGE_SHIFT;
+ base_pfn = addr >> PAGE_SHIFT;
+ npages = len >> PAGE_SHIFT;
} else {
/* CMA fallback: allocate a contiguous, page-backed region. */
- if (!setup.size || !PAGE_ALIGNED(setup.size)) {
- ret = -EINVAL;
- goto err_free_info;
- }
+ if (!setup.size || !PAGE_ALIGNED(setup.size))
+ return -EINVAL;
npages = setup.size >> PAGE_SHIFT;
pages = alloc_contig_pages(npages, GFP_KERNEL, numa_node_id(), NULL);
- if (!pages) {
- ret = -ENOMEM;
- goto err_free_info;
- }
+ if (!pages)
+ return -ENOMEM;
/* Provider path skips KVM's folio-clear; zero to avoid data leak. */
memset(page_to_virt(pages), 0, (size_t)npages << PAGE_SHIFT);
- info->base_pfn = page_to_pfn(pages);
- info->npages = npages;
- info->cma_pages = pages;
+ base_pfn = page_to_pfn(pages);
}
- info->absent = kvzalloc(BITS_TO_LONGS(info->npages) * sizeof(unsigned long),
- GFP_KERNEL);
- if (!info->absent) {
- ret = -ENOMEM;
- goto err_free_pages;
+ fd = gmem_new_provider_fd(base_pfn, npages, pages, setup.flags, 0,
+ NULL, GMEM_ALLOW_ALL, NULL);
+ if (fd < 0 && pages)
+ free_contig_range(page_to_pfn(pages), npages);
+ return fd;
+}
+
+/*
+ * Lazily create the root region on first NEW_CHILD. Uses the module params
+ * if given (page-less), else a CMA region of @size bytes. Idempotent once
+ * created; a later different @size is ignored.
+ */
+static int gmem_root_ensure(u64 size)
+{
+ unsigned long npages;
+ struct page *pages = NULL;
+
+ lockdep_assert_held(&gmem_root.lock);
+ if (gmem_root.npages)
+ return 0;
+
+ if (addr && len) {
+ gmem_root.base_pfn = addr >> PAGE_SHIFT;
+ npages = len >> PAGE_SHIFT;
+ } else {
+ if (!size || !PAGE_ALIGNED(size))
+ return -EINVAL;
+ npages = size >> PAGE_SHIFT;
+ pages = alloc_contig_pages(npages, GFP_KERNEL, numa_node_id(), NULL);
+ if (!pages)
+ return -ENOMEM;
+ memset(page_to_virt(pages), 0, (size_t)npages << PAGE_SHIFT);
+ gmem_root.base_pfn = page_to_pfn(pages);
}
+ gmem_root.owned = kvzalloc_objs(unsigned long, BITS_TO_LONGS(npages));
+ gmem_root.donated = kvzalloc_objs(unsigned long, BITS_TO_LONGS(npages));
+ if (!gmem_root.owned || !gmem_root.donated) {
+ kvfree(gmem_root.owned);
+ kvfree(gmem_root.donated);
+ gmem_root.owned = NULL;
+ gmem_root.donated = NULL;
+ if (pages)
+ free_contig_range(page_to_pfn(pages), npages);
+ return -ENOMEM;
+ }
+ gmem_root.cma_pages = pages;
+ gmem_root.npages = npages;
+ return 0;
+}
- INIT_LIST_HEAD(&info->dmabufs);
- mutex_init(&info->dmabufs_lock);
- mutex_init(&info->lock);
+static void gmem_root_teardown(void)
+{
+ if (!gmem_root.npages)
+ return;
+ WARN_ON(!list_empty(&gmem_root.children));
+ if (gmem_root.cma_pages)
+ free_contig_range(gmem_root.base_pfn, gmem_root.npages);
+ kvfree(gmem_root.owned);
+ kvfree(gmem_root.donated);
+ memset(&gmem_root, 0, sizeof(gmem_root));
+ mutex_init(&gmem_root.lock);
+ INIT_LIST_HEAD(&gmem_root.children);
+}
+
+/* Validate a page-aligned [offset, +len) against the root; return page bounds. */
+static int gmem_root_range(u64 offset, u64 length,
+ unsigned long *first, unsigned long *last)
+{
+ if (!length || !PAGE_ALIGNED(offset) || !PAGE_ALIGNED(length))
+ return -EINVAL;
+ *first = offset >> PAGE_SHIFT;
+ *last = *first + (length >> PAGE_SHIFT); /* exclusive */
+ if (*last <= *first || *last > gmem_root.npages)
+ return -EINVAL;
+ return 0;
+}
+
+/* Resolve a child fd created by NEW_CHILD; returns a referenced file. */
+static struct file *gmem_get_child(int fd, struct gmem_info **infop)
+{
+ struct file *f = fget(fd);
+ struct gmem_info *info;
+
+ if (!f)
+ return ERR_PTR(-EBADF);
+ if (f->f_op != &gmem_provider_fops) {
+ fput(f);
+ return ERR_PTR(-EINVAL);
+ }
+ info = to_gmem_info(f);
+ if (!info->owned) {
+ fput(f);
+ return ERR_PTR(-EINVAL);
+ }
+ *infop = info;
+ return f;
+}
+static long gmem_ctl_new_child(void __user *uarg)
+{
+ struct gmem_provider_new_child nc;
+ struct gmem_info *info;
+ unsigned long first, last;
+ unsigned long *owned;
+ int fd, ret;
+
+ if (copy_from_user(&nc, uarg, sizeof(nc)))
+ return -EFAULT;
+ if ((nc.flags & ~GMEM_PROVIDER_FLAG_MMAP_CAPABLE) || nc.pad ||
+ (nc.allow & ~GMEM_ALLOW_ALL))
+ return -EINVAL;
+
+ /* nc.kvm_fd is legacy and ignored: binding happens in KVM_CREATE_GUEST_MEMFD. */
+
+ mutex_lock(&gmem_root.lock);
+ ret = gmem_root_ensure(nc.offset + nc.len);
+ if (ret)
+ goto out_unlock;
+ ret = gmem_root_range(nc.offset, nc.len, &first, &last);
+ if (ret)
+ goto out_unlock;
/*
- * Pin THIS_MODULE while the provider fd is alive. The fd is created
- * with kvm_gmem_fops (owned by kvm.ko), which does not pin us, so
- * rmmod of gmem_provider is otherwise free to run behind our ops.
+ * A carve is the range this child may ever hold; it is the VM's whole
+ * view of memory and becomes its memslot. Carves may overlap: that is
+ * how a range can later MOVE from one VM to another. Ownership is
+ * per page and exclusive. The new child is granted every page of its
+ * carve that no other child owns and the root has not parked; the
+ * rest it can only receive by MOVE or RECLAIM. A carve with nothing
+ * to grant is refused as a likely mistake.
*/
- if (!try_module_get(THIS_MODULE)) {
- ret = -ENODEV;
- goto err_free_pages;
+ if (find_next_zero_bit(gmem_root.owned, last, first) >= last) {
+ ret = -EBUSY;
+ goto out_unlock;
}
- info->backing.ops = &gmem_ops;
- info->kvm = kvm;
- info->mmap_capable = !!(setup.flags & GMEM_PROVIDER_FLAG_MMAP_CAPABLE);
+ /*
+ * Allocate the ownership bitmap before the fd exists, so a failure
+ * here has nothing to unwind. It is handed to the new info below.
+ */
+ owned = kvzalloc_objs(unsigned long, BITS_TO_LONGS(last - first));
+ if (!owned) {
+ ret = -ENOMEM;
+ goto out_unlock;
+ }
+ /* Establish the grant before publishing the fd. */
+ {
+ unsigned long i;
- fd = anon_inode_getfd("[gmem-provider]", &kvm_gmem_fops,
- &info->backing, O_RDWR | O_CLOEXEC);
+ for (i = first; i < last; i++) {
+ if (test_bit(i, gmem_root.owned) ||
+ test_bit(i, gmem_root.donated))
+ continue;
+ __set_bit(i - first, owned);
+ __set_bit(i, gmem_root.owned);
+ }
+ }
+ fd = gmem_new_provider_fd(gmem_root.base_pfn + first,
+ last - first, NULL, nc.flags, first,
+ owned, nc.allow, &info);
if (fd < 0) {
+ unsigned long i;
+
+ for_each_set_bit(i, owned, last - first)
+ __clear_bit(first + i, gmem_root.owned);
+ kvfree(owned);
ret = fd;
- goto err_module_put;
+ goto out_unlock;
}
+ mutex_unlock(&gmem_root.lock);
return fd;
-err_module_put:
- module_put(THIS_MODULE);
+out_unlock:
+ mutex_unlock(&gmem_root.lock);
+ return ret;
+}
-err_free_pages:
- if (pages)
- free_contig_range(page_to_pfn(pages), npages);
-err_free_info:
- kvfree(info->absent);
- kfree(info);
-err_put_kvm:
- kvm_put_kvm(kvm);
+/*
+ * Take [first, last) away from @info: clear ownership, then revoke from both
+ * of its importers. Ownership is cleared before the revoke so a racing fault
+ * that slips in re-reads "not owned" and gets the scratch frame or fails.
+ * Caller holds gmem_root.lock; we take info->lock inside it.
+ */
+static void gmem_child_lose(struct gmem_info *info,
+ unsigned long first, unsigned long last)
+{
+ unsigned long s = first - info->root_index, e = last - info->root_index;
+
+ mutex_lock(&info->lock);
+ bitmap_clear(info->owned, s, e - s);
+ mutex_unlock(&info->lock);
+ gmem_revoke_range(info, s, e);
+ gmem_unmap_host(info, s, e);
+ bitmap_clear(gmem_root.owned, first, last - first);
+}
+
+/* Give [first, last) to @info and invalidate so its importers pick it up. */
+static void gmem_child_gain(struct gmem_info *info,
+ unsigned long first, unsigned long last)
+{
+ unsigned long s = first - info->root_index, e = last - info->root_index;
+
+ mutex_lock(&info->lock);
+ bitmap_set(info->owned, s, e - s);
+ mutex_unlock(&info->lock);
+ gmem_revoke_range(info, s, e);
+ bitmap_set(gmem_root.owned, first, last - first);
+}
+
+/* Does child @info's carved range contain [first, last)? */
+static bool gmem_child_covers(struct gmem_info *info,
+ unsigned long first, unsigned long last)
+{
+ return first >= info->root_index &&
+ last <= info->root_index + info->npages;
+}
+
+/* Does child @info currently own every page of [first, last)? */
+static bool gmem_child_owns(struct gmem_info *info,
+ unsigned long first, unsigned long last)
+{
+ unsigned long s = first - info->root_index, e = last - info->root_index;
+
+ return gmem_child_covers(info, first, last) &&
+ find_next_zero_bit(info->owned, e, s) >= e;
+}
+
+static long gmem_ctl_move(void __user *uarg)
+{
+ struct gmem_provider_move mv;
+ struct gmem_info *src, *dst;
+ struct file *sf, *df;
+ unsigned long first, last;
+ long ret;
+
+ if (copy_from_user(&mv, uarg, sizeof(mv)))
+ return -EFAULT;
+ sf = gmem_get_child(mv.src_fd, &src);
+ if (IS_ERR(sf))
+ return PTR_ERR(sf);
+ df = gmem_get_child(mv.dst_fd, &dst);
+ if (IS_ERR(df)) {
+ fput(sf);
+ return PTR_ERR(df);
+ }
+
+ mutex_lock(&gmem_root.lock);
+ ret = gmem_root_range(mv.offset, mv.len, &first, &last);
+ if (ret)
+ goto out;
+ if (src == dst || !gmem_child_owns(src, first, last) ||
+ !gmem_child_covers(dst, first, last)) {
+ ret = -EINVAL;
+ goto out;
+ }
+ /*
+ * Revoke on the source first, grant on the destination second. No two
+ * importers hold the range at once. Both children's info->lock are
+ * taken in turn under gmem_root.lock, never nested with each other.
+ */
+ gmem_child_lose(src, first, last);
+ gmem_child_gain(dst, first, last);
+ ret = 0;
+out:
+ mutex_unlock(&gmem_root.lock);
+ fput(df);
+ fput(sf);
+ return ret;
+}
+
+static long gmem_ctl_donate(void __user *uarg, bool reclaim)
+{
+ struct gmem_provider_donate d;
+ struct gmem_info *info;
+ struct file *f;
+ unsigned long first, last;
+ long ret;
+
+ if (copy_from_user(&d, uarg, sizeof(d)))
+ return -EFAULT;
+ if (d.pad)
+ return -EINVAL;
+ f = gmem_get_child(d.fd, &info);
+ if (IS_ERR(f))
+ return PTR_ERR(f);
+
+ mutex_lock(&gmem_root.lock);
+ ret = gmem_root_range(d.offset, d.len, &first, &last);
+ if (ret)
+ goto out;
+ if (!reclaim) {
+ /* DONATE: the child must own it; park it at the root. */
+ if (!gmem_child_owns(info, first, last)) {
+ ret = -EINVAL;
+ goto out;
+ }
+ gmem_child_lose(info, first, last);
+ bitmap_set(gmem_root.donated, first, last - first);
+ } else {
+ /* RECLAIM: must be donated and inside this child's range. */
+ if (!gmem_child_covers(info, first, last) ||
+ find_next_zero_bit(gmem_root.donated, last, first) < last) {
+ ret = -EINVAL;
+ goto out;
+ }
+ bitmap_clear(gmem_root.donated, first, last - first);
+ gmem_child_gain(info, first, last);
+ }
+ ret = 0;
+out:
+ mutex_unlock(&gmem_root.lock);
+ fput(f);
return ret;
}
+static long gmem_ctl_set_scratch(void __user *uarg)
+{
+ struct gmem_provider_scratch sc;
+ struct gmem_info *info;
+
+ if (copy_from_user(&sc, uarg, sizeof(sc)))
+ return -EFAULT;
+ if (sc.pad || sc.enable > 1)
+ return -EINVAL;
+
+ /*
+ * Flipping the mode changes what every revoked page reports, so every
+ * child is re-invalidated in full: cheap for a PoC, and it guarantees
+ * no importer keeps a stale hole or a stale scratch mapping.
+ */
+ mutex_lock(&gmem_root.lock);
+ WRITE_ONCE(gmem_root.scratch_enabled, !!sc.enable);
+ list_for_each_entry(info, &gmem_root.children, root_link)
+ gmem_revoke_range(info, 0, info->npages);
+ mutex_unlock(&gmem_root.lock);
+ return 0;
+}
+
+static long gmem_ctl_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
+{
+ void __user *uarg = (void __user *)arg;
+
+ switch (cmd) {
+ case GMEM_PROVIDER_SETUP: return gmem_ctl_setup(uarg);
+ case GMEM_PROVIDER_NEW_CHILD: return gmem_ctl_new_child(uarg);
+ case GMEM_PROVIDER_MOVE: return gmem_ctl_move(uarg);
+ case GMEM_PROVIDER_DONATE: return gmem_ctl_donate(uarg, false);
+ case GMEM_PROVIDER_RECLAIM: return gmem_ctl_donate(uarg, true);
+ case GMEM_PROVIDER_SET_SCRATCH: return gmem_ctl_set_scratch(uarg);
+ default: return -ENOTTY;
+ }
+}
+
static const struct file_operations gmem_ctl_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = gmem_ctl_ioctl,
@@ -749,20 +1040,34 @@ static struct miscdevice gmem_dev = {
static int __init gmem_provider_init(void)
{
+ int ret;
+
if ((addr || len) &&
(!addr || !len || !PAGE_ALIGNED(addr) || !PAGE_ALIGNED(len))) {
pr_err("gmem_provider: addr= and len= must both be set and page aligned\n");
return -EINVAL;
}
- return misc_register(&gmem_dev);
+ gmem_scratch_page = alloc_page(GFP_KERNEL | __GFP_ZERO);
+ if (!gmem_scratch_page)
+ return -ENOMEM;
+ mutex_init(&gmem_root.lock);
+ INIT_LIST_HEAD(&gmem_root.children);
+
+ ret = misc_register(&gmem_dev);
+ if (ret)
+ __free_page(gmem_scratch_page);
+ return ret;
}
module_init(gmem_provider_init);
static void __exit gmem_provider_exit(void)
{
misc_deregister(&gmem_dev);
+ gmem_root_teardown();
+ __free_page(gmem_scratch_page);
}
module_exit(gmem_provider_exit);
MODULE_LICENSE("GPL");
+MODULE_IMPORT_NS("DMA_BUF");
MODULE_DESCRIPTION("Sample guest_memfd provider (external page-less range or CMA fallback)");
diff --git a/samples/kvm/gmem_provider.h b/samples/kvm/gmem_provider.h
index 45f1b8257f60..1c8f5cacd2be 100644
--- a/samples/kvm/gmem_provider.h
+++ b/samples/kvm/gmem_provider.h
@@ -6,19 +6,18 @@
#include <linux/types.h>
/*
- * ioctl on /dev/gmem_provider: create a guest_memfd provider fd and return it
- * for use with KVM_SET_USER_MEMORY_REGION2.
+ * ioctl on /dev/gmem_provider: create a memory-owner fd and return it. The
+ * holder may mmap it and request a dma-buf for KVM or iommufd.
*
* If the module was loaded with addr=/len=, the fd is backed by that fixed
* page-less physical range and @size is ignored. Otherwise the fd is backed
* by a @size-byte physically contiguous region from alloc_contig_pages().
*/
/* Flags for struct gmem_provider_setup.flags */
-#define GMEM_PROVIDER_FLAG_MMAP_CAPABLE (1u << 0) /* fd is mmap()-able; slot becomes gmem-only.
- Refused for coco VMs (SEV-SNP/TDX). */
+#define GMEM_PROVIDER_FLAG_MMAP_CAPABLE (1u << 0) /* fd and its dma-bufs are mmap()-able */
struct gmem_provider_setup {
- __s32 kvm_fd; /* an open KVM VM fd */
+ __s32 kvm_fd; /* ignored */
__u32 flags; /* GMEM_PROVIDER_FLAG_* */
__u64 size; /* CMA fallback size in bytes, page aligned */
};
@@ -27,10 +26,9 @@ struct gmem_provider_setup {
#define GMEM_PROVIDER_SETUP _IOW(GMEM_PROVIDER_IOCTL_BASE, 1, struct gmem_provider_setup)
/*
- * ioctl on a provider fd (returned by SETUP): flip a byte range of the backing
- * between present and absent. Revoking (present=0) marks the range absent and
- * zaps the guest's NPT/EPT so the next access re-faults; get_pfn() then refuses
- * the range until it is restored (present=1). Models overcommit page reclaim.
+ * ioctl on an owner fd: flip a byte range between present and absent.
+ * Revoking a range invalidates every exported dma-buf, so importers discard
+ * their old mappings and observe a hole until the range is restored.
*/
struct gmem_provider_present {
__u64 offset; /* byte offset into the provider region, page aligned */
@@ -42,12 +40,134 @@ struct gmem_provider_present {
#define GMEM_PROVIDER_SET_PRESENT _IOW(GMEM_PROVIDER_IOCTL_BASE, 2, struct gmem_provider_present)
/*
- * ioctl on a provider fd (returned by SETUP): export the backing region as a
- * dynamic dma-buf and return an fd for it, suitable for
+ * ioctl on a provider fd: make a byte range read-only for the guest, or
+ * writable again. KVM maps a read-only page without write permission and a
+ * guest write to it exits to userspace with KVM_EXIT_MEMORY_FAULT. Existing
+ * mappings of the range are dropped so the change takes effect on the next
+ * access.
+ */
+struct gmem_provider_readonly {
+ __u64 offset; /* byte offset into the provider region, page aligned */
+ __u64 len; /* byte length, page aligned */
+ __u32 readonly; /* 1 = guest may not write, 0 = guest may write */
+ __u32 pad;
+};
+
+#define GMEM_PROVIDER_SET_READONLY \
+ _IOW(GMEM_PROVIDER_IOCTL_BASE, 4, struct gmem_provider_readonly)
+
+/*
+ * ioctl on an owner fd: export its memory as a dynamic dma-buf and return an
+ * fd suitable for
* IOMMU_IOAS_MAP_FILE. Revoking the region (SET_PRESENT present=0) fans out
- * to the exported dma-buf via dma_buf_invalidate_mappings(), causing iommufd
+ * to the exported dma-buf via dma_buf_invalidate_mappings_range(), causing importers
* to tear down the IOMMU mapping so DMA to the reclaimed range faults.
*/
#define GMEM_PROVIDER_GET_DMABUF _IO(GMEM_PROVIDER_IOCTL_BASE, 3)
+/*
+ * ---- Toy descriptor tree (a memory owner as a provider) -------------------
+ *
+ * The control device owns one backing region (the "root"). NEW_CHILD carves a
+ * sub-range of it into a new provider fd bound to one VM. A child's pages can
+ * be MOVEd to another child, DONATEd to the root (absent from every consumer)
+ * and RECLAIMed. Every change revokes the range from KVM and from the
+ * child's exported dma-bufs.
+ *
+ * Every child fd carries an ioctl allowlist, fixed at NEW_CHILD by the
+ * creator. Ioctls not in the list fail with -EPERM.
+ */
+
+/* Allowlist bits for struct gmem_provider_new_child.allow */
+#define GMEM_ALLOW_SET_PRESENT (1u << 0)
+#define GMEM_ALLOW_SET_READONLY (1u << 1)
+#define GMEM_ALLOW_GET_DMABUF (1u << 2)
+#define GMEM_ALLOW_GET_STATS (1u << 3)
+#define GMEM_ALLOW_ALL (GMEM_ALLOW_SET_PRESENT | \
+ GMEM_ALLOW_SET_READONLY | \
+ GMEM_ALLOW_GET_DMABUF | \
+ GMEM_ALLOW_GET_STATS)
+
+/*
+ * ioctl on /dev/gmem_provider: carve @len bytes at @offset of the root into a
+ * new child fd. The range must be available. Without addr=/len=, the first
+ * child fixes the CMA root size; create and close a sizing child first if
+ * later children extend beyond it. @allow becomes the child's ioctl allowlist.
+ */
+struct gmem_provider_new_child {
+ __s32 kvm_fd; /* ignored */
+ __u32 flags; /* GMEM_PROVIDER_FLAG_* */
+ __u64 offset; /* byte offset into the root region, page aligned */
+ __u64 len; /* byte length, page aligned */
+ __u32 allow; /* GMEM_ALLOW_* */
+ __u32 pad;
+};
+
+#define GMEM_PROVIDER_NEW_CHILD _IOW(GMEM_PROVIDER_IOCTL_BASE, 5, struct gmem_provider_new_child)
+
+/*
+ * ioctl on /dev/gmem_provider: move [@offset, @offset+@len) of the root region
+ * from child @src_fd to child @dst_fd. The range must currently be owned by
+ * @src_fd and must fall inside @dst_fd's carved range. The source is
+ * invalidated first, then the destination is granted, so no two children own
+ * the range at once.
+ */
+struct gmem_provider_move {
+ __s32 src_fd;
+ __s32 dst_fd;
+ __u64 offset; /* byte offset into the root region, page aligned */
+ __u64 len; /* byte length, page aligned */
+};
+
+#define GMEM_PROVIDER_MOVE _IOW(GMEM_PROVIDER_IOCTL_BASE, 6, struct gmem_provider_move)
+
+/*
+ * ioctl on /dev/gmem_provider: DONATE revokes [@offset, +@len) from the child
+ * that owns it and parks it at the root; RECLAIM returns a donated range to
+ * @fd, which must be the child whose carved range contains it. The toy does
+ * no scrub and no hotplug; it models only the ownership state.
+ */
+struct gmem_provider_donate {
+ __s32 fd; /* DONATE: owning child (checked); RECLAIM: recipient */
+ __u32 pad;
+ __u64 offset; /* byte offset into the root region, page aligned */
+ __u64 len; /* byte length, page aligned */
+};
+
+#define GMEM_PROVIDER_DONATE _IOW(GMEM_PROVIDER_IOCTL_BASE, 7, struct gmem_provider_donate)
+#define GMEM_PROVIDER_RECLAIM _IOW(GMEM_PROVIDER_IOCTL_BASE, 8, struct gmem_provider_donate)
+
+/*
+ * ioctl on /dev/gmem_provider: select what a revoked page reports to its
+ * consumers. With @enable=0 (default) a revoked page is absent: get_phys()
+ * reports it as not backed, so a guest access exits and a device DMA faults. With
+ * @enable=1 a revoked page reports the module's scratch frame, read-only, on
+ * both paths, so a device that cannot tolerate a fault lands on a harmless
+ * page. Scratch mode applies to children created by NEW_CHILD only; a
+ * SETUP provider always reports a revoked page as absent. Changing the mode
+ * re-invalidates every child so consumers re-read.
+ */
+struct gmem_provider_scratch {
+ __u32 enable;
+ __u32 pad;
+};
+
+#define GMEM_PROVIDER_SET_SCRATCH \
+ _IOW(GMEM_PROVIDER_IOCTL_BASE, 9, struct gmem_provider_scratch)
+
+/*
+ * ioctl on a child fd: read back the child's ownership state, for tests.
+ */
+struct gmem_provider_stats {
+ __u64 region_offset; /* child's carved range within the root */
+ __u64 region_len;
+ __u64 owned_pages; /* pages currently granted to this child */
+ __u64 absent_pages; /* pages revoked (moved out, donated, or SET_PRESENT 0) */
+ __u64 readonly_pages;
+ __u32 allow;
+ __u32 pad;
+};
+
+#define GMEM_PROVIDER_GET_STATS _IOR(GMEM_PROVIDER_IOCTL_BASE, 10, struct gmem_provider_stats)
+
#endif /* _SAMPLES_KVM_GMEM_PROVIDER_H */
diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm
index 12004a487c32..6accbc56ae69 100644
--- a/tools/testing/selftests/kvm/Makefile.kvm
+++ b/tools/testing/selftests/kvm/Makefile.kvm
@@ -78,11 +78,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_readonly_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_poc_test
TEST_GEN_PROGS_x86 += x86/hwcr_msr_test
TEST_GEN_PROGS_x86 += x86/hyperv_clock
TEST_GEN_PROGS_x86 += x86/hyperv_cpuid
diff --git a/tools/testing/selftests/kvm/gmem_provider_nvme_dma_test.c b/tools/testing/selftests/kvm/gmem_provider_nvme_dma_test.c
index 665b19028e13..b5ef6b9b1385 100644
--- a/tools/testing/selftests/kvm/gmem_provider_nvme_dma_test.c
+++ b/tools/testing/selftests/kvm/gmem_provider_nvme_dma_test.c
@@ -35,8 +35,8 @@ struct gmem_provider_setup {
__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 GMEM_PROVIDER_FLAG_MMAP_CAPABLE (1u << 0)
struct gmem_provider_present {
__u64 offset;
@@ -94,7 +94,7 @@ int main(void)
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;
+ int gmem_ctl, gmem_fd, dmabuf_fd, prov_fd, vm_fd = -1, iommufd_fd, vfio_fd;
void *provider_hva, *sq_buf, *cq_buf, *bar;
uint16_t pci_cmd;
uint16_t expected_vid;
@@ -120,22 +120,34 @@ int main(void)
vm = ioctl(kvm, KVM_CREATE_VM, 0);
TEST_ASSERT(vm >= 0, "KVM_CREATE_VM errno=%d", errno);
setup.kvm_fd = vm;
+ vm_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);
+ prov_fd = ioctl(gmem_ctl, GMEM_PROVIDER_SETUP, &setup);
+ TEST_ASSERT(prov_fd >= 0, "SETUP errno=%d", errno);
+
+ /* Export once; KVM and iommufd import the same dma-buf. */
+ dmabuf_fd = ioctl(prov_fd, GMEM_PROVIDER_GET_DMABUF);
+ TEST_ASSERT(dmabuf_fd >= 0, "GET_DMABUF errno=%d", errno);
+ {
+ struct kvm_create_guest_memfd cgm = {
+ .size = PROVIDER_SIZE,
+ .flags = GUEST_MEMFD_FLAG_MMAP | GUEST_MEMFD_FLAG_USE_DMABUF,
+ .dmabuf_fd = dmabuf_fd,
+ };
+ gmem_fd = ioctl(vm_fd, KVM_CREATE_GUEST_MEMFD, &cgm);
+ TEST_ASSERT(gmem_fd >= 0, "KVM_CREATE_GUEST_MEMFD(dmabuf) 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. */
+ /* IOAS + the same dma-buf KVM imported. */
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;
@@ -367,10 +379,10 @@ int main(void)
munmap(bar, reg.size);
close(vfio_fd);
- close(dmabuf_fd);
close(iommufd_fd);
munmap(provider_hva, PROVIDER_SIZE);
close(gmem_fd);
+ close(dmabuf_fd);
close(gmem_ctl);
return 0;
}
diff --git a/tools/testing/selftests/kvm/include/kvm_util.h b/tools/testing/selftests/kvm/include/kvm_util.h
index 04a910164a29..c168c0880ebf 100644
--- a/tools/testing/selftests/kvm/include/kvm_util.h
+++ b/tools/testing/selftests/kvm/include/kvm_util.h
@@ -664,17 +664,38 @@ static inline bool is_smt_on(void)
void vm_create_irqchip(struct kvm_vm *vm);
-static inline int __vm_create_guest_memfd(struct kvm_vm *vm, u64 size,
- u64 flags)
+static inline int __vm_create_guest_memfd_dmabuf(struct kvm_vm *vm,
+ u64 size, u64 flags,
+ int dmabuf_fd)
{
struct kvm_create_guest_memfd guest_memfd = {
.size = size,
.flags = flags,
+ .dmabuf_fd = dmabuf_fd,
};
return __vm_ioctl(vm, KVM_CREATE_GUEST_MEMFD, &guest_memfd);
}
+static inline int __vm_create_guest_memfd(struct kvm_vm *vm, u64 size,
+ u64 flags)
+{
+ return __vm_create_guest_memfd_dmabuf(vm, size, flags, 0);
+}
+
+/* A guest_memfd that imports the dma-buf @dmabuf_fd for its memory. */
+static inline int vm_create_guest_memfd_dmabuf(struct kvm_vm *vm,
+ u64 size, u64 flags,
+ int dmabuf_fd)
+{
+ int fd = __vm_create_guest_memfd_dmabuf(vm, size,
+ flags | GUEST_MEMFD_FLAG_USE_DMABUF,
+ dmabuf_fd);
+
+ TEST_ASSERT(fd >= 0, KVM_IOCTL_ERROR(KVM_CREATE_GUEST_MEMFD, fd));
+ return fd;
+}
+
static inline int vm_create_guest_memfd(struct kvm_vm *vm, u64 size,
u64 flags)
{
diff --git a/tools/testing/selftests/kvm/x86/gmem_poc_test.c b/tools/testing/selftests/kvm/x86/gmem_poc_test.c
new file mode 100644
index 000000000000..bdb64fe949ad
--- /dev/null
+++ b/tools/testing/selftests/kvm/x86/gmem_poc_test.c
@@ -0,0 +1,824 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * gmem_poc_test - one provider, a control process and two VMMs.
+ *
+ * Two roles in one process, kept apart on purpose:
+ *
+ * ctl: owns /dev/gmem_provider (the root). Creates a child per VM with a
+ * narrowed allowlist, MOVEs a range between children, DONATEs and
+ * RECLAIMs, flips scratch mode. Never touches a VM.
+ * vmm: owns one child fd, one KVM VM and one iommufd IOAS. Creates the
+ * VM's guest_memfd with the child as provider fd, binds it to the
+ * memslot, and maps the guest_memfd's dma-buf into the IOAS on a
+ * mock domain. The provider revokes into guest_memfd; KVM and the
+ * device follow. The VMM never touches the control fd, and its
+ * allowlist would stop it.
+ *
+ * guest_memfd sees one flat fd per VM; every relationship between fds lives
+ * in the provider. The scenarios:
+ *
+ * 1. Launch: each guest runs on its child and its write lands.
+ * 2. Move: a range leaves A for B. A faults on it and exits to its VMM
+ * with KVM_EXIT_MEMORY_FAULT; A's device mapping of it is a
+ * hole and the rest is intact; B reads what A wrote there.
+ * 3. Donate: a range leaves A for the root and comes back on RECLAIM.
+ * 4. Read-only: a guest write to a read-only page exits; after clearing
+ * the bit the same write lands.
+ * 5. Scratch: with scratch mode on, A's donated range resolves in the
+ * IOAS to one frame that is none of A's, and reads as zero.
+ * 6. Fragment: alternate scratch and owned pages in one 2 MiB block read
+ * correctly and map at 4K; an untouched block maps at 2M.
+ * 7. Allowlist: B's fd cannot reach an ioctl the control process did not grant.
+ * 8. Window: a VMM window mmap()ed through the guest_memfd fd over a
+ * range the provider donates faults afterwards: the
+ * revoke tore the host PTEs down too.
+ *
+ * A's guest_memfd is created while another thread flips SET_PRESENT and
+ * SET_READONLY on its child, so attachment races exporter invalidation.
+ *
+ * Runs on the iommufd mock domain: no device needed. Requires
+ * /dev/gmem_provider (samples/kvm/gmem_provider.ko) and /dev/iommu.
+ */
+#include <fcntl.h>
+#include <errno.h>
+#include <setjmp.h>
+#include <pthread.h>
+#include <signal.h>
+#include <stdatomic.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"
+
+/*
+ * The iommufd mock domain's test ABI. Only the three probes we need; the
+ * iommufd selftest helper header brings the kselftest harness and its own
+ * bitops with it, which do not coexist with the KVM selftest library.
+ */
+#include "../../../../../drivers/iommu/iommufd/iommufd_test.h"
+
+/* Mirrors samples/kvm/gmem_provider.h */
+#define GMEM_PROVIDER_FLAG_MMAP_CAPABLE (1u << 0)
+#define GMEM_ALLOW_SET_PRESENT (1u << 0)
+#define GMEM_ALLOW_SET_READONLY (1u << 1)
+#define GMEM_ALLOW_GET_DMABUF (1u << 2)
+#define GMEM_ALLOW_GET_STATS (1u << 3)
+
+struct gmem_provider_present { __u64 offset, len; __u32 present, pad; };
+struct gmem_provider_readonly { __u64 offset, len; __u32 readonly, pad; };
+struct gmem_provider_new_child {
+ __s32 kvm_fd; __u32 flags; __u64 offset, len; __u32 allow, pad;
+};
+
+struct gmem_provider_move { __s32 src_fd, dst_fd; __u64 offset, len; };
+
+struct gmem_provider_donate { __s32 fd; __u32 pad; __u64 offset, len; };
+
+struct gmem_provider_scratch { __u32 enable, pad; };
+
+struct gmem_provider_stats {
+ __u64 region_offset, region_len, owned_pages, absent_pages, readonly_pages;
+ __u32 allow, pad;
+};
+
+#define GMEM_PROVIDER_SET_PRESENT _IOW('G', 2, struct gmem_provider_present)
+#define GMEM_PROVIDER_SET_READONLY _IOW('G', 4, struct gmem_provider_readonly)
+#define GMEM_PROVIDER_NEW_CHILD _IOW('G', 5, struct gmem_provider_new_child)
+#define GMEM_PROVIDER_MOVE _IOW('G', 6, struct gmem_provider_move)
+#define GMEM_PROVIDER_DONATE _IOW('G', 7, struct gmem_provider_donate)
+#define GMEM_PROVIDER_RECLAIM _IOW('G', 8, struct gmem_provider_donate)
+#define GMEM_PROVIDER_SET_SCRATCH _IOW('G', 9, struct gmem_provider_scratch)
+#define GMEM_PROVIDER_GET_DMABUF _IO('G', 3)
+#define GMEM_PROVIDER_GET_STATS _IOR('G', 10, struct gmem_provider_stats)
+
+#define PAGE 0x1000ULL
+#define CHILD_SIZE 0x800000ULL /* 8 MiB per child */
+#define GPA (1ULL << 32) /* each VM maps its child here */
+#define IOVA (1ULL << 28) /* inside the mock domain's aperture */
+#define MAGIC_A 0xa11ce000a11ce000ULL
+#define MAGIC_B 0xb0bb0bb0b0bb0bb0ULL
+
+/*
+ * Layout of the root region. A and B are carved so that they OVERLAP on
+ * [SHARED_OFF, +SHARED_LEN): a carve is the range a child may ever hold,
+ * ownership within it is per page. the control process creates A owning its whole carve and
+ * B owning its carve minus the shared window, so the window starts with A
+ * and can MOVE to B. This is the carve-out shape: two VMs, two fds,
+ * one range that changes hands, no shared object in guest_memfd.
+ */
+#define A_OFF 0ULL
+#define SHARED_LEN (4 * PAGE)
+#define B_OFF (CHILD_SIZE - SHARED_LEN) /* B's carve begins at the window */
+#define SHARED_OFF B_OFF /* root offset of the window */
+#define ROOT_SIZE (B_OFF + CHILD_SIZE)
+
+/* The shared window as each VM sees it. */
+#define A_SHARED_GPA (GPA + (SHARED_OFF - A_OFF))
+#define B_SHARED_GPA (GPA + (SHARED_OFF - B_OFF))
+#define A_SHARED_IOVA (IOVA + (SHARED_OFF - A_OFF))
+
+/* A page of A's that is never moved, for read-only and control checks. */
+#define RO_OFF (64 * PAGE)
+#define RO_GPA (GPA + RO_OFF)
+
+/* A page B owns from the start (B's offset 0 is the window it lacks). */
+#define B_HOME_OFF (CHILD_SIZE / 2)
+#define B_HOME_GPA (GPA + B_HOME_OFF)
+
+#define FRAG_OFF 0ULL
+#define FRAG_LEN 0x200000ULL /* one 2 MiB-aligned block */
+#define HUGE_OFF 0x200000ULL /* untouched 2 MiB block */
+#define FRAG_MAGIC 0x5a5a5a5a5a5a5a5aULL
+
+/* ------------------------------------------------------------------------ */
+/* Guest: read args from a fixed GVA, act, report. */
+
+struct guest_args {
+ uint64_t write_gpa; /* 0 = skip */
+ uint64_t write_val;
+ uint64_t read_gpa; /* 0 = skip; value returned via GUEST_SYNC */
+ uint64_t scan_gpa;
+ uint64_t scan_pages;
+ uint64_t scan_value;
+};
+
+static void guest_code(struct guest_args *a)
+{
+ if (a->write_gpa)
+ *(volatile uint64_t *)a->write_gpa = a->write_val;
+ if (a->read_gpa)
+ GUEST_SYNC(*(volatile uint64_t *)a->read_gpa);
+ if (a->scan_pages) {
+ uint64_t i;
+
+ for (i = 0; i < a->scan_pages; i++) {
+ uint64_t value = *(volatile uint64_t *)(a->scan_gpa + i * PAGE);
+
+ GUEST_ASSERT_EQ(value, i & 1 ? a->scan_value : 0);
+ }
+ }
+ GUEST_DONE();
+}
+
+/* ------------------------------------------------------------------------ */
+/* the control process role: the only holder of the control fd. */
+
+struct vmm_control { int ctl; };
+
+static void ctl_open(struct vmm_control *c)
+{
+ c->ctl = open("/dev/gmem_provider", O_RDWR);
+ __TEST_REQUIRE(c->ctl >= 0, "gmem_provider not loaded");
+}
+
+static int ctl_new_child(struct vmm_control *c, int kvm_fd, uint64_t off, uint64_t len,
+ uint32_t allow)
+{
+ struct gmem_provider_new_child nc = {
+ .kvm_fd = kvm_fd, .flags = GMEM_PROVIDER_FLAG_MMAP_CAPABLE,
+ .offset = off, .len = len, .allow = allow,
+ };
+ int fd = ioctl(c->ctl, GMEM_PROVIDER_NEW_CHILD, &nc);
+
+ TEST_ASSERT(fd >= 0, "NEW_CHILD(%#llx,%#llx) errno=%d",
+ (unsigned long long)off, (unsigned long long)len, errno);
+ return fd;
+}
+
+static int ctl_move(struct vmm_control *c, int src, int dst, uint64_t off, uint64_t len)
+{
+ struct gmem_provider_move mv = { .src_fd = src, .dst_fd = dst,
+ .offset = off, .len = len };
+
+ return ioctl(c->ctl, GMEM_PROVIDER_MOVE, &mv) ? -errno : 0;
+}
+
+static void ctl_donate(struct vmm_control *c, int fd, uint64_t off, uint64_t len, bool reclaim)
+{
+ struct gmem_provider_donate d = { .fd = fd, .offset = off, .len = len };
+
+ TEST_ASSERT(!ioctl(c->ctl, reclaim ? GMEM_PROVIDER_RECLAIM
+ : GMEM_PROVIDER_DONATE, &d),
+ "%s errno=%d", reclaim ? "RECLAIM" : "DONATE", errno);
+}
+
+static void ctl_scratch(struct vmm_control *c, bool on)
+{
+ struct gmem_provider_scratch sc = { .enable = on };
+
+ TEST_ASSERT(!ioctl(c->ctl, GMEM_PROVIDER_SET_SCRATCH, &sc),
+ "SET_SCRATCH errno=%d", errno);
+}
+
+/* ------------------------------------------------------------------------ */
+/* VMM role: one child fd, one VM, one IOAS. */
+
+struct vmm {
+ const char *name;
+ int child; /* control fd: windows, stats, allowed ioctls */
+ int gmem; /* the VM's memory object: memslot fd, window mmaps */
+ int dmabuf; /* the child's dma-buf: what KVM and iommufd both import */
+ int iommufd;
+ uint32_t ioas, stdev, hwpt;
+ struct kvm_vm *vm;
+ struct kvm_vcpu *vcpu;
+ void *hva; /* host window over the whole child */
+ gva_t args_gva;
+};
+
+static void vmm_create(struct vmm *v)
+{
+ struct vm_shape shape = { .mode = VM_MODE_DEFAULT,
+ .type = KVM_X86_SW_PROTECTED_VM };
+
+ v->vm = vm_create_shape_with_one_vcpu(shape, &v->vcpu, guest_code);
+ v->args_gva = vm_alloc_page(v->vm);
+}
+
+static int mock_domain_create(struct vmm *v)
+{
+ struct iommu_test_cmd cmd = {
+ .size = sizeof(cmd), .op = IOMMU_TEST_OP_MOCK_DOMAIN, .id = v->ioas,
+ };
+
+ if (ioctl(v->iommufd, IOMMU_TEST_CMD, &cmd))
+ return -errno;
+ v->stdev = cmd.mock_domain.out_stdev_id;
+ v->hwpt = cmd.mock_domain.out_hwpt_id;
+ return 0;
+}
+
+static bool vmm_iova_mapped(struct vmm *v, uint64_t iova)
+{
+ struct iommu_test_cmd cmd = {
+ .size = sizeof(cmd), .op = IOMMU_TEST_OP_MD_CHECK_MAPPED, .id = v->hwpt,
+ .check_mapped = { .mapped = true, .iova = iova, .length = PAGE },
+ };
+
+ return !ioctl(v->iommufd, IOMMU_TEST_CMD, &cmd);
+}
+
+static uint64_t vmm_iova_phys(struct vmm *v, uint64_t iova)
+{
+ struct iommu_test_cmd cmd = {
+ .size = sizeof(cmd), .op = IOMMU_TEST_OP_MD_IOVA_TO_PHYS, .id = v->hwpt,
+ .iova_to_phys = { .iova = iova },
+ };
+
+ if (ioctl(v->iommufd, IOMMU_TEST_CMD, &cmd))
+ return 0;
+ return cmd.iova_to_phys.out_phys;
+}
+
+/*
+ * Whether the importer understands the full get_phys() contract: several
+ * ranges, holes, and a ranged invalidation it answers by re-reading the
+ * layout. Without that, iommufd maps only a buffer whose layout is one
+ * range (-EOPNOTSUPP otherwise), and any change to the buffer costs the
+ * device its whole mapping for good. Detected at attach time from B,
+ * whose child has a hole at launch; the device-plane checks below say
+ * what to expect either way.
+ */
+static bool ranged_import = true;
+
+/* Map [off, off+len) of the child into the IOAS at the matching IOVA. */
+static int vmm_map_range(struct vmm *v, uint64_t off, uint64_t len)
+{
+ struct iommu_ioas_map_file map = {
+ .size = sizeof(map),
+ .flags = IOMMU_IOAS_MAP_FIXED_IOVA | IOMMU_IOAS_MAP_READABLE |
+ IOMMU_IOAS_MAP_WRITEABLE,
+ .ioas_id = v->ioas, .fd = v->dmabuf,
+ .start = off, .length = len, .iova = IOVA + off,
+ };
+
+ return ioctl(v->iommufd, IOMMU_IOAS_MAP_FILE, &map) ? -errno : 0;
+}
+
+/*
+ * the control process hands the VMM its child fd and the range it owns today. The VMM
+ * creates the VM's guest_memfd from it (provider fd), binds the whole view
+ * to the memslot, and maps the owned part into the IOAS with
+ * IOMMU_IOAS_MAP_FILE on the guest_memfd's dma-buf. iommufd refuses to map a range
+ * guest_memfd reports as a hole, so a VMM maps what it has and maps more
+ * when the control process tells it a range arrived (see scenario_move).
+ */
+struct create_race {
+ int child;
+ atomic_bool stop;
+ atomic_int error;
+};
+
+static void *create_race_thread(void *arg)
+{
+ struct create_race *race = arg;
+ struct gmem_provider_present present = { .offset = RO_OFF, .len = PAGE };
+ struct gmem_provider_readonly readonly = { .offset = RO_OFF, .len = PAGE };
+
+ while (!atomic_load(&race->stop)) {
+ present.present = 0;
+ if (ioctl(race->child, GMEM_PROVIDER_SET_PRESENT, &present))
+ break;
+ present.present = 1;
+ if (ioctl(race->child, GMEM_PROVIDER_SET_PRESENT, &present))
+ break;
+ readonly.readonly = 1;
+ if (ioctl(race->child, GMEM_PROVIDER_SET_READONLY, &readonly))
+ break;
+ readonly.readonly = 0;
+ if (ioctl(race->child, GMEM_PROVIDER_SET_READONLY, &readonly))
+ break;
+ }
+ if (!atomic_load(&race->stop))
+ atomic_store(&race->error, errno ?: EIO);
+ return NULL;
+}
+
+static void finish_create_race(struct create_race *race, pthread_t thread)
+{
+ struct gmem_provider_present present = {
+ .offset = RO_OFF, .len = PAGE, .present = 1,
+ };
+ struct gmem_provider_readonly readonly = {
+ .offset = RO_OFF, .len = PAGE, .readonly = 0,
+ };
+
+ atomic_store(&race->stop, true);
+ TEST_ASSERT(!pthread_join(thread, NULL), "pthread_join");
+ TEST_ASSERT(!atomic_load(&race->error), "create race errno=%d",
+ atomic_load(&race->error));
+ TEST_ASSERT(!ioctl(race->child, GMEM_PROVIDER_SET_PRESENT, &present),
+ "restore present errno=%d", errno);
+ TEST_ASSERT(!ioctl(race->child, GMEM_PROVIDER_SET_READONLY, &readonly),
+ "restore writable errno=%d", errno);
+}
+
+static void vmm_attach(struct vmm *v, int child_fd, uint64_t own_off,
+ uint64_t own_len, uint64_t gmem_flags, bool race_create)
+{
+ struct iommu_ioas_alloc alloc = { .size = sizeof(alloc) };
+ struct create_race race = { .child = child_fd };
+ void *reservation;
+ pthread_t thread;
+ int r;
+
+ v->child = child_fd;
+
+ /*
+ * KVM leg: a guest_memfd whose memory the child provides. The child
+ * is the provider fd; guest_memfd is the VM's one memory object. The
+ * host window is the provider fd's mmap.
+ */
+ v->dmabuf = ioctl(v->child, GMEM_PROVIDER_GET_DMABUF);
+ TEST_ASSERT(v->dmabuf >= 0, "GMEM_PROVIDER_GET_DMABUF errno=%d", errno);
+ if (race_create)
+ TEST_ASSERT(!pthread_create(&thread, NULL, create_race_thread, &race),
+ "pthread_create");
+ v->gmem = vm_create_guest_memfd_dmabuf(v->vm, CHILD_SIZE, gmem_flags,
+ v->dmabuf);
+ if (race_create)
+ finish_create_race(&race, thread);
+ reservation = mmap(NULL, CHILD_SIZE + FRAG_LEN, PROT_NONE,
+ MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
+ TEST_ASSERT(reservation != MAP_FAILED, "%s: reserve HVA errno=%d",
+ v->name, errno);
+ v->hva = (void *)(((uintptr_t)reservation + FRAG_LEN - 1) &
+ ~((uintptr_t)FRAG_LEN - 1));
+ v->hva = mmap(v->hva, CHILD_SIZE, PROT_READ | PROT_WRITE,
+ MAP_SHARED | MAP_FIXED, v->child, 0);
+ TEST_ASSERT(v->hva != MAP_FAILED, "%s: mmap child errno=%d", v->name,
+ errno);
+ r = __vm_set_user_memory_region2(v->vm, 10, KVM_MEM_GUEST_MEMFD, GPA,
+ CHILD_SIZE, v->hva, v->gmem, 0);
+ TEST_ASSERT(!r, "%s: SET_USER_MEMORY_REGION2 r=%d errno=%d", v->name, r, errno);
+ virt_map(v->vm, GPA, GPA, CHILD_SIZE / PAGE);
+ /* Private, so every guest access goes through get_pfn(), not the HVA. */
+ vm_mem_set_private(v->vm, GPA, CHILD_SIZE);
+
+ /* DMA leg: the same guest_memfd, into an IOAS on a mock domain. */
+ v->iommufd = open("/dev/iommu", O_RDWR);
+ __TEST_REQUIRE(v->iommufd >= 0, "iommufd unavailable");
+ TEST_ASSERT(!ioctl(v->iommufd, IOMMU_IOAS_ALLOC, &alloc), "IOAS_ALLOC errno=%d", errno);
+ v->ioas = alloc.out_ioas_id;
+
+ /* A mock device on a mock domain, so the IOAS is really programmed. */
+ r = mock_domain_create(v);
+ TEST_ASSERT(!r, "%s: mock domain r=%d (CONFIG_IOMMUFD_TEST?)", v->name, r);
+
+ r = vmm_map_range(v, own_off, own_len);
+ if (r == -EOPNOTSUPP && own_off) {
+ /* B's layout has a hole at launch; only a ranged importer takes it. */
+ ranged_import = false;
+ pr_info(" iommufd maps one range; device checks expect full revoke\n");
+ return;
+ }
+ TEST_ASSERT(!r, "%s: IOAS_MAP_FILE(dma-buf) r=%d", v->name, r);
+}
+
+/*
+ * Continue the guest where it stopped. KVM_RUN returns -EFAULT for
+ * KVM_EXIT_MEMORY_FAULT, which several scenarios expect, so do not assert
+ * on it here; the expect_*() helpers check the exit.
+ */
+static void vmm_resume(struct vmm *v)
+{
+ int r = _vcpu_run(v->vcpu);
+
+ TEST_ASSERT(!r || (errno == EFAULT &&
+ v->vcpu->run->exit_reason == KVM_EXIT_MEMORY_FAULT),
+ "%s: KVM_RUN r=%d errno=%d exit=%s", v->name, r, errno,
+ exit_reason_str(v->vcpu->run->exit_reason));
+}
+
+/* Run the guest from the top with fresh args. */
+static void vmm_run(struct vmm *v, uint64_t write_gpa, uint64_t write_val,
+ uint64_t read_gpa)
+{
+ struct guest_args *a = addr_gva2hva(v->vm, v->args_gva);
+
+ a->write_gpa = write_gpa;
+ a->write_val = write_val;
+ a->read_gpa = read_gpa;
+ a->scan_pages = 0;
+ vcpu_arch_set_entry_point(v->vcpu, guest_code);
+ vcpu_args_set(v->vcpu, 1, v->args_gva);
+ vmm_resume(v);
+}
+
+static void vmm_scan(struct vmm *v, uint64_t gpa, uint64_t pages,
+ uint64_t value)
+{
+ struct guest_args *a = addr_gva2hva(v->vm, v->args_gva);
+
+ memset(a, 0, sizeof(*a));
+ a->scan_gpa = gpa;
+ a->scan_pages = pages;
+ a->scan_value = value;
+ vcpu_arch_set_entry_point(v->vcpu, guest_code);
+ vcpu_args_set(v->vcpu, 1, v->args_gva);
+ vmm_resume(v);
+}
+
+static void expect_done(struct vmm *v)
+{
+ struct kvm_run *run = v->vcpu->run;
+ struct ucall uc;
+
+ TEST_ASSERT(run->exit_reason != KVM_EXIT_MEMORY_FAULT,
+ "%s: unexpected memory fault gpa=%#llx size=%#llx flags=%#llx",
+ v->name, (unsigned long long)run->memory_fault.gpa,
+ (unsigned long long)run->memory_fault.size,
+ (unsigned long long)run->memory_fault.flags);
+ TEST_ASSERT(get_ucall(v->vcpu, &uc) == UCALL_DONE, "%s: guest exit %s",
+ v->name, exit_reason_str(run->exit_reason));
+}
+
+static uint64_t expect_sync_then_done(struct vmm *v)
+{
+ struct ucall uc;
+ uint64_t val;
+
+ TEST_ASSERT(get_ucall(v->vcpu, &uc) == UCALL_SYNC, "%s: guest exit %s",
+ v->name, exit_reason_str(v->vcpu->run->exit_reason));
+ val = uc.args[1];
+ vmm_resume(v);
+ expect_done(v);
+ return val;
+}
+
+static void expect_memory_fault(struct vmm *v, uint64_t gpa)
+{
+ struct kvm_run *run = v->vcpu->run;
+
+ TEST_ASSERT(run->exit_reason == KVM_EXIT_MEMORY_FAULT,
+ "%s: want KVM_EXIT_MEMORY_FAULT, got %s", v->name,
+ exit_reason_str(run->exit_reason));
+ TEST_ASSERT(run->memory_fault.gpa == gpa, "%s: fault gpa %#llx, want %#llx",
+ v->name, (unsigned long long)run->memory_fault.gpa,
+ (unsigned long long)gpa);
+}
+
+static void vmm_stats(struct vmm *v, struct gmem_provider_stats *st)
+{
+ TEST_ASSERT(!ioctl(v->child, GMEM_PROVIDER_GET_STATS, st),
+ "%s: GET_STATS errno=%d", v->name, errno);
+}
+
+static void vmm_destroy(struct vmm *v)
+{
+ kvm_vm_free(v->vm);
+ close(v->gmem);
+ close(v->iommufd);
+ munmap(v->hva, CHILD_SIZE);
+ close(v->child);
+}
+
+/* ------------------------------------------------------------------------ */
+/* Scenarios. */
+
+static void scenario_launch(struct vmm *a, struct vmm *b)
+{
+ pr_info("1. launch\n");
+ vmm_run(a, GPA, MAGIC_A, 0);
+ expect_done(a);
+ vmm_run(b, B_HOME_GPA, MAGIC_B, 0);
+ expect_done(b);
+ TEST_ASSERT(*(volatile uint64_t *)a->hva == MAGIC_A, "A's write missing");
+ TEST_ASSERT(*(volatile uint64_t *)(b->hva + B_HOME_OFF) == MAGIC_B, "B's write missing");
+ TEST_ASSERT(vmm_iova_mapped(a, IOVA) && vmm_iova_mapped(a, A_SHARED_IOVA),
+ "A: IOAS not fully mapped after launch");
+}
+
+static void scenario_move(struct vmm_control *c, struct vmm *a, struct vmm *b)
+{
+ struct gmem_provider_stats st;
+ int r;
+
+ pr_info("2. move A -> B\n");
+
+ /* A writes into the window while it still owns it. */
+ vmm_run(a, A_SHARED_GPA, MAGIC_A, 0);
+ expect_done(a);
+
+ /* B does not own the window yet: a read faults out to B's VMM ... */
+ vmm_run(b, 0, 0, B_SHARED_GPA);
+ expect_memory_fault(b, B_SHARED_GPA);
+ /* ... and iommufd refuses to map a hole (or a layout with one in it). */
+ r = vmm_map_range(b, SHARED_OFF - B_OFF, SHARED_LEN);
+ TEST_ASSERT(r == (ranged_import ? -EFAULT : -EOPNOTSUPP),
+ "B: mapping an unowned window returned %d", r);
+
+ /* A move outside the destination's carve is refused. */
+ r = ctl_move(c, a->child, b->child, RO_OFF, PAGE);
+ TEST_ASSERT(r == -EINVAL, "MOVE outside B's carve returned %d", r);
+
+ /* The real move. */
+ r = ctl_move(c, a->child, b->child, SHARED_OFF, SHARED_LEN);
+ TEST_ASSERT(!r, "MOVE returned %d", r);
+
+ vmm_stats(a, &st);
+ TEST_ASSERT(st.owned_pages == CHILD_SIZE / PAGE - SHARED_LEN / PAGE,
+ "A owns %llu pages after move", (unsigned long long)st.owned_pages);
+ vmm_stats(b, &st);
+ TEST_ASSERT(st.owned_pages == CHILD_SIZE / PAGE,
+ "B owns %llu pages after move", (unsigned long long)st.owned_pages);
+
+ /* A's device mapping: exactly the window is a hole, neighbours intact. */
+ TEST_ASSERT(!vmm_iova_mapped(a, A_SHARED_IOVA), "A: moved IOVA still mapped");
+ TEST_ASSERT(!vmm_iova_mapped(a, A_SHARED_IOVA + SHARED_LEN - PAGE),
+ "A: last moved IOVA still mapped");
+ if (ranged_import) {
+ TEST_ASSERT(vmm_iova_mapped(a, A_SHARED_IOVA - PAGE), "A: page before window lost");
+ TEST_ASSERT(vmm_iova_mapped(a, IOVA), "A: base lost");
+ } else {
+ /* Whole-buffer revocation: the untouched pages went with the window. */
+ TEST_ASSERT(!vmm_iova_mapped(a, IOVA), "A: base survived a whole-buffer revoke");
+ }
+
+ /* A's guest: the window is gone. */
+ vmm_run(a, 0, 0, A_SHARED_GPA);
+ expect_memory_fault(a, A_SHARED_GPA);
+
+ /* B's guest: the window is here, and it holds what A wrote. */
+ vmm_run(b, 0, 0, B_SHARED_GPA);
+ TEST_ASSERT(expect_sync_then_done(b) == MAGIC_A, "B does not see A's write");
+
+ /*
+ * B's device: the control process told B's VMM the window arrived; now
+ * it maps. B's layout is one range from here on, so this works with
+ * either importer.
+ */
+ r = vmm_map_range(b, SHARED_OFF - B_OFF, SHARED_LEN);
+ TEST_ASSERT(!r, "B: mapping the received window r=%d", r);
+ TEST_ASSERT(vmm_iova_mapped(b, IOVA + (SHARED_OFF - B_OFF)), "B: window not mapped");
+}
+
+static void scenario_donate(struct vmm_control *c, struct vmm *a)
+{
+ struct gmem_provider_stats st;
+ uint64_t off = A_OFF + 8 * PAGE, gpa = GPA + 8 * PAGE, iova = IOVA + 8 * PAGE;
+
+ pr_info("3. donate + reclaim\n");
+ ctl_donate(c, a->child, off, 2 * PAGE, false);
+ vmm_stats(a, &st);
+ TEST_ASSERT(st.owned_pages == CHILD_SIZE / PAGE - SHARED_LEN / PAGE - 2,
+ "A owns %llu after donate", (unsigned long long)st.owned_pages);
+ if (ranged_import) {
+ TEST_ASSERT(!vmm_iova_mapped(a, iova) && !vmm_iova_mapped(a, iova + PAGE),
+ "A: donated IOVAs still mapped");
+ TEST_ASSERT(vmm_iova_mapped(a, iova - PAGE) && vmm_iova_mapped(a, iova + 2 * PAGE),
+ "A: neighbours of donation lost");
+ }
+ vmm_run(a, 0, 0, gpa);
+ expect_memory_fault(a, gpa);
+
+ ctl_donate(c, a->child, off, 2 * PAGE, true);
+ vmm_stats(a, &st);
+ TEST_ASSERT(st.owned_pages == CHILD_SIZE / PAGE - SHARED_LEN / PAGE,
+ "A owns %llu after reclaim", (unsigned long long)st.owned_pages);
+ if (ranged_import)
+ TEST_ASSERT(vmm_iova_mapped(a, iova), "A: reclaimed IOVA not remapped");
+ vmm_run(a, 0, 0, gpa);
+ expect_sync_then_done(a);
+}
+
+static void scenario_readonly(struct vmm *a)
+{
+ struct gmem_provider_readonly ro = { .offset = RO_OFF, .len = PAGE, .readonly = 1 };
+
+ pr_info("4. read-only\n");
+ TEST_ASSERT(!ioctl(a->child, GMEM_PROVIDER_SET_READONLY, &ro),
+ "SET_READONLY errno=%d", errno);
+
+ vmm_run(a, RO_GPA, MAGIC_A, 0);
+ expect_memory_fault(a, RO_GPA);
+ TEST_ASSERT(*(volatile uint64_t *)(a->hva + RO_OFF) != MAGIC_A, "RO write landed");
+
+ /* Clear the bit and let the guest retry the same instruction. */
+ ro.readonly = 0;
+ TEST_ASSERT(!ioctl(a->child, GMEM_PROVIDER_SET_READONLY, &ro), "clear RO errno=%d", errno);
+ vmm_resume(a);
+ expect_done(a);
+ TEST_ASSERT(*(volatile uint64_t *)(a->hva + RO_OFF) == MAGIC_A,
+ "write after clearing RO did not land");
+}
+
+static void scenario_scratch(struct vmm_control *c, struct vmm *a)
+{
+ uint64_t off = A_OFF + 8 * PAGE, gpa = GPA + 8 * PAGE, iova = IOVA + 8 * PAGE;
+ uint64_t own0, own_before, p0, p1;
+
+ pr_info("5. scratch\n");
+ if (!ranged_import) {
+ pr_info(" skipped: needs the device mapping A lost in scenario 2\n");
+ return;
+ }
+ own0 = vmm_iova_phys(a, IOVA);
+ own_before = vmm_iova_phys(a, iova - PAGE);
+ TEST_ASSERT(own0 && own_before, "A: expected pages unmapped before scratch test");
+
+ ctl_scratch(c, true);
+ ctl_donate(c, a->child, off, 2 * PAGE, false);
+
+ p0 = vmm_iova_phys(a, iova);
+ p1 = vmm_iova_phys(a, iova + PAGE);
+ TEST_ASSERT(p0 && p0 == p1, "scratch: donated IOVAs -> %#llx, %#llx; want one frame",
+ (unsigned long long)p0, (unsigned long long)p1);
+ TEST_ASSERT(p0 != own0 && p0 != own_before, "scratch frame is one of A's own");
+ TEST_ASSERT(vmm_iova_phys(a, IOVA) == own0, "scratch: unrelated IOVA changed");
+
+ /* The guest reads the scratch page: no fault, and it is zero. */
+ vmm_run(a, 0, 0, gpa);
+ TEST_ASSERT(expect_sync_then_done(a) == 0, "scratch page must read as zero");
+
+ ctl_donate(c, a->child, off, 2 * PAGE, true);
+ ctl_scratch(c, false);
+ TEST_ASSERT(vmm_iova_phys(a, iova) != p0, "after reclaim IOVA still on scratch");
+}
+
+static void scenario_fragmented(struct vmm_control *c, struct vmm *a)
+{
+ uint64_t p4k_before, p2m_before, p4k_after, p2m_after;
+ unsigned long i;
+
+ pr_info("6. fragmented layout\n");
+ for (i = 0; i < (FRAG_LEN / PAGE); i++)
+ *(uint64_t *)(a->hva + FRAG_OFF + i * PAGE) = FRAG_MAGIC;
+ *(uint64_t *)(a->hva + HUGE_OFF) = FRAG_MAGIC;
+ p4k_before = vm_get_stat(a->vm, pages_4k);
+ p2m_before = vm_get_stat(a->vm, pages_2m);
+ ctl_scratch(c, true);
+ for (i = 0; i < (FRAG_LEN / PAGE); i += 2)
+ ctl_donate(c, a->child, A_OFF + FRAG_OFF + i * PAGE, PAGE, false);
+
+ vmm_scan(a, GPA + FRAG_OFF, FRAG_LEN / PAGE, FRAG_MAGIC);
+ expect_done(a);
+ p4k_after = vm_get_stat(a->vm, pages_4k);
+ TEST_ASSERT(p4k_after > p4k_before,
+ "fragmented block did not add 4K mappings");
+ /* The fragmented block's own 2M mapping was zapped; measure from here. */
+ p2m_before = vm_get_stat(a->vm, pages_2m);
+
+ vmm_run(a, 0, 0, GPA + HUGE_OFF);
+ TEST_ASSERT(expect_sync_then_done(a) == FRAG_MAGIC,
+ "untouched block data changed");
+ p2m_after = vm_get_stat(a->vm, pages_2m);
+ pr_info(" fragmented levels: 4K %llu->%llu, 2M %llu->%llu\n",
+ (unsigned long long)p4k_before, (unsigned long long)p4k_after,
+ (unsigned long long)p2m_before, (unsigned long long)p2m_after);
+ TEST_ASSERT(p2m_after > p2m_before,
+ "untouched block did not add a 2M mapping");
+
+ for (i = 0; i < (FRAG_LEN / PAGE); i += 2)
+ ctl_donate(c, a->child, A_OFF + FRAG_OFF + i * PAGE, PAGE, true);
+ ctl_scratch(c, false);
+}
+
+static void scenario_allowlist(struct vmm *b)
+{
+ struct gmem_provider_present p = { .offset = 0, .len = PAGE, .present = 0 };
+ struct gmem_provider_stats st;
+
+ pr_info("7. allowlist\n");
+ TEST_ASSERT(ioctl(b->child, GMEM_PROVIDER_SET_PRESENT, &p) && errno == EPERM,
+ "B: SET_PRESENT must be denied");
+ vmm_stats(b, &st);
+ TEST_ASSERT(!(st.allow & GMEM_ALLOW_SET_PRESENT), "B allow=%#x", st.allow);
+ TEST_ASSERT(st.owned_pages == CHILD_SIZE / PAGE, "B lost pages to a denied ioctl");
+}
+
+/* 8. A window through the gmem fd dies with the range it covers. */
+static sigjmp_buf window_jmp;
+static void window_sig(int sig)
+{
+ siglongjmp(window_jmp, sig);
+}
+
+static void scenario_window(struct vmm_control *c, struct vmm *a)
+{
+ uint64_t off = A_OFF + 8 * PAGE;
+ struct sigaction sa = { .sa_handler = window_sig }, old_bus, old_segv;
+ volatile uint64_t *win;
+ int sig;
+
+ pr_info("8. host window teardown\n");
+ win = mmap(NULL, PAGE, PROT_READ | PROT_WRITE, MAP_SHARED, a->gmem, off);
+ TEST_ASSERT(win != MAP_FAILED, "mmap(gmem, window) errno=%d", errno);
+ *win = MAGIC_A;
+ TEST_ASSERT(*(volatile uint64_t *)(a->hva + off) == MAGIC_A,
+ "window write not visible through the child mapping");
+
+ ctl_donate(c, a->child, off, PAGE, false);
+
+ sigaction(SIGBUS, &sa, &old_bus);
+ sigaction(SIGSEGV, &sa, &old_segv);
+ sig = sigsetjmp(window_jmp, 1);
+ if (!sig) {
+ (void)*win; /* must fault: the PTE was torn down */
+ sigaction(SIGBUS, &old_bus, NULL);
+ sigaction(SIGSEGV, &old_segv, NULL);
+ TEST_FAIL("window still readable after the range was donated");
+ }
+ sigaction(SIGBUS, &old_bus, NULL);
+ sigaction(SIGSEGV, &old_segv, NULL);
+ pr_info(" window access after donate: signal %d, as expected\n", sig);
+
+ ctl_donate(c, a->child, off, PAGE, true);
+ munmap((void *)win, PAGE);
+}
+
+int main(void)
+{
+ struct vmm_control ctl;
+ struct vmm a = { .name = "A" }, b = { .name = "B" };
+ int a_fd, b_fd, sizing_fd;
+
+ TEST_REQUIRE(kvm_check_cap(KVM_CAP_VM_TYPES) & BIT(KVM_X86_SW_PROTECTED_VM));
+ ctl_open(&ctl);
+ vmm_create(&a);
+ vmm_create(&b);
+
+ /* Size a CMA-backed root for both overlapping child windows. */
+ sizing_fd = ctl_new_child(&ctl, a.vm->fd, 0, ROOT_SIZE, 0);
+ close(sizing_fd);
+
+ /*
+ * The control process carves the root. A carve is a VM's whole view;
+ * carves may overlap, but page ownership is exclusive. A is created
+ * first and gets its full carve, including the shared window. B is
+ * created second and gets only unowned pages, so its view has a hole
+ * until the control process MOVEs the window over.
+ * Neither VMM gets a management ioctl; those live on the control fd.
+ */
+ a_fd = ctl_new_child(&ctl, a.vm->fd, A_OFF, CHILD_SIZE,
+ GMEM_ALLOW_SET_PRESENT | GMEM_ALLOW_SET_READONLY |
+ GMEM_ALLOW_GET_DMABUF |
+ GMEM_ALLOW_GET_STATS);
+ b_fd = ctl_new_child(&ctl, b.vm->fd, B_OFF, CHILD_SIZE,
+ GMEM_ALLOW_GET_DMABUF | GMEM_ALLOW_GET_STATS);
+ vmm_attach(&a, a_fd, 0, CHILD_SIZE, GUEST_MEMFD_FLAG_MMAP, true);
+ vmm_attach(&b, b_fd, SHARED_LEN, CHILD_SIZE - SHARED_LEN, 0, false);
+
+ scenario_launch(&a, &b);
+ scenario_move(&ctl, &a, &b);
+ scenario_donate(&ctl, &a);
+ scenario_readonly(&a);
+ scenario_scratch(&ctl, &a);
+ scenario_fragmented(&ctl, &a);
+ scenario_allowlist(&b);
+ scenario_window(&ctl, &a);
+
+ vmm_destroy(&a);
+ vmm_destroy(&b);
+ close(ctl.ctl);
+ pr_info("gmem_poc: all scenarios passed\n");
+ return 0;
+}
diff --git a/tools/testing/selftests/kvm/x86/gmem_provider_hugepage_test.c b/tools/testing/selftests/kvm/x86/gmem_provider_hugepage_test.c
index fbbef9761e64..9c3bcfbe2cae 100644
--- a/tools/testing/selftests/kvm/x86/gmem_provider_hugepage_test.c
+++ b/tools/testing/selftests/kvm/x86/gmem_provider_hugepage_test.c
@@ -36,8 +36,18 @@ struct gmem_provider_setup {
__u64 size;
};
#define GMEM_PROVIDER_SETUP _IOW('G', 1, struct gmem_provider_setup)
+#define GMEM_PROVIDER_GET_DMABUF _IO('G', 3)
#define GMEM_PROVIDER_FLAG_MMAP_CAPABLE (1u << 0)
+/* The dma-buf for a sample-provider child: what KVM and iommufd both import. */
+static int child_dmabuf(int child_fd)
+{
+ int fd = ioctl(child_fd, GMEM_PROVIDER_GET_DMABUF);
+
+ TEST_ASSERT(fd >= 0, "GMEM_PROVIDER_GET_DMABUF errno=%d", errno);
+ return fd;
+}
+
#define DATA_SLOT 10
#define DATA_GPA (1ULL << 32) /* 4G: 1G-aligned */
#define DATA_SIZE ((uint64_t)SZ_1G + SZ_2M)
@@ -59,7 +69,8 @@ int main(void)
struct kvm_vcpu *vcpu;
struct kvm_vm *vm;
struct ucall uc;
- int gmem_ctl, gmem_fd, r;
+ int gmem_ctl, gmem_fd, dmabuf_fd, r;
+ int prov_fd;
void *resv, *hva;
uint64_t p4k, p2m, p1g;
@@ -73,8 +84,17 @@ int main(void)
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);
+ prov_fd = ioctl(gmem_ctl, GMEM_PROVIDER_SETUP, &setup);
+ TEST_ASSERT(prov_fd >= 0, "GMEM_PROVIDER_SETUP failed, errno %d", errno);
+
+ /*
+ * The provider fd is not a guest_memfd; it is the provider fd. The
+ * guest_memfd KVM returns is what the memslot binds, what the host
+ * mmaps (through the provider's mmap), and what iommufd would map.
+ */
+ dmabuf_fd = child_dmabuf(prov_fd);
+ gmem_fd = vm_create_guest_memfd_dmabuf(vm, DATA_SIZE,
+ GUEST_MEMFD_FLAG_MMAP, dmabuf_fd);
/*
* Map the provider at a 1G-aligned host VA so the slot's userspace_addr
@@ -86,7 +106,7 @@ int main(void)
TEST_ASSERT(resv != MAP_FAILED, "reserve VA failed, errno %d", errno);
hva = (void *)(((uintptr_t)resv + SZ_1G - 1) & ~((uintptr_t)SZ_1G - 1));
hva = mmap(hva, DATA_SIZE, PROT_READ | PROT_WRITE,
- MAP_SHARED | MAP_FIXED, gmem_fd, 0);
+ MAP_SHARED | MAP_FIXED, prov_fd, 0);
TEST_ASSERT(hva != MAP_FAILED,
"mmap(provider) failed, errno %d", errno);
@@ -125,6 +145,8 @@ int main(void)
kvm_vm_free(vm);
munmap(hva, DATA_SIZE);
close(gmem_fd);
+ close(dmabuf_fd);
+ close(prov_fd);
close(gmem_ctl);
return 0;
}
diff --git a/tools/testing/selftests/kvm/x86/gmem_provider_iommufd_test.c b/tools/testing/selftests/kvm/x86/gmem_provider_iommufd_test.c
index 885dffa0b659..b5f4afae5383 100644
--- a/tools/testing/selftests/kvm/x86/gmem_provider_iommufd_test.c
+++ b/tools/testing/selftests/kvm/x86/gmem_provider_iommufd_test.c
@@ -6,8 +6,8 @@
* 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.
+ * GMEM_PROVIDER_GET_DMABUF on the child -> IOMMU_IOAS_MAP_FILE, which walks
+ * iopt_map_dmabuf -> dma_buf_get_phys -> the provider's get_phys op.
*
* 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
@@ -36,8 +36,17 @@ struct gmem_provider_setup {
__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 GMEM_PROVIDER_FLAG_MMAP_CAPABLE (1u << 0)
+
+/* The dma-buf for a sample-provider child: what KVM and iommufd both import. */
+static int child_dmabuf(int child_fd)
+{
+ int fd = ioctl(child_fd, GMEM_PROVIDER_GET_DMABUF);
+
+ TEST_ASSERT(fd >= 0, "GMEM_PROVIDER_GET_DMABUF errno=%d", errno);
+ return fd;
+}
#define DATA_SLOT 10
#define DATA_GPA (1ULL << 32)
@@ -64,6 +73,7 @@ int main(void)
struct kvm_vm *vm;
struct ucall uc;
int gmem_ctl, gmem_fd, dmabuf_fd, iommufd, r;
+ int prov_fd;
void *hva;
TEST_REQUIRE(kvm_check_cap(KVM_CAP_VM_TYPES) & BIT(KVM_X86_SW_PROTECTED_VM));
@@ -80,13 +90,23 @@ int main(void)
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);
+ prov_fd = ioctl(gmem_ctl, GMEM_PROVIDER_SETUP, &setup);
+ TEST_ASSERT(prov_fd >= 0, "GMEM_PROVIDER_SETUP failed errno=%d", errno);
+
+ /*
+ * The provider fd is not a guest_memfd. Export its dma-buf and hand
+ * that to KVM as the provider fd; the guest_memfd KVM returns is what
+ * the memslot binds and what the host mmaps (mmap goes through the
+ * exporter). iommufd would import the very same dma-buf.
+ */
+ dmabuf_fd = child_dmabuf(prov_fd);
+ gmem_fd = vm_create_guest_memfd_dmabuf(vm, DATA_SIZE,
+ GUEST_MEMFD_FLAG_MMAP, dmabuf_fd);
/*
* 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);
+ hva = mmap(NULL, DATA_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, prov_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,
@@ -105,16 +125,15 @@ int main(void)
/*
* 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.
+ * iopt_map_dmabuf -> dma_buf_get_phys -> the get_phys op.
*/
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);
+ /* IOMMU_IOAS_MAP_FILE takes the same dma-buf KVM imported. */
+ pr_info("guest_memfd fd=%d imports dma-buf fd=%d\n", gmem_fd, dmabuf_fd);
map.size = sizeof(map);
map.flags = IOMMU_IOAS_MAP_FIXED_IOVA |
@@ -127,7 +146,7 @@ int main(void)
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)",
+ " (dma_buf_get_phys 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);
@@ -153,6 +172,7 @@ int main(void)
close(iommufd);
munmap(hva, DATA_SIZE);
close(gmem_fd);
+ close(prov_fd);
close(gmem_ctl);
return 0;
}
diff --git a/tools/testing/selftests/kvm/x86/gmem_provider_readonly_test.c b/tools/testing/selftests/kvm/x86/gmem_provider_readonly_test.c
new file mode 100644
index 000000000000..a1b8b7ba5640
--- /dev/null
+++ b/tools/testing/selftests/kvm/x86/gmem_provider_readonly_test.c
@@ -0,0 +1,172 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * gmem_provider_readonly_test - exercise per-range read-only from a provider.
+ *
+ * Marks a provider-backed page read-only via an ioctl on the provider fd and
+ * checks that KVM honours the provider's answer: the guest can still read the
+ * page, a guest write exits to userspace with KVM_EXIT_MEMORY_FAULT rather than
+ * landing, and clearing the bit lets the write through. This is the mechanism
+ * a hypervisor uses to protect a page it shares with the guest, such as a
+ * information page a helper VM reads, without giving up the mapping.
+ *
+ * The test opens the provider with GMEM_PROVIDER_FLAG_MMAP_CAPABLE at SETUP time
+ * (gmem-only). Load the module with a backing region of at least DATA_SIZE.
+ */
+#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 "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;
+};
+
+struct gmem_provider_readonly {
+ __u64 offset;
+ __u64 len;
+ __u32 readonly;
+ __u32 pad;
+};
+
+#define GMEM_PROVIDER_SETUP _IOW('G', 1, struct gmem_provider_setup)
+#define GMEM_PROVIDER_GET_DMABUF _IO('G', 3)
+#define GMEM_PROVIDER_FLAG_MMAP_CAPABLE (1u << 0)
+#define GMEM_PROVIDER_SET_READONLY _IOW('G', 4, struct gmem_provider_readonly)
+
+/* The dma-buf for a sample-provider child: what KVM and iommufd both import. */
+static int child_dmabuf(int child_fd)
+{
+ int fd = ioctl(child_fd, GMEM_PROVIDER_GET_DMABUF);
+
+ TEST_ASSERT(fd >= 0, "GMEM_PROVIDER_GET_DMABUF errno=%d", errno);
+ return fd;
+}
+
+#define DATA_SLOT 10
+#define DATA_GPA (1ULL << 32)
+#define DATA_SIZE 0x200000ULL /* 2 MiB region */
+#define MAGIC 0x1234abcdULL
+#define MAGIC2 0xfeedf00dULL
+
+/*
+ * Phase 1: read the page and report it.
+ * Phase 2: write to it. With the page read-only this never returns to the
+ * guest until userspace clears the bit; then it completes and the
+ * guest reports what it wrote.
+ */
+static void guest_code(void)
+{
+ GUEST_SYNC(*(volatile uint64_t *)DATA_GPA);
+ *(volatile uint64_t *)DATA_GPA = MAGIC2;
+ GUEST_SYNC(*(volatile uint64_t *)DATA_GPA);
+ 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 gmem_provider_readonly req;
+ struct kvm_vcpu *vcpu;
+ struct kvm_vm *vm;
+ struct ucall uc;
+ int gmem_ctl, gmem_fd, dmabuf_fd, r;
+ int prov_fd;
+ 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 module not loaded (/dev/gmem_provider absent)");
+
+ vm = vm_create_shape_with_one_vcpu(shape, &vcpu, guest_code);
+
+ setup.kvm_fd = vm->fd;
+ setup.size = DATA_SIZE;
+ prov_fd = ioctl(gmem_ctl, GMEM_PROVIDER_SETUP, &setup);
+ TEST_ASSERT(prov_fd >= 0, "GMEM_PROVIDER_SETUP failed, errno %d", errno);
+
+ /*
+ * The provider fd is not a guest_memfd; it is the provider fd. The
+ * guest_memfd KVM returns is what the memslot binds, what the host
+ * mmaps (through the provider's mmap), and what iommufd would map.
+ */
+ dmabuf_fd = child_dmabuf(prov_fd);
+ gmem_fd = vm_create_guest_memfd_dmabuf(vm, DATA_SIZE,
+ GUEST_MEMFD_FLAG_MMAP, dmabuf_fd);
+
+ hva = mmap(NULL, DATA_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, prov_fd, 0);
+ TEST_ASSERT(hva != MAP_FAILED, "mmap(provider) 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: %d errno %d", r, errno);
+ virt_map(vm, DATA_GPA, DATA_GPA, 1);
+
+ /* Seed the page from the host before the guest ever touches it. */
+ *(volatile uint64_t *)hva = MAGIC;
+
+ /* 1) Make the page read-only for the guest. */
+ req = (struct gmem_provider_readonly){ .offset = 0, .len = 4096, .readonly = 1 };
+ r = ioctl(prov_fd, GMEM_PROVIDER_SET_READONLY, &req);
+ TEST_ASSERT(!r, "set readonly ioctl failed, errno %d", errno);
+
+ /* 2) Guest read must still work and see the host's value. */
+ vcpu_run(vcpu);
+ TEST_ASSERT(get_ucall(vcpu, &uc) == UCALL_SYNC, "expected UCALL_SYNC");
+ TEST_ASSERT(uc.args[1] == MAGIC, "guest read 0x%lx, want MAGIC",
+ (unsigned long)uc.args[1]);
+ pr_info("read-only: guest read 0x%llx\n", MAGIC);
+
+ /* 3) Guest write must exit to userspace, not land. */
+ r = _vcpu_run(vcpu);
+ TEST_ASSERT(r == -1 && errno == EFAULT &&
+ vcpu->run->exit_reason == KVM_EXIT_MEMORY_FAULT,
+ "read-only write: expected KVM_EXIT_MEMORY_FAULT (r=%d errno=%d exit_reason=%u %s)",
+ r, errno, vcpu->run->exit_reason,
+ exit_reason_str(vcpu->run->exit_reason));
+ TEST_ASSERT(*(volatile uint64_t *)hva == MAGIC,
+ "guest write landed on a read-only page: host sees 0x%lx",
+ (unsigned long)*(volatile uint64_t *)hva);
+ pr_info("read-only: guest write exited with KVM_EXIT_MEMORY_FAULT, page unchanged\n");
+
+ /* 4) Make it writable again; the retried write must complete. */
+ req.readonly = 0;
+ r = ioctl(prov_fd, GMEM_PROVIDER_SET_READONLY, &req);
+ TEST_ASSERT(!r, "clear readonly ioctl failed, errno %d", errno);
+
+ vcpu_run(vcpu);
+ TEST_ASSERT(get_ucall(vcpu, &uc) == UCALL_SYNC, "expected UCALL_SYNC after clear");
+ TEST_ASSERT(uc.args[1] == MAGIC2, "after clear guest read 0x%lx, want MAGIC2",
+ (unsigned long)uc.args[1]);
+ TEST_ASSERT(*(volatile uint64_t *)hva == MAGIC2,
+ "host sees 0x%lx after guest write, want MAGIC2",
+ (unsigned long)*(volatile uint64_t *)hva);
+ pr_info("writable: guest write 0x%llx landed -- read-only path works\n", MAGIC2);
+
+ vcpu_run(vcpu);
+ TEST_ASSERT(get_ucall(vcpu, &uc) == UCALL_DONE, "expected UCALL_DONE");
+
+ kvm_vm_free(vm);
+ munmap(hva, DATA_SIZE);
+ close(gmem_fd);
+ close(dmabuf_fd);
+ close(prov_fd);
+ close(gmem_ctl);
+ return 0;
+}
diff --git a/tools/testing/selftests/kvm/x86/gmem_provider_revoke_test.c b/tools/testing/selftests/kvm/x86/gmem_provider_revoke_test.c
index 415972aa8a7e..df2e9fbd91d8 100644
--- a/tools/testing/selftests/kvm/x86/gmem_provider_revoke_test.c
+++ b/tools/testing/selftests/kvm/x86/gmem_provider_revoke_test.c
@@ -38,9 +38,19 @@ struct gmem_provider_present {
__u32 pad;
};
#define GMEM_PROVIDER_SETUP _IOW('G', 1, struct gmem_provider_setup)
+#define GMEM_PROVIDER_GET_DMABUF _IO('G', 3)
#define GMEM_PROVIDER_FLAG_MMAP_CAPABLE (1u << 0)
#define GMEM_PROVIDER_SET_PRESENT _IOW('G', 2, struct gmem_provider_present)
+/* The dma-buf for a sample-provider child: what KVM and iommufd both import. */
+static int child_dmabuf(int child_fd)
+{
+ int fd = ioctl(child_fd, GMEM_PROVIDER_GET_DMABUF);
+
+ TEST_ASSERT(fd >= 0, "GMEM_PROVIDER_GET_DMABUF errno=%d", errno);
+ return fd;
+}
+
#define DATA_SLOT 10
#define DATA_GPA (1ULL << 32)
#define DATA_SIZE 0x200000ULL /* 2 MiB region */
@@ -64,7 +74,8 @@ int main(void)
struct kvm_vcpu *vcpu;
struct kvm_vm *vm;
struct ucall uc;
- int gmem_ctl, gmem_fd, r;
+ int gmem_ctl, gmem_fd, dmabuf_fd, r;
+ int prov_fd;
void *hva;
TEST_REQUIRE(kvm_check_cap(KVM_CAP_VM_TYPES) & BIT(KVM_X86_SW_PROTECTED_VM));
@@ -77,10 +88,19 @@ int main(void)
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);
+ prov_fd = ioctl(gmem_ctl, GMEM_PROVIDER_SETUP, &setup);
+ TEST_ASSERT(prov_fd >= 0, "GMEM_PROVIDER_SETUP failed, errno %d", errno);
+
+ /*
+ * The provider fd is not a guest_memfd; it is the provider fd. The
+ * guest_memfd KVM returns is what the memslot binds, what the host
+ * mmaps (through the provider's mmap), and what iommufd would map.
+ */
+ dmabuf_fd = child_dmabuf(prov_fd);
+ gmem_fd = vm_create_guest_memfd_dmabuf(vm, DATA_SIZE,
+ GUEST_MEMFD_FLAG_MMAP, dmabuf_fd);
- hva = mmap(NULL, DATA_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, gmem_fd, 0);
+ hva = mmap(NULL, DATA_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, prov_fd, 0);
TEST_ASSERT(hva != MAP_FAILED,
"mmap(provider) failed, errno %d", errno);
@@ -98,7 +118,7 @@ int main(void)
/* 2) Revoke: mark absent and zap the guest NPT (provider->KVM). */
req = (struct gmem_provider_present){ .offset = 0, .len = 4096, .present = 0 };
- r = ioctl(gmem_fd, GMEM_PROVIDER_SET_PRESENT, &req);
+ r = ioctl(prov_fd, GMEM_PROVIDER_SET_PRESENT, &req);
TEST_ASSERT(!r, "revoke ioctl failed, errno %d", errno);
/* 3) Guest re-reads -> re-fault into absent get_pfn -> must NOT see MAGIC. */
@@ -117,7 +137,7 @@ int main(void)
/* 4) Restore: mark present again. */
req.present = 1;
- r = ioctl(gmem_fd, GMEM_PROVIDER_SET_PRESENT, &req);
+ r = ioctl(prov_fd, GMEM_PROVIDER_SET_PRESENT, &req);
TEST_ASSERT(!r, "restore ioctl failed, errno %d", errno);
/* 5) Re-enter: the fault re-maps via get_pfn, guest reads MAGIC again. */
@@ -130,6 +150,8 @@ int main(void)
kvm_vm_free(vm);
munmap(hva, DATA_SIZE);
close(gmem_fd);
+ close(dmabuf_fd);
+ close(prov_fd);
close(gmem_ctl);
return 0;
}
diff --git a/tools/testing/selftests/kvm/x86/gmem_provider_test.c b/tools/testing/selftests/kvm/x86/gmem_provider_test.c
index d7caa11616df..d926b2033843 100644
--- a/tools/testing/selftests/kvm/x86/gmem_provider_test.c
+++ b/tools/testing/selftests/kvm/x86/gmem_provider_test.c
@@ -1,195 +1,11 @@
// SPDX-License-Identifier: GPL-2.0
-/*
- * gmem_provider_test - exercise the samples/kvm gmem_provider module through a
- * full SEV-SNP guest launch, then re-bind the same provider fd to a second VM
- * (the live-update path).
- *
- * The provider module must be loaded first, in either mode:
- * insmod gmem_provider.ko addr=0x5D40000000 len=0x1000000 # page-less
- * insmod gmem_provider.ko # CMA fallback
- *
- * The test skips (KSFT_SKIP) if /dev/gmem_provider or SNP support is absent.
- *
- * Note: with the current kvm_gmem_populate() ABI the launch source is an
- * ordinary anonymous buffer (pinned via get_user_pages_fast); the provider's
- * populate() copies it into the backing. No /dev/mem mapping is needed.
- */
+/* CoCo support for dma-buf-backed guest_memfd is outside this series. */
#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <unistd.h>
-#include <fcntl.h>
-#include <errno.h>
-#include <sys/ioctl.h>
-#include <sys/mman.h>
-#include <stdint.h>
-#include <linux/kvm.h>
-#include <asm/kvm.h>
-/* Mirrors samples/kvm/gmem_provider.h */
-struct gmem_provider_setup {
- int32_t kvm_fd;
- uint32_t pad;
- uint64_t size;
-};
-#define GMEM_PROVIDER_SETUP _IOW('G', 1, struct gmem_provider_setup)
-
-#define KSFT_PASS 0
-#define KSFT_FAIL 1
#define KSFT_SKIP 4
-#define GUEST_MEM_SIZE (16UL * 1024 * 1024)
-#define PAGE_SIZE_4K 4096UL
-
-static int sev_ioctl(int vm_fd, int sev_fd, int cmd, void *data)
-{
- struct kvm_sev_cmd sev_cmd = {
- .id = cmd,
- .data = (uint64_t)(unsigned long)data,
- .sev_fd = sev_fd,
- };
- return ioctl(vm_fd, KVM_MEMORY_ENCRYPT_OP, &sev_cmd);
-}
-
-static int launch_snp_vm(int kvm_fd, int gmem_fd, void *src, int vm_num)
-{
- int vm_fd, vcpu_fd, sev_fd, ret;
-
- printf("[VM%d] KVM_CREATE_VM (SNP)\n", vm_num);
- vm_fd = ioctl(kvm_fd, KVM_CREATE_VM, KVM_X86_SNP_VM);
- if (vm_fd < 0) { perror("KVM_CREATE_VM"); return -1; }
-
- sev_fd = open("/dev/sev", O_RDWR);
- if (sev_fd < 0) { perror("open /dev/sev"); close(vm_fd); return -1; }
-
- struct kvm_sev_init init = { 0 };
- ret = sev_ioctl(vm_fd, sev_fd, KVM_SEV_INIT2, &init);
- if (ret) { perror("KVM_SEV_INIT2"); goto out; }
-
- struct kvm_userspace_memory_region2 region = {
- .slot = 0,
- .flags = KVM_MEM_GUEST_MEMFD,
- .guest_phys_addr = 0,
- .memory_size = GUEST_MEM_SIZE,
- .userspace_addr = (uint64_t)(unsigned long)src,
- .guest_memfd = gmem_fd,
- .guest_memfd_offset = 0,
- };
- ret = ioctl(vm_fd, KVM_SET_USER_MEMORY_REGION2, ®ion);
- if (ret) { perror("KVM_SET_USER_MEMORY_REGION2"); goto out; }
-
- struct kvm_memory_attributes attrs = {
- .address = 0,
- .size = GUEST_MEM_SIZE,
- .attributes = KVM_MEMORY_ATTRIBUTE_PRIVATE,
- };
- ret = ioctl(vm_fd, KVM_SET_MEMORY_ATTRIBUTES, &attrs);
- if (ret) { perror("KVM_SET_MEMORY_ATTRIBUTES"); goto out; }
-
- struct kvm_sev_snp_launch_start start = { .policy = 0x30000 };
- ret = sev_ioctl(vm_fd, sev_fd, KVM_SEV_SNP_LAUNCH_START, &start);
- if (ret) { perror("SNP_LAUNCH_START"); goto out; }
-
- printf("[VM%d] SNP_LAUNCH_UPDATE (code + zero, %luMB)\n",
- vm_num, GUEST_MEM_SIZE >> 20);
- struct kvm_sev_snp_launch_update update = {
- .gfn_start = 0,
- .uaddr = (uint64_t)(unsigned long)src,
- .len = PAGE_SIZE_4K,
- .type = KVM_SEV_SNP_PAGE_TYPE_NORMAL,
- };
- ret = sev_ioctl(vm_fd, sev_fd, KVM_SEV_SNP_LAUNCH_UPDATE, &update);
- if (ret) { perror("SNP_LAUNCH_UPDATE code"); goto out; }
-
- struct kvm_sev_snp_launch_update update_zero = {
- .gfn_start = 1,
- .uaddr = (uint64_t)(unsigned long)(src + PAGE_SIZE_4K),
- .len = GUEST_MEM_SIZE - PAGE_SIZE_4K,
- .type = KVM_SEV_SNP_PAGE_TYPE_ZERO,
- };
- ret = sev_ioctl(vm_fd, sev_fd, KVM_SEV_SNP_LAUNCH_UPDATE, &update_zero);
- if (ret) { perror("SNP_LAUNCH_UPDATE zero"); goto out; }
-
- struct kvm_sev_snp_launch_finish finish = { 0 };
- ret = sev_ioctl(vm_fd, sev_fd, KVM_SEV_SNP_LAUNCH_FINISH, &finish);
- if (ret) { perror("SNP_LAUNCH_FINISH"); goto out; }
-
- vcpu_fd = ioctl(vm_fd, KVM_CREATE_VCPU, 0);
- if (vcpu_fd < 0) { perror("KVM_CREATE_VCPU"); ret = -1; goto out; }
-
- printf("[VM%d] *** launch succeeded ***\n", vm_num);
- close(vcpu_fd);
- ret = 0;
-out:
- close(sev_fd);
- close(vm_fd);
- return ret;
-}
-
int main(void)
{
- int kvm_fd, gmem_ctl_fd, gmem_fd, tmp_vm;
- void *src;
-
- setbuf(stdout, NULL);
-
- kvm_fd = open("/dev/kvm", O_RDWR);
- if (kvm_fd < 0) {
- printf("SKIP: cannot open /dev/kvm (%s)\n", strerror(errno));
- return KSFT_SKIP;
- }
-
- gmem_ctl_fd = open("/dev/gmem_provider", O_RDWR);
- if (gmem_ctl_fd < 0) {
- printf("SKIP: /dev/gmem_provider not present -- load gmem_provider.ko (%s)\n",
- strerror(errno));
- return KSFT_SKIP;
- }
-
- /* Provider setup needs a VM fd; ownership transfers to each VM on bind. */
- tmp_vm = ioctl(kvm_fd, KVM_CREATE_VM, KVM_X86_SNP_VM);
- if (tmp_vm < 0) {
- printf("SKIP: cannot create SNP VM -- host not SNP-capable? (%s)\n",
- strerror(errno));
- return KSFT_SKIP;
- }
-
- struct gmem_provider_setup setup = {
- .kvm_fd = tmp_vm,
- .size = GUEST_MEM_SIZE,
- };
- gmem_fd = ioctl(gmem_ctl_fd, GMEM_PROVIDER_SETUP, &setup);
- if (gmem_fd < 0) {
- perror("GMEM_PROVIDER_SETUP");
- return KSFT_FAIL;
- }
- close(tmp_vm);
- printf("provider gmem_fd = %d (persists across VMs)\n", gmem_fd);
-
- /* Launch source: ordinary anonymous memory holding a HLT at gfn 0. */
- src = mmap(NULL, GUEST_MEM_SIZE, PROT_READ | PROT_WRITE,
- MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
- if (src == MAP_FAILED) { perror("mmap src"); return KSFT_FAIL; }
- memset(src, 0, GUEST_MEM_SIZE);
- ((uint8_t *)src)[0] = 0xf4; /* HLT */
-
- if (launch_snp_vm(kvm_fd, gmem_fd, src, 1) != 0) {
- printf("FAIL: VM1 launch failed\n");
- return KSFT_FAIL;
- }
-
- usleep(100000);
-
- /* Re-bind the SAME provider fd to a fresh VM (live-update path). */
- if (launch_snp_vm(kvm_fd, gmem_fd, src, 2) != 0) {
- printf("FAIL: VM2 re-bind launch failed\n");
- return KSFT_FAIL;
- }
-
- printf("PASS: page-less/provider-backed SNP launch + re-bind succeeded\n");
- munmap(src, GUEST_MEM_SIZE);
- close(gmem_fd);
- close(gmem_ctl_fd);
- close(kvm_fd);
- return KSFT_PASS;
+ puts("SKIP: dma-buf-backed CoCo is outside this series");
+ return KSFT_SKIP;
}
diff --git a/tools/testing/selftests/kvm/x86/gmem_provider_vfio_test.c b/tools/testing/selftests/kvm/x86/gmem_provider_vfio_test.c
index 97a66a295b14..7f3c640aea78 100644
--- a/tools/testing/selftests/kvm/x86/gmem_provider_vfio_test.c
+++ b/tools/testing/selftests/kvm/x86/gmem_provider_vfio_test.c
@@ -5,7 +5,7 @@
*
* Steps:
* 1. SETUP provider (KVM VM shape not required for this test).
- * 2. iommufd IOAS + GET_DMABUF + IOMMU_IOAS_MAP_FILE - IOAS holds the
+ * 2. guest_memfd from the provider fd; IOMMU_IOAS_MAP_FILE on its dma-buf - 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
@@ -39,8 +39,17 @@ struct gmem_provider_setup {
__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 GMEM_PROVIDER_FLAG_MMAP_CAPABLE (1u << 0)
+
+/* The dma-buf for a sample-provider child: what KVM and iommufd both import. */
+static int child_dmabuf(int child_fd)
+{
+ int fd = ioctl(child_fd, GMEM_PROVIDER_GET_DMABUF);
+
+ TEST_ASSERT(fd >= 0, "GMEM_PROVIDER_GET_DMABUF errno=%d", errno);
+ return fd;
+}
#define DATA_SIZE 0x200000ULL
#define IOVA_BASE (1ULL << 34)
@@ -53,7 +62,7 @@ int main(void)
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;
+ int gmem_ctl, gmem_fd, dmabuf_fd, prov_fd, vm_fd = -1, iommufd_fd, vfio_fd, r;
gmem_ctl = open("/dev/gmem_provider", O_RDWR);
__TEST_REQUIRE(gmem_ctl >= 0, "gmem_provider module not loaded");
@@ -80,20 +89,34 @@ int main(void)
vm = ioctl(kvm, KVM_CREATE_VM, 0);
TEST_ASSERT(vm >= 0, "KVM_CREATE_VM errno=%d", errno);
setup.kvm_fd = vm;
+ vm_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);
+ prov_fd = ioctl(gmem_ctl, GMEM_PROVIDER_SETUP, &setup);
+ TEST_ASSERT(prov_fd >= 0, "GMEM_PROVIDER_SETUP errno=%d", errno);
+
+ /*
+ * The provider fd is the provider fd of a guest_memfd, and iommufd maps
+ * the dma-buf; a device and a guest import the same one.
+ */
+ {
+ struct kvm_create_guest_memfd cgm;
+
+ dmabuf_fd = child_dmabuf(prov_fd);
+ cgm = (struct kvm_create_guest_memfd) {
+ .size = DATA_SIZE,
+ .flags = GUEST_MEMFD_FLAG_MMAP | GUEST_MEMFD_FLAG_USE_DMABUF,
+ .dmabuf_fd = dmabuf_fd,
+ };
+ gmem_fd = ioctl(vm_fd, KVM_CREATE_GUEST_MEMFD, &cgm);
+ TEST_ASSERT(gmem_fd >= 0, "KVM_CREATE_GUEST_MEMFD(dmabuf) errno=%d", errno);
+ }
- /* IOAS + provider dma-buf */
+ /* IOAS + the same dma-buf KVM imported. */
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;
@@ -126,8 +149,8 @@ int main(void)
pr_info("real passthrough device now has IOMMU domain covering provider region\n");
close(vfio_fd);
- close(dmabuf_fd);
close(iommufd_fd);
+ close(dmabuf_fd);
close(gmem_fd);
close(gmem_ctl);
return 0;
--
2.47.3
next prev parent reply other threads:[~2026-10-05 9:56 UTC|newest]
Thread overview: 47+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-20 11:03 [RFC PATCH v2 00/11] KVM: Allow alternative providers of guest_memfd backed by PFNMAP memory David Woodhouse
2026-07-20 11:03 ` [RFC PATCH v2 01/11] KVM: selftests: sev_smoke_test: Only run VM types the host offers David Woodhouse
2026-07-20 11:03 ` [RFC PATCH v2 02/11] KVM: selftests: sev_init2_tests: Derive SEV availability from KVM David Woodhouse
2026-07-20 11:03 ` [RFC PATCH v2 03/11] KVM: SEV: Remove struct page dependency from SNP gmem paths David Woodhouse
2026-07-20 11:03 ` [RFC PATCH v2 04/11] KVM: guest_memfd: Introduce guest memory ops and route native gmem through them David Woodhouse
2026-07-20 11:03 ` [RFC PATCH v2 05/11] iommufd: Look up private-interconnect phys via exporter symbols David Woodhouse
2026-07-20 11:03 ` [RFC PATCH v2 06/11] iommufd: Plumb dma-buf memory-type (RAM vs MMIO) through the phys map David Woodhouse
2026-07-20 11:03 ` [RFC PATCH v2 07/11] KVM: guest_memfd: Add ops-driven page revocation David Woodhouse
2026-07-20 11:03 ` [RFC PATCH v2 08/11] samples/kvm: Add guest_memfd backing sample David Woodhouse
2026-07-20 11:03 ` [RFC PATCH v2 09/11] selftests/kvm: gmem_provider KVM-only tests David Woodhouse
2026-07-20 11:03 ` [RFC PATCH v2 10/11] selftests/kvm: gmem_provider iommufd tests David Woodhouse
2026-07-20 11:03 ` [RFC PATCH v2 11/11] samples/kvm, selftests/kvm: Allow the gmem_provider NVMe DMA test on arm64 David Woodhouse
2026-07-20 15:11 ` [RFC PATCH v2 00/11] KVM: Allow alternative providers of guest_memfd backed by PFNMAP memory Paolo Bonzini
2026-07-20 16:39 ` David Woodhouse
2026-07-23 0:24 ` Ackerley Tng
2026-07-23 9:40 ` David Woodhouse
2026-07-23 16:01 ` Ackerley Tng
2026-10-05 9:55 ` [RFC PATCH 0/6] KVM: guest_memfd: back guest_memfd with an imported dma-buf Fred Griffoul
2026-10-05 9:55 ` [RFC PATCH 1/6] KVM: guest_memfd: Add a writable result to get_pfn() Fred Griffoul
2026-10-05 9:55 ` [RFC PATCH 2/6] dma-buf: Add get_phys() to describe a physical run Fred Griffoul
2026-10-05 10:07 ` Christian König
2026-10-05 13:20 ` Fred Griffoul
2026-10-05 14:53 ` Christian König
2026-10-12 14:46 ` Jason Gunthorpe
2026-10-12 15:29 ` Fred Griffoul
2026-10-05 9:55 ` [RFC PATCH 3/6] dma-buf: Add ranged mapping invalidation Fred Griffoul
2026-10-05 10:08 ` Christian König
2026-10-05 9:55 ` [RFC PATCH 4/6] dma-buf: Allow dynamic attach without a device Fred Griffoul
2026-10-05 9:55 ` [RFC PATCH 5/6] KVM: guest_memfd: Add dma-buf backing Fred Griffoul
2026-10-05 9:55 ` Fred Griffoul [this message]
2026-10-06 18:32 ` [RFC PATCH 0/9] mm: Memory providers for guest_memfd and iommufd Fred Griffoul
2026-10-06 18:32 ` [PATCH 1/9] KVM: guest_memfd: Add a writable result to get_pfn() Fred Griffoul
2026-10-06 18:32 ` [PATCH 2/9] mm: Add memory providers Fred Griffoul
2026-10-06 18:32 ` [PATCH 3/9] KVM: guest_memfd: Add a memory provider backing Fred Griffoul
2026-10-06 18:32 ` [PATCH 4/9] iommufd: Track the domains of pages that are not pinned Fred Griffoul
2026-10-06 18:32 ` [PATCH 5/9] iommufd: Map memory provider files Fred Griffoul
2026-10-06 18:32 ` [PATCH 6/9] iommufd/selftest: Add mock-domain IOVA queries Fred Griffoul
2026-10-06 18:32 ` [PATCH 7/9] iommufd/selftest: Add a mock memory provider Fred Griffoul
2026-10-06 18:32 ` [PATCH 8/9] samples/kvm: Add a memory provider sample Fred Griffoul
2026-10-06 18:32 ` [PATCH 9/9] KVM: selftests: Test a memory provider shared by KVM and iommufd Fred Griffoul
2026-10-10 7:36 ` [RFC PATCH 0/9] mm: Memory providers for guest_memfd " Aneesh Kumar K.V
2026-10-12 9:58 ` Fred Griffoul
2026-10-12 10:07 ` David Woodhouse
2026-10-12 14:41 ` Ackerley Tng
2026-10-12 15:07 ` Fred Griffoul
2026-10-12 15:13 ` David Woodhouse
2026-10-12 15:48 ` Fred Griffoul
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261005095552.52748-7-griffoul@gmail.com \
--to=griffoul@gmail.com \
--cc=ackerleytng@google.com \
--cc=alex@shazbot.org \
--cc=bp@alien8.de \
--cc=catalin.marinas@arm.com \
--cc=christian.koenig@amd.com \
--cc=dave.hansen@linux.intel.com \
--cc=dri-devel@lists.freedesktop.org \
--cc=dwmw2@infradead.org \
--cc=hpa@zytor.com \
--cc=iommu@lists.linux.dev \
--cc=jgg@ziepe.ca \
--cc=joey.gouly@arm.com \
--cc=joro@8bytes.org \
--cc=kevin.tian@intel.com \
--cc=kvm@vger.kernel.org \
--cc=kvmarm@lists.linux.dev \
--cc=linaro-mm-sig@lists.linaro.org \
--cc=linux-arm-kernel@lists.infradead.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-kselftest@vger.kernel.org \
--cc=linux-media@vger.kernel.org \
--cc=linux-trace-kernel@vger.kernel.org \
--cc=mathieu.desnoyers@efficios.com \
--cc=maz@kernel.org \
--cc=mhiramat@kernel.org \
--cc=mingo@redhat.com \
--cc=oupton@kernel.org \
--cc=pbonzini@redhat.com \
--cc=robin.murphy@arm.com \
--cc=rostedt@goodmis.org \
--cc=seanjc@google.com \
--cc=seiden@linux.ibm.com \
--cc=shuah@kernel.org \
--cc=sumit.semwal@linaro.org \
--cc=suzuki.poulose@arm.com \
--cc=tglx@kernel.org \
--cc=will@kernel.org \
--cc=x86@kernel.org \
--cc=yuzenghui@huawei.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.