On Mon, Oct 26, 2026 at 20:06:54 +0100, Denis V. Lunev wrote:
> From: Denis V. Lunev <[email protected]>
> 
> Since Linux kernel commit de572491a975 ("KVM: x86: AMD's IBPB is not
> equivalent to Intel's IBPB") KVM offers spec-ctrl on AMD hosts only
> when the CPU enumerates IBPB_RET, which Zen 1-3 do not. A host-model
> guest started on such a CPU under an older kernel has spec-ctrl in its
> live definition. On a destination with the newer kernel QEMU filters
> the feature out and the incoming migration fails:
> 
>   guest CPU doesn't match specification: missing features: spec-ctrl
> 
> Restoring a save image or reverting a snapshot fails the same way, so
> the guest cannot be moved to the new kernel without a restart.
> 
> Dropping the bit in libvirt costs the guest nothing beyond what KVM
> already withholds. KVM grants PRED_CMD for spec-ctrl or ibpb and
> SPEC_CTRL for spec-ctrl or any of ibrs, amd-stibp and amd-ssbd (kernel
> commit 39485ed95d6b), and checks the values written to SPEC_CTRL
> against the host, not the guest CPUID (kernel commit 841c2be09fe4).
> Both predate de572491a975. Since kernel commit e592ec657d84 KVM also
> ignores guest CPUID bits it does not support, so a host without
> IBPB_RET treats the guest as lacking spec-ctrl even if the bit stayed.
> QEMU drops the bit either way; libvirt only decides whether the guest
> survives.
> 
> When an existing domain is started (incoming migration, restore,
> snapshot revert) and the hypervisor dropped a required spec-ctrl while
> providing ibpb and one of ibrs, amd-stibp and amd-ssbd, disable
> spec-ctrl in the live definition and log a warning. spec-ctrl is also
> removed from the features QEMU reports as filtered, because
> check='full' fails on any filtered feature whatever the definition
> says. Fresh starts and any other missing or extra feature still fail
> with check='full'.
> 
> Signed-off-by: Denis V. Lunev <[email protected]>
> ---
> RHEL-149512 reports the same failure.
> 
> Linux commit: https://git.kernel.org/torvalds/c/de572491a975
> KVM thread:   
> https://lore.kernel.org/all/[email protected]/
> RHEL bug:     https://redhat.atlassian.net/browse/RHEL-149512
> 
>  src/cpu/cpu_x86.c                             | 55 ++++++++++++
>  src/cpu/cpu_x86.h                             |  4 +
>  src/libvirt_private.syms                      |  1 +
>  src/qemu/qemu_process.c                       | 15 +++-
>  tests/cputest.c                               | 87 +++++++++++++++++++
>  ...PYC-7713-64-Core-no-spec-ctrl-disabled.xml |  4 +
>  ...EPYC-7713-64-Core-no-spec-ctrl-enabled.xml | 11 +++
>  ...3-64-Core-no-spec-ctrl-no-ibpb-enabled.xml | 11 +++
>  8 files changed, 186 insertions(+), 2 deletions(-)
>  create mode 100644 
> tests/cputestdata/x86_64-cpuid-EPYC-7713-64-Core-no-spec-ctrl-disabled.xml
>  create mode 100644 
> tests/cputestdata/x86_64-cpuid-EPYC-7713-64-Core-no-spec-ctrl-enabled.xml
>  create mode 100644 
> tests/cputestdata/x86_64-cpuid-EPYC-7713-64-Core-no-spec-ctrl-no-ibpb-enabled.xml
> 
> diff --git a/src/cpu/cpu_x86.c b/src/cpu/cpu_x86.c
> index 18c5756ef9..b986720224 100644
> --- a/src/cpu/cpu_x86.c
> +++ b/src/cpu/cpu_x86.c
> @@ -3269,6 +3269,61 @@ virCPUx86UpdateLive(virCPUDef *cpu,
>  }
>  
>  
> +/*
> + * Since kernel commit de572491a975 KVM no longer offers spec-ctrl on AMD
> + * hosts without IBPB_RET, while a guest started before still requires it.
> + *
> + * Returns 1 if spec-ctrl was disabled in @cpu, 0 if not, -1 on error.
> + */
> +int
> +virCPUx86UpdateLiveSpecCtrl(virCPUDef *cpu,
> +                            const virCPUData *dataEnabled,
> +                            virCPUData *dataDisabled)
> +{
> +    const char *specCtrlMSR[] = { "ibrs", "amd-stibp", "amd-ssbd" };
> +    virCPUx86Map *map;
> +    virCPUx86Feature *specCtrl;
> +    virCPUFeatureDef *feature;
> +    const virCPUx86Data *enabled;
> +    size_t i;
> +    int rc;
> +
> +    if (!dataEnabled)
> +        return 0;

This will become redundant if the function is called from a different
place (see below).

> +
> +    if (!(feature = virCPUDefFindFeature(cpu, "spec-ctrl")) ||
> +        feature->policy != VIR_CPU_FEATURE_REQUIRE)
> +        return 0;
> +
> +    if (!(map = virCPUx86GetMap()))
> +        return -1;
> +
> +    enabled = &dataEnabled->data.x86;
> +
> +    if ((rc = x86FeatureInData("spec-ctrl", enabled, map)) != 0)
> +        return rc < 0 ? -1 : 0;

I think it would be a bit more readable without the ternary operator,
e.g.:

        if ((rc = ...) < 0)
            return -1;

        if (rc == 1)
            return 0;

> +
> +    if ((rc = x86FeatureInData("ibpb", enabled, map)) <= 0)
> +        return rc;
> +
> +    for (i = 0; i < G_N_ELEMENTS(specCtrlMSR); i++) {
> +        if ((rc = x86FeatureInData(specCtrlMSR[i], enabled, map)) < 0)
> +            return -1;
> +        if (rc == 1)
> +            break;

Instead of breaking and than checking whether the for loop did ended
early we could just update the CPU here and return.

> +    }
> +
> +    if (i == G_N_ELEMENTS(specCtrlMSR))
> +        return 0;
> +

This code is making the changes so it should log a message about it
rather than leaving it up to the caller

> +    virCPUDefUpdateFeature(cpu, "spec-ctrl", VIR_CPU_FEATURE_DISABLE);
> +    if (dataDisabled && (specCtrl = x86FeatureFind(map, "spec-ctrl")))
> +        x86DataSubtract(&dataDisabled->data.x86, &specCtrl->data);
> +
> +    return 1;
> +}
> +
> +
>  static int
>  virCPUx86GetModels(char ***models)
>  {
> diff --git a/src/cpu/cpu_x86.h b/src/cpu/cpu_x86.h
> index bbc2a16447..0b05e5c695 100644
> --- a/src/cpu/cpu_x86.h
> +++ b/src/cpu/cpu_x86.h
> @@ -55,3 +55,7 @@ bool virCPUx86FeatureFilterDropMSR(const char *name,
>  
>  int virCPUx86GetAddedFeatures(const char *modelName,
>                                const char * const **features);
> +
> +int virCPUx86UpdateLiveSpecCtrl(virCPUDef *cpu,
> +                                const virCPUData *dataEnabled,
> +                                virCPUData *dataDisabled);
> diff --git a/src/libvirt_private.syms b/src/libvirt_private.syms
> index 798d2d28c8..6b2eb3eef4 100644
> --- a/src/libvirt_private.syms
> +++ b/src/libvirt_private.syms
> @@ -1608,6 +1608,7 @@ virCPUx86DataSetVendor;
>  virCPUx86FeatureFilterDropMSR;
>  virCPUx86FeatureFilterSelectMSR;
>  virCPUx86GetAddedFeatures;
> +virCPUx86UpdateLiveSpecCtrl;
>  
>  # datatypes.h
>  virConnectClass;
> diff --git a/src/qemu/qemu_process.c b/src/qemu/qemu_process.c
> index 3f1960f314..9eee738334 100644
> --- a/src/qemu/qemu_process.c
> +++ b/src/qemu/qemu_process.c
> @@ -4799,10 +4799,12 @@ qemuProcessUpdateLiveGuestCPU(virDomainObj *vm,
>  
>  static int
>  qemuProcessUpdateAndVerifyCPU(virDomainObj *vm,
> -                              virDomainAsyncJob asyncJob)
> +                              virDomainAsyncJob asyncJob,
> +                              unsigned int flags)
>  {
>      g_autoptr(virCPUData) cpu = NULL;
>      g_autoptr(virCPUData) disabled = NULL;
> +    int rc;
>  
>      if (qemuProcessFetchGuestCPU(vm, asyncJob, &cpu, &disabled) < 0)
>          return -1;
> @@ -4810,6 +4812,15 @@ qemuProcessUpdateAndVerifyCPU(virDomainObj *vm,
>      if (qemuProcessVerifyCPU(vm, cpu) < 0)
>          return -1;
>  
> +    if (!(flags & VIR_QEMU_PROCESS_START_NEW) &&

The more I think about this the more I'm leaning towards doing this
update even for freshly started domains. Imagine you have defined
domains which require spec-ctrl and then update the host to the version
that drops spec-ctrl. You'd have to update all affected domain XMLs to
be able to start them for no real reason.

> +        ARCH_IS_X86(vm->def->os.arch) && vm->def->cpu) {
> +        if ((rc = virCPUx86UpdateLiveSpecCtrl(vm->def->cpu, cpu, disabled)) 
> < 0)
> +            return -1;
> +        if (rc > 0)
> +            VIR_WARN("Hypervisor no longer provides spec-ctrl, disabling it 
> in the live CPU of domain %s",
> +                     vm->def->name);

This VIR_WARN should be moved inside virCPUx86UpdateLiveSpecCtrl. And
since warnings are not really visible to end users, I think VIR_INFO
would be good enough.

> +    }
> +

Anyway, the call to virCPUx86UpdateLiveSpecCtrl should be moved inside
virCPUUpdateLive.

>      if (qemuProcessUpdateLiveGuestCPU(vm, cpu, disabled) < 0)
>          return -1;
>  
> @@ -8639,7 +8650,7 @@ qemuProcessLaunch(virConnectPtr conn,
>      }
>  
>      VIR_DEBUG("Verifying and updating provided guest CPU");
> -    if (qemuProcessUpdateAndVerifyCPU(vm, asyncJob) < 0)
> +    if (qemuProcessUpdateAndVerifyCPU(vm, asyncJob, flags) < 0)
>          goto cleanup;
>  
>      VIR_DEBUG("Detecting IOThread PIDs");
> diff --git a/tests/cputest.c b/tests/cputest.c
> index e29c81cb8e..38ed45cfdb 100644
> --- a/tests/cputest.c
> +++ b/tests/cputest.c

I believe that calling virCPUx86UpdateLiveSpecCtrl from virCPUUpdateLive
would limit the required cputest modifications to test cases.

Jirka

Reply via email to