Skip to content

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

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 resolveld.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.

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