49546393c6
Push this down to VirtualMemory since some parts of the VM go there directly instead of through Page. TEST=ci Change-Id: Icc65242e9099e8785a38ba9e38311146d8b5845b Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/510260 Commit-Queue: Ryan Macnak <rmacnak@google.com> Reviewed-by: Alexander Markov <alexmarkov@google.com>
909 lines
33 KiB
C++
909 lines
33 KiB
C++
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
|
|
#include "vm/globals.h"
|
|
#if defined(DART_HOST_OS_ANDROID) || defined(DART_HOST_OS_LINUX) || \
|
|
defined(DART_HOST_OS_MACOS)
|
|
|
|
#include "vm/virtual_memory.h"
|
|
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <sys/mman.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/syscall.h>
|
|
#include <unistd.h>
|
|
|
|
#if defined(DART_HOST_OS_ANDROID) || defined(DART_HOST_OS_LINUX)
|
|
#include <sys/prctl.h>
|
|
#endif
|
|
|
|
#if defined(DART_HOST_OS_MACOS)
|
|
#include <mach/mach_init.h>
|
|
#include <mach/vm_map.h>
|
|
#endif
|
|
|
|
#if defined(DART_ENABLE_RX_WORKAROUNDS)
|
|
#include <dispatch/dispatch.h>
|
|
#include <dispatch/source.h>
|
|
#include <mach/mach.h>
|
|
#include <mach/mach_port.h>
|
|
#include <mach/thread_act.h>
|
|
|
|
#include "platform/syslog.h"
|
|
#include "vm/cpu.h"
|
|
#endif
|
|
|
|
#include "platform/assert.h"
|
|
#include "platform/thread_sanitizer.h"
|
|
#include "platform/utils.h"
|
|
#include "vm/heap/pages.h"
|
|
#include "vm/isolate.h"
|
|
#include "vm/virtual_memory_compressed.h"
|
|
|
|
// #define VIRTUAL_MEMORY_LOGGING 1
|
|
#if defined(VIRTUAL_MEMORY_LOGGING)
|
|
#define LOG_INFO(msg, ...) OS::PrintErr(msg, ##__VA_ARGS__)
|
|
#else
|
|
#define LOG_INFO(msg, ...)
|
|
#endif // defined(VIRTUAL_MEMORY_LOGGING)
|
|
|
|
namespace dart {
|
|
|
|
// standard MAP_FAILED causes "error: use of old-style cast" as it
|
|
// defines MAP_FAILED as ((void *) -1)
|
|
#undef MAP_FAILED
|
|
#define MAP_FAILED reinterpret_cast<void*>(-1)
|
|
|
|
DECLARE_FLAG(bool, write_protect_code);
|
|
|
|
#if defined(DART_TARGET_OS_LINUX)
|
|
DECLARE_FLAG(bool, generate_perf_events_symbols);
|
|
DECLARE_FLAG(bool, generate_perf_jitdump);
|
|
#endif
|
|
|
|
uword VirtualMemory::page_size_ = 0;
|
|
VirtualMemory* VirtualMemory::compressed_heap_ = nullptr;
|
|
|
|
#if defined(DART_ENABLE_RX_WORKAROUNDS)
|
|
bool VirtualMemory::should_dual_map_executable_pages_ = false;
|
|
#endif // defined(DART_ENABLE_RX_WORKAROUNDS)
|
|
|
|
static void* Map(void* addr,
|
|
size_t length,
|
|
int prot,
|
|
int flags,
|
|
int fd,
|
|
off_t offset) {
|
|
void* result = mmap(addr, length, prot, flags, fd, offset);
|
|
int error = errno;
|
|
LOG_INFO("mmap(%p, 0x%" Px ", %u, ...): %p\n", addr, length, prot, result);
|
|
if ((result == MAP_FAILED) && (error != ENOMEM)) {
|
|
const int kBufferSize = 1024;
|
|
char error_buf[kBufferSize];
|
|
FATAL("mmap failed: %d (%s)", error,
|
|
Utils::StrError(error, error_buf, kBufferSize));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static void Unmap(uword start, uword end) {
|
|
ASSERT(start <= end);
|
|
uword size = end - start;
|
|
if (size == 0) {
|
|
return;
|
|
}
|
|
|
|
if (munmap(reinterpret_cast<void*>(start), size) != 0) {
|
|
int error = errno;
|
|
const int kBufferSize = 1024;
|
|
char error_buf[kBufferSize];
|
|
FATAL("munmap failed: %d (%s)", error,
|
|
Utils::StrError(error, error_buf, kBufferSize));
|
|
}
|
|
}
|
|
|
|
static void* GenericMapAligned(void* hint,
|
|
int prot,
|
|
intptr_t size,
|
|
intptr_t alignment,
|
|
intptr_t allocated_size,
|
|
int map_flags) {
|
|
#if defined(DART_HOST_OS_MACOS) && !defined(USING_THREAD_SANITIZER)
|
|
// Allocate aligned memory in one step when possible.
|
|
// vm_map doesn't support MAP_JIT.
|
|
// tsan is missing an interceptor for mach_vm_map.
|
|
if ((map_flags & MAP_JIT) == 0) {
|
|
vm_address_t address = 0;
|
|
vm_prot_t cur_prot = 0;
|
|
if ((prot & PROT_READ) != 0) cur_prot |= VM_PROT_READ;
|
|
if ((prot & PROT_WRITE) != 0) cur_prot |= VM_PROT_WRITE;
|
|
if ((prot & PROT_EXEC) != 0) cur_prot |= VM_PROT_EXECUTE;
|
|
vm_prot_t max_prot = VM_PROT_ALL;
|
|
const kern_return_t result =
|
|
vm_map(mach_task_self(), &address, size, /*mask=*/alignment - 1,
|
|
VM_FLAGS_ANYWHERE, MEMORY_OBJECT_NULL, /*offset=*/0,
|
|
/*copy=*/FALSE, cur_prot, max_prot, VM_INHERIT_DEFAULT);
|
|
if (result != KERN_SUCCESS) {
|
|
return nullptr;
|
|
}
|
|
return reinterpret_cast<void*>(address);
|
|
}
|
|
#endif
|
|
void* address = Map(hint, allocated_size, prot, map_flags, -1, 0);
|
|
if (address == MAP_FAILED) {
|
|
return nullptr;
|
|
}
|
|
|
|
const uword base = reinterpret_cast<uword>(address);
|
|
const uword aligned_base = Utils::RoundUp(base, alignment);
|
|
|
|
Unmap(base, aligned_base);
|
|
Unmap(aligned_base + size, base + allocated_size);
|
|
return reinterpret_cast<void*>(aligned_base);
|
|
}
|
|
|
|
intptr_t VirtualMemory::CalculatePageSize() {
|
|
const intptr_t page_size = getpagesize();
|
|
ASSERT(page_size != 0);
|
|
ASSERT(Utils::IsPowerOfTwo(page_size));
|
|
return page_size;
|
|
}
|
|
|
|
#if defined(DART_ENABLE_RX_WORKAROUNDS)
|
|
// 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 {
|
|
|
|
// Handler for EXC_BAD_ACCESS which resumes the thread at the caller of the
|
|
// function (sets PC = LR and R0 = kExceptionalReturnValue). This exception
|
|
// handler is used to check if we can successfully create executable code
|
|
// dynamically: see |CheckIfRXWorks| below.
|
|
//
|
|
// Note: the handler is using Mach kernel APIs for exception handling instead
|
|
// of POSIX signals because using Mach APIs allows to intercept EXC_BAD_ACCESS
|
|
// before it stops the debugger.
|
|
class ScopedExcBadAccessHandler {
|
|
public:
|
|
static constexpr int32_t kExceptionalReturnValue = 0xDEADDEAD;
|
|
|
|
ScopedExcBadAccessHandler() {
|
|
mach_port_options_t options;
|
|
memset(&options, 0, sizeof(options));
|
|
options.flags = MPO_INSERT_SEND_RIGHT;
|
|
|
|
mach_port_t exception_port = MACH_PORT_NULL;
|
|
kern_return_t kr =
|
|
mach_port_construct(mach_task_self(), &options, 0, &exception_port);
|
|
RELEASE_ASSERT(kr == KERN_SUCCESS);
|
|
|
|
dispatch_source_t source = source_ =
|
|
dispatch_source_create(DISPATCH_SOURCE_TYPE_MACH_RECV, exception_port,
|
|
0, DISPATCH_TARGET_QUEUE_DEFAULT);
|
|
RELEASE_ASSERT(source);
|
|
|
|
// Process exceptions: ProcessMachExceptionRaiseStateMessage decodes
|
|
// the message and forwards it to IgnoreExceptionAndReturnToCaller.
|
|
dispatch_source_set_event_handler(source, ^{
|
|
constexpr mach_msg_size_t kMaxMessageSize = 5 * KB;
|
|
mach_msg_server_once(ProcessMachExceptionRaiseStateMessage,
|
|
kMaxMessageSize, exception_port,
|
|
MACH_MSG_TIMEOUT_NONE);
|
|
});
|
|
|
|
// When this handler is no longer needed destroy the port.
|
|
dispatch_source_set_cancel_handler(source, ^{
|
|
mach_port_deallocate(mach_task_self(), exception_port);
|
|
// Note: don't capture this because it will be invalid by the time
|
|
// cancelation handler is called.
|
|
dispatch_release(source);
|
|
});
|
|
|
|
dispatch_resume(source);
|
|
|
|
old_mask_count_ = 1; // We expect at most one old handler.
|
|
kern_return_t result = thread_swap_exception_ports(
|
|
mach_thread_self(), EXC_MASK_BAD_ACCESS, exception_port,
|
|
MACH_EXCEPTION_CODES | EXCEPTION_STATE, MACHINE_THREAD_STATE,
|
|
&old_exception_mask_, &old_mask_count_, &old_handler_, &old_behavior_,
|
|
&old_flavor_);
|
|
RELEASE_ASSERT(result == KERN_SUCCESS);
|
|
RELEASE_ASSERT(old_mask_count_ == 1);
|
|
}
|
|
|
|
~ScopedExcBadAccessHandler() {
|
|
kern_return_t result =
|
|
thread_set_exception_ports(mach_thread_self(), old_exception_mask_,
|
|
old_handler_, old_behavior_, old_flavor_);
|
|
RELEASE_ASSERT(result == KERN_SUCCESS);
|
|
dispatch_source_cancel(source_);
|
|
}
|
|
|
|
private:
|
|
// This exception handler simply ignores the EXC_BAD_ACCESS and
|
|
// makes the thread continue at the caller frame by setting PC to LR and
|
|
// X0 to a special signal value.
|
|
//
|
|
// The signature of this handler matches |catch_exception_raise_state|.
|
|
//
|
|
// See https://developer.apple.com/documentation/kernel/1537255-catch_exception_raise_state
|
|
static kern_return_t IgnoreExceptionAndReturnToCaller(
|
|
mach_port_t exception_port,
|
|
exception_type_t exception,
|
|
const mach_exception_data_t code,
|
|
mach_msg_type_number_t code_count,
|
|
int* flavor,
|
|
const thread_state_t old_state,
|
|
mach_msg_type_number_t old_state_count,
|
|
thread_state_t new_state,
|
|
mach_msg_type_number_t* new_state_count) {
|
|
// Copy old_state into new_state.
|
|
memmove(new_state, old_state, sizeof(*old_state) * old_state_count);
|
|
*new_state_count = old_state_count;
|
|
|
|
// Update X0 and PC so that we can successfully resume execution.
|
|
auto arm_new_state =
|
|
reinterpret_cast<arm_unified_thread_state_t*>(new_state);
|
|
arm_new_state->ts_64.__x[0] = kExceptionalReturnValue;
|
|
arm_new_state->ts_64.__pc = arm_new_state->ts_64.__lr;
|
|
return KERN_SUCCESS;
|
|
}
|
|
|
|
// The code in |ProcessMachExceptionRaiseStateMessage| and corresponding
|
|
// structure definitions are based on output of the mig
|
|
// (Mach Interface Generator, see |man mig|) applied to mach/mach_exc.defs.
|
|
//
|
|
// Including mig output directly is undesirable because it relies on
|
|
// linking to exception handling routines by name (e.g. it expects
|
|
// special symbols like mach_catch_exception_raise_state,
|
|
// mach_catch_exception_raise_state_identity, to be defined). This might
|
|
// make Dart VM harder to embed - as some other part of the code base might
|
|
// want to use mig generated exception handling code and already define
|
|
// this symbols.
|
|
//
|
|
// Thus we rewrite that code dropping all irrelevant bits and making the
|
|
// code more readable.
|
|
//
|
|
// Request message for mach_exception_raise_state has two variadic arrays
|
|
// inside, so we split it into two chunks each ending with a corresponding
|
|
// variadic array.
|
|
#define TRAILING_ARRAY(Type, name, count, max_count) \
|
|
Type* name() { return reinterpret_cast<Type*>(this + 1); } \
|
|
bool IsValid() const { return count <= max_count; } \
|
|
mach_msg_size_t Size() const { return sizeof(*this) + sizeof(Type) * count; }
|
|
|
|
// A helper method for parsing a message which contains variadic arrays
|
|
// inside. Such message is split into separate chunks each ending with
|
|
// a trailing array.
|
|
template <typename... Ts>
|
|
static std::tuple<bool, Ts*...> ParseMessage(mach_msg_header_t* header) {
|
|
uword current = reinterpret_cast<uword>(header);
|
|
mach_msg_size_t remaining = header->msgh_size;
|
|
const uword message_end = current + remaining;
|
|
|
|
std::tuple<bool, Ts*...> result{
|
|
true, [¤t, &remaining]() -> Ts* {
|
|
if (remaining >= sizeof(Ts)) {
|
|
Ts* chunk = reinterpret_cast<Ts*>(current);
|
|
const auto chunk_size = chunk->Size();
|
|
if (chunk->IsValid() && chunk_size <= remaining) {
|
|
current += chunk_size;
|
|
remaining -= chunk_size;
|
|
return chunk;
|
|
}
|
|
}
|
|
|
|
// Once an error is encountered shortcut the rest of the parsing
|
|
// by setting number of remaining bytes to 0.
|
|
remaining = 0;
|
|
current = 0;
|
|
return nullptr;
|
|
}()...};
|
|
|
|
// If we did not fully parse the message - we have either failed or
|
|
// we have unparsed bytes. Either case is an error.
|
|
if (current != message_end) {
|
|
return {false, static_cast<Ts*>(nullptr)...};
|
|
}
|
|
return result;
|
|
}
|
|
|
|
#pragma pack(push, 4)
|
|
static constexpr natural_t kMaxCodeCount = 2;
|
|
static constexpr natural_t kMaxStateCount = 1296;
|
|
|
|
struct RequestChunk0 {
|
|
mach_msg_header_t Head;
|
|
NDR_record_t NDR;
|
|
exception_type_t exception;
|
|
mach_msg_type_number_t code_count; // <= kMaxCodeCount
|
|
TRAILING_ARRAY(int64_t, code, code_count, kMaxCodeCount);
|
|
};
|
|
|
|
struct RequestChunk1 {
|
|
int flavor;
|
|
mach_msg_type_number_t old_state_count; // <= kMaxStateCount
|
|
TRAILING_ARRAY(natural_t, old_state, old_state_count, kMaxStateCount);
|
|
};
|
|
|
|
struct Reply {
|
|
mach_msg_header_t Head;
|
|
NDR_record_t NDR;
|
|
kern_return_t RetCode;
|
|
int flavor;
|
|
mach_msg_type_number_t new_state_count;
|
|
TRAILING_ARRAY(natural_t, new_state, new_state_count, kMaxStateCount);
|
|
};
|
|
#pragma pack(pop)
|
|
|
|
static boolean_t ReplyWithError(mach_msg_header_t* reply_header,
|
|
kern_return_t code) {
|
|
auto reply = reinterpret_cast<mig_reply_error_t*>(reply_header);
|
|
reply->RetCode = code;
|
|
reply->NDR = NDR_record;
|
|
return FALSE;
|
|
}
|
|
|
|
static boolean_t ProcessMachExceptionRaiseStateMessage(
|
|
mach_msg_header_t* request_header,
|
|
mach_msg_header_t* reply_header) {
|
|
reply_header->msgh_bits =
|
|
MACH_MSGH_BITS(MACH_MSGH_BITS_REMOTE(request_header->msgh_bits), 0);
|
|
reply_header->msgh_remote_port = request_header->msgh_remote_port;
|
|
// Minimal size: will update later if success.
|
|
reply_header->msgh_size =
|
|
static_cast<mach_msg_size_t>(sizeof(mig_reply_error_t));
|
|
reply_header->msgh_local_port = MACH_PORT_NULL;
|
|
reply_header->msgh_id = request_header->msgh_id + 100;
|
|
reply_header->msgh_reserved = 0;
|
|
|
|
if (request_header->msgh_id != 2406) {
|
|
return ReplyWithError(reply_header, MIG_BAD_ID);
|
|
}
|
|
|
|
if (request_header->msgh_bits & MACH_MSGH_BITS_COMPLEX) {
|
|
return ReplyWithError(reply_header, MIG_BAD_ARGUMENTS);
|
|
}
|
|
|
|
auto [ok, req0, req1] =
|
|
ParseMessage<RequestChunk0, RequestChunk1>(request_header);
|
|
if (!ok) {
|
|
return ReplyWithError(reply_header, MIG_BAD_ARGUMENTS);
|
|
}
|
|
|
|
auto reply = reinterpret_cast<Reply*>(reply_header);
|
|
reply->new_state_count = kMaxStateCount;
|
|
reply->RetCode = IgnoreExceptionAndReturnToCaller(
|
|
request_header->msgh_local_port, req0->exception, req0->code(),
|
|
req0->code_count, &req1->flavor, req1->old_state(),
|
|
req1->old_state_count, reply->new_state(), &reply->new_state_count);
|
|
if (reply->RetCode != KERN_SUCCESS) {
|
|
return ReplyWithError(reply_header, reply->RetCode);
|
|
}
|
|
|
|
reply->NDR = NDR_record;
|
|
reply->flavor = req1->flavor;
|
|
reply->Head.msgh_size = reply->Size();
|
|
return TRUE;
|
|
}
|
|
|
|
dispatch_source_t source_ = nullptr;
|
|
|
|
// Old exception handler (e.g. one installed by the debugger or some
|
|
// other library).
|
|
natural_t old_mask_count_ = 0;
|
|
exception_mask_t old_exception_mask_ = 0;
|
|
mach_port_t old_handler_ = MACH_PORT_NULL;
|
|
exception_behavior_t old_behavior_ = 0;
|
|
thread_state_flavor_t old_flavor_ = 0;
|
|
|
|
DISALLOW_COPY_AND_ASSIGN(ScopedExcBadAccessHandler);
|
|
};
|
|
|
|
// Check if we can generate machine code dynamically by creating a small
|
|
// function in memory and then trying to execute it.
|
|
//
|
|
// Returns true if that was successful.
|
|
//
|
|
// Note: we use Syslog::PrintErr below instead of OS::PrintErr to send
|
|
// output to the same location where FATAL message would be reported to if any.
|
|
bool CheckIfRXWorks() {
|
|
// Try creating executable VirtualMemory.
|
|
std::unique_ptr<VirtualMemory> mem{
|
|
VirtualMemory::Allocate(VirtualMemory::PageSize(), /*is_executable=*/true,
|
|
/*is_compressed=*/false, /*name=*/nullptr)};
|
|
if (mem == nullptr) {
|
|
Syslog::PrintErr("Failed to map a test RX page");
|
|
return false;
|
|
}
|
|
|
|
// Freshly created virtual memory should have signs of debugger script
|
|
// working. See the comment above for the example of an LLDB script.
|
|
const bool debugger_script_loaded =
|
|
memcmp(mem->address(), "IHELPED!", 8) == 0;
|
|
|
|
// Flip memory to RW write a simple function that computes a 32-bit integer
|
|
// square and then flip protection back to R/RX.
|
|
mem->Protect(VirtualMemory::kReadWrite);
|
|
constexpr uint32_t kSquareFunctionCode[] = {
|
|
0x1b007c00, // mul w0, w0, w0
|
|
0xd65f03c0 // ret
|
|
};
|
|
memmove(mem->address(), kSquareFunctionCode, sizeof(kSquareFunctionCode));
|
|
VirtualMemory::WriteProtectCode(mem->address(), mem->size());
|
|
|
|
// Get executable entry point and check that write have succeeded.
|
|
const uword entry_point = mem->start() + mem->OffsetToExecutableAlias();
|
|
if (memcmp(reinterpret_cast<void*>(entry_point), kSquareFunctionCode,
|
|
sizeof(kSquareFunctionCode)) != 0) {
|
|
Syslog::PrintErr("Failed to write executable code: code mismatch");
|
|
return false;
|
|
}
|
|
CPU::FlushICache(entry_point, sizeof(kSquareFunctionCode));
|
|
|
|
constexpr int32_t kInput = 11;
|
|
constexpr int32_t kExpectedOutput = kInput * kInput;
|
|
|
|
// Invoke square function and catch any potential EXC_BAD_ACCESS.
|
|
int32_t result = 0;
|
|
{
|
|
ScopedExcBadAccessHandler exception_handler;
|
|
auto square = reinterpret_cast<int32_t (*)(int32_t)>(entry_point);
|
|
result = square(kInput);
|
|
}
|
|
|
|
// Validate that the code we have generated produced expected result.
|
|
if (result != kExpectedOutput) {
|
|
Syslog::PrintErr(
|
|
"Failed to execute code (error: %s, debugger assist: %s)\n",
|
|
result == ScopedExcBadAccessHandler::kExceptionalReturnValue
|
|
? "EXC_BAD_ACCESS"
|
|
: "unknown",
|
|
debugger_script_loaded ? "ok" : "not detected");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
} // namespace
|
|
#endif
|
|
|
|
void VirtualMemory::Init() {
|
|
if (FLAG_old_gen_heap_size < 0 || FLAG_old_gen_heap_size > kMaxAddrSpaceMB) {
|
|
OS::PrintErr(
|
|
"warning: value specified for --old_gen_heap_size %d is larger than"
|
|
" the physically addressable range, using 0(unlimited) instead.`\n",
|
|
FLAG_old_gen_heap_size);
|
|
FLAG_old_gen_heap_size = 0;
|
|
}
|
|
if (FLAG_new_gen_semi_max_size < 0 ||
|
|
FLAG_new_gen_semi_max_size > kMaxAddrSpaceMB) {
|
|
OS::PrintErr(
|
|
"warning: value specified for --new_gen_semi_max_size %d is larger"
|
|
" than the physically addressable range, using %" Pd " instead.`\n",
|
|
FLAG_new_gen_semi_max_size, kDefaultNewGenSemiMaxSize);
|
|
FLAG_new_gen_semi_max_size = kDefaultNewGenSemiMaxSize;
|
|
}
|
|
page_size_ = CalculatePageSize();
|
|
|
|
#if defined(DART_ENABLE_RX_WORKAROUNDS)
|
|
bool can_jit = true;
|
|
if (IsAtLeastIOS26_0()) {
|
|
should_dual_map_executable_pages_ = true;
|
|
can_jit = CheckIfRXWorks();
|
|
}
|
|
#if defined(DART_INCLUDE_SIMULATOR)
|
|
FLAG_use_simulator = !can_jit;
|
|
Syslog::PrintErr("Dart execution mode: %s\n",
|
|
FLAG_use_simulator ? "simulator" : "JIT");
|
|
#else
|
|
if (!can_jit) {
|
|
FATAL(
|
|
"Unable to JIT: failed to create executable machine code dynamically "
|
|
"due to OS restrictions");
|
|
}
|
|
#endif
|
|
#endif
|
|
|
|
#if defined(DART_COMPRESSED_POINTERS)
|
|
ASSERT(compressed_heap_ == nullptr);
|
|
compressed_heap_ = Reserve(kGuardRegionSize * 2 + kCompressedHeapSize,
|
|
kCompressedHeapAlignment);
|
|
if (compressed_heap_ == nullptr) {
|
|
int error = errno;
|
|
const int kBufferSize = 1024;
|
|
char error_buf[kBufferSize];
|
|
FATAL("Failed to reserve region for compressed heap: %d (%s)", error,
|
|
Utils::StrError(error, error_buf, kBufferSize));
|
|
}
|
|
VirtualMemoryCompressedHeap::Init(
|
|
reinterpret_cast<void*>(compressed_heap_->start() + kGuardRegionSize),
|
|
kCompressedHeapSize);
|
|
#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) {
|
|
size_t max_map_count = 0;
|
|
int count = fscanf(fp, "%zu", &max_map_count);
|
|
fclose(fp);
|
|
if (count == 1) {
|
|
size_t max_heap_pages = FLAG_old_gen_heap_size * MB / Page::kPageSize;
|
|
if (max_map_count < max_heap_pages) {
|
|
OS::PrintErr(
|
|
"warning: vm.max_map_count (%zu) is not large enough to support "
|
|
"--old_gen_heap_size=%d. Consider increasing it with `sysctl -w "
|
|
"vm.max_map_count=%zu`\n",
|
|
max_map_count, FLAG_old_gen_heap_size, max_heap_pages);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void VirtualMemory::Cleanup() {
|
|
#if defined(DART_COMPRESSED_POINTERS)
|
|
delete compressed_heap_;
|
|
#endif // defined(DART_COMPRESSED_POINTERS)
|
|
page_size_ = 0;
|
|
#if defined(DART_COMPRESSED_POINTERS)
|
|
compressed_heap_ = nullptr;
|
|
VirtualMemoryCompressedHeap::Cleanup();
|
|
#endif // defined(DART_COMPRESSED_POINTERS)
|
|
}
|
|
|
|
VirtualMemory* VirtualMemory::AllocateAligned(intptr_t size,
|
|
intptr_t alignment,
|
|
bool is_executable,
|
|
bool is_compressed,
|
|
const char* name) {
|
|
// When FLAG_write_protect_code is active, code memory (indicated by
|
|
// is_executable = true) is allocated as non-executable and later
|
|
// changed to executable via VirtualMemory::Protect.
|
|
ASSERT(Utils::IsAligned(size, PageSize()));
|
|
ASSERT(Utils::IsPowerOfTwo(alignment));
|
|
ASSERT(Utils::IsAligned(alignment, PageSize()));
|
|
ASSERT(name != nullptr);
|
|
|
|
// Ignore executable for gen_snapshot/simulator, but still let the heap
|
|
// track code and data pages separately.
|
|
if (!VirtualMemory::ExecutesGeneratedCode()) {
|
|
is_executable = false;
|
|
}
|
|
|
|
#if defined(DART_COMPRESSED_POINTERS)
|
|
if (is_compressed) {
|
|
RELEASE_ASSERT(!is_executable);
|
|
MemoryRegion region =
|
|
VirtualMemoryCompressedHeap::Allocate(size, alignment);
|
|
if (region.pointer() == nullptr) {
|
|
return nullptr;
|
|
}
|
|
Commit(region.pointer(), region.size());
|
|
return new VirtualMemory(region, region);
|
|
}
|
|
#endif // defined(DART_COMPRESSED_POINTERS)
|
|
|
|
const intptr_t allocated_size = size + alignment - PageSize();
|
|
|
|
#if defined(DART_ENABLE_RX_WORKAROUNDS)
|
|
// We need to map the original page using RX for dual mapping to have
|
|
// effect on iOS.
|
|
const int prot = (is_executable && should_dual_map_executable_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 && IsAtLeastMacOSX10_14() &&
|
|
!ShouldDualMapExecutablePages()) {
|
|
map_flags |= MAP_JIT;
|
|
}
|
|
#endif // defined(DART_HOST_OS_MACOS)
|
|
|
|
void* hint = nullptr;
|
|
// Some 64-bit microarchitectures store only the low 32-bits of targets as
|
|
// part of indirect branch prediction, predicting that the target's upper bits
|
|
// will be same as the call instruction's address. This leads to misprediction
|
|
// for indirect calls crossing a 4GB boundary. We ask mmap to place our
|
|
// generated code near the VM binary to avoid this.
|
|
if (is_executable) {
|
|
hint = reinterpret_cast<void*>(&Dart_Initialize);
|
|
}
|
|
void* address =
|
|
GenericMapAligned(hint, prot, size, alignment, allocated_size, map_flags);
|
|
#if defined(DART_HOST_OS_LINUX)
|
|
// On WSL 1 trying to allocate memory close to the binary by supplying a hint
|
|
// fails with ENOMEM for unclear reason. Some reports suggest that this might
|
|
// be related to the alignment of the hint but aligning it by 64Kb does not
|
|
// make the issue go away in our experiments. Instead just retry without any
|
|
// hint.
|
|
if (address == nullptr && hint != nullptr &&
|
|
Utils::IsWindowsSubsystemForLinux()) {
|
|
address = GenericMapAligned(nullptr, prot, size, alignment, allocated_size,
|
|
map_flags);
|
|
}
|
|
#endif
|
|
if (address == nullptr) {
|
|
return nullptr;
|
|
}
|
|
|
|
#if defined(DART_ENABLE_RX_WORKAROUNDS)
|
|
if (is_executable && should_dual_map_executable_pages_) {
|
|
// |address| is mapped RX, create a corresponding RW alias through which
|
|
// we will write into the executable mapping.
|
|
vm_address_t writable_address = 0;
|
|
vm_prot_t cur_protection, max_protection;
|
|
const kern_return_t result =
|
|
vm_remap(mach_task_self(), &writable_address, size,
|
|
/*mask=*/alignment - 1, VM_FLAGS_ANYWHERE, mach_task_self(),
|
|
reinterpret_cast<vm_address_t>(address), /*copy=*/FALSE,
|
|
&cur_protection, &max_protection, VM_INHERIT_NONE);
|
|
if (result != KERN_SUCCESS) {
|
|
munmap(address, size);
|
|
return nullptr;
|
|
}
|
|
|
|
NOTIFY_DEBUGGER_ABOUT_RX_PAGES(reinterpret_cast<void*>(address), size);
|
|
|
|
Protect(reinterpret_cast<void*>(writable_address), size, kReadWrite);
|
|
|
|
MemoryRegion region(address, size);
|
|
MemoryRegion writable_alias(reinterpret_cast<void*>(writable_address),
|
|
size);
|
|
return new VirtualMemory(writable_alias, region, writable_alias);
|
|
}
|
|
#endif // defined(DART_ENABLE_RX_WORKAROUNDS)
|
|
|
|
#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.
|
|
#if !defined(PR_SET_VMA)
|
|
#define PR_SET_VMA 0x53564d41
|
|
#endif
|
|
#if !defined(PR_SET_VMA_ANON_NAME)
|
|
#define PR_SET_VMA_ANON_NAME 0
|
|
#endif
|
|
prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, address, size, name);
|
|
#endif
|
|
|
|
MemoryRegion region(reinterpret_cast<void*>(address), size);
|
|
return new VirtualMemory(region, region);
|
|
}
|
|
|
|
VirtualMemory* VirtualMemory::Reserve(intptr_t size, intptr_t alignment) {
|
|
ASSERT(Utils::IsAligned(size, PageSize()));
|
|
ASSERT(Utils::IsPowerOfTwo(alignment));
|
|
ASSERT(Utils::IsAligned(alignment, PageSize()));
|
|
intptr_t allocated_size = size + alignment - PageSize();
|
|
void* address =
|
|
GenericMapAligned(nullptr, PROT_NONE, size, alignment, allocated_size,
|
|
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE);
|
|
if (address == nullptr) {
|
|
return nullptr;
|
|
}
|
|
MemoryRegion region(address, size);
|
|
return new VirtualMemory(region, region);
|
|
}
|
|
|
|
void VirtualMemory::Commit(void* address, intptr_t size) {
|
|
ASSERT(Utils::IsAligned(address, PageSize()));
|
|
ASSERT(Utils::IsAligned(size, PageSize()));
|
|
void* result = mmap(address, size, PROT_READ | PROT_WRITE,
|
|
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
|
|
if (result == MAP_FAILED) {
|
|
int error = errno;
|
|
const int kBufferSize = 1024;
|
|
char error_buf[kBufferSize];
|
|
FATAL("Failed to commit: %d (%s)", error,
|
|
Utils::StrError(error, error_buf, kBufferSize));
|
|
}
|
|
}
|
|
|
|
void VirtualMemory::Decommit(void* address, intptr_t size) {
|
|
ASSERT(Utils::IsAligned(address, PageSize()));
|
|
ASSERT(Utils::IsAligned(size, PageSize()));
|
|
void* result =
|
|
mmap(address, size, PROT_NONE,
|
|
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED, -1, 0);
|
|
if (result == MAP_FAILED) {
|
|
int error = errno;
|
|
const int kBufferSize = 1024;
|
|
char error_buf[kBufferSize];
|
|
FATAL("Failed to decommit: %d (%s)", error,
|
|
Utils::StrError(error, error_buf, kBufferSize));
|
|
}
|
|
}
|
|
|
|
VirtualMemory::~VirtualMemory() {
|
|
#if defined(DART_COMPRESSED_POINTERS)
|
|
if (VirtualMemoryCompressedHeap::Contains(reserved_.pointer()) &&
|
|
(this != compressed_heap_)) {
|
|
Decommit(reserved_.pointer(), reserved_.size());
|
|
VirtualMemoryCompressedHeap::Free(reserved_.pointer(), reserved_.size());
|
|
return;
|
|
}
|
|
#endif // defined(DART_COMPRESSED_POINTERS)
|
|
if (vm_owns_region()) {
|
|
Unmap(reserved_.start(), reserved_.end());
|
|
#if defined(DART_ENABLE_RX_WORKAROUNDS)
|
|
if (reserved_.start() != executable_alias_.start()) {
|
|
Unmap(executable_alias_.start(), executable_alias_.end());
|
|
}
|
|
#endif // defined(DART_ENABLE_RX_WORKAROUNDS)
|
|
}
|
|
}
|
|
|
|
bool VirtualMemory::FreeSubSegment(void* address, intptr_t size) {
|
|
#if defined(DART_COMPRESSED_POINTERS)
|
|
// Don't free the sub segment if it's managed by the compressed pointer heap.
|
|
if (VirtualMemoryCompressedHeap::Contains(address)) {
|
|
return false;
|
|
}
|
|
#endif // defined(DART_COMPRESSED_POINTERS)
|
|
const uword start = reinterpret_cast<uword>(address);
|
|
Unmap(start, start + size);
|
|
return true;
|
|
}
|
|
|
|
void VirtualMemory::Protect(void* address, intptr_t size, Protection mode) {
|
|
#if defined(DEBUG)
|
|
Thread* thread = Thread::Current();
|
|
ASSERT(thread == nullptr || thread->IsDartMutatorThread() ||
|
|
thread->isolate() == nullptr ||
|
|
thread->isolate()->mutator_thread()->IsAtSafepoint());
|
|
#endif
|
|
uword start_address = reinterpret_cast<uword>(address);
|
|
uword end_address = start_address + size;
|
|
uword page_address = Utils::RoundDown(start_address, PageSize());
|
|
int prot = 0;
|
|
switch (mode) {
|
|
case kNoAccess:
|
|
prot = PROT_NONE;
|
|
break;
|
|
case kReadOnly:
|
|
prot = PROT_READ;
|
|
break;
|
|
case kReadWrite:
|
|
prot = PROT_READ | PROT_WRITE;
|
|
break;
|
|
case kReadExecute:
|
|
prot = PROT_READ | PROT_EXEC;
|
|
break;
|
|
case kReadWriteExecute:
|
|
prot = PROT_READ | PROT_WRITE | PROT_EXEC;
|
|
break;
|
|
}
|
|
if (mprotect(reinterpret_cast<void*>(page_address),
|
|
end_address - page_address, prot) != 0) {
|
|
int error = errno;
|
|
const int kBufferSize = 1024;
|
|
char error_buf[kBufferSize];
|
|
LOG_INFO("mprotect(0x%" Px ", 0x%" Px ", %u) failed\n", page_address,
|
|
end_address - page_address, prot);
|
|
FATAL("mprotect failed: %d (%s)", error,
|
|
Utils::StrError(error, error_buf, kBufferSize));
|
|
}
|
|
LOG_INFO("mprotect(0x%" Px ", 0x%" Px ", %u) ok\n", page_address,
|
|
end_address - page_address, prot);
|
|
}
|
|
|
|
void VirtualMemory::DontNeed(void* address, intptr_t size) {
|
|
uword start_address = reinterpret_cast<uword>(address);
|
|
uword end_address = start_address + size;
|
|
uword page_address = Utils::RoundDown(start_address, PageSize());
|
|
#if defined(DART_HOST_OS_MACOS)
|
|
int advice = MADV_FREE;
|
|
#else
|
|
int advice = MADV_DONTNEED;
|
|
#endif
|
|
if (madvise(reinterpret_cast<void*>(page_address), end_address - page_address,
|
|
advice) != 0) {
|
|
int error = errno;
|
|
const int kBufferSize = 1024;
|
|
char error_buf[kBufferSize];
|
|
FATAL("madvise failed: %d (%s)", error,
|
|
Utils::StrError(error, error_buf, kBufferSize));
|
|
}
|
|
}
|
|
|
|
#if defined(DART_HOST_OS_MACOS)
|
|
// TODO(52579): Reenable on Fuchsia.
|
|
bool VirtualMemory::DuplicateRX(VirtualMemory* target) {
|
|
const intptr_t aligned_size = Utils::RoundUp(size(), PageSize());
|
|
ASSERT_LESS_OR_EQUAL(aligned_size, target->size());
|
|
|
|
// Mac is special cased because iOS doesn't allow allocating new executable
|
|
// memory, so the default approach would fail. We are allowed to make new
|
|
// mappings of existing executable memory using vm_remap though, which is
|
|
// effectively the same for non-writable memory.
|
|
const mach_port_t task = mach_task_self();
|
|
const vm_address_t source_address = reinterpret_cast<vm_address_t>(address());
|
|
const vm_size_t mem_size = aligned_size;
|
|
const vm_prot_t read_execute = VM_PROT_READ | VM_PROT_EXECUTE;
|
|
vm_prot_t current_protection = read_execute;
|
|
vm_prot_t max_protection = read_execute;
|
|
vm_address_t target_address =
|
|
reinterpret_cast<vm_address_t>(target->address());
|
|
kern_return_t status = vm_remap(
|
|
task, &target_address, mem_size,
|
|
/*mask=*/0,
|
|
/*flags=*/VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE, task, source_address,
|
|
/*copy=*/true, ¤t_protection, &max_protection,
|
|
/*inheritance=*/VM_INHERIT_NONE);
|
|
if (status != KERN_SUCCESS) {
|
|
return false;
|
|
}
|
|
ASSERT(reinterpret_cast<void*>(target_address) == target->address());
|
|
ASSERT_EQUAL(current_protection & read_execute, read_execute);
|
|
ASSERT_EQUAL(max_protection & read_execute, read_execute);
|
|
return true;
|
|
}
|
|
#endif // defined(DART_HOST_OS_MACOS)
|
|
|
|
} // namespace dart
|
|
|
|
#endif // defined(DART_HOST_OS_ANDROID) || defined(DART_HOST_OS_LINUX) || \
|
|
// defined(DART_HOST_OS_MACOS)
|