# Ladybird Flatpak — local build helperThis repo does **not** maintain its own Flatpak manifest for[Ladybird](https://github.com/LadybirdBrowser/ladybird). It builds**upstream's own manifest** instead:[`Meta/CMake/flatpak/org.ladybird.Ladybird.json`](https://github.com/LadybirdBrowser/ladybird/blob/master/Meta/CMake/flatpak/org.ladybird.Ladybird.json),maintained by the LadybirdBrowser team and built + tested on every single commit and PRvia their own[`.github/workflows/flatpak.yml`](https://github.com/LadybirdBrowser/ladybird/blob/master/.github/workflows/flatpak.yml)(both x86_64 and aarch64, currently green).## Why not our own manifestAn earlier version of this repo (see the `vcpkg-approach` branch) built an independentmanifest from scratch, driving Ladybird's normal vcpkg-based build inside Flatpak'snetwork-less sandbox. That got genuinely close — a real CI run compiled 68 of 69 vcpkgports successfully, including Skia — after solving a long chain of non-obvious problems(vcpkg's own bootstrap binary, googlesource's non-reproducible archive endpoint, a Daggercache-volume bug, bubblewrap needing privileged nesting, and more; see that branch'sREADME for the full account). Partway through fixing the very last vendoring gap, itturned out upstream had already solved the entire problem, more simply, by not usingvcpkg for the Flatpak build at all — every dependency is its own plain Flatpak modulewith a git-tag source — and had it running green in CI on every commit already. Buildingand maintaining a second, independent manifest against a fast-moving browser codebase,when a first-party one already exists and stays in lockstep with the code byconstruction, isn't a good use of ongoing effort. This repo pivoted to just building thatone instead.## Local buildRequires [`go-task`](https://taskfile.dev) and [`distrobox`](https://distrobox.it/) (samereasoning as before: `flatpak-builder` has no Homebrew formula and needs deepostree/bubblewrap/polkit integration that doesn't suit an immutable host directly).```task install-tools # once: creates the distrobox, installs flatpak-builder,# installs the org.kde.Platform/Sdk 6.10 runtime this manifest usestask build # clones LadybirdBrowser/ladybird into ./ladybird if needed (applying# patches/ -- see "Running it" below), then builds# Meta/CMake/flatpak/org.ladybird.Ladybird.jsontask run # flatpak run org.ladybird.Ladybird````task update` pulls the latest `ladybird/` checkout and reapplies `patches/` (`gitreset --hard` wipes them, same as a fresh clone would). `task clean` removes local buildartifacts (not the checkout itself).**Build confirmed working end to end on 2026-09-30**: `task build` compiles everydependency module (angle, skia, openssl, ffmpeg, etc.) and the main `Ladybird` moduleitself, and installs cleanly. Two build-time things worth knowing if you hit them:- `flatpak-builder` needs `--install-deps-from=flathub` (already in `task build`). Withoutit, it fails immediately with `Requested extensionorg.freedesktop.Sdk.Extension.llvm20/x86_64/6.10 not installed` — `llvm20`/`rust-stable`are never published under a branch numbered "6.10" on Flathub (confirmed directly againstthe remote), so a naive pre-install of the runtime doesn't satisfy the SDK's declaredextension points. `--install-deps-from` makes flatpak-builder resolve and install themcorrectly itself; upstream's CI gets this for free because its container image(`ghcr.io/flathub-infra/flatpak-github-actions:kde-6.10`) already has them deployed.- `--disable-rofiles-fuse` (already in `task build`) is needed inside the distrobox, sameas discovered on the `vcpkg-approach` branch — no FUSE mount available in the container.Unlike that branch's Dagger module, this distrobox did *not* need any extra privilegegrant for bubblewrap's per-step sandbox (it just worked), so `InsecureRootCapabilities`wasn't needed here — evidently distrobox containers already have sufficient namespacepermissions by default on this host.## Running it: three sandbox bugs found, fixed, and verifiedAt today's pinned master commit (`124f0c9871`), the app **would not run at all** withLinux sandboxing enabled — found three separate bugs doing so, all in Ladybird's own C++sandbox code, not in packaging. All three are now fixed by `patches/sandbox-fixes.patch`(applied automatically by `task clone`/`task update`/`task apply-patches` — **not**committed to Ladybird's own repo, since none of this is upstreamed yet) and **confirmedworking**: a real `flatpak run org.ladybird.Ladybird` (no `--disable-sandbox`, no otherflags) starts `RequestServer`, `Compositor`, and two `WebContent` processes, all stay up,and pages load with no crash loop.1. **Fontconfig loads after the sandbox restricts filesystem access, so it fails** —tracked upstream as[#11775](https://github.com/LadybirdBrowser/ladybird/issues/11775) (open, filed2026-09-15, reported against NixOS but is a general Linux-sandbox issue, notNix-specific — Flatpak's own filesystem restriction triggers the identical failure).The issue includes an unmerged, untested-by-its-author patch (force fontconfig to loadits config before the Landlock/seccomp restrictions go up, then hand Skia that already-loaded `FcConfig*` instead of letting it load a fresh one from inside the sandbox).Adapted into the patch here (`Services/Compositor/SandboxLinux.cpp`,`Services/RendererSandboxLinux.cpp`, `Libraries/LibGfx/Font/TypefaceSkia.cpp`) to thischeckout's current `Gfx::GlobalFontConfig` singleton (a `get()` accessor that didn'texist when the issue's patch was written). Confirmed fixed in isolation: the`Fontconfig error: Cannot load default config file` line disappeared from a rebuild withjust this change, before the other two below were found.2. **A second, separate, not-yet-reported bug**: even with the fontconfig fix, `WebContent`crash-looped immediately with `Runtime error: Landlock must be applied before theprocess starts a second thread` — a check that, per `git log`, didn't exist beforecommit `3fea952607` (2026-09-19, 11 days before our pin), added as a safety net whilefixing an *analogous* bug in `Compositor` (GPU driver threads starting before Landlockwas applied). That fix reordered `Compositor`'s startup; `WebContent` apparently has thesame ordering problem and was never updated to match, so it now hits the newly-strictcheck instead of silently running with the same gap `Compositor` had. Root-caused byadding temporary `/proc/self/task` diagnostics: the second thread is OpenSSL's threadpool (`OSSL_set_max_threads`, for its thread-pool provider) *and* SDL's gamepad hotplugthread (`SDL_Init(SDL_INIT_GAMEPAD)`), both called before the sandbox in`Services/WebContent/main.cpp`. Fixed the same way the precedent did: moved both toafter `RendererSandbox::apply_sandbox()`.3. **Moving SDL's init that late surfaced a third, related issue**: its HIDAPI/libusbbackend starts its own event thread, which then hits a seccomp violation(`disallowed syscall unknown (294)` — `sendmmsg`, used on the netlink socket libusbopens for device hotplug) that `WebContent`'s seccomp policy doesn't grant. Tried`SDL_HINT_HIDAPI_LIBUSB=0` (meant to make HIDAPI prefer the udev/hidraw backend insteadof libusb) — did **not** avoid it in testing (`hid_init()` still calls `libusb_init()`regardless; not fully root-caused why). Rather than widen the seccomp policy for afeature most pages never use (and risk over-granting — `LibSandbox`'s existing`allow_network()` that provides `sendmmsg` is a much broader grant than this narrow caseneeds), **the patch just skips gamepad initialization entirely while sandboxed**(`--disable-sandbox` still gets it). A real, known tradeoff, not a hidden one: theGamepad Web API won't work in the sandboxed build until someone scopes a properlynarrow seccomp allowance for whatever libusb's event thread actually needs, or finds whythe HIDAPI hint didn't take.If a leftover session from a crashed run leaves the browser refusing to start at all with`Runtime error: connect: Connection refused (errno=111)`, a stale single-instancelock/socket is why (hit repeatedly while testing, mostly from `timeout`-killing a testrun non-gracefully) — `task clean-runtime` removes it.## Fourth bug: every HTTPS load failed with "SSL verification failed"Once the sandbox bugs above were fixed and the browser actually ran, every single HTTPSpage failed with `Load failed: Request finished with error: SSL verification failed` —reproduced even with `--disable-sandbox`, so not a sandboxing issue at all. Root cause:the manifest's `openssl` module configures `--openssldir=/app/ssl`, and nothing in themanifest ever populates `/app/ssl/certs` with any CA certificates — confirmed empty aftera build. `Libraries/LibTLS/TLSv12.cpp` falls back to `SSL_CTX_set_default_verify_paths()`whenever no app-level custom certificate is set (the normal case), which resolves againstthat empty compiled-in default, so every chain fails to verify. The `org.kde.Platform`runtime itself does provide a real, current CA bundle at `/etc/ssl/cert.pem` (confirmedvalid PEM content, readable from inside the sandbox) — OpenSSL's default-path logic checksthe `SSL_CERT_FILE`/`SSL_CERT_DIR` environment variables before falling back to itscompiled-in paths, so pointing it at the runtime's bundle is a one-line fix with no C++changes needed: `patches/flatpak-ssl-cert-path.patch` adds`--env=SSL_CERT_FILE=/etc/ssl/cert.pem` to the manifest's `finish-args`. Confirmed fixedwith `--headless=text`: `https://example.com`, `https://news.ycombinator.com`, and`https://www.google.com` all now fetch and render real page content instead of erroring.## Flathub submissionNot yet submitted anywhere findable (checked `flathub/flathub` and the `flathub` org foran existing `org.ladybird.Ladybird` as of 2026-09-30). Since upstream owns and activelymaintains this manifest, they're the natural ones to submit it — worth raising with theLadybirdBrowser maintainers (e.g. as a repo issue) rather than a third party submitting ontheir behalf, unless they've already indicated otherwise somewhere this hasn't turned up.