Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/gates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion docs/report/01-summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
9 changes: 5 additions & 4 deletions docs/report/08-test-results.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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 |

Expand Down
39 changes: 38 additions & 1 deletion docs/report/09-the-second-boundary.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
2 changes: 1 addition & 1 deletion docs/report/10-measured-versus-assumed.md
Original file line number Diff line number Diff line change
@@ -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`.
Expand Down
123 changes: 41 additions & 82 deletions docs/todo/PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
20 changes: 19 additions & 1 deletion experiments/30-run-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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 -----------------------------"
Expand Down
4 changes: 2 additions & 2 deletions scripts/verify-gates.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading