From c8cf715ce40570fff82a7c5b732828e759f74893 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 21 Jan 2025 21:40:22 +1300 Subject: [PATCH 01/15] Prevent NDK from capturing EXC_BAD_ACCESS signal for managed NullReferenceExceptions --- src/Sentry/Platforms/Cocoa/SentrySdk.cs | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/Sentry/Platforms/Cocoa/SentrySdk.cs b/src/Sentry/Platforms/Cocoa/SentrySdk.cs index c7b438f12d..ebb84764ab 100644 --- a/src/Sentry/Platforms/Cocoa/SentrySdk.cs +++ b/src/Sentry/Platforms/Cocoa/SentrySdk.cs @@ -154,20 +154,35 @@ private static void InitSentryCocoaSdk(SentryOptions options) // The managed exception is what a .NET developer would expect, and it is sent by the Sentry.NET SDK // But we also get a native SIGABRT since it crashed the application, which is sent by the Sentry Cocoa SDK. // This is partially due to our setting ObjCRuntime.MarshalManagedExceptionMode.UnwindNativeCode above. - // Thankfully, we can see Xamarin's unhandled exception handler on the stack trace, so we can filter them out. - // Here is the function that calls abort(), which we will use as a filter: - // https://github.com/xamarin/xamarin-macios/blob/c55fbdfef95028ba03d0f7a35aebca03bd76f852/runtime/runtime.m#L1114-L1122 nativeOptions.BeforeSend = evt => { + options.LogDebug("***** Intercepted event *****"); + // There should only be one exception on the event in this case if (evt.Exceptions?.Length == 1) { // It will match the following characteristics var ex = evt.Exceptions[0]; - if (ex.Type == "SIGABRT" && ex.Value == "Signal 6, Code 0" && + + // Thankfully, sometimes we can see Xamarin's unhandled exception handler on the stack trace, so we can filter + // them out. Here is the function that calls abort(), which we will use as a filter: + // https://github.com/xamarin/xamarin-macios/blob/c55fbdfef95028ba03d0f7a35aebca03bd76f852/runtime/runtime.m#L1114-L1122 + if (ex.Type == "EXC_BAD_ACCESS" && ex.Value == "Signal 6, Code 0" && ex.Stacktrace?.Frames.Any(f => f.Function == "xamarin_unhandled_exception_handler") is true) { - // Don't sent it + // Don't send it + return null!; + } + + // In the case of NullReferenceExceptions, Xamarin's unhandled exception handler doesn't seem to catch + // it... so we filter these explicitly irrespective of the stack trace. This is possibly a bit dangerous + // as we could be filtering exceptions from native code blocks that don't get captured by our managed + // SDK. We don't have any easy way to know whether the exception is managed code (compiled to native) + // or bona fide native code though. + // See: https://github.com/getsentry/sentry-dotnet/issues/3776 + if (ex.Type == "SIGABRT" && ex.Value.Contains("Attempted to dereference null pointer.")) + { + // Don't send it return null!; } } From 90d41c3fd5ee1fdb3edb93bb72037de2fd576941 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 21 Jan 2025 21:48:48 +1300 Subject: [PATCH 02/15] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf2cf291e5..63e31e59e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ - .NET on iOS: Add experimental EnableAppHangTrackingV2 configuration flag to the options binding SDK ([#3877](https://github.com/getsentry/sentry-dotnet/pull/3877)) - Added `SentryOptions.DisableSentryHttpMessageHandler`. Useful if you're using `OpenTelemetry.Instrumentation.Http` and ending up with duplicate spans. ([#3879](https://github.com/getsentry/sentry-dotnet/pull/3879)) +### Fixes + +- Prevent Native EXC_BAD_ACCESS signal errors from being captured when managed NullRefrenceExceptions occur ([#3909](https://github.com/getsentry/sentry-dotnet/pull/3909)) + ### Dependencies - Bump Native SDK from v0.7.17 to v0.7.18 ([#3891](https://github.com/getsentry/sentry-dotnet/pull/3891)) From 37900d7e2aceaee4b034a3ea0251b36e3d45a870 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 29 Jan 2025 10:07:26 +1300 Subject: [PATCH 03/15] Update SentrySdk.cs --- src/Sentry/Platforms/Cocoa/SentrySdk.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sentry/Platforms/Cocoa/SentrySdk.cs b/src/Sentry/Platforms/Cocoa/SentrySdk.cs index ebb84764ab..f1ffd59dbc 100644 --- a/src/Sentry/Platforms/Cocoa/SentrySdk.cs +++ b/src/Sentry/Platforms/Cocoa/SentrySdk.cs @@ -167,7 +167,7 @@ private static void InitSentryCocoaSdk(SentryOptions options) // Thankfully, sometimes we can see Xamarin's unhandled exception handler on the stack trace, so we can filter // them out. Here is the function that calls abort(), which we will use as a filter: // https://github.com/xamarin/xamarin-macios/blob/c55fbdfef95028ba03d0f7a35aebca03bd76f852/runtime/runtime.m#L1114-L1122 - if (ex.Type == "EXC_BAD_ACCESS" && ex.Value == "Signal 6, Code 0" && + if (ex.Type == "SIGABRT" && ex.Value == "Signal 6, Code 0" && ex.Stacktrace?.Frames.Any(f => f.Function == "xamarin_unhandled_exception_handler") is true) { // Don't send it From 892719fd4171a49f88ed61a3ca3cb1f62e5644cf Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 29 Jan 2025 22:37:17 +1300 Subject: [PATCH 04/15] Update SentrySdk.cs --- src/Sentry/Platforms/Cocoa/SentrySdk.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Sentry/Platforms/Cocoa/SentrySdk.cs b/src/Sentry/Platforms/Cocoa/SentrySdk.cs index f1ffd59dbc..8f8c8e1bb6 100644 --- a/src/Sentry/Platforms/Cocoa/SentrySdk.cs +++ b/src/Sentry/Platforms/Cocoa/SentrySdk.cs @@ -174,13 +174,10 @@ private static void InitSentryCocoaSdk(SentryOptions options) return null!; } - // In the case of NullReferenceExceptions, Xamarin's unhandled exception handler doesn't seem to catch - // it... so we filter these explicitly irrespective of the stack trace. This is possibly a bit dangerous - // as we could be filtering exceptions from native code blocks that don't get captured by our managed - // SDK. We don't have any easy way to know whether the exception is managed code (compiled to native) - // or bona fide native code though. + // Similar workaround for NullReferenceExceptions. We don't have any easy way to know whether the + // exception is managed code (compiled to native) or original native code though. // See: https://github.com/getsentry/sentry-dotnet/issues/3776 - if (ex.Type == "SIGABRT" && ex.Value.Contains("Attempted to dereference null pointer.")) + if (ex.Type == "EXC_BAD_ACCESS") { // Don't send it return null!; From 2b35173f43c5f3c73e07d3709d05dc325971bf42 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 29 Jan 2025 22:37:47 +1300 Subject: [PATCH 05/15] Update SentrySdk.cs --- src/Sentry/Platforms/Cocoa/SentrySdk.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Sentry/Platforms/Cocoa/SentrySdk.cs b/src/Sentry/Platforms/Cocoa/SentrySdk.cs index 8f8c8e1bb6..4b303c0749 100644 --- a/src/Sentry/Platforms/Cocoa/SentrySdk.cs +++ b/src/Sentry/Platforms/Cocoa/SentrySdk.cs @@ -156,8 +156,6 @@ private static void InitSentryCocoaSdk(SentryOptions options) // This is partially due to our setting ObjCRuntime.MarshalManagedExceptionMode.UnwindNativeCode above. nativeOptions.BeforeSend = evt => { - options.LogDebug("***** Intercepted event *****"); - // There should only be one exception on the event in this case if (evt.Exceptions?.Length == 1) { From b8672d23154c461e1e5218776bc7a8325e8eb185 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 30 Jan 2025 12:03:19 +1300 Subject: [PATCH 06/15] Bump sample to net9.0 --- samples/Sentry.Samples.Ios/Info.plist | 76 +++++++++---------- .../Sentry.Samples.Ios.csproj | 6 +- 2 files changed, 42 insertions(+), 40 deletions(-) diff --git a/samples/Sentry.Samples.Ios/Info.plist b/samples/Sentry.Samples.Ios/Info.plist index fb0738075a..ffefa6aa7c 100644 --- a/samples/Sentry.Samples.Ios/Info.plist +++ b/samples/Sentry.Samples.Ios/Info.plist @@ -2,43 +2,43 @@ - CFBundleDisplayName - Sentry.Samples.Ios - CFBundleIdentifier - io.sentry.dotnet.samples.ios - CFBundleShortVersionString - 1.0 - MinimumOSVersion - 11.0 - CFBundleVersion - 1.0 - LSRequiresIPhoneOS - - UIDeviceFamily - - 1 - 2 - - UILaunchStoryboardName - LaunchScreen - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - XSAppIconAssets - Assets.xcassets/AppIcon.appiconset + CFBundleDisplayName + Sentry.Samples.Ios + CFBundleIdentifier + io.sentry.dotnet.samples.ios + CFBundleShortVersionString + 1.0 + MinimumOSVersion + 12.2 + CFBundleVersion + 1.0 + LSRequiresIPhoneOS + + UIDeviceFamily + + 1 + 2 + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/AppIcon.appiconset diff --git a/samples/Sentry.Samples.Ios/Sentry.Samples.Ios.csproj b/samples/Sentry.Samples.Ios/Sentry.Samples.Ios.csproj index 9ecfce8114..11372b2e40 100644 --- a/samples/Sentry.Samples.Ios/Sentry.Samples.Ios.csproj +++ b/samples/Sentry.Samples.Ios/Sentry.Samples.Ios.csproj @@ -1,13 +1,15 @@ - net8.0-ios17.0 + net9.0-ios18.0 Exe enable true - 11.0 + 12.2 true true + + IL3050;IL3053 From 8c9756175ecf891eaafe2c84262837e67a7a33f2 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 30 Jan 2025 15:08:55 +1300 Subject: [PATCH 07/15] Replaced AppDomainUnhandledExceptionIntegration with RuntimeMarshalManagedExceptionIntegration when using CocoaSDK --- src/Sentry/Platforms/Cocoa/RuntimeAdapter.cs | 29 ++++++++++++ ...ntimeMarshalManagedExceptionIntegration.cs | 45 +++++++++++++++++++ src/Sentry/SentryOptions.cs | 28 ++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 src/Sentry/Platforms/Cocoa/RuntimeAdapter.cs create mode 100644 src/Sentry/Platforms/Cocoa/RuntimeMarshalManagedExceptionIntegration.cs diff --git a/src/Sentry/Platforms/Cocoa/RuntimeAdapter.cs b/src/Sentry/Platforms/Cocoa/RuntimeAdapter.cs new file mode 100644 index 0000000000..9bc7551cd4 --- /dev/null +++ b/src/Sentry/Platforms/Cocoa/RuntimeAdapter.cs @@ -0,0 +1,29 @@ +using ObjCRuntime; + +namespace Sentry.Cocoa; + +internal interface IRuntime +{ + internal event MarshalManagedExceptionHandler MarshalManagedException; + internal event MarshalObjectiveCExceptionHandler MarshalObjectiveCException; +} + +internal sealed class RuntimeAdapter : IRuntime +{ + public static RuntimeAdapter Instance { get; } = new(); + + private RuntimeAdapter() + { + Runtime.MarshalManagedException += OnMarshalManagedException; + Runtime.MarshalObjectiveCException += OnMarshalObjectiveCException; + } + + public event MarshalManagedExceptionHandler? MarshalManagedException; + public event MarshalObjectiveCExceptionHandler? MarshalObjectiveCException; + + [SecurityCritical] + private void OnMarshalManagedException(object sender, MarshalManagedExceptionEventArgs e) => MarshalManagedException?.Invoke(this, e); + + [SecurityCritical] + private void OnMarshalObjectiveCException(object sender, MarshalObjectiveCExceptionEventArgs e) => MarshalObjectiveCException?.Invoke(this, e); +} diff --git a/src/Sentry/Platforms/Cocoa/RuntimeMarshalManagedExceptionIntegration.cs b/src/Sentry/Platforms/Cocoa/RuntimeMarshalManagedExceptionIntegration.cs new file mode 100644 index 0000000000..8fd922e80d --- /dev/null +++ b/src/Sentry/Platforms/Cocoa/RuntimeMarshalManagedExceptionIntegration.cs @@ -0,0 +1,45 @@ +using ObjCRuntime; +using Sentry.Extensibility; +using Sentry.Integrations; + +namespace Sentry.Cocoa; + +/// +/// When AOT Compiling iOS applications, the AppDomain UnhandledExceptionHandler doesn't fire. So instead we intercept +/// the Runtime.RuntimeMarshalManagedException event. +/// +internal class RuntimeMarshalManagedExceptionIntegration : ISdkIntegration +{ + private readonly IRuntime _runtime; + private IHub? _hub; + private SentryOptions? _options; + + internal RuntimeMarshalManagedExceptionIntegration(IRuntime? runtime = null) + => _runtime = runtime ?? RuntimeAdapter.Instance; + + public void Register(IHub hub, SentryOptions options) + { + _hub = hub; + _options = options; + _runtime.MarshalManagedException += Handle; + } + + // Internal for testability + [SecurityCritical] + internal void Handle(object sender, MarshalManagedExceptionEventArgs e) + { + _options?.LogDebug("Runtime Marshal Managed Exception"); + + if (e.Exception is { } ex) + { + ex.SetSentryMechanism( + "Runtime.MarshalManagedException", + "This exception was caught by the .NET Runtime Marshal Managed Exception global error handler. " + + "The application may have crashed as a result of this exception.", + handled: false); + + // Call the internal implementation, so that we still capture even if the hub has been disabled. + _hub?.CaptureExceptionInternal(ex); + } + } +} diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs index de8d930066..7c99988acf 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -18,6 +18,11 @@ using Sentry.Android.AssemblyReader; #endif +#if IOS || MACCATALYST +using ObjCRuntime; +using Sentry.Cocoa; +#endif + namespace Sentry; /// @@ -162,10 +167,17 @@ internal IEnumerable Integrations yield return new AutoSessionTrackingIntegration(); } +#if IOS || MACCATALYST + if ((_defaultIntegrations & DefaultIntegrations.RuntimeMarshalManagedExceptionIntegration) != 0) + { + yield return new RuntimeMarshalManagedExceptionIntegration(); + } +#else if ((_defaultIntegrations & DefaultIntegrations.AppDomainUnhandledExceptionIntegration) != 0) { yield return new AppDomainUnhandledExceptionIntegration(); } +#endif if ((_defaultIntegrations & DefaultIntegrations.AppDomainProcessExitIntegration) != 0) { @@ -1239,7 +1251,11 @@ public SentryOptions() _integrations = new(); _defaultIntegrations = DefaultIntegrations.AutoSessionTrackingIntegration | +#if IOS || MACCATALYST + DefaultIntegrations.RuntimeMarshalManagedExceptionIntegration | +#else DefaultIntegrations.AppDomainUnhandledExceptionIntegration | +#endif DefaultIntegrations.AppDomainProcessExitIntegration | DefaultIntegrations.AutoSessionTrackingIntegration | DefaultIntegrations.UnobservedTaskExceptionIntegration @@ -1663,11 +1679,19 @@ public void ApplyDefaultTags(IHasTags hasTags) public void DisableDuplicateEventDetection() => RemoveEventProcessor(); +#if IOS || MACCATALYST + /// + /// Disables the capture of errors through . + /// + public void DisableRuntimeMarshalManagedExceptionCapture() => + RemoveDefaultIntegration(DefaultIntegrations.RuntimeMarshalManagedExceptionIntegration); +#else /// /// Disables the capture of errors through . /// public void DisableAppDomainUnhandledExceptionCapture() => RemoveDefaultIntegration(DefaultIntegrations.AppDomainUnhandledExceptionIntegration); +#endif #if HAS_DIAGNOSTIC_INTEGRATION /// @@ -1725,7 +1749,11 @@ public void DisableSystemDiagnosticsMetricsIntegration() internal enum DefaultIntegrations { AutoSessionTrackingIntegration = 1 << 0, +#if IOS || MACCATALYST + RuntimeMarshalManagedExceptionIntegration = 1 << 1, +#else AppDomainUnhandledExceptionIntegration = 1 << 1, +#endif AppDomainProcessExitIntegration = 1 << 2, UnobservedTaskExceptionIntegration = 1 << 3, #if NETFRAMEWORK From 96845d170fd15bd149053d523fcfa68fba71976b Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 30 Jan 2025 15:58:34 +1300 Subject: [PATCH 08/15] Added separate Managed and Native crash buttons to the iOS sample --- samples/Sentry.Samples.Ios/AppDelegate.cs | 123 +++++++++++------- samples/Sentry.Samples.Ios/Info.plist | 2 +- .../Sentry.Samples.Ios.csproj | 4 +- 3 files changed, 81 insertions(+), 48 deletions(-) diff --git a/samples/Sentry.Samples.Ios/AppDelegate.cs b/samples/Sentry.Samples.Ios/AppDelegate.cs index 8ba822292f..a83db2591a 100644 --- a/samples/Sentry.Samples.Ios/AppDelegate.cs +++ b/samples/Sentry.Samples.Ios/AppDelegate.cs @@ -1,3 +1,5 @@ +using ObjCRuntime; + namespace Sentry.Samples.Ios; [Register("AppDelegate")] @@ -23,77 +25,108 @@ public override bool FinishedLaunching(UIApplication application, NSDictionary l // https://docs.sentry.io/platforms/apple/guides/ios/configuration/ // Enable Native iOS SDK App Hangs detection options.Native.EnableAppHangTracking = true; + + options.CacheDirectoryPath = Path.Combine(Path.GetTempPath(), "test12"); }); // create a new window instance based on the screen size Window = new UIWindow(UIScreen.MainScreen.Bounds); - // determine the background color for the view (SystemBackground requires iOS >= 13.0) + // determine control colours (SystemBackground requires iOS >= 13.0) var backgroundColor = UIDevice.CurrentDevice.CheckSystemVersion(13, 0) #pragma warning disable CA1416 ? UIColor.SystemBackground #pragma warning restore CA1416 : UIColor.White; + var buttonConfig = UIButtonConfiguration.TintedButtonConfiguration; - // create a UIViewController with a single UILabel var vc = new UIViewController(); - vc.View!.AddSubview(new UILabel(Window!.Frame) + + var label = new UILabel { BackgroundColor = backgroundColor, TextAlignment = UITextAlignment.Center, Text = "Hello, iOS!", + AutoresizingMask = UIViewAutoresizing.All + }; + + // UIButton for managed crash + var managedCrashButton = new UIButton(UIButtonType.RoundedRect) + { AutoresizingMask = UIViewAutoresizing.All, - }); + Configuration = buttonConfig + }; + managedCrashButton.SetTitle("Managed Crash", UIControlState.Normal); + managedCrashButton.TouchUpInside += delegate + { + Console.WriteLine("Managed Crash button clicked!"); + try + { + string s = null!; + Console.WriteLine("Length: {0}", s.Length); + } + catch (Exception e) + { + SentrySdk.CaptureException(e); + } + }; + + // UIButton for native crash + var nativeCrashButton = new UIButton(UIButtonType.System) + { + Configuration = buttonConfig + }; + nativeCrashButton.SetTitle("Native Crash", UIControlState.Normal); + nativeCrashButton.TouchUpInside += delegate + { + Console.WriteLine("Native Crash button clicked!"); +#pragma warning disable CS0618 // Type or member is obsolete + SentrySdk.CauseCrash(CrashType.Native); +#pragma warning restore CS0618 // Type or member is obsolete + }; + + // create a UIStackView to hold the label and buttons + var stackView = new UIStackView(new UIView[] { label, managedCrashButton, nativeCrashButton }) + { + Axis = UILayoutConstraintAxis.Vertical, + Distribution = UIStackViewDistribution.FillEqually, + Alignment = UIStackViewAlignment.Center, + Spacing = 10, + TranslatesAutoresizingMaskIntoConstraints = false, + }; + + // add the stack view to the view controller's view + vc.View!.BackgroundColor = backgroundColor; + vc.View.AddSubview(stackView); + + // set constraints for the stack view + NSLayoutConstraint.ActivateConstraints([ + stackView.CenterXAnchor.ConstraintEqualTo(vc.View.CenterXAnchor), + stackView.CenterYAnchor.ConstraintEqualTo(vc.View.CenterYAnchor), + stackView.WidthAnchor.ConstraintEqualTo(vc.View.WidthAnchor, 0.8f), + stackView.HeightAnchor.ConstraintEqualTo(vc.View.HeightAnchor, 0.5f) + ]); + Window.RootViewController = vc; // make the window visible Window.MakeKeyAndVisible(); + AppDomain.CurrentDomain.UnhandledException += (_, _) => + { + Console.WriteLine("In UnhandledException Handler"); + }; - // Try out the Sentry SDK - SentrySdk.CaptureMessage("From iOS"); - - // Uncomment to try these - // throw new Exception("Test Unhandled Managed Exception"); - // SentrySdk.CauseCrash(CrashType.Native); - + Runtime.MarshalManagedException += (_, _) => { - var tx = SentrySdk.StartTransaction("app", "run"); - var count = 10; - for (var i = 0; i < count; i++) - { - FindPrimeNumber(100000); - } + Console.WriteLine("In MarshalManagedException Handler"); + }; - tx.Finish(); - } + Runtime.MarshalObjectiveCException += (_, _) => + { + Console.WriteLine("In MarshalObjectiveCException Handler"); + }; return true; } - - private static long FindPrimeNumber(int n) - { - int count = 0; - long a = 2; - while (count < n) - { - long b = 2; - int prime = 1;// to check if found a prime - while (b * b <= a) - { - if (a % b == 0) - { - prime = 0; - break; - } - b++; - } - if (prime > 0) - { - count++; - } - a++; - } - return (--a); - } } diff --git a/samples/Sentry.Samples.Ios/Info.plist b/samples/Sentry.Samples.Ios/Info.plist index ffefa6aa7c..38f2e8adc8 100644 --- a/samples/Sentry.Samples.Ios/Info.plist +++ b/samples/Sentry.Samples.Ios/Info.plist @@ -9,7 +9,7 @@ CFBundleShortVersionString 1.0 MinimumOSVersion - 12.2 + 15.0 CFBundleVersion 1.0 LSRequiresIPhoneOS diff --git a/samples/Sentry.Samples.Ios/Sentry.Samples.Ios.csproj b/samples/Sentry.Samples.Ios/Sentry.Samples.Ios.csproj index 11372b2e40..45cd8a28c6 100644 --- a/samples/Sentry.Samples.Ios/Sentry.Samples.Ios.csproj +++ b/samples/Sentry.Samples.Ios/Sentry.Samples.Ios.csproj @@ -1,11 +1,11 @@ - net9.0-ios18.0 + net9.0-ios18.0;net8.0-ios17.0 Exe enable true - 12.2 + 15.0 true true From 341aa838c60befc0d912dcf6d61ce0e5034fb834 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 30 Jan 2025 16:13:37 +1300 Subject: [PATCH 09/15] Update Sentry.Samples.Ios.csproj --- samples/Sentry.Samples.Ios/Sentry.Samples.Ios.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/Sentry.Samples.Ios/Sentry.Samples.Ios.csproj b/samples/Sentry.Samples.Ios/Sentry.Samples.Ios.csproj index 45cd8a28c6..76b122d042 100644 --- a/samples/Sentry.Samples.Ios/Sentry.Samples.Ios.csproj +++ b/samples/Sentry.Samples.Ios/Sentry.Samples.Ios.csproj @@ -1,7 +1,7 @@ - net9.0-ios18.0;net8.0-ios17.0 + net9.0-ios18.0 Exe enable true From 6be8cfba132d35c0446046c234e5a8e755f83899 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 30 Jan 2025 16:50:54 +1300 Subject: [PATCH 10/15] Added additional crash scenario to UI --- samples/Sentry.Samples.Ios/AppDelegate.cs | 46 +++++++++++++---------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/samples/Sentry.Samples.Ios/AppDelegate.cs b/samples/Sentry.Samples.Ios/AppDelegate.cs index a83db2591a..a5c1b872e4 100644 --- a/samples/Sentry.Samples.Ios/AppDelegate.cs +++ b/samples/Sentry.Samples.Ios/AppDelegate.cs @@ -39,6 +39,8 @@ public override bool FinishedLaunching(UIApplication application, NSDictionary l #pragma warning restore CA1416 : UIColor.White; var buttonConfig = UIButtonConfiguration.TintedButtonConfiguration; + var terminalButtonConfig = UIButtonConfiguration.TintedButtonConfiguration; + terminalButtonConfig.BaseBackgroundColor = UIColor.SystemRed; var vc = new UIViewController(); @@ -50,7 +52,7 @@ public override bool FinishedLaunching(UIApplication application, NSDictionary l AutoresizingMask = UIViewAutoresizing.All }; - // UIButton for managed crash + // UIButton for a managed exception that we'll catch and handle (won't crash the app) var managedCrashButton = new UIButton(UIButtonType.RoundedRect) { AutoresizingMask = UIViewAutoresizing.All, @@ -62,8 +64,7 @@ public override bool FinishedLaunching(UIApplication application, NSDictionary l Console.WriteLine("Managed Crash button clicked!"); try { - string s = null!; - Console.WriteLine("Length: {0}", s.Length); + throw new Exception("Catch this!"); } catch (Exception e) { @@ -71,22 +72,42 @@ public override bool FinishedLaunching(UIApplication application, NSDictionary l } }; + // UIButton for unhandled managed exception + var unhandledCrashButton = new UIButton(UIButtonType.RoundedRect) + { + AutoresizingMask = UIViewAutoresizing.All, + Configuration = terminalButtonConfig + }; + unhandledCrashButton.SetTitle("Unhandled Crash", UIControlState.Normal); + unhandledCrashButton.TouchUpInside += delegate + { + Console.WriteLine("Unhandled Crash button clicked!"); + string s = null!; + // This will cause a NullReferenceException that will crash the app before Sentry can send the event. + // Since we're using a caching transport though, the exception will be written to disk and sent the + // next time the app is launched. + Console.WriteLine("Length: {0}", s.Length); + }; + // UIButton for native crash var nativeCrashButton = new UIButton(UIButtonType.System) { - Configuration = buttonConfig + Configuration = terminalButtonConfig }; nativeCrashButton.SetTitle("Native Crash", UIControlState.Normal); nativeCrashButton.TouchUpInside += delegate { Console.WriteLine("Native Crash button clicked!"); #pragma warning disable CS0618 // Type or member is obsolete + // This will cause a native crash that will crash the application before + // Sentry gets a chance to send the event. Since we've enabled caching however, + // the event will be written to disk and sent the next time the app is launched. SentrySdk.CauseCrash(CrashType.Native); #pragma warning restore CS0618 // Type or member is obsolete }; // create a UIStackView to hold the label and buttons - var stackView = new UIStackView(new UIView[] { label, managedCrashButton, nativeCrashButton }) + var stackView = new UIStackView(new UIView[] { label, managedCrashButton, unhandledCrashButton, nativeCrashButton }) { Axis = UILayoutConstraintAxis.Vertical, Distribution = UIStackViewDistribution.FillEqually, @@ -112,21 +133,6 @@ public override bool FinishedLaunching(UIApplication application, NSDictionary l // make the window visible Window.MakeKeyAndVisible(); - AppDomain.CurrentDomain.UnhandledException += (_, _) => - { - Console.WriteLine("In UnhandledException Handler"); - }; - - Runtime.MarshalManagedException += (_, _) => - { - Console.WriteLine("In MarshalManagedException Handler"); - }; - - Runtime.MarshalObjectiveCException += (_, _) => - { - Console.WriteLine("In MarshalObjectiveCException Handler"); - }; - return true; } } From 69d0d078d6c9d04534b13032ed6a912d0cb8339b Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 30 Jan 2025 16:52:50 +1300 Subject: [PATCH 11/15] Removed code that sets the MarshalManagedExceptionMode No longer necessary as we aren't using the UnhandledException event to catch these exceptions anymore --- src/Sentry/Platforms/Cocoa/SentrySdk.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Sentry/Platforms/Cocoa/SentrySdk.cs b/src/Sentry/Platforms/Cocoa/SentrySdk.cs index 4b303c0749..182195e078 100644 --- a/src/Sentry/Platforms/Cocoa/SentrySdk.cs +++ b/src/Sentry/Platforms/Cocoa/SentrySdk.cs @@ -10,11 +10,6 @@ public static partial class SentrySdk private static void InitSentryCocoaSdk(SentryOptions options) { options.LogDebug("Initializing native SDK"); - // Workaround for https://github.com/xamarin/xamarin-macios/issues/15252 - ObjCRuntime.Runtime.MarshalManagedException += (_, args) => - { - args.ExceptionMode = ObjCRuntime.MarshalManagedExceptionMode.UnwindNativeCode; - }; // Set default release and distribution options.Release ??= GetDefaultReleaseString(); From 316e75ab2ec3fe56fa2dd1a3d57717f0d26d0770 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 30 Jan 2025 17:42:30 +1300 Subject: [PATCH 12/15] Unit tests --- ...MarshalManagedExceptionIntegrationTests.cs | 70 +++++++++++++++++++ test/Sentry.Tests/SentryOptionsTests.cs | 22 ++++++ 2 files changed, 92 insertions(+) create mode 100644 test/Sentry.Tests/Platforms/iOS/RuntimeMarshalManagedExceptionIntegrationTests.cs diff --git a/test/Sentry.Tests/Platforms/iOS/RuntimeMarshalManagedExceptionIntegrationTests.cs b/test/Sentry.Tests/Platforms/iOS/RuntimeMarshalManagedExceptionIntegrationTests.cs new file mode 100644 index 0000000000..e6cf544034 --- /dev/null +++ b/test/Sentry.Tests/Platforms/iOS/RuntimeMarshalManagedExceptionIntegrationTests.cs @@ -0,0 +1,70 @@ +# if IOS +using ObjCRuntime; +using Sentry.Cocoa; + +namespace Sentry.Tests.Platforms.iOS; + +public class RuntimeMarshalManagedExceptionIntegrationTests +{ + private class Fixture + { + public IHub Hub { get; } = Substitute.For(); + public IRuntime Runtime { get; } = Substitute.For(); + + public RuntimeMarshalManagedExceptionIntegration GetSut() => new(Runtime); + } + + private readonly Fixture _fixture = new(); + private SentryOptions SentryOptions { get; } = new(); + + [Fact] + public void Handle_WithException_CaptureEvent() + { + var sut = _fixture.GetSut(); + sut.Register(_fixture.Hub, SentryOptions); + + sut.Handle(this, new MarshalManagedExceptionEventArgs{Exception = new Exception()}); + + _fixture.Hub.Received(1).CaptureEvent(Arg.Any()); + } + + [Fact] + public void Handle_WithException_IsHandledFalse() + { + var sut = _fixture.GetSut(); + sut.Register(_fixture.Hub, SentryOptions); + + var exception = new Exception(); + sut.Handle(this, new MarshalManagedExceptionEventArgs{Exception = exception}); + Assert.Equal(false, exception.Data[Mechanism.HandledKey]); + Assert.True(exception.Data.Contains(Mechanism.MechanismKey)); + + var stackTraceFactory = Substitute.For(); + var exceptionProcessor = new MainExceptionProcessor(SentryOptions, () => stackTraceFactory); + var @event = new SentryEvent(exception); + + exceptionProcessor.Process(exception, @event); + Assert.NotNull(@event.SentryExceptions?.ToList().Single(p => p.Mechanism?.Handled == false)); + } + + [Fact] + public void Handle_NoException_NoCaptureEvent() + { + var sut = _fixture.GetSut(); + sut.Register(_fixture.Hub, SentryOptions); + + sut.Handle(this, new MarshalManagedExceptionEventArgs()); + + _fixture.Hub.DidNotReceive().CaptureEvent(Arg.Any()); + } + + [Fact] + public void Register_UnhandledException_Subscribes() + { + var sut = _fixture.GetSut(); + sut.Register(_fixture.Hub, SentryOptions); + + _fixture.Runtime.Received().MarshalManagedException += sut.Handle; + } +} +#endif diff --git a/test/Sentry.Tests/SentryOptionsTests.cs b/test/Sentry.Tests/SentryOptionsTests.cs index cd20a5c7d3..7a12849443 100644 --- a/test/Sentry.Tests/SentryOptionsTests.cs +++ b/test/Sentry.Tests/SentryOptionsTests.cs @@ -1,6 +1,8 @@ namespace Sentry.Tests; #if NETFRAMEWORK using Sentry.PlatformAbstractions; +#elif IOS || MACCATALYST +using Sentry.Cocoa; #endif public partial class SentryOptionsTests { @@ -285,6 +287,16 @@ public void DisableNetFxInstallationsEventProcessor_RemovesDisableNetFxInstallat } #endif +#if IOS || MACCATALYST + [Fact] + public void DisableRuntimeMarshalManagedExceptionCapture_RemovesRuntimeMarshalManagedExceptionIntegration() + { + var sut = new SentryOptions(); + sut.DisableRuntimeMarshalManagedExceptionCapture(); + Assert.DoesNotContain(sut.Integrations, + p => p is RuntimeMarshalManagedExceptionIntegration); + } +#else [Fact] public void DisableAppDomainUnhandledExceptionCapture_RemovesAppDomainUnhandledExceptionIntegration() { @@ -293,6 +305,7 @@ public void DisableAppDomainUnhandledExceptionCapture_RemovesAppDomainUnhandledE Assert.DoesNotContain(sut.Integrations, p => p is AppDomainUnhandledExceptionIntegration); } +#endif [Fact] public void DisableTaskUnobservedTaskExceptionCapture_UnobservedTaskExceptionIntegration() @@ -581,12 +594,21 @@ public void GetAllEventProcessors_NoAdding_FirstReturned_DuplicateDetectionProce _ = Assert.IsType(sut.GetAllEventProcessors().First()); } +#if IOS || MACCATALYST + [Fact] + public void Integrations_Includes_RuntimeMarshalManagedExceptionIntegration() + { + var sut = new SentryOptions(); + Assert.Contains(sut.Integrations, i => i.GetType() == typeof(RuntimeMarshalManagedExceptionIntegration)); + } +#else [Fact] public void Integrations_Includes_AppDomainUnhandledExceptionIntegration() { var sut = new SentryOptions(); Assert.Contains(sut.Integrations, i => i.GetType() == typeof(AppDomainUnhandledExceptionIntegration)); } +#endif [Fact] public void Integrations_Includes_AppDomainProcessExitIntegration() From 2d22ca3b2f548271596f2415e9a882212a07ee1a Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Thu, 30 Jan 2025 04:56:46 +0000 Subject: [PATCH 13/15] Format code --- .../iOS/RuntimeMarshalManagedExceptionIntegrationTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Sentry.Tests/Platforms/iOS/RuntimeMarshalManagedExceptionIntegrationTests.cs b/test/Sentry.Tests/Platforms/iOS/RuntimeMarshalManagedExceptionIntegrationTests.cs index e6cf544034..8971ada618 100644 --- a/test/Sentry.Tests/Platforms/iOS/RuntimeMarshalManagedExceptionIntegrationTests.cs +++ b/test/Sentry.Tests/Platforms/iOS/RuntimeMarshalManagedExceptionIntegrationTests.cs @@ -23,7 +23,7 @@ public void Handle_WithException_CaptureEvent() var sut = _fixture.GetSut(); sut.Register(_fixture.Hub, SentryOptions); - sut.Handle(this, new MarshalManagedExceptionEventArgs{Exception = new Exception()}); + sut.Handle(this, new MarshalManagedExceptionEventArgs { Exception = new Exception() }); _fixture.Hub.Received(1).CaptureEvent(Arg.Any()); } @@ -35,7 +35,7 @@ public void Handle_WithException_IsHandledFalse() sut.Register(_fixture.Hub, SentryOptions); var exception = new Exception(); - sut.Handle(this, new MarshalManagedExceptionEventArgs{Exception = exception}); + sut.Handle(this, new MarshalManagedExceptionEventArgs { Exception = exception }); Assert.Equal(false, exception.Data[Mechanism.HandledKey]); Assert.True(exception.Data.Contains(Mechanism.MechanismKey)); From 1497b616083d0643147a9c44a1120876d5e4af4a Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 31 Jan 2025 14:46:40 +1300 Subject: [PATCH 14/15] Review feedback --- .../Cocoa/RuntimeMarshalManagedExceptionIntegration.cs | 2 +- src/Sentry/Platforms/Cocoa/SentrySdk.cs | 3 ++- .../iOS/RuntimeMarshalManagedExceptionIntegrationTests.cs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Sentry/Platforms/Cocoa/RuntimeMarshalManagedExceptionIntegration.cs b/src/Sentry/Platforms/Cocoa/RuntimeMarshalManagedExceptionIntegration.cs index 8fd922e80d..530437d2ce 100644 --- a/src/Sentry/Platforms/Cocoa/RuntimeMarshalManagedExceptionIntegration.cs +++ b/src/Sentry/Platforms/Cocoa/RuntimeMarshalManagedExceptionIntegration.cs @@ -28,7 +28,7 @@ public void Register(IHub hub, SentryOptions options) [SecurityCritical] internal void Handle(object sender, MarshalManagedExceptionEventArgs e) { - _options?.LogDebug("Runtime Marshal Managed Exception"); + _options?.LogDebug("Runtime Marshal Managed Exception mode {0}", e.ExceptionMode.ToString("G")); if (e.Exception is { } ex) { diff --git a/src/Sentry/Platforms/Cocoa/SentrySdk.cs b/src/Sentry/Platforms/Cocoa/SentrySdk.cs index 182195e078..dcc8e48006 100644 --- a/src/Sentry/Platforms/Cocoa/SentrySdk.cs +++ b/src/Sentry/Platforms/Cocoa/SentrySdk.cs @@ -148,7 +148,6 @@ private static void InitSentryCocoaSdk(SentryOptions options) // When we have an unhandled managed exception, we send that to Sentry twice - once managed and once native. // The managed exception is what a .NET developer would expect, and it is sent by the Sentry.NET SDK // But we also get a native SIGABRT since it crashed the application, which is sent by the Sentry Cocoa SDK. - // This is partially due to our setting ObjCRuntime.MarshalManagedExceptionMode.UnwindNativeCode above. nativeOptions.BeforeSend = evt => { // There should only be one exception on the event in this case @@ -164,6 +163,7 @@ private static void InitSentryCocoaSdk(SentryOptions options) ex.Stacktrace?.Frames.Any(f => f.Function == "xamarin_unhandled_exception_handler") is true) { // Don't send it + options.LogDebug("Discarded {0} error ({1}). Captured as managed exception instead.", ex.Type, ex.Value); return null!; } @@ -173,6 +173,7 @@ private static void InitSentryCocoaSdk(SentryOptions options) if (ex.Type == "EXC_BAD_ACCESS") { // Don't send it + options.LogDebug("Discarded {0} error ({1}). Captured as managed exception instead.", ex.Type, ex.Value); return null!; } } diff --git a/test/Sentry.Tests/Platforms/iOS/RuntimeMarshalManagedExceptionIntegrationTests.cs b/test/Sentry.Tests/Platforms/iOS/RuntimeMarshalManagedExceptionIntegrationTests.cs index 8971ada618..5ba83b0c28 100644 --- a/test/Sentry.Tests/Platforms/iOS/RuntimeMarshalManagedExceptionIntegrationTests.cs +++ b/test/Sentry.Tests/Platforms/iOS/RuntimeMarshalManagedExceptionIntegrationTests.cs @@ -1,4 +1,4 @@ -# if IOS +#if IOS using ObjCRuntime; using Sentry.Cocoa; From 6c20eb47859d2c70f2a1537fd62ff7c99025f913 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 3 Feb 2025 12:27:06 +1300 Subject: [PATCH 15/15] Flush when the exception is likely terminal --- samples/Sentry.Samples.Ios/AppDelegate.cs | 3 ++- .../Cocoa/RuntimeMarshalManagedExceptionIntegration.cs | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/samples/Sentry.Samples.Ios/AppDelegate.cs b/samples/Sentry.Samples.Ios/AppDelegate.cs index a5c1b872e4..80e9ea09a5 100644 --- a/samples/Sentry.Samples.Ios/AppDelegate.cs +++ b/samples/Sentry.Samples.Ios/AppDelegate.cs @@ -18,6 +18,7 @@ public override bool FinishedLaunching(UIApplication application, NSDictionary l { options.Dsn = "https://eb18e953812b41c3aeb042e666fd3b5c@o447951.ingest.sentry.io/5428537"; options.Debug = true; + options.SampleRate = 1.0F; options.TracesSampleRate = 1.0; options.ProfilesSampleRate = 1.0; @@ -26,7 +27,7 @@ public override bool FinishedLaunching(UIApplication application, NSDictionary l // Enable Native iOS SDK App Hangs detection options.Native.EnableAppHangTracking = true; - options.CacheDirectoryPath = Path.Combine(Path.GetTempPath(), "test12"); + options.CacheDirectoryPath = Path.GetTempPath(); }); // create a new window instance based on the screen size diff --git a/src/Sentry/Platforms/Cocoa/RuntimeMarshalManagedExceptionIntegration.cs b/src/Sentry/Platforms/Cocoa/RuntimeMarshalManagedExceptionIntegration.cs index 530437d2ce..396221cb79 100644 --- a/src/Sentry/Platforms/Cocoa/RuntimeMarshalManagedExceptionIntegration.cs +++ b/src/Sentry/Platforms/Cocoa/RuntimeMarshalManagedExceptionIntegration.cs @@ -40,6 +40,9 @@ internal void Handle(object sender, MarshalManagedExceptionEventArgs e) // Call the internal implementation, so that we still capture even if the hub has been disabled. _hub?.CaptureExceptionInternal(ex); + + // This is likely a terminal exception so try to send the crash report before shutting down + _hub?.Flush(); } } }