commit ae5a02a4ae3f15832cae5b972d6b928320f26623
parent c5899fea4182a2cc597568712047d0b29cf5002c
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 17 Jun 2026 08:00:17 -0700
test: add ELF DSO harness, aarch64 all-clang lane (DYNLIB.md milestone 1)
test/link/elf-dso-aa64/ is the shared-library test fixture described in
doc/plan/DYNLIB.md §10. It has three planned lanes (only lane A is
implemented now; B/C follow as kit ld -shared and kit cc -fPIC land):
A — all-clang: clang -fPIC + lld -shared produce libfoo.so; a freestanding
consumer links against it. Structural checks via readelf assert ET_DYN,
e_entry 0, no PT_INTERP, PT_DYNAMIC present, DT_SONAME, exported symbols
(foo_add / foo_counter / foo_get_counter), foo_hidden absent from .dynsym,
bar_external present as UND, and correct consumer DT_NEEDED / RPATH.
Runtime: exec_dso_podman runs the consumer in alpine (musl) and
debian:bookworm-slim (glibc) arm64 containers; all 14 checks pass.
The fixture uses -nostdlib to avoid needing a cross sysroot, and freestanding
_start + aarch64 SVC for exit so the consumer needs no libc at compile time.
RPATH $ORIGIN makes libfoo.so findable at runtime from within the container's
/work mount without any LD_LIBRARY_PATH plumbing.
mk/test.mk: add test-link-dso-aa64 target (opt-in, not in DEFAULT_TEST_TARGETS
since it requires clang aarch64 cross + ld.lld + provisioned podman images).
Diffstat:
4 files changed, 476 insertions(+), 0 deletions(-)
diff --git a/mk/test.mk b/mk/test.mk
@@ -105,6 +105,7 @@ TEST_TARGETS = \
test-link-reloc-apply \
test-link-jit-tls-relax \
test-link-coff-weak-alias \
+ test-link-dso-aa64 \
test-macho \
test-native-direct-target \
test-opt \
@@ -903,6 +904,16 @@ test-link: lib $(ROUNDTRIP_BIN) $(ROUNDTRIP_BIN_MACHO) $(LINK_EXE_RUNNER) $(JIT_
test-link-x64: lib rt-x86_64-linux $(ROUNDTRIP_BIN) $(LINK_EXE_RUNNER) $(JIT_RUNNER)
@KIT_TEST_ARCH=x64 KIT_TEST_PATHS=RE bash test/link/run.sh
+# test-link-dso-aa64: ELF shared-library (DSO) harness targeting aarch64-linux.
+# Lane A (all-clang) uses clang + lld to produce libfoo.so and a freestanding
+# consumer, runs readelf structural checks, and executes the consumer inside
+# podman containers (alpine/musl and debian/glibc). No kit linker or codegen
+# is involved at lane A; lanes B (kit ld -shared) and C (all-kit) follow as the
+# kit DSO implementation lands. Opt-in: requires clang aarch64 cross + ld.lld +
+# readelf + provisioned podman images (make test-images) for runtime checks.
+test-link-dso-aa64:
+ bash test/link/elf-dso-aa64/run.sh
+
test-macho: lib $(TEST_RT_DEP) $(ROUNDTRIP_BIN_MACHO) $(LINK_EXE_RUNNER) $(JIT_RUNNER)
KIT_TEST_OBJ=macho \
KIT_TEST_ARCH=$${KIT_TEST_ARCH:-aa64} \
diff --git a/test/link/elf-dso-aa64/consumer.c b/test/link/elf-dso-aa64/consumer.c
@@ -0,0 +1,39 @@
+/* test/link/elf-dso-aa64/consumer.c — DSO consumer; freestanding aarch64.
+ *
+ * Dynamically linked against libfoo.so. Provides bar_external so that
+ * libfoo.so's undefined import is satisfied at runtime via the executable's
+ * exported symbol table (requires --export-dynamic / -rdynamic at link time).
+ *
+ * Uses a direct SVC for exit so the binary needs no libc and no sysroot at
+ * compile time; only the dynamic linker and libfoo.so are needed at runtime.
+ *
+ * Exit codes:
+ * 0 all checks passed
+ * 1 foo_add(10, 32) != 42
+ * 2 foo_get_counter() != 42
+ */
+
+extern int foo_add(int a, int b);
+extern int foo_get_counter(void);
+
+/* Satisfies libfoo.so's undefined import of bar_external. Exported to the
+ * runtime dynamic symbol table by --export-dynamic on the consumer link. */
+int bar_external(int x) { (void)x; return 0; }
+
+static __attribute__((noreturn)) void do_exit(int code)
+{
+ /* aarch64 Linux: x8 = NR_exit_group (94), x0 = exit code */
+ register long x8 __asm__("x8") = 94;
+ register long x0 __asm__("x0") = (long)code;
+ __asm__ volatile("svc 0" : : "r"(x8), "r"(x0) : "memory");
+ __builtin_unreachable();
+}
+
+void _start(void)
+{
+ /* foo_add(10, 32) = 10 + 32 + bar_external(0) = 10 + 32 + 0 = 42 */
+ if (foo_add(10, 32) != 42) do_exit(1);
+ /* foo_get_counter() reads foo_counter which was initialised to 42 */
+ if (foo_get_counter() != 42) do_exit(2);
+ do_exit(0);
+}
diff --git a/test/link/elf-dso-aa64/lib_foo.c b/test/link/elf-dso-aa64/lib_foo.c
@@ -0,0 +1,33 @@
+/* test/link/elf-dso-aa64/lib_foo.c — shared-library source fixture.
+ *
+ * Exported (appear in .dynsym as GLOBAL DEFAULT defined):
+ * foo_add — function; calls bar_external (undefined import)
+ * foo_get_counter — function returning foo_counter
+ * foo_counter — data, initial value 42
+ *
+ * Hidden (must NOT appear in .dynsym as a defined exported symbol):
+ * foo_hidden — function with __attribute__((visibility("hidden")))
+ *
+ * Undefined import (appears in .dynsym as GLOBAL DEFAULT UND):
+ * bar_external — called by foo_add; left unresolved at DSO link time
+ */
+
+extern int bar_external(int x);
+
+int foo_counter = 42;
+
+int foo_add(int a, int b)
+{
+ return a + b + bar_external(0);
+}
+
+int foo_get_counter(void)
+{
+ return foo_counter;
+}
+
+__attribute__((visibility("hidden")))
+int foo_hidden(int x)
+{
+ return x * 2;
+}
diff --git a/test/link/elf-dso-aa64/run.sh b/test/link/elf-dso-aa64/run.sh
@@ -0,0 +1,393 @@
+#!/usr/bin/env bash
+# test/link/elf-dso-aa64/run.sh — ELF shared-library harness, aarch64 target.
+#
+# Three lanes (only lane A is implemented now; B and C follow after the kit
+# linker and codegen support lands):
+#
+# A — all-clang baseline: clang compiles PIC objects, clang (via lld) links
+# the DSO and consumer, readelf structural checks, podman runtime exec.
+# This lane proves the test fixture, sysroot, container, and readelf
+# checks are correct before any kit linker or codegen work is in scope.
+#
+# B — kit ld -shared: clang still compiles PIC objects but kit ld produces
+# the DSO. Isolates ELF DSO production from kit codegen. (NOT YET)
+#
+# C — all-kit: kit cc -fPIC compiles the objects, kit ld -shared links.
+# Validates kit-generated PIC code and DSO interposition choices. (NOT YET)
+#
+# Prerequisites (checked at runtime; missing prereqs produce SKIP, not FAIL):
+# clang — with --target=aarch64-linux-gnu cross-compilation support
+# lld — ld.lld on PATH (needed for ELF shared library link from macOS)
+# readelf — llvm-readelf or GNU readelf (structural checks)
+# podman — for runtime execution inside an aarch64 container
+# images: alpine (musl) and/or debian:bookworm-slim (glibc)
+#
+# Usage:
+# bash test/link/elf-dso-aa64/run.sh # all structural + runtime
+# KIT_TEST_ALLOW_SKIP=1 bash ... # allow skips (CI-friendly)
+# KIT_LANE=A bash ... # explicit lane filter (default A)
+#
+# Output: structured pass/fail via kit_sh_report.sh; exit 0 = all pass.
+
+set -u
+
+ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
+BUILD_DIR="$ROOT/build/test/link/elf-dso-aa64"
+mkdir -p "$BUILD_DIR"
+
+KIT_KIT_DIR="$ROOT/test/lib"
+. "$ROOT/test/lib/kit_sh_kit.sh"
+kit_report_init
+
+KIT_SKIP_IS_FAILURE="${KIT_SKIP_IS_FAILURE:-0}"
+
+CLANG_TARGET="--target=aarch64-linux-gnu"
+work="$BUILD_DIR"
+
+# ---- tool detection -----------------------------------------------------------
+
+have_clang_aa64=0
+if clang $CLANG_TARGET -c -x c - -o /dev/null < /dev/null 2>/dev/null; then
+ have_clang_aa64=1
+fi
+
+have_lld=0
+command -v ld.lld >/dev/null 2>&1 && have_lld=1
+
+have_readelf=0
+READELF_BIN=""
+if command -v llvm-readelf >/dev/null 2>&1; then
+ READELF_BIN="$(command -v llvm-readelf)"; have_readelf=1
+elif command -v readelf >/dev/null 2>&1; then
+ READELF_BIN="$(command -v readelf)"; have_readelf=1
+fi
+
+# exec_target.sh requirements
+have_qemu=0
+QEMU_BIN=""
+have_podman=0
+is_aarch64=0
+arch_raw="$(uname -m 2>/dev/null || true)"
+{ [ "$arch_raw" = "aarch64" ] || [ "$arch_raw" = "arm64" ]; } && is_aarch64=1
+command -v podman >/dev/null 2>&1 && have_podman=1
+
+EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR"
+export have_qemu QEMU_BIN have_podman is_aarch64 EXEC_TARGET_MOUNT_ROOT
+
+# shellcheck source=../../lib/exec_target.sh
+. "$ROOT/test/lib/exec_target.sh"
+
+# ---- exec_dso_podman ----------------------------------------------------------
+# Like exec_target_run but podman-only. Dynamically linked DSO consumers need a
+# full OS environment (dynamic linker + libc); qemu-user alone cannot satisfy
+# that without a sysroot. Both files (libfoo.so + consumer) must live in the
+# same directory; the consumer's RPATH $ORIGIN then resolves to /work inside the
+# container (exec_target_run mounts the directory as /work).
+#
+# Sets RUN_RC to the container exit code, or 127 when podman is unavailable /
+# the image is not provisioned.
+exec_dso_podman() {
+ local tag="$1" exe="$2" out="$3" err="$4"
+ RUN_RC=127
+ [ "${have_podman:-0}" -eq 1 ] || return 0
+ _exec_target_image_present "$tag" || return 0
+ local dir base platform image platform_flag=()
+ dir="$(cd "$(dirname "$exe")" && pwd)"
+ base="$(basename "$exe")"
+ platform="$(_exec_target_platform "$tag")"
+ image="$(_exec_target_image "$tag")"
+ if ! _exec_target_podman_native "$tag"; then
+ platform_flag=(--platform "$platform")
+ fi
+ # Mount the directory as /work so both the consumer and libfoo.so are
+ # accessible at the same relative paths ($ORIGIN expands to /work).
+ podman run --rm --pull=never \
+ ${platform_flag[@]+"${platform_flag[@]}"} \
+ --net=none \
+ -v "$dir":/work:Z -w /work \
+ "$image" "./$base" \
+ >"$out" 2>"$err"
+ RUN_RC=$?
+}
+
+# ---- lane A: all-clang --------------------------------------------------------
+#
+# Structural checks (require clang + lld + readelf):
+# A/build-lib compile lib_foo.c -fPIC and link libfoo.so
+# A/header-dyn ET_DYN, e_entry = 0
+# A/no-interp no PT_INTERP in program headers
+# A/has-dynamic PT_DYNAMIC present
+# A/soname DT_SONAME = libfoo.so
+# A/export-foo-add foo_add in .dynsym as GLOBAL defined
+# A/export-foo-ctr foo_counter in .dynsym as GLOBAL defined
+# A/export-foo-get foo_get_counter in .dynsym as GLOBAL defined
+# A/hidden-absent foo_hidden NOT in .dynsym as a GLOBAL defined symbol
+# A/import-bar bar_external in .dynsym as UND
+#
+# Runtime checks (require podman + provisioned image; SKIP if unavailable):
+# A/run-musl consumer-musl exits 0 inside alpine (musl) container
+# A/run-glibc consumer-glibc exits 0 inside debian (glibc) container
+
+LANE="${KIT_LANE:-A}"
+if [ "$LANE" != A ]; then
+ skip_test "A" "KIT_LANE=$LANE (skipping lane A)"
+ kit_summary test-link-dso-aa64; kit_exit
+fi
+
+# ---- A: prerequisites --------------------------------------------------------
+
+if [ $have_clang_aa64 -eq 0 ]; then
+ skip_test "A" "clang --target=aarch64-linux-gnu unavailable"
+ kit_summary test-link-dso-aa64; kit_exit
+fi
+if [ $have_lld -eq 0 ]; then
+ skip_test "A" "ld.lld unavailable (needed for ELF shared library link)"
+ kit_summary test-link-dso-aa64; kit_exit
+fi
+if [ $have_readelf -eq 0 ]; then
+ skip_test "A" "readelf / llvm-readelf unavailable (needed for structural checks)"
+ kit_summary test-link-dso-aa64; kit_exit
+fi
+
+LIBFOO_O="$work/lib_foo.o"
+LIBFOO_SO="$work/libfoo.so"
+CONSUMER_O="$work/consumer.o"
+CONSUMER_MUSL="$work/consumer-musl"
+CONSUMER_GLIBC="$work/consumer-glibc"
+
+SRC_DIR="$(cd "$(dirname "$0")" && pwd)"
+
+# ---- A/build-lib: compile lib_foo.c + link libfoo.so -------------------------
+
+if ! clang $CLANG_TARGET -fuse-ld=lld \
+ -O1 -fno-stack-protector -fPIC \
+ -c "$SRC_DIR/lib_foo.c" -o "$LIBFOO_O" \
+ >"$work/compile-lib.out" 2>"$work/compile-lib.err"; then
+ not_ok "A/build-lib" "$work/compile-lib.err"
+ kit_summary test-link-dso-aa64; kit_exit
+fi
+
+# -Wl,--allow-shlib-undefined: bar_external is intentionally unresolved in the
+# DSO; it will appear as UND in .dynsym and be satisfied at runtime by the
+# consumer. This is the default for -shared with GNU/LLVM linkers but we spell
+# it out to be explicit about the policy.
+if ! clang $CLANG_TARGET -fuse-ld=lld \
+ -shared -fPIC -nostdlib \
+ -Wl,--allow-shlib-undefined \
+ -Wl,-soname,libfoo.so \
+ "$LIBFOO_O" -o "$LIBFOO_SO" \
+ >"$work/link-lib.out" 2>"$work/link-lib.err"; then
+ not_ok "A/build-lib" "$work/link-lib.err"
+ kit_summary test-link-dso-aa64; kit_exit
+fi
+ok "A/build-lib"
+
+# ---- A: structural checks on libfoo.so ---------------------------------------
+
+# Dump ELF header, program headers, dynamic section, and dynamic symbol table
+# once each into per-check files; all grep/awk checks read those cached files.
+DUMP_HDR="$work/readelf-hdr.txt"
+DUMP_PHDR="$work/readelf-phdr.txt"
+DUMP_DYN="$work/readelf-dyn.txt"
+DUMP_DYNSYM="$work/readelf-dynsym.txt"
+
+"$READELF_BIN" -h "$LIBFOO_SO" >"$DUMP_HDR" 2>/dev/null
+"$READELF_BIN" -l "$LIBFOO_SO" >"$DUMP_PHDR" 2>/dev/null
+"$READELF_BIN" -d "$LIBFOO_SO" >"$DUMP_DYN" 2>/dev/null
+"$READELF_BIN" --dyn-syms "$LIBFOO_SO" >"$DUMP_DYNSYM" 2>/dev/null
+
+# A/header-dyn: ELF type must be DYN, entry address must be 0x0
+if grep -q "Type:.*DYN" "$DUMP_HDR" && \
+ grep -qE "Entry point address:\s+0x0\b" "$DUMP_HDR"; then
+ ok "A/header-dyn"
+else
+ printf 'FAIL: expected Type: DYN and entry 0x0\n' >"$work/A-header-dyn.diag"
+ grep "Type:\|Entry point" "$DUMP_HDR" >>"$work/A-header-dyn.diag"
+ not_ok "A/header-dyn" "$work/A-header-dyn.diag"
+fi
+
+# A/no-interp: PT_INTERP must be absent (DSOs are loaded by another program's
+# interpreter, not their own)
+if grep -q "INTERP" "$DUMP_PHDR"; then
+ printf 'FAIL: unexpected PT_INTERP in DSO program headers\n' >"$work/A-no-interp.diag"
+ grep "INTERP" "$DUMP_PHDR" >>"$work/A-no-interp.diag"
+ not_ok "A/no-interp" "$work/A-no-interp.diag"
+else
+ ok "A/no-interp"
+fi
+
+# A/has-dynamic: PT_DYNAMIC must be present
+if grep -q "DYNAMIC" "$DUMP_PHDR"; then
+ ok "A/has-dynamic"
+else
+ printf 'FAIL: PT_DYNAMIC absent from DSO program headers\n' >"$work/A-has-dynamic.diag"
+ cat "$DUMP_PHDR" >>"$work/A-has-dynamic.diag"
+ not_ok "A/has-dynamic" "$work/A-has-dynamic.diag"
+fi
+
+# A/soname: DT_SONAME must be present and match libfoo.so
+if grep -q "(SONAME)" "$DUMP_DYN" && grep -q "libfoo.so" "$DUMP_DYN"; then
+ ok "A/soname"
+else
+ printf 'FAIL: DT_SONAME absent or wrong\n' >"$work/A-soname.diag"
+ grep "SONAME\|soname\|libfoo" "$DUMP_DYN" >>"$work/A-soname.diag"
+ not_ok "A/soname" "$work/A-soname.diag"
+fi
+
+# A/export-foo-add: foo_add must be in .dynsym as a GLOBAL defined symbol
+if grep -q "GLOBAL\|WEAK" "$DUMP_DYNSYM" && grep "foo_add" "$DUMP_DYNSYM" | grep -qv "UND"; then
+ ok "A/export-foo-add"
+else
+ printf 'FAIL: foo_add not exported in .dynsym\n' >"$work/A-export-foo-add.diag"
+ grep "foo_add" "$DUMP_DYNSYM" >>"$work/A-export-foo-add.diag" || true
+ not_ok "A/export-foo-add" "$work/A-export-foo-add.diag"
+fi
+
+# A/export-foo-ctr: foo_counter must be in .dynsym as a GLOBAL defined symbol
+if grep "foo_counter" "$DUMP_DYNSYM" | grep -qv "UND"; then
+ ok "A/export-foo-ctr"
+else
+ printf 'FAIL: foo_counter not exported in .dynsym\n' >"$work/A-export-foo-ctr.diag"
+ grep "foo_counter" "$DUMP_DYNSYM" >>"$work/A-export-foo-ctr.diag" || true
+ not_ok "A/export-foo-ctr" "$work/A-export-foo-ctr.diag"
+fi
+
+# A/export-foo-get: foo_get_counter must be in .dynsym as a GLOBAL defined symbol
+if grep "foo_get_counter" "$DUMP_DYNSYM" | grep -qv "UND"; then
+ ok "A/export-foo-get"
+else
+ printf 'FAIL: foo_get_counter not exported in .dynsym\n' >"$work/A-export-foo-get.diag"
+ grep "foo_get_counter" "$DUMP_DYNSYM" >>"$work/A-export-foo-get.diag" || true
+ not_ok "A/export-foo-get" "$work/A-export-foo-get.diag"
+fi
+
+# A/hidden-absent: foo_hidden must NOT appear as a GLOBAL/WEAK defined symbol
+# in .dynsym. Hidden symbols stay in .symtab only; they must not be preemptible.
+# If it appears at all in .dynsym it must be STV_HIDDEN (Vis == HIDDEN) and not
+# GLOBAL-or-WEAK DEFAULT, so check that no GLOBAL DEFAULT line mentions it.
+if grep "foo_hidden" "$DUMP_DYNSYM" | grep -q "GLOBAL.*DEFAULT.*[0-9]"; then
+ printf 'FAIL: foo_hidden appears as GLOBAL DEFAULT in .dynsym\n' >"$work/A-hidden-absent.diag"
+ grep "foo_hidden" "$DUMP_DYNSYM" >>"$work/A-hidden-absent.diag"
+ not_ok "A/hidden-absent" "$work/A-hidden-absent.diag"
+else
+ ok "A/hidden-absent"
+fi
+
+# A/import-bar: bar_external must be in .dynsym as UND (undefined import)
+if grep "bar_external" "$DUMP_DYNSYM" | grep -q "UND"; then
+ ok "A/import-bar"
+else
+ printf 'FAIL: bar_external not in .dynsym as UND\n' >"$work/A-import-bar.diag"
+ grep "bar_external" "$DUMP_DYNSYM" >>"$work/A-import-bar.diag" || true
+ not_ok "A/import-bar" "$work/A-import-bar.diag"
+fi
+
+# ---- A: build consumers for runtime execution --------------------------------
+# Two variants, one per libc ABI, so each can run in its matching container.
+# Both are freestanding (no libc calls, no sysroot required at compile time).
+# --export-dynamic exports bar_external to the executable's .dynsym so that
+# libfoo.so's dynamic reference to it is resolved by the loader at startup.
+
+# Musl interpreter (alpine linux/arm64)
+MUSL_INTERP="/lib/ld-musl-aarch64.so.1"
+if ! clang $CLANG_TARGET -fuse-ld=lld \
+ -O1 -ffreestanding -fno-stack-protector \
+ -nostdlib -rdynamic \
+ -Wl,--dynamic-linker="$MUSL_INTERP" \
+ -Wl,-e,_start \
+ -Wl,-rpath,\$ORIGIN \
+ "$SRC_DIR/consumer.c" \
+ -L"$work" -lfoo \
+ -o "$CONSUMER_MUSL" \
+ >"$work/build-consumer-musl.out" 2>"$work/build-consumer-musl.err"; then
+ skip_test "A/run-musl" "consumer-musl build failed; see $work/build-consumer-musl.err"
+ have_consumer_musl=0
+else
+ have_consumer_musl=1
+fi
+
+# Glibc interpreter (debian:bookworm-slim linux/arm64)
+GLIBC_INTERP="/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1"
+if ! clang $CLANG_TARGET -fuse-ld=lld \
+ -O1 -ffreestanding -fno-stack-protector \
+ -nostdlib -rdynamic \
+ -Wl,--dynamic-linker="$GLIBC_INTERP" \
+ -Wl,-e,_start \
+ -Wl,-rpath,\$ORIGIN \
+ "$SRC_DIR/consumer.c" \
+ -L"$work" -lfoo \
+ -o "$CONSUMER_GLIBC" \
+ >"$work/build-consumer-glibc.out" 2>"$work/build-consumer-glibc.err"; then
+ skip_test "A/run-glibc" "consumer-glibc build failed; see $work/build-consumer-glibc.err"
+ have_consumer_glibc=0
+else
+ have_consumer_glibc=1
+fi
+
+# ---- A: check consumer DT_NEEDED and RPATH (structural, glibc variant) -------
+# Only if the glibc consumer built successfully. The musl and glibc consumers
+# have identical dynamic metadata (same NEEDED/RPATH) — check once.
+if [ $have_consumer_glibc -eq 1 ]; then
+ DUMP_CON_DYN="$work/readelf-consumer-dyn.txt"
+ "$READELF_BIN" -d "$CONSUMER_GLIBC" >"$DUMP_CON_DYN" 2>/dev/null
+
+ # DT_NEEDED must reference libfoo.so (matching the soname we embedded)
+ if grep -q "(NEEDED)" "$DUMP_CON_DYN" && grep "(NEEDED)" "$DUMP_CON_DYN" | grep -q "libfoo.so"; then
+ ok "A/consumer-needed"
+ else
+ printf 'FAIL: consumer missing DT_NEEDED libfoo.so\n' >"$work/A-consumer-needed.diag"
+ grep "NEEDED\|libfoo" "$DUMP_CON_DYN" >>"$work/A-consumer-needed.diag" || true
+ not_ok "A/consumer-needed" "$work/A-consumer-needed.diag"
+ fi
+
+ # RPATH or RUNPATH must contain $ORIGIN
+ if grep -E "(RPATH|RUNPATH)" "$DUMP_CON_DYN" | grep -q 'ORIGIN'; then
+ ok "A/consumer-rpath"
+ else
+ printf 'FAIL: consumer missing RPATH/RUNPATH with \$ORIGIN\n' >"$work/A-consumer-rpath.diag"
+ grep -E "RPATH|RUNPATH" "$DUMP_CON_DYN" >>"$work/A-consumer-rpath.diag" || true
+ not_ok "A/consumer-rpath" "$work/A-consumer-rpath.diag"
+ fi
+fi
+
+# ---- A/run-musl: runtime execution in alpine (musl) container ----------------
+if [ $have_consumer_musl -eq 1 ]; then
+ if [ "${have_podman:-0}" -ne 1 ]; then
+ skip_test "A/run-musl" "podman unavailable"
+ elif ! _exec_target_image_present "aarch64-linux"; then
+ skip_test "A/run-musl" "aarch64 musl image not provisioned (run: make test-images)"
+ else
+ exec_dso_podman "aarch64-linux" "$CONSUMER_MUSL" \
+ "$work/run-musl.out" "$work/run-musl.err"
+ if [ "$RUN_RC" -eq 0 ]; then
+ ok "A/run-musl"
+ else
+ printf 'consumer-musl exited %d\n' "$RUN_RC" >"$work/A-run-musl.diag"
+ [ -s "$work/run-musl.err" ] && sed 's/^/ | /' "$work/run-musl.err" >>"$work/A-run-musl.diag"
+ not_ok "A/run-musl" "$work/A-run-musl.diag"
+ fi
+ fi
+fi
+
+# ---- A/run-glibc: runtime execution in debian (glibc) container --------------
+if [ $have_consumer_glibc -eq 1 ]; then
+ if [ "${have_podman:-0}" -ne 1 ]; then
+ skip_test "A/run-glibc" "podman unavailable"
+ elif ! _exec_target_image_present "aarch64-linux-glibc"; then
+ skip_test "A/run-glibc" "aarch64 glibc image not provisioned (run: make test-images)"
+ else
+ exec_dso_podman "aarch64-linux-glibc" "$CONSUMER_GLIBC" \
+ "$work/run-glibc.out" "$work/run-glibc.err"
+ if [ "$RUN_RC" -eq 0 ]; then
+ ok "A/run-glibc"
+ else
+ printf 'consumer-glibc exited %d\n' "$RUN_RC" >"$work/A-run-glibc.diag"
+ [ -s "$work/run-glibc.err" ] && sed 's/^/ | /' "$work/run-glibc.err" >>"$work/A-run-glibc.diag"
+ not_ok "A/run-glibc" "$work/A-run-glibc.diag"
+ fi
+ fi
+fi
+
+# ---- summary -----------------------------------------------------------------
+kit_summary test-link-dso-aa64
+kit_exit