diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index e039d2fafa..19083b7018 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -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 diff --git a/flake.nix b/flake.nix index 86470e1959..22be84c24d 100644 --- a/flake.nix +++ b/flake.nix @@ -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 diff --git a/scripts/nix/README.md b/scripts/nix/README.md index e16e17f644..eb8a17366d 100644 --- a/scripts/nix/README.md +++ b/scripts/nix/README.md @@ -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 diff --git a/scripts/nix/check-macho-info-tables.nix b/scripts/nix/check-macho-info-tables.nix new file mode 100644 index 0000000000..b351da3a65 --- /dev/null +++ b/scripts/nix/check-macho-info-tables.nix @@ -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" +'' diff --git a/scripts/nix/darwin-info-table-overlay.nix b/scripts/nix/darwin-info-table-overlay.nix new file mode 100644 index 0000000000..e4df84417a --- /dev/null +++ b/scripts/nix/darwin-info-table-overlay.nix @@ -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 ]) + ); + }; +} diff --git a/scripts/nix/darwin-info-table-project.nix b/scripts/nix/darwin-info-table-project.nix new file mode 100644 index 0000000000..c413ffc3ff --- /dev/null +++ b/scripts/nix/darwin-info-table-project.nix @@ -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 ]; + }) + ]; +} diff --git a/scripts/nix/ghc914-macho-info-tables.patch b/scripts/nix/ghc914-macho-info-tables.patch new file mode 100644 index 0000000000..a7d48080d0 --- /dev/null +++ b/scripts/nix/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)) diff --git a/scripts/nix/macho-info-tables/InfoTables.hs b/scripts/nix/macho-info-tables/InfoTables.hs new file mode 100644 index 0000000000..da1bfdda28 --- /dev/null +++ b/scripts/nix/macho-info-tables/InfoTables.hs @@ -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 diff --git a/scripts/nix/macho-info-tables/check.py b/scripts/nix/macho-info-tables/check.py new file mode 100644 index 0000000000..e367cbf92b --- /dev/null +++ b/scripts/nix/macho-info-tables/check.py @@ -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(" command_end: + raise ValueError(f"{path}: invalid load command") + if cmd == 0x19: # LC_SEGMENT_64 + count, = unpack(" 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(" len(data): + raise ValueError(f"{path}: truncated string table") + symbols = {} + for i in range(symbol_count): + index, kind, section, desc, address = unpack("= 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(" {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)) diff --git a/scripts/nix/native-packages.nix b/scripts/nix/native-packages.nix index 7fd1df1836..c6bf0075c1 100644 --- a/scripts/nix/native-packages.nix +++ b/scripts/nix/native-packages.nix @@ -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