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
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
14 changes: 13 additions & 1 deletion mk/tests.mk
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down Expand Up @@ -77,25 +78,31 @@ 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

## Run the unit test suite plus busybox applet validation
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"
Expand Down Expand Up @@ -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
Expand Down
35 changes: 29 additions & 6 deletions src/syscall/proc.c
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
35 changes: 35 additions & 0 deletions src/syscall/proc.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hook contract is underspecified in three ways:

  • Stop semantics: state what the function returns when tick returns nonzero (currently exit code 0, indistinguishable from guest exit 0).
  • Concurrency and lifetime: when one vcpu_run_hooks_t/opaque is shared across worker vCPUs, tick is called concurrently from multiple threads, so it must be thread-safe. The implementation keeps no copy and dereferences hooks/opaque every iteration, so the caller must keep both alive until the function returns.
  • Cooperative-only timing: the tick fires only at the top of the loop (host round-trips), not after hv_vcpu_run() returns. A worker (timeout_sec == 0, no alarm()) spinning entirely in EL0 never reaches the top, so a hook cannot interrupt a runaway guest -- only exit_group's hv_vcpus_exit() kick can. "before each hv_vcpu_run() re-entry" reads as periodic; clarify it is checked at host boundaries only.


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.
Expand All @@ -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);
62 changes: 62 additions & 0 deletions tests/test-vcpu-run-hooks-host.c
Original file line number Diff line number Diff line change
@@ -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 <assert.h>
#include <stddef.h>
#include <stdio.h>

#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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The runtime half of this test is tautological: given the initializer, hooks.tick/hooks.opaque are never NULL, so the if can't fire and main always reaches PASS; the tick function is defined but never called. The real value is the two _Static_assert's, which fire at compile time -- building the object is what matters, not running the binary, yet mk/tests.mk prints a "unit test" banner and executes it, implying runtime validation that does not exist. The new executable behavior (invoke tick, break on nonzero) has no coverage since no in-tree caller passes non-NULL hooks. Either make it a compile-only target, or have main actually call tick(g, opaque) and assert on both the non-NULL and NULL-opaque short-circuit results.

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;
}
Loading