From 4a894b31c328bae8dee8cfcc20f6a59f97b8ab3f Mon Sep 17 00:00:00 2001 From: Ben Zeng Date: Tue, 16 Dec 2025 20:33:06 +0200 Subject: [PATCH 1/2] auto-patchelf: add --preserve-origin arg If --preserve-origin is set and this ELF file's existing RUNPATH/RPATH contains a $ORIGIN entry that resolves to the same directory, that $ORIGIN entry is added instead of an absolute path. --- .../au/auto-patchelf/source/auto-patchelf.py | 57 +++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/pkgs/by-name/au/auto-patchelf/source/auto-patchelf.py b/pkgs/by-name/au/auto-patchelf/source/auto-patchelf.py index bf8882818dd9..360756375f48 100644 --- a/pkgs/by-name/au/auto-patchelf/source/auto-patchelf.py +++ b/pkgs/by-name/au/auto-patchelf/source/auto-patchelf.py @@ -204,6 +204,36 @@ def find_dependency(soname: str, soarch: str, soabi: str) -> Optional[Path]: return None +def find_first_matching_rpath_with_origin(binary: Path, lib_dir: Path, rpaths: list[str]) -> Optional[Path]: + """ + If lib_dir equals one of the existing $ORIGIN-based RPATH entries + (after resolving $ORIGIN relative to this binary), return that $ORIGIN + entry to preserve it; otherwise return None. + + Note: we return a Path constructed from the RPATH string (which may + contain $ORIGIN). This is intentional so that the caller can treat it + uniformly with other Path entries when building the final RPATH string + (via Path.as_posix). We do not expect this Path to exist on the + filesystem; it functions as a display value. + """ + try: + # resolve(strict=False) does not require the path to exist, but may still + # raise RuntimeError (cycles) or OSError in odd environments. Guard those. + lib_dir_realpath = lib_dir.resolve() + except (OSError, RuntimeError): + return None + for rpath in rpaths: + if "$ORIGIN" not in rpath: + continue + try: + candidate_rpath_realpath = (binary.parent / rpath.replace("$ORIGIN", ".")).resolve() + except (OSError, RuntimeError): + continue + if candidate_rpath_realpath == lib_dir_realpath: + return Path(rpath) + return None + + @dataclass class Dependency: file: Path # The file that contains the dependency @@ -211,7 +241,7 @@ class Dependency: found: bool = False # Whether it was found somewhere -def auto_patchelf_file(path: Path, runtime_deps: list[Path], append_rpaths: list[Path] = [], keep_libc: bool = False, extra_args: list[str] = []) -> list[Dependency]: +def auto_patchelf_file(path: Path, runtime_deps: list[Path], append_rpaths: list[Path] = [], keep_libc: bool = False, preserve_origin: bool = False, extra_args: list[str] = []) -> list[Dependency]: try: with open_elf(path) as elf: @@ -242,6 +272,7 @@ def auto_patchelf_file(path: Path, runtime_deps: list[Path], append_rpaths: list file_is_dynamic_executable = is_dynamic_executable(elf) file_dependencies = get_dependencies(elf) + get_dlopen_dependencies(elf) + existing_rpaths = get_rpath(elf) except ELFError: return [] @@ -279,7 +310,10 @@ def auto_patchelf_file(path: Path, runtime_deps: list[Path], append_rpaths: list # and resolved automatically by the dynamic linker, unless # keep_libc is enabled. # 3. If a candidate is found in our library dependencies, that - # dependency should be added to rpath. + # dependency should be added to rpath. If --preserve-origin + # is set and this file's existing RUNPATH/RPATH contains a $ORIGIN + # entry that resolves to the same directory, we add that $ORIGIN + # entry instead of an absolute path. # 4. If all of the above fail, libc dependencies should still be # considered found. This is in contrast to step 2, because # enabling keep_libc should allow libc to be found in step 3 @@ -299,7 +333,8 @@ def auto_patchelf_file(path: Path, runtime_deps: list[Path], append_rpaths: list was_found = True break elif found_dependency := find_dependency(candidate.name, file_arch, file_osabi): - rpath.append(found_dependency) + origin_rpath_entry = find_first_matching_rpath_with_origin(path, found_dependency, existing_rpaths) if preserve_origin else None + rpath.append(origin_rpath_entry or found_dependency) dependencies.append(Dependency(path, candidate, found=True)) print(f" {candidate} -> found: {found_dependency}") was_found = True @@ -315,6 +350,12 @@ def auto_patchelf_file(path: Path, runtime_deps: list[Path], append_rpaths: list rpath.extend(append_rpaths) + # Keep any existing $ORIGIN entries that didn't match a discovered dependency (i.e. dlopen-only lookups) + if preserve_origin: + for existing_rpath in existing_rpaths: + if "$ORIGIN" in existing_rpath: + rpath.append(Path(existing_rpath)) + # Dedup the rpath rpath_str = ":".join(dict.fromkeys(map(Path.as_posix, rpath))) @@ -335,6 +376,7 @@ def auto_patchelf( ignore_missing: list[str] = [], append_rpaths: list[Path] = [], keep_libc: bool = False, + preserve_origin: bool = False, add_existing: bool = True, extra_args: list[str] = []) -> None: @@ -351,7 +393,7 @@ def auto_patchelf( dependencies = [] for path in chain.from_iterable(glob(p, '*', recursive) for p in paths_to_patch): if not path.is_symlink() and path.is_file(): - dependencies += auto_patchelf_file(path, runtime_deps, append_rpaths, keep_libc, extra_args) + dependencies += auto_patchelf_file(path, runtime_deps, append_rpaths, keep_libc, preserve_origin, extra_args) missing = [dep for dep in dependencies if not dep.found] @@ -431,6 +473,12 @@ def main() -> None: action="store_true", help="Attempt to search for and relink libc dependencies.", ) + parser.add_argument( + "--preserve-origin", + dest="preserve_origin", + action="store_true", + help="When possible, replace absolute RPATH entries with original $ORIGIN entries that resolve to the same directory.", + ) parser.add_argument( "--ignore-existing", dest="add_existing", @@ -460,6 +508,7 @@ def main() -> None: args.ignore_missing, append_rpaths=args.append_rpaths, keep_libc=args.keep_libc, + preserve_origin=args.preserve_origin, add_existing=args.add_existing, extra_args=args.extra_args) From 4f0b794d1500e644ebd25d13335cfccc65ebcd47 Mon Sep 17 00:00:00 2001 From: Florian Klink Date: Wed, 17 Dec 2025 15:13:30 +0200 Subject: [PATCH 2/2] tests.auto-patchelf-hook-preserve-origin: init This adds a test for the autoPatchelfHook --preserve-origin feature. --- .../default.nix | 91 +++++++++++++++++++ .../lib-main.c | 14 +++ pkgs/test/default.nix | 2 + pkgs/test/stdenv-inputs/default.nix | 3 + 4 files changed, 110 insertions(+) create mode 100644 pkgs/test/auto-patchelf-hook-preserve-origin/default.nix create mode 100644 pkgs/test/auto-patchelf-hook-preserve-origin/lib-main.c diff --git a/pkgs/test/auto-patchelf-hook-preserve-origin/default.nix b/pkgs/test/auto-patchelf-hook-preserve-origin/default.nix new file mode 100644 index 000000000000..7685f41e91c9 --- /dev/null +++ b/pkgs/test/auto-patchelf-hook-preserve-origin/default.nix @@ -0,0 +1,91 @@ +{ + lib, + stdenv, + tests, + autoPatchelfHook, + patchelf, +}: + +let + foo = tests.stdenv-inputs.foo; + bar = tests.stdenv-inputs.bar; + + # similar to lib-check in stdenv-inputs, but we ship + # binary and libraries in the same output, and only + # $ORIGIN/../lib in RUNPATH. + lib-check = stdenv.mkDerivation { + name = "lib-check-bundle"; + + buildInputs = [ + foo + bar + ]; + + nativeBuildInputs = [ + patchelf + ]; + + buildCommand = '' + $CC -lfoo -lbar -Wl,-rpath,'$ORIGIN/../lib' -o lib-check ${./lib-main.c} + + # Shrink RUNPATH to only keep the $ORIGIN/../lib one, + # dropping references to foo and bar store path. + patchelf --shrink-rpath --allowed-rpath-prefixes '$ORIGIN/../lib' lib-check + + + mkdir -p $out/lib $out/bin + cp ${lib.getDev foo}/lib/* ${lib.getDev bar}/lib/* $out/lib/ + cp lib-check $out/bin/ + + # Make sure the binary still works + $out/bin/lib-check + ''; + + disallowedReferences = [ + (lib.getDev foo) + (lib.getDev bar) + ]; + }; + # We treat `lib-check` as binaries and libraries coming from somewhere, + # and run `autoPatchelfHook` on them, but setting `--preserve-origin`. + # If we wouldn't,`autoPatchelfHook` would replace `RUNPATH` with a + # (self-)reference. + lib-check-autopatchelfed = stdenv.mkDerivation { + name = "lib-check-autopatchelfed"; + + nativeBuildInputs = [ + autoPatchelfHook + ]; + + autoPatchelfFlags = [ "--preserve-origin" ]; + + dontUnpack = true; + + # we don't set buildCommand because we want to ensure fixupPhase + # (containing autoPatchelfHook) is run. + installPhase = '' + mkdir -p $out + cp -R ${lib-check}/* $out + ''; + + # Should not refer to our source nor to itself. + disallowedReferences = [ + lib-check + "out" + ]; + }; +in +stdenv.mkDerivation { + name = "auto-patchelf-hook-preserve-origin"; + + buildCommand = '' + # Ensure the binary still works + ${lib-check-autopatchelfed}/bin/lib-check + touch $out + ''; + + meta.platforms = lib.platforms.all; + passthru = { + inherit lib-check lib-check-autopatchelfed; + }; +} diff --git a/pkgs/test/auto-patchelf-hook-preserve-origin/lib-main.c b/pkgs/test/auto-patchelf-hook-preserve-origin/lib-main.c new file mode 100644 index 000000000000..c9488fe43e55 --- /dev/null +++ b/pkgs/test/auto-patchelf-hook-preserve-origin/lib-main.c @@ -0,0 +1,14 @@ +#include + +extern unsigned int foo(void); +extern unsigned int bar(void); + +int main(int argc, char **argv) +{ + if (foo() != 42) + return 1; + if (bar() != 42) + return 1; + fprintf(stderr, "ok\n"); + return 0; +} diff --git a/pkgs/test/default.nix b/pkgs/test/default.nix index 4c613135e977..da33cd9c07b0 100644 --- a/pkgs/test/default.nix +++ b/pkgs/test/default.nix @@ -225,6 +225,8 @@ in auto-patchelf-hook = callPackage ./auto-patchelf-hook { }; + auto-patchelf-hook-preserve-origin = callPackage ./auto-patchelf-hook-preserve-origin { }; + # Accumulate all passthru.tests from arrayUtilities into a single attribute set. arrayUtilities = recurseIntoAttrs ( concatMapAttrs ( diff --git a/pkgs/test/stdenv-inputs/default.nix b/pkgs/test/stdenv-inputs/default.nix index 9ffe2e6479ce..2615c905045d 100644 --- a/pkgs/test/stdenv-inputs/default.nix +++ b/pkgs/test/stdenv-inputs/default.nix @@ -70,4 +70,7 @@ stdenv.mkDerivation { ''; meta.platforms = lib.platforms.all; + passthru = { + inherit foo bar; + }; }