Skip to content

iOS: SentryEvent.Exception is null for native crashes, so a BeforeSend callback that dereferences it throws #5620

Description

@jamescrosswell

Summary

On iOS, a user's managed BeforeSend callback is invoked for native crashes, and on that path SentryEvent.Exception is always null. A callback that dereferences Exception — entirely correct on every other path — throws a NullReferenceException for native crashes only.

This needs investigating rather than an obvious fix, hence a separate issue. Found while reviewing #5610, which changes what happens after the callback throws but does not introduce the null.

Why the callback runs at all

We deliberately hook the Cocoa SDK's own BeforeSend so that managed event processors and the user's BeforeSend also apply to native events — otherwise a native crash would bypass both:

nativeOptions.BeforeSend = evt => ProcessOnBeforeSend(options, evt)!;

ProcessOnBeforeSend converts the native event into a managed SentryEvent, runs managed processors, then calls SentryEventHelper.DoBeforeSend.

Why Exception is null

The conversion populates Exception only from an NSError:

var exception = sentryEvent.Error == null ? null : new NSErrorException(sentryEvent.Error);

var exception = sentryEvent.Error == null ? null : new NSErrorException(sentryEvent.Error);

A signal or Mach-exception crash (SIGSEGV, EXC_BAD_ACCESS, …) carries no NSError, so Error is null and Exception stays null. The native crash detail is still present on the event — it's in SentryExceptions from the deserialized wire-format payload — it just never reaches the Exception property.

The path

flowchart TD
    A["Native crash<br/><i>SIGSEGV, no NSError</i>"] --> B["Crash report written to disk<br/><i>process is dead</i>"]
    B --> C["Next app launch<br/><i>Cocoa SDK reads the report</i>"]
    C --> D["ToSentryEvent()<br/><i>Exception stays null</i>"]
    D --> E["User BeforeSend runs<br/><i>e.Exception.Message</i>"]
    E --> F["NullReferenceException"]
    F --> G["Before #5610<br/><i>crash still reported,<br/>breadcrumb attached</i>"]
    F --> H["After #5610<br/><i>crash report dropped</i>"]
Loading

Why it's easy to miss

Exception is populated on every managed path, so if (e.Exception.Message.Contains("token")) is correct everywhere an author would test it, and throws only here. Because it throws only here, managed errors keep arriving normally and just the native crashes are affected — the symptom is "we stopped getting iOS crash reports", with nothing pointing at BeforeSend.

Relationship to #5610

#5610 aligns BeforeSend failures with the callback error isolation spec (#5535): the item is dropped rather than sent with the exception stapled on as a breadcrumb. That is the right behaviour in general, and on this path it turns a degraded crash report into no crash report. It is logged at error level and counted in client reports, so it isn't silent, but neither is visible unless you go looking.

So #5610 changes the consequence; this issue is about the cause. ProcessOnBeforeSend gained test coverage for the throwing-callback case in #5610 (test/Sentry.Tests/Platforms/iOS/SentrySdkTests.cs).

Worth deciding

  • Should Exception be populated for native crashes — e.g. synthesised from SentryExceptions — or is null correct because there is no managed exception object?
  • If null is correct, is it documented anywhere a user writing BeforeSend would see it? SetBeforeSend's XML docs don't mention that the callback also runs for native events.
  • Does the same gap exist on the Android bridge, where Native.EnableBeforeSend routes native events through the managed callback too?

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    .NETPull requests that update .net codeBugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions