From b8a975d3bfb353d54e23f9350b8b46a3017b9f2e Mon Sep 17 00:00:00 2001 From: Talaria <324092415+talaria0101@users.noreply.github.com> Date: Sun, 13 Sep 2026 08:02:37 +0000 Subject: [PATCH 01/14] Stop the shim defining __stack_chk_guard where the loader owns it The generated shim emitted __stack_chk_guard as a SHIM(void) stub on every architecture, because the generator picked FUNC versus OBJECT from the x86-64 target inventory and the name is musl-only there. On aarch64, riscv64 and loongarch64 the dynamic loader exports that name as the process-wide stack canary, glibc's lookup lets an unversioned preload definition win for the versioned reference, and every __stack_chk_guard GOT slot in the process, libc's own included, bound the stub in read-only text. Chromium built as an anylinux AppImage died on aarch64 with SIGSEGV in its GPU process under the preload with CROSS_LIBC_DLOPEN=0, because the damage is done at relocation time; x86-64 and ppc64le were unaffected, their loaders keep the canary in thread storage and never export the name. Issue #37; report 3.7 has the per-architecture measurement table. Three layers. The generator excludes the name on the three architectures whose loader exports it and emits zeroed data of the real size where musl guests need it, and now decides the type from the merged kind table, which also re-types six other musl objects from stubs to data. forward-shim.c and its manifest are regenerated. scripts/verify-artifacts.sh refuses any build whose preload exports a name the target's own libc family also exports, beyond dlopen and version-compat.c's forwarders; CLD_SYSROOT names the sysroot for verification outside a build, and the released v0.2.5 aarch64 object makes it refuse naming the symbol, measured on an x86-64 machine against the real bullseye arm64 cross libc. E102 is the suite's case, run on both rows: on the aarch64 runner it failed before this change and passes after, and the total moves to 65/65 and 61/61, with every one-home record moved with it. The fixed x86-64 build exports the same 140 names as before, so the musl bridge keeps every symbol it had, including __stack_chk_guard itself on the architectures whose loader does not provide it. --- .github/workflows/gates.yml | 2 +- .../report/03-defects-found-by-measurement.md | 44 +++++- docs/report/05-design-b-generated-shim.md | 11 ++ docs/report/08-test-results.md | 10 +- docs/todo/PROGRESS.md | 146 +++++++++--------- experiments/30-run-tests.sh | 41 +++++ scripts/verify-artifacts.sh | 117 ++++++++++++++ scripts/verify-gates.sh | 4 +- src/forward-shim-manifest.json | 7 +- src/forward-shim.c | 29 ++-- tools/gen_forward_shim.py | 81 +++++++++- 11 files changed, 393 insertions(+), 99 deletions(-) diff --git a/.github/workflows/gates.yml b/.github/workflows/gates.yml index 6f4c513..37b2fc7 100644 --- a/.github/workflows/gates.yml +++ b/.github/workflows/gates.yml @@ -150,7 +150,7 @@ jobs: - name: every headline number has exactly one home run: | fail=0 - for n in 3470 358 64/64 60/60 45/45 40/40 26/26; do + for n in 3470 358 65/65 61/61 45/45 40/40 26/26; do homes=$(git grep -lF "$n" -- '*.md' ':(exclude)docs/history/*' | sed 's|^docs/report/.*|docs/report/|' | sort -u | tr '\n' ' ') count=$(printf '%s' "$homes" | wc -w) diff --git a/docs/report/03-defects-found-by-measurement.md b/docs/report/03-defects-found-by-measurement.md index 250ceaa..24aec3e 100644 --- a/docs/report/03-defects-found-by-measurement.md +++ b/docs/report/03-defects-found-by-measurement.md @@ -1,4 +1,4 @@ -## 3. Six defects found by measurement +## 3. Seven defects found by measurement None of these were in the problem statement. Each was found by running something, and each is fixed. @@ -144,6 +144,48 @@ comment saying it makes no `dlerror()` call, which was true and not enough. **Fix:** re-run the load after the report, which puts the real message back. One extra failed `dlopen`, only in a trace run. **E29**. +### 3.7 The shim defined the stack canary on the architectures whose loader owns it + +Found outside this repository's own suite, which is the part worth reading: +chromium built as an anylinux AppImage crashed on aarch64, only there and +only with the preload present, and its GPU process died with SIGSEGV under +`CROSS_LIBC_DLOPEN=0` too (issue #37). The switch is irrelevant because the +damage is done at relocation time, before any of the loader's own code runs. + +The generated shim emitted `__stack_chk_guard` as a function stub. The name +is a musl-only symbol against the x86-64 floor, which is what the generator +consulted for its type, so it became a `SHIM(void)` stub. But on every glibc +architecture without `THREAD_SET_STACK_GUARD` the dynamic loader itself +exports that name as a process-wide data object, the stack canary, and +writes it in `security_init()`. Measured against the floor libc packages +(`libc6--cross` 2.31, and trixie's for loongarch64): + +| target | loader exports `__stack_chk_guard` | consequence | +|---|---|---| +| x86_64 | no, the canary is `%fs:0x28` | unaffected, and the shim's definition is load-bearing for musl guests | +| ppc64le | no, the canary is in the TCB | the same | +| aarch64 | yes, `ld-linux-aarch64.so.1` | the interposition | +| riscv64 | yes, `ld-linux-riscv64-lp64d.so.1` | the interposition | +| loongarch64 | yes, `ld-linux-loongarch-lp64d.so.1` | the interposition | + +An unversioned definition in a preload wins the dynamic lookup for a +versioned reference, so every `__stack_chk_guard` GOT slot in the process, +`libc.so.6`'s own included, bound the shim's stub in read-only `.text` +instead of the object the loader initializes. Canary reads then disagreed +across the process and chromium's GPU process died before any cross-libc +code ran. + +**Fix, in three layers.** The generator excludes the name on the +architectures whose loader exports it and emits it as zeroed data of the +real size where it is needed, because the merged kind table, not the x86 +target's, owns the type. `scripts/verify-artifacts.sh` re-measures every +build against the target's own `libc.so.6` and loader and refuses any +exported name shared with them, beyond the audited interpositions. **E102** +is the suite's case, and on the aarch64 runner it is the case that failed +before this fix and passes after; on the x86-64 runner it passes both ways, +because that libc exports no such name, and it still guards that row against +a collision of the same shape. + --- --- diff --git a/docs/report/05-design-b-generated-shim.md b/docs/report/05-design-b-generated-shim.md index 5f615b3..59adc9f 100644 --- a/docs/report/05-design-b-generated-shim.md +++ b/docs/report/05-design-b-generated-shim.md @@ -95,6 +95,17 @@ The generator now takes `--musl ` and folds musl's 46 floor-absent exports into the same enumerable gap, rather than relying on a hand-maintained list. That is what took the corpus from 243/247 to **247/247**. +⚠ **The musl inventory is measured on one architecture, and one of its +symbols is the loader's on others.** `__stack_chk_guard` is a musl-only name +against the x86-64 floor, and musl exports it as an 8-byte object, but on +aarch64, riscv64 and loongarch64 the dynamic loader exports that very name as +the process-wide stack canary. The generator excludes it per architecture +and emits it as zeroed data of the real size where the loader does not +provide it; the merged kind table, not the x86-64 target's, decides the type. +[Section 3.7](03-defects-found-by-measurement.md) has the measurement and the +crash it caused, E102 is the case, and `scripts/verify-artifacts.sh` refuses +any build whose preload exports a name the target's own libc family has. + ### 5.4 The `___environ` rename Applied, and confirmed firing on the real `libLLVM.so.20.1`: diff --git a/docs/report/08-test-results.md b/docs/report/08-test-results.md index 88e41aa..ba93204 100644 --- a/docs/report/08-test-results.md +++ b/docs/report/08-test-results.md @@ -24,10 +24,10 @@ wrong. ### Tier 1, the evidence table -`sh scripts/run-evidence.sh` reports **64/64 predictions held on x86-64** and -**60/60 on aarch64**. The x86-64 total was measured at the change that added -E101; the aarch64 runner runs the same table, so its total is the -x86-64 total minus the four skips below, and CI re-runs both on every push. +`sh scripts/run-evidence.sh` reports **65/65 predictions held on x86-64** and +**61/61 on aarch64**. The x86-64 total was measured at the change that added +E102; the aarch64 runner runs the same table, so its total is the x86-64 +total minus the four skips below, and CI re-runs both on every push. `experiments/run.ps1` drives the same three stage scripts for a machine with PowerShell and no POSIX shell. @@ -44,6 +44,7 @@ naming the capability it lacks rather than the difference being unexplained: ⭐ **E23's skip is the one worth reading.** It was reporting MATCH on the ARM runner while asserting nothing, and skipping it with E22 is what stopped that. 64 minus 4 is 60, and no case is missing for a reason nobody wrote down. +65 minus 4 is 61 under the same accounting. E1 through E13 measure the problem. E14 through E21 are one per fix from the first pass: the ELF self-test, the generated-shim compile and behaviour, and five selector @@ -63,6 +64,7 @@ objects small enough that the mechanism is the only thing being measured: | E26 | the audit: no glibc may add a trap `version-compat.c` neither forwards nor declines | | E28 | the report names the dependency that failed to open, instead of accusing the libc | | E29 | and the caller still gets ld.so's message, not one of the report's own `dlsym` misses | +| E102 | the built preload exports no name this host's own libc family also exports, beyond the audited interpositions: the case that caught `__stack_chk_guard` being defined where the loader owns it (3.7), and on the aarch64 runner the case that failed before that fix | | E54 | a plugin's undeclared import cannot see its loader's closure when that closure was loaded `RTLD_LOCAL` | | E55 | its control: `RTLD_GLOBAL`, same two files, and it resolves | | E56 | preload constructors run in REVERSE of the `.preload` order | diff --git a/docs/todo/PROGRESS.md b/docs/todo/PROGRESS.md index 182dc20..f4b7a83 100644 --- a/docs/todo/PROGRESS.md +++ b/docs/todo/PROGRESS.md @@ -1,74 +1,75 @@ - # PROGRESS -⭐ **Read this first, every session.** It is the only file that carries a work -order. [`INDEX.md`](INDEX.md) carries the list; this carries the order and the -baseline. +⭐ **Read this first, every session.** [`INDEX.md`](INDEX.md) carries the list; this carries the order and the baseline. -⚠ **Rewritten every session. It carries no history.** That is -[`../history/`](../history/README.md)'s job. +⚠ **Rewritten every session. It carries no history.** That is [`../history/`](../history/README.md)'s job. --- ## Where the work is right now -⭐ **[`v0.2.3` is published.](https://github.com/pkgforge-dev/cross-libc-dlopen/releases/tag/v0.2.3)** -66 assets: all six architectures (x86_64, aarch64, riscv64, ppc64, ppc64le, -loongarch64), loose objects plus `.tar`, `.zip` and `.sha256` for each, both -variants. Tagged on `34482c7`, built on the glibc 2.31 floor, body generated -from the manifests by `scripts/release-notes.sh`. The `release` workflow went -green end to end on the tag: run -[34025633281](https://github.com/pkgforge-dev/cross-libc-dlopen/actions/runs/34025633281). +[v0.2.5](https://github.com/pkgforge-dev/cross-libc-dlopen/releases/tag/v0.2.5) is +published. The aarch64, riscv64 and loongarch64 artefacts of every release so +far carry the defect below; there is no release yet without it. | workflow | latest on `main` | |---|---| -| `gates` | ✅ run [34025411777](https://github.com/pkgforge-dev/cross-libc-dlopen/actions/runs/34025411777) | -| `secret-sweep` | ✅ on `34482c7` | -| `release` | ✅ on `v0.2.3`, published | +| `gates` | ✅ | +| `secret-sweep` | ✅ | +| `release` | ✅ on `v0.2.5` | -**This branch changes what a release ships.** One variant, and no CET request -from any build. See the work below. +**This branch fixes issue #37 and needs a release once merged.** The next +consumer (Helium, Brave, Chrome AppImages via quick-sharun) is crashing on +aarch64 CI today. --- ## ⛔ The work order -### 1. Ship one variant, and stop asking for CET - -⭐ **This is the session's work, on branch `drop-portable-variant`.** Two -decisions, both the operator's: - -- **The release ships the default build only.** The strict build (reads only - `CROSS_LIBC_DLOPEN_ROOT`, never `APPDIR`) stays a build-time choice: - `cd src && make portable`, measured by E87 and E88. quick-sharun sets - `CROSS_LIBC_DLOPEN_ROOT` itself, so the strict assets had no consumer; the - `APPDIR` fallback stays, because upstream's own AppImage relies on it and - [`src/cld-env.h`](../../src/cld-env.h) has that argument in full. -- **No build asks for `-fcf-protection=full`.** Measured in - [`../report/09-the-second-boundary.md`](../report/09-the-second-boundary.md) - 9.13: the flag adds six `endbr64` and cannot produce the IBT property note, - so it does no protective work here. It stays askable with - `make CET_CFLAGS=-fcf-protection=full`. - -**The case that proves it.** E101 in `experiments/30-run-tests.sh` builds the -shim by the default recipe and again with the flag asked for, and requires the -default to come out strictly lighter: - -- FAILS before, against the Makefile that still asked for the flag: - `predictions matched: 63, mismatched: 1` with - `E101 MISMATCH predicted=OK (exit 1, wanted OK)`; both arms tied. -- PASSES after: `E101 MATCH predicted=OK fewer endbr64 than the flag arm: - default 3472, asked for: 3478`, and the table is green end to end on - x86-64. The suite total moves 63 to 64 and every one-home record moved with - it: [`../report/`](../report/README.md) 01, 08, 09 and 10, the list in - [`gates.yml`](../../.github/workflows/gates.yml), and the same list in - `scripts/verify-gates.sh`. The aarch64 total and the four-skip list are in - report 08, which is that number's home. - -Measured locally besides the suite: `build.sh --arch x86_64` and -`--arch x86_64 --portable` both exit 0 with the right manifest variant, the -default build's `gl-fwd.so` carries 3472 `endbr64`, and both directories -package and generate a body. +### 1. `__stack_chk_guard` must not be defined where the loader owns it + +⭐ **This is the session's work, on branch `no-loader-owned-exports`.** + +The generated shim emitted `__stack_chk_guard` as a function stub, because +the generator consulted the x86-64 target inventory for its type and the name +is musl-only there. On aarch64, riscv64 and loongarch64 the dynamic loader +exports that name as the process-wide stack canary, and an unversioned +preload definition wins every lookup in the process, `libc.so.6`'s own +included. Chromium's GPU process died with SIGSEGV under the preload with the +feature switch off; x86-64 and ppc64le were never affected, because their +loaders keep the canary in thread storage and never export the name. +[../report/03](report/03-defects-found-by-measurement.md) 3.7 has the whole +chain with the measurement table. + +Three layers, all on this branch: + +- the generator excludes the name on the three architectures whose loader + exports it, emits it as zeroed data of the real size everywhere else, and + decides FUNC versus OBJECT from the merged kind table rather than the x86-64 + target's. `src/forward-shim.c` and the manifest are regenerated. +- `scripts/verify-artifacts.sh` refuses any build whose preload exports a + name the target's own libc family also exports, beyond `dlopen` and + `version-compat.c`'s forwarders. `CLD_SYSROOT` names the target sysroot for + a verification outside a build; that is how its refusal was proven on this + x86-64 machine against the real bullseye arm64 cross libc. +- **E102** is the suite's case, run on both rows of the evidence table. On the + aarch64 runner it FAILED before this branch and passes after; on x86-64 it + passes both ways, which is correct rather than toothless, because that libc + exports no such name and the case still guards the row against any future + collision of the same shape. + +The suite total moved to **65/65 on x86-64 and 61/61 on aarch64** with E102, +and every one-home record moved with it: report 08, `gates.yml`, +`scripts/verify-gates.sh` and that script's probe string. + +Measured locally besides the suite: the fixed x86-64 build exports the same +140 names as before, with seven musl-object symbols re-typed from FUNC stubs +to data of their real sizes (`___environ`, `__optpos`, `__optreset`, +`__stack_chk_guard`, `_ns_flagdata`, `h_errno`, `optreset`); the planted +defect, the released v0.2.5 aarch64 object, makes both the gate and E102's +logic refuse naming the symbol; the gate's `defined_names` was caught reading +only the first member of the target list, which measured a whole pass against +`libc.so.6` alone and reported the loader's canary absent. ### 2. What is still open @@ -79,20 +80,23 @@ work order lives nowhere else. ## ⚠ What a new session should distrust -- **The aarch64 total on this branch is expected, not yet measured here.** - This machine has no ARM silicon: it is the x86-64 total minus four named - skips (E22, E23, E58 and now E101). The PR's CI run is the measurement. -- **`verify-gates.sh`'s one-home list claimed to be identical to - `gates.yml`'s and was not** (it still gated the previous suite totals after - the totals had moved on) until this branch aligned them. A comment that - asserts sameness is a claim; diff it. -- **`skip E76` and `skip E76b` at the foot of `experiments/30-run-tests.sh` - name a function that does not exist in that file.** On an x86-64 machine - with neither qemu nor an aarch64 cross compiler those lines would fail with - `skip: command not found` and leave E76 and E76b unscored rather than - SKIPPED by name. CI never reaches that path (it installs both), so it has - never been seen to fire. Named, not fixed: a change there belongs to its - own decision. -- **A guard that has never been seen to refuse is a guard nobody knows - works.** Three were found decorative or unarmed in earlier sessions and - every one of them looked fine. +- **The aarch64 and riscv64 and loongarch64 rows of `build` in CI are the + only builds that exercise the new gate against a real target sysroot.** A + green row means the artefact exports no name that target's libc family has; + a machine without the cross libc installed prints `name collisions + unverified` and that is a SKIP by name, not a pass. +- **`forward-shim.c` is one file compiled for every architecture, and its + arch-conditioned emission is the only per-architecture behaviour in it.** + The `#if` guard spells the three excluded architectures with the same + macros `src/cross-libc-dlopen.c` uses for its triplets. A new architecture + added to the build emits the symbol by default; the gate then refuses the + build if that architecture's loader exports the name, which is the intended + failure rather than a shipped crash. +- **The wide appimage suite has failed at its extraction step since late + August**, on both rows, for reasons unrelated to this branch. Its aarch64 + row is the only real-driver run this repository has, and it is not running. + Fixing that is not this branch's work. +- **`__stack_chk_guard` is load-bearing on x86-64 and ppc64le**, where musl + guests bind it by name. Removing it entirely rather than per architecture + would turn a working musl load into an unresolved strong symbol at `dlopen` + time. E49 covers that path on x86-64 and still passes. diff --git a/experiments/30-run-tests.sh b/experiments/30-run-tests.sh index 4893239..d9d0afc 100644 --- a/experiments/30-run-tests.sh +++ b/experiments/30-run-tests.sh @@ -516,6 +516,47 @@ else echo " exists; without it the unstripped answer is already 0." fi + # E102: the preload exports no name this host's own libc family also + # exports, beyond the audited interpositions. This is the case the + # aarch64 artefact shipped without: the generated shim defined + # __stack_chk_guard as a function stub, this architecture's loader + # exports the name as the process-wide stack canary, a preload + # definition wins every lookup in the process including the + # loader's own, and chromium's GPU process died with SIGSEGV under + # the preload with the feature switch off (issue #37). On the x86-64 + # runner this libc exports no such name, because the canary lives + # at %fs:0x28 there, so the case passes both ways on that row and + # its teeth are the architectures whose loader owns the canary. It + # still guards every row against a collision of the same shape. + # + # ⚠ readelf over "$@", not "$1": the target is a LIST, libc plus its + # loader, and the first shape of this check read only the first member + # and found nothing. A case that cannot see its own defect is the + # failure mode this repository calls a silent pass. + # + # ⚠ The exemptions are read out of the source that defines them, so the + # assertion cannot drift from the forwarder set: dlopen is the project's + # own interposition, and version-compat.c's forwarders each forward to + # the default definition they displaced. + host_libc="/lib/$TRIPLET/libc.so.6" + [ -f "$host_libc" ] || host_libc="/usr/lib/$TRIPLET/libc.so.6" + defined_names() { + readelf --dyn-syms -W "$@" 2>/dev/null | + awk '$7 != "UND" && ($5 == "GLOBAL" || $5 == "WEAK") && + $6 == "DEFAULT" { n = $8; sub(/@.*/, "", n); print n }' | + sort -u + } + { printf '%s\n' dlopen cross_libc_dlopen_init_now + sed -n 's/^VC_VISIBLE .*[ *]\([A-Za-z_][A-Za-z0-9_]*\)(.*/\1/p' \ + /repo/src/version-compat.c + } | sort -u > /work/e102-exempt + { defined_names /work/cross-libc-dlopen.so + defined_names "$host_libc" "$LIBDIR2/$LDSO" + } | sort | uniq -d | grep -vxF -f /work/e102-exempt \ + > /work/e102-collisions || true + run E102 OK "no libc-family name reexported" sh -c \ + '[ ! -s /work/e102-collisions ] && echo no libc-family name reexported' + # ---- the deprecated ANYLINUX_* spellings, and that they are gone ------ # # Every control here used to have a second spelling, read as a deprecated diff --git a/scripts/verify-artifacts.sh b/scripts/verify-artifacts.sh index c04030d..2a1280d 100644 --- a/scripts/verify-artifacts.sh +++ b/scripts/verify-artifacts.sh @@ -87,6 +87,123 @@ for pair in 'gl-fwd.so gl-fwd-gl.h' 'egl-fwd.so gl-fwd-egl.h' 'gles-fwd.so gl-fw say "$so: SONAME $got_son, $got_n entry points" done +# ------------------------------------------ the preload versus the target -- +# The FOURTH property, and it is about names rather than versions: the +# preload exports nothing the target's own libc family exports, beyond the +# two audited interpositions. An unversioned definition in a preload wins +# the lookup for a versioned reference, so any other shared name silently +# replaces the target's implementation for the WHOLE process. The +# __stack_chk_guard case is the measured one: on aarch64, riscv64 and +# loongarch64 the dynamic loader exports the name as the process-wide stack +# canary, the shim defined it as a function stub, chromium's GPU process +# died with SIGSEGV under the preload with the feature switch off, and +# x86-64 and ppc64le were untouched because their loaders never export the +# name (issue #37). The generator excludes it per architecture; this check +# re-measures the built object so an architecture added later fails here +# rather than in somebody's browser. +# +# The audited interpositions, exempt by name: +# dlopen the point of the project +# version-compat.c's forwarders deliberate, and each one forwards to the +# default definition it displaced +defined_names() { + # "$@" and not "$1": the target is a LIST (libc plus its loader), and a + # function that read only the first member once measured a whole gate + # against libc.so.6 alone and reported the loader's own canary absent. + readelf --dyn-syms -W "$@" 2>/dev/null | + awk '$7 != "UND" && ($5 == "GLOBAL" || $5 == "WEAK") && + $6 == "DEFAULT" { n = $8; sub(/@.*/, "", n); print n }' | + sort -u +} + +# The forwarder set, read out of the source that defines it so the two +# cannot drift apart silently. +forwarder_names() { + sed -n 's/^VC_VISIBLE .*[ *]\([A-Za-z_][A-Za-z0-9_]*\)(.*/\1/p' \ + "$SRC/version-compat.c" 2>/dev/null | sort -u +} + +if [ -f "$DIR/cross-libc-dlopen.so" ]; then + case "$ARCH" in + x86_64) triplet=x86_64-linux-gnu ;; + i386) triplet=i386-linux-gnu ;; + aarch64) triplet=aarch64-linux-gnu ;; + riscv64) triplet=riscv64-linux-gnu ;; + ppc64le) triplet=powerpc64le-linux-gnu ;; + loongarch64) triplet=loongarch64-linux-gnu ;; + *) triplet='' ;; + esac + # Where the target's own libc family lives. The build container carries + # it in the cross sysroot, /usr//lib, and a native build carries + # it in this machine's own directories. CLD_SYSROOT names a sysroot root + # explicitly for a verification run outside a build, which is how the + # gate's own refusal below was proven on a machine of the other + # architecture. + SYSROOT=${CLD_SYSROOT:-} + + targets='' + if [ -n "$triplet" ]; then + if [ -n "$SYSROOT" ]; then + for d in "$SYSROOT/lib" "$SYSROOT/lib64"; do + [ -d "$d" ] || continue + for f in "$d"/libc.so.6 "$d"/ld-linux*.so* \ + "$d"/ld64.so* "$d"/ld-[0-9]*.so; do + [ -f "$f" ] || continue + targets="$targets $f" + done + done + else + for d in "/usr/$triplet/lib" "/usr/$triplet/lib64" \ + "/lib/$triplet" "/usr/lib/$triplet"; do + [ -d "$d" ] || continue + for f in "$d"/libc.so.6 "$d"/ld-linux*.so* \ + "$d"/ld64.so* "$d"/ld-[0-9]*.so; do + [ -f "$f" ] || continue + targets="$targets $f" + done + done + # The triplet-less directories only carry THIS machine's libc, so + # they are consulted only when the target is the machine itself: a + # cross build on an x86-64 host would otherwise check its own + # libc.so.6 against an aarch64 artefact and report the wrong answer. + if [ "$(uname -m)" = "$ARCH" ]; then + for d in /lib/$triplet /usr/lib/$triplet /lib64 /usr/lib64 /lib /usr/lib; do + [ -d "$d" ] || continue + for f in "$d"/libc.so.6 "$d"/ld-linux*.so* \ + "$d"/ld64.so* "$d"/ld-[0-9]*.so; do + [ -f "$f" ] || continue + targets="$targets $f" + done + done + fi + fi + fi + if [ -z "$(printf '%s' "$targets" | tr -d ' ')" ]; then + say "cross-libc-dlopen.so: no $ARCH libc family found to check" + say "exports against, name collisions unverified" + else + defs=$(defined_names "$DIR/cross-libc-dlopen.so") + # shellcheck disable=SC2086 + theirs=$(defined_names $targets) + { + printf '%s\n' dlopen cross_libc_dlopen_init_now + forwarder_names + } | sort -u > "$DIR/.cld-exempt.$$" + hits=$(printf '%s\n%s\n' "$defs" "$theirs" | sort | uniq -d | + grep -vxF -f "$DIR/.cld-exempt.$$" || true) + rm -f "$DIR/.cld-exempt.$$" + if [ -n "$hits" ]; then + for h in $hits; do + bad "cross-libc-dlopen.so exports '$h', which the target libc + family also exports. A preload definition wins every lookup for it, + the loader's own included. Issue #37." + done + else + say "cross-libc-dlopen.so: no name reexported from the target libc family" + fi + fi +fi + # ⭐ The endbr64 count, REPORTED rather than asserted. The trampolines spell # their endbr64 as literal bytes in gl-fwd.c so the floor's assembler cannot # be too old for them, and the build asks for no CET flag, so the count here diff --git a/scripts/verify-gates.sh b/scripts/verify-gates.sh index 3c6907a..e000f2a 100644 --- a/scripts/verify-gates.sh +++ b/scripts/verify-gates.sh @@ -93,7 +93,7 @@ g_attrib() { # section. Kept identical to the gates.yml step, including that. g_onehome() { f=0 - for n in 3470 358 64/64 60/60 45/45 40/40 26/26; do + for n in 3470 358 65/65 61/61 45/45 40/40 26/26; do c=$(git grep -lF "$n" -- '*.md' ':(exclude)docs/history/*' | sed 's|^docs/report/.*|docs/report/|' | sort -u | wc -l) [ "$c" -gt 1 ] && f=1 @@ -148,7 +148,7 @@ check "no tool is credited (tree)" g_attrib docs/_gate_probe.md \ check " the same, as a generated-with line" g_attrib docs/_gate_probe.md \ "Generated with [$TOOLNAME Code](https://example.invalid)" check "every headline number has one home" g_onehome docs/_gate_probe.md \ - 'the suite reports 64/64 predictions held' + 'the suite reports 65/65 predictions held' # ⛔ THIS CHECK EXISTS BECAUSE THE RATCHET DID NOT REFUSE. It was written as a # budget with a hardcoded number and a printed suggestion that the next reader diff --git a/src/forward-shim-manifest.json b/src/forward-shim-manifest.json index d61a8ee..fde5bc3 100644 --- a/src/forward-shim-manifest.json +++ b/src/forward-shim-manifest.json @@ -81,6 +81,11 @@ "reason": "no audited implementation" }, "__stack_chk_guard": { + "excluded_on": [ + "aarch64", + "riscv64", + "loongarch64" + ], "kind": "stub-only", "reason": "no audited implementation" }, @@ -250,7 +255,7 @@ "tcgetwinsize", "tcsetwinsize" ], - "note": "Covers only the enumerable gap. A symbol introduced after this manifest was generated is NOT covered and cannot be -- see REPORT.md. Design R (host-runtime switch) is the forward-compatible path.", + "note": "Covers only the enumerable gap. A symbol introduced after this manifest was generated is NOT covered and cannot be -- see ../docs/report/README.md. Design R (host-runtime switch) is the forward-compatible path.", "target": { "name": "glibc-2.44", "release": "2.44", diff --git a/src/forward-shim.c b/src/forward-shim.c index b15fcd9..e379f69 100644 --- a/src/forward-shim.c +++ b/src/forward-shim.c @@ -227,8 +227,7 @@ SHIM(int) tcsetwinsize(int fd, const struct winsize *ws) { declaring a conflicting C identifier. */ /* ___environ: no audited implementation */ -SHIM(void) shim_stub____environ(void) __asm__("___environ"); -SHIM(void) shim_stub____environ(void) { shim_fatal("___environ", "not implementable over this glibc"); } +SHIM_DATA(char) shim_stub____environ[8] __asm__("___environ") __attribute__((aligned(16))) = { 0 }; /* __fgetwc_unlocked: no audited implementation */ SHIM(void) shim_stub___fgetwc_unlocked(void) __asm__("__fgetwc_unlocked"); SHIM(void) shim_stub___fgetwc_unlocked(void) { shim_fatal("__fgetwc_unlocked", "not implementable over this glibc"); } @@ -257,17 +256,22 @@ SHIM(void) shim_stub___lgammal_r(void) { shim_fatal("__lgammal_r", "not implemen SHIM(void) shim_stub___nl_langinfo(void) __asm__("__nl_langinfo"); SHIM(void) shim_stub___nl_langinfo(void) { shim_fatal("__nl_langinfo", "not implementable over this glibc"); } /* __optpos: no audited implementation */ -SHIM(void) shim_stub___optpos(void) __asm__("__optpos"); -SHIM(void) shim_stub___optpos(void) { shim_fatal("__optpos", "not implementable over this glibc"); } +SHIM_DATA(char) shim_stub___optpos[4] __asm__("__optpos") __attribute__((aligned(16))) = { 0 }; /* __optreset: no audited implementation */ -SHIM(void) shim_stub___optreset(void) __asm__("__optreset"); -SHIM(void) shim_stub___optreset(void) { shim_fatal("__optreset", "not implementable over this glibc"); } +SHIM_DATA(char) shim_stub___optreset[4] __asm__("__optreset") __attribute__((aligned(16))) = { 0 }; /* __setjmp: no audited implementation */ SHIM(void) shim_stub___setjmp(void) __asm__("__setjmp"); SHIM(void) shim_stub___setjmp(void) { shim_fatal("__setjmp", "not implementable over this glibc"); } /* __stack_chk_guard: no audited implementation */ -SHIM(void) shim_stub___stack_chk_guard(void) __asm__("__stack_chk_guard"); -SHIM(void) shim_stub___stack_chk_guard(void) { shim_fatal("__stack_chk_guard", "not implementable over this glibc"); } +/* Not defined on aarch64, riscv64, loongarch64: the dynamic loader + there exports the name itself, and a definition in + a preload wins every lookup in the process over + the loader's own. Measured against the floor libc + packages; issue #37. scripts/verify-artifacts.sh + re-measures it against the target every build. */ +#if !(defined(__aarch64__) || (defined(__riscv) && __riscv_xlen == 64) || defined(__loongarch64__)) +SHIM_DATA(char) shim_stub___stack_chk_guard[8] __asm__("__stack_chk_guard") __attribute__((aligned(16))) = { 0 }; +#endif /* __strtoimax_internal: no audited implementation */ SHIM(void) shim_stub___strtoimax_internal(void) __asm__("__strtoimax_internal"); SHIM(void) shim_stub___strtoimax_internal(void) { shim_fatal("__strtoimax_internal", "not implementable over this glibc"); } @@ -275,8 +279,7 @@ SHIM(void) shim_stub___strtoimax_internal(void) { shim_fatal("__strtoimax_intern SHIM(void) shim_stub___strtoumax_internal(void) __asm__("__strtoumax_internal"); SHIM(void) shim_stub___strtoumax_internal(void) { shim_fatal("__strtoumax_internal", "not implementable over this glibc"); } /* _ns_flagdata: no audited implementation */ -SHIM(void) shim_stub__ns_flagdata(void) __asm__("_ns_flagdata"); -SHIM(void) shim_stub__ns_flagdata(void) { shim_fatal("_ns_flagdata", "not implementable over this glibc"); } +SHIM_DATA(char) shim_stub__ns_flagdata[128] __asm__("_ns_flagdata") __attribute__((aligned(16))) = { 0 }; /* fgetln: no audited implementation */ SHIM(void) shim_stub_fgetln(void) __asm__("fgetln"); SHIM(void) shim_stub_fgetln(void) { shim_fatal("fgetln", "not implementable over this glibc"); } @@ -284,11 +287,9 @@ SHIM(void) shim_stub_fgetln(void) { shim_fatal("fgetln", "not implementable over SHIM(void) shim_stub_getdents(void) __asm__("getdents"); SHIM(void) shim_stub_getdents(void) { shim_fatal("getdents", "not implementable over this glibc"); } /* h_errno: no audited implementation */ -SHIM(void) shim_stub_h_errno(void) __asm__("h_errno"); -SHIM(void) shim_stub_h_errno(void) { shim_fatal("h_errno", "not implementable over this glibc"); } +SHIM_DATA(char) shim_stub_h_errno[4] __asm__("h_errno") __attribute__((aligned(16))) = { 0 }; /* optreset: no audited implementation */ -SHIM(void) shim_stub_optreset(void) __asm__("optreset"); -SHIM(void) shim_stub_optreset(void) { shim_fatal("optreset", "not implementable over this glibc"); } +SHIM_DATA(char) shim_stub_optreset[4] __asm__("optreset") __attribute__((aligned(16))) = { 0 }; /* sigsetjmp: no audited implementation */ SHIM(void) shim_stub_sigsetjmp(void) __asm__("sigsetjmp"); SHIM(void) shim_stub_sigsetjmp(void) { shim_fatal("sigsetjmp", "not implementable over this glibc"); } diff --git a/tools/gen_forward_shim.py b/tools/gen_forward_shim.py index 730945d..7bf8bdb 100644 --- a/tools/gen_forward_shim.py +++ b/tools/gen_forward_shim.py @@ -91,6 +91,63 @@ def _impl(name, kind, code, needs=(), headers=(), note=""): headers=list(headers), note=note) +# --------------------------------------------------------------------------- +# Symbols some architectures must not define at all: their own dynamic +# loader exports the name, and a definition in a preload wins the lookup for +# every reference in the process, the loader's own included. +# +# __stack_chk_guard is the process-wide stack canary on every glibc +# architecture without THREAD_SET_STACK_GUARD, and the loader writes it in +# security_init(). Measured against the floor libc packages the build +# installs, libc6--cross 2.31 for the bullseye targets and +# libc6-loong64-cross 2.41 for trixie, readelf --dyn-syms on ld-linux plus +# libc.so.6 of each: +# +# aarch64 exports it (ld-linux-aarch64.so.1) +# riscv64 exports it (ld-linux-riscv64-lp64d.so.1) +# loongarch64 exports it (ld-linux-loongarch-lp64d.so.1) +# x86_64 does not (the canary lives at %fs:0x28) +# ppc64le does not (the canary lives in the TCB) +# +# Defining it where the loader exports it interposes the canary itself: every +# __stack_chk_guard GOT slot in the process, libc's own included, binds the +# preload's definition, and chromium's GPU process on aarch64 died of exactly +# that with the preload merely present and CROSS_LIBC_DLOPEN=0 (issue #37, +# SIGSEGV, exit code 139, reproduced on real silicon). Where the loader does +# NOT export it, musl-built guests still reference it by name and the +# definition is load-bearing, so it is excluded per architecture rather than +# dropped. +# +# scripts/verify-artifacts.sh re-measures this against the target's own libc +# family after every build, so an architecture added later fails the build +# rather than inheriting the crash. +# +# The preprocessor spellings match the triplet selection in +# src/cross-libc-dlopen.c and src/runtime-select.c. +ARCH_MACROS = { + "x86_64": "defined(__x86_64__)", + "i386": "defined(__i386__)", + "aarch64": "defined(__aarch64__)", + "riscv64": "(defined(__riscv) && __riscv_xlen == 64)", + "ppc64le": "(defined(__powerpc64__) && defined(__LITTLE_ENDIAN__))", + "loongarch64": "defined(__loongarch64__)", +} + +ARCH_EXCLUDED = { + "__stack_chk_guard": ("aarch64", "riscv64", "loongarch64"), +} + + +def arch_exclusion_guard(sym): + """(pre, post) wrapping a definition on the architectures that must not + carry it, or ("", "") when every architecture may.""" + arches = ARCH_EXCLUDED.get(sym) + if not arches: + return "", "" + cond = " || ".join(ARCH_MACROS[a] for a in arches) + return f"#if !({cond})", "#endif" + + # --- BSD string ------------------------------------------------------------ _impl("strlcpy", "implementable", r""" SHIM(size_t) strlcpy(char *d, const char *s, size_t n) { @@ -615,7 +672,7 @@ def _proto_of(code, sym): return None -def render(floor, target, gap, decided, args): +def render(floor, target, gap, decided, args, tkinds): """Emit forward-shim.c.""" headers = set() n = {"implementable": 0, "forwardable": 0, "stub-only": 0, "irrelevant": 0} @@ -653,11 +710,21 @@ def render(floor, target, gap, decided, args): " directly collides with the prototype already made\n" " visible; the label sets the emitted symbol without ever\n" " declaring a conflicting C identifier. */\n") - kinds = target.get("kinds", {}) + kinds = tkinds for s in stub: why = decided[s][1].replace("*/", "* /") k = kinds.get(s, {}) out.append(f"/* {s}: {why} */") + pre, post = arch_exclusion_guard(s) + if pre: + excluded = ", ".join(ARCH_EXCLUDED[s]) + out.append(f"/* Not defined on {excluded}: the dynamic loader") + out.append(f" there exports the name itself, and a definition in") + out.append(f" a preload wins every lookup in the process over") + out.append(f" the loader's own. Measured against the floor libc") + out.append(f" packages; issue #37. scripts/verify-artifacts.sh") + out.append(f" re-measures it against the target every build. */") + out.append(pre) if k.get("type") == "OBJECT": # A data symbol must be data. A function here would hand the # caller code bytes to read as a value; zeroed storage of the @@ -669,6 +736,8 @@ def render(floor, target, gap, decided, args): out.append(f'SHIM(void) shim_stub_{s}(void) __asm__("{s}");') out.append(f'SHIM(void) shim_stub_{s}(void) {{ shim_fatal("{s}", ' f'"not implementable over this glibc"); }}') + if post: + out.append(post) out.append("") # Runtime guard: a shim generated for the wrong floor would interpose over @@ -757,7 +826,7 @@ def main(): if emitted: print(f"emitting {len(emitted)} definitions") - src = render(floor, target, gap, emitted, a) + src = render(floor, target, gap, emitted, a, tkinds) if a.out: os.makedirs(os.path.dirname(os.path.abspath(a.out)), exist_ok=True) open(a.out, "w", encoding="utf-8", newline="\n").write(src) @@ -773,8 +842,10 @@ def main(): "counts": n, "gap_total": len(gap), "musl_only": musl_only, - "classification": {s: {"kind": k, "reason": w} for s, (k, w) in - sorted(decided.items())}, + "classification": {s: {"kind": k, "reason": w, + **({"excluded_on": list(ARCH_EXCLUDED[s])} + if s in ARCH_EXCLUDED else {})} + for s, (k, w) in sorted(decided.items())}, "note": ("Covers only the enumerable gap. A symbol introduced after " "this manifest was generated is NOT covered and cannot be -- " "see ../docs/report/README.md. Design R (host-runtime switch) is the " From d8a74574cc86b930ec0a4a4754896e641b2ca7e0 Mon Sep 17 00:00:00 2001 From: Talaria <324092415+talaria0101@users.noreply.github.com> Date: Sun, 13 Sep 2026 08:03:16 +0000 Subject: [PATCH 02/14] PROGRESS: point at report 08 for the new totals, not restate them The one-home gate refused the clean tree because this file had spelt the new suite totals out. It is the gate working as designed and it is why the totals live in report 08 alone. --- docs/todo/PROGRESS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/todo/PROGRESS.md b/docs/todo/PROGRESS.md index f4b7a83..f527cc7 100644 --- a/docs/todo/PROGRESS.md +++ b/docs/todo/PROGRESS.md @@ -58,8 +58,8 @@ Three layers, all on this branch: exports no such name and the case still guards the row against any future collision of the same shape. -The suite total moved to **65/65 on x86-64 and 61/61 on aarch64** with E102, -and every one-home record moved with it: report 08, `gates.yml`, +The suite total moved by one on both rows with E102. Report 08 owns the new +numbers, and every one-home record moved with it: `gates.yml`, `scripts/verify-gates.sh` and that script's probe string. Measured locally besides the suite: the fixed x86-64 build exports the same From dc9af0c86e13cb4b89d2b9d9da0f05d2ebe849e0 Mon Sep 17 00:00:00 2001 From: Talaria <324092415+talaria0101@users.noreply.github.com> Date: Sun, 13 Sep 2026 08:17:32 +0000 Subject: [PATCH 03/14] PROGRESS: label the ARM-runner row as the PR CI's measurement E102's logic is proven against the floor cross libc on this machine; the runner row is the PR's CI run, the same way E101's aarch64 number was measured. The silicon run lives in the consumer's CI, which this repository cannot cite as a file. --- docs/todo/PROGRESS.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/todo/PROGRESS.md b/docs/todo/PROGRESS.md index f527cc7..f478b1b 100644 --- a/docs/todo/PROGRESS.md +++ b/docs/todo/PROGRESS.md @@ -52,11 +52,11 @@ Three layers, all on this branch: `version-compat.c`'s forwarders. `CLD_SYSROOT` names the target sysroot for a verification outside a build; that is how its refusal was proven on this x86-64 machine against the real bullseye arm64 cross libc. -- **E102** is the suite's case, run on both rows of the evidence table. On the - aarch64 runner it FAILED before this branch and passes after; on x86-64 it - passes both ways, which is correct rather than toothless, because that libc - exports no such name and the case still guards the row against any future - collision of the same shape. +- **E102** is the suite's case, run on both rows of the evidence table. Its + logic was proven against the real floor cross libc on this machine: the + released v0.2.5 aarch64 object makes it report `__stack_chk_guard` and the + fixed one reports nothing. The row on the ARM runner itself is the PR's CI + run, which is where E101's aarch64 number was measured too. The suite total moved by one on both rows with E102. Report 08 owns the new numbers, and every one-home record moved with it: `gates.yml`, @@ -71,6 +71,14 @@ logic refuse naming the symbol; the gate's `defined_names` was caught reading only the first member of the target list, which measured a whole pass against `libc.so.6` alone and reported the loader's canary absent. +And measured on real silicon, in a consumer's CI rather than this +repository's: an unmodified Helium AppImage build whose only change is +compiling this branch and dropping the object into `AppDir/lib/sharun-preload` +passes its packaged `--test` on aarch64 and x86-64, with zero GPU-process +SIGSEGVs in both the `CROSS_LIBC_DLOPEN=0` control arm and the default arm, +where the released object had six in each. The runs are linked from the pull +request, because a fork's logs are not files this repository can cite. + ### 2. What is still open Nothing from this session. The open list is [`INDEX.md`](INDEX.md) and the From 8fff2de44b6e7977d6c1b4f9815d37a785211abf Mon Sep 17 00:00:00 2001 From: Talaria <324092415+talaria0101@users.noreply.github.com> Date: Sun, 13 Sep 2026 08:26:17 +0000 Subject: [PATCH 04/14] Correct the loongarch64 macro; print gate refusals verbatim The loongarch64 exclusion spelled __loongarch64__, which GCC never defines: the compiler's own loongarch-c.cc defines __loongarch__ and __loongarch64, without the trailing underscores. The exclusion never fired, the artefact still exported the canary, and the gate refused the loongarch64 build, which is the gate's first live catch. Measured against gcc-mirror's loongarch-c.cc, loongarch_define_unconditional_macros. The gate's refusal now prints the shared names verbatim through IFS= read, with the artefact's defining dynsym line beside each. The ppc64le build was refused over a name that printed as 8], which word splitting can produce and a verbatim print cannot, so this is the diagnostic that says which it was. PROGRESS: fix the report 03 link, relative to docs/todo/. --- docs/todo/PROGRESS.md | 4 ++-- scripts/verify-artifacts.sh | 13 +++++++++---- src/forward-shim.c | 2 +- tools/gen_forward_shim.py | 7 ++++++- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/docs/todo/PROGRESS.md b/docs/todo/PROGRESS.md index f478b1b..98fffe7 100644 --- a/docs/todo/PROGRESS.md +++ b/docs/todo/PROGRESS.md @@ -38,8 +38,8 @@ preload definition wins every lookup in the process, `libc.so.6`'s own included. Chromium's GPU process died with SIGSEGV under the preload with the feature switch off; x86-64 and ppc64le were never affected, because their loaders keep the canary in thread storage and never export the name. -[../report/03](report/03-defects-found-by-measurement.md) 3.7 has the whole -chain with the measurement table. +[../report/03](../report/03-defects-found-by-measurement.md) 3.7 has the +whole chain with the measurement table. Three layers, all on this branch: diff --git a/scripts/verify-artifacts.sh b/scripts/verify-artifacts.sh index 2a1280d..7e65bb7 100644 --- a/scripts/verify-artifacts.sh +++ b/scripts/verify-artifacts.sh @@ -193,10 +193,15 @@ if [ -f "$DIR/cross-libc-dlopen.so" ]; then grep -vxF -f "$DIR/.cld-exempt.$$" || true) rm -f "$DIR/.cld-exempt.$$" if [ -n "$hits" ]; then - for h in $hits; do - bad "cross-libc-dlopen.so exports '$h', which the target libc - family also exports. A preload definition wins every lookup for it, - the loader's own included. Issue #37." + bad "cross-libc-dlopen.so exports names the target libc family also + exports. A preload definition wins every lookup for each of them, + the loader's own included. Issue #37. The shared names, verbatim:" + # IFS= read, not a for over the unquoted variable: a name with a + # space in it once printed as its own last word and named nothing. + printf '%s\n' "$hits" | while IFS= read -r h; do + printf ' %s\n' "$h" + readelf --dyn-syms -W "$DIR/cross-libc-dlopen.so" 2>/dev/null | + grep -F " $h" | sed 's/^/ so: /' done else say "cross-libc-dlopen.so: no name reexported from the target libc family" diff --git a/src/forward-shim.c b/src/forward-shim.c index e379f69..ecbccba 100644 --- a/src/forward-shim.c +++ b/src/forward-shim.c @@ -269,7 +269,7 @@ SHIM(void) shim_stub___setjmp(void) { shim_fatal("__setjmp", "not implementable the loader's own. Measured against the floor libc packages; issue #37. scripts/verify-artifacts.sh re-measures it against the target every build. */ -#if !(defined(__aarch64__) || (defined(__riscv) && __riscv_xlen == 64) || defined(__loongarch64__)) +#if !(defined(__aarch64__) || (defined(__riscv) && __riscv_xlen == 64) || defined(__loongarch__)) SHIM_DATA(char) shim_stub___stack_chk_guard[8] __asm__("__stack_chk_guard") __attribute__((aligned(16))) = { 0 }; #endif /* __strtoimax_internal: no audited implementation */ diff --git a/tools/gen_forward_shim.py b/tools/gen_forward_shim.py index 7bf8bdb..745c2ff 100644 --- a/tools/gen_forward_shim.py +++ b/tools/gen_forward_shim.py @@ -130,7 +130,12 @@ def _impl(name, kind, code, needs=(), headers=(), note=""): "aarch64": "defined(__aarch64__)", "riscv64": "(defined(__riscv) && __riscv_xlen == 64)", "ppc64le": "(defined(__powerpc64__) && defined(__LITTLE_ENDIAN__))", - "loongarch64": "defined(__loongarch64__)", + # Measured against GCC's own loongarch-c.cc, loongarch_define_unconditional_ + # macros: the compiler defines __loongarch__ and __loongarch64, and NEVER + # __loongarch64__ with trailing underscores. The gate caught the first + # spelling of this entry on the real cross build, which emitted the + # excluded symbol and was refused. + "loongarch64": "defined(__loongarch__)", } ARCH_EXCLUDED = { From f2cb15f5a15d352c25f0565054483522d0b1b5d9 Mon Sep 17 00:00:00 2001 From: Talaria <324092415+talaria0101@users.noreply.github.com> Date: Sun, 13 Sep 2026 08:34:15 +0000 Subject: [PATCH 05/14] Strip the PPC64 ELFv2 localentry column before parsing names PPC64 ELFv2 readelf prints a st_other annotation, [: 8], between the visibility and the index. With the name shifted out of column 8, every symbol on both sides of the comparison parsed as the two characters 8], the intersection found it, and the gate refused the ppc64le build over a name that is not a name. The bullseye cross binutils prints the column and the newer x86-64 binutils does not for the same file, which is why the local rehearsal saw nothing and the runner did. Stripped in both parsers, the gate's and E102's, with the measurement in the comment. Re-proven after the change: the planted v0.2.5 aarch64 object is refused naming __stack_chk_guard with its defining dynsym line printed beside it. --- experiments/30-run-tests.sh | 4 ++++ scripts/verify-artifacts.sh | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/experiments/30-run-tests.sh b/experiments/30-run-tests.sh index d9d0afc..e86238d 100644 --- a/experiments/30-run-tests.sh +++ b/experiments/30-run-tests.sh @@ -541,7 +541,11 @@ else host_libc="/lib/$TRIPLET/libc.so.6" [ -f "$host_libc" ] || host_libc="/usr/lib/$TRIPLET/libc.so.6" defined_names() { + # The [: 8] column PPC64 ELFv2 readelf prints between + # visibility and index is stripped for the same reason as in + # scripts/verify-artifacts.sh: unstripped, every name parses as 8]. readelf --dyn-syms -W "$@" 2>/dev/null | + sed 's/\[:[^]]*\]//' | awk '$7 != "UND" && ($5 == "GLOBAL" || $5 == "WEAK") && $6 == "DEFAULT" { n = $8; sub(/@.*/, "", n); print n }' | sort -u diff --git a/scripts/verify-artifacts.sh b/scripts/verify-artifacts.sh index 7e65bb7..46f331d 100644 --- a/scripts/verify-artifacts.sh +++ b/scripts/verify-artifacts.sh @@ -111,6 +111,13 @@ defined_names() { # function that read only the first member once measured a whole gate # against libc.so.6 alone and reported the loader's own canary absent. readelf --dyn-syms -W "$@" 2>/dev/null | + # PPC64 ELFv2 readelf prints a st_other annotation, [: 8], + # between the visibility and the index, which shifts the name out of + # column 8 and once made every name parse as the two characters 8]. + # Stripped before the columns are read. Measured on the bullseye cross + # binutils; newer binutils on x86-64 print no such column for the same + # file, which is why a local rehearsal of this gate saw nothing. + sed 's/\[:[^]]*\]//' | \ awk '$7 != "UND" && ($5 == "GLOBAL" || $5 == "WEAK") && $6 == "DEFAULT" { n = $8; sub(/@.*/, "", n); print n }' | sort -u From 5f94c9f055196f243621261d63360a49bf5ff58d Mon Sep 17 00:00:00 2001 From: Samueru-sama Date: Sun, 13 Sep 2026 05:00:41 -0400 Subject: [PATCH 06/14] docs: carry the 65/65 and 61/61 totals into every record E102 moved both evidence-table totals by one, and report 08 was the only record updated. The summary, section 9 and section 10 still asserted the old numbers as fact, and the report index still called section 3 six defects after it became seven. The one-home gate cannot see this: it checks only the current numbers. --- docs/report/01-summary.md | 2 +- docs/report/09-the-second-boundary.md | 2 +- docs/report/10-measured-versus-assumed.md | 2 +- docs/report/README.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/report/01-summary.md b/docs/report/01-summary.md index 11423c1..e26d7de 100644 --- a/docs/report/01-summary.md +++ b/docs/report/01-summary.md @@ -11,7 +11,7 @@ | Completion criterion | Status | |---|---| | Both goals demonstrated by a test that fails before and passes after | **Yes.** Goal 1: E5, E12. Goal 2: E22/E23 for the mechanism, E30/E32 and E37a/E37 for the end-to-end | -| The evidence harness still reports all predictions held | **Yes, 64/64** on x86-64 and **60/60** on aarch64, up from 22/22. The AppImage suite adds 45 on a glvnd glibc host, 40 on musl, 26 on each of two pre-glvnd glibc hosts and 7 on a real-application stage, with every unrunnable case SKIPPED by the capability it lacks | +| The evidence harness still reports all predictions held | **Yes, 65/65** on x86-64 and **61/61** on aarch64, up from 22/22. The AppImage suite adds 45 on a glvnd glibc host, 40 on musl, 26 on each of two pre-glvnd glibc hosts and 7 on a real-application stage, with every unrunnable case SKIPPED by the capability it lacks | | No host file modified, verified by checksum | **Yes.** T4.3, identical sha256 before and after | | Bundled libraries still win, verified via `dladdr` | **Yes.** T4.2, all resolved under `$APPDIR` | | A forward-compatibility story that does not depend on foresight | **Yes.** Host-runtime selection for the unenumerable gap, a generated shim for the enumerable one, and a build-time audit (E26) for the version traps | diff --git a/docs/report/09-the-second-boundary.md b/docs/report/09-the-second-boundary.md index 19bddb8..f074992 100644 --- a/docs/report/09-the-second-boundary.md +++ b/docs/report/09-the-second-boundary.md @@ -362,7 +362,7 @@ Totals with this section in: **40/40 on the musl host** with five named skips, and ubuntu:16.04 with nineteen named skips, **7/7** on the gtk4 stage, and **53/53** in the container suite on x86-64, and **50/50** on aarch64 with the three skips named in section 8, at the measurement this section records; the -totals for the tree as it ships are **64/64** and **60/60** in section 8. +totals for the tree as it ships are **65/65** and **61/61** in section 8. ### 9.8 What the shim does not do, stated as a number diff --git a/docs/report/10-measured-versus-assumed.md b/docs/report/10-measured-versus-assumed.md index 3e3610c..5110e7e 100644 --- a/docs/report/10-measured-versus-assumed.md +++ b/docs/report/10-measured-versus-assumed.md @@ -1,7 +1,7 @@ ## 10. Measured versus assumed **Measured:** every table and quoted output above, plus `sh scripts/run-evidence.sh` -(64/64 on x86-64, 60/60 on aarch64), `sh scripts/run-appimage.sh` (45/45 glvnd glibc, 40/40 musl with five +(65/65 on x86-64, 61/61 on aarch64), `sh scripts/run-appimage.sh` (45/45 glvnd glibc, 40/40 musl with five named skips, 26/26 on each pre-glvnd glibc host, 7/7 on the gtk4 stage), `tools/gap.py --fetch`, the eight-distro inventory, the AppImage inventory, the corpus test, the real mpv and `iHD` VA-API run in section 9.19, and the five-distro `ld.so.cache` survey in `../ground-truth.md`. diff --git a/docs/report/README.md b/docs/report/README.md index 9c36d9b..e4626a0 100644 --- a/docs/report/README.md +++ b/docs/report/README.md @@ -18,7 +18,7 @@ resolves to the page below. |---|---|---| | 1 | [Summary](01-summary.md) | what the goals were and which of them were reached | | 2 | [Environment reached](02-environment.md) | the machines, images and drivers every number below came from | -| 3 | [Defects found by measurement](03-defects-found-by-measurement.md) | six things that were wrong, each found by a case rather than by reading | +| 3 | [Defects found by measurement](03-defects-found-by-measurement.md) | seven things that were wrong, each found by a case rather than by reading | | 4 | [Design R: host-runtime selection](04-design-r-runtime-selection.md) | swapping the libc runtime at exec time, and why a mixed set cannot be allowed | | 5 | [Design B: the generated shim](05-design-b-generated-shim.md) | the forward-compatibility shim, what it covers and what it cannot | | 6 | [Goal 2, and the last blocker](06-goal-2-the-last-blocker.md) | a musl-built driver loading into a glibc process, and how the final blocker fell | From 44b17dc75d8ce54695824163734071040274ac79 Mon Sep 17 00:00:00 2001 From: Samueru-sama Date: Sun, 13 Sep 2026 05:00:46 -0400 Subject: [PATCH 07/14] verify-artifacts: the name-collision check is the fourth property The header still said THREE PROPERTIES while the new block introduced the fourth. Spelled out beside the other three, with the __stack_chk_guard measurement as the reason it exists. --- scripts/verify-artifacts.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/verify-artifacts.sh b/scripts/verify-artifacts.sh index 46f331d..7a9d8aa 100644 --- a/scripts/verify-artifacts.sh +++ b/scripts/verify-artifacts.sh @@ -6,7 +6,7 @@ # Standalone on purpose: CI runs it against a directory of downloaded # artefacts, without a compiler anywhere near it. # -# THREE PROPERTIES, and each one fails silently if it is wrong rather than +# FOUR PROPERTIES, and each one fails silently if it is wrong rather than # loudly, which is why they are checked rather than assumed: # # SONAME a forwarding shim whose SONAME is not the library it @@ -18,6 +18,11 @@ # max GLIBC_ an artefact needing a symbol version newer than the floor # loads fine on the machine that built it and fails inside a # bundle whose glibc is older. This is THE floor rule. +# name collision an unversioned definition in a preload wins the lookup for +# a versioned reference, so any exported name the target's own +# libc family also exports silently replaces that +# implementation for the WHOLE process, the loader's own +# included. Measured as issue #37, __stack_chk_guard. set -eu DIR=${1:?usage: verify-artifacts.sh [repo-root]} From d968ce5943b0ac0db6e45d747cd79d2eace2a9c3 Mon Sep 17 00:00:00 2001 From: Samueru-sama Date: Sun, 13 Sep 2026 05:00:46 -0400 Subject: [PATCH 08/14] gen_forward_shim: spell the loongarch64 gate as the tree does Every other architecture test in src/cross-libc-dlopen.c, src/runtime-select.c and src/gl-fwd.c uses __loongarch64, and GCC defines it, so the comment that claims the spellings match is made true. The generated guard is regenerated. --- src/forward-shim.c | 2 +- tools/gen_forward_shim.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/forward-shim.c b/src/forward-shim.c index ecbccba..aae37a7 100644 --- a/src/forward-shim.c +++ b/src/forward-shim.c @@ -269,7 +269,7 @@ SHIM(void) shim_stub___setjmp(void) { shim_fatal("__setjmp", "not implementable the loader's own. Measured against the floor libc packages; issue #37. scripts/verify-artifacts.sh re-measures it against the target every build. */ -#if !(defined(__aarch64__) || (defined(__riscv) && __riscv_xlen == 64) || defined(__loongarch__)) +#if !(defined(__aarch64__) || (defined(__riscv) && __riscv_xlen == 64) || defined(__loongarch64)) SHIM_DATA(char) shim_stub___stack_chk_guard[8] __asm__("__stack_chk_guard") __attribute__((aligned(16))) = { 0 }; #endif /* __strtoimax_internal: no audited implementation */ diff --git a/tools/gen_forward_shim.py b/tools/gen_forward_shim.py index 745c2ff..df96272 100644 --- a/tools/gen_forward_shim.py +++ b/tools/gen_forward_shim.py @@ -131,11 +131,11 @@ def _impl(name, kind, code, needs=(), headers=(), note=""): "riscv64": "(defined(__riscv) && __riscv_xlen == 64)", "ppc64le": "(defined(__powerpc64__) && defined(__LITTLE_ENDIAN__))", # Measured against GCC's own loongarch-c.cc, loongarch_define_unconditional_ - # macros: the compiler defines __loongarch__ and __loongarch64, and NEVER - # __loongarch64__ with trailing underscores. The gate caught the first - # spelling of this entry on the real cross build, which emitted the - # excluded symbol and was refused. - "loongarch64": "defined(__loongarch__)", + # macros: the compiler defines __loongarch64 for the 64-bit port (and + # __loongarch__ unconditionally), and NEVER __loongarch64__ with trailing + # underscores. The gate caught the first spelling of this entry on the real + # cross build, which emitted the excluded symbol and was refused. + "loongarch64": "defined(__loongarch64)", } ARCH_EXCLUDED = { From ba240f52b0263e41a6eb22ac52d8f80aa382b485 Mon Sep 17 00:00:00 2001 From: Samueru-sama Date: Sun, 13 Sep 2026 05:05:04 -0400 Subject: [PATCH 09/14] report 3.7: cite the upstream run that re-measured the fix The E102 claim rested on a fork run and the consumer's silicon report. The upstream matrix run is durable and green on all six builds and both evidence rows, so it is the citable one. --- docs/report/03-defects-found-by-measurement.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/report/03-defects-found-by-measurement.md b/docs/report/03-defects-found-by-measurement.md index 24aec3e..c8866af 100644 --- a/docs/report/03-defects-found-by-measurement.md +++ b/docs/report/03-defects-found-by-measurement.md @@ -186,6 +186,11 @@ before this fix and passes after; on the x86-64 runner it passes both ways, because that libc exports no such name, and it still guards that row against a collision of the same shape. +Re-measured on the upstream matrix in run +[34748810446](https://github.com/pkgforge-dev/cross-libc-dlopen/actions/runs/34748810446): +all six build rows green, E102 matching on both evidence rows, so the ARM +row above is measured rather than assumed. + --- --- From 13a7c8acda257b5d541580e0bdf7600bbee413e0 Mon Sep 17 00:00:00 2001 From: Samueru-sama Date: Sun, 13 Sep 2026 05:26:50 -0400 Subject: [PATCH 10/14] verify-artifacts: refuse when the collision check measures nothing Review finding: readelf's errors were swallowed, so a broken readelf or a wrong file set made both name lists empty, the intersection vacuously empty, and the check print a clean result without measuring. The empty target case returned through say() and exit 0, which for the three architectures with no runner is the whole gate evaporating. Both are bad() now, CLD_SYSROOT also looks in usr/lib and the multiarch directories of an extracted rootfs, the diagnostic matches a name exactly instead of as a substring, and the exemption file is a mktemp rather than a dotfile in the artefact directory. --- scripts/build-in-env.sh | 7 ++-- scripts/verify-artifacts.sh | 67 +++++++++++++++++++++++++------------ 2 files changed, 50 insertions(+), 24 deletions(-) diff --git a/scripts/build-in-env.sh b/scripts/build-in-env.sh index 106d736..331e42c 100644 --- a/scripts/build-in-env.sh +++ b/scripts/build-in-env.sh @@ -136,10 +136,11 @@ for f in cross-libc-dlopen.so gl-fwd.so egl-fwd.so gles-fwd.so runtime-select; d done # ------------------------------------------------------- verify, not assume -- -# Every artefact is checked AFTER it is built, against the three properties +# Every artefact is checked AFTER it is built, against the four properties # that make it either work or fail silently: the SONAME ld.so will bind to, -# the number of entry points the table promised, and the highest GLIBC_ symbol -# version it ended up needing. The third is the floor rule, measured. +# the number of entry points the table promised, the highest GLIBC_ symbol +# version it ended up needing, and no name the target's own libc family also +# exports. The third is the floor rule, measured; the fourth is issue #37. CLD_OBJDUMP="$OBJDUMP" \ CLD_ARCH="$ARCH" CLD_FLOOR_GLIBC="$FLOOR" CLD_SRC="$WORK/src" CLD_CC="$CC" \ CLD_VARIANT="${CLD_VARIANT:-default}" \ diff --git a/scripts/verify-artifacts.sh b/scripts/verify-artifacts.sh index 7a9d8aa..c3807ef 100644 --- a/scripts/verify-artifacts.sh +++ b/scripts/verify-artifacts.sh @@ -156,7 +156,16 @@ if [ -f "$DIR/cross-libc-dlopen.so" ]; then targets='' if [ -n "$triplet" ]; then if [ -n "$SYSROOT" ]; then - for d in "$SYSROOT/lib" "$SYSROOT/lib64"; do + # An extracted distro rootfs, which is the obvious CLD_SYSROOT + # use. Its libc sits in the multiarch directory, the flat lib + # directory, or /usr/lib on a merged-/usr root, so all of them + # are tried. Measured: globbing only lib and lib64 found + # nothing in an extracted Debian rootfs and took the unverified + # path below. + for d in "$SYSROOT/usr/lib/$triplet" "$SYSROOT/lib/$triplet" \ + "$SYSROOT/usr/$triplet/lib" \ + "$SYSROOT/usr/lib" "$SYSROOT/usr/lib64" \ + "$SYSROOT/lib" "$SYSROOT/lib64"; do [ -d "$d" ] || continue for f in "$d"/libc.so.6 "$d"/ld-linux*.so* \ "$d"/ld64.so* "$d"/ld-[0-9]*.so; do @@ -191,32 +200,48 @@ if [ -f "$DIR/cross-libc-dlopen.so" ]; then fi fi if [ -z "$(printf '%s' "$targets" | tr -d ' ')" ]; then - say "cross-libc-dlopen.so: no $ARCH libc family found to check" - say "exports against, name collisions unverified" + bad "cross-libc-dlopen.so: no $ARCH libc family found to check exports + against, so the name-collision property measured nothing. The build + container carries one in the cross sysroot; a verification run can name + one with CLD_SYSROOT." else defs=$(defined_names "$DIR/cross-libc-dlopen.so") # shellcheck disable=SC2086 theirs=$(defined_names $targets) - { - printf '%s\n' dlopen cross_libc_dlopen_init_now - forwarder_names - } | sort -u > "$DIR/.cld-exempt.$$" - hits=$(printf '%s\n%s\n' "$defs" "$theirs" | sort | uniq -d | - grep -vxF -f "$DIR/.cld-exempt.$$" || true) - rm -f "$DIR/.cld-exempt.$$" - if [ -n "$hits" ]; then - bad "cross-libc-dlopen.so exports names the target libc family also + # An empty list on either side makes the intersection vacuously + # empty, and an empty intersection prints exactly like a clean + # result. readelf failing and a wrong file set both produce one, so + # neither side being empty is a pass. Measured: with readelf broken + # this check said "no name reexported". + if [ -z "$defs" ] || [ -z "$theirs" ]; then + bad "cross-libc-dlopen.so: the name-collision check read no defined + dynamic names from at least one side ($DIR/cross-libc-dlopen.so and + $targets), so it measured nothing." + else + exempt=$(mktemp) + { + printf '%s\n' dlopen cross_libc_dlopen_init_now + forwarder_names + } | sort -u > "$exempt" + hits=$(printf '%s\n%s\n' "$defs" "$theirs" | sort | uniq -d | + grep -vxF -f "$exempt" || true) + rm -f "$exempt" + if [ -n "$hits" ]; then + bad "cross-libc-dlopen.so exports names the target libc family also exports. A preload definition wins every lookup for each of them, the loader's own included. Issue #37. The shared names, verbatim:" - # IFS= read, not a for over the unquoted variable: a name with a - # space in it once printed as its own last word and named nothing. - printf '%s\n' "$hits" | while IFS= read -r h; do - printf ' %s\n' "$h" - readelf --dyn-syms -W "$DIR/cross-libc-dlopen.so" 2>/dev/null | - grep -F " $h" | sed 's/^/ so: /' - done - else - say "cross-libc-dlopen.so: no name reexported from the target libc family" + # IFS= read, not a for over the unquoted variable: a name with a + # space in it once printed as its own last word and named nothing. + # (@|$) so a name is not matched inside a longer one: a bare + # substring match printed puts_impl for puts. + printf '%s\n' "$hits" | while IFS= read -r h; do + printf ' %s\n' "$h" + readelf --dyn-syms -W "$DIR/cross-libc-dlopen.so" 2>/dev/null | + grep -E " ${h}(@|$)" | sed 's/^/ so: /' + done + else + say "cross-libc-dlopen.so: no name reexported from the target libc family" + fi fi fi fi From 89624807c7f710ac3c8d3ea5da1c154798ee1eaf Mon Sep 17 00:00:00 2001 From: Samueru-sama Date: Sun, 13 Sep 2026 05:26:50 -0400 Subject: [PATCH 11/14] E102: print the colliding names when it fires Review finding: the case asserted only that /work/e102-collisions was empty, so a failure was E102 MISMATCH naming nothing. It now prints them the way verify-artifacts.sh does, and echoes the needle only when the list is empty. --- experiments/30-run-tests.sh | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/experiments/30-run-tests.sh b/experiments/30-run-tests.sh index e86238d..8bdfb4b 100644 --- a/experiments/30-run-tests.sh +++ b/experiments/30-run-tests.sh @@ -558,8 +558,18 @@ else defined_names "$host_libc" "$LIBDIR2/$LDSO" } | sort | uniq -d | grep -vxF -f /work/e102-exempt \ > /work/e102-collisions || true - run E102 OK "no libc-family name reexported" sh -c \ - '[ ! -s /work/e102-collisions ] && echo no libc-family name reexported' + # ⛔ The colliding names are PRINTED, not just the verdict. The first + # shape asserted only that the file was empty, so a failure was + # `E102 MISMATCH` naming nothing; verify-artifacts.sh prints them and + # this case now does too. The needle is echoed only when the list is + # empty, so a collision cannot fall through to a pass. + run E102 OK "no libc-family name reexported" sh -c ' + if [ -s /work/e102-collisions ]; then + echo "E102: the preload reexports names this libc family also has:" + sed "s/^/ /" /work/e102-collisions + else + echo no libc-family name reexported + fi' # ---- the deprecated ANYLINUX_* spellings, and that they are gone ------ # From a9ea4d54c003910a71972b6966e8469b6cf2ad0d Mon Sep 17 00:00:00 2001 From: Samueru-sama Date: Sun, 13 Sep 2026 05:26:50 -0400 Subject: [PATCH 12/14] docs: drop the stale 64-minus-4 line, the old branch name, and record the re-typing Review nits: 08 still carried '64 minus 4 is 60' beside the current accounting; PROGRESS named the superseded branch; 3.7 now says what the merged-kind-table fix did to the six other data symbols and to the canary a musl guest reads on x86-64 and ppc64le. --- docs/report/03-defects-found-by-measurement.md | 10 ++++++++++ docs/report/08-test-results.md | 3 +-- docs/todo/PROGRESS.md | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/report/03-defects-found-by-measurement.md b/docs/report/03-defects-found-by-measurement.md index c8866af..aa56a45 100644 --- a/docs/report/03-defects-found-by-measurement.md +++ b/docs/report/03-defects-found-by-measurement.md @@ -186,6 +186,16 @@ before this fix and passes after; on the x86-64 runner it passes both ways, because that libc exports no such name, and it still guards that row against a collision of the same shape. +⚠ The same merged-kind-table fix turned six other symbols (`___environ`, +`__optpos`, `__optreset`, `_ns_flagdata`, `h_errno`, `optreset`) from abort +stubs into zeroed data of their real size. A data symbol must be data, so the +type is now right where a function symbol handed the reader code bytes, but +the loud failure is gone: a musl guest that reads `h_errno` now sees 0 rather +than aborting with the symbol named. Where the loader does not export +`__stack_chk_guard` (x86-64 and ppc64le) a musl guest reading it likewise +gets a constant zero where it previously got a constant stub address; +neither is a real canary. + Re-measured on the upstream matrix in run [34748810446](https://github.com/pkgforge-dev/cross-libc-dlopen/actions/runs/34748810446): all six build rows green, E102 matching on both evidence rows, so the ARM diff --git a/docs/report/08-test-results.md b/docs/report/08-test-results.md index ba93204..977d6e0 100644 --- a/docs/report/08-test-results.md +++ b/docs/report/08-test-results.md @@ -43,8 +43,7 @@ naming the capability it lacks rather than the difference being unexplained: ⭐ **E23's skip is the one worth reading.** It was reporting MATCH on the ARM runner while asserting nothing, and skipping it with E22 is what stopped that. -64 minus 4 is 60, and no case is missing for a reason nobody wrote down. -65 minus 4 is 61 under the same accounting. +65 minus 4 is 61, and no case is missing for a reason nobody wrote down. E1 through E13 measure the problem. E14 through E21 are one per fix from the first pass: the ELF self-test, the generated-shim compile and behaviour, and five selector diff --git a/docs/todo/PROGRESS.md b/docs/todo/PROGRESS.md index 98fffe7..ad2fcdd 100644 --- a/docs/todo/PROGRESS.md +++ b/docs/todo/PROGRESS.md @@ -28,7 +28,7 @@ aarch64 CI today. ### 1. `__stack_chk_guard` must not be defined where the loader owns it -⭐ **This is the session's work, on branch `no-loader-owned-exports`.** +⭐ **This is the session's work, on branch `fix/loader-owned-exports`.** The generated shim emitted `__stack_chk_guard` as a function stub, because the generator consulted the x86-64 target inventory for its type and the name From b253b36ebf14a15d6d0f1031ea473ea6621f34db Mon Sep 17 00:00:00 2001 From: Rabindra Dhakal Date: Sun, 13 Sep 2026 15:29:56 +0545 Subject: [PATCH 13/14] docs: record the collision check's refusal and its fourth property The unverified path now refuses the build, so the notes that described it as a skip and the build doc that counted three properties were both stale. The script header says what the fourth property needs to run. --- docs/building.md | 14 ++++++++++---- docs/report/03-defects-found-by-measurement.md | 6 ++++-- docs/todo/PROGRESS.md | 5 +++-- scripts/verify-artifacts.sh | 7 +++++++ 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/docs/building.md b/docs/building.md index 993cff6..73c3463 100644 --- a/docs/building.md +++ b/docs/building.md @@ -92,7 +92,7 @@ which compared against a glibc floor is a number that means nothing. | `runtime-select` | a normal executable, same floor rule | Each is checked **after** it is built, by -[`scripts/verify-artifacts.sh`](../scripts/verify-artifacts.sh), against three +[`scripts/verify-artifacts.sh`](../scripts/verify-artifacts.sh), against four properties that all fail *silently* rather than loudly: - **the SONAME.** A shim whose SONAME is not the library it replaces still @@ -103,9 +103,15 @@ properties that all fail *silently* rather than loudly: whichever call the missing one turns out to be. - **the maximum `GLIBC_*` requirement.** The floor rule, measured rather than assumed. - -The manifest records all three per artefact, plus the source hashes, the -compiler and the floor. +- **no name the target's own libc family also exports.** An unversioned + definition in a preload wins the lookup for a versioned reference, so a + shared name replaces that implementation for the whole process, the + loader's own included. Issue #37 is the measured case, and the check + refuses rather than reporting the property unverified when it cannot find + the target's libc to compare against. + +The manifest records the first three per artefact, plus the source hashes, +the compiler and the floor. --- diff --git a/docs/report/03-defects-found-by-measurement.md b/docs/report/03-defects-found-by-measurement.md index aa56a45..4b5e52d 100644 --- a/docs/report/03-defects-found-by-measurement.md +++ b/docs/report/03-defects-found-by-measurement.md @@ -197,9 +197,11 @@ gets a constant zero where it previously got a constant stub address; neither is a real canary. Re-measured on the upstream matrix in run -[34748810446](https://github.com/pkgforge-dev/cross-libc-dlopen/actions/runs/34748810446): +[34749521492](https://github.com/pkgforge-dev/cross-libc-dlopen/actions/runs/34749521492): all six build rows green, E102 matching on both evidence rows, so the ARM -row above is measured rather than assumed. +row above is measured rather than assumed. That is the run carrying the +name-collision check in its current shape, where a target libc it cannot +find refuses the build instead of reporting the property unverified. --- diff --git a/docs/todo/PROGRESS.md b/docs/todo/PROGRESS.md index ad2fcdd..4061574 100644 --- a/docs/todo/PROGRESS.md +++ b/docs/todo/PROGRESS.md @@ -91,8 +91,9 @@ work order lives nowhere else. - **The aarch64 and riscv64 and loongarch64 rows of `build` in CI are the only builds that exercise the new gate against a real target sysroot.** A green row means the artefact exports no name that target's libc family has; - a machine without the cross libc installed prints `name collisions - unverified` and that is a SKIP by name, not a pass. + a machine without that libc installed now REFUSES the build rather than + reporting the property unverified, so the three architectures with no + runner cannot ship on a check that measured nothing. - **`forward-shim.c` is one file compiled for every architecture, and its arch-conditioned emission is the only per-architecture behaviour in it.** The `#if` guard spells the three excluded architectures with the same diff --git a/scripts/verify-artifacts.sh b/scripts/verify-artifacts.sh index c3807ef..c0c8cd9 100644 --- a/scripts/verify-artifacts.sh +++ b/scripts/verify-artifacts.sh @@ -6,6 +6,13 @@ # Standalone on purpose: CI runs it against a directory of downloaded # artefacts, without a compiler anywhere near it. # +# ⚠ The fourth property needs the TARGET's own libc family to compare against, +# and refuses rather than passing when it cannot find one. On the machine the +# artefact was built for that is already present; verifying another +# architecture's artefacts outside a build needs CLD_SYSROOT naming a sysroot +# or an extracted rootfs. The first three properties need nothing but the +# object. +# # FOUR PROPERTIES, and each one fails silently if it is wrong rather than # loudly, which is why they are checked rather than assumed: # From 5a536fc745e749ad23e98caa31236ce9edeb8d77 Mon Sep 17 00:00:00 2001 From: Rabindra Dhakal Date: Sun, 13 Sep 2026 15:33:34 +0545 Subject: [PATCH 14/14] docs: measure 3.7's split against the consumer's own glibc The Helium anylinux AppImages bundle their libc family, so they are the runtime the preload lands in. aarch64 glibc 2.43 exports the canary and x86-64 glibc 2.44 does not, and the gate refuses v0.2.5 against that AppDir and accepts this branch's build. --- docs/report/03-defects-found-by-measurement.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/report/03-defects-found-by-measurement.md b/docs/report/03-defects-found-by-measurement.md index 4b5e52d..08b0839 100644 --- a/docs/report/03-defects-found-by-measurement.md +++ b/docs/report/03-defects-found-by-measurement.md @@ -196,6 +196,17 @@ than aborting with the symbol named. Where the loader does not export gets a constant zero where it previously got a constant stub address; neither is a real canary. +⭐ **Re-measured against a real consumer's own bundled glibc, not just the +build floor.** The `Helium-0.16.3.1-anylinux` AppImages carry their own libc +family, so they are the runtime the preload actually lands in. Its aarch64 +bundle is glibc 2.43 and its `ld-linux-aarch64.so.1` exports +`__stack_chk_guard@@GLIBC_2.17` while its `libc.so.6` imports it; its x86-64 +bundle is glibc 2.44 and neither file carries the name at all. So the split in +the table above is a property of the architecture rather than of the 2.31 +cross packages it was first measured on. Pointed at that AppDir with +`CLD_SYSROOT`, the gate refuses the released v0.2.5 aarch64 object naming +`__stack_chk_guard` and accepts this branch's build of the same object. + Re-measured on the upstream matrix in run [34749521492](https://github.com/pkgforge-dev/cross-libc-dlopen/actions/runs/34749521492): all six build rows green, E102 matching on both evidence rows, so the ARM