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 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)
Expand Down
14 changes: 10 additions & 4 deletions docs/building.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ which compared against a glibc floor is a number that means nothing.
| `runtime-select` | a normal executable, same floor rule |

Each is checked **after** it is built, by
[`scripts/verify-artifacts.sh`](../scripts/verify-artifacts.sh), against three
[`scripts/verify-artifacts.sh`](../scripts/verify-artifacts.sh), against four
properties that all fail *silently* rather than loudly:

- **the SONAME.** A shim whose SONAME is not the library it replaces still
Expand All @@ -103,9 +103,15 @@ properties that all fail *silently* rather than loudly:
whichever call the missing one turns out to be.
- **the maximum `GLIBC_*` requirement.** The floor rule, measured rather than
assumed.

The manifest records all three per artefact, plus the source hashes, the
compiler and the floor.
- **no name the target's own libc family also exports.** An unversioned
definition in a preload wins the lookup for a versioned reference, so a
shared name replaces that implementation for the whole process, the
loader's own included. Issue #37 is the measured case, and the check
refuses rather than reporting the property unverified when it cannot find
the target's libc to compare against.

The manifest records the first three per artefact, plus the source hashes,
the compiler and the floor.

---

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, 64/64** on x86-64 and **60/60** on aarch64, up from 22/22. The AppImage suite adds 45 on a glvnd glibc host, 40 on musl, 26 on each of two pre-glvnd glibc hosts and 7 on a real-application stage, with every unrunnable case SKIPPED by the capability it lacks |
| The evidence harness still reports all predictions held | **Yes, 65/65** on x86-64 and **61/61** on aarch64, up from 22/22. The AppImage suite adds 45 on a glvnd glibc host, 40 on musl, 26 on each of two pre-glvnd glibc hosts and 7 on a real-application stage, with every unrunnable case SKIPPED by the capability it lacks |
| No host file modified, verified by checksum | **Yes.** T4.3, identical sha256 before and after |
| Bundled libraries still win, verified via `dladdr` | **Yes.** T4.2, all resolved under `$APPDIR` |
| A forward-compatibility story that does not depend on foresight | **Yes.** Host-runtime selection for the unenumerable gap, a generated shim for the enumerable one, and a build-time audit (E26) for the version traps |
Expand Down
72 changes: 71 additions & 1 deletion docs/report/03-defects-found-by-measurement.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -144,6 +144,76 @@ 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-<target>-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.

⚠ The same merged-kind-table fix turned six other symbols (`___environ`,
`__optpos`, `__optreset`, `_ns_flagdata`, `h_errno`, `optreset`) from abort
stubs into zeroed data of their real size. A data symbol must be data, so the
type is now right where a function symbol handed the reader code bytes, but
the loud failure is gone: a musl guest that reads `h_errno` now sees 0 rather
than aborting with the symbol named. Where the loader does not export
`__stack_chk_guard` (x86-64 and ppc64le) a musl guest reading it likewise
gets a constant zero where it previously got a constant stub address;
neither is a real canary.

⭐ **Re-measured against a real consumer's own bundled glibc, not just the
build floor.** The `Helium-0.16.3.1-anylinux` AppImages carry their own libc
family, so they are the runtime the preload actually lands in. Its aarch64
bundle is glibc 2.43 and its `ld-linux-aarch64.so.1` exports
`__stack_chk_guard@@GLIBC_2.17` while its `libc.so.6` imports it; its x86-64
bundle is glibc 2.44 and neither file carries the name at all. So the split in
the table above is a property of the architecture rather than of the 2.31
cross packages it was first measured on. Pointed at that AppDir with
`CLD_SYSROOT`, the gate refuses the released v0.2.5 aarch64 object naming
`__stack_chk_guard` and accepts this branch's build of the same object.

Re-measured on the upstream matrix in run
[34749521492](https://github.com/pkgforge-dev/cross-libc-dlopen/actions/runs/34749521492):
all six build rows green, E102 matching on both evidence rows, so the ARM
row above is measured rather than assumed. That is the run carrying the
name-collision check in its current shape, where a target libc it cannot
find refuses the build instead of reporting the property unverified.

---

---
Expand Down
11 changes: 11 additions & 0 deletions docs/report/05-design-b-generated-shim.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,17 @@ The generator now takes `--musl <inventory>` 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`:
Expand Down
11 changes: 6 additions & 5 deletions docs/report/08-test-results.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

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.
64 minus 4 is 60, and no case is missing for a reason nobody wrote down.
65 minus 4 is 61, and no case is missing for a reason nobody wrote down.

E1 through E13 measure the problem. E14 through E21 are one per fix from the first pass: the
ELF self-test, the generated-shim compile and behaviour, and five selector
Expand All @@ -63,6 +63,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 |
Expand Down
2 changes: 1 addition & 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 **64/64** and **60/60** in section 8.
totals for the tree as it ships are **65/65** and **61/61** in section 8.

### 9.8 What the shim does not do, stated as a number

Expand Down
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`
(64/64 on x86-64, 60/60 on aarch64), `sh scripts/run-appimage.sh` (45/45 glvnd glibc, 40/40 musl with five
(65/65 on x86-64, 61/61 on aarch64), `sh scripts/run-appimage.sh` (45/45 glvnd glibc, 40/40 musl with five
named skips, 26/26 on each pre-glvnd glibc host, 7/7 on the gtk4 stage), `tools/gap.py --fetch`, the eight-distro inventory, the AppImage inventory,
the corpus test, the real mpv and `iHD` VA-API run in section 9.19, and the five-distro `ld.so.cache` survey in
`../ground-truth.md`.
Expand Down
2 changes: 1 addition & 1 deletion docs/report/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ resolves to the page below.
|---|---|---|
| 1 | [Summary](01-summary.md) | what the goals were and which of them were reached |
| 2 | [Environment reached](02-environment.md) | the machines, images and drivers every number below came from |
| 3 | [Defects found by measurement](03-defects-found-by-measurement.md) | six things that were wrong, each found by a case rather than by reading |
| 3 | [Defects found by measurement](03-defects-found-by-measurement.md) | seven things that were wrong, each found by a case rather than by reading |
| 4 | [Design R: host-runtime selection](04-design-r-runtime-selection.md) | swapping the libc runtime at exec time, and why a mixed set cannot be allowed |
| 5 | [Design B: the generated shim](05-design-b-generated-shim.md) | the forward-compatibility shim, what it covers and what it cannot |
| 6 | [Goal 2, and the last blocker](06-goal-2-the-last-blocker.md) | a musl-built driver loading into a glibc process, and how the final blocker fell |
Expand Down
Loading