From b8e4a84c3e7d2ba74c3a97b9391b1545380efa86 Mon Sep 17 00:00:00 2001 From: Ajmal Razaq Date: Wed, 16 Sep 2026 02:13:26 +0500 Subject: [PATCH 1/2] Use selective LD_PRELOAD to keep bundled libraries in sync with the host distro MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Linux tarball bundles every non-glibc runtime dependency from the CI runner (Ubuntu). On newer distros the bundled versions of system-integration libraries (xkbcommon, fontconfig, NSS, …) are too old, causing crashes on startup (dead_hamza keysym parse failure, missing NSSUTIL_3.108 symbols, incompatible fontconfig schemas). Fix this by LD_PRELOADing the host copy of every bundled library that exists on the running system, except for CEF's own graphics runtime (libEGL.so, libGLESv2.so, libvulkan.so.1, libvk_swiftshader.so, libcef.so) which must remain bundled to avoid ABI/rendering failures. This is an exclusion-based approach — a small protected_libs list names the CEF-owned files that must stay bundled, while everything else in libexec/ is automatically overridden when a host copy is found. No manually maintained whitelist is needed: new libraries added to the bundle will get the host-override treatment automatically. Multi-architecture support: the launcher probes distro-specific library directories (Debian multiarch, Fedora/RHEL lib64, Arch flat /usr/lib) at runtime so it works across all major Linux distributions. Fixes #753 --- scripts/release/packaging/linux/tarball.sh | 78 +++++++++++++++++++ scripts/release/tests/linux-packaging.test.ts | 8 ++ 2 files changed, 86 insertions(+) diff --git a/scripts/release/packaging/linux/tarball.sh b/scripts/release/packaging/linux/tarball.sh index ace63e41a..4b363b563 100755 --- a/scripts/release/packaging/linux/tarball.sh +++ b/scripts/release/packaging/linux/tarball.sh @@ -76,6 +76,84 @@ if resolved_path="$(readlink -f "$script_path" 2>/dev/null)"; then fi bin_dir="$(cd "$(dirname "$script_path")" && pwd)" + +# CEF ships its own graphics runtime that must remain bundled — loading host +# versions of these libraries causes ABI/rendering failures. Every other +# library in libexec/ may safely be replaced by the host copy when one exists, +# which keeps system-integration libraries (xkbcommon, fontconfig, NSS, …) in +# sync with the running distro. +protected_libs=( + libcef.so + libEGL.so + libGLESv2.so + libvk_swiftshader.so + libvulkan.so.1 +) + +is_protected() { + local name="$1" + for p in "${protected_libs[@]}"; do + [[ "$name" == "$p" ]] && return 0 + done + return 1 +} + +case "$(uname -m)" in + x86_64 | amd64) + system_lib_dirs=( + /usr/lib/x86_64-linux-gnu + /usr/lib64 + /usr/lib + /usr/local/lib64 + /usr/local/lib + /lib/x86_64-linux-gnu + /lib64 + /lib + ) + ;; + aarch64 | arm64) + system_lib_dirs=( + /usr/lib/aarch64-linux-gnu + /usr/lib64 + /usr/lib + /usr/local/lib + /lib/aarch64-linux-gnu + /lib64 + /lib + ) + ;; + *) + system_lib_dirs=( + /usr/lib64 + /usr/lib + /usr/local/lib64 + /usr/local/lib + /lib64 + /lib + ) + ;; +esac + +libexec_dir="${bin_dir}/../libexec" +preloads=() +for lib_path in "${libexec_dir}"/*.so "${libexec_dir}"/*.so.*; do + [[ -f "$lib_path" ]] || continue + lib="$(basename "$lib_path")" + is_protected "$lib" && continue + + for dir in "${system_lib_dirs[@]}"; do + if [[ -f "${dir}/${lib}" ]]; then + preloads+=("${dir}/${lib}") + break + fi + done +done + +if [[ ${#preloads[@]} -gt 0 ]]; then + preload_str="$(IFS=:; echo "${preloads[*]}")" + export LD_PRELOAD="${preload_str}${LD_PRELOAD:+:$LD_PRELOAD}" +fi + exec "${bin_dir}/../libexec/athas" \ --ozone-platform=x11 \ --disable-vulkan \ diff --git a/scripts/release/tests/linux-packaging.test.ts b/scripts/release/tests/linux-packaging.test.ts index 93733095e..c6ede72ad 100644 --- a/scripts/release/tests/linux-packaging.test.ts +++ b/scripts/release/tests/linux-packaging.test.ts @@ -61,6 +61,14 @@ describe("Linux release packaging", () => { expect(script).toContain("is_glibc_runtime_library"); expect(script).toContain("patchelf --add-rpath '$ORIGIN'"); expect(script).toContain("${app_dir_name}/libexec/libgdk_pixbuf-2.0.so.0"); + // Launcher protects CEF graphics libs while preloading host copies for + // every other bundled library — preventing ABI drift without a manually + // maintained whitelist. + expect(script).toContain("protected_libs=("); + expect(script).toContain("is_protected"); + expect(script).toContain("system_lib_dirs=("); + expect(script).toContain('export LD_PRELOAD="${preload_str}${LD_PRELOAD:+:$LD_PRELOAD}"'); + expect(script).not.toContain("export LD_LIBRARY_PATH="); }); it("preserves the root-owned setuid sandbox contract in Debian packages", () => { From 71a57d7886aa7f33fd2b7df1a393ffda5bb00c79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20=C3=96zg=C3=BCl?= Date: Thu, 17 Sep 2026 17:45:07 +0300 Subject: [PATCH 2/2] Limit Linux host overrides to integration libraries Keep compiler and CEF runtimes bundled instead of replacing every shared library with the host copy. Select XKB and NSS dependency groups together and retain the bundle when a required host library is missing. Exercise launcher selection, fallback, library paths, and argument forwarding with executable shell regression tests. --- scripts/release/packaging/linux/tarball.sh | 64 +++---- scripts/release/tests/linux-packaging.test.ts | 7 - .../tests/linux-tarball-launcher.test.ts | 180 ++++++++++++++++++ 3 files changed, 212 insertions(+), 39 deletions(-) create mode 100644 scripts/release/tests/linux-tarball-launcher.test.ts diff --git a/scripts/release/packaging/linux/tarball.sh b/scripts/release/packaging/linux/tarball.sh index 4b363b563..efa9c1913 100755 --- a/scripts/release/packaging/linux/tarball.sh +++ b/scripts/release/packaging/linux/tarball.sh @@ -77,27 +77,6 @@ fi bin_dir="$(cd "$(dirname "$script_path")" && pwd)" -# CEF ships its own graphics runtime that must remain bundled — loading host -# versions of these libraries causes ABI/rendering failures. Every other -# library in libexec/ may safely be replaced by the host copy when one exists, -# which keeps system-integration libraries (xkbcommon, fontconfig, NSS, …) in -# sync with the running distro. -protected_libs=( - libcef.so - libEGL.so - libGLESv2.so - libvk_swiftshader.so - libvulkan.so.1 -) - -is_protected() { - local name="$1" - for p in "${protected_libs[@]}"; do - [[ "$name" == "$p" ]] && return 0 - done - return 1 -} - case "$(uname -m)" in x86_64 | amd64) system_lib_dirs=( @@ -136,18 +115,39 @@ esac libexec_dir="${bin_dir}/../libexec" preloads=() -for lib_path in "${libexec_dir}"/*.so "${libexec_dir}"/*.so.*; do - [[ -f "$lib_path" ]] || continue - lib="$(basename "$lib_path")" - is_protected "$lib" && continue - - for dir in "${system_lib_dirs[@]}"; do - if [[ -f "${dir}/${lib}" ]]; then - preloads+=("${dir}/${lib}") - break - fi + +prefer_host_libraries() { + local lib dir host_path + local group_preloads=() + + for lib in "$@"; do + [[ -f "${libexec_dir}/${lib}" ]] || continue + host_path="" + for dir in "${system_lib_dirs[@]}"; do + if [[ -f "${dir}/${lib}" ]]; then + host_path="${dir}/${lib}" + break + fi + done + + # Keep a dependency group bundled when any required host copy is missing. + [[ -n "$host_path" ]] || return 0 + group_preloads+=("$host_path") done -done + + if [[ ${#group_preloads[@]} -gt 0 ]]; then + preloads+=("${group_preloads[@]}") + fi +} + +# Only replace libraries that integrate with host data or runtime modules. +# CEF graphics and compiler runtimes such as libstdc++ must stay bundled. +prefer_host_libraries libxkbcommon.so.0 libxkbcommon-x11.so.0 +prefer_host_libraries libfontconfig.so.1 +prefer_host_libraries \ + libnspr4.so libplc4.so libplds4.so \ + libnssutil3.so libnss3.so libsmime3.so libssl3.so \ + libfreebl3.so libfreeblpriv3.so libsoftokn3.so libnssckbi.so libnssdbm3.so if [[ ${#preloads[@]} -gt 0 ]]; then preload_str="$(IFS=:; echo "${preloads[*]}")" diff --git a/scripts/release/tests/linux-packaging.test.ts b/scripts/release/tests/linux-packaging.test.ts index c6ede72ad..22f5ddd73 100644 --- a/scripts/release/tests/linux-packaging.test.ts +++ b/scripts/release/tests/linux-packaging.test.ts @@ -61,13 +61,6 @@ describe("Linux release packaging", () => { expect(script).toContain("is_glibc_runtime_library"); expect(script).toContain("patchelf --add-rpath '$ORIGIN'"); expect(script).toContain("${app_dir_name}/libexec/libgdk_pixbuf-2.0.so.0"); - // Launcher protects CEF graphics libs while preloading host copies for - // every other bundled library — preventing ABI drift without a manually - // maintained whitelist. - expect(script).toContain("protected_libs=("); - expect(script).toContain("is_protected"); - expect(script).toContain("system_lib_dirs=("); - expect(script).toContain('export LD_PRELOAD="${preload_str}${LD_PRELOAD:+:$LD_PRELOAD}"'); expect(script).not.toContain("export LD_LIBRARY_PATH="); }); diff --git a/scripts/release/tests/linux-tarball-launcher.test.ts b/scripts/release/tests/linux-tarball-launcher.test.ts new file mode 100644 index 000000000..edf940177 --- /dev/null +++ b/scripts/release/tests/linux-tarball-launcher.test.ts @@ -0,0 +1,180 @@ +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; + +const packagingScript = fs.readFileSync( + path.resolve(import.meta.dirname, "../packaging/linux/tarball.sh"), + "utf8", +); +const launcher = packagingScript.split("<<'EOF'\n")[1]?.split("\nEOF")[0]; + +function shellQuote(value: string) { + return `'${value.replace(/'/g, "'\\''")}'`; +} + +function runLauncher({ + bundled = [], + host = [], + arch = "x86_64", + preload = "", + args = [], +}: { + bundled?: string[]; + host?: string[]; + arch?: string; + preload?: string; + args?: string[]; +}) { + if (!launcher) throw new Error("Tarball launcher heredoc was not found"); + + const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "athas-launcher-"))); + try { + for (const file of [...bundled.map((name) => `/libexec/${name}`), ...host]) { + const filePath = path.join(root, file); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, ""); + } + const binDir = path.join(root, "bin"); + fs.mkdirSync(binDir); + const launcherPath = path.join(binDir, "athas"); + // Redirect only the host filesystem and final exec; run the real selection logic. + const fixtureLauncher = launcher + .replace( + /^[ \t]+\/(?:usr\/)?lib\S*$/gm, + (line) => ` ${shellQuote(path.join(root, line.trim()))}`, + ) + .replace( + '\ncase "$(uname -m)" in', + `\nexport LD_PRELOAD=${shellQuote(preload)}\ncase "$(uname -m)" in`, + ); + fs.writeFileSync( + launcherPath, + [ + `uname() { printf '%s\\n' ${shellQuote(arch)}; }`, + `exec() { printf '%s\\0' "\${LD_PRELOAD-}" "$@"; }`, + fixtureLauncher, + ].join("\n"), + ); + const env = { ...process.env }; + delete env.LD_PRELOAD; + const result = spawnSync("bash", [launcherPath, ...args], { env, encoding: "utf8" }); + expect(result.error).toBeUndefined(); + expect(result.stderr).toBe(""); + expect(result.status).toBe(0); + const [selected, executable, ...forwardedArgs] = result.stdout.split("\0").slice(0, -1); + expect(path.normalize(executable!)).toBe(path.join(root, "libexec/athas")); + return { + preloads: selected ? selected.split(":").map((file) => file.replace(root, "")) : [], + args: forwardedArgs, + }; + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +} + +describe("Linux tarball launcher", () => { + it("replaces only host-integration libraries, retaining compiler and CEF runtimes", () => { + const integration = ["libxkbcommon.so.0", "libfontconfig.so.1", "libnssutil3.so"]; + const retained = [ + "libstdc++.so.6", + "libgcc_s.so.1", + "libcef.so", + "libEGL.so", + "libGLESv2.so", + "libvk_swiftshader.so", + "libvulkan.so.1", + "libglib-2.0.so.0", + "libunrelated.so.1", + ]; + const libraries = [...integration, ...retained]; + const result = runLauncher({ + bundled: libraries, + host: libraries.map((lib) => `/usr/lib/${lib}`), + }); + expect(result.preloads).toEqual(integration.map((lib) => `/usr/lib/${lib}`)); + }); + + it.each([ + ["x86_64", "/usr/lib/x86_64-linux-gnu"], + ["x86_64", "/usr/lib64"], + ["x86_64", "/usr/lib"], + ["x86_64", "/lib/x86_64-linux-gnu"], + ["aarch64", "/usr/lib/aarch64-linux-gnu"], + ["aarch64", "/lib/aarch64-linux-gnu"], + ["aarch64", "/lib64"], + ["aarch64", "/lib"], + ])("finds host libraries on %s in %s", (arch, dir) => { + const file = `${dir}/libfontconfig.so.1`; + expect(runLauncher({ arch, bundled: ["libfontconfig.so.1"], host: [file] }).preloads).toEqual([ + file, + ]); + }); + + it("prefers the architecture-specific directory and does not preload both copies", () => { + const file = "/usr/lib/x86_64-linux-gnu/libfontconfig.so.1"; + expect( + runLauncher({ + bundled: ["libfontconfig.so.1"], + host: [file, "/usr/lib/libfontconfig.so.1"], + }).preloads, + ).toEqual([file]); + }); + + it("keeps bundled libraries when host copies are unavailable", () => { + expect( + runLauncher({ bundled: ["libxkbcommon.so.0", "libfontconfig.so.1", "libnssutil3.so"] }) + .preloads, + ).toEqual([]); + }); + + it("does not preload host libraries that are not bundled", () => { + expect(runLauncher({ host: ["/usr/lib/libfontconfig.so.1"] }).preloads).toEqual([]); + }); + + it.each([ + { name: "XKB", libraries: ["libxkbcommon.so.0", "libxkbcommon-x11.so.0"] }, + { + name: "NSS/NSPR", + libraries: ["libnspr4.so", "libplc4.so", "libplds4.so", "libnssutil3.so", "libnss3.so"], + }, + ])("switches the bundled $name group together", ({ libraries }) => { + const host = libraries.map((lib) => `/usr/lib/${lib}`); + expect(runLauncher({ bundled: libraries, host }).preloads).toEqual(host); + for (const missing of host) { + expect( + runLauncher({ + bundled: [...libraries, "libfontconfig.so.1"], + host: [...host.filter((file) => file !== missing), "/usr/lib/libfontconfig.so.1"], + }).preloads, + ).toEqual(["/usr/lib/libfontconfig.so.1"]); + } + }); + + it.each([true, false])( + "preserves caller preloads and arguments with host selection %s", + (found) => { + const file = "/usr/lib/libfontconfig.so.1"; + const result = runLauncher({ + bundled: ["libfontconfig.so.1"], + host: found ? [file] : [], + preload: "/custom/first.so:/custom/second.so", + args: ["/workspace/with spaces", "--new-window", ""], + }); + expect(result.preloads).toEqual([ + ...(found ? [file] : []), + "/custom/first.so", + "/custom/second.so", + ]); + expect(result.args).toEqual([ + "--ozone-platform=x11", + "--disable-vulkan", + "--disable-features=Vulkan", + "/workspace/with spaces", + "--new-window", + "", + ]); + }, + ); +});