[RFC PATCH 12/14] samples/kvm: Add read-only pages and large blocks to the sample provider
From: Fred Griffoul
Date: Tue Oct 27 2026 - 12:03:08 EST
From: Fred Griffoul <fgriffo@xxxxxxxxxxxx>
The sample provider reports every page as writable and of order 0, and
does nothing when a page comes back, so tests cannot exercise read-only
frames, large mappings, or a device mapping that follows a page made
present again.
Add MPS_SET_READONLY, report the largest aligned block of pages in the
same state, and call kvm_gmem_restore() when pages become present or
writable again; making pages read only revokes them.
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Fred Griffoul <fgriffo@xxxxxxxxxxxx>
---
samples/kvm/mem_provider_sample.c | 89 +++++++++++++++++++++++++++----
samples/kvm/mem_provider_sample.h | 9 ++++
2 files changed, 87 insertions(+), 11 deletions(-)
diff --git a/samples/kvm/mem_provider_sample.c b/samples/kvm/mem_provider_sample.c
index f30654a60dd8..670e36207e59 100644
--- a/samples/kvm/mem_provider_sample.c
+++ b/samples/kvm/mem_provider_sample.c
@@ -6,7 +6,8 @@
*
* The control device /dev/mem_provider_sample creates provider files.
* Each file allocates its own contiguous memory from alloc_contig_pages().
- * Pages can be dynamically marked present or absent.
+ * Pages can be dynamically marked present or absent, and read only (for
+ * tests).
* See mem_provider_sample.h.
*
* Confidential VMs are not supported.
@@ -39,6 +40,7 @@ struct mps_info {
struct page *cma_pages; /* from alloc_contig_pages() */
unsigned long *absent; /* taken away by MPS_SET_PRESENT */
+ unsigned long *readonly; /* set by MPS_SET_READONLY */
struct inode *gmem_inode;
};
@@ -83,6 +85,9 @@ static void mps_set_present(struct mps_info *info, unsigned long start,
mutex_lock(&info->lock);
if (present) {
bitmap_clear(info->absent, start, end - start);
+ /* Tell the users of guest_memfd that cannot fault. */
+ if (info->gmem_inode)
+ kvm_gmem_restore(info->gmem_inode, start, end - start);
} else {
bitmap_set(info->absent, start, end - start);
if (info->gmem_inode) {
@@ -95,6 +100,31 @@ static void mps_set_present(struct mps_info *info, unsigned long start,
mutex_unlock(&info->lock);
}
+/*
+ * Change what get_pfn() reports for [start, end). Making it read only takes
+ * write access away, so revoke it: the users of the guest_memfd map it again
+ * read only. Making it writable takes nothing away, so only tell the users
+ * that cannot fault. The pages keep their contents.
+ */
+static void mps_set_readonly(struct mps_info *info, unsigned long start,
+ unsigned long end, bool value)
+{
+ unsigned long pfn = info->base_pfn + start;
+
+ mutex_lock(&info->lock);
+ if (value) {
+ bitmap_set(info->readonly, start, end - start);
+ if (info->gmem_inode)
+ kvm_gmem_revoke(info->gmem_inode, start, end - start,
+ &pfn);
+ } else {
+ bitmap_clear(info->readonly, start, end - start);
+ if (info->gmem_inode)
+ kvm_gmem_restore(info->gmem_inode, start, end - start);
+ }
+ mutex_unlock(&info->lock);
+}
+
static long mps_get_stats(struct mps_info *info, void __user *uarg)
{
struct mps_stats st = {};
@@ -114,11 +144,13 @@ static int mps_file_release(struct inode *inode, struct file *file)
if (info->cma_pages)
free_contig_range(info->base_pfn, info->npages);
bitmap_free(info->absent);
+ bitmap_free(info->readonly);
kfree(info);
return 0;
}
-static long mps_file_set_present(struct mps_info *info, void __user *uarg)
+static long mps_file_set_range(struct mps_info *info, void __user *uarg,
+ unsigned int cmd)
{
struct mps_set_present sp;
unsigned long start, end;
@@ -129,9 +161,13 @@ static long mps_file_set_present(struct mps_info *info, void __user *uarg)
if (sp.value > 1 || sp.pad)
return -EINVAL;
ret = mps_range(info, sp.offset, sp.len, &start, &end);
- if (!ret)
+ if (ret)
+ return ret;
+ if (cmd == MPS_SET_PRESENT)
mps_set_present(info, start, end, sp.value);
- return ret;
+ else
+ mps_set_readonly(info, start, end, sp.value);
+ return 0;
}
static long mps_file_ioctl(struct file *file, unsigned int cmd,
@@ -144,7 +180,8 @@ static long mps_file_ioctl(struct file *file, unsigned int cmd,
case MPS_GET_STATS:
return mps_get_stats(info, uarg);
case MPS_SET_PRESENT:
- return mps_file_set_present(info, uarg);
+ case MPS_SET_READONLY:
+ return mps_file_set_range(info, uarg, cmd);
default:
return -ENOTTY;
}
@@ -178,11 +215,28 @@ static void mps_gmem_release(void *provider)
fput(resource_file);
}
+/* Is every bit of @bitmap in [first, first + n) equal to @value? */
+static bool mps_bits_are(unsigned long *bitmap, unsigned long first,
+ unsigned long n, bool value)
+{
+ unsigned long end = first + n;
+
+ return (value ? find_next_zero_bit(bitmap, end, first) :
+ find_next_bit(bitmap, end, first)) >= end;
+}
+
+/*
+ * The frame behind @index, and the largest aligned block around it that is
+ * present and has the same attributes: the file's frames are contiguous.
+ * Lockless: a change is followed by kvm_gmem_revoke() or kvm_gmem_restore().
+ */
static int mps_gmem_get_pfn(void *provider, pgoff_t index, unsigned long *pfn,
int *max_order, u32 *attrs)
{
struct file *resource_file = provider;
struct mps_info *info = resource_file->private_data;
+ bool ro;
+ int order;
if (index >= info->npages)
return -EINVAL;
@@ -190,10 +244,22 @@ static int mps_gmem_get_pfn(void *provider, pgoff_t index, unsigned long *pfn,
if (test_bit(index, info->absent))
return -EFAULT;
+ ro = test_bit(index, info->readonly);
*pfn = info->base_pfn + index;
- if (max_order)
- *max_order = 0;
-
+ *attrs = ro ? GUEST_MEMFD_PFN_READONLY : 0;
+
+ if (!max_order)
+ return 0;
+ for (order = PUD_ORDER; order; order--) {
+ unsigned long first = ALIGN_DOWN(index, 1UL << order);
+ unsigned long n = 1UL << order;
+
+ if (first + n <= info->npages &&
+ mps_bits_are(info->absent, first, n, false) &&
+ mps_bits_are(info->readonly, first, n, ro))
+ break;
+ }
+ *max_order = order;
return 0;
}
@@ -255,9 +321,10 @@ static long mps_ctl_new_file(void __user *uarg)
return -ENOMEM;
info->absent = bitmap_zalloc(npages, GFP_KERNEL);
- if (!info->absent) {
+ info->readonly = bitmap_zalloc(npages, GFP_KERNEL);
+ if (!info->absent || !info->readonly) {
ret = -ENOMEM;
- goto err_free_info;
+ goto err_free_absent;
}
pages = alloc_contig_pages(npages, GFP_KERNEL | __GFP_ZERO,
@@ -293,7 +360,7 @@ static long mps_ctl_new_file(void __user *uarg)
free_contig_range(page_to_pfn(pages), npages);
err_free_absent:
bitmap_free(info->absent);
-err_free_info:
+ bitmap_free(info->readonly);
kfree(info);
return ret;
}
diff --git a/samples/kvm/mem_provider_sample.h b/samples/kvm/mem_provider_sample.h
index b9f10286cb30..57f494dc46fa 100644
--- a/samples/kvm/mem_provider_sample.h
+++ b/samples/kvm/mem_provider_sample.h
@@ -51,4 +51,13 @@ struct mps_set_present {
#define MPS_SET_PRESENT \
_IOW(MPS_IOCTL_BASE, 12, struct mps_set_present)
+/*
+ * ioctl on a provider fd, with the same argument as MPS_SET_PRESENT: report
+ * [@offset, @offset + @len) as read only (GUEST_MEMFD_PFN_READONLY, @value 1)
+ * or writable again (@value 0). Every mapping follows. For tests: the
+ * sample does not really share the pages.
+ */
+#define MPS_SET_READONLY \
+ _IOW(MPS_IOCTL_BASE, 13, struct mps_set_present)
+
#endif /* _SAMPLES_KVM_MEM_PROVIDER_SAMPLE_H */
--
2.47.3