blob: 646c9dfd4d8163b43529c8493aae8192ae3c7adf [file] [log] [blame]
// Copyright 2020 The Crashpad Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import <XCTest/XCTest.h>
#include <objc/runtime.h>
#include <sys/sysctl.h>
#include <vector>
#import "Service/Sources/EDOClientService.h"
#include "build/build_config.h"
#include "client/length_delimited_ring_buffer.h"
#import "test/ios/host/cptest_shared_object.h"
#include "util/mac/sysctl.h"
#include "util/mach/exception_types.h"
#include "util/mach/mach_extensions.h"
namespace crashpad {
namespace {
#if TARGET_OS_SIMULATOR
// macOS 14.0 is 23A344, macOS 13.6.5 is 22G621, so if the first two characters
// in the kern.osversion are > 22, this build will reproduce the simulator bug
// in crbug.com/328282286
bool IsMacOSVersion143OrGreaterAndiOS16OrLess() {
if (__builtin_available(iOS 17, *)) {
return false;
}
std::string build = crashpad::ReadStringSysctlByName("kern.osversion", false);
return std::stoi(build.substr(0, 2)) > 22;
}
#endif
} // namespace
} // namespace crashpad
@interface CPTestTestCase : XCTestCase {
XCUIApplication* app_;
CPTestSharedObject* rootObject_;
}
@end
@implementation CPTestTestCase
+ (void)setUp {
[CPTestTestCase swizzleHandleCrashUnderSymbol];
[CPTestTestCase swizleMayTerminateOutOfBandWithoutCrashReport];
// Override EDO default error handler. Without this, the default EDO error
// handler will throw an error and fail the test.
EDOSetClientErrorHandler(^(NSError* error){
// Do nothing.
});
}
// Swizzle away the -[XCUIApplicationImpl handleCrashUnderSymbol:] callback.
// Without this, any time the host app is intentionally crashed, the test is
// immediately failed.
+ (void)swizzleHandleCrashUnderSymbol {
SEL originalSelector = NSSelectorFromString(@"handleCrashUnderSymbol:");
SEL swizzledSelector = @selector(handleCrashUnderSymbol:);
Method originalMethod = class_getInstanceMethod(
objc_getClass("XCUIApplicationImpl"), originalSelector);
Method swizzledMethod =
class_getInstanceMethod([self class], swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
}
// Swizzle away the time consuming 'Checking for crash reports corresponding to'
// from -[XCUIApplicationProcess swizleMayTerminateOutOfBandWithoutCrashReport]
// that is unnecessary for these tests.
+ (void)swizleMayTerminateOutOfBandWithoutCrashReport {
SEL originalSelector =
NSSelectorFromString(@"mayTerminateOutOfBandWithoutCrashReport");
SEL swizzledSelector = @selector(mayTerminateOutOfBandWithoutCrashReport);
Method originalMethod = class_getInstanceMethod(
objc_getClass("XCUIApplicationProcess"), originalSelector);
Method swizzledMethod =
class_getInstanceMethod([self class], swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
}
// This gets called after tearDown, so there's no straightforward way to
// test that this is called. However, not swizzling this out will cause every
// crashing test to fail.
- (void)handleCrashUnderSymbol:(id)arg1 {
}
- (BOOL)mayTerminateOutOfBandWithoutCrashReport {
return YES;
}
- (void)setUp {
app_ = [[XCUIApplication alloc] init];
[app_ launch];
rootObject_ = [EDOClientService rootObjectWithPort:12345];
[rootObject_ clearPendingReports];
XCTAssertEqual([rootObject_ pendingReportCount], 0);
XCTAssertTrue(app_.state == XCUIApplicationStateRunningForeground);
}
- (void)verifyCrashReportException:(uint32_t)exception {
// Confirm the app is not running.
XCTAssertTrue([app_ waitForState:XCUIApplicationStateNotRunning timeout:15]);
XCTAssertTrue(app_.state == XCUIApplicationStateNotRunning);
// Restart app to get the report signal.
[app_ launch];
XCTAssertTrue(app_.state == XCUIApplicationStateRunningForeground);
rootObject_ = [EDOClientService rootObjectWithPort:12345];
XCTAssertEqual([rootObject_ pendingReportCount], 1);
NSNumber* report_exception;
XCTAssertTrue([rootObject_ pendingReportException:&report_exception]);
XCTAssertEqual(report_exception.unsignedIntValue, exception);
NSString* rawLogContents = [rootObject_ rawLogContents];
XCTAssertFalse([rawLogContents containsString:@"allocator used in handler."]);
}
- (void)testEDO {
NSString* result = [rootObject_ testEDO];
XCTAssertEqualObjects(result, @"crashpad");
}
- (void)testKillAbort {
[rootObject_ crashKillAbort];
[self verifyCrashReportException:EXC_SOFT_SIGNAL];
NSNumber* report_exception;
XCTAssertTrue([rootObject_ pendingReportExceptionInfo:&report_exception]);
XCTAssertEqual(report_exception.intValue, SIGABRT);
}
- (void)testTrap {
[rootObject_ crashTrap];
#if defined(ARCH_CPU_X86_64)
[self verifyCrashReportException:EXC_BAD_INSTRUCTION];
#elif defined(ARCH_CPU_ARM64)
[self verifyCrashReportException:EXC_BREAKPOINT];
#else
#error Port to your CPU architecture
#endif
}
- (void)testAbort {
[rootObject_ crashAbort];
[self verifyCrashReportException:EXC_SOFT_SIGNAL];
NSNumber* report_exception;
XCTAssertTrue([rootObject_ pendingReportExceptionInfo:&report_exception]);
XCTAssertEqual(report_exception.intValue, SIGABRT);
}
- (void)testBadAccess {
[rootObject_ crashBadAccess];
[self verifyCrashReportException:EXC_BAD_ACCESS];
}
- (void)testException {
[rootObject_ crashException];
// After https://reviews.llvm.org/D141222 exceptions call
// __libcpp_verbose_abort, which Chromium sets to `brk 0` in release.
// After https://crrev.com/c/5375084, Chromium does not set `brk 0` for local
// release builds and official DCHECK builds.
#if defined(CRASHPAD_IS_IN_CHROMIUM) && defined(NDEBUG) && \
defined(OFFICIAL_BUILD) && !defined(DCHECK_ALWAYS_ON)
[self verifyCrashReportException:SIGABRT];
#else
[self verifyCrashReportException:EXC_SOFT_SIGNAL];
NSNumber* report_exception;
XCTAssertTrue([rootObject_ pendingReportExceptionInfo:&report_exception]);
XCTAssertEqual(report_exception.intValue, SIGABRT);
#endif
}
- (void)testNSException {
[rootObject_ crashNSException];
[self verifyCrashReportException:crashpad::kMachExceptionFromNSException];
NSDictionary* dict = [rootObject_ getAnnotations];
NSString* userInfo =
[dict[@"objects"][0] valueForKeyPath:@"exceptionUserInfo"];
XCTAssertTrue([userInfo containsString:@"Error Object=<CPTestSharedObject"]);
XCTAssertTrue([[dict[@"objects"][1] valueForKeyPath:@"exceptionReason"]
isEqualToString:@"Intentionally throwing error."]);
XCTAssertTrue([[dict[@"objects"][2] valueForKeyPath:@"exceptionName"]
isEqualToString:@"NSInternalInconsistencyException"]);
}
- (void)testNotAnNSException {
[rootObject_ crashNotAnNSException];
// When @throwing something other than an NSException the
// UncaughtExceptionHandler is not called, so the application SIGABRTs.
[self verifyCrashReportException:EXC_SOFT_SIGNAL];
NSNumber* report_exception;
XCTAssertTrue([rootObject_ pendingReportExceptionInfo:&report_exception]);
XCTAssertEqual(report_exception.intValue, SIGABRT);
}
- (void)testUnhandledNSException {
[rootObject_ crashUnhandledNSException];
[self verifyCrashReportException:crashpad::kMachExceptionFromNSException];
NSDictionary* dict = [rootObject_ getAnnotations];
NSString* uncaught_flag =
[dict[@"objects"][0] valueForKeyPath:@"UncaughtNSException"];
XCTAssertTrue([uncaught_flag containsString:@"true"]);
NSString* userInfo =
[dict[@"objects"][1] valueForKeyPath:@"exceptionUserInfo"];
XCTAssertTrue([userInfo containsString:@"Error Object=<CPTestSharedObject"]);
XCTAssertTrue([[dict[@"objects"][2] valueForKeyPath:@"exceptionReason"]
isEqualToString:@"Intentionally throwing error."]);
XCTAssertTrue([[dict[@"objects"][3] valueForKeyPath:@"exceptionName"]
isEqualToString:@"NSInternalInconsistencyException"]);
}
- (void)testcrashUnrecognizedSelectorAfterDelay {
[rootObject_ crashUnrecognizedSelectorAfterDelay];
[self verifyCrashReportException:crashpad::kMachExceptionFromNSException];
NSDictionary* dict = [rootObject_ getAnnotations];
XCTAssertTrue([[dict[@"objects"][0] valueForKeyPath:@"exceptionReason"]
containsString:
@"CPTestSharedObject does_not_exist]: unrecognized selector"]);
XCTAssertTrue([[dict[@"objects"][1] valueForKeyPath:@"exceptionName"]
isEqualToString:@"NSInvalidArgumentException"]);
}
- (void)testCatchUIGestureEnvironmentNSException {
// Tap the button with the string UIGestureEnvironmentException.
[app_.buttons[@"UIGestureEnvironmentException"] tap];
[self verifyCrashReportException:crashpad::kMachExceptionFromNSException];
NSDictionary* dict = [rootObject_ getAnnotations];
XCTAssertTrue([[dict[@"objects"][0] valueForKeyPath:@"exceptionReason"]
containsString:@"NSArray0 objectAtIndex:]: index 42 beyond bounds"]);
XCTAssertTrue([[dict[@"objects"][1] valueForKeyPath:@"exceptionName"]
isEqualToString:@"NSRangeException"]);
}
- (void)testCatchNSException {
[rootObject_ catchNSException];
// The app should not crash
XCTAssertTrue(app_.state == XCUIApplicationStateRunningForeground);
// No report should be generated.
[rootObject_ processIntermediateDumps];
XCTAssertEqual([rootObject_ pendingReportCount], 0);
}
- (void)testCrashCoreAutoLayoutSinkhole {
[rootObject_ crashCoreAutoLayoutSinkhole];
[self verifyCrashReportException:crashpad::kMachExceptionFromNSException];
NSDictionary* dict = [rootObject_ getAnnotations];
XCTAssertTrue([[dict[@"objects"][0] valueForKeyPath:@"exceptionReason"]
containsString:@"Unable to activate constraint with anchors"]);
XCTAssertTrue([[dict[@"objects"][1] valueForKeyPath:@"exceptionName"]
isEqualToString:@"NSGenericException"]);
}
- (void)testRecursion {
[rootObject_ crashRecursion];
[self verifyCrashReportException:EXC_BAD_ACCESS];
}
- (void)testClientAnnotations {
[rootObject_ crashKillAbort];
// Set app launch args to trigger different client annotations.
NSArray<NSString*>* old_args = app_.launchArguments;
app_.launchArguments = @[ @"--alternate-client-annotations" ];
[self verifyCrashReportException:EXC_SOFT_SIGNAL];
NSNumber* report_exception;
XCTAssertTrue([rootObject_ pendingReportExceptionInfo:&report_exception]);
XCTAssertEqual(report_exception.intValue, SIGABRT);
app_.launchArguments = old_args;
// Confirm the initial crash took the standard annotations.
NSDictionary* dict = [rootObject_ getProcessAnnotations];
XCTAssertTrue([dict[@"crashpad"] isEqualToString:@"yes"]);
XCTAssertTrue([dict[@"plat"] isEqualToString:@"iOS"]);
XCTAssertTrue([dict[@"prod"] isEqualToString:@"xcuitest"]);
XCTAssertTrue([dict[@"ver"] isEqualToString:@"1"]);
// Confirm passing alternate client annotation args works.
[rootObject_ clearPendingReports];
[rootObject_ crashKillAbort];
[self verifyCrashReportException:EXC_SOFT_SIGNAL];
XCTAssertTrue([rootObject_ pendingReportExceptionInfo:&report_exception]);
XCTAssertEqual(report_exception.intValue, SIGABRT);
dict = [rootObject_ getProcessAnnotations];
XCTAssertTrue([dict[@"crashpad"] isEqualToString:@"no"]);
XCTAssertTrue([dict[@"plat"] isEqualToString:@"macOS"]);
XCTAssertTrue([dict[@"prod"] isEqualToString:@"some_app"]);
XCTAssertTrue([dict[@"ver"] isEqualToString:@"42"]);
}
#if TARGET_OS_SIMULATOR
- (void)testCrashWithCrashInfoMessage {
if (@available(iOS 15.0, *)) {
// Figure out how to test this on iOS15.
return;
}
[rootObject_ crashWithCrashInfoMessage];
[self verifyCrashReportException:EXC_BAD_ACCESS];
NSDictionary* dict = [rootObject_ getAnnotations];
NSString* dyldMessage = dict[@"vector"][0];
XCTAssertTrue([dyldMessage isEqualToString:@"dyld: in dlsym()"]);
}
#endif
// TODO(justincohen): Codesign crashy_initializer.so so it can run on devices.
#if TARGET_OS_SIMULATOR
- (void)testCrashWithDyldErrorString {
if (@available(iOS 15.0, *)) {
// iOS 15 uses dyld4, which doesn't use CRSetCrashLogMessage2
return;
}
[rootObject_ crashWithDyldErrorString];
#if defined(ARCH_CPU_X86_64)
[self verifyCrashReportException:EXC_BAD_INSTRUCTION];
#elif defined(ARCH_CPU_ARM64)
[self verifyCrashReportException:EXC_BREAKPOINT];
#else
#error Port to your CPU architecture
#endif
NSArray* vector = [rootObject_ getAnnotations][@"vector"];
// This message is set by dyld-353.2.1/src/ImageLoaderMachO.cpp
// ImageLoaderMachO::doInitialization().
NSString* module = @"crashpad_snapshot_test_module_crashy_initializer.so";
XCTAssertTrue([vector[0] hasSuffix:module]);
}
#endif
- (void)testCrashWithAnnotations {
#if TARGET_OS_SIMULATOR
// This test will fail on older (<iOS17 simulators) when running on macOS 14.3
// or newer due to a bug in Simulator. crbug.com/328282286
if (crashpad::IsMacOSVersion143OrGreaterAndiOS16OrLess()) {
return;
}
#endif
[rootObject_ crashWithAnnotations];
[self verifyCrashReportException:EXC_SOFT_SIGNAL];
NSNumber* report_exception;
XCTAssertTrue([rootObject_ pendingReportExceptionInfo:&report_exception]);
XCTAssertEqual(report_exception.intValue, SIGABRT);
NSDictionary* dict = [rootObject_ getAnnotations];
NSDictionary* simpleMap = dict[@"simplemap"];
XCTAssertTrue([simpleMap[@"#TEST# empty_value"] isEqualToString:@""]);
XCTAssertTrue([simpleMap[@"#TEST# key"] isEqualToString:@"value"]);
XCTAssertTrue([simpleMap[@"#TEST# longer"] isEqualToString:@"shorter"]);
XCTAssertTrue([simpleMap[@"#TEST# pad"] isEqualToString:@"crash"]);
XCTAssertTrue([simpleMap[@"#TEST# x"] isEqualToString:@"y"]);
XCTAssertTrue([[dict[@"objects"][0] valueForKeyPath:@"#TEST# same-name"]
isEqualToString:@"same-name 4"]);
XCTAssertTrue([[dict[@"objects"][1] valueForKeyPath:@"#TEST# same-name"]
isEqualToString:@"same-name 3"]);
XCTAssertTrue([[dict[@"objects"][2] valueForKeyPath:@"#TEST# one"]
isEqualToString:@"moocow"]);
// Ensure `ring_buffer` is present but not `busy_ring_buffer`.
XCTAssertEqual(1u, [dict[@"ringbuffers"] count]);
NSData* ringBufferNSData =
[dict[@"ringbuffers"][0] valueForKeyPath:@"#TEST# ring_buffer"];
crashpad::RingBufferData ringBufferData;
XCTAssertTrue(ringBufferData.DeserializeFromBuffer(ringBufferNSData.bytes,
ringBufferNSData.length));
crashpad::LengthDelimitedRingBufferReader reader(ringBufferData);
std::vector<uint8_t> ringBufferEntry;
XCTAssertTrue(reader.Pop(ringBufferEntry));
NSString* firstEntry = [[NSString alloc] initWithBytes:ringBufferEntry.data()
length:ringBufferEntry.size()
encoding:NSUTF8StringEncoding];
XCTAssertEqualObjects(firstEntry, @"hello");
ringBufferEntry.clear();
XCTAssertTrue(reader.Pop(ringBufferEntry));
NSString* secondEntry = [[NSString alloc] initWithBytes:ringBufferEntry.data()
length:ringBufferEntry.size()
encoding:NSUTF8StringEncoding];
XCTAssertEqualObjects(secondEntry, @"goodbye");
ringBufferEntry.clear();
XCTAssertFalse(reader.Pop(ringBufferEntry));
}
- (void)testDumpWithoutCrash {
[rootObject_ generateDumpWithoutCrash:10 threads:3];
// The app should not crash
XCTAssertTrue(app_.state == XCUIApplicationStateRunningForeground);
XCTAssertEqual([rootObject_ pendingReportCount], 30);
}
- (void)testSimultaneousCrash {
[rootObject_ crashConcurrentSignalAndMach];
// Confirm the app is not running.
XCTAssertTrue([app_ waitForState:XCUIApplicationStateNotRunning timeout:15]);
XCTAssertTrue(app_.state == XCUIApplicationStateNotRunning);
[app_ launch];
XCTAssertTrue(app_.state == XCUIApplicationStateRunningForeground);
rootObject_ = [EDOClientService rootObjectWithPort:12345];
XCTAssertEqual([rootObject_ pendingReportCount], 1);
}
- (void)testSimultaneousNSException {
[rootObject_ catchConcurrentNSException];
// The app should not crash
XCTAssertTrue(app_.state == XCUIApplicationStateRunningForeground);
// No report should be generated.
[rootObject_ processIntermediateDumps];
XCTAssertEqual([rootObject_ pendingReportCount], 0);
}
- (void)testCrashInHandlerReentrant {
XCTAssertTrue(app_.state == XCUIApplicationStateRunningForeground);
rootObject_ = [EDOClientService rootObjectWithPort:12345];
[rootObject_ crashInHandlerReentrant];
// Confirm the app is not running.
XCTAssertTrue([app_ waitForState:XCUIApplicationStateNotRunning timeout:15]);
XCTAssertTrue(app_.state == XCUIApplicationStateNotRunning);
[app_ launch];
XCTAssertTrue(app_.state == XCUIApplicationStateRunningForeground);
rootObject_ = [EDOClientService rootObjectWithPort:12345];
XCTAssertEqual([rootObject_ pendingReportCount], 0);
NSString* rawLogContents = [rootObject_ rawLogContents];
NSString* errmsg = @"Cannot DumpExceptionFromSignal without writer";
XCTAssertTrue([rawLogContents containsString:errmsg]);
}
- (void)testFailureWhenHandlerAllocates {
XCTAssertTrue(app_.state == XCUIApplicationStateRunningForeground);
rootObject_ = [EDOClientService rootObjectWithPort:12345];
[rootObject_ allocateWithForbiddenAllocators];
// Confirm the app is not running.
XCTAssertTrue([app_ waitForState:XCUIApplicationStateNotRunning timeout:15]);
XCTAssertTrue(app_.state == XCUIApplicationStateNotRunning);
[app_ launch];
XCTAssertTrue(app_.state == XCUIApplicationStateRunningForeground);
rootObject_ = [EDOClientService rootObjectWithPort:12345];
XCTAssertEqual([rootObject_ pendingReportCount], 0);
NSString* rawLogContents = [rootObject_ rawLogContents];
XCTAssertTrue([rawLogContents containsString:@"allocator used in handler."]);
}
@end