chromium/third_party/crashpad/crashpad/test/ios/crash_type_xctest.mm

// 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