Skip to content

fix: AOT interop with managed .NET runtimes#6193

Open
jpnurmi wants to merge 12 commits intomainfrom
jpnurmi/mono-interop
Open

fix: AOT interop with managed .NET runtimes#6193
jpnurmi wants to merge 12 commits intomainfrom
jpnurmi/mono-interop

Conversation

@jpnurmi
Copy link
Copy Markdown
Collaborator

@jpnurmi jpnurmi commented Sep 18, 2025

📜 Description

TLDR; reorder .NET/Mono vs. Sentry Cocoa signal handlers to allow the .NET runtime to convert certain signals to managed .NET exceptions where appropriate, and chain actual native crashes to Sentry Cocoa.

Before

┌──────────────┐     ┌───────────┐     ┌────────┐
│ Sentry Cocoa │────>│ .NET/Mono │────>│ System │
└──────────────┘     └───────────┘     └────────┘

After:

┌───────────┐     ┌──────────────┐     ┌────────┐
│ .NET/Mono │────>│ Sentry Cocoa │────>│ System │
└───────────┘     └──────────────┘     └────────┘

Changes

Mostly gated by the SENTRY_CRASH_MANAGED_RUNTIME compile-time flag to minimize impact on normal SDK operation:

  1. Signal handler preloading — A __attribute__((constructor)) preloads SentryCrash's signal handlers before the managed runtime starts, ensuring the correct handler chain order: managed runtime → SentryCrash → system. This lets the managed runtime handle signals like SIGSEGV (for NullReferenceException) and SIGFPE (for DivideByZeroException) before SentryCrash sees them.

  2. Mach exception mask filtering — Excludes EXC_MASK_BAD_ACCESS and EXC_MASK_ARITHMETIC from Mach exception monitoring, since these correspond to signals the managed runtime handles. Other Mach exceptions (EXC_BAD_INSTRUCTION, EXC_SOFTWARE, EXC_BREAKPOINT) are still monitored.

  3. ignoreNextSignal: API — New method on PrivateSentrySDKOnly that tells SentryCrash to ignore the next occurrence of a given signal on the calling thread (thread-local, one-shot). Used by hybrid SDKs to prevent duplicate crash reports when the managed runtime is about to raise a signal (e.g. SIGABRT via abort()) for an exception that has already been captured: https://github.com/dotnet/macios/blob/d0d53e8230a79fd505ddf7cef2642493249ccb78/runtime/runtime.m#L1003-L1011

  4. NULL-guard g_onExceptionEvent — Prevents a crash if a signal fires between the constructor preload and sentrycrash_install().

  5. volatile crash pointer — The +[SentrySDK crash] test method now uses volatile to force an actual SIGSEGV instead of letting the compiler optimize the null dereference into a trap instruction (SIGTRAP).

💡 Motivation and Context

Fixes redundant native crash events when using Sentry with .NET on iOS:

  • Managed exceptions (e.g. ApplicationException) produced a duplicate SIGABRT event
  • NullReferenceException in AOT mode produced a duplicate EXC_BAD_ACCESS event

See: getsentry/sentry-dotnet#3954

💚 How did you test it?

Sentry .NET integration tests for iOS:

📝 Checklist

You have to check all boxes before merging:

  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.

@codecov
Copy link
Copy Markdown

codecov bot commented Sep 18, 2025

Codecov Report

❌ Patch coverage is 22.72727% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.390%. Comparing base (2b71685) to head (355c4ae).
⚠️ Report is 2 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...ash/Recording/Monitors/SentryCrashMonitor_Signal.c 17.647% 14 Missing ⚠️
Sources/Sentry/PrivateSentrySDKOnly.m 0.000% 1 Missing ⚠️
Sources/Sentry/SentrySDKInternal.m 0.000% 1 Missing ⚠️
Sources/SentryCrash/Recording/SentryCrashC.c 0.000% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@              Coverage Diff              @@
##              main     #6193       +/-   ##
=============================================
- Coverage   85.412%   85.390%   -0.022%     
=============================================
  Files          487       487               
  Lines        29086     29104       +18     
  Branches     12592     12603       +11     
=============================================
+ Hits         24843     24852        +9     
- Misses        4193      4201        +8     
- Partials        50        51        +1     
Files with missing lines Coverage Δ
...entryCrash/Recording/Monitors/SentryCrashMonitor.c 84.210% <100.000%> (+0.210%) ⬆️
...ording/Monitors/SentryCrashMonitor_MachException.c 36.286% <ø> (ø)
Sources/Sentry/PrivateSentrySDKOnly.m 72.727% <0.000%> (-0.607%) ⬇️
Sources/Sentry/SentrySDKInternal.m 84.716% <0.000%> (ø)
Sources/SentryCrash/Recording/SentryCrashC.c 76.829% <0.000%> (-0.949%) ⬇️
...ash/Recording/Monitors/SentryCrashMonitor_Signal.c 55.200% <17.647%> (-6.062%) ⬇️

... and 7 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 2b71685...355c4ae. Read the comment docs.

jpnurmi added a commit to getsentry/sentry-dotnet that referenced this pull request Sep 18, 2025
This reverts "Use pre-built version of sentry-cocoa SDK (#3727)"
commit d179ec9 and restores the
modules/sentry-cocoa Git module checked out at:
getsentry/sentry-cocoa#6193
@jpnurmi jpnurmi force-pushed the jpnurmi/mono-interop branch 2 times, most recently from 2f90356 to ed98b04 Compare September 18, 2025 11:46
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Sep 18, 2025

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 1218.20 ms 1256.60 ms 38.40 ms
Size 24.14 KiB 1.13 MiB 1.10 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
f194b9c 1216.71 ms 1249.02 ms 32.31 ms
5e2ec54 1199.69 ms 1239.09 ms 39.40 ms
2c4362a 1231.50 ms 1255.95 ms 24.45 ms
5c68ec6 1233.11 ms 1264.06 ms 30.96 ms
16b3235 1234.51 ms 1257.84 ms 23.33 ms
bb418da 1227.60 ms 1265.90 ms 38.30 ms
d492bc8 1214.12 ms 1242.19 ms 28.06 ms
64a365a 1225.60 ms 1255.49 ms 29.89 ms
d8db577 1206.83 ms 1244.39 ms 37.56 ms
62d9cf0 1228.72 ms 1258.29 ms 29.57 ms

App size

Revision Plain With Sentry Diff
f194b9c 24.14 KiB 1.12 MiB 1.10 MiB
5e2ec54 24.14 KiB 1.10 MiB 1.08 MiB
2c4362a 24.14 KiB 1.07 MiB 1.04 MiB
5c68ec6 24.14 KiB 1.13 MiB 1.10 MiB
16b3235 24.14 KiB 1.11 MiB 1.09 MiB
bb418da 24.14 KiB 1.04 MiB 1.02 MiB
d492bc8 24.14 KiB 1.11 MiB 1.09 MiB
64a365a 24.14 KiB 1.09 MiB 1.06 MiB
d8db577 24.14 KiB 1.06 MiB 1.04 MiB
62d9cf0 24.14 KiB 1.09 MiB 1.07 MiB

Previous results on branch: jpnurmi/mono-interop

Startup times

Revision Plain With Sentry Diff
58546af 1217.57 ms 1248.98 ms 31.40 ms
4a46856 1236.53 ms 1262.79 ms 26.26 ms
446c562 1227.20 ms 1256.17 ms 28.97 ms
2488cd0 1215.08 ms 1241.24 ms 26.16 ms
3a8f309 1223.75 ms 1245.77 ms 22.02 ms

App size

Revision Plain With Sentry Diff
58546af 24.14 KiB 1.13 MiB 1.11 MiB
4a46856 24.14 KiB 1.13 MiB 1.10 MiB
446c562 24.14 KiB 1.13 MiB 1.10 MiB
2488cd0 24.14 KiB 1.13 MiB 1.11 MiB
3a8f309 24.14 KiB 1.13 MiB 1.10 MiB

@jpnurmi
Copy link
Copy Markdown
Collaborator Author

jpnurmi commented Sep 18, 2025

Screen.Recording.2025-09-18.at.11.54.52.mov

@jpnurmi jpnurmi changed the title [WIP] fix: interop with managed Mono/CoreCLR runtimes [WIP] fix: interop with managed .NET runtimes Sep 29, 2025
@jpnurmi jpnurmi force-pushed the jpnurmi/mono-interop branch from ed98b04 to 8559ea9 Compare October 28, 2025 12:47
@jpnurmi jpnurmi changed the title [WIP] fix: interop with managed .NET runtimes [WIP] fix: AOT interop with managed .NET runtimes Oct 28, 2025
@jpnurmi jpnurmi changed the base branch from main to v8.x October 28, 2025 12:58
@jpnurmi jpnurmi force-pushed the jpnurmi/mono-interop branch from db30de0 to dd33f14 Compare October 28, 2025 16:20
@jpnurmi jpnurmi changed the base branch from v8.x to main October 28, 2025 16:20
@jpnurmi jpnurmi force-pushed the jpnurmi/mono-interop branch from dd33f14 to e51c82e Compare October 29, 2025 13:13
@jpnurmi jpnurmi changed the title [WIP] fix: AOT interop with managed .NET runtimes fix: AOT interop with managed .NET runtimes Nov 3, 2025
@philprime
Copy link
Copy Markdown
Member

Hey @jpnurmi what's the progress on this PR?

@jpnurmi
Copy link
Copy Markdown
Collaborator Author

jpnurmi commented Mar 13, 2026

I had quite some trouble passing the CI last time I tried. 😅 I should totally revive this because Sentry .NET is still very much in need of a solution for eliminating redundant EXC_BAD_ACCESS for managed exceptions that were handled by the managed runtime.

@jpnurmi jpnurmi force-pushed the jpnurmi/mono-interop branch from be528ee to e1a14ae Compare March 24, 2026 12:40
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 24, 2026

Semver Impact of This PR

🟢 Patch (bug fixes)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


New Features ✨

  • Add per-call attachAllThreads override by itaybre in #7767
  • Add attachAllThreads option by itaybre in #7764

Bug Fixes 🐛

  • AOT interop with managed .NET runtimes by jpnurmi in #6193
  • Move SessionTracker file I/O off the main thread by denrase in #7704
  • Copy incoming tags dict to prevent crash by itaybre in #7763
  • Per-instance unmaskView propagates to child views by denrase in #7733

Documentation 📚

  • Add SentryCrash analysis and improvement plan document by itaybre in #7528

Internal Changes 🔧

Deps

  • Bump ruby/setup-ruby from 1.295.0 to 1.298.0 by dependabot in #7751
  • Bump codecov/codecov-action from 5.5.3 to 6.0.0 by dependabot in #7749
  • Bump getsentry/craft from 2.25.0 to 2.25.2 by dependabot in #7748
  • Bump fastlane-plugin-sentry from 2.4.0 to 2.5.0 by dependabot in #7746
  • Bump actions/deploy-pages from 4.0.5 to 5.0.0 by dependabot in #7747
  • Update clang-format version by sentry-mobile-updater in #7755
  • Bump getsentry/craft/.github/workflows/changelog-preview.yml from 2.25.0 to 2.25.2 by dependabot in #7750
  • Bump activesupport from 7.2.3 to 7.2.3.1 by dependabot in #7732
  • Bump fastlane-plugin-sentry from 2.3.0 to 2.4.0 by dependabot in #7724
  • Bump ruby/setup-ruby from 1.292.0 to 1.295.0 by dependabot in #7728
  • Bump getsentry/craft from 2.24.1 to 2.25.0 by dependabot in #7729
  • Bump codecov/codecov-action from 5.5.2 to 5.5.3 by dependabot in #7725
  • Bump actions/create-github-app-token from 2.2.1 to 3.0.0 by dependabot in #7726
  • Bump getsentry/craft/.github/workflows/changelog-preview.yml from 2.24.1 to 2.25.0 by dependabot in #7727
  • Bump json from 2.18.1 to 2.19.2 by dependabot in #7709

Other

  • Update validate-pr workflow by stephanie-anderson in #7769
  • Pin GitHub Actions to full-length commit SHAs by joshuarli in #7730

Other

  • impr: Align app lifecycle breadcrumb states with app context by denrase in #7703

🤖 This preview updates automatically when you update the PR.

@jpnurmi jpnurmi force-pushed the jpnurmi/mono-interop branch from e1a14ae to bab06c0 Compare March 24, 2026 12:47
@jpnurmi jpnurmi marked this pull request as ready for review March 24, 2026 13:16
@itaybre itaybre added the ready-to-merge Use this label to trigger all PR workflows label Mar 24, 2026
@itaybre
Copy link
Copy Markdown
Contributor

itaybre commented Mar 24, 2026

@jpnurmi added ready-to-merge label so you can get more checks and confirm everything works.

Let me know if this is actually ready to review

@sentry
Copy link
Copy Markdown

sentry bot commented Mar 24, 2026

Sentry Build Distribution

App Name App ID Version Configuration Install Page
SDK-Size io.sentry.sample.SDK-Size 9.8.0 (1) Release Install Build

@jpnurmi
Copy link
Copy Markdown
Collaborator Author

jpnurmi commented Mar 24, 2026

@itaybre thanks! @supervacuus will also take a look in the coming days.

Copy link
Copy Markdown
Contributor

@supervacuus supervacuus left a comment

Choose a reason for hiding this comment

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

The biggest concern with this setup lies at the core of the approach you chose: using __attribute__((constructor)) runs counter to the SDK's entire life-cycle management.

So, while the changes are limited and easy to review, and the pain part is isolated to builds using SENTRY_CRASH_MANAGED_RUNTIME, i.e., no influence on normal SDK operation, I have to warn you about the side effects:

  • Since the ctor lines up previous <- sentry for the .NET handler to chain on, close()/uninstall() restores our previous handler, removing the .NET handler from the chain. So any hard-fault-triggered .NET exception that comes in after close() will be unhandled wrt the .NET runtime.
  • start() -> close() -> start() would lead to a second run that runs with Sentry but without the runtime handler.
  • enableCrashHandler = false no longer gates signal-handler installation

Fixing these likely requires SENTRY_CRASH_MANAGED_RUNTIME-specific uninstall/reinstall behavior, not just the ctor itself, and you could argue that none of those ever happen in downstream usage, but those are just the ones I immediately caught, and I think they highlight the cost of jumping a core concern outside the life-cycle boundary.

jpnurmi and others added 4 commits March 27, 2026 15:06
Add SENTRY_CRASH_MANAGED_RUNTIME compile-time flag that:
- Preloads signal handlers via __attribute__((constructor)) before
  the managed runtime starts, ensuring correct handler chain order
- Excludes EXC_MASK_BAD_ACCESS and EXC_MASK_ARITHMETIC from Mach
  exception monitoring (handled by the managed runtime via signals)

Add ignoreNextSignal: API on PrivateSentrySDKOnly to let hybrid SDKs
tell SentryCrash to skip the next occurrence of a signal on the
calling thread (thread-local, one-shot).

NULL-guard g_onExceptionEvent callback in handleException to prevent
crash if a signal fires between preload and full sentrycrash_install.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The flag was originally needed to prevent sentrycrash_setMonitoring()
from re-enabling Mach exceptions after preload. Now that the Mach
exception mask is controlled at compile time via
SENTRY_CRASH_MANAGED_RUNTIME in SentryCrashMonitor_MachException.c,
there is no runtime state to guard.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Preserve the managed runtime's signal handler chain across
SDK lifecycle transitions (close/start) by skipping uninstall
when not handling a crash. On the crash path, uninstall proceeds
normally since the process is terminating.
@jpnurmi jpnurmi force-pushed the jpnurmi/mono-interop branch from b95d5a3 to c12a651 Compare March 27, 2026 14:12
jpnurmi added 3 commits March 27, 2026 15:31
When the signal monitor is disabled under managed runtime, restore
the previous handler for the signal before re-raising to prevent
an infinite loop. Without SA_NODEFER, the raised signal would be
re-delivered to the same handler after it returns.
The flag is redundant now that handleSignal() restores individual
handlers before re-raising. uninstallSignalHandler() can be a
blanket no-op under SENTRY_CRASH_MANAGED_RUNTIME.
@jpnurmi
Copy link
Copy Markdown
Collaborator Author

jpnurmi commented Mar 27, 2026

@supervacuus thanks for the view! 🙏 I've addressed the close/reinstall scenario.

@jpnurmi
Copy link
Copy Markdown
Collaborator Author

jpnurmi commented Mar 27, 2026

By the way, there are integration tests prepared in sentry-dotnet. This patch fixes both scenarios with redundant native exceptions:

With this PR and expected failures removed:

$ pwsh integration-test/ios.Tests.ps1
[...]
[+] /Users/jpnurmi/Projects/sentry/sentry-dotnet/integration-test/ios.Tests.ps1 180.08s (178.93s|113ms)
Tests completed in 180.08s
Tests Passed: 6, Failed: 0, Skipped: 0, Inconclusive: 0, NotRun: 0

@jpnurmi
Copy link
Copy Markdown
Collaborator Author

jpnurmi commented Mar 31, 2026

Hi @philipphofmann @philprime @noahsmartin @itaybre, this PR is ready for review when you get a chance. It adds managed runtime (.NET/Mono) signal handler interop, solving a long-standing issue for .NET on iOS (getsentry/sentry-dotnet#3954). Most of it is gated behind SENTRY_CRASH_MANAGED_RUNTIME to minimize the impact on normal SDK operation. @supervacuus already provided feedback on the lifecycle concerns, which have been addressed in the latest commits. Thanks! 🙏

Copy link
Copy Markdown
Contributor

@itaybre itaybre left a comment

Choose a reason for hiding this comment

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

Almost LGTM, can we document SENTRY_CRASH_MANAGED_RUNTIME and the constructor in the develop_docs folder for future reference?

@jpnurmi
Copy link
Copy Markdown
Collaborator Author

jpnurmi commented Apr 1, 2026

Thanks for the review! Sounds good, I'll take a look tomorrow.

P.S. The .NET ecosystem likes to own the term "managed" but technically, many other runtimes are managed too. I might rename it to SENTRY_CRASH_DOTNET_RUNTIME, or maybe just SENTRY_CRASH_DOTNET. What do you think?

@itaybre
Copy link
Copy Markdown
Contributor

itaybre commented Apr 1, 2026

Thanks for the review! Sounds good, I'll take a look tomorrow.

P.S. The .NET ecosystem likes to own the term "managed" but technically, many other runtimes are managed too. I might rename it to SENTRY_CRASH_DOTNET_RUNTIME, or maybe just SENTRY_CRASH_DOTNET. What do you think?

I would prefer to avoid DOTNET in the name, just in case some other SDK needs this in the future.
Aside from that, I am fine with that

@jpnurmi
Copy link
Copy Markdown
Collaborator Author

jpnurmi commented Apr 2, 2026

Fair enough, I kept SENTRY_CRASH_MANAGED_RUNTIME and only mentioned .NET/Mono as an example in the developer docs. Hard to say how compatible this would be with some other managed runtimes, but we can leave the door open.

Reset the flag on any signal delivery, not just the matching
one. Prevents a stale flag from silently suppressing a later
unrelated signal.
Instead of returning early from the signal handler when a signal
is ignored, skip crash processing and fall through to the existing
restore + raise path. This avoids undefined behavior when the
ignored signal originates from abort().
Copy link
Copy Markdown

@cursor cursor bot left a comment

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.

Remove the SENTRY_CRASH_MANAGED_RUNTIME guard so ignored signals
are properly re-raised regardless of the compile flag. Harmless
for the non-managed case where uninstall already restored them.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-merge Use this label to trigger all PR workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants