fix(native): resolve symbol names in crash stacktraces on macOS#1856
Open
tustanivsky wants to merge 4 commits into
Open
fix(native): resolve symbol names in crash stacktraces on macOS#1856tustanivsky wants to merge 4 commits into
tustanivsky wants to merge 4 commits into
Conversation
The crash daemon had no symbol resolution on macOS: the ELF resolver covers Linux/Android only and the dbghelp path is Windows-only, so frames carried bare instruction addresses and the external crash reporter displayed unreadable stacks. Resolve names from the Mach-O symbol table of the containing module (fat/universal-aware, scanned in fixed-size chunks and cached per module). Since nlist entries carry no size, matches are bounded by LC_FUNCTION_STARTS - only a symbol placed exactly at the containing function's entry is accepted, so addresses in symbol-table gaps stay unresolved instead of being attributed to neighboring functions. Files are validated by LC_UUID equality with the loaded image, and stripped modules fall back to the dSYM companion (checked both next to the binary and next to the enclosing .app bundle), validated the same way.
macho_locate_symtab zeroes the whole macho_sym_info_t, so the state set
at function entry was wiped by the first candidate probe. When no usable
companion was found the slot stayed at 0 ("unresolved") and the dSYM
search re-ran for every frame in the module instead of once.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 443094a. Configure here.
mujacica
approved these changes
Jul 10, 2026
jpnurmi
approved these changes
Jul 12, 2026
jpnurmi
left a comment
Collaborator
There was a problem hiding this comment.
Now that all integration-tested desktop platforms are covered, we could drop the "win32 or linux" conditions in test_integration_native.py:
sentry-native/tests/test_integration_native.py
Lines 997 to 1001 in 252e4c6
sentry-native/tests/test_integration_native.py
Lines 1044 to 1048 in 252e4c6
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

This PR fixes address-only crash callstacks produced by the
nativebackend on macOS - frames carried bareinstruction_addrs with no function names, so the Sentry's external crash reporter (and any consumer of the client-side event) displayed unreadable stacks.Problem
The crash daemon has no symbol resolution on macOS.
enrich_frame_with_symbolis compiled for Linux/Android only (ELF, #1836) and the Windows path resolves names throughdbghelp(#1811). On macOS the daemon's frame-pointer walk only attached the containing module to each frame and never setfunction.Proposed fix
Implement the macOS counterpart of the ELF resolver in the daemon:
LC_SYMTAB/nlist_64) of the containing module, read from disk out-of-process. Universal (fat) binaries are resolved to the slice matching the daemon's architecture. Tables are scanned in fixed-size chunks with only the winning name read from the string table, and the resolved source is cached per module - large tables are never loaded wholesale.LC_FUNCTION_STARTS:nlistentries carry no size (unlike ELFst_size), so a name is only accepted when a symbol sits exactly at the entry of the function containing the address. The payload survivesstrip, so addresses in symbol-table gaps stay unresolved instead of being attributed to unrelated neighboring functions - an unresolved frame beats a wrong name.LC_UUIDequality with the loaded image (the Mach-O analog of the GNU build-id check). This also correctly rejects on-disk stubs of system dylibs that don't match the dyld-shared-cache images actually loaded; those frames are left for server-side symbolication.<X>.dSYM/Contents/Resources/DWARF/<binary>) when the module's own table misses (stripped local symbols), checked both next to the binary and next to the enclosing.appbundle (the UE packaged-game layout), UUID-validated.Testing
Checked manually against a UE 5.8 sample project (packaged Development build, macOS) by capturing a null-pointer crash:
Related items