diff --git a/integration-test/ios.Tests.ps1 b/integration-test/ios.Tests.ps1 index 3c38696f69..92097c5b44 100644 --- a/integration-test/ios.Tests.ps1 +++ b/integration-test/ios.Tests.ps1 @@ -11,12 +11,14 @@ BeforeDiscovery { $script:simulator = Get-IosSimulatorUdid -PreferredStates @('Booted') } -Describe 'iOS app (, )' -ForEach @( +Describe 'iOS app (, , )' -ForEach @( # Note: we can't run against net10 and net9 becaus .NET 10 requires Xcode 26.2 and .NET 9 requires Xcode 26.0. # The macOS GitHub Actions runners only have Xcode 26.1+ installed and no support for Xcode 26.2 is planned for # net9.0-ios: https://github.com/dotnet/macios/issues/24199#issuecomment-3819021247 - @{ tfm = "net10.0-ios26.2"; configuration = "Release" } - @{ tfm = "net10.0-ios26.2"; configuration = "Debug" } + # + # TODO: add coreclr when available + @{ tfm = "net10.0-ios26.2"; configuration = "Release"; runtime = "mono" } + @{ tfm = "net10.0-ios26.2"; configuration = "Debug"; runtime = "mono" } ) -Skip:(-not $script:simulator) { BeforeAll { . $PSScriptRoot/../scripts/device-test-utils.ps1 @@ -29,10 +31,12 @@ Describe 'iOS app (, )' -ForEach @( $rid = "iossimulator-$arch" Write-Host "::group::Build Sentry.Maui.Device.IntegrationTestApp.csproj" + $useMonoRuntime = if ($runtime -eq 'mono') { 'true' } else { 'false' } dotnet build Sentry.Maui.Device.IntegrationTestApp.csproj ` --configuration $configuration ` --framework $tfm ` - --runtime $rid + --runtime $rid ` + -p:UseMonoRuntime=$useMonoRuntime | ForEach-Object { Write-Host $_ } Write-Host '::endgroup::' $LASTEXITCODE | Should -Be 0 @@ -90,7 +94,7 @@ Describe 'iOS app (, )' -ForEach @( UninstallIosApp } - It 'captures managed crash ()' { + It 'captures managed crash (, )' { $result = Invoke-SentryServer { param([string]$url) RunIosApp -Dsn $url -TestArg "Managed" @@ -99,12 +103,11 @@ Describe 'iOS app (, )' -ForEach @( $result.HasErrors() | Should -BeFalse $result.Envelopes() | Should -AnyElementMatch "`"type`":`"System.ApplicationException`"" - # TODO: fix redundant SIGABRT (#3954) - { $result.Envelopes() | Should -Not -AnyElementMatch "`"type`":`"SIGABRT`"" } | Should -Throw - { $result.Envelopes() | Should -HaveCount 1 } | Should -Throw + $result.Envelopes() | Should -Not -AnyElementMatch "`"type`":`"(EXC_[A-Z_]+|SIG[A-Z]+)`"" + $result.Envelopes() | Should -HaveCount 1 } - It 'captures native crash ()' { + It 'captures native crash (, )' { $result = Invoke-SentryServer { param([string]$url) RunIosApp -Dsn $url -TestArg "Native" @@ -112,12 +115,12 @@ Describe 'iOS app (, )' -ForEach @( } $result.HasErrors() | Should -BeFalse - $result.Envelopes() | Should -AnyElementMatch "`"type`":`"EXC_[A-Z_]+`"" + $result.Envelopes() | Should -AnyElementMatch "`"type`":`"(EXC_[A-Z_]+|SIG[A-Z]+)`"" $result.Envelopes() | Should -Not -AnyElementMatch "`"type`":`"System.\w+Exception`"" $result.Envelopes() | Should -HaveCount 1 } - It 'captures null reference exception ()' { + It 'captures null reference exception (, )' { $result = Invoke-SentryServer { param([string]$url) RunIosApp -Dsn $url -TestArg "NullReferenceException" @@ -126,12 +129,7 @@ Describe 'iOS app (, )' -ForEach @( $result.HasErrors() | Should -BeFalse $result.Envelopes() | Should -AnyElementMatch "`"type`":`"System.NullReferenceException`"" - # TODO: fix redundant EXC_BAD_ACCESS in Release (#3954) - if ($configuration -eq 'Release') { - { $result.Envelopes() | Should -Not -AnyElementMatch "`"type`":`"EXC_BAD_ACCESS`"" } | Should -Throw - } else { - $result.Envelopes() | Should -Not -AnyElementMatch "`"type`":`"EXC_BAD_ACCESS`"" - $result.Envelopes() | Should -HaveCount 1 - } + $result.Envelopes() | Should -Not -AnyElementMatch "`"type`":`"(EXC_[A-Z_]+|SIG[A-Z]+)`"" + $result.Envelopes() | Should -HaveCount 1 } } diff --git a/scripts/build-sentry-cocoa.sh b/scripts/build-sentry-cocoa.sh index 6e18143f1a..f3ef2b7558 100755 --- a/scripts/build-sentry-cocoa.sh +++ b/scripts/build-sentry-cocoa.sh @@ -50,7 +50,8 @@ xcodebuild archive -project Sentry.xcodeproj \ -sdk "$ios_sdk" \ -archivePath ./Carthage/output-ios.xcarchive \ SKIP_INSTALL=NO \ - BUILD_LIBRARY_FOR_DISTRIBUTION=YES + BUILD_LIBRARY_FOR_DISTRIBUTION=YES \ + GCC_PREPROCESSOR_DEFINITIONS='$(inherited) SENTRY_CRASH_MANAGED_RUNTIME=1' ./scripts/remove-architectures.sh ./Carthage/output-ios.xcarchive arm64e xcodebuild archive -project Sentry.xcodeproj \ -scheme Sentry \ @@ -58,7 +59,8 @@ xcodebuild archive -project Sentry.xcodeproj \ -sdk "$ios_simulator_sdk" \ -archivePath ./Carthage/output-iossimulator.xcarchive \ SKIP_INSTALL=NO \ - BUILD_LIBRARY_FOR_DISTRIBUTION=YES + BUILD_LIBRARY_FOR_DISTRIBUTION=YES \ + GCC_PREPROCESSOR_DEFINITIONS='$(inherited) SENTRY_CRASH_MANAGED_RUNTIME=1' xcodebuild -create-xcframework \ -framework ./Carthage/output-ios.xcarchive/Products/Library/Frameworks/Sentry.framework \ -framework ./Carthage/output-iossimulator.xcarchive/Products/Library/Frameworks/Sentry.framework \ @@ -73,7 +75,8 @@ xcodebuild archive -project Sentry.xcodeproj \ -destination 'generic/platform=macOS,variant=Mac Catalyst' \ -archivePath ./Carthage/output-maccatalyst.xcarchive \ SKIP_INSTALL=NO \ - BUILD_LIBRARY_FOR_DISTRIBUTION=YES + BUILD_LIBRARY_FOR_DISTRIBUTION=YES \ + GCC_PREPROCESSOR_DEFINITIONS='$(inherited) SENTRY_CRASH_MANAGED_RUNTIME=1' ./scripts/remove-architectures.sh ./Carthage/output-maccatalyst.xcarchive arm64e xcodebuild -create-xcframework \ -framework ./Carthage/output-maccatalyst.xcarchive/Products/Library/Frameworks/Sentry.framework \ diff --git a/src/Sentry/Platforms/Cocoa/RuntimeAdapter.cs b/src/Sentry/Platforms/Cocoa/RuntimeAdapter.cs index 9bc7551cd4..21e9200201 100644 --- a/src/Sentry/Platforms/Cocoa/RuntimeAdapter.cs +++ b/src/Sentry/Platforms/Cocoa/RuntimeAdapter.cs @@ -6,6 +6,8 @@ internal interface IRuntime { internal event MarshalManagedExceptionHandler MarshalManagedException; internal event MarshalObjectiveCExceptionHandler MarshalObjectiveCException; + internal bool IsMono { get; } + internal void IgnoreNextSignal(int signal); } internal sealed class RuntimeAdapter : IRuntime @@ -21,6 +23,10 @@ private RuntimeAdapter() public event MarshalManagedExceptionHandler? MarshalManagedException; public event MarshalObjectiveCExceptionHandler? MarshalObjectiveCException; + public bool IsMono { get; } = Type.GetType("Mono.Runtime") != null; + + public void IgnoreNextSignal(int signal) => SentryCocoaHybridSdk.IgnoreNextSignal(signal); + [SecurityCritical] private void OnMarshalManagedException(object sender, MarshalManagedExceptionEventArgs e) => MarshalManagedException?.Invoke(this, e); diff --git a/src/Sentry/Platforms/Cocoa/RuntimeMarshalManagedExceptionIntegration.cs b/src/Sentry/Platforms/Cocoa/RuntimeMarshalManagedExceptionIntegration.cs index 396221cb79..df1444970b 100644 --- a/src/Sentry/Platforms/Cocoa/RuntimeMarshalManagedExceptionIntegration.cs +++ b/src/Sentry/Platforms/Cocoa/RuntimeMarshalManagedExceptionIntegration.cs @@ -43,6 +43,24 @@ internal void Handle(object sender, MarshalManagedExceptionEventArgs e) // This is likely a terminal exception so try to send the crash report before shutting down _hub?.Flush(); + + // Under Mono, Disable/UnwindNativeCode call mono_raise_exception which can unwind into + // a managed catch and never hit abort(). The thread-local ignore flag would then linger + // and silently swallow an unrelated later SIGABRT on this thread, so skip arming it. See + // https://github.com/dotnet/macios/blob/be8a2ca1057242f745ef58011a02ffe21326d180/runtime/runtime.m#L2215 + if (_runtime.IsMono && e.ExceptionMode is MarshalManagedExceptionMode.Disable + or MarshalManagedExceptionMode.UnwindNativeCode) + { + return; + } + + // Otherwise the runtime will call abort() after we return — directly via + // xamarin_assertion_message, or indirectly via the uncaught-NSException handler for + // ThrowObjectiveCException. Tell SentryCrash to ignore that SIGABRT so we don't emit a + // duplicate native crash for an exception we've already captured. See + // https://github.com/dotnet/macios/blob/be8a2ca1057242f745ef58011a02ffe21326d180/runtime/runtime.m#L2285 + const int SIGABRT = 6; + _runtime.IgnoreNextSignal(SIGABRT); } } } diff --git a/test/Sentry.Tests/Platforms/iOS/RuntimeMarshalManagedExceptionIntegrationTests.cs b/test/Sentry.Tests/Platforms/iOS/RuntimeMarshalManagedExceptionIntegrationTests.cs index 5ba83b0c28..b0c12feaa0 100644 --- a/test/Sentry.Tests/Platforms/iOS/RuntimeMarshalManagedExceptionIntegrationTests.cs +++ b/test/Sentry.Tests/Platforms/iOS/RuntimeMarshalManagedExceptionIntegrationTests.cs @@ -66,5 +66,62 @@ public void Register_UnhandledException_Subscribes() _fixture.Runtime.Received().MarshalManagedException += sut.Handle; } + + [Theory] + [InlineData(MarshalManagedExceptionMode.Default)] + [InlineData(MarshalManagedExceptionMode.ThrowObjectiveCException)] + [InlineData(MarshalManagedExceptionMode.Abort)] + public void Handle_Mono_AbortingMode_IgnoresSigabrt(MarshalManagedExceptionMode mode) + { + _fixture.Runtime.IsMono.Returns(true); + var sut = _fixture.GetSut(); + sut.Register(_fixture.Hub, SentryOptions); + + sut.Handle(this, new MarshalManagedExceptionEventArgs { Exception = new Exception(), ExceptionMode = mode }); + + _fixture.Runtime.Received(1).IgnoreNextSignal(6); + } + + [Theory] + [InlineData(MarshalManagedExceptionMode.Disable)] + [InlineData(MarshalManagedExceptionMode.UnwindNativeCode)] + public void Handle_Mono_NonAbortingMode_DoesNotIgnoreSigabrt(MarshalManagedExceptionMode mode) + { + _fixture.Runtime.IsMono.Returns(true); + var sut = _fixture.GetSut(); + sut.Register(_fixture.Hub, SentryOptions); + + sut.Handle(this, new MarshalManagedExceptionEventArgs { Exception = new Exception(), ExceptionMode = mode }); + + _fixture.Runtime.DidNotReceive().IgnoreNextSignal(Arg.Any()); + } + + [Theory] + [InlineData(MarshalManagedExceptionMode.Default)] + [InlineData(MarshalManagedExceptionMode.Disable)] + [InlineData(MarshalManagedExceptionMode.UnwindNativeCode)] + [InlineData(MarshalManagedExceptionMode.ThrowObjectiveCException)] + [InlineData(MarshalManagedExceptionMode.Abort)] + public void Handle_CoreCLR_AnyMode_IgnoresSigabrt(MarshalManagedExceptionMode mode) + { + _fixture.Runtime.IsMono.Returns(false); + var sut = _fixture.GetSut(); + sut.Register(_fixture.Hub, SentryOptions); + + sut.Handle(this, new MarshalManagedExceptionEventArgs { Exception = new Exception(), ExceptionMode = mode }); + + _fixture.Runtime.Received(1).IgnoreNextSignal(6); + } + + [Fact] + public void Handle_NoException_DoesNotIgnoreSigabrt() + { + var sut = _fixture.GetSut(); + sut.Register(_fixture.Hub, SentryOptions); + + sut.Handle(this, new MarshalManagedExceptionEventArgs()); + + _fixture.Runtime.DidNotReceive().IgnoreNextSignal(Arg.Any()); + } } #endif