From 00afd113ad9caebeabab3e43149cc707669f5634 Mon Sep 17 00:00:00 2001 From: Moritz Angermann Date: Fri, 16 Oct 2026 22:50:56 +0900 Subject: [PATCH] build: fix native GHC ELF section alignment --- .github/workflows/nix.yml | 5 ++ flake.nix | 7 ++ scripts/nix/README.md | 14 ++++ scripts/nix/check-elf-alignment.nix | 22 ++++++ scripts/nix/elf-alignment/Probe.hs | 22 ++++++ scripts/nix/elf-alignment/probe.c | 3 + scripts/nix/elf-alignment/repack.py | 69 +++++++++++++++++++ .../ghc914-rts-elf-section-alignment.patch | 16 +++++ scripts/nix/native-packages.nix | 15 ++-- scripts/nix/native-rts-overlay.nix | 17 +++++ scripts/nix/native-rts-project.nix | 21 ++++++ 11 files changed, 206 insertions(+), 5 deletions(-) create mode 100644 scripts/nix/check-elf-alignment.nix create mode 100644 scripts/nix/elf-alignment/Probe.hs create mode 100644 scripts/nix/elf-alignment/probe.c create mode 100644 scripts/nix/elf-alignment/repack.py create mode 100644 scripts/nix/ghc914-rts-elf-section-alignment.patch create mode 100644 scripts/nix/native-rts-overlay.nix create mode 100644 scripts/nix/native-rts-project.nix diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index eb5f8cbd44..a8d7005cf3 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -88,6 +88,11 @@ jobs: - name: Show outputs from this checkout run: make nix-list + - name: Check native Linux GHC ELF section alignment + if: ${{ matrix.system == 'x86_64-linux' && matrix.target == 'native' }} + timeout-minutes: 120 + run: nix build .#checks.x86_64-linux.ghc-elf-alignment --no-link -L --max-jobs 1 --timeout "$NIX_BUILD_TIMEOUT" + # Use the compiler fork in flake.lock, including both Android ABIs. # Stock GHC installed by ghcup is not interchangeable with this compiler. - name: Build existing Haskell outputs diff --git a/flake.nix b/flake.nix index 479e3946b5..891ce55338 100644 --- a/flake.nix +++ b/flake.nix @@ -680,6 +680,13 @@ # build all packages in hydra. hydraJobs = packages; + checks = pkgs.lib.optionalAttrs (system == "x86_64-linux") { + ghc-elf-alignment = import ./scripts/nix/check-elf-alignment.nix { + inherit pkgs; + compiler = (compilerSelection pkgs).ghc914-sh; + }; + }; + devShell = let updateCmd = pkgs.writeShellApplication { name = "update-sha256map"; diff --git a/scripts/nix/README.md b/scripts/nix/README.md index 1ec08fe57b..95f58e1a31 100644 --- a/scripts/nix/README.md +++ b/scripts/nix/README.md @@ -9,6 +9,20 @@ platform; Android ARMv7 does not require a separate branch or tag. build context, including auxiliary iserv projects, so the pinned compiler can use its existing cache. Application dependencies keep the project's current packages. +On Linux x86_64, `native-rts-project.nix` patches the final compiler's RTS ELF +loader to honor section alignment even when its file offset is unaligned. +The stage2 RTS package patch preserves the configured source, stage1 compiler, +plan, and package IDs, but rebuilds the final compiler and its dependent slices. +Native, auxiliary, and cross compiler selection share this patched compiler. +Darwin and ARM compiler configurations are unchanged. This native compiler fix +does not patch target RTS sources or target iserv runtimes, including musl. + +The focused regression runs before the native x86_64 Linux CI build. To run it +from this checkout, use `nix build .#checks.x86_64-linux.ghc-elf-alignment -L`. +It loads the same 8192-byte C object with aligned and deliberately packed section +file offsets, requiring both to retain 64-byte memory alignment and their data. +It is a flake check, separate from the 21 existing Hydra artifact outputs. + Cross projects add `ghc-internal` to the Cabal dependencies of the pinned `composition-1.0.2.2` source. This preserves its deliberate avoidance of `base` while making GHC 9.14's wired-in types available in isolated target package diff --git a/scripts/nix/check-elf-alignment.nix b/scripts/nix/check-elf-alignment.nix new file mode 100644 index 0000000000..d344fb4afd --- /dev/null +++ b/scripts/nix/check-elf-alignment.nix @@ -0,0 +1,22 @@ +{ pkgs, compiler }: +assert pkgs.stdenv.hostPlatform.isLinux && pkgs.stdenv.hostPlatform.isx86_64; +assert pkgs.stdenv.buildPlatform.config == pkgs.stdenv.hostPlatform.config; +pkgs.runCommandCC "ghc914-elf-section-alignment-check" { + nativeBuildInputs = [ pkgs.python3 ]; +} '' + set -euo pipefail + mkdir -p "$out" + $CC -ffreestanding -std=c11 -Werror=vla -pedantic -Wall -Wextra -O2 \ + -c ${./elf-alignment/probe.c} -o "$out/original.o" + python3 ${./elf-alignment/repack.py} \ + "$out/original.o" "$out/packed.o" "$out/elf-layout.json" + # A valid ET_REL section's file offset need not match its memory alignment. + # Both paths must resolve the same bytes at a 64-byte-aligned address. + for variant in original packed; do + ${compiler}/bin/ghc -ignore-dot-ghci -package ghci ${./elf-alignment/Probe.hs} \ + -e "probeObject \"$out/$variant.o\"" | tee "$out/$variant.txt" + grep -Fx '(0,1,0)' "$out/$variant.txt" + done + printf '%s\n' 'PASS: original and packed ELF sections honor 64-byte alignment' \ + | tee "$out/result.txt" +'' diff --git a/scripts/nix/elf-alignment/Probe.hs b/scripts/nix/elf-alignment/Probe.hs new file mode 100644 index 0000000000..50f96cd694 --- /dev/null +++ b/scripts/nix/elf-alignment/Probe.hs @@ -0,0 +1,22 @@ +module Probe (probeObject) where + +import Control.Monad (unless) +import Data.Word (Word8) +import Foreign.Ptr (castPtr, ptrToWordPtr) +import Foreign.Storable (peekElemOff) +import GHCi.ObjLink (ShouldRetainCAFs (RetainCAFs), initObjLinker, loadObj, lookupSymbol, resolveObjs) + +probeObject :: FilePath -> IO () +probeObject object = do + initObjLinker RetainCAFs + loadObj object + resolved <- resolveObjs + unless resolved (fail "resolveObjs returned False") + address <- lookupSymbol "probe" + case address of + Nothing -> fail "probe symbol not found" + Just pointer -> do + first <- peekElemOff (castPtr pointer) 0 :: IO Word8 + lastByte <- peekElemOff (castPtr pointer) 8191 :: IO Word8 + unless (first == 1 && lastByte == 0) (fail "probe payload changed") + print (ptrToWordPtr pointer `mod` 64, first, lastByte) diff --git a/scripts/nix/elf-alignment/probe.c b/scripts/nix/elf-alignment/probe.c new file mode 100644 index 0000000000..4748e599ab --- /dev/null +++ b/scripts/nix/elf-alignment/probe.c @@ -0,0 +1,3 @@ +#include + +_Alignas(64) const uint8_t probe[8192] = {1}; diff --git a/scripts/nix/elf-alignment/repack.py b/scripts/nix/elf-alignment/repack.py new file mode 100644 index 0000000000..e1c0c8c212 --- /dev/null +++ b/scripts/nix/elf-alignment/repack.py @@ -0,0 +1,69 @@ +"""Repack the tiny ELF64 probe without changing section data or alignment.""" + +import hashlib +import json +import pathlib +import struct +import sys + +original_path, packed_path, report_path = map(pathlib.Path, sys.argv[1:]) +original = original_path.read_bytes() +assert original[:6] == b"\x7fELF\x02\x01" # ELF64, little endian +header_format = "<16sHHIQQQIHHHHHH" +section_format = "= cut and moved[4] != 0: + moved[4] += padding + struct.pack_into(section_format, packed, header[6] + i * entry_size, *moved) + if section[1] not in (0, 8): # SHT_NULL/SHT_NOBITS have no file payload. + assert original[section[4] : section[4] + section[5]] == packed[ + moved[4] : moved[4] + moved[5] + ], section_name(section) + observations.append( + { + "name": section_name(section), + "size": section[5], + "alignment": section[8], + "originalOffset": section[4], + "packedOffset": moved[4], + } + ) +packed_path.write_bytes(packed) +report = { + "paddingBytes": padding, + "allSectionPayloadsUnchanged": True, + "symbolsAndRelocationsUnchanged": True, + "sha256": { + path.name: hashlib.sha256(path.read_bytes()).hexdigest() + for path in (original_path, packed_path) + }, + "sections": observations, +} +report_path.write_text(json.dumps(report, indent=2) + "\n") +print(json.dumps(report, indent=2)) diff --git a/scripts/nix/ghc914-rts-elf-section-alignment.patch b/scripts/nix/ghc914-rts-elf-section-alignment.patch new file mode 100644 index 0000000000..935fc3b407 --- /dev/null +++ b/scripts/nix/ghc914-rts-elf-section-alignment.patch @@ -0,0 +1,16 @@ +--- a/linker/Elf.c ++++ b/linker/Elf.c +@@ -1008,8 +1008,11 @@ + } + // use the m32 allocator if either the image is not mapped + // (i.e. we cannot map the sections separately), or if the section +- // size is small. +- else if (!oc->imageMapped || size < getPageSize() / 3) { ++ // size is small. A section's file offset need not satisfy its ++ // in-memory alignment. mmap preserves the offset within the page, ++ // so copy such sections into aligned storage as well. ++ else if (!oc->imageMapped || size < getPageSize() / 3 ++ || (align > 1 && offset % align != 0)) { + bool executable = kind == SECTIONKIND_CODE_OR_RODATA; + m32_allocator *allocator = executable ? oc->rx_m32 : oc->rw_m32; + // Correctly align the section. This is particularly important for diff --git a/scripts/nix/native-packages.nix b/scripts/nix/native-packages.nix index a599e8e652..7fd1df1836 100644 --- a/scripts/nix/native-packages.nix +++ b/scripts/nix/native-packages.nix @@ -2,8 +2,13 @@ system: # Keep haskell.nix's existing host pins, including Intel Darwin's 26.05 pin. # Its flake omits ARM64 Linux, so construct that native set from the same inputs. -haskellNix.legacyPackages.${system} or (import haskellNix.inputs.nixpkgs { - localSystem = { inherit system; }; - config = haskellNix.config; - overlays = [ haskellNix.overlay ]; -}) +let + native = haskellNix.legacyPackages.${system} or (import haskellNix.inputs.nixpkgs { + localSystem = { inherit system; }; + config = haskellNix.config; + overlays = [ haskellNix.overlay ]; + }); +in +if system == "x86_64-linux" +then native.appendOverlays [ (import ./native-rts-overlay.nix) ] +else native diff --git a/scripts/nix/native-rts-overlay.nix b/scripts/nix/native-rts-overlay.nix new file mode 100644 index 0000000000..1e67cb67fe --- /dev/null +++ b/scripts/nix/native-rts-overlay.nix @@ -0,0 +1,17 @@ +final: prev: +let + inherit (prev.stdenv) buildPlatform hostPlatform targetPlatform; + active = hostPlatform.isLinux && hostPlatform.isx86_64 + && buildPlatform.config == hostPlatform.config + && hostPlatform.config == targetPlatform.config; +in prev.lib.optionalAttrs active { + haskell-nix = prev.haskell-nix // { + # Cover the compiler's own stage2 project as well as evalWith variants. + # Reusing the public project hook keeps native/auxiliary/cross users on + # one compiler rather than patching only an application's final link. + cabalProject' = projectModule: prev.haskell-nix.cabalProject' ( + [ ./native-rts-project.nix ] + ++ (if builtins.isList projectModule then projectModule else [ projectModule ]) + ); + }; +} diff --git a/scripts/nix/native-rts-project.nix b/scripts/nix/native-rts-project.nix new file mode 100644 index 0000000000..42883f5f17 --- /dev/null +++ b/scripts/nix/native-rts-project.nix @@ -0,0 +1,21 @@ +{ config, lib, pkgs, ... }: +let + inherit (pkgs.stdenv) buildPlatform hostPlatform targetPlatform; +in +lib.mkIf ( + hostPlatform.isLinux && hostPlatform.isx86_64 + && buildPlatform.config == hostPlatform.config + && hostPlatform.config == targetPlatform.config + && config.name == "ghc914-sh-stage2" + && config.compiler-nix-name == "ghc914-sh-stage1" +) { + modules = [ + ({ lib, ... }: { + # Local RTS package patches reach the v2 tarball without changing the + # compiler's configured source, stage1 bootstrap, plan, or package IDs. + # A valid ELF section may have an unaligned file offset; the loader + # must still honor its declared memory alignment for TH object code. + packages.rts.patches = lib.mkAfter [ ./ghc914-rts-elf-section-alignment.patch ]; + }) + ]; +}