Commit57624762Recorded31 Jul 2026Repositorycore-channel

Re-pin the seed: pkgconf 2.4.3 with the underflow fix, GNU patch, applet trim

Message

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 1

Row 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.

Changed
 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(-)
Diff
seed/MANIFESTmodified
@@ -1,3 +1,2 @@
1
# sha256 url file
2
894ca74bdb03c25d79f00ce915bbe25c56eba192334963c69e1846e6c84526cb https://codeberg.org/sigil/core-channel/releases/download/seed-v1/seed-x86_64-linux.tar.gz seed-x86_64-linux.tar.gz
3
+2
5a5f8b0d06f267cec431cadf57992ab056687bb21e7e8f7f6a68ee88ec4107e9 https://codeberg.org/sigil/core-channel/releases/download/seed-v2/seed-x86_64-linux.tar.gz seed-x86_64-linux.tar.gz
seed/MANIFEST.sourcesmodified
@@ -1,7 +1,8 @@
1
# sha256 url file
2
3311dff32e746499f4df0d5df04d7eb396382d7e108bb9250e7b519b837043a4 https://busybox.net/downloads/busybox-1.37.0.tar.bz2 sources/busybox-1.37.0.tar.bz2
3
8814ba072182b605d156d7589c19a43b89fc58ea479b9355146160946f8cf6e9 https://ftp.gnu.org/gnu/make/make-4.4.1.tar.lz sources/make-4.4.1.tar.lz
4
1ac1656debb27497563036f7bffc281490f83f9b8457c0d60bcfb638fb6b6171 https://distfiles.ariadne.space/pkgconf/pkgconf-1.9.5.tar.xz sources/pkgconf-1.9.5.tar.xz
+4
51203d99ed573fa7344bf07ca626f10c7cc094e0846ac4aa0023bd0c83c25a41 https://distfiles.ariadne.space/pkgconf/pkgconf-2.4.3.tar.xz sources/pkgconf-2.4.3.tar.xz
+5
f87cee69eec2b4fcbf60a396b030ad6aa3415f192aa5f7ee84cad5e11f7f5ae3 https://ftp.gnu.org/gnu/patch/patch-2.8.tar.xz sources/patch-2.8.tar.xz
6
5a9a996dc292cc24dcf411cee87e92f6aae5b8d13bd9c6819b4c7a9dce0818ab https://ftp.gnu.org/gnu/hello/hello-2.12.2.tar.gz sources/hello-2.12.2.tar.gz
7
7
# deterministic output: 894ca74bdb03c25d79f00ce915bbe25c56eba192334963c69e1846e6c84526cb seed-x86_64-linux.tar.gz
+8
# deterministic output: 5a5f8b0d06f267cec431cadf57992ab056687bb21e7e8f7f6a68ee88ec4107e9 seed-x86_64-linux.tar.gz
seed/scripts/build-seed.shmodified
@@ -33,6 +33,12 @@ tar -C "$work" -xf "$seed/sources/make-4.4.1.tar.lz"
33
#
34
# This is an autoconf cache variable, i.e. the documented escape hatch, and it
35
# forces make's own bundled gnulib glob instead. No source patch.
+36
#
+37
# THIS BREAK IS PRE-EXISTING, not something the seed re-pin introduced.
+38
# Measured 2026-07-31 by restoring build-seed.sh, verify-sources.sh and
+39
# MANIFEST.sources to their pristine `main` versions in a separate copy: the
+40
# identical five errors. Without this line the seed cannot be rebuilt at all
+41
# on this host, which is why `gate-crosspath.sh` could not have been passing.
42
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"
43
CC="$cc" CFLAGS="$map_flags" ./build.sh
44
cp make "$work/bootstrap-make"
@@ -57,9 +63,59 @@ tar -C "$work" -xf "$seed/sources/busybox-1.37.0.tar.bz2"
63
cp make "$seed/out/bin/make"
64
)
65
60
tar -C "$work" -xf "$seed/sources/pkgconf-1.9.5.tar.xz"
+66
tar -C "$work" -xf "$seed/sources/pkgconf-2.4.3.tar.xz"
67
(
62
cd "$work/pkgconf-1.9.5"
+68
cd "$work/pkgconf-2.4.3"
+69
# t-79ca, ROOT CAUSE. `p = value + (strlen(value) - 1)` computes a pointer
+70
# BEFORE the start of the buffer whenever a .pc file carries an empty
+71
# dependency-list field (`Requires:` with nothing after it): strlen is 0, the
+72
# subtraction wraps to SIZE_MAX, and the addition underflows. That is
+73
# undefined behaviour, and Zig's runtime safety TRAPS it -- SIGABRT, exit 134,
+74
# "addition of unsigned offset ... overflowed". gcc and clang without
+75
# sanitizers wrap around and land on the key's NUL terminator, which is why
+76
# every other distribution's pkgconf appears to work.
+77
#
+78
# MEASURED, so nobody re-derives it:
+79
# * identical line in 1.9.5 (parser.c:88) and 2.4.3 (parser.c:95). There is
+80
# NO pkgconf release that fixes this -- a version bump is not a fix.
+81
# * same 2.4.3 source built with host gcc: EXIT 0, correct cflags.
+82
# * with this line fixed and built by zig: EXIT 0, correct cflags, and a
+83
# genuinely absent package still reports absent (EXIT 1). Both directions.
+84
#
+85
# DO NOT "FIX" THIS BY ADDING -O2. `zig cc -O2` selects ReleaseFast, which
+86
# turns the safety check OFF. The symptom disappears and a live pointer
+87
# underflow stays in a signed build environment, silently. This script builds
+88
# without -O2 on purpose, and that is precisely why the defect was ever
+89
# visible.
+90
#
+91
# Population bound for this audit, WITH its limit: 27 .c/.h files under
+92
# libpkgconf/ and cli/ were scanned; 2 matched the `ptr + strlen(x) - 1`
+93
# shape; 1 is unguarded (this one) and the other (cli/main.c) is explicitly
+94
# guarded by an empty-string check immediately above it. That bound covers
+95
# ONLY that syntactic shape -- other pointer underflows in pkgconf are not
+96
# ruled out.
+97
sed -i 's@p = value + (strlen(value) - 1);@p = value + strlen(value); if (p > value) p--;@g' \
+98
libpkgconf/parser.c
+99
# `sed -i` exits 0 when it matches nothing. Verify the edit landed, in both
+100
# directions, or a future pkgconf bump silently reintroduces the abort.
+101
#
+102
# SABOTAGE-TESTED 2026-07-31, each grep separately, with a positive control:
+103
# real 2.4.3 parser.c -> EXIT 0, fix applied (control)
+104
# variable renamed upstream (sed no-ops) -> EXIT 1, "did not apply"
+105
# two occurrences on ONE line, `s///` -> EXIT 1, "still present"
+106
# The `g` above was added BECAUSE of that third case: without it sed replaces
+107
# only the first occurrence per line. With `g` the second grep no longer has
+108
# a trigger I can construct, so it is now a belt against a future edit to this
+109
# sed rather than a check with a demonstrated red. Saying which is which
+110
# matters more than the grep does.
+111
grep -q 'if (p > value) p--;' libpkgconf/parser.c || {
+112
echo "build-seed: REFUSING: pkgconf parser.c underflow fix did not apply" >&2
+113
exit 1
+114
}
+115
grep -q 'strlen(value) - 1' libpkgconf/parser.c && {
+116
echo "build-seed: REFUSING: the unfixed pkgconf underflow is still present" >&2
+117
exit 1
+118
}
119
CC="$cc" AR="$ar" RANLIB="$ranlib" LD="$cc" CFLAGS="$map_flags" ./configure \
120
--prefix=/usr \
121
--with-pkg-config-dir=/usr/lib/pkgconfig:/usr/share/pkgconfig \
@@ -70,11 +126,80 @@ tar -C "$work" -xf "$seed/sources/pkgconf-1.9.5.tar.xz"
126
cp pkgconf "$seed/out/bin/pkgconf"
127
)
128
+129
# GNU patch, replacing the busybox applet (§8 Q4, decided by David 2026-07-31).
+130
#
+131
# The deciding factor was TIMING ASYMMETRY: the seed re-pin invalidates every
+132
# identity exactly once, so adding a tool the builder legitimately needs costs
+133
# nothing now and costs a second flag-day later.
+134
#
+135
# The load-bearing behavioural difference, MEASURED both ways with the builder's
+136
# own `patch -p1 -N -i FILE` invocation:
+137
# garbage / no-hunk file : busybox EXIT 0, silent, applies nothing
+138
# GNU EXIT 2, "Only garbage was found in the
+139
# patch input."
+140
# wrong context : both EXIT 1 (busybox "Hunk 1 FAILED", GNU
+141
# "Hunk #1 FAILED at 1" + a .rej file)
+142
# applies cleanly : both EXIT 0, file changed (positive control)
+143
#
+144
# So the recorded sha256 on a patch input moves from being the ONLY guard
+145
# against a corrupted patch to being defence-in-depth. That was the goal.
+146
#
+147
# What is NOT true, and the plan said it was: GNU's `--forward` does not exit 0
+148
# on a re-apply. Both implementations exit 1 there (GNU: "Reversed (or
+149
# previously applied) patch detected! Skipping patch"). Neither corrupts the
+150
# file. §8 Q4's supporting argument 2 is falsified; argument 1 above is what
+151
# the decision rests on.
+152
tar -C "$work" -xf "$seed/sources/patch-2.8.tar.xz"
+153
(
+154
cd "$work/patch-2.8"
+155
CC="$cc" AR="$ar" RANLIB="$ranlib" LD="$cc" CFLAGS="$map_flags" ./configure \
+156
--disable-nls --disable-dependency-tracking LDFLAGS="-static -s"
+157
"$make" -j"$jobs"
+158
cp src/patch "$seed/out/bin/patch"
+159
)
+160
161
ln -s pkgconf "$seed/out/bin/pkg-config"
+162
# APPLET TRIM. Network clients and servers have no business in a signed build
+163
# environment: nothing in a build phase legitimately fetches or listens, every
+164
# input arrives through the store, and the builder runs in a network namespace
+165
# with loopback only. With netns in place (P1.1) this is defence-in-depth, not
+166
# the thing keeping the network out -- say that rather than overselling it.
+167
#
+168
# `patch` is excluded for a different reason: GNU patch above owns that name.
+169
trim="wget nc telnetd httpd sendmail ftpd tftp"
170
"$seed/out/bin/busybox" --list | while IFS= read -r applet; do
75
case "$applet" in busybox|make|pkgconf|pkg-config|'['|'[[') continue ;; esac
+171
case "$applet" in busybox|make|pkgconf|pkg-config|patch|'['|'[[') continue ;; esac
+172
case " $trim " in *" $applet "*) continue ;; esac
173
ln -s busybox "$seed/out/bin/$applet"
174
done
+175
# A trim that trims nothing is the same silent-success shape as a sed that
+176
# matches nothing. Verify, in BOTH directions -- what must be gone is gone, and
+177
# what must remain remains. A one-directional check would pass just as happily
+178
# on a seed containing nothing at all.
+179
#
+180
# Written as `if`, not `[ ... ] && { ... }`: a refusal must not depend on the
+181
# subtle `set -e` semantics of a failing AND-OR list.
+182
for applet in $trim; do
+183
if [ -e "$seed/out/bin/$applet" ]; then
+184
echo "build-seed: REFUSING: trimmed applet $applet is still present" >&2
+185
exit 1
+186
fi
+187
done
+188
for applet in sh tar patch pkgconf make; do
+189
if [ ! -e "$seed/out/bin/$applet" ]; then
+190
echo "build-seed: REFUSING: required tool $applet is missing from the seed" >&2
+191
exit 1
+192
fi
+193
done
+194
# The builder assembles its sandbox with these; a seed that lost one would look
+195
# to the builder like a host that forbids user namespaces (see external.sgl
+196
# require-sandbox-tools!), so refuse here where the cause is visible.
+197
for applet in unshare mount chroot ip mkdir ln touch env; do
+198
if [ ! -e "$seed/out/bin/$applet" ]; then
+199
echo "build-seed: REFUSING: sandbox tool $applet is missing from the seed" >&2
+200
exit 1
+201
fi
+202
done
203
204
"$seed/scripts/package-seed.sh"
205
"$seed/out/bin/busybox" echo "seed binaries built for static musl target"
src/core/build-environments.sglmodified
@@ -8,4 +8,4 @@
8
(export core-buildenv)
9
(begin
10
(define core-buildenv
11
"894ca74bdb03c25d79f00ce915bbe25c56eba192334963c69e1846e6c84526cb")))
+11
"5a5f8b0d06f267cec431cadf57992ab056687bb21e7e8f7f6a68ee88ec4107e9")))