glibc: the relocation neither relocated nor left the file intact — and the seven rules - #522
Merged
Conversation
…nted
Sixteen recipes pinned `xim:glibc@2.39`. The pin was not conservatism, it was
the only safe option: with more than one glibc installed, "which version is
this dependency" had several independent answers, and a package could end up
with its interpreter from one payload and its libc from another — a fault
before `main` reporting an undefined GLIBC_PRIVATE symbol.
xlings 2026.8.5.3 and libxpkg 0.0.50 make that unconstructible, so a floor is
safe now, and a floor is what these recipes actually mean: glibc is backward
compatible, so `>=2.39` reads "at least this", and a pin refuses a newer
runtime that would serve perfectly well. That refusal is what kept
`xim:glibc@2.44` from being usable by anything at all.
Verified before pushing rather than after, because this exact change was
attempted once and CI found four failures in it. All four are gone, run
through `.github/scripts/posix-test.sh` with the fixed client:
glibc (uninstall) openssl (uninstall) bun (install) llvm (install)
→ failures: 0
`docs/V2/xpackage-spec.md` gains `resolved_deps`: what the resolver decided,
available to install() and config(), total rather than only-if-declared. It
spells out the two shapes a recipe must NOT write — a hand-built payload path,
and a {lib64, lib} probe — because both were real code, and both are how a
recipe becomes a second answerer to a question that has one.
Design: xlings/.agents/docs/2026-08-05-dependency-resolution-single-source.md
xlings 2026.8.5.3 refuses to finish an install that produced a split, so this should never fire. That is the reason to have it: a check that never fires is the one that turns "we believe this cannot happen" into something CI states, and it costs one string compare per binary. The failure it guards against passes every other check here. The package installs, its shims appear, its uninstall is clean — and it faults before `main` on a user's machine with `undefined symbol: __pointer_chk_guard, version GLIBC_PRIVATE`, naming neither package nor version. Linux and patchelf only. macOS has no INTERP to pair, and without patchelf the check is skipped rather than failed: an unverifiable run is not a bad one.
… subos shell The vendor library is the host's file and cannot carry an RPATH of ours, so its dependencies are gathered into lib/xlings-deps and that directory is declared on LD_LIBRARY_PATH. glibc's libraries were in there. LD_LIBRARY_PATH is inherited by every child, and most children in a subos are host binaries under the host loader. ld.so and libc.so.6 are two halves of one build talking over GLIBC_PRIVATE, so `xlings subos use` returned a /bin/bash that died of SIGSEGV before printing a character -- on a host whose glibc was the same upstream version as ours, merely a different build. libc.so.6 was never usable for its stated purpose either: the vendor is dlopen'd into a running process whose libc is long since bound, and an already-loaded SONAME is not searched for. Measured one library at a time rather than reasoned about, because the first attempt (drop the whole glibc row) broke NVIDIA: nothing from glibc -> NVIDIA device gone from EGL enumeration libc.so.6 only -> device gone AND bash segfaults libpthread/librt/libdl -> RTX 4080/PCIe/SSE2, bash fine Those three are the vendor's DT_NEEDED minus libc, and since glibc 2.34 they are compatibility stubs -- 27, 13 and 9 defined symbols, their implementations moved into libc.so.6. libm has 1203 symbols and nothing here asks for it.
The tarball is a prebuilt, so it carries the absolute paths of the machine
that built it -- a machine still using the `.xlings_data` home layout xlings
abandoned long ago, so those paths cannot exist anywhere. The recipe knew
this and had a rewrite for it. Measured on the payload on disk, that rewrite
was wrong three ways at once, and reported success every time.
* It named six files. Five files hold build paths; four of the five ARE on
that list and were processed wrongly, and the fifth was never opened.
* Its pattern `([^%s)]+)/<marker>/lib` runs leftward through anything that
is not whitespace or `)`. On `bin/ldd` it swallowed `RTLDLIST="` with the
path, leaving
./ld-linux.so.2 .64/ld-linux-x86-64.so.2 .x32/ld-linux-x32.so.2"
The ldd we ship does not survive `bash -n` -- 2.39 and 2.44 alike. It has
been shipping like that.
* Its tail was anchored at `/lib`, so `TEXTDOMAINDIR=<build>/share/locale`
in the same file was untouched: the build path stayed in the artifact,
which was the only thing the code existed to remove. And `/libx32` matched
`/lib` plus a leftover `x32` -- that is where `.x32/` came from.
Now delegated to elfpatch.relocate_build_paths (libxpkg 0.0.51), which
enumerates the payload, anchors on a whole absolute path token, and then
ASSERTS the result: no build path may remain, and every rewritten shell
script must still parse.
Verified end to end against a slice of a real home. Before: five files with
build paths, `bash -n bin/ldd` fails at line 38. After: zero (the only
remaining hit is the word `.xlings_data` inside this comment), `bash -n`
passes, `libc.so`'s GROUP names real files, and `ldd` prints resolved
addresses for a payload binary instead of not running at all.
The capability is probed with type(), not truthiness -- an unknown field on a
module proxy is truthy on every client, a trap this repo has hit twice. On an
older xlings the recipe rewrites only the linker scripts, where the greedy
pattern had nothing to swallow, and says plainly that the bin/ scripts keep
their build paths and that `xlings self update` fixes them. It no longer
corrupts them: an `ldd` naming a directory that does not exist is strictly
better than an `ldd` bash cannot parse.
They existed only in xlings design documents, which is where rules go to be
agreed with and not applied. Normative text now, in the spec recipes are
written against.
Each carries an executable criterion, because a rule judged by reading is a
rule that drifts. R3's, for instance: if a fix ADDS a path rather than
REMOVING one it is a workaround -- libxpkg 0.0.49 fails that test and 0.0.50
passes it, and neither needed an opinion to decide.
Every rule is anchored to a defect that shipped: deps_exports for R1,
libxpkg 0.0.49 for R3, glibc's path relocation for R4, elfpatch._find_tool for
R6, the nvidia dependency table for R7.
Two additions to the rules themselves:
* contract text may not say "if X is absent, fall back to Y". That one
sentence authorises every reader to implement its own Y, so the number of
answerers grows with the number of readers. Say "X must be present" and
make the writer guarantee it, or say an absent X is an error.
* a subos.env declaration of a variable that can load CODE into a process is
a privileged operation and must say why RPATH cannot serve the same need.
There is essentially one honest answer -- a library that dlopen's its own
siblings by bare SONAME, which no RPATH mechanism reaches. Variables that
cause DATA to be found are ordinary; PATH is a third category and belongs
to R6.
Audited the existing text against the new rule: the mirror "fallback" wording
is a retry over byte-identical sources, and `binding` defaulting to the
declaring package is applied by the writer. Neither is a violation.
`--prefix` is compiled into ld.so as its default library search path and into glibc's own binaries as their INTERP. It has been the BUILD MACHINE's path -- `/home/xlings/.xlings_data/...`, a home layout xlings abandoned years ago -- so every artifact leaked the builder's disk layout, and the next person to read it could not tell a deliberately dead path from a stale one. What was never wrong is that it points nowhere. For a relocatable package the build prefix can never equal the install path, so a default search that finds nothing is structural: everything must come from DT_RPATH, which is rule 2 stated as a property of the artifact rather than as an intention. An unpatched binary then fails LOUDLY at execve with ENOENT instead of quietly picking up the host's loader and mispairing GLIBC_PRIVATE. So: `/nonexistent/xlings-use-rpath-not-default-search`. It says which kind of dead path it is without a document, and `/nonexistent` has distribution precedent (Debian gives it to system users) so nobody creates one by accident. The recipe now relocates BOTH markers. A published tarball still carries the old one and the next build will carry the new one; dropping the old the day the pipeline changes would leave every existing release unrelocated with nothing to say so -- and adding only the old one would do the same to the next build. A marker that is not present rewrites nothing and asserts nothing remains, which is the right outcome for a payload from the other pipeline rather than an error. Takes effect on the next glibc build. No existing tarball changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Chain: openxlings/libxpkg#35 → mcpplibs/mcpp-index#164 → openxlings/xlings#488 → this.
The
lddwe ship does not survivebash -nglibc's tarball is a prebuilt, so it carries the absolute paths of the machine that built it — a machine still on the
.xlings_datahome layout xlings abandoned long ago, so those paths cannot exist anywhere. The recipe knew this and had a rewrite for it. Measured on the payload on disk, that rewrite was wrong three ways at once and reported success every time.It named six files. Five files hold build paths; four of the five are on that list and were processed wrongly, and the fifth was never opened.
Its pattern ran leftward through quotes and variable names.
([^%s)]+)/<marker>/libswallowedRTLDLIST="along with the path:bash -non~/.xlings/data/xpkgs/xim-x-glibc/2.39/bin/lddfails at line 38. 2.44 too. It has been shipping like that.Its tail was anchored at
/lib, soTEXTDOMAINDIR=<build>/share/localein the same file was untouched — the build path stayed in the artifact, which was the only thing the code existed to remove. (/libx32matching/libplus a leftoverx32is where.x32/came from.)And it wrote if it had substituted anything and called that success, so "still has build paths" and "we corrupted the file" both produced exactly the output of a clean run: nothing.
Now delegated to
elfpatch.relocate_build_paths(libxpkg 0.0.51), which enumerates the payload, anchors on a whole absolute path token, and asserts the result.Verified end to end against a slice of a real home
bash -n bin/lddlibc.soGROUP./libc.so.6ldd <payload binary>The capability is probed with
type(), not truthiness — an unknown field on a module proxy is truthy on every client, a trap this repo has hit twice. On an older xlings the recipe rewrites only the linker scripts, where the greedy pattern had nothing to swallow, and says plainly what is left undone. It no longer corruptsbin/: anlddnaming a directory that does not exist is strictly better than anlddbash cannot parse.The seven rules, each with a criterion that can be run
They existed only in xlings design documents, which is where rules go to be agreed with and not applied. Normative text now, in the spec recipes are written against.
R3's criterion, for instance: if a fix ADDS a path rather than REMOVING one it is a workaround — libxpkg 0.0.49 fails that test and 0.0.50 passes it, and neither needed an opinion to decide. Every rule is anchored to a defect that shipped:
deps_exportsfor R1, libxpkg 0.0.49 for R3, this glibc relocation for R4,elfpatch._find_toolfor R6, the nvidia dependency table for R7.Two additions:
subos.envdeclaration of a variable that can load code into a process is a privileged operation and must say why RPATH cannot serve the same need. There is exactly one honest answer, and 2026.8.6.1: three places a view answered a question the payload owns, and the subos layer's one live version xlings#488's gate verification narrowed it: the object that callsdlopenis the host's own file, which we cannot patch.Audited the existing spec text against the new rule — the mirror "fallback" wording is a retry over byte-identical sources, and
bindingdefaulting to the declaring package is applied by the writer. Neither is a violation.Tests
pytest tests/ -m static— 743 passed.