Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fatalError call with message does not report message value in Sentry Dashboard #662

Closed
martindufort opened this issue Jul 14, 2020 · 9 comments · Fixed by #1596
Closed

Comments

@martindufort
Copy link

Important Details

How are you running Sentry?
Saas (sentry.io)

Description

  • Using the SentrySDK Cocoa (Pod) Version 5.1.4

In macOS Swift app, using fatalError(message:String) to report an abnormal condition.
Error is reported within the Sentry dashboard but the message is not displayed anywhere.

Steps to Reproduce

This is the code that produced this error:

@IBAction public func crashMe(_ sender: Any?) {

   fatalError("Forcing a crash within the Advanced Preferences section")
}

What you expected to happen

The relevant message Forcing a crash within the Advanced Preferences section should be captured and displayed in the dashboard.
Please review event: https://sentry.io/share/issue/68d48cf4f07440cbad16a672580f9cae/

@philipphofmann philipphofmann transferred this issue from getsentry/sentry Aug 11, 2020
@philipphofmann
Copy link
Member

Thanks, @martindufort for opening this issue. I can easily reproduce this issue.

Calling fatalError prints a message and stops execution. Our internal exception handler currently just receives the mach exception EXC_BAD_INSTRUCTION (SIGILL), which doesn't contain the message from fatalError. When I try this code on macOS and wait for the problem report from Apple to open I see the message from fatalError in application specific information. The fix would be to figure out where we can pick up this information and add it to the SentryEvent. This could be a bit complicated to fix and therefore I can't promise when we are going to fix this.

@martindufort
Copy link
Author

Thanks for validating this @philipphofmann.

I've written a workaround function called fatalCondition(). It supercedes fatalError and sends a proper Sentry Event.
The only thing is that it creates two events for the same error but I can alway ignore the 'dumb' one.

import Sentry

public func fatalCondition(_ message: @autoclosure () -> String = String(), file: StaticString = #file, line: UInt = #line) -> Never {
	let event = Sentry.Event(level: .error)
	event.logger = "FatalCondition"
	event.message = message()
	event.extra = [
		"file_name": file,
		"line_number": line
	]

	SentrySDK.currentHub().capture(event: event, scope: nil)
	
	// Also propagate a fatal error
	fatalError(message)
}

@sammydre
Copy link

@philipphofmann It's possible to extract this information from the Apple Crash Reporting section in some cases. A collection of information on this can be found here: https://bugzilla.mozilla.org/show_bug.cgi?id=1577886

You can see it in action in the code, e.g. https://github.com/apple/swift/blob/6527a216fb2f0264f5009cda379c961ebf59fe65/stdlib/public/runtime/Errors.cpp#L363

@philipphofmann
Copy link
Member

Thanks, @sammydre for the information.

@philipphofmann
Copy link
Member

This post summararizes different resources from the web and investigations.

Application Specific Information
As mentioned above one way to retrieve the message when calling fatalError or assert would be to get this from the Application Specific Information. This information is included on crash reports on macOS, but not on iOS as you can see below. This could be because iOS on-device prunes the Application Specific Information from crash logs because of user privacy concerns, as suggested in https://forums.swift.org/t/intercept-fatalerror-or-other-termination-messages-from-swift/6190/11.

macOS

// ...

Crashed Thread:        0  Dispatch queue: com.apple.main-thread

Exception Type:        EXC_BAD_INSTRUCTION (SIGILL)
Exception Codes:       0x0000000000000001, 0x0000000000000000
Exception Note:        EXC_CORPSE_NOTIFY

Termination Signal:    Terminated: 15
Termination Reason:    Namespace SIGNAL, Code 0xf
Terminating Process:   Xcode [75489]

Application Specific Information:
Performing @selector(captureMessage:) from sender NSButton 0x7fef78d059a0
Fatal error: myMessage: file macOS_Swift/ViewController.swift, line 27
 

Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
0   libswiftCore.dylib            	0x00007fff2c713124 _assertionFailure(_:_:file:line:flags:) + 532
1   io.sentry.macOS-Swift         	0x0000000103dd2be9 ViewController.captureMessage(_:) + 185 (ViewController.swift:27)

// ...

iOS

// ...

Exception Type:  EXC_BREAKPOINT (SIGTRAP)
Exception Codes: 0x0000000000000001, 0x000000018baf4cfc
Termination Signal: Trace/BPT trap: 5
Termination Reason: Namespace SIGNAL, Code 0x5
Terminating Process: exc handler [18443]
Triggered by Thread:  0

Thread 0 name:  Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0   libswiftCore.dylib            	0x000000018baf4cfc 0x18babe000 + 224508
1   libswiftCore.dylib            	0x000000018baf4cfc 0x18babe000 + 224508

// ...

According to a post in Bugzilla, an article from Alastair's Place and an issue on KSCrash getsectdatafromFramework can be used to retrieve Application Specific Information.

#define CRASH_ALIGN __attribute__((aligned(8)))
typedef struct {
  unsigned    version   CRASH_ALIGN;
  const char *message   CRASH_ALIGN;
  const char *signature CRASH_ALIGN;
  const char *backtrace CRASH_ALIGN;
  const char *message2  CRASH_ALIGN;
  void       *reserved  CRASH_ALIGN;
  void       *reserved2 CRASH_ALIGN;
} crash_info_t;

unsigned long size = 0;
crash_info_t* infoPtr = (crash_info_t*)getsectdatafromFramework("AppKit", SEG_DATA, "__crash_info", &size);

I put this code into SentryCrashMonitor.sentrycrashcm_handleException and call fatalError in our macOS-Swift sample app. The crash_info only contains

version = 5
message = Performing @selector(captureMessage:) from sender NSButton 0x7fe1a9d1dc20 

All the other fields of crash info are null.
When trying this on iOS getsectdatafromFramework returns nil. So I stopped this approach because it is not working reliably and I didn't find any official documentation from apple on crash_info_t.

Competition:

Logs
The console of Xcode displays the fatal error message like this:

2020-11-20 14:32:18.002298+0100 iOS-Swift[18702:2083667] Fatal error: my message: file iOS_Swift/ViewController.swift, line 33

Also device console logs display fatal error messages:
Screenshot 2020-11-20 at 14 10 23

This means that Xcode and the device console can somehow catch the message from somewhere.

Swift Compiler
According to a post in the Swift forum:

The Swift compiler in development now emits fatal error traps with an artificial inline frame, which will allow crash reporters than understand debug info to show the error directly as part of the backtrace. Future toolchains that include this change should be able to do better.

Other relevant resources:

@sammydre
Copy link

We did some research into this at work and unfortunately haven't found the time to write it all up, but I thought it might be helpful to mention some of the pieces here.

  • We found by disassembling the output of the swift compiler an optional unwrap is compiled directly into a call to assertionFailure
  • ... Except that asserts are compiled out in "release" builds, so it's not possible (as far as we could tell) to catch these
  • If asserts are compiled in, then examining the crash info always finds the reason
  • One can ensure asserts are compiled in, even on an optimised build, by compiling with -O -assert-config Debug -- this is what we use
  • Another idea I had was trying to use mach_override_ptr() to overrride _swift_runtime_on_report() but I never did try that method out after finding looking at __crash_info was sufficient. I'm not sure it adds anything in the end.

The example code I had to look for crash reasons was as follows:

#define CRASH_ALIGN __attribute__((aligned(8)))
 
typedef struct {
  unsigned    version   CRASH_ALIGN;
  const char *message   CRASH_ALIGN;
  const char *signature CRASH_ALIGN;
  const char *backtrace CRASH_ALIGN;
  const char *message2  CRASH_ALIGN;
  void       *reserved  CRASH_ALIGN;
  void       *reserved2 CRASH_ALIGN;
} crash_info_t;
 
void dumpCrashReports()
{
  const unsigned images_count = _dyld_image_count();
  for (unsigned i = 0; i < images_count; i++) {
    const char *name = _dyld_get_image_name(i);
    const struct mach_header* header = _dyld_get_image_header(i);
    if (!header) {
      fprintf(stderr, "image %02d: (%s) null\n", i, name);
      continue;
    }
 
#if 1 // 64-bit
    uint64_t size = 0;
    char* data_ptr = getsectdatafromheader_64(
        (const struct mach_header_64*)(header),
        SEG_DATA,
        "__crash_info",
        &size);
#else
    unsigned int size;
    char* code_ptr = getsectdatafromheader(header, SEG_TEXT, SECT_TEXT, &size);
#endif
 
    data_ptr += _dyld_get_image_vmaddr_slide(i);
 
    fprintf(stderr, "image %02d: (%s) ptr %p size %lu\n", i, name, data_ptr, size);
 
    if (data_ptr && size) {
      crash_info_t *cr = (crash_info_t *)data_ptr;
      fprintf(stderr, "  version %u message %s signature %s backtrace %s message2 %s\n",
          cr->version, cr->message, cr->signature, cr->backtrace, cr->message2);
    }
  }
}

Hope that is helpful @philipphofmann . If it is possible to get the crash info without needing to change the assert compilation mode, that would obviously be superior, but I'd argue the approach I've outlined here would be sufficient for many users.

@sammydre
Copy link

To further elaborate on what the Swift compiler does here:

Swift compiler version:

$ swiftc --version
Apple Swift version 5.1 (swiftlang-1100.0.270.13 clang-1100.0.33.7)
Target: x86_64-apple-darwin18.5.0

Example file that crashes via an unwrap:

$ cat explicitly_unwrap.swift
let a: Int? = nil
let b = a!

Compiler output with Debug -- note the call to assertionFailure

$ swiftc -O -assert-config Debug -emit-sil ./explicitly_unwrap.swift                                                                                                                [15/34169]sil_stage canonical

import Builtin
import Swift
import SwiftShims

@_hasStorage @_hasInitialValue let a: Int? { get }

@_hasStorage @_hasInitialValue let b: Int { get }

// a
sil_global hidden [let] @$s17explicitly_unwrap1aSiSgvp : $Optional<Int>

// b
sil_global hidden [let] @$s17explicitly_unwrap1bSivp : $Int

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  alloc_global @$s17explicitly_unwrap1aSiSgvp     // id: %2
  %3 = global_addr @$s17explicitly_unwrap1aSiSgvp : $*Optional<Int> // user: %5
  %4 = enum $Optional<Int>, #Optional.none!enumelt // user: %5
  store %4 to %3 : $*Optional<Int>                // id: %5
  alloc_global @$s17explicitly_unwrap1bSivp       // id: %6
  %7 = string_literal utf8 "./explicitly_unwrap.swift" // user: %9
  %8 = integer_literal $Builtin.Word, 25          // user: %11
  %9 = builtin "ptrtoint_Word"(%7 : $Builtin.RawPointer) : $Builtin.Word // user: %11
  %10 = integer_literal $Builtin.Int8, 2          // users: %17, %11
  %11 = struct $StaticString (%9 : $Builtin.Word, %8 : $Builtin.Word, %10 : $Builtin.Int8) // user: %36
  %12 = integer_literal $Builtin.Int64, 2         // user: %13
  %13 = struct $UInt (%12 : $Builtin.Int64)       // user: %36
  %14 = string_literal utf8 "Fatal error"         // user: %16
  %15 = integer_literal $Builtin.Word, 11         // user: %17
  %16 = builtin "ptrtoint_Word"(%14 : $Builtin.RawPointer) : $Builtin.Word // user: %17
  %17 = struct $StaticString (%16 : $Builtin.Word, %15 : $Builtin.Word, %10 : $Builtin.Int8) // user: %36
  %18 = string_literal utf8 "Unexpectedly found nil while unwrapping an Optional value" // user: %22
  %19 = integer_literal $Builtin.Int1, 0          // user: %25
  %20 = integer_literal $Builtin.Int64, -3458764513820540871 // user: %21
  %21 = struct $UInt64 (%20 : $Builtin.Int64)     // user: %30
  %22 = builtin "ptrtoint_Word"(%18 : $Builtin.RawPointer) : $Builtin.Word // user: %23
  %23 = builtin "zextOrBitCast_Word_Int64"(%22 : $Builtin.Word) : $Builtin.Int64 // user: %25
  %24 = integer_literal $Builtin.Int64, 32        // user: %25
  %25 = builtin "usub_with_overflow_Int64"(%23 : $Builtin.Int64, %24 : $Builtin.Int64, %19 : $Builtin.Int1) : $(Builtin.Int64, Builtin.Int1) // user: %26
  %26 = tuple_extract %25 : $(Builtin.Int64, Builtin.Int1), 0 // user: %28
  %27 = integer_literal $Builtin.Int64, -9223372036854775808 // user: %28
  %28 = builtin "stringObjectOr_Int64"(%26 : $Builtin.Int64, %27 : $Builtin.Int64) : $Builtin.Int64 // user: %29
  %29 = value_to_bridge_object %28 : $Builtin.Int64 // user: %30
  %30 = struct $_StringObject (%21 : $UInt64, %29 : $Builtin.BridgeObject) // user: %31
  %31 = struct $_StringGuts (%30 : $_StringObject) // user: %32
  %32 = struct $String (%31 : $_StringGuts)       // user: %36
  %33 = integer_literal $Builtin.Int32, 1         // user: %34
  %34 = struct $UInt32 (%33 : $Builtin.Int32)     // user: %36
  // function_ref _assertionFailure(_:_:file:line:flags:)
  %35 = function_ref @$ss17_assertionFailure__4file4line5flagss5NeverOs12StaticStringV_SSAHSus6UInt32VtF : $@convention(thin) (StaticString, @guaranteed String, StaticString, UInt, UInt32) -> Never // user: %36
  %36 = apply %35(%17, %32, %11, %13, %34) : $@convention(thin) (StaticString, @guaranteed String, StaticString, UInt, UInt32) -> Never
  unreachable                                     // id: %37
} // end sil function 'main'

// _assertionFailure(_:_:file:line:flags:)
sil [noinline] [_semantics "programtermination_point"] @$ss17_assertionFailure__4file4line5flagss5NeverOs12StaticStringV_SSAHSus6UInt32VtF : $@convention(thin) (StaticString, @guaranteed String, StaticString,
UInt, UInt32) -> Never

Compiler output in Release -- note how it just goes to an int_trap without seemingly any reporting of what error happened:

$ swiftc -O -assert-config Release -emit-sil ./explicitly_unwrap.swift
sil_stage canonical

import Builtin
import Swift
import SwiftShims

@_hasStorage @_hasInitialValue let a: Int? { get }

@_hasStorage @_hasInitialValue let b: Int { get }

// a
sil_global hidden [let] @$s17explicitly_unwrap1aSiSgvp : $Optional<Int>

// b
sil_global hidden [let] @$s17explicitly_unwrap1bSivp : $Int

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  alloc_global @$s17explicitly_unwrap1aSiSgvp     // id: %2
  %3 = global_addr @$s17explicitly_unwrap1aSiSgvp : $*Optional<Int> // user: %5
  %4 = enum $Optional<Int>, #Optional.none!enumelt // user: %5
  store %4 to %3 : $*Optional<Int>                // id: %5
  alloc_global @$s17explicitly_unwrap1bSivp       // id: %6
  %7 = builtin "int_trap"() : $Never
  unreachable                                     // id: %8
} // end sil function 'main'

@philipphofmann
Copy link
Member

Thanks a lot for the detailed explanation @sammydre 👏. I should have iterated over all the images and look for the crash_info_t in each of them, like you explained. Some quick investigation showed me, that I can find the message in currenlibswiftCore. We now need to find out how we can add this info to our events and we need to make sure that works properly.

This only works for asserts in release, if you have them enabled, of course, but it seems like this should work for fatalError also in release.

Example with fatalError:

func sample() {
    fatalError("Won't work")
}

Debug

swiftc -O -assert-config Debug -emit-sil sample.swift

// ...

// sample()
sil hidden @$s6sampleAAyyF : $@convention(thin) () -> () {
bb0:
  %0 = string_literal utf8 "sample/sample.swift"  // user: %2
  %1 = integer_literal $Builtin.Word, 19          // user: %4
  %2 = builtin "ptrtoint_Word"(%0 : $Builtin.RawPointer) : $Builtin.Word // user: %4
  %3 = integer_literal $Builtin.Int8, 2           // users: %10, %4
  %4 = struct $StaticString (%2 : $Builtin.Word, %1 : $Builtin.Word, %3 : $Builtin.Int8) // user: %21
  %5 = integer_literal $Builtin.Int64, 2          // user: %6
  %6 = struct $UInt (%5 : $Builtin.Int64)         // user: %21
  %7 = string_literal utf8 "Fatal error"          // user: %9
  %8 = integer_literal $Builtin.Word, 11          // user: %10
  %9 = builtin "ptrtoint_Word"(%7 : $Builtin.RawPointer) : $Builtin.Word // user: %10
  %10 = struct $StaticString (%9 : $Builtin.Word, %8 : $Builtin.Word, %3 : $Builtin.Int8) // user: %21
  %11 = integer_literal $Builtin.Int64, 8031924143688413015 // user: %12
  %12 = struct $UInt64 (%11 : $Builtin.Int64)     // user: %15
  %13 = integer_literal $Builtin.Int64, -1585267068834387086 // user: %14
  %14 = value_to_bridge_object %13 : $Builtin.Int64 // user: %15
  %15 = struct $_StringObject (%12 : $UInt64, %14 : $Builtin.BridgeObject) // user: %16
  %16 = struct $_StringGuts (%15 : $_StringObject) // user: %17
  %17 = struct $String (%16 : $_StringGuts)       // user: %21
  %18 = integer_literal $Builtin.Int32, 1         // user: %19
  %19 = struct $UInt32 (%18 : $Builtin.Int32)     // user: %21
  // function_ref _assertionFailure(_:_:file:line:flags:)
  %20 = function_ref @$ss17_assertionFailure__4file4line5flagss5NeverOs12StaticStringV_SSAHSus6UInt32VtF : $@convention(thin) (StaticString, @guaranteed String, StaticString, UInt, UInt32) -> Never // user: %21
  %21 = apply %20(%10, %17, %4, %6, %19) : $@convention(thin) (StaticString, @guaranteed String, StaticString, UInt, UInt32) -> Never
  unreachable                                     // id: %22
} // end sil function '$s6sampleAAyyF'

// _assertionFailure(_:_:file:line:flags:)
sil [noinline] [_semantics "programtermination_point"] @$ss17_assertionFailure__4file4line5flagss5NeverOs12StaticStringV_SSAHSus6UInt32VtF : $@convention(thin) (StaticString, @guaranteed String, StaticString, UInt, UInt32) -> Never

// ...

Release

swiftc -O -assert-config Release -emit-sil sample.swift

// ...

// sample()
sil hidden @$s6sampleAAyyF : $@convention(thin) () -> () {
bb0:
  %0 = string_literal utf8 "sample/sample.swift"  // user: %2
  %1 = integer_literal $Builtin.Word, 19          // user: %4
  %2 = builtin "ptrtoint_Word"(%0 : $Builtin.RawPointer) : $Builtin.Word // user: %4
  %3 = integer_literal $Builtin.Int8, 2           // users: %10, %4
  %4 = struct $StaticString (%2 : $Builtin.Word, %1 : $Builtin.Word, %3 : $Builtin.Int8) // user: %21
  %5 = integer_literal $Builtin.Int64, 2          // user: %6
  %6 = struct $UInt (%5 : $Builtin.Int64)         // user: %21
  %7 = string_literal utf8 "Fatal error"          // user: %9
  %8 = integer_literal $Builtin.Word, 11          // user: %10
  %9 = builtin "ptrtoint_Word"(%7 : $Builtin.RawPointer) : $Builtin.Word // user: %10
  %10 = struct $StaticString (%9 : $Builtin.Word, %8 : $Builtin.Word, %3 : $Builtin.Int8) // user: %21
  %11 = integer_literal $Builtin.Int64, 8031924143688413015 // user: %12
  %12 = struct $UInt64 (%11 : $Builtin.Int64)     // user: %15
  %13 = integer_literal $Builtin.Int64, -1585267068834387086 // user: %14
  %14 = value_to_bridge_object %13 : $Builtin.Int64 // user: %15
  %15 = struct $_StringObject (%12 : $UInt64, %14 : $Builtin.BridgeObject) // user: %16
  %16 = struct $_StringGuts (%15 : $_StringObject) // user: %17
  %17 = struct $String (%16 : $_StringGuts)       // user: %21
  %18 = integer_literal $Builtin.Int32, 0         // user: %19
  %19 = struct $UInt32 (%18 : $Builtin.Int32)     // user: %21
  // function_ref _assertionFailure(_:_:file:line:flags:)
  %20 = function_ref @$ss17_assertionFailure__4file4line5flagss5NeverOs12StaticStringV_SSAHSus6UInt32VtF : $@convention(thin) (StaticString, @guaranteed String, StaticString, UInt, UInt32) -> Never // user: %21
  %21 = apply %20(%10, %17, %4, %6, %19) : $@convention(thin) (StaticString, @guaranteed String, StaticString, UInt, UInt32) -> Never
  unreachable                                     // id: %22
} // end sil function '$s6sampleAAyyF'

// _assertionFailure(_:_:file:line:flags:)
sil [noinline] [_semantics "programtermination_point"] @$ss17_assertionFailure__4file4line5flagss5NeverOs12StaticStringV_SSAHSus6UInt32VtF : $@convention(thin) (StaticString, @guaranteed String, StaticString, UInt, UInt32) -> Never

// ...

@github-actions
Copy link

github-actions bot commented Nov 8, 2021

This issue has gone three weeks without activity. In another week, I will close it.

But! If you comment or otherwise update it, I will reset the clock, and if you label it Status: Backlog or Status: In Progress, I will leave it alone ... forever!


"A weed is but an unloved flower." ― Ella Wheeler Wilcox 🥀

@philipphofmann philipphofmann self-assigned this Dec 28, 2021
philipphofmann added a commit that referenced this issue Dec 29, 2021
The SDK was not able to retrieve the messages from Swifts fatalError, assert, or
precondition.This is fixed now by getting the message from the crashInfoMessage
of the binary image of libswiftCore.dylib.

Fixes GH-662
philipphofmann added a commit that referenced this issue Dec 29, 2021
The SDK was not able to retrieve the messages from Swifts fatalError, assert, or
precondition.This is fixed now by getting the message from the crashInfoMessage
of the binary image of libswiftCore.dylib.

Fixes GH-662
philipphofmann added a commit that referenced this issue Dec 29, 2021
The SDK was not able to retrieve the messages from Swifts fatalError, assert, or
precondition.This is fixed now by getting the message from the crashInfoMessage
of the binary image of libswiftCore.dylib.

Fixes GH-662
philipphofmann added a commit that referenced this issue Dec 29, 2021
The SDK was not able to retrieve the messages from Swifts fatalError, assert, or
precondition.This is fixed now by getting the message from the crashInfoMessage
of the binary image of libswiftCore.dylib.

Fixes GH-662
philipphofmann added a commit that referenced this issue Jan 3, 2022
The SDK was not able to retrieve the messages from Swifts fatalError, assert, or
precondition. This is fixed now by getting the message from the crashInfoMessage
of the binary image of libswiftCore.dylib.

Fixes GH-662

Co-authored-by: Dhiogo Brustolin <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants