Repository navigation
gl-fwd.so segfaults on ppc64le when a GTK3 application preloads it #59
Description
gl-fwd.so segfaults on ppc64le when a GTK3 application preloads it, and I could not reduce it below the full GTK3 environment
Spotted while adding ppc64le/loongarch64/riscv64 to the Anylinux-AppImages demo matrix (pkgforge-dev/Anylinux-AppImages#969). The gtk3 demo builds fine on all four new arches and runs on three of them; on ppc64le only it dies.
The observation
The demo AppImage on ppc64le exits ~1s after extraction with status 0 and no output, which quick-sharun --test reports as a failure because the process is not alive after 12s. The exit status is lost through xvfb-run and the uruntime wrapper. Bisecting the preload directory by preloading one library at a time against the AppImage's own shared/bin/gtk3-demo with the AppDir's lib/:
LD_PRELOAD |
result |
|---|---|
| (none) | alive (124 = the 12s timeout) |
anylinux.so |
alive |
cross-libc-dlopen.so |
alive |
egl-fwd.so |
alive |
gles-fwd.so |
alive |
gl-fwd.so |
segfault (139) |
It is not the application and not the display: the distro gtk3-demo package stays alive for the full window in the same container, and the AppImage fails identically with a working Xvfb DISPLAY=:99. gl-fwd.so is only deployed when USE_HOST_DRIVERS_EXPERIMENTAL=1, which the gtk3 demo sets.
Why it is only ppc64le
- ppc64 (BE) passes because
quick-sharunforce-disablesUSE_HOST_DRIVERS_EXPERIMENTALthere (no cross-libc-dlopen support), sogl-fwd.sois never deployed. - riscv64 and loongarch64 deploy it too, and pass. So this is not "the shim is broken on the new arches", it is ppc64le-specific.
What I could not reproduce, and how hard I tried
I built the ppc64le trampolines and ran them under qemu-ppc64le against a real ppc64le glibc sysroot (Debian 2.41 and 2.44), using both the shipped v0.2.7 gl-fwd.so and a fresh zig build. Seven call shapes, all all passed:
- the repo's own
t_args/t_fret/t_absentvehicle frommake gl-fwd-qemu-check - the full generated 3470-symbol table, linked with
-lGLsoDT_NEEDEDbinds the shim dlopen+dlsym- unresolved slots reaching
glfwd_resolve_asm→glfwd_resolve_one→glfwd_absent - 13 live float arguments through the
stfd/lfdsave/restore - repeated calls through a patched slot
- a resolved target living in a separate module
Theories I chased and eliminated:
std 3,0(1)/mr 3,0/mflr 0ordering inglfwd_resolve_asmis correct — r0 is consumed into r3 beforemflrclobbers it.- Frame offsets: the 192-byte frame has no overlapping slots (
stfd 13→160..168,std 11→168..176, LR→184..192, 176..184 unused). - ELFv2 global-entry prologue is not needed: both
glfwd_resolve_asmandglfwd_absentareLOCAL HIDDEN, so the same-module TOC in r2 is valid. __tls_get_addr_opt(a PowerPC-only symbol the ppc64le shim imports) is exported by both the bundled and the host libc, and preloading onto a trivial binary succeeds.ld64.so.2inDT_NEEDEDis normal — x86_64 listsld-linux-x86-64.so.2and riscv64 listsld-linux-riscv64-lp64d.so.1the same way.
So the crash needs something the full GTK3 AppDir environment provides and my harness does not. My best guess is a GDK/GL path taken only by real GTK, but that is a guess and I would rather hand over the reproducer than a theory.
Reproducer
On a ppc64le container under qemu-user (this is what the AppImage CI does):
# from a checkout of Anylinux-AppImages at the PR above
sh ./useful-tools/demo/gtk3-demo-appimage.sh # builds dist/gtk3-demo-ppc64le.AppImage
APP=$(ls dist/*.AppImage)
APPIMAGE_EXTRACT_AND_RUN=1 APPIMAGE_TARGET_DIR=$PWD/_d "$APP" >/dev/null 2>&1 || :
cd _d
xvfb-run -a -- env LD_LIBRARY_PATH=$PWD/lib:$PWD/lib/sharun-preload \
LD_PRELOAD=$PWD/lib/sharun-preload/gl-fwd.so ./shared/bin/gtk3-demo; echo "exit=$?"
# exit=139, and no output at allPreloading any of the other four libraries the same way gives 124 (still running at the timeout).
Also worth flagging: CI never runs the check that would have caught this
make gl-fwd-qemu-check exists, is documented in docs/building.md, and docs/report/10-measured-versus-assumed.md §10 cites it as the evidence for the cross-arch trampolines. But no workflow runs it: gates.yml builds ppc64le and runs scripts/verify-artifacts.sh, which is a build gate, not a run gate.
$ grep -rn "gl-fwd-qemu-check\|gl-fwd-asm-check" .github/ scripts/
(nothing)
That is how a ppc64le-only runtime defect can ship. Wiring it into the build matrix is a separate, small change, and I am happy to send it — but it would not by itself explain this report, since the check passes for the shipped binary here.
Environment
cross-libc-dlopenv0.2.7, shippedgl-fwd.sofor ppc64le, 852,960 bytes, max symbolGLIBC_2.22,DT_NEEDED:libdl.so.2,libc.so.6,ld64.so.2- ArchPOWER ppc64le packages,
mesa1:26.2.3-1,gtk31:3.24.52-2,gtk3-demos1:3.24.52-2 qemu-ppc64le7.2.0 under aghcr.io/pkgforge-dev/archlinuxppc64le container- Host: x86_64, kernel 7.2.11,
binfmt_miscregistered byanylinux-setup-action'sregister-binfmt.sh
Requested by Samueru via errand.
Conversation: https://discord.com/channels/1313385177703256064/1561764843965649021
Activity
talaria0101 commentedon Oct 22, 2026
Found a ppc64le bug in the resolver that is a candidate cause. I have not confirmed it is what crashes gtk3-demo, because I could not run the GTK3 AppImage.
Bug.
glfwd_resolve_asminsrc/gl-fwd.c(ELFv2) saves the argument registers at0..56(sp). That is the frame header a C callee owns: back chain at 0, CR at 8, LR at 16, TOC at 24.glfwd_resolve_oneis not a leaf, and its generated code doesstd 0,16(1)beforestdu, so it overwrites the saved r5. The first call through any slot gets a corrupted third integer argument. Later calls go straight to the patched slot, so they are fine.Why the existing check missed it.
t_args(int, long, double, const char *)only uses r3, r4, f1 and r5 on the first call, and the pointer it checks is read back as garbage only if the clobbered value is dereferenced, which the test does not catch. (I did not work out why it still passed.)Reproduction. ppc64le under
qemu-ppc64le7.2 with the Debian ppc64el sysroot. The target takes 8 long arguments (1..8) and returns 0 if all match. First call through the shim on currentmain: nonzero (third argument wrong). Second call: 0.Fix. Save registers above a 112-byte header (48 bytes ELFv1 header plus a 64-byte parameter area, which also covers ELFv2). The frame goes from 192 to 304 bytes. I also added a first-call 8-argument case to
make gl-fwd-qemu-check. On the oldgl-fwd.cthat target printsFAIL ... ppc64le, on the new oneok. aarch64 also passes. riscv64 and loongarch64 were not run (no cross-gcc here) and their code is untouched.The fix is committed on a local branch and not pushed. Please rerun the gtk3 demo with a
gl-fwd.sobuilt from it. If it still segfaults after that, there is a second cause.I have not touched the CI point (nothing runs
gl-fwd-qemu-check).Nemo-010 commentedon Oct 22, 2026
Confirmed: your resolver bug is the cause, and the fix makes the gtk3-demo ppc64le AppImage start.
Your bug reproduces exactly as you described. ppc64le target taking 8 longs, first call through the shim on current
main(3095c15):The third argument is corrupted on the first call and correct on the second, which is the patched-slot signature you predicted. The mechanism is in the generated code for
glfwd_resolve_one— afterstdu 1, -128(1)it doesstd 0, 144(1), which is offset 16 of the caller's frame, the slotglfwd_resolve_asmsaved r5 into:Your fix works. Moving the saved registers above a 112-byte header (frame 192 -> 304) gives:
And it fixes the gtk3-demo segfault. I built the gtk3-demo AppImage for ppc64le under
qemu-ppc64lein the Anylinux-AppImages CI, then swapped onlylib/sharun-preload/gl-fwd.sofor a build of the patchedgl-fwd.c:gl-fwd.soshared/bin/gtk3-demoAnd the real path,
./AppRunwith the fixed library in place:So the answer to "if it still segfaults after that, there is a second cause" is: it does not, there is no second cause. One bug, and your fix closes it.
For the record, this also explains why I could not reproduce it earlier with the
t_args-style vehicle:t_args(int, long, double, const char *)passes r3, r4, f1 and r5, and r5 is the clobbered slot, but the corrupted value is theconst char *and the existing case only dereferencesd[0]/d[1]— a garbage pointer there is a crash or not depending on what the clobbered value happens to be, not a clean assertion failure. Your 8-long case is the one that pins it deterministically. That is worth keeping.Two notes on the AppImages side, no action needed here:
USE_HOST_DRIVERS_EXPERIMENTALfor that demo sogl-fwd.sois never deployed — would have hidden this. I did not apply it.The CI point still stands: nothing runs
gl-fwd-qemu-check, which is why this shipped. Your new first-call 8-argument case is exactly what that check needs to run.Nemo-010 commentedon Oct 22, 2026
I re-verified your patch against current
main(3095c15) from scratch, and it holds. Since the branch you have is local, here is the exact change as a diff in case that is useful — one hunk,src/gl-fwd.conly, nothing else touched.Reproduced on current
main:Built with
zig cc -target powerpc64le-linux-gnu, run underqemu-ppc64le7.2 against a Debian ppc64el glibc 2.41 sysroot. The target is your 8-long vehicle (1..8, returns 0 only if all match).And the gtk3-demo AppImage, ppc64le, built in the Anylinux-AppImages CI with only
lib/sharun-preload/gl-fwd.soswapped:gl-fwd.soshared/bin/gtk3-demo./AppRunwith the patched library also stays alive. So there is no second cause.The patch, as verified
That is the whole change: the frame moves from 192 to 304 bytes and every saved slot moves above a 112-byte header (48-byte ELFv1 header plus a 64-byte parameter area, which covers ELFv2 as well). LR moves from 184 to 296, so it still sits in the last 8 bytes of the frame.
I am not opening a PR for this because the fix is yours and you said it is on a local branch — it is your call how it lands. If you would rather I open one from a fork with your change plus the
make gl-fwd-qemu-checkcase, say so and I will.Two loose ends this leaves, both outside the code change:
gl-fwd-qemu-check.gates.ymlbuilds ppc64le and runsscripts/verify-artifacts.sh, which never executes a trampoline. Your new first-call 8-argument case is exactly what that target needs to carry, and wiring the target into the build matrix is what stops this class of bug shipping again.gl-fwd.soon ppc64le with host drivers still crashes. update sharun to 3.7.1, deploy its dash and notice, and build demos on riscv64, loongarch64, ppc64le and ppc64 Anylinux-AppImages#969 is green everywhere except that one job and will stay red until a release carries this.update cross-libc-dlopen to v0.2.8
update cross-libc-dlopen to v0.2.8
Correct two claims the review passes did not measure