[PATCH RFC v2 48/50] samples/kvm: Support dynamic revocation via kvm_gmem_revoke

From: Ackerley Tng via B4 Relay

Date: Thu Oct 22 2026 - 19:42:06 EST


From: Ackerley Tng <ackerleytng@xxxxxxxxxx>

Support dynamic revocation in the sample memory provider module by
calling kvm_gmem_revoke() when pages are marked absent via
MPS_SET_PRESENT.

Note down the attaching gmem inode during .attach() and disallow double
attachment to the same provider file.

Clear the revoked pages after kvm_gmem_revoke() using mps_zero_range()
to ensure that stale data is wiped and cannot be observed when pages are
later marked present or reused.

Regarding locking and ordering:
- In mps_set_present(), mark the absent bitmap first before revoking so
any concurrent page fault immediately sees the page as absent and
rejects the lookup with -EFAULT.
- kvm_gmem_revoke() is called under info->lock to fully serialize
MPS_SET_PRESENT operations against each other and prevent races
between marking pages absent and present.
- The PFN passed to kvm_gmem_revoke() is derived from info->base_pfn +
start. The mapping between page index and physical PFN is static and
immutable for the lifetime of the provider file, ensuring that the
pfn mapping remains completely stable across the revoke call.
- Passing pfn directly to kvm_gmem_revoke() allows guest_memfd to
reclaim the physical range directly without calling back into the
provider. This keeps the lock hierarchy strictly top-down (info->lock
-> filemap_invalidate_lock -> mmu_lock) with no callbacks or deadlocks.

For now, nothing needs to be done when setting a page present, since users
should (re-)fault and (re-)obtain the PFN via .get_pfn(), to be added in a
later patch.

For users that don't support faulting, like the IOMMU, the counterpart to
kvm_gmem_revoke() will be made available for the first user to indicate
that the memory is now available. guest_memfd should then do the page table
mappings as necessary, based on the shared/private status.

Signed-off-by: Ackerley Tng <ackerleytng@xxxxxxxxxx>
---
samples/Kconfig | 2 +-
samples/kvm/mem_provider_sample.c | 42 +++++++++++++++++++++++++++++++++------
2 files changed, 37 insertions(+), 7 deletions(-)

diff --git a/samples/Kconfig b/samples/Kconfig
index 29a0efe49ac3a..1b227eec8bb6d 100644
--- a/samples/Kconfig
+++ b/samples/Kconfig
@@ -328,7 +328,7 @@ source "samples/damon/Kconfig"

config SAMPLE_KVM_MEM_PROVIDER
tristate "Build sample memory provider -- loadable module only"
- depends on CONTIG_ALLOC && X86_64 && m
+ depends on CONTIG_ALLOC && X86_64 && m && KVM_GUEST_MEMFD
help
This builds a sample memory provider.
Its files can back a guest_memfd and be mapped by iommufd.
diff --git a/samples/kvm/mem_provider_sample.c b/samples/kvm/mem_provider_sample.c
index dee15179959b2..3fa0d1a4335b5 100644
--- a/samples/kvm/mem_provider_sample.c
+++ b/samples/kvm/mem_provider_sample.c
@@ -39,13 +39,13 @@ struct mps_info {
struct page *cma_pages; /* from alloc_contig_pages() */

unsigned long *absent; /* taken away by MPS_SET_PRESENT */
+
+ struct inode *gmem_inode;
};

-static void mps_gmem_put_pfn_range(void *provider, pgoff_t index,
- size_t nr_pages)
+static void mps_zero_range(struct mps_info *info, pgoff_t index,
+ size_t nr_pages)
{
- struct file *resource_file = provider;
- struct mps_info *info = resource_file->private_data;
size_t i;

if (index >= info->npages)
@@ -56,6 +56,14 @@ static void mps_gmem_put_pfn_range(void *provider, pgoff_t index,
clear_highpage(info->cma_pages + index + i);
}

+static void mps_gmem_put_pfn_range(void *provider, pgoff_t index,
+ size_t nr_pages)
+{
+ struct file *resource_file = provider;
+
+ mps_zero_range(resource_file->private_data, index, nr_pages);
+}
+
/* Byte range [offset, offset + len) of @info as page indices. */
static int mps_range(struct mps_info *info, u64 offset, u64 len,
unsigned long *start, unsigned long *end)
@@ -73,10 +81,17 @@ static void mps_set_present(struct mps_info *info, unsigned long start,
unsigned long end, bool present)
{
mutex_lock(&info->lock);
- if (present)
+ if (present) {
bitmap_clear(info->absent, start, end - start);
- else
+ } else {
bitmap_set(info->absent, start, end - start);
+ if (info->gmem_inode) {
+ unsigned long pfn = info->base_pfn + start;
+
+ kvm_gmem_revoke(info->gmem_inode, start, end - start, &pfn);
+ mps_zero_range(info, start, end - start);
+ }
+ }
mutex_unlock(&info->lock);
}

@@ -138,12 +153,27 @@ static long mps_file_ioctl(struct file *file, unsigned int cmd,
static void *mps_gmem_attach(struct inode *gmem_inode,
struct file *resource_file)
{
+ struct mps_info *info = resource_file->private_data;
+
+ mutex_lock(&info->lock);
+ if (info->gmem_inode) {
+ mutex_unlock(&info->lock);
+ return ERR_PTR(-EBUSY);
+ }
+ info->gmem_inode = gmem_inode;
+ mutex_unlock(&info->lock);
+
return get_file(resource_file);
}

static void mps_gmem_release(void *provider)
{
struct file *resource_file = provider;
+ struct mps_info *info = resource_file->private_data;
+
+ mutex_lock(&info->lock);
+ info->gmem_inode = NULL;
+ mutex_unlock(&info->lock);

fput(resource_file);
}

--
2.56.0.257.gf7497c7d27-goog