Skip to content

fix(native): resolve symbol names in crash stacktraces on macOS#1856

Open
tustanivsky wants to merge 4 commits into
masterfrom
fix/native-stacktrace-macos
Open

fix(native): resolve symbol names in crash stacktraces on macOS#1856
tustanivsky wants to merge 4 commits into
masterfrom
fix/native-stacktrace-macos

Conversation

@tustanivsky

Copy link
Copy Markdown
Collaborator

This PR fixes address-only crash callstacks produced by the native backend on macOS - frames carried bare instruction_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_symbol is compiled for Linux/Android only (ELF, #1836) and the Windows path resolves names through dbghelp (#1811). On macOS the daemon's frame-pointer walk only attached the containing module to each frame and never set function.

Proposed fix

Implement the macOS counterpart of the ELF resolver in the daemon:

  • Resolve names from the Mach-O symbol table (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.
  • Bound matches with LC_FUNCTION_STARTS: nlist entries carry no size (unlike ELF st_size), so a name is only accepted when a symbol sits exactly at the entry of the function containing the address. The payload survives strip, so addresses in symbol-table gaps stay unresolved instead of being attributed to unrelated neighboring functions - an unresolved frame beats a wrong name.
  • Validate files by LC_UUID equality 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.
  • Follow the dSYM companion (<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 .app bundle (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:

Screenshot 2026-07-10 at 14 27 34

Related items

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.
@tustanivsky tustanivsky marked this pull request as ready for review July 10, 2026 13:49
Comment thread src/backends/native/sentry_crash_daemon.c
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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread src/backends/native/sentry_crash_daemon.c

@jpnurmi jpnurmi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that all integration-tested desktop platforms are covered, we could drop the "win32 or linux" conditions in test_integration_native.py:

if sys.platform == "win32" or sys.platform == "linux":
# At least some frames should have symbolicated function names
assert any(
frame.get("function") is not None for frame in exc["stacktrace"]["frames"]
)

if sys.platform == "win32" or sys.platform == "linux":
# At least some frames should have symbolicated function names
assert any(
frame.get("function") is not None for frame in exc["stacktrace"]["frames"]
)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants