diff --git a/.github/workflows/gates.yml b/.github/workflows/gates.yml index 37b2fc7..beb58bd 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 65/65 61/61 45/45 40/40 26/26; do + for n in 3470 358 66/66 62/62 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/01-summary.md b/docs/report/01-summary.md index e26d7de..b5d6c03 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, 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 | +| The evidence harness still reports all predictions held | **Yes, 66/66** on x86-64 and **62/62** 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/08-test-results.md b/docs/report/08-test-results.md index 977d6e0..fb85a30 100644 --- a/docs/report/08-test-results.md +++ b/docs/report/08-test-results.md @@ -24,9 +24,9 @@ wrong. ### Tier 1, the evidence table -`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 +`sh scripts/run-evidence.sh` reports **66/66 predictions held on x86-64** and +**62/62 on aarch64**. The x86-64 total was measured at the change that added +E103; 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. @@ -43,7 +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. -65 minus 4 is 61, and no case is missing for a reason nobody wrote down. +66 minus 4 is 62, 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 @@ -75,6 +75,7 @@ objects small enough that the mechanism is the only thing being measured: | E72 | an entry point the target does not provide, CALLED: a line naming it, and zero returned | | E73 | the distinct-name call count an application can be measured by | | E75 / E75b | the shim finds a target in a directory only `/etc/ld.so.conf` names, and does not when the conf file is removed | +| E103 | a directory the host names first holds a wrong-class library: the shim skips it and finds the one that loads, instead of reporting no target | | E75c / E75d / E75e / E75f | a shim with no target serves each name from the provider behind it in the lookup order; the no-preload control; the log line pinning the fallthrough; the same in EAGER mode | | E76 / E76b | the aarch64 trampolines and resolver RUN, under qemu-user, forwarding and absent paths both | diff --git a/docs/report/09-the-second-boundary.md b/docs/report/09-the-second-boundary.md index f074992..28dcbc6 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 **65/65** and **61/61** in section 8. +totals for the tree as it ships are **66/66** and **62/62** in section 8. ### 9.8 What the shim does not do, stated as a number @@ -1496,6 +1496,43 @@ repaired tree holds all of them. --- +### 9.21 The host lookup stopped on a library the loader would refuse + +`glfwd_try_soname` accepted the first directory in which its own soname +existed, and `glfwd_each_dir` stopped the walk there. `ld.so` refuses a +candidate of the other ELF class before it reads anything else, so a host whose +own answer named a 32bit library directory before the 64bit one produced a +target that could not load, and the directory after it was never reached. The +shim reported no target and the application failed with its own message about +visuals. + +Reproduced by removing `libGL.so.1` from the host-drivers demo AppDir, so that +the shim takes the host path instead of the bundled dispatcher, and naming the +32bit directory first with the explicit handoff: + +``` +CROSS_LIBC_DLOPEN_GL_HOST_DIR=/usr/lib/i386-linux-gnu:/usr/lib/x86_64-linux-gnu \ +CROSS_LIBC_DLOPEN_DEBUG=1 ./AppDir/AppRun glxgears + + [cross-libc-dlopen.so] >> cross-libc dlopen failed: /usr/lib/i386-linux-gnu/libGL.so.1 + [gl-fwd.so] >> host /usr/lib/i386-linux-gnu/libGL.so.1 would not load: /usr/lib/i386-linux-gnu/libGL.so.1.7.0: wrong ELF class: ELFCLASS32 + [gl-fwd.so] >> libGL.so.1: no target; 358 of 3470 entry points fall through to the next provider in scope + [gl-fwd.so] >> ABSENT entry point called: glXChooseVisual -- no target on this host and no next provider for it; returning zero +Error: couldn't get an RGB, Double-buffered visual +``` + +The repair is `glfwd_wrong_class`: the candidate's ELF ident is read, and one +that is an ELF of the other class is skipped so the walk continues to the next +directory. A candidate that is not recognisable as an ELF is left to `dlopen` +exactly as before, so nothing that loads today stops being chosen. + +E103 is the case. It uses the five-name miniature of section O, with the +`libtgt.so` in the directory named first carrying the other EI_CLASS. Before the +repair the shim reports `no target`; after it, it reports the `libtgt.so` in the +directory named second and the probe passes. + +--- + --- [REPORT index](README.md) | [previous](08-test-results.md) | [next](10-measured-versus-assumed.md) diff --git a/docs/report/10-measured-versus-assumed.md b/docs/report/10-measured-versus-assumed.md index 5110e7e..cf597f6 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` -(65/65 on x86-64, 61/61 on aarch64), `sh scripts/run-appimage.sh` (45/45 glvnd glibc, 40/40 musl with five +(66/66 on x86-64, 62/62 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/todo/PROGRESS.md b/docs/todo/PROGRESS.md index 4061574..7bf71bb 100644 --- a/docs/todo/PROGRESS.md +++ b/docs/todo/PROGRESS.md @@ -8,104 +8,63 @@ ## Where the work is right now -[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. +[v0.2.6](https://github.com/pkgforge-dev/cross-libc-dlopen/releases/tag/v0.2.6) is +published. | workflow | latest on `main` | |---|---| | `gates` | ✅ | | `secret-sweep` | ✅ | -| `release` | ✅ on `v0.2.5` | +| `release` | ✅ on `v0.2.6` | -**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. +**This branch fixes the host lookup that stopped on a library the loader would +refuse, on branch `fix/gl-fwd-wrong-class`, and needs a release once merged. +`v0.2.6` was released from `main` before it, so no released artefact carries +the fix.** --- ## ⛔ The work order -### 1. `__stack_chk_guard` must not be defined where the loader owns it - -⭐ **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 -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. 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`, -`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. - -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. +### 1. `glfwd_try_soname` must not stop on a library the loader will refuse + +`glfwd_try_soname` took the first directory in which its own soname existed, and +`glfwd_each_dir` stopped the walk there. A candidate of the other ELF class +cannot load, so a host whose own answer named a 32bit library directory first +produced a target that could not load, and the directory after it was never +reached. Reproduced by removing `libGL.so.1` from the host-drivers demo AppDir, +which forces the host path, and naming the 32bit directory first with +`CROSS_LIBC_DLOPEN_GL_HOST_DIR`. + +The repair reads the candidate's ELF ident and skips a candidate of the other +class, so the walk continues. `docs/report/09-the-second-boundary.md` 9.21 has +the transcript, E103 is the case, and `docs/report/08-test-results.md` owns the +new totals. ### 2. What is still open -Nothing from this session. The open list is [`INDEX.md`](INDEX.md) and the -work order lives nowhere else. +`glfwd_try_vendor` accepts a matching filename without checking its class, so a +host whose only vendor library is the wrong class is reported as having one. It +is a different path, reached only when the bundle carries a dispatcher, and it +needs a bundled-dispatcher case to measure. Nothing in this session's change +touches it. --- ## ⚠ What a new session should distrust -- **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 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 - 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. +- **E103 forces the order through `/etc/ld.so.conf.d`, and a host whose own + answer puts the 64bit directory first never reaches the defect.** Debian 10 + is such a host: `libc6-i386` names `/usr/lib32` and `/lib32` in + `zz_i386-biarch-compat.conf`, which sorts after `x86_64-linux-gnu.conf`. So + the case pins the predicate, not a claim about any one distribution. +- **The host-drivers demo AppDir bundles the glvnd dispatcher, so the shim + takes the bundled path on a glvnd host and never reaches the host lookup.** + Removing `libGL.so.1` from that AppDir is what exposes the path, and the + deletion is a harness step rather than a change to any shipped AppImage. +- **A Qt6 GLX path does not link `libGL.so.1`.** `libqxcb-glx-integration.so` + and `libQt6Gui.so.6` need `libGLX.so.0` and `libOpenGL.so.0`, so which arm a + Qt application reaches depends on what else in its AppDir links + `libGL.so.1`. Check `readelf -d` before assuming. +- **`docs/todo/INDEX.md` carries the counts it carried before this session.** + Reconcile them if an entry moves. diff --git a/experiments/30-run-tests.sh b/experiments/30-run-tests.sh index 8bdfb4b..f4f5de5 100644 --- a/experiments/30-run-tests.sh +++ b/experiments/30-run-tests.sh @@ -1022,8 +1022,19 @@ echo "-- O. the shim asks the HOST where its libraries are ------------" # using the mechanism's own presence, not an env switch invented to disable it. mkdir -p /opt/cross-libc-unguessable-42 /etc/ld.so.conf.d cp libtgt.so /opt/cross-libc-unguessable-42/libtgt.so +# E103's decoy: the same library with EI_CLASS flipped, so it is an ELF of the +# other class and ld.so refuses it. A genuine ELF32 cannot be built on every +# runner (as --32 is x86 only), and the ident byte is what decides the class on +# all of them. +mkdir -p /opt/cross-libc-wrongclass-41 +cp libtgt.so /opt/cross-libc-wrongclass-41/libtgt.so +printf '\001' | dd of=/opt/cross-libc-wrongclass-41/libtgt.so bs=1 seek=4 conv=notrunc status=none [ -f /etc/ld.so.conf ] || printf 'include /etc/ld.so.conf.d/*.conf\n' > /etc/ld.so.conf confdir() { printf '/opt/cross-libc-unguessable-42\n' > /etc/ld.so.conf.d/zz-cross-libc.conf; } +# The same file with the wrong-class directory named first, which is what a +# host whose own answer orders its 32bit library directory before the 64bit one +# looks like. +confdir_decoy() { printf '/opt/cross-libc-wrongclass-41\n/opt/cross-libc-unguessable-42\n' > /etc/ld.so.conf.d/zz-cross-libc.conf; } noconf() { rm -f /etc/ld.so.conf.d/zz-cross-libc.conf; } # No CROSS_LIBC_DLOPEN_GL_HOST_DIR here: the whole question is whether the shim finds it # without being told, so the handoff that would tell it is left out. @@ -1032,6 +1043,13 @@ fwd_noenv() { env LD_PRELOAD="$PWD/tgt-fwd.so" CROSS_LIBC_DLOPEN_DEBUG=1 "$@"; } # E75: the directory is named ONLY by /etc/ld.so.conf.d, and the shim finds it. confdir run E75 OK "target /opt/cross-libc-unguessable-42/libtgt.so" fwd_noenv ./tramp2 +# E103: the same run with a directory the host names FIRST holding a +# wrong-class libtgt.so. The walk used to stop at the first name that +# existed, dlopen then refused that file, and the shim reported no target +# even though the next named directory held one that loads. The shim skips +# a candidate of the other class and keeps walking. +confdir_decoy +run E103 OK "target /opt/cross-libc-unguessable-42/libtgt.so" fwd_noenv ./tramp2 # E75b: the control that must FAIL. Same directory, same library, same # binary, with the conf file gone, so nothing names it and the shim comes up # empty. Without this, E75 would also pass if the shim had simply @@ -1131,7 +1149,7 @@ run E75f OK "OK: next-provider ints=205" \ env LD_PRELOAD="$PWD/tgt-fwd.so" CROSS_LIBC_DLOPEN_GL_EAGER=1 ./nextprov # Left REMOVED, not restored: section P runs after this one and its aarch64 # shim would otherwise find the x86-64 libtgt.so through this very conf file. -rm -rf /opt/cross-libc-unguessable-42 +rm -rf /opt/cross-libc-unguessable-42 /opt/cross-libc-wrongclass-41 echo echo "-- P. the aarch64 trampolines, RUN -----------------------------" diff --git a/scripts/verify-gates.sh b/scripts/verify-gates.sh index e000f2a..68d8fad 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 65/65 61/61 45/45 40/40 26/26; do + for n in 3470 358 66/66 62/62 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 65/65 predictions held' + 'the suite reports 66/66 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/gl-fwd.c b/src/gl-fwd.c index 85e9064..febb911 100644 --- a/src/gl-fwd.c +++ b/src/gl-fwd.c @@ -58,6 +58,8 @@ #endif #include #include +#include +#include #include #include #include @@ -868,12 +870,26 @@ struct glfwd_soname_lookup { char path[PATH_MAX]; }; +/* A candidate of the other ELF class cannot load, and the walk would stop on + * it before reaching the directory that can answer. Skip one, and leave a + * candidate that is not recognisable as an ELF to dlopen as before. */ +static int glfwd_wrong_class(const char *path) { + unsigned char id[EI_NIDENT]; + int fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) + return 0; + ssize_t n = read(fd, id, sizeof id); + close(fd); + return n == (ssize_t)sizeof id && memcmp(id, ELFMAG, SELFMAG) == 0 && + id[EI_CLASS] != (sizeof(void *) == 8 ? ELFCLASS64 : ELFCLASS32); +} + static int glfwd_try_soname(const char *dir, void *ctx) { struct glfwd_soname_lookup *c = ctx; char buf[PATH_MAX]; if (snprintf(buf, sizeof buf, "%s/%s", dir, c->name) >= (int)sizeof buf) return 0; - if (access(buf, R_OK) != 0) + if (access(buf, R_OK) != 0 || glfwd_wrong_class(buf)) return 0; snprintf(c->path, sizeof c->path, "%s", buf); return 1;