From 179824cfe015bebe5219ae31ec43435071b7ff54 Mon Sep 17 00:00:00 2001 From: Moritz Angermann Date: Tue, 2 Jun 2026 13:21:55 +0900 Subject: [PATCH 1/3] GHC/Build/Link: relativize absolute rpaths when --enable-relocatable depLibraryPaths returns absolute library paths whenever the dep's libdir is not under the package's own install prefix (i.e. almost always, in a cabal-store layout where each package gets its own hash-suffixed subdir). The previous getRPaths only prefixed `@loader_path` / `$ORIGIN` to already-relative paths, so those absolute store paths were baked directly into LC_RPATH (Mach-O) and DT_RUNPATH (ELF). On macOS up to and including Sonoma (macOS 14) dyld silently falls through to subsequent rpath entries when an absolute rpath does not exist. Sequoia (macOS 15) dyld treats the same condition as fatal and aborts the binary on launch. This affected the stable-haskell GHC bindist: a `/Volumes/WorkSpace/_work/ghc/ghc/ _build/stage2/store/host/aarch64-apple-darwin/lib` rpath baked in by the build runner caused `ghc --numeric-version` to SIGABRT when the bindist was installed on any macos-15 host. When the user has opted into relocatable mode (`--enable-relocatable`, or `relocatable: True` in a cabal project), this commit replaces such absolute rpaths with a `shortRelativePath`-computed relative form against the artifact's install directory. The relative form is well-formed (no `/Volumes` prefix to abort dyld on), and harmless when the bindist layout no longer matches the build store (dyld treats it as a normal missing rpath and falls through to subsequent entries). Non-relocatable builds (the default) are unchanged: absolute paths still pass through to preserve the existing semantics for non-relocated installs. The PackageDescription, InstallDirs, and shortRelativePath utility this needs are all already in scope or come from already-imported modules; only `bindir` and `libdir` field accessors are pulled in freshly from `Distribution.Simple.InstallDirs`. Companion to commit 010b365582c in stable-haskell/ghc, which strips the same leaked rpaths post-build with `install_name_tool` while this Cabal-side fix propagates through bindist rebuilds. --- .../src/Distribution/Simple/GHC/Build/Link.hs | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/Cabal/src/Distribution/Simple/GHC/Build/Link.hs b/Cabal/src/Distribution/Simple/GHC/Build/Link.hs index ac1067764f8..86ab3a6178a 100644 --- a/Cabal/src/Distribution/Simple/GHC/Build/Link.hs +++ b/Cabal/src/Distribution/Simple/GHC/Build/Link.hs @@ -55,6 +55,8 @@ import System.FilePath , replaceExtension ) +import Distribution.Simple.InstallDirs (bindir, libdir) + -- | Links together the object files of the Haskell modules and extra sources -- using the context in which the component is being built. -- @@ -624,7 +626,45 @@ getRPaths pbci = do let hostPref = case hostOS of OSX -> "@loader_path" _ -> "$ORIGIN" - relPath p = if isRelative p then hostPref p else p + -- The artifact's eventual install directory; absolute rpaths are + -- expressed as `@loader_path`/`$ORIGIN`-relative to this when the + -- user has opted into relocatable mode (`relocatable: True` or + -- `--enable-relocatable`). + -- + -- Mirrors the executable/library split in + -- 'Distribution.Simple.LocalBuildInfo.depLibraryPaths'. + installDirs = absoluteComponentInstallDirs + (localPkgDescr lbi) lbi (componentUnitId clbi) NoCopyDest + isExe = case clbi of + ExeComponentLocalBuildInfo{} -> True + _ -> False + relDir + | isExe = bindir installDirs + | otherwise = libdir installDirs + -- Convert an rpath entry to its loader-relative form. + -- + -- * Already-relative paths get the `@loader_path` / `$ORIGIN` + -- prefix as before. + -- * Absolute paths normally pass through unchanged. In + -- `relocatable` mode we replace them with a relative-form + -- expression (`@loader_path/../../...`) computed against the + -- binary's install dir, so the resulting binary does not bake + -- a build-host absolute path into LC_RPATH / DT_RUNPATH. + -- + -- This matters on macOS 15 (Sequoia), whose dyld treats an + -- unresolvable absolute rpath as fatal — older dyld silently + -- falls through to subsequent rpath entries. Pre-fix, a + -- bindist produced under e.g. + -- `/Volumes/WorkSpace/_work/ghc/ghc/_build/stage2/store/...` + -- would abort-trap on launch when relocated off the build + -- host. Post-fix, the same entry becomes + -- `@loader_path/../..//lib`, which dyld treats + -- as a normal missing-directory rpath when the bindist + -- layout no longer matches the store layout. + relPath p + | isRelative p = hostPref p + | relocatable lbi = hostPref shortRelativePath relDir p + | otherwise = p rpaths = toNubListR (map relPath libraryPaths) <> toNubListR (map getSymbolicPath $ extraLibDirs bi) From 19578903edd2545edaa79ec9ab17e0cb3ad560a5 Mon Sep 17 00:00:00 2001 From: Moritz Angermann Date: Tue, 2 Jun 2026 14:02:48 +0900 Subject: [PATCH 2/3] GHC/Build/Link: drop the relocatable gate, always relativize abs rpaths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Initial version of this fix gated the new relativization on `relocatable lbi` to keep non-relocatable builds byte-identical. The gate doesn't help in practice: * the stable-haskell GHC bindist build requires the new behavior (else the darwin host binaries abort-trap on macOS 15); * setting `relocatable: True` to flip the gate also triggers `checkRelocatable` (which refuses cabal-store layouts whose deps live in sibling prefixes) and changes how `library-dirs` are written into .conf files — neither change is desirable for the rpath fix, and the latter actively breaks the bindist post-stage2 .conf rewriting in our Makefile pipeline. The new always-on behavior is a strict improvement for every relocatable scenario (cabal-store relocation, bindist relocation, nix-style closure moves) and only a theoretical regression for the "copy a single executable to an unrelated host and expect the same absolute path to still resolve" case, which has never been part of cabal's documented contract. Companion to stable-haskell/ghc commit that drops the matching `relocatable: True` flag from configure.ac. --- .../src/Distribution/Simple/GHC/Build/Link.hs | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/Cabal/src/Distribution/Simple/GHC/Build/Link.hs b/Cabal/src/Distribution/Simple/GHC/Build/Link.hs index 86ab3a6178a..feca1864502 100644 --- a/Cabal/src/Distribution/Simple/GHC/Build/Link.hs +++ b/Cabal/src/Distribution/Simple/GHC/Build/Link.hs @@ -645,11 +645,10 @@ getRPaths pbci = do -- -- * Already-relative paths get the `@loader_path` / `$ORIGIN` -- prefix as before. - -- * Absolute paths normally pass through unchanged. In - -- `relocatable` mode we replace them with a relative-form - -- expression (`@loader_path/../../...`) computed against the - -- binary's install dir, so the resulting binary does not bake - -- a build-host absolute path into LC_RPATH / DT_RUNPATH. + -- * Absolute paths are rewritten to a relative-form expression + -- (`@loader_path/../../...`) computed against the binary's + -- install dir, so the resulting binary does not bake a + -- build-host absolute path into LC_RPATH / DT_RUNPATH. -- -- This matters on macOS 15 (Sequoia), whose dyld treats an -- unresolvable absolute rpath as fatal — older dyld silently @@ -661,10 +660,16 @@ getRPaths pbci = do -- `@loader_path/../..//lib`, which dyld treats -- as a normal missing-directory rpath when the bindist -- layout no longer matches the store layout. + -- + -- Not gated on `relocatable lbi`: that flag also triggers + -- `checkRelocatable` (which refuses cabal-store layouts + -- where deps live in sibling prefixes) and changes how + -- library-dirs are emitted in .conf files. Both behaviors + -- are independent of rpath generation and incompatible with + -- the stable-haskell GHC bindist assembly pipeline. relPath p - | isRelative p = hostPref p - | relocatable lbi = hostPref shortRelativePath relDir p - | otherwise = p + | isRelative p = hostPref p + | otherwise = hostPref shortRelativePath relDir p rpaths = toNubListR (map relPath libraryPaths) <> toNubListR (map getSymbolicPath $ extraLibDirs bi) From 10b2a8db799117f66ed5a07ace0f37a4a21139d1 Mon Sep 17 00:00:00 2001 From: Moritz Angermann Date: Sun, 14 Jun 2026 21:12:18 +0900 Subject: [PATCH 3/3] GHC/Build/Link: include $ORIGIN/@loader_path in rpaths (musl sibling deps) The rpath relativization (added for the darwin LC_RPATH-leak fix) never adds the artifact's own directory to the runpath. depLibraryPaths can return the parent directory for a dependency installed in the same directory as the artifact, so that entry relativizes to "$ORIGIN/.." and the dependency is left unfound. glibc papers over this because GHC sets LD_LIBRARY_PATH at runtime before dlopen and glibc re-reads it per dlopen; musl reads LD_LIBRARY_PATH only at process startup and ignores the runtime change, so the missing self-rpath is fatal there. Concretely, a Backpack signature implementation (libHSp) installed next to the instantiated unit (libHSindef) fails to load under musl (GHC testsuite backpack/cabal/T14304 on Alpine), though it passes on every glibc platform. Always prepend the loader-relative self directory ($ORIGIN, or @loader_path on macOS) to the rpaths so a same-directory sibling is found. This is a relative entry, so it does not reintroduce the absolute build-host path the relativization removed (no macOS regression). --- Cabal/src/Distribution/Simple/GHC/Build/Link.hs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Cabal/src/Distribution/Simple/GHC/Build/Link.hs b/Cabal/src/Distribution/Simple/GHC/Build/Link.hs index feca1864502..12f7d8914f2 100644 --- a/Cabal/src/Distribution/Simple/GHC/Build/Link.hs +++ b/Cabal/src/Distribution/Simple/GHC/Build/Link.hs @@ -671,7 +671,18 @@ getRPaths pbci = do | isRelative p = hostPref p | otherwise = hostPref shortRelativePath relDir p rpaths = - toNubListR (map relPath libraryPaths) + -- Always search the artifact's own directory ($ORIGIN / + -- @loader_path) first, so a sibling library installed alongside it + -- is found by the dynamic loader. depLibraryPaths can yield the + -- parent directory for a same-directory dependency (relativized to + -- "$ORIGIN/.."), which leaves the dependency unfound. glibc papers + -- over this via the runtime LD_LIBRARY_PATH GHC sets before + -- dlopen, but musl reads LD_LIBRARY_PATH only at process startup, + -- so the missing self-rpath is fatal there: a Backpack signature + -- implementation (libHSp) installed next to the instantiated unit + -- (libHSindef) fails to load (testsuite T14304 on Alpine/musl). + toNubListR [hostPref] + <> toNubListR (map relPath libraryPaths) <> toNubListR (map getSymbolicPath $ extraLibDirs bi) return rpaths else return mempty