fix(runtime): build and keep crash records on macOS #511

Closed
mooki wants to merge 8 commits from mooki/Longshot:mac/crash-records into main
Member

Type: fix · Risk: low · Tested on: macOS 26.7.1, Apple M4 Max (game-free checks, arm64 and Rosetta x86_64; no game run) · Needs: none

Why

On a Mac the game cannot write a crash record or a report run id: #75's crash records cannot be built there.
host/hos_report_run.c calls Linux's getrandom(), and host/hos_crash_record.c reads memory with Linux's
process_vm_readv and finds modules in /proc/self/maps. With the macOS build flags (#453/#479), both files fail to
compile, and tests/test_report_run.py and tests/test_crash_record.py fail on a Mac. This is part of the macOS port
(#420).

How to reproduce

Seen on: macOS 26.7.1, Apple M4 Max, in the game-free tests on main (the code is macOS only; Windows and Linux
build these files today).
Settings and switches: defaults.
Steps: on a Mac, in a checkout of main: python -I -B tests/test_report_run.py and python -I -B tests/test_crash_record.py.
Expected: both pass: a run id from the kernel's generator, and crash records with module offsets on macOS.
Actual: both fail to compile. At 9c9d69ed (before the rebase): host/hos_report_run.c:26:19: error: unknown type name 'ssize_t' and call to undeclared function 'getrandom'; the crash-record fixture stops at <ucontext.h>'s
#error, the missing <sys/prctl.h> and getrandom. At 6c9edffe both are among main's failures on this Mac.

What changed

The run id takes its bytes from getentropy(2) on macOS, and crash records read memory through
mach_vm_read_overwrite and find modules in dyld's image list instead of /proc. Every change is inside __APPLE__
(C) or sys.platform == "darwin" (Python). Windows and Linux text is unchanged: unifdef -U__APPLE__ on each changed C
file gives main's file byte for byte.

  • Read first: safe_read() and module_at() in host/hos_crash_record.c, then hos_report_run_id() in
    host/hos_report_run.c.
  • runtime, run id: hos_report_run_id() in host/hos_report_run.c takes its 16 bytes from getentropy(2).
    • Why getentropy and not arc4random_buf: it is the same kind of source as Linux's getrandom(…, 0), a direct read of
      the kernel generator, and it can fail, so the existing "unknown" path stays meaningful. arc4random_buf cannot report
      a failure (it returns void), and it is a user-space generator behind a lock.
    • Partial reads and blocking: getentropy either fills all of the buffer (up to 256 bytes) and returns 0, or returns
      -1. It never returns part of the buffer, so Linux's partial-read loop has no Darwin counterpart. It does not block:
      random(4) says Darwin's Fortuna generator makes no blocking distinction. The id is made once at startup, never on the
      crash path.
  • runtime, crash records: host/hos_crash_record.c.
    • safe_read() uses mach_vm_read_overwrite() on mach_task_self(): an unmapped or protected page comes back as an
      error, not a fault. This is Darwin's counterpart of process_vm_readv on its own pid.
    • module_at() reads dyld's image list. hos_crash_record_start() asks for its address once, with
      task_info(TASK_DYLD_INFO). At crash time, dyld_all_image_infos, its infoArray, the chosen image's
      mach_header_64 and load commands, and its path are each read through safe_read(). The image is the one whose
      header is nearest at or below the pc. It counts only if its __TEXT segment (vmaddr + slide, slide = header -
      vmaddr) holds the pc. The offset is taken from the header, which is dladdr's load base. dyld itself, which is not in
      infoArray, is checked last.
    • The log's path at startup comes from fcntl(F_GETPATH) instead of /proc/self/fd/2.
  • tests: tests/crash_record_test.c builds on Darwin.
    • <sys/ucontext.h> instead of <ucontext.h>, which needs _XOPEN_SOURCE there and whose getcontext is deprecated.
      The signal-metadata case gives on_signal a zeroed context with Darwin's pointer-valued uc_mcontext.
    • No <sys/prctl.h>: core files are turned off with setrlimit(RLIMIT_CORE, 0). macOS still writes its own crash
      reports.
    • The signal-metadata codes are the ones measured on this Mac (below), since Darwin has no SI_TKILL.
    • The authored Linux maps text is replaced by dyld-list checks:
      • A library loaded after startup (/usr/lib/libz.1.dylib, shown not loaded before) is found by name, at
        dladdr's offset.
      • Image data, the stack and page zero are refused.
      • These calls run with the fixture's allocation poison on.
    • tests/test_crash_record.py: on Darwin, the signal and crash records' signal_code must be 1 or 2 (see Reviewer
      notes). The module-offset PASS line names what Darwin checks.
  • tests, glue with #436 (c5e997f6): tests/crash_record_test.c includes darwin_fault.h on Apple, and
    tests/test_crash_record.py links host/posix/darwin_fault.c on macOS. This branch carries #436 (merge a59cec88
    of #436's rebased head 861d9aba), whose Darwin exception adapter the fixture needs.

Notes for reviewers

Depends on #436 (mac/fault-contexts): this branch carries #436's rebased head 861d9aba (merge a59cec88) and
must land after it. This PR's own commits are ff17351e, 0986f2aa, 00749bec and the glue c5e997f6.

  • Rebased on 6c9edffe: the rebase was clean; ff17351e, 0986f2aa and 00749bec are 52c53a66, 27751844
    and 0935e43f rebased. The branch now carries #436 (merge a59cec88, #436's rebased head 861d9aba) and the glue
    commit c5e997f6: crash_record_test.c includes darwin_fault.h on Apple, test_crash_record.py links
    darwin_fault.c on macOS. The exit-inventory glue line is in #436 itself (below).

What runs in the crash handler on Darwin, and why each is safe there:

  • mach_task_self(): a macro that reads the cached task port, with no call.
  • mach_vm_read_overwrite(): one Mach message to the kernel, which serves this task's own port. There is no user-space
    server, lock or allocation, and an unreadable source returns KERN_INVALID_ADDRESS/KERN_PROTECTION_FAILURE
    instead of faulting. Crash reporters (PLCrashReporter, KSCrash) use the same call for safe copies [inference: their
    documented practice; not reviewed here].
  • dyld's image list, read only through that call. dyld clears infoArray while it changes the list
    (<mach-o/dyld_images.h>). Neither dladdr nor _dyld_image_count / _dyld_get_image_* is called, since they can
    take dyld's lock.
  • memset, memcmp, basename_of and hos_crash_redact: plain memory work with no allocation, as on the Linux path.
    The fixture's allocation poison is on during these calls.
  • Startup only, never in the handler: task_info(TASK_DYLD_INFO), fcntl(F_GETPATH) and getentropy.

Measured Darwin signal codes, and an open decision for #436:

  • On this Mac (measured with probes), a SIGSEGV sent with raise, kill or pthread_kill arrives with
    si_code 2 on arm64 and 1 under Rosetta, and si_pid 0. Those are the same codes as a real fault: the 0x40 store is
    in __PAGEZERO, so arm64 reports SEGV_ACCERR.
  • A sent SIGABRT arrives with si_code 0.
  • So #75's rule in on_signal, if (si->si_code <= 0) raise(sig);, re-raises a sent SIGABRT on Darwin. A sent
    SIGSEGV/SIGBUS/SIGILL/SIGFPE is not re-raised, as was true everywhere before #75: the handler returns and the process
    goes on. The test's signal child therefore exits 91 instead of dying by the signal.
  • The tests assert what Darwin does. Whether to re-raise every Darwin fault signal belongs to #436. It would lose
    Apple's crash report at the faulting instruction [inference: not measured]. I recommend keeping it as is.

With #436:

  • The two merge without a text conflict. #436's Darwin context_from/darwin_fault_action and #75's
    ExceptionInformation[13/14], SIGABRT and re-raise lines sit side by side.
  • test_crash_record then needs two pieces of glue, first written as a scratch diff for whichever of the two landed
    second. With #436 first and this PR second they are split:
    • The fixture includes "darwin_fault.h", and the build adds host/posix/darwin_fault.c on Darwin: this PR's
      c5e997f6.
    • #75's exit inventory classifies #436's _exit(128 + sig) in darwin_fault_signal as fatal: in #436, its end
      commit 861d9aba. The inventory scans every host/**/*.c, so with #75 on main #436 fails it on every platform
      without that line, whatever order the two land in.
  • On arm64, #436's CONTEXT bridge fills only Rip and Rsp, so the record's host backtrace has one frame, the pc.

Measurements

Verdict: fixed on macOS, and nothing else moves: at this head test_report_run and test_crash_record go from FAIL
on main to pass (the crash records on arm64 and on x86_64 under Rosetta), and the full suite changes by those two and
#436's carried check only. Before the rebase test_report_run failed on main and passed here in two interleaved
rounds. Pass or fail: no timing median.

Commits: main 6c9edffe5d (2026-11-14); this PR mac/crash-records @
c5e997f6ff. Before the rebase: main 9c9d69ed09
(2026-11-14); this PR @ 0935e43f18.
Hardware: Apple M4 Max (16 CPUs), 64 GB, macOS 26.7.1, Apple clang 21 (the tests' gcc is Apple clang here)
Scene: none (game-free checks; no GPU, no game)
Method: after the rebase, the two tests and #436's check on this head (the crash records with --cc cc and with a
cc -arch x86_64 wrapper under Rosetta), and the full suite on main @ 6c9edffe and on this head, compared by test
name. Before the rebase: tests/run_all.py test_report_run test_crash_record, run main, this PR, main, this PR. The
results are pass or fail and the same in both rounds, so there is no timing median. Also the full game-free suite,
compared by test name with main's run on this Mac.
How to reproduce: python -I -B tests/test_report_run.py and python -I -B tests/test_crash_record.py in each checkout

Before: main @ 6c9edffe After: this PR @ c5e997f6
tests/test_report_run.py on macOS FAIL (in main's full suite on this Mac) PASS
tests/test_crash_record.py on macOS, --cc cc and a cc -arch x86_64 wrapper FAIL (in main's full suite on this Mac) PASS: 56 completed child runs plus locked-owner/kill cases, 13 PASS lines, on both
tests/test_darwin_fault_context.py --arch arm64 --arch x86_64 (#436, carried) absent on main PASS Darwin fault bridge
full game-free suite on macOS (tests/run_all.py, 6 at once) 531 passed, 32 failed, 17 skipped in 181 s 534 passed, 30 failed, 17 skipped in 178 s

Before the rebase (history at the commits the columns name):

main @ 9c9d69ed this PR @ 0935e43f
tests/test_report_run.py, rounds 1 and 2 FAIL: getrandom undeclared, ssize_t unknown PASS
tests/test_crash_record.py, rounds 1 and 2 FAIL: <ucontext.h> #error, no <sys/prctl.h>, getrandom FAIL: only the POSIX exception adapter (main's x86-64 Linux gregs; Darwin's is #436)
tests/test_crash_record.py with #436 merged (scratch, below) — PASS, arm64 and x86_64 (Rosetta)
host/hos_report_run.c, host/hos_crash_record.c with #479's macOS host flags, arm64 and x86_64 4 of 4 fail 4 of 4 compile
full game-free suite on macOS (tests/run_all.py --jobs 6) 533 passed, 20 failed, 17 skipped 534 passed, 19 failed, 17 skipped
main @ 9c9d69ed (test_report_run):
host/hos_report_run.c:26:19: error: unknown type name 'ssize_t'
host/hos_report_run.c:26:31: error: call to undeclared function 'getrandom'

this PR @ 0935e43f, merged with #436 (mac/fault-contexts @ 7731dc0c) and its two glue lines (Reviewer notes):
PASS: sent signals re-raised; real faults return to preserve original core siginfo
PASS: module offsets equal dladdr load-base offsets; a library loaded after startup found; data, stack and page zero refused
PASS: allocation-poisoned writer, bounds, short writes/EINTR, privacy, fatal/device-lost and real fatal children
...
PASS: 56 completed child runs plus locked-owner/kill cases

A Darwin crash record from the fixture's real fault (arm64): module_offset: fixture.exe+0x4138, signal: 11,
signal_code: 2. Its raised SIGSEGV gives module_offset: libsystem_kernel.dylib+0x95e8.

The new Darwin checks catch real faults. Two changes were tried in a scratch copy of this PR's files:

  • module_at offset off by 16: fails at offset == pcs[i] - (uintptr_t)info.dli_fbase.
  • No __TEXT bound: fails at !module_at((uintptr_t)guest_memory, …).

The unmodified copy passes.

Screenshots: none; nothing on screen.

Flags and settings

None.

Testing

Tested:

  • Re-tested after the rebase (head c5e997f6 on 6c9edffe; the branch now carries #436 and the glue):

    • python -I -B tests/test_report_run.py: PASS.
    • python -I -B tests/test_crash_record.py --cc cc, and with --cc a cc -arch x86_64 wrapper (Rosetta):
      PASS: 56 completed child runs plus locked-owner/kill cases, 13 PASS lines, on both.
    • python -I -B tests/test_darwin_fault_context.py --arch arm64 --arch x86_64 (#436's own check): PASS Darwin fault bridge.
  • On an Apple M4 Max with macOS 26.7.1, game-free only.

  • test_report_run fails on main and passes here, in two interleaved rounds (before the rebase, at 0935e43f).

  • test_crash_record before this branch carried #436 (0935e43f): all 44 compiler errors were in the fixture's lines
    127-154, the adapter pasted from main's host/posix/win32_posix.c (greg_t, REG_RAX, …). Nothing in this PR's
    files failed.

  • test_crash_record on the scratch merge before the rebase (0935e43f with #436 @ 7731dc0c merged on top, plus the
    two glue lines): passed natively, and passed with the fixture built -arch x86_64 under Rosetta.

    • On the same tree, test_darwin_fault_context (#436's own check) and test_report_run passed.
  • Module offsets are checked against dladdr in the test only, outside any handler: the fixture's executable,
    libsystem_malloc's malloc, and a library loaded after startup.

  • nm -u of hos_crash_record.o (macOS flags) imports _mach_vm_read_overwrite and _task_info, and no _dladdr or
    _dyld_*.

  • The full suite after the rebase, tests/run_all.py, 6 at once, on this branch, compared by test name with main on the
    same Mac (the runner labels macOS linux):

    main @ 6c9edffe:     531 passed, 32 failed, 17 skipped in 181 s (linux, 6 at once)
    this PR @ c5e997f6:  534 passed, 30 failed, 17 skipped in 178 s (linux, 6 at once)
    
    • Three statuses changed: tests/test_report_run.py and tests/test_crash_record.py, FAIL on main, pass here; and
      #436's tests/test_darwin_fault_context.py, which this branch carries, is new to main and passes.
    • The other 30 failures also fail on main, by name. The same 17 are skipped.
  • Before the rebase, at 0935e43f against main @ 9c9d69ed (Measurements), only one status changed:
    tests/test_report_run.py, FAIL on main, passed here. The other 19 failures were main's on this Mac, by name. Trees
    were clean before and after.

  • After the rebase onto main aa95d35f, at c4d95017 (c5e997f6 before the rebases onto e090cc48 and aa95d35f):
    Sweep3's checks of this head (the C/C++ it changes compiled, and its own tests run, on arm64 and x86_64 where they
    apply) gave the same results as before; python -I -B tests/run_all.py on wave H's merge f76fcb1b, which carries
    this head: 559/26/17 (passed/failed/skipped) against main aa95d35f's 543/33/17 on this Mac, no new failure
    (reports/2026-11-15-rebase-sweep-3.md).

Not tested:

  • Linux and Windows. Their preprocessed text is main's (unifdef above); the Python changes are behind
    sys.platform == "darwin".
  • The game: no macOS game build on today's main yet. The two files were compiled alone with #479's host flags
    (-arch arm64|x86_64 -mmacosx-version-min=14.4 -isysroot <SDK> -ffp-contract=off, x86_64 adds
    -march=x86-64-v2 -mcx16, then -I host/posix -I host -Wno-everything -O2 -g0 -DA64_POLLING). The game's generated
    include folders were left out: these files do not use them.
  • A real crash in the game on macOS.

Must be tested: None: this PR's own changes are macOS only. unifdef -U__APPLE__ of host/hos_crash_record.c,
host/hos_report_run.c and tests/crash_record_test.c gives main's text byte for byte, and its
tests/test_crash_record.py lines run only under sys.platform == "darwin". The one change in this branch that runs
elsewhere is #436's, carried: the exit inventory's posix/darwin_fault.c entry, a text scan of host/**/*.c that gives
the same answer on every OS and passes here. The macOS side is covered under Tested. The same comparison at the rebased
head c4d95017 against #436's head gives the same result.

Risks

What could break: macOS only. If dyld's list cannot be read, module_at reports unknown, never a guessed module.
That happens if the list is changing (infoArray NULL, or infoArray, its count or infoArrayChangeTimestamp differ
before and after the read) or a read fails. Landing order: after #436, which this branch carries (merge a59cec88);
it must land after it.

Turning it off: revert this PR's own commits (ff17351e, 0986f2aa, 00749bec and the glue c5e997f6); that
restores main as it is with #436.

Compatibility: nothing changes on Windows and Linux: they compile main's text. Saves, longshot_options.ini, the
shader and pipeline caches and the vk ABI are unchanged. On Linux the builder compiles the host's objects again once
(main's nx2/build.py keys them on every host/posix/*.h, and #436, carried, adds darwin_fault.h).

Issues

See #420 (macOS tracking).
See #75 (crash records).

Checklist

Everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description) of the status line's type.
  • Why gives the reason plainly and links the issues that fit (the tracker searched).
  • Measurements has a verdict, or one line saying why there is nothing to measure.
  • Testing names what was tested, what wasn't, and what must still be tested, where and how. (nothing to test elsewhere: Must be tested is None)
  • Rebased on current main: on aa95d35f (#761, 2026-11-15), head c4d95017, on #436 bccfd1c2, without a
    conflict; this PR's own commits, old → new: ff17351e → d3e86ddd, 0986f2aa → aa039e14, 00749bec → adebf9db,
    a59cec88 → 5d78393d, c5e997f6 → c4d95017; the commit names elsewhere in this description are from before the
    rebase.
  • python -I -B tests/run_all.py passes (any failure that also fails on main is named). After the rebase: 534
    passed, 30 failed, 17 skipped, against main's 531 passed, 32 failed, 17 skipped at 6c9edffe; every failure also
    fails on main (under Testing). After the rebase onto main aa95d35f (head c4d95017): wave H's merge f76fcb1b,
    559/26/17 against main aa95d35f's 543/33/17, no new failure.
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

When it applies

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • A game-free test added for the fix, failing on main and passing here. test_report_run (from #75) fails on
    main and passes here. The Darwin checks added to test_crash_record pass on this branch, which carries #436's
    exception adapter, on arm64 and on x86_64 under Rosetta.
  • A check under tests/ that reads changed source updated in this PR, not weakened.
**Type:** fix · **Risk:** low · **Tested on:** macOS 26.7.1, Apple M4 Max (game-free checks, arm64 and Rosetta x86_64; no game run) · **Needs:** none ## Why On a Mac the game cannot write a crash record or a report run id: #75's crash records cannot be built there. `host/hos_report_run.c` calls Linux's `getrandom()`, and `host/hos_crash_record.c` reads memory with Linux's `process_vm_readv` and finds modules in `/proc/self/maps`. With the macOS build flags (#453/#479), both files fail to compile, and `tests/test_report_run.py` and `tests/test_crash_record.py` fail on a Mac. This is part of the macOS port (#420). ## How to reproduce **Seen on:** macOS 26.7.1, Apple M4 Max, in the game-free tests on `main` (the code is macOS only; Windows and Linux build these files today). **Settings and switches:** defaults. **Steps:** on a Mac, in a checkout of `main`: `python -I -B tests/test_report_run.py` and `python -I -B tests/test_crash_record.py`. **Expected:** both pass: a run id from the kernel's generator, and crash records with module offsets on macOS. **Actual:** both fail to compile. At `9c9d69ed` (before the rebase): `host/hos_report_run.c:26:19: error: unknown type name 'ssize_t'` and `call to undeclared function 'getrandom'`; the crash-record fixture stops at `<ucontext.h>`'s #error, the missing `<sys/prctl.h>` and `getrandom`. At `6c9edffe` both are among `main`'s failures on this Mac. ## What changed The run id takes its bytes from `getentropy(2)` on macOS, and crash records read memory through `mach_vm_read_overwrite` and find modules in dyld's image list instead of `/proc`. Every change is inside `__APPLE__` (C) or `sys.platform == "darwin"` (Python). Windows and Linux text is unchanged: `unifdef -U__APPLE__` on each changed C file gives main's file byte for byte. - Read first: `safe_read()` and `module_at()` in `host/hos_crash_record.c`, then `hos_report_run_id()` in `host/hos_report_run.c`. - runtime, run id: `hos_report_run_id()` in `host/hos_report_run.c` takes its 16 bytes from `getentropy(2)`. - Why getentropy and not `arc4random_buf`: it is the same kind of source as Linux's `getrandom(…, 0)`, a direct read of the kernel generator, and it can fail, so the existing "unknown" path stays meaningful. `arc4random_buf` cannot report a failure (it returns void), and it is a user-space generator behind a lock. - Partial reads and blocking: `getentropy` either fills all of the buffer (up to 256 bytes) and returns 0, or returns -1. It never returns part of the buffer, so Linux's partial-read loop has no Darwin counterpart. It does not block: random(4) says Darwin's Fortuna generator makes no blocking distinction. The id is made once at startup, never on the crash path. - runtime, crash records: `host/hos_crash_record.c`. - `safe_read()` uses `mach_vm_read_overwrite()` on `mach_task_self()`: an unmapped or protected page comes back as an error, not a fault. This is Darwin's counterpart of `process_vm_readv` on its own pid. - `module_at()` reads dyld's image list. `hos_crash_record_start()` asks for its address once, with `task_info(TASK_DYLD_INFO)`. At crash time, `dyld_all_image_infos`, its `infoArray`, the chosen image's `mach_header_64` and load commands, and its path are each read through `safe_read()`. The image is the one whose header is nearest at or below the pc. It counts only if its `__TEXT` segment (vmaddr + slide, slide = header - vmaddr) holds the pc. The offset is taken from the header, which is `dladdr`'s load base. dyld itself, which is not in `infoArray`, is checked last. - The log's path at startup comes from `fcntl(F_GETPATH)` instead of `/proc/self/fd/2`. - tests: `tests/crash_record_test.c` builds on Darwin. - `<sys/ucontext.h>` instead of `<ucontext.h>`, which needs `_XOPEN_SOURCE` there and whose `getcontext` is deprecated. The signal-metadata case gives `on_signal` a zeroed context with Darwin's pointer-valued `uc_mcontext`. - No `<sys/prctl.h>`: core files are turned off with `setrlimit(RLIMIT_CORE, 0)`. macOS still writes its own crash reports. - The signal-metadata codes are the ones measured on this Mac (below), since Darwin has no `SI_TKILL`. - The authored Linux maps text is replaced by dyld-list checks: - A library loaded after startup (`/usr/lib/libz.1.dylib`, shown not loaded before) is found by name, at `dladdr`'s offset. - Image data, the stack and page zero are refused. - These calls run with the fixture's allocation poison on. - `tests/test_crash_record.py`: on Darwin, the `signal` and `crash` records' `signal_code` must be 1 or 2 (see Reviewer notes). The module-offset PASS line names what Darwin checks. - tests, glue with #436 (`c5e997f6`): `tests/crash_record_test.c` includes `darwin_fault.h` on Apple, and `tests/test_crash_record.py` links `host/posix/darwin_fault.c` on macOS. This branch carries #436 (merge `a59cec88` of #436's rebased head `861d9aba`), whose Darwin exception adapter the fixture needs. ### Notes for reviewers **Depends on #436** (`mac/fault-contexts`): this branch carries #436's rebased head `861d9aba` (merge `a59cec88`) and must land after it. This PR's own commits are `ff17351e`, `0986f2aa`, `00749bec` and the glue `c5e997f6`. - **Rebased on `6c9edffe`:** the rebase was clean; `ff17351e`, `0986f2aa` and `00749bec` are `52c53a66`, `27751844` and `0935e43f` rebased. The branch now carries #436 (merge `a59cec88`, #436's rebased head `861d9aba`) and the glue commit `c5e997f6`: `crash_record_test.c` includes `darwin_fault.h` on Apple, `test_crash_record.py` links `darwin_fault.c` on macOS. The exit-inventory glue line is in #436 itself (below). **What runs in the crash handler on Darwin, and why each is safe there:** - `mach_task_self()`: a macro that reads the cached task port, with no call. - `mach_vm_read_overwrite()`: one Mach message to the kernel, which serves this task's own port. There is no user-space server, lock or allocation, and an unreadable source returns `KERN_INVALID_ADDRESS`/`KERN_PROTECTION_FAILURE` instead of faulting. Crash reporters (PLCrashReporter, KSCrash) use the same call for safe copies [inference: their documented practice; not reviewed here]. - dyld's image list, read only through that call. dyld clears `infoArray` while it changes the list (`<mach-o/dyld_images.h>`). Neither `dladdr` nor `_dyld_image_count` / `_dyld_get_image_*` is called, since they can take dyld's lock. - `memset`, `memcmp`, `basename_of` and `hos_crash_redact`: plain memory work with no allocation, as on the Linux path. The fixture's allocation poison is on during these calls. - Startup only, never in the handler: `task_info(TASK_DYLD_INFO)`, `fcntl(F_GETPATH)` and `getentropy`. **Measured Darwin signal codes, and an open decision for #436:** - On this Mac (measured with probes), a SIGSEGV sent with `raise`, `kill` or `pthread_kill` arrives with `si_code` 2 on arm64 and 1 under Rosetta, and `si_pid` 0. Those are the same codes as a real fault: the 0x40 store is in `__PAGEZERO`, so arm64 reports `SEGV_ACCERR`. - A sent SIGABRT arrives with `si_code` 0. - So #75's rule in `on_signal`, `if (si->si_code <= 0) raise(sig);`, re-raises a sent SIGABRT on Darwin. A sent SIGSEGV/SIGBUS/SIGILL/SIGFPE is not re-raised, as was true everywhere before #75: the handler returns and the process goes on. The test's `signal` child therefore exits 91 instead of dying by the signal. - The tests assert what Darwin does. Whether to re-raise every Darwin fault signal belongs to #436. It would lose Apple's crash report at the faulting instruction [inference: not measured]. I recommend keeping it as is. **With #436:** - The two merge without a text conflict. #436's Darwin `context_from`/`darwin_fault_action` and #75's `ExceptionInformation[13/14]`, SIGABRT and re-raise lines sit side by side. - `test_crash_record` then needs two pieces of glue, first written as a scratch diff for whichever of the two landed second. With #436 first and this PR second they are split: - The fixture includes `"darwin_fault.h"`, and the build adds `host/posix/darwin_fault.c` on Darwin: this PR's `c5e997f6`. - #75's exit inventory classifies #436's `_exit(128 + sig)` in `darwin_fault_signal` as fatal: in #436, its end commit `861d9aba`. The inventory scans every `host/**/*.c`, so with #75 on main #436 fails it on every platform without that line, whatever order the two land in. - On arm64, #436's CONTEXT bridge fills only Rip and Rsp, so the record's host backtrace has one frame, the pc. ## Measurements **Verdict:** fixed on macOS, and nothing else moves: at this head `test_report_run` and `test_crash_record` go from FAIL on `main` to pass (the crash records on arm64 and on x86_64 under Rosetta), and the full suite changes by those two and #436's carried check only. Before the rebase `test_report_run` failed on `main` and passed here in two interleaved rounds. Pass or fail: no timing median. Commits: main 6c9edffe5da1f5b5383977aeab3f544c0de25d4c (2026-11-14); this PR `mac/crash-records` @ c5e997f6ff75eaa4f0e0c561cfe8059dfecf8ee3. Before the rebase: main 9c9d69ed09e2c24965d35f1696aa5c8dd8fe6e40 (2026-11-14); this PR @ 0935e43f18f149d5e77019cf98fc01512249854d. Hardware: Apple M4 Max (16 CPUs), 64 GB, macOS 26.7.1, Apple clang 21 (the tests' `gcc` is Apple clang here) Scene: none (game-free checks; no GPU, no game) Method: after the rebase, the two tests and #436's check on this head (the crash records with `--cc cc` and with a `cc -arch x86_64` wrapper under Rosetta), and the full suite on `main` @ 6c9edffe and on this head, compared by test name. Before the rebase: `tests/run_all.py test_report_run test_crash_record`, run main, this PR, main, this PR. The results are pass or fail and the same in both rounds, so there is no timing median. Also the full game-free suite, compared by test name with main's run on this Mac. How to reproduce: `python -I -B tests/test_report_run.py` and `python -I -B tests/test_crash_record.py` in each checkout | | Before: main @ 6c9edffe | After: this PR @ c5e997f6 | |---|---|---| | `tests/test_report_run.py` on macOS | FAIL (in `main`'s full suite on this Mac) | PASS | | `tests/test_crash_record.py` on macOS, `--cc cc` and a `cc -arch x86_64` wrapper | FAIL (in `main`'s full suite on this Mac) | `PASS: 56 completed child runs plus locked-owner/kill cases`, 13 PASS lines, on both | | `tests/test_darwin_fault_context.py --arch arm64 --arch x86_64` (#436, carried) | absent on `main` | `PASS Darwin fault bridge` | | full game-free suite on macOS (`tests/run_all.py`, 6 at once) | 531 passed, 32 failed, 17 skipped in 181 s | 534 passed, 30 failed, 17 skipped in 178 s | Before the rebase (history at the commits the columns name): | | main @ 9c9d69ed | this PR @ 0935e43f | |---|---|---| | `tests/test_report_run.py`, rounds 1 and 2 | FAIL: `getrandom` undeclared, `ssize_t` unknown | PASS | | `tests/test_crash_record.py`, rounds 1 and 2 | FAIL: `<ucontext.h>` #error, no `<sys/prctl.h>`, `getrandom` | FAIL: only the POSIX exception adapter (main's x86-64 Linux `gregs`; Darwin's is #436) | | `tests/test_crash_record.py` with #436 merged (scratch, below) | — | PASS, arm64 and x86_64 (Rosetta) | | `host/hos_report_run.c`, `host/hos_crash_record.c` with #479's macOS host flags, arm64 and x86_64 | 4 of 4 fail | 4 of 4 compile | | full game-free suite on macOS (`tests/run_all.py --jobs 6`) | 533 passed, 20 failed, 17 skipped | 534 passed, 19 failed, 17 skipped | ``` main @ 9c9d69ed (test_report_run): host/hos_report_run.c:26:19: error: unknown type name 'ssize_t' host/hos_report_run.c:26:31: error: call to undeclared function 'getrandom' this PR @ 0935e43f, merged with #436 (mac/fault-contexts @ 7731dc0c) and its two glue lines (Reviewer notes): PASS: sent signals re-raised; real faults return to preserve original core siginfo PASS: module offsets equal dladdr load-base offsets; a library loaded after startup found; data, stack and page zero refused PASS: allocation-poisoned writer, bounds, short writes/EINTR, privacy, fatal/device-lost and real fatal children ... PASS: 56 completed child runs plus locked-owner/kill cases ``` A Darwin crash record from the fixture's real fault (arm64): `module_offset: fixture.exe+0x4138`, `signal: 11`, `signal_code: 2`. Its raised SIGSEGV gives `module_offset: libsystem_kernel.dylib+0x95e8`. The new Darwin checks catch real faults. Two changes were tried in a scratch copy of this PR's files: - `module_at` offset off by 16: fails at `offset == pcs[i] - (uintptr_t)info.dli_fbase`. - No `__TEXT` bound: fails at `!module_at((uintptr_t)guest_memory, …)`. The unmodified copy passes. Screenshots: none; nothing on screen. ### Flags and settings None. ## Testing **Tested:** - Re-tested after the rebase (head `c5e997f6` on `6c9edffe`; the branch now carries #436 and the glue): - `python -I -B tests/test_report_run.py`: PASS. - `python -I -B tests/test_crash_record.py --cc cc`, and with `--cc` a `cc -arch x86_64` wrapper (Rosetta): `PASS: 56 completed child runs plus locked-owner/kill cases`, 13 PASS lines, on both. - `python -I -B tests/test_darwin_fault_context.py --arch arm64 --arch x86_64` (#436's own check): `PASS Darwin fault bridge`. - On an Apple M4 Max with macOS 26.7.1, game-free only. - `test_report_run` fails on main and passes here, in two interleaved rounds (before the rebase, at `0935e43f`). - `test_crash_record` before this branch carried #436 (`0935e43f`): all 44 compiler errors were in the fixture's lines 127-154, the adapter pasted from main's `host/posix/win32_posix.c` (`greg_t`, `REG_RAX`, …). Nothing in this PR's files failed. - `test_crash_record` on the scratch merge before the rebase (`0935e43f` with #436 @ `7731dc0c` merged on top, plus the two glue lines): passed natively, and passed with the fixture built `-arch x86_64` under Rosetta. - On the same tree, `test_darwin_fault_context` (#436's own check) and `test_report_run` passed. - Module offsets are checked against `dladdr` in the test only, outside any handler: the fixture's executable, `libsystem_malloc`'s `malloc`, and a library loaded after startup. - `nm -u` of `hos_crash_record.o` (macOS flags) imports `_mach_vm_read_overwrite` and `_task_info`, and no `_dladdr` or `_dyld_*`. - The full suite after the rebase, `tests/run_all.py`, 6 at once, on this branch, compared by test name with main on the same Mac (the runner labels macOS `linux`): ``` main @ 6c9edffe: 531 passed, 32 failed, 17 skipped in 181 s (linux, 6 at once) this PR @ c5e997f6: 534 passed, 30 failed, 17 skipped in 178 s (linux, 6 at once) ``` - Three statuses changed: `tests/test_report_run.py` and `tests/test_crash_record.py`, FAIL on main, pass here; and #436's `tests/test_darwin_fault_context.py`, which this branch carries, is new to main and passes. - The other 30 failures also fail on main, by name. The same 17 are skipped. - Before the rebase, at `0935e43f` against main @ `9c9d69ed` (Measurements), only one status changed: `tests/test_report_run.py`, FAIL on main, passed here. The other 19 failures were main's on this Mac, by name. Trees were clean before and after. - **After the rebase onto main aa95d35f**, at `c4d95017` (`c5e997f6` before the rebases onto e090cc48 and aa95d35f): Sweep3's checks of this head (the C/C++ it changes compiled, and its own tests run, on arm64 and x86_64 where they apply) gave the same results as before; `python -I -B tests/run_all.py` on wave H's merge `f76fcb1b`, which carries this head: 559/26/17 (passed/failed/skipped) against main aa95d35f's 543/33/17 on this Mac, no new failure (`reports/2026-11-15-rebase-sweep-3.md`). **Not tested:** - Linux and Windows. Their preprocessed text is main's (unifdef above); the Python changes are behind `sys.platform == "darwin"`. - The game: no macOS game build on today's main yet. The two files were compiled alone with #479's host flags (`-arch arm64|x86_64 -mmacosx-version-min=14.4 -isysroot <SDK> -ffp-contract=off`, x86_64 adds `-march=x86-64-v2 -mcx16`, then `-I host/posix -I host -Wno-everything -O2 -g0 -DA64_POLLING`). The game's generated include folders were left out: these files do not use them. - A real crash in the game on macOS. **Must be tested:** None: this PR's own changes are macOS only. `unifdef -U__APPLE__` of `host/hos_crash_record.c`, `host/hos_report_run.c` and `tests/crash_record_test.c` gives `main`'s text byte for byte, and its `tests/test_crash_record.py` lines run only under `sys.platform == "darwin"`. The one change in this branch that runs elsewhere is #436's, carried: the exit inventory's `posix/darwin_fault.c` entry, a text scan of `host/**/*.c` that gives the same answer on every OS and passes here. The macOS side is covered under Tested. The same comparison at the rebased head `c4d95017` against #436's head gives the same result. ## Risks **What could break:** macOS only. If dyld's list cannot be read, `module_at` reports `unknown`, never a guessed module. That happens if the list is changing (`infoArray` NULL, or `infoArray`, its count or `infoArrayChangeTimestamp` differ before and after the read) or a read fails. Landing order: after #436, which this branch carries (merge `a59cec88`); it must land after it. **Turning it off:** revert this PR's own commits (`ff17351e`, `0986f2aa`, `00749bec` and the glue `c5e997f6`); that restores main as it is with #436. **Compatibility:** nothing changes on Windows and Linux: they compile `main`'s text. Saves, `longshot_options.ini`, the shader and pipeline caches and the vk ABI are unchanged. On Linux the builder compiles the host's objects again once (`main`'s `nx2/build.py` keys them on every `host/posix/*.h`, and #436, carried, adds `darwin_fault.h`). ## Issues See #420 (macOS tracking). See #75 (crash records). ## Checklist ### Everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`) of the status line's type. - [x] Why gives the reason plainly and links the issues that fit (the tracker searched). - [x] Measurements has a verdict, or one line saying why there is nothing to measure. - [x] Testing names what was tested, what wasn't, and what must still be tested, where and how. (nothing to test elsewhere: Must be tested is None) - [x] Rebased on current `main`: on aa95d35f (#761, 2026-11-15), head `c4d95017`, on #436 `bccfd1c2`, without a conflict; this PR's own commits, old → new: `ff17351e` → `d3e86ddd`, `0986f2aa` → `aa039e14`, `00749bec` → `adebf9db`, `a59cec88` → `5d78393d`, `c5e997f6` → `c4d95017`; the commit names elsewhere in this description are from before the rebase. - [x] `python -I -B tests/run_all.py` passes (any failure that also fails on `main` is named). After the rebase: 534 passed, 30 failed, 17 skipped, against main's 531 passed, 32 failed, 17 skipped at `6c9edffe`; every failure also fails on main (under Testing). After the rebase onto main aa95d35f (head `c4d95017`): wave H's merge `f76fcb1b`, 559/26/17 against main aa95d35f's 543/33/17, no new failure. - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### When it applies - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [x] A game-free test added for the fix, failing on `main` and passing here. `test_report_run` (from #75) fails on main and passes here. The Darwin checks added to `test_crash_record` pass on this branch, which carries #436's exception adapter, on arm64 and on x86_64 under Rosetta. - [x] A check under `tests/` that reads changed source updated in this PR, not weakened.
macOS has no getrandom. getentropy(2) fills all 16 bytes from the kernel generator or fails (no partial result, at most 256 bytes per call); a failure keeps the id "unknown", as on Linux. Apple-gated; Windows and Linux text unchanged (unifdef -U__APPLE__).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
safe_read uses mach_vm_read_overwrite on the task's own port (Darwin has no process_vm_readv). module_at reads dyld's image list from TASK_DYLD_INFO (found at startup) and the candidate image's Mach-O __TEXT range, only through safe_read: no dladdr or _dyld_* call, which can take dyld's lock, on the crash path. The log path comes from fcntl F_GETPATH instead of /proc/self/fd/2. Apple-gated; Windows and Linux text unchanged (unifdef -U__APPLE__).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
test(runtime): build the crash-record fixture on macOS
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 14s
CI / Conventional Commit title (pull_request) Successful in 11s
CI / Workflow lint (actionlint) (pull_request) Successful in 11s
Third-party parts / As their notes say (pull_request) Successful in 16s
Website / Website on Cloudflare (pull_request_target) Has been skipped
0935e43f18
Darwin has no <sys/prctl.h> and needs _XOPEN_SOURCE for <ucontext.h>: no core file through RLIMIT_CORE, a pointer-valued authored context, Darwin's measured signal codes (no SI_TKILL), and dyld-list module checks (a library loaded after startup matches dladdr; data, stack and page zero are refused) in place of the authored maps text. Apple-gated; Linux and Windows unchanged.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
mooki force-pushed mac/crash-records from 0935e43f18
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 14s
CI / Conventional Commit title (pull_request) Successful in 11s
CI / Workflow lint (actionlint) (pull_request) Successful in 11s
Third-party parts / As their notes say (pull_request) Successful in 16s
Website / Website on Cloudflare (pull_request_target) Has been skipped
to c5e997f6ff
All checks were successful
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Third-party parts / As their notes say (pull_request) Successful in 9s
PR description / Description follows the template (pull_request_target) Successful in 4s
2026-11-15 00:34:44 +00:00
Compare
mooki force-pushed mac/crash-records from c5e997f6ff
All checks were successful
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Third-party parts / As their notes say (pull_request) Successful in 9s
PR description / Description follows the template (pull_request_target) Successful in 4s
to 493b65cb4e
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 16s
CI / Workflow lint (actionlint) (pull_request) Successful in 23s
PR description / Description follows the template (pull_request_target) Successful in 17s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 22s
Third-party parts / As their notes say (pull_request) Successful in 31s
2026-11-15 07:16:56 +00:00
Compare
mooki force-pushed mac/crash-records from 493b65cb4e
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 16s
CI / Workflow lint (actionlint) (pull_request) Successful in 23s
PR description / Description follows the template (pull_request_target) Successful in 17s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 22s
Third-party parts / As their notes say (pull_request) Successful in 31s
to c4d95017aa
Some checks failed
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 6s
Third-party parts / As their notes say (pull_request) Successful in 10s
PR description / Description follows the template (pull_request_target) Successful in 5s
2026-11-15 13:09:41 +00:00
Compare
mooki changed title from WIP: fix(runtime): build and keep crash records on macOS to fix(runtime): build and keep crash records on macOS 2026-11-15 13:13:28 +00:00
Author
Member

Folded into #845 (feat(macos): run the host on macOS: build, runtime, control, crash records and the Vulkan loader)

Folded into #845 (feat(macos): run the host on macOS: build, runtime, control, crash records and the Vulkan loader)
mooki closed this pull request 2026-11-15 18:07:36 +00:00
Some checks failed
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 6s
Third-party parts / As their notes say (pull_request) Successful in 10s
PR description / Description follows the template (pull_request_target) Successful in 5s

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Longshot!511
No description provided.