fix(nix): keep Intel Darwin info tables with their entry code

Mark x86 Mach-O info entries as alternate entries in both GHC stages, so LLD preserves tables-next-to-code. Add a production compiler object/link regression to the existing Intel CI job.
This commit is contained in:
Moritz Angermann committed 2026-10-17 03:32:41 +09:00
1 parent 0e5ab60e9a
commit ad69b2b1a6
10 files changed
+261 -5

No files matched your search

+5
View File
@@ -110,6 +110,11 @@ jobs:
shell: bash
run: make nix-build 2>&1 | tee nix-build.log
- name: Check native Intel Darwin GHC info-table placement
if: ${{ matrix.system == 'x86_64-darwin' && matrix.target == 'all' }}
timeout-minutes: 10
run: nix build .#checks.x86_64-darwin.ghc-macho-info-tables --no-link -L --max-jobs 1 --timeout 300
# Diagnose the same native library after failure. Keep the canonical
# failure and its flags intact; this is a separate diagnostic derivation.
- name: Trace native Linux GHC and capture faults after failure
+5
View File
@@ -660,6 +660,11 @@
inherit pkgs;
compiler = (compilerSelection pkgs).ghc914-sh;
};
} // pkgs.lib.optionalAttrs (system == "x86_64-darwin") {
ghc-macho-info-tables = import ./scripts/nix/check-macho-info-tables.nix {
inherit pkgs;
compiler = (compilerSelection pkgs).ghc914-sh;
};
};
devShell = let
+21 -5
View File
@@ -14,8 +14,8 @@ 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 ELF 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`.
@@ -23,6 +23,20 @@ It loads the same 8192-byte C object with aligned and deliberately packed sectio
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.
On Intel Darwin, `darwin-info-table-project.nix` patches the GHC x86 backend in
both compiler stages. It marks info-table entry symbols as Mach-O alternate
entries, keeping each table and its code in the same linker subsection. The
observed Setup GC abort reads closure type zero because LLD inserts eight bytes
between a valid `FUN_STATIC` table and its entry. Relinking the actual cached
`directory` object reproduces that layout error; changing only alternate-entry
symbol flags preserves the table's original 24-byte distance and closure type 14.
The patched compiler rebuild and full Intel outputs still require CI validation.
Run `nix build .#checks.x86_64-darwin.ghc-macho-info-tables -L` to compile a Haskell
fixture with the selected compiler and check its table placement after a real
LLD link. CI runs this check after the Intel artifact builds. It checks object
metadata, table distances, and closure types without executing the fixture.
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
@@ -209,9 +223,11 @@ dereferencing them, then forwards to real `abort`. Its constructor
restores the original loader environment before Setup starts compiler children
and disables core output for the traced process tree. This adds a loaded image
to Setup; it changes no debugger permissions or security settings. Nonzero
exits and terminating signals remain failures. The Intel GHC abort-tracer runtime
is unverified until CI executes it; native ARM64 and Rosetta/x86 C controls
cover the helper.
exits and terminating signals remain failures. The hosted Intel replay captured
14 frames through `evacuate1` and `scavenge_loop1`. A local Setup built with the
same cached compiler and target dependency IDs reproduces the panic in 1.55
seconds, and its traced replay matches nine cloud frame offsets. Native ARM64
and Rosetta/x86 C controls also cover the helper.
A successful probe is diagnostic evidence.
Hydra continues to consume
+20
View File
@@ -0,0 +1,20 @@
{ pkgs, compiler }:
assert pkgs.stdenv.hostPlatform.isDarwin && pkgs.stdenv.hostPlatform.isx86_64;
assert pkgs.stdenv.buildPlatform.config == pkgs.stdenv.hostPlatform.config;
pkgs.runCommandCC "ghc914-macho-info-table-check" {
nativeBuildInputs = [ pkgs.python3 ];
} ''
set -euo pipefail
mkdir -p "$out"
${compiler}/bin/ghc -O -c ${./macho-info-tables/InfoTables.hs} \
-odir "$out" -hidir "$out" -o "$out/InfoTables.o"
# Undefined Haskell runtime references are permitted in this layout fixture.
# This checks the real compiler's object and real LLD's table placement;
# application/runtime qualification remains in the existing build goals.
${pkgs.llvmPackages_21.lld}/bin/ld64.lld -dylib -arch x86_64 \
-platform_version macos ${pkgs.stdenv.hostPlatform.darwinMinVersion} ${pkgs.stdenv.hostPlatform.darwinMinVersion} \
-undefined dynamic_lookup -dead_strip -no_fixup_chains \
"$out/InfoTables.o" -o "$out/InfoTables.dylib"
python3 ${./macho-info-tables/check.py} \
"$out/InfoTables.o" "$out/InfoTables.dylib" | tee "$out/result.json"
''
+14
View File
@@ -0,0 +1,14 @@
final: prev:
let
inherit (prev.stdenv) buildPlatform hostPlatform targetPlatform;
active = hostPlatform.isDarwin && hostPlatform.isx86_64
&& buildPlatform.config == hostPlatform.config
&& hostPlatform.config == targetPlatform.config;
in prev.lib.optionalAttrs active {
haskell-nix = prev.haskell-nix // {
cabalProject' = projectModule: prev.haskell-nix.cabalProject' (
[ ./darwin-info-table-project.nix ]
++ (if builtins.isList projectModule then projectModule else [ projectModule ])
);
};
}
+18
View File
@@ -0,0 +1,18 @@
{ config, lib, pkgs, ... }:
let
inherit (pkgs.stdenv) buildPlatform hostPlatform targetPlatform;
in
lib.mkIf (
hostPlatform.isDarwin && hostPlatform.isx86_64
&& buildPlatform.config == hostPlatform.config
&& hostPlatform.config == targetPlatform.config
&& lib.elem config.name [ "ghc914-sh-stage1" "ghc914-sh-stage2" ]
) {
modules = [
({ lib, ... }: {
# Stage1 must emit correct info tables for the stage2 boot libraries;
# stage2 must retain that fix for application and interpreter objects.
packages.ghc.patches = lib.mkAfter [ ./ghc914-macho-info-tables.patch ];
})
];
}
@@ -0,0 +1,17 @@
diff --git a/GHC/CmmToAsm/X86/Ppr.hs b/GHC/CmmToAsm/X86/Ppr.hs
--- a/GHC/CmmToAsm/X86/Ppr.hs
+++ b/GHC/CmmToAsm/X86/Ppr.hs
@@ -180,6 +180,13 @@ pprBasicBlock config info_env (BasicBlock blockid instrs)
pprAlignForSection platform Text $$
infoTableLoc $$
vcat (map (pprData config) info) $$
+ -- The entry point is inside the info table's Mach-O subsection.
+ -- Giving it a separate subsection lets the linker insert padding
+ -- between the table and the code, breaking tables-next-to-code.
+ -- Keep the ordinary dead-strip-preventer label as the atom start.
+ ppWhen (platformHasSubsectionsViaSymbols platform)
+ (line (text "\t.alt_entry " <> pprAsmLabel platform infoLbl)) $$
+
pprLabel platform infoLbl $$
c $$
ppWhen (ncgDwarfEnabled config) (line (pprBlockEndLabel platform infoLbl))
@@ -0,0 +1,50 @@
{-# OPTIONS_GHC -fno-worker-wrapper #-}
module InfoTables where
{-# NOINLINE f01 #-}
f01 :: Int -> Int
f01 x = x + 1
{-# NOINLINE f02 #-}
f02 :: Int -> Int -> Int
f02 x y = x + y
{-# NOINLINE f03 #-}
f03 :: Int -> Int -> Int -> Int
f03 x y z = x + y * z
{-# NOINLINE f04 #-}
f04 :: Int -> Int
f04 x = if x > 4 then f01 x else f02 x 4
{-# NOINLINE f05 #-}
f05 :: Int -> Int -> Int
f05 x y = f03 x y 5
{-# NOINLINE f06 #-}
f06 :: Int -> Int -> Int -> Int
f06 x y z = f04 x + f05 y z
{-# NOINLINE f07 #-}
f07 :: Int -> Int
f07 x = f06 x 7 x
{-# NOINLINE f08 #-}
f08 :: Int -> Int -> Int
f08 x y = f07 x + f04 y
{-# NOINLINE f09 #-}
f09 :: Int -> Int -> Int -> Int
f09 x y z = f08 x y + z
{-# NOINLINE f10 #-}
f10 :: Int -> Int
f10 x = f09 x 10 x
{-# NOINLINE f11 #-}
f11 :: Int -> Int -> Int
f11 x y = f10 x + f10 y
{-# NOINLINE f12 #-}
f12 :: Int -> Int -> Int -> Int
f12 x y z = f11 x y + f11 y z
+109
View File
@@ -0,0 +1,109 @@
"""Check tables-next-to-code across a real Mach-O link without executing it."""
import argparse
import json
import pathlib
import struct
def macho(path):
data = path.read_bytes()
def unpack(fmt, offset):
size = struct.calcsize(fmt)
if offset < 0 or offset + size > len(data):
raise ValueError(f"{path}: truncated Mach-O at {offset}")
return struct.unpack_from(fmt, data, offset)
magic, cpu, _, _, commands, command_bytes, _, _ = unpack("<8I", 0)
if magic != 0xFEEDFACF or cpu != 0x01000007:
raise ValueError(f"{path}: expected little-endian x86_64 Mach-O")
sections = []
symtab = None
offset = 32
command_end = offset + command_bytes
for _ in range(commands):
cmd, size = unpack("<II", offset)
if size < 8 or offset + size > command_end:
raise ValueError(f"{path}: invalid load command")
if cmd == 0x19: # LC_SEGMENT_64
count, = unpack("<I", offset + 64)
if 72 + count * 80 > size:
raise ValueError(f"{path}: truncated section list")
for i in range(count):
fields = unpack("<16s16sQQIIIIIIII", offset + 72 + i * 80)
sections.append((fields[2], fields[3], fields[4]))
elif cmd == 2: # LC_SYMTAB
symtab = unpack("<IIII", offset + 8)
offset += size
if offset != command_end or symtab is None:
raise ValueError(f"{path}: missing symbol table or malformed commands")
symbol_offset, symbol_count, string_offset, string_size = symtab
if string_offset + string_size > len(data):
raise ValueError(f"{path}: truncated string table")
symbols = {}
for i in range(symbol_count):
index, kind, section, desc, address = unpack("<IBBHQ", symbol_offset + 16 * i)
if kind & 0xE0 or kind & 0x0E != 0x0E: # Skip STAB and non-N_SECT.
continue
if index >= string_size or not 1 <= section <= len(sections):
raise ValueError(f"{path}: invalid symbol")
end = data.find(b"\0", string_offset + index, string_offset + string_size)
if end < 0:
raise ValueError(f"{path}: unterminated symbol")
name = data[string_offset + index:end].decode("utf-8")
# Other local names, such as func.eh, may legitimately repeat. Only
# table/entry names need unique resolution for this layout check.
if not (name.startswith("_dsp_") or name.endswith("_info")):
continue
if name in symbols:
raise ValueError(f"{path}: ambiguous symbol {name}")
symbols[name] = (address, section, desc)
def info_type(name):
address, section, _ = symbols[name]
start, length, file_offset = sections[section - 1]
if not start + 8 <= address < start + length:
raise ValueError(f"{path}: info table outside section: {name}")
return unpack("<I", file_offset + address - start - 8)[0]
return symbols, info_type
def check(original, linked):
before, before_type = macho(original)
after, after_type = macho(linked)
rows = []
discarded = 0
for anchor in sorted(before):
if not anchor.startswith("_dsp_") or not anchor.endswith("_dsp"):
continue
info = anchor[5:-4]
if info not in before:
raise ValueError(f"{original}: missing info symbol for {anchor}")
if not before[info][2] & 0x200: # N_ALT_ENTRY
raise ValueError(f"{original}: {info} starts an independent subsection")
if info not in after and anchor not in after:
discarded += 1
continue
if info not in after or anchor not in after:
raise ValueError(f"{linked}: lost table or entry: {info}")
old_distance = before[info][0] - before[anchor][0]
new_distance = after[info][0] - after[anchor][0]
old_type, new_type = before_type(info), after_type(info)
if old_distance <= 0 or new_distance != old_distance:
raise ValueError(f"{info}: table/entry distance changed {old_distance} -> {new_distance}")
if old_type == 0 or new_type != old_type:
raise ValueError(f"{info}: closure type changed {old_type} -> {new_type}")
rows.append({"symbol": info, "tableBytes": old_distance, "closureType": old_type})
if len(rows) < 12:
raise ValueError(f"expected at least 12 retained procedure tables, got {len(rows)}")
return {"result": "PASS", "discardedProcedures": discarded, "tables": rows}
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("object", type=pathlib.Path)
parser.add_argument("linked", type=pathlib.Path)
args = parser.parse_args()
print(json.dumps(check(args.object, args.linked), indent=2))
+2
View File
@@ -11,4 +11,6 @@ let
in
if system == "x86_64-linux"
then native.appendOverlays [ (import ./native-rts-overlay.nix) ]
else if system == "x86_64-darwin"
then native.appendOverlays [ (import ./darwin-info-table-overlay.nix) ]
else native