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/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. 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..aef3417baa4 --- /dev/null +++ b/runtime/latu-runtime-manager.c @@ -0,0 +1,2151 @@ +/* + * 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" + " latu-runtime-manager current [--abi ABI]" + " [--program PROGRAM]\n" + " latu-runtime-manager list [--program PROGRAM]\n" + " latu-runtime-manager inspect-root [--abi ABI]" + " [--] ROOT\n" + " latu-runtime-manager doctor [--abi ABI]" + " [--program PROGRAM]\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; +} + +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 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 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) +{ + 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 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) { + 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 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) +{ + 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 = false; + int 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], "inspect-root")) { + return inspect_root_command(argc - 2, argv + 2); + } + + if (argc >= 2 && + (!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 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); + return status; + } + + 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; + } + + 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); + 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-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' diff --git a/tests/runtime/test-latu-runtime-manager.sh b/tests/runtime/test-latu-runtime-manager.sh index eb859be5191..4d1e7435cc1 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 @@ -14,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 @@ -55,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' + +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' + +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'