diff --git a/BUILD.gn b/BUILD.gn index 17c380e5..0b16a6df 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -45,7 +45,6 @@ if (crashpad_is_in_chromium || crashpad_is_in_fuchsia) { "handler:handler_test", "minidump:minidump_test", "snapshot:snapshot_test", - "util:util_test", ] } if (crashpad_is_in_fuchsia) { @@ -63,9 +62,7 @@ if (crashpad_is_in_chromium || crashpad_is_in_fuchsia) { "util/net/testdata/binary_http_body.dat", ] - outputs = [ - "$root_out_dir/crashpad_test_data/{{source}}", - ] + outputs = [ "$root_out_dir/crashpad_test_data/{{source}}" ] } deps += [ ":crashpad_test_data" ] @@ -222,9 +219,6 @@ if (crashpad_is_in_chromium || crashpad_is_in_fuchsia) { "test:gmock_main", "util:util_test", ] - if (crashpad_is_ios) { - deps -= [ "util:util_test" ] - } } } diff --git a/test/ios/crash_type_xctest.mm b/test/ios/crash_type_xctest.mm index 5369e3cf..45291243 100644 --- a/test/ios/crash_type_xctest.mm +++ b/test/ios/crash_type_xctest.mm @@ -193,6 +193,21 @@ XCTAssertTrue(_app.state == XCUIApplicationStateRunningForeground); } +- (void)testCatchUIGestureEnvironmentNSException { + XCTAssertTrue(_app.state == XCUIApplicationStateRunningForeground); + + // Tap the button with the string UIGestureEnvironmentException. + [_app.buttons[@"UIGestureEnvironmentException"] tap]; + + // Confirm the app is not running. + XCTAssertTrue([_app waitForState:XCUIApplicationStateNotRunning timeout:15]); + XCTAssertTrue(_app.state == XCUIApplicationStateNotRunning); + + // TODO: Query the app for crash data + [_app launch]; + XCTAssertTrue(_app.state == XCUIApplicationStateRunningForeground); +} + - (void)testCatchNSException { XCTAssertTrue(_app.state == XCUIApplicationStateRunningForeground); diff --git a/test/ios/host/cptest_crash_view_controller.mm b/test/ios/host/cptest_crash_view_controller.mm index 90f92dae..8e49b027 100644 --- a/test/ios/host/cptest_crash_view_controller.mm +++ b/test/ios/host/cptest_crash_view_controller.mm @@ -22,6 +22,40 @@ - (void)loadView { self.view = [[UIView alloc] init]; + + UIStackView* buttonStack = [[UIStackView alloc] init]; + buttonStack.axis = UILayoutConstraintAxisVertical; + buttonStack.spacing = 6; + + UIButton* button = [UIButton new]; + [button setTitle:@"UIGestureEnvironmentException" + forState:UIControlStateNormal]; + UITapGestureRecognizer* tapGesture = [[UITapGestureRecognizer alloc] + initWithTarget:self + action:@selector(throwUIGestureEnvironmentException)]; + [button addGestureRecognizer:tapGesture]; + [button setTranslatesAutoresizingMaskIntoConstraints:NO]; + [button.widthAnchor constraintEqualToConstant:16.0].active = YES; + [button.heightAnchor constraintEqualToConstant:16.0].active = YES; + + [buttonStack addArrangedSubview:button]; + + [self.view addSubview:buttonStack]; + + [buttonStack setTranslatesAutoresizingMaskIntoConstraints:NO]; + + [NSLayoutConstraint activateConstraints:@[ + [buttonStack.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor], + [buttonStack.topAnchor constraintEqualToAnchor:self.view.topAnchor], + [buttonStack.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], + [buttonStack.trailingAnchor + constraintEqualToAnchor:self.view.trailingAnchor], + ]]; +} + +- (void)throwUIGestureEnvironmentException { + NSArray* empty_array = @[]; + [empty_array objectAtIndex:42]; } - (void)viewDidLoad { diff --git a/util/BUILD.gn b/util/BUILD.gn index 18cc112f..183a0a82 100644 --- a/util/BUILD.gn +++ b/util/BUILD.gn @@ -548,7 +548,7 @@ static_library("util") { configs += [ "..:disable_ubsan" ] } -if (!crashpad_is_android) { +if (!crashpad_is_android && !crashpad_is_ios) { crashpad_executable("http_transport_test_server") { testonly = true sources = [ "net/http_transport_test_server.cc" ] @@ -639,13 +639,13 @@ source_set("util_test") { "thread/worker_thread_test.cc", ] - if (!crashpad_is_android) { + if (!crashpad_is_android && !crashpad_is_ios) { # Android requires an HTTPTransport implementation. sources += [ "net/http_transport_test.cc" ] } if (crashpad_is_posix || crashpad_is_fuchsia) { - if (!crashpad_is_fuchsia) { + if (!crashpad_is_fuchsia && !crashpad_is_ios) { sources += [ "posix/process_info_test.cc", "posix/signals_test.cc", @@ -680,6 +680,27 @@ source_set("util_test") { ] } + if (crashpad_is_ios) { + sources += [ "ios/exception_processor_test.mm" ] + + sources -= [ + "file/directory_reader_test.cc", + "file/file_io_test.cc", + "file/filesystem_test.cc", + "misc/capture_context_test.cc", + "misc/clock_test.cc", + "misc/paths_test.cc", + "net/http_body_test.cc", + "net/http_multipart_builder_test.cc", + "process/process_memory_range_test.cc", + "process/process_memory_test.cc", + "stream/file_encoder_test.cc", + "synchronization/semaphore_test.cc", + "thread/thread_test.cc", + "thread/worker_thread_test.cc", + ] + } + if (crashpad_is_linux || crashpad_is_android) { sources += [ "linux/auxiliary_vector_test.cc", @@ -739,7 +760,7 @@ source_set("util_test") { deps += [ "../third_party/lss" ] } - if (!crashpad_is_android) { + if (!crashpad_is_android && !crashpad_is_ios) { data_deps = [ ":http_transport_test_server" ] if (crashpad_use_boringssl_for_http_transport_socket) { diff --git a/util/ios/exception_processor.mm b/util/ios/exception_processor.mm index 818d4980..f767fb14 100644 --- a/util/ios/exception_processor.mm +++ b/util/ios/exception_processor.mm @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -109,6 +110,14 @@ bool ModulePathMatchesSinkhole(const char* path, const char* sinkhole) { #endif } +int LoggingUnwStep(unw_cursor_t* cursor) { + int rv = unw_step(cursor); + if (rv < 0) { + LOG(ERROR) << "unw_step: " << rv; + } + return rv; +} + id ObjcExceptionPreprocessor(id exception) { // Unwind the stack looking for any exception handlers. If an exception // handler is encountered, test to see if it is a function known to catch- @@ -152,7 +161,7 @@ id ObjcExceptionPreprocessor(id exception) { exception_header->unwindHeader.exception_class = kOurExceptionClass; bool handler_found = false; - while (unw_step(&cursor) > 0) { + while (LoggingUnwStep(&cursor) > 0) { unw_proc_info_t frame_info; if (unw_get_proc_info(&cursor, &frame_info) != UNW_ESUCCESS) { continue; @@ -226,14 +235,14 @@ id ObjcExceptionPreprocessor(id exception) { // Everything in this library is a sinkhole, specifically // _dispatch_client_callout. Both are needed here depending on whether // the debugger is attached (introspection only appears when a simulator - // is attached to a debugger. - // only). + // is attached to a debugger). "/usr/lib/system/introspection/libdispatch.dylib", "/usr/lib/system/libdispatch.dylib", // __CFRunLoopDoTimers and __CFRunLoopRun are sinkholes. Consider also // checking that a few frames up is CFRunLoopRunSpecific(). - "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation"}; + "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation", + }; Dl_info dl_info; if (dladdr(reinterpret_cast(frame_info.start_ip), &dl_info) != @@ -245,6 +254,44 @@ id ObjcExceptionPreprocessor(id exception) { } } + // Some sinkholes are harder to find. _UIGestureEnvironmentUpdate + // in UIKitCore is an example. UIKitCore can't be added to + // kExceptionLibraryPathSinkholes because it uses Objective-C exceptions + // internally and also has has non-sinkhole handlers. Since + // _UIGestureEnvironmentUpdate is always called from + // -[UIGestureEnvironment _deliverEvent:toGestureRecognizers:usingBlock:], + // inspect the caller frame info to match the sinkhole. + constexpr const char* kUIKitCorePath = + "/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore"; + if (ModulePathMatchesSinkhole(dl_info.dli_fname, kUIKitCorePath)) { + unw_proc_info_t caller_frame_info; + if (LoggingUnwStep(&cursor) > 0 && + unw_get_proc_info(&cursor, &caller_frame_info) == UNW_ESUCCESS) { + static IMP uigesture_deliver_event_imp = [] { + IMP imp = class_getMethodImplementation( + NSClassFromString(@"UIGestureEnvironment"), + NSSelectorFromString( + @"_deliverEvent:toGestureRecognizers:usingBlock:")); + + // From 10.15.0 objc4-779.1/runtime/objc-class.mm + // class_getMethodImplementation returns nil or _objc_msgForward on + // failure. + if (!imp || imp == _objc_msgForward) { + LOG(WARNING) << "Unable to find -[UIGestureEnvironment " + "_deliverEvent:toGestureRecognizers:usingBlock:]"; + return reinterpret_cast(NULL); + } + return imp; + }(); + + if (uigesture_deliver_event_imp == + reinterpret_cast(caller_frame_info.start_ip)) { + TerminatingFromUncaughtNSException(exception, + "_UIGestureEnvironmentUpdate"); + } + } + } + handler_found = true; break; diff --git a/util/ios/exception_processor_test.mm b/util/ios/exception_processor_test.mm new file mode 100644 index 00000000..c9aa7169 --- /dev/null +++ b/util/ios/exception_processor_test.mm @@ -0,0 +1,41 @@ +// Copyright 2020 The Crashpad Authors. All rights reserved. +// +// 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 +#include +#include + +#include "gtest/gtest.h" +#include "testing/platform_test.h" + +namespace crashpad { +namespace test { +namespace { + +using IOSExceptionProcessor = PlatformTest; + +TEST_F(IOSExceptionProcessor, SelectorExists) { + IMP uigesture_deliver_event_imp = class_getMethodImplementation( + NSClassFromString(@"UIGestureEnvironment"), + NSSelectorFromString(@"_deliverEvent:toGestureRecognizers:usingBlock:")); + + // From 10.15.0 objc4-779.1/runtime/objc-class.mm + // class_getMethodImplementation returns nil or _objc_msgForward on failure. + ASSERT_TRUE(uigesture_deliver_event_imp); + ASSERT_NE(uigesture_deliver_event_imp, _objc_msgForward); +} + +} // namespace +} // namespace test +} // namespace crashpad