diff --git a/runtime/bin/file_macos.cc b/runtime/bin/file_macos.cc index a469c9b244a..3cf27a0d9eb 100644 --- a/runtime/bin/file_macos.cc +++ b/runtime/bin/file_macos.cc @@ -94,9 +94,11 @@ MappedMemory* File::Map(MapType type, // Try to allocate near the VM's binary. hint = reinterpret_cast(&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 @@ MappedMemory* File::Map(MapType type, 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 b7f0eaf872c..da2b07590fa 100644 --- a/runtime/bin/virtual_memory_posix.cc +++ b/runtime/bin/virtual_memory_posix.cc @@ -45,7 +45,7 @@ VirtualMemory* VirtualMemory::Allocate(intptr_t size, 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 082c1ce4bec..01ed447bd27 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 // NOLINT #include // NOLINT namespace dart { @@ -81,10 +80,10 @@ int Utils::Unlink(const char* path) { 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 @@ int32_t DarwinMajorVersionInternal() { 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 @@ int32_t MacOSMinorVersion(int32_t version) { 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 @@ int32_t MacOSMajorVersion(int32_t version) { } } // 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 @@ char* CheckIsAtLeastMinRequiredMacOSVersion() { 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 d25c204614c..5f0e19a8931 100644 --- a/runtime/platform/utils_macos.h +++ b/runtime/platform/utils_macos.h @@ -17,16 +17,28 @@ namespace dart { 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 @@ DEFINE_IS_OS_FUNCS(10_14) // // 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 0aac4a2dc6a..490549316ef 100644 --- a/runtime/vm/dart.cc +++ b/runtime/vm/dart.cc @@ -267,7 +267,7 @@ char* Dart::DartInit(const Dart_InitializeParams* params) { #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 378f5999a8f..9ec56357c9f 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 7ee27f27233..8271f6c52e6 100644 --- a/runtime/vm/virtual_memory.h +++ b/runtime/vm/virtual_memory.h @@ -119,6 +119,10 @@ class VirtualMemory { 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 07d4dd537dc..f430c7a5f6f 100644 --- a/runtime/vm/virtual_memory_posix.cc +++ b/runtime/vm/virtual_memory_posix.cc @@ -52,6 +52,9 @@ DECLARE_FLAG(bool, generate_perf_jitdump); 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 @@ static MemoryRegion ClipToAlignedRegion(MemoryRegion region, size_t alignment) { } #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(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(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 @@ void VirtualMemory::Init() { 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 @@ void VirtualMemory::Init() { 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 @@ VirtualMemory* VirtualMemory::AllocateAligned(intptr_t size, #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 @@ VirtualMemory* VirtualMemory::AllocateAligned(intptr_t size, 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(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.