From d674f864c2d3016d58c861fe0b7080c553cfe7b1 Mon Sep 17 00:00:00 2001 From: Hanlu Li Date: Sun, 9 Aug 2026 07:14:04 +0800 Subject: [PATCH 1/6] runtime: replace status script with native manager Preserve the stable two-line status contract and rooted symlink checks while replacing the installed shell helper with a native executable. The manager links only GLib, so building it does not pull in qemuutil. Install and package the compiled manager rather than the removed script. Validate installation through Ninja so the build directory is never read by a different Meson version. Drive the generic dual-target lat-pr-fast matrix with one system Meson per environment, and keep target-dependent install coverage in the top-level Meson build. Signed-off-by: Hanlu Li --- .github/.ci/aosc/Dockerfile | 1 + .github/.ci/debian/Dockerfile | 1 + .github/workflows/build-docker-images.yml | 2 +- .github/workflows/tests.yml | 62 ++- latxbuild/build-release.sh | 2 +- meson.build | 17 + runtime/latu-runtime-manager | 114 ----- runtime/latu-runtime-manager.c | 494 +++++++++++++++++++++ runtime/meson.build | 13 +- tests/README.md | 9 +- tests/meson.build | 31 +- tests/runtime/test-latu-install.sh | 22 +- tests/runtime/test-latu-runtime-manager.sh | 8 +- 13 files changed, 581 insertions(+), 195 deletions(-) delete mode 100755 runtime/latu-runtime-manager create mode 100644 runtime/latu-runtime-manager.c diff --git a/.github/.ci/aosc/Dockerfile b/.github/.ci/aosc/Dockerfile index fb3aa977cdd..4fac5cbcce4 100644 --- a/.github/.ci/aosc/Dockerfile +++ b/.github/.ci/aosc/Dockerfile @@ -12,6 +12,7 @@ ARG DEPENDENCIES=" \ libglib2.0-dev \ libssl-dev \ make \ + meson \ ninja \ pkg-config \ python3-setuptools \ diff --git a/.github/.ci/debian/Dockerfile b/.github/.ci/debian/Dockerfile index d6cfe591030..580ea809d4c 100644 --- a/.github/.ci/debian/Dockerfile +++ b/.github/.ci/debian/Dockerfile @@ -14,6 +14,7 @@ ARG DEPENDENCIES=" \ libssl-dev \ lsb-release \ make \ + meson \ ninja-build \ pkg-config \ python3-setuptools \ diff --git a/.github/workflows/build-docker-images.yml b/.github/workflows/build-docker-images.yml index f3a56014478..8198ae69b44 100644 --- a/.github/workflows/build-docker-images.yml +++ b/.github/workflows/build-docker-images.yml @@ -19,7 +19,7 @@ on: jobs: build: runs-on: ${{ matrix.IMAGE.RUNNER }} - timeout-minutes: 10 + timeout-minutes: 20 permissions: contents: read packages: write diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4e1c745f961..5541e5c3a5b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -38,46 +38,32 @@ permissions: # Test registration and suite selection rules are documented in tests/README.md. jobs: - latu-runtime-manager: - name: "latu runtime manager (${{ matrix.container.name }})" + meson-fast: + name: "test (${{ matrix.container.name }}, lat-pr-fast)" runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 40 strategy: fail-fast: false matrix: container: - - { name: "latx-runner-aosc", tag: "loong64" } - - { name: "latx-runner-debian", tag: "loong64" } - - { name: "latx-runner-fedora", tag: "loongarch64" } - - steps: - - uses: actions/checkout@v7 - with: - persist-credentials: false - - - name: Setup QEMU - uses: docker/setup-qemu-action@v4 - - - name: Run dual-translator status checks - run: | - docker run --rm \ - --platform linux/loong64 \ - --volume "$(pwd):/io" \ - --workdir /io \ - "ghcr.io/${{ github.repository_owner }}/${{ matrix.container.name }}:${{ matrix.container.tag }}" \ - sh tests/runtime/test-latu-runtime-manager.sh - - meson-fast: - name: "test (latx-runner-debian, lat-pr-fast)" - runs-on: ubuntu-latest - timeout-minutes: 25 + - name: "latx-runner-aosc" + tag: "loong64" + sanitizers: false + prepare_meson: "command -v meson >/dev/null || oma install -y meson" + - name: "latx-runner-debian" + tag: "loong64" + sanitizers: true + prepare_meson: "command -v meson >/dev/null || { apt-get update && apt-get install -y meson; }" + - name: "latx-runner-fedora" + tag: "loongarch64" + sanitizers: false + prepare_meson: "command -v meson >/dev/null" steps: - uses: actions/checkout@v7 with: persist-credentials: false - submodules: true - name: Setup QEMU uses: docker/setup-qemu-action@v4 @@ -88,27 +74,33 @@ jobs: --platform linux/loong64 \ --volume "$(pwd):/io" \ --workdir /io \ - ghcr.io/lat-opensource/latx-runner-debian:loong64 \ + "ghcr.io/${{ github.repository_owner }}/${{ matrix.container.name }}:${{ matrix.container.tag }}" \ sh -eux -c ' + ${{ matrix.container.prepare_meson }} + meson --version export CFLAGS="-Wno-error=unused-but-set-variable -Wno-error=unused-function -Wformat -Werror=format-y2k" mkdir -p build64-tests cd build64-tests + sanitizer_flags= + if [ "${{ matrix.container.sanitizers }}" = true ]; then + sanitizer_flags="--enable-debug --enable-sanitizers" + fi ../configure \ - --target-list=x86_64-linux-user \ + --target-list=x86_64-linux-user,i386-linux-user \ --enable-latx \ - --enable-debug \ --enable-tests \ - --enable-sanitizers \ --disable-werror \ --optimize-O1 \ --extra-ldflags=-ldl \ --disable-docs \ - --with-git-submodules=ignore + --meson=meson \ + --with-git-submodules=ignore \ + $sanitizer_flags cd .. export LATX_RUNTIME_TEST_SKIP_GUEST_EXECUTION=1 ASAN_OPTIONS=detect_leaks=0 \ UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 \ - python3 -B meson/meson.py test \ + meson test \ -C build64-tests \ --suite lat-pr-fast \ --print-errorlogs diff --git a/latxbuild/build-release.sh b/latxbuild/build-release.sh index 66212637be5..b819b6162d1 100755 --- a/latxbuild/build-release.sh +++ b/latxbuild/build-release.sh @@ -87,7 +87,7 @@ package() { mkdir -p $pkgdir/$pkgname-$pkgver/usr/{bin,lib/binfmt.d,lib/sysctl.d} install -Dm755 -s $srcdir/build32/latx-i386 $pkgdir/$pkgname-$pkgver/usr/bin/latx-i386 install -Dm755 -s $srcdir/build64/latx-x86_64 $pkgdir/$pkgname-$pkgver/usr/bin/latx-x86_64 - install -Dm755 $srcdir/runtime/latu-runtime-manager \ + install -Dm755 -s $srcdir/build64/runtime/latu-runtime-manager \ $pkgdir/$pkgname-$pkgver/usr/bin/latu-runtime-manager cat >$pkgdir/$pkgname-$pkgver/usr/lib/binfmt.d/latx-i386.conf <&2 - exit 2 -} - -probe_translator() -{ - name=$1 - if [ "$translator_dir_status" = unknown ]; then - echo unknown - return - fi - path=$translator_dir/$name - - if [ -L "$path" ]; then - if ! resolved_path=$(readlink -f -- "$path" 2>/dev/null); then - echo unknown - return - fi - case "$translator_root" in - /|'') ;; - *) - case "$resolved_path" in - "$translator_root"|"$translator_root"/*) ;; - *) - echo unknown - return - ;; - esac - ;; - esac - path=$resolved_path - fi - - if [ -f "$path" ] && [ -x "$path" ]; then - echo present - else - echo missing - fi -} - -case "$#" in - 1) - [ "$1" = status ] || usage - manager_path=$0 - case "$manager_path" in - */*) ;; - *) - if ! manager_path=$(command -v "$manager_path" 2>/dev/null); then - echo 'latu-runtime-manager: cannot locate its executable' >&2 - exit 2 - fi - ;; - esac - if [ -L "$manager_path" ]; then - if ! manager_path=$(readlink -f -- "$manager_path" 2>/dev/null); then - echo 'latu-runtime-manager: cannot resolve its executable' >&2 - exit 2 - fi - fi - if ! translator_dir=$(CDPATH= cd -P -- "$(dirname -- "$manager_path")" \ - 2>/dev/null && pwd); then - echo 'latu-runtime-manager: cannot locate its installation directory' >&2 - exit 2 - fi - translator_dir_status=known - translator_root= - ;; - 3) - [ "$1" = status ] || usage - [ "$2" = --root ] || usage - [ -n "$3" ] || usage - if ! root=$(CDPATH= cd -P -- "$3" 2>/dev/null && pwd); then - echo "latu-runtime-manager: invalid root directory: $3" >&2 - exit 2 - fi - - translator_dir=$root/usr/bin - translator_dir_status=known - translator_root=$root - if resolved_dir=$(CDPATH= cd -P -- "$translator_dir" \ - 2>/dev/null && pwd); then - case "$root" in - /) - translator_dir=$resolved_dir - ;; - *) - case "$resolved_dir" in - "$root"|"$root"/*) - translator_dir=$resolved_dir - ;; - *) - translator_dir_status=unknown - ;; - esac - ;; - esac - elif [ -e "$translator_dir" ] || [ -L "$translator_dir" ]; then - translator_dir_status=unknown - elif [ -d "$root/usr" ] && [ ! -x "$root/usr" ]; then - translator_dir_status=unknown - fi - ;; - *) - usage - ;; -esac - -printf 'translator_x86_64=%s\n' "$(probe_translator latx-x86_64)" -printf 'translator_i386=%s\n' "$(probe_translator latx-i386)" diff --git a/runtime/latu-runtime-manager.c b/runtime/latu-runtime-manager.c new file mode 100644 index 00000000000..6d1f86eef50 --- /dev/null +++ b/runtime/latu-runtime-manager.c @@ -0,0 +1,494 @@ +/* + * SPDX-FileCopyrightText: 2026 LAT Project Authors + * + * SPDX-License-Identifier: GPL-2.0-only + */ + +#include "qemu/osdep.h" + +#include +#include + +typedef enum TranslatorDirectoryStatus { + TRANSLATOR_DIRECTORY_KNOWN, + TRANSLATOR_DIRECTORY_UNKNOWN, +} TranslatorDirectoryStatus; + +typedef enum TranslatorStatus { + TRANSLATOR_PRESENT, + TRANSLATOR_MISSING, + TRANSLATOR_UNKNOWN, +} TranslatorStatus; + +typedef enum QueryStatus { + QUERY_SELECTED, + QUERY_TRANSLATOR_MISSING, + QUERY_TRANSLATOR_UNKNOWN, + QUERY_TRANSLATOR_FAILED, + QUERY_INVALID_OUTPUT, +} QueryStatus; + +typedef enum InspectionStatus { + INSPECTION_READY, + INSPECTION_NOT_REQUIRED, + INSPECTION_MISSING, + INSPECTION_INVALID, + INSPECTION_UNKNOWN, +} InspectionStatus; + +typedef enum ReadinessStatus { + READINESS_READY, + READINESS_READY_WITH_HOST_FALLBACK, + READINESS_UNAVAILABLE, + READINESS_BROKEN, + READINESS_UNKNOWN, +} ReadinessStatus; + +typedef enum LoaderSource { + LOADER_SOURCE_NONE, + LOADER_SOURCE_RUNTIME_ROOT, + LOADER_SOURCE_HOST_FALLBACK, +} LoaderSource; + +typedef struct TranslatorLocation { + char *directory; + char *root; + TranslatorDirectoryStatus directory_status; +} TranslatorLocation; + +typedef struct RuntimeInfo { + long schema_version; + char *guest_abi; + char *runtime_root; + char *runtime_source; +} RuntimeInfo; + +typedef struct RuntimeQuery { + const char *guest_abi; + TranslatorStatus translator_status; + QueryStatus query_status; + RuntimeInfo info; +} RuntimeQuery; + +typedef struct RuntimeInspection { + const char *guest_abi; + const char *runtime_root; + const char *loader_path; + char *canonical_root; + char *configured_loader; + char *resolved_loader; + bool configured_loader_selected; + InspectionStatus status; + const char *reason; + char *effective_loader; + InspectionStatus effective_status; + const char *effective_reason; + LoaderSource loader_source; +} RuntimeInspection; + +typedef struct ProgramInfo { + const char *path; + const char *guest_abi; + char *interpreter; + bool is_static; +} ProgramInfo; + +typedef struct LoaderCheck { + char *resolved_path; + InspectionStatus status; + const char *reason; +} LoaderCheck; + +typedef struct StringBuffer { + char *data; + size_t length; + size_t capacity; +} StringBuffer; + +typedef struct JsonParser { + const char *cursor; + const char *end; +} JsonParser; + +#define RUNTIME_INFO_LIMIT (1024 * 1024) +#define RUNTIME_INFO_TIMEOUT_MS 10000 + +static const char *program_name = "latu-runtime-manager"; + +static void report_error(const char *format, ...) +{ + va_list arguments; + + fprintf(stderr, "%s: ", program_name); + va_start(arguments, format); + vfprintf(stderr, format, arguments); + va_end(arguments); + fputc('\n', stderr); +} + +static void *xmalloc(size_t size) +{ + return g_malloc(size); +} + +static char *xstrdup(const char *value) +{ + return g_strdup(value); +} + +static char *path_join(const char *left, const char *right) +{ + size_t left_length = strlen(left); + bool needs_separator = left_length && left[left_length - 1] != '/'; + size_t size = left_length + needs_separator + strlen(right) + 1; + char *result = xmalloc(size); + + snprintf(result, size, "%s%s%s", left, needs_separator ? "/" : "", + right); + return result; +} + +static char *absolute_path(const char *path) +{ + g_autofree char *current_directory = NULL; + + if (path[0] == '/') { + return xstrdup(path); + } + current_directory = g_get_current_dir(); + return path_join(current_directory, path); +} + +static char *find_program_in_path(const char *name) +{ + const char *path = getenv("PATH"); + const char *start; + + if (!path) { + return NULL; + } + + for (start = path; ; ) { + const char *end = strchr(start, ':'); + size_t length = end ? (size_t)(end - start) : strlen(start); + const char *directory = length ? start : "."; + size_t directory_length = length ? length : 1; + size_t size = directory_length + 1 + strlen(name) + 1; + char *candidate = xmalloc(size); + + snprintf(candidate, size, "%.*s/%s", (int)directory_length, + directory, name); + if (access(candidate, X_OK) == 0) { + return candidate; + } + g_free(candidate); + + if (!end) { + break; + } + start = end + 1; + } + return NULL; +} + +static void usage(void) +{ + fprintf(stderr, "usage: latu-runtime-manager status [--root ROOT]\n"); + exit(2); +} + +static bool path_is_within(const char *root, const char *path) +{ + size_t length; + + if (!strcmp(root, "/")) { + return true; + } + + length = strlen(root); + return !strncmp(root, path, length) && + (path[length] == '\0' || path[length] == '/'); +} + +static char *resolve_manager_path(const char *argv0) +{ + char *candidate = NULL; + char *resolved; + char proc_path[PATH_MAX + 1]; + ssize_t length; + + length = readlink("/proc/self/exe", proc_path, sizeof(proc_path) - 1); + if (length >= 0) { + proc_path[length] = '\0'; + candidate = xstrdup(proc_path); + } else if (strchr(argv0, '/')) { + candidate = xstrdup(argv0); + } else { + candidate = find_program_in_path(argv0); + if (!candidate) { + report_error("cannot locate the manager executable"); + return NULL; + } + } + + resolved = realpath(candidate, NULL); + g_free(candidate); + if (!resolved) { + report_error("cannot resolve the manager executable"); + return NULL; + } + + return resolved; +} + +static bool locate_sibling_translators(const char *argv0, + TranslatorLocation *location) +{ + char *manager_path = resolve_manager_path(argv0); + char *manager_path_copy; + char *directory; + char *resolved; + + if (!manager_path) { + return false; + } + + manager_path_copy = xstrdup(manager_path); + directory = dirname(manager_path_copy); + resolved = realpath(directory, NULL); + g_free(manager_path); + g_free(manager_path_copy); + if (!resolved) { + report_error("cannot locate the manager installation directory"); + return false; + } + + location->directory = resolved; + location->root = xstrdup(""); + location->directory_status = TRANSLATOR_DIRECTORY_KNOWN; + return true; +} + +static bool path_exists_without_following(const char *path) +{ + struct stat statbuf; + + return lstat(path, &statbuf) == 0; +} + +static bool missing_symlink_is_confined(const char *path, const char *root) +{ + char *current = xstrdup(path); + + for (int link_count = 0; link_count < 40; link_count++) { + char target[PATH_MAX + 1]; + ssize_t target_length; + char *current_copy = NULL; + char *candidate = NULL; + char *candidate_directory_copy = NULL; + char *candidate_name_copy = NULL; + char *resolved_directory = NULL; + char *resolved_target = NULL; + struct stat statbuf; + int stat_status; + bool target_missing; + + target_length = readlink(current, target, sizeof(target) - 1); + if (target_length < 0 || target_length == sizeof(target) - 1) { + g_free(current); + return false; + } + target[target_length] = '\0'; + if (target[0] == '/') { + candidate = xstrdup(target); + } else { + current_copy = xstrdup(current); + candidate = path_join(dirname(current_copy), target); + } + + candidate_directory_copy = xstrdup(candidate); + candidate_name_copy = xstrdup(candidate); + resolved_directory = realpath(dirname(candidate_directory_copy), NULL); + if (resolved_directory) { + resolved_target = path_join(resolved_directory, + basename(candidate_name_copy)); + } + g_free(current_copy); + g_free(candidate); + g_free(candidate_directory_copy); + g_free(candidate_name_copy); + g_free(resolved_directory); + if (!resolved_target || + (root[0] && !path_is_within(root, resolved_target))) { + g_free(resolved_target); + g_free(current); + return false; + } + + stat_status = lstat(resolved_target, &statbuf); + target_missing = stat_status < 0 && + (errno == ENOENT || errno == ENOTDIR); + if (target_missing) { + g_free(resolved_target); + g_free(current); + return true; + } + if (stat_status < 0 || !S_ISLNK(statbuf.st_mode)) { + g_free(resolved_target); + g_free(current); + return false; + } + + g_free(current); + current = resolved_target; + } + + g_free(current); + return false; +} + +static bool locate_rooted_translators(const char *input_root, + TranslatorLocation *location) +{ + char *directory; + char *usr; + char *root; + char *resolved; + struct stat statbuf; + + root = realpath(input_root, NULL); + if (!root || stat(root, &statbuf) < 0 || !S_ISDIR(statbuf.st_mode)) { + report_error("invalid root directory: %s", input_root); + g_free(root); + return false; + } + + usr = path_join(root, "usr"); + directory = path_join(usr, "bin"); + location->root = root; + location->directory_status = TRANSLATOR_DIRECTORY_KNOWN; + + resolved = realpath(directory, NULL); + if (resolved) { + if (stat(resolved, &statbuf) == 0 && S_ISDIR(statbuf.st_mode) && + path_is_within(root, resolved)) { + location->directory = resolved; + } else { + location->directory = xstrdup(directory); + location->directory_status = TRANSLATOR_DIRECTORY_UNKNOWN; + g_free(resolved); + } + g_free(directory); + g_free(usr); + return true; + } + + location->directory = directory; + if (path_exists_without_following(directory)) { + location->directory_status = TRANSLATOR_DIRECTORY_UNKNOWN; + g_free(usr); + return true; + } + + if (stat(usr, &statbuf) == 0 && S_ISDIR(statbuf.st_mode) && + access(usr, X_OK) < 0) { + location->directory_status = TRANSLATOR_DIRECTORY_UNKNOWN; + } + g_free(usr); + + return true; +} + +static const char *translator_status_name(TranslatorStatus status) +{ + switch (status) { + case TRANSLATOR_PRESENT: + return "present"; + case TRANSLATOR_MISSING: + return "missing"; + case TRANSLATOR_UNKNOWN: + return "unknown"; + } + abort(); +} + +static TranslatorStatus resolve_translator(const TranslatorLocation *location, + const char *name, + char **resolved_path) +{ + char *path; + struct stat statbuf; + char resolved[PATH_MAX + 1]; + + if (location->directory_status == TRANSLATOR_DIRECTORY_UNKNOWN) { + return TRANSLATOR_UNKNOWN; + } + + path = path_join(location->directory, name); + if (lstat(path, &statbuf) == 0 && S_ISLNK(statbuf.st_mode)) { + if (!realpath(path, resolved)) { + int resolve_errno = errno; + + if ((resolve_errno == ENOENT || resolve_errno == ENOTDIR) && + missing_symlink_is_confined(path, location->root)) { + g_free(path); + return TRANSLATOR_MISSING; + } + g_free(path); + return TRANSLATOR_UNKNOWN; + } + if (location->root[0] && + !path_is_within(location->root, resolved)) { + g_free(path); + return TRANSLATOR_UNKNOWN; + } + g_free(path); + path = xstrdup(resolved); + } + + if (stat(path, &statbuf) == 0 && S_ISREG(statbuf.st_mode) && + access(path, X_OK) == 0) { + if (resolved_path) { + *resolved_path = path; + } else { + g_free(path); + } + return TRANSLATOR_PRESENT; + } + g_free(path); + return TRANSLATOR_MISSING; +} + +int main(int argc, char **argv) +{ + TranslatorLocation location = { 0 }; + bool located; + TranslatorStatus status; + + if (strrchr(argv[0], '/')) { + program_name = strrchr(argv[0], '/') + 1; + } else if (argv[0][0]) { + program_name = argv[0]; + } + + if (argc == 2 && !strcmp(argv[1], "status")) { + located = locate_sibling_translators(argv[0], &location); + } else if (argc == 4 && !strcmp(argv[1], "status") && + !strcmp(argv[2], "--root") && argv[3][0]) { + located = locate_rooted_translators(argv[3], &location); + } else { + usage(); + } + if (!located) { + return 2; + } + + status = resolve_translator(&location, "latx-x86_64", NULL); + printf("translator_x86_64=%s\n", translator_status_name(status)); + status = resolve_translator(&location, "latx-i386", NULL); + printf("translator_i386=%s\n", translator_status_name(status)); + + g_free(location.directory); + g_free(location.root); + return 0; +} diff --git a/runtime/meson.build b/runtime/meson.build index 814b4334690..3b11ff079af 100644 --- a/runtime/meson.build +++ b/runtime/meson.build @@ -1,5 +1,8 @@ -install_data( - 'latu-runtime-manager', - install_dir: get_option('bindir'), - install_mode: 'rwxr-xr-x', -) +if host_machine.system() == 'linux' + latu_runtime_manager = executable( + 'latu-runtime-manager', + files('latu-runtime-manager.c'), + dependencies: glib, + install: true, + ) +endif diff --git a/tests/README.md b/tests/README.md index 9e3d35bb07f..4b419baf494 100644 --- a/tests/README.md +++ b/tests/README.md @@ -58,15 +58,20 @@ target's `syscall_nr_generated` sources. ## Run the suites -Configure the build with `--enable-tests`, then run: +Configure the build with `--enable-tests` and reuse the same Meson executable +for both configuration and test execution. For example, configure with +`--meson=meson`, then run: ```sh -python3 -B meson/meson.py test \ +meson test \ -C build64-tests \ --suite lat-pr-fast \ --print-errorlogs ``` +Meson build data is version-specific. Do not configure with a system Meson +and then run tests with the bundled `meson/meson.py`, or vice versa. + To run the integration suite, replace `lat-pr-fast` with `latx-integration`. diff --git a/tests/meson.build b/tests/meson.build index 51e421bfc27..1ede97e5f50 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -4,26 +4,15 @@ endif subdir('unit') -test( - 'test-latu-runtime-manager', - find_program('runtime/test-latu-runtime-manager.sh'), - protocol: 'exitcode', - suite: 'lat-pr-fast', -) - -latu_install_depends = [] -foreach name, emulator : emulators - latu_install_depends += emulator -endforeach - -test( - 'test-latu-install', - find_program('runtime/test-latu-install.sh'), - args: [python.full_path(), project_source_root / 'meson' / 'meson.py', - project_build_root, get_option('prefix')], - depends: latu_install_depends, - protocol: 'exitcode', - suite: 'lat-pr-fast', -) +if host_machine.system() == 'linux' + test( + 'test-latu-runtime-manager', + find_program('runtime/test-latu-runtime-manager.sh'), + args: [latu_runtime_manager.full_path()], + depends: latu_runtime_manager, + protocol: 'exitcode', + suite: 'lat-pr-fast', + ) +endif subdir('integration') diff --git a/tests/runtime/test-latu-install.sh b/tests/runtime/test-latu-install.sh index b859ae8ba43..95333d9972c 100755 --- a/tests/runtime/test-latu-install.sh +++ b/tests/runtime/test-latu-install.sh @@ -1,15 +1,14 @@ #!/bin/sh set -eu -[ "$#" -eq 4 ] || { - echo "usage: $0 PYTHON MESON_PY BUILD_DIR PREFIX" >&2 +[ "$#" -eq 3 ] || { + echo "usage: $0 NINJA BUILD_DIR PREFIX" >&2 exit 2 } -python=$1 -meson_py=$2 -builddir=$3 -prefix=$4 +ninja=$1 +builddir=$2 +prefix=$3 workdir=$(mktemp -d) trap 'rm -rf "$workdir"' EXIT HUP INT TERM @@ -21,15 +20,10 @@ fail() destdir=$workdir/dest install_log=$workdir/install.log -if [ -f "$meson_py" ]; then - "$python" "$meson_py" install -C "$builddir" --no-rebuild \ - --destdir "$destdir" > "$install_log" 2>&1 -else - "$python" -m mesonbuild.mesonmain install -C "$builddir" \ - --no-rebuild --destdir "$destdir" > "$install_log" 2>&1 -fi || { +DESTDIR="$destdir" "$ninja" -C "$builddir" install \ + > "$install_log" 2>&1 || { cat "$install_log" >&2 - fail 'meson install into DESTDIR failed' + fail 'ninja install into DESTDIR failed' } manager=$destdir$prefix/bin/latu-runtime-manager diff --git a/tests/runtime/test-latu-runtime-manager.sh b/tests/runtime/test-latu-runtime-manager.sh index eb859be5191..c183b1cfee9 100755 --- a/tests/runtime/test-latu-runtime-manager.sh +++ b/tests/runtime/test-latu-runtime-manager.sh @@ -1,8 +1,12 @@ #!/bin/sh set -eu -srcdir=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd) -manager=$srcdir/runtime/latu-runtime-manager +[ "$#" -eq 1 ] || { + echo "usage: $0 LATU_RUNTIME_MANAGER" >&2 + exit 2 +} + +manager=$1 workdir=$(mktemp -d) trap 'rm -rf "$workdir"' EXIT HUP INT TERM From f3034a8abbdb0b7dfe23734f9745c4fe0b1106f2 Mon Sep 17 00:00:00 2001 From: Hanlu Li Date: Sun, 9 Aug 2026 07:14:17 +0800 Subject: [PATCH 2/6] runtime: query selected guest runtimes Add current and list commands backed by the versioned --runtime-info interface of each translator. The manager preserves translator configuration precedence instead of parsing LATX configuration itself. Default queries cover x86_64 and i386, while --abi remains an optional filter. A selected runtime is successful when the peer ABI is unavailable; an unknown or failed peer still dominates with exit status 2. Bound translator output to 1 MiB and ten seconds, parse strict UTF-8 JSON, and cover failure and escaping cases. Signed-off-by: Hanlu Li --- runtime/latu-runtime-manager.c | 824 ++++++++++++++++++++- tests/runtime/test-latu-runtime-manager.sh | 373 +++++++++- 2 files changed, 1176 insertions(+), 21 deletions(-) diff --git a/runtime/latu-runtime-manager.c b/runtime/latu-runtime-manager.c index 6d1f86eef50..ed39eccf5e6 100644 --- a/runtime/latu-runtime-manager.c +++ b/runtime/latu-runtime-manager.c @@ -193,7 +193,11 @@ static char *find_program_in_path(const char *name) static void usage(void) { - fprintf(stderr, "usage: latu-runtime-manager status [--root ROOT]\n"); + fprintf(stderr, + "usage: latu-runtime-manager status [--root ROOT]\n" + " latu-runtime-manager current [--abi ABI]" + " [--program PROGRAM]\n" + " latu-runtime-manager list [--program PROGRAM]\n"); exit(2); } @@ -459,11 +463,800 @@ static TranslatorStatus resolve_translator(const TranslatorLocation *location, return TRANSLATOR_MISSING; } +static void string_buffer_reserve(StringBuffer *buffer, size_t extra) +{ + size_t required = buffer->length + extra + 1; + + if (required <= buffer->capacity) { + return; + } + if (!buffer->capacity) { + buffer->capacity = 128; + } + while (buffer->capacity < required) { + if (buffer->capacity > SIZE_MAX / 2) { + report_error("runtime information is too large"); + exit(2); + } + buffer->capacity *= 2; + } + buffer->data = g_realloc(buffer->data, buffer->capacity); +} + +static void string_buffer_append_byte(StringBuffer *buffer, unsigned char byte) +{ + string_buffer_reserve(buffer, 1); + buffer->data[buffer->length++] = byte; + buffer->data[buffer->length] = '\0'; +} + +static bool string_buffer_append_codepoint(StringBuffer *buffer, + uint32_t codepoint) +{ + if (!codepoint || codepoint > 0x10ffff || + (codepoint >= 0xd800 && codepoint <= 0xdfff)) { + return false; + } + if (codepoint <= 0x7f) { + string_buffer_append_byte(buffer, codepoint); + } else if (codepoint <= 0x7ff) { + string_buffer_append_byte(buffer, 0xc0 | (codepoint >> 6)); + string_buffer_append_byte(buffer, 0x80 | (codepoint & 0x3f)); + } else if (codepoint <= 0xffff) { + string_buffer_append_byte(buffer, 0xe0 | (codepoint >> 12)); + string_buffer_append_byte(buffer, 0x80 | ((codepoint >> 6) & 0x3f)); + string_buffer_append_byte(buffer, 0x80 | (codepoint & 0x3f)); + } else { + string_buffer_append_byte(buffer, 0xf0 | (codepoint >> 18)); + string_buffer_append_byte(buffer, 0x80 | ((codepoint >> 12) & 0x3f)); + string_buffer_append_byte(buffer, 0x80 | ((codepoint >> 6) & 0x3f)); + string_buffer_append_byte(buffer, 0x80 | (codepoint & 0x3f)); + } + return true; +} + +static void json_skip_space(JsonParser *parser) +{ + while (parser->cursor < parser->end && + (*parser->cursor == ' ' || *parser->cursor == '\t' || + *parser->cursor == '\r' || *parser->cursor == '\n')) { + parser->cursor++; + } +} + +static int json_hex_value(char value) +{ + if (value >= '0' && value <= '9') { + return value - '0'; + } + if (value >= 'a' && value <= 'f') { + return value - 'a' + 10; + } + if (value >= 'A' && value <= 'F') { + return value - 'A' + 10; + } + return -1; +} + +static bool json_parse_hex4(JsonParser *parser, uint32_t *value) +{ + uint32_t result = 0; + + if ((size_t)(parser->end - parser->cursor) < 4) { + return false; + } + for (int i = 0; i < 4; i++) { + int digit = json_hex_value(*parser->cursor++); + + if (digit < 0) { + return false; + } + result = (result << 4) | digit; + } + *value = result; + return true; +} + +static char *json_parse_string(JsonParser *parser) +{ + StringBuffer result = { 0 }; + + if (parser->cursor >= parser->end || *parser->cursor++ != '"') { + return NULL; + } + + while (parser->cursor < parser->end) { + unsigned char byte = *parser->cursor++; + + if (byte == '"') { + if (!result.data) { + result.data = xstrdup(""); + } + return result.data; + } + if (byte < 0x20) { + break; + } + if (byte != '\\') { + string_buffer_append_byte(&result, byte); + continue; + } + if (parser->cursor >= parser->end) { + break; + } + + byte = *parser->cursor++; + switch (byte) { + case '"': + case '\\': + case '/': + string_buffer_append_byte(&result, byte); + break; + case 'b': + string_buffer_append_byte(&result, '\b'); + break; + case 'f': + string_buffer_append_byte(&result, '\f'); + break; + case 'n': + string_buffer_append_byte(&result, '\n'); + break; + case 'r': + string_buffer_append_byte(&result, '\r'); + break; + case 't': + string_buffer_append_byte(&result, '\t'); + break; + case 'u': { + uint32_t codepoint; + + if (!json_parse_hex4(parser, &codepoint)) { + goto invalid; + } + if (codepoint >= 0xd800 && codepoint <= 0xdbff) { + uint32_t low; + + if ((size_t)(parser->end - parser->cursor) < 6 || + parser->cursor[0] != '\\' || + parser->cursor[1] != 'u') { + goto invalid; + } + parser->cursor += 2; + if (!json_parse_hex4(parser, &low) || + low < 0xdc00 || low > 0xdfff) { + goto invalid; + } + codepoint = 0x10000 + ((codepoint - 0xd800) << 10) + + (low - 0xdc00); + } + if (!string_buffer_append_codepoint(&result, codepoint)) { + goto invalid; + } + break; + } + default: + goto invalid; + } + } + +invalid: + g_free(result.data); + return NULL; +} + +static bool json_parse_integer(JsonParser *parser, long *value) +{ + const char *start = parser->cursor; + char *end; + gint64 parsed; + + if (parser->cursor < parser->end && *parser->cursor == '-') { + parser->cursor++; + } + if (parser->cursor >= parser->end || + *parser->cursor < '0' || *parser->cursor > '9') { + parser->cursor = start; + return false; + } + if (*parser->cursor == '0') { + parser->cursor++; + } else { + while (parser->cursor < parser->end && + *parser->cursor >= '0' && *parser->cursor <= '9') { + parser->cursor++; + } + } + if (parser->cursor < parser->end && + (*parser->cursor == '.' || *parser->cursor == 'e' || + *parser->cursor == 'E')) { + parser->cursor = start; + return false; + } + + errno = 0; + parsed = g_ascii_strtoll(start, &end, 10); + if (errno || end != parser->cursor || parsed < LONG_MIN || + parsed > LONG_MAX) { + parser->cursor = start; + return false; + } + *value = parsed; + return true; +} + +static bool runtime_source_valid(const char *source) +{ + return !strcmp(source, "default") || + !strcmp(source, "system_config") || + !strcmp(source, "user_config") || + !strcmp(source, "environment") || + !strcmp(source, "command_line"); +} + +static bool utf8_valid(const char *value) +{ + const unsigned char *cursor = (const unsigned char *)value; + + while (*cursor) { + size_t length; + uint32_t codepoint; + + if (*cursor < 0x80) { + cursor++; + continue; + } + if ((*cursor & 0xe0) == 0xc0) { + length = 2; + codepoint = *cursor & 0x1f; + } else if ((*cursor & 0xf0) == 0xe0) { + length = 3; + codepoint = *cursor & 0x0f; + } else if ((*cursor & 0xf8) == 0xf0) { + length = 4; + codepoint = *cursor & 0x07; + } else { + return false; + } + for (size_t i = 1; i < length; i++) { + if ((cursor[i] & 0xc0) != 0x80) { + return false; + } + codepoint = (codepoint << 6) | (cursor[i] & 0x3f); + } + if ((length == 2 && codepoint < 0x80) || + (length == 3 && codepoint < 0x800) || + (length == 4 && codepoint < 0x10000) || + codepoint > 0x10ffff || + (codepoint >= 0xd800 && codepoint <= 0xdfff)) { + return false; + } + cursor += length; + } + return true; +} + +static bool runtime_info_parse(const char *json, size_t json_length, + const char *expected_abi, RuntimeInfo *info) +{ + JsonParser parser = { json, json + json_length }; + bool have_schema = false; + bool have_abi = false; + bool have_root = false; + bool have_source = false; + + json_skip_space(&parser); + if (parser.cursor >= parser.end || *parser.cursor++ != '{') { + return false; + } + + for (;;) { + char *key; + + json_skip_space(&parser); + if (parser.cursor < parser.end && *parser.cursor == '}') { + parser.cursor++; + break; + } + key = json_parse_string(&parser); + if (!key) { + goto invalid; + } + json_skip_space(&parser); + if (parser.cursor >= parser.end || *parser.cursor++ != ':') { + g_free(key); + goto invalid; + } + json_skip_space(&parser); + + if (!strcmp(key, "schema_version") && !have_schema) { + have_schema = json_parse_integer(&parser, + &info->schema_version); + if (!have_schema) { + g_free(key); + goto invalid; + } + } else if (!strcmp(key, "guest_abi") && !have_abi) { + info->guest_abi = json_parse_string(&parser); + have_abi = info->guest_abi != NULL; + if (!have_abi) { + g_free(key); + goto invalid; + } + } else if (!strcmp(key, "runtime_root") && !have_root) { + info->runtime_root = json_parse_string(&parser); + have_root = info->runtime_root != NULL; + if (!have_root) { + g_free(key); + goto invalid; + } + } else if (!strcmp(key, "runtime_source") && !have_source) { + info->runtime_source = json_parse_string(&parser); + have_source = info->runtime_source != NULL; + if (!have_source) { + g_free(key); + goto invalid; + } + } else { + g_free(key); + goto invalid; + } + g_free(key); + + json_skip_space(&parser); + if (parser.cursor < parser.end && *parser.cursor == ',') { + parser.cursor++; + json_skip_space(&parser); + if (parser.cursor < parser.end && *parser.cursor == '}') { + goto invalid; + } + continue; + } + if (parser.cursor < parser.end && *parser.cursor == '}') { + parser.cursor++; + break; + } + goto invalid; + } + + json_skip_space(&parser); + if (parser.cursor != parser.end || !have_schema || !have_abi || + !have_root || !have_source || info->schema_version != 1 || + strcmp(info->guest_abi, expected_abi) || + !runtime_source_valid(info->runtime_source) || + !utf8_valid(info->guest_abi) || !utf8_valid(info->runtime_root) || + !utf8_valid(info->runtime_source)) { + goto invalid; + } + return true; + +invalid: + g_free(info->guest_abi); + g_free(info->runtime_root); + g_free(info->runtime_source); + memset(info, 0, sizeof(*info)); + return false; +} + +static char *read_translator_info(const char *translator, + const char *program, int *exit_status, + size_t *output_length) +{ + StringBuffer output = { 0 }; + int pipefd[2]; + pid_t child; + int status; + bool overflow = false; + bool read_failed = false; + bool timed_out = false; + struct timespec start_time; + int64_t deadline = -1; + + if (pipe(pipefd) < 0) { + return NULL; + } + child = fork(); + if (child < 0) { + close(pipefd[0]); + close(pipefd[1]); + return NULL; + } + if (child == 0) { + int nullfd; + + close(pipefd[0]); + if (dup2(pipefd[1], STDOUT_FILENO) < 0) { + _exit(127); + } + close(pipefd[1]); + nullfd = open("/dev/null", O_WRONLY); + if (nullfd >= 0) { + dup2(nullfd, STDERR_FILENO); + close(nullfd); + } + if (program) { + char *const args[] = { + (char *)translator, (char *)"--runtime-info", (char *)"--", + (char *)program, NULL, + }; + execv(translator, args); + } else { + char *const args[] = { + (char *)translator, (char *)"--runtime-info", NULL, + }; + execv(translator, args); + } + _exit(127); + } + + close(pipefd[1]); + if (clock_gettime(CLOCK_MONOTONIC, &start_time) == 0) { + deadline = (int64_t)start_time.tv_sec * 1000 + + start_time.tv_nsec / 1000000 + + RUNTIME_INFO_TIMEOUT_MS; + } + for (;;) { + char chunk[4096]; + struct pollfd pollfd = { + .fd = pipefd[0], + .events = POLLIN, + }; + int timeout = RUNTIME_INFO_TIMEOUT_MS; + int poll_status; + ssize_t length; + + if (deadline >= 0) { + struct timespec now; + + if (clock_gettime(CLOCK_MONOTONIC, &now) == 0) { + int64_t remaining = deadline - + ((int64_t)now.tv_sec * 1000 + now.tv_nsec / 1000000); + + if (remaining <= 0) { + timed_out = true; + break; + } + timeout = remaining > INT_MAX ? INT_MAX : (int)remaining; + } + } + do { + poll_status = poll(&pollfd, 1, timeout); + } while (poll_status < 0 && errno == EINTR); + if (!poll_status) { + timed_out = true; + break; + } + if (poll_status < 0) { + read_failed = true; + break; + } + + do { + length = read(pipefd[0], chunk, sizeof(chunk)); + } while (length < 0 && errno == EINTR); + + if (length < 0) { + read_failed = true; + break; + } + if (!length) { + break; + } + if (overflow) { + continue; + } + if (output.length + (size_t)length > RUNTIME_INFO_LIMIT) { + g_free(output.data); + output.data = NULL; + output.length = 0; + output.capacity = 0; + overflow = true; + break; + } + string_buffer_reserve(&output, length); + memcpy(output.data + output.length, chunk, length); + output.length += length; + output.data[output.length] = '\0'; + } + if (overflow || read_failed || timed_out) { + kill(child, SIGKILL); + } + close(pipefd[0]); + + while (waitpid(child, &status, 0) < 0) { + if (errno != EINTR) { + g_free(output.data); + return NULL; + } + } + if (WIFEXITED(status)) { + *exit_status = WEXITSTATUS(status); + } else { + *exit_status = 128; + } + if (overflow || read_failed || timed_out) { + g_free(output.data); + } + if (overflow) { + *output_length = 0; + return xstrdup(""); + } + if (read_failed || timed_out) { + *output_length = 0; + return NULL; + } + if (!output.data) { + output.data = xstrdup(""); + } + *output_length = output.length; + return output.data; +} + +static RuntimeQuery query_runtime(const TranslatorLocation *location, + const char *guest_abi, + const char *program) +{ + RuntimeQuery query = { .guest_abi = guest_abi }; + const char *name = !strcmp(guest_abi, "x86_64") ? + "latx-x86_64" : "latx-i386"; + char *translator = NULL; + char *output; + int exit_status; + size_t output_length; + + query.translator_status = resolve_translator(location, name, &translator); + if (query.translator_status == TRANSLATOR_MISSING) { + query.query_status = QUERY_TRANSLATOR_MISSING; + return query; + } + if (query.translator_status == TRANSLATOR_UNKNOWN) { + query.query_status = QUERY_TRANSLATOR_UNKNOWN; + return query; + } + + output = read_translator_info(translator, program, &exit_status, + &output_length); + g_free(translator); + if (!output || exit_status != 0) { + g_free(output); + query.query_status = QUERY_TRANSLATOR_FAILED; + return query; + } + if (!runtime_info_parse(output, output_length, guest_abi, &query.info)) { + g_free(output); + query.query_status = QUERY_INVALID_OUTPUT; + return query; + } + g_free(output); + query.query_status = QUERY_SELECTED; + return query; +} + +static void runtime_info_free(RuntimeInfo *info) +{ + g_free(info->guest_abi); + g_free(info->runtime_root); + g_free(info->runtime_source); + memset(info, 0, sizeof(*info)); +} + +static void json_write_string(const char *value) +{ + const unsigned char *cursor = (const unsigned char *)value; + + putchar('"'); + while (*cursor) { + switch (*cursor) { + case '"': + fputs("\\\"", stdout); + cursor++; + break; + case '\\': + fputs("\\\\", stdout); + cursor++; + break; + case '\b': + fputs("\\b", stdout); + cursor++; + break; + case '\f': + fputs("\\f", stdout); + cursor++; + break; + case '\n': + fputs("\\n", stdout); + cursor++; + break; + case '\r': + fputs("\\r", stdout); + cursor++; + break; + case '\t': + fputs("\\t", stdout); + cursor++; + break; + default: + if (*cursor < 0x20) { + printf("\\u%04x", *cursor++); + } else if (*cursor < 0x80) { + putchar(*cursor++); + } else { + size_t length; + uint32_t codepoint; + bool valid = true; + + if ((*cursor & 0xe0) == 0xc0) { + length = 2; + codepoint = *cursor & 0x1f; + } else if ((*cursor & 0xf0) == 0xe0) { + length = 3; + codepoint = *cursor & 0x0f; + } else if ((*cursor & 0xf8) == 0xf0) { + length = 4; + codepoint = *cursor & 0x07; + } else { + fputs("\\ufffd", stdout); + cursor++; + break; + } + for (size_t i = 1; i < length; i++) { + if ((cursor[i] & 0xc0) != 0x80) { + valid = false; + break; + } + codepoint = (codepoint << 6) | (cursor[i] & 0x3f); + } + if ((length == 2 && codepoint < 0x80) || + (length == 3 && codepoint < 0x800) || + (length == 4 && codepoint < 0x10000) || + codepoint > 0x10ffff || + (codepoint >= 0xd800 && codepoint <= 0xdfff)) { + valid = false; + } + if (!valid) { + fputs("\\ufffd", stdout); + cursor++; + break; + } + fwrite(cursor, 1, length, stdout); + cursor += length; + } + } + } + putchar('"'); +} + +static const char *query_status_name(QueryStatus status) +{ + switch (status) { + case QUERY_SELECTED: + return "selected"; + case QUERY_TRANSLATOR_MISSING: + return "translator_missing"; + case QUERY_TRANSLATOR_UNKNOWN: + return "translator_unknown"; + case QUERY_TRANSLATOR_FAILED: + return "translator_query_failed"; + case QUERY_INVALID_OUTPUT: + return "invalid_runtime_info"; + } + abort(); +} + +static void print_runtime_query(const RuntimeQuery *query) +{ + fputs("{\"schema_version\":1,\"guest_abi\":", stdout); + json_write_string(query->guest_abi); + fputs(",\"translator_status\":", stdout); + json_write_string(translator_status_name(query->translator_status)); + fputs(",\"query_status\":", stdout); + json_write_string(query_status_name(query->query_status)); + fputs(",\"runtime_root\":", stdout); + if (query->query_status == QUERY_SELECTED) { + json_write_string(query->info.runtime_root); + } else { + fputs("null", stdout); + } + fputs(",\"runtime_source\":", stdout); + if (query->query_status == QUERY_SELECTED) { + json_write_string(query->info.runtime_source); + } else { + fputs("null", stdout); + } + fputs("}\n", stdout); +} + +static int query_exit_status(const RuntimeQuery *query) +{ + switch (query->query_status) { + case QUERY_SELECTED: + return 0; + case QUERY_TRANSLATOR_MISSING: + return 1; + case QUERY_TRANSLATOR_UNKNOWN: + case QUERY_TRANSLATOR_FAILED: + case QUERY_INVALID_OUTPUT: + return 2; + } + abort(); +} + +static bool guest_abi_valid(const char *guest_abi) +{ + return !strcmp(guest_abi, "x86_64") || !strcmp(guest_abi, "i386"); +} + +static int query_both_runtimes(const TranslatorLocation *location, + const char *program) +{ + RuntimeQuery x86_64_query = query_runtime(location, "x86_64", program); + RuntimeQuery i386_query = query_runtime(location, "i386", program); + int x86_64_status; + int i386_status; + int status; + + print_runtime_query(&x86_64_query); + print_runtime_query(&i386_query); + x86_64_status = query_exit_status(&x86_64_query); + i386_status = query_exit_status(&i386_query); + status = x86_64_status == 2 || i386_status == 2 ? 2 : + x86_64_status == 0 || i386_status == 0 ? 0 : 1; + runtime_info_free(&x86_64_query.info); + runtime_info_free(&i386_query.info); + return status; +} + +static int current_command(const TranslatorLocation *location, int argc, + char **argv) +{ + const char *guest_abi = NULL; + const char *program = NULL; + RuntimeQuery query; + int status; + + for (int i = 0; i < argc; ) { + if (!strcmp(argv[i], "--abi") && i + 1 < argc && !guest_abi) { + guest_abi = argv[i + 1]; + } else if (!strcmp(argv[i], "--program") && i + 1 < argc && + !program && argv[i + 1][0]) { + program = argv[i + 1]; + } else { + usage(); + } + i += 2; + } + if (guest_abi && !guest_abi_valid(guest_abi)) { + usage(); + } + + if (!guest_abi) { + return query_both_runtimes(location, program); + } + + query = query_runtime(location, guest_abi, program); + print_runtime_query(&query); + status = query_exit_status(&query); + runtime_info_free(&query.info); + return status; +} + +static int list_command(const TranslatorLocation *location, int argc, + char **argv) +{ + const char *program = NULL; + if (argc == 2 && !strcmp(argv[0], "--program") && argv[1][0]) { + program = argv[1]; + } else if (argc) { + usage(); + } + + return query_both_runtimes(location, program); +} + int main(int argc, char **argv) { TranslatorLocation location = { 0 }; - bool located; - TranslatorStatus status; + bool located = false; + int status; if (strrchr(argv[0], '/')) { program_name = strrchr(argv[0], '/') + 1; @@ -471,6 +1264,21 @@ int main(int argc, char **argv) program_name = argv[0]; } + if (argc >= 2 && + (!strcmp(argv[1], "current") || !strcmp(argv[1], "list"))) { + if (!locate_sibling_translators(argv[0], &location)) { + return 2; + } + if (!strcmp(argv[1], "current")) { + status = current_command(&location, argc - 2, argv + 2); + } else { + status = list_command(&location, argc - 2, argv + 2); + } + g_free(location.directory); + g_free(location.root); + return status; + } + if (argc == 2 && !strcmp(argv[1], "status")) { located = locate_sibling_translators(argv[0], &location); } else if (argc == 4 && !strcmp(argv[1], "status") && @@ -483,10 +1291,12 @@ int main(int argc, char **argv) return 2; } - status = resolve_translator(&location, "latx-x86_64", NULL); - printf("translator_x86_64=%s\n", translator_status_name(status)); - status = resolve_translator(&location, "latx-i386", NULL); - printf("translator_i386=%s\n", translator_status_name(status)); + printf("translator_x86_64=%s\n", + translator_status_name(resolve_translator( + &location, "latx-x86_64", NULL))); + printf("translator_i386=%s\n", + translator_status_name(resolve_translator( + &location, "latx-i386", NULL))); g_free(location.directory); g_free(location.root); diff --git a/tests/runtime/test-latu-runtime-manager.sh b/tests/runtime/test-latu-runtime-manager.sh index c183b1cfee9..3c363df8d48 100755 --- a/tests/runtime/test-latu-runtime-manager.sh +++ b/tests/runtime/test-latu-runtime-manager.sh @@ -18,28 +18,40 @@ fail() assert_status() { - expected=$1 - actual=$2 - context=$3 - [ "$actual" -eq "$expected" ] || - fail "$context: expected status $expected, got $actual" + assert_expected=$1 + assert_actual=$2 + assert_context=$3 + [ "$assert_actual" -eq "$assert_expected" ] || + fail "$assert_context: expected status $assert_expected," \ + "got $assert_actual" } assert_output() { - expected=$1 - actual=$2 - context=$3 - [ "$actual" = "$expected" ] || { - echo "FAIL: $context: unexpected output" >&2 + output_expected=$1 + output_actual=$2 + output_context=$3 + [ "$output_actual" = "$output_expected" ] || { + echo "FAIL: $output_context: unexpected output" >&2 echo 'expected:' >&2 - printf '%s\n' "$expected" >&2 + printf '%s\n' "$output_expected" >&2 echo 'actual:' >&2 - printf '%s\n' "$actual" >&2 + printf '%s\n' "$output_actual" >&2 exit 1 } } +assert_contains() +{ + contains_expected=$1 + contains_actual=$2 + contains_context=$3 + case "$contains_actual" in + *"$contains_expected"*) ;; + *) fail "$contains_context: missing '$contains_expected'" ;; + esac +} + install_translator() { install_root=$1 @@ -59,6 +71,141 @@ run_status() set -e } +install_query_translator() +{ + query_bin=$1 + query_name=$2 + query_json=$3 + cat > "$query_bin/$query_name" <<'EOF' +#!/bin/sh +set -eu + +name=${0##*/} +abi=${name#latx-} +[ "$#" -ge 1 ] && [ "$1" = --runtime-info ] || exit 2 +if [ -n "${LATU_TEST_ARGV_LOG-}" ]; then + : > "$LATU_TEST_ARGV_LOG.$abi" + for argument do + printf '%s\n' "$argument" >> "$LATU_TEST_ARGV_LOG.$abi" + done +fi +if [ -e "$0.exit" ]; then + exit "$(cat "$0.exit")" +fi +cat "$0.json" +EOF + chmod +x "$query_bin/$query_name" + printf '%s\n' "$query_json" > "$query_bin/$query_name.json" +} + +runtime_info_json() +{ + info_abi=$1 + info_root=$2 + info_source=$3 + printf '{"schema_version":1,"guest_abi":"%s",' "$info_abi" + printf '"runtime_root":"%s","runtime_source":"%s"}' \ + "$info_root" "$info_source" +} + +runtime_query_json() +{ + query_abi=$1 + query_translator=$2 + query_status=$3 + query_root=$4 + query_source=$5 + printf '{"schema_version":1,"guest_abi":"%s",' "$query_abi" + printf '"translator_status":"%s","query_status":"%s",' \ + "$query_translator" "$query_status" + printf '"runtime_root":%s,"runtime_source":%s}' \ + "$query_root" "$query_source" +} + +make_loader() +{ + loader_root=$1 + loader_abi=$2 + case "$loader_abi" in + x86_64) loader_path=$loader_root/lib64/ld-linux-x86-64.so.2 ;; + i386) loader_path=$loader_root/lib/ld-linux.so.2 ;; + *) fail "unsupported loader ABI: $loader_abi" ;; + esac + mkdir -p "${loader_path%/*}" + python3 - "$loader_abi" "$loader_path" <<'PY' +import struct +import sys + +abi, path = sys.argv[1:] +if abi == "x86_64": + ident = b"\x7fELF" + bytes([2, 1, 1, 0]) + bytes(8) + ehdr = struct.pack( + "<16sHHIQQQIHHHHHH", ident, 3, 62, 1, 0, 64, 0, 0, + 64, 56, 1, 0, 0, 0) + phdr = struct.pack(" "$workdir/usage.stdout" 2> "$workdir/usage.stderr" +"$manager" install > "$workdir/usage.stdout" 2> "$workdir/usage.stderr" usage_status=$? set -e assert_status 2 "$usage_status" 'unsupported subcommand' grep -F 'usage: latu-runtime-manager status [--root ROOT]' \ "$workdir/usage.stderr" > /dev/null || fail 'usage diagnostic missing' -echo 'PASS: distribution-neutral dual-translator status' +query_bin=$workdir/query-bin +mkdir -p "$query_bin" +cp "$manager" "$query_bin/latu-runtime-manager" +install_query_translator "$query_bin" latx-x86_64 \ + "$(runtime_info_json x86_64 /runtime/64 user_config)" +install_query_translator "$query_bin" latx-i386 \ + "$(runtime_info_json i386 /runtime/32 system_config)" + +expected=$(runtime_query_json x86_64 present selected \ + '"/runtime/64"' '"user_config"') +set +e +actual=$("$query_bin/latu-runtime-manager" current --abi x86_64 \ + 2> "$workdir/current.stderr") +actual_status=$? +set -e +assert_status 0 "$actual_status" 'current x86-64 selection' +assert_output "$expected" "$actual" 'current x86-64 selection' + +i386_expected=$(runtime_query_json i386 present selected \ + '"/runtime/32"' '"system_config"') +expected_list="$expected +$i386_expected" +set +e +actual=$("$query_bin/latu-runtime-manager" current \ + 2> "$workdir/current-all.stderr") +actual_status=$? +set -e +assert_status 0 "$actual_status" 'current selection without explicit ABI' +assert_output "$expected_list" "$actual" \ + 'current selection without explicit ABI' + +set +e +actual=$("$query_bin/latu-runtime-manager" list \ + 2> "$workdir/list.stderr") +actual_status=$? +set -e +assert_status 0 "$actual_status" 'list selected runtimes' +assert_output "$expected_list" "$actual" 'list selected runtimes' + +LATU_TEST_ARGV_LOG=$workdir/argv \ + "$query_bin/latu-runtime-manager" current --abi i386 \ + --program /guest/app > "$workdir/program.stdout" || + fail 'program-specific current query failed' +assert_output '--runtime-info +-- +/guest/app' "$(cat "$workdir/argv.i386")" \ + 'program-specific translator arguments' + +mv "$query_bin/latx-i386" "$query_bin/latx-i386.absent" +expected_missing=$(runtime_query_json i386 missing translator_missing \ + null null) +set +e +actual=$("$query_bin/latu-runtime-manager" current --abi i386 \ + 2> "$workdir/missing-query.stderr") +actual_status=$? +set -e +assert_status 1 "$actual_status" 'missing translator query' +assert_output "$expected_missing" "$actual" 'missing translator query' + +set +e +actual=$("$query_bin/latu-runtime-manager" current \ + 2> "$workdir/missing-peer-query.stderr") +actual_status=$? +set -e +assert_status 0 "$actual_status" 'one selected default query' +assert_output "$expected +$expected_missing" "$actual" 'one selected default query' +mv "$query_bin/latx-i386.absent" "$query_bin/latx-i386" + +invalid_expected=$(runtime_query_json x86_64 present invalid_runtime_info \ + null null) +for invalid_case in \ + '{' \ + '{"schema_version":2,"guest_abi":"x86_64",'\ +'"runtime_root":"/runtime/64","runtime_source":"default"}' \ + '{"schema_version":1,"guest_abi":"i386",'\ +'"runtime_root":"/runtime/64","runtime_source":"default"}' \ + '{"schema_version":1,"guest_abi":"x86_64",'\ +'"runtime_root":"/runtime/64","runtime_source":"provider"}' \ + '{"schema_version":1,"guest_abi":"x86_64",'\ +'"runtime_root":"/runtime/64","runtime_source":"default",}' \ + '{"schema_version":1,"guest_abi":"x86_64",'\ +'"runtime_root":"/runtime/64","runtime_source":"default"} trailing' +do + printf '%s\n' "$invalid_case" > "$query_bin/latx-x86_64.json" + set +e + actual=$("$query_bin/latu-runtime-manager" current --abi x86_64 \ + 2> "$workdir/invalid.stderr") + actual_status=$? + set -e + assert_status 2 "$actual_status" 'invalid runtime information' + assert_output "$invalid_expected" "$actual" \ + 'invalid runtime information' +done + +printf '%s' \ + '{"schema_version":1,"guest_abi":"x86_64","runtime_root":"' \ + > "$query_bin/latx-x86_64.json" +printf '\377' >> "$query_bin/latx-x86_64.json" +printf '%s\n' '","runtime_source":"default"}' \ + >> "$query_bin/latx-x86_64.json" +set +e +actual=$("$query_bin/latu-runtime-manager" current --abi x86_64 \ + 2> "$workdir/invalid-utf8.stderr") +actual_status=$? +set -e +assert_status 2 "$actual_status" 'invalid UTF-8 runtime information' +assert_output "$invalid_expected" "$actual" \ + 'invalid UTF-8 runtime information' + +printf '%s' \ + "$(runtime_info_json x86_64 /runtime/64 default)" \ + > "$query_bin/latx-x86_64.json" +printf '\000trailing' >> "$query_bin/latx-x86_64.json" +set +e +actual=$("$query_bin/latu-runtime-manager" current --abi x86_64 \ + 2> "$workdir/embedded-nul.stderr") +actual_status=$? +set -e +assert_status 2 "$actual_status" 'embedded NUL runtime information' +assert_output "$invalid_expected" "$actual" \ + 'embedded NUL runtime information' + +marker=$workdir/must-not-exist +special_input='line\nquote\"slash\\\u4e2d$(touch '$marker')' +printf '%s\n' \ + "$(runtime_info_json x86_64 "$special_input" environment)" \ + > "$query_bin/latx-x86_64.json" +special_root='"line\nquote\"slash\\中$(touch '$marker')"' +special_expected=$(runtime_query_json x86_64 present selected \ + "$special_root" '"environment"') +set +e +actual=$("$query_bin/latu-runtime-manager" current --abi x86_64 \ + 2> "$workdir/special.stderr") +actual_status=$? +set -e +assert_status 0 "$actual_status" 'escaped runtime root' +assert_output "$special_expected" "$actual" 'escaped runtime root' +[ ! -e "$marker" ] || fail 'runtime information executed shell content' + +printf '%s\n' \ + "$(runtime_info_json x86_64 /runtime/64 default)" \ + > "$query_bin/latx-x86_64.json" +printf '3\n' > "$query_bin/latx-x86_64.exit" +failed_expected=$(runtime_query_json x86_64 present \ + translator_query_failed null null) +set +e +actual=$("$query_bin/latu-runtime-manager" current --abi x86_64 \ + 2> "$workdir/failed.stderr") +actual_status=$? +set -e +assert_status 2 "$actual_status" 'translator query failure' +assert_output "$failed_expected" "$actual" 'translator query failure' +rm "$query_bin/latx-x86_64.exit" + +cat > "$query_bin/latx-x86_64" <<'EOF' +#!/bin/sh +trap '' PIPE +while :; do + dd if=/dev/zero bs=65536 count=32 2> /dev/null || : +done +EOF +chmod +x "$query_bin/latx-x86_64" +set +e +actual=$("$query_bin/latu-runtime-manager" current --abi x86_64 \ + 2> "$workdir/oversized.stderr") +actual_status=$? +set -e +assert_status 2 "$actual_status" 'oversized translator response' +assert_output "$failed_expected" "$actual" 'oversized translator response' +install_query_translator "$query_bin" latx-x86_64 \ + "$(runtime_info_json x86_64 /runtime/64 default)" + +set +e +"$query_bin/latu-runtime-manager" current --abi amd64 \ + > "$workdir/abi.stdout" 2> "$workdir/abi.stderr" +abi_status=$? +set -e +assert_status 2 "$abi_status" 'unknown guest ABI' + +echo 'PASS: distribution-neutral runtime queries' From 8711ce7c0bcc086f4ba345051afe9430b8c402b0 Mon Sep 17 00:00:00 2001 From: Hanlu Li Date: Sun, 9 Aug 2026 07:14:23 +0800 Subject: [PATCH 3/6] runtime: inspect guest runtime roots Add inspect-root for read-only validation of the canonical x86_64 and i386 loader paths. A valid loader must be a readable regular ELF of the expected class and machine with a loadable segment. Resolve symlink chains without accepting paths that escape the inspected root, and report stable ready, missing, invalid, or unknown results with machine-readable exit statuses. Signed-off-by: Hanlu Li --- runtime/latu-runtime-manager.c | 377 ++++++++++++++++++++- tests/runtime/test-latu-runtime-manager.sh | 197 ++++++++++- 2 files changed, 572 insertions(+), 2 deletions(-) diff --git a/runtime/latu-runtime-manager.c b/runtime/latu-runtime-manager.c index ed39eccf5e6..43d09793873 100644 --- a/runtime/latu-runtime-manager.c +++ b/runtime/latu-runtime-manager.c @@ -197,7 +197,9 @@ static void usage(void) "usage: latu-runtime-manager status [--root ROOT]\n" " latu-runtime-manager current [--abi ABI]" " [--program PROGRAM]\n" - " latu-runtime-manager list [--program PROGRAM]\n"); + " latu-runtime-manager list [--program PROGRAM]\n" + " latu-runtime-manager inspect-root [--abi ABI]" + " [--] ROOT\n"); exit(2); } @@ -1185,6 +1187,375 @@ static bool guest_abi_valid(const char *guest_abi) return !strcmp(guest_abi, "x86_64") || !strcmp(guest_abi, "i386"); } +static const char *default_loader_path(const char *guest_abi) +{ + return !strcmp(guest_abi, "x86_64") ? + "/lib64/ld-linux-x86-64.so.2" : "/lib/ld-linux.so.2"; +} + +static uint16_t read_le16(const unsigned char *value) +{ + return value[0] | ((uint16_t)value[1] << 8); +} + +static uint32_t read_le32(const unsigned char *value) +{ + return value[0] | ((uint32_t)value[1] << 8) | + ((uint32_t)value[2] << 16) | ((uint32_t)value[3] << 24); +} + +static uint64_t read_le64(const unsigned char *value) +{ + return read_le32(value) | ((uint64_t)read_le32(value + 4) << 32); +} + +static bool loader_elf_valid(int fd, const char *guest_abi) +{ + unsigned char header[64]; + uint64_t program_offset; + uint16_t program_entry_size; + uint16_t program_count; + uint16_t machine; + size_t header_size; + unsigned char expected_class; + bool have_load = false; + ssize_t length; + struct stat statbuf; + + do { + length = pread(fd, header, sizeof(header), 0); + } while (length < 0 && errno == EINTR); + + if (!strcmp(guest_abi, "x86_64")) { + header_size = 64; + expected_class = 2; + } else { + header_size = 52; + expected_class = 1; + } + if (length < 0 || (size_t)length < header_size || + memcmp(header, "\177ELF", 4) || header[4] != expected_class || + header[5] != 1 || header[6] != 1) { + return false; + } + machine = read_le16(header + 18); + if ((read_le16(header + 16) != 2 && read_le16(header + 16) != 3) || + (expected_class == 2 ? machine != 62 : + (machine != 3 && machine != 6))) { + return false; + } + + if (expected_class == 2) { + program_offset = read_le64(header + 32); + program_entry_size = read_le16(header + 54); + program_count = read_le16(header + 56); + if (read_le16(header + 52) != 64 || program_entry_size != 56) { + return false; + } + } else { + program_offset = read_le32(header + 28); + program_entry_size = read_le16(header + 42); + program_count = read_le16(header + 44); + if (read_le16(header + 40) != 52 || program_entry_size != 32) { + return false; + } + } + if (!program_count || program_offset > INT64_MAX || + fstat(fd, &statbuf) < 0 || statbuf.st_size < 0 || + program_offset > (uint64_t)statbuf.st_size || + program_count > ((uint64_t)statbuf.st_size - program_offset) / + program_entry_size) { + return false; + } + + for (uint16_t i = 0; i < program_count; i++) { + unsigned char program_type[4]; + uint64_t offset = program_offset + + (uint64_t)i * program_entry_size; + + if (offset > INT64_MAX) { + return false; + } + do { + length = pread(fd, program_type, sizeof(program_type), offset); + } while (length < 0 && errno == EINTR); + if (length != sizeof(program_type)) { + return false; + } + if (read_le32(program_type) == 1) { + have_load = true; + } + } + return have_load; +} + +static LoaderCheck inspect_loader_file(const char *guest_abi, + const char *path, + const char *confinement_root) +{ + LoaderCheck check = { + .status = INSPECTION_UNKNOWN, + .reason = "loader_uninspectable", + }; + struct stat statbuf; + bool entry_exists; + int resolve_errno; + int fd; + + check.resolved_path = realpath(path, NULL); + if (!check.resolved_path) { + resolve_errno = errno; + entry_exists = path_exists_without_following(path); + if (!entry_exists && + (resolve_errno == ENOENT || resolve_errno == ENOTDIR)) { + check.status = INSPECTION_MISSING; + check.reason = "loader_not_found"; + } else if (entry_exists && + (resolve_errno == ENOENT || resolve_errno == ENOTDIR)) { + check.status = INSPECTION_INVALID; + check.reason = "loader_symlink_broken"; + } + return check; + } + if (!utf8_valid(check.resolved_path)) { + g_free(check.resolved_path); + check.resolved_path = NULL; + check.reason = "loader_path_not_utf8"; + return check; + } + if (confinement_root && + !path_is_within(confinement_root, check.resolved_path)) { + check.reason = "loader_escapes_root"; + return check; + } + + fd = open(check.resolved_path, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + check.status = errno == EACCES ? + INSPECTION_INVALID : INSPECTION_UNKNOWN; + check.reason = errno == EACCES ? + "loader_permission_denied" : "loader_uninspectable"; + return check; + } + if (fstat(fd, &statbuf) < 0) { + close(fd); + return check; + } + if (!S_ISREG(statbuf.st_mode)) { + close(fd); + check.status = INSPECTION_INVALID; + check.reason = "loader_not_regular"; + return check; + } + if (!loader_elf_valid(fd, guest_abi)) { + close(fd); + check.status = INSPECTION_INVALID; + check.reason = "loader_invalid_elf"; + return check; + } + close(fd); + check.status = INSPECTION_READY; + check.reason = "ready"; + return check; +} + +static RuntimeInspection inspect_runtime_root(const char *guest_abi, + const char *input_root, + const char *loader_path) +{ + RuntimeInspection inspection = { + .guest_abi = guest_abi, + .runtime_root = input_root, + .loader_path = loader_path, + .status = INSPECTION_UNKNOWN, + .reason = "root_uninspectable", + .effective_status = INSPECTION_UNKNOWN, + .effective_reason = "not_checked", + }; + g_autofree char *configured_root = NULL; + const char *relative_loader = loader_path[0] == '/' ? + loader_path + 1 : loader_path; + LoaderCheck check; + struct stat statbuf; + + if (!input_root[0]) { + inspection.status = INSPECTION_MISSING; + inspection.reason = "runtime_root_empty"; + return inspection; + } + configured_root = absolute_path(input_root); + inspection.configured_loader = loader_path[0] == '/' ? + path_join(configured_root, + relative_loader) : + xstrdup(loader_path); + inspection.configured_loader_selected = + loader_path[0] == '/' && + access(inspection.configured_loader, F_OK) == 0; + + inspection.canonical_root = realpath(input_root, NULL); + if (!inspection.canonical_root) { + if (errno == ENOENT || errno == ENOTDIR) { + inspection.status = INSPECTION_MISSING; + inspection.reason = "root_not_found"; + } + return inspection; + } + if (!utf8_valid(inspection.canonical_root)) { + g_free(inspection.canonical_root); + inspection.canonical_root = NULL; + inspection.status = INSPECTION_UNKNOWN; + inspection.reason = "root_path_not_utf8"; + return inspection; + } + inspection.runtime_root = inspection.canonical_root; + if (stat(inspection.canonical_root, &statbuf) < 0) { + return inspection; + } + if (!S_ISDIR(statbuf.st_mode)) { + inspection.status = INSPECTION_INVALID; + inspection.reason = "root_not_directory"; + return inspection; + } + + check = inspect_loader_file(guest_abi, inspection.configured_loader, + inspection.canonical_root); + inspection.resolved_loader = check.resolved_path; + inspection.status = check.status; + inspection.reason = check.reason; + return inspection; +} + +static const char *inspection_status_name(InspectionStatus status) +{ + switch (status) { + case INSPECTION_READY: + return "ready"; + case INSPECTION_NOT_REQUIRED: + return "not_required"; + case INSPECTION_MISSING: + return "missing"; + case INSPECTION_INVALID: + return "invalid"; + case INSPECTION_UNKNOWN: + return "unknown"; + } + abort(); +} + +static int inspection_exit_status(const RuntimeInspection *inspection) +{ + switch (inspection->status) { + case INSPECTION_READY: + case INSPECTION_NOT_REQUIRED: + return 0; + case INSPECTION_MISSING: + case INSPECTION_INVALID: + return 1; + case INSPECTION_UNKNOWN: + return 2; + } + abort(); +} + +static void print_runtime_inspection(const RuntimeInspection *inspection) +{ + fputs("{\"schema_version\":1,\"guest_abi\":", stdout); + json_write_string(inspection->guest_abi); + fputs(",\"runtime_root\":", stdout); + if (inspection->runtime_root) { + json_write_string(inspection->runtime_root); + } else { + fputs("null", stdout); + } + fputs(",\"loader_path\":", stdout); + json_write_string(inspection->loader_path); + fputs(",\"resolved_loader\":", stdout); + if (inspection->resolved_loader) { + json_write_string(inspection->resolved_loader); + } else { + fputs("null", stdout); + } + fputs(",\"inspection_status\":", stdout); + json_write_string(inspection_status_name(inspection->status)); + fputs(",\"reason\":", stdout); + json_write_string(inspection->reason); + fputs("}\n", stdout); +} + +static void runtime_inspection_free(RuntimeInspection *inspection) +{ + g_free(inspection->canonical_root); + g_free(inspection->configured_loader); + g_free(inspection->resolved_loader); + g_free(inspection->effective_loader); + memset(inspection, 0, sizeof(*inspection)); +} + +static int inspect_root_command(int argc, char **argv) +{ + const char *guest_abi = NULL; + const char *root = NULL; + int status = 0; + + for (int i = 0; i < argc; ) { + if (!strcmp(argv[i], "--abi") && i + 1 < argc && !guest_abi) { + guest_abi = argv[i + 1]; + i += 2; + } else if (!strcmp(argv[i], "--") && !root && i + 2 == argc) { + root = argv[i + 1]; + i += 2; + } else if (argv[i][0] != '-' && !root) { + root = argv[i++]; + } else { + usage(); + } + } + if (!root || !root[0] || (guest_abi && !guest_abi_valid(guest_abi))) { + usage(); + } + if (!utf8_valid(root)) { + report_error("runtime root is not valid UTF-8"); + for (int i = 0; i < 2; i++) { + const char *abi = i ? "i386" : "x86_64"; + RuntimeInspection inspection = { + .guest_abi = abi, + .runtime_root = NULL, + .loader_path = i ? "/lib/ld-linux.so.2" : + "/lib64/ld-linux-x86-64.so.2", + .status = INSPECTION_UNKNOWN, + .reason = "root_path_not_utf8", + }; + + if (!guest_abi || !strcmp(guest_abi, abi)) { + print_runtime_inspection(&inspection); + } + } + return 2; + } + + if (!guest_abi || !strcmp(guest_abi, "x86_64")) { + RuntimeInspection inspection = inspect_runtime_root( + "x86_64", root, default_loader_path("x86_64")); + + print_runtime_inspection(&inspection); + status = inspection_exit_status(&inspection); + runtime_inspection_free(&inspection); + } + if (!guest_abi || !strcmp(guest_abi, "i386")) { + RuntimeInspection inspection = inspect_runtime_root( + "i386", root, default_loader_path("i386")); + int i386_status; + + print_runtime_inspection(&inspection); + i386_status = inspection_exit_status(&inspection); + if (i386_status > status) { + status = i386_status; + } + runtime_inspection_free(&inspection); + } + return status; +} + static int query_both_runtimes(const TranslatorLocation *location, const char *program) { @@ -1264,6 +1635,10 @@ int main(int argc, char **argv) program_name = argv[0]; } + if (argc >= 2 && !strcmp(argv[1], "inspect-root")) { + return inspect_root_command(argc - 2, argv + 2); + } + if (argc >= 2 && (!strcmp(argv[1], "current") || !strcmp(argv[1], "list"))) { if (!locate_sibling_translators(argv[0], &location)) { diff --git a/tests/runtime/test-latu-runtime-manager.sh b/tests/runtime/test-latu-runtime-manager.sh index 3c363df8d48..5018f5b7606 100755 --- a/tests/runtime/test-latu-runtime-manager.sh +++ b/tests/runtime/test-latu-runtime-manager.sh @@ -545,4 +545,199 @@ abi_status=$? set -e assert_status 2 "$abi_status" 'unknown guest ABI' -echo 'PASS: distribution-neutral runtime queries' +runtime_root=$workdir/runtime-both +make_loader "$runtime_root" x86_64 +make_loader "$runtime_root" i386 +set +e +actual=$("$query_bin/latu-runtime-manager" inspect-root "$runtime_root" \ + 2> "$workdir/inspect.stderr") +actual_status=$? +set -e +assert_status 0 "$actual_status" 'dual-ABI runtime root' +assert_contains '"guest_abi":"x86_64"' "$actual" \ + 'dual-ABI x86-64 inspection' +assert_contains '"guest_abi":"i386"' "$actual" \ + 'dual-ABI i386 inspection' +ready_count=$(printf '%s\n' "$actual" | \ + grep -c '"inspection_status":"ready","reason":"ready"') +assert_status 2 "$ready_count" 'dual-ABI ready records' + +set +e +actual=$("$query_bin/latu-runtime-manager" inspect-root --abi x86_64 \ + "$runtime_root" 2> "$workdir/inspect-one.stderr") +actual_status=$? +set -e +assert_status 0 "$actual_status" 'single-ABI runtime root' +assert_contains '"guest_abi":"x86_64"' "$actual" \ + 'single-ABI x86-64 inspection' +case "$actual" in + *'"guest_abi":"i386"'*) fail 'single-ABI inspection reported i386' ;; +esac + +missing_root=$workdir/runtime-missing +mkdir -p "$missing_root" +set +e +actual=$("$query_bin/latu-runtime-manager" inspect-root --abi i386 \ + "$missing_root" 2> "$workdir/inspect-missing.stderr") +actual_status=$? +set -e +assert_status 1 "$actual_status" 'missing runtime loader' +assert_contains '"inspection_status":"missing","reason":"loader_not_found"' \ + "$actual" 'missing runtime loader' + +invalid_root=$workdir/runtime-invalid +make_loader "$invalid_root" i386 +mkdir -p "$invalid_root/lib64" +cp "$invalid_root/lib/ld-linux.so.2" \ + "$invalid_root/lib64/ld-linux-x86-64.so.2" +set +e +actual=$("$query_bin/latu-runtime-manager" inspect-root --abi x86_64 \ + "$invalid_root" 2> "$workdir/inspect-invalid.stderr") +actual_status=$? +set -e +assert_status 1 "$actual_status" 'wrong loader ABI' +assert_contains '"inspection_status":"invalid","reason":"loader_invalid_elf"' \ + "$actual" 'wrong loader ABI' + +truncated_root=$workdir/runtime-truncated +mkdir -p "$truncated_root/lib64" +printf '\177ELF\002\001\001' > \ + "$truncated_root/lib64/ld-linux-x86-64.so.2" +set +e +actual=$("$query_bin/latu-runtime-manager" inspect-root --abi x86_64 \ + "$truncated_root" 2> "$workdir/inspect-truncated.stderr") +actual_status=$? +set -e +assert_status 1 "$actual_status" 'truncated loader' +assert_contains '"inspection_status":"invalid","reason":"loader_invalid_elf"' \ + "$actual" 'truncated loader' + +bad_ehsize_root=$workdir/runtime-bad-ehsize +make_loader "$bad_ehsize_root" x86_64 +python3 - "$bad_ehsize_root/lib64/ld-linux-x86-64.so.2" <<'PY' +import sys + +with open(sys.argv[1], "r+b") as loader: + loader.seek(52) + loader.write(b"\0\0") +PY +set +e +actual=$("$query_bin/latu-runtime-manager" inspect-root --abi x86_64 \ + "$bad_ehsize_root" 2> "$workdir/inspect-ehsize.stderr") +actual_status=$? +set -e +assert_status 1 "$actual_status" 'invalid ELF header size' +assert_contains '"inspection_status":"invalid","reason":"loader_invalid_elf"' \ + "$actual" 'invalid ELF header size' + +bad_phentsize_root=$workdir/runtime-bad-phentsize +make_loader "$bad_phentsize_root" x86_64 +python3 - "$bad_phentsize_root/lib64/ld-linux-x86-64.so.2" <<'PY' +import sys + +with open(sys.argv[1], "r+b") as loader: + loader.seek(54) + loader.write(b"\x40\0") +PY +set +e +actual=$("$query_bin/latu-runtime-manager" inspect-root --abi x86_64 \ + "$bad_phentsize_root" 2> "$workdir/inspect-phentsize.stderr") +actual_status=$? +set -e +assert_status 1 "$actual_status" 'invalid ELF program header size' +assert_contains '"inspection_status":"invalid","reason":"loader_invalid_elf"' \ + "$actual" 'invalid ELF program header size' + +em486_root=$workdir/runtime-em486 +make_loader "$em486_root" i386 +python3 - "$em486_root/lib/ld-linux.so.2" <<'PY' +import sys + +with open(sys.argv[1], "r+b") as loader: + loader.seek(18) + loader.write(b"\x06\0") +PY +set +e +actual=$("$query_bin/latu-runtime-manager" inspect-root --abi i386 \ + "$em486_root" 2> "$workdir/inspect-em486.stderr") +actual_status=$? +set -e +assert_status 0 "$actual_status" 'EM_486 loader' +assert_contains '"inspection_status":"ready","reason":"ready"' \ + "$actual" 'EM_486 loader' + +symlink_root=$workdir/runtime-symlink +mkdir -p "$symlink_root/usr/lib/runtime" "$symlink_root/lib64" +make_loader "$workdir/symlink-loader" x86_64 +cp "$workdir/symlink-loader/lib64/ld-linux-x86-64.so.2" \ + "$symlink_root/usr/lib/runtime/loader.so" +ln -s ../usr/lib/runtime/loader.so \ + "$symlink_root/lib64/ld-linux-x86-64.so.2" +set +e +actual=$("$query_bin/latu-runtime-manager" inspect-root --abi x86_64 \ + "$symlink_root" 2> "$workdir/inspect-symlink.stderr") +actual_status=$? +set -e +assert_status 0 "$actual_status" 'root-confined loader symlink' +assert_contains '"inspection_status":"ready","reason":"ready"' \ + "$actual" 'root-confined loader symlink' + +escape_root=$workdir/runtime-escape +outside_loader=$workdir/outside-loader +make_loader "$outside_loader" x86_64 +mkdir -p "$escape_root/lib64" +ln -s "$outside_loader/lib64/ld-linux-x86-64.so.2" \ + "$escape_root/lib64/ld-linux-x86-64.so.2" +set +e +actual=$("$query_bin/latu-runtime-manager" inspect-root --abi x86_64 \ + "$escape_root" 2> "$workdir/inspect-escape.stderr") +actual_status=$? +set -e +assert_status 2 "$actual_status" 'escaping loader symlink' +assert_contains '"inspection_status":"unknown","reason":"loader_escapes_root"' \ + "$actual" 'escaping loader symlink' + +broken_root=$workdir/runtime-broken-link +mkdir -p "$broken_root/lib" +ln -s nowhere "$broken_root/lib/ld-linux.so.2" +set +e +actual=$("$query_bin/latu-runtime-manager" inspect-root --abi i386 \ + "$broken_root" 2> "$workdir/inspect-broken.stderr") +actual_status=$? +set -e +assert_status 1 "$actual_status" 'broken loader symlink' +assert_contains \ + '"inspection_status":"invalid",'\ +'"reason":"loader_symlink_broken"' \ + "$actual" 'broken loader symlink' + +directory_root=$workdir/runtime-directory-loader +mkdir -p "$directory_root/lib/ld-linux.so.2" +set +e +actual=$("$query_bin/latu-runtime-manager" inspect-root --abi i386 \ + "$directory_root" 2> "$workdir/inspect-directory.stderr") +actual_status=$? +set -e +assert_status 1 "$actual_status" 'directory loader' +assert_contains '"inspection_status":"invalid","reason":"loader_not_regular"' \ + "$actual" 'directory loader' + +set +e +actual=$("$query_bin/latu-runtime-manager" inspect-root --abi x86_64 \ + "$workdir/no-such-runtime" 2> "$workdir/inspect-no-root.stderr") +actual_status=$? +set -e +assert_status 1 "$actual_status" 'missing runtime root' +assert_contains '"inspection_status":"missing","reason":"root_not_found"' \ + "$actual" 'missing runtime root' + +dash_root=$workdir/-runtime +make_loader "$dash_root" x86_64 +( + cd "$workdir" + "$query_bin/latu-runtime-manager" inspect-root --abi x86_64 -- -runtime +) > "$workdir/inspect-dash.stdout" || fail 'dash-prefixed root failed' +assert_contains '"inspection_status":"ready","reason":"ready"' \ + "$(cat "$workdir/inspect-dash.stdout")" 'dash-prefixed root' + +echo 'PASS: distribution-neutral runtime inspection' From 9b90a5bd89cc6cf01e53163c2b3721c4c00006e2 Mon Sep 17 00:00:00 2001 From: Hanlu Li Date: Sun, 9 Aug 2026 07:14:28 +0800 Subject: [PATCH 4/6] runtime: diagnose loaders for guest programs Add doctor for the complete translator, runtime selection, and loader chain. With --program, inspect the guest ELF to select its ABI and PT_INTERP; static guests require no loader. Report configured-root and effective loader status separately. Match LAT lookup behavior by accepting an absolute host loader only when the configured copy is absent, while an existing invalid configured loader remains a failure. When the requested program cannot be inspected, emit a complete unknown NDJSON record with a stable program_reason before returning exit status 2. Signed-off-by: Hanlu Li --- runtime/latu-runtime-manager.c | 478 ++++++++++++++++++++- tests/runtime/test-latu-runtime-manager.sh | 247 ++++++++++- 2 files changed, 721 insertions(+), 4 deletions(-) diff --git a/runtime/latu-runtime-manager.c b/runtime/latu-runtime-manager.c index 43d09793873..aef3417baa4 100644 --- a/runtime/latu-runtime-manager.c +++ b/runtime/latu-runtime-manager.c @@ -199,7 +199,9 @@ static void usage(void) " [--program PROGRAM]\n" " latu-runtime-manager list [--program PROGRAM]\n" " latu-runtime-manager inspect-root [--abi ABI]" - " [--] ROOT\n"); + " [--] ROOT\n" + " latu-runtime-manager doctor [--abi ABI]" + " [--program PROGRAM]\n"); exit(2); } @@ -1289,6 +1291,144 @@ static bool loader_elf_valid(int fd, const char *guest_abi) return have_load; } +static bool pread_exact(int fd, void *buffer, size_t size, uint64_t offset) +{ + ssize_t length; + + if (offset > INT64_MAX) { + return false; + } + do { + length = pread(fd, buffer, size, offset); + } while (length < 0 && errno == EINTR); + return length == size; +} + +static bool inspect_program(const char *path, ProgramInfo *program, + const char **reason) +{ + unsigned char header[64]; + struct stat statbuf; + uint64_t program_offset; + uint16_t program_entry_size; + uint16_t program_count; + uint16_t machine; + unsigned char elf_class; + bool have_load = false; + int fd; + + memset(program, 0, sizeof(*program)); + program->path = path; + fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + *reason = errno == ENOENT || errno == ENOTDIR ? + "program_not_found" : "program_uninspectable"; + return false; + } + if (fstat(fd, &statbuf) < 0 || !S_ISREG(statbuf.st_mode) || + statbuf.st_size < 0 || + !pread_exact(fd, header, sizeof(header), 0) || + memcmp(header, "\177ELF", 4) || header[5] != 1 || header[6] != 1) { + *reason = "program_invalid_elf"; + close(fd); + return false; + } + + elf_class = header[4]; + machine = read_le16(header + 18); + if (elf_class == 2 && machine == 62) { + program->guest_abi = "x86_64"; + program_offset = read_le64(header + 32); + program_entry_size = read_le16(header + 54); + program_count = read_le16(header + 56); + if (read_le16(header + 52) != 64 || program_entry_size != 56) { + goto invalid; + } + } else if (elf_class == 1 && (machine == 3 || machine == 6)) { + program->guest_abi = "i386"; + program_offset = read_le32(header + 28); + program_entry_size = read_le16(header + 42); + program_count = read_le16(header + 44); + if (read_le16(header + 40) != 52 || program_entry_size != 32) { + goto invalid; + } + } else { + goto invalid; + } + + if ((read_le16(header + 16) != 2 && read_le16(header + 16) != 3) || + !program_count || program_offset > (uint64_t)statbuf.st_size || + program_count > ((uint64_t)statbuf.st_size - program_offset) / + program_entry_size) { + goto invalid; + } + + for (uint16_t i = 0; i < program_count; i++) { + unsigned char program_header[56]; + uint64_t offset = program_offset + + (uint64_t)i * program_entry_size; + uint32_t type; + + if (!pread_exact(fd, program_header, program_entry_size, offset)) { + goto invalid; + } + type = read_le32(program_header); + if (type == 1) { + have_load = true; + } else if (type == 3) { + uint64_t interpreter_offset; + uint64_t interpreter_size; + char *interpreter; + + if (program->interpreter) { + goto invalid; + } + if (elf_class == 2) { + interpreter_offset = read_le64(program_header + 8); + interpreter_size = read_le64(program_header + 32); + } else { + interpreter_offset = read_le32(program_header + 4); + interpreter_size = read_le32(program_header + 16); + } + if (interpreter_size < 2 || interpreter_size > PATH_MAX || + interpreter_offset > (uint64_t)statbuf.st_size || + interpreter_size > + (uint64_t)statbuf.st_size - interpreter_offset) { + goto invalid; + } + interpreter = xmalloc(interpreter_size); + if (!pread_exact(fd, interpreter, interpreter_size, + interpreter_offset) || + interpreter[interpreter_size - 1] != '\0' || + memchr(interpreter, '\0', interpreter_size - 1)) { + g_free(interpreter); + goto invalid; + } + program->interpreter = interpreter; + } + } + close(fd); + if (!have_load) { + goto invalid_closed; + } + program->is_static = !program->interpreter; + return true; + +invalid: + close(fd); +invalid_closed: + g_free(program->interpreter); + memset(program, 0, sizeof(*program)); + *reason = "program_invalid_elf"; + return false; +} + +static void program_info_free(ProgramInfo *program) +{ + g_free(program->interpreter); + memset(program, 0, sizeof(*program)); +} + static LoaderCheck inspect_loader_file(const char *guest_abi, const char *path, const char *confinement_root) @@ -1425,6 +1565,34 @@ static RuntimeInspection inspect_runtime_root(const char *guest_abi, return inspection; } +static void resolve_effective_loader(RuntimeInspection *inspection) +{ + LoaderCheck check; + + if (inspection->status == INSPECTION_NOT_REQUIRED) { + inspection->effective_status = INSPECTION_NOT_REQUIRED; + inspection->effective_reason = "static_program"; + return; + } + if (inspection->configured_loader_selected) { + inspection->loader_source = LOADER_SOURCE_RUNTIME_ROOT; + inspection->effective_status = inspection->status; + inspection->effective_reason = inspection->reason; + inspection->effective_loader = + xstrdup(inspection->resolved_loader ? + inspection->resolved_loader : + inspection->configured_loader); + return; + } + + inspection->loader_source = LOADER_SOURCE_HOST_FALLBACK; + check = inspect_loader_file(inspection->guest_abi, + inspection->loader_path, NULL); + inspection->effective_status = check.status; + inspection->effective_reason = check.reason; + inspection->effective_loader = check.resolved_path; +} + static const char *inspection_status_name(InspectionStatus status) { switch (status) { @@ -1556,6 +1724,305 @@ static int inspect_root_command(int argc, char **argv) return status; } +static const char *readiness_status_name(ReadinessStatus status) +{ + switch (status) { + case READINESS_READY: + return "ready"; + case READINESS_READY_WITH_HOST_FALLBACK: + return "ready_with_host_fallback"; + case READINESS_UNAVAILABLE: + return "unavailable"; + case READINESS_BROKEN: + return "broken"; + case READINESS_UNKNOWN: + return "unknown"; + } + abort(); +} + +static const char *loader_source_name(LoaderSource source) +{ + switch (source) { + case LOADER_SOURCE_NONE: + return "none"; + case LOADER_SOURCE_RUNTIME_ROOT: + return "runtime_root"; + case LOADER_SOURCE_HOST_FALLBACK: + return "host_fallback"; + } + abort(); +} + +static ReadinessStatus doctor_readiness(const RuntimeQuery *query, + const RuntimeInspection *inspection) +{ + switch (query->query_status) { + case QUERY_TRANSLATOR_MISSING: + return READINESS_UNAVAILABLE; + case QUERY_TRANSLATOR_UNKNOWN: + case QUERY_TRANSLATOR_FAILED: + case QUERY_INVALID_OUTPUT: + return READINESS_UNKNOWN; + case QUERY_SELECTED: + break; + } + + switch (inspection->effective_status) { + case INSPECTION_READY: + return inspection->loader_source == LOADER_SOURCE_HOST_FALLBACK ? + READINESS_READY_WITH_HOST_FALLBACK : READINESS_READY; + case INSPECTION_NOT_REQUIRED: + return READINESS_READY; + case INSPECTION_MISSING: + case INSPECTION_INVALID: + return READINESS_BROKEN; + case INSPECTION_UNKNOWN: + return READINESS_UNKNOWN; + } + abort(); +} + +static void print_doctor_result(const RuntimeQuery *query, + const RuntimeInspection *inspection, + ReadinessStatus readiness, + const ProgramInfo *program) +{ + const char *loader_path = inspection ? inspection->loader_path : + default_loader_path(query->guest_abi); + const char *reason; + + if (query->query_status != QUERY_SELECTED) { + reason = query_status_name(query->query_status); + } else if (inspection->status == INSPECTION_NOT_REQUIRED) { + reason = "static_program"; + } else if (readiness == READINESS_READY_WITH_HOST_FALLBACK) { + reason = "host_fallback"; + } else { + reason = inspection->effective_reason; + } + + fputs("{\"schema_version\":1,\"guest_abi\":", stdout); + json_write_string(query->guest_abi); + fputs(",\"translator_status\":", stdout); + json_write_string(translator_status_name(query->translator_status)); + fputs(",\"query_status\":", stdout); + json_write_string(query_status_name(query->query_status)); + fputs(",\"runtime_root\":", stdout); + if (query->query_status == QUERY_SELECTED) { + json_write_string(query->info.runtime_root); + } else { + fputs("null", stdout); + } + fputs(",\"runtime_source\":", stdout); + if (query->query_status == QUERY_SELECTED) { + json_write_string(query->info.runtime_source); + } else { + fputs("null", stdout); + } + fputs(",\"loader_path\":", stdout); + if (loader_path) { + json_write_string(loader_path); + } else { + fputs("null", stdout); + } + fputs(",\"configured_loader\":", stdout); + if (inspection && inspection->configured_loader) { + json_write_string(inspection->configured_loader); + } else { + fputs("null", stdout); + } + fputs(",\"resolved_loader\":", stdout); + if (inspection && inspection->resolved_loader) { + json_write_string(inspection->resolved_loader); + } else { + fputs("null", stdout); + } + fputs(",\"inspection_status\":", stdout); + if (inspection) { + json_write_string(inspection_status_name(inspection->status)); + } else { + json_write_string("not_checked"); + } + fputs(",\"readiness\":", stdout); + json_write_string(readiness_status_name(readiness)); + fputs(",\"reason\":", stdout); + json_write_string(reason); + fputs(",\"runtime_root_status\":", stdout); + if (inspection) { + json_write_string(inspection_status_name(inspection->status)); + } else { + json_write_string("not_checked"); + } + fputs(",\"runtime_root_reason\":", stdout); + if (inspection) { + json_write_string(inspection->reason); + } else { + json_write_string("not_checked"); + } + fputs(",\"effective_loader\":", stdout); + if (inspection && inspection->effective_loader) { + json_write_string(inspection->effective_loader); + } else { + fputs("null", stdout); + } + fputs(",\"effective_loader_status\":", stdout); + if (inspection) { + json_write_string(inspection_status_name( + inspection->effective_status)); + } else { + json_write_string("not_checked"); + } + fputs(",\"loader_source\":", stdout); + json_write_string(inspection ? + loader_source_name(inspection->loader_source) : "none"); + fputs(",\"program\":", stdout); + if (program) { + json_write_string(program->path); + } else { + fputs("null", stdout); + } + fputs(",\"program_status\":", stdout); + json_write_string(program ? + (program->is_static ? "static" : "dynamic") : + "not_checked"); + fputs(",\"program_reason\":null", stdout); + fputs("}\n", stdout); +} + +static void print_program_failure(const char *program, const char *reason) +{ + fputs("{\"schema_version\":1,\"guest_abi\":null," + "\"translator_status\":\"not_checked\"," + "\"query_status\":\"not_checked\",\"runtime_root\":null," + "\"runtime_source\":null,\"loader_path\":null," + "\"configured_loader\":null,\"resolved_loader\":null," + "\"inspection_status\":\"not_checked\"," + "\"readiness\":\"unknown\",\"reason\":", stdout); + json_write_string(reason); + fputs(",\"runtime_root_status\":\"not_checked\"," + "\"runtime_root_reason\":\"not_checked\"," + "\"effective_loader\":null," + "\"effective_loader_status\":\"not_checked\"," + "\"loader_source\":\"none\",\"program\":", stdout); + json_write_string(program); + fputs(",\"program_status\":\"invalid\",\"program_reason\":", stdout); + json_write_string(reason); + fputs("}\n", stdout); +} + +static ReadinessStatus doctor_one(const TranslatorLocation *location, + const char *guest_abi, + const ProgramInfo *program) +{ + RuntimeQuery query = query_runtime(location, guest_abi, + program ? program->path : NULL); + RuntimeInspection inspection; + RuntimeInspection *inspection_ptr = NULL; + ReadinessStatus readiness; + + if (query.query_status == QUERY_SELECTED) { + if (program && program->is_static) { + inspection = (RuntimeInspection) { + .guest_abi = guest_abi, + .runtime_root = query.info.runtime_root, + .status = INSPECTION_NOT_REQUIRED, + .reason = "static_program", + .effective_status = INSPECTION_NOT_REQUIRED, + .effective_reason = "static_program", + }; + } else { + inspection = inspect_runtime_root( + guest_abi, query.info.runtime_root, + program ? program->interpreter : + default_loader_path(guest_abi)); + resolve_effective_loader(&inspection); + } + inspection_ptr = &inspection; + } + readiness = doctor_readiness(&query, inspection_ptr); + print_doctor_result(&query, inspection_ptr, readiness, program); + + if (inspection_ptr) { + runtime_inspection_free(inspection_ptr); + } + runtime_info_free(&query.info); + return readiness; +} + +static int doctor_command(const TranslatorLocation *location, int argc, + char **argv) +{ + const char *guest_abi = NULL; + const char *program_path = NULL; + const char *program_reason = NULL; + ProgramInfo program = { 0 }; + ProgramInfo *program_ptr = NULL; + bool any_ready = false; + bool any_broken = false; + bool any_unknown = false; + int status; + + for (int i = 0; i < argc; ) { + if (!strcmp(argv[i], "--abi") && i + 1 < argc && !guest_abi) { + guest_abi = argv[i + 1]; + } else if (!strcmp(argv[i], "--program") && i + 1 < argc && + !program_path && argv[i + 1][0]) { + program_path = argv[i + 1]; + } else { + usage(); + } + i += 2; + } + if (guest_abi && !guest_abi_valid(guest_abi)) { + usage(); + } + if (program_path) { + if (!inspect_program(program_path, &program, &program_reason)) { + print_program_failure(program_path, program_reason); + report_error("cannot inspect program '%s': %s", + program_path, program_reason); + return 2; + } + program_ptr = &program; + if (guest_abi && strcmp(guest_abi, program.guest_abi)) { + report_error("program ABI %s does not match requested ABI %s", + program.guest_abi, guest_abi); + program_info_free(&program); + return 2; + } + guest_abi = program.guest_abi; + } + + for (int i = 0; i < 2; i++) { + const char *abi = i ? "i386" : "x86_64"; + ReadinessStatus readiness; + + if (guest_abi && strcmp(guest_abi, abi)) { + continue; + } + readiness = doctor_one(location, abi, program_ptr); + switch (readiness) { + case READINESS_READY: + case READINESS_READY_WITH_HOST_FALLBACK: + any_ready = true; + break; + case READINESS_UNAVAILABLE: + break; + case READINESS_BROKEN: + any_broken = true; + break; + case READINESS_UNKNOWN: + any_unknown = true; + break; + } + } + + status = any_unknown ? 2 : any_broken || !any_ready ? 1 : 0; + program_info_free(&program); + return status; +} + static int query_both_runtimes(const TranslatorLocation *location, const char *program) { @@ -1640,14 +2107,18 @@ int main(int argc, char **argv) } if (argc >= 2 && - (!strcmp(argv[1], "current") || !strcmp(argv[1], "list"))) { + (!strcmp(argv[1], "current") || !strcmp(argv[1], "list") || + !strcmp(argv[1], "doctor"))) { if (!locate_sibling_translators(argv[0], &location)) { return 2; } + if (!strcmp(argv[1], "current")) { status = current_command(&location, argc - 2, argv + 2); - } else { + } else if (!strcmp(argv[1], "list")) { status = list_command(&location, argc - 2, argv + 2); + } else { + status = doctor_command(&location, argc - 2, argv + 2); } g_free(location.directory); g_free(location.root); @@ -1662,6 +2133,7 @@ int main(int argc, char **argv) } else { usage(); } + if (!located) { return 2; } diff --git a/tests/runtime/test-latu-runtime-manager.sh b/tests/runtime/test-latu-runtime-manager.sh index 5018f5b7606..4d1e7435cc1 100755 --- a/tests/runtime/test-latu-runtime-manager.sh +++ b/tests/runtime/test-latu-runtime-manager.sh @@ -740,4 +740,249 @@ make_loader "$dash_root" x86_64 assert_contains '"inspection_status":"ready","reason":"ready"' \ "$(cat "$workdir/inspect-dash.stdout")" 'dash-prefixed root' -echo 'PASS: distribution-neutral runtime inspection' +printf '%s\n' \ + "$(runtime_info_json x86_64 "$runtime_root" user_config)" \ + > "$query_bin/latx-x86_64.json" +printf '%s\n' \ + "$(runtime_info_json i386 "$runtime_root" system_config)" \ + > "$query_bin/latx-i386.json" +set +e +actual=$("$query_bin/latu-runtime-manager" doctor \ + 2> "$workdir/doctor.stderr") +actual_status=$? +set -e +assert_status 0 "$actual_status" 'healthy dual-ABI doctor' +doctor_ready_count=$(printf '%s\n' "$actual" | \ + grep -c '"inspection_status":"ready","readiness":"ready","reason":"ready"') +assert_status 2 "$doctor_ready_count" 'healthy dual-ABI doctor records' + +mv "$query_bin/latx-i386" "$query_bin/latx-i386.absent" +set +e +actual=$("$query_bin/latu-runtime-manager" doctor \ + 2> "$workdir/doctor-partial.stderr") +actual_status=$? +set -e +assert_status 0 "$actual_status" 'one ready ABI doctor' +assert_contains '"guest_abi":"x86_64"' "$actual" \ + 'one ready ABI doctor' +assert_contains \ + '"guest_abi":"i386","translator_status":"missing",'\ +'"query_status":"translator_missing"' \ + "$actual" 'unavailable i386 doctor' +assert_contains \ + '"inspection_status":"not_checked","readiness":"unavailable",'\ +'"reason":"translator_missing"' \ + "$actual" 'unavailable i386 doctor' +mv "$query_bin/latx-i386.absent" "$query_bin/latx-i386" + +printf '%s\n' \ + "$(runtime_info_json x86_64 "$invalid_root" environment)" \ + > "$query_bin/latx-x86_64.json" +set +e +actual=$("$query_bin/latu-runtime-manager" doctor \ + 2> "$workdir/doctor-broken.stderr") +actual_status=$? +set -e +assert_status 1 "$actual_status" 'broken runtime doctor' +assert_contains '"guest_abi":"x86_64"' "$actual" \ + 'broken runtime doctor' +assert_contains \ + '"inspection_status":"invalid","readiness":"broken",'\ +'"reason":"loader_invalid_elf"' \ + "$actual" 'broken runtime doctor' +assert_contains '"guest_abi":"i386"' "$actual" \ + 'healthy peer runtime doctor' + +printf '%s\n' '{"schema_version":1,"guest_abi":"x86_64"}' \ + > "$query_bin/latx-x86_64.json" +set +e +actual=$("$query_bin/latu-runtime-manager" doctor \ + 2> "$workdir/doctor-unknown.stderr") +actual_status=$? +set -e +assert_status 2 "$actual_status" 'invalid runtime-info doctor' +assert_contains '"query_status":"invalid_runtime_info"' "$actual" \ + 'invalid runtime-info doctor' +assert_contains \ + '"inspection_status":"not_checked","readiness":"unknown",'\ +'"reason":"invalid_runtime_info"' \ + "$actual" 'invalid runtime-info doctor' + +mv "$query_bin/latx-x86_64" "$query_bin/latx-x86_64.absent" +mv "$query_bin/latx-i386" "$query_bin/latx-i386.absent" +set +e +actual=$("$query_bin/latu-runtime-manager" doctor \ + 2> "$workdir/doctor-unavailable.stderr") +actual_status=$? +set -e +assert_status 1 "$actual_status" 'no translator doctor' +unavailable_count=$(printf '%s\n' "$actual" | \ + grep -c '"readiness":"unavailable","reason":"translator_missing"') +assert_status 2 "$unavailable_count" 'no translator doctor records' +mv "$query_bin/latx-x86_64.absent" "$query_bin/latx-x86_64" +mv "$query_bin/latx-i386.absent" "$query_bin/latx-i386" + +printf '%s\n' \ + "$(runtime_info_json x86_64 "$runtime_root" default)" \ + > "$query_bin/latx-x86_64.json" +set +e +actual=$("$query_bin/latu-runtime-manager" doctor --abi x86_64 \ + 2> "$workdir/doctor-one.stderr") +actual_status=$? +set -e +assert_status 0 "$actual_status" 'single-ABI doctor' +assert_contains '"guest_abi":"x86_64"' "$actual" 'single-ABI doctor' +case "$actual" in + *'"guest_abi":"i386"'*) fail 'single-ABI doctor reported i386' ;; +esac + +program_dir=$workdir/programs +x86_64_program=$program_dir/x86_64-dynamic +make_program "$x86_64_program" x86_64 /lib64/ld-linux-x86-64.so.2 +LATU_TEST_ARGV_LOG=$workdir/doctor-program-argv \ + "$query_bin/latu-runtime-manager" doctor --program "$x86_64_program" \ + > "$workdir/doctor-program.stdout" || + fail 'program doctor did not infer x86-64 ABI' +actual=$(cat "$workdir/doctor-program.stdout") +assert_contains '"guest_abi":"x86_64"' "$actual" \ + 'program doctor inferred x86-64 ABI' +assert_contains '"loader_path":"/lib64/ld-linux-x86-64.so.2"' "$actual" \ + 'program doctor used PT_INTERP' +case "$actual" in + *'"guest_abi":"i386"'*) fail 'x86-64 program doctor queried i386' ;; +esac +[ ! -e "$workdir/doctor-program-argv.i386" ] || + fail 'x86-64 program doctor executed i386 translator' + +i386_program=$program_dir/i386-dynamic +make_program "$i386_program" i386 /lib/ld-linux.so.2 +LATU_TEST_ARGV_LOG=$workdir/doctor-i386-argv \ + "$query_bin/latu-runtime-manager" doctor --program "$i386_program" \ + > "$workdir/doctor-i386.stdout" || + fail 'program doctor did not infer i386 ABI' +actual=$(cat "$workdir/doctor-i386.stdout") +assert_contains '"guest_abi":"i386"' "$actual" \ + 'program doctor inferred i386 ABI' +assert_contains '"loader_path":"/lib/ld-linux.so.2"' "$actual" \ + 'i386 program doctor used PT_INTERP' +case "$actual" in + *'"guest_abi":"x86_64"'*) fail 'i386 program doctor queried x86-64' ;; +esac +[ ! -e "$workdir/doctor-i386-argv.x86_64" ] || + fail 'i386 program doctor executed x86-64 translator' + +set +e +"$query_bin/latu-runtime-manager" doctor --abi x86_64 \ + --program "$i386_program" > "$workdir/doctor-mismatch.stdout" \ + 2> "$workdir/doctor-mismatch.stderr" +actual_status=$? +set -e +assert_status 2 "$actual_status" 'program and requested ABI mismatch' +grep -F 'does not match requested ABI' "$workdir/doctor-mismatch.stderr" \ + > /dev/null || fail 'program ABI mismatch diagnostic missing' + +custom_root=$workdir/runtime-custom-loader +custom_interp=/opt/latu/lib/ld-custom-x86-64.so.1 +make_loader "$workdir/custom-loader-source" x86_64 +mkdir -p "$custom_root${custom_interp%/*}" +cp "$workdir/custom-loader-source/lib64/ld-linux-x86-64.so.2" \ + "$custom_root$custom_interp" +custom_program=$program_dir/x86_64-custom-interp +make_program "$custom_program" x86_64 "$custom_interp" +printf '%s\n' \ + "$(runtime_info_json x86_64 "$custom_root" user_config)" \ + > "$query_bin/latx-x86_64.json" +actual=$("$query_bin/latu-runtime-manager" doctor \ + --program "$custom_program") || fail 'custom PT_INTERP doctor failed' +assert_contains '"loader_path":"/opt/latu/lib/ld-custom-x86-64.so.1"' \ + "$actual" 'custom PT_INTERP path' +assert_contains '"inspection_status":"ready","readiness":"ready"' \ + "$actual" 'custom PT_INTERP readiness' + +host_loader_root=$workdir/host-loader-source +make_loader "$host_loader_root" x86_64 +host_loader=$host_loader_root/lib64/ld-linux-x86-64.so.2 +fallback_program=$program_dir/x86_64-host-fallback +make_program "$fallback_program" x86_64 "$host_loader" +fallback_root=$workdir/runtime-without-configured-loader +mkdir -p "$fallback_root" +printf '%s\n' \ + "$(runtime_info_json x86_64 "$fallback_root" environment)" \ + > "$query_bin/latx-x86_64.json" +actual=$("$query_bin/latu-runtime-manager" doctor \ + --program "$fallback_program") || fail 'host loader fallback failed' +assert_contains '"inspection_status":"missing"' "$actual" \ + 'host fallback root status' +assert_contains '"readiness":"ready_with_host_fallback"' "$actual" \ + 'host fallback readiness' +assert_contains '"effective_loader_status":"ready"' "$actual" \ + 'host fallback effective loader status' +assert_contains '"loader_source":"host_fallback"' "$actual" \ + 'host fallback source' + +configured_fallback=$fallback_root${host_loader} +mkdir -p "${configured_fallback%/*}" +cp "$workdir/custom-loader-source/lib64/ld-linux-x86-64.so.2" \ + "$configured_fallback" +python3 - "$configured_fallback" <<'PY' +import sys + +with open(sys.argv[1], "r+b") as loader: + loader.seek(18) + loader.write(b"\x03\0") +PY +set +e +actual=$("$query_bin/latu-runtime-manager" doctor \ + --program "$fallback_program" 2> "$workdir/no-fallback.stderr") +actual_status=$? +set -e +assert_status 1 "$actual_status" 'existing invalid root loader' +assert_contains '"effective_loader_status":"invalid"' "$actual" \ + 'invalid configured loader blocks fallback' +assert_contains '"loader_source":"runtime_root"' "$actual" \ + 'invalid configured loader source' + +static_program=$program_dir/x86_64-static +make_program "$static_program" x86_64 +printf '%s\n' \ + "$(runtime_info_json x86_64 "$workdir/no-such-static-runtime" default)" \ + > "$query_bin/latx-x86_64.json" +actual=$("$query_bin/latu-runtime-manager" doctor \ + --program "$static_program") || fail 'static program doctor failed' +assert_contains '"loader_path":null' "$actual" \ + 'static program has no loader' +assert_contains '"inspection_status":"not_required",'\ +'"readiness":"ready","reason":"static_program"' "$actual" \ + 'static program readiness' + +printf 'not an ELF\n' > "$program_dir/invalid" +set +e +"$query_bin/latu-runtime-manager" doctor --program "$program_dir/invalid" \ + > "$workdir/doctor-invalid.stdout" \ + 2> "$workdir/doctor-invalid.stderr" +actual_status=$? +set -e +assert_status 2 "$actual_status" 'invalid program ELF' +grep -F 'program_invalid_elf' "$workdir/doctor-invalid.stderr" \ + > /dev/null || fail 'invalid program diagnostic missing' +actual=$(cat "$workdir/doctor-invalid.stdout") +assert_contains '"guest_abi":null' "$actual" \ + 'invalid program ABI is unknown' +assert_contains '"program_status":"invalid",'\ +'"program_reason":"program_invalid_elf"' "$actual" \ + 'invalid program structured diagnosis' + +set +e +"$query_bin/latu-runtime-manager" doctor \ + --program "$program_dir/does-not-exist" \ + > "$workdir/doctor-missing-program.stdout" \ + 2> "$workdir/doctor-missing-program.stderr" +actual_status=$? +set -e +assert_status 2 "$actual_status" 'missing program ELF' +actual=$(cat "$workdir/doctor-missing-program.stdout") +assert_contains '"program_status":"invalid",'\ +'"program_reason":"program_not_found"' "$actual" \ + 'missing program structured diagnosis' + +echo 'PASS: distribution-neutral runtime queries and diagnosis' From 5c6c9fe98254091b6ed54db92710385e1f840c15 Mon Sep 17 00:00:00 2001 From: Hanlu Li Date: Sun, 9 Aug 2026 07:14:33 +0800 Subject: [PATCH 5/6] tests: exercise diagnostics with real translators Register target-dependent diagnostics in the top-level Meson build and run them through the generic lat-pr-fast suite. The test invokes both built translators for runtime-info queries and manager orchestration. Use synthetic loader ELF files and skip guest execution in nested CI, keeping the gate deterministic and independent of a preinstalled x86 rootfs. Signed-off-by: Hanlu Li --- meson.build | 15 ++ .../runtime/test-latu-runtime-manager-real.sh | 156 ++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100755 tests/runtime/test-latu-runtime-manager-real.sh diff --git a/meson.build b/meson.build index 0e49ed19dac..8e475beb79e 100644 --- a/meson.build +++ b/meson.build @@ -777,6 +777,21 @@ subdir('docs') subdir('runtime') if get_option('tests').enabled() and host_machine.system() == 'linux' + if 'latx-x86_64' in emulators and 'latx-i386' in emulators + test( + 'test-latu-runtime-manager-real', + find_program('tests/runtime/test-latu-runtime-manager-real.sh'), + args: [python.full_path(), latu_runtime_manager.full_path(), + emulators['latx-x86_64'].full_path(), + emulators['latx-i386'].full_path()], + depends: [latu_runtime_manager, emulators['latx-x86_64'], + emulators['latx-i386']], + protocol: 'exitcode', + suite: 'lat-pr-fast', + timeout: 300, + ) + endif + latu_install_depends = [latu_runtime_manager] foreach name, emulator : emulators latu_install_depends += emulator diff --git a/tests/runtime/test-latu-runtime-manager-real.sh b/tests/runtime/test-latu-runtime-manager-real.sh new file mode 100755 index 00000000000..aedf8c5252f --- /dev/null +++ b/tests/runtime/test-latu-runtime-manager-real.sh @@ -0,0 +1,156 @@ +#!/bin/sh +set -eu + +[ "$#" -eq 4 ] || { + echo "usage: $0 PYTHON MANAGER LATX_X86_64 LATX_I386" >&2 + exit 2 +} + +python=$1 +manager=$("$python" -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' \ + "$2") +latx_x86_64=$("$python" -c \ + 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$3") +latx_i386=$("$python" -c \ + 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$4") +workdir=$(mktemp -d) +trap 'rm -rf "$workdir"' EXIT HUP INT TERM + +fail() +{ + echo "FAIL: $*" >&2 + exit 1 +} + +make_loader() +{ + loader_root=$1 + loader_abi=$2 + case "$loader_abi" in + x86_64) loader_path=$loader_root/lib64/ld-linux-x86-64.so.2 ;; + i386) loader_path=$loader_root/lib/ld-linux.so.2 ;; + *) fail "unsupported loader ABI: $loader_abi" ;; + esac + mkdir -p "${loader_path%/*}" + "$python" - "$loader_abi" "$loader_path" <<'PY' +import struct +import sys + +abi, path = sys.argv[1:] +if abi == "x86_64": + ident = b"\x7fELF" + bytes([2, 1, 1, 0]) + bytes(8) + ehdr = struct.pack( + "<16sHHIQQQIHHHHHH", ident, 3, 62, 1, 0, 64, 0, 0, + 64, 56, 1, 0, 0, 0) + phdr = struct.pack(" "$home/.config/latx-x86_64.conf" < "$home/.config/latx-i386.conf" < "$workdir/current-x86_64.json" || + fail 'real x86-64 current query failed' +"$python" - "$workdir/current-x86_64.json" "$runtime_x86_64" <<'PY' +import json +import sys + +with open(sys.argv[1], encoding="utf-8") as source: + record = json.load(source) +assert record["guest_abi"] == "x86_64", record +assert record["query_status"] == "selected", record +assert record["runtime_root"] == sys.argv[2], record +assert record["runtime_source"] == "user_config", record +PY + +( + unset LAT_LD_PREFIX XDG_CONFIG_HOME + HOME=$home + export HOME + "$bin/latu-runtime-manager" current --abi i386 --program /usr/bin/app +) > "$workdir/current-i386-app.json" || + fail 'real i386 program query failed' +"$python" - "$workdir/current-i386-app.json" "$app_i386" <<'PY' +import json +import sys + +with open(sys.argv[1], encoding="utf-8") as source: + record = json.load(source) +assert record["guest_abi"] == "i386", record +assert record["query_status"] == "selected", record +assert record["runtime_root"] == sys.argv[2], record +assert record["runtime_source"] == "user_config", record +PY + +( + unset LAT_LD_PREFIX XDG_CONFIG_HOME + HOME=$home + export HOME + "$bin/latu-runtime-manager" list +) > "$workdir/list.jsonl" || fail 'real dual-ABI list failed' +"$python" - "$workdir/list.jsonl" \ + "$runtime_x86_64" "$runtime_i386" <<'PY' +import json +import sys + +with open(sys.argv[1], encoding="utf-8") as source: + records = [json.loads(line) for line in source] +assert [item["guest_abi"] for item in records] == ["x86_64", "i386"] +assert [item["runtime_root"] for item in records] == sys.argv[2:] +assert all(item["query_status"] == "selected" for item in records) +PY + +( + unset LAT_LD_PREFIX XDG_CONFIG_HOME + HOME=$home + export HOME + "$bin/latu-runtime-manager" doctor +) > "$workdir/doctor.jsonl" || fail 'real dual-ABI doctor failed' +"$python" - "$workdir/doctor.jsonl" <<'PY' +import json +import sys + +with open(sys.argv[1], encoding="utf-8") as source: + records = [json.loads(line) for line in source] +assert [item["guest_abi"] for item in records] == ["x86_64", "i386"] +assert all(item["query_status"] == "selected" for item in records) +assert all(item["inspection_status"] == "ready" for item in records) +assert all(item["readiness"] == "ready" for item in records) +PY + +echo 'PASS: real dual-ABI translator runtime diagnostics' From cb63f58d1d8ebb4ccb0c1641a4736ac881d8dc1c Mon Sep 17 00:00:00 2001 From: Hanlu Li Date: Sun, 9 Aug 2026 07:15:27 +0800 Subject: [PATCH 6/6] docs: describe read-only runtime diagnostics Document status, current, list, inspect-root, and doctor, including the default dual-ABI view and optional ABI filter. Clarify the read-only boundary, structured output and exit statuses, program ELF handling, and the distinction between a self-contained runtime root and LAT host-loader fallback. Signed-off-by: Hanlu Li --- docs/tools/index.rst | 1 + docs/tools/latu-runtime-manager.rst | 140 ++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 docs/tools/latu-runtime-manager.rst diff --git a/docs/tools/index.rst b/docs/tools/index.rst index d923834a739..63e9ef3c6be 100644 --- a/docs/tools/index.rst +++ b/docs/tools/index.rst @@ -11,6 +11,7 @@ Contents: :maxdepth: 2 qemu-img + latu-runtime-manager qemu-storage-daemon qemu-nbd qemu-pr-helper diff --git a/docs/tools/latu-runtime-manager.rst b/docs/tools/latu-runtime-manager.rst new file mode 100644 index 00000000000..26daaf3d83d --- /dev/null +++ b/docs/tools/latu-runtime-manager.rst @@ -0,0 +1,140 @@ +LATU guest runtime diagnostics +============================== + +Synopsis +-------- + +| **latu-runtime-manager status** [**--root** *HOST_ROOT*] +| **latu-runtime-manager current** [**--abi** *ABI*] + [**--program** *PROGRAM*] +| **latu-runtime-manager list** [**--program** *PROGRAM*] +| **latu-runtime-manager inspect-root** [**--abi** *ABI*] [**--**] *ROOT* +| **latu-runtime-manager doctor** [**--abi** *ABI*] + [**--program** *PROGRAM*] + +Description +----------- + +``latu-runtime-manager`` reports whether the LATX translators and their +selected x86 guest runtimes are usable. It is a read-only diagnostic tool: it +does not download a runtime, change LATX configuration, register ``binfmt``, +or start a guest program. + +The supported guest ABI names are ``x86_64`` and ``i386``. The default +commands report both ABIs, so users do not need to choose one for routine +diagnosis. ``--abi`` is an optional filter for scripts and targeted +troubleshooting. The manager still tracks the ABIs independently because one +translator may be absent and existing LATX configurations may select different +runtime roots. A single root containing both loader trees remains the simplest +layout when both translators are used. + +Except for ``status``, command output is newline-delimited JSON with +``schema_version`` set to 1. Paths are JSON strings, so spaces, control +characters, and non-ASCII characters cannot change the record boundaries. + +Commands +-------- + +``status`` + Report whether ``latx-x86_64`` and ``latx-i386`` are present beside the + manager. The output is the stable two-line interface introduced with the + manager:: + + translator_x86_64=present + translator_i386=present + + ``--root`` inspects ``HOST_ROOT/usr/bin`` without executing a translator. + This is an offline host installation check; it does not inspect a guest + runtime root. + +``current`` + Ask the translators for their effective ``LAT_LD_PREFIX`` and the source + that selected it. By default both ABIs are reported; ``--abi`` limits the + query to one. Without ``--program``, only global configuration is applied. + With ``--program``, each translator also applies a matching per-program + configuration section. This command treats *PROGRAM* as a configuration + key and does not open or execute it. The manager consumes the translator's + versioned ``--runtime-info`` contract instead of reimplementing LATX + configuration precedence. + +``list`` + Query both translators and print one query result per ABI. This reports the + runtime roots currently selected by the installed translators; it does not + scan the filesystem or claim to enumerate every rootfs stored on the + machine. A later runtime store can add discoverable installed runtimes + without changing the meaning of these query results. + +``inspect-root`` + Inspect an explicit guest runtime root. Without ``--abi``, both ABI loader + paths are checked. The canonical loader paths are + ``/lib64/ld-linux-x86-64.so.2`` for ``x86_64`` and + ``/lib/ld-linux.so.2`` for ``i386``. + + A ready loader must resolve inside the runtime root, be a regular readable + ELF file with the expected class and machine, and contain a loadable + segment. A symlink that resolves outside the runtime root is reported as + ``unknown`` instead of being followed as a host file. Use ``--`` before a + root whose name begins with ``-``. + + This self-contained-runtime check is intentionally stricter than LAT's host + path fallback: a loader found outside the selected root is never accepted as + proof that the root itself is ready. + +``doctor`` + Perform the complete read-only chain: find the translator, query its + effective runtime selection, and inspect the loader LAT would select. + Without ``--abi`` or ``--program``, both ABIs are checked independently. + + With ``--program``, the manager opens the ELF file without executing it, + reads its ABI and ``PT_INTERP``, and checks only the matching translator. If + ``--abi`` is also present, it must match the file. A static ELF needs no + interpreter and is reported ready once its translator query succeeds. If + the file cannot be inspected, the manager returns an ``unknown`` record with + ``guest_abi`` set to null and a stable ``program_reason`` before exiting. + + For a dynamic ELF, ``runtime_root_status`` describes the configured copy of + its actual ``PT_INTERP`` path. ``effective_loader_status`` and ``readiness`` + describe the loader LAT would use: the configured-root copy when it exists, + otherwise LAT's absolute host-path fallback. An existing but invalid + configured loader blocks that fallback. The ``readiness`` field is one of: + + ``ready`` + The translator selected a runtime with a valid loader. + + ``ready_with_host_fallback`` + The configured root lacks the requested loader, but LAT's host-path + fallback resolves to a valid loader. This is executable according to the + current lookup rules, but is not a self-contained runtime root. + + ``unavailable`` + This ABI's translator is not installed. When the other ABI is ready this + does not make the overall diagnosis fail. + + ``broken`` + The selection was obtained, but the root or loader is missing or invalid. + + ``unknown`` + The manager could not safely complete the query, for example because the + translator returned an unsupported runtime-information contract or a + loader escaped the root. + +Exit status +----------- + +``status`` keeps its compatibility behavior and returns zero after a +successful inspection even when a translator is absent. The JSON query +commands use the following statuses: + +``0`` + The requested query completed and is healthy. For default ``current`` and + ``list`` queries, at least one ABI is selected and any other ABI is merely + unavailable. For the default dual-ABI ``doctor``, at least one ABI is ready + and any other ABI is merely unavailable. ``inspect-root`` remains a check + of every ABI requested, so use ``--abi`` for an intentionally single-ABI + root. + +``1`` + The result is known but unavailable, missing, or invalid. + +``2`` + The command line is invalid or the result cannot be determined safely.