[RFC PATCH 04/14] KVM: guest_memfd: Keep truncated folios until the exports are updated
From: Fred Griffoul
Date: Tue Oct 27 2026 - 11:59:04 EST
From: Fred Griffoul <fgriffo@xxxxxxxxxxxx>
A punch hole frees the folios it truncates at once, but an export may
still map them, since it holds no reference: a device would then write
into freed memory.
Truncate a folio mode range batch by batch, holding a reference on each
folio until the exports are updated, so that a folio goes back to the
allocator only once no device maps it.
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Fred Griffoul <fgriffo@xxxxxxxxxxxx>
---
include/linux/guest_memfd.h | 28 ++++++++++++++--------------
virt/kvm/guest_memfd.c | 30 ++++++++++++++++++++++++++++--
2 files changed, 42 insertions(+), 16 deletions(-)
diff --git a/include/linux/guest_memfd.h b/include/linux/guest_memfd.h
index fdf7f8404d6c..ba3eebb1ed1a 100644
--- a/include/linux/guest_memfd.h
+++ b/include/linux/guest_memfd.h
@@ -89,14 +89,14 @@ static inline int kvm_gmem_revoke(struct inode *inode, pgoff_t start, size_t nr_
* A subsystem that maps guest_memfd pages, such as iommufd, can neither pin
* nor fault them. It attaches to the file, looks up the frame behind each
* shared page without taking a reference, and guest_memfd calls its update()
- * whenever what a lookup returns for a range may have changed: conversions and
- * memory failure. update() makes the consumer's mappings of the range match
- * what a lookup returns now: a frame, or a hole for a private page or a page
- * without a frame. It may be called for pages that did not change, and for
- * ranges that do not follow the blocks that lookups report: the consumer may
- * then remap whole blocks, also outside the range, e.g. to merge them into
- * larger mappings. A consumer that can fault may instead only remove its
- * mappings of the range.
+ * whenever what a lookup returns for a range may have changed: conversions,
+ * punch holes and memory failure. update() makes the consumer's mappings of
+ * the range match what a lookup returns now: a frame, or a hole for a private
+ * page or a page without a frame. It may be called for pages that did not
+ * change, and for ranges that do not follow the blocks that lookups report:
+ * the consumer may then remap whole blocks, also outside the range, e.g. to
+ * merge them into larger mappings. A consumer that can fault may instead only
+ * remove its mappings of the range.
*
* Until update() returns, the frames it replaces stay valid, so a consumer
* may switch each mapping to its new target in one step; once it returns,
@@ -104,12 +104,12 @@ static inline int kvm_gmem_revoke(struct inode *inode, pgoff_t start, size_t nr_
* that needs memory to apply one, e.g. to split a large mapping, reserves it
* beforehand.
*
- * guest_memfd calls update() with its invalidate lock held, exclusive when
- * pages convert and shared on memory failure, possibly with a folio of the
- * file locked; so update() may only call kvm_gmem_export_lookup_pfn(). Under
- * the shared lock, updates may run at the same time, also for overlapping
- * ranges. The consumer holds a lock across a lookup and the mapping it
- * installs, which update() also takes. Lock order:
+ * guest_memfd calls update() with its invalidate lock held, exclusive when it
+ * takes frames away or pages convert and shared on memory failure, possibly
+ * with a folio of the file locked; so update() may only call
+ * kvm_gmem_export_lookup_pfn(). Under the shared lock, updates may run at the
+ * same time, also for overlapping ranges. The consumer holds a lock across a
+ * lookup and the mapping it installs, which update() also takes. Lock order:
*
* guest_memfd invalidate lock
* consumer lock
diff --git a/virt/kvm/guest_memfd.c b/virt/kvm/guest_memfd.c
index 81031483a1f1..65a951fa49c2 100644
--- a/virt/kvm/guest_memfd.c
+++ b/virt/kvm/guest_memfd.c
@@ -424,7 +424,7 @@ static void kvm_gmem_invalidate_end(struct inode *inode, pgoff_t start,
/*
* What a lookup returns for [start, end) may have changed: exclusive lock when
- * pages converted, shared on memory failure.
+ * frames were taken away or pages converted, at least shared on memory failure.
*/
static void kvm_gmem_update_exports(struct inode *inode, pgoff_t start,
pgoff_t end)
@@ -437,6 +437,32 @@ static void kvm_gmem_update_exports(struct inode *inode, pgoff_t start,
e->ops->update(e, start, end - start);
}
+/*
+ * Truncate [start, end) of a folio mode file. The exports map the folios
+ * without references, so hold the folios until the exports are updated.
+ */
+static void kvm_gmem_truncate(struct inode *inode, pgoff_t start, pgoff_t end)
+{
+ struct folio_batch fbatch;
+ pgoff_t next = start;
+ unsigned int i;
+
+ folio_batch_init(&fbatch);
+ while (filemap_get_folios(inode->i_mapping, &next, end - 1, &fbatch)) {
+ pgoff_t first = max(fbatch.folios[0]->index, start);
+ pgoff_t last = min(next, end);
+
+ truncate_inode_pages_range(inode->i_mapping,
+ (loff_t)first << PAGE_SHIFT,
+ ((loff_t)last << PAGE_SHIFT) - 1);
+ kvm_gmem_update_exports(inode, first, last);
+ for (i = 0; i < folio_batch_count(&fbatch); i++)
+ folio_put(fbatch.folios[i]);
+ folio_batch_reinit(&fbatch);
+ cond_resched();
+ }
+}
+
#ifdef CONFIG_HAVE_KVM_ARCH_GMEM_RECLAIM
static void kvm_gmem_pfn_reclaim(struct gmem_inode *gi, pgoff_t start, pgoff_t end)
{
@@ -499,7 +525,7 @@ static int __kvm_gmem_revoke(struct inode *inode, pgoff_t start,
if (put_pfn_range)
gmem_provider_put_pfn_range(GMEM_I(inode), start, nr_pages);
} else {
- truncate_inode_pages_range(inode->i_mapping, offset, offset + len - 1);
+ kvm_gmem_truncate(inode, start, end);
}
kvm_gmem_invalidate_end(inode, start, end);
--
2.47.3