Skip to content

fix: prevent redundant native exceptions on iOS#5126

Draft
jpnurmi wants to merge 1 commit intojpnurmi/build/cocoa-submodulefrom
jpnurmi/fix/ios-cocoa-preload
Draft

fix: prevent redundant native exceptions on iOS#5126
jpnurmi wants to merge 1 commit intojpnurmi/build/cocoa-submodulefrom
jpnurmi/fix/ios-cocoa-preload

Conversation

@jpnurmi
Copy link
Copy Markdown
Collaborator

@jpnurmi jpnurmi commented Apr 9, 2026

Fixes duplicate native exceptions (#3954) on iOS by reordering signal handlers to let .NET/Mono handle managed exceptions first, and then chain real native exceptions to Sentry.

Before:

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

After:

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

Good news: the re-ordered signal handlers are working as expected.

Bad news: The early signal handler installation in getsentry/sentry-cocoa#6193 is guarded behind a SENTRY_CRASH_MANAGED_RUNTIME compile-time flag to avoid affecting normal Cocoa SDK users. iOS integration tests were happily passing while developing with a local modules/sentry-cocoa clone, but I do realize now that this cannot work with the pre-built Sentry.xcframework release bundles built without SENTRY_CRASH_MANAGED_RUNTIME... 🤦

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 9, 2026

Semver Impact of This PR

None (no version bump detected)

📋 Changelog Preview

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


Fixes 🐛

  • fix: prevent redundant native exceptions on iOS by jpnurmi in #5126
  • fix: prevent redundant native exceptions on iOS by jpnurmi in #5126
  • fix: prevent redundant native exceptions on Android/Mono by jpnurmi in #4676
    • Note: opt in by setting options.Native.ExperimentalOptions.SignalHandlerStrategy to Sentry.Android.SignalHandlerStrategy.ChainAtStart

Dependencies ⬆️

Deps

  • chore(deps): update Cocoa SDK to v9.10.0 by github-actions in #5132
  • chore(deps): update Cocoa SDK to v9.9.0 by github-actions in #5115
  • chore(deps): update Java SDK to v8.38.0 by github-actions in #5124
  • chore(deps): update Native SDK to v0.13.6 by github-actions in #5128

🤖 This preview updates automatically when you update the PR.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 9, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 73.98%. Comparing base (94f4583) to head (0febb68).

Additional details and impacted files
@@                      Coverage Diff                       @@
##           jpnurmi/build/cocoa-submodule    #5126   +/-   ##
==============================================================
  Coverage                          73.98%   73.98%           
==============================================================
  Files                                499      499           
  Lines                              18067    18067           
  Branches                            3520     3520           
==============================================================
  Hits                               13366    13366           
  Misses                              3839     3839           
  Partials                             862      862           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jpnurmi jpnurmi changed the title fix(ios): preload Cocoa crash handlers for managed runtime interop fix: prevent redundant native exceptions on iOS Apr 9, 2026
@jpnurmi
Copy link
Copy Markdown
Collaborator Author

jpnurmi commented Apr 9, 2026

Bad news: The early signal handler installation in getsentry/sentry-cocoa#6193 is guarded behind a SENTRY_CRASH_MANAGED_RUNTIME compile-time flag to avoid affecting normal Cocoa SDK users. iOS integration tests were happily passing while developing with a local modules/sentry-cocoa clone, but I do realize now that this cannot work with the pre-built Sentry.xcframework release bundles built without SENTRY_CRASH_MANAGED_RUNTIME... 🤦

@jamescrosswell @Flash0ver Is turning sentry-cocoa into a submodule and building it on the fly a deal-breaker? Maybe we could cache sentry-cocoa builds in the CI similarly to sentry-native? I hate that it slows down local builds, though... 😭

@jpnurmi jpnurmi force-pushed the jpnurmi/fix/ios-cocoa-preload branch from c4f6ee3 to 0fdf17d Compare April 11, 2026 08:56
@jpnurmi
Copy link
Copy Markdown
Collaborator Author

jpnurmi commented Apr 11, 2026

My biggest concern was the impact of replacing the officially released and signed bundle with an unsigned self-built bundle.

TL;DR: dotnet-ios re-signs the nested framework with --force during every app build, overwriting whatever signature the NuGet shipped. This is empirically true for both the released sentry-dotnet with the "signed" Cocoa bundle and a locally-built unsigned one — both produce byte-equivalent signing provenance in the final .app.

Details

Surprising finding about Sentry's release signing

The Apple Distribution: GetSentry LLC (97JCY7859U) signature that sentry-cocoa's release pipeline applies (via scripts/compress-xcframework.sh:18) only covers the outer xcframework wrapper, not the per-platform .framework bundles inside. Proof from the released Sentry.Bindings.Cocoa 6.3.0 on nuget.org:

$ codesign -dv --verbose=4 Sentry-9.7.0.xcframework
Authority=Apple Distribution: GetSentry LLC (97JCY7859U)
Authority=Apple Worldwide Developer Relations Certification Authority
Authority=Apple Root CA
TeamIdentifier=97JCY7859U
Sealed Resources version=2 rules=10 files=789

$ codesign -dv --verbose=4 Sentry-9.7.0.xcframework/ios-arm64/Sentry.framework
.../ios-arm64/Sentry.framework: code object is not signed at all

The GetSentry LLC signature lives on a bundle dotnet-ios never touches — it only ever reaches for the inner per-platform framework. For .NET iOS consumers, the outer signature is vestigial.

dotnet-ios re-signs with --force, unconditionally

From the xamarin-macios source (Codesign.cs:297-298):

args.Add ("-v");
args.Add ("--force");

ComputeCodesignItems.cs:315-318 walks $(AppBundleDir)/Frameworks/*.framework and queues every nested framework for signing; the signing identity inherits from the enclosing app bundle (detected developer cert on device, - ad-hoc on simulator).

Verified empirically — minimal dotnet new ios project with <PackageReference Include="Sentry" Version="6.3.0" />, simulator build, -bl:/tmp/released.binlog:

$ dotnet msbuild /tmp/released.binlog -t:__dummy__ -v:n 2>&1 \
  | grep "codesign execution started" | grep "Sentry.framework"
Tool /usr/bin/codesign execution started with arguments: \
-v --force --timestamp=none --sign - .../ReleasedTest.app/Frameworks/Sentry.framework

Before/after the re-sign:

=== simulator slice as shipped inside the nupkg ===
CDHash=ea0393046fef43a760798f6f8f378d7d9eeaf5b7
Signature=adhoc
Sealed Resources version=2 rules=10 files=71

=== same framework, after dotnet build, in .app/Frameworks/ ===
CDHash=7253e238f521373a2c2486ae44325b283bd2b642   # different
Signature=adhoc
Sealed Resources version=2 rules=10 files=1       # regenerated from scratch
$ codesign --verify --strict --verbose=4 .../Sentry.framework
.../Sentry.framework: valid on disk
.../Sentry.framework: satisfies its Designated Requirement

Identical behavior for a locally-built, fully unsigned Cocoa bundle

Same setup, but with a branch that builds sentry-cocoa from source and ships the xcframework without any codesigning. Baseline in the local nupkg:

$ codesign -dv --verbose=4 .../Sentry.xcframework/ios-arm64/Sentry.framework
.../ios-arm64/Sentry.framework: code object is not signed at all

dotnet build binlog:

Tool /usr/bin/codesign execution started with arguments: \
-v --force --timestamp=none --sign - .../LocalTest.app/Frameworks/Sentry.framework

Exact same command, same --force, same ad-hoc output. codesign --verify --strict passes.

On a real device build, it's the app developer's identity, not Sentry's

Running the same invocation dotnet-ios would run on a device build, with a free personal-team Apple Development cert:

$ codesign -v --force --timestamp=none --sign "Apple Development: <Name> (<TeamID>)" Sentry.framework
Sentry.framework: replacing existing signature
Sentry.framework: signed bundle with Mach-O thin (arm64) [io.sentry.Sentry]

$ codesign -dv --verbose=4 Sentry.framework
Authority=Apple Development: <Name> (<TeamID>)
Authority=Apple Worldwide Developer Relations Certification Authority
Authority=Apple Root CA
TeamIdentifier=<TeamID>

The Sentry.framework that reaches Apple's submission validator carries the app developer's signature, not Sentry's. Apple's "commonly used SDKs" signature check is satisfied by that signature regardless of what state the NuGet shipped the framework in.

Implications

  1. Shipping an unsigned Sentry.framework in the NuGet is not a regression. The signing state of the framework inside the NuGet has no observable effect on what ends up in the consumer's .app.
  2. No new signing infrastructure needed on the sentry-dotnet side — no fastlane match, no rcodesign, no new Apple cert, no new secrets, no changes to xcodebuild invocations. The framework's signing state coming out of the local build is already fine as-is.
  3. Any mental model that included "Sentry ships a signed framework whose Apple Distribution signature matters for end-user App Store submission" was wrong. iOS .NET / MAUI consumers of Sentry have been shipping ad-hoc-signed-then-developer-re-signed frameworks inside their App Store apps all along. Nothing here changes.

jpnurmi added a commit that referenced this pull request Apr 13, 2026
Allows building sentry-cocoa with `SENTRY_CRASH_MANAGED_RUNTIME` for
eliminating duplicate native exceptions on iOS.

See also:
- getsentry/sentry-cocoa#6193
- #5126
@jpnurmi jpnurmi changed the base branch from main to jpnurmi/build/cocoa-submodule April 13, 2026 07:20
@jpnurmi jpnurmi force-pushed the jpnurmi/fix/ios-cocoa-preload branch from 42ca1ff to 1b3eee3 Compare April 13, 2026 07:22
@jpnurmi
Copy link
Copy Markdown
Collaborator Author

jpnurmi commented Apr 13, 2026

Problem: On iOS, we needed Sentry Cocoa to install signal handlers before the .NET runtime. We couldn't call any API from .NET because it was too late. The .NET runtime was already up and running and had installed its signal handlers.

Fix: The solution was to pre-install signal handlers very early at Sentry Cocoa library load time. No full SDK init, just pre-install the signal handlers, and make them operate in a special passthrough mode until the SDK is fully initialized. The problem was that we did not want this to affect normal SDK users, so I guarded the behavior behind a compile-time flag to make it an easier sell.

Deploy: To be able to ship Sentry.NET + Sentry Cocoa with SENTRY_CRASH_MANAGED_RUNTIME=1, we have two options:

  1. Switch back to the sentry-cocoa submodule and build it on the fly:
    build: switch to sentry-cocoa submodule for managed builds #5131
  2. Ask the Sentry Cocoa team to host a prebuilt "Managed" variant alongside the other (Dynamic, WithARM64e, WithoutUIKitOrAppKit etc.) variants they already have (thanks @Flash0ver for the idea!)
    https://github.com/getsentry/sentry-cocoa/commits/jpnurmi/build/managed/ (untested)

Note: It's also a viable option to go with a submodule for the short term, and switch back to prebuilt releases as soon as an official Managed variant is available.

@jamescrosswell
Copy link
Copy Markdown
Collaborator

Deploy: To be able to ship Sentry.NET + Sentry Cocoa with SENTRY_CRASH_MANAGED_RUNTIME=1, we have two options:

  1. Switch back to the sentry-cocoa submodule and build it on the fly:
    build: switch to sentry-cocoa submodule for managed builds #5131
  2. Ask the Sentry Cocoa team to host a prebuilt "Managed" variant alongside the other (Dynamic, WithARM64e, WithoutUIKitOrAppKit etc.) variants they already have (thanks @Flash0ver for the idea!)
    https://github.com/getsentry/sentry-cocoa/commits/jpnurmi/build/managed/ (untested)

I think I'm not understanding the sequence of events that leads to sentry-cocoa being loaded. I thought no code from sentry-cocoa would be executed (in the context of a c# application at least) until we call SentrySdk.InitSentryCocoaSdk... in which case maybe it is possible to pass some ambient context in somehow so the code that does the signal handler preloading could be executed conditionally at runtime.

e.g. we could do this when initialising the .NET SDK (before calling InitSentryCocoaSdk):

Environment.SetEnvironmentVariable("SENTRY_MANAGED_RUNTIME", "true", EnvironmentVariableTarget.Process);

And then code like this could check for the existence of that environment variable rather than using a compile time pragma.

But all of that is probably unnecessary - we could just pass something in via the options if the load sequence was as I (presumably falsely) assumed above.

How does the signal handler registration happen for sentry-cocoa when it's being used from a hybrid SDK then? How does your code pre register the signal handlers before we call InitSentryCocoaSdk from the .NET SDK?

@jpnurmi
Copy link
Copy Markdown
Collaborator Author

jpnurmi commented Apr 14, 2026

How does the signal handler registration happen for sentry-cocoa when it's being used from a hybrid SDK then? How does your code pre register the signal handlers before we call InitSentryCocoaSdk from the .NET SDK?

The .NET/Mono runtime is initialized from the native main(). At that point, Mono saves potential previous signal handlers for chaining. In other words, Sentry's signal handlers must already be installed. (From the kernel's perspective, there's only one handler per signal. "Chaining" of signal handlers is a concept built into Mono and Sentry.

Since Sentry's signal handlers must be in place before the .NET runtime initializes, calling any API, specifying environment variables, or anything alike from within .NET itself is too late, because the .NET runtime is already running. We use __attribute__((constructor)) to "preload" Sentry's signal handlers when the library is loaded into memory, before the native main() and the whole .NET runtime initialization:
https://github.com/getsentry/sentry-cocoa/blob/27df7b7938a6e2fc50ad08d729f5a031342fa097/Sources/SentryCrash/Recording/SentryCrashC.c#L72-L79

jpnurmi added a commit that referenced this pull request Apr 14, 2026
Allows building sentry-cocoa with `SENTRY_CRASH_MANAGED_RUNTIME` for
eliminating duplicate native exceptions on iOS.

See also:
- getsentry/sentry-cocoa#6193
- #5126
@jpnurmi jpnurmi force-pushed the jpnurmi/build/cocoa-submodule branch from 4071bee to acf4165 Compare April 14, 2026 06:52
@jpnurmi jpnurmi force-pushed the jpnurmi/fix/ios-cocoa-preload branch from 1b3eee3 to dc98de1 Compare April 14, 2026 07:57
@jamescrosswell
Copy link
Copy Markdown
Collaborator

Since Sentry's signal handlers must be in place before the .NET runtime initializes, calling any API, specifying environment variables, or anything alike from within .NET itself is too late

I see... dang.

In that case, we don't have too many choices. We either build sentry-cocoa ourselves or wait until a special build for .NET can be made available.

How much time are we talking about adding (presumably at least every time we start a new branch locally - at least in my case - I'm using git work trees)?

@jpnurmi
Copy link
Copy Markdown
Collaborator Author

jpnurmi commented Apr 14, 2026

Depends on network vs. CPU speed, but building Sentry.Bindings.Cocoa alone locally on my M1 goes up from 1 min to 1 min 30 s. Not as bad as I thought. Maybe it got better now that multiple parallel build-sentry-cocoa.sh invocations result in only one build (#5131 (comment)).

Prebuilt release (current main)

$ git clean -xdf && git submodule foreach git clean -xdf && dotnet build-server shutdown
Entering 'modules/Ben.Demystifier'
Entering 'modules/perfview'
Entering 'modules/sentry-native'
Shutting down MSBuild server...
Shutting down VB/C# compiler server...
VB/C# compiler server shut down successfully.
MSBuild server shut down successfully.
$ dotnet build -c Release src/Sentry.Bindings.Cocoa/Sentry.Bindings.Cocoa.csproj | cat
  Determining projects to restore...
  Restored /Users/jpnurmi/Projects/sentry/sentry-dotnet/src/Sentry.Bindings.Cocoa/Sentry.Bindings.Cocoa.csproj (in 1.96 sec).
  Setting up the Cocoa SDK version '9.9.0'.
    % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                   Dload  Upload   Total   Spent    Left  Speed

    0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
    0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0

    3  126M    3 4510k    0     0  7181k      0  0:00:18 --:--:--  0:00:18 7181k
   12  126M   12 16.2M    0     0   9.9M      0  0:00:12  0:00:01  0:00:11 11.8M
   19  126M   19 25.0M    0     0  9757k      0  0:00:13  0:00:02  0:00:11 10.3M
   28  126M   28 36.0M    0     0   9.9M      0  0:00:12  0:00:03  0:00:09 10.5M
   36  126M   36 46.5M    0     0  10.0M      0  0:00:12  0:00:04  0:00:08 10.5M
   47  126M   47 59.7M    0     0  10.6M      0  0:00:11  0:00:05  0:00:06 11.0M
   56  126M   56 71.4M    0     0  10.7M      0  0:00:11  0:00:06  0:00:05 11.0M
   67  126M   67 85.7M    0     0  11.2M      0  0:00:11  0:00:07  0:00:04 12.1M
   75  126M   75 95.2M    0     0  11.0M      0  0:00:11  0:00:08  0:00:03 11.8M
   83  126M   83  106M    0     0  11.0M      0  0:00:11  0:00:09  0:00:02 11.8M
   91  126M   91  116M    0     0  10.9M      0  0:00:11  0:00:10  0:00:01 11.3M
  100  126M  100  126M    0     0  10.9M      0  0:00:11  0:00:11 --:--:-- 11.1M
  Archive:  ../../modules/sentry-cocoa/Sentry-9.9.0.xcframework.zip
[...]
  Sentry.Bindings.Cocoa -> /Users/jpnurmi/Projects/sentry/sentry-dotnet/src/Sentry.Bindings.Cocoa/bin/Release/net9.0-ios18.0/Sentry.Bindings.Cocoa.dll
  Sentry.Bindings.Cocoa -> /Users/jpnurmi/Projects/sentry/sentry-dotnet/src/Sentry.Bindings.Cocoa/bin/Release/net9.0-maccatalyst18.0/Sentry.Bindings.Cocoa.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:51.85

Submodule (jpnurmi/build/cocoa-submodule)

$ git clean -xdf && git submodule foreach git clean -xdf && dotnet build-server shutdown
Entering 'modules/Ben.Demystifier'
Entering 'modules/perfview'
Entering 'modules/sentry-cocoa' # <===
Entering 'modules/sentry-native'
Shutting down MSBuild server...
Shutting down VB/C# compiler server...
VB/C# compiler server shut down successfully.
MSBuild server shut down successfully.
$ dotnet build -c Release src/Sentry.Bindings.Cocoa/Sentry.Bindings.Cocoa.csproj | cat
  Determining projects to restore...
  Restored /Users/jpnurmi/Projects/sentry/sentry-dotnet/src/Sentry.Bindings.Cocoa/Sentry.Bindings.Cocoa.csproj (in 114 ms).
  Building the Cocoa SDK from source.
  ::group::Building sentry-cocoa for iOS and iOS simulator
  Command line invocation:
      /Applications/Xcode_26.2.app/Contents/Developer/usr/bin/xcodebuild archive -project Sentry.xcodeproj -scheme Sentry -configuration Release -sdk iphoneos26.2 -archivePath ./Carthage/output-ios.xcarchive SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES
[...]
  Sentry.Bindings.Cocoa -> /Users/jpnurmi/Projects/sentry/sentry-dotnet/src/Sentry.Bindings.Cocoa/bin/Release/net9.0-ios18.0/Sentry.Bindings.Cocoa.dll
  Sentry.Bindings.Cocoa -> /Users/jpnurmi/Projects/sentry/sentry-dotnet/src/Sentry.Bindings.Cocoa/bin/Release/net9.0-maccatalyst18.0/Sentry.Bindings.Cocoa.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:01:28.44

@jamescrosswell
Copy link
Copy Markdown
Collaborator

Doesn't look like there's any way around it. I guess on the up side, we no longer have to download the binaries... so there's that. 🌤️

@jpnurmi
Copy link
Copy Markdown
Collaborator Author

jpnurmi commented Apr 14, 2026

Hmm, right... my comparison was actually not quite fair. The overhead of downloading moves to the init phase of your worktree, because you need to clone the sentry-cocoa submodule too... Gladly, sentry-cocoa initializes faster than sentry-native and doesn't have any submodules of its own, but it still easily adds an extra 15-30s per worktree init.

- Build sentry-cocoa with SENTRY_CRASH_MANAGED_RUNTIME to preload signal
  handlers and exclude EXC_BAD_ACCESS/EXC_ARITHMETIC from Mach monitoring
- Call PrivateSentrySDKOnly.IgnoreNextSignal(SIGABRT) from
  MarshalManagedException to prevent duplicate native crash events
- Update iOS integration tests to expect no duplicate events (#3954)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jpnurmi jpnurmi force-pushed the jpnurmi/fix/ios-cocoa-preload branch from dc98de1 to 0febb68 Compare April 14, 2026 17:30
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.

2 participants