KVM: arm64: Fix guest feature sanitization and pKVM state synchronization
Subsystems (4)
cover KVM: arm64: Fix guest feature sanitization and pKVM state synchronization
This series addresses state management and feature synchronization vulnerabilities in both standard KVM and pKVM implementations on arm64. The primary focus is ensuring that the hypervisor correctly handles architectural extensions during context switches to prevent state corruption.
The series is structured as follows:
* Patch 1: Addresses an issue in KVM/arm64 in general where FEAT_S1POE is exposed to guests based solely on hardware capability. If the host kernel is built without CONFIG_ARM64_POE, it will not context-switch POR_EL1. Masking the S1POE bit in ID_AA64MMFR3_EL1 when system_supports_poe() is false prevents state corruption.
* Patch 2: Fixes a bug in pKVM non-protected guest initialization. Previously, pkvm_init_features_from_host() copied the initialized flag without copying the actual id_regs array. This caused EL2 feature checks (such as ctxt_has_tcrx()) to silently fail, breaking the save/restore logic for system registers like TCR2_EL1, PIR_EL1, and POR_EL1 during world switches. The fix initializes the ID registers.
* Patch 3: Removes a redundant kern_hyp_va() macro invocation in unpin_host_sve_state(). The sve_state pointer is already initialized as a hypervisor virtual address. While idempotent, the macro is unnecessary here.
Based on Linux 6.19.
Cheers,
/fuad
Fuad Tabba (3):
KVM: arm64: Hide S1POE from guests when not supported by the host
KVM: arm64: Fix ID register initialization for non-protected pKVM
guests
KVM: arm64: Remove redundant kern_hyp_va() in unpin_host_sve_state()
arch/arm64/include/asm/kvm_host.h | 3 ++- arch/arm64/kvm/hyp/nvhe/pkvm.c | 39 ++++++++++++++++++++++++++++--- arch/arm64/kvm/sys_regs.c | 3 +++ 3 files changed, 41 insertions(+), 4 deletions(-)
base-commit: 05f7e89ab9731565d8a62e3b5d1ec206485eeb0b
Signature · 1 line
2.53.0.239.g8d8fc8a987-goog
PATCH 1/3 KVM: arm64: Hide S1POE from guests when not supported by the host
When CONFIG_ARM64_POE is disabled, KVM does not save/restore POR_EL1. However, ID_AA64MMFR3_EL1 sanitisation currently exposes the feature to guests whenever the hardware supports it, ignoring the host kernel configuration.
If a guest detects this feature and attempts to use it, the host will fail to context-switch POR_EL1, potentially leading to state corruption.
Fix this by masking ID_AA64MMFR3_EL1.S1POE and preventing KVM from advertising the feature when the host does not support it, i.e., system_supports_poe() is false.
--- arch/arm64/include/asm/kvm_host.h | 3 ++- arch/arm64/kvm/sys_regs.c | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-)
arch/arm64/include/asm/kvm_host.h +2 -1
@@ -1592,7 +1592,8 @@ void kvm_set_vm_id_reg(struct kvm *kvm, u32 reg, u64 val); (kvm_has_feat((k), ID_AA64MMFR3_EL1, S1PIE, IMP))#define kvm_has_s1poe (k) \ - (kvm_has_feat((k), ID_AA64MMFR3_EL1, S1POE, IMP)) + (system_supports_poe() && \ + kvm_has_feat((k), ID_AA64MMFR3_EL1, S1POE, IMP))#define kvm_has_ras (k) \ (kvm_has_feat((k), ID_AA64PFR0_EL1, RAS, IMP))
arch/arm64/kvm/sys_regs.c +3 -0
@@ -1816,6 +1816,9 @@ static u64 __kvm_read_sanitised_id_reg(const struct kvm_vcpu *vcpu,ID_AA64MMFR3_EL1_SCTLRX |ID_AA64MMFR3_EL1_S1POE |ID_AA64MMFR3_EL1_S1PIE ; + +if (!system_supports_poe ()) + val &= ~ID_AA64MMFR3_EL1_S1POE ;break ;case SYS_ID_MMFR4_EL1 : val &= ~ID_MMFR4_EL1_CCIDX ;
Signature · 1 line
2.53.0.239.g8d8fc8a987-goog
Marc Zyngier
maintainerRe: [PATCH v1 1/3] KVM: arm64: Hide S1POE from guests when not supported by the host
Hi Fuad,
Quoted 5 lines · Fuad Tabba
When
CONFIG_ARM64_POEis disabled, KVM does not save/restorePOR_EL1. However,ID_AA64MMFR3_EL1sanitisation currently exposes the feature to guests whenever the hardware supports it, ignoring the host kernel configuration.
This is the umpteenth time we get caught by this. PAN was the latest instance until this one. Maybe an approach would be to have a default override when a config option is not enabled, so that KVM is consistent with the rest of the kernel?
Quoted 20 lines
If a guest detects this feature and attempts to use it, the host will fail to context-switch
POR_EL1, potentially leading to state corruption.Fix this by masking ID_AA64MMFR3_EL1.S1POE and preventing KVM from advertising the feature when the host does not support it, i.e.,
system_supports_poe()is false.Fixes 70ed7238297f ("KVM: arm64: Sanitise ID_AA64MMFR3_EL1")Signed-off-by Fuad Tabba <[email protected]>--- arch/arm64/include/asm/kvm_host.h | 3 ++- arch/arm64/kvm/sys_regs.c | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-)arch/arm64/include/asm/kvm_host.h +0
-0
arch/arm64/include/asm/kvm_host.h void kvm_set_vm_id_reg(struct kvm *kvm, u32 reg, u64 val); · line 1596
Quoted 6 lines
arch/arm64/include/asm/kvm_host.h +2
-1@@ -1592,7 +1592,8 @@ void kvm_set_vm_id_reg(struct kvm *kvm, u32 reg, u64 val); (kvm_has_feat((k), ID_AA64MMFR3_EL1, S1PIE, IMP))#define kvm_has_s1poe (k) \ - (kvm_has_feat((k), ID_AA64MMFR3_EL1, S1POE, IMP)) + (system_supports_poe() && \ + kvm_has_feat((k), ID_AA64MMFR3_EL1, S1POE, IMP))
Why do we need to further key this on system_supports_poe()? I can see this is a potential optimisation, but I don't think this is part of the minimal fix.
Quoted 8 lines
arch/arm64/include/asm/kvm_host.h +0
-0#define kvm_has_ras (k) \ (kvm_has_feat((k), ID_AA64PFR0_EL1, RAS, IMP))arch/arm64/kvm/sys_regs.c +0
-0
arch/arm64/kvm/sys_regs.c static u64 __kvm_read_sanitised_id_reg(const struct kvm_vcpu *vcpu, · line 1821
Quoted 6 lines
arch/arm64/kvm/sys_regs.c +3
-0@@ -1816,6 +1816,9 @@ static u64 __kvm_read_sanitised_id_reg(const struct kvm_vcpu *vcpu,ID_AA64MMFR3_EL1_SCTLRX |ID_AA64MMFR3_EL1_S1POE |ID_AA64MMFR3_EL1_S1PIE ; + +if (!system_supports_poe ()) + val &= ~ID_AA64MMFR3_EL1_S1POE ;
How about S1PIE? It seems to have a similar problem, in the sense that it has extra state. But I guess because we don't put it behind a config option, we context-switch it anyway and all is good?
Thanks,
M.
Signature · 1 line
Without deviation from the norm, progress is not possible.
Re: [PATCH v1 1/3] KVM: arm64: Hide S1POE from guests when not supported by the host
Hi Marc,
Quoted 11 lines · Marc Zyngier
Hi Fuad,
Quoted 5 lines · Fuad Tabba
When
CONFIG_ARM64_POEis disabled, KVM does not save/restorePOR_EL1. However,ID_AA64MMFR3_EL1sanitisation currently exposes the feature to guests whenever the hardware supports it, ignoring the host kernel configuration.
Quoted 4 lines
This is the umpteenth time we get caught by this. PAN was the latest instance until this one. Maybe an approach would be to have a default override when a config option is not enabled, so that KVM is consistent with the rest of the kernel?
I spoke to Will about this, and one thing he'll look into is whether this value in struct arm64_ftr_reg can be made consistent with the cpu configuration itself (in cpufeature.c itself) . This would avoid the problem altogether if possible. The question is whether the kernel needs to somehow know that a certain feature exists even if it's disabled in the config...
If he thinks it's not doable at that level, I'll look into alternatives to make it correct by construction.
Quoted 22 lines
Quoted 22 lines
If a guest detects this feature and attempts to use it, the host will fail to context-switch
POR_EL1, potentially leading to state corruption.Fix this by masking ID_AA64MMFR3_EL1.S1POE and preventing KVM from advertising the feature when the host does not support it, i.e.,
system_supports_poe()is false.Fixes 70ed7238297f ("KVM: arm64: Sanitise ID_AA64MMFR3_EL1")Signed-off-by Fuad Tabba <[email protected]>--- arch/arm64/include/asm/kvm_host.h | 3 ++- arch/arm64/kvm/sys_regs.c | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-)arch/arm64/include/asm/kvm_host.h +0
-0@@ -1592,7 +1592,8 @@ void kvm_set_vm_id_reg(struct kvm *kvm, u32 reg, u64 val); (kvm_has_feat ((k),ID_AA64MMFR3_EL1 ,S1PIE ,IMP ))
Quoted 8 lines
arch/arm64/include/asm/kvm_host.h void kvm_set_vm_id_reg(struct kvm *kvm, u32 reg, u64 val); · line 1596
Quoted 4 lines
arch/arm64/include/asm/kvm_host.h +2
-1#define kvm_has_s1poe (k) \ - (kvm_has_feat((k), ID_AA64MMFR3_EL1, S1POE, IMP)) + (system_supports_poe() && \ + kvm_has_feat((k), ID_AA64MMFR3_EL1, S1POE, IMP))Why do we need to further key this on
system_supports_poe()? I can see this is a potential optimisation, but I don't think this is part of the minimal fix.
I did this to be consistent with similar checks, e.g., kvm_has_fpmr(). I can remove it or put it in a separate patch, whichever you prefer.
Quoted 11 lines
Quoted 11 lines
arch/arm64/include/asm/kvm_host.h +0
-0#define kvm_has_ras (k) \ (kvm_has_feat((k), ID_AA64PFR0_EL1, RAS, IMP))arch/arm64/kvm/sys_regs.c +0
-0@@ -1816,6 +1816,9 @@ static u64 __kvm_read_sanitised_id_reg(const struct kvm_vcpu *vcpu,ID_AA64MMFR3_EL1_SCTLRX | ID_AA64MMFR3_EL1_S1POE |
Quoted 8 lines
arch/arm64/kvm/sys_regs.c static u64 __kvm_read_sanitised_id_reg(const struct kvm_vcpu *vcpu, · line 1821
Quoted 4 lines
arch/arm64/kvm/sys_regs.c +3
-0ID_AA64MMFR3_EL1_S1PIE ; + +if (!system_supports_poe ()) + val &= ~ID_AA64MMFR3_EL1_S1POE ;How about S1PIE? It seems to have a similar problem, in the sense that it has extra state. But I guess because we don't put it behind a config option, we context-switch it anyway and all is good?
Like you said, S1PIE doesn't have a config option. I thought if I were to add one to S1PIE, then why not add one to the other features that don't have config options... It is conceivable that a config option might be added in the future, but like you noted above, I think we need a deeper fix that nips this problem in the bud.
Let's see if Will thinks that we can fix this at the arm64 cpufeature layer, rather than somewhere in KVM.
Cheers,
/fuad
Quoted 6 lines
Thanks,
M.--
Without deviation from the norm, progress is not possible.
Marc Zyngier
maintainerRe: [PATCH v1 1/3] KVM: arm64: Hide S1POE from guests when not supported by the host
Quoted 27 lines · Fuad Tabba
Hi Marc,
Quoted 11 lines · Marc Zyngier
Hi Fuad,
Quoted 5 lines · Fuad Tabba
When
CONFIG_ARM64_POEis disabled, KVM does not save/restorePOR_EL1. However,ID_AA64MMFR3_EL1sanitisation currently exposes the feature to guests whenever the hardware supports it, ignoring the host kernel configuration.Quoted 4 lines
This is the umpteenth time we get caught by this. PAN was the latest instance until this one. Maybe an approach would be to have a default override when a config option is not enabled, so that KVM is consistent with the rest of the kernel?
I spoke to Will about this, and one thing he'll look into is whether this value in
struct arm64_ftr_regcan be made consistent with the cpu configuration itself (incpufeature.citself) . This would avoid the problem altogether if possible. The question is whether the kernel needs to somehow know that a certain feature exists even if it's disabled in the config...
Quoted 2 lines
If he thinks it's not doable at that level, I'll look into alternatives to make it correct by construction.
What I currently have for that is rather ugly:
arch/arm64/include/asm/cpufeature.h +1 -0
@@ -971,6 +971,7 @@ struct arm64_ftr_reg *get_arm64_ftr_reg(u32 sys_id);extern struct arm64_ftr_override id_aa64mmfr0_override;extern struct arm64_ftr_override id_aa64mmfr1_override;extern struct arm64_ftr_override id_aa64mmfr2_override; +extern struct arm64_ftr_override id_aa64mmfr3_override;extern struct arm64_ftr_override id_aa64pfr0_override;extern struct arm64_ftr_override id_aa64pfr1_override;extern struct arm64_ftr_override id_aa64zfr0_override;
arch/arm64/kernel/cpufeature.c +3 -1
@@ -778,6 +778,7 @@ static const struct arm64_ftr_bits ftr_raz[] = {struct arm64_ftr_override __read_mostly id_aa64mmfr0_override;struct arm64_ftr_override __read_mostly id_aa64mmfr1_override;struct arm64_ftr_override __read_mostly id_aa64mmfr2_override; +struct arm64_ftr_override __read_mostly id_aa64mmfr3_override;struct arm64_ftr_override __read_mostly id_aa64pfr0_override;struct arm64_ftr_override __read_mostly id_aa64pfr1_override;struct arm64_ftr_override __read_mostly id_aa64zfr0_override;
@@ -850,7 +851,8 @@ static const struct __ftr_reg_entry { &id_aa64mmfr1_override),ARM64_FTR_REG_OVERRIDE (SYS_ID_AA64MMFR2_EL1 , ftr_id_aa64mmfr2, &id_aa64mmfr2_override), -ARM64_FTR_REG (SYS_ID_AA64MMFR3_EL1 , ftr_id_aa64mmfr3), +ARM64_FTR_REG_OVERRIDE (SYS_ID_AA64MMFR3_EL1 , ftr_id_aa64mmfr3, + &id_aa64mmfr3_override), ARM64_FTR_REG(SYS_ID_AA64MMFR4_EL1, ftr_id_aa64mmfr4),/* Op1 = 0, CRn = 10, CRm = 4 */
arch/arm64/kernel/image-vars.h +1 -0
@@ -51,6 +51,7 @@ PI_EXPORT_SYM(id_aa64isar2_override);PI_EXPORT_SYM (id_aa64mmfr0_override);PI_EXPORT_SYM (id_aa64mmfr1_override);PI_EXPORT_SYM (id_aa64mmfr2_override); +PI_EXPORT_SYM (id_aa64mmfr3_override);PI_EXPORT_SYM (id_aa64pfr0_override);PI_EXPORT_SYM (id_aa64pfr1_override);PI_EXPORT_SYM (id_aa64smfr0_override);
arch/arm64/kernel/pi/idreg-override.c +24 -0
@@ -24,10 +24,12 @@static u64 __boot_status__initdata ;typedef bool filter_t (u64 val); +typedef void cfg_override_t (struct arm64_ftr_override *);struct ftr_set_desc {char name[FTR_DESC_NAME_LEN ]; PREL64(struct arm64_ftr_override, override); + PREL64(cfg_override_t, cfg_override);struct {char name[FTR_DESC_FIELD_LEN ];u8 shift;
@@ -106,6 +108,22 @@ static const struct ftr_set_desc mmfr2 __prel64_initconst = { }, }; +static void __init cfg_mmfr3_override (struct arm64_ftr_override *override) +{ +#ifndef CONFIG_ARM64_POE + override->mask |=ID_AA64MMFR3_EL1_S1POE_MASK ; +#endif +} + +static const struct ftr_set_desc mmfr3 __prel64_initconst = { + .name ="id_aa64mmfr3" , + .override = &id_aa64mmfr3_override, + .cfg_override = cfg_mmfr3_override, + .fields = { + {} + }, +}; +static bool __init pfr0_sve_filter (u64 val) {/*
@@ -221,6 +239,7 @@ PREL64(const struct ftr_set_desc, reg) regs[] __prel64_initconst = { { &mmfr0 }, { &mmfr1 }, { &mmfr2 }, + { &mmfr3 }, { &pfr0 }, { &pfr1 }, { &isar1 },
@@ -398,14 +417,19 @@ void __init init_feature_override(u64 boot_status, const void *fdt, {struct arm64_ftr_override *override;const struct ftr_set_desc *reg; +cfg_override_t *cfg_override;int i;for (i =0 ; i <ARRAY_SIZE (regs); i++) { reg =prel64_pointer (regs[i].reg); override =prel64_pointer (reg->override); + cfg_override =prel64_pointer (reg->cfg_override); override->val =0 ; override->mask =0 ; + +if (cfg_override) +cfg_override (override); } __boot_status = boot_status;
which works, but is not super friendly.
Looking at the arm64_ftr_reg structure, this could work if FTR_VISIBLE_IF_IS_ENABLED() didn't simply put "HIDDEN" when the feature is not present, but forced things to be disabled altogether. The problem is that "HIDDEN" means not shown to userspace, and that we have plenty of HIDDEN features that must make it into KVM.
I'll have a think.
M.
Signature · 1 line
Without deviation from the norm, progress is not possible.
Re: [PATCH v1 1/3] KVM: arm64: Hide S1POE from guests when not supported by the host
/fuad
Quoted 151 lines · Marc Zyngier
Quoted 27 lines · Fuad Tabba
Hi Marc,
Quoted 11 lines · Marc Zyngier
Hi Fuad,
Quoted 5 lines · Fuad Tabba
When
CONFIG_ARM64_POEis disabled, KVM does not save/restorePOR_EL1. However,ID_AA64MMFR3_EL1sanitisation currently exposes the feature to guests whenever the hardware supports it, ignoring the host kernel configuration.Quoted 4 lines
This is the umpteenth time we get caught by this. PAN was the latest instance until this one. Maybe an approach would be to have a default override when a config option is not enabled, so that KVM is consistent with the rest of the kernel?
I spoke to Will about this, and one thing he'll look into is whether this value in
struct arm64_ftr_regcan be made consistent with the cpu configuration itself (incpufeature.citself) . This would avoid the problem altogether if possible. The question is whether the kernel needs to somehow know that a certain feature exists even if it's disabled in the config...Quoted 2 lines
If he thinks it's not doable at that level, I'll look into alternatives to make it correct by construction.
What I currently have for that is rather ugly:
arch/arm64/include/asm/cpufeature.h +1
-0@@ -971,6 +971,7 @@ struct arm64_ftr_reg *get_arm64_ftr_reg(u32 sys_id);extern struct arm64_ftr_override id_aa64mmfr0_override;extern struct arm64_ftr_override id_aa64mmfr1_override;extern struct arm64_ftr_override id_aa64mmfr2_override; +extern struct arm64_ftr_override id_aa64mmfr3_override;extern struct arm64_ftr_override id_aa64pfr0_override;extern struct arm64_ftr_override id_aa64pfr1_override;extern struct arm64_ftr_override id_aa64zfr0_override;arch/arm64/kernel/cpufeature.c +3
-1@@ -778,6 +778,7 @@ static const struct arm64_ftr_bits ftr_raz[] = {struct arm64_ftr_override __read_mostly id_aa64mmfr0_override;struct arm64_ftr_override __read_mostly id_aa64mmfr1_override;struct arm64_ftr_override __read_mostly id_aa64mmfr2_override; +struct arm64_ftr_override __read_mostly id_aa64mmfr3_override;struct arm64_ftr_override __read_mostly id_aa64pfr0_override;struct arm64_ftr_override __read_mostly id_aa64pfr1_override;struct arm64_ftr_override __read_mostly id_aa64zfr0_override;@@ -850,7 +851,8 @@ static const struct __ftr_reg_entry { &id_aa64mmfr1_override),ARM64_FTR_REG_OVERRIDE (SYS_ID_AA64MMFR2_EL1 , ftr_id_aa64mmfr2, &id_aa64mmfr2_override), -ARM64_FTR_REG (SYS_ID_AA64MMFR3_EL1 , ftr_id_aa64mmfr3), +ARM64_FTR_REG_OVERRIDE (SYS_ID_AA64MMFR3_EL1 , ftr_id_aa64mmfr3, + &id_aa64mmfr3_override), ARM64_FTR_REG(SYS_ID_AA64MMFR4_EL1, ftr_id_aa64mmfr4),/* Op1 = 0, CRn = 10, CRm = 4 */ arch/arm64/kernel/image-vars.h +1
-0@@ -51,6 +51,7 @@ PI_EXPORT_SYM(id_aa64isar2_override);PI_EXPORT_SYM (id_aa64mmfr0_override);PI_EXPORT_SYM (id_aa64mmfr1_override);PI_EXPORT_SYM (id_aa64mmfr2_override); +PI_EXPORT_SYM (id_aa64mmfr3_override);PI_EXPORT_SYM (id_aa64pfr0_override);PI_EXPORT_SYM (id_aa64pfr1_override);PI_EXPORT_SYM (id_aa64smfr0_override);arch/arm64/kernel/pi/idreg-override.c +24
-0@@ -24,10 +24,12 @@static u64 __boot_status__initdata ;typedef bool filter_t (u64 val); +typedef void cfg_override_t (struct arm64_ftr_override *);struct ftr_set_desc {char name[FTR_DESC_NAME_LEN ]; PREL64(struct arm64_ftr_override, override); + PREL64(cfg_override_t, cfg_override);struct {char name[FTR_DESC_FIELD_LEN ];u8 shift;@@ -106,6 +108,22 @@ static const struct ftr_set_desc mmfr2 __prel64_initconst = { }, }; +static void __init cfg_mmfr3_override (struct arm64_ftr_override *override) +{ +#ifndef CONFIG_ARM64_POE + override->mask |=ID_AA64MMFR3_EL1_S1POE_MASK ; +#endif +} + +static const struct ftr_set_desc mmfr3 __prel64_initconst = { + .name ="id_aa64mmfr3" , + .override = &id_aa64mmfr3_override, + .cfg_override = cfg_mmfr3_override, + .fields = { + {} + }, +}; +static bool __init pfr0_sve_filter (u64 val) {/* @@ -221,6 +239,7 @@ PREL64(const struct ftr_set_desc, reg) regs[] __prel64_initconst = { { &mmfr0 }, { &mmfr1 }, { &mmfr2 }, + { &mmfr3 }, { &pfr0 }, { &pfr1 }, { &isar1 },@@ -398,14 +417,19 @@ void __init init_feature_override(u64 boot_status, const void *fdt, {struct arm64_ftr_override *override;const struct ftr_set_desc *reg; +cfg_override_t *cfg_override;int i;for (i =0 ; i <ARRAY_SIZE (regs); i++) { reg =prel64_pointer (regs[i].reg); override =prel64_pointer (reg->override); + cfg_override =prel64_pointer (reg->cfg_override); override->val =0 ; override->mask =0 ; + +if (cfg_override) +cfg_override (override); } __boot_status = boot_status;
Quoted 1 line
which works, but is not super friendly.
Ouch! Yeah, no easy solutions here. For now, as a fix to be able to backport, would you like me to respin this without the change to kvm_has_s1poe(), or with that change as a separate patch?
Cheers,
/fuad
Quoted 12 lines
Looking at the
arm64_ftr_regstructure, this could work ifFTR_VISIBLE_IF_IS_ENABLED()didn't simply put "HIDDEN" when the feature is not present, but forced things to be disabled altogether. The problem is that "HIDDEN" means not shown to userspace, and that we have plenty of HIDDEN features that must make it into KVM.I'll have a think.
M.--
Without deviation from the norm, progress is not possible.
Marc Zyngier
maintainerRe: [PATCH v1 1/3] KVM: arm64: Hide S1POE from guests when not supported by the host
Quoted 3 lines · Fuad Tabba
Ouch! Yeah, no easy solutions here. For now, as a fix to be able to backport, would you like me to respin this without the change to
kvm_has_s1poe(), or with that change as a separate patch?
I'll remove the hunk myself when applying the series, no need to resend (unless I find another issue in the rest of the series, but it looks good so far).
FWIW, I have an alternative set of hacks at [1], which appear to work, but we need to collectively convince ourselves that this is the correct thing to do (i.e. the required traps are always set, irrespective of the configuration).
This affects PAuth, SVE, MTE and S1POE. SME and GCS are not supported in KVM, and BTI has no additional state, so these shouldn't be affected.
Thanks,
M.
[1] https://git.kernel.org/pub/scm/linux/kernel/git/maz/arm-platforms.git/log/?h=arm64/ftr_config
Signature · 1 line
Without deviation from the norm, progress is not possible.
PATCH 2/3 KVM: arm64: Fix ID register initialization for non-protected pKVM guests
In reply to Fuad TabbaIn protected mode, the hypervisor maintains a separate instance of the kvm structure for each VM. For non-protected VMs, this structure is initialized from the host's kvm state.
Currently, pkvm_init_features_from_host() copies the KVM_ARCH_FLAG_ID_REGS_INITIALIZED flag from the host without the underlying id_regs data being initialized. This results in the hypervisor seeing the flag as set while the ID registers remain zeroed.
Consequently, kvm_has_feat() checks at EL2 fail (return 0) for non-protected VMs. This breaks logic that relies on feature detection, such as ctxt_has_tcrx() for TCR2_EL1 support. As a result, certain system registers (e.g., TCR2_EL1, PIR_EL1, POR_EL1) are not saved/restored during the world switch, which could lead to state corruption.
Fix this by explicitly copying the ID registers from the host kvm to the hypervisor kvm for non-protected VMs during vCPU initialization, since we trust the host with its non-protected guests' features. Also ensure KVM_ARCH_FLAG_ID_REGS_INITIALIZED is cleared initially in pkvm_init_features_from_host so that vm_copy_id_regs can properly initialize them and set the flag once done.
--- arch/arm64/kvm/hyp/nvhe/pkvm.c | 37 ++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-)
arch/arm64/kvm/hyp/nvhe/pkvm.c +35 -2
@@ -344,6 +344,8 @@ static void pkvm_init_features_from_host(struct pkvm_hyp_vm *hyp_vm, const struc/* No restrictions for non-protected VMs. */ if (!kvm_vm_is_protected (kvm)) { +clear_bit (KVM_ARCH_FLAG_ID_REGS_INITIALIZED , &host_arch_flags); + hyp_vm->kvm.arch.flags = host_arch_flags; bitmap_copy(kvm->arch.vcpu_features,
@@ -471,6 +473,36 @@ static int pkvm_vcpu_init_sve(struct pkvm_hyp_vcpu *hyp_vcpu, struct kvm_vcpu *hreturn ret; } +static int vm_copy_id_regs (struct pkvm_hyp_vcpu *hyp_vcpu) +{ +struct pkvm_hyp_vm *hyp_vm =pkvm_hyp_vcpu_to_hyp_vm (hyp_vcpu); +const struct kvm *host_kvm = hyp_vm->host_kvm; +struct kvm *kvm = &hyp_vm->kvm; + +if (!test_bit (KVM_ARCH_FLAG_ID_REGS_INITIALIZED , &host_kvm->arch.flags)) +return -EINVAL ; + +if (test_bit (KVM_ARCH_FLAG_ID_REGS_INITIALIZED , &kvm->arch.flags)) +return 0 ; + +memcpy (kvm->arch.id_regs, host_kvm->arch.id_regs,sizeof (kvm->arch.id_regs)); +set_bit (KVM_ARCH_FLAG_ID_REGS_INITIALIZED , &kvm->arch.flags); + +return 0 ; +} + +static int pkvm_vcpu_init_sysregs (struct pkvm_hyp_vcpu *hyp_vcpu) +{ +int ret =0 ; + +if (pkvm_hyp_vcpu_is_protected (hyp_vcpu)) +kvm_init_pvm_id_regs (&hyp_vcpu->vcpu); +else + ret =vm_copy_id_regs (hyp_vcpu); + +return ret; +} +static int init_pkvm_hyp_vcpu (struct pkvm_hyp_vcpu *hyp_vcpu,struct pkvm_hyp_vm *hyp_vm,struct kvm_vcpu *host_vcpu)
@@ -490,8 +522,9 @@ static int init_pkvm_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu, hyp_vcpu->vcpu.arch.cflags =READ_ONCE (host_vcpu->arch.cflags); hyp_vcpu->vcpu.arch.mp_state.mp_state =KVM_MP_STATE_STOPPED ; -if (pkvm_hyp_vcpu_is_protected (hyp_vcpu)) -kvm_init_pvm_id_regs (&hyp_vcpu->vcpu); + ret =pkvm_vcpu_init_sysregs (hyp_vcpu); +if (ret) + goto done; ret =pkvm_vcpu_init_traps (hyp_vcpu);if (ret)
Signature · 1 line
2.53.0.239.g8d8fc8a987-goog
Marc Zyngier
maintainerRe: [PATCH v1 2/3] KVM: arm64: Fix ID register initialization for non-protected pKVM guests
Quoted 35 lines · Fuad Tabba
In protected mode, the hypervisor maintains a separate instance of the
kvmstructure for each VM. For non-protected VMs, this structure is initialized from the host'skvmstate.Currently,
pkvm_init_features_from_host()copies theKVM_ARCH_FLAG_ID_REGS_INITIALIZEDflag from the host without the underlyingid_regsdata being initialized. This results in the hypervisor seeing the flag as set while the ID registers remain zeroed.Consequently,
kvm_has_feat()checks at EL2 fail (return 0) for non-protected VMs. This breaks logic that relies on feature detection, such asctxt_has_tcrx()forTCR2_EL1support. As a result, certain system registers (e.g.,TCR2_EL1,PIR_EL1,POR_EL1) are not saved/restored during the world switch, which could lead to state corruption.Fix this by explicitly copying the ID registers from the host
kvmto the hypervisorkvmfor non-protected VMs during vCPU initialization, since we trust the host with its non-protected guests' features. Also ensureKVM_ARCH_FLAG_ID_REGS_INITIALIZEDis cleared initially inpkvm_init_features_from_hostso thatvm_copy_id_regscan properly initialize them and set the flag once done.Fixes 41d6028e28bd ("KVM: arm64: Convert the SVE guest vcpu flag to a vm flag")Signed-off-by Fuad Tabba <[email protected]>--- arch/arm64/kvm/hyp/nvhe/pkvm.c | 37 ++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-)arch/arm64/kvm/hyp/nvhe/pkvm.c +0
-0
arch/arm64/kvm/hyp/nvhe/pkvm.c static void pkvm_init_features_from_host(struct pkvm_hyp_vm *hyp_vm, const struc · line 349
Quoted 6 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +2
-0@@ -344,6 +344,8 @@ static void pkvm_init_features_from_host(struct pkvm_hyp_vm *hyp_vm, const struc/* No restrictions for non-protected VMs. */ if (!kvm_vm_is_protected (kvm)) { +clear_bit (KVM_ARCH_FLAG_ID_REGS_INITIALIZED , &host_arch_flags); + hyp_vm->kvm.arch.flags = host_arch_flags;
Can't you just have
hyp_vm->kvm.arch.flags &= ~BIT_ULL(KVM_ARCH_FLAG_ID_REGS_INITIALIZED);
since there are no atomicity requirements here?
Quoted 10 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +4
-0bitmap_copy(kvm->arch.vcpu_features,@@ -471,6 +473,36 @@ static int pkvm_vcpu_init_sve(struct pkvm_hyp_vcpu *hyp_vcpu, struct kvm_vcpu *hreturn ret; } +static int vm_copy_id_regs (struct pkvm_hyp_vcpu *hyp_vcpu) +{ +struct pkvm_hyp_vm *hyp_vm =pkvm_hyp_vcpu_to_hyp_vm (hyp_vcpu); +const struct kvm *host_kvm = hyp_vm->host_kvm;
arch/arm64/kvm/hyp/nvhe/pkvm.c static int pkvm_vcpu_init_sve(struct pkvm_hyp_vcpu *hyp_vcpu, struct kvm_vcpu *h · line 489
Quoted 10 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +10
-0+struct kvm *kvm = &hyp_vm->kvm; + +if (!test_bit (KVM_ARCH_FLAG_ID_REGS_INITIALIZED , &host_kvm->arch.flags)) +return -EINVAL ; + +if (test_bit (KVM_ARCH_FLAG_ID_REGS_INITIALIZED , &kvm->arch.flags)) +return 0 ; + +memcpy (kvm->arch.id_regs, host_kvm->arch.id_regs,sizeof (kvm->arch.id_regs)); +set_bit (KVM_ARCH_FLAG_ID_REGS_INITIALIZED , &kvm->arch.flags);
This looks a bit odd. Can you have another vcpu doing this in parallel? You seem to be holding vm_table_lock at this stage, so that's probably OK, but I'd have expected something like:
if (test_and_set_bit(KVM_ARCH_FLAG_ID_REGS_INITIALIZED, &kvm->arch.flags)) return 0; memcpy(kvm->arch.id_regs, host_kvm->arch.id_regs, sizeof(kvm->arch.id_regs));
which makes the intent slightly clearer.
Thanks,
M.
Signature · 1 line
Without deviation from the norm, progress is not possible.
Re: [PATCH v1 2/3] KVM: arm64: Fix ID register initialization for non-protected pKVM guests
Quoted 47 lines · Marc Zyngier
Quoted 35 lines · Fuad Tabba
In protected mode, the hypervisor maintains a separate instance of the
kvmstructure for each VM. For non-protected VMs, this structure is initialized from the host'skvmstate.Currently,
pkvm_init_features_from_host()copies theKVM_ARCH_FLAG_ID_REGS_INITIALIZEDflag from the host without the underlyingid_regsdata being initialized. This results in the hypervisor seeing the flag as set while the ID registers remain zeroed.Consequently,
kvm_has_feat()checks at EL2 fail (return 0) for non-protected VMs. This breaks logic that relies on feature detection, such asctxt_has_tcrx()forTCR2_EL1support. As a result, certain system registers (e.g.,TCR2_EL1,PIR_EL1,POR_EL1) are not saved/restored during the world switch, which could lead to state corruption.Fix this by explicitly copying the ID registers from the host
kvmto the hypervisorkvmfor non-protected VMs during vCPU initialization, since we trust the host with its non-protected guests' features. Also ensureKVM_ARCH_FLAG_ID_REGS_INITIALIZEDis cleared initially inpkvm_init_features_from_hostso thatvm_copy_id_regscan properly initialize them and set the flag once done.Fixes 41d6028e28bd ("KVM: arm64: Convert the SVE guest vcpu flag to a vm flag")Signed-off-by Fuad Tabba <[email protected]>--- arch/arm64/kvm/hyp/nvhe/pkvm.c | 37 ++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-)arch/arm64/kvm/hyp/nvhe/pkvm.c +0
-0arch/arm64/kvm/hyp/nvhe/pkvm.c static void pkvm_init_features_from_host(struct pkvm_hyp_vm *hyp_vm, const struc · line 349
Quoted 6 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +2
-0@@ -344,6 +344,8 @@ static void pkvm_init_features_from_host(struct pkvm_hyp_vm *hyp_vm, const struc/* No restrictions for non-protected VMs. */ if (!kvm_vm_is_protected (kvm)) { +clear_bit (KVM_ARCH_FLAG_ID_REGS_INITIALIZED , &host_arch_flags); + hyp_vm->kvm.arch.flags = host_arch_flags;Can't you just have
Quoted 3 lines
hyp_vm->kvm.arch.flags &= ~BIT_ULL(KVM_ARCH_FLAG_ID_REGS_INITIALIZED);since there are no atomicity requirements here?
I'll fix this.
Quoted 16 lines
Quoted 16 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +10
-0bitmap_copy(kvm->arch.vcpu_features,@@ -471,6 +473,36 @@ static int pkvm_vcpu_init_sve(struct pkvm_hyp_vcpu *hyp_vcpu, struct kvm_vcpu *hreturn ret; } +static int vm_copy_id_regs (struct pkvm_hyp_vcpu *hyp_vcpu) +{ +struct pkvm_hyp_vm *hyp_vm =pkvm_hyp_vcpu_to_hyp_vm (hyp_vcpu); +const struct kvm *host_kvm = hyp_vm->host_kvm; +struct kvm *kvm = &hyp_vm->kvm; + +if (!test_bit (KVM_ARCH_FLAG_ID_REGS_INITIALIZED , &host_kvm->arch.flags)) +return -EINVAL ; + +if (test_bit (KVM_ARCH_FLAG_ID_REGS_INITIALIZED , &kvm->arch.flags))
Quoted 8 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c static int pkvm_vcpu_init_sve(struct pkvm_hyp_vcpu *hyp_vcpu, struct kvm_vcpu *h · line 489
Quoted 4 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +4
-0+return 0 ; + +memcpy (kvm->arch.id_regs, host_kvm->arch.id_regs,sizeof (kvm->arch.id_regs)); +set_bit (KVM_ARCH_FLAG_ID_REGS_INITIALIZED , &kvm->arch.flags);This looks a bit odd. Can you have another vcpu doing this in parallel? You seem to be holding
vm_table_lockat this stage, so that's probably OK, but I'd have expected something like:
You're right, it cannot happen in parallel, but another vCPU could have beaten this one to it.
Quoted 7 lines
if (test_and_set_bit(KVM_ARCH_FLAG_ID_REGS_INITIALIZED, &kvm->arch.flags)) return 0; memcpy(kvm->arch.id_regs, host_kvm->arch.id_regs, sizeof(kvm->arch.id_regs));which makes the intent slightly clearer.
I agree. I'll fix this too.
Thanks,
/fuad
Quoted 7 lines
Thanks,
M.--
Without deviation from the norm, progress is not possible.
PATCH 3/3 KVM: arm64: Remove redundant kern_hyp_va() in unpin_host_sve_state()
In reply to Fuad TabbaThe sve_state pointer in hyp_vcpu->vcpu.arch is initialized as a hypervisor virtual address during vCPU initialization in pkvm_vcpu_init_sve().
unpin_host_sve_state() calls kern_hyp_va() on this address. Since kern_hyp_va() is idempotent, it's not a bug. However, it is unnecessary and potentially confusing. Remove the redundant conversion.
--- arch/arm64/kvm/hyp/nvhe/pkvm.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
arch/arm64/kvm/hyp/nvhe/pkvm.c +1 -1
@@ -393,7 +393,7 @@ static void unpin_host_sve_state(struct pkvm_hyp_vcpu *hyp_vcpu)if (!vcpu_has_feature (&hyp_vcpu->vcpu,KVM_ARM_VCPU_SVE ))return ; - sve_state =kern_hyp_va (hyp_vcpu->vcpu.arch.sve_state); + sve_state = hyp_vcpu->vcpu.arch.sve_state;hyp_unpin_shared_mem (sve_state, sve_state +vcpu_sve_state_size (&hyp_vcpu->vcpu)); }
Signature · 1 line
2.53.0.239.g8d8fc8a987-goog