Re: [PATCH v4 35/48] KVM: arm64: gic-v5: Implement save/restore mechanisms for ISTs

Sascha Bischoff <[email protected]>
Newsgroups org.kernel.vger.kvm,dev.linux.lists.kvmarm,dev.linux.lists.sashiko-reviews
Message-ID <[email protected]>
On Fri, 2026-07-24 at 12:02 +0000, [email protected] wrote:
> Thank you for your contribution! Sashiko AI review found 8 potential
> issue(s) to consider:
> - [Critical] Retrieving `vcpu0` without verifying it exists leads to
> a NULL pointer dereference.
> - [Critical] Inverted lock check with `kvm_trylock_all_vcpus` leaks
> vCPU locks and runs lockless.
> - [High] Incomplete cleanup on IST restore failure leaves a dangling
> pointer in `vmi->h_lpi_ist`, leading to a double free.
> - [High] Missing validation of LPI INTIDs during IST restore allows
> userspace to inject invalid INTIDs.
> - [High] SPI IST restore incorrectly uses the array index directly as
> the absolute INTID, causing wrong interrupts to be injected.
> - [High] Quiescence check masks lost quiescence state and causes
> silent data corruption.
> - [Medium] KVM_DEV_ARM_VGIC_GRP_IST unconditionally fails for VMs
> configured with 0 SPIs, breaking live migration.
> - [Medium] The failure path in `vgic_v5_irs_restore_ists` incorrectly
> issues a redundant `VMTE_MAKE_INVALID` command, risking a memory
> leak.
> --
> 
> --- Patch [35]: [PATCH v4 35/48] KVM: arm64: gic-v5: Implement
> save/restore mechanisms for ISTs ---
> commit 5c47453f1884c74d1478029039189ee4ba68421a
> Author: Sascha Bischoff <[email protected]>
> Subject: KVM: arm64: gic-v5: Implement save/restore mechanisms for
> ISTs
> 
> This patch introduces save and restore mechanisms for GICv5 Interrupt
> State
> Tables (ISTs). It provides userspace migration storage for SPI and
> LPI
> buffers via KVM_DEV_ARM_VGIC_GRP_IST and ensures the VM is quiesced
> and
> validated during operations.
> 
> > diff --git a/arch/arm64/kvm/vgic/vgic-v5-tables.c
> > b/arch/arm64/kvm/vgic/vgic-v5-tables.c
> > index 718410e8c9f3b..f220267266c29 100644
> > --- a/arch/arm64/kvm/vgic/vgic-v5-tables.c
> > +++ b/arch/arm64/kvm/vgic/vgic-v5-tables.c
> 
> [ ... ]
> 
> > @@ -1630,6 +1630,28 @@ static int vgic_v5_restore_linear_ist(struct
> > kvm *kvm,
> >  				      const struct
> > vgic_v5_ist_desc *ist,
> >  				      u32 __user *uaddr, size_t
> > nr_entries,
> >  				      u32 intid_type)
> >  {
> >  	__le32 h_iste;
> >  	size_t index;
> >  	int ret;
> >  
> >  	for (index = 0; index < nr_entries; index++) {
> >  		void *h_iste_addr = ist->base + index * ist-
> > >iste_size;
> >  
> >  		ret = get_user(h_iste, uaddr);
> >  		if (ret)
> >  			return ret;
> >  
> >  		ret = vgic_v5_restore_ist_entry(kvm, ist,
> > h_iste_addr,
> >  					h_iste, index,
> > intid_type);
> 
> [Severity: High]
> Should the SPI base offset be added to the index here?

No. Definitely not. That's a GICv3 thing, but not a GICv5 thing.

> 
> When restoring SPIs, passing the raw array index as the intid will
> track and
> inject interrupts starting at 0. Since hardware SPIs start at INTID
> 32, does
> this incorrectly inject SGIs and PPIs instead of the intended SPIs?

Except they don't with GICv5.

> 
> >  		if (ret)
> >  			return ret;
> >  
> >  		uaddr++;
> >  	}
> >  
> >  	return 0;
> >  }
> 
> [ ... ]
> 
> > @@ -1662,6 +1684,41 @@ static int
> > vgic_v5_restore_two_level_ist(struct kvm *kvm,
> >  					 const struct
> > vgic_v5_ist_desc *ist,
> >  					 u32 __user *uaddr, u32
> > intid_type)
> >  {
> 
> [ ... ]
> 
> >  		for (h_l2_index = 0; h_l2_index <
> > shape.l2_entries; h_l2_index++) {
> >  			void *h_iste_addr = h_l2_ist_base +
> >  					    h_l2_index * ist-
> > >iste_size;
> >  			u32 intid = h_l1_index * shape.l2_entries
> > + h_l2_index;
> >  
> >  			ret = get_user(h_iste, uaddr);
> >  			if (ret)
> >  				return ret;
> >  
> >  			ret = vgic_v5_restore_ist_entry(kvm, ist,
> > h_iste_addr,
> >  							h_iste,
> > intid,
> >  							intid_type
> > );
> 
> [Severity: High]
> Is there any validation missing for the generated LPI INTIDs?

No? Why would there be? Only LPIs in the table are valid. LPIs outside
of the table are not valid.

> 
> The iteration calculates intid starting from 0. If userspace sets
> pending
> bits for these lower indices, KVM would track them and inject them
> via the
> VDPEND instruction at EL2.

Yes? And?

> 
> Since valid LPI INTIDs must be >= 8192, could injecting lower IDs
> result in
> architecturally unpredictable hardware behavior?

No, that's GICv3 again!

> 
> >  			if (ret)
> >  				return ret;
> >  
> >  			uaddr++;
> >  		}
> >  	}
> >  
> >  	return 0;
> >  }
> 
> > diff --git a/arch/arm64/kvm/vgic/vgic-v5.c
> > b/arch/arm64/kvm/vgic/vgic-v5.c
> > index 6af1264ed2371..153a8fcd4db7c 100644
> > --- a/arch/arm64/kvm/vgic/vgic-v5.c
> > +++ b/arch/arm64/kvm/vgic/vgic-v5.c
> 
> [ ... ]
> 
> > @@ -1301,6 +1301,292 @@ void vgic_v5_save_state(struct kvm_vcpu
> > *vcpu)
> >  	__vgic_v5_save_ppi_state(cpu_if);
> >  	dsb(sy);
> >  }
> > +
> > +static int vgic_v5_irs_status_is_quiesced(u32 statusr)
> > +{
> > +	if (statusr & GICV5_IRS_SAVE_VM_STATUSR_Q)
> > +		return 0;
> > +
> > +	return -EBUSY;
> > +}
> > +
> > +static int vgic_v5_irs_is_quiesced(u16 vm_id)
> > +{
> > +	u32 statusr;
> > +	int ret;
> > +
> > +	ret = vgic_v5_irs_save_vm_op(vm_id, false, &statusr);
> 
> [Severity: High]
> Does this quiescence check potentially lose hardware state?
> 
> By calling vgic_v5_irs_save_vm_op() with save=false, it issues a
> SAVE_VMR
> command with Q=1 and S=0 (Quiesce but don't save).

That's not what this is doing. The Q bit is to query if the VM become
non-quiescent. It isn't stopping the save. We are tracking if it has
become non-quiescent since we started the save op (the last time we
wrote S=1).

> 
> If the VM had unexpectedly unquiesced (e.g., interrupted by
> hardware),
> forcing it to quiesce without saving would destroy its modified
> internal
> state before it can be exported to userspace.

Nope.

> 
> > +	if (ret)
> > +		return ret;
> > +
> > +	return vgic_v5_irs_status_is_quiesced(statusr);
> > +}
> > +
> > +static int vgic_v5_copy_ist_attr(struct kvm_device_attr *attr,
> > +				 struct kvm_vgic_v5_ist *ist_attr)
> > +{
> > +	void __user *uaddr = (void __user *)(unsigned long)attr-
> > >addr;
> > +
> > +	if (!uaddr)
> > +		return -EINVAL;
> > +
> > +	if (copy_from_user(ist_attr, uaddr, sizeof(*ist_attr)))
> > +		return -EFAULT;
> > +
> > +	return 0;
> > +}
> > +
> > +static int vgic_v5_validate_ist_user_buffer(__u64 addr, __u64
> > size,
> > +					    size_t expected)
> > +{
> > +	if (!addr || size != expected)
> > +		return -EINVAL;
> 
> [Severity: Medium]
> Is there an issue here when restoring a VM configured with 0 SPIs?

That's not doable. We have a minimum of 32.

> 
> If userspace passes a size of 0 and an address of 0, the check if
> (!addr)
> will reject it with -EINVAL.
> 
> If userspace passes a dummy non-zero address to bypass this, it fails
> later
> in vgic_v5_get_spi_ist_desc():
> 
> arch/arm64/kvm/vgic/vgic-v5-tables.c:vgic_v5_get_spi_ist_desc() {
>     ...
>     if (!ist->base && userspace_buf)
>         return -ENOENT;
>     ...
> }
> 
> Does this catch-22 prevent live migration for VMs without SPIs?

No, because that's not a thing we support.

> 
> > +
> > +	return 0;
> > +}
> 
> [ ... ]
> 
> > +int vgic_v5_irs_save_ists(struct kvm *kvm, struct kvm_device_attr
> > *attr)
> > +{
> > +	struct kvm_vgic_v5_ist ist_attr;
> > +	u16 vm_id = vgic_v5_vm_id(kvm);
> > +	u32 statusr;
> > +	int ret = 0;
> > +
> > +	mutex_lock(&kvm->lock);
> > +
> > +	if (kvm_trylock_all_vcpus(kvm)) {
> > +		mutex_unlock(&kvm->lock);
> > +		return -EBUSY;
> > +	}
> 
> [Severity: Critical]
> Is the return value of kvm_trylock_all_vcpus() handled correctly
> here?
> 
> The function returns true on successful lock acquisition and false on
> failure.

Where on earth did you pull that from? It returns 0 on success and -
EINTR on failure.

> Returning -EBUSY when it is true means we leak the vCPU locks
> and hang the VM.
> 
> Conversely, failing to acquire the locks lets us proceed lockless,
> and then
> unlocking unowned mutexes in the out_unlock path would cause issues.

This isn't an issue.

> 
> > +
> > +	mutex_lock(&kvm->arch.config_lock);
> 
> [ ... ]
> 
> > +static void vgic_v5_restore_cleanup(struct kvm *kvm,
> > +				    struct kvm_vcpu *vcpu,
> > +				    bool lpi_ist_allocated)
> > +{
> > +	/*
> > +	 * We are on the restore failure path, so we do a best-
> > effort
> > +	 * cleanup. These commands might fail, but at this stage
> > this is the
> > +	 * best we can realistically do.
> > +	 */
> > +	if (lpi_ist_allocated) {
> > +		if (!vgic_v5_send_command(vcpu,
> > VMTE_MAKE_INVALID))
> > +			vgic_v5_lpi_ist_free(kvm);
> > +	}
> 
> [Severity: High]
> Could this leave a dangling pointer behind?
> 
> When vgic_v5_lpi_ist_free() is called on a failure path, it frees the
> memory
> but does not set vmi->h_lpi_ist to NULL. If a subsequent teardown
> happens in
> vgic_v5_vmte_release(), it might observe the populated pointer and
> attempt
> to free it again, causing a double-free.

No. vgic_v5_lpi_ist_free() ultimately calls
vgic_v5_free_allocated_lpi_ist(), which clears vmi->h_lpi_ist after
freeing it (and clears the L2 pointer for two-level ISTs).
Consequently, a subsequent VMTE teardown will not attempt a second
free.

> 
> [Severity: Medium]
> Are we risking a memory leak by issuing a redundant invalidate
> command?
> 
> If we reach the failure path from a point where the VMTE is already
> invalid,
> this function will issue VMTE_MAKE_INVALID again because the
> vmte_invalid
> state is not passed in. If the hardware driver returns an error when
> asked
> to invalidate an already-invalid VMTE, vgic_v5_send_command() will
> fail.

The hardware has no way to indicate this error. It will just be a NOP.

> 
> This would skip the vgic_v5_lpi_ist_free() call and permanently leak
> the LPI
> IST memory.

Except it wouldn't. This is fine.

> 
> > +
> > +	vgic_v5_send_command(vcpu, VMTE_MAKE_VALID);
> > +}
> > +
> > +int vgic_v5_irs_restore_ists(struct kvm *kvm, struct
> > kvm_device_attr *attr)
> > +{
> > +	bool lpi_ist_allocated = false, vmte_invalid = false;
> > +	struct kvm_vcpu *vcpu0 = kvm_get_vcpu(kvm, 0);
> 
> [Severity: Critical]
> What happens if userspace invokes this ioctl before creating any
> vCPUs?
> 
> kvm_get_vcpu(kvm, 0) would return NULL. Later, this NULL vcpu0 is
> passed to
> vgic_v5_send_command(), which dereferences it inside
> vgic_v5_vpe_db().
> 
> Should we verify vcpu0 exists before using it to prevent a potential
> NULL
> pointer dereference?

We reject an uninitialised vGIC before we reach this point. We require
vcpu0 as part of initialising the vGICv5 so we can't make it this far
without a vCPU0.

> 
> > +	struct kvm_vgic_v5_ist ist_attr;
> > +	int ret = 0;
> > +
> > +	mutex_lock(&kvm->lock);
> > +
> > +	if (kvm_trylock_all_vcpus(kvm)) {
> > +		mutex_unlock(&kvm->lock);
> > +		return -EBUSY;
> > +	}
> 
> [Severity: Critical]
> As noted in vgic_v5_irs_save_ists(), does this correctly handle the
> boolean
> return value of kvm_trylock_all_vcpus()? It looks like this behaves
> in the
> exact opposite way as intended, leaking locks on success and running
> lockless on failure.

This again? No. Not an issue. See above.

> 
> > +
> > +	mutex_lock(&kvm->arch.config_lock);
> > +
> > +	if (!vgic_initialized(kvm)) {
> 
> [ ... ]
> 

Thanks,
Sascha
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.