Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
35 changes: 34 additions & 1 deletion Sources/Sentry/SentryCrashReportConverter.m
Original file line number Diff line number Diff line change
Expand Up @@ -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<NSString *> *crashInfoMessages = [self crashInfoMessagesFromBinaryImages];

Expand Down Expand Up @@ -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) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nil image name falsely matches AppKit substring check

Low Severity

If image[@"name"] is nil, [nil rangeOfString:@"AppKit"] returns a zero-filled NSRange with location = 0. Since NSNotFound is NSIntegerMax, the comparison .location != NSNotFound evaluates to YES, causing a binary image with no name to be falsely identified as AppKit. This would make isMachExceptionFromAppKitCrashOnException return YES incorrectly, suppressing the notable address enhancement for crashes that aren't _crashOnException:.

Fix in Cursor Fix in Web

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there are images with no names, but we should cover ourselves just in case

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: Are we 100% that both conditions are enough (exception type and appkit present)?
We already removed _crashOnException from the stacktrace, right? If not adding that could help

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point, will check if we have better heuristics for this so we only filter out unusual messages.

return YES;
}
}

return NO;
}

- (void)enhanceValueFromNotableAddresses:(SentryException *)exception
{
// Gatekeeper fixes https://github.com/getsentry/sentry-cocoa/issues/231
Expand Down
233 changes: 233 additions & 0 deletions Tests/SentryTests/SentryCrashReportConverterTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading