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
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ SRCS := \
core/vdso.c \
core/shim-globals.c \
core/bootstrap.c \
core/launch.c \
core/rosetta.c \
core/sysroot.c \
runtime/thread.c \
Expand Down
6 changes: 6 additions & 0 deletions mk/tests.mk
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
test-sysroot-pathmax test-sysroot-corpus \
test-sysroot-name-soak check-soak \
check-name-caseexact test-sysroot-path-matrix \
test-usage-synopsis \
probe-volume-naming perf

## Build and run the assembly hello world test
Expand Down Expand Up @@ -179,6 +180,7 @@ check: $(ELFUSE_BIN) $(TEST_DEPS) check-syscall-coverage \
$(call run-lane,test-sysroot-pathmax,guest paths at the host path ceiling)
$(call run-lane,test-sysroot-corpus,frozen on-disk spelling corpus)
$(call run-lane,test-sysroot-path-matrix,addressing modes agree across the path matrix)
$(call run-lane,test-usage-synopsis,usage synopsis renderings)
$(call run-lane,test-shebang-host,shebang parser unit test)
$(call run-lane,test-gva-contracts,gva-math.h call-site contract checks)
$(call run-lane,test-proctitle-host,proctitle argv-tail regression)
Expand Down Expand Up @@ -986,6 +988,10 @@ test-sysroot-procfs-exec: $(ELFUSE_BIN) $(BUILD_DIR)/test-procfs-exec
test-timeout-disable: $(ELFUSE_BIN) $(TEST_HELLO_DEP)
@$(ELFUSE_BIN) --timeout 0 $(TEST_DIR)/test-hello > /dev/null

## Check the --help and argument-error usage synopses against each other
test-usage-synopsis: $(ELFUSE_BIN)
@bash tests/test-usage-synopsis.sh $(ELFUSE_BIN)

## Run GDB stub integration tests (LLDB <-> elfuse gdbstub)
test-gdbstub: $(ELFUSE_BIN) $(TEST_DIR)/test-hello
@bash tests/test-gdbstub.sh -e $(ELFUSE_BIN) -v
Expand Down
212 changes: 212 additions & 0 deletions src/core/launch.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
/* elfuse VM launch: bring-up + GDB + run loop + teardown
*
* Copyright 2026 elfuse contributors
* SPDX-License-Identifier: Apache-2.0
*
* Implementation of elfuse_launch (contract and caller/callee ownership in
* launch.h). It lives apart from src/main.c so bring-up is one path behind a
* struct, leaving CLI concerns (option parsing, sysroot provisioning, the
* shebang loop) in main().
*
* shim_blob.h is included here, not in src/main.c, so the static
* shim_bin / shim_bin_len blob has a single object definition site.
*/

#include "launch.h"

#include <Hypervisor/Hypervisor.h>
#include <Hypervisor/hv_vcpu.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>

#include "core/bootstrap.h"
#include "core/guest.h"
#include "core/shim-globals.h"
#include "core/sysroot.h"

#include "runtime/futex.h" /* futex_interrupt_request */
#include "runtime/procemu.h" /* proc_pty_release_process_slaves */
#include "runtime/thread.h"
#include "syscall/poll.h" /* wakeup_pipe_signal */
#include "syscall/proc.h"

#include "debug/gdbstub.h"
#include "debug/log.h"
#include "debug/syscall-hist.h"

/* Embedded shim binary (generated by xxd -i from shim.bin). */
#include "shim_blob.h"

/* The shim code slot in the infra reserve is sized tight (INFRA_SHIM_SLOT, a
* few x the current blob) so the rest of the reserve goes to the page-table
* pool. If the shim ever outgrows the slot it would overlap the shim-data
* block; fail the build loudly rather than corrupt memory at boot. Enlarge
* INFRA_SHIM_SLOT (and shrink the pool to match) if this fires.
*/
_Static_assert(sizeof(shim_bin) <= INFRA_SHIM_SLOT,
"shim blob exceeds its infra slot; bump INFRA_SHIM_SLOT");

int elfuse_launch(const launch_args_t *args)
{
extern char **environ;
char **envp_use = environ;

guest_t g;
bool guest_initialized = false;
guest_bootstrap_t boot;
/* Local copy of the temp flag (ownership contract in launch.h): the
* caller's launch_args_t is const, and the flag must drop once the
* unlink happens.
*/
bool elf_host_temp = args->elf_host_temp;
/* The guest-visible entrypoint path is argv[0]; elf_path is the
* resolved host path to that binary. They differ when path
* translation or a FUSE-materialized temp is involved.
*/
const char *elf_guest_path = (args->guest_argc > 0 && args->guest_argv)
? args->guest_argv[0]
: args->elf_path;

if (guest_bootstrap_prepare(
&g, args->elf_path, elf_host_temp, elf_guest_path, args->sysroot,
args->guest_argc, args->guest_argv, envp_use, shim_bin,
shim_bin_len, args->verbose, &guest_initialized, &boot) < 0)
goto fail;

/* A FUSE-materialized temp has been loaded; drop it once the guest
* has its own mapping, unless Rosetta still needs the reopenable
* host path.
*/
if (elf_host_temp && !g.is_rosetta) {
unlink(args->elf_path);
elf_host_temp = false;
}

/* Reject GDB for a Rosetta (x86_64) guest here, not just in main(): the
* stub exposes the aarch64 shim's register/memory view, which is the wrong
* architecture for a Rosetta-translated x86_64 guest. main() rejects it up
* front via a static ELF probe, but enforcing it in elfuse_launch (once
* bring-up has set g.is_rosetta) makes every caller inherit the constraint,
* including the planned OCI run helper.
*/
if (args->gdb_port > 0 && g.is_rosetta) {
log_error(LAUNCH_GDB_X86_64_MSG);
goto fail;
}

if (args->sysroot) {
bool case_sensitive = true;
bool case_preserving = true;
if (sysroot_probe_case_sensitivity(args->sysroot, &case_sensitive,
&case_preserving) == 0)
proc_set_sysroot_casefold(case_preserving && !case_sensitive);
else
proc_set_sysroot_casefold(false);
} else {
proc_set_sysroot_casefold(false);
}

hv_vcpu_t vcpu;
hv_vcpu_exit_t *vexit;
if (guest_bootstrap_create_vcpu(&g, &boot, args->verbose, &vcpu, &vexit) <
0)
goto fail;

/* GDB setup must happen before the first run so entry-stop and
* hardware breakpoints can affect the initial vCPU.
*/
if (args->gdb_port > 0) {
if (gdb_stub_init(args->gdb_port, &g) < 0) {
log_error("failed to initialize GDB stub");
goto fail;
}
gdb_stub_sync_debug_regs(vcpu);
if (args->gdb_stop_on_entry)
gdb_stub_wait_for_attach();
}

/* vcpu_run_loop owns guest execution until exit, fatal signal, or timeout.
*/
int exit_code =
vcpu_run_loop(vcpu, vexit, &g, args->verbose, args->timeout_sec, NULL);

/* Tear down debugger state before joining workers: a worker parked in
* gdb_stub_handle_stop() stays active (not deactivated) until this
* broadcasts resume_cond, so joining first would just time out and
* detach it while it is still paused.
*/
gdb_stub_shutdown();
Comment thread
jserv marked this conversation as resolved.

/* Join worker vCPU threads before guest_destroy unmaps the guest slab: a
* sibling still mid-iteration in its own run loop would fault on freed
* guest memory and crash the host with SIGSEGV, masking the real exit
* code. The join is a no-op once workers have wound down (the common
* single-threaded case).
*
* vcpu_run_loop can also return via a bare break (alarm timeout 124, a
* fatal default-disposition signal, or ELR_EL1==0) with no one having
* requested exit_group or kicked the siblings out of hv_vcpu_run. Mirror
* guest_destroy's request-interrupt prefix here first; otherwise this join
* burns its full poll cap and detaches every worker, and guest_destroy's
* own interrupt-join skips them (it honors join_abandoned), leaving live
* pthreads to fault on the imminent unmap.
*/
if (!proc_exit_group_requested())
proc_request_exit_group(0);
futex_interrupt_request();
wakeup_pipe_signal();
thread_interrupt_all();
/* Workers parked on internal condvars (fork barrier, ptrace stop/wait)
* see neither the pipe nor the vCPU kick; broadcast so they re-check the
* exit-group flag and terminate before the join below gives up on them.
*/
thread_wake_exit_waiters();
thread_join_workers();

/* Diagnostic counter dump runs before guest_destroy so the
* shim_data mapping is still valid. ELFUSE_SHIM_STATS is the gate;
* an unset variable produces no output.
*/
if (shim_globals_stats_enabled())
shim_globals_counters_dump(&g);

/* Dump the startup histogram before guest_destroy so any
* cleanup-path syscalls (closing host fds, unmapping the slab) do
* not appear in the captured set. The dump is a no-op when
* ELFUSE_STARTUP_TRACE=syscalls was not requested.
*/
syscall_hist_dump();

/* Give back any pty slaves this process still holds before the guest
* teardown below. The guest's stdio slaves are closed by the kernel,
* not by the guest, so they never pass through the per-fd close hook; a
* master in another process would otherwise wait forever for a hangup
* this exit should have produced. Bring-up failures skip this: the fail
* path is only reachable before the run loop, so no slave exists yet.
*/
proc_pty_release_process_slaves();

if (guest_initialized)
guest_destroy(&g);

/* Rosetta guests keep the FUSE-materialized temp alive for the whole run
* (the translator reopens the host path); drop it now that the guest is
* gone so repeated Rosetta launches do not accumulate temp files.
*/
if (elf_host_temp)
unlink(args->elf_path);

return exit_code;

fail:
/* Bring-up failed: unwind whatever exists so far, including the temp
* unlink this side owns past the prepare call (contract in launch.h).
*/
if (guest_initialized)
guest_destroy(&g);
if (elf_host_temp)
unlink(args->elf_path);
return 1;
}
80 changes: 80 additions & 0 deletions src/core/launch.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/* elfuse VM launch entry: post-CLI bring-up + run loop + teardown
*
* Copyright 2026 elfuse contributors
* SPDX-License-Identifier: Apache-2.0
*
* elfuse_launch is the single entry point for "run a guest binary in a
* fresh HVF VM until it exits". main() is its only in-tree caller; keeping
* bring-up behind one struct is what lets another front end (the planned
* OCI run helper) reuse this path instead of growing a second bring-up.
*
* The function owns the guest_t, the vCPU, the GDB stub, the run loop, the
* diagnostic dumps, and guest teardown; it does NOT own the elf_path /
* sysroot / guest_argv heap copies or the sysroot_mount the host CLI may
* have provisioned. Those stay with the caller so behaviors that need the
* original CLI argv (proctitle rewriting, --create-sysroot detach on exit,
* host cwd save+restore) stay coherent however the launch was kicked off.
*
* The caller owns every pointer in launch_args_t for the duration of the
* call; elfuse_launch reads but never frees them. Per-field lifetime and
* ownership notes live on the struct members below.
*/

#pragma once

#include <stdbool.h>
#include <stdint.h>

typedef struct {
/* Host path to the guest ELF; may be a FUSE-materialized temp when
* elf_host_temp is set.
*/
const char *elf_path;

/* elf_path is a FUSE-materialized temp to unlink once
* guest_bootstrap_prepare has loaded it (kept for Rosetta guests, which
* reopen the path). The caller owns the unlink on any pre-prepare
* failure; elfuse_launch owns it from the prepare call onward,
* including a prepare that fails.
*/
bool elf_host_temp;

/* Host filesystem path to the sysroot the guest sees as / (absolute),
* or NULL when the guest runs without a sysroot.
*/
const char *sysroot;

/* Argv the guest sees. guest_argv[0] is the guest-visible entrypoint
* path (what the guest reads back via /proc/self/exe and argv[0]); it
* differs from elf_path (the resolved host path) under path translation
* or a FUSE-materialized temp.
*/
int guest_argc;
const char **guest_argv;

/* GDB Remote Serial Protocol port (0 disables the stub) and whether
* to halt before the first guest instruction.
*/
int gdb_port;
bool gdb_stop_on_entry;

/* Per-iteration vCPU run timeout. 0 disables (no alarm()). */
int timeout_sec;

bool verbose;
} launch_args_t;

/* Diagnostic for rejecting --gdb on an x86_64 (Rosetta) guest, shared by
* main()'s early static-ELF probe and elfuse_launch's authoritative
* post-bring-up check so the two sites cannot print divergent messages
* (tests/test-rosetta-cli.sh pins this text).
*/
#define LAUNCH_GDB_X86_64_MSG \
"--gdb is not supported for x86_64 guests; the current stub " \
"only exposes the translated aarch64 view"

/* Bring up the guest VM, run it to exit / signal / timeout, tear down,
* return the exit code. Returns 1 on bring-up failure (with a log
* message) and the guest's exit status otherwise.
*/
int elfuse_launch(const launch_args_t *args);
Loading
Loading