diff --git a/runtime/platform/utils_macos.cc b/runtime/platform/utils_macos.cc index 01ed447bd27..fa82cb20e06 100644 --- a/runtime/platform/utils_macos.cc +++ b/runtime/platform/utils_macos.cc @@ -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 diff --git a/runtime/platform/utils_macos.h b/runtime/platform/utils_macos.h index 5f0e19a8931..e2e2adc47cf 100644 --- a/runtime/platform/utils_macos.h +++ b/runtime/platform/utils_macos.h @@ -31,6 +31,7 @@ int32_t DarwinVersion(); } DEFINE_IS_OS_FUNCS(18_4, 180400) +DEFINE_IS_OS_FUNCS(26_0, 260000) #else diff --git a/runtime/tests/vm/dart/macos_dual_mapping_smoke_script.dart b/runtime/tests/vm/dart/macos_dual_mapping_smoke_script.dart new file mode 100644 index 00000000000..e1b5cc3b4de --- /dev/null +++ b/runtime/tests/vm/dart/macos_dual_mapping_smoke_script.dart @@ -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 args) { + // Use some FFI callbacks to test that image pages are correctly setup. + final nativeCallable = NativeCallable.isolateLocal( + foo, + exceptionalReturn: 0, + ); + final result = nativeCallable.nativeFunction.asFunction()( + 1, + ); + nativeCallable.close(); + + final String encoded = base64.encode(args[0].codeUnits); + final String decoded = String.fromCharCodes(base64.decode(encoded)); + print('$result$decoded'); +} diff --git a/runtime/tests/vm/dart/macos_dual_mapping_smoke_test.dart b/runtime/tests/vm/dart/macos_dual_mapping_smoke_test.dart new file mode 100644 index 00000000000..30bbb1844ae --- /dev/null +++ b/runtime/tests/vm/dart/macos_dual_mapping_smoke_test.dart @@ -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 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(' runDart( String prefix, List arguments, { bool printOut = true, + String? dartExecutable, }) { final augmentedArguments = [] ..addAll(Platform.executableArguments) @@ -61,7 +62,7 @@ Future runDart( ..addAll(arguments); return runBinary( prefix, - Platform.executable, + dartExecutable ?? Platform.executable, augmentedArguments, printOut: printOut, ); @@ -100,6 +101,7 @@ Future runBinary( Map? 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}', diff --git a/runtime/tests/vm/vm.status b/runtime/tests/vm/vm.status index b739a6a6d5b..93a224bc1e8 100644 --- a/runtime/tests/vm/vm.status +++ b/runtime/tests/vm/vm.status @@ -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). diff --git a/runtime/vm/code_patcher_ia32.cc b/runtime/vm/code_patcher_ia32.cc index 59558c84435..73f8cb45632 100644 --- a/runtime/vm/code_patcher_ia32.cc +++ b/runtime/vm/code_patcher_ia32.cc @@ -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); diff --git a/runtime/vm/compiler/relocation_test.cc b/runtime/vm/compiler/relocation_test.cc index 600202c8e3e..f5b1619ff60 100644 --- a/runtime/vm/compiler/relocation_test.cc +++ b/runtime/vm/compiler/relocation_test.cc @@ -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(address), size, diff --git a/runtime/vm/debugger_ia32.cc b/runtime/vm/debugger_ia32.cc index f244a34d9e0..da26c140f89 100644 --- a/runtime/vm/debugger_ia32.cc +++ b/runtime/vm/debugger_ia32.cc @@ -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: diff --git a/runtime/vm/ffi_callback_metadata.cc b/runtime/vm/ffi_callback_metadata.cc index 9fd7ec1efdd..57c23749a98 100644 --- a/runtime/vm/ffi_callback_metadata.cc +++ b/runtime/vm/ffi_callback_metadata.cc @@ -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 diff --git a/runtime/vm/heap/page.cc b/runtime/vm/heap/page.cc index 9044d175011..8a18ff4be68 100644 --- a/runtime/vm/heap/page.cc +++ b/runtime/vm/heap/page.cc @@ -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; diff --git a/runtime/vm/heap/page.h b/runtime/vm/heap/page.h index 7f1978f7df4..db9cf06ec33 100644 --- a/runtime/vm/heap/page.h +++ b/runtime/vm/heap/page.h @@ -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(); diff --git a/runtime/vm/heap/pages.cc b/runtime/vm/heap/pages.cc index b4e37698ed3..97de4147306 100644 --- a/runtime/vm/heap/pages.cc +++ b/runtime/vm/heap/pages.cc @@ -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, diff --git a/runtime/vm/object.cc b/runtime/vm/object.cc index cade951a0a5..299811b6b8d 100644 --- a/runtime/vm/object.cc +++ b/runtime/vm/object.cc @@ -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(instrs.PayloadStart()), + MemoryRegion region(reinterpret_cast( + 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(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) diff --git a/runtime/vm/object.h b/runtime/vm/object.h index d058cfabed2..e6502173a75 100644 --- a/runtime/vm/object.h +++ b/runtime/vm/object.h @@ -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(instr->untag()))); + return reinterpret_cast(instr->untag()) + + page->OffsetToExecutableAlias() + HeaderSize(); + } + return reinterpret_cast(instr->untag()) + HeaderSize(); + } + + static word WritablePayloadStart(const InstructionsPtr instr) { return reinterpret_cast(instr->untag()) + HeaderSize(); } diff --git a/runtime/vm/virtual_memory.cc b/runtime/vm/virtual_memory.cc index 7f1a1938712..9e5a9b35b74 100644 --- a/runtime/vm/virtual_memory.cc +++ b/runtime/vm/virtual_memory.cc @@ -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(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(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(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(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(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(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(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) diff --git a/runtime/vm/virtual_memory.h b/runtime/vm/virtual_memory.h index 8271f6c52e6..7c331917c74 100644 --- a/runtime/vm/virtual_memory.h +++ b/runtime/vm/virtual_memory.h @@ -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); }; diff --git a/runtime/vm/virtual_memory_posix.cc b/runtime/vm/virtual_memory_posix.cc index f430c7a5f6f..5478553778d 100644 --- a/runtime/vm/virtual_memory_posix.cc +++ b/runtime/vm/virtual_memory_posix.cc @@ -19,6 +19,11 @@ #include #endif +#if defined(DART_HOST_OS_MACOS) +#include +#include +#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(address), /*copy=*/FALSE, + &cur_protection, &max_protection, VM_INHERIT_NONE); + if (result != KERN_SUCCESS) { + munmap(address, size); + return nullptr; + } + Protect(reinterpret_cast(writable_address), size, kReadWrite); + + MemoryRegion region(address, size); + MemoryRegion writable_alias(reinterpret_cast(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) } }