build: fix native GHC ELF section alignment

This commit is contained in:
Moritz Angermann committed 2026-10-16 22:51:54 +09:00
1 parent 2951153b49
commit 00afd113ad
11 files changed
+206 -5

No files matched your search

+5
View File
@@ -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
+7
View File
@@ -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";
+14
View File
@@ -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
+22
View File
@@ -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"
''
+22
View File
@@ -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)
+3
View File
@@ -0,0 +1,3 @@
#include <stdint.h>
_Alignas(64) const uint8_t probe[8192] = {1};
+69
View File
@@ -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 = "<IIQQQQIIQQ"
header = list(struct.unpack_from(header_format, original))
assert header[1:3] == [1, 62] # ET_REL, EM_X86_64
assert header[10] == 0 # No program headers in this fixture.
section_offset, entry_size, count, string_index = header[6], *header[11:14]
assert entry_size == struct.calcsize(section_format) == 64
sections = [
list(struct.unpack_from(section_format, original, section_offset + i * entry_size))
for i in range(count)
]
strings = sections[string_index]
names = original[strings[4] : strings[4] + strings[5]]
def section_name(section):
return names[section[0] :].split(b"\0", 1)[0].decode()
rodata = next(section for section in sections if section_name(section) == ".rodata")
assert rodata[5] == 8192 and rodata[8] == 64 and rodata[4] % 64 == 0
cut = rodata[4]
padding = 8
packed = bytearray(original[:cut] + b"\0" * padding + original[cut:])
header[6] += padding
struct.pack_into(header_format, packed, 0, *header)
observations = []
for i, section in enumerate(sections):
moved = section.copy()
if moved[4] >= 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))
@@ -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
+10 -5
View File
@@ -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
+17
View File
@@ -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 ])
);
};
}
+21
View File
@@ -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 ];
})
];
}