[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>
This commit is contained in:
Vyacheslav Egorov
2025-02-28 02:24:38 -08:00
committed by Commit Queue
parent d8c48032bb
commit 939699a9cf
8 changed files with 247 additions and 59 deletions
+12 -5
View File
@@ -94,9 +94,11 @@ MappedMemory* File::Map(MapType type,
// 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 @@ 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) {
+1 -1
View File
@@ -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)
+67 -44
View File
@@ -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 @@ 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
+19 -5
View File
@@ -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);
+1 -1
View File
@@ -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;
}
-1
View File
@@ -9,7 +9,6 @@
// Forward declarations.
struct tm;
namespace dart {
// Forward declarations.
+4
View File
@@ -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);
};
+143 -2
View File
@@ -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<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 @@ 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<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.