Firefox 92 on a PowerMac G5: Cross-Compiling 2021 Code with a 2026 Toolchain

Or: how far you can push big-endian ppc64 before the GPU driver says no.

My PowerMac G5 runs ArchPOWER — Arch Linux for ppc64, big-endian, using the ELFv2 ABI. Firefox 157 runs there, but only with software rendering. So the idea was simple enough: Firefox 92 was the last release with the classic OpenGL compositor, before WebRender became mandatory. If anything could give this machine GPU-accelerated browsing, it would be FF92.

This is the write-up of that experiment: the cross-toolchain, the crashes, the big-endian surprises, what works today, and an honest verdict on what probably never will.

The setup

  • Target: PowerMac G5 (dual PPC970MP), ArchPOWER, powerpc64-unknown-linux-gnu, big-endian, ELFv2
  • Build host: Apple Silicon Mac, with an Arch Linux ARM (aarch64) VM under QEMU/HVF
  • Method: full cross-compilation, sysroot pulled from the G5

One thing worth repeating, because it trips people up constantly: ELFv2 is an ABI, not an endianness. The PPC970 has no problem with big-endian ELFv2 — ArchPOWER’s GCC is built with --with-abi=elfv2, and readelf -h on every binary produced here reports Flags: 0x2, abiv2.

The toolchain

The C/C++ side ended up as a GCC 16.2.0 cross-toolchain configured with --with-abi=elfv2, matching the native GCC on the G5. Earlier attempts with clang/lld ran into ELFv2 friction; using the same compiler family as the target turned out to be the pragmatic choice. Clang 23 is still used as the actual compiler driver in some places, with GNU ld from the cross-binutils doing the linking.

Rust was the interesting part. Modern rustc targets powerpc64-unknown-linux-gnu as ELFv1 by default; ELFv2 is only assumed for ppc64le (and musl). So I built a patched rustc 1.99.1 that forces the ELFv2 ABI for big-endian ppc64, following the approach ArchPOWER uses. Verified the hard way: compiled a test binary, copied it to the G5, ran it.

Epyrus: the assembler lesson

Epyrus (UXP-based) was the warm-up. It built, but crashed on startup. The root cause was a patch I had written myself for xptcinvoke_asm_ppc64_linux.S: I had “fixed” the frame and LR layout for ELFv2. That was wrong. The upstream code already handles ELFv2 correctly; the only thing needed was syntax fixes so clang’s assembler accepts it (.set register aliases replaced, %rN/%fN forms).

Reverting to the upstream semantics and keeping only the syntax changes made Epyrus work: GUI starts, account setup and main window appear, stable, no core dumps.

Lesson: when porting assembly between ABIs, change as little as possible, and never “improve” the stack layout on a hunch.

Firefox 92: 2021 code, 2026 compilers

Building FF92 with clang 23, binutils 2.44, glibc 2.4x and Python 3.9 is an exercise in archaeology. The build system needed Python ≤ 3.11 (3.9 here), and then a long tail of small incompatibilities:

Problem Fix
RustTarget::Stable_1_25 gone in bindgen 0.72 use Stable_1_33
bindgen 0.56 too old for modern libclang upgrade to 0.72, pass --target, --sysroot, -mabi=elfv2
-fexperimental-new-pass-manager unknown to clang 23 remove from flags.configure
-Werror=implicit-int-conversion in Skia/WebGPU moz.build drop the flag
[#include <cstdint>] missing all over (newer libstdc++ doesn’t transitively include it) add includes (Rect.h, pingsender.cpp, …)
glibc ≥ 2.36 provides arc4random* rename libevent’s copies, make them non-static
ExclusiveData move ctor used rhs.lock instead of rhs.lock_ fix the typo, delete the unusable move ctor
#![feature(stdsimd)] in qcms drop the neon-only feature gates
\.S files forced through the external assembler .abiversion 2 at the top, cross as in PATH
Wayland headers expand to wl_proxy_marshal_flags link wayland-client explicitly

Then came the actual runtime problems.

The Stylo crash

Firefox started and immediately died in Servo’s style system. The crash moved between runs: CustomPropertiesBuilder::build, Cascade::apply_properties, StyleAdjuster::adjust_for_prohibited_display_contents. Pointers that should have been valid were garbage; one register held 0x0100000000000000 — which turned out to be the contents of a struct, not a pointer to it.

That randomness is the signature. It is not a logic bug; it is undefined behaviour finally being taken seriously by a much newer LLVM. FF92’s Rust code is from 2021; rustc 1.99 ships LLVM 23 and optimizes far more aggressively.

The pragmatic test that turned the tide: rebuild the Rust side with -C opt-level=1 instead of the default -O3.

# mozconfig
export RUSTFLAGS="-C opt-level=1"

The Stylo crashes disappeared completely. The browser got past layout, into graphics initialisation. That is the point where you know you are dealing with a miscompile and not with your own patches.

NSS and the illegal instruction

Next: SIGILL in libfreeblpriv3.so, on lxvd2x/stxvd2x — VSX vector instructions. The PPC970 predates VSX by a generation.

The cause is subtle: NSS builds sha512.c and friends with -mvsx so the POWER8 crypto paths compile. The POWER8-specific code is guarded at runtime, but -mvsx applies to the whole translation unit — so clang happily auto-vectorised the generic C fallback with VSX instructions too. Runtime detection cannot save you from your own code generator.

Fix, in NSS:

# security/nss/coreconf/config.gypi
'disable_crypto_vsx%': 1,

plus export NSS_DISABLE_CRYPTO_VSX=1. On a G5 this loses nothing: there is no VSX to use.

WebRender and big-endian formats

With Stylo and NSS sorted, the next crash was a deliberate one:

MOZ_RELEASE_ASSERT(false, "Unhandled external image format");

in RenderTextureHostSWGL::UpdatePlanes(). The switch accepted B8G8R8A8/B8G8R8X8 — the little-endian layouts — but on big-endian the native format is A8R8G8B8/X8R8G8B8, which fell into default: and aborted.

Adding the two big-endian formats to that switch fixed it:

case gfx::SurfaceFormat::B8G8R8A8:
case gfx::SurfaceFormat::B8G8R8X8:
case gfx::SurfaceFormat::A8R8G8B8:   // big-endian native
case gfx::SurfaceFormat::X8R8G8B8:
    internalFormat = LOCAL_GL_RGBA8;

Then the content process crashed in SharedSurfacesChild::ShareInternal() with the same pattern:

MOZ_RELEASE_ASSERT(format == SurfaceFormat::B8G8R8X8 ||
                   format == SurfaceFormat::B8G8R8A8, "bad format");

The correct fix here is not to add cases, but to use the endian-agnostic synonyms Gecko already provides:

MOZ_RELEASE_ASSERT(format == SurfaceFormat::X8R8G8B8_UINT32 ||
                   format == SurfaceFormat::A8R8G8B8_UINT32, "bad format");

On little-endian those aliases are B8G8R8X8/B8G8R8A8; on big-endian they resolve to the native forms. This is the trick the newer big-endian patches use everywhere.

It runs

After those fixes: Firefox 92 starts on the G5, renders pages, and stays up. Tabs, address bar, text, tables, form controls, the whole about:support page. WebGL works (Mesa NV43, GL 2.1). And on a clean session, with layers.acceleration.force-enabled=true and MOZ_WEBRENDER=0, about:support reports:

Compositing: OpenGL

So the legacy OpenGL compositor does engage on this machine. That was the goal. Unfortunately, that is not the end of the story.

The bug that is still open: images have wrong colours

CSS colours are correct. Images are not. Using a test page of solid-colour PNGs and sampling actual screen pixels, the mapping is:

PNG should be displayed as
black (0,0,0) (0,0,0)
white (255,255,255) (255,0,255)
cyan (0,255,255) (255,0,255)
magenta (255,0,255) (255,0,0)
yellow (255,255,0) (0,0,255)
gray (128,128,128) (128,0,128)
orange (255,128,0) (0,0,128)

In other words: out = (blue, 0, green). The green channel is always zero, red and blue are shifted. It happens identically with software compositing and with OpenGL, so it is not a compositor problem — it is in the decode/draw path.

Firefox 157 on the same machine does not have this bug. That is good news and bad news: the fix exists upstream, but it is a patch series, not a single line. My first attempt — changing the hard-coded little-endian formats in gfxPlatform::Optimal2DFormatForContent() to the endian-agnostic aliases — turned the whole browser window black. Reverted. The formats have to be consistent across decoder, surface, cairo and Skia; flipping one switch just moves the mismatch.

This is exactly the territory René Rebe (T2/SDE) and others covered for newer Gecko versions. Backporting that work to FF92 is possible, but it is a project with an unknown number of files, not an afternoon.

The honest verdict on GPU acceleration

The GPU is a GeForce 6600 LE (NV43) running nouveau. Mesa reports OpenGL 2.1, and that is the hard ceiling for this hardware/driver combination.

  • WebRender’s GL backend needs GL 3.3. Not available, not patchable.
  • The legacy OpenGL compositor (the one FF92 still has) does start — but in practice the machine shows rendering corruption that spreads beyond the application window, into terminal windows, and it also happens with Epyrus, independent of Firefox.

That last observation is the important one. It predates all of this work. And there is a very mundane explanation worth testing: no compositing manager was running. Without one there is no backing store; when windows overlap and an application fails to repaint, whatever was underneath stays on screen as garbage. That matches “the window produces glitches, the content becomes unreadable, and it spreads over the terminal window” much better than a GPU hang does — the kernel log shows no nouveau lockups at all.

Trying picom --backend xrender (or another compositor) is a fifteen-minute experiment that could improve the whole desktop, Epyrus included, without touching a single line of Firefox.

So: GPU acceleration on this machine is a driver/stack question, not a Firefox patch. FF157 runs software-rendered for a reason.

What I would tell the next person trying this

  1. Match the ABI, not the era. ELFv2 on big-endian ppc64 is real and works. But an old codebase plus a new compiler is a different animal: expect undefined behaviour to become visible.
  2. -C opt-level=1 is a great canary. If your crashes disappear at lower optimisation levels, you have a miscompile/UB problem, not a logic bug.
  3. Big-endian: use the aliases. A8R8G8B8_UINT32, OS_RGBA, X8R8G8B8_UINT32 exist precisely so you do not hard-code a byte order. Every hard-coded B8G8R8A8 is a latent BE bug.
  4. -mvsx poisons whole files. Runtime CPU detection is worthless if the compiler vectorised the fallback path with instructions the CPU does not have.
  5. Symbolise by offset. Copying a 1.1 GB libxul.so with DWARF across the network once, then using addr2line against offsets from coredumpctl/gdb, saved hours.
  6. Measure, don’t guess. xwd + ffmpeg + od -An -tu1 on a colour-swatch page turns “colours look wrong” into an exact channel mapping. XTest key injection gets you scrolling and screenshots on a headless-ish box.
  7. Keep the VM’s disk in mind. A Firefox build plus rustc plus LLVM filled a 59 GB disk and broke mach package with a confusing No space left on device mid-copy.

Where this leaves the project

What exists now:

  • A reproducible cross-build of Epyrus for ppc64 BE/ELFv2 that runs on the G5.
  • A working Firefox 92 build: starts, renders, WebGL works, stable under load — with software compositing, and with a known big-endian image colour bug.
  • A set of patches that are, as far as I can tell, the missing pieces for FF92 on big-endian ELFv2: Rust opt-level, NSS without VSX, SWGL endian formats, SharedSurfacesChild aliases, xptcall syntax, plus the usual “old code vs. new libc” fixes.

What does not exist: a GPU-accelerated, colour-correct, daily-driver browser on this hardware. FF92 gets close on the first two counts and fails on the third for reasons that live below Firefox.

If someone with deeper big-endian Gecko knowledge — wants to pick up the image-path backport, the measurements above should make it straightforward to verify each step. And if you just want to browse on a G5 today: FF157 with software rendering is still the sane answer.

Sometimes the value of an experiment is not the thing you hoped to build, but the map of where the walls are.

Webmentions

Reaktionen aus dem IndieWeb (Replies, Likes, Reposts).

  1. Lade Webmentions …