Skip to content

Commit d194fce

Browse files
mralephCommit Queue
authored andcommitted
[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 <[email protected]> Reviewed-by: Alexander Markov <[email protected]> Commit-Queue: Slava Egorov <[email protected]>
1 parent a226405 commit d194fce

18 files changed

+354
-49
lines changed

runtime/platform/utils_macos.cc

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,15 @@ int32_t DarwinVersionInternal() {
141141
int32_t minor_version = 0;
142142

143143
#if defined(DART_HOST_OS_IOS)
144-
// We do not expect to run on version of iOS <12.0 so we can assume that
145-
// kernel version is off by 6 from iOS version (e.g. kernel 18.0 is iOS 12.0).
146-
// This only holds starting from iOS 4.0.
147-
major_version = kernel_major_version - 6;
144+
if (kernel_major_version >= 25) {
145+
// Starting from iOS 26 kernel versions are 1 behind OS version.
146+
major_version = kernel_major_version + 1;
147+
} else {
148+
// We do not expect to run on version of iOS <12.0 so we can assume that
149+
// kernel version is off by 6 from iOS version (e.g. kernel 18.0 is
150+
// iOS 12.0). This only holds starting from iOS 4.0.
151+
major_version = kernel_major_version - 6;
152+
}
148153
if (major_version >= 15) {
149154
// After iOS 15 minor version of kernel is the same as minor version of
150155
// the iOS release. Before iOS 15 these numbers were not in sync. However

runtime/platform/utils_macos.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ int32_t DarwinVersion();
3131
}
3232

3333
DEFINE_IS_OS_FUNCS(18_4, 180400)
34+
DEFINE_IS_OS_FUNCS(26_0, 260000)
3435

3536
#else
3637

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:convert';
6+
import 'dart:ffi';
7+
8+
int foo(int v) {
9+
return v + 41;
10+
}
11+
12+
void main(List<String> args) {
13+
// Use some FFI callbacks to test that image pages are correctly setup.
14+
final nativeCallable = NativeCallable<Int32 Function(Int32)>.isolateLocal(
15+
foo,
16+
exceptionalReturn: 0,
17+
);
18+
final result = nativeCallable.nativeFunction.asFunction<int Function(int)>()(
19+
1,
20+
);
21+
nativeCallable.close();
22+
23+
final String encoded = base64.encode(args[0].codeUnits);
24+
final String decoded = String.fromCharCodes(base64.decode(encoded));
25+
print('$result$decoded');
26+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
// OtherResources=macos_dual_mapping_smoke_script.dart
6+
7+
// Tests that dual mapping works on Mac OS. This is a smoke test for
8+
// functionality enabled in development mode on iOS 26 devices.
9+
//
10+
// To test this code path we use --force_dual_mapping_of_code_pages which
11+
// assumes that VM does not execute from a snapshot. Hence the need to run
12+
// from Kernel directly.
13+
14+
import 'dart:io' show Platform, File;
15+
16+
import 'package:expect/expect.dart';
17+
import 'package:path/path.dart' as path;
18+
import 'snapshot_test_helper.dart';
19+
20+
compileAndRunMinimalDillTest(List<String> extraCompilationArgs) async {
21+
final testScriptUri = Platform.script.resolve(
22+
'macos_dual_mapping_smoke_script.dart',
23+
);
24+
final message = 'Round_trip_message';
25+
final expectedResponse = '42$message';
26+
27+
await withTempDir((String temp) async {
28+
final minimalDillPath = path.join(temp, 'test.dill');
29+
await runGenKernel('BUILD DILL FILE', [
30+
'--no-link-platform',
31+
...extraCompilationArgs,
32+
'--output=$minimalDillPath',
33+
testScriptUri.toFilePath(),
34+
]);
35+
36+
{
37+
final result = await runDart('RUN FROM DILL FILE', [
38+
minimalDillPath,
39+
message,
40+
]);
41+
expectOutput(expectedResponse, result);
42+
}
43+
44+
final String unsignedDartExecutable;
45+
final entitlementsInfo = (await runBinary(
46+
'CHECKING ENTITLEMENTS',
47+
'codesign',
48+
['-d', '--entitlements', '-', '--xml', Platform.executable],
49+
allowNonZeroExitCode: true,
50+
)).processResult;
51+
Expect.isTrue(entitlementsInfo.stderr.startsWith('Executable='));
52+
if (entitlementsInfo.stdout.contains('<?xml') &&
53+
!entitlementsInfo.stdout.contains(
54+
'com.apple.security.cs.allow-unsigned-executable-memory',
55+
)) {
56+
// Non-empty entitlements without
57+
// com.apple.security.cs.allow-unsigned-executable-memory will make
58+
// this test fail so strip them.
59+
unsignedDartExecutable = path.join(temp, 'dart');
60+
File(Platform.executable).copySync(unsignedDartExecutable);
61+
await runBinary('REMOVING SIGNATURE', 'codesign', [
62+
'--remove-signature',
63+
unsignedDartExecutable,
64+
]);
65+
await runBinary('RESIGNING', 'codesign', [
66+
'-s',
67+
'-',
68+
unsignedDartExecutable,
69+
]);
70+
} else {
71+
unsignedDartExecutable = Platform.executable;
72+
}
73+
74+
{
75+
final result = await runDart('RUN FROM DILL FILE', [
76+
'--force_dual_mapping_of_code_pages',
77+
minimalDillPath,
78+
message,
79+
], dartExecutable: unsignedDartExecutable);
80+
expectOutput(expectedResponse, result);
81+
}
82+
});
83+
}
84+
85+
void main() async {
86+
// Only supported on MacOS.
87+
if (!Platform.isMacOS || isAOTRuntime) {
88+
return;
89+
}
90+
await compileAndRunMinimalDillTest([]);
91+
}

runtime/tests/vm/dart/snapshot_test_helper.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,15 @@ Future<Result> runDart(
5454
String prefix,
5555
List<String> arguments, {
5656
bool printOut = true,
57+
String? dartExecutable,
5758
}) {
5859
final augmentedArguments = <String>[]
5960
..addAll(Platform.executableArguments)
6061
..add('--verbosity=warning')
6162
..addAll(arguments);
6263
return runBinary(
6364
prefix,
64-
Platform.executable,
65+
dartExecutable ?? Platform.executable,
6566
augmentedArguments,
6667
printOut: printOut,
6768
);
@@ -100,6 +101,7 @@ Future<Result> runBinary(
100101
Map<String, String>? environment,
101102
bool runInShell = false,
102103
bool printOut = true,
104+
bool allowNonZeroExitCode = false,
103105
}) async {
104106
print("+ $binary " + arguments.join(" "));
105107
final processResult = await Process.run(
@@ -127,7 +129,7 @@ Command stderr:
127129
${processResult.stderr}''');
128130
}
129131

130-
if (result.processResult.exitCode != 0) {
132+
if (!allowNonZeroExitCode && result.processResult.exitCode != 0) {
131133
reportError(
132134
result,
133135
'[$prefix] Process finished with non-zero exit code ${result.processResult.exitCode}',

runtime/tests/vm/vm.status

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,9 @@ dart/stacktrace_mixin_application_test: SkipByDesign # Relies symbol names in st
336336
[ $compiler == dart2analyzer || $compiler == dart2js ]
337337
dart/data_uri*test: Skip # Data uri's not supported by dart2js or the analyzer.
338338

339+
[ $compiler != dartk || $runtime != vm || $system != macos ]
340+
dart/macos_dual_mapping_smoke: SkipByDesign
341+
339342
[ $mode == debug || $runtime != dart_precompiled || $system == android ]
340343
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).
341344
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).

runtime/vm/code_patcher_ia32.cc

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,8 @@ void CodePatcher::PatchStaticCallAt(uword return_address,
190190
auto zone = thread->zone();
191191
const Instructions& instrs = Instructions::Handle(zone, code.instructions());
192192
thread->isolate_group()->RunWithStoppedMutators([&]() {
193-
WritableInstructionsScope writable(instrs.PayloadStart(), instrs.Size());
193+
WritableInstructionsScope writable(instrs.WritablePayloadStart(),
194+
instrs.Size());
194195
ASSERT(code.ContainsInstructionAt(return_address));
195196
StaticCall call(return_address, code);
196197
call.set_target(new_target);
@@ -233,7 +234,8 @@ void CodePatcher::PatchInstanceCallAtWithMutatorsStopped(
233234
ASSERT(caller_code.ContainsInstructionAt(return_address));
234235
const Instructions& instrs =
235236
Instructions::Handle(zone, caller_code.instructions());
236-
WritableInstructionsScope writable(instrs.PayloadStart(), instrs.Size());
237+
WritableInstructionsScope writable(instrs.WritablePayloadStart(),
238+
instrs.Size());
237239
InstanceCall call(return_address, caller_code);
238240
call.set_data(data);
239241
call.set_target(target);

runtime/vm/compiler/relocation_test.cc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ struct RelocatorTestHelper {
260260
}
261261

262262
if (FLAG_write_protect_code) {
263+
ASSERT(!VirtualMemory::ShouldDualMapExecutablePages());
263264
const uword address = UntaggedObject::ToAddr(instructions.ptr());
264265
const auto size = instructions.ptr()->untag()->HeapSize();
265266
VirtualMemory::Protect(reinterpret_cast<void*>(address), size,

runtime/vm/debugger_ia32.cc

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ void CodeBreakpoint::PatchCode() {
3131
const Instructions& instrs = Instructions::Handle(zone, code.instructions());
3232
Code& stub_target = Code::Handle(zone);
3333
thread->isolate_group()->RunWithStoppedMutators([&]() {
34-
WritableInstructionsScope writable(instrs.PayloadStart(), instrs.Size());
34+
WritableInstructionsScope writable(instrs.WritablePayloadStart(),
35+
instrs.Size());
3536
switch (breakpoint_kind_) {
3637
case UntaggedPcDescriptors::kIcCall: {
3738
stub_target = StubCode::ICCallBreakpoint().ptr();
@@ -61,7 +62,8 @@ void CodeBreakpoint::RestoreCode() {
6162
const Code& code = Code::Handle(zone, code_);
6263
const Instructions& instrs = Instructions::Handle(zone, code.instructions());
6364
thread->isolate_group()->RunWithStoppedMutators([&]() {
64-
WritableInstructionsScope writable(instrs.PayloadStart(), instrs.Size());
65+
WritableInstructionsScope writable(instrs.WritablePayloadStart(),
66+
instrs.Size());
6567
switch (breakpoint_kind_) {
6668
case UntaggedPcDescriptors::kIcCall:
6769
case UntaggedPcDescriptors::kUnoptStaticCall:

runtime/vm/ffi_callback_metadata.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ VirtualMemory* FfiCallbackMetadata::AllocateTrampolinePage() {
118118
// is_executable=true so that pages get allocated with MAP_JIT flag if
119119
// necessary. Otherwise OS will kill us with a codesigning violation if
120120
// hardened runtime is enabled.
121-
const bool is_executable = true;
121+
const bool is_executable = !VirtualMemory::ShouldDualMapExecutablePages();
122122
#else
123123
const bool is_executable = false;
124124
#endif

0 commit comments

Comments
 (0)