diff --git a/scripts/nix/trace-intel-setup.nix b/scripts/nix/trace-intel-setup.nix new file mode 100644 index 0000000000..3054d32632 --- /dev/null +++ b/scripts/nix/trace-intel-setup.nix @@ -0,0 +1,85 @@ +# Failure-only diagnostic for the pinned Intel Darwin compiler's ghc-boot Setup. +# Resolve lldbPath with xcrun on the runner before evaluation. Only this +# diagnostic build may use `--option sandbox false` to reach Apple's debugger. +{ original, lldbPath +, expectedDrv ? "/nix/store/vghjgkr64qjigddh3gnf3c103wbvpxmz-ghc-boot-lib-ghc-boot-9.14.drv" +}: +let + shellQuote = value: "'" + builtins.replaceStrings [ "'" ] [ "'\\''" ] value + "'"; + needle = " printf '#!/bin/sh\\nexec %s \"$@\"\\n' \"$g\" > \"$wrapBin/$gName\""; + replacement = '' + printf '#!/bin/sh\nrealGhc="%s"\n' "$g" > "$wrapBin/$gName" + cat >> "$wrapBin/$gName" <<'SIMPLEX_SETUP_COMPILER' + setupOutput= + previous= + setupSource= + setupMake= + for arg do + [ "$previous" != -o ] || setupOutput=$arg + case "$arg" in + --make) setupMake=1 ;; + */ghc-boot-9.14/setup/setup.hs) setupSource=1 ;; + esac + previous=$arg + done + "$realGhc" "$@" + status=$? + [ "$status" -eq 0 ] || exit "$status" + case "$setupOutput" in + */ghc-boot-9.14/setup/setup) + if [ -n "$setupMake" ] && [ -n "$setupSource" ]; then + realSetup=$setupOutput.lldb-real + cp -p "$setupOutput" "$realSetup" || exit 1 + cmp "$setupOutput" "$realSetup" || exit 1 + echo 'Intel Setup diagnostic: preserving compiler-produced executable' + sha256sum "$realSetup" + printf '#!/bin/sh\nset -eu\nrealSetup="%s"\noriginalSetup="%s"\n' \ + "$realSetup" "$setupOutput" > "$setupOutput" + cat >> "$setupOutput" <<'SIMPLEX_SETUP_EXECUTION' + configure= + for arg do + [ "$arg" != configure ] || configure=1 + done + if [ -z "$configure" ]; then + exec "$realSetup" "$@" + fi + argvFile=$originalSetup.lldb-argv + printf '%s\0' "$originalSetup" "$@" > "$argvFile" + env -0 > "$argvFile.env" + : "''${NIX_BUILD_TOP:?Intel Setup diagnostic requires NIX_BUILD_TOP}" + : > "$NIX_BUILD_TOP/intel-setup-lldb-attempted" + # LLDB rejects a Python module whose basename contains Nix's hash prefix. + driverDir=$originalSetup.lldb-driver + mkdir -p "$driverDir" + cp -f ${./trace_intel_setup_lldb.py} "$driverDir/trace_intel_setup_lldb.py" + echo 'Intel Setup diagnostic: actual configure under host LLDB (diagnostic permits sandbox=false)' + ${shellQuote lldbPath} --version + exec timeout --kill-after=10s 300s ${shellQuote lldbPath} --batch --no-lldbinit \ + -o "command script import \"$driverDir/trace_intel_setup_lldb.py\"" \ + -o "intel-setup-run \"$argvFile\"" -- "$realSetup" + SIMPLEX_SETUP_EXECUTION + chmod +x "$setupOutput" + fi + ;; + esac + exit 0 + SIMPLEX_SETUP_COMPILER + ''; + occurrences = phase: builtins.div + (builtins.stringLength phase - builtins.stringLength + (builtins.replaceStrings [ needle ] [ "" ] phase)) + (builtins.stringLength needle); +in +assert original.system == "x86_64-darwin"; +assert original.identifier.name == "ghc-boot"; +assert original.drvPath == expectedDrv; +assert builtins.isString lldbPath && builtins.substring 0 1 lldbPath == "/"; +original.overrideAttrs (old: + assert occurrences old.buildPhase == 1; { + buildPhase = builtins.replaceStrings [ needle ] [ replacement ] old.buildPhase + '' + if [ ! -f "$NIX_BUILD_TOP/intel-setup-lldb-attempted" ]; then + echo 'ERROR: ghc-boot Setup configure was not intercepted by LLDB' >&2 + exit 1 + fi + ''; + }) diff --git a/scripts/nix/trace_intel_setup_lldb.py b/scripts/nix/trace_intel_setup_lldb.py new file mode 100644 index 0000000000..c981b5073e --- /dev/null +++ b/scripts/nix/trace_intel_setup_lldb.py @@ -0,0 +1,184 @@ +"""LLDB command for the unchanged compiler-produced ghc-boot Setup binary.""" + +import hashlib +import json +import os +from pathlib import Path +import shlex + +import lldb + + +def nul_entries(path): + data = path.read_bytes() + if not data.endswith(b"\0"): + raise ValueError("incomplete NUL-delimited capture") + return [os.fsdecode(item) for item in data[:-1].split(b"\0")] + + +def drain(process): + for reader in (process.GetSTDOUT, process.GetSTDERR): + while True: + text = reader(65536) + if not text: + break + print(text, end="", flush=True) + + +def describe_stop(process): + stops = [] + for thread in process: + data = [thread.GetStopReasonDataAtIndex(i) + for i in range(thread.GetStopReasonDataCount())] + stops.append({"thread": thread.GetThreadID(), + "reason": thread.GetStopReason(), "data": data, + "description": thread.GetStopDescription(1024)}) + print("Intel Setup stop: " + json.dumps(stops), flush=True) + return stops + + +def dump_stop(target, process): + # Use SB objects directly: a Python command's nested CLI execution context + # can still refer to the pre-launch process even after SBTarget.Launch. + for thread in process: + print("Thread " + str(thread.GetThreadID()), flush=True) + for index in range(min(thread.GetNumFrames(), 80)): + print(str(thread.GetFrameAtIndex(index)), flush=True) + if thread.GetNumFrames() > 80: + print("Backtrace truncated after 80 frames", flush=True) + stopped = next((thread for thread in process + if thread.GetStopReason() not in (lldb.eStopReasonNone, + lldb.eStopReasonInvalid)), None) + if stopped is not None and stopped.GetNumFrames(): + frame = stopped.GetFrameAtIndex(0) + for group in frame.GetRegisters(): + print("Registers: " + str(group.GetName()), flush=True) + for register in group: + print(str(register.GetName()) + " = " + str(register.GetValue()), flush=True) + print("Instructions at stopped PC:", flush=True) + for instruction in target.ReadInstructions(frame.GetPCAddress(), 12): + print(str(instruction), flush=True) + print("Loaded images:", flush=True) + for index in range(target.GetNumModules()): + module = target.GetModuleAtIndex(index) + print(str(module.GetFileSpec()) + " UUID=" + str(module.GetUUIDString()), flush=True) + + +def stopped_signals(process): + reasons = [(thread.GetStopReason(), + thread.GetStopReasonDataAtIndex(0) if thread.GetStopReasonDataCount() else None) + for thread in process + if thread.GetStopReason() not in (lldb.eStopReasonNone, lldb.eStopReasonInvalid)] + return [number for reason, number in reasons + if reason == lldb.eStopReasonSignal and number is not None], reasons + + +def run(debugger, command, _result, _internal_dict): + process = None + code = 1 + try: + arguments = shlex.split(command) + if len(arguments) != 1: + raise ValueError("expected one argv-capture pathname") + capture = Path(arguments[0]) + argv = nul_entries(capture) + environment_file = Path(str(capture) + ".env") + environment_bytes = environment_file.read_bytes() + environment = nul_entries(environment_file) + environment_file.unlink() + target = debugger.GetSelectedTarget() + executable = target.GetExecutable() + executable_path = Path(executable.fullpath) + print("Intel Setup launch: " + json.dumps({ + "executable": str(executable_path), "argv": argv, + "cwd": os.getcwd(), + "executable_sha256": hashlib.sha256(executable_path.read_bytes()).hexdigest(), + "environment_sha256": hashlib.sha256(environment_bytes).hexdigest(), + "environment_entries": len(environment), + }), flush=True) + + # Supplying the executable separately leaves argv[0] equal to Cabal's + # original Setup pathname, rather than the retained sibling's name. + launch = lldb.SBLaunchInfo(argv) + launch.SetExecutableFile(executable, False) + launch.SetArguments(argv, False) + launch.SetWorkingDirectory(os.getcwd()) + launch.SetEnvironmentEntries(environment, False) + launch.SetLaunchFlags(launch.GetLaunchFlags() + & ~lldb.eLaunchFlagDisableASLR + | lldb.eLaunchFlagStopAtEntry) + # Synchronous launch waits past LLDB's transient initial loader stop. + # The shell wrapper bounds this call and the debugger at 300 seconds. + debugger.SetAsync(False) + error = lldb.SBError() + process = target.Launch(launch, error) + if error.Fail() or not process.IsValid(): + raise RuntimeError("LLDB launch failed: " + str(error)) + + unix_signals = process.GetUnixSignals() + benign = set() + for name in ("SIGPIPE", "SIGALRM", "SIGVTALRM", "SIGPROF"): + number = unix_signals.GetSignalNumberFromName(name) + if number < 0 or not all((unix_signals.SetShouldStop(number, False), + unix_signals.SetShouldNotify(number, False), + unix_signals.SetShouldSuppress(number, False))): + raise RuntimeError("cannot configure LLDB signal policy for " + name) + benign.add(number) + print("Intel Setup signals pass/nostop/nonotify: SIGPIPE SIGALRM SIGVTALRM SIGPROF", flush=True) + for name in ("SIGABRT", "SIGSEGV"): + number = unix_signals.GetSignalNumberFromName(name) + if number < 0 or not all((unix_signals.SetShouldStop(number, True), + unix_signals.SetShouldNotify(number, True), + unix_signals.SetShouldSuppress(number, False))): + raise RuntimeError("cannot preserve LLDB fault policy for " + name) + _, first_reasons = stopped_signals(process) + print("Intel Setup entry stop: " + json.dumps(first_reasons), flush=True) + initial_stop = unix_signals.GetSignalNumberFromName("SIGSTOP") + entry_reasons = (lldb.eStopReasonExec, lldb.eStopReasonTrace, + lldb.eStopReasonBreakpoint, lldb.eStopReasonPlanComplete) + # Continue the deliberate entry stop, but preserve any genuine fault + # occurring before entry. Apply policy before GHC starts its ticker. + if process.GetState() == lldb.eStateStopped and first_reasons and all( + reason in entry_reasons + or (reason == lldb.eStopReasonSignal and (number in benign or number == initial_stop)) + for reason, number in first_reasons): + error = process.Continue() + if error.Fail(): + raise RuntimeError("LLDB continue failed: " + str(error)) + # A benign signal may already have been queued when policy changed. + while process.GetState() == lldb.eStateStopped: + numbers, reasons = stopped_signals(process) + if not numbers or len(numbers) != len(reasons) or not set(numbers) <= benign: + break + print("Intel Setup resuming already-stopped benign signal: " + str(numbers), flush=True) + error = process.Continue() + if error.Fail(): + raise RuntimeError("LLDB continue failed: " + str(error)) + + drain(process) + state = process.GetState() + if state == lldb.eStateExited: + code = process.GetExitStatus() + print("Intel Setup exited: " + str(code), flush=True) + elif state in (lldb.eStateStopped, lldb.eStateCrashed, lldb.eStateSuspended): + stops = describe_stop(process) + dump_stop(target, process) + signals = [stop["data"][0] for stop in stops + if stop["reason"] == lldb.eStopReasonSignal and stop["data"]] + code = 128 + int(signals[0]) if signals else 1 + else: + raise RuntimeError("unexpected LLDB process state: " + str(state)) + except Exception as error: + code = 1 + print("Intel Setup diagnostic error: " + str(error), flush=True) + finally: + if process is not None and process.IsValid(): + if process.GetState() not in (lldb.eStateExited, lldb.eStateDetached): + process.Kill() + drain(process) + # LLDB's own successful batch execution must not mask a failed child. + debugger.HandleCommand("quit " + str(code if 0 <= code <= 255 else 1)) + + +def __lldb_init_module(debugger, _internal_dict): + debugger.HandleCommand("command script add -f trace_intel_setup_lldb.run intel-setup-run")