From 03b83dd4df13de3eb4f59d24fd9ef6a95c0fd420 Mon Sep 17 00:00:00 2001 From: Moritz Angermann Date: Sat, 17 Oct 2026 09:55:24 +0900 Subject: [PATCH] Handle singleton COMDAT functions in i386 musl RTS --- .github/workflows/nix.yml | 7 + flake.nix | 4 + scripts/nix/README.md | 29 ++++ scripts/nix/check-elf-comdat-functions.nix | 95 ++++++++++++++ scripts/nix/check-musl-iserv-scope.nix | 1 + scripts/nix/check-musl-rts-scope.nix | 59 +++++++++ scripts/nix/elf-comdat/Probe.hs | 34 +++++ scripts/nix/elf-comdat/fixture.S | 36 +++++ .../nix/ghc914-rts-elf-comdat-functions.patch | 124 ++++++++++++++++++ scripts/nix/musl-iserv-overlay.nix | 2 +- scripts/nix/musl-rts-project.nix | 17 +++ 11 files changed, 407 insertions(+), 1 deletion(-) create mode 100644 scripts/nix/check-elf-comdat-functions.nix create mode 100644 scripts/nix/check-musl-rts-scope.nix create mode 100644 scripts/nix/elf-comdat/Probe.hs create mode 100644 scripts/nix/elf-comdat/fixture.S create mode 100644 scripts/nix/ghc914-rts-elf-comdat-functions.patch create mode 100644 scripts/nix/musl-rts-project.nix diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 0f329a258b..7e662b3e74 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -106,6 +106,13 @@ jobs: run: | nix eval --impure --json --file scripts/nix/check-musl-iserv-scope.nix \ --apply 'f: f { lib = (builtins.getFlake (toString ./.)).inputs.nixpkgs.lib; }' + nix eval --impure --json --file scripts/nix/check-musl-rts-scope.nix \ + --apply 'f: f { lib = (builtins.getFlake (toString ./.)).inputs.nixpkgs.lib; }' + + - name: Check scoped ELF COMDAT function coalescing + if: ${{ matrix.system == 'x86_64-linux' && matrix.target == 'linux-static' }} + timeout-minutes: 120 + run: nix build .#checks.x86_64-linux.ghc-elf-comdat-functions --no-link -L --max-jobs 1 --timeout "$NIX_BUILD_TIMEOUT" - name: Check Intel Setup failure selection if: ${{ matrix.system == 'x86_64-darwin' && matrix.target == 'all' }} diff --git a/flake.nix b/flake.nix index 1ecf713327..cf74bc38da 100644 --- a/flake.nix +++ b/flake.nix @@ -671,6 +671,10 @@ hydraJobs = packages; checks = pkgs.lib.optionalAttrs (system == "x86_64-linux") { + ghc-elf-comdat-functions = import ./scripts/nix/check-elf-comdat-functions.nix { + inherit pkgs; + compiler = (compilerSelection pkgs).ghc914-sh; + }; ghc-elf-alignment = import ./scripts/nix/check-elf-alignment.nix { inherit pkgs; compiler = (compilerSelection pkgs).ghc914-sh; diff --git a/scripts/nix/README.md b/scripts/nix/README.md index 8396861824..60a8a21357 100644 --- a/scripts/nix/README.md +++ b/scripts/nix/README.md @@ -60,6 +60,24 @@ The scope check uses the real Nix module fixed point in the Linux static CI group, including `pkgs` supplied through `_module.args`. Repeat it with `nix eval --impure --json --file scripts/nix/check-musl-iserv-scope.nix --apply 'f: f { lib = (builtins.getFlake (toString ./.)).inputs.nixpkgs.lib; }'`. +The i386 musl target RTS also handles duplicate hidden global functions when +both definitions are the whole, sole member of COMDAT groups with the same +signature. This covers GCC's `__x86.get_pc_thunk.*` PIC helpers. Coalescing is +checked against the current symbol owner during discovery and loading; it +does not persist weak-symbol status or suppress ordinary strong conflicts. +Multi-member groups remain outside this patch's scope. The source patch applies +to application and interpreter projects, including profiling. Native compilers +and other cross targets keep their existing RTS sources. + +Run the native x86_64 Linux regression with +`nix build .#checks.x86_64-linux.ghc-elf-comdat-functions --no-link -L --max-jobs 1 --timeout 7200`. +It links the same configured RTS sources with and without the patch, verifies +the replacement objects in both linker maps, checks relocations in both load +orders, and rejects strong conflicts and stale archive-owner decisions. Each +probe has a 15-second bound. Module scope is checked separately by +`scripts/nix/check-musl-rts-scope.nix` using the same invocation as the iserv +scope check above. + Windows patches Basement's `INVALID_HANDLE_VALUE` expression to `INT_PTR`, so hsc2hs receives the signed pointer-sized integer expected by the Haskell API. The generated constant converts through Haskell `Int` with `Prelude.fromIntegral`; @@ -359,6 +377,17 @@ records are byte-identical. The changed project skeleton rebuilds auxiliary boot derivations. An i686/QEMU C control reproduces the old static-loader error and loads the same `libz.so` with dynamic C linkage. +The newer focused static run `38974180411` at `43262a5c0` built and installed +both normal and profiling i686 interpreters after the private-symbol fix. Its +x86_64 static CLI also built. The i686 `wai-app-static` Template Haskell splice +then failed: the runtime linker rejected duplicate `__x86.get_pc_thunk.bx` and +`.ax` definitions before unresolved lookups, a QEMU target SIGSEGV, and proxy +EOF. The scoped COMDAT candidate cross-compiles both changed C files against +real GHC 9.14 headers with the original warning messages unchanged. Its physical +source producer changes exactly `Linker.c`, `linker/Elf.c`, and `linker/Elf.h`. +The runtime regression and final i686 TH/application builds remain unverified, +including profiling execution. + Intel Darwin reproduced its Setup GC abort and produced the direct-abort trace used for the info-table diagnosis above. The local renderer regression passes, while the patched compiler bootstrap and full Intel outputs await CI. Windows diff --git a/scripts/nix/check-elf-comdat-functions.nix b/scripts/nix/check-elf-comdat-functions.nix new file mode 100644 index 0000000000..ab22ebafae --- /dev/null +++ b/scripts/nix/check-elf-comdat-functions.nix @@ -0,0 +1,95 @@ +{ pkgs, compiler }: +assert pkgs.stdenv.hostPlatform.isLinux && pkgs.stdenv.hostPlatform.isx86_64; +let + source = pkgs.applyPatches { + name = "ghc914-rts-single-function-comdat"; + src = compiler.configured-src + "/rts"; + patches = [ ./ghc914-rts-elf-comdat-functions.patch ]; + }; +in pkgs.runCommandCC "ghc914-elf-comdat-functions-check" {} '' + set -euo pipefail + mkdir -p "$out" + includes=() + for package in rts rts-headers rts-fs libffi-clib; do + ${compiler}/bin/ghc-pkg field "$package" include-dirs --simple-output \ + > "$out/$package-include-dirs.txt" + for dir in $(cat "$out/$package-include-dirs.txt"); do + includes+=("-I$dir") + done + done + # Use one consistent set of private headers; installed copies also carry + # pragma-once headers which must not be included as distinct source files. + # The upstream RTS uses GNU C extensions. The fixture itself is assembly. + for variant in baseline patched; do + if [ "$variant" = baseline ]; then tree=${compiler.configured-src}/rts; else tree=${source}; fi + mkdir -p "$out/$variant-objects" + for file in linker/Elf.c Linker.c; do + object=$(basename "$file" .c) + $CC -std=gnu11 -Werror=vla -pedantic -Wall -Wextra -O2 -fPIC \ + -mno-avx -mno-avx2 -DCOMPILING_RTS -DINTERNAL_LIBFFI -DFS_NAMESPACE=rts \ + -iquote "$tree" -iquote "$tree/linker" \ + "''${includes[@]}" -I"$tree" \ + -c "$tree/$file" -o "$out/$variant-objects/$object.o" \ + 2>&1 | tee "$out/$variant-$object-compile.log" + done + done + cp ${./elf-comdat/Probe.hs} Probe.hs + for variant in baseline patched; do + ${compiler}/bin/ghc -ignore-dot-ghci -package ghci -j1 -O0 Probe.hs \ + "$out/$variant-objects/Elf.o" "$out/$variant-objects/Linker.o" \ + -o "$out/$variant" -optl-Wl,-Map="$out/$variant.map" + if grep -E '\.a\((Elf|Linker)\.o\)' "$out/$variant.map"; then + echo 'Original RTS linker object was unexpectedly extracted' >&2 + exit 1 + fi + done + for variant in a b; do + if [ "$variant" = a ]; then value=17; addend=100; else value=29; addend=200; fi + common=(-DPROBE_VALUE=$value -DPROBE_READER=reader_$variant -DPROBE_ADDEND=$addend) + for kind in comdat strong plain-group different-signature weak; do + flags=(-DGROUP_SIGNATURE=comdat_function) + case "$kind" in + strong) flags+=(-DNO_GROUP) ;; + plain-group) flags+=(-DPLAIN_GROUP) ;; + different-signature) flags=(-DGROUP_SIGNATURE=other_signature -DOTHER_SIGNATURE) ;; + weak) flags+=(-DNO_GROUP -DWEAK_FUNCTION) ;; + esac + $CC -c "''${common[@]}" "''${flags[@]}" ${./elf-comdat/fixture.S} \ + -o "$out/$kind-$variant.o" + done + $AR rc "$out/comdat-$variant.a" "$out/comdat-$variant.o" + done + $CC -c -DNO_GROUP -DGROUP_SIGNATURE=comdat_function \ + -DPROBE_VALUE=41 -DPROBE_READER=reader_strong -DPROBE_ADDEND=300 \ + ${./elf-comdat/fixture.S} -o "$out/strong-third.o" + expect_failure() { + name=$1; shift + status=0 + timeout 15s "$@" > "$out/$name.log" 2>&1 || status=$? + if [ "$status" -ne 1 ]; then + echo "Expected ordinary failure for $name, got status $status" >&2 + exit 1 + fi + grep -F 'duplicate definition for symbol' "$out/$name.log" + grep -F 'comdat_function' "$out/$name.log" + } + expect_failure baseline-comdat "$out/baseline" "$out/comdat-a.o" "$out/comdat-b.o" + timeout 15s "$out/patched" "$out/comdat-a.o" "$out/comdat-b.o" | tee "$out/comdat.txt" + grep -Fx '[17,117,217]' "$out/comdat.txt" + timeout 15s "$out/patched" "$out/comdat-b.o" "$out/comdat-a.o" | tee "$out/comdat-reversed.txt" + grep -Fx '[29,129,229]' "$out/comdat-reversed.txt" + for executable in baseline patched; do + timeout 15s "$out/$executable" "$out/weak-a.o" "$out/weak-b.o" | tee "$out/$executable-weak.txt" + grep -Fx '[17,117,217]' "$out/$executable-weak.txt" + done + for kind in strong plain-group; do + expect_failure "$kind" "$out/patched" "$out/$kind-a.o" "$out/$kind-b.o" + done + expect_failure different-signature "$out/patched" "$out/different-signature-a.o" "$out/comdat-b.o" + expect_failure mixed-comdat-first "$out/patched" "$out/comdat-a.o" "$out/strong-b.o" + expect_failure mixed-strong-first "$out/patched" "$out/strong-a.o" "$out/comdat-b.o" + expect_failure archive-owner-change "$out/patched" --archive-conflict \ + "$out/comdat-a.a" "$out/comdat-b.a" "$out/strong-third.o" + printf '%s\n' 'PASS: scoped COMDAT coalescing preserves relocations and strong conflicts' \ + | tee "$out/result.txt" +'' diff --git a/scripts/nix/check-musl-iserv-scope.nix b/scripts/nix/check-musl-iserv-scope.nix index 35973e121e..1c05d7550f 100644 --- a/scripts/nix/check-musl-iserv-scope.nix +++ b/scripts/nix/check-musl-iserv-scope.nix @@ -25,6 +25,7 @@ let profiling = lib.mkOption { type = lib.types.bool; }; cabalProjectLocal = lib.mkOption { type = lib.types.lines; default = ""; }; src = lib.mkOption { type = lib.types.anything; default = null; }; + modules = lib.mkOption { type = lib.types.listOf lib.types.anything; default = []; }; }; config = { inherit name builderVersion profiling; diff --git a/scripts/nix/check-musl-rts-scope.nix b/scripts/nix/check-musl-rts-scope.nix new file mode 100644 index 0000000000..0dac70c5f4 --- /dev/null +++ b/scripts/nix/check-musl-rts-scope.nix @@ -0,0 +1,59 @@ +# Use the pinned lib and the real project module; no compiler or project IFD. +{ lib, projectModule ? import ./musl-rts-project.nix }: +let + apply = { musl ? true, i386 ? true, linux ? true, + compiler ? "ghc914-sh", builderVersion ? 2, + name ? "simplex-chat", profiling ? false }: + let + project = lib.evalModules { + modules = [ projectModule { + options = { + name = lib.mkOption { type = lib.types.str; }; + profiling = lib.mkOption { type = lib.types.bool; }; + compiler-nix-name = lib.mkOption { type = lib.types.str; }; + builderVersion = lib.mkOption { type = lib.types.int; }; + modules = lib.mkOption { type = lib.types.listOf lib.types.anything; default = []; }; + }; + config = { + inherit name profiling builderVersion; + compiler-nix-name = compiler; + # Match cabalProject's module fixed point, not specialArgs. + _module.args.pkgs.stdenv = { + hostPlatform = { isMusl = musl; isx86_32 = i386; }; + buildPlatform.isLinux = linux; + }; + }; + } ]; + }; + package = lib.evalModules { + modules = [ { + options.packages = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule { + options.patches = lib.mkOption { type = lib.types.listOf lib.types.path; default = []; }; + }); + default = {}; + }; + # Existing patches must remain ordered before this one. + config.packages.rts.patches = [ ./musl-iserv-public-libc.patch ]; + } ] ++ project.config.modules; + }; + in package.config.packages.rts.patches; + unchanged = [ ./musl-iserv-public-libc.patch ]; + selected = unchanged ++ [ ./ghc914-rts-elf-comdat-functions.patch ]; +in +assert apply {} == selected; +assert apply { name = "iserv-proxy"; } == selected; +assert apply { profiling = true; } == selected; +assert apply { musl = false; } == unchanged; +assert apply { i386 = false; } == unchanged; +assert apply { linux = false; } == unchanged; +assert apply { compiler = "ghc963"; } == unchanged; +assert apply { compiler = "ghc914-sh-stage1"; } == unchanged; +assert apply { builderVersion = 1; } == unchanged; +{ + moduleArgumentFixedPointEvaluated = true; + i386MuslApplicationAndInterpreterCovered = true; + profilingCovered = true; + existingPatchOrderPreserved = true; + otherPlatformsCompilersAndBuildersUnchanged = true; +} diff --git a/scripts/nix/elf-comdat/Probe.hs b/scripts/nix/elf-comdat/Probe.hs new file mode 100644 index 0000000000..b90e31c095 --- /dev/null +++ b/scripts/nix/elf-comdat/Probe.hs @@ -0,0 +1,34 @@ +{-# LANGUAGE ForeignFunctionInterface #-} +module Main (main) where + +import Control.Monad (unless) +import Foreign.C.Types (CInt (..)) +import Foreign.Ptr (FunPtr, castPtrToFunPtr) +import GHCi.ObjLink (ShouldRetainCAFs (RetainCAFs), initObjLinker, loadArchive, loadObj, lookupSymbol, resolveObjs) +import System.Environment (getArgs) + +foreign import ccall "dynamic" callInt :: FunPtr (IO CInt) -> IO CInt + +main :: IO () +main = do + arguments <- getArgs + initObjLinker RetainCAFs + case arguments of + [first, second] -> do + mapM_ loadObj [first, second] + resolve + mapM invoke ["comdat_function", "reader_a", "reader_b"] >>= print + ["--archive-conflict", first, second, strong] -> do + -- Discovery initially selects the first archive's COMDAT definition. + -- Loading an ordinary strong definition can replace that unused owner. + -- The second archive must not retain stale permission to coalesce. + mapM_ loadArchive [first, second] + loadObj strong + resolve + invoke "reader_b" >>= print + _ -> fail "expected two objects, or --archive-conflict and three paths" + where + resolve = resolveObjs >>= \ok -> unless ok (fail "resolveObjs returned False") + invoke name = lookupSymbol name >>= maybe + (fail (name <> " not found")) + (callInt . castPtrToFunPtr) diff --git a/scripts/nix/elf-comdat/fixture.S b/scripts/nix/elf-comdat/fixture.S new file mode 100644 index 0000000000..5a9982a76e --- /dev/null +++ b/scripts/nix/elf-comdat/fixture.S @@ -0,0 +1,36 @@ +#if defined(NO_GROUP) +.text +#elif defined(PLAIN_GROUP) +.section .text.comdat_function,"axG",@progbits,GROUP_SIGNATURE +#else +.section .text.comdat_function,"axG",@progbits,GROUP_SIGNATURE,comdat +#endif +#if defined(WEAK_FUNCTION) +.weak comdat_function +#else +.globl comdat_function +#endif +.hidden comdat_function +.type comdat_function,@function +comdat_function: + mov $PROBE_VALUE, %eax + ret +.size comdat_function, .-comdat_function + +#if defined(OTHER_SIGNATURE) +.globl other_signature +.set other_signature, comdat_function +#endif + +.text +.globl PROBE_READER +.type PROBE_READER,@function +PROBE_READER: + sub $8, %rsp + call comdat_function + add $8, %rsp + add $PROBE_ADDEND, %eax + ret +.size PROBE_READER, .-PROBE_READER + +.section .note.GNU-stack,"",@progbits diff --git a/scripts/nix/ghc914-rts-elf-comdat-functions.patch b/scripts/nix/ghc914-rts-elf-comdat-functions.patch new file mode 100644 index 0000000000..4f47db9d01 --- /dev/null +++ b/scripts/nix/ghc914-rts-elf-comdat-functions.patch @@ -0,0 +1,124 @@ +--- a/linker/Elf.c ++++ b/linker/Elf.c +@@ -796,6 +796,76 @@ + } + #endif +- ++ ++/* Recognize only a COMDAT group containing one whole hidden function. ++ * GCC's i386 PIC thunks use this form with a GLOBAL, not WEAK, signature. ++ * This is deliberately not general multi-section COMDAT selection: retaining ++ * duplicate initializer, TLS, data, or partial-function sections is unsafe. ++ */ ++static bool ++isSingleFunctionComdat_ELF(ObjectCode *oc, const SymbolName *name) ++{ ++ if (oc == NULL || oc->type != STATIC_OBJECT || oc->info == NULL ++ || oc->info->elfHeader == NULL || oc->image == NULL ++ || oc->fileSize <= 0) { ++ return false; ++ } ++ ++ const Elf_Shdr *shdr = oc->info->sectionHeader; ++ const Elf_Word shnum = elf_shnum(oc->info->elfHeader); ++ const size_t imageSize = (size_t)oc->fileSize; ++ for (Elf_Word i = 0; i < shnum; i++) { ++ const Elf_Shdr *group = &shdr[i]; ++ if (group->sh_type != SHT_GROUP ++ || group->sh_size != 2 * sizeof(Elf_Word) ++ || group->sh_offset > imageSize ++ || group->sh_size > imageSize - group->sh_offset) { ++ continue; ++ } ++ Elf_Word entries[2]; ++ memcpy(entries, oc->image + group->sh_offset, sizeof(entries)); ++ if (entries[0] != GRP_COMDAT || entries[1] == SHN_UNDEF ++ || entries[1] >= shnum) { ++ continue; ++ } ++ const Elf_Shdr *member = &shdr[entries[1]]; ++ const Elf_Word required = SHF_ALLOC | SHF_EXECINSTR | SHF_GROUP; ++ if (member->sh_type != SHT_PROGBITS ++ || (member->sh_flags & required) != required ++ || (member->sh_flags & (SHF_WRITE | SHF_TLS)) != 0) { ++ continue; ++ } ++ for (ElfSymbolTable *table = oc->info->symbolTables; ++ table != NULL; table = table->next) { ++ if (table->index != group->sh_link ++ || group->sh_info >= table->n_symbols) { ++ continue; ++ } ++ const ElfSymbol *signature = &table->symbols[group->sh_info]; ++ const Elf_Sym *symbol = signature->elf_sym; ++ if (signature->name != NULL && strcmp(signature->name, name) == 0 ++ && ELF_ST_BIND(symbol->st_info) == STB_GLOBAL ++ && ELF_ST_TYPE(symbol->st_info) == STT_FUNC ++ && ELF_ST_VISIBILITY(symbol->st_other) == STV_HIDDEN ++ && symbol->st_shndx < SHN_LORESERVE ++ && symbol->st_shndx == entries[1] ++ && symbol->st_value == 0 && symbol->st_size != 0 ++ && symbol->st_size == member->sh_size) { ++ return true; ++ } ++ } ++ } ++ return false; ++} ++ ++bool ++ocIsComdatDuplicate_ELF(ObjectCode *oc, const SymbolName *name) ++{ ++ RtsSymbolInfo *existing = lookupStrHashTable(symhash, name); ++ return existing != NULL && existing->owner != oc ++ && isSingleFunctionComdat_ELF(oc, name) ++ && isSingleFunctionComdat_ELF(existing->owner, name); ++} ++ + int + ocGetNames_ELF ( ObjectCode* oc ) + { +@@ -1234,7 +1304,9 @@ + setWeakSymbol(oc, nm); + } + if (!ghciInsertSymbolTable(oc->fileName, symhash, +- nm, symbol->addr, isWeak, sym_type, symbol->elf_sym->st_size, oc) ++ nm, symbol->addr, ++ isWeak || ocIsComdatDuplicate_ELF(oc, nm), ++ sym_type, symbol->elf_sym->st_size, oc) + ) { + goto fail; + } +--- a/linker/Elf.h ++++ b/linker/Elf.h +@@ -10,6 +10,7 @@ + void ocDeinit_ELF ( ObjectCode* oc ); + int ocVerifyImage_ELF ( ObjectCode* oc ); + int ocGetNames_ELF ( ObjectCode* oc ); ++bool ocIsComdatDuplicate_ELF(ObjectCode *oc, const SymbolName *name); + int ocResolve_ELF ( ObjectCode* oc ); + int ocRunInit_ELF ( ObjectCode* oc ); + int ocRunFini_ELF ( ObjectCode* oc ); +--- a/Linker.c ++++ b/Linker.c +@@ -1645,10 +1645,19 @@ + Symbol_t symbol; + for (x = 0; x < oc->n_symbols; x++) { + symbol = oc->symbols[x]; ++ bool isWeak = isSymbolWeak(oc, symbol.name); ++#if defined(OBJFORMAT_ELF) ++ /* Archive discovery can change the selected owner. Recheck COMDAT ++ * eligibility now; never persist conditional acceptance as weakness. ++ */ ++ if (symbol.name != NULL && !isWeak) { ++ isWeak = ocIsComdatDuplicate_ELF(oc, symbol.name); ++ } ++#endif + if ( symbol.name + && !ghciInsertSymbolTable(oc->fileName, symhash, symbol.name, + symbol.addr, +- isSymbolWeak(oc, symbol.name), ++ isWeak, + symbol.type, 0, oc)) { + return 0; + } diff --git a/scripts/nix/musl-iserv-overlay.nix b/scripts/nix/musl-iserv-overlay.nix index 8ab81129f4..62359d5bd2 100644 --- a/scripts/nix/musl-iserv-overlay.nix +++ b/scripts/nix/musl-iserv-overlay.nix @@ -4,7 +4,7 @@ prev.lib.optionalAttrs ( ) { haskell-nix = prev.haskell-nix // { cabalProject' = projectModule: prev.haskell-nix.cabalProject' ( - [ ({ config, lib, pkgs, ... }: + [ ./musl-rts-project.nix ({ config, lib, pkgs, ... }: lib.mkIf ( pkgs.stdenv.hostPlatform.isMusl && pkgs.stdenv.buildPlatform.isLinux diff --git a/scripts/nix/musl-rts-project.nix b/scripts/nix/musl-rts-project.nix new file mode 100644 index 0000000000..f81bd5d373 --- /dev/null +++ b/scripts/nix/musl-rts-project.nix @@ -0,0 +1,17 @@ +{ config, lib, pkgs, ... }: +lib.mkIf ( + pkgs.stdenv.hostPlatform.isMusl + && pkgs.stdenv.hostPlatform.isx86_32 + && pkgs.stdenv.buildPlatform.isLinux + && config.compiler-nix-name == "ghc914-sh" + && config.builderVersion == 2 +) { + # Cross projects build their target RTS alongside the boot libraries. + # Cover both application and auxiliary interpreter projects, including + # profiling, while keeping the native compiler and its bootstrap intact. + modules = [ + ({ lib, ... }: { + packages.rts.patches = lib.mkAfter [ ./ghc914-rts-elf-comdat-functions.patch ]; + }) + ]; +}