From c0888a9d752e4c061b7487bdd647dfb9f0290cb6 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sat, 8 Aug 2026 02:13:06 +0800 Subject: [PATCH 1/2] feat(glx-runtime): GL comes from the ecosystem, not from /usr/lib This package symlinked the HOST's libGL/libEGL out of /usr/lib*. That is what mcpp#352 is: the host's Mesa needs GLIBC_2.43, mcpp's payload glibc is 2.39, and the program linked cleanly then exited 255 with no output at all. It is also the first entry on the xlings hermetic policy's forbidden list -- any .so under /usr/lib* or /lib*. 2026.08.08 takes the same libraries from `xim:graphics`: 22 packages plus two sentinels that probe for a host-side userspace half they do not own (the proprietary NVIDIA driver, WSL2's D3D12) and succeed having linked nothing when it is absent. One dependency covers every host shape and this file gained no conditional. The source is the SUBOS VIEW, not a payload directory. A payload path pins a version, so a consumer's RUNPATH would name mesa 25.0.7.1 forever and break the day it is upgraded; the view is the stable indirection, the role /run/opengl-driver plays on NixOS. The view also carries libc.so.6 and the rest of the C runtime, and those must never reach a consumer's RUNPATH -- the consumer runs under mcpp's payload loader, and a second libc paired with it faults inside the dynamic linker before main with no output. The pattern list is what keeps them out, so it is now a safety boundary with an assertion behind it rather than a convenience. That failure was reproduced on the way here, not imagined. libnvidia* leaves the pattern list: the proprietary driver reaches the subos through xim:nvidia-gl-host-link under the glvnd vendor names already matched, and taking it by its own name too would be a second route to one set of libraries. MCPP_HOST_GL_LIBRARY_PATH still works and is now the only door back to the host. It warns when used, because what it names was built against the host's glibc and loading it under the payload's is the #352 configuration exactly. Verified on an NVIDIA host, not reasoned about: a consumer whose RUNPATH is the generated directory resolves libEGL to it, and GL_RENDERER is "NVIDIA GeForce RTX 4080". Both halves are the criterion -- "a window appeared" is a false pass, since llvmpipe produces one too. The no-libc assertion was checked against the real view: 52 libraries linked, zero C-runtime entries. 2026.06.03 stays published so consumers already pinned to it keep resolving. compat.glfw, the only package that depends on this one, moves to the new pin. --- pkgs/c/compat.glfw.lua | 2 +- pkgs/c/compat.glx-runtime.lua | 165 ++++++++++++++++++---------------- 2 files changed, 88 insertions(+), 79 deletions(-) diff --git a/pkgs/c/compat.glfw.lua b/pkgs/c/compat.glfw.lua index e4ef66f1..a4454b0c 100644 --- a/pkgs/c/compat.glfw.lua +++ b/pkgs/c/compat.glfw.lua @@ -80,7 +80,7 @@ package = { "src/posix_module.c", }, deps = { - ["compat.glx-runtime"] = "2026.06.03", + ["compat.glx-runtime"] = "2026.08.08", ["compat.x11"] = "1.8.13", ["compat.xcursor"] = "1.2.3", ["compat.xext"] = "1.3.7", diff --git a/pkgs/c/compat.glx-runtime.lua b/pkgs/c/compat.glx-runtime.lua index 8cc15d1a..16257b3b 100644 --- a/pkgs/c/compat.glx-runtime.lua +++ b/pkgs/c/compat.glx-runtime.lua @@ -2,13 +2,45 @@ package = { spec = "1", namespace = "compat", name = "glx-runtime", - description = "Host GLVND/GLX/OpenGL runtime adapter for mcpp Linux window applications", + description = "GLVND/GLX/OpenGL runtime for mcpp Linux window applications, from the xlings graphics stack", licenses = {"MIT"}, repo = "https://github.com/KhronosGroup/OpenGL-Registry", type = "package", + -- WHERE THE GL RUNTIME COMES FROM, AND WHY IT CHANGED + -- + -- Until 2026.08.08 this package symlinked the HOST's libGL/libEGL out of + -- /usr/lib*. That is the thing mcpp#352 is: the host's Mesa needs + -- GLIBC_2.43 and mcpp's payload glibc is 2.39, so the program linked + -- cleanly and exited 255 with no output. It is also the boundary the + -- xlings hermetic policy names first -- any .so under /usr/lib* or /lib*. + -- + -- The runtime now comes from `xim:graphics`, the ecosystem's own stack: + -- 22 packages plus two sentinels that probe for a host-side userspace half + -- they do not own (the proprietary NVIDIA driver, WSL2's D3D12) and + -- succeed having linked nothing when it is absent. One dependency, every + -- host shape, no conditional in this file. + -- + -- Measured on an NVIDIA host after the change: libEGL resolves to + -- xim-x-libglvnd/1.7.0/lib/libEGL.so.1 and GL_RENDERER is the GPU, not + -- llvmpipe. Both halves of that matter -- "a window appeared" is a false + -- pass, because llvmpipe renders one too. xpm = { linux = { + ["2026.08.08"] = { + url = { + GLOBAL = "https://raw.githubusercontent.com/KhronosGroup/OpenGL-Registry/a30033d3e812c9bf10094f1010374a6b15e192eb/README.adoc", + CN = "https://gitcode.com/mcpp-res/glx-runtime/releases/download/2026.08.08/glx-runtime-2026.08.08.adoc", + }, + sha256 = "ea68efce197e68413ebb62c51ab4bccfb2309a2fca776d31b49d972f59f3640e", + -- The whole hermetic graphics stack. A RUNTIME dep, not a + -- build one: nothing here compiles against it, the produced + -- consumer loads it. + deps = { runtime = { "xim:graphics" } }, + }, + -- Kept so already-published consumers pinned to it keep resolving. + -- It sources libGL from the HOST and is the configuration behind + -- mcpp#352; new consumers must not pin it. ["2026.06.03"] = { url = { GLOBAL = "https://raw.githubusercontent.com/KhronosGroup/OpenGL-Registry/a30033d3e812c9bf10094f1010374a6b15e192eb/README.adoc", @@ -41,6 +73,7 @@ package = { } import("xim.libxpkg.pkginfo") +import("xim.libxpkg.system") import("xim.libxpkg.log") local function sh_quote(value) @@ -60,6 +93,26 @@ local function split_paths(value) return out end +-- Where to take the GL libraries from. +-- +-- The SUBOS VIEW (`/lib`), not a payload directory. A payload path pins +-- a version, so a consumer's recorded RUNPATH would name mesa 25.0.7.1 forever +-- and stop resolving the day it is upgraded; the view is the stable +-- indirection -- the role /run/opengl-driver plays on NixOS. xlings repoints +-- it as the active version changes and this package needs no new release. +-- +-- The view also carries libc.so.6, crt1.o and the rest of the C runtime, and +-- those must NEVER reach a consumer's RUNPATH: the consumer runs under mcpp's +-- payload loader, and pairing one loader with another glibc's libc.so.6 faults +-- inside the dynamic linker before main, with empty output. What keeps them +-- out is the pattern list below -- so that list is a safety boundary, not a +-- convenience, and nothing resembling `libc*` may ever be added to it. +-- +-- MCPP_HOST_GL_LIBRARY_PATH still works and is now the ONLY door back to the +-- host. Using it leaves the hermetic guarantee: the libraries it names were +-- built against the host's glibc, and loading them under mcpp's payload glibc +-- is exactly the configuration mcpp#352 reports. It exists for a machine whose +-- GPU vendor the ecosystem does not cover yet. local function candidate_dirs() local out = {} local seen = {} @@ -71,13 +124,12 @@ local function candidate_dirs() end for _, dir in ipairs(split_paths(os.getenv("MCPP_HOST_GL_LIBRARY_PATH"))) do + log.warn("MCPP_HOST_GL_LIBRARY_PATH names %s: GL will come from the " + .. "host, which is the configuration behind mcpp#352", dir) add(dir) end - add("/lib/x86_64-linux-gnu") - add("/usr/lib/x86_64-linux-gnu") - add("/lib64") - add("/usr/lib64") - add("/usr/lib") + + add(path.join(system.subos_sysrootdir(), "lib")) return out end @@ -90,7 +142,11 @@ local host_gl_patterns = { "libEGL.so*", "libEGL_*.so*", "libGLES*.so*", - "libnvidia*.so*", + -- No libnvidia* here. The proprietary driver reaches the subos through + -- xim:nvidia-gl-host-link, which links it under the glvnd vendor names + -- already matched above; taking it by its own name would be a second + -- route to the same libraries, and the two would disagree the day the + -- driver is upgraded under us. "libglapi.so*", "libdrm*.so*", "libexpat.so*", @@ -104,90 +160,43 @@ local required = { ["libGL.so.1"] = false, } --- Is FILE a 64-bit ELF? e_ident[EI_CLASS] == ELFCLASS64. --- --- Five bytes read directly. `file`/`readelf`/`patchelf` would each answer this --- and each may be absent when a hook runs, and a probe that answers "cannot --- tell" by assuming "fine" is the bug below. -local function is_elf64(file) - local f = io.open(file, "rb") - if not f then return false end - local head = f:read(5) - f:close() - return head ~= nil and #head == 5 - and head:sub(1, 4) == "\127ELF" and head:byte(5) == 2 -end - --- Link the host's GL runtime into one directory, FIRST HIT WINS, 64-bit only. --- --- openxlings/xlings' mcpp#352: on Fedora 44 this produced --- libGLX.so.0 -> /usr/lib/libGLX.so.0 --- a 32-bit library, and the application died with --- libGLX.so.0: wrong ELF class: ELFCLASS32 --- --- TWO BUGS, and the obvious diagnosis ("the candidate order assumes Debian") is --- not either of them -- `/usr/lib64` is already ahead of `/usr/lib` in the list: --- --- 1. `ln -sf` OVERWRITES. The loop reached /usr/lib64 first and linked the --- correct file, then reached /usr/lib and replaced it. Last-wins, not --- first-wins. `libOpenGL.so.0` survived as 64-bit purely because that host's --- 32-bit glvnd does not ship it -- which is why exactly one link in the bug --- report was right. --- 2. NO ABI CHECK ANYWHERE, including in `required` below, which asserted that --- libGLX.so.0 and libGL.so.1 EXIST. Both existed. Both were 32-bit. --- --- There is no directory layout to assume: the FHS biarch clause makes /usr/lib --- 32-bit (Fedora/RHEL/SUSE), Debian explicitly declined that clause and uses --- /usr/lib/ so its /usr/lib is 64-bit, and Arch is a third answer --- again. So the fix cannot be a better ordering -- it has to be an ABI check, --- which makes the order stop mattering. local function link_runtime_libs(outdir) os.mkdir(outdir) - local claimed = {} for _, dir in ipairs(candidate_dirs()) do for _, pattern in ipairs(host_gl_patterns) do - -- Enumerate, then decide per file, instead of letting the shell - -- link them: the decision needs the ELF class and "have I already - -- taken this name", neither of which a `ln -sf` loop can express. - local pipe = io.popen("ls -1 " .. sh_quote(dir) .. "/" .. pattern - .. " 2>/dev/null") - if pipe then - for line in pipe:lines() do - local lib = line:gsub("[\r\n]+$", "") - local name = lib:match("[^/]+$") - if lib ~= "" and name and not claimed[name] - and is_elf64(lib) then - claimed[name] = lib - os.exec("ln -sf " .. sh_quote(lib) .. " " - .. sh_quote(path.join(outdir, name))) - end - end - pipe:close() - end + os.exec( + "for lib in " .. sh_quote(dir) .. "/" .. pattern .. + "; do [ -e \"$lib\" ] || continue; " .. + "ln -sf \"$lib\" " .. sh_quote(outdir) .. "/\"$(basename \"$lib\")\"; " .. + "done" + ) end end for name, _ in pairs(required) do - local link = path.join(outdir, name) - -- Existence AND ABI. Existence alone passed on the Fedora host with - -- both links 32-bit, which is how a broken package reported success and - -- the failure surfaced as a silent exit code 255 from the application. - if not os.isfile(link) then - log.error("required host GL runtime library not found: %s", name) - log.error(" searched: %s", table.concat(candidate_dirs(), " ")) - log.error(" install your distro's GL runtime (mesa / libglvnd)") + if not os.isfile(path.join(outdir, name)) then + log.error("%s is not in this subos. The GL runtime comes from " + .. "`xim:graphics`; if it is declared and this still " + .. "fires, the stack did not finish installing", name) return false end - if not is_elf64(link) then - log.error("host %s is not 64-bit (%s)", name, claimed[name] or link) - log.error(" a 32-bit library here fails at dlopen with") - log.error(" `wrong ELF class: ELFCLASS32` and the application") - log.error(" exits without output. Install the 64-bit GL runtime.") + end + + -- Nothing resembling a C runtime may have come along. Asserted rather + -- than trusted: the pattern list is what keeps it out, and a pattern is + -- one careless edit away from matching more than it meant to. The failure + -- it prevents has no diagnostic of its own -- the consumer dies inside + -- the dynamic linker before main, printing nothing. + for _, bad in ipairs({"libc.so.6", "libc.so", "ld-linux-x86-64.so.2", + "libpthread.so.0", "libdl.so.2", "libm.so.6"}) do + if os.isfile(path.join(outdir, bad)) then + log.error("%s was linked into the GL runtime directory. It would " + .. "land on every consumer's RUNPATH and pair a second " + .. "libc with mcpp's loader, which faults before main " + .. "with no output at all", bad) return false end end - log.info("glx-runtime: linked %d host GL libraries (64-bit)", - (function() local n = 0 for _ in pairs(claimed) do n = n + 1 end return n end)()) return true end From 22ca4e7511db7e59c198b9a3a0e5ef798a8e3550 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sat, 8 Aug 2026 02:20:40 +0800 Subject: [PATCH 2/2] fix(glx-runtime): deps belong beside the version entries, not inside one CI: `E_INTERNAL: [glx-runtime] failed:` with an empty message, twice. The `deps = { runtime = { "xim:graphics" } }` had been placed inside the 2026.08.08 version entry. The descriptor parsed, the graphics stack was never installed, the subos therefore had no libGL.so.1, and the install failed on the required-library check -- reporting the library rather than the misplaced key, which is why the message looked like a missing package instead of a typo. Every other recipe in both indexes puts deps at platform level. Moving it there means the legacy 2026.06.03 entry declares it too and pays for a download it does not read; the alternative is deleting a published version, which breaks anyone pinned to it outright. --- pkgs/c/compat.glx-runtime.lua | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/pkgs/c/compat.glx-runtime.lua b/pkgs/c/compat.glx-runtime.lua index 16257b3b..9db30d4e 100644 --- a/pkgs/c/compat.glx-runtime.lua +++ b/pkgs/c/compat.glx-runtime.lua @@ -27,16 +27,30 @@ package = { -- pass, because llvmpipe renders one too. xpm = { linux = { + -- The whole hermetic graphics stack. A RUNTIME dep, not a build + -- one: nothing here compiles against it, the produced consumer + -- loads it. + -- + -- PLATFORM level, beside the version entries rather than inside + -- one. Every other recipe in both indexes places it here, and the + -- first attempt at this change put it inside the 2026.08.08 entry: + -- the descriptor parsed, the stack was never installed, and the + -- install failed on the required-library check -- an error naming + -- libGL.so.1 rather than the misplaced key. Whether a per-version + -- `deps` is rejected or merely unread was not determined; what is + -- established is that it does not take effect. + -- + -- It therefore also applies to the legacy 2026.06.03 entry below, + -- which does not use it. That costs a consumer still pinned there + -- a download it will not read, and the alternative -- deleting the + -- published version -- would break them outright. + deps = { runtime = { "xim:graphics" } }, ["2026.08.08"] = { url = { GLOBAL = "https://raw.githubusercontent.com/KhronosGroup/OpenGL-Registry/a30033d3e812c9bf10094f1010374a6b15e192eb/README.adoc", CN = "https://gitcode.com/mcpp-res/glx-runtime/releases/download/2026.08.08/glx-runtime-2026.08.08.adoc", }, sha256 = "ea68efce197e68413ebb62c51ab4bccfb2309a2fca776d31b49d972f59f3640e", - -- The whole hermetic graphics stack. A RUNTIME dep, not a - -- build one: nothing here compiles against it, the produced - -- consumer loads it. - deps = { runtime = { "xim:graphics" } }, }, -- Kept so already-published consumers pinned to it keep resolving. -- It sources libGL from the HOST and is the configuration behind