diff --git a/CHANGELOG.md b/CHANGELOG.md index 964becf277..bc313c805b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Per-instance unmaskView propagates to child views (#7733) - **Warning:** If you relied on children of an unmasked view still being individually redacted, verify your Session Replay redaction after updating. An explicit `maskView(_:)` on a descendant still takes precedence. +- Don't overwrite exception value with notable addresses for AppKit _crashOnException crashes (#7734) ## 9.8.0 diff --git a/Sources/Sentry/SentryCrashReportConverter.m b/Sources/Sentry/SentryCrashReportConverter.m index 48494d43c7..e44e9a5423 100644 --- a/Sources/Sentry/SentryCrashReportConverter.m +++ b/Sources/Sentry/SentryCrashReportConverter.m @@ -460,7 +460,10 @@ - (SentryDebugMeta *)debugMetaFromBinaryImageDictionary:(NSDictionary *)sourceIm exception = [[SentryException alloc] initWithValue:@"Unknown Exception" type:exceptionType]; } - [self enhanceValueFromNotableAddresses:exception]; + // AppKit's _crashOnException: produces garbage notable addresses; skip enhancement for those. + if (![self isMachExceptionFromAppKitCrashOnException]) { + [self enhanceValueFromNotableAddresses:exception]; + } NSArray *crashInfoMessages = [self crashInfoMessagesFromBinaryImages]; @@ -518,6 +521,36 @@ - (SentryException *)parseNSException return [[SentryException alloc] initWithValue:reason type:type]; } +/// EXC_BREAKPOINT with a top frame in AppKit indicates @c _crashOnException:. +- (BOOL)isMachExceptionFromAppKitCrashOnException +{ + if ([self.threads count] == 0 || self.crashedThreadIndex >= [self.threads count]) { + return NO; + } + + NSString *exceptionType = self.exceptionContext[@"type"]; + if (![exceptionType isEqualToString:@"mach"] && ![exceptionType isEqualToString:@"signal"]) { + return NO; + } + + if (![self.exceptionContext[@"mach"][@"exception_name"] isEqualToString:@"EXC_BREAKPOINT"]) { + return NO; + } + + // Only check top frames; AppKit is always deeper in the stack on macOS main-thread crashes. + NSArray *frames = [self rawStackTraceForThreadIndex:self.crashedThreadIndex]; + NSUInteger limit = MIN(frames.count, 3u); + for (NSUInteger i = 0; i < limit; i++) { + uintptr_t addr = (uintptr_t)[frames[i][@"instruction_addr"] unsignedLongLongValue]; + NSDictionary *image = [self binaryImageForAddress:addr]; + if (image != nil && [image[@"name"] rangeOfString:@"AppKit"].location != NSNotFound) { + return YES; + } + } + + return NO; +} + - (void)enhanceValueFromNotableAddresses:(SentryException *)exception { // Gatekeeper fixes https://github.com/getsentry/sentry-cocoa/issues/231 diff --git a/Tests/SentryTests/SentryCrashReportConverterTests.m b/Tests/SentryTests/SentryCrashReportConverterTests.m index 9e5fdfa2ef..8c5dba68b5 100644 --- a/Tests/SentryTests/SentryCrashReportConverterTests.m +++ b/Tests/SentryTests/SentryCrashReportConverterTests.m @@ -1158,4 +1158,237 @@ - (void)testUserException_whenCrashInfoMessagePresent_shouldPreserveOriginalReas XCTAssertEqualObjects(messages.firstObject, unrelatedCrashInfo); } +#pragma mark - NSException falling through to mach monitor + +- (void)testMachException_whenCrashedViaCrashOnException_shouldNotEnhanceWithNotableAddresses +{ + // -- Arrange -- + // Simulates an NSException that fell through to the mach monitor via AppKit's + // _crashOnException:. The notable addresses contain garbage (stack addresses, + // format strings) that would overwrite the mach exception value. + NSDictionary *mockReport = @{ + @"crash" : @ { + @"threads" : @[ @{ + @"index" : @0, + @"crashed" : @YES, + @"current_thread" : @YES, + @"backtrace" : @ { + @"contents" : @[ + @{ @"instruction_addr" : @0x1A2C6DC7C }, + @{ @"instruction_addr" : @0x1020C9ED8 } + ] + }, + @"notable_addresses" : @ { + @"r14" : + @ { @"type" : @"string", @"value" : @"0x000000019de89d54 start + 7184" }, + @"stack1" : @ { + @"type" : @"string", + @"value" : @"terminating with %s exception of type %s" + } + } + } ], + @"error" : @ { + @"type" : @"mach", + @"mach" : @ { + @"exception" : @6, + @"exception_name" : @"EXC_BREAKPOINT", + @"code" : @1, + @"subcode" : @0 + } + } + }, + @"binary_images" : @[ @{ + @"name" : @"/System/Library/Frameworks/AppKit.framework/Versions/C/AppKit", + @"image_addr" : @0x1A26E3000, + @"image_size" : @0x1740000 + } ], + @"system" : @ { @"application_stats" : @ { @"application_in_foreground" : @YES } } + }; + + // -- Act -- + SentryCrashReportConverter *reportConverter = + [[SentryCrashReportConverter alloc] initWithReport:mockReport inAppLogic:self.inAppLogic]; + SentryEvent *event = [reportConverter convertReportToEvent]; + + // -- Assert -- + SentryException *exception = event.exceptions.firstObject; + XCTAssertEqualObjects(exception.type, @"EXC_BREAKPOINT"); + XCTAssertFalse([exception.value containsString:@"start + 7184"], + @"Mach exception caused by _crashOnException: must not have its value overwritten by " + @"notable addresses. Got: %@", + exception.value); + XCTAssertFalse([exception.value containsString:@"terminating with"], + @"Mach exception caused by _crashOnException: must not have its value overwritten by " + @"notable addresses. Got: %@", + exception.value); + XCTAssertTrue([exception.value containsString:@"Exception 6"], + @"Should keep original mach exception value. Got: %@", exception.value); +} + +- (void) + testMachException_whenCrashedViaCrashOnExceptionWithoutSymbolNames_shouldNotEnhanceWithNotableAddresses +{ + // -- Arrange -- + // Real mach crash reports have no symbol_name (resolved server-side). Detection falls back + // to checking for EXC_BREAKPOINT + AppKit frame by instruction address range. + NSDictionary *mockReport = @{ + @"crash" : @ { + @"threads" : @[ @{ + @"index" : @0, + @"crashed" : @YES, + @"current_thread" : @YES, + @"backtrace" : @ { + @"contents" : @[ + @{ @"instruction_addr" : @0x1A2C6DC7C }, + @{ @"instruction_addr" : @0x1020C9ED8 } + ] + }, + @"notable_addresses" : @ { + @"r14" : + @ { @"type" : @"string", @"value" : @"0x000000019de89d54 start + 7184" } + } + } ], + @"error" : @ { + @"type" : @"mach", + @"mach" : @ { + @"exception" : @6, + @"exception_name" : @"EXC_BREAKPOINT", + @"code" : @1, + @"subcode" : @0 + } + } + }, + @"binary_images" : @[ @{ + @"name" : @"/System/Library/Frameworks/AppKit.framework/Versions/C/AppKit", + @"image_addr" : @0x1A26E3000, + @"image_size" : @0x1740000 + } ], + @"system" : @ { @"application_stats" : @ { @"application_in_foreground" : @YES } } + }; + + // -- Act -- + SentryCrashReportConverter *reportConverter = + [[SentryCrashReportConverter alloc] initWithReport:mockReport inAppLogic:self.inAppLogic]; + SentryEvent *event = [reportConverter convertReportToEvent]; + + // -- Assert -- + SentryException *exception = event.exceptions.firstObject; + XCTAssertFalse([exception.value containsString:@"start + 7184"], + @"EXC_BREAKPOINT in AppKit should not be enhanced with notable addresses. Got: %@", + exception.value); + XCTAssertTrue([exception.value containsString:@"Exception 6"], + @"Should keep original mach exception value. Got: %@", exception.value); +} + +- (void) + testMachException_whenEXCBreakpointWithAppKitDeeperInStack_shouldStillEnhanceWithNotableAddresses +{ + // -- Arrange -- + // Simulates a Swift runtime trap (e.g., force-unwrap nil) on the main thread of a macOS app. + // The crash frame (0) is in libswiftCore, but AppKit is present deeper in the stack because + // the code was invoked from an AppKit event handler. This must NOT be mistaken for + // _crashOnException: — notable addresses may contain the Swift assertion message. + NSDictionary *mockReport = @{ + @"crash" : @ { + @"threads" : @[ @{ + @"index" : @0, + @"crashed" : @YES, + @"current_thread" : @YES, + @"backtrace" : @ { + @"contents" : @[ + @{ @"instruction_addr" : @0x1900A0000 }, // frame 0: libswiftCore + @{ @"instruction_addr" : @0x100004000 }, // frame 1: MyApp + @{ @"instruction_addr" : @0x100004100 }, // frame 2: MyApp + @{ @"instruction_addr" : @0x1A2C6DC7C } // frame 3+: AppKit (event dispatch) + ] + }, + @"notable_addresses" : + @ { @"r14" : @ { @"type" : @"string", @"value" : @"unexpectedly found nil" } } + } ], + @"error" : @ { + @"type" : @"mach", + @"mach" : @ { + @"exception" : @6, + @"exception_name" : @"EXC_BREAKPOINT", + @"code" : @1, + @"subcode" : @0 + } + } + }, + @"binary_images" : @[ + @{ + @"name" : @"/usr/lib/swift/libswiftCore.dylib", + @"image_addr" : @0x190000000, + @"image_size" : @0x400000 + }, + @{ + @"name" : @"/System/Library/Frameworks/AppKit.framework/Versions/C/AppKit", + @"image_addr" : @0x1A26E3000, + @"image_size" : @0x1740000 + } + ], + @"system" : @ { @"application_stats" : @ { @"application_in_foreground" : @YES } } + }; + + // -- Act -- + SentryCrashReportConverter *reportConverter = + [[SentryCrashReportConverter alloc] initWithReport:mockReport inAppLogic:self.inAppLogic]; + SentryEvent *event = [reportConverter convertReportToEvent]; + + // -- Assert -- + SentryException *exception = event.exceptions.firstObject; + XCTAssertEqualObjects(exception.type, @"EXC_BREAKPOINT"); + XCTAssertTrue([exception.value containsString:@"unexpectedly found nil"], + @"EXC_BREAKPOINT with AppKit only deeper in the stack should still enhance with notable " + @"addresses (Swift runtime trap, not _crashOnException:). Got: %@", + exception.value); +} + +- (void)testMachException_withoutCrashOnException_shouldStillEnhanceWithNotableAddresses +{ + // -- Arrange -- + // A regular mach crash (e.g., EXC_BAD_ACCESS) — notable addresses provide useful context. + NSDictionary *mockReport = @{ + @"crash" : @ { + @"threads" : @[ @{ + @"index" : @0, + @"crashed" : @YES, + @"current_thread" : @YES, + @"backtrace" : @ { + @"contents" : @[ @{ + @"instruction_addr" : @0x1000, + @"symbol_name" : @"doCrash", + @"object_name" : @"MyApp" + } ] + }, + @"notable_addresses" : + @ { @"r14" : @ { @"type" : @"string", @"value" : @"objectAtIndex:" } } + } ], + @"error" : @ { + @"type" : @"mach", + @"mach" : @ { + @"exception" : @1, + @"exception_name" : @"EXC_BAD_ACCESS", + @"code" : @1, + @"subcode" : @0 + } + } + }, + @"binary_images" : @[], + @"system" : @ { @"application_stats" : @ { @"application_in_foreground" : @YES } } + }; + + // -- Act -- + SentryCrashReportConverter *reportConverter = + [[SentryCrashReportConverter alloc] initWithReport:mockReport inAppLogic:self.inAppLogic]; + SentryEvent *event = [reportConverter convertReportToEvent]; + + // -- Assert -- + SentryException *exception = event.exceptions.firstObject; + XCTAssertEqualObjects(exception.type, @"EXC_BAD_ACCESS"); + XCTAssertTrue([exception.value containsString:@"objectAtIndex:"], + @"Regular mach exception should still be enhanced with notable addresses. Got: %@", + exception.value); +} + @end