diff --git a/Makefile b/Makefile index c7dbcade..5e33d6d2 100644 --- a/Makefile +++ b/Makefile @@ -202,6 +202,11 @@ $(BUILD_DIR)/test-thread-churn: tests/test-thread-churn.c | $(BUILD_DIR) @echo " CROSS $< (with -lpthread)" $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -o $@ $< -lpthread +# test-cntvct-thread verifies cloned vCPUs inherit EL0 timer access. +$(BUILD_DIR)/test-cntvct-thread: tests/test-cntvct-thread.c | $(BUILD_DIR) + @echo " CROSS $< (with -lpthread)" + $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -o $@ $< -lpthread + # test-poll uses pthread_kill to verify blocked read signal delivery. $(BUILD_DIR)/test-poll: tests/test-poll.c | $(BUILD_DIR) @echo " CROSS $< (with -lpthread)" diff --git a/src/core/bootstrap.c b/src/core/bootstrap.c index 481929a1..db62eda6 100644 --- a/src/core/bootstrap.c +++ b/src/core/bootstrap.c @@ -733,11 +733,8 @@ int guest_bootstrap_create_vcpu(guest_t *g, */ shim_globals_set_singleton(g); - /* CNTKCTL_EL1.EL0VCTEN | EL0PCTEN: allow EL0 to read {CNTVCT,CNTPCT}_EL0. - * Required by the vDSO clock_gettime fast path (and is the default on - * native Linux), without which the guest gets 0 back from MRS. - */ - HV_CHECK(hv_vcpu_set_sys_reg(vcpu, HV_SYS_REG_CNTKCTL_EL1, 0x3ULL)); + HV_CHECK(hv_vcpu_set_sys_reg(vcpu, HV_SYS_REG_CNTKCTL_EL1, + CNTKCTL_EL1_EL0_TIMER_EN)); HV_CHECK(hv_vcpu_get_sys_reg(vcpu, HV_SYS_REG_SCTLR_EL1, &sctlr)); log_debug("SCTLR_EL1 default=0x%llx", (unsigned long long) sctlr); diff --git a/src/hvutil.h b/src/hvutil.h index f5c61a25..3f7024d1 100644 --- a/src/hvutil.h +++ b/src/hvutil.h @@ -49,6 +49,12 @@ #define SCTLR_UCT (1ULL << 15) /* EL0 access to CTR_EL0 (cache type) */ #define SCTLR_UCI (1ULL << 26) /* EL0 cache maintenance (IC IVAU, DC CVA*) */ +/* CNTKCTL_EL1 bits exposed to EL0. Linux enables both virtual and physical + * counter reads for userspace; vDSO clock_gettime and runtime timer fast paths + * depend on cloned vCPUs matching bootstrap's timer access configuration. + */ +#define CNTKCTL_EL1_EL0_TIMER_EN 0x3ULL /* EL0VCTEN | EL0PCTEN */ + /* RES1 bits in SCTLR_EL1: these MUST be 1 for correct behavior. Apple * Hypervisor.framework returns default SCTLR=0x0, so the setup code must set * them explicitly. The hardware eventually auto-updates them, but the initial diff --git a/src/runtime/forkipc.c b/src/runtime/forkipc.c index 03cadcee..fec586a5 100644 --- a/src/runtime/forkipc.c +++ b/src/runtime/forkipc.c @@ -863,6 +863,8 @@ static void *thread_create_and_run(void *arg) WORKER_HV(hv_vcpu_set_sys_reg(vcpu, HV_SYS_REG_TCR_EL1, tca->tcr)); WORKER_HV(hv_vcpu_set_sys_reg(vcpu, HV_SYS_REG_TTBR0_EL1, tca->ttbr0)); WORKER_HV(hv_vcpu_set_sys_reg(vcpu, HV_SYS_REG_CPACR_EL1, tca->cpacr)); + WORKER_HV(hv_vcpu_set_sys_reg(vcpu, HV_SYS_REG_CNTKCTL_EL1, + CNTKCTL_EL1_EL0_TIMER_EN)); /* All worker vCPUs in the process share the same shim_globals base (one VM * per process); a fresh TPIDR_EL1 set is still required because HVF created @@ -1212,6 +1214,8 @@ static void *vm_clone_thread_run(void *arg) HV_CHECK(hv_vcpu_set_sys_reg(vcpu, HV_SYS_REG_TCR_EL1, tca->tcr)); HV_CHECK(hv_vcpu_set_sys_reg(vcpu, HV_SYS_REG_TTBR0_EL1, tca->ttbr0)); HV_CHECK(hv_vcpu_set_sys_reg(vcpu, HV_SYS_REG_CPACR_EL1, tca->cpacr)); + HV_CHECK(hv_vcpu_set_sys_reg(vcpu, HV_SYS_REG_CNTKCTL_EL1, + CNTKCTL_EL1_EL0_TIMER_EN)); HV_CHECK(hv_vcpu_set_sys_reg(vcpu, HV_SYS_REG_SCTLR_EL1, tca->sctlr)); HV_CHECK(hv_vcpu_set_sys_reg(vcpu, HV_SYS_REG_SP_EL1, tca->sp_el1)); HV_CHECK(hv_vcpu_set_sys_reg(vcpu, HV_SYS_REG_SP_EL0, tca->child_stack)); diff --git a/tests/test-cntvct-thread.c b/tests/test-cntvct-thread.c new file mode 100644 index 00000000..7089366e --- /dev/null +++ b/tests/test-cntvct-thread.c @@ -0,0 +1,105 @@ +/* + * test-cntvct-thread.c -- EL0 timer counter access on cloned vCPUs + * + * Copyright 2026 elfuse contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Linux exposes EL0 timer counter reads on AArch64. elfuse's vDSO fast path + * depends on CNTVCT_EL0, so this test verifies worker vCPUs created by + * clone(CLONE_THREAD) get the same virtual-counter configuration as the + * bootstrap vCPU. + * + * This does not exercise elfuse's CLONE_VM-without-CLONE_THREAD worker path. + * The production fix uses the same CNTKCTL_EL1_EL0_TIMER_EN constant for both + * clone paths so they cannot drift, but this regression test stays on the + * pthread/CLONE_THREAD path to remain valid on the QEMU reference lane too. + */ + +#include +#include + +#include "test-harness.h" + +#if !defined(__aarch64__) +#error "test-cntvct-thread is aarch64-only" +#endif + +int passes = 0, fails = 0; + +typedef struct { + uint64_t first; + uint64_t second; +} timer_sample_t; + +static inline uint64_t read_cntvct(void) +{ + uint64_t value; + __asm__ volatile("mrs %0, cntvct_el0" : "=r"(value)); + return value; +} + +static void expect_timer_sample(const char *label, const timer_sample_t *sample) +{ + char msg[128]; + + /* Today elfuse's trapped-MRS fallback returns 0 for CNTVCT_EL0, so the + * nonzero check catches missing CNTKCTL_EL1.EL0VCTEN on cloned vCPUs. If a + * CNTVCT_EL0 host fallback is added later, this regression test must grow a + * stronger signal that EL0 direct counter access was configured. + */ + snprintf(msg, sizeof(msg), "%s CNTVCT_EL0 was zero or non-monotonic", + label); + EXPECT_TRUE(sample->first != 0 && sample->second >= sample->first, msg); +} + +static void *read_timer_thread(void *arg) +{ + timer_sample_t *sample = (timer_sample_t *) arg; + + sample->first = read_cntvct(); + sample->second = read_cntvct(); + + return NULL; +} + +static void test_main_vcpu_timers(void) +{ + TEST("main vCPU EL0 timers"); + + timer_sample_t sample = {0}; + sample.first = read_cntvct(); + sample.second = read_cntvct(); + expect_timer_sample("main vCPU", &sample); +} + +static void test_clone_thread_vcpu_timers(void) +{ + TEST("clone(CLONE_THREAD) vCPU EL0 timers"); + + timer_sample_t sample = {0}; + pthread_t thread; + int err = pthread_create(&thread, NULL, read_timer_thread, &sample); + if (err != 0) { + FAIL("pthread_create failed"); + return; + } + + err = pthread_join(thread, NULL); + if (err != 0) { + FAIL("pthread_join failed"); + return; + } + + expect_timer_sample("clone(CLONE_THREAD) vCPU", &sample); +} + +int main(void) +{ + printf("test-cntvct-thread: EL0 timer cloned vCPU tests\n\n"); + + test_main_vcpu_timers(); + test_clone_thread_vcpu_timers(); + + SUMMARY("test-cntvct-thread"); + return fails ? 1 : 0; +} diff --git a/tests/test-matrix.sh b/tests/test-matrix.sh index 18a20920..a5380bb8 100755 --- a/tests/test-matrix.sh +++ b/tests/test-matrix.sh @@ -674,6 +674,8 @@ run_unit_tests() printf "\nThreading\n" test_check "$runner" "test-thread" "0 failed" "$bindir/test-thread" test_check "$runner" "test-pthread" "0 failed" "$bindir/test-pthread" + test_check "$runner" "test-cntvct-thread" "0 failed" \ + "$bindir/test-cntvct-thread" test_check "$runner" "test-osync-requeue" "0 failed" "$bindir/test-osync-requeue" test_check "$runner" "test-simd-clone" "0 failed" "$bindir/test-simd-clone" test_check "$runner" "test-stress" "0 failed" "$bindir/test-stress" @@ -1221,8 +1223,8 @@ run_suite() # observed counts diverge. apple-unknown is the fallback row for SoC strings the # detector does not recognize yet. EXPECTED_BASELINES=( - "elfuse-aarch64|237|0" - "qemu-aarch64|217|0" + "elfuse-aarch64|238|0" + "qemu-aarch64|218|0" "elfuse-x86_64:apple-m1-m2|71|0" "elfuse-x86_64:apple-m3-plus|71|0" "elfuse-x86_64:apple-unknown|71|0"