Re: [PATCH v3 3/7] KVM: pfncache: Use RCU for readers instead of a rwlock

Sean Christopherson <[email protected]>
Newsgroups dev.linux.lists.sashiko-reviews,org.kernel.vger.kvm
Message-ID <[email protected]>
On Wed, Aug 05, 2026, [email protected] wrote:
> Replace the per-cache rwlock with RCU for the read side.

I don't hate the idea, but I am very against using RCU.  Unless it's "impossible",
e.g. because synchronize_srcu() allocates memory and breaks OOM kill, I would
strongly prefer to use SRCU, probably with a dedicated kvm->gpc_srcu, so that
synchronization doesn't need to wait on all CPUs in the system.  The tail latencies
for synchronize_rcu() are horrendous, especially for many-CPU systems.  If it
were only mmu_notifiers that got hit, it miiiight be acceptable, but since this
will affect vCPU tasks in the refresh() path as well, normal RCU is pretty much
a non-starter.

Even SRCU could be problematic: if synchronize_srcu_expedited() is forced to wait,
the wait time can easily get to 20+ milliseconds, which again is a non-starter for
things like steal-time updates and nVMX pages.  Simply using a per-GPC SRCU would
be gross, as gfn_to_pfn_cache_invalidate_start() would become absurdly complex in
order to juggle gpc_lock with synchronize_srcu_expedited().

A somewhat crazy idea would be to have a per-VM gpc_srcu, *and* a per-GPC srcu.
Readers would take both, refresh() would sync gpc->srcu, and invalidation would
sync kvm->gpc_srcu.  That way, refresh() wouldn't need to wait on concurrent
readers of *other* GPCs.

Actually, a better idea: use kvm->gpc_srcu to synchronize invalidations and
refresh() for GPCs that aren't tightly coupled to a vCPU, but for GPCs that are
*only* accessed by a single loaded vCPU, protect readers and refresh() with
vcpu->mutex.  That way, single-vCPU GPCs wouldn't need to synchronize() on refresh(),
because by definition there can't be concurrent readers with refresh().

That would basically punt on optimizing most of the Xen GPCs, but that's probably
ok?  Because the hot path GPCs, e.g. runstate_cache{,2}, are generally associated
1:1 with a vCPU, i.e. can avoid synchronizing on SRCU.  The one GPC that I see
as being problematic is vcpu_info_cache, because it's accesses cross-vCPU and so
the owning vCPU would need to synchronize() on refresh().  But if you're ok with
potentially high tail latencies if the vcpu_info_cache page is migrated or
reclaimed, then I doubt anyone else will complain.

> > --- a/virt/kvm/pfncache.c
> > +++ b/virt/kvm/pfncache.c
> > @@ -26,35 +26,49 @@ void gfn_to_pfn_cache_invalidate_start(struct kvm *kvm, unsigned long start,
> >  				       unsigned long end)
> >  {
> >  	struct gfn_to_pfn_cache *gpc;
> > +	bool cleared = false;
> >  
> >  	spin_lock(&kvm->gpc_lock);
> >  	list_for_each_entry(gpc, &kvm->gpc_list, list) {
> > -		read_lock_irq(&gpc->lock);
> > -
> > -		/* Only a single page so no need to care about length */
> > -		if (gpc->valid && !is_error_noslot_pfn(gpc->pfn) &&
> > +		if (smp_load_acquire(&gpc->valid) &&
> >  		    gpc->uhva >= start && gpc->uhva < end) {
> > -			read_unlock_irq(&gpc->lock);
> > -
> > -			/*
> > -			 * There is a small window here where the cache could
> > -			 * be modified, and invalidation would no longer be
> > -			 * necessary. Hence check again whether invalidation
> > -			 * is still necessary once the write lock has been
> > -			 * acquired.
> > -			 */
> > -
> > -			write_lock_irq(&gpc->lock);
> > -			if (gpc->valid && !is_error_noslot_pfn(gpc->pfn) &&
> > -			    gpc->uhva >= start && gpc->uhva < end)
> > -				gpc->valid = false;
> > -			write_unlock_irq(&gpc->lock);
> > -			continue;
> > +			WRITE_ONCE(gpc->valid, false);
> > +			cleared = true;
> >  		}
> > -
> > -		read_unlock_irq(&gpc->lock);
> >  	}
> >  	spin_unlock(&kvm->gpc_lock);
> > +
> > +	/*
> > +	 * Readers may still be using the old mapping, having sampled
> > +	 * gpc->valid before it was cleared. Wait for them all to drain
> > +	 * before the caller proceeds to zap the page tables; like a TLB
> > +	 * shootdown, this guarantees no access via the stale mapping
> > +	 * once the invalidation completes.
> > +	 *
> > +	 * This wait is safe even on unblockable ranges (the OOM reaper):
> > +	 * the constraint there is not "no sleeping" but "no blocking on
> > +	 * anything which may itself depend on memory allocation to make
> > +	 * progress" (see the reasoning in commit 312364f3534c and its
> > +	 * discussion). An RCU grace period has no such dependency: GPC
> > +	 * readers never allocate, never take mmap_lock, and never sleep.
> > +	 */
> > +	if (cleared)
> > +		synchronize_rcu();
> >  }
> 
> [Severity: Critical]
> Could this conditional wait bypass the grace period and lead to a regression
> with physical pages being freed too early?
> 
> If a concurrent mutator like __kvm_gpc_refresh transitions gpc->valid to
> false and is currently blocking on its own synchronize_rcu wait, the MMU
> notifier will see valid as false, skip the wait since cleared is false, and
> return immediately.
> 
> This would allow the host memory subsystem to free the page while the
> original mutator is still waiting for active RCU readers to finish.

Ya, pretty sure, Sashiko is correct.  The mmu_notifier rules require that all
mappings are removed before .invalidate_range_start() completes.  Seems like the
easiest solution is to key off of gpc->active, not gpc->valid.

> [ ... ]
> > @@ -74,6 +88,22 @@ bool kvm_gpc_check(struct gfn_to_pfn_cache *gpc, unsigned long len)
> >  {
> >  	struct kvm_memslots *slots = kvm_memslots(gpc->kvm);
> >  
> > +	RCU_LOCKDEP_WARN(!rcu_read_lock_held(),
> > +			 "kvm_gpc_check() without RCU read lock");
> > +
> > +	/*
> > +	 * Check valid *first*. The acquire pairs with the release-publish
> > +	 * in hva_to_pfn_retry(), so every field read below — and any use
> > +	 * of gpc->khva by the caller — is guaranteed to be from the
> > +	 * published generation, not a stale value reordered from before
> > +	 * the publish. The fields are then stable for the remainder of
> > +	 * the RCU read-side critical section, because every mutator
> > +	 * clears valid and waits a full grace period before changing
> > +	 * anything.
> > +	 */
> > +	if (!smp_load_acquire(&gpc->valid))
> > +		return false;
> > +
> >  	if (!gpc->active)
> >  		return false;
> 
> [Severity: Medium]
> Should the read of gpc->generation use READ_ONCE?

+1, the {WRITE,READ}_ONCE() usage looks to be very inconsistent.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.