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
beanstalk: approved @ 1697603
beanstalk: approved @ 2c5a62e
beanstalk: approved @ 4fd82b7
beanstalk: approved @ 64d6d09
beanstalk: approved @ 6f91fae
beanstalk: approved @ 70a78f9
beanstalk: approved @ 8d361dc
beanstalk: approved @ 9d8356e
beanstalk: approved @ abf2227
beanstalk: approved @ d82f202
beanstalk: approved @ e5f1e40
beanstalk: approved @ ee8ab5f
ci: approved
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
regression
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?
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.ccalls Linux'sgetrandom(), andhost/hos_crash_record.creads memory with Linux'sprocess_vm_readvand finds modules in/proc/self/maps. With the macOS build flags (#453/#479), both files fail tocompile, and
tests/test_report_run.pyandtests/test_crash_record.pyfail 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 Linuxbuild these files today).
Settings and switches: defaults.
Steps: on a Mac, in a checkout of
main:python -I -B tests/test_report_run.pyandpython -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'andcall to undeclared function 'getrandom'; the crash-record fixture stops at<ucontext.h>'s#error, the missing
<sys/prctl.h>andgetrandom. At6c9edffeboth are amongmain's failures on this Mac.What changed
The run id takes its bytes from
getentropy(2)on macOS, and crash records read memory throughmach_vm_read_overwriteand 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 Cfile gives main's file byte for byte.
safe_read()andmodule_at()inhost/hos_crash_record.c, thenhos_report_run_id()inhost/hos_report_run.c.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.
c5e997f6):tests/crash_record_test.cincludesdarwin_fault.hon Apple, andtests/test_crash_record.pylinkshost/posix/darwin_fault.con macOS. This branch carries #436 (mergea59cec88of #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 head861d9aba(mergea59cec88) andmust land after it. This PR's own commits are
ff17351e,0986f2aa,00749becand the gluec5e997f6.6c9edffe: the rebase was clean;ff17351e,0986f2aaand00749becare52c53a66,27751844and
0935e43frebased. The branch now carries #436 (mergea59cec88, #436's rebased head861d9aba) and the gluecommit
c5e997f6:crash_record_test.cincludesdarwin_fault.hon Apple,test_crash_record.pylinksdarwin_fault.con 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-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:
raise,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 pieces of glue, first written as a scratch diff for whichever of the two landedsecond. With #436 first and this PR second they are split:
"darwin_fault.h", and the build addshost/posix/darwin_fault.con Darwin: this PR'sc5e997f6._exit(128 + sig)indarwin_fault_signalas fatal: in #436, its endcommit
861d9aba. The inventory scans everyhost/**/*.c, so with #75 on main #436 fails it on every platformwithout that line, whatever order the two land in.
Measurements
Verdict: fixed on macOS, and nothing else moves: at this head
test_report_runandtest_crash_recordgo from FAILon
mainto 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_runfailed onmainand passed here in two interleavedrounds. Pass or fail: no timing median.
Commits: main
6c9edffe5d(2026-11-14); this PRmac/crash-records@c5e997f6ff. Before the rebase: main9c9d69ed09(2026-11-14); this PR @
0935e43f18.Hardware: 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: after the rebase, the two tests and #436's check on this head (the crash records with
--cc ccand with acc -arch x86_64wrapper under Rosetta), and the full suite onmain@6c9edffeand on this head, compared by testname. Before the rebase:
tests/run_all.py test_report_run test_crash_record, run main, this PR, main, this PR. Theresults 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.pyandpython -I -B tests/test_crash_record.pyin each checkout6c9edffec5e997f6tests/test_report_run.pyon macOSmain's full suite on this Mac)tests/test_crash_record.pyon macOS,--cc ccand acc -arch x86_64wrappermain's full suite on this Mac)PASS: 56 completed child runs plus locked-owner/kill cases, 13 PASS lines, on bothtests/test_darwin_fault_context.py --arch arm64 --arch x86_64(#436, carried)mainPASS Darwin fault bridgetests/run_all.py, 6 at once)Before the rebase (history at the commits the columns name):
9c9d69ed0935e43ftests/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.
Screenshots: none; nothing on screen.
Flags and settings
None.
Testing
Tested:
Re-tested after the rebase (head
c5e997f6on6c9edffe; 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--ccacc -arch x86_64wrapper (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_runfails on main and passes here, in two interleaved rounds (before the rebase, at0935e43f).test_crash_recordbefore this branch carried #436 (0935e43f): all 44 compiler errors were in the fixture's lines127-154, the adapter pasted from main's
host/posix/win32_posix.c(greg_t,REG_RAX, …). Nothing in this PR'sfiles failed.
test_crash_recordon the scratch merge before the rebase (0935e43fwith #436 @7731dc0cmerged on top, plus thetwo glue lines): passed natively, and passed with the fixture built
-arch x86_64under Rosetta.test_darwin_fault_context(#436's own check) andtest_report_runpassed.Module offsets are checked against
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_*.The full suite after the rebase,
tests/run_all.py, 6 at once, on this branch, compared by test name with main on thesame Mac (the runner labels macOS
linux):tests/test_report_run.pyandtests/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.Before the rebase, at
0935e43fagainst 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. Treeswere clean before and after.
After the rebase onto main
aa95d35f, atc4d95017(c5e997f6before the rebases ontoe090cc48andaa95d35f):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.pyon wave H's mergef76fcb1b, which carriesthis 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:
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.
Must be tested: None: this PR's own changes are macOS only.
unifdef -U__APPLE__ofhost/hos_crash_record.c,host/hos_report_run.candtests/crash_record_test.cgivesmain's text byte for byte, and itstests/test_crash_record.pylines run only undersys.platform == "darwin". The one change in this branch that runselsewhere is #436's, carried: the exit inventory's
posix/darwin_fault.centry, a text scan ofhost/**/*.cthat givesthe same answer on every OS and passes here. The macOS side is covered under Tested. The same comparison at the rebased
head
c4d95017against #436's head gives the same result.Risks
What could break: macOS only. If dyld's list cannot be read,
module_atreportsunknown, never a guessed module.That happens if the list is changing (
infoArrayNULL, orinfoArray, its count orinfoArrayChangeTimestampdifferbefore 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,00749becand the gluec5e997f6); thatrestores main as it is with #436.
Compatibility: nothing changes on Windows and Linux: they compile
main's text. Saves,longshot_options.ini, theshader and pipeline caches and the vk ABI are unchanged. On Linux the builder compiles the host's objects again once
(
main'snx2/build.pykeys them on everyhost/posix/*.h, and #436, carried, addsdarwin_fault.h).Issues
See #420 (macOS tracking).
See #75 (crash records).
Checklist
Everyone
type(scope): description) of the status line's type.main: onaa95d35f(#761, 2026-11-15), headc4d95017, on #436bccfd1c2, without aconflict; 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 therebase.
python -I -B tests/run_all.pypasses (any failure that also fails onmainis named). After the rebase: 534passed, 30 failed, 17 skipped, against main's 531 passed, 32 failed, 17 skipped at
6c9edffe; every failure alsofails on main (under Testing). After the rebase onto main
aa95d35f(headc4d95017): wave H's mergef76fcb1b,559/26/17 against main aa95d35f's 543/33/17, no new failure.
third_party/untouched (a new folder with aPROVENANCE.mdinstead).When it applies
Assisted-by:/Co-Authored-By:trailers.mainand passing here.test_report_run(from #75) fails onmain and passes here. The Darwin checks added to
test_crash_recordpass on this branch, which carries #436'sexception adapter, on arm64 and on x86_64 under Rosetta.
tests/that reads changed source updated in this PR, not weakened.0935e43f18c5e997f6ffc5e997f6ff493b65cb4e493b65cb4ec4d95017aaWIP: fix(runtime): build and keep crash records on macOSto fix(runtime): build and keep crash records on macOSFolded into #845 (feat(macos): run the host on macOS: build, runtime, control, crash records and the Vulkan loader)
Pull request closed