diff --git a/Makefile b/Makefile index d50e35d3..7a473bae 100644 --- a/Makefile +++ b/Makefile @@ -153,6 +153,12 @@ $(BUILD_DIR)/test-fork-ipc-protocol-host: \ @echo " LD $@" $(Q)$(CC) $(CFLAGS) -o $@ $^ +## Build the vCPU run-loop hook API compile test (native macOS binary). +$(BUILD_DIR)/test-vcpu-run-hooks-host: \ + $(BUILD_DIR)/test-vcpu-run-hooks-host.o | $(BUILD_DIR) + @echo " LD $@" + $(Q)$(CC) $(CFLAGS) -o $@ $^ + ## Build the identity override host test (native macOS binary) $(BUILD_DIR)/test-identity-override-host: \ $(BUILD_DIR)/test-identity-override-host.o \ diff --git a/mk/tests.mk b/mk/tests.mk index 5f07a44c..453fb90d 100644 --- a/mk/tests.mk +++ b/mk/tests.mk @@ -12,7 +12,8 @@ test-full test-multi-vcpu test-rwx test-sysroot-rename \ test-case-collision test-case-collision-fallback test-getdents64-overlong \ test-sysroot-host-fallback test-sysroot-case-exact \ - test-sysroot-create-paths test-fork-ipc-protocol-host test-identity-override-host \ + test-sysroot-create-paths test-fork-ipc-protocol-host \ + test-vcpu-run-hooks-host test-identity-override-host \ test-proctitle-host test-proctitle-low-stack \ test-sysroot-procfs-exec test-timeout-disable test-fuse-alpine \ test-sysroot-nofollow test-sysroot-chdir test-sysroot-symlink-escape \ @@ -77,12 +78,15 @@ SANITIZER_SECTIONS := Threading|Stress|Signal.*thread|Fork edge|CoW fork|Guard p check-sanitizer: $(ELFUSE_BIN) $(TEST_DEPS) \ $(BUILD_DIR)/test-tlbi-encoder-host \ $(BUILD_DIR)/test-fork-ipc-protocol-host \ + $(BUILD_DIR)/test-vcpu-run-hooks-host \ $(BUILD_DIR)/test-identity-override-host @bash tests/driver.sh -e $(ELFUSE_BIN) -d $(TEST_DIR) -v -s '$(SANITIZER_SECTIONS)' @printf "\n$(BLUE)━━━ TLBI RVAE1IS encoder unit test ━━━$(RESET)\n" @$(BUILD_DIR)/test-tlbi-encoder-host @printf "\n$(BLUE)━━━ fork IPC protocol identity unit test ━━━$(RESET)\n" @$(BUILD_DIR)/test-fork-ipc-protocol-host + @printf "\n$(BLUE)━━━ vCPU run-loop hook API unit test ━━━$(RESET)\n" + @$(BUILD_DIR)/test-vcpu-run-hooks-host @printf "\n$(BLUE)━━━ identity override unit test ━━━$(RESET)\n" @$(BUILD_DIR)/test-identity-override-host @@ -90,12 +94,15 @@ check-sanitizer: $(ELFUSE_BIN) $(TEST_DEPS) \ check: $(ELFUSE_BIN) $(TEST_DEPS) check-syscall-coverage \ $(BUILD_DIR)/test-tlbi-encoder-host \ $(BUILD_DIR)/test-fork-ipc-protocol-host \ + $(BUILD_DIR)/test-vcpu-run-hooks-host \ $(BUILD_DIR)/test-identity-override-host @bash tests/driver.sh -e $(ELFUSE_BIN) -d $(TEST_DIR) -v @printf "\n$(BLUE)━━━ TLBI RVAE1IS encoder unit test ━━━$(RESET)\n" @$(BUILD_DIR)/test-tlbi-encoder-host @printf "\n$(BLUE)━━━ fork IPC protocol identity unit test ━━━$(RESET)\n" @$(BUILD_DIR)/test-fork-ipc-protocol-host + @printf "\n$(BLUE)━━━ vCPU run-loop hook API unit test ━━━$(RESET)\n" + @$(BUILD_DIR)/test-vcpu-run-hooks-host @printf "\n$(BLUE)━━━ identity override unit test ━━━$(RESET)\n" @$(BUILD_DIR)/test-identity-override-host @printf "\n$(BLUE)━━━ shebang parser unit test ━━━$(RESET)\n" @@ -752,6 +759,11 @@ test-rwx: $(BUILD_DIR)/test-rwx test-fork-ipc-protocol-host: $(BUILD_DIR)/test-fork-ipc-protocol-host $(BUILD_DIR)/test-fork-ipc-protocol-host +# vCPU run-loop hook API regression +## Run the vCPU run-loop hook API unit test +test-vcpu-run-hooks-host: $(BUILD_DIR)/test-vcpu-run-hooks-host + $(BUILD_DIR)/test-vcpu-run-hooks-host + # Proctitle argv-tail regression ## Run the deterministic argv-tail overshoot guard test test-proctitle-host: $(BUILD_DIR)/test-proctitle-host diff --git a/src/syscall/proc.c b/src/syscall/proc.c index 8d28088d..d1f37c98 100644 --- a/src/syscall/proc.c +++ b/src/syscall/proc.c @@ -2962,12 +2962,13 @@ static const hv_sys_reg_t hvc4_sysregs[] = { * Both modes check proc_exit_group_requested so the main thread also reacts to * exit_group called by a worker. */ -int vcpu_run_loop(hv_vcpu_t vcpu, - hv_vcpu_exit_t *vexit, - guest_t *g, - bool verbose, - int timeout_sec, - int *wait_status_out) +int vcpu_run_loop_with_hooks(hv_vcpu_t vcpu, + hv_vcpu_exit_t *vexit, + guest_t *g, + bool verbose, + int timeout_sec, + int *wait_status_out, + const vcpu_run_hooks_t *hooks) { int exit_code = 0; (void) signal_take_termination_wait_status(); @@ -2998,6 +2999,17 @@ int vcpu_run_loop(hv_vcpu_t vcpu, exit_code = proc_exit_group_code(); break; } + if (hooks && hooks->tick) { + int tick_ret = hooks->tick(g, hooks->opaque); + if (tick_ret != 0) { + exit_code = tick_ret; + break; + } + if (proc_exit_group_requested()) { + exit_code = proc_exit_group_code(); + break; + } + } if (verbose) { uint64_t pc; @@ -4118,3 +4130,14 @@ int vcpu_run_loop(hv_vcpu_t vcpu, } return exit_code; } + +int vcpu_run_loop(hv_vcpu_t vcpu, + hv_vcpu_exit_t *vexit, + guest_t *g, + bool verbose, + int timeout_sec, + int *wait_status_out) +{ + return vcpu_run_loop_with_hooks(vcpu, vexit, g, verbose, timeout_sec, + wait_status_out, NULL); +} diff --git a/src/syscall/proc.h b/src/syscall/proc.h index f8497b1a..84db90d0 100644 --- a/src/syscall/proc.h +++ b/src/syscall/proc.h @@ -498,6 +498,34 @@ int proc_exit_group_requested(void); */ void proc_request_hvc6_yield(void); +/* Optional embedder hook. Called on the vCPU owner thread at host boundaries + * (before each hv_vcpu_run() entry or re-entry). + * + * Stop semantics: + * Return nonzero to stop the loop. A nonzero tick return is propagated + * and returned as the run loop's exit code. + * + * Cooperative-only timing: + * The tick is only checked at host boundaries, not during guest execution. + * A guest spinning entirely in EL0 without VM-exiting will never trigger + * the tick, meaning a tick-initiated hook cannot interrupt a runaway guest. + * To force interruption of a runaway guest, use hv_vcpus_exit(). + * + * Concurrency and lifetime: + * When a single vcpu_run_hooks_t and its opaque data are shared across + * multiple vCPUs, the tick function will be invoked concurrently from + * multiple host threads and must be thread-safe. The run loop does not keep + * a copy of the hooks structure and dereferences it every iteration; the + * caller must ensure the hooks structure and opaque lifetime extend until + * all run loops return. + */ +typedef int (*vcpu_run_loop_tick_fn)(guest_t *g, void *opaque); + +typedef struct vcpu_run_hooks { + vcpu_run_loop_tick_fn tick; + void *opaque; +} vcpu_run_hooks_t; + /* Run the vCPU execution loop. * * Returns the exit code. @@ -513,3 +541,10 @@ int vcpu_run_loop(hv_vcpu_t vcpu, bool verbose, int timeout_sec, int *wait_status_out); +int vcpu_run_loop_with_hooks(hv_vcpu_t vcpu, + hv_vcpu_exit_t *vexit, + guest_t *g, + bool verbose, + int timeout_sec, + int *wait_status_out, + const vcpu_run_hooks_t *hooks); diff --git a/tests/test-vcpu-run-hooks-host.c b/tests/test-vcpu-run-hooks-host.c new file mode 100644 index 00000000..cd965bc2 --- /dev/null +++ b/tests/test-vcpu-run-hooks-host.c @@ -0,0 +1,62 @@ +/* + * Native-host compile test for vcpu_run_loop hook API compatibility. + * + * Copyright 2026 elfuse contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include "syscall/proc.h" + +typedef int vcpu_run_loop_sig(hv_vcpu_t vcpu, + hv_vcpu_exit_t *vexit, + guest_t *g, + bool verbose, + int timeout_sec, + int *wait_status_out); + +typedef int vcpu_run_loop_with_hooks_sig(hv_vcpu_t vcpu, + hv_vcpu_exit_t *vexit, + guest_t *g, + bool verbose, + int timeout_sec, + int *wait_status_out, + const vcpu_run_hooks_t *hooks); + +_Static_assert(__builtin_types_compatible_p(__typeof__(vcpu_run_loop), + vcpu_run_loop_sig), + "vcpu_run_loop compatibility signature changed"); +_Static_assert( + __builtin_types_compatible_p(__typeof__(vcpu_run_loop_with_hooks), + vcpu_run_loop_with_hooks_sig), + "vcpu_run_loop_with_hooks signature changed"); + +static int tick(guest_t *g, void *opaque) +{ + return g != NULL && opaque != NULL; +} + +int main(void) +{ + vcpu_run_hooks_t hooks = { + .tick = tick, + .opaque = (void *) 0x1, + }; + + if (hooks.tick == NULL || hooks.opaque == NULL) { + fprintf(stderr, "test-vcpu-run-hooks-host: API wiring failed\n"); + return 1; + } + + /* Verify tick actually functions with non-NULL and NULL parameters */ + assert(hooks.tick((guest_t *) 0x1234, hooks.opaque) == 1); + assert(hooks.tick(NULL, hooks.opaque) == 0); + assert(hooks.tick((guest_t *) 0x1234, NULL) == 0); + assert(hooks.tick(NULL, NULL) == 0); + + printf("test-vcpu-run-hooks-host: PASS\n"); + return 0; +}