Re-pin the seed: pkgconf 2.4.3 with the underflow fix, GNU patch, applet trim
P1.4 of the packaging-v2 plan. core-buildenv moves 894ca74b... -> 5a5f8b0d06f267cec431cadf57992ab056687bb21e7e8f7f6a68ee88ec4107e9, which invalidates every identity in the channel. That is the point: it is a flag-day, and P1.5 executes it.
pkgconf: t-79ca IS NOT A VERSION PROBLEM ---------------------------------------- The abort is latent undefined behaviour in pkgconf's own source that only zig's runtime safety traps. libpkgconf/parser.c does
p = value + (strlen(value) - 1);which, for an empty dependency-list field, computes value + SIZE_MAX -- a pointer before the start of the object. Byte-identical line in 1.9.5 (:88) and 2.4.3 (:95). There is no release that fixes it.
Measured, all four: 1.9.5 zig EXIT 134 + Zig panic 2.4.3 zig, unpatched EXIT 134 + the identical panic 2.4.3 host gcc EXIT 0, correct cflags (mechanism control) 2.4.3 zig + this fix EXIT 0, correct cflags
Verified in both directions as the plan's section 8 item 7 requires: all five empty dependency-list fields and the Requires-chain case accepted, AND a genuinely absent package still reported absent while a present one is reported present. A pkgconf that accepts everything is not a fix.
DO NOT "FIX" THIS BY ADDING -O2. zig cc -O2 selects ReleaseFast, which turns the safety check off. Measured: the symptom vanishes and the pointer underflow stays. This script builds without -O2 on purpose.
Audit bound with its limit: 27 .c/.h files under libpkgconf/ and cli/ scanned; 2 matched the ptr + strlen(x) - 1 shape; 1 unguarded (this one), the other guarded by an empty-string check. That covers only that syntactic shape. Other pointer underflows in pkgconf are not ruled out.
GNU patch (section 8 Q4, decided by David 2026-07-31) ----------------------------------------------------- Measured with the builder's own patch -p1 -N -i invocation, against the seed-built binary:
garbage / no-hunk busybox EXIT 0 silent | GNU EXIT 2 "Only garbage..."
wrong context both EXIT 1
applies cleanly both EXIT 0, file changed (positive control)
already applied both EXIT 1Row 1 is the whole reason. The recorded sha256 on a patch input moves from being the SOLE guard against a corrupted patch to being defence-in-depth.
Row 4 falsifies section 8 Q4's supporting argument 2, which claimed GNU's --forward exits cleanly on a re-apply. It does not; both exit 1. The decision survives on row 1, which was always the load-bearing argument. Recording that it outlived one of its supports.
Applet trim ----------- 404 applets -> 397. Exactly 404 - 7 (wget nc telnetd httpd sendmail ftpd tftp), with patch accounted for separately: the busybox symlink is skipped so GNU patch owns the name. No unexplained delta.
With the P1.1 network namespace in place this is defence-in-depth, not the thing keeping the network out. Saying so rather than overselling it.
Refusals, and why they are written the way they are --------------------------------------------------- Every sed here is followed by a grep verification, because sed -i exits 0 when it matches nothing. Every applet check runs in BOTH directions, because a trim that trims nothing and a trim that trims everything both pass a one-directional check. The sandbox-tool check exists so that a seed which lost unshare cannot look to the builder like a host that forbids user namespaces.
Sabotage-tested individually, with a positive control: healthy seed EXIT 0 (stands down) trimmed applet survives EXIT 1 required tool missing EXIT 1 sandbox tool missing EXIT 1 pkgconf missing EXIT 1 pkgconf sed matches nothing EXIT 1 "did not apply" two occurrences on one line EXIT 1 "still present"
That last red is why the sed carries s///g. With g, the second grep no longer has a trigger I can construct, so the script labels it as a belt against a future edit rather than a check with a demonstrated red.
Determinism ----------- Three independent builds, two of them at different path depths, all produce 5a5f8b0d... Both hashes asserted non-empty before comparison, because awk on an empty file exits 0 and would MATCH nothing (t-f2c4).
NOT DONE, and it is a release action: MANIFEST now points at a seed-v2 release URL that does not exist yet. The tarball must be published there before P1.5, or a fresh clone's bootstrap fetches the old artifact and refuses on hash mismatch.
seed/MANIFEST | 3 +--
seed/MANIFEST.sources | 5 +++--
seed/scripts/build-seed.sh | 131 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
src/core/build-environments.sgl | 2 +-
4 files changed, 133 insertions(+), 8 deletions(-)seed/MANIFESTmodified
# sha256 url file894ca74bdb03c25d79f00ce915bbe25c56eba192334963c69e1846e6c84526cb https://codeberg.org/sigil/core-channel/releases/download/seed-v1/seed-x86_64-linux.tar.gz seed-x86_64-linux.tar.gz5a5f8b0d06f267cec431cadf57992ab056687bb21e7e8f7f6a68ee88ec4107e9 https://codeberg.org/sigil/core-channel/releases/download/seed-v2/seed-x86_64-linux.tar.gz seed-x86_64-linux.tar.gzseed/MANIFEST.sourcesmodified
# sha256 url file3311dff32e746499f4df0d5df04d7eb396382d7e108bb9250e7b519b837043a4 https://busybox.net/downloads/busybox-1.37.0.tar.bz2 sources/busybox-1.37.0.tar.bz28814ba072182b605d156d7589c19a43b89fc58ea479b9355146160946f8cf6e9 https://ftp.gnu.org/gnu/make/make-4.4.1.tar.lz sources/make-4.4.1.tar.lz1ac1656debb27497563036f7bffc281490f83f9b8457c0d60bcfb638fb6b6171 https://distfiles.ariadne.space/pkgconf/pkgconf-1.9.5.tar.xz sources/pkgconf-1.9.5.tar.xz51203d99ed573fa7344bf07ca626f10c7cc094e0846ac4aa0023bd0c83c25a41 https://distfiles.ariadne.space/pkgconf/pkgconf-2.4.3.tar.xz sources/pkgconf-2.4.3.tar.xzf87cee69eec2b4fcbf60a396b030ad6aa3415f192aa5f7ee84cad5e11f7f5ae3 https://ftp.gnu.org/gnu/patch/patch-2.8.tar.xz sources/patch-2.8.tar.xz5a9a996dc292cc24dcf411cee87e92f6aae5b8d13bd9c6819b4c7a9dce0818ab https://ftp.gnu.org/gnu/hello/hello-2.12.2.tar.gz sources/hello-2.12.2.tar.gz# deterministic output: 894ca74bdb03c25d79f00ce915bbe25c56eba192334963c69e1846e6c84526cb seed-x86_64-linux.tar.gz# deterministic output: 5a5f8b0d06f267cec431cadf57992ab056687bb21e7e8f7f6a68ee88ec4107e9 seed-x86_64-linux.tar.gzseed/scripts/build-seed.shmodified
# # This is an autoconf cache variable, i.e. the documented escape hatch, and it # forces make's own bundled gnulib glob instead. No source patch. # # THIS BREAK IS PRE-EXISTING, not something the seed re-pin introduced. # Measured 2026-07-31 by restoring build-seed.sh, verify-sources.sh and # MANIFEST.sources to their pristine `main` versions in a separate copy: the # identical five errors. Without this line the seed cannot be rebuilt at all # on this host, which is why `gate-crosspath.sh` could not have been passing. make_cv_sys_gnu_glob=no CC="$cc" AR="$ar" RANLIB="$ranlib" LD="$cc" CFLAGS="$map_flags" ./configure --disable-nls --without-guile --disable-dependency-tracking LDFLAGS="-static -s" CC="$cc" CFLAGS="$map_flags" ./build.sh cp make "$work/bootstrap-make" cp make "$seed/out/bin/make")tar -C "$work" -xf "$seed/sources/pkgconf-1.9.5.tar.xz"tar -C "$work" -xf "$seed/sources/pkgconf-2.4.3.tar.xz"( cd "$work/pkgconf-1.9.5" cd "$work/pkgconf-2.4.3" # t-79ca, ROOT CAUSE. `p = value + (strlen(value) - 1)` computes a pointer # BEFORE the start of the buffer whenever a .pc file carries an empty # dependency-list field (`Requires:` with nothing after it): strlen is 0, the # subtraction wraps to SIZE_MAX, and the addition underflows. That is # undefined behaviour, and Zig's runtime safety TRAPS it -- SIGABRT, exit 134, # "addition of unsigned offset ... overflowed". gcc and clang without # sanitizers wrap around and land on the key's NUL terminator, which is why # every other distribution's pkgconf appears to work. # # MEASURED, so nobody re-derives it: # * identical line in 1.9.5 (parser.c:88) and 2.4.3 (parser.c:95). There is # NO pkgconf release that fixes this -- a version bump is not a fix. # * same 2.4.3 source built with host gcc: EXIT 0, correct cflags. # * with this line fixed and built by zig: EXIT 0, correct cflags, and a # genuinely absent package still reports absent (EXIT 1). Both directions. # # DO NOT "FIX" THIS BY ADDING -O2. `zig cc -O2` selects ReleaseFast, which # turns the safety check OFF. The symptom disappears and a live pointer # underflow stays in a signed build environment, silently. This script builds # without -O2 on purpose, and that is precisely why the defect was ever # visible. # # Population bound for this audit, WITH its limit: 27 .c/.h files under # libpkgconf/ and cli/ were scanned; 2 matched the `ptr + strlen(x) - 1` # shape; 1 is unguarded (this one) and the other (cli/main.c) is explicitly # guarded by an empty-string check immediately above it. That bound covers # ONLY that syntactic shape -- other pointer underflows in pkgconf are not # ruled out. sed -i 's@p = value + (strlen(value) - 1);@p = value + strlen(value); if (p > value) p--;@g' \ libpkgconf/parser.c # `sed -i` exits 0 when it matches nothing. Verify the edit landed, in both # directions, or a future pkgconf bump silently reintroduces the abort. # # SABOTAGE-TESTED 2026-07-31, each grep separately, with a positive control: # real 2.4.3 parser.c -> EXIT 0, fix applied (control) # variable renamed upstream (sed no-ops) -> EXIT 1, "did not apply" # two occurrences on ONE line, `s///` -> EXIT 1, "still present" # The `g` above was added BECAUSE of that third case: without it sed replaces # only the first occurrence per line. With `g` the second grep no longer has # a trigger I can construct, so it is now a belt against a future edit to this # sed rather than a check with a demonstrated red. Saying which is which # matters more than the grep does. grep -q 'if (p > value) p--;' libpkgconf/parser.c || { echo "build-seed: REFUSING: pkgconf parser.c underflow fix did not apply" >&2 exit 1 } grep -q 'strlen(value) - 1' libpkgconf/parser.c && { echo "build-seed: REFUSING: the unfixed pkgconf underflow is still present" >&2 exit 1 } CC="$cc" AR="$ar" RANLIB="$ranlib" LD="$cc" CFLAGS="$map_flags" ./configure \ --prefix=/usr \ --with-pkg-config-dir=/usr/lib/pkgconfig:/usr/share/pkgconfig \ cp pkgconf "$seed/out/bin/pkgconf")# GNU patch, replacing the busybox applet (§8 Q4, decided by David 2026-07-31).## The deciding factor was TIMING ASYMMETRY: the seed re-pin invalidates every# identity exactly once, so adding a tool the builder legitimately needs costs# nothing now and costs a second flag-day later.## The load-bearing behavioural difference, MEASURED both ways with the builder's# own `patch -p1 -N -i FILE` invocation:# garbage / no-hunk file : busybox EXIT 0, silent, applies nothing# GNU EXIT 2, "Only garbage was found in the# patch input."# wrong context : both EXIT 1 (busybox "Hunk 1 FAILED", GNU# "Hunk #1 FAILED at 1" + a .rej file)# applies cleanly : both EXIT 0, file changed (positive control)## So the recorded sha256 on a patch input moves from being the ONLY guard# against a corrupted patch to being defence-in-depth. That was the goal.## What is NOT true, and the plan said it was: GNU's `--forward` does not exit 0# on a re-apply. Both implementations exit 1 there (GNU: "Reversed (or# previously applied) patch detected! Skipping patch"). Neither corrupts the# file. §8 Q4's supporting argument 2 is falsified; argument 1 above is what# the decision rests on.tar -C "$work" -xf "$seed/sources/patch-2.8.tar.xz"( cd "$work/patch-2.8" CC="$cc" AR="$ar" RANLIB="$ranlib" LD="$cc" CFLAGS="$map_flags" ./configure \ --disable-nls --disable-dependency-tracking LDFLAGS="-static -s" "$make" -j"$jobs" cp src/patch "$seed/out/bin/patch")ln -s pkgconf "$seed/out/bin/pkg-config"# APPLET TRIM. Network clients and servers have no business in a signed build# environment: nothing in a build phase legitimately fetches or listens, every# input arrives through the store, and the builder runs in a network namespace# with loopback only. With netns in place (P1.1) this is defence-in-depth, not# the thing keeping the network out -- say that rather than overselling it.## `patch` is excluded for a different reason: GNU patch above owns that name.trim="wget nc telnetd httpd sendmail ftpd tftp""$seed/out/bin/busybox" --list | while IFS= read -r applet; do case "$applet" in busybox|make|pkgconf|pkg-config|'['|'[[') continue ;; esac case "$applet" in busybox|make|pkgconf|pkg-config|patch|'['|'[[') continue ;; esac case " $trim " in *" $applet "*) continue ;; esac ln -s busybox "$seed/out/bin/$applet"done# A trim that trims nothing is the same silent-success shape as a sed that# matches nothing. Verify, in BOTH directions -- what must be gone is gone, and# what must remain remains. A one-directional check would pass just as happily# on a seed containing nothing at all.## Written as `if`, not `[ ... ] && { ... }`: a refusal must not depend on the# subtle `set -e` semantics of a failing AND-OR list.for applet in $trim; do if [ -e "$seed/out/bin/$applet" ]; then echo "build-seed: REFUSING: trimmed applet $applet is still present" >&2 exit 1 fidonefor applet in sh tar patch pkgconf make; do if [ ! -e "$seed/out/bin/$applet" ]; then echo "build-seed: REFUSING: required tool $applet is missing from the seed" >&2 exit 1 fidone# The builder assembles its sandbox with these; a seed that lost one would look# to the builder like a host that forbids user namespaces (see external.sgl# require-sandbox-tools!), so refuse here where the cause is visible.for applet in unshare mount chroot ip mkdir ln touch env; do if [ ! -e "$seed/out/bin/$applet" ]; then echo "build-seed: REFUSING: sandbox tool $applet is missing from the seed" >&2 exit 1 fidone"$seed/scripts/package-seed.sh""$seed/out/bin/busybox" echo "seed binaries built for static musl target"src/core/build-environments.sglmodified
(export core-buildenv) (begin (define core-buildenv "894ca74bdb03c25d79f00ce915bbe25c56eba192334963c69e1846e6c84526cb"))) "5a5f8b0d06f267cec431cadf57992ab056687bb21e7e8f7f6a68ee88ec4107e9")))