diff --git a/pkg/native_stack_traces/lib/src/macho.dart b/pkg/native_stack_traces/lib/src/macho.dart index adeddfa1487..1e0b38ed388 100644 --- a/pkg/native_stack_traces/lib/src/macho.dart +++ b/pkg/native_stack_traces/lib/src/macho.dart @@ -121,10 +121,15 @@ class LoadCommand { LoadCommand._(this.cmd, this.cmdsize); + static const LC_REQ_DYLD = 0x80000000; + static const LC_SEGMENT = 0x1; static const LC_SYMTAB = 0x2; + static const LC_ID_DYLIB = 0xd; static const LC_SEGMENT_64 = 0x19; static const LC_UUID = 0x1b; + static const LC_RPATH = 0x1c | LC_REQ_DYLD; + static const LC_CODE_SIGNATURE = 0x1d; // Only used in vm/dart tests. static const LC_BUILD_VERSION = 0x32; static LoadCommand fromReader(Reader reader) { @@ -147,6 +152,10 @@ class LoadCommand { case LC_BUILD_VERSION: command = BuildVersionCommand.fromReader(reader, cmd, cmdsize); break; + case LC_ID_DYLIB: + command = DylibCommand.fromReader(reader, cmd, cmdsize); + case LC_RPATH: + command = RunPathCommand.fromReader(reader, cmd, cmdsize); default: break; } @@ -355,6 +364,86 @@ class UuidCommand extends LoadCommand { } } +class DylibInfo { + final String name; + final int timestamp; + final int currentVersion; + final int compatibilityVersion; + + const DylibInfo._(this.name, this.timestamp, this.currentVersion, + this.compatibilityVersion); + + static DylibInfo fromReader(Reader reader, int cmdsize) { + final start = reader.offset - 8; // cmd + cmdsize + final offset = _readMachOUint32(reader); + final timestamp = _readMachOUint32(reader); + final currentVersion = _readMachOUint32(reader); + final compatibilityVersion = _readMachOUint32(reader); + reader.seek(start + offset, absolute: true); + final name = reader.readNullTerminatedString(maxSize: cmdsize - offset); + return DylibInfo._(name, timestamp, currentVersion, compatibilityVersion); + } + + void writeToStringBuffer(StringBuffer buffer) { + buffer + ..write(' Name: ') + ..writeln(name) + ..write(' Timestamp: ') + ..writeln(timestamp) + ..write(' Current version: ') + ..writeln(currentVersion) + ..write(' Compatibility version: ') + ..writeln(compatibilityVersion); + } + + @override + String toString() { + final buffer = StringBuffer(); + writeToStringBuffer(buffer); + return buffer.toString(); + } +} + +class DylibCommand extends LoadCommand { + final DylibInfo info; + + DylibCommand._(super.cmd, super.cmdsize, this.info) : super._(); + + static DylibCommand fromReader(Reader reader, int cmd, int cmdsize) => + DylibCommand._(cmd, cmdsize, DylibInfo.fromReader(reader, cmdsize)); + + @override + void writeToStringBuffer(StringBuffer buffer) { + if (cmd == LoadCommand.LC_ID_DYLIB) { + buffer.writeln('LC_ID_DYLIB:'); + info.writeToStringBuffer(buffer); + } else { + throw StateError("Unexpected command code $cmd"); + } + } +} + +class RunPathCommand extends LoadCommand { + String path; + + RunPathCommand._(super.cmd, super.cmdsize, this.path) : super._(); + + static RunPathCommand fromReader(Reader reader, int cmd, int cmdsize) { + final start = reader.offset - 8; // cmd + cmdsize + final offset = _readMachOUint32(reader); + reader.seek(start + offset, absolute: true); + final path = reader.readNullTerminatedString(maxSize: cmdsize - offset); + return RunPathCommand._(cmd, cmdsize, path); + } + + @override + void writeToStringBuffer(StringBuffer buffer) { + buffer + ..write('Run path: ') + ..write(path); + } +} + class Version { final int x; final int y; @@ -688,6 +777,7 @@ class MachO extends DwarfContainer { Reader.fromTypedData(reader.bdata, wordSize: _header.wordSize, endian: _header.endian); + Iterable get commands => _commands; Iterable commandsWhereType() => _commands.whereType(); diff --git a/runtime/platform/mach_o.h b/runtime/platform/mach_o.h index b5e658a9bcb..f104279b19a 100644 --- a/runtime/platform/mach_o.h +++ b/runtime/platform/mach_o.h @@ -115,6 +115,10 @@ struct load_command { // The description of the LC_* constants are followed by the name of // the specific C structure describing their contents in parentheses. +// Flag stored in high bit for LC_* constants that denotes sections +// the dynamic linker must understand to properly load the library. +static constexpr uint32_t LC_REQ_DYLD = 0x80000000; + // A portion of the file that is mapped into memory when the // object file is loaded. (segment_command) static constexpr uint32_t LC_SEGMENT = 0x1; @@ -132,6 +136,7 @@ static constexpr uint32_t LC_ID_DYLIB = 0xd; static constexpr uint32_t LC_SEGMENT_64 = 0x19; // The UUID, used as a build identifier. (uuid_command) static constexpr uint32_t LC_UUID = 0x1b; +static constexpr uint32_t LC_RPATH = (0x1c | LC_REQ_DYLD); // The code signature which protects the preceding portion of the object file. // Must be the last contents in the object file. (linkedit_data_command) static constexpr uint32_t LC_CODE_SIGNATURE = 0x1d; @@ -452,6 +457,12 @@ struct linkedit_data_command { uint32_t datasize; }; +struct rpath_command { + uint32_t cmd; // LC_RPATH + uint32_t cmdsize; + lc_str path; +}; + // Magic numbers for code signature blobs. static constexpr uint32_t CSMAGIC_CODEDIRECTORY = 0xfade0c02; diff --git a/runtime/tests/vm/dart/unobfuscated_static_symbols_test.dart b/runtime/tests/vm/dart/unobfuscated_static_symbols_test.dart index efaab5b8de1..3ca3b510294 100644 --- a/runtime/tests/vm/dart/unobfuscated_static_symbols_test.dart +++ b/runtime/tests/vm/dart/unobfuscated_static_symbols_test.dart @@ -13,7 +13,6 @@ import "dart:io"; import 'package:expect/expect.dart'; import 'package:native_stack_traces/src/dwarf_container.dart'; -import 'package:native_stack_traces/src/macho.dart' as macho; import 'package:path/path.dart' as path; import 'use_flag_test_helper.dart'; @@ -61,43 +60,11 @@ Future main(List args) async { }); } -Future?> retrieveDebugMap( - SnapshotType snapshotType, - String snapshotPath, -) async { - // Don't check the debug map of assembled Mach-O snapshots. - if (snapshotType != SnapshotType.machoDylib) return null; - final dsymutil = llvmTool('dsymutil'); - if (dsymutil == null) { - // Only return a null debug map if this part of the test should be - // skipped on the current configuration. - if (Platform.isWindows || Platform.isFuchsia) { - // The identifier isn't provided on these platforms due to the lack - // of a basename implementation, so no debug map can be extracted. - return null; - } - if (isSimulator) { - // clangBuildToolsDir uses Abi.current(), so it returns the buildtools - // dir for the architecture being simulated, not the host. - return null; - } - throw StateError('Expected dsymutil'); - } - return await runOutput(dsymutil, ['--dump-debug-map', snapshotPath]); -} - -final hasMinOSVersionOption = Platform.isMacOS || Platform.isIOS; -final expectedVersion = hasMinOSVersionOption ? macho.Version(1, 2, 3) : null; - Future checkSnapshotType( String tempDir, String scriptDill, SnapshotType snapshotType, ) async { - final commonOptions = []; - if (hasMinOSVersionOption && snapshotType == SnapshotType.machoDylib) { - commonOptions.add('--macho-min-os-version=$expectedVersion'); - } // Run the AOT compiler without Dwarf stack trace, once without obfuscation, // once with obfuscation, and once with obfuscation and saving debugging // information. @@ -109,13 +76,12 @@ Future checkSnapshotType( scriptDill, snapshotType, scriptUnobfuscatedSnapshot, - commonOptions, + const [], ); final unobfuscatedCase = TestCase( snapshotType, scriptUnobfuscatedSnapshot, snapshotType.fromFile(scriptUnobfuscatedSnapshot)!, - debugMap: await retrieveDebugMap(snapshotType, scriptUnobfuscatedSnapshot), ); final scriptObfuscatedOnlySnapshot = path.join( @@ -123,17 +89,12 @@ Future checkSnapshotType( 'obfuscated-only-$snapshotType.so', ); await createSnapshot(scriptDill, snapshotType, scriptObfuscatedOnlySnapshot, [ - ...commonOptions, '--obfuscate', ]); final obfuscatedOnlyCase = TestCase( snapshotType, scriptObfuscatedOnlySnapshot, snapshotType.fromFile(scriptObfuscatedOnlySnapshot)!, - debugMap: await retrieveDebugMap( - snapshotType, - scriptObfuscatedOnlySnapshot, - ), ); // Don't compare to separate debugging information for assembled snapshots @@ -151,7 +112,6 @@ Future checkSnapshotType( 'obfuscated-debug-$snapshotType.so', ); await createSnapshot(scriptDill, snapshotType, scriptObfuscatedSnapshot, [ - ...commonOptions, '--obfuscate', '--save-debugging-info=$scriptDebuggingInfo', ]); @@ -160,7 +120,6 @@ Future checkSnapshotType( scriptObfuscatedSnapshot, snapshotType.fromFile(scriptObfuscatedSnapshot)!, debuggingInfoContainer: snapshotType.fromFile(scriptDebuggingInfo)!, - debugMap: await retrieveDebugMap(snapshotType, scriptObfuscatedSnapshot), ); final scriptStrippedSnapshot = path.join( @@ -172,9 +131,8 @@ Future checkSnapshotType( 'obfuscated-separate-debug-$snapshotType.so', ); await createSnapshot(scriptDill, snapshotType, scriptStrippedSnapshot, [ - ...commonOptions, - '--strip', '--obfuscate', + '--strip', '--save-debugging-info=$scriptSeparateDebuggingInfo', ]); strippedCase = TestCase( @@ -184,8 +142,6 @@ Future checkSnapshotType( debuggingInfoContainer: snapshotType.fromFile( scriptSeparateDebuggingInfo, )!, - // No N_OSO symbol in stripped Mach-O snapshots. - debugMap: null, ); } @@ -216,14 +172,12 @@ class TestCase { final String snapshotPath; final DwarfContainer? container; final DwarfContainer? debuggingInfoContainer; - final List? debugMap; TestCase( this.type, this.snapshotPath, this.container, { this.debuggingInfoContainer, - this.debugMap, }); } @@ -238,13 +192,6 @@ Future checkCases( ]; checkStaticSymbolTables(unobfuscated, obfuscateds); await checkTraces(unobfuscated, obfuscateds); - if (unobfuscated.debugMap != null) { - checkDebugMaps( - unobfuscated.debugMap!, - unstrippedObfuscateds.map((c) => c.debugMap!).toList(), - ); - } - checkMachOSnapshots(unobfuscated, obfuscateds); } Future checkTraces( @@ -361,105 +308,3 @@ void expectSimilarStaticSymbols(Set expected, Set got) { 'more than $allowedDifferences.', ); } - -final _tripleLineRegExp = RegExp(r'triple:\s+(.*)'); -final _timestampLineRegExp = RegExp(r'timestamp:\s+(.*)'); -// We only check that the number of symbols were the same. -final _symbolLineRegExp = RegExp(r'{ sym: '); - -void checkDebugMaps(List expected, List> cases) { - // The dump should look like the following YAML: - // --- - // triple: '--' - // binary-path: - // objects: - // - filename: - // - timestamp: 0 - // - symbols: - // - { sym: , ... } - // ... - // ... - // - // The initial --- and ending ... are literal, as those are used to - // separate multiple YAML documents in a single stream. - // - // For all test cases: - // - The triple should be the same. - // - The binary-path and filename lines should exist, though the filenames - // may be different. - // - The timestamp should be 0. - // - The number of symbols should be the same. - Expect.isTrue(expected.length > 7); - for (final c in cases) { - Expect.equals(expected.length, c.length); - } - for (int i = 0; i < expected.length; i++) { - final expectedLine = expected[i]; - final isSymbol = _symbolLineRegExp.hasMatch(expectedLine); - final expectedTriple = _tripleLineRegExp.firstMatch(expectedLine)?.group(1); - - final expectedTimestampMatch = _timestampLineRegExp.firstMatch( - expectedLine, - ); - if (expectedTimestampMatch != null) { - final expectedTimestamp = int.tryParse(expectedTimestampMatch.group(1)!); - // The timestamp (value of the N_OSO symbol) in our snapshots is always 0. - Expect.equals(0, expectedTimestamp); - } - - // Lines that are allowed to have varying field values. - final prefixOnlyLinePrefixes = ['binary-path: ', ' - filename: ']; - var expectedPrefixEnd = -1; - if (prefixOnlyLinePrefixes.any((s) => expectedLine.startsWith(s))) { - expectedPrefixEnd = expectedLine.indexOf(':'); - } - - for (final c in cases) { - final gotLine = c[i]; - if (expectedTriple != null) { - final gotTriple = _tripleLineRegExp.firstMatch(gotLine)?.group(1); - Expect.equals(expectedTriple, gotTriple); - } else if (isSymbol) { - Expect.isTrue(_symbolLineRegExp.hasMatch(gotLine)); - } else if (expectedPrefixEnd > 0) { - // If there's a unhandled field name, check that those match and don't - // check the rest of the line (as, say, the filename will differ). - Expect.stringEquals( - expectedLine.substring(0, expectedLine.indexOf(':')), - gotLine.substring(0, expectedLine.indexOf(':')), - ); - } else { - // Check line equality for anything not already covered. - Expect.stringEquals(expectedLine, gotLine); - } - } - } -} - -// Checks for MachO snapshots (not separate debugging information). -void checkMachOSnapshots(TestCase unobfuscated, List obfuscateds) { - checkMachOSnapshot(unobfuscated); - obfuscateds.forEach(checkMachOSnapshot); -} - -void checkMachOSnapshot(TestCase testCase) { - // The checks below are only for snapshots, not for debugging information. - final snapshot = testCase.container; - if (snapshot is! macho.MachO) return; - final buildVersion = snapshot - .commandsWhereType() - .singleOrNull; - final expectedPlatform = Platform.isMacOS - ? macho.Platform.PLATFORM_MACOS - : Platform.isIOS - ? macho.Platform.PLATFORM_IOS - : null; - Expect.equals(expectedPlatform, buildVersion?.platform); - if (testCase.type == SnapshotType.machoDylib) { - Expect.equals(expectedVersion, buildVersion?.minOS); - Expect.equals(expectedVersion, buildVersion?.sdk); - if (buildVersion != null) { - Expect.isEmpty(buildVersion.toolVersions); - } - } -} diff --git a/runtime/tests/vm/dart/use_flag_test_helper.dart b/runtime/tests/vm/dart/use_flag_test_helper.dart index 38c14c48811..88539775e7c 100644 --- a/runtime/tests/vm/dart/use_flag_test_helper.dart +++ b/runtime/tests/vm/dart/use_flag_test_helper.dart @@ -257,8 +257,15 @@ Future> runOutput( String executable, List args, { bool ignoreStdErr = false, + bool printStdout = true, + bool printStderr = true, }) async { - final result = await runHelper(executable, args); + final result = await runHelper( + executable, + args, + printStdout: printStdout, + printStderr: printStderr, + ); if (result.exitCode != 0) { throw 'Command failed with unexpected exit code (was ${result.exitCode})'; @@ -271,8 +278,18 @@ Future> runOutput( return LineSplitter.split(result.stdout).toList(growable: false); } -Future> runError(String executable, List args) async { - final result = await runHelper(executable, args); +Future> runError( + String executable, + List args, { + bool printStdout = true, + bool printStderr = true, +}) async { + final result = await runHelper( + executable, + args, + printStdout: printStdout, + printStderr: printStderr, + ); if (result.exitCode == 0) { throw 'Command did not fail with non-zero exit code'; diff --git a/runtime/tests/vm/dart/use_macho_options_test.dart b/runtime/tests/vm/dart/use_macho_options_test.dart new file mode 100644 index 00000000000..319e8f3d2da --- /dev/null +++ b/runtime/tests/vm/dart/use_macho_options_test.dart @@ -0,0 +1,350 @@ +// 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. + +// This test checks various command line options related to the Mach-O +// snapshot writer. Note that some of these options may make the written +// snapshot unrunnable, as they are meant to be used in a larger workflow +// (e.g., not emitting a code signature because it will be added later by +// XCode). + +// OtherResources=use_save_debugging_info_flag_program.dart + +import "dart:io"; + +import 'package:expect/expect.dart'; +import 'package:native_stack_traces/src/macho.dart' as macho; +import 'package:path/path.dart' as path; + +import 'use_flag_test_helper.dart'; + +Future main(List 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('use-macho-options-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, [ + '--aot', + '--platform=$platformDill', + '-o', + scriptDill, + script, + ]); + + final testCases = [ + for (final t in testsToRun) await compileSnapshot(tempDir, scriptDill, t), + ]; + await checkCases(testCases); + }); +} + +final canRetrieveDebugMap = + // The identifier isn't provided on these platforms due to the lack + // of a basename implementation, so no debug map can be extracted. + !Platform.isWindows && + !Platform.isFuchsia && + // clangBuildToolsDir uses Abi.current(), so it returns the buildtools + // dir for the architecture being simulated, not the host. + !isSimulator; + +Future?> retrieveDebugMap(String snapshotPath) async { + final dsymutil = llvmTool('dsymutil'); + if (dsymutil == null) { + if (!canRetrieveDebugMap) return null; + throw StateError('Expected dsymutil'); + } + // Don't clutter the log with the output from dsymutil as it's large. + return await runOutput(dsymutil, [ + '--dump-debug-map', + snapshotPath, + ], printStdout: false); +} + +Future compileSnapshot( + String tempDir, + String scriptDill, + TestType testType, +) async { + final additionalOptions = [ + if (testType == TestType.AddRunPaths) + '--macho-rpath=${machORunPaths.join(',')}', + if (testType == TestType.MinOSVersion) + '--macho-min-os-version=$expectedVersion', + if (testType == TestType.NoLinkerSignature) '--no-macho-linker-signature', + if (testType == TestType.ReplaceInstallName) + '--macho-install-name=$machoInstallName', + ]; + + final scriptSnapshot = path.join(tempDir, 'output.so'); + await createSnapshot( + scriptDill, + SnapshotType.machoDylib, + scriptSnapshot, + additionalOptions, + ); + return TestCase( + testType, + scriptSnapshot, + macho.MachO.fromFile(scriptSnapshot)!, + debugMap: await retrieveDebugMap(scriptSnapshot), + ); +} + +@pragma('vm:platform-const') +final isApplePlatform = Platform.isMacOS || Platform.isIOS; +@pragma('vm:platform-const') +final expectedVersion = isApplePlatform ? macho.Version(1, 2, 3) : null; +const machoInstallName = '@rpath/App.framework/App'; +const machORunPaths = [ + '@executable_path/Frameworks', + '@loader_path/Frameworks', +]; + +enum TestType { + AddRunPaths, + MinOSVersion, + NoLinkerSignature, + ReplaceInstallName, +} + +@pragma('vm:platform-const') +final testsToRun = [ + if (isApplePlatform) TestType.MinOSVersion, + if (isApplePlatform) TestType.AddRunPaths, + TestType.ReplaceInstallName, + TestType.NoLinkerSignature, +]; + +class TestCase { + final TestType type; + final String snapshotPath; + final macho.MachO snapshot; + final List? debugMap; + + TestCase(this.type, this.snapshotPath, this.snapshot, {this.debugMap}); +} + +Future checkCases(List testCases) async { + // We want to make sure the debug maps are consistent across cases. + checkDebugMaps(testCases); + for (final c in testCases) { + checkInstallName(c); + checkRunPaths(c); + checkBuildVersion(c); + checkCodeSignature(c); + } + // Unsigned snapshots are not runnable. + final runnableCases = testCases + .where((c) => c.type != TestType.NoLinkerSignature) + .toList(); + await checkRunnable(runnableCases); +} + +Future checkRunnable(List testCases) async { + Expect.isNotEmpty(testCases); + final traces = [ + for (final c in testCases) + await runError(dartPrecompiledRuntime, [ + c.snapshotPath, + ], printStderr: false), + ]; + + // Use the first testcase's stack trace as the expected result. + final expectedTrace = traces.first; + print(''); + print("Stack trace 1:"); + expectedTrace.forEach(print); + + if (traces.length == 1) { + // On non-Apple platforms, there's only one runnable case. + print(''); + print('No other runnable test cases to compare.'); + return; + } + + for (int i = 1; i < testCases.length; i++) { + final gotTrace = traces[i]; + print(''); + print("Stack trace ${i + 1}:"); + print(gotTrace); + + Expect.deepEquals(expectedTrace, gotTrace); + } +} + +final _tripleLineRegExp = RegExp(r'triple:\s+(.*)'); +final _timestampLineRegExp = RegExp(r'timestamp:\s+(.*)'); +// We only check that the number of symbols were the same. +final _symbolLineRegExp = RegExp(r'{ sym: '); + +void checkDebugMaps(List testCases) { + // Not a platform where we can test debug maps. + if (!canRetrieveDebugMap) return; + + for (final c in testCases) { + Expect.isNotNull(c.debugMap, 'Debug map for test ${c.type} missing'); + } + + // Like with the runnable testcases, use the first one as the expected + // result for the others. + final expected = testCases.first.debugMap!; + final got = testCases.skip(1).map((t) => t.debugMap!).toList(); + + // The dump should look like the following YAML: + // --- + // triple: '--' + // binary-path: + // objects: + // - filename: + // - timestamp: 0 + // - symbols: + // - { sym: , ... } + // ... + // ... + // + // The initial --- and ending ... are literal, as those are used to + // separate multiple YAML documents in a single stream. + // + // For all test cases: + // - The triple should be the same. + // - The binary-path and filename lines should exist, though the filenames + // may be different. + // - The timestamp should be 0. + // - The number of symbols should be the same. + Expect.isTrue(expected.length > 7); + for (final c in got) { + Expect.equals(expected.length, c.length); + } + for (int i = 0; i < expected.length; i++) { + final expectedLine = expected[i]; + final isSymbol = _symbolLineRegExp.hasMatch(expectedLine); + final expectedTriple = _tripleLineRegExp.firstMatch(expectedLine)?.group(1); + + final expectedTimestampMatch = _timestampLineRegExp.firstMatch( + expectedLine, + ); + if (expectedTimestampMatch != null) { + final expectedTimestamp = int.tryParse(expectedTimestampMatch.group(1)!); + // The timestamp (value of the N_OSO symbol) in our snapshots is always 0. + Expect.equals(0, expectedTimestamp); + } + + // Lines that are allowed to have varying field values. + final prefixOnlyLinePrefixes = ['binary-path: ', ' - filename: ']; + var expectedPrefixEnd = -1; + if (prefixOnlyLinePrefixes.any((s) => expectedLine.startsWith(s))) { + expectedPrefixEnd = expectedLine.indexOf(':'); + } + + for (final c in got) { + final gotLine = c[i]; + if (expectedTriple != null) { + final gotTriple = _tripleLineRegExp.firstMatch(gotLine)?.group(1); + Expect.equals(expectedTriple, gotTriple); + } else if (isSymbol) { + Expect.isTrue(_symbolLineRegExp.hasMatch(gotLine)); + } else if (expectedPrefixEnd > 0) { + // If there's a unhandled field name, check that those match and don't + // check the rest of the line (as, say, the filename will differ). + Expect.stringEquals( + expectedLine.substring(0, expectedLine.indexOf(':')), + gotLine.substring(0, expectedLine.indexOf(':')), + ); + } else { + // Check line equality for anything not already covered. + Expect.stringEquals(expectedLine, gotLine); + } + } + } +} + +void checkInstallName(TestCase testCase) { + final dylibCommands = testCase.snapshot + .commandsWhereType(); + Expect.isNotEmpty(dylibCommands); + final idDylib = dylibCommands + .where((c) => c.cmd == macho.LoadCommand.LC_ID_DYLIB) + .singleOrNull; + Expect.isNotNull(idDylib); + if (idDylib == null) return; + final expectedName = testCase.type == TestType.ReplaceInstallName + ? machoInstallName + // No Utils::Basename implementation in runtime/platform for Windows + // or Fuchsia, so for now an empty string is used instead of the full + // path (which could leak information). + : (Platform.isWindows || Platform.isFuchsia) + ? "" + : path.basename(testCase.snapshotPath); + Expect.equals(expectedName, idDylib.info.name); +} + +void checkRunPaths(TestCase testCase) { + final runPathCommands = testCase.snapshot + .commandsWhereType(); + if (testCase.type != TestType.AddRunPaths) { + Expect.isEmpty(runPathCommands); + } else { + Expect.isNotEmpty(runPathCommands); + for (final rpath in runPathCommands) { + Expect.isTrue( + machORunPaths.contains(rpath.path), + "${rpath.path} not in [${machORunPaths.join(", ")}]", + ); + } + } +} + +void checkCodeSignature(TestCase testCase) { + final codeSignatureCommands = testCase.snapshot.commands.where( + (s) => s.cmd == macho.LoadCommand.LC_CODE_SIGNATURE, + ); + if (testCase.type == TestType.NoLinkerSignature) { + Expect.isEmpty(codeSignatureCommands); + } else { + Expect.equals(1, codeSignatureCommands.length); + } +} + +void checkBuildVersion(TestCase testCase) { + final buildVersion = testCase.snapshot + .commandsWhereType() + .singleOrNull; + if (buildVersion == null) { + Expect.isFalse(isApplePlatform); + return; + } + Expect.isTrue(isApplePlatform); + final expectedPlatform = Platform.isIOS + ? macho.Platform.PLATFORM_IOS + : macho.Platform.PLATFORM_MACOS; + Expect.equals(expectedPlatform, buildVersion.platform); + if (testCase.type == TestType.MinOSVersion) { + Expect.equals(expectedVersion, buildVersion.minOS); + Expect.equals(expectedVersion, buildVersion.sdk); + } + Expect.isEmpty(buildVersion.toolVersions); +} diff --git a/runtime/vm/mach_o.cc b/runtime/vm/mach_o.cc index 8fbb5c2729c..9bc67655b64 100644 --- a/runtime/vm/mach_o.cc +++ b/runtime/vm/mach_o.cc @@ -23,11 +23,27 @@ namespace dart { +DEFINE_FLAG(bool, + macho_linker_signature, + true, + "Whether to include a ad-hoc linker-signed code signature block"); + +DEFINE_FLAG(charp, + macho_install_name, + nullptr, + "The install name to be used for the dynamic library. " + "The output filename is used if not provided."); + #if defined(DART_TARGET_OS_MACOS) || defined(DART_TARGET_OS_MACOS_IOS) DEFINE_FLAG(charp, macho_min_os_version, nullptr, "The minimum OS version required for MacOS/iOS Mach-O snapshots"); + +DEFINE_FLAG(charp, + macho_rpath, + nullptr, + "Run paths to be added at runtime (comma delimited)"); #endif static constexpr intptr_t kLinearInitValue = -1; @@ -63,6 +79,7 @@ static constexpr intptr_t kLinearInitValue = -1; #if defined(DART_TARGET_OS_MACOS) || defined(DART_TARGET_OS_MACOS_IOS) #define FOR_EACH_MACOS_ONLY_CONCRETE_MACHO_CONTENTS_TYPE(V) \ + V(MachORunPath) \ V(MachOBuildVersion) \ V(MachOLoadDylib) #else @@ -1173,6 +1190,42 @@ class MachOBuildVersion : public MachOCommand { DISALLOW_COPY_AND_ASSIGN(MachOBuildVersion); }; + +class MachORunPath : public MachOCommand { + public: + static constexpr uint32_t kCommandCode = mach_o::LC_RPATH; + + MachORunPath(const char* path, intptr_t length) + : MachOCommand(kCommandCode, + /*needs_offset=*/false, + /*in_segment=*/false), + path_(path), + length_(length) {} + + uint32_t cmdsize() const override { + return Utils::RoundUp(HeaderSize() + length_ + 1, kLoadCommandAlignment); + } + + void WriteLoadCommand(MachOWriteStream* stream) const override { + const intptr_t start = stream->Position(); + MachOCommand::WriteLoadCommand(stream); + stream->Write32(HeaderSize()); // path.offset + ASSERT_EQUAL(HeaderSize(), stream->Position() - start); + stream->WriteFixedLengthCString(path_, length_); + stream->WriteByte('\0'); // Null-terminate the string. + stream->Align(kLoadCommandAlignment); + } + + void Accept(Visitor* visitor) override { visitor->VisitMachORunPath(this); } + + private: + uint32_t HeaderSize() const { return sizeof(mach_o::rpath_command); } + + const char* const path_; + const intptr_t length_; + + DISALLOW_COPY_AND_ASSIGN(MachORunPath); +}; #endif #undef MACHO_XYZ_VERSION_ENCODING @@ -1933,9 +1986,13 @@ MachOWriter::MachOWriter(Zone* zone, const char* path, Dwarf* dwarf) : SharedObjectWriter(zone, stream, type, dwarf), - header_(*new (zone) - MachOHeader(zone, type, IsStripped(dwarf), id, path, dwarf)) { -} + header_(*new (zone) MachOHeader( + zone, + type, + IsStripped(dwarf), + FLAG_macho_install_name != nullptr ? FLAG_macho_install_name : id, + path, + dwarf)) {} void MachOWriter::AddText(const char* name, intptr_t label, @@ -2402,8 +2459,19 @@ void MachOHeader::GenerateMiscellaneousCommands() { #if defined(DART_TARGET_OS_MACOS) || defined(DART_TARGET_OS_MACOS_IOS) ASSERT(!HasCommand(MachOBuildVersion::kCommandCode)); ASSERT(!HasCommand(MachOLoadDylib::kCommandCode)); + ASSERT(!HasCommand(MachORunPath::kCommandCode)); commands_.Add(new (zone_) MachOBuildVersion()); commands_.Add(MachOLoadDylib::CreateLoadSystemDylib(zone_)); + if (FLAG_macho_rpath != nullptr) { + const char* current = FLAG_macho_rpath; + for (const char* next = current;; next += 1) { + if (*next == ',' || *next == '\0') { + commands_.Add(new (zone_) MachORunPath(current, next - current)); + if (*next == '\0') break; + current = next + 1; + } + } + } #endif } } @@ -2580,7 +2648,7 @@ void MachOHeader::FinalizeCommands() { for (auto* const c : linkedit_commands) { linkedit_segment->AddContents(c); } - if (type_ == SnapshotType::Snapshot) { + if (type_ == SnapshotType::Snapshot && FLAG_macho_linker_signature) { // Also include an embedded ad-hoc linker signed code signature as the // last contents of the linkedit segment (which is the last segment). auto* const signature = new (zone_) MachOCodeSignature(identifier_);