Skip to content

Commit ebf452d

Browse files
wip: sanitizers
1 parent c15153b commit ebf452d

File tree

7 files changed

+377
-14
lines changed

7 files changed

+377
-14
lines changed

pkgs/test/test/io.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ final Future<String> packageDir = Isolate.resolvePackageUri(
2121
return dir;
2222
});
2323

24+
/// The root directory of the Dart SDK.
25+
final String sdkDir = p.dirname(p.dirname(Platform.resolvedExecutable));
26+
2427
/// The platform-specific message emitted when a nonexistent file is loaded.
2528
final String noSuchFileMessage =
2629
Platform.isWindows

pkgs/test/test/runner/compiler_runtime_matrix_test.dart

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ void main() {
2323
for (var compiler in runtime.supportedCompilers) {
2424
// Ignore the platforms we can't run on this OS.
2525
if ((runtime == Runtime.edge && !Platform.isWindows) ||
26-
(runtime == Runtime.safari && !Platform.isMacOS)) {
26+
(runtime == Runtime.safari && !Platform.isMacOS) ||
27+
(runtime == Runtime.vmAsan && !Platform.isLinux) ||
28+
(runtime == Runtime.vmMsan && !Platform.isLinux) ||
29+
(runtime == Runtime.vmTsan && !Platform.isLinux)) {
2730
continue;
2831
}
2932
String? skipReason;
@@ -36,6 +39,15 @@ void main() {
3639
skipReason = 'https://github.com/dart-lang/test/issues/1942';
3740
} else if (runtime == Runtime.firefox && Platform.isMacOS) {
3841
skipReason = 'https://github.com/dart-lang/test/pull/2276';
42+
} else if (runtime == Runtime.vmAsan &&
43+
!File('$sdkDir/bin/dartaotruntime_asan').existsSync()) {
44+
skipReason = 'SDK too old';
45+
} else if (runtime == Runtime.vmMsan &&
46+
!File('$sdkDir/bin/dartaotruntime_msan').existsSync()) {
47+
skipReason = 'SDK too old';
48+
} else if (runtime == Runtime.vmTsan &&
49+
!File('$sdkDir/bin/dartaotruntime_tsan').existsSync()) {
50+
skipReason = 'SDK too old';
3951
}
4052
group(
4153
'--runtime ${runtime.identifier} --compiler ${compiler.identifier}',
@@ -53,6 +65,7 @@ void main() {
5365
await d.file('test.dart', _goodTest).create();
5466
var test = await runTest(testArgs);
5567

68+
emitsThrough(test.stderr);
5669
expect(
5770
test.stdout,
5871
emitsThrough(contains('+1: All tests passed!')),

pkgs/test/test/runner/runner_test.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,15 @@ Output:
129129
''';
130130

131131
final _runtimes =
132-
'[vm (default), chrome, firefox'
132+
'[vm (default), vm-asan, vm-msan, vm-tsan, chrome, firefox'
133133
'${Platform.isMacOS ? ', safari' : ''}'
134134
', edge, node]';
135135

136136
final _runtimeCompilers = [
137137
'[vm]: kernel (default), source, exe',
138+
'[vm-asan]: exe (default)',
139+
'[vm-msan]: exe (default)',
140+
'[vm-tsan]: exe (default)',
138141
'[chrome]: dart2js (default), dart2wasm',
139142
'[firefox]: dart2js (default), dart2wasm',
140143
if (Platform.isMacOS) '[safari]: dart2js (default)',
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
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+
@TestOn('vm && linux')
6+
library;
7+
8+
import 'dart:io';
9+
import 'package:test/test.dart';
10+
import 'package:test_descriptor/test_descriptor.dart' as d;
11+
12+
import '../io.dart';
13+
14+
void main() {
15+
setUpAll(precompileTestExecutable);
16+
17+
String? skipReason;
18+
if (!File('$sdkDir/bin/dartaotruntime_asan').existsSync()) {
19+
skipReason = 'SDK too old';
20+
}
21+
22+
test('asan success', () async {
23+
final testSource = '''
24+
@TestOn('vm-asan')
25+
library asan_environment_test;
26+
27+
import 'package:test/test.dart';
28+
29+
void main() {
30+
test('const', () {
31+
// I.e., correct during kernel compilation.
32+
expect(const bool.fromEnvironment("dart.vm.asan"), equals(true));
33+
34+
expect(const bool.fromEnvironment("dart.vm.msan"), equals(false));
35+
expect(const bool.fromEnvironment("dart.vm.tsan"), equals(false));
36+
});
37+
38+
test('new', () {
39+
// I.e., correct during VM lookup.
40+
expect(new bool.fromEnvironment("dart.vm.asan"), equals(true));
41+
42+
expect(new bool.fromEnvironment("dart.vm.msan"), equals(false));
43+
expect(new bool.fromEnvironment("dart.vm.tsan"), equals(false));
44+
});
45+
}
46+
''';
47+
48+
await d.file('test.dart', testSource).create();
49+
var test = await runTest(['test.dart', '-p', 'vm-asan']);
50+
51+
expect(test.stdout, emitsThrough(contains('+2: All tests passed!')));
52+
await test.shouldExit(0);
53+
}, skip: skipReason);
54+
55+
test('asan failure', () async {
56+
final testSource = '''
57+
@TestOn('vm-asan')
58+
library asan_test;
59+
60+
import 'package:test/test.dart';
61+
import 'dart:ffi';
62+
63+
@Native<Pointer Function(IntPtr)>(symbol: 'malloc')
64+
external Pointer malloc(int size);
65+
@Native<Void Function(Pointer)>(symbol: 'free')
66+
external void free(Pointer ptr);
67+
@Native<Void Function(Pointer, Int, Size)>(symbol: 'memset')
68+
external void memset(Pointer ptr, int char, int size);
69+
70+
void main() {
71+
test('use-after-free', () {
72+
var p = malloc(sizeOf<Long>()).cast<Long>();
73+
free(p);
74+
memset(p, 42, sizeOf<Long>()); // ASAN: heap-use-after-free
75+
});
76+
}
77+
''';
78+
79+
await d.file('test.dart', testSource).create();
80+
var test = await runTest(['test.dart', '-p', 'vm-asan']);
81+
82+
expect(
83+
test.stderr,
84+
emitsThrough(
85+
containsInOrder(['AddressSanitizer: heap-use-after-free', 'memset']),
86+
),
87+
);
88+
await test.shouldExit(6);
89+
}, skip: skipReason);
90+
91+
test('msan success', () async {
92+
final testSource = '''
93+
@TestOn('vm-msan')
94+
library msan_environment_test;
95+
96+
import 'package:test/test.dart';
97+
98+
void main() {
99+
test('const', () {
100+
// I.e., correct during kernel compilation.
101+
expect(const bool.fromEnvironment("dart.vm.msan"), equals(true));
102+
103+
expect(const bool.fromEnvironment("dart.vm.asan"), equals(false));
104+
expect(const bool.fromEnvironment("dart.vm.tsan"), equals(false));
105+
});
106+
107+
test('new', () {
108+
// I.e., correct during VM lookup.
109+
expect(new bool.fromEnvironment("dart.vm.msan"), equals(true));
110+
111+
expect(new bool.fromEnvironment("dart.vm.asan"), equals(false));
112+
expect(new bool.fromEnvironment("dart.vm.tsan"), equals(false));
113+
});
114+
}
115+
''';
116+
117+
await d.file('test.dart', testSource).create();
118+
var test = await runTest(['test.dart', '-p', 'vm-msan']);
119+
120+
expect(test.stdout, emitsThrough(contains('+2: All tests passed!')));
121+
await test.shouldExit(0);
122+
}, skip: skipReason);
123+
124+
test('msan failure', () async {
125+
final testSource = '''
126+
@TestOn('vm-msan')
127+
library msan_test;
128+
129+
import 'dart:ffi';
130+
import 'package:test/test.dart';
131+
132+
@Native<Pointer Function(IntPtr)>(symbol: 'malloc')
133+
external Pointer malloc(int size);
134+
@Native<Void Function(Pointer)>(symbol: 'free')
135+
external void free(Pointer ptr);
136+
@Native<Void Function(Pointer, Pointer, Size)>(symbol: 'memcmp')
137+
external void memcmp(Pointer a, Pointer b, int size);
138+
139+
void main() {
140+
test('uninitialized', () {
141+
var a = malloc(8);
142+
var b = malloc(8);
143+
memcmp(a, b, 8); // MSAN: use-of-uninitialized-value
144+
free(b);
145+
free(a);
146+
});
147+
}
148+
''';
149+
150+
await d.file('test.dart', testSource).create();
151+
var test = await runTest(['test.dart', '-p', 'vm-msan']);
152+
153+
expect(
154+
test.stderr,
155+
emitsThrough(
156+
containsInOrder([
157+
'MemorySanitizer: use-of-uninitialized-value',
158+
'memcmp',
159+
]),
160+
),
161+
);
162+
await test.shouldExit(6);
163+
}, skip: skipReason);
164+
165+
test('tsan success', () async {
166+
final testSource = '''
167+
@TestOn('vm-tsan')
168+
library tsan_environment_test;
169+
170+
import 'package:test/test.dart';
171+
172+
void main() {
173+
test('const', () {
174+
// I.e., correct during kernel compilation.
175+
expect(const bool.fromEnvironment("dart.vm.tsan"), equals(true));
176+
177+
expect(const bool.fromEnvironment("dart.vm.asan"), equals(false));
178+
expect(const bool.fromEnvironment("dart.vm.msan"), equals(false));
179+
});
180+
181+
test('new', () {
182+
// I.e., correct during VM lookup.
183+
expect(new bool.fromEnvironment("dart.vm.tsan"), equals(true));
184+
185+
expect(new bool.fromEnvironment("dart.vm.asan"), equals(false));
186+
expect(new bool.fromEnvironment("dart.vm.msan"), equals(false));
187+
});
188+
}
189+
''';
190+
191+
await d.file('test.dart', testSource).create();
192+
var test = await runTest(['test.dart', '-p', 'vm-tsan']);
193+
194+
expect(test.stdout, emitsThrough(contains('+2: All tests passed!')));
195+
await test.shouldExit(0);
196+
}, skip: skipReason);
197+
198+
test('tsan failure', () async {
199+
final testSource = '''
200+
@TestOn('vm-tsan')
201+
library tsan_test;
202+
203+
import 'dart:ffi';
204+
import 'dart:isolate';
205+
import 'package:test/test.dart';
206+
207+
@Native<Pointer Function(IntPtr)>(symbol: 'malloc')
208+
external Pointer malloc(int size);
209+
@Native<Void Function(Pointer)>(symbol: 'free')
210+
external void free(Pointer ptr);
211+
@Native<Void Function(Pointer, Int, Size)>(symbol: 'memset', isLeaf: true)
212+
external void memset_leaf(Pointer ptr, int char, int size);
213+
@Native<Void Function(IntPtr)>(symbol: 'usleep', isLeaf: true)
214+
external void usleep_leaf(int useconds);
215+
216+
child(addr) {
217+
var p = Pointer<IntPtr>.fromAddress(addr);
218+
for (var i = 0; i < 50000; i++) {
219+
memset_leaf(p, 42, sizeOf<IntPtr>()); // TSAN: data race
220+
usleep_leaf(100);
221+
}
222+
}
223+
224+
void main() {
225+
test('data race', () async {
226+
var p = malloc(sizeOf<IntPtr>()).cast<IntPtr>();
227+
var f = Isolate.run(() => child(p.address));
228+
229+
for (var i = 0; i < 50000; i++) {
230+
p[0] = p[0] + 1; // TSAN: data race
231+
usleep_leaf(100);
232+
}
233+
234+
await f;
235+
free(p);
236+
});
237+
}
238+
''';
239+
240+
await d.file('test.dart', testSource).create();
241+
var test = await runTest(['test.dart', '-p', 'vm-tsan']);
242+
243+
expect(test.stderr, emitsThrough(contains('ThreadSanitizer: data race')));
244+
await test.shouldExit(6);
245+
}, skip: skipReason);
246+
}

pkgs/test_api/lib/src/backend/runtime.dart

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,27 @@ final class Runtime {
1515
Compiler.source,
1616
Compiler.exe,
1717
], isDartVM: true);
18+
static const Runtime vmAsan = Runtime(
19+
'VM with Address Sanitizer',
20+
'vm-asan',
21+
Compiler.exe,
22+
[Compiler.exe],
23+
isDartVM: true,
24+
);
25+
static const Runtime vmMsan = Runtime(
26+
'VM with Memory Sanitizer',
27+
'vm-msan',
28+
Compiler.exe,
29+
[Compiler.exe],
30+
isDartVM: true,
31+
);
32+
static const Runtime vmTsan = Runtime(
33+
'VM with Thread Sanitizer',
34+
'vm-tsan',
35+
Compiler.exe,
36+
[Compiler.exe],
37+
isDartVM: true,
38+
);
1839

1940
/// Google Chrome.
2041
static const Runtime chrome = Runtime(
@@ -69,6 +90,9 @@ final class Runtime {
6990
/// The platforms that are supported by the test runner by default.
7091
static const List<Runtime> builtIn = [
7192
Runtime.vm,
93+
Runtime.vmAsan,
94+
Runtime.vmMsan,
95+
Runtime.vmTsan,
7296
Runtime.chrome,
7397
Runtime.firefox,
7498
Runtime.safari,

pkgs/test_core/lib/src/runner/loader.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,12 @@ class Loader {
6565
/// Creates a new loader that loads tests on platforms defined in
6666
/// [Configuration.current].
6767
Loader() {
68-
_registerPlatformPlugin([Runtime.vm], VMPlatform.new);
68+
_registerPlatformPlugin([
69+
Runtime.vm,
70+
if (File('$sdkDir/bin/dartaotruntime_asan').existsSync()) Runtime.vmAsan,
71+
if (File('$sdkDir/bin/dartaotruntime_msan').existsSync()) Runtime.vmMsan,
72+
if (File('$sdkDir/bin/dartaotruntime_tsan').existsSync()) Runtime.vmTsan,
73+
], VMPlatform.new);
6974

7075
platformCallbacks.forEach((runtime, plugin) {
7176
_registerPlatformPlugin([runtime], plugin);

0 commit comments

Comments
 (0)