diff --git a/Makefile b/Makefile index d1415cfe..03b0f16b 100644 --- a/Makefile +++ b/Makefile @@ -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 \ diff --git a/mk/tests.mk b/mk/tests.mk index dcf96628..e76926c1 100644 --- a/mk/tests.mk +++ b/mk/tests.mk @@ -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 @@ -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) @@ -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 diff --git a/src/core/launch.c b/src/core/launch.c new file mode 100644 index 00000000..18dc1f70 --- /dev/null +++ b/src/core/launch.c @@ -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 +#include +#include +#include +#include +#include + +#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(); + + /* 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; +} diff --git a/src/core/launch.h b/src/core/launch.h new file mode 100644 index 00000000..fe2ed9f9 --- /dev/null +++ b/src/core/launch.h @@ -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 +#include + +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); diff --git a/src/main.c b/src/main.c index 36d79a2c..3f540d63 100644 --- a/src/main.c +++ b/src/main.c @@ -12,11 +12,11 @@ * - Guest memory identity-mapped at GVA=GPA with 2MiB block page tables. * - Syscall handlers that translate Linux syscalls to macOS equivalents. * - * Usage: elfuse [--verbose] [--timeout N] [--sysroot PATH] [args...] + * Usage: elfuse [options] [args...]; `elfuse --help` lists the + * options. The flag list lives once in ELFUSE_USAGE_BODY below; --help and + * the argument-error paths render it differently but cannot drift apart. */ -#include -#include #include #include #include @@ -31,22 +31,17 @@ #include "core/bootstrap.h" #include "core/guest.h" +#include "core/launch.h" #include "core/rosetta.h" -#include "core/shim-globals.h" #include "core/sysroot.h" #include "runtime/forkipc.h" -#include "runtime/futex.h" /* futex_interrupt_request */ -#include "runtime/procemu.h" /* proc_pty_release_process_slaves */ #include "runtime/proctitle.h" -#include "runtime/thread.h" #include "syscall/fuse.h" #include "syscall/path.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" @@ -109,45 +104,6 @@ static void free_guest_argv(const char **guest_argv, int guest_argc) free((void *) guest_argv); } -static void cleanup_main_resources(guest_t *g, - bool guest_initialized, - sysroot_mount_t *sysroot_mount, - const char *host_cwd, - const char **guest_argv, - int guest_argc, - char *elf_path, - char *sysroot_path) -{ - /* guest_destroy may defer HVF teardown to process exit when a worker vCPU - * is still live, but the remaining cleanup below touches only host state - * (mounts, cwd, heap) -- never HVF or guest memory -- so it is safe to run - * regardless and must run so a deferred teardown does not orphan the - * sysroot mount or the FUSE-materialized temp ELF, which process exit will - * not reclaim. - */ - if (guest_initialized) - guest_destroy(g); - rosettad_clear_binary_path(); - if (host_cwd && host_cwd[0] != '\0' && chdir(host_cwd) < 0) - (void) chdir("/"); - sysroot_cleanup_mount(sysroot_mount); - free_guest_argv(guest_argv, guest_argc); - free((void *) elf_path); - free((void *) sysroot_path); -} - -/* 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"); - /* The infra-reserve layout invariants documented in guest.h are derived from * raw offset constants, so a future edit that grows the pool by shifting one * offset without the others would silently overlap two regions. Enforce them at @@ -239,6 +195,24 @@ static int host_dc_zva_assert(void) return 0; } +/* Usage synopsis: one flag list, two renderings. @sep joins the groups, + * with a space for ELFUSE_USAGE and a newline plus an indent to the column + * after "usage: elfuse " for ELFUSE_USAGE_WRAPPED. + * + * The error paths need the flat form because log_error stamps a timestamp + * and a source location on the first line only, so a wrapped string prints + * ragged under the prefix; --help has no prefix and takes the wrapped form, + * which fits 80 columns. Sharing one body keeps the flag list from drifting + * between them (one copy had already lost the --gdb flags). + */ +#define ELFUSE_USAGE_BODY(sep) \ + "usage: elfuse [--verbose] [--timeout N] [--sysroot PATH]" sep \ + "[--create-sysroot PATH] [--no-rosetta] [--fakeroot]" sep \ + "[--gdb PORT] [--gdb-stop-on-entry] [args...]" + +#define ELFUSE_USAGE ELFUSE_USAGE_BODY(" ") +#define ELFUSE_USAGE_WRAPPED ELFUSE_USAGE_BODY("\n ") + int main(int argc, char **argv) { log_init(); @@ -263,6 +237,22 @@ int main(int argc, char **argv) bool gdb_stop_on_entry = false; bool fakeroot = false; int arg_start = 1; + /* Everything the shared cleanup label reads is declared and initialized + * here, above the option loop, so any later error path can `goto cleanup`: + * a goto that skipped an initializer would leave the unwind reading + * indeterminate state. Zero state makes every cleanup step a no-op. + */ + char *elf_path = NULL; + char *sysroot_path = NULL; + const char **guest_argv = NULL; + int guest_argc = 0; + sysroot_mount_t sysroot_mount; + char host_cwd[LINUX_PATH_MAX]; + char elf_host_path[LINUX_PATH_MAX]; + bool elf_host_temp = false; + bool have_host_cwd = (getcwd(host_cwd, sizeof(host_cwd)) != NULL); + int exit_code = 1; + memset(&sysroot_mount, 0, sizeof(sysroot_mount)); /* 'elfuse rosettad translate ' runs the real Apple rosettad * binary inside an elfuse guest to materialise an AOT translation. The @@ -291,11 +281,8 @@ int main(int argc, char **argv) } if (!strcmp(argv[1], "--help") || !strcmp(argv[1], "-h")) { printf( - "usage: elfuse [--verbose] [--timeout N] [--sysroot PATH]\n" - " [--create-sysroot PATH]\n" - " [--no-rosetta] [--fakeroot]\n" - " [--gdb PORT] [--gdb-stop-on-entry]\n" - " [args...]\n" + ELFUSE_USAGE_WRAPPED + "\n" "\n" "Options:\n" " -h, --help Show this help and exit\n" @@ -331,7 +318,7 @@ int main(int argc, char **argv) } if (host_dc_zva_assert() < 0) - return 1; + goto cleanup; /* Parse elfuse options until the first guest argv element. */ while (arg_start < argc && argv[arg_start][0] == '-') { @@ -352,7 +339,7 @@ int main(int argc, char **argv) if (parse_int_arg(argv[arg_start + 1], 0, INT_MAX, &fork_child_fd) < 0) { log_error("invalid fork child fd: %s", argv[arg_start + 1]); - return 1; + goto cleanup; } arg_start += 2; } else if (!strcmp(argv[arg_start], "--vfork-notify-fd") && @@ -360,7 +347,7 @@ int main(int argc, char **argv) if (parse_int_arg(argv[arg_start + 1], 0, INT_MAX, &vfork_notify_fd) < 0) { log_error("invalid vfork notify fd: %s", argv[arg_start + 1]); - return 1; + goto cleanup; } arg_start += 2; } else if (!strcmp(argv[arg_start], "--sysroot") && @@ -380,7 +367,7 @@ int main(int argc, char **argv) } else if (!strcmp(argv[arg_start], "--gdb") && arg_start + 1 < argc) { if (parse_int_arg(argv[arg_start + 1], 1, 65535, &gdb_port) < 0) { log_error("invalid GDB port: %s", argv[arg_start + 1]); - return 1; + goto cleanup; } if (!verbose) log_set_level(LOG_INFO); @@ -393,19 +380,15 @@ int main(int argc, char **argv) break; } else { log_error("unknown option: %s", argv[arg_start]); - log_error( - "usage: elfuse [--verbose] [--timeout N] " - "[--sysroot PATH] [--create-sysroot PATH] [--no-rosetta] " - "[--fakeroot] [--gdb PORT] " - "[--gdb-stop-on-entry] [args...]"); - return 1; + log_error(ELFUSE_USAGE); + goto cleanup; } } if (sysroot && create_sysroot) { log_error( "use either --sysroot PATH or --create-sysroot PATH, not both"); - return 1; + goto cleanup; } /* ELFUSE_NO_ROSETTA=1 mirrors --no-rosetta for environments where passing @@ -446,7 +429,7 @@ int main(int argc, char **argv) "ELFUSE_FAKEROOT_EXEC must be an absolute path shorter than %d " "bytes", LINUX_PATH_MAX); - return 1; + goto cleanup; } /* Top-level processes establish the capacity; fork helpers normally inherit @@ -455,14 +438,14 @@ int main(int argc, char **argv) * internal host reserve. */ if (host_nofile_ensure_capacity() < 0) - return 1; + goto cleanup; /* Block the vCPU-preemption signals and start the sigwait thread before any * vCPU thread exists, so both the normal path and the fork-child path below * inherit the block on every thread they spawn. */ if (proc_preempt_init() < 0) - return 1; + goto cleanup; /* Fork-child mode: receive VM state over IPC and run */ if (fork_child_fd >= 0) @@ -470,21 +453,17 @@ int main(int argc, char **argv) timeout_sec); if (arg_start >= argc) { - log_error( - "usage: elfuse [--verbose] [--timeout N] " - "[--sysroot PATH] [--create-sysroot PATH] [--no-rosetta] " - "[--fakeroot] [args...]"); - return 1; + log_error(ELFUSE_USAGE); + goto cleanup; } /* Copy elf_path and guest_argv to heap because the original argv string * data lives in a contiguous stack region that elfuse clobbers below for * the process title (PostgreSQL/nginx argv-clobber technique). */ - char *elf_path = strdup(argv[arg_start]); + elf_path = strdup(argv[arg_start]); bool have_sysroot = (sysroot != NULL || create_sysroot != NULL); const char *sysroot_src = create_sysroot ? create_sysroot : sysroot; - char *sysroot_path = NULL; if (have_sysroot) { sysroot_path = (char *) calloc(LINUX_PATH_MAX, 1); if (sysroot_path) { @@ -493,34 +472,22 @@ int main(int argc, char **argv) if (src_len >= LINUX_PATH_MAX) { log_error("sysroot path too long (%zu bytes, max %d): %s", src_len, LINUX_PATH_MAX - 1, sysroot_src); - free(elf_path); - free(sysroot_path); - return 1; + goto cleanup; } } } sysroot = sysroot_path; - int guest_argc = argc - arg_start; - const char **guest_argv = - (const char **) calloc((size_t) guest_argc, sizeof(char *)); - guest_t g; - bool guest_initialized = false; - sysroot_mount_t sysroot_mount; - char host_cwd[LINUX_PATH_MAX]; - char elf_host_path[LINUX_PATH_MAX]; - bool elf_host_temp = false; - bool have_host_cwd = (getcwd(host_cwd, sizeof(host_cwd)) != NULL); - int exit_code; - memset(&sysroot_mount, 0, sizeof(sysroot_mount)); + guest_argc = argc - arg_start; + guest_argv = (const char **) calloc((size_t) guest_argc, sizeof(char *)); if (!elf_path || (have_sysroot && !sysroot_path) || !guest_argv) { log_error("out of memory"); - goto fail; + goto cleanup; } for (int i = 0; i < guest_argc; i++) { guest_argv[i] = strdup(argv[arg_start + i]); if (!guest_argv[i]) { log_error("out of memory"); - goto fail; + goto cleanup; } } @@ -528,20 +495,20 @@ int main(int argc, char **argv) if (sysroot_create_mount(sysroot_path, &sysroot_mount) < 0) { log_error("failed to provision case-sensitive sysroot at %s: %s", sysroot_path, strerror(errno)); - goto fail; + goto cleanup; } size_t mounted_len = str_copy_trunc( sysroot_path, sysroot_mount.mount_path, LINUX_PATH_MAX); if (mounted_len >= LINUX_PATH_MAX) { log_error("mounted sysroot path too long: %s", sysroot_mount.mount_path); - goto fail; + goto cleanup; } sysroot = sysroot_path; } if (have_sysroot && sysroot_validate_case_sensitivity(sysroot) < 0) - goto fail; + goto cleanup; proc_set_sysroot(sysroot); @@ -553,7 +520,7 @@ int main(int argc, char **argv) &elf_host_temp) < 0) { log_error("failed to resolve ELF path %s: %s", elf_path, strerror(errno)); - goto fail; + goto cleanup; } /* Check if the file starts with "#!" */ @@ -568,7 +535,7 @@ int main(int argc, char **argv) if (rc < 0) { log_error("empty or invalid shebang interpreter in %s", elf_path); - goto fail; + goto cleanup; } /* The current path is a script. Bound the resolution chain only once a @@ -580,7 +547,7 @@ int main(int argc, char **argv) "too many levels of shebang recursion (max %d) " "resolving %s", ELF_SHEBANG_MAX_DEPTH, argv[arg_start]); - goto fail; + goto cleanup; } shebang_depth++; @@ -592,14 +559,14 @@ int main(int argc, char **argv) (const char **) calloc((size_t) new_argc, sizeof(char *)); if (!new_argv) { log_error("out of memory"); - goto fail; + goto cleanup; } new_argv[0] = strdup(interp); if (!new_argv[0]) { log_error("out of memory"); free((void *) new_argv); - goto fail; + goto cleanup; } if (has_arg) { new_argv[1] = strdup(arg); @@ -607,7 +574,7 @@ int main(int argc, char **argv) log_error("out of memory"); free((void *) new_argv[0]); free((void *) new_argv); - goto fail; + goto cleanup; } } @@ -624,7 +591,7 @@ int main(int argc, char **argv) char *new_elf_path = strdup(interp); if (!new_elf_path) { log_error("out of memory"); - goto fail; + goto cleanup; } free(elf_path); elf_path = new_elf_path; @@ -640,140 +607,58 @@ int main(int argc, char **argv) elf_info_t probe_info; if (guest_bootstrap_probe_elf(elf_host_path, &probe_info) == 0 && probe_info.e_machine == EM_X86_64) { - log_error( - "--gdb is not supported for x86_64 guests; the current stub " - "only exposes the translated aarch64 view"); - goto fail; - } - } - - guest_bootstrap_t boot; - extern char **environ; - - if (guest_bootstrap_prepare(&g, elf_host_path, elf_host_temp, elf_path, - sysroot, guest_argc, guest_argv, environ, - shim_bin, shim_bin_len, verbose, - &guest_initialized, &boot) < 0) - goto fail; - if (elf_host_temp && !g.is_rosetta) { - unlink(elf_host_path); - elf_host_temp = false; - } - - if (have_sysroot) { - bool case_sensitive = true; - bool case_preserving = true; - if (sysroot_probe_case_sensitivity(sysroot, &case_sensitive, - &case_preserving) == 0) { - proc_set_sysroot_casefold(case_preserving && !case_sensitive); - } else { - proc_set_sysroot_casefold(false); + log_error(LAUNCH_GDB_X86_64_MSG); + goto cleanup; } - } else { - proc_set_sysroot_casefold(false); } - runtime_set_process_title(argc, argv, elf_path); - - hv_vcpu_t vcpu; - hv_vcpu_exit_t *vexit; - if (guest_bootstrap_create_vcpu(&g, &boot, 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. + /* Rewrite the host-visible process title from the guest entrypoint. This + * clobbers the original argv block (already snapshotted into the heap + * elf_path / guest_argv above), so it must run before elfuse_launch hands + * control to the guest but after the shebang loop has fixed elf_path. */ - if (gdb_port > 0) { - if (gdb_stub_init(gdb_port, &g) < 0) { - log_error("failed to initialize GDB stub"); - goto fail; - } - /* Mirror any preconfigured breakpoints/watchpoints into this vCPU. */ - gdb_stub_sync_debug_regs(vcpu); - - if (gdb_stop_on_entry) - gdb_stub_wait_for_attach(); - } - - /* vcpu_run_loop owns guest execution until exit, fatal signal, or timeout. - */ - exit_code = vcpu_run_loop(vcpu, vexit, &g, verbose, 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(); - - /* Wait for worker vCPU threads to stop before tearing down guest memory. - * The main thread leaves the run loop as soon as it observes the exit_group - * flag, but sibling vCPU threads may still be mid-iteration in their own - * run loops (e.g. touching shim_globals). cleanup_main_resources unmaps the - * guest slab via guest_destroy, so a still-running worker would fault on - * freed guest memory and crash the host with SIGSEGV, masking the real exit - * code. thread_join_workers() is a no-op once the workers have already - * wound down (the common single-threaded case). - * - * vcpu_run_loop can also return here without anyone having requested - * exit_group or kicked the siblings out of hv_vcpu_run: the alarm timeout - * (exit_code 124), a fatal default-disposition signal, or ELR_EL1==0 all - * bail out with a bare break. On those paths siblings are still spinning in - * the guest, so mirror guest_destroy's request-interrupt prefix before - * joining -- otherwise this call burns its full poll cap, detaches every - * worker, and guest_destroy's own request-interrupt-join (which honors - * join_abandoned) skips them, 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(); + runtime_set_process_title(argc, argv, elf_path); - /* 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. + /* Hand the bring-up, run loop, and guest teardown to elfuse_launch. main() + * retains ownership of the original argv (proctitle above), the sysroot + * mount (detached at the cleanup label after the guest exits so the + * mount stays live for the whole run), host cwd, and the heap elf_path / + * sysroot_path / guest_argv copies. */ - 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. + launch_args_t largs = { + .elf_path = elf_host_path, + .elf_host_temp = elf_host_temp, + .sysroot = sysroot, + .guest_argc = guest_argc, + .guest_argv = guest_argv, + .gdb_port = gdb_port, + .gdb_stop_on_entry = gdb_stop_on_entry, + .timeout_sec = timeout_sec, + .verbose = verbose, + }; + /* Ownership of the temp unlink transfers here (contract in launch.h); + * drop main()'s claim so the shared cleanup below cannot double-unlink. */ - syscall_hist_dump(); - goto cleanup; + elf_host_temp = false; + exit_code = elfuse_launch(&largs); -fail: - exit_code = 1; cleanup: - /* Single unwind for every exit past the heap-copy allocations: frees the - * caller-owned heap copies (guest_destroy included, via - * cleanup_main_resources, once the guest came up), detaches the sysroot - * mount, restores the host cwd, and drops a still-owned FUSE-materialized - * temp ELF, which the post-prepare error paths and a Rosetta guest's - * teardown previously leaked. + /* Single unwind for every exit past the state block above: frees the + * caller-owned heap copies, detaches the sysroot mount, restores the host + * cwd, and drops a still-owned FUSE-materialized temp ELF. This must run + * even after a bring-up whose HVF teardown guest_destroy deferred to + * process exit, because process exit reclaims neither the sysroot mount + * nor the temp ELF. The guest itself belongs to elfuse_launch, which + * destroys it on every exit path, so nothing here touches HVF or guest + * memory. */ - /* Give back any pty slaves this process still holds before the fd table - * goes away. 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 that this exit - * should have produced. - */ - proc_pty_release_process_slaves(); - - cleanup_main_resources(&g, guest_initialized, &sysroot_mount, - have_host_cwd ? host_cwd : NULL, guest_argv, - guest_argc, elf_path, sysroot_path); + rosettad_clear_binary_path(); + if (have_host_cwd && host_cwd[0] != '\0' && chdir(host_cwd) < 0) + (void) chdir("/"); + sysroot_cleanup_mount(&sysroot_mount); + free_guest_argv(guest_argv, guest_argc); + free(elf_path); + free(sysroot_path); if (elf_host_temp) unlink(elf_host_path); diff --git a/tests/lib/rosetta-test.sh b/tests/lib/report.sh similarity index 89% rename from tests/lib/rosetta-test.sh rename to tests/lib/report.sh index b49a7505..5314d662 100644 --- a/tests/lib/rosetta-test.sh +++ b/tests/lib/report.sh @@ -1,4 +1,5 @@ -# Shared reporting helpers for the tests/test-rosetta-*.sh scripts. +# Shared reporting helpers for standalone test scripts (the +# tests/test-rosetta-*.sh suite). # # Copyright 2026 elfuse contributors # SPDX-License-Identifier: Apache-2.0 @@ -8,7 +9,7 @@ # Sources tests/lib/test-runner.sh and exposes report_pass / report_fail # / report_skip on top of test_report so per-binary output matches the # matrix runner's aarch64 format ([ OK ] / [ FAIL ] / [ SKIP ] aligned -# to TEST_LABEL_WIDTH). Each Rosetta script still owns its pass/fail +# to TEST_LABEL_WIDTH). Each script still owns its pass/fail # /skip/total counters; this lib only centralizes the report sites and # the trailing Results: summary line that tests/test-matrix.sh scrapes. @@ -16,9 +17,9 @@ # matrix output looks uniform across aarch64 and x86_64 modes. : "${TEST_LABEL_WIDTH:=45}" -_rosetta_test_lib_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +_report_lib_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=tests/lib/test-runner.sh -. "${_rosetta_test_lib_dir}/test-runner.sh" +. "${_report_lib_dir}/test-runner.sh" # report_pass / report_fail / report_skip accept a single label argument # matching the original Rosetta helpers' single-string contract; the diff --git a/tests/test-rosetta-alpine.sh b/tests/test-rosetta-alpine.sh index 42e3b92f..5b3e7175 100755 --- a/tests/test-rosetta-alpine.sh +++ b/tests/test-rosetta-alpine.sh @@ -42,8 +42,8 @@ DATA="${SHORTDIR}/data" # Shared report_pass / report_fail / report_skip + Results: summary emitter. # Matches the matrix runner's aarch64 per-binary format so tests/test-matrix.sh # elfuse-x86_64 output reads uniformly. -# shellcheck source=tests/lib/rosetta-test.sh -. "$(dirname "$0")/lib/rosetta-test.sh" +# shellcheck source=tests/lib/report.sh +. "$(dirname "$0")/lib/report.sh" pass=0 fail=0 diff --git a/tests/test-rosetta-audit.sh b/tests/test-rosetta-audit.sh index c69e2a56..40ce9c3f 100644 --- a/tests/test-rosetta-audit.sh +++ b/tests/test-rosetta-audit.sh @@ -17,8 +17,8 @@ ROSETTA_PATH="${MATRIX_ROSETTA_TRANSLATOR:-/Library/Apple/usr/libexec/oah/Rosett AUDIT_BIN="$(pwd)/tests/fixtures/rosetta/x86_64-rosetta-audit" TLS0_BIN="$(pwd)/tests/fixtures/rosetta/x86_64-rosetta-tls0" -# shellcheck source=tests/lib/rosetta-test.sh -. "$(dirname "$0")/lib/rosetta-test.sh" +# shellcheck source=tests/lib/report.sh +. "$(dirname "$0")/lib/report.sh" pass=0 fail=0 diff --git a/tests/test-rosetta-cli.sh b/tests/test-rosetta-cli.sh index 563cff1d..db31bdf4 100755 --- a/tests/test-rosetta-cli.sh +++ b/tests/test-rosetta-cli.sh @@ -12,8 +12,8 @@ ELFUSE="${1:-build/elfuse}" # colors. The matrix runner reads only the Results: line emitted by # report_summary at the bottom; per-binary lines now match the aarch64 # modes' [ OK ] / [ FAIL ] format. -# shellcheck source=tests/lib/rosetta-test.sh -. "$(dirname "$0")/lib/rosetta-test.sh" +# shellcheck source=tests/lib/report.sh +. "$(dirname "$0")/lib/report.sh" pass=0 fail=0 diff --git a/tests/test-rosetta-execfd.sh b/tests/test-rosetta-execfd.sh index d2aa7050..843b9e65 100755 --- a/tests/test-rosetta-execfd.sh +++ b/tests/test-rosetta-execfd.sh @@ -35,8 +35,8 @@ FIXTURES="${FIXTURES_DIR:-externals/test-fixtures}" BUSYBOX="$(pwd)/${FIXTURES}/x86_64-musl/staticbin/bin/busybox" ROSETTA_PATH="${MATRIX_ROSETTA_TRANSLATOR:-/Library/Apple/usr/libexec/oah/RosettaLinux/rosetta}" -# shellcheck source=tests/lib/rosetta-test.sh -. "$(dirname "$0")/lib/rosetta-test.sh" +# shellcheck source=tests/lib/report.sh +. "$(dirname "$0")/lib/report.sh" pass=0 fail=0 diff --git a/tests/test-rosetta-failure-modes.sh b/tests/test-rosetta-failure-modes.sh index 362e4efd..6c6573f2 100755 --- a/tests/test-rosetta-failure-modes.sh +++ b/tests/test-rosetta-failure-modes.sh @@ -39,8 +39,8 @@ SHORTDIR=/tmp/elfuse-rfm # Shared report_pass / report_fail / report_skip + Results: summary # emitter. Matches the matrix runner's aarch64 per-binary format so # tests/test-matrix.sh elfuse-x86_64 output reads uniformly. -# shellcheck source=tests/lib/rosetta-test.sh -. "$(dirname "$0")/lib/rosetta-test.sh" +# shellcheck source=tests/lib/report.sh +. "$(dirname "$0")/lib/report.sh" pass=0 fail=0 diff --git a/tests/test-rosetta-glibc.sh b/tests/test-rosetta-glibc.sh index d8e97f87..1dcd136c 100644 --- a/tests/test-rosetta-glibc.sh +++ b/tests/test-rosetta-glibc.sh @@ -23,8 +23,8 @@ TLS_BIN="${ROOTFS}/usr/bin/tls-probe" GDTLS_BIN="${ROOTFS}/usr/bin/gdtls-probe" PTHREAD_TLS_BIN="${ROOTFS}/usr/bin/pthread-tls-probe" -# shellcheck source=tests/lib/rosetta-test.sh -. "$(dirname "$0")/lib/rosetta-test.sh" +# shellcheck source=tests/lib/report.sh +. "$(dirname "$0")/lib/report.sh" pass=0 fail=0 diff --git a/tests/test-rosetta-jit.sh b/tests/test-rosetta-jit.sh index c388edeb..de7a358d 100644 --- a/tests/test-rosetta-jit.sh +++ b/tests/test-rosetta-jit.sh @@ -17,8 +17,8 @@ ROOTFS="${FIXTURES}/x86_64-musl/rootfs" ROSETTA_PATH="${MATRIX_ROSETTA_TRANSLATOR:-/Library/Apple/usr/libexec/oah/RosettaLinux/rosetta}" LUAJIT="${ROOTFS}/usr/bin/luajit" -# shellcheck source=tests/lib/rosetta-test.sh -. "$(dirname "$0")/lib/rosetta-test.sh" +# shellcheck source=tests/lib/report.sh +. "$(dirname "$0")/lib/report.sh" pass=0 fail=0 diff --git a/tests/test-rosetta-madvise.sh b/tests/test-rosetta-madvise.sh index c0ee3f48..99f94bdb 100644 --- a/tests/test-rosetta-madvise.sh +++ b/tests/test-rosetta-madvise.sh @@ -27,8 +27,8 @@ esac ROSETTA_PATH="${MATRIX_ROSETTA_TRANSLATOR:-/Library/Apple/usr/libexec/oah/RosettaLinux/rosetta}" MADV_BIN="$(pwd)/tests/fixtures/rosetta/x86_64-rosetta-madvise" -# shellcheck source=tests/lib/rosetta-test.sh -. "$(dirname "$0")/lib/rosetta-test.sh" +# shellcheck source=tests/lib/report.sh +. "$(dirname "$0")/lib/report.sh" pass=0 fail=0 diff --git a/tests/test-rosetta-mremap.sh b/tests/test-rosetta-mremap.sh index 70c6f9b8..2c1d7613 100644 --- a/tests/test-rosetta-mremap.sh +++ b/tests/test-rosetta-mremap.sh @@ -26,8 +26,8 @@ esac ROSETTA_PATH="${MATRIX_ROSETTA_TRANSLATOR:-/Library/Apple/usr/libexec/oah/RosettaLinux/rosetta}" MREMAP_BIN="$(pwd)/tests/fixtures/rosetta/x86_64-rosetta-mremap" -# shellcheck source=tests/lib/rosetta-test.sh -. "$(dirname "$0")/lib/rosetta-test.sh" +# shellcheck source=tests/lib/report.sh +. "$(dirname "$0")/lib/report.sh" pass=0 fail=0 diff --git a/tests/test-rosetta-msync.sh b/tests/test-rosetta-msync.sh index 65164e62..1bd8c876 100644 --- a/tests/test-rosetta-msync.sh +++ b/tests/test-rosetta-msync.sh @@ -27,8 +27,8 @@ esac ROSETTA_PATH="${MATRIX_ROSETTA_TRANSLATOR:-/Library/Apple/usr/libexec/oah/RosettaLinux/rosetta}" MSYNC_BIN="$(pwd)/tests/fixtures/rosetta/x86_64-rosetta-msync" -# shellcheck source=tests/lib/rosetta-test.sh -. "$(dirname "$0")/lib/rosetta-test.sh" +# shellcheck source=tests/lib/report.sh +. "$(dirname "$0")/lib/report.sh" pass=0 fail=0 diff --git a/tests/test-rosetta-statics.sh b/tests/test-rosetta-statics.sh index 87e730e4..8733c7b9 100755 --- a/tests/test-rosetta-statics.sh +++ b/tests/test-rosetta-statics.sh @@ -43,8 +43,8 @@ STATICBIN="" # Shared report_pass / report_fail / report_skip + Results: summary # emitter. Matches the matrix runner's aarch64 per-binary format so # tests/test-matrix.sh elfuse-x86_64 output reads uniformly. -# shellcheck source=tests/lib/rosetta-test.sh -. "$(dirname "$0")/lib/rosetta-test.sh" +# shellcheck source=tests/lib/report.sh +. "$(dirname "$0")/lib/report.sh" pass=0 fail=0 @@ -54,7 +54,12 @@ total=0 # Run a binary, check exit code and (optionally) stdout regex. # Args: