Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand Down
7 changes: 2 additions & 5 deletions src/core/bootstrap.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 6 additions & 0 deletions src/hvutil.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions src/runtime/forkipc.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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));
Expand Down
105 changes: 105 additions & 0 deletions tests/test-cntvct-thread.c
Original file line number Diff line number Diff line change
@@ -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 <pthread.h>
#include <stdint.h>

#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;
}
6 changes: 4 additions & 2 deletions tests/test-matrix.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
Loading