AtlatestRepositorycore-channel

core-channel / tree / seed / scriptsbuild-seed.sh

1#!/bin/sh
2set -eu
3
4seed=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
5repo=$(CDPATH= cd -- "$seed/.." && pwd)
6work=/tmp/sigil-channel/seed-build
7rm -rf "$work"
8mkdir -p "$work"
9export ZIG_GLOBAL_CACHE_DIR="$work/zig-global-cache"
10export ZIG_LOCAL_CACHE_DIR="$work/zig-local-cache"
11export KCONFIG_NOTIMESTAMP=1
12export SOURCE_DATE_EPOCH=0
13jobs=${JOBS:-$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 2)}
14cc="$repo/toolchain/bin/x86_64-linux-musl-cc"
15ar="$repo/toolchain/bin/x86_64-linux-musl-ar"
16ranlib="$repo/toolchain/bin/x86_64-linux-musl-ranlib"
17copy_strip="$repo/toolchain/bin/copy-strip"
18map_flags="-g0 -ffile-prefix-map=$work=. -fdebug-prefix-map=$work=. -ffile-prefix-map=$repo=. -fdebug-prefix-map=$repo=."
20"$seed/scripts/verify-sources.sh"
21rm -rf "$seed/out"
22mkdir -p "$seed/out/bin"
24tar -C "$work" -xf "$seed/sources/make-4.4.1.tar.lz"
26 cd "$work/make-4.4.1"
27 # make_cv_sys_gnu_glob=no: configure's "checking if system libc has working
28 # GNU glob" answers YES against musl, and that is a FALSE POSITIVE. The check
29 # compiles and RUNS a glob program, which musl passes; but musl's `glob_t` has
30 # none of the GNU `gl_opendir`/`gl_readdir`/`gl_closedir`/`gl_lstat`/`gl_stat`
31 # members that make's `dir_setup_glob()` then assigns unconditionally, so the
32 # build dies with five "no member named" errors in src/dir.c.
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"
46make="$work/bootstrap-make"
48tar -C "$work" -xf "$seed/sources/busybox-1.37.0.tar.bz2"
50 cd "$work/busybox-1.37.0"
51 # Zig 0.16's linker rejects GNU ld's diagnostic-only --warn-common flag.
52 sed -i 's/-Wl,--warn-common //g; /-Wl,--warn-common \\/d; s@echo "-Wl,-Map,$EXE.map -Wl,--verbose"@echo ""@' scripts/trylink
53 "$make" defconfig HOSTCC="$cc"
54 sed -i 's/^# CONFIG_STATIC is not set$/CONFIG_STATIC=y/' .config
55 sed -i 's/^CONFIG_TC=y$/# CONFIG_TC is not set/' .config
56 sed -i 's/^CONFIG_EXTRA_LDFLAGS=""$/CONFIG_EXTRA_LDFLAGS="-s"/' .config
57 "$make" -j"$jobs" CC="$cc" HOSTCC="$cc" AR="$ar" RANLIB="$ranlib" STRIP="$copy_strip" KCFLAGS="$map_flags" HOSTCFLAGS="$map_flags"
58 cp busybox "$seed/out/bin/busybox"
62 cd "$work/make-4.4.1"
63 cp make "$seed/out/bin/make"
66tar -C "$work" -xf "$seed/sources/pkgconf-2.4.3.tar.xz"
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 \
122 --with-system-libdir=/usr/lib \
123 --with-system-includedir=/usr/include \
124 --disable-shared --enable-static --disable-dependency-tracking LDFLAGS="-static -s"
125 "$make" -j"$jobs"
126 cp pkgconf "$seed/out/bin/pkgconf"
129# GNU patch, replacing the busybox applet (§8 Q4, decided by David 2026-07-31).
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.
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)
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.
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.
152tar -C "$work" -xf "$seed/sources/patch-2.8.tar.xz"
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"
161ln -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.
168# `patch` is excluded for a different reason: GNU patch above owns that name.
169trim="wget nc telnetd httpd sendmail ftpd tftp"
170"$seed/out/bin/busybox" --list | while IFS= read -r applet; do
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"
174done
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.
180# Written as `if`, not `[ ... ] && { ... }`: a refusal must not depend on the
181# subtle `set -e` semantics of a failing AND-OR list.
182for 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
187done
188for 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
193done
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.
197for 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
202done
204"$seed/scripts/package-seed.sh"
205"$seed/out/bin/busybox" echo "seed binaries built for static musl target"