mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-04 19:27:55 +00:00
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:
1 parent
0e5ab60e9a
commit
ad69b2b1a6
10 files changed
+261
-5
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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"
|
||||
''
|
||||
@@ -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 ])
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
@@ -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))
|
||||
@@ -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
|
||||
Reference in new issue
Block a user