WIP: fix(runtime): build and keep crash records on macOS #511
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
good first issue
gpu/amd
gpu/igpu
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/macos
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
ci: approved
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!511
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:mac/crash-records"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why
On macOS, #75's crash records cannot be built:
host/hos_report_run.ccalls Linux'sgetrandom(), andhost/hos_crash_record.creads memory with Linux'sprocess_vm_readvand finds modules in/proc/self/maps. With themacOS build flags (#453/#479), both files fail to compile, and
tests/test_report_run.pyandtests/test_crash_record.pyfail on a Mac.What changed
Every change is inside
__APPLE__(C) orsys.platform == "darwin"(Python). Windows and Linux text is unchanged:unifdef -U__APPLE__on each changed C file gives main's file byte for byte.hos_report_run_id()inhost/hos_report_run.ctakes its 16 bytes fromgetentropy(2).arc4random_buf: it is the same kind of source as Linux'sgetrandom(…, 0), a direct read ofthe kernel generator, and it can fail, so the existing "unknown" path stays meaningful.
arc4random_bufcannot reporta failure (it returns void), and it is a user-space generator behind a lock.
getentropyeither 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.
host/hos_crash_record.c.safe_read()usesmach_vm_read_overwrite()onmach_task_self(): an unmapped or protected page comes back as anerror, not a fault. This is Darwin's counterpart of
process_vm_readvon its own pid.module_at()reads dyld's image list.hos_crash_record_start()asks for its address once, withtask_info(TASK_DYLD_INFO). At crash time,dyld_all_image_infos, itsinfoArray, the chosen image'smach_header_64and load commands, and its path are each read throughsafe_read(). The image is the one whoseheader is nearest at or below the pc. It counts only if its
__TEXTsegment (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 ininfoArray, is checked last.fcntl(F_GETPATH)instead of/proc/self/fd/2.tests/crash_record_test.cbuilds on Darwin.<sys/ucontext.h>instead of<ucontext.h>, which needs_XOPEN_SOURCEthere and whosegetcontextis deprecated.The signal-metadata case gives
on_signala zeroed context with Darwin's pointer-valueduc_mcontext.<sys/prctl.h>: core files are turned off withsetrlimit(RLIMIT_CORE, 0). macOS still writes its own crashreports.
SI_TKILL./usr/lib/libz.1.dylib, shown not loaded before) is found by name, atdladdr's offset.tests/test_crash_record.py: on Darwin, thesignalandcrashrecords'signal_codemust be 1 or 2 (see Reviewernotes). The module-offset PASS line names what Darwin checks.
Flags and settings
None.
Measurements
main:
9c9d69ed09(2026-11-14)this PR: mac/crash-records @
0935e43f18Hardware: Apple M4 Max (16 CPUs), 64 GB, macOS 26.7.1, Apple clang 21 (the tests'
gccis Apple clang here)Scene: none (game-free checks; no GPU, no game)
Method:
tests/run_all.py test_report_run test_crash_record, run main, this PR, main, this PR. The results are pass orfail 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.pyandpython -I -B tests/test_crash_record.pyin each checkout9c9d69ed0935e43ftests/test_report_run.py, rounds 1 and 2getrandomundeclared,ssize_tunknowntests/test_crash_record.py, rounds 1 and 2<ucontext.h>#error, no<sys/prctl.h>,getrandomgregs; Darwin's is #436)tests/test_crash_record.pywith #436 merged (scratch, below)host/hos_report_run.c,host/hos_crash_record.cwith #479's macOS host flags, arm64 and x86_64tests/run_all.py --jobs 6)A Darwin crash record from the fixture's real fault (arm64):
module_offset: fixture.exe+0x4138,signal: 11,signal_code: 2. Its raised SIGSEGV givesmodule_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_atoffset off by 16: fails atoffset == pcs[i] - (uintptr_t)info.dli_fbase.__TEXTbound: fails at!module_at((uintptr_t)guest_memory, …).The unmodified copy passes.
Testing
Tested:
test_report_runfails on main and passes here, in two interleaved rounds.test_crash_record, without #436: all 44 compiler errors are in the fixture's lines 127-154, the adapter pasted frommain's
host/posix/win32_posix.c(greg_t,REG_RAX, …). Nothing in this PR's files fails.test_crash_recordwith #436 merged on top, plus the two glue lines: passes natively, and passes with the fixture built-arch x86_64under Rosetta.test_darwin_fault_context(#436's own check) andtest_report_runpass.dladdrin the test only, outside any handler: the fixture's executable,libsystem_malloc'smalloc, and a library loaded after startup.nm -uofhos_crash_record.o(macOS flags) imports_mach_vm_read_overwriteand_task_info, and no_dladdror_dyld_*.tests/test_report_run.py, FAIL on main,passes here. The other 19 failures are main's on this Mac, by name. Trees were clean before and after.
Not tested:
sys.platform == "darwin".(
-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 generatedinclude folders were left out: these files do not use them.
Needs other testers: None to merge. Linux and Windows CI would confirm that nothing changed there.
Screenshots
Nothing on screen.
Risk and rollback
macOS only. If dyld's list cannot be read,
module_atreportsunknown, never a guessed module. That happens if thelist is changing (
infoArrayNULL, orinfoArray, its count orinfoArrayChangeTimestampdiffer before and after theread) or a read fails. Reverting the three commits restores main.
Reviewer notes
Base: this PR is on main
9c9d69ed. Main has since moved to543846eb(#385, #465), and neither commit touchesthese files.
git merge-tree --write-tree 543846eb mac/crash-recordsmerges cleanly.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-spaceserver, lock or allocation, and an unreadable source returns
KERN_INVALID_ADDRESS/KERN_PROTECTION_FAILUREinstead of faulting. Crash reporters (PLCrashReporter, KSCrash) use the same call for safe copies [inference: their
documented practice; not reviewed here].
infoArraywhile it changes the list(
<mach-o/dyld_images.h>). Neitherdladdrnor_dyld_image_count/_dyld_get_image_*is called, since they cantake dyld's lock.
memset,memcmp,basename_ofandhos_crash_redact: plain memory work with no allocation, as on the Linux path.The fixture's allocation poison is on during these calls.
task_info(TASK_DYLD_INFO),fcntl(F_GETPATH)andgetentropy.Measured Darwin signal codes, and an open decision for #436:
reports/MacMainFixes-files/probes/), a SIGSEGV sent withraise,killorpthread_killarrives withsi_code2 on arm64 and 1 under Rosetta, andsi_pid0. Those are the same codes as a real fault: the 0x40 store isin
__PAGEZERO, so arm64 reportsSEGV_ACCERR.si_code0.on_signal,if (si->si_code <= 0) raise(sig);, re-raises a sent SIGABRT on Darwin. A sentSIGSEGV/SIGBUS/SIGILL/SIGFPE is not re-raised, as was true everywhere before #75: the handler returns and the process
goes on. The test's
signalchild therefore exits 91 instead of dying by the signal.Apple's crash report at the faulting instruction [inference: not measured]. I recommend keeping it as is.
With #436:
context_from/darwin_fault_actionand #75'sExceptionInformation[13/14], SIGABRT and re-raise lines sit side by side.test_crash_recordthen needs two lines, carried by whichever of the two lands second(
reports/MacMainFixes-files/scratch-glue-for-436.diff):"darwin_fault.h", and the build addshost/posix/darwin_fault.con Darwin._exit(128 + sig)indarwin_fault_signalas fatal. This one failson every platform once both are on main, because the inventory scans every
host/**/*.c.Issues
See #75 and #436 (and #420, macOS tracking).
Checklist
For everyone
type(scope): description).main. Branched from9c9d69ed; main has moved to543846eb, which merges cleanly (neither new commit touches these files). It is rebased in the sweep before ready.python -I -B tests/run_all.pyrun and passing (any failure that also fails onmainis named above).third_party/untouched (a new folder with aPROVENANCE.mdinstead).If applicable
Assisted-by:/Co-Authored-By:trailers.mainand passing here.test_report_run(from #75) fails on main and passes here. The Darwin checks added totest_crash_recordpass once #436's exception adapter is present.tests/that reads changed source updated in this PR, not weakened.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.