fix(overlay): preload libstdc++ so webkit2gtk's JSC can't hijack std::call_once#214
Conversation
…:call_once Arch's webkit2gtk 2.52.5-1 regressed to exporting libstdc++'s private std::call_once machinery (__once_proxy, __once_callable, __once_call) from libjavascriptcoregtk; 2.52.3 imported them correctly. Electrobun's libNativeWrapper.so links webkit2gtk even in CEF mode, so JSC lands in the overlay process ahead of libstdc++ and wins interposition for libstdc++'s own internal references. That splits std::call_once's TLS state in two: mesa's libLLVM (deepbind scope) writes the callback into libstdc++'s __once_callable, then libstdc++'s __once_proxy reads JSC's copy — NULL — and the process jumps to address 0x0. Trigger: radeonsi's LLVM shader-compiler init during CEF GL bring-up, ~200ms after start, on AMD hardware. zink escaped it only because ACO never touches LLVM. Fix: wrap the bun binary in a shim that prepends libstdc++.so.6 to LD_PRELOAD, restoring the real libstdc++ to the front of the lookup scope so every object binds the same copy. Injected post-build from inject-patched-wrapper.sh (runs from every build entry point). Idempotent; bare soname so the loader picks the right per-distro path; harmless on unaffected systems. The shim wraps bun itself because the electrobun launcher REPLACES LD_PRELOAD for its child, so unit-level env can never reach the process that matters. patchelf --remove-needed on the wrapper does not work as an alternative: Zig emits eager GLOB_DAT relocations for address-taken webkit symbols, so the load fails on undefined symbols. The real fix — rebuilding the wrapper without the webkit link in CEF mode — belongs upstream in electrobun. Verified on CachyOS / Radeon 890M (the machine that reproduces the crash 100%): with the shim the overlay service comes up on native radeonsi with NRestarts=0 and the #211 zink fallback never arms; the identical build without the shim segfaults on first start. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Testing this fix on your machine — no merge, no release, no build neededThe fix is a small shell shim around the installed 0. Confirm you're affected (10 seconds)nm -D /usr/lib/libjavascriptcoregtk-4.1.so.0 | grep -E "once_proxy|once_call"
Also check the crash signature matches: 1. Apply the shim to your existing installcd ~/.local/share/loadout-overlay/bin
mv bun bun.real
cat > bun <<'SHIM'
#!/bin/sh
export LD_PRELOAD="libstdc++.so.6${LD_PRELOAD:+:$LD_PRELOAD}"
exec "$(dirname "$0")/bun.real" "$@"
SHIM
chmod +x bun
# make sure the zink fallback marker isn't masking the test (harmless if absent)
rm -f "/run/user/$(id -u)/loadout-overlay-gl-fallback"
systemctl --user restart loadout-overlay2. Verify# 1 = healthy; the overlay should also actually open with your wake button
systemctl --user is-active loadout-overlay
# must be 0 new panics after the restart
journalctl --user -u loadout-overlay --since '-2min' | grep -c panic
# proves you're on NATIVE radeonsi, not the zink workaround:
# this file must NOT exist
ls "/run/user/$(id -u)/loadout-overlay-gl-fallback" 2>&1Expected: service 3. Roll back (if anything is off)cd ~/.local/share/loadout-overlay/bin
rm bun && mv bun.real bun
systemctl --user restart loadout-overlayNotes
Please report back: your GPU, distro, the 🤖 Generated with Claude Code |
…esting
One command takes a tester from an open PR to a running install:
sh scripts/install-pr.sh 214
Fetches GitHub's refs/pull/N/head (works for fork PRs without knowing
the contributor's branch), shallow-clones into ~/.cache/loadout-pr-test/
so an existing checkout is never touched, then runs the standard
bun install --frozen-lockfile + build-and-install. Accepts a branch
name as well as a PR number; re-running re-fetches the PR's current
head. Reverting is the README install one-liner.
Deliberately not under ~/.cache/loadout: the backend runs as root and
owns that directory, so a user-run script cannot create anything there
(found the hard way — first test run failed on mkdir).
Verified end-to-end with PR #214 itself on CachyOS: fetched 36324fc,
built (inject step shimmed the bun binary as designed), installed;
overlay active on native radeonsi with zero panics.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Easier way to test:
|
…nstall-pr - inject-patched-wrapper.sh: iterate find output line-by-line in both loops instead of word-splitting; the CEF tree already contains space-laden names and a space in any parent dir would have silently broken the loop. Verified against a 'dir with space' build tree, including idempotent rerun and empty-tree cases. - install-pr.sh: default to the public https remote (no SSH keys needed by testers); keep origin in sync with LOADOUT_REPO when the cached workdir is reused. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The radeonsi GL-init segfault, root-caused
This is the crash #211's zink fallback works around. Full chain, every link verified on the reproducing machine (CachyOS, Radeon 890M):
[extra]) regressed:libjavascriptcoregtk-4.1.so.0now exports libstdc++'s privatestd::call_oncemachinery —__once_proxy(T),_ZSt11__once_call/_ZSt15__once_callable(B). In 2.52.3 they areU(imports, correct). Diffed directly between the two packages in the pacman cache.libNativeWrapper.solinks webkit2gtk even in CEF mode (DT_NEEDED), so JSC enters the overlay process ahead of libstdc++ and wins interposition —LD_DEBUG=bindingsshows even libstdc++'s own internal references binding to JSC's copies.libLLVM(deepbind scope) binds the real libstdc++, so when radeonsi initializes its LLVM shader compiler (LLVMInitializeAMDGPUTarget→std::call_once), the writer stores the callback in libstdc++'s TLS slot and__once_proxyreads JSC's — NULL →call *0x0→ the exactSegmentation fault at address 0x0from the reports.GOT forensics for the record: at crash, libLLVM's
__once_proxyGOT slot is correctly resolved to libstdc++ — the NULL is one level down, in the interposed TLS variable.gdbshowsrip=0x0reached frompthread_onceinternals.Fix
A 9-line shim injected over the
bunbinary post-build (frominject-patched-wrapper.sh, which already runs from every build entry point): prependlibstdc++.so.6toLD_PRELOADand execbun.real. The real libstdc++ then heads the global lookup scope and every object binds the samestd::call_oncestate.LD_PRELOADfor its child (verified: an inherited entry is dropped), so unit-levelEnvironment=can never reach the process that matters. Upstream electrobun issue material.patchelf --remove-needed(tried): Zig emits eagerGLOB_DATrelocations for address-taken webkit symbols — the load dies onundefined symbol: webkit_web_context_get_default. The clean fix (wrapper built without webkit in CEF mode) belongs upstream./usr/libArch,/usr/lib64Fedora/Bazzite). Harmless where the bug is absent: libstdc++ is in the process either way. Idempotent injection; helpers inherit via env.Verification (CachyOS / Radeon 890M / 100% repro machine)
NRestarts=0, ready flag written, #211's zink marker never arms, native radeonsiMinimal-repro matrix that pinned the cause: plain
eglinfounder the exact CEF preloads works; plaindlopen(libLLVM) + LLVMInitializeAMDGPUTarget()works; only the JSC-containing process crashes — and preloading libstdc++ cures it.Relation to #211
Complementary, not competing. This removes the known trigger; #211's detect-and-retry remains the safety net for whatever regresses next (it caught this for a month). With both merged, affected machines stop crash-cycling entirely and stay on native radeonsi. Recommend #211's thread notes the root cause (comment incoming).
Follow-ups not in this PR: Arch bug for the webkit2gtk 2.52.5 symbol leak; electrobun issues for the
LD_PRELOADclobber and the unconditional webkit link.🤖 Generated with Claude Code