[vm] Use dual mapping of code pages on certain OS versions
Heap references always point into R/RW mapping (this simplifies the marker) and entry points are retargeted into RX. To simplify implementation we assume that there are no image pages created - which means we can always go from Instructions to the start of the page and check where it is dual mapped to. TEST=vm/dart/macos_dual_mapping_smoke_test and manually on physical device Change-Id: Idbe02b7b695b3c048072cf92f7d062f5ab6e1beb Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/433940 Reviewed-by: Siva Annamalai <asiva@google.com> Reviewed-by: Alexander Markov <alexmarkov@google.com> Commit-Queue: Slava Egorov <vegorov@google.com>
This commit is contained in:
committed by
Commit Queue
parent
a226405a70
commit
d194fcecaf
@@ -141,10 +141,15 @@ int32_t DarwinVersionInternal() {
|
||||
int32_t minor_version = 0;
|
||||
|
||||
#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 (kernel_major_version >= 25) {
|
||||
// Starting from iOS 26 kernel versions are 1 behind OS version.
|
||||
major_version = kernel_major_version + 1;
|
||||
} else {
|
||||
// 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
|
||||
|
||||
@@ -31,6 +31,7 @@ int32_t DarwinVersion();
|
||||
}
|
||||
|
||||
DEFINE_IS_OS_FUNCS(18_4, 180400)
|
||||
DEFINE_IS_OS_FUNCS(26_0, 260000)
|
||||
|
||||
#else
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2025, 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.
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:ffi';
|
||||
|
||||
int foo(int v) {
|
||||
return v + 41;
|
||||
}
|
||||
|
||||
void main(List<String> args) {
|
||||
// Use some FFI callbacks to test that image pages are correctly setup.
|
||||
final nativeCallable = NativeCallable<Int32 Function(Int32)>.isolateLocal(
|
||||
foo,
|
||||
exceptionalReturn: 0,
|
||||
);
|
||||
final result = nativeCallable.nativeFunction.asFunction<int Function(int)>()(
|
||||
1,
|
||||
);
|
||||
nativeCallable.close();
|
||||
|
||||
final String encoded = base64.encode(args[0].codeUnits);
|
||||
final String decoded = String.fromCharCodes(base64.decode(encoded));
|
||||
print('$result$decoded');
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) 2025, 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.
|
||||
|
||||
// OtherResources=macos_dual_mapping_smoke_script.dart
|
||||
|
||||
// Tests that dual mapping works on Mac OS. This is a smoke test for
|
||||
// functionality enabled in development mode on iOS 26 devices.
|
||||
//
|
||||
// To test this code path we use --force_dual_mapping_of_code_pages which
|
||||
// assumes that VM does not execute from a snapshot. Hence the need to run
|
||||
// from Kernel directly.
|
||||
|
||||
import 'dart:io' show Platform, File;
|
||||
|
||||
import 'package:expect/expect.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'snapshot_test_helper.dart';
|
||||
|
||||
compileAndRunMinimalDillTest(List<String> extraCompilationArgs) async {
|
||||
final testScriptUri = Platform.script.resolve(
|
||||
'macos_dual_mapping_smoke_script.dart',
|
||||
);
|
||||
final message = 'Round_trip_message';
|
||||
final expectedResponse = '42$message';
|
||||
|
||||
await withTempDir((String temp) async {
|
||||
final minimalDillPath = path.join(temp, 'test.dill');
|
||||
await runGenKernel('BUILD DILL FILE', [
|
||||
'--no-link-platform',
|
||||
...extraCompilationArgs,
|
||||
'--output=$minimalDillPath',
|
||||
testScriptUri.toFilePath(),
|
||||
]);
|
||||
|
||||
{
|
||||
final result = await runDart('RUN FROM DILL FILE', [
|
||||
minimalDillPath,
|
||||
message,
|
||||
]);
|
||||
expectOutput(expectedResponse, result);
|
||||
}
|
||||
|
||||
final String unsignedDartExecutable;
|
||||
final entitlementsInfo = (await runBinary(
|
||||
'CHECKING ENTITLEMENTS',
|
||||
'codesign',
|
||||
['-d', '--entitlements', '-', '--xml', Platform.executable],
|
||||
allowNonZeroExitCode: true,
|
||||
)).processResult;
|
||||
Expect.isTrue(entitlementsInfo.stderr.startsWith('Executable='));
|
||||
if (entitlementsInfo.stdout.contains('<?xml') &&
|
||||
!entitlementsInfo.stdout.contains(
|
||||
'com.apple.security.cs.allow-unsigned-executable-memory',
|
||||
)) {
|
||||
// Non-empty entitlements without
|
||||
// com.apple.security.cs.allow-unsigned-executable-memory will make
|
||||
// this test fail so strip them.
|
||||
unsignedDartExecutable = path.join(temp, 'dart');
|
||||
File(Platform.executable).copySync(unsignedDartExecutable);
|
||||
await runBinary('REMOVING SIGNATURE', 'codesign', [
|
||||
'--remove-signature',
|
||||
unsignedDartExecutable,
|
||||
]);
|
||||
await runBinary('RESIGNING', 'codesign', [
|
||||
'-s',
|
||||
'-',
|
||||
unsignedDartExecutable,
|
||||
]);
|
||||
} else {
|
||||
unsignedDartExecutable = Platform.executable;
|
||||
}
|
||||
|
||||
{
|
||||
final result = await runDart('RUN FROM DILL FILE', [
|
||||
'--force_dual_mapping_of_code_pages',
|
||||
minimalDillPath,
|
||||
message,
|
||||
], dartExecutable: unsignedDartExecutable);
|
||||
expectOutput(expectedResponse, result);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void main() async {
|
||||
// Only supported on MacOS.
|
||||
if (!Platform.isMacOS || isAOTRuntime) {
|
||||
return;
|
||||
}
|
||||
await compileAndRunMinimalDillTest([]);
|
||||
}
|
||||
@@ -54,6 +54,7 @@ Future<Result> runDart(
|
||||
String prefix,
|
||||
List<String> arguments, {
|
||||
bool printOut = true,
|
||||
String? dartExecutable,
|
||||
}) {
|
||||
final augmentedArguments = <String>[]
|
||||
..addAll(Platform.executableArguments)
|
||||
@@ -61,7 +62,7 @@ Future<Result> runDart(
|
||||
..addAll(arguments);
|
||||
return runBinary(
|
||||
prefix,
|
||||
Platform.executable,
|
||||
dartExecutable ?? Platform.executable,
|
||||
augmentedArguments,
|
||||
printOut: printOut,
|
||||
);
|
||||
@@ -100,6 +101,7 @@ Future<Result> runBinary(
|
||||
Map<String, String>? environment,
|
||||
bool runInShell = false,
|
||||
bool printOut = true,
|
||||
bool allowNonZeroExitCode = false,
|
||||
}) async {
|
||||
print("+ $binary " + arguments.join(" "));
|
||||
final processResult = await Process.run(
|
||||
@@ -127,7 +129,7 @@ Command stderr:
|
||||
${processResult.stderr}''');
|
||||
}
|
||||
|
||||
if (result.processResult.exitCode != 0) {
|
||||
if (!allowNonZeroExitCode && result.processResult.exitCode != 0) {
|
||||
reportError(
|
||||
result,
|
||||
'[$prefix] Process finished with non-zero exit code ${result.processResult.exitCode}',
|
||||
|
||||
@@ -336,6 +336,9 @@ dart/stacktrace_mixin_application_test: SkipByDesign # Relies symbol names in st
|
||||
[ $compiler == dart2analyzer || $compiler == dart2js ]
|
||||
dart/data_uri*test: Skip # Data uri's not supported by dart2js or the analyzer.
|
||||
|
||||
[ $compiler != dartk || $runtime != vm || $system != macos ]
|
||||
dart/macos_dual_mapping_smoke: SkipByDesign
|
||||
|
||||
[ $mode == debug || $runtime != dart_precompiled || $system == android ]
|
||||
dart/emit_aot_size_info_flag_test: SkipByDesign # This test is for VM AOT only and is quite slow (so we don't run it in debug mode).
|
||||
dart/split_aot_kernel_generation2_test: SkipByDesign # This test is for VM AOT only and is quite slow (so we don't run it in debug mode).
|
||||
|
||||
@@ -190,7 +190,8 @@ void CodePatcher::PatchStaticCallAt(uword return_address,
|
||||
auto zone = thread->zone();
|
||||
const Instructions& instrs = Instructions::Handle(zone, code.instructions());
|
||||
thread->isolate_group()->RunWithStoppedMutators([&]() {
|
||||
WritableInstructionsScope writable(instrs.PayloadStart(), instrs.Size());
|
||||
WritableInstructionsScope writable(instrs.WritablePayloadStart(),
|
||||
instrs.Size());
|
||||
ASSERT(code.ContainsInstructionAt(return_address));
|
||||
StaticCall call(return_address, code);
|
||||
call.set_target(new_target);
|
||||
@@ -233,7 +234,8 @@ void CodePatcher::PatchInstanceCallAtWithMutatorsStopped(
|
||||
ASSERT(caller_code.ContainsInstructionAt(return_address));
|
||||
const Instructions& instrs =
|
||||
Instructions::Handle(zone, caller_code.instructions());
|
||||
WritableInstructionsScope writable(instrs.PayloadStart(), instrs.Size());
|
||||
WritableInstructionsScope writable(instrs.WritablePayloadStart(),
|
||||
instrs.Size());
|
||||
InstanceCall call(return_address, caller_code);
|
||||
call.set_data(data);
|
||||
call.set_target(target);
|
||||
|
||||
@@ -260,6 +260,7 @@ struct RelocatorTestHelper {
|
||||
}
|
||||
|
||||
if (FLAG_write_protect_code) {
|
||||
ASSERT(!VirtualMemory::ShouldDualMapExecutablePages());
|
||||
const uword address = UntaggedObject::ToAddr(instructions.ptr());
|
||||
const auto size = instructions.ptr()->untag()->HeapSize();
|
||||
VirtualMemory::Protect(reinterpret_cast<void*>(address), size,
|
||||
|
||||
@@ -31,7 +31,8 @@ void CodeBreakpoint::PatchCode() {
|
||||
const Instructions& instrs = Instructions::Handle(zone, code.instructions());
|
||||
Code& stub_target = Code::Handle(zone);
|
||||
thread->isolate_group()->RunWithStoppedMutators([&]() {
|
||||
WritableInstructionsScope writable(instrs.PayloadStart(), instrs.Size());
|
||||
WritableInstructionsScope writable(instrs.WritablePayloadStart(),
|
||||
instrs.Size());
|
||||
switch (breakpoint_kind_) {
|
||||
case UntaggedPcDescriptors::kIcCall: {
|
||||
stub_target = StubCode::ICCallBreakpoint().ptr();
|
||||
@@ -61,7 +62,8 @@ void CodeBreakpoint::RestoreCode() {
|
||||
const Code& code = Code::Handle(zone, code_);
|
||||
const Instructions& instrs = Instructions::Handle(zone, code.instructions());
|
||||
thread->isolate_group()->RunWithStoppedMutators([&]() {
|
||||
WritableInstructionsScope writable(instrs.PayloadStart(), instrs.Size());
|
||||
WritableInstructionsScope writable(instrs.WritablePayloadStart(),
|
||||
instrs.Size());
|
||||
switch (breakpoint_kind_) {
|
||||
case UntaggedPcDescriptors::kIcCall:
|
||||
case UntaggedPcDescriptors::kUnoptStaticCall:
|
||||
|
||||
@@ -118,7 +118,7 @@ VirtualMemory* FfiCallbackMetadata::AllocateTrampolinePage() {
|
||||
// is_executable=true so that pages get allocated with MAP_JIT flag if
|
||||
// necessary. Otherwise OS will kill us with a codesigning violation if
|
||||
// hardened runtime is enabled.
|
||||
const bool is_executable = true;
|
||||
const bool is_executable = !VirtualMemory::ShouldDualMapExecutablePages();
|
||||
#else
|
||||
const bool is_executable = false;
|
||||
#endif
|
||||
|
||||
@@ -293,10 +293,11 @@ void Page::ResetProgressBar() {
|
||||
|
||||
void Page::WriteProtect(bool read_only) {
|
||||
ASSERT(!is_image());
|
||||
|
||||
VirtualMemory::Protection prot;
|
||||
if (read_only) {
|
||||
if (is_executable()) {
|
||||
// When dual mapping code pages we don't change protection on RX page, but
|
||||
// flip RW to R and back.
|
||||
if (is_executable() && !VirtualMemory::ShouldDualMapExecutablePages()) {
|
||||
prot = VirtualMemory::kReadExecute;
|
||||
} else {
|
||||
prot = VirtualMemory::kReadOnly;
|
||||
|
||||
@@ -105,6 +105,9 @@ class Page {
|
||||
uword start() const { return memory_->start(); }
|
||||
uword end() const { return memory_->end(); }
|
||||
bool Contains(uword addr) const { return memory_->Contains(addr); }
|
||||
intptr_t OffsetToExecutableAlias() const {
|
||||
return memory_->OffsetToExecutableAlias();
|
||||
}
|
||||
|
||||
uword object_start() const {
|
||||
return is_new() ? new_object_start() : old_object_start();
|
||||
|
||||
@@ -1534,6 +1534,12 @@ uword PageSpace::AllocateSnapshotLockedSlow(FreeList* freelist, intptr_t size) {
|
||||
}
|
||||
|
||||
void PageSpace::SetupImagePage(void* pointer, uword size, bool is_executable) {
|
||||
if (VirtualMemory::ShouldDualMapExecutablePages()) {
|
||||
// See |Instructions::PayloadStart| for more details about this restriction.
|
||||
FATAL(
|
||||
"Dual mapping of executable pages assumes no image pages in the heap");
|
||||
}
|
||||
|
||||
// Setup a Page so precompiled Instructions can be traversed.
|
||||
// Instructions are contiguous at [pointer, pointer + size). Page
|
||||
// expects to find objects at [memory->start() + ObjectStartOffset,
|
||||
|
||||
@@ -18714,7 +18714,8 @@ CodePtr Code::FinalizeCode(FlowGraphCompiler* compiler,
|
||||
|
||||
// Copy the instructions into the instruction area and apply all fixups.
|
||||
// Embedded pointers are still in handles at this point.
|
||||
MemoryRegion region(reinterpret_cast<void*>(instrs.PayloadStart()),
|
||||
MemoryRegion region(reinterpret_cast<void*>(
|
||||
Instructions::WritablePayloadStart(instrs.ptr())),
|
||||
instrs.Size());
|
||||
assembler->FinalizeInstructions(region);
|
||||
|
||||
@@ -18741,10 +18742,15 @@ CodePtr Code::FinalizeCode(FlowGraphCompiler* compiler,
|
||||
// Write protect instructions and, if supported by OS, use dual mapping
|
||||
// for execution.
|
||||
if (FLAG_write_protect_code) {
|
||||
// Note: when dual mapping is used we have separate RX and RW mappings.
|
||||
// RX mapping never changes protection while RW mapping flips between
|
||||
// R and RW.
|
||||
uword address = UntaggedObject::ToAddr(instrs.ptr());
|
||||
VirtualMemory::Protect(reinterpret_cast<void*>(address),
|
||||
instrs.ptr()->untag()->HeapSize(),
|
||||
VirtualMemory::kReadExecute);
|
||||
VirtualMemory::ShouldDualMapExecutablePages()
|
||||
? VirtualMemory::kReadOnly
|
||||
: VirtualMemory::kReadExecute);
|
||||
}
|
||||
|
||||
// Hook up Code and Instructions objects.
|
||||
@@ -18766,7 +18772,7 @@ CodePtr Code::FinalizeCode(FlowGraphCompiler* compiler,
|
||||
}
|
||||
#endif
|
||||
|
||||
CPU::FlushICache(instrs.PayloadStart(), instrs.Size());
|
||||
CPU::FlushICache(region.start(), region.size());
|
||||
}
|
||||
|
||||
#if defined(INCLUDE_IL_PRINTER)
|
||||
|
||||
@@ -5790,10 +5790,29 @@ class Instructions : public Object {
|
||||
return SizeBits::decode(instr->untag()->size_and_flags_);
|
||||
}
|
||||
|
||||
// When dual mapping all GC references (e.g. instruction fields inside
|
||||
// Code) point into R/RW mapping. Entry points however all point into RX
|
||||
// memory. This makes marking simpler: because marker does not need to
|
||||
// translate RX mapping into RW mapping.
|
||||
uword PayloadStart() const { return PayloadStart(ptr()); }
|
||||
uword MonomorphicEntryPoint() const { return MonomorphicEntryPoint(ptr()); }
|
||||
uword EntryPoint() const { return EntryPoint(ptr()); }
|
||||
static uword PayloadStart(const InstructionsPtr instr) {
|
||||
if (VirtualMemory::ShouldDualMapExecutablePages()) {
|
||||
// Caveat: we assume that dual mapping is currently only enabled in iOS
|
||||
// in Flutter development mode. This mode does not use snapshots which
|
||||
// include image pages which means we can assume that all Instruction
|
||||
// objects are allocated on VM owned pages.
|
||||
// There is a check in |PageSpace::SetupImagePage| which enforces this.
|
||||
auto page = Page::Of(instr);
|
||||
RELEASE_ASSERT(page->Contains(reinterpret_cast<uword>(instr->untag())));
|
||||
return reinterpret_cast<uword>(instr->untag()) +
|
||||
page->OffsetToExecutableAlias() + HeaderSize();
|
||||
}
|
||||
return reinterpret_cast<uword>(instr->untag()) + HeaderSize();
|
||||
}
|
||||
|
||||
static word WritablePayloadStart(const InstructionsPtr instr) {
|
||||
return reinterpret_cast<uword>(instr->untag()) + HeaderSize();
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ bool VirtualMemory::InSamePage(uword address0, uword address1) {
|
||||
void VirtualMemory::Truncate(intptr_t new_size) {
|
||||
ASSERT(Utils::IsAligned(new_size, PageSize()));
|
||||
ASSERT(new_size <= size());
|
||||
// We are not expected to call this with executable pages.
|
||||
ASSERT(OffsetToExecutableAlias() == 0);
|
||||
if (reserved_.size() ==
|
||||
region_.size()) { // Don't create holes in reservation.
|
||||
if (FreeSubSegment(reinterpret_cast<void*>(start() + new_size),
|
||||
@@ -49,41 +51,53 @@ bool VirtualMemory::DuplicateRX(VirtualMemory* target) {
|
||||
const intptr_t aligned_size = Utils::RoundUp(size(), PageSize());
|
||||
ASSERT_LESS_OR_EQUAL(aligned_size, target->size());
|
||||
|
||||
#if defined(DART_HOST_OS_MACOS) && defined(DART_PRECOMPILED_RUNTIME)
|
||||
// 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;
|
||||
#if defined(DART_HOST_OS_MACOS)
|
||||
#if defined(DART_PRECOMPILED_RUNTIME)
|
||||
const bool should_remap = true;
|
||||
#else
|
||||
const bool should_remap = ShouldDualMapExecutablePages();
|
||||
#endif
|
||||
|
||||
if (should_remap) {
|
||||
// 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)
|
||||
|
||||
#else // defined(DART_HOST_OS_MACOS)
|
||||
// TODO(52497): Use dual mapping on platforms where it's supported.
|
||||
// Check that target doesn't overlap with this.
|
||||
ASSERT(target->start() >= end() || target->end() <= start());
|
||||
memcpy(target->address(), address(), size()); // NOLINT
|
||||
Protect(target->address(), aligned_size, kReadExecute);
|
||||
Protect(
|
||||
target->address(), aligned_size,
|
||||
VirtualMemory::ShouldDualMapExecutablePages() ? kReadOnly : kReadExecute);
|
||||
RELEASE_ASSERT(!VirtualMemory::ShouldDualMapExecutablePages() ||
|
||||
target->OffsetToExecutableAlias() != 0);
|
||||
return true;
|
||||
#endif // defined(DART_HOST_OS_MACOS)
|
||||
}
|
||||
|
||||
#endif // !defined(DART_TARGET_OS_FUCHSIA)
|
||||
|
||||
@@ -16,6 +16,13 @@
|
||||
|
||||
namespace dart {
|
||||
|
||||
#if defined(DART_HOST_OS_MACOS) && !defined(DART_PRECOMPILED_RUNTIME)
|
||||
// We only enable dual mapping of code on iOS (for Flutter debug mode)
|
||||
// and Mac OS X (for smoke testing of the dual mapping code path
|
||||
// on Dart bots).
|
||||
#define DART_SUPPORT_DUAL_MAPPING_OF_CODE
|
||||
#endif
|
||||
|
||||
class VirtualMemory {
|
||||
public:
|
||||
enum Protection {
|
||||
@@ -34,6 +41,14 @@ class VirtualMemory {
|
||||
void* address() const { return region_.pointer(); }
|
||||
intptr_t size() const { return region_.size(); }
|
||||
|
||||
DART_FORCE_INLINE intptr_t OffsetToExecutableAlias() const {
|
||||
#if defined(DART_SUPPORT_DUAL_MAPPING_OF_CODE)
|
||||
return executable_alias_.start() - region_.start();
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(DART_HOST_OS_FUCHSIA)
|
||||
static void Init(zx_handle_t vmex_resource);
|
||||
#else
|
||||
@@ -41,6 +56,14 @@ class VirtualMemory {
|
||||
#endif
|
||||
static void Cleanup();
|
||||
|
||||
DART_FORCE_INLINE static bool ShouldDualMapExecutablePages() {
|
||||
#if defined(DART_SUPPORT_DUAL_MAPPING_OF_CODE)
|
||||
return should_dual_map_executable_pages_;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Contains(uword addr) const { return region_.Contains(addr); }
|
||||
|
||||
// Changes the protection of the virtual memory area.
|
||||
@@ -104,13 +127,34 @@ class VirtualMemory {
|
||||
static void Commit(void* address, intptr_t size);
|
||||
static void Decommit(void* address, intptr_t size);
|
||||
|
||||
#if defined(DART_SUPPORT_DUAL_MAPPING_OF_CODE)
|
||||
// These constructors are only used internally when reserving new virtual
|
||||
// spaces. They do not reserve any virtual address space on their own.
|
||||
VirtualMemory(const MemoryRegion& region,
|
||||
const MemoryRegion& executable_alias,
|
||||
const MemoryRegion& reserved)
|
||||
: region_(region),
|
||||
executable_alias_(executable_alias),
|
||||
reserved_(reserved) {}
|
||||
#endif
|
||||
|
||||
VirtualMemory(const MemoryRegion& region, const MemoryRegion& reserved)
|
||||
: region_(region), reserved_(reserved) {}
|
||||
: region_(region),
|
||||
#if defined(DART_SUPPORT_DUAL_MAPPING_OF_CODE)
|
||||
executable_alias_(region),
|
||||
#endif
|
||||
reserved_(reserved) {
|
||||
}
|
||||
|
||||
MemoryRegion region_;
|
||||
|
||||
#if defined(DART_SUPPORT_DUAL_MAPPING_OF_CODE)
|
||||
// For dual mapped RW+RX pages this will contain address of the executable
|
||||
// alias. Objects will be allocated in the writable mapping but entry points
|
||||
// will point into executable (RX) alias.
|
||||
MemoryRegion executable_alias_;
|
||||
#endif
|
||||
|
||||
// The underlying reservation not yet given back to the OS.
|
||||
// Its address might disagree with region_ due to aligned allocations.
|
||||
// Its size might disagree with region_ due to Truncate.
|
||||
@@ -123,6 +167,10 @@ class VirtualMemory {
|
||||
static bool notify_debugger_about_rx_pages_;
|
||||
#endif
|
||||
|
||||
#if defined(DART_SUPPORT_DUAL_MAPPING_OF_CODE)
|
||||
static bool should_dual_map_executable_pages_;
|
||||
#endif
|
||||
|
||||
DISALLOW_IMPLICIT_CONSTRUCTORS(VirtualMemory);
|
||||
};
|
||||
|
||||
|
||||
@@ -19,6 +19,11 @@
|
||||
#include <sys/prctl.h>
|
||||
#endif
|
||||
|
||||
#if defined(DART_HOST_OS_MACOS)
|
||||
#include <mach/mach_init.h>
|
||||
#include <mach/vm_map.h>
|
||||
#endif
|
||||
|
||||
#include "platform/assert.h"
|
||||
#include "platform/utils.h"
|
||||
#include "vm/heap/pages.h"
|
||||
@@ -45,6 +50,14 @@ namespace dart {
|
||||
|
||||
DECLARE_FLAG(bool, write_protect_code);
|
||||
|
||||
#if defined(DART_HOST_OS_MACOS)
|
||||
// For testing on Mac OS.
|
||||
DEFINE_FLAG(bool,
|
||||
force_dual_mapping_of_code_pages,
|
||||
false,
|
||||
"Force dual mapping of RX pages");
|
||||
#endif
|
||||
|
||||
#if defined(DART_TARGET_OS_LINUX)
|
||||
DECLARE_FLAG(bool, generate_perf_events_symbols);
|
||||
DECLARE_FLAG(bool, generate_perf_jitdump);
|
||||
@@ -56,6 +69,10 @@ VirtualMemory* VirtualMemory::compressed_heap_ = nullptr;
|
||||
bool VirtualMemory::notify_debugger_about_rx_pages_ = false;
|
||||
#endif
|
||||
|
||||
#if defined(DART_SUPPORT_DUAL_MAPPING_OF_CODE)
|
||||
bool VirtualMemory::should_dual_map_executable_pages_ = false;
|
||||
#endif // defined(DART_SUPPORT_DUAL_MAPPING_OF_CODE)
|
||||
|
||||
static void* Map(void* addr,
|
||||
size_t length,
|
||||
int prot,
|
||||
@@ -276,8 +293,27 @@ void VirtualMemory::Init() {
|
||||
FLAG_new_gen_semi_max_size = kDefaultNewGenSemiMaxSize;
|
||||
}
|
||||
page_size_ = CalculatePageSize();
|
||||
|
||||
#if defined(DART_SUPPORT_DUAL_MAPPING_OF_CODE)
|
||||
if (FLAG_force_dual_mapping_of_code_pages) {
|
||||
should_dual_map_executable_pages_ = true;
|
||||
}
|
||||
#endif // defined(DART_SUPPORT_DUAL_MAPPING_OF_CODE)
|
||||
|
||||
#if defined(DART_HOST_OS_IOS) && !defined(DART_PRECOMPILED_RUNTIME)
|
||||
notify_debugger_about_rx_pages_ = CheckIfNeedDebuggerHelpWithRX();
|
||||
if (IsAtLeastIOS26_0()) {
|
||||
// Ideally we would want to test if dual mapping works or not and give a
|
||||
// meaningful error message (i.e. assembling and then calling a simple
|
||||
// function and then catching a SIGBUG signal if that fails). However
|
||||
// setting signal handler does not prevent debugger from breaking on
|
||||
// exception. It is possible to use Mach exception ports to suppress
|
||||
// exception EXC_BAD_ACCESS from reaching the debugger but required
|
||||
// code is rather complicated - so we simply turn dual mapping on
|
||||
// and expect it to work.
|
||||
should_dual_map_executable_pages_ = true;
|
||||
} else {
|
||||
notify_debugger_about_rx_pages_ = CheckIfNeedDebuggerHelpWithRX();
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(DART_COMPRESSED_POINTERS)
|
||||
@@ -391,8 +427,17 @@ VirtualMemory* VirtualMemory::AllocateAligned(intptr_t size,
|
||||
|
||||
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_)
|
||||
#if defined(DART_SUPPORT_DUAL_MAPPING_OF_CODE)
|
||||
#if defined(DART_HOST_OS_IOS)
|
||||
const bool notify_debugger_about_rx_pages = notify_debugger_about_rx_pages_;
|
||||
#else
|
||||
const bool notify_debugger_about_rx_pages = false;
|
||||
#endif
|
||||
|
||||
// We need to map the original page using RX for dual mapping to have
|
||||
// effect on iOS.
|
||||
const int prot = (is_executable && (notify_debugger_about_rx_pages ||
|
||||
should_dual_map_executable_pages_))
|
||||
? PROT_READ | PROT_EXEC
|
||||
: PROT_READ | PROT_WRITE;
|
||||
#else
|
||||
@@ -403,7 +448,8 @@ VirtualMemory* VirtualMemory::AllocateAligned(intptr_t size,
|
||||
|
||||
int map_flags = MAP_PRIVATE | MAP_ANONYMOUS;
|
||||
#if (defined(DART_HOST_OS_MACOS) && !defined(DART_HOST_OS_IOS))
|
||||
if (is_executable && IsAtLeastMacOSX10_14()) {
|
||||
if (is_executable && IsAtLeastMacOSX10_14() &&
|
||||
!ShouldDualMapExecutablePages()) {
|
||||
map_flags |= MAP_JIT;
|
||||
}
|
||||
#endif // defined(DART_HOST_OS_MACOS)
|
||||
@@ -444,6 +490,30 @@ VirtualMemory* VirtualMemory::AllocateAligned(intptr_t size,
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(DART_SUPPORT_DUAL_MAPPING_OF_CODE)
|
||||
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;
|
||||
}
|
||||
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_SUPPORT_DUAL_MAPPING_OF_CODE)
|
||||
|
||||
#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.
|
||||
@@ -515,6 +585,11 @@ VirtualMemory::~VirtualMemory() {
|
||||
#endif // defined(DART_COMPRESSED_POINTERS)
|
||||
if (vm_owns_region()) {
|
||||
Unmap(reserved_.start(), reserved_.end());
|
||||
#if defined(DART_SUPPORT_DUAL_MAPPING_OF_CODE)
|
||||
if (reserved_.start() != executable_alias_.start()) {
|
||||
Unmap(executable_alias_.start(), executable_alias_.end());
|
||||
}
|
||||
#endif // defined(DART_SUPPORT_DUAL_MAPPING_OF_CODE)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user