B line: interpose every glvnd entry point, with provenance in the proof - #532
Merged
Conversation
…ory and
the search path
Two things go away together, and neither could have been fixed by getting the
other right.
**The hand-written table.** `lib/xlings-deps/` held SONAMEs someone listed:
glibc's three 2.34+ stubs, libX11, libXext, libGLdispatch, libxcb, libXau,
libXdmcp. Measured against the vendor's actual DT_NEEDED closure it was missing
libm, libdrm, libgbm, libgcc_s and libwayland-* — every one of which was
therefore resolving from the HOST, silently, which is the leak this package
exists to close. A list of what someone thought of is not a measurement (R7).
**LD_LIBRARY_PATH's lack of scope.** It is inherited by every child of the
subos shell, and most of those are host binaries on the host loader. That is
how a libc in that directory once returned a `/bin/bash` that died of SIGSEGV
before printing a character.
An interposer replaces both: ~9 KB of empty object carrying the vendor's
SONAME, NEEDing the real vendor by absolute path so `dlsym` still reaches its
entry points through the handle's dependency tree, and holding DT_RPATH —
transitive along the load chain, where DT_RUNPATH is not — naming the closure
the RESOLVER computed rather than one this file lists.
Measured on this host, with LD_LIBRARY_PATH removed entirely:
before GL_RENDERER = llvmpipe (LLVM 20.1.7, 256 bits)
after GL_RENDERER = NVIDIA GeForce RTX 4080/PCIe/SSE2, PIXEL=336699
The probe renders and reads the pixel back; a renderer string is something a
half-working stack prints too.
The host driver directory is not declared either. The vendor dlopens its own
siblings by bare SONAME at runtime and those must match the host's kernel
module — but the host loader finds them through its own ld.so cache, with no
help from us.
PRECONDITION, and it is why only OUR vendor JSON names the interposer: it may
only be loaded by a consumer whose INTERP points into our payload. The host's
glvnd keeps using the host's vendor, so both rules hold at once. Handing one to
a host binary fails as `librt.so.1: undefined symbol: __pointer_chk_guard,
version GLIBC_PRIVATE`.
Degradation is loud, not silent: a client whose libxpkg predates
host_link_interposer says so and says what to run, and the install line now
reports whether the interposer exists — without that, "the vendor's
dependencies resolve to our payloads" and "they resolve to the host's" print
the same thing, and the second still renders.
…ve it Completes the B line: the NVIDIA vendor's dependency closure is bound per-object by RPATH instead of broadcast to the process through LD_LIBRARY_PATH. WHAT CHANGED - interposer-stub: new package carrying the empty ELF object that patchelf turns into an interposer. There is no compiler at install time and patchelf edits objects rather than creating them, so the object has to be shipped (AD-12). - nvidia-gl-host-link: builds an interposer for EVERY glvnd entry point -- libEGL_nvidia.so.0, libGLX_nvidia.so.0, libGLESv1_CM_nvidia.so.1, libGLESv2_nvidia.so.2 -- and drops the `LD_LIBRARY_PATH` subos.env declaration that used to make the vendor's deps resolve. Interposing only libEGL, as the first cut did, is not "most of it": glvnd dlopens each vendor library BY NAME, so each is the root of its own load chain, and DT_RPATH is transitive only DOWN a chain, never across to another root. Measured: EGL rendered on the RTX 4080 while GLX still pulled the vendor's entire closure out of /usr/lib. libGLX_nvidia.so.0 is also the Vulkan ICD, so that one root carries two APIs. The install line now reports interposers as a fraction (4/4) of the entry points the host actually has. A bare "yes" made one-of-four read as success, and the three uncovered roots were exactly the APIs nothing probed. - patchelf is declared as a build dep (`deps.build`). It is what BUILDS the interposer; without it there is no vendor entry point at all. VERIFICATION `.agents/tools/graphics/verify-host-link.sh` -- 12 checks, all passing on a real RTX 4080 (driver 550.144.03) in an isolated home. Both probes now report the PATH of every GL object they mapped, because the renderer string alone cannot carry the claim: `glxinfo` run inside a subos printed "NVIDIA GeForce RTX 4080/PCIe/SSE2" while every single object came from /usr/lib -- the host binary under the host loader, with our payload contributing nothing. That output is indistinguishable from success. The harness asserts our interposer is among the mapped files, that the host's real vendor arrived behind it by absolute DT_NEEDED, that LD_LIBRARY_PATH is empty, and that the host's own driver files were not modified. A CONSUMER REQUIREMENT THIS EXPOSES The vendor dlopen is served by the CALLING object's search path, and libGLX.so.0's own RPATH is `$ORIGIN` -- it cannot see the vendor package. What makes it resolve is that DT_RPATH is searched transitively up the load chain to the executable. DT_RUNPATH is not: the same probe built with --enable-new-dtags finds no vendor at all, with the X connection and the GLX extension both fine. Documented in glxprobe.c.
Self-review of the previous commit found a forward-compatibility hole in
my own change.
`deps = { "a", "b", build = {...} }` -- a positional runtime list with a
`build` key beside it -- is handled correctly only from libxpkg 0.0.52.
Every client before that takes the legacy array branch on it: `build` is
dropped and the positional entries are copied into build_deps in its
place. Silently. The install succeeds having done neither thing.
The index serves every client version, and unlike a Lua function there
is nothing in a recipe that can probe the loader's shape handling. So
the recipe must not depend on the fix at all: the split form,
`deps = { runtime = {...}, build = {...} }`, means the same thing on
every client that has ever existed.
`tests/test_deps_shape.py` now fails any recipe that mixes them. The
check is brace-matched rather than pattern-matched -- a `deps` table
almost always contains nested tables and comments, and `\{[^}]*\}` stops
at the first inner `}` and then draws a conclusion from half a table.
Measured before writing it: exactly one recipe in the index was the
mixed shape (this one, added by the previous commit). 155 recipes
scanned, 947 static assertions pass, and injecting the mixed shape back
fails precisely the one package.
With push alone, changed-files compares HEAD against the previous commit, so the install test only ever sees the last push -- never the change the PR proposes. A PR that adds a package in one commit and depends on it in the next ends with a docs-only commit that touches no pkgs/ file, the step is skipped, and the check reports green. Measured on this PR: a passing linux-install-test that ran zero packages, one push after the run that actually failed.
install() branched on `is_host("windows")`, and the `else` arm ran
__link_cuda_backends() and __install_systemd_user_service(). Both are
linux-only: the CUDA sentinel is declared only in `xpm.linux`, and
systemd is not a thing on macOS. So macOS took the linux path, and the
first thing it did was ask for the install dir of a package macOS never
resolves.
openxlings/xlings#487 reported this as "dependency resolution does not
filter by platform". It does -- xlings's resolver reads
`runtime_deps[platform]` -- and the recipe's declaration was right too.
The branch was what was wrong. The message ("cannot get install dir")
named an internal state and sent the reader to paths; libxpkg 0.0.53
makes it name the platform instead.
The recipe's other two hooks already guard on `is_host("linux")`;
install() was the outlier. Scanned every recipe with an
`is_host("windows")` branch for the same shape -- ollama was the only
one.
Also: interposer-stub declares `namespace = "xim"`.
`config --add-xpkg` registers a recipe into the LOCAL index, and without
a declared namespace it lands under `local:`. CI pre-registers every
changed recipe so a PR that adds a stack can install it -- but a dep
written `xim:interposer-stub` does not match a `local:` registration.
The failure reads "package 'xim:interposer-stub@>=0.1' not found" for a
file sitting in the same diff. Declaring it makes the local registration
and the published one the same address; verified locally, the address
now resolves.
Sunrisepeak
added a commit
to openxlings/xlings
that referenced
this pull request
Aug 6, 2026
0.0.53 adds the diagnostic half of #487. `cannot get install dir` named an internal state and covered two causes pointing in opposite directions: - not a dependency of this package ON THIS PLATFORM - declared here, but the payload never landed It now says which, and names the package, the platform, and what the deps here actually are. The issue's own hypothesis -- that dependency resolution does not filter by platform -- does not hold: resolver.cppm reads `pkg->xpm.runtime_deps.find(platform)`, and ollama declares the CUDA sentinel only under `xpm.linux`. The cause was ollama's install hook branching on `is_host("windows")` when the real distinction was linux, so macOS took the linux path. Fixed in openxlings/xim-pkgindex#532; the message is fixed here because it is what sent the reader to paths.
Sunrisepeak
added a commit
to openxlings/xlings
that referenced
this pull request
Aug 6, 2026
…oven by provenance (#490) * docs: the B line's core claim, proven on the real NVIDIA stack The gate (§2.7) proved the MECHANISM works — DT_RPATH transitivity, dlsym through the handle, GLX needing no process-global variable. This proves it works on the actual driver, and measures the boundary the gate could not see. Baseline, with glprobe, which renders and reads a pixel back rather than printing a version string: host env GL_RENDERER = NVIDIA GeForce RTX 4080/PCIe/SSE2 PIXEL=336699 in subos GL_RENDERER = llvmpipe (LLVM 20.1.7, 256 bits) PIXEL=336699 §2.6's defect, measured: the same host-linked binary drops from the GPU to software rendering inside the subos, silently. Both pixels are correct, so "does it render" cannot catch this — only the renderer name can. With a 27 KB interposer built by patchelf alone, and LD_LIBRARY_PATH carrying ONLY the host driver directory — lib/xlings-deps not on it at all: GL_RENDERER = NVIDIA GeForce RTX 4080/PCIe/SSE2 PIXEL=336699 RESULT=ok B2's acceptance criterion, satisfied. The boundary came out of the same experiment. Handing that interposer to a HOST binary fails as librt.so.1: undefined symbol: __pointer_chk_guard, version GLIBC_PRIVATE which is the 2026-08-05 crash verbatim: the interposer's RPATH names OUR glibc, and the consumer's libc is the host's. Not a defect — the domain of applicability, and consistent with §2.3, which says the process the vendor is dlopen'd into is ours by construction. But it is a precondition B1 has to put in the contract rather than leave implied: an object produced by host_link_interposer may only be loaded by a consumer whose INTERP points into our payload; host binaries must keep using the host's own vendor. Which is precisely the argument for B4: bake the vendor directory into the libglvnd WE build, and the two paths separate by construction instead of by an environment variable that every child inherits. Also corrects two wrong calls of mine about the install itself. It was never network-infeasible: a fresh isolated home defaults to the GLOBAL mirror, and `xlings config --mirror CN` installs all 22 packages in minutes. Before that I had declared it hung on twenty seconds of no growth in one directory, while it was between finishing its downloads and extracting them. * 2026.8.6.3: the B line lands, and a loader shape that dropped build deps Pins libxpkg 0.0.52, which brings two things: - `elfpatch.host_link_interposer` — the mechanism the B line is built on. From a shipped empty ELF stub, patchelf produces an object with the vendor's SONAME, the HOST vendor as an absolute DT_NEEDED, and the payload closure as DT_RPATH. The vendor's dependencies then resolve out of our payloads along one load chain, instead of being broadcast to the whole process through LD_LIBRARY_PATH. - A `deps` table mixing a positional list with `build = {...}` used to drop the build deps silently and copy the positional entries into build_deps in their place. Declared, reported as installed, neither done. Companion: openxlings/libxpkg#36, openxlings/xim-pkgindex#532. The doc's §7.7 records how this went, because the interesting part is that every step produced a passing result first: - an empty payload that installed "successfully" — caught only by the hook's own assertion, and caused by a resource key nested one level too deep; - `glxinfo` printing "NVIDIA GeForce RTX 4080/PCIe/SSE2" inside the subos while every object came from /usr/lib — the host binary under the host loader, our payload contributing nothing. That output is identical whether the B line works or never happened; - `interposer: yes` with one of four glvnd entry points covered. Each vendor library is dlopened BY NAME and so is the root of its own load chain; DT_RPATH is transitive only down a chain. EGL rendered on the GPU while GLX pulled its whole closure from the host. Each was found by measuring the artifact rather than the log line, and each fix is now asserted by `xim-pkgindex/.agents/tools/graphics/verify-host-link.sh` — 12 checks on a real RTX 4080, driver 550.144.03, in an isolated home. Also corrects a claim I made earlier in this work: elfpatch does NOT silently fall back to the host's patchelf. `_find_tool` resolves payload → subos view → home bin → host, warns when it leaves the payload, and `tool_payload_dir` scans the whole store — so a home that has patchelf at all uses it, declared or not. The real gap is narrow: a home that has never installed patchelf. * pin libxpkg 0.0.53: install_dir names the cause (#487) 0.0.53 adds the diagnostic half of #487. `cannot get install dir` named an internal state and covered two causes pointing in opposite directions: - not a dependency of this package ON THIS PLATFORM - declared here, but the payload never landed It now says which, and names the package, the platform, and what the deps here actually are. The issue's own hypothesis -- that dependency resolution does not filter by platform -- does not hold: resolver.cppm reads `pkg->xpm.runtime_deps.find(platform)`, and ollama declares the CUDA sentinel only under `xpm.linux`. The cause was ollama's install hook branching on `is_host("windows")` when the real distinction was linux, so macOS took the linux path. Fixed in openxlings/xim-pkgindex#532; the message is fixed here because it is what sent the reader to paths. * ci: bump mcpp to 2026.8.6.1 so it can see a freshly published index CI pinned mcpp 2026.8.3.3 while the index's latest is 2026.8.6.1, and that pin is what made `mcpplibs.xpkg@0.0.53` read as "not found" long after it was published. The index side was verified correct at every layer before touching this: the mcpp-index commit has the 0.0.53 entry, the published artifact `mcpp-index-84fa166.tar.gz` contains it, the rolling pointer `mcpp-index-pointers.json` names 84fa166, and `releases/latest` resolves to that tag. A local mcpp 2026.8.5.4 has 0.0.53 in its live index copy. So it was not publish lag and not a stale pointer. The workflows' `xlings update` step refreshes the XIM index and its three sub-indexes -- visible in the log -- and never touches the mcpplibs one, which is fetched by mcpp itself. Bumping mcpp means bumping XIM_PKGINDEX_REF with it, in all six workflows: the two are pinned as a known-good pair, and moving one alone is how a run ends up resolving a new client against an old index. The new ref is xim-pkgindex bf969a6 -- main with #532 (the B line) merged, so CI resolves against the index that carries interposer-stub. * 2026.8.6.3: the client's own version constant The version-consistency contract caught it: mcpp.toml said 2026.8.6.3 and src/core/config.cppm still said 2026.8.6.2. A release whose binary reports the previous version is exactly what that contract exists to stop. * ci: installing mcpp is not switching to it, so assert the version `xlings install -y` lays the payload down; the shim keeps resolving whatever version was already active. The client says so -- "installed, but 'x' still resolves to ..." -- but with ~/.mcpp restored from an Actions cache that one line is the only sign that the mcpp about to run is the old build. Measured 2026-08-06: `.xlings.json` pinned mcpp 2026.8.6.1, the run used the cached 2026.8.3.3, and its index snapshot predated the dependency this PR needs. It surfaced as `mcpplibs.xpkg@0.0.53 not found` against an index that demonstrably had it -- the mcpp-index commit, the published artifact mcpp-index-84fa166.tar.gz, the rolling pointer, and releases/latest all carry the entry, and a local mcpp 2026.8.5.4 refetches it from a wiped registry on both mirrors. Every layer I could check was correct, which is what made the version the last place to look. So: switch explicitly, then ASSERT the version rather than print it. A version line in a log is only ever read after something has already gone wrong; a failed assert names the cause at the point it happens. Seven workflows, including release.yml. * ci: on a build failure, print the index the runner actually holds `mcpplibs.xpkg@<ver> not found` has now been diagnosed five times from the outside. Each check was correct: index commit has the entry published artifact mcpp-index-84fa166.tar.gz has it rolling pointer names 84fa166 releases/latest resolves to that tag publish lag still failing 30+ minutes later mcpp version pin bumped 2026.8.3.3 -> 2026.8.6.1, assert passes install-vs-use the assert proves the right mcpp runs mcpp.lock hash regenerates byte-identical from the published index Every one of those is inference about a file only the runner can see. So the failure path now prints it: the version keys in the mcpplibs index under both registry roots, the index snapshots present, and `mcpp self version`. Guessing a sixth time would cost another CI round either way; this way the round produces an answer instead of another elimination. * ci: the diagnostic hung off a command that never ran The index dump added one commit ago sat inside `mcpp test || { ... }`, with `mcpp build` running unguarded above it. The shell is `bash -e`, so a failing `mcpp build` aborted the step before the block was reached. The diagnostic existed, never ran, and the failure output was identical to a run where it had -- an entire CI round spent adding output that could not print. Now it is a separate step with `if: failure()`, so it covers any failing step in the job rather than one command's non-zero exit, and it also reports both registry roots, the snapshots present, and the mcpp/xlings versions. * ci: force `mcpp index update`, and assert what the refresh landed The prune drops the resolved indexes, and mcpp's own re-fetch happily uses what it finds in the restored Actions cache -- so a freshly published dependency reads as "not found" against an index that demonstrably has it. `mcpplibs.xpkg@0.0.53` failed this way while the index commit, the published artifact mcpp-index-84fa166.tar.gz, the rolling pointer and releases/latest all carried the entry, and a local mcpp at the same version refetched it from a wiped registry on both mirrors. `mcpp index update` is the refresh. `mcpp index status` prints the revision each index is actually at -- locally, `mcpplibs 84fa166`, which is the one datum this failure needed and nothing was printing. The assert matters as much as the refresh: calling update alone would leave "refreshed" and "still stale" producing identical output, which is the exact failure mode the step exists to end. It greps the refreshed index for the version mcpp.toml pins and, on a miss, prints the index revision and the versions that ARE there before failing. Seven workflows, including release.yml. * ci: the index assert must slice the platform section first A whole-file grep for `["0.0.53"]` passes on a file where the entry exists only under `xpm.linux` -- which is exactly the bug this assert was written to catch and did not. Both mcpp-index bumps edited the linux section alone; linux CI went green and macOS/Windows failed against a file that literally contains the string. Falsified before committing: against the broken file the check reports linux ok, macosx MISSING, windows MISSING; against the fixed one all three pass. * ci: the Windows index assert was still a whole-file match The platform-aware fix landed in six workflows and skipped this one -- the PowerShell block did not match the POSIX text my patch keyed on. So Windows kept the version of the check that passes on the broken file: `Select-String` over the whole descriptor finds `["0.0.53"]` in the linux block and reports success. Measured on this PR: macOS failed AT the assert, naming `xpm.macosx`, while Windows sailed through it and failed two steps later in `mcpp build` with a message about a package rather than a platform -- the exact difference the assert exists to make. Now it slices the `windows = {` block first, and on a miss prints the index revision and the versions present anywhere in the file, so the "it's right there" confusion is answered in the same output. Falsified before committing by replicating the slicing rule against the pre-fix descriptor: windows-block MISSING on the broken file, ok on the fixed one. * ci: the index assert reads the pin from mcpp.toml, never a literal `0.0.53` was baked into all seven workflows. That is a value that changes: the next libxpkg bump moves `mcpp.toml` and leaves every workflow asserting the previous version, which then passes against an index that still carries it. The check would go stale without ever failing -- the same shape as everything else this session has been chasing, this time in the check itself. Now the version is read from `[dependencies.mcpplibs] xpkg` at runtime, so the only constant is where the source of truth lives. Falsified before committing, all four combinations: new index + pin 0.0.53 / macosx ok new index + pin 0.0.53 / windows ok old index + pin 0.0.53 / macosx FAIL (the platform-slice defect) new index + pin 0.0.99 / macosx FAIL (the pin moved, assert follows) The first extraction attempt returned an empty string -- `gsub(/.*"|".*/,"")` is greedy to the LAST quote -- and only the falsification caught it. It would have made every job fail on the guard rather than pass silently, but wrong either way. * ci: drop the index assert, keep the refresh Per review: `mcpp index update` alone is enough. The assert added on top of it turned a transient condition into a hard failure on a timer nobody controls, and its own literal version was a value that changes. `mcpp index status` stays -- it prints the revision each index landed on, which is what makes a stale-index failure readable instead of surfacing two layers away as `<pkg>@<ver> not found`. Seven workflows; release.yml had three copies of the block.
This was referenced Aug 6, 2026
Sunrisepeak
added a commit
that referenced
this pull request
Aug 7, 2026
…tcome (#543) * test(graphics): one coverage matrix, run per machine, with a third outcome Verification of this stack was three scripts covering one slice each -- verify-host-link.sh (NVIDIA only), selfcontained-check.sh (empty host), and whatever got typed that day. Each had its own setup, none knew about the others, and the union was never reported. So "the ecosystem works" rested on one machine with one GPU, and every other cell was untested in a way that produced no output. verify-stack.sh creates a subos, installs `graphics`, and walks the matrix: software rendering, NVIDIA proprietary (delegated to the provenance verifier), radeonsi / iris / nouveau, WSL2 d3d12, Vulkan, X11, Wayland, a real GUI application, and empty-host self-containment. THE POINT IS THE THIRD OUTCOME. A cell that could not be exercised here is printed, counted, and listed again in the summary WITH ITS REASON. Skips do not fail the run -- treating "I have no AMD GPU" as a failure would make the script useless to everyone -- but they are never silent, because "we lack that hardware" and "it works" must not look alike. The summary's skip list is the recruitment list: no one machine has NVIDIA, AMD, Intel and WSL2 at once, so coverage is the union of runs by different people, and --json exists so those runs can be aggregated. Writing it immediately caught two false passes in itself, both the exact shape it exists to prevent: * `nouveau` reported PASS on this host. MESA_LOADER_DRIVER_OVERRIDE=nouveau rendered fine -- on llvmpipe -- and the cell only checked RESULT=ok. A hardware cell now asserts the renderer is not the software fallback, and nouveau is correctly reported as not-exercisable while the proprietary nvidia.ko owns the GPU. * `xlings install graphics` printed "0 package(s)" as a pass. That is what a re-run looks like; it now says "already satisfied" instead of a count that reads as coverage -- the same trap #532 hit in CI. First run on an RTX 4080 / driver 550.144.03: pass 13, fail 0, not-exercised 6 (amd, intel, WSL2, Vulkan, Wayland, and the empty-host check which is currently INCONCLUSIVE by its own control run). * feat(libXinerama): the first gap the coverage matrix named, closed end to end verify-stack.sh reported it as a non-fatal unresolved dlopen under the real-GUI cell, which is the only place it could have shown up: libXinerama is on no DT_NEEDED path -- mesa does not need it, nothing in the rendering closure does -- and a toolkit dlopens it to ask where the monitors are. It appears in no dependency graph derived from ELF metadata, and the surfaceless probe that was this stack's acceptance criterion could never miss it. godot printed libXinerama.so.1: cannot open shared object file and started anyway, falling back to single-screen geometry. A non-fatal dlopen failure survives every test that only asks "did it run". Built with build-in-subos.sh (leak check: no host references), published to a new xlings-res/libXinerama, recipe added, pulled into `graphics`. Verified by reinstalling and running godot: the line is gone, and it still reports OpenGL API 3.3.0 NVIDIA 550.144.03 on the RTX 4080. Also fixes build-in-subos.sh, which is why the build failed the first time. It spliced only `<dep>/lib/pkgconfig` into PKG_CONFIG_LIBDIR. A PROTOCOL-ONLY package installs no library and puts its .pc in `share/pkgconfig` -- xorgproto ships 40 of them there, xcb-proto likewise -- so every protocol package was invisible to pkg-config while its HEADERS were still spliced in via -I. That combination is why the gap lasted: mesa builds fine because it includes the headers and never asks pkg-config for a protocol module; libXinerama does ask, and died on `XINERAMA_CFLAGS ... no such package` for a package sitting right there with its headers already on the command line. GLOBAL mirror only, stated in the recipe rather than papered over: `gtc` can publish a release into an existing GitCode project but cannot create the project, and xlings-res/libXinerama does not exist there yet. A CN URL pointing at a missing project fails at download time instead of falling back, which is worse than not having one. * feat(vulkan): a loader, so the ICD mesa already ships is no longer unread The matrix reported Vulkan as an empty cell: mesa builds RADV and rewrites its ICD manifest to an absolute path in our payload, and nothing ever read it, because an ICD is a DRIVER and a driver is loaded BY a loader. No libvulkan.so.1 meant no vulkaninfo and a dead zink in a payload that ships it. vulkan-headers 1.4.313 and vulkan-loader (from tag vulkan-sdk-1.4.313.0), both built in a subos and leak-checked, published to xlings-res, pulled into `graphics`. Verified: lib/libvulkan.so.1 reaches <subos>/lib and its DT_NEEDED closure resolves with 0 unresolved under our own loader. Discovery needed no new declaration -- the loader searches $XDG_DATA_DIRS/vulkan/icd.d and mesa's config() already prepends its share directory there. VK_DRIVER_FILES would have been wrong: it is an override that suppresses system discovery. ## And a bug in build-in-subos.sh that produced a mislabeled payload The download cache was keyed on the URL's basename. Every GitHub archive URL is `.../archive/refs/tags/v<tag>.tar.gz`, so the basename carries the tag and nothing about the project -- and Vulkan-Headers and Vulkan-Loader are both released as v1.4.313. The loader build found the headers' tarball already in $SRC, skipped the download, then configured, built, staged, LEAK-CHECKED and packaged the wrong source. Every step reported success and the artifact was published to xlings-res as `vulkan-loader` while containing Vulkan-Headers. It was caught only because the installed package had no lib/ directory. The release has been deleted and replaced with the real loader; the cache is now keyed on NAME-VERSION. Two smaller things the same build surfaced: Vulkan-Loader's tags are `vulkan-sdk-<x>` and not `v<x>` (GitHub's archive endpoint answers 200 for a ref that is not the tag you meant), and xrandr.pc Requires.private xrender, which was simply missing from --deps -- a missing dependency, not a tooling fault. * fix(vulkan): the loader found the HOST's ICDs, and the cell called that a pass Adding a Vulkan loader immediately broke two of the matrix's own cells, both by being too weak. Recording them because each is the shape the matrix exists for. **"software rendering (llvmpipe)" started failing with** zink Vulkan 1.3 (NVIDIA GeForce RTX 4080 (NVIDIA_PROPRIETARY)) Selecting the mesa vendor is not selecting SOFTWARE. With a working libvulkan.so.1 present, mesa switched to zink -- its GL-over-Vulkan driver, a real and welcome capability, and emphatically not the CPU path this cell is named for. It now forces LIBGL_ALWAYS_SOFTWARE and asserts the renderer is one of llvmpipe/softpipe/swrast. (That zink came alive at all is the loader paying for itself: the recipe predicted "zink is dead in a payload that ships it".) **"Vulkan loader + ICDs" reported PASS with "0 ICD manifest(s)".** A loader with none of OUR ICDs in the subos is not Vulkan support -- it is a loader that finds the HOST's ICDs and succeeds. Same boundary the GL side needed interposers for, one API over, and it looks like a pass from every angle except asking whose ICD answered. Zero ICDs is now a failure with that sentence as its message. Which then named the real defect: mesa's ICD manifest lived only in its payload. The loader reads $XDG_DATA_DIRS/vulkan/icd.d and mesa puts ${subosdir}/share on that list -- so a manifest that never reaches the subos is never found. graphics.declare_vulkan_icd() places them, the same shared-directory shape the glvnd vendor JSON uses and for the same reason. Verified: radeon_icd.x86_64.json now lands in <subos>/share/vulkan/icd.d, the cell passes on OUR ICD, and the software cell is back to llvmpipe. Matrix on this host: pass 13, fail 0, not-exercised 6. * test(graphics): vendorprobe — and it found that the stack is not self-contained The tool the last round said to write, written. It dlopens a glvnd vendor directly and reports whether the object loaded and whether __egl_Main is there, which is the one question every other elimination had left. First run answered it, and not at the step I predicted: DLOPEN=fail ERR=libXau.so.6: cannot open shared object file The vendor never loads at all; __egl_Main is never reached. With LD_LIBRARY_PATH=<subos>/lib it loads and the entry point is present -- so the missing thing is a library, not an entry point. The chain, each link measured: libxcb.so.1 NEEDs libXau.so.6 libxcb's RUNPATH is only $ORIGIN -- its own payload directory libXau.so.6 lives in a DIFFERENT payload DT_RUNPATH is not transitive, so nothing above libxcb can help it => libXau is resolved from the HOST <subos>/lib does contain libXau.so.6. Nothing on libxcb's search path points there. The file is present and the path is not. So the stack has never been self-contained, and the gap is a SECOND-LEVEL dependency. Outside a container nobody notices, because every Linux machine has libXau in its ld.so.cache. The empty-host container has no cache, the vendor fails to load, and that surfaces as zero vendors and EGL_BAD_PARAMETER -- which is what S1 has been reporting. This is exactly what the S3 assertion was written to catch, one layer lower than expected: not a GL renderer coming from the host, but an X11 second-level dependency. I was wrong to write that S1's failure was uninformative. The control run failing too does not mean the test is broken; it means both runs failed for the same real defect. The INCONCLUSIVE gate is still worth keeping -- it turns a message that would blame the closure into an honest "could not tell" -- but this time the closure really is incomplete. Not fixed here: elfpatch writes a full closure RPATH onto executables and leaves payload libraries with $ORIGIN. Either each payload library's RPATH should cover its own closure, or <subos>/lib should be appended to it -- which is precisely what §B1 just did for the interposer, and this shows the same reasoning applies to the whole stack. That is a blast-radius decision, not a drive-by edit. * fix(graphics): the payloads resolved from the host, and every test said pass `exports.runtime.libdirs` is what a package OFFERS its dependents. Something has to CONSUME it, and that something is `elfpatch` — which no recipe in this index, or in mcpp-index, ever called. `elfpatch.closure_lib_paths()` is a public, documented libxpkg API that had zero callers in the entire ecosystem. So `libxcb.so.1` shipped as DT_NEEDED libXau.so.6, libXdmcp.so.6, libc.so.6 DT_RUNPATH $ORIGIN with libXau in a different payload. It resolved anyway — from the host's /etc/ld.so.cache, on every machine that has libxau, which is every desktop Linux. The stack looked self-contained and never was. The subos link directory does not cover this, and the reason is one sentence of ld.so's search order: if an object has DT_RUNPATH, no ancestor's DT_RPATH is consulted for its dependencies. A consumer with `<subos>/lib` in a transitive DT_RPATH cannot serve libxcb's search for libXau, because libxcb has a RUNPATH of its own. Measured on a sealed bwrap with no /usr at all: as shipped EGL_CLIENT_EXTENSIONS= , surfaceless refused 0x300c exit 1 sealed GL_RENDERER=llvmpipe (LLVM 20.1.7) PIXEL=336699 exit 0 libs/selfcontain.lua wraps the closure patch; 28 recipes call it from install(). Under-declared direct deps are completed at the same time — a closure is only as complete as the deps list it is computed from, and `runtime_deps` is direct, not transitive. glibc is patched by nothing: it is the root, and rewriting the loader's own payload is how this was nearly broken while being investigated. The verify-stack cell that has reported INCONCLUSIVE since it was written now reads `✓ empty-host self-containment — S1-S4 pass`. Both arms of that A/B had been failing for the same real defect, which is why it never accused anything. pass 14 fail 0 not-exercised-here 5 * fix(fontconfig): a bare dep name is only unambiguous until CI touches both CI registers every changed recipe a second time under `local:`. fontconfig declared bare `expat@2.6.2`, so the moment this PR also touched expat the name had two candidates and the install died with a candidate list — a failure caused by the SHAPE of the PR, not by either recipe. 62 bare dep names remain index-wide (cairo, glib, libpng, harfbuzz...). Each is latent in exactly the same way: fine until some unrelated change happens to hit the depender and the dependee together. * fix(selfcontain): refuse to call an install that produced nothing a success Every recipe ends `os.mv(srcdir, install_dir); return true` with the move unchecked. When the extracted source directory is already gone the move does nothing, install() returns true, and xlings prints a tick over an EMPTY payload. Reproduced locally, not inferred: xim:libffi@3.4.4 installed as a dependency -- consumes the srcdir local:libffi@3.4.4 ✓ done, 1 package installed -- payload directory empty The only complaint came from the *config* hook two steps later, about pkgconfig globs — the error named the wrong subsystem because by then nothing remembered that the payload never arrived. That is what CI is currently reporting for pkgs/l/libffi.lua. The check cannot fire in CI's install test, and the comment says so: under `config --add-xpkg` every `xim.pkgindex.*` import is a no-op proxy, so that job does not exercise selfcontain.seal at all. Cell 6 of verify-stack.sh is the evidence that the seal works; a green linux-install-test is not. * fix(ci): a recipe under test must REPLACE its published copy, not coexist `config --add-xpkg` registers the changed recipe under `local:` while the published one stays under `xim:`. Two candidates for one package is a state that never exists after merge, and it breaks the run in two ways that both read as bugs in the diff: 1. AMBIGUITY. Any recipe naming a dep without a namespace now has two candidates. Published `xim:fontconfig` says `expat@2.6.2`, so a PR that merely touched expat broke fontconfig AND graphics. Whether a PR passed depended on which OTHER packages it happened to touch. 2. DOUBLE INSTALL FROM ONE EXTRACTION. `xim:libffi` arrives as another package's dependency and its hook MOVES the extracted tree into place. `local:libffi` then has no download artifact, so no extraction; the recipe's os.mv finds nothing, install() returns true, and xlings prints a tick over an EMPTY payload. The only complaint came from the config hook two steps later, about pkgconfig globs. So the recipe is written OVER the published one, in place, in the same namespace. Deleting the published copy instead was tried and is NOT equivalent: it also leaves one candidate, but removes the `xim:` NAME, so every self-qualifying dep (`xim:expat@2.6.2`, `xim:libffi@>=3.4`) stops resolving — 3 failures became 3 different failures. Overlay keeps the name. libs/ is overlaid into the same index for the reason the local-index copy already documents: a recipe imports `xim.pkgindex.*` from the index it was loaded from, and a missing libs/ turns every helper call into a truthy no-op. That blindness was described in this file and never fixed; the same job could not execute selfcontain.seal at all. Falls back to --add-xpkg when the index path is a symlink (a developer pointing their home at the checkout) so the copy can never write into the source tree. Validated against a CI-shaped home (index populated from origin/main, not the working copy): 21 packages, 0 failures, including the four that were red. * feat(mirrors): CN mirrors for the three new graphics packages These shipped GLOBAL-only with a comment saying gtc could publish a release but not create a GitCode project. That was wrong — `gtc repo create` exists. The actual blocker was different and only shows up on an EMPTY project: tagging a release fails with `main is not exist`, because there is no branch to target. Pushing a README first is what makes the release possible. Created xlings-res/{libXinerama,vulkan-headers,vulkan-loader} on GitCode, pushed the same README the GitHub mirror carries, published the tag and uploaded the payload. Verified by DOWNLOADING each asset from the CN URL and comparing sha256 to the artifact the recipe pins — not by checking that the URL exists. All three match byte for byte. * fix(ci): overlay only a recipe that already exists upstream The previous commit overlaid every changed recipe into the index. That is right for a change to a PUBLISHED package and wrong for one the PR ADDS: the package is not in the index, so asking for it by its index name makes xlings say 'xim:libXinerama' not in current index; refreshing index... and the refresh re-fetches the whole index, overwriting the file just placed there. The install then fails with `not found` — all three new packages died in 21 seconds. Overlay now requires the published copy to exist. That is exactly the case that produces the duplicate candidate, and the only case overlay can serve. A new package keeps the --add-xpkg / local: path, where it is unambiguous anyway: nothing published shares the name. This is also the namespace rule the index already follows — new package referenced bare, changed published one with xim:. Validated on the union of both failure sets — the 3 new packages that just failed and the 4 that were red before: 7 tested, 0 failures. --------- Co-authored-by: sunrisepeak <x.d2learn.org@gmail.com>
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.
Completes the B line of the subos architecture proposal: the NVIDIA
vendor's dependency closure is bound per-object by RPATH instead of
broadcast to the process through
LD_LIBRARY_PATH.Requires openxlings/libxpkg#36 (libxpkg 0.0.52).
What changed
pkgs/i/interposer-stub.lua(new) — the empty ELF object that patchelfturns into an interposer. There is no compiler at install time and patchelf
edits objects rather than creating them, so the object is shipped (AD-12).
x86_64 8984 B, aarch64 66264 B, zero DT_NEEDED.
pkgs/n/nvidia-gl-host-link.lua— an interposer for every glvndentry point, and the
LD_LIBRARY_PATHsubos.envdeclaration is gone.Interposing only libEGL, as the first cut did, is not "most of it". glvnd
dlopens each vendor library BY NAME, so each is the root of its own load
chain, and DT_RPATH is transitive only down a chain — never across to
another root. Measured: EGL rendered on the RTX 4080 while GLX still pulled
the vendor's entire closure out of
/usr/lib.libGLX_nvidia.so.0is alsothe Vulkan ICD, so that one root carries two APIs.
The install line reports interposers as a fraction (
4/4) of the entrypoints the host has. A bare "yes" made one-of-four read as success, and the
three uncovered roots were exactly the APIs nothing probed.
patchelfis declared as a build dep — it is what builds the interposer.Verification
.agents/tools/graphics/verify-host-link.sh— 12 checks, all passing on areal RTX 4080 (driver 550.144.03) in an isolated home.
Why the probes now print load paths
A renderer string cannot carry this claim.
glxinforun inside a subosprinted
NVIDIA GeForce RTX 4080/PCIe/SSE2while every object came from/usr/lib— the host binary under the host loader, our payloadcontributing nothing. That output is indistinguishable from success.
Both probes now report the path of every GL object mapped, and the harness
asserts our interposer is among them.
A consumer requirement this exposes
The vendor dlopen is served by the calling object's search path, and
libGLX.so.0's own RPATH is$ORIGIN— it cannot see the vendor package.What makes it resolve is that DT_RPATH is searched transitively up the
load chain to the executable. DT_RUNPATH is not: the same probe built
with
--enable-new-dtagsfinds no vendor at all, with the X connection andthe GLX extension both fine. Documented in
glxprobe.c.