Skip to content

Commit 38ce338

Browse files
committed
fix(compat.glx-runtime): ln -sf was last-wins, and nothing checked the ABI (mcpp#352)
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/<triplet> 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
1 parent 52da54a commit 38ce338

1 file changed

Lines changed: 72 additions & 7 deletions

File tree

pkgs/c/compat.glx-runtime.lua

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,25 +104,90 @@ local required = {
104104
["libGL.so.1"] = false,
105105
}
106106

107+
-- Is FILE a 64-bit ELF? e_ident[EI_CLASS] == ELFCLASS64.
108+
--
109+
-- Five bytes read directly. `file`/`readelf`/`patchelf` would each answer this
110+
-- and each may be absent when a hook runs, and a probe that answers "cannot
111+
-- tell" by assuming "fine" is the bug below.
112+
local function is_elf64(file)
113+
local f = io.open(file, "rb")
114+
if not f then return false end
115+
local head = f:read(5)
116+
f:close()
117+
return head ~= nil and #head == 5
118+
and head:sub(1, 4) == "\127ELF" and head:byte(5) == 2
119+
end
120+
121+
-- Link the host's GL runtime into one directory, FIRST HIT WINS, 64-bit only.
122+
--
123+
-- openxlings/xlings' mcpp#352: on Fedora 44 this produced
124+
-- libGLX.so.0 -> /usr/lib/libGLX.so.0
125+
-- a 32-bit library, and the application died with
126+
-- libGLX.so.0: wrong ELF class: ELFCLASS32
127+
--
128+
-- TWO BUGS, and the obvious diagnosis ("the candidate order assumes Debian") is
129+
-- not either of them -- `/usr/lib64` is already ahead of `/usr/lib` in the list:
130+
--
131+
-- 1. `ln -sf` OVERWRITES. The loop reached /usr/lib64 first and linked the
132+
-- correct file, then reached /usr/lib and replaced it. Last-wins, not
133+
-- first-wins. `libOpenGL.so.0` survived as 64-bit purely because that host's
134+
-- 32-bit glvnd does not ship it -- which is why exactly one link in the bug
135+
-- report was right.
136+
-- 2. NO ABI CHECK ANYWHERE, including in `required` below, which asserted that
137+
-- libGLX.so.0 and libGL.so.1 EXIST. Both existed. Both were 32-bit.
138+
--
139+
-- There is no directory layout to assume: the FHS biarch clause makes /usr/lib
140+
-- 32-bit (Fedora/RHEL/SUSE), Debian explicitly declined that clause and uses
141+
-- /usr/lib/<triplet> so its /usr/lib is 64-bit, and Arch is a third answer
142+
-- again. So the fix cannot be a better ordering -- it has to be an ABI check,
143+
-- which makes the order stop mattering.
107144
local function link_runtime_libs(outdir)
108145
os.mkdir(outdir)
146+
local claimed = {}
109147
for _, dir in ipairs(candidate_dirs()) do
110148
for _, pattern in ipairs(host_gl_patterns) do
111-
os.exec(
112-
"for lib in " .. sh_quote(dir) .. "/" .. pattern ..
113-
"; do [ -e \"$lib\" ] || continue; " ..
114-
"ln -sf \"$lib\" " .. sh_quote(outdir) .. "/\"$(basename \"$lib\")\"; " ..
115-
"done"
116-
)
149+
-- Enumerate, then decide per file, instead of letting the shell
150+
-- link them: the decision needs the ELF class and "have I already
151+
-- taken this name", neither of which a `ln -sf` loop can express.
152+
local pipe = io.popen("ls -1 " .. sh_quote(dir) .. "/" .. pattern
153+
.. " 2>/dev/null")
154+
if pipe then
155+
for line in pipe:lines() do
156+
local lib = line:gsub("[\r\n]+$", "")
157+
local name = lib:match("[^/]+$")
158+
if lib ~= "" and name and not claimed[name]
159+
and is_elf64(lib) then
160+
claimed[name] = lib
161+
os.exec("ln -sf " .. sh_quote(lib) .. " "
162+
.. sh_quote(path.join(outdir, name)))
163+
end
164+
end
165+
pipe:close()
166+
end
117167
end
118168
end
119169

120170
for name, _ in pairs(required) do
121-
if not os.isfile(path.join(outdir, name)) then
171+
local link = path.join(outdir, name)
172+
-- Existence AND ABI. Existence alone passed on the Fedora host with
173+
-- both links 32-bit, which is how a broken package reported success and
174+
-- the failure surfaced as a silent exit code 255 from the application.
175+
if not os.isfile(link) then
122176
log.error("required host GL runtime library not found: %s", name)
177+
log.error(" searched: %s", table.concat(candidate_dirs(), " "))
178+
log.error(" install your distro's GL runtime (mesa / libglvnd)")
179+
return false
180+
end
181+
if not is_elf64(link) then
182+
log.error("host %s is not 64-bit (%s)", name, claimed[name] or link)
183+
log.error(" a 32-bit library here fails at dlopen with")
184+
log.error(" `wrong ELF class: ELFCLASS32` and the application")
185+
log.error(" exits without output. Install the 64-bit GL runtime.")
123186
return false
124187
end
125188
end
189+
log.info("glx-runtime: linked %d host GL libraries (64-bit)",
190+
(function() local n = 0 for _ in pairs(claimed) do n = n + 1 end return n end)())
126191
return true
127192
end
128193

0 commit comments

Comments
 (0)