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 1/5] 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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