[vm] Add NOTIFY_DEBUGGER_ABOUT_RX_PAGES hook

Estimated: adds ~500ms of latency per code space page allocation.

TEST=manually

Change-Id: I31bc1927fd9a775c312974a1c435d29c875d01eb
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/412281
Reviewed-by: Siva Annamalai <asiva@google.com>
Commit-Queue: Slava Egorov <vegorov@google.com>
diff --git a/runtime/bin/file_macos.cc b/runtime/bin/file_macos.cc
index a469c9b..3cf27a0 100644
--- a/runtime/bin/file_macos.cc
+++ b/runtime/bin/file_macos.cc
@@ -94,9 +94,11 @@
       // Try to allocate near the VM's binary.
       hint = reinterpret_cast<void*>(&Dart_Initialize);
       prot = PROT_READ | PROT_EXEC;
-      if (IsAtLeastOS10_14()) {
+#if !defined(DART_HOST_OS_IOS)
+      if (IsAtLeastMacOSX10_14()) {
         map_flags |= (MAP_JIT | MAP_ANONYMOUS);
       }
+#endif
       break;
     case kReadWrite:
       prot = PROT_READ | PROT_WRITE;
@@ -107,10 +109,15 @@
     map_flags |= MAP_FIXED;
   }
   void* addr = start;
-  if ((type == kReadExecute) && IsAtLeastOS10_14()) {
-    // Due to codesigning restrictions, we cannot map the file as executable
-    // directly. We must first copy it into an anonymous mapping and then mark
-    // the mapping as executable.
+#if !defined(DART_HOST_OS_IOS)
+  // Due to codesigning restrictions, we cannot map the file as executable
+  // directly. We must first copy it into an anonymous mapping and then mark
+  // the mapping as executable.
+  const bool should_copy = (type == kReadExecute) && IsAtLeastMacOSX10_14();
+#else
+  const bool should_copy = false;
+#endif
+  if (should_copy) {
     if (addr == nullptr) {
       addr = mmap(hint, length, (PROT_READ | PROT_WRITE), map_flags, -1, 0);
       if (addr == MAP_FAILED) {
diff --git a/runtime/bin/virtual_memory_posix.cc b/runtime/bin/virtual_memory_posix.cc
index b7f0eaf..da2b075 100644
--- a/runtime/bin/virtual_memory_posix.cc
+++ b/runtime/bin/virtual_memory_posix.cc
@@ -45,7 +45,7 @@
 
   int map_flags = MAP_PRIVATE | MAP_ANONYMOUS;
 #if (defined(DART_HOST_OS_MACOS) && !defined(DART_HOST_OS_IOS))
-  if (is_executable && IsAtLeastOS10_14()) {
+  if (is_executable && IsAtLeastMacOSX10_14()) {
     map_flags |= MAP_JIT;
   }
 #endif  // defined(DART_HOST_OS_MACOS)
diff --git a/runtime/platform/utils_macos.cc b/runtime/platform/utils_macos.cc
index 082c1ce..01ed447 100644
--- a/runtime/platform/utils_macos.cc
+++ b/runtime/platform/utils_macos.cc
@@ -8,7 +8,6 @@
 #include "platform/utils.h"
 #include "platform/utils_macos.h"
 
-#include <errno.h>        // NOLINT
 #include <sys/utsname.h>  // NOLINT
 
 namespace dart {
@@ -81,10 +80,10 @@
 
 namespace internal {
 
-// Returns the running system's Darwin major version. Don't call this, it's
-// an implementation detail and its result is meant to be cached by
-// MacOSXMinorVersion.
-int32_t DarwinMajorVersionInternal() {
+namespace {
+// Extracts the version of the running kernel from utsname.release.
+bool GetDarwinKernelVersionFromUname(int32_t* kernel_major_version,
+                                     int32_t* kernel_minor_version) {
   // uname is implemented as a simple series of sysctl system calls to
   // obtain the relevant data from the kernel. The data is
   // compiled right into the kernel, so no threads or blocking or other
@@ -92,80 +91,103 @@
 
   struct utsname uname_info;
   if (uname(&uname_info) != 0) {
-    FATAL("Fatal error in DarwinMajorVersionInternal : invalid return uname");
-    return 0;
+    FATAL("GetDarwinKernelVersionFromUname: uname failed");
+    return false;
   }
 
   if (strcmp(uname_info.sysname, "Darwin") != 0) {
     FATAL(
-        "Fatal error in DarwinMajorVersionInternal : unexpected uname"
+        "GetDarwinKernelVersionFromUname: unexpected uname"
         " sysname '%s'",
         uname_info.sysname);
-    return 0;
+    return false;
   }
 
-  int32_t darwin_major_version = 0;
+  *kernel_major_version = 0;
+  *kernel_minor_version = 0;
   char* dot = strchr(uname_info.release, '.');
-  if (dot) {
-    errno = 0;
+  if (dot != nullptr && dot != uname_info.release) {
     char* end_ptr = nullptr;
-    darwin_major_version = strtol(uname_info.release, &end_ptr, 10);
-    if (errno != 0 || (end_ptr == uname_info.release)) {
-      dot = nullptr;
+    *kernel_major_version = strtol(uname_info.release, &end_ptr, 10);
+    if (end_ptr == dot) {  // Expected to parse until `.`
+      char* minor_start = dot + 1;
+      *kernel_minor_version = strtol(minor_start, &end_ptr, 10);
+      if (end_ptr != minor_start) {
+        return true;
+      }
     }
   }
 
-  if (!dot) {
-    FATAL(
-        "Fatal error in DarwinMajorVersionInternal :"
-        " could not parse uname release '%s'",
-        uname_info.release);
+  FATAL(
+      "GetDarwinKernelVersionFromUname: "
+      " could not parse uname release '%s'",
+      uname_info.release);
+  return false;
+}
+
+}  // namespace
+
+// Returns the running system's Mac OS X or iOS version which matches the
+// encoding of *_X_VERSION_* defines in AvailabilityVersions.h
+int32_t DarwinVersionInternal() {
+  int32_t kernel_major_version;
+  int32_t kernel_minor_version;
+  if (!GetDarwinKernelVersionFromUname(&kernel_major_version,
+                                       &kernel_minor_version)) {
     return 0;
   }
 
-  return darwin_major_version;
-}
+  int32_t major_version = 0;
+  int32_t minor_version = 0;
 
-// Returns the running system's Mac OS X version which matches the encoding
-// of MAC_OS_X_VERSION_* defines in AvailabilityMacros.h
-int32_t MacOSXVersionInternal() {
-  const int32_t darwin_major_version = DarwinMajorVersionInternal();
-
-  int32_t major_version;
-  int32_t minor_version;
-
-  if (darwin_major_version < 20) {
+#if defined(DART_HOST_OS_IOS)
+  // We do not expect to run on version of iOS <12.0 so we can assume that
+  // kernel version is off by 6 from iOS version (e.g. kernel 18.0 is iOS 12.0).
+  // This only holds starting from iOS 4.0.
+  major_version = kernel_major_version - 6;
+  if (major_version >= 15) {
+    // After iOS 15 minor version of kernel is the same as minor version of
+    // the iOS release. Before iOS 15 these numbers were not in sync. However
+    // We do not expect to check minor version numbers for older iOS
+    // releases so we just keep it at 0 for them.
+    minor_version = kernel_minor_version;
+  }
+  const int32_t field_multiplier = 100;
+#else
+  if (kernel_major_version < 20) {
     // For Mac OS X 10.* minor version is off by 4 from Darwin's major
     // version, e.g. 5.* is v10.1.*, 6.* is v10.2.* and so on.
     // Pretend that anything below Darwin v5 is just Mac OS X Cheetah (v10.0).
     major_version = 10;
-    minor_version = Utils::Maximum(0, darwin_major_version - 4);
+    minor_version = Utils::Maximum(0, kernel_major_version - 4);
   } else {
     // Starting from Darwin v20 major version increment in lock-step:
     // Darwin v20 - Mac OS X v11, Darwin v21 - Mac OS X v12, etc
-    major_version = (darwin_major_version - 9);
+    major_version = (kernel_major_version - 9);
     minor_version = 0;
   }
 
   // Caveat: MAC_OS_X_VERSION_* is encoded using decimal encoding.
   // Starting at MAC_OS_X_VERSION_10_10 versions use 2 decimal digits for
   // minor version and patch number.
-  const int32_t field_multiplier = (darwin_major_version < 14) ? 10 : 100;
+  const int32_t field_multiplier = (kernel_major_version < 14) ? 10 : 100;
+#endif
   const int32_t major_multiplier = field_multiplier * field_multiplier;
   const int32_t minor_multiplier = field_multiplier;
 
   return major_version * major_multiplier + minor_version * minor_multiplier;
 }
 
-int32_t MacOSXVersion() {
-  static int mac_os_x_version = MacOSXVersionInternal();
-  return mac_os_x_version;
+int32_t DarwinVersion() {
+  static int version = DarwinVersionInternal();
+  return version;
 }
 
 }  // namespace internal
 
+#if !defined(DART_HOST_OS_IOS)
 namespace {
-int32_t MacOSMinorVersion(int32_t version) {
+int32_t MacOSXMinorVersion(int32_t version) {
   // Caveat: MAC_OS_X_VERSION_* is encoded using decimal encoding.
   // Starting at MAC_OS_X_VERSION_10_10 versions use 2 decimal digits for
   // minor version and patch number.
@@ -174,7 +196,7 @@
   return (version / field_multiplier) % field_multiplier;
 }
 
-int32_t MacOSMajorVersion(int32_t version) {
+int32_t MacOSXMajorVersion(int32_t version) {
   // Caveat: MAC_OS_X_VERSION_* is encoded using decimal encoding.
   // Starting at MAC_OS_X_VERSION_10_10 versions use 2 decimal digits for
   // minor version and patch number.
@@ -184,8 +206,8 @@
 }
 }  // namespace
 
-char* CheckIsAtLeastMinRequiredMacOSVersion() {
-  const int32_t current_version = internal::MacOSXVersion();
+char* CheckIsAtLeastMinRequiredMacOSXVersion() {
+  const int32_t current_version = internal::DarwinVersion();
 
   if (current_version >= MAC_OS_X_VERSION_MIN_REQUIRED) {
     return nullptr;
@@ -194,10 +216,11 @@
   return Utils::SCreate(
       "Current Mac OS X version %d.%d is lower than minimum supported version "
       "%d.%d",
-      MacOSMajorVersion(current_version), MacOSMinorVersion(current_version),
-      MacOSMajorVersion(MAC_OS_X_VERSION_MIN_REQUIRED),
-      MacOSMinorVersion(MAC_OS_X_VERSION_MIN_REQUIRED));
+      MacOSXMajorVersion(current_version), MacOSXMinorVersion(current_version),
+      MacOSXMajorVersion(MAC_OS_X_VERSION_MIN_REQUIRED),
+      MacOSXMinorVersion(MAC_OS_X_VERSION_MIN_REQUIRED));
 }
+#endif
 
 }  // namespace dart
 
diff --git a/runtime/platform/utils_macos.h b/runtime/platform/utils_macos.h
index d25c204..5f0e19a 100644
--- a/runtime/platform/utils_macos.h
+++ b/runtime/platform/utils_macos.h
@@ -17,16 +17,28 @@
 namespace internal {
 
 // Returns the running system's Mac OS X version which matches the encoding
-// of MAC_OS_X_VERSION_* defines in AvailabilityMacros.h
-int32_t MacOSXVersion();
+// of MAC_OS_X_VERSION_* defines in AvailabilityVersions.h
+int32_t DarwinVersion();
 
 }  // namespace internal
 
+#if defined(DART_HOST_OS_IOS)
+
+// Run-time OS version checks.
+#define DEFINE_IS_OS_FUNCS(VERSION_NAME, VALUE)                                \
+  inline bool IsAtLeastIOS##VERSION_NAME() {                                   \
+    return (internal::DarwinVersion() >= 180400);                              \
+  }
+
+DEFINE_IS_OS_FUNCS(18_4, 180400)
+
+#else
+
 // Run-time OS version checks.
 #define DEFINE_IS_OS_FUNCS(VERSION)                                            \
-  inline bool IsAtLeastOS##VERSION() {                                         \
+  inline bool IsAtLeastMacOSX##VERSION() {                                     \
     return (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_##VERSION) ||    \
-           (internal::MacOSXVersion() >= MAC_OS_X_VERSION_##VERSION);          \
+           (internal::DarwinVersion() >= MAC_OS_X_VERSION_##VERSION);          \
   }
 
 DEFINE_IS_OS_FUNCS(10_14)
@@ -36,7 +48,9 @@
 //
 // Otherwise returns a malloc allocated error string with human readable
 // current and expected versions.
-char* CheckIsAtLeastMinRequiredMacOSVersion();
+char* CheckIsAtLeastMinRequiredMacOSXVersion();
+
+#endif  // defined(DART_HOST_OS_IOS)
 
 inline uint16_t Utils::HostToBigEndian16(uint16_t value) {
   return OSSwapHostToBigInt16(value);
diff --git a/runtime/vm/dart.cc b/runtime/vm/dart.cc
index 0aac4a2..4905493 100644
--- a/runtime/vm/dart.cc
+++ b/runtime/vm/dart.cc
@@ -267,7 +267,7 @@
 #endif
 
 #if defined(DART_HOST_OS_MACOS) && !defined(DART_HOST_OS_IOS)
-  char* error = CheckIsAtLeastMinRequiredMacOSVersion();
+  char* error = CheckIsAtLeastMinRequiredMacOSXVersion();
   if (error != nullptr) {
     return error;
   }
diff --git a/runtime/vm/os.h b/runtime/vm/os.h
index 378f599..9ec5635 100644
--- a/runtime/vm/os.h
+++ b/runtime/vm/os.h
@@ -9,7 +9,6 @@
 
 // Forward declarations.
 struct tm;
-
 namespace dart {
 
 // Forward declarations.
diff --git a/runtime/vm/virtual_memory.h b/runtime/vm/virtual_memory.h
index 7ee27f2..8271f6c 100644
--- a/runtime/vm/virtual_memory.h
+++ b/runtime/vm/virtual_memory.h
@@ -119,6 +119,10 @@
   static uword page_size_;
   static VirtualMemory* compressed_heap_;
 
+#if defined(DART_HOST_OS_IOS) && !defined(DART_PRECOMPILED_RUNTIME)
+  static bool notify_debugger_about_rx_pages_;
+#endif
+
   DISALLOW_IMPLICIT_CONSTRUCTORS(VirtualMemory);
 };
 
diff --git a/runtime/vm/virtual_memory_posix.cc b/runtime/vm/virtual_memory_posix.cc
index 07d4dd5..f430c7a 100644
--- a/runtime/vm/virtual_memory_posix.cc
+++ b/runtime/vm/virtual_memory_posix.cc
@@ -52,6 +52,9 @@
 
 uword VirtualMemory::page_size_ = 0;
 VirtualMemory* VirtualMemory::compressed_heap_ = nullptr;
+#if defined(DART_HOST_OS_IOS) && !defined(DART_PRECOMPILED_RUNTIME)
+bool VirtualMemory::notify_debugger_about_rx_pages_ = false;
+#endif
 
 static void* Map(void* addr,
                  size_t length,
@@ -137,6 +140,125 @@
 }
 #endif  // LARGE_RESERVATIONS_MAY_FAIL
 
+#if defined(DART_HOST_OS_IOS) && !defined(DART_PRECOMPILED_RUNTIME)
+// The function NOTIFY_DEBUGGER_ABOUT_RX_PAGES is a hook point for the debugger.
+//
+// We expect that LLBD is configured to intercept calls to this function and
+// takes care of writing into all pages covered by [base, base+size) address
+// range.
+//
+// For example, you can define the following Python helper script:
+//
+// ```python
+// # rx_helper.py
+// import lldb
+//
+// def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict):
+//     """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages."""
+//     base = frame.register["x0"].GetValueAsAddress()
+//     page_len = frame.register["x1"].GetValueAsUnsigned()
+
+//     # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the
+//     # first page to see if handled it correctly. This makes diagnosing
+//     # misconfiguration (e.g. missing breakpoint) easier.
+//     data = bytearray(page_len)
+//     data[0:8] = b'IHELPED!';
+
+//     error = lldb.SBError()
+//     frame.GetThread().GetProcess().WriteMemory(base, data, error)
+//     if not error.Success():
+//         print(f'Failed to write into {base}[+{page_len}]', error)
+//         return
+//
+// def __lldb_init_module(debugger: lldb.SBDebugger, _):
+//     target = debugger.GetDummyTarget()
+//     # Caveat: must use BreakpointCreateByRegEx here and not
+//     # BreakpointCreateByName. For some reasons callback function does not
+//     # get carried over from dummy target for the later.
+//     bp = target.bpCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$")
+//     bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__))
+//     bp.SetAutoContinue(True)
+//     print("-- LLDB integration loaded --")
+// ```
+//
+// Which is then imported into LLDB via `.lldbinit` script:
+//
+// ```
+// # .lldbinit
+// command script import --relative-to-command-file rx_helper.py
+// ```
+//
+// XCode allows configuring custom LLDB Init Files: see Product -> Scheme ->
+// Run -> Info -> LLDB Init File, you can use `$(SRCROOT)/...` to place LLDB
+// script inside project directory itself.
+//
+__attribute__((noinline)) __attribute__((visibility("default"))) extern "C" void
+NOTIFY_DEBUGGER_ABOUT_RX_PAGES(void* base, size_t size) {
+  // Note: need this to prevent LLVM from optimizing it away even with
+  // noinline.
+  asm volatile("" ::"r"(base), "r"(size) : "memory");
+}
+
+namespace {
+bool CheckIfNeedDebuggerHelpWithRX() {
+  // Do not expect any problems before iOS 18.4.
+  if (!IsAtLeastIOS18_4()) {
+    return false;
+  }
+
+  if (!FLAG_write_protect_code) {
+    FATAL("Must run with --write-protect-code on this OS");
+  }
+
+  // Helper to check if RX->RW->RX->RW->RX flip works, with and without
+  // debugger assistance.
+  const auto does_rx_rw_rx_flip_work = [](bool notify_debugger) {
+    const intptr_t size = VirtualMemory::PageSize();
+    void* page = Map(NULL, size, PROT_READ | PROT_EXEC,
+                     MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+    if (page == MAP_FAILED) {
+      FATAL("Failed to map a test RX page (ENOMEM)");
+    }
+
+    if (notify_debugger) {
+      NOTIFY_DEBUGGER_ABOUT_RX_PAGES(page, size);
+      if (strncmp(reinterpret_cast<const char*>(page), "IHELPED!", 8) != 0) {
+        FATAL("NOTIFY_DEBUGGER_ABOUT_RX_PAGES was not intercepted as expected");
+      }
+    }
+
+    bool failed_to_return_to_rx = false;
+    // Need to try twice: the first RW->RX flip might work, some lazy checking
+    // is involved.
+    for (intptr_t i = 0; i < 2; i++) {
+      // Do not expect this one to fail.
+      VirtualMemory::Protect(page, size, VirtualMemory::kReadWrite);
+      reinterpret_cast<int64_t*>(page)[i] = kBreakInstructionFiller;
+      // This one might fail so we call mprotect directly and check if
+      // it failed.
+      if (mprotect(page, size, PROT_READ | PROT_EXEC) != 0) {
+        failed_to_return_to_rx = true;
+      }
+    }
+    munmap(page, size);
+    return !failed_to_return_to_rx;
+  };
+
+  // First try without debugger assistance.
+  if (does_rx_rw_rx_flip_work(/*notify_debugger=*/false)) {
+    return false;  // All works.
+  }
+
+  // RX->RW->RX->RW->RX does not seem to work. Try asking debugger for help.
+  if (!does_rx_rw_rx_flip_work(/*notify_debugger=*/true)) {
+    FATAL("Unable to flip between RX and RW memory protection on pages");
+  }
+
+  return true;  // Debugger can help us.
+}
+}  // namespace
+#endif
+
 void VirtualMemory::Init() {
   if (FLAG_old_gen_heap_size < 0 || FLAG_old_gen_heap_size > kMaxAddrSpaceMB) {
     OS::PrintErr(
@@ -154,6 +276,10 @@
     FLAG_new_gen_semi_max_size = kDefaultNewGenSemiMaxSize;
   }
   page_size_ = CalculatePageSize();
+#if defined(DART_HOST_OS_IOS) && !defined(DART_PRECOMPILED_RUNTIME)
+  notify_debugger_about_rx_pages_ = CheckIfNeedDebuggerHelpWithRX();
+#endif
+
 #if defined(DART_COMPRESSED_POINTERS)
   ASSERT(compressed_heap_ == nullptr);
 #if defined(LARGE_RESERVATIONS_MAY_FAIL)
@@ -187,7 +313,6 @@
   VirtualMemoryCompressedHeap::Init(compressed_heap_->address(),
                                     compressed_heap_->size());
 #endif  // defined(DART_COMPRESSED_POINTERS)
-
 #if defined(DART_HOST_OS_LINUX) || defined(DART_HOST_OS_ANDROID)
   FILE* fp = fopen("/proc/sys/vm/max_map_count", "r");
   if (fp != nullptr) {
@@ -265,13 +390,20 @@
 #endif  // defined(DART_COMPRESSED_POINTERS)
 
   const intptr_t allocated_size = size + alignment - PageSize();
+
+#if defined(DART_HOST_OS_IOS) && !defined(DART_PRECOMPILED_RUNTIME)
+  const int prot = (is_executable && notify_debugger_about_rx_pages_)
+                       ? PROT_READ | PROT_EXEC
+                       : PROT_READ | PROT_WRITE;
+#else
   const int prot =
       PROT_READ | PROT_WRITE |
       ((is_executable && !FLAG_write_protect_code) ? PROT_EXEC : 0);
+#endif
 
   int map_flags = MAP_PRIVATE | MAP_ANONYMOUS;
 #if (defined(DART_HOST_OS_MACOS) && !defined(DART_HOST_OS_IOS))
-  if (is_executable && IsAtLeastOS10_14()) {
+  if (is_executable && IsAtLeastMacOSX10_14()) {
     map_flags |= MAP_JIT;
   }
 #endif  // defined(DART_HOST_OS_MACOS)
@@ -303,6 +435,15 @@
     return nullptr;
   }
 
+#if defined(DART_HOST_OS_IOS) && !defined(DART_PRECOMPILED_RUNTIME)
+  if (is_executable && notify_debugger_about_rx_pages_) {
+    NOTIFY_DEBUGGER_ABOUT_RX_PAGES(reinterpret_cast<void*>(address), size);
+    // Once debugger is notified we can flip RX to RW without loosing
+    // ability to flip back to RX.
+    Protect(address, size, kReadWrite);
+  }
+#endif
+
 #if defined(DART_HOST_OS_ANDROID) || defined(DART_HOST_OS_LINUX)
   // PR_SET_VMA was only added to mainline Linux in 5.17, and some versions of
   // the Android NDK have incorrect headers, so we manually define it if absent.