[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.