Skip to content

fix: prevent redundant native exceptions on iOS#5126

Draft
jpnurmi wants to merge 6 commits intomainfrom
jpnurmi/fix/ios-cocoa-preload
Draft

fix: prevent redundant native exceptions on iOS#5126
jpnurmi wants to merge 6 commits intomainfrom
jpnurmi/fix/ios-cocoa-preload

Conversation

@jpnurmi
Copy link
Copy Markdown
Collaborator

@jpnurmi jpnurmi commented Apr 9, 2026

Note

Depends on sentry-cocoa 9.10.0 (getsentry/sentry-cocoa#6193)

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

🤖 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.99%. Comparing base (39ea4d8) to head (42ca1ff).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5126      +/-   ##
==========================================
- Coverage   74.12%   73.99%   -0.14%     
==========================================
  Files         499      499              
  Lines       18067    18067              
  Branches     3520     3520              
==========================================
- Hits        13392    13368      -24     
- Misses       3813     3839      +26     
+ Partials      862      860       -2     

☔ 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 and others added 2 commits April 11, 2026 10:55
- 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 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.

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.

1 participant