From: David Woodhouse <[email protected]> A minimal in-tree implementation of the guest_memfd ops interface, useful both as reference code and as the substrate for the selftests that follow.
Features: - Backs a slot from a fixed page-less physical range specified by the module parameters addr= and len=, or falls back to a CMA-allocated contiguous region when neither is given. The addr=/len= mode lets a reserved DRAM carve-out (mem=... or an explicit e820/DT reservation) become KVM slot backing without any struct page. - Per-fd mmap capability via GMEM_PROVIDER_FLAG_MMAP_CAPABLE at GMEM_PROVIDER_SETUP time. An mmap-capable fd is bound as a gmem-only slot (KVM_MEMSLOT_GMEM_ONLY) so the host mmap and the guest fault path both route through this implementation. The mmap-capable fd is refused for hardware-encrypted VM types (SEV, SEV-ES, SEV-SNP, TDX) where a host view onto private-marked pages can only fault; the sample enforces this at bind time. KVM_X86_SW_PROTECTED_VM has no hardware encryption and is allowed. - Page revocation via GMEM_PROVIDER_SET_PRESENT. Marking a range absent tracks it in a bitmap, invokes kvm_gmem_invalidate_range() to zap the guest NPT/EPT, and fans dma_buf_invalidate_mappings() out to any exported dma-buf so iommufd tears down its mapping. Subsequent get_pfn() on the range returns -EFAULT until it is restored with present=1. Models overcommit-style reclaim without needing the platform 's real reclaim machinery. - A dma-buf exporter (GMEM_PROVIDER_GET_DMABUF) that yields an fd suitable for IOMMU_IOAS_MAP_FILE. The exporter mirrors the shape of drivers/vfio/pci/vfio_pci_dmabuf.c: dma_buf_attach_revocable, ->map_dma_buf via dma_buf_phys_vec_to_sgt, a single-range phys_vec, and the "well-known symbol" hook for iommufd's private-interconnect phys-map lookup. The sample is layered on top of the shared kvm_gmem_fops: its fd is created with those fops and file->private_data points at a kvm_gmem_backing embedded at the head of its per-instance state. Release / mmap / ioctl for the fd all run through the ops table, so the sample owns no file_operations of its own. Exposing kvm_gmem_fops for module use required promoting its export from EXPORT_SYMBOL_FOR_KVM_INTERNAL to EXPORT_SYMBOL_GPL and adding it to the KVM external-export allowlist. SEV-SNP bind takes a psmash + rmp_make_shared sweep over the range so a new SNP VM can transition its pages private and re-encrypt them -- harmless on non-SNP hosts, where the calls return -ENODEV. Based on a proof-of-concept by Connor Williamson <[email protected]>. Signed-off-by: David Woodhouse (Kiro) <[email protected]> --- arch/x86/kvm/Makefile | 3 +- samples/Kconfig | 18 + samples/Makefile | 1 + samples/kvm/Makefile | 2 + samples/kvm/gmem_provider.c | 651 ++++++++++++++++++++++++++++++++++++ samples/kvm/gmem_provider.h | 53 +++ virt/kvm/guest_memfd.c | 2 +- 7 files changed, 728 insertions(+), 2 deletions(-) create mode 100644 samples/kvm/Makefile create mode 100644 samples/kvm/gmem_provider.c create mode 100644 samples/kvm/gmem_provider.h diff --git a/arch/x86/kvm/Makefile b/arch/x86/kvm/Makefile index cb963d6ecaab..535fb8cc259f 100644 --- a/arch/x86/kvm/Makefile +++ b/arch/x86/kvm/Makefile @@ -64,7 +64,8 @@ exports_grep_trailer := --include='*.[ch]' -nrw $(srctree)/virt/kvm $(srctree)/a -e kvm_get_kvm \ -e kvm_get_kvm_safe \ -e kvm_put_kvm \ - -e kvm_gmem_invalidate_range + -e kvm_gmem_invalidate_range \ + -e kvm_gmem_fops # Force grep to emit a goofy group separator that can in turn be replaced with # the above newline macro (newlines in Make are a nightmare). Note, grep only diff --git a/samples/Kconfig b/samples/Kconfig index a75e8e78330d..3482659638b5 100644 --- a/samples/Kconfig +++ b/samples/Kconfig @@ -326,6 +326,24 @@ source "samples/rust/Kconfig" source "samples/damon/Kconfig" +config SAMPLE_KVM_GMEM_PROVIDER + tristate "Build sample guest_memfd provider -- loadable module only" + depends on KVM_GUEST_MEMFD && X86_64 && m + help + This builds a sample external guest_memfd provider module with two + backing modes. Loaded with addr=/len= it backs guest memory with a + fixed physical range that has no struct page -- for example memory + carved out of the kernel with mem= on the command line. Without + those params it falls back to a physically contiguous, page-backed + region from alloc_contig_pages(), sized by the setup ioctl (easier to + run, no boot param). + + It demonstrates the guest_memfd provider ABI including 2M and 1G + guest mappings and, on SEV-SNP hosts, the RMP reset that lets a + page-less range be re-bound to a new VM across a live update. + + If unsure, say N. + endif # SAMPLES config HAVE_SAMPLE_FTRACE_DIRECT diff --git a/samples/Makefile b/samples/Makefile index 07641e177bd8..e85397e5e34f 100644 --- a/samples/Makefile +++ b/samples/Makefile @@ -37,6 +37,7 @@ obj-$(CONFIG_SAMPLE_TPS6594_PFSM) += pfsm/ subdir-$(CONFIG_SAMPLE_WATCHDOG) += watchdog subdir-$(CONFIG_SAMPLE_WATCH_QUEUE) += watch_queue obj-$(CONFIG_SAMPLE_KMEMLEAK) += kmemleak/ +obj-$(CONFIG_SAMPLE_KVM_GMEM_PROVIDER) += kvm/ obj-$(CONFIG_SAMPLE_CORESIGHT_SYSCFG) += coresight/ obj-$(CONFIG_SAMPLE_FPROBE) += fprobe/ obj-$(CONFIG_SAMPLES_RUST) += rust/ diff --git a/samples/kvm/Makefile b/samples/kvm/Makefile new file mode 100644 index 000000000000..dcad6e53ea78 --- /dev/null +++ b/samples/kvm/Makefile @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: GPL-2.0 +obj-$(CONFIG_SAMPLE_KVM_GMEM_PROVIDER) += gmem_provider.o diff --git a/samples/kvm/gmem_provider.c b/samples/kvm/gmem_provider.c new file mode 100644 index 000000000000..e30580fecd16 --- /dev/null +++ b/samples/kvm/gmem_provider.c @@ -0,0 +1,651 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * gmem_provider - sample external guest_memfd provider. + * + * Demonstrates the KVM guest_memfd provider ABI with two backing modes: + * + * - 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. + * + * - 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. + * + * 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. + */ + +#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/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> +#include <linux/pgtable.h> +#include <linux/slab.h> + +#if IS_ENABLED(CONFIG_AMD_MEM_ENCRYPT) +#include <asm/sev.h> +#endif + +#include "gmem_provider.h" + +MODULE_IMPORT_NS("DMA_BUF"); + +static unsigned long long addr; +module_param(addr, ullong, 0444); +MODULE_PARM_DESC(addr, "physical base of an external page-less backing region (optional)"); + +static unsigned long long len; +module_param(len, ullong, 0444); +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; + + /* Kept locally: kvm_gmem_ops does not carry a kvm pointer. */ + struct kvm *kvm; + + bool mmap_capable; /* -> KVM_MEMSLOT_GMEM_ONLY at bind */ + + 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 */ + unsigned long *absent; /* bitmap of currently-revoked pages */ + struct list_head dmabufs; /* struct gmem_dmabuf entries */ + struct mutex dmabufs_lock; +}; + +static struct gmem_info *to_gmem_info(struct file *file) +{ + return container_of(file->private_data, struct gmem_info, backing); +} + +/* 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); +} + +static void gmem_unmap_pfn(kvm_pfn_t pfn, void *vaddr) +{ + if (!vaddr) + return; + if (pfn_valid(pfn)) + kunmap_local(vaddr); + else + memunmap(vaddr); +} + +/* 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) +{ + 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; + + 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; +} + +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) +{ + 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->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; +} + +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; + + 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) +{ + struct gmem_info *info = to_gmem_info(file); + 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. + */ +#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 + + /* Record the binding so the revoke ioctl can translate offset -> gfn. */ + info->base_gfn = slot->base_gfn; + info->pgoff = start; + +#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. + */ + { + unsigned long i; + + for (i = 0; i < slot->npages; i += PTRS_PER_PMD) { + unsigned long pfn = info->base_pfn + start + i; + + if (IS_ALIGNED(pfn, PTRS_PER_PMD)) + psmash(pfn); + } + 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); + } + } +#endif + + /* Claim (or, on re-bind, transfer) VM ownership; pins the VM. */ + if (info->kvm != kvm) { + kvm_get_kvm(kvm); + if (info->kvm) + kvm_put_kvm(info->kvm); + info->kvm = 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; +} + +static void gmem_unbind(struct file *file, struct kvm *kvm, + struct kvm_memory_slot *slot) +{ +#if IS_ENABLED(CONFIG_AMD_MEM_ENCRYPT) + struct gmem_info *info = to_gmem_info(file); + unsigned long start = slot->gmem.pgoff; + unsigned long i; + + for (i = 0; i < slot->npages; i++) { + unsigned long pfn = info->base_pfn + start + i; + + rmp_make_shared(pfn, PG_LEVEL_4K); + } +#endif +} + +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 void gmem_release(struct file *file) +{ + struct gmem_info *info = to_gmem_info(file); + + if (info->kvm) + kvm_put_kvm(info->kvm); + if (info->cma_pages) + free_contig_range(info->base_pfn, info->npages); + bitmap_free(info->absent); + kfree(info); +} + +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); + + if (vma->vm_pgoff + npages > info->npages) + return -EINVAL; + + /* Page-less backing: map raw PFNs, not folios. */ + 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); +} + +/* + * 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). + * + * 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. + */ +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)) + 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; +} + +static void gmem_dma_buf_unmap(struct dma_buf_attachment *attach, + struct sg_table *sgt, + enum dma_data_direction dir) +{ + struct gmem_dmabuf *priv = attach->dmabuf->priv; + + dma_resv_assert_held(priv->dmabuf->resv); + dma_buf_free_sgt(attach, sgt, dir); + kref_put(&priv->kref, gmem_dma_buf_done); +} + +static void gmem_dma_buf_release(struct dma_buf *dmabuf) +{ + struct gmem_dmabuf *priv = dmabuf->priv; + + 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); + kfree(priv); +} + +static const struct dma_buf_ops gmem_dma_buf_ops = { + .attach = gmem_dma_buf_attach, + .map_dma_buf = gmem_dma_buf_map, + .unmap_dma_buf = gmem_dma_buf_unmap, + .release = gmem_dma_buf_release, +}; + +/* + * Private interconnect for iommufd (mirrors vfio_pci_dma_buf_iommufd_map). + * Returns the single contiguous phys range for the exported region so iommufd + * can program the IOMMU directly, bypassing the DMA API. + */ +int gmem_provider_dma_buf_iommufd_map(struct dma_buf_attachment *attach, + struct phys_vec *phys); +int gmem_provider_dma_buf_iommufd_map(struct dma_buf_attachment *attach, + struct phys_vec *phys) +{ + struct gmem_dmabuf *priv; + + dma_resv_assert_held(attach->dmabuf->resv); + if (attach->dmabuf->ops != &gmem_dma_buf_ops) + return -EOPNOTSUPP; + priv = attach->dmabuf->priv; + if (priv->revoked) + return -ENODEV; + *phys = priv->phys; + return 0; +} +EXPORT_SYMBOL_FOR_MODULES(gmem_provider_dma_buf_iommufd_map, "iommufd"); + +/* 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); + DEFINE_DMA_BUF_EXPORT_INFO(exp_info); + struct gmem_dmabuf *priv; + int fd; + + 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.flags = O_RDWR; + exp_info.priv = priv; + + priv->dmabuf = dma_buf_export(&exp_info); + if (IS_ERR(priv->dmabuf)) { + fd = PTR_ERR(priv->dmabuf); + kfree(priv); + return fd; + } + + mutex_lock(&info->dmabufs_lock); + list_add(&priv->list, &info->dmabufs); + mutex_unlock(&info->dmabufs_lock); + + fd = dma_buf_fd(priv->dmabuf, O_CLOEXEC); + if (fd < 0) + dma_buf_put(priv->dmabuf); + return fd; +} + +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; + + if (cmd == GMEM_PROVIDER_GET_DMABUF) + return gmem_provider_get_dmabuf(file); + + if (cmd != GMEM_PROVIDER_SET_PRESENT) + return -ENOTTY; + if (copy_from_user(&p, (void __user *)arg, sizeof(p))) + return -EFAULT; + if (!p.len || !PAGE_ALIGNED(p.offset) || !PAGE_ALIGNED(p.len)) + return -EINVAL; + + start_index = p.offset >> PAGE_SHIFT; + end_index = start_index + (p.len >> PAGE_SHIFT); + if (end_index > info->npages || end_index < start_index) + return -EINVAL; + + if (p.present) { + /* Restore: next guest fault calls get_pfn() and re-maps. */ + bitmap_clear(info->absent, start_index, end_index - start_index); + } else { + /* Revoke: mark absent, then zap the guest NPT/EPT for the range. */ + bitmap_set(info->absent, start_index, end_index - start_index); + if (info->kvm) + kvm_gmem_invalidate_range(info->kvm, + info->base_gfn + start_index - info->pgoff, + info->base_gfn + end_index - info->pgoff); + + /* + * 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); + } + return 0; +} + +static long gmem_ctl_ioctl(struct file *file, unsigned int cmd, unsigned long arg) +{ + struct gmem_provider_setup setup; + 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; + + if (copy_from_user(&setup, (void __user *)arg, sizeof(setup))) + return -EFAULT; + if (setup.flags & ~GMEM_PROVIDER_FLAG_MMAP_CAPABLE) + return -EINVAL; + + kvm_file = fget(setup.kvm_fd); + if (!kvm_file) + return -EBADF; + kvm = kvm_file->private_data; + if (!kvm) { + fput(kvm_file); + return -EINVAL; + } + kvm_get_kvm(kvm); + fput(kvm_file); + + info = kzalloc(sizeof(*info), GFP_KERNEL); + if (!info) { + ret = -ENOMEM; + goto err_put_kvm; + } + + if (addr && len) { + /* External page-less range from module params. */ + info->base_pfn = addr >> PAGE_SHIFT; + info->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; + } + npages = setup.size >> PAGE_SHIFT; + pages = alloc_contig_pages(npages, GFP_KERNEL, numa_node_id(), NULL); + if (!pages) { + ret = -ENOMEM; + goto err_free_info; + } + /* 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; + } + + info->absent = bitmap_zalloc(info->npages, GFP_KERNEL); + if (!info->absent) { + ret = -ENOMEM; + goto err_free_pages; + } + + INIT_LIST_HEAD(&info->dmabufs); + mutex_init(&info->dmabufs_lock); + + info->backing.ops = &gmem_ops; + info->kvm = kvm; + info->mmap_capable = !!(setup.flags & GMEM_PROVIDER_FLAG_MMAP_CAPABLE); + + fd = anon_inode_getfd("[gmem-provider]", &kvm_gmem_fops, + &info->backing, O_RDWR | O_CLOEXEC); + if (fd < 0) { + ret = fd; + goto err_free_pages; + } + return fd; + +err_free_pages: + if (pages) + free_contig_range(page_to_pfn(pages), npages); +err_free_info: + bitmap_free(info->absent); + kfree(info); +err_put_kvm: + kvm_put_kvm(kvm); + return ret; +} + +static const struct file_operations gmem_ctl_fops = { + .owner = THIS_MODULE, + .unlocked_ioctl = gmem_ctl_ioctl, + .compat_ioctl = gmem_ctl_ioctl, +}; + +static struct miscdevice gmem_dev = { + .minor = MISC_DYNAMIC_MINOR, + .name = "gmem_provider", + .fops = &gmem_ctl_fops, +}; + +static int __init gmem_provider_init(void) +{ + 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); +} +module_init(gmem_provider_init); + +static void __exit gmem_provider_exit(void) +{ + misc_deregister(&gmem_dev); +} +module_exit(gmem_provider_exit); + +MODULE_LICENSE("GPL"); +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 new file mode 100644 index 000000000000..45f1b8257f60 --- /dev/null +++ b/samples/kvm/gmem_provider.h @@ -0,0 +1,53 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef _SAMPLES_KVM_GMEM_PROVIDER_H +#define _SAMPLES_KVM_GMEM_PROVIDER_H + +#include <linux/ioctl.h> +#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. + * + * 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). */ + +struct gmem_provider_setup { + __s32 kvm_fd; /* an open KVM VM fd */ + __u32 flags; /* GMEM_PROVIDER_FLAG_* */ + __u64 size; /* CMA fallback size in bytes, page aligned */ +}; + +#define GMEM_PROVIDER_IOCTL_BASE 'G' +#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. + */ +struct gmem_provider_present { + __u64 offset; /* byte offset into the provider region, page aligned */ + __u64 len; /* byte length, page aligned */ + __u32 present; /* 0 = revoke (absent), 1 = restore (present) */ + __u32 pad; +}; + +#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 + * 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 tear down the IOMMU mapping so DMA to the reclaimed range faults. + */ +#define GMEM_PROVIDER_GET_DMABUF _IO(GMEM_PROVIDER_IOCTL_BASE, 3) + +#endif /* _SAMPLES_KVM_GMEM_PROVIDER_H */ diff --git a/virt/kvm/guest_memfd.c b/virt/kvm/guest_memfd.c index 6c13742de068..2cb1b6a9d73f 100644 --- a/virt/kvm/guest_memfd.c +++ b/virt/kvm/guest_memfd.c @@ -546,7 +546,7 @@ struct file_operations kvm_gmem_fops = { .unlocked_ioctl = kvm_gmem_fops_ioctl, .compat_ioctl = kvm_gmem_fops_ioctl, }; -EXPORT_SYMBOL_FOR_KVM_INTERNAL(kvm_gmem_fops); +EXPORT_SYMBOL_GPL(kvm_gmem_fops); static int kvm_gmem_migrate_folio(struct address_space *mapping, struct folio *dst, struct folio *src, -- 2.54.0

