Add another iOS library path sinkhole.

Add another sinkhole for _UIGestureEnvironmentUpdate.

Bug: crashpad:31
Change-Id: I4084ee3064b1e483ac2a426d5b30e40cc27594d0
Reviewed-on: https://chromium-review.googlesource.com/c/crashpad/crashpad/+/2145017
Commit-Queue: Justin Cohen <justincohen@chromium.org>
Reviewed-by: Mark Mentovai <mark@chromium.org>
Reviewed-by: Justin Cohen <justincohen@chromium.org>
Reviewed-by: Robert Sesek <rsesek@chromium.org>
GitOrigin-RevId: ea4af71c2ac7a2daa50e4d22bc48270b1a66c83a
diff --git a/BUILD.gn b/BUILD.gn
index 17c380e..0b16a6d 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -45,7 +45,6 @@
         "handler:handler_test",
         "minidump:minidump_test",
         "snapshot:snapshot_test",
-        "util:util_test",
       ]
     }
     if (crashpad_is_in_fuchsia) {
@@ -63,9 +62,7 @@
           "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 @@
       "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 5369e3c..4529124 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 90f92da..8e49b02 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 18cc112..183a0a8 100644
--- a/util/BUILD.gn
+++ b/util/BUILD.gn
@@ -548,7 +548,7 @@
   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 @@
     "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 @@
     ]
   }
 
+  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 @@
     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 818d498..f767fb1 100644
--- a/util/ios/exception_processor.mm
+++ b/util/ios/exception_processor.mm
@@ -20,6 +20,7 @@
 #include <dlfcn.h>
 #include <libunwind.h>
 #include <mach-o/loader.h>
+#include <objc/message.h>
 #include <objc/objc-exception.h>
 #include <objc/objc.h>
 #include <objc/runtime.h>
@@ -109,6 +110,14 @@
 #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 @@
   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 @@
         // 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<const void*>(frame_info.start_ip), &dl_info) !=
@@ -245,6 +254,44 @@
       }
     }
 
+    // Some <redacted> 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<IMP>(NULL);
+          }
+          return imp;
+        }();
+
+        if (uigesture_deliver_event_imp ==
+            reinterpret_cast<IMP>(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 0000000..c9aa716
--- /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 <Foundation/Foundation.h>
+#include <objc/message.h>
+#include <objc/runtime.h>
+
+#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