diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index a8d7005cf3..205a11f6cd 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -154,6 +154,15 @@ jobs: lldb_path=$(xcrun --find lldb) printf 'Intel Setup host debugger: %s\n' "$lldb_path" "$lldb_path" --version + clang_path=$(xcrun --find clang) + sdk_path=$(xcrun --sdk macosx --show-sdk-path) + printf 'Intel Setup abort tracer compiler: %s\n' "$clang_path" + "$clang_path" --version + abort_tracer="$GITHUB_WORKSPACE/intel-setup-abort.dylib" + "$clang_path" -isysroot "$sdk_path" -arch x86_64 \ + -std=c11 -Werror=vla -pedantic -Wall -Wextra -Werror \ + -dynamiclib -O0 -g -fno-omit-frame-pointer \ + scripts/nix/trace_intel_setup_abort.c -o "$abort_tracer" flake_ref="git+file://$GITHUB_WORKSPACE?rev=$(git rev-parse HEAD)" original=$(cat < \"$wrapBin/$gName\""; replacement = '' @@ -56,7 +58,7 @@ let ${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" + -o "intel-setup-run \"$argvFile\" ${shellQuote (toString abortTracer)}" -- "$realSetup" SIMPLEX_SETUP_EXECUTION chmod +x "$setupOutput" fi @@ -74,6 +76,7 @@ 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 == "/"; +assert builtins.isString abortTracerPath && builtins.substring 0 1 abortTracerPath == "/"; original.overrideAttrs (old: assert occurrences old.buildPhase == 1; { buildPhase = builtins.replaceStrings [ needle ] [ replacement ] old.buildPhase + '' diff --git a/scripts/nix/trace_intel_setup_abort.c b/scripts/nix/trace_intel_setup_abort.c new file mode 100644 index 0000000000..c4ce0e27a4 --- /dev/null +++ b/scripts/nix/trace_intel_setup_abort.c @@ -0,0 +1,196 @@ +/* Diagnostic-only abort interposer. Never loaded by the canonical build. */ +#define _DARWIN_C_SOURCE 1 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void line(const char *format, ...) +{ + char buffer[2048]; + va_list arguments; + va_start(arguments, format); + int count = vsnprintf(buffer, sizeof(buffer), format, arguments); + va_end(arguments); + if (count < 0) { + return; + } + size_t length = (size_t)count; + if (length >= sizeof(buffer)) { + length = sizeof(buffer) - 1; + buffer[length - 1] = '\n'; + } + size_t written = 0; + while (written < length) { + ssize_t result = write(STDERR_FILENO, buffer + written, length - written); + if (result > 0) { + written += (size_t)result; + } else if (result < 0 && errno == EINTR) { + continue; + } else { + break; + } + } +} + +__attribute__((constructor)) static void restore_child_environment(void) +{ + const char *present = getenv("SIMPLEX_SETUP_ABORT_DYLD_PRESENT"); + const char *previous = getenv("SIMPLEX_SETUP_ABORT_DYLD_VALUE"); + bool valid = present != NULL && + (strcmp(present, "0") == 0 || strcmp(present, "1") == 0); + if (!valid || (strcmp(present, "1") == 0 && previous == NULL)) { + line("Intel Setup abort tracer: invalid private loader metadata\n"); + _exit(125); + } + int restored = strcmp(present, "1") == 0 + ? setenv("DYLD_INSERT_LIBRARIES", previous, 1) + : unsetenv("DYLD_INSERT_LIBRARIES"); + if (restored != 0 || unsetenv("SIMPLEX_SETUP_ABORT_DYLD_PRESENT") != 0 || + unsetenv("SIMPLEX_SETUP_ABORT_DYLD_VALUE") != 0) { + line("Intel Setup abort tracer: cannot restore child loader environment\n"); + _exit(125); + } + /* The fallback reports addresses, never a core or arbitrary memory. */ + struct rlimit limit; + if (getrlimit(RLIMIT_CORE, &limit) != 0) { + line("Intel Setup abort tracer: cannot read core limit\n"); + _exit(125); + } + limit.rlim_cur = 0; + if (setrlimit(RLIMIT_CORE, &limit) != 0) { + line("Intel Setup abort tracer: cannot disable diagnostic core output\n"); + _exit(125); + } + line("Intel Setup abort tracer loaded; child loader environment restored; core output disabled\n"); +} + +static void address_details(uintptr_t value) +{ + /* Resolve an address without reading any pointed-to closure data. */ + Dl_info address = {0}; + if (dladdr((const void *)value, &address) != 0) { + uintptr_t start = (uintptr_t)address.dli_saddr; + bool named = address.dli_sname != NULL && address.dli_saddr != NULL && value >= start; + line(" address image=%s base=%p symbol=%s+0x%" PRIxPTR "\n", + address.dli_fname != NULL ? address.dli_fname : "", + address.dli_fbase, named ? address.dli_sname : "", + named ? value - start : 0); + } +} + +static void saved_registers(unw_cursor_t *cursor) +{ +#if defined(__x86_64__) + static const int registers[] = { + UNW_X86_64_RBX, UNW_X86_64_RBP, UNW_X86_64_R12, + UNW_X86_64_R13, UNW_X86_64_R14, UNW_X86_64_R15 + }; + static const char *const names[] = { "rbx", "rbp", "r12", "r13", "r14", "r15" }; +#elif defined(__aarch64__) || defined(__arm64__) + static const int registers[] = { + UNW_ARM64_X19, UNW_ARM64_X20, UNW_ARM64_X21, UNW_ARM64_X22, + UNW_ARM64_X23, UNW_ARM64_X24, UNW_ARM64_X25, UNW_ARM64_X26, + UNW_ARM64_X27, UNW_ARM64_X28, UNW_ARM64_FP, UNW_ARM64_LR + }; + static const char *const names[] = { + "x19", "x20", "x21", "x22", "x23", "x24", "x25", "x26", + "x27", "x28", "fp", "lr" + }; +#else +#error "Intel Setup abort tracer requires a 64-bit Darwin target" +#endif + for (size_t index = 0; index < sizeof(registers) / sizeof(registers[0]); ++index) { + unw_word_t value = 0; + int result = unw_get_reg(cursor, registers[index], &value); + if (result == 0) { + line(" saved %s=0x%" PRIxPTR "\n", names[index], (uintptr_t)value); + address_details((uintptr_t)value); + } else { + line(" saved %s unavailable (%d)\n", names[index], result); + } + } +} + +static void trace_frames(void) +{ + unw_context_t context; + unw_cursor_t cursor; + if (unw_getcontext(&context) != 0 || unw_init_local(&cursor, &context) != 0) { + line("Intel Setup abort tracer: local unwind initialization failed\n"); + return; + } + for (unsigned int frame = 0; frame < 80; ++frame) { + unw_word_t ip = 0; + unw_word_t sp = 0; + unw_word_t offset = 0; + char symbol[512] = ""; + int ip_result = unw_get_reg(&cursor, UNW_REG_IP, &ip); + int sp_result = unw_get_reg(&cursor, UNW_REG_SP, &sp); + int symbol_result = unw_get_proc_name(&cursor, symbol, sizeof(symbol), &offset); + symbol[sizeof(symbol) - 1] = '\0'; + Dl_info image = {0}; + int image_result = ip_result == 0 ? dladdr((const void *)(uintptr_t)ip, &image) : 0; + line("Intel Setup abort frame %u ip=0x%" PRIxPTR " (%d) sp=0x%" PRIxPTR + " (%d) symbol=%s+0x%" PRIxPTR " (%d) image=%s base=%p\n", + frame, (uintptr_t)ip, ip_result, (uintptr_t)sp, sp_result, + symbol, (uintptr_t)offset, symbol_result, + image_result != 0 && image.dli_fname != NULL ? image.dli_fname : "", + image_result != 0 ? image.dli_fbase : NULL); + saved_registers(&cursor); + int result = unw_step(&cursor); + if (result <= 0) { + line("Intel Setup abort unwind finished (%d)\n", result); + return; + } + } + line("Intel Setup abort backtrace truncated after 80 frames\n"); +} + +static void trace_images(void) +{ + uint32_t count = _dyld_image_count(); + uint32_t bound = count < 256 ? count : 256; + for (uint32_t index = 0; index < bound; ++index) { + const char *name = _dyld_get_image_name(index); + line("Intel Setup abort image %u base=%p slide=0x%" PRIxPTR " path=%s\n", + index, (const void *)_dyld_get_image_header(index), + (uintptr_t)_dyld_get_image_vmaddr_slide(index), + name != NULL ? name : ""); + } + if (bound != count) { + line("Intel Setup abort images truncated at 256 of %u\n", count); + } +} + +/* Avoid overlapping or recursive logging if a failed unwind itself aborts. */ +static atomic_flag tracing = ATOMIC_FLAG_INIT; + +_Noreturn static void trace_abort(void) +{ + if (!atomic_flag_test_and_set_explicit(&tracing, memory_order_relaxed)) { + line("Intel Setup abort tracer: abort called; current-thread unwind is best-effort\n"); + trace_frames(); + trace_images(); + } + /* dyld excludes the interposing image's own reference, calling real abort. */ + abort(); +} + +/* Darwin's __interpose ABI is two pointer-sized words; no function/data cast. */ +__attribute__((used, section("__DATA,__interpose"))) +static const struct { + void (*replacement)(void); + void (*original)(void); +} abort_interpose = { trace_abort, abort }; diff --git a/scripts/nix/trace_intel_setup_lldb.py b/scripts/nix/trace_intel_setup_lldb.py index c981b5073e..84bf798c06 100644 --- a/scripts/nix/trace_intel_setup_lldb.py +++ b/scripts/nix/trace_intel_setup_lldb.py @@ -5,10 +5,49 @@ import json import os from pathlib import Path import shlex +import subprocess +import time import lldb +def permission_denied_before_launch(error, process): + # Never repeat a process that LLDB launched, even if it already exited. + denied = "this is a non-interactive debug session, cannot get permission to debug processes." + pid = process.GetProcessID() if process.IsValid() else 0 + return error.Fail() and denied in str(error) and pid == 0 + + +def abort_fallback(executable, argv, environment, tracer, deadline): + keys = ("SIMPLEX_SETUP_ABORT_DYLD_PRESENT", "SIMPLEX_SETUP_ABORT_DYLD_VALUE") + child_environment = dict(item.split("=", 1) for item in environment) + if any(key in child_environment for key in keys): + raise ValueError("abort tracer private environment key already exists") + original_dyld = child_environment.get("DYLD_INSERT_LIBRARIES") + child_environment[keys[0]] = "0" if original_dyld is None else "1" + if original_dyld is not None: + child_environment[keys[1]] = original_dyld + child_environment["DYLD_INSERT_LIBRARIES"] = str(tracer) + ( + ":" + original_dyld if original_dyld else "") + remaining = deadline - time.monotonic() + if remaining <= 0: + raise TimeoutError("Intel Setup overall diagnostic deadline expired") + digest = hashlib.sha256(executable.read_bytes()).hexdigest() + print("Intel Setup fallback: original LLDB permission denial retained; " + "diagnostic-only abort dylib inserted into unchanged Setup; " + "constructor restores child loader environment; no core output", flush=True) + result = subprocess.run(argv, executable=str(executable), env=child_environment, + timeout=remaining, check=False) + if hashlib.sha256(executable.read_bytes()).hexdigest() != digest: + raise RuntimeError("compiler-produced Setup bytes changed during fallback") + code = 128 - result.returncode if result.returncode < 0 else result.returncode + print("Intel Setup fallback result: " + json.dumps({ + "returncode": result.returncode, "shell_status": code, + "executable_sha256": digest, "unchanged": True, + }), flush=True) + return code + + def nul_entries(path): data = path.read_bytes() if not data.endswith(b"\0"): @@ -76,11 +115,13 @@ def stopped_signals(process): def run(debugger, command, _result, _internal_dict): process = None code = 1 + deadline = time.monotonic() + 300 try: arguments = shlex.split(command) - if len(arguments) != 1: - raise ValueError("expected one argv-capture pathname") + if len(arguments) not in (1, 2): + raise ValueError("expected argv-capture pathname and optional abort dylib") capture = Path(arguments[0]) + tracer = Path(arguments[1]) if len(arguments) == 2 else None argv = nul_entries(capture) environment_file = Path(str(capture) + ".env") environment_bytes = environment_file.read_bytes() @@ -113,6 +154,14 @@ def run(debugger, command, _result, _internal_dict): error = lldb.SBError() process = target.Launch(launch, error) if error.Fail() or not process.IsValid(): + print("Intel Setup LLDB launch failure: " + json.dumps({ + "error": str(error), "valid": process.IsValid(), + "pid": process.GetProcessID() if process.IsValid() else 0, + "state": process.GetState() if process.IsValid() else None, + }), flush=True) + if tracer is not None and permission_denied_before_launch(error, process): + code = abort_fallback(executable_path, argv, environment, tracer, deadline) + return raise RuntimeError("LLDB launch failed: " + str(error)) unix_signals = process.GetUnixSignals()