On Tue, Sep 01, 2026 at 10:47:12PM +0100, Mark Brown wrote:
> When running in vEL2 ERET is emulated, this means that we must emulate
> the behaviour of GCSCR_EL2.EXLOCKEN. Add a test that validates the
> behaviour.
>
> As per DDI0487 R_WTXBY PSTATE.EXLOCK is set to the value of
> GCSCR_ELx.EXLOCKEN when taking an exception to the same exception level.
> As per DDI0487 R_TYTWB if on exception return to the same EL
> GCSCR_ELx.EXLOCKEN is 1 and PSTATE.EXLOCK is not 1 then the exception
> return is illegal.
>
> We nest handling of two UDFs to allow us to both observe and modify
> PSTATE.EXLOCK, checking that expected exceptions are generated for ERET.
> We also repeat the UDF tests having enabled fine grained traps for ERET
> to try to ensure that the slow paths are taken.
>
> We also attempt to write to both SPSR_ELx and ELR_ELx, again using a UDF
> handler to control PSTATE.EXLOCK. We do all this both with and without
> having enabled EXLOCKEN, though only for code running at EL2 due to
> framework limitations.
>
> For completeness we also inject a data abort from EL0 to EL2 with
> GCSCR_EL2.EXLOCKEN set, ensuring that PSTATE.EXLOCK is not set.
>
> Signed-off-by: Mark Brown <[email protected]>
> ---
> tools/testing/selftests/kvm/Makefile.kvm | 1 +
> tools/testing/selftests/kvm/arm64/gcs_exceptions.c | 346
> +++++++++++++++++++++
> 2 files changed, 347 insertions(+)
>
Hi Mark,
Maybe because I am new to this type of selftest (i.e. the helpers), or
maybe because I don't understand some of the used mechanisms here, but this
selftest felt hard to understand, even though the commit message made
it sound very simple.
So I will try to cover what I could understand, so please let me know if I
got something wrong.
>From both tests, the lower_el() one is the simplest: if the features are
there, you set TGE, EXLOCKEN and EXLOCK, and then do an KVM_SET_VCPU_EVENTS
ioctl which injects a dabt and commit_pending_events(). It should change
the PSTATE which you read and make sure EXLOCK=0.
(This should be about the last sentence in your commit msg)
For same_el(), 3 exception handlers are assigned to the vcpu: unknown,
ilegal, and GCS. You then run the guest once, and exit. Then you check the
exit reason.
In the guest code, you run a few tests for EXTLOCK={0,1} by basically
counting and checking how many times each handler was called, and if the
expected EXLOCK/EXLOCKEN states are correct. (Relying on R_WTXBY)
When you run test_udf_exception() it will cause an udf(COLLECT) that will
itself cause an udf(VALIDATE), and if EXLOCKEN=1, it will cause an illegal
exception, which will by itself cause an udf(REPAIR). On EXLOCKEN=0,
setting EXLOCK=0 should not take an exception, and thus there should be no
illegal++, an just 2 undef. None should cause a gcs exception.
That makes sense with the tests you do after the udf, as well as deciding
to check IL based on EXLOCKEN status.
As for test_sysreg(), it will run for 4 tested registers with both EXLOCKEN
states. What it does it take an udf(reg), which will read then write that
read value to that register. If EXLOCKEN=1 it's expected to cause a gcs
fault. That makes sense with the gcs and udf counters check in the bottom.
The part of re-running test_udf_exception() with HFGITR_EL2 set, IIUC, it
that part of fine-grained traps. It seems correct, although I am not seeing
any tests of the EC syndrome being 0x1A to make sure it's in use.
There are also a lot of expect_exlock being used as an int, even if it's a
bool. I understand the idea of summing it as we expect it to be 1 on true
or 0 on false, but is it really ok?
Maybe we could change it to int, so we just set it when we enter
test_sysreg() or test_udf_exception() like:
- expect_exlock = enable_exlock;
+ expect_exlock = enable_exlock ? 1 : 0;
If I got the workings right, and both above suggestions (0x1A and
bool->int) are either fixed or considered not an issue, then:
Reviewed-by: Leonardo Bras <[email protected]>
Thanks!
Leo
> diff --git a/tools/testing/selftests/kvm/Makefile.kvm
> b/tools/testing/selftests/kvm/Makefile.kvm
> index 96bab7002d39..0286453ab223 100644
> --- a/tools/testing/selftests/kvm/Makefile.kvm
> +++ b/tools/testing/selftests/kvm/Makefile.kvm
> @@ -179,6 +179,7 @@ TEST_GEN_PROGS_arm64 += arm64/hello_el2
> TEST_GEN_PROGS_arm64 += arm64/host_sve
> TEST_GEN_PROGS_arm64 += arm64/hypercalls
> TEST_GEN_PROGS_arm64 += arm64/external_aborts
> +TEST_GEN_PROGS_arm64 += arm64/gcs_exceptions
> TEST_GEN_PROGS_arm64 += arm64/mmio_sign_ext
> TEST_GEN_PROGS_arm64 += arm64/page_fault_test
> TEST_GEN_PROGS_arm64 += arm64/psci_test
> diff --git a/tools/testing/selftests/kvm/arm64/gcs_exceptions.c
> b/tools/testing/selftests/kvm/arm64/gcs_exceptions.c
> new file mode 100644
> index 000000000000..de5bc6590727
> --- /dev/null
> +++ b/tools/testing/selftests/kvm/arm64/gcs_exceptions.c
> @@ -0,0 +1,346 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * Test KVM's handling of Guarded Control Stack exception state.
> + */
> +
> +#include "kvm_util.h"
> +#include "processor.h"
> +#include "test_util.h"
> +#include "ucall.h"
> +
> +#include <asm/ptrace.h>
> +#include <asm/sysreg.h>
> +
> +#define PSTATE_IL_BIT BIT(20)
> +
> +static void require_guest_gcs(struct kvm_vcpu *vcpu)
> +{
> + u64 pfr1 = vcpu_get_reg(vcpu,
> + KVM_ARM64_SYS_REG(SYS_ID_AA64PFR1_EL1));
> +
> + TEST_REQUIRE(SYS_FIELD_GET(ID_AA64PFR1_EL1, GCS, pfr1) >=
> + ID_AA64PFR1_EL1_GCS_IMP);
> +}
> +
> +static bool expect_exlock;
> +static unsigned int undef_count;
> +static unsigned int illegal_count;
> +static unsigned int gcs_count;
> +static u64 handler_pstate;
> +static u64 illegal_pstate;
> +
> +static u64 expected_illegal_pc;
> +static enum udf_mode {
> + UDF_COLLECT, /* Initial UDF we collect PSTATE from */
> + UDF_VALIDATE, /* Secondary UDF that collects and validates */
> + UDF_REPAIR, /* UDF to clear EXLOCK */
> + UDF_ELR_EL1, /* Write to ELR_EL1 with EXLOCK */
> + UDF_ELR_EL2, /* Write to ELR_EL2 with EXLOCK */
> + UDF_SPSR_EL1, /* Write to SPSR_EL1 with EXLOCK */
> + UDF_SPSR_EL2, /* Write to SPSR_EL2 with EXLOCK */
> +} udf_mode;
> +
> +static void guest_undef_handler(struct ex_regs *regs)
> +{
> + u64 esr = read_sysreg(esr_el2);
> + u64 val;
> +
> + undef_count++;
> +
> + /* Just skip the instruction either way. */
> + GUEST_ASSERT_EQ(esr, ESR_ELx_IL);
> + regs->pc += 4;
> +
> + switch (udf_mode) {
> + case UDF_COLLECT:
> + GUEST_ASSERT_EQ(read_sysreg_s(SYS_GCSCR_EL2) &
> GCSCR_ELx_EXLOCKEN,
> + expect_exlock ? GCSCR_ELx_EXLOCKEN : 0);
> +
> + /*
> + * If we want the nested exception handler to change
> + * PSTATE it can't have EXLOCK set.
> + */
> + write_sysreg_s(0, SYS_GCSCR_EL2);
> + isb();
> +
> + /*
> + * Take another exception without returning so we can
> + * collect the EXLOCK state we're seeing here and make
> + * our return illegal.
> + */
> + expected_illegal_pc = regs->pc;
> + udf_mode = UDF_VALIDATE;
> + asm volatile("udf #0" ::: "memory");
> +
> + /* Reenable so PSTATE.EXLOCK is checked */
> + write_sysreg_s(expect_exlock ? GCSCR_ELx_EXLOCKEN : 0,
> + SYS_GCSCR_EL2);
> + isb();
> + break;
> +
> + case UDF_VALIDATE:
> + GUEST_ASSERT_EQ(read_sysreg_s(SYS_GCSCR_EL2) &
> GCSCR_ELx_EXLOCKEN,
> + 0);
> +
> + /* Validate the EXLOCK of the original exception. */
> + handler_pstate = regs->pstate;
> + GUEST_ASSERT_EQ(!!(regs->pstate & PSR_EXLOCK_BIT),
> + expect_exlock);
> +
> + /* Clearing PSTATE.EXLOCK triggers an illegal ERET. */
> + regs->pstate &= ~PSR_EXLOCK_BIT;
> + break;
> +
> + case UDF_REPAIR:
> + GUEST_ASSERT_EQ(read_sysreg_s(SYS_GCSCR_EL2) &
> GCSCR_ELx_EXLOCKEN,
> + 0);
> +
> + /* Disable EXLOCK for the requesting context */
> + regs->pstate &= ~PSR_EXLOCK_BIT;
> + break;
> +
> + case UDF_ELR_EL1:
> + GUEST_ASSERT_EQ(read_sysreg_s(SYS_GCSCR_EL2) &
> GCSCR_ELx_EXLOCKEN,
> + expect_exlock ? GCSCR_ELx_EXLOCKEN : 0);
> +
> + asm volatile("mrs %0, elr_el1 \n\
> + msr elr_el1, %0"
> + : "=r"(val) :: "memory");
> + break;
> +
> + case UDF_ELR_EL2:
> + GUEST_ASSERT_EQ(read_sysreg_s(SYS_GCSCR_EL2) &
> GCSCR_ELx_EXLOCKEN,
> + expect_exlock ? GCSCR_ELx_EXLOCKEN : 0);
> +
> + asm volatile("mrs %0, elr_el2 \n\
> + msr elr_el2, %0"
> + : "=r"(val) :: "memory");
> + break;
> +
> + case UDF_SPSR_EL1:
> + GUEST_ASSERT_EQ(read_sysreg_s(SYS_GCSCR_EL2) &
> GCSCR_ELx_EXLOCKEN,
> + expect_exlock ? GCSCR_ELx_EXLOCKEN : 0);
> +
> + asm volatile("mrs %0, spsr_el1 \n\
> + msr spsr_el1, %0"
> + : "=r"(val) :: "memory");
> + break;
> +
> + case UDF_SPSR_EL2:
> + GUEST_ASSERT_EQ(read_sysreg_s(SYS_GCSCR_EL2) &
> GCSCR_ELx_EXLOCKEN,
> + expect_exlock ? GCSCR_ELx_EXLOCKEN : 0);
> +
> + asm volatile("mrs %0, spsr_el2 \n\
> + msr spsr_el2, %0"
> + : "=r"(val) :: "memory");
> + break;
> +
> + default:
> + GUEST_FAIL("Invalid udf_mode");
> + }
> +}
> +
> +static void guest_illegal_handler(struct ex_regs *regs)
> +{
> + u64 esr = read_sysreg(esr_el2);
> +
> + illegal_count++;
> + illegal_pstate = regs->pstate;
> +
> + GUEST_ASSERT_EQ(ESR_ELx_EC(esr), ESR_ELx_EC_ILL);
> + GUEST_ASSERT(esr & ESR_ELx_IL);
> + GUEST_ASSERT(regs->pstate & PSTATE_IL_BIT);
> + GUEST_ASSERT(!(regs->pstate & PSR_EXLOCK_BIT));
> + GUEST_ASSERT_EQ(regs->pc, expected_illegal_pc);
> +
> + /* Disable exception locking so we can repair the return state. */
> + write_sysreg_s(0, SYS_GCSCR_EL2);
> + isb();
> +
> + /* We need our own EXLOCK clearing so we can repair the original */
> + udf_mode = UDF_REPAIR;
> + asm volatile("udf #0" ::: "memory");
> +
> + /* Disable EXLOCK for the generating context, making things legal */
> + regs->pstate &= ~(PSTATE_IL_BIT | PSR_EXLOCK_BIT);
> +}
> +
> +static void guest_gcs_handler(struct ex_regs *regs)
> +{
> + u64 esr = read_sysreg(esr_el2);
> +
> + gcs_count++;
> +
> + GUEST_ASSERT_EQ(ESR_ELx_EC(esr), ESR_ELx_EC_GCS);
> + GUEST_ASSERT_EQ(FIELD_GET(ESR_ELx_ExType_MASK, esr),
> + ESR_ELx_ExType_EXLOCK);
> +
> + /* Disabling EXLOCKEN makes sysreg writes legal. */
> + write_sysreg_s(0, SYS_GCSCR_EL2);
> + isb();
> +}
> +
> +static noinline void test_udf_exception(bool enable_exlock)
> +{
> + unsigned int initial_undef = undef_count;
> + unsigned int initial_illegal = illegal_count;
> + unsigned int initial_gcs = gcs_count;
> +
> + expect_exlock = enable_exlock;
> + write_sysreg_s(enable_exlock ? GCSCR_ELx_EXLOCKEN : 0,
> + SYS_GCSCR_EL2);
> + isb();
> +
> + udf_mode = UDF_COLLECT;
> + asm volatile("udf #0" ::: "memory");
> +
> + /* Leave GCS disabled even if a broken ERET skipped the ILL handler. */
> + write_sysreg_s(0, SYS_GCSCR_EL2);
> + isb();
> +
> + GUEST_ASSERT_EQ(undef_count, initial_undef + 2 + enable_exlock);
> + GUEST_ASSERT_EQ(illegal_count, initial_illegal + enable_exlock);
> + GUEST_ASSERT_EQ(!!(handler_pstate & PSR_EXLOCK_BIT), enable_exlock);
> + if (enable_exlock)
> + GUEST_ASSERT(illegal_pstate & PSTATE_IL_BIT);
> + GUEST_ASSERT_EQ(initial_gcs, gcs_count);
> +}
> +
> +static noinline void test_sysreg(enum udf_mode reg, bool enable_exlock)
> +{
> + unsigned int initial_undef = undef_count;
> + unsigned int initial_gcs = gcs_count;
> +
> + expect_exlock = enable_exlock;
> +
> + write_sysreg_s(enable_exlock ? GCSCR_ELx_EXLOCKEN : 0,
> + SYS_GCSCR_EL2);
> + isb();
> +
> + udf_mode = reg;
> + asm volatile("udf #0" ::: "memory");
> +
> + /* Ensure EXLOCK is disabled */
> + write_sysreg_s(0, SYS_GCSCR_EL2);
> + isb();
> +
> + GUEST_ASSERT_EQ(gcs_count, initial_gcs + expect_exlock);
> + GUEST_ASSERT_EQ(undef_count, initial_undef + 1);
> +}
> +
> +static void guest_code(void)
> +{
> + GUEST_ASSERT_EQ(get_current_el(), 2);
> +
> + /*
> + * Try straightforwardly generating an exception without and
> + * with EXLOCK.
> + */
> + test_udf_exception(false);
> + test_sysreg(UDF_ELR_EL1, false);
> + test_sysreg(UDF_ELR_EL2, false);
> + test_sysreg(UDF_SPSR_EL1, false);]
> + test_sysreg(UDF_SPSR_EL2, false);
> +
> + test_udf_exception(true);
> + test_sysreg(UDF_ELR_EL1, true);
> + test_sysreg(UDF_ELR_EL2, true);
> + test_sysreg(UDF_SPSR_EL1, true);
> + test_sysreg(UDF_SPSR_EL2, true);
> +
> + /* Force ERET directly through the slow emulation path as well. */
> + sysreg_clear_set_s(SYS_HFGITR_EL2, 0, HFGITR_EL2_ERET);
> + isb();
> +
> + test_udf_exception(false);
> +
> + test_udf_exception(true);
> +
> + sysreg_clear_set_s(SYS_HFGITR_EL2, HFGITR_EL2_ERET, 0);
> + isb();
> + GUEST_DONE();
> +}
> +
> +static void test_exception_from_same_el(void)
> +{
> + struct kvm_vcpu_init init;
> + struct kvm_vcpu *vcpu;
> + struct kvm_vm *vm;
> + struct ucall uc;
> +
> + vm = vm_create(1);
> + kvm_get_default_vcpu_target(vm, &init);
> + init.features[0] |= BIT(KVM_ARM_VCPU_HAS_EL2);
> + vcpu = aarch64_vcpu_add(vm, 0, &init, guest_code);
> + require_guest_gcs(vcpu);
> +
> + vm_init_descriptor_tables(vm);
> + vcpu_init_descriptor_tables(vcpu);
> + vm_install_sync_handler(vm, VECTOR_SYNC_CURRENT,
> + ESR_ELx_EC_UNKNOWN, guest_undef_handler);
> + vm_install_sync_handler(vm, VECTOR_SYNC_CURRENT,
> + ESR_ELx_EC_ILL, guest_illegal_handler);
> + vm_install_sync_handler(vm, VECTOR_SYNC_CURRENT,
> + ESR_ELx_EC_GCS, guest_gcs_handler);
> + kvm_arch_vm_finalize_vcpus(vm);
> +
> + vcpu_run(vcpu);
> + switch (get_ucall(vcpu, &uc)) {
> + case UCALL_DONE:
> + break;
> + case UCALL_ABORT:
> + REPORT_GUEST_ASSERT(uc);
> + break;
> + default:
> + TEST_FAIL("Unhandled ucall: %ld", uc.cmd);
> + }
> +
> + kvm_vm_free(vm);
> +}
> +
> +static void test_exception_from_lower_el(void)
> +{
> + struct kvm_vcpu_events events = {};
> + struct kvm_vcpu_init init;
> + struct kvm_vcpu *vcpu;
> + struct kvm_vm *vm;
> + u64 pstate;
> +
> + vm = vm_create(1);
> + kvm_get_default_vcpu_target(vm, &init);
> + init.features[0] |= BIT(KVM_ARM_VCPU_HAS_EL2);
> + vcpu = aarch64_vcpu_add(vm, 0, &init, NULL);
> + require_guest_gcs(vcpu);
> + kvm_arch_vm_finalize_vcpus(vm);
> +
> + /*
> + * Inject an EL0 data abort to vEL2 with EXLOCK enabled for
> + * vEL2.
> + */
> + vcpu_set_reg(vcpu, KVM_ARM64_SYS_REG(SYS_HCR_EL2),
> + vcpu_get_reg(vcpu, KVM_ARM64_SYS_REG(SYS_HCR_EL2)) |
> + HCR_EL2_TGE);
> + vcpu_set_reg(vcpu, KVM_ARM64_SYS_REG(SYS_GCSCR_EL2),
> + GCSCR_ELx_EXLOCKEN);
> + vcpu_set_reg(vcpu, ARM64_CORE_REG(regs.pstate),
> + PSR_MODE_EL0t | PSR_EXLOCK_BIT);
> +
> + events.exception.ext_dabt_pending = true;
> + vcpu_events_set(vcpu, &events);
> +
> + pstate = vcpu_get_reg(vcpu, ARM64_CORE_REG(regs.pstate));
> + TEST_ASSERT_EQ(pstate & PSR_MODE_MASK, PSR_MODE_EL2h);
> + TEST_ASSERT(!(pstate & PSR_EXLOCK_BIT),
> + "EXLOCK set on exception from a lower EL");
> +
> + kvm_vm_free(vm);
> +}
> +
> +int main(void)
> +{
> + TEST_REQUIRE(kvm_check_cap(KVM_CAP_ARM_EL2));
> + test_exception_from_same_el();
> + test_exception_from_lower_el();
> +
> + return 0;
> +}
>
> --
> 2.47.3
>