Skip to content

fix-elf: don't write RPATH onto ld-linux-*.so.* (loader SIGSEGVs at startup) #345

Open

Description

@tannevaled

Summary

The post-install SLOW rpath fixes pass in fix-elf writes a transitive-deps RPATH chain onto every ELF in the install directory, including ld-linux-*.so.*. The loader parses its own RPATH at startup — before anything else is resolvable — and SIGSEGVs (exit 139), making the bottle unusable as a dynamic linker.

Surfaced building gnu.org/glibc (CI run 26226054550) but the bug applies to any package that ships its own ld-linux-*.so.*.

Reproduction

In the brewkit test sandbox of a glibc bottle:

$ "$LIBDIR/$LDSO" --version
Segmentation fault         "$LIBDIR/$LDSO" --version
$ echo $?
139
$ "$LIBDIR/$LDSO" /bin/true   # any binary, same result
Segmentation fault

readelf -d on the installed ld-linux-aarch64.so.1:

0x000000000000000f (RPATH)  Library rpath: [
  $ORIGIN/../../../../../sourceware.org/bzip2/v1:
  $ORIGIN/../../../../../lz4.org/v1:
  $ORIGIN/../../../../../curl.se/ca-certs/v2026:
  $ORIGIN/../../../../binutils/v2:
  $ORIGIN/../../../../gmp/v6:
  $ORIGIN/../../../../mpfr/v4:
  $ORIGIN/../../../../mpc/v1:
  …(46 entries total)…
]
0x000000000000000e (SONAME) Library soname: [ld-linux-aarch64.so.1]

Full log: https://github.com/pkgxdev/pantry/actions/runs/26226054550/job/77173340700 (search for "ld.so --version exit:").

Why it's wrong

ld.so is special: it's both the kernel-recognized program loader AND a shared object, and it bootstraps itself from nothing. It MUST NOT have RPATH / RUNPATH for two reasons:

  1. No NEEDED entries to resolve — ld.so has no DT_NEEDED; RPATH is dead weight at best.
  2. Parsed before any libraries exist — the loader resolves its own RPATH during early startup, before TLS/PLT/GOT are fully set up. A non-trivial RPATH (especially with $ORIGIN/... going to directories that may not exist on the consumer's machine) blows up the relocation phase.

Nix and the glibc upstream test suite both explicitly strip RPATH from ld.so post-link for this reason.

Workaround in the recipe

For pkgxdev/pantry#12968 I switched the recipe's test to a static-linked binary, sidestepping our own ld.so entirely:

test:
  script:
    - gcc -static -o test-static test.c \
        -nostdinc -isystem {{prefix}}/include \
        -B "$LIBDIR" -L "$LIBDIR"
    - out=$(./test-static)

That gets the recipe to PASS in CI but it means the bottle's dynamic-loader path is never exercised by brewkit's test step. Cross-distro verification has to happen out-of-band (we did it on Alpine 3.18, Debian 11, Ubuntu 22.04 across 9 glibc versions × 2 arches — see projects/gnu.org/glibc/README.md).

I also tried patchelf --remove-rpath on ld.so at test time — clears the RPATH cleanly per readelf -d, but the loader still SIGSEGVs. So fix-elf is doing more damage to ld.so than just RPATH pollution — likely relocation tables or program-header offsets get rewritten. That's a deeper investigation than I want to gate the glibc PR on.

Proposed fix

In fix-elf.ts (or whichever pass is responsible for the SLOW rpath fixes), skip any ELF whose basename matches ld-linux-*.so.* or ld-*.so.*:

const LOADER_RE = /^ld[-.].*\.so(\.\d+)*$/;
if (LOADER_RE.test(path.basename())) {
  console.info(`skip ${path}: loader (must have no RPATH)`);
  continue;
}

(Same pattern applies to fix-machos.rb and macOS's dyld, though I haven't tested whether it has the same problem.)

Open questions

  1. Should we additionally never touch INIT/FINI/RELA/RELR/HASH etc. on the loader? Just leaving its dynamic section verbatim is probably the safest default.
  2. Is there a brewkit skip: … directive the recipe could use today as a stop-gap (e.g. skip: linux-loader) before this is fixed in code?

Happy to PR if there's appetite — point me at the right file.

Activity

  1. jhheider commented on May 21, 2026

    @jhheider
    Contributor

    build.skip: fix-patchelf exists today. almost certainly the right fix, unless it's too blunt an instrument. you can always skip for a package, then manually rewrite any needed libs/bins.

  2. tannevaled commented on May 21, 2026

    @tannevaled
    ContributorAuthor

    Excellent — didn't know about build.skip: fix-patchelf. That's the per-package escape hatch I was missing.

    For pkgxdev/pantry#12968 I'll spin up a follow-up commit:

    • build.skip: fix-patchelf on the glibc recipe
    • Manually patchelf RPATH only where it's actually needed (gencat, getconf, ldd, etc. in bin/) — leave ld.so alone

    If ld.so becomes usable that way I can revert the static-link test back to a real dynamic-loader test (the static-link sidesteps the loader entirely, which is fine for "does libc.a work" but doesn't exercise the loader path).

    For the global picture — any package that ships its own loader (alternate libc, custom HPC stacks) hits this same wall, so a long-term fix-elf skip rule based on filename pattern still seems worth it. But the per-package build.skip is the right immediate answer, agreed.

    Will report back once the experiment lands on #12968.

  3. tannevaled commented on May 29, 2026

    @tannevaled
    ContributorAuthor

    Status update (post-glibc-recipe-landing):

    build.skip: fix-patchelf is the workaround in production now (see pkgxdev/pantry's glibc recipe). It works well per-package but the underlying fix-elf behavior is still wrong for any package shipping its own loader (alternate libcs, custom HPC stacks, …).

    Leaving this issue open as tracking — when there's appetite for a brewkit-side fix, the proposal sketched above (filename-pattern skip in fix-elf.ts: ^ld[-.].*\.so(\.\d+)*$) still applies. No urgency now that the per-package escape is documented and consumed in real code.

    Anyone hitting this in their own recipe: declare build.skip: fix-patchelf and patchelf the ELFs you actually want post-install, leaving your ld.so alone. See pkgxdev/pantry's gnu.org/glibc/package.yml for the pattern in production.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions