From 82a231e5f96681c6b5f7b854ed0e887451ee3bf3 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Wed, 19 Aug 2026 06:24:11 -0400 Subject: [PATCH] [ADD] test-chroot: read the binaries, not the declarations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The resolver reported satisfied. The rootfs installed 102 packages. bash, coreutils, tar, sed and find all ran inside the chroot, on s390x, against glibc 2.44. Every check passed. Then a fourth check, reading ELF headers instead of metadata, found seventeen libraries that some shipped binary asks for and no shipped package provides. Most are the ordinary stage-1 artefact -- the host's soname where Arch's differs, libgpgme.so.11 against our .45 -- and stage 2 dissolves those by rebuilding inside the chroot. One was not. kbd's loadkeys needed libxkbcommon.so.0 while kbd declared glibc, gzip and pam. An UNDER-DECLARED dependency: invisible to pacman's resolver and to this port's own closure computation, because both read declarations. And the cause was mine -- libxkbcommon-dev went into install_host_deps for systemd, and kbd, built later, probed for it and linked it. Arch declares it nowhere, so its chroot fails the same probe; --disable-xkb converges rather than diverges. Also here: the test now uses its own package cache. The shared one held a coreutils from before its selinux fix, at the same pkgver-pkgrel, and pacman called it corrupted. --- FR --- Le résolveur se déclarait satisfait. Le rootfs installait 102 paquets. bash, coreutils, tar, sed et find tournaient tous dans le chroot, sur s390x, contre la glibc 2.44. Toutes les vérifications passaient. Puis une quatrième, lisant les en-têtes ELF au lieu des métadonnées, a trouvé dix-sept bibliothèques qu'un binaire livré réclame et qu'aucun paquet livré ne fournit. La plupart sont l'artefact ordinaire de l'étage 1 — le soname de l'hôte là où celui d'Arch diffère, libgpgme.so.11 contre notre .45 — et l'étage 2 les dissout en reconstruisant dans le chroot. Une ne l'était pas. Le loadkeys de kbd réclamait libxkbcommon.so.0 quand kbd déclarait glibc, gzip et pam. Une dépendance SOUS-DÉCLARÉE : invisible au résolveur de pacman comme au calcul de fermeture de ce portage, puisque tous deux lisent des déclarations. Et la cause était mienne — libxkbcommon-dev est entré dans install_host_deps pour systemd, et kbd, bâti plus tard, l'a sondé et lié. Arch ne le déclare nulle part, son chroot échoue donc à la même sonde ; --disable-xkb converge au lieu de diverger. Aussi ici : le test utilise désormais son propre cache de paquets. Le cache partagé gardait un coreutils d'avant son correctif selinux, au même pkgver-pkgrel, et pacman le déclarait corrompu. Assisted-by: Claude Opus 5 --- patches/pkgbuild/kbd.sh | 44 ++++++++++++++++++++++++++ scripts/test-chroot.sh | 68 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 106 insertions(+), 6 deletions(-) create mode 100755 patches/pkgbuild/kbd.sh diff --git a/patches/pkgbuild/kbd.sh b/patches/pkgbuild/kbd.sh new file mode 100755 index 0000000..0e86f2b --- /dev/null +++ b/patches/pkgbuild/kbd.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# kbd: --disable-xkb. Found by nothing except the artefact. +# +# $ readelf -d usr/bin/loadkeys | grep NEEDED +# (NEEDED) Shared library: [libxkbcommon.so.0] +# +# $ bsdtar -xOf kbd-*.pkg.tar.gz .PKGINFO | grep depend +# depend = glibc +# depend = gzip +# depend = pam +# +# THE POINT OF THIS HOOK IS THE GAP BETWEEN THOSE TWO OUTPUTS. loadkeys needs +# a library the package does not declare, so pacman's resolver is satisfied, +# the rootfs installs cleanly, and loadkeys does not start. No dependency +# check can see this -- not pacman's, and not the closure computation in this +# port, because both read declarations. Only reading the binaries finds it. +# scripts/test-chroot.sh grew a fourth check for exactly this class. +# +# WHERE IT CAME FROM, and it is worth admitting plainly: configure.ac:362 +# +# AC_ARG_ENABLE([xkb], ... [default=auto]) +# AS_IF([test "x$USE_XKB" != xno], +# [PKG_CHECK_MODULES(XKBCOMMON, xkbcommon, [USE_XKB=yes], [USE_XKB=no])]) +# +# 'auto' again -- a default read from the build host. libxkbcommon-dev is on +# this machine because it was added to install_host_deps FOR SYSTEMD. kbd, an +# unrelated package built later, probed for it, found it, and silently linked +# a library nothing in this port ships. One host package, one broken binary in +# a different package, undeclared. +# +# Arch's kbd declares neither libxkbcommon in depends nor in makedepends, so +# Arch's build chroot fails the same probe and Arch ships loadkeys without xkb +# support. Disabling converges with Arch rather than diverging from it. +# +# The alternative was building libxkbcommon, and it is not close: measured, it +# wants libxcb and xkeyboard-config behind it -- an X11 subtree, to generate +# keymaps from an XKB database, on a mainframe with no graphics adapter. +set -euo pipefail +grep -q '^\s\+--disable-tests$' PKGBUILD || { + echo "kbd: configure block not in the expected form" >&2; exit 1; } +sed -i 's|^\(\s*\)--disable-tests$|\1--disable-tests \\\n\1--disable-xkb|' PKGBUILD +[ "$(grep -c -- '--disable-xkb' PKGBUILD)" = 1 ] || { + echo "kbd: --disable-xkb not inserted exactly once" >&2; exit 1; } +echo "kbd: --disable-xkb (host libxkbcommon-dev, installed for systemd, leaked in)" diff --git a/scripts/test-chroot.sh b/scripts/test-chroot.sh index f7a9cbd..add3529 100755 --- a/scripts/test-chroot.sh +++ b/scripts/test-chroot.sh @@ -19,7 +19,14 @@ # 2. ARTEFACT -- static audit of every package for host contamination that # does not raise an error: Debian multiarch libdirs, files # under /usr/local, binaries linked to libselinux. -# 3. RUN -- chroot in and execute the binaries. The only check that +# 3. SONAME -- every library any shipped binary ASKS for, minus every +# library the repository SHIPS. This is the check that reads +# binaries instead of declarations, and it is the only one +# that can see an under-declared dependency: kbd's loadkeys +# needed libxkbcommon.so.0 while kbd declared glibc, gzip +# and pam. RESOLVE was satisfied, the rootfs installed, and +# loadkeys could not start. +# 4. RUN -- chroot in and execute the binaries. The only check that # can catch a missing ld.so, because a package whose # interpreter is absent installs perfectly. # @@ -30,6 +37,19 @@ WORK="${WORK:-$HOME/work/arch-s390x}" REPO="${REPO:-$WORK/repo/s390x}" ROOT="${ROOT:-$WORK/rootfs-test}" CONF="$WORK/pacman-test.conf" +# A cache of its own, wiped every run. +# +# pacman's default cache is /var/cache/pacman/pkg, which is HOST-WIDE and +# survives between runs. A package rebuilt at the same pkgver-pkgrel -- which +# every fix in this port does -- leaves the old file there while core.db +# records the new checksum, and the next install stops on +# +# File .../coreutils-9.11-2-s390x.pkg.tar.gz is corrupted +# (invalid or corrupted package (checksum)) +# +# which reads like a damaged build rather than a stale copy. The shared cache +# is also not this test's to empty: other work on this host uses it. +CACHE="$WORK/pacman-test.cache" # The set a rootfs needs to reach a shell prompt and manage itself. filesystem # is not optional and not obvious: its usr-merge symlinks are what create @@ -61,11 +81,11 @@ note "1. RESOLVE -- pacman's own dependency check, --nodeps OFF" # The root and its dbpath must EXIST before alpm will initialise -- pacman # reports that as "failed to resolve path ... passed to --root", which reads # like a bad argument rather than a directory it declined to create. -sudo rm -rf "$ROOT.probe"; sudo mkdir -p "$ROOT.probe/var/lib/pacman" +sudo rm -rf "$ROOT.probe" "$CACHE"; sudo mkdir -p "$ROOT.probe/var/lib/pacman" "$CACHE" # -Syp, not -Sp. A fresh dbpath has no sync database, and without -y pacman # reports every package as "target not found" -- which reads like an empty # repository rather than an unread index. -if sudo pacman --root "$ROOT.probe" --config "$CONF" \ +if sudo pacman --root "$ROOT.probe" --config "$CONF" --cachedir "$CACHE" \ --noconfirm -Syp "${PKGS[@]}" > "$WORK/resolve.txt" 2>&1; then good "resolver satisfied ($(grep -c '^file://' "$WORK/resolve.txt") packages)" else @@ -84,9 +104,45 @@ for f in "$REPO"/*.pkg.tar.*; do done [ "$n" -eq 0 ] && good "no Debian multiarch libdir anywhere" -note "3. RUN -- install for real, then chroot" -sudo rm -rf "$ROOT"; sudo mkdir -p "$ROOT/var/lib/pacman" -if ! sudo pacman --root "$ROOT" --config "$CONF" --noconfirm -Sy "${PKGS[@]}" \ +note "3. SONAME -- what binaries ask for versus what the repository ships" +# Two passes over the packages: collect the soname each shared library +# DECLARES, and every soname each binary REQUESTS. The difference is a set of +# libraries that will be missing at runtime on the target. +# +# Most entries here are the ordinary stage-1 artefact -- the host's soname +# version rather than Arch's, e.g. libgpgme.so.11 where our gpgme package +# ships .45 -- and stage 2 resolves those by rebuilding inside the chroot. +# What must not be ignored is the other kind: a library no package in the +# repository provides at any version. +_sa=$(mktemp -d) +: > "$_sa/have"; : > "$_sa/want" +for f in "$REPO"/*.pkg.tar.*; do + rm -rf "$_sa/x"; mkdir -p "$_sa/x" + bsdtar -xf "$f" -C "$_sa/x" usr 2>/dev/null || continue + _pn=$(bsdtar -xOf "$f" .PKGINFO 2>/dev/null | sed -n 's/^pkgname = //p') + while IFS= read -r b; do + readelf -d "$b" 2>/dev/null | sed -n 's/.*Library soname: \[\(.*\)\].*/\1/p' >> "$_sa/have" + readelf -d "$b" 2>/dev/null | sed -n 's/.*Shared library: \[\(.*\)\].*/\1/p' \ + | sed "s|^|$_pn |" >> "$_sa/want" + done < <(find "$_sa/x" -type f 2>/dev/null) +done +sort -u "$_sa/have" > "$_sa/have.s" +awk '{print $2}' "$_sa/want" | sort -u > "$_sa/want.s" +if [ -s "$(comm -13 "$_sa/have.s" "$_sa/want.s" > "$_sa/miss"; echo "$_sa/miss")" ]; then + bad "$(wc -l < "$_sa/miss") soname(s) requested and never shipped:" + while read -r m; do + printf ' %-24s <- %s\n' "$m" \ + "$(awk -v m="$m" '$2==m {print $1}' "$_sa/want" | sort -u | tr '\n' ' ')" + done < "$_sa/miss" +else + good "every requested soname is shipped by some package" +fi +rm -rf "$_sa" + +note "4. RUN -- install for real, then chroot" +sudo rm -rf "$ROOT" "$CACHE"; sudo mkdir -p "$ROOT/var/lib/pacman" "$CACHE" +if ! sudo pacman --root "$ROOT" --config "$CONF" --cachedir "$CACHE" \ + --noconfirm -Sy "${PKGS[@]}" \ > "$WORK/install.txt" 2>&1; then bad "install failed, see $WORK/install.txt" tail -15 "$WORK/install.txt" | sed 's/^/ /'