Re: [PATCH v4 35/48] KVM: arm64: gic-v5: Implement save/restore mechanisms for ISTs
Sascha Bischoff <[email protected]> Fri, 31 Jul 2026 16:43:43 +0000
| Newsgroups | dev.linux.lists.kvmarm,dev.linux.lists.sashiko-reviews,org.kernel.vger.kvm |
|---|---|
| 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