From 4a76d7426c07d0e9480ffff7b8ced8cb9ee47a12 Mon Sep 17 00:00:00 2001 From: Chun-Hung Tseng Date: Fri, 7 Aug 2026 15:15:31 +0200 Subject: [PATCH 1/5] Rename tests/lib/rosetta-test.sh to report.sh The report_pass / report_fail / report_skip helpers and the Results: summary emitter are not Rosetta-specific: any standalone test script that wants its per-binary output to match the matrix runner's format needs exactly this file. Name the library for what it does rather than for its first consumer, and drop the Rosetta wording from its header and its internal variable so a new non-Rosetta user does not read as borrowing another suite's private helper. Mechanical beyond that: the eleven test-rosetta-*.sh scripts update their source line and shellcheck directive in place. --- tests/lib/{rosetta-test.sh => report.sh} | 9 +++++---- tests/test-rosetta-alpine.sh | 4 ++-- tests/test-rosetta-audit.sh | 4 ++-- tests/test-rosetta-cli.sh | 4 ++-- tests/test-rosetta-execfd.sh | 4 ++-- tests/test-rosetta-failure-modes.sh | 4 ++-- tests/test-rosetta-glibc.sh | 4 ++-- tests/test-rosetta-jit.sh | 4 ++-- tests/test-rosetta-madvise.sh | 4 ++-- tests/test-rosetta-mremap.sh | 4 ++-- tests/test-rosetta-msync.sh | 4 ++-- tests/test-rosetta-statics.sh | 4 ++-- 12 files changed, 27 insertions(+), 26 deletions(-) rename tests/lib/{rosetta-test.sh => report.sh} (89%) 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..103d35ec 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 From d38efb2a91900f180a92a093a6e28d560262107e Mon Sep 17 00:00:00 2001 From: Chun-Hung Tseng Date: Fri, 7 Aug 2026 15:17:06 +0200 Subject: [PATCH 2/5] Derive every usage synopsis from one flag list main() carried three hand-maintained copies of the usage line: the --help block, the unknown-option error, and the missing-elf-path error. They had already drifted; the missing-elf-path copy predates the GDB stub and never learned [--gdb PORT] [--gdb-stop-on-entry], so the most common error path advertised an incomplete flag set. Define the flag list once in ELFUSE_USAGE_BODY and expand it at two separators. ELFUSE_USAGE joins the groups with spaces for the two log_error sites, which need the flat form because log_impl stamps its timestamp and source location on the first line only and a wrapped string would print ragged under that prefix. ELFUSE_USAGE_WRAPPED breaks and indents the same groups for --help, which has no prefix. A flag added to the list now reaches all three sites. --help keeps a wrapped, aligned synopsis, re-flowed from five lines to three and still within 80 columns; the missing-elf-path error gains the GDB flags it had been missing. tests/test-usage-synopsis.sh compares the renderings against each other rather than sampling the flag list, since sampling is what let the copies drift unnoticed: it unwraps --help's block and requires it to equal the flat line both error paths print, and requires those two to match. Run against the parent commit it fails the width and single-line assertions, and against main it fails the flat-form comparison, reporting the missing GDB flags. --- mk/tests.mk | 6 ++ src/main.c | 40 +++++++----- tests/test-usage-synopsis.sh | 117 +++++++++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+), 15 deletions(-) create mode 100755 tests/test-usage-synopsis.sh 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/main.c b/src/main.c index 36d79a2c..09ae278b 100644 --- a/src/main.c +++ b/src/main.c @@ -12,7 +12,9 @@ * - 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 @@ -239,6 +241,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(); @@ -291,11 +311,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" @@ -393,11 +410,7 @@ 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...]"); + log_error(ELFUSE_USAGE); return 1; } } @@ -470,10 +483,7 @@ 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...]"); + log_error(ELFUSE_USAGE); return 1; } diff --git a/tests/test-usage-synopsis.sh b/tests/test-usage-synopsis.sh new file mode 100755 index 00000000..61952028 --- /dev/null +++ b/tests/test-usage-synopsis.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +# test-usage-synopsis.sh -- Pin the usage synopsis renderings against drift +# +# Copyright 2026 elfuse contributors +# SPDX-License-Identifier: Apache-2.0 +# +# Usage: tests/test-usage-synopsis.sh +# +# main() prints its usage synopsis at three sites: the --help block, the +# unknown-option error, and the missing-elf-path error. They were once three +# hand-maintained string literals and had drifted; the missing-elf-path copy +# predated the GDB stub and never learned [--gdb PORT] [--gdb-stop-on-entry], +# so the most common error path advertised an incomplete flag set. All three +# now expand ELFUSE_USAGE_BODY, which --help renders wrapped and the error +# paths render flat. +# +# Rather than sample the flag list, which is what let the old copies drift +# unnoticed, this compares the renderings against each other: a flag reaching +# one site and not another fails regardless of which flag it is. Asserted: +# +# 1. --help's wrapped synopsis, with its line breaks and indent collapsed +# back to single spaces, is byte-identical to the flat form both error +# paths print. This is the oracle the drift bug would have failed. +# 2. The two error paths print the same flat line as each other. +# 3. Every printed --help synopsis line fits 80 columns, and continuation +# lines align under the first line's flags. log_error stamps a prefix on +# the first line only, so the error paths must stay one line and are +# checked for exactly that. +# +# A regression re-splits the sites into independent literals, or rewraps the +# synopsis past 80 columns, and this fails without needing to know the flags. + +set -euo pipefail + +ELFUSE="${1:?Usage: $0 }" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# Labels here are sentences rather than tool names, so widen the column past +# the library default before report.sh reads it. +TEST_LABEL_WIDTH="${TEST_LABEL_WIDTH:-50}" +# shellcheck source=tests/lib/report.sh +. "$SCRIPT_DIR/lib/report.sh" + +# report_pass / report_skip in tests/lib/report.sh increment these; only +# fail is read directly, by the exit status at the bottom. +# shellcheck disable=SC2034 +pass=0 +fail=0 +# shellcheck disable=SC2034 +skip=0 + +# Compare one rendering against another and report through the shared +# emitter, so this suite lines up with the rest of make check's output. +check() +{ + local label="$1" want="$2" got="$3" + if [ "$want" = "$got" ]; then + report_pass "$label" + else + report_fail "$label" + printf ' want: %s\n' "$want" >&2 + printf ' got: %s\n' "$got" >&2 + fi +} + +# The synopsis block runs from the "usage:" line to the first blank line. +help_block="$("$ELFUSE" --help | awk '/^usage: /{f=1} f{if (!NF) exit; print}')" + +# Collapsing runs of whitespace undoes the wrap without assuming how many +# lines it takes or where the breaks fall, so re-grouping the flags is not a +# failure while dropping or renaming one is. +help_flat="$(printf '%s\n' "$help_block" | tr '\n' ' ' | tr -s ' ')" +help_flat="${help_flat% }" + +# Both error paths write through log_error, whose "HH:MM:SS ERROR file:line: " +# prefix is stripped by cutting at the "usage:" the synopsis starts with. +usage_line_of() +{ + sed -n 's/.*\(usage: elfuse .*\)$/\1/p' +} + +# Both argument errors exit nonzero, which under `set -e -o pipefail` would +# abort the script before a single check ran; capture the status instead so a +# changed exit code is asserted rather than fatal. +no_args_err="$("$ELFUSE" 2>&1 || true)" +no_args_err="$(printf '%s\n' "$no_args_err" | usage_line_of)" +bad_flag_err="$("$ELFUSE" --no-such-flag 2>&1 || true)" +bad_flag_err="$(printf '%s\n' "$bad_flag_err" | usage_line_of)" + +check "--help wrapped form unwraps to the flat form" "$help_flat" "$no_args_err" +check "both error paths print one flat synopsis" "$no_args_err" "$bad_flag_err" + +# One line each: a wrapped string would print ragged under the log prefix. +check "missing-elf-path error is a single line" "1" \ + "$(printf '%s\n' "$no_args_err" | grep -c .)" +check "unknown-option error is a single line" "1" \ + "$(printf '%s\n' "$bad_flag_err" | grep -c .)" + +# 80 columns is the width every --help reader is assumed to have; a longer +# line wraps at an arbitrary column and defeats the alignment below. +over80="$(printf '%s\n' "$help_block" | awk 'length($0) > 80' | wc -l | tr -d ' ')" +check "every --help synopsis line fits 80 columns" "0" "$over80" + +# Continuation lines are indented to the column after "usage: elfuse ", so +# the flags form one block under the first line's. +indent="$(printf '%s' 'usage: elfuse ' | wc -c | tr -d ' ')" +misaligned="$(printf '%s\n' "$help_block" | awk -v n="$indent" \ + 'NR > 1 && substr($0, 1, n) != sprintf("%*s", n, "")' | wc -l | tr -d ' ')" +check "continuation lines align under the flags" "0" "$misaligned" + +# More than one line proves the wrap is real; a single-line --help synopsis +# is the regression that made the flags unscannable on an 80-column terminal. +lines="$(printf '%s\n' "$help_block" | grep -c .)" +check "--help wraps the synopsis over several lines" "yes" \ + "$([ "$lines" -gt 1 ] && echo yes || echo "no ($lines line)")" + +report_summary +[ "$fail" -eq 0 ] From 4e5454a36b9fd67a927bcf583046ee9ea01fc802 Mon Sep 17 00:00:00 2001 From: Chun-Hung Tseng Date: Fri, 7 Aug 2026 15:21:06 +0200 Subject: [PATCH 3/5] Extract elfuse_launch into src/core/launch.c elfuse_launch owns guest bring-up: guest_bootstrap_prepare, the FUSE-temp unlink, the sysroot casefold probe, vCPU creation, GDB init/sync/wait, the run loop, gdb_stub_shutdown, the shim counter and syscall histogram dumps, and guest_destroy. main() retains the original CLI argv (proctitle rewriting), option parsing, sysroot provisioning, the shebang loop, host cwd, and the heap resource cleanup, and now hands off through launch_args_t so another launcher (the planned OCI run helper) can share one bring-up path. Bring-up failures unwind through a single fail label instead of repeating the guest_destroy-plus-unlink tail at every error site. The --gdb x86_64 refusal is enforced inside elfuse_launch, once bring-up has set g.is_rosetta; main() keeps its early static-ELF probe as a fast pre-bring-up reject, and the two sites share LAUNCH_GDB_X86_64_MSG (tests/test-rosetta-cli.sh pins the text) so their diagnostics cannot diverge. Enforcing the rule at the launch layer makes every caller inherit it, not only main()'s parser. Ownership of the FUSE-materialized temp ELF moves with the bring-up: elfuse_launch owns the unlink from the prepare call onward, and main() drops its claim before handing off, so main's shared goto unwind cannot double-unlink a path whose ownership has transferred. runtime_set_process_title moves ahead of the handoff: it touches only the original argv, which main() owns, so the title rewrite is a caller concern, not a bring-up step. The embedded shim blob include moves along with its only consumer. --- Makefile | 1 + src/core/launch.c | 212 ++++++++++++++++++++++++++++++++++++++++++++++ src/core/launch.h | 80 +++++++++++++++++ src/main.c | 192 +++++++++-------------------------------- 4 files changed, 332 insertions(+), 153 deletions(-) create mode 100644 src/core/launch.c create mode 100644 src/core/launch.h 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/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 09ae278b..a829295d 100644 --- a/src/main.c +++ b/src/main.c @@ -17,8 +17,6 @@ * the argument-error paths render it differently but cannot drift apart. */ -#include -#include #include #include #include @@ -33,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" @@ -111,24 +104,20 @@ 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, +/* Releases the host state main() owns: the sysroot mount, the host cwd, and the + * heap copies of argv. The guest itself belongs to elfuse_launch, which + * destroys it on every exit path, so nothing here touches HVF or guest memory. + * This must still run after a bring-up whose HVF teardown guest_destroy + * deferred to process exit, because process exit reclaims neither the sysroot + * mount nor the FUSE-materialized temp ELF. + */ +static void cleanup_main_resources(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("/"); @@ -138,18 +127,6 @@ static void cleanup_main_resources(guest_t *g, 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 @@ -513,8 +490,6 @@ int main(int argc, char **argv) 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]; @@ -650,140 +625,51 @@ 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); - } - } 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. - */ - if (gdb_port > 0) { - if (gdb_stub_init(gdb_port, &g) < 0) { - log_error("failed to initialize GDB stub"); + log_error(LAUNCH_GDB_X86_64_MSG); 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. + /* 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. */ - 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 in cleanup_main_resources 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(); + elf_host_temp = false; + exit_code = elfuse_launch(&largs); goto cleanup; 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. + * caller-owned heap copies, detaches the sysroot mount, restores the host + * cwd, and drops a still-owned FUSE-materialized temp ELF. */ - /* 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); + cleanup_main_resources(&sysroot_mount, have_host_cwd ? host_cwd : NULL, + guest_argv, guest_argc, elf_path, sysroot_path); if (elf_host_temp) unlink(elf_host_path); From e99c3bd2cedcd9560121282e51b7e7599f450b80 Mon Sep 17 00:00:00 2001 From: Chun-Hung Tseng Date: Tue, 11 Aug 2026 17:44:02 +0200 Subject: [PATCH 4/5] Route main() error paths through one cleanup label main() unwound through three shapes: a fail/cleanup label pair that called cleanup_main_resources (a single-caller helper taking six parameters, all locals of main), inline frees in the sysroot-too-long branch, and bare `return 1` on every earlier error. Each new resource had to pick its unwind, and the inline branch had already drifted once. Declare and zero-initialize everything the unwind reads above the option loop, fold the helper's body into a single cleanup label, and route every error exit through it. Zero state makes each cleanup step a no-op, so an early goto is as safe as a late one. The label doubles as the success epilogue: elfuse_launch's return falls through with its exit code, and exit_code starts at 1 so no error site assigns it. --version, --help, and fork-child mode keep their direct returns: they are not error paths and own nothing the label frees. The coming launch-flag work adds three more heap resources to main(); with one label they join one list instead of choosing among three unwinds. --- src/main.c | 127 ++++++++++++++++++++++++----------------------------- 1 file changed, 58 insertions(+), 69 deletions(-) diff --git a/src/main.c b/src/main.c index a829295d..3f540d63 100644 --- a/src/main.c +++ b/src/main.c @@ -104,29 +104,6 @@ static void free_guest_argv(const char **guest_argv, int guest_argc) free((void *) guest_argv); } -/* Releases the host state main() owns: the sysroot mount, the host cwd, and the - * heap copies of argv. The guest itself belongs to elfuse_launch, which - * destroys it on every exit path, so nothing here touches HVF or guest memory. - * This must still run after a bring-up whose HVF teardown guest_destroy - * deferred to process exit, because process exit reclaims neither the sysroot - * mount nor the FUSE-materialized temp ELF. - */ -static void cleanup_main_resources(sysroot_mount_t *sysroot_mount, - const char *host_cwd, - const char **guest_argv, - int guest_argc, - char *elf_path, - char *sysroot_path) -{ - 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); -} - /* 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 @@ -260,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 @@ -325,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] == '-') { @@ -346,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") && @@ -354,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") && @@ -374,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); @@ -388,14 +381,14 @@ int main(int argc, char **argv) } else { log_error("unknown option: %s", argv[arg_start]); log_error(ELFUSE_USAGE); - return 1; + 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 @@ -436,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 @@ -445,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) @@ -461,17 +454,16 @@ int main(int argc, char **argv) if (arg_start >= argc) { log_error(ELFUSE_USAGE); - return 1; + 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) { @@ -480,32 +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 *)); - 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; } } @@ -513,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); @@ -538,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 "#!" */ @@ -553,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 @@ -565,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++; @@ -577,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); @@ -592,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; } } @@ -609,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; @@ -626,7 +608,7 @@ int main(int argc, char **argv) if (guest_bootstrap_probe_elf(elf_host_path, &probe_info) == 0 && probe_info.e_machine == EM_X86_64) { log_error(LAUNCH_GDB_X86_64_MSG); - goto fail; + goto cleanup; } } @@ -639,7 +621,7 @@ int main(int argc, char **argv) /* 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 in cleanup_main_resources after the guest exits so the + * 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. */ @@ -659,17 +641,24 @@ int main(int argc, char **argv) */ elf_host_temp = false; exit_code = elfuse_launch(&largs); - goto cleanup; -fail: - exit_code = 1; cleanup: - /* Single unwind for every exit past the heap-copy allocations: frees the + /* 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. + * 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. */ - cleanup_main_resources(&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); From 35d2cfdb2c67a146664a307bd887d21775562949 Mon Sep 17 00:00:00 2001 From: Chun-Hung Tseng Date: Wed, 12 Aug 2026 10:41:53 +0200 Subject: [PATCH 5/5] Rename test-rosetta-statics.sh's run_check helper tests/lib/report.sh sources tests/lib/test-runner.sh, which defines a run_check taking and resolving the tool name through test_tool_path. test-rosetta-statics.sh defined its own run_check taking