From 38ce338074695c0fc9c8c44098c8a05afa9ee1f2 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 7 Aug 2026 05:10:52 +0800 Subject: [PATCH] fix(compat.glx-runtime): `ln -sf` was last-wins, and nothing checked the ABI (mcpp#352) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Fedora 44 this package produced libGLX.so.0 -> /usr/lib/libGLX.so.0 # 32-bit libOpenGL.so.0 -> /usr/lib64/libOpenGL.so.0 and the application died with `libGLX.so.0: wrong ELF class: ELFCLASS32`, surfacing as a silent exit code 255 with no output at all. THE OBVIOUS DIAGNOSIS IS NOT THE BUG. The issue reads this as "generated assuming the Debian layout", but `/usr/lib64` is already ahead of `/usr/lib` in `candidate_dirs()`. Two other things were wrong: 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 only because that host's 32-bit glvnd does not ship it -- which is why exactly one link in the bug report was right, and that is evidence rather than coincidence. 2. **No ABI check anywhere**, including in `required`, which asserted that libGLX.so.0 and libGL.so.1 EXIST. Both existed. Both were 32-bit. So the package reported success. There is no layout to assume, which is why a better ordering is not the fix: 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. An ABI check makes the order stop mattering. So: enumerate and decide per file (first-wins, keyed by soname), read e_ident[EI_CLASS] out of the file itself -- five bytes, no external tool, because `file`/`readelf` may not be installed when a hook runs -- and make `required` assert 64-bit rather than merely present. Both failure paths now name the cause and what to install. Verified against a forged biarch host (32-bit libGLX.so.0 in /usr/lib, the real one in /usr/lib64): both required links resolve to /usr/lib64 and both are ELF64. The same fixture under the old loop lands on /usr/lib. The same three rules -- ask the loader, check the ABI, first hit wins -- are now one shared module in the xlings index (libs/hostlib.lua), where four call sites had four answers and three were wrong. Refs: mcpp-community/mcpp#352 Design: openxlings/xlings .agents/docs/2026-08-07-graphics-experience-industry-survey-and-plan.md §8.1, §9.2 --- pkgs/c/compat.glx-runtime.lua | 79 +++++++++++++++++++++++++++++++---- 1 file changed, 72 insertions(+), 7 deletions(-) diff --git a/pkgs/c/compat.glx-runtime.lua b/pkgs/c/compat.glx-runtime.lua index 2f70e563..8cc15d1a 100644 --- a/pkgs/c/compat.glx-runtime.lua +++ b/pkgs/c/compat.glx-runtime.lua @@ -104,25 +104,90 @@ 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 - os.exec( - "for lib in " .. sh_quote(dir) .. "/" .. pattern .. - "; do [ -e \"$lib\" ] || continue; " .. - "ln -sf \"$lib\" " .. sh_quote(outdir) .. "/\"$(basename \"$lib\")\"; " .. - "done" - ) + -- 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 end end for name, _ in pairs(required) do - if not os.isfile(path.join(outdir, name)) then + 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)") + 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.") 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