commit 7a1ff3b3618cd6590d0c4a7e63700f96c21b6975
parent e073a97068ef9d63659cb52cb182be91f3b46c51
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 17 Jun 2026 08:59:18 -0700
test: cover DSO libc dependency resolution
Diffstat:
3 files changed, 135 insertions(+), 9 deletions(-)
diff --git a/mk/test.mk b/mk/test.mk
@@ -905,13 +905,12 @@ test-link-x64: lib rt-x86_64-linux $(ROUNDTRIP_BIN) $(LINK_EXE_RUNNER) $(JIT_RUN
@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:
+# Uses kit cc/ld to produce DSOs and freestanding consumers, runs readelf
+# structural checks, and executes consumers inside podman containers when
+# provisioned. The make target provisions build/musl-sysroot so the libc
+# dependency lane is active; direct script runs skip that lane if it is absent.
+# Opt-in: requires readelf; runtime checks need provisioned images.
+test-link-dso-aa64: bin build/musl-sysroot/PROVENANCE
bash test/link/elf-dso-aa64/run.sh
test-macho: lib $(TEST_RT_DEP) $(ROUNDTRIP_BIN_MACHO) $(LINK_EXE_RUNNER) $(JIT_RUNNER)
diff --git a/test/link/elf-dso-aa64/lib_foo_libc.c b/test/link/elf-dso-aa64/lib_foo_libc.c
@@ -0,0 +1,32 @@
+/* test/link/elf-dso-aa64/lib_foo_libc.c — DSO fixture with a libc dependency.
+ *
+ * This is intentionally headerless so the compile step does not need a sysroot:
+ * the link step supplies libc.so as a DSO input, and the produced shared object
+ * must carry libc's DT_NEEDED SONAME plus UND dynsym imports for malloc/free.
+ * The allocation is deliberately large enough to go through libc's syscall-
+ * backed allocator path on musl.
+ */
+
+extern void* malloc(__SIZE_TYPE__ size);
+extern void free(void* p);
+extern int bar_external(int x);
+
+int foo_counter = 42;
+
+int foo_add(int a, int b)
+{
+ char* p = (char*)malloc(256u * 1024u);
+ int ok;
+ if (!p)
+ return -1;
+ p[0] = 'k';
+ p[256u * 1024u - 1u] = 't';
+ ok = (p[0] == 'k' && p[256u * 1024u - 1u] == 't');
+ free(p);
+ return a + b + bar_external(0) + (ok ? 0 : -1);
+}
+
+int foo_get_counter(void)
+{
+ return foo_counter;
+}
diff --git a/test/link/elf-dso-aa64/run.sh b/test/link/elf-dso-aa64/run.sh
@@ -1,8 +1,9 @@
#!/usr/bin/env bash
# test/link/elf-dso-aa64/run.sh — ELF shared-library harness, aarch64 target.
#
-# Single kit lane: kit cc compiles PIC objects, kit ld links libfoo.so, kit cc
-# links the consumers, then readelf and podman verify the result.
+# Kit lanes: kit cc compiles PIC objects, kit ld links DSOs, kit cc links the
+# consumers, then readelf and podman verify the result. Lane B links a produced
+# DSO against libc.so so the loader must resolve a dependency of a dependency.
#
# Prerequisites (checked at runtime; missing prereqs produce SKIP, not FAIL):
# build/kit — built kit binary
@@ -107,6 +108,12 @@ exec_dso_podman() {
# 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
+#
+# Libc-dependency checks (require extracted aarch64 musl sysroot):
+# B/build-libc-lib compile lib_foo_libc.c and link libfoo_libc.so with libc.so
+# B/libc-needed libfoo_libc.so has a libc DT_NEEDED entry
+# B/import-malloc malloc is UND in libfoo_libc.so .dynsym
+# B/run-musl consumer-libc-musl exits 0 inside alpine container
# ---- A: prerequisites --------------------------------------------------------
@@ -124,6 +131,10 @@ LIBFOO_SO="$work/libfoo.so"
CONSUMER_O="$work/consumer.o"
CONSUMER_MUSL="$work/consumer-musl"
CONSUMER_GLIBC="$work/consumer-glibc"
+LIBFOO_LIBC_O="$work/lib_foo_libc.o"
+LIBFOO_LIBC_SO="$work/libfoo_libc.so"
+CONSUMER_LIBC_MUSL="$work/consumer-libc-musl"
+MUSL_SYSROOT="${KIT_MUSL_SYSROOT:-$ROOT/build/musl-sysroot}"
SRC_DIR="$(cd "$(dirname "$0")" && pwd)"
@@ -355,6 +366,90 @@ if [ $have_consumer_glibc -eq 1 ]; then
fi
fi
+# ---- B: produced DSO depends on libc.so --------------------------------------
+# This lane links the shared object itself against the extracted aarch64 musl
+# libc.so. The consumer only names libfoo_libc.so; libc is pulled transitively
+# from libfoo_libc.so's DT_NEEDED entry at runtime.
+
+have_libc_lane=0
+if [ ! -f "$MUSL_SYSROOT/usr/lib/libc.so" ]; then
+ skip_test "B" "aarch64 musl sysroot unavailable (run: make test-libc-musl, or set KIT_MUSL_SYSROOT)"
+else
+ if ! "$KIT" cc $KIT_TARGET \
+ -O1 -fPIC \
+ -c "$SRC_DIR/lib_foo_libc.c" -o "$LIBFOO_LIBC_O" \
+ >"$work/compile-libc-lib.out" 2>"$work/compile-libc-lib.err"; then
+ not_ok "B/build-libc-lib" "$work/compile-libc-lib.err"
+ elif ! "$KIT" ld \
+ -shared \
+ -soname libfoo_libc.so \
+ "$LIBFOO_LIBC_O" "$MUSL_SYSROOT/usr/lib/libc.so" \
+ -o "$LIBFOO_LIBC_SO" \
+ >"$work/link-libc-lib.out" 2>"$work/link-libc-lib.err"; then
+ not_ok "B/build-libc-lib" "$work/link-libc-lib.err"
+ else
+ ok "B/build-libc-lib"
+ have_libc_lane=1
+ fi
+fi
+
+if [ $have_libc_lane -eq 1 ]; then
+ DUMP_LIBC_DYN="$work/readelf-libc-dyn.txt"
+ DUMP_LIBC_DYNSYM="$work/readelf-libc-dynsym.txt"
+ "$READELF_BIN" -d "$LIBFOO_LIBC_SO" >"$DUMP_LIBC_DYN" 2>/dev/null
+ "$READELF_BIN" --dyn-syms "$LIBFOO_LIBC_SO" >"$DUMP_LIBC_DYNSYM" 2>/dev/null
+
+ if grep -q "(NEEDED)" "$DUMP_LIBC_DYN" && grep "(NEEDED)" "$DUMP_LIBC_DYN" | grep -q "libc"; then
+ ok "B/libc-needed"
+ else
+ printf 'FAIL: produced DSO missing a libc DT_NEEDED entry\n' >"$work/B-libc-needed.diag"
+ grep "NEEDED\|libc" "$DUMP_LIBC_DYN" >>"$work/B-libc-needed.diag" || true
+ not_ok "B/libc-needed" "$work/B-libc-needed.diag"
+ fi
+
+ if grep "malloc" "$DUMP_LIBC_DYNSYM" | grep -q "UND"; then
+ ok "B/import-malloc"
+ else
+ printf 'FAIL: malloc not in libfoo_libc.so .dynsym as UND\n' >"$work/B-import-malloc.diag"
+ grep "malloc" "$DUMP_LIBC_DYNSYM" >>"$work/B-import-malloc.diag" || true
+ not_ok "B/import-malloc" "$work/B-import-malloc.diag"
+ fi
+
+ if ! "$KIT" cc $KIT_TARGET \
+ -O1 -ffreestanding \
+ -nostdlib -rdynamic \
+ -e _start \
+ -Wl,-dynamic-linker,"$MUSL_INTERP" \
+ -Wl,-rpath,\$ORIGIN \
+ "$SRC_DIR/consumer.c" \
+ -L"$work" -lfoo_libc \
+ -o "$CONSUMER_LIBC_MUSL" \
+ >"$work/build-consumer-libc-musl.out" 2>"$work/build-consumer-libc-musl.err"; then
+ skip_test "B/run-musl" "consumer-libc-musl build failed; see $work/build-consumer-libc-musl.err"
+ have_consumer_libc_musl=0
+ else
+ have_consumer_libc_musl=1
+ fi
+
+ if [ $have_consumer_libc_musl -eq 1 ]; then
+ if [ "${have_podman:-0}" -ne 1 ]; then
+ skip_test "B/run-musl" "podman unavailable"
+ elif ! _exec_target_image_present "aarch64-linux"; then
+ skip_test "B/run-musl" "aarch64 musl image not provisioned (run: make test-images)"
+ else
+ exec_dso_podman "aarch64-linux" "$CONSUMER_LIBC_MUSL" \
+ "$work/run-libc-musl.out" "$work/run-libc-musl.err"
+ if [ "$RUN_RC" -eq 0 ]; then
+ ok "B/run-musl"
+ else
+ printf 'consumer-libc-musl exited %d\n' "$RUN_RC" >"$work/B-run-musl.diag"
+ [ -s "$work/run-libc-musl.err" ] && sed 's/^/ | /' "$work/run-libc-musl.err" >>"$work/B-run-musl.diag"
+ not_ok "B/run-musl" "$work/B-run-musl.diag"
+ fi
+ fi
+ fi
+fi
+
# ---- summary -----------------------------------------------------------------
kit_summary test-link-dso-aa64
kit_exit