blob: 384ddf442fdb3dcd1c1c77c2ba92ab1a9cd6b52c [file] [log] [blame]
// Copyright (c) 2023, 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.
// This test ensures that when running in obfuscated mode, the AOT compiler
// generates a snapshot with obfuscated runtime information, but that static
// symbol tables in the unstripped snapshot and/or separate debugging
// information remains unobfuscated.
// OtherResources=use_save_debugging_info_flag_program.dart
import "dart:io";
import 'package:expect/expect.dart';
import 'package:native_stack_traces/elf.dart';
import 'package:native_stack_traces/src/dwarf_container.dart';
import 'package:native_stack_traces/src/macho.dart';
import 'package:path/path.dart' as path;
import 'use_flag_test_helper.dart';
Future<void> main(List<String> args) async {
if (!isAOTRuntime) {
return; // Running in JIT: AOT binaries not available.
}
if (Platform.isAndroid) {
return; // SDK tree and dart_bootstrap not available on the test device.
}
// These are the tools we need to be available to run on a given platform:
if (!await testExecutable(genSnapshot)) {
throw "Cannot run test as $genSnapshot not available";
}
if (!await testExecutable(dartPrecompiledRuntime)) {
throw "Cannot run test as $dartPrecompiledRuntime not available";
}
if (!File(platformDill).existsSync()) {
throw "Cannot run test as $platformDill does not exist";
}
await withTempDir('unobfuscated-static-symbols-test', (String tempDir) async {
final cwDir = path.dirname(Platform.script.toFilePath());
final script = path.join(
cwDir,
'use_save_debugging_info_flag_program.dart',
);
final scriptDill = path.join(tempDir, 'flag_program.dill');
// Compile script to Kernel IR.
await run(genKernel, <String>[
'--aot',
'--platform=$platformDill',
'-o',
scriptDill,
script,
]);
await checkElf(tempDir, scriptDill);
await checkAssembly(tempDir, scriptDill);
});
}
const commonGenSnapshotArgs = <String>[
// Make sure that the runs are deterministic so we can depend on the same
// snapshot being generated each time.
'--deterministic',
];
Future<void> checkElf(String tempDir, String scriptDill) async {
// Run the AOT compiler without Dwarf stack trace, once without obfuscation,
// once with obfuscation, and once with obfuscation and saving debugging
// information.
final scriptUnobfuscatedSnapshot = path.join(tempDir, 'unobfuscated-elf.so');
await run(genSnapshot, <String>[
...commonGenSnapshotArgs,
'--snapshot-kind=app-aot-elf',
'--elf=$scriptUnobfuscatedSnapshot',
scriptDill,
]);
final unobfuscatedCase = TestCase(
scriptUnobfuscatedSnapshot,
Elf.fromFile(scriptUnobfuscatedSnapshot)!,
);
final scriptObfuscatedOnlySnapshot = path.join(
tempDir,
'obfuscated-only-elf.so',
);
await run(genSnapshot, <String>[
...commonGenSnapshotArgs,
'--obfuscate',
'--snapshot-kind=app-aot-elf',
'--elf=$scriptObfuscatedOnlySnapshot',
scriptDill,
]);
final obfuscatedOnlyCase = TestCase(
scriptObfuscatedOnlySnapshot,
Elf.fromFile(scriptObfuscatedOnlySnapshot)!,
);
final scriptObfuscatedSnapshot = path.join(tempDir, 'obfuscated-elf.so');
final scriptDebuggingInfo = path.join(tempDir, 'obfuscated-debug-elf.so');
await run(genSnapshot, <String>[
...commonGenSnapshotArgs,
'--obfuscate',
'--snapshot-kind=app-aot-elf',
'--elf=$scriptObfuscatedSnapshot',
'--save-debugging-info=$scriptDebuggingInfo',
scriptDill,
]);
final obfuscatedCase = TestCase(
scriptObfuscatedSnapshot,
Elf.fromFile(scriptObfuscatedSnapshot)!,
Elf.fromFile(scriptDebuggingInfo)!,
);
final scriptStrippedSnapshot = path.join(
tempDir,
'obfuscated-stripped-elf.so',
);
final scriptSeparateDebuggingInfo = path.join(
tempDir,
'obfuscated-separate-debug-elf.so',
);
await run(genSnapshot, <String>[
...commonGenSnapshotArgs,
'--strip',
'--obfuscate',
'--snapshot-kind=app-aot-elf',
'--elf=$scriptStrippedSnapshot',
'--save-debugging-info=$scriptSeparateDebuggingInfo',
scriptDill,
]);
final strippedCase = TestCase(
scriptStrippedSnapshot,
/*container=*/ null, // No static symbols in stripped snapshot.
Elf.fromFile(scriptSeparateDebuggingInfo)!,
);
await checkCases(unobfuscatedCase, <TestCase>[
obfuscatedOnlyCase,
obfuscatedCase,
strippedCase,
]);
}
Future<void> checkAssembly(String tempDir, String scriptDill) async {
// Currently there are no appropriate buildtools on the simulator trybots as
// normally they compile to ELF and don't need them for compiling assembly
// snapshots.
if (isSimulator || (!Platform.isLinux && !Platform.isMacOS)) return;
// Run the AOT compiler without Dwarf stack trace, once without obfuscation,
// once with obfuscation, and once with obfuscation and saving debugging
// information.
final scriptUnobfuscatedAssembly = path.join(
tempDir,
'unobfuscated-assembly.S',
);
final scriptUnobfuscatedSnapshot = path.join(
tempDir,
'unobfuscated-assembly.so',
);
await run(genSnapshot, <String>[
...commonGenSnapshotArgs,
'--snapshot-kind=app-aot-assembly',
'--assembly=$scriptUnobfuscatedAssembly',
scriptDill,
]);
await assembleSnapshot(
scriptUnobfuscatedAssembly,
scriptUnobfuscatedSnapshot,
);
final unobfuscatedCase = TestCase(
scriptUnobfuscatedSnapshot,
Platform.isMacOS
? MachO.fromFile(scriptUnobfuscatedSnapshot)!
: Elf.fromFile(scriptUnobfuscatedSnapshot)!,
);
final scriptObfuscatedOnlyAssembly = path.join(
tempDir,
'obfuscated-only-assembly.S',
);
final scriptObfuscatedOnlySnapshot = path.join(
tempDir,
'obfuscated-only-assembly.so',
);
await run(genSnapshot, <String>[
...commonGenSnapshotArgs,
'--obfuscate',
'--snapshot-kind=app-aot-assembly',
'--assembly=$scriptObfuscatedOnlyAssembly',
scriptDill,
]);
await assembleSnapshot(
scriptObfuscatedOnlyAssembly,
scriptObfuscatedOnlySnapshot,
);
final obfuscatedOnlyCase = TestCase(
scriptObfuscatedOnlySnapshot,
Platform.isMacOS
? MachO.fromFile(scriptObfuscatedOnlySnapshot)!
: Elf.fromFile(scriptObfuscatedOnlySnapshot)!,
);
await checkCases(unobfuscatedCase, <TestCase>[obfuscatedOnlyCase]);
}
class TestCase {
final String snapshotPath;
final DwarfContainer? container;
final DwarfContainer? debuggingInfoContainer;
TestCase(this.snapshotPath, this.container, [this.debuggingInfoContainer]);
}
Future<void> checkCases(
TestCase unobfuscated,
List<TestCase> obfuscateds,
) async {
checkStaticSymbolTables(unobfuscated, obfuscateds);
await checkTraces(unobfuscated, obfuscateds);
}
Future<void> checkTraces(
TestCase unobfuscated,
List<TestCase> obfuscateds,
) async {
// Run the resulting scripts, saving the stack traces.
final expectedTrace = await runError(dartPrecompiledRuntime, <String>[
unobfuscated.snapshotPath,
]);
print('');
print("Original stack trace:");
expectedTrace.forEach(print);
final obfuscatedTraces = <List<String>>[];
for (int i = 0; i < obfuscateds.length; i++) {
obfuscatedTraces.add(
await runError(dartPrecompiledRuntime, <String>[
obfuscateds[i].snapshotPath,
]),
);
print('');
print("Obfuscated stack trace ${i + 1}:");
obfuscatedTraces[i].forEach(print);
if (i != 0) {
// Compare with the previous trace, as all obfuscated traces should be
// the same as the obfuscation is deterministic.
Expect.deepEquals(obfuscatedTraces[i - 1], obfuscatedTraces[i]);
}
}
// The unobfuscated trace should differ from all obfuscated traces.
Expect.isNotEmpty(obfuscateds);
final gotTrace = obfuscatedTraces[0];
Expect.equals(expectedTrace.length, gotTrace.length);
bool differs = false;
for (int i = 0; i < expectedTrace.length; i++) {
if (expectedTrace[i] != gotTrace[i]) {
differs = true;
}
}
Expect.isTrue(
differs,
'The obfuscated traces are identical to the unobfuscated trace',
);
}
void checkStaticSymbolTables(TestCase expected, List<TestCase> cases) {
final expectedSymbolNames = expected.container!.staticSymbols
.map((o) => o.name)
.toSet();
if (expected.debuggingInfoContainer != null) {
expectSimilarStaticSymbols(
expectedSymbolNames,
expected.debuggingInfoContainer!.staticSymbols.map((o) => o.name).toSet(),
);
}
for (final got in cases) {
if (got.container != null) {
expectSimilarStaticSymbols(
expectedSymbolNames,
got.container!.staticSymbols.map((o) => o.name).toSet(),
);
}
if (got.debuggingInfoContainer != null) {
expectSimilarStaticSymbols(
expectedSymbolNames,
got.debuggingInfoContainer!.staticSymbols.map((o) => o.name).toSet(),
);
}
}
}
const kMaxPercentAllowedDifferences = 0.01;
void expectSimilarStaticSymbols(Set<String> expected, Set<String> got) {
final allowedDifferences = (expected.length * kMaxPercentAllowedDifferences)
.floor();
// There are cases where we cannot assume that we have the exact same symbols
// in both snapshots (e.g., because we're using an assembler that adds
// symbols with randomly generated names). Instead, we compare them manually,
// counting the number of symbols not found in one or the other, and allow
// a small number of differences. (We generate _a lot_ of static symbols, so
// if the vast majority match we can assume that no obfuscation happened.)
final onlyExpected = <String>[];
for (final name in expected) {
if (!got.contains(name)) {
onlyExpected.add(name);
}
}
print('');
print('Symbols found only in expected:');
onlyExpected.forEach(print);
final onlyGot = <String>[];
for (final name in got) {
if (!expected.contains(name)) {
onlyGot.add(name);
}
}
print('');
print('Symbols found only in got:');
onlyGot.forEach(print);
final differences = onlyExpected.length + onlyGot.length;
Expect.isTrue(
differences <= allowedDifferences,
'Got $differences different symbols, which is '
'more than $allowedDifferences.',
);
}