diff --git a/pkg/dart2native/lib/dart2native_macho.dart b/pkg/dart2native/lib/dart2native_macho.dart index eefaf9d5214..1c45d130890 100644 --- a/pkg/dart2native/lib/dart2native_macho.dart +++ b/pkg/dart2native/lib/dart2native_macho.dart @@ -265,6 +265,32 @@ Future pipeStream(RandomAccessFile from, RandomAccessFile to, return numWritten; } +class _MacOSVersion { + final int? _major; + final int? _minor; + + static final _regexp = RegExp(r'Version (?\d+).(?\d+)'); + static const _parseFailure = 'Could not determine macOS version'; + + const _MacOSVersion._internal(this._major, this._minor); + + static const _unknown = _MacOSVersion._internal(null, null); + + factory _MacOSVersion() { + if (!Platform.isMacOS) return _unknown; + final match = + _regexp.matchAsPrefix(Platform.operatingSystemVersion) as RegExpMatch?; + if (match == null) return _unknown; + final minor = int.tryParse(match.namedGroup('minor')!); + final major = int.tryParse(match.namedGroup('major')!); + return _MacOSVersion._internal(major, minor); + } + + bool get isValid => _major != null; + int get major => _major ?? (throw _parseFailure); + int get minor => _minor ?? (throw _parseFailure); +} + // Writes an "appended" dart runtime + script snapshot file in a format // compatible with MachO executables. Future writeAppendedMachOExecutable( @@ -312,17 +338,39 @@ Future writeAppendedMachOExecutable( await stream.close(); if (machOFile.hasCodeSignature) { + if (!Platform.isMacOS) { + throw 'Cannot sign MachO binary on non-macOS platform'; + } + // After writing the modified file, we perform ad-hoc signing (no identity) - // similar to the linker (the linker-signed option flag) to ensure that any - // LC_CODE_SIGNATURE block has the correct CD hashes. This is necessary for - // platforms where signature verification is always on (e.g., OS X on M1). + // to ensure that any LC_CODE_SIGNATURE block has the correct CD hashes. + // This is necessary for platforms where signature verification is always on + // (e.g., OS X on M1). // // We use the `-f` flag to force signature overwriting as the official // Dart binaries (including dartaotruntime) are fully signed. - final signingProcess = await Process.run( - 'codesign', ['-f', '-o', 'linker-signed', '-s', '-', outputPath]); + final args = ['-f', '-s', '-', outputPath]; + + // If running on macOS >=11.0, then the linker-signed option flag can be + // used to create a signature that does not need to be force overridden. + final version = _MacOSVersion(); + if (version.isValid && version.major >= 11) { + final signingProcess = + await Process.run('codesign', ['-o', 'linker-signed', ...args]); + if (signingProcess.exitCode == 0) { + return; + } + print('Failed to add a linker signed signature, ' + 'adding a regular signature instead.'); + } + + // If that fails or we're running on an older or undetermined version of + // macOS, we fall back to signing without the linker-signed option flag. + // Thus, to sign the binary, the developer must force signature overwriting. + final signingProcess = await Process.run('codesign', args); if (signingProcess.exitCode != 0) { - print('Subcommand terminated with exit code ${signingProcess.exitCode}.'); + print('Failed to replace the dartaotruntime signature, '); + print('subcommand terminated with exit code ${signingProcess.exitCode}.'); if (signingProcess.stdout.isNotEmpty) { print('Subcommand stdout:'); print(signingProcess.stdout); diff --git a/pkg/dartdev/test/commands/compile_test.dart b/pkg/dartdev/test/commands/compile_test.dart index e5c15b0e0ac..0d497b4106d 100644 --- a/pkg/dartdev/test/commands/compile_test.dart +++ b/pkg/dartdev/test/commands/compile_test.dart @@ -187,7 +187,7 @@ void defineCompileTests() { expect(result.stdout, contains('2: foo')); }); - test('Compile and run executable', () async { + Future basicCompileTest() async { final p = project(mainSrc: 'void main() { print("I love executables"); }'); final inFile = path.canonicalize(path.join(p.dirPath, p.relativeFilePath)); final outFile = path.canonicalize(path.join(p.dirPath, 'lib', 'main.exe')); @@ -213,7 +213,9 @@ void defineCompileTests() { expect(result.stderr, isEmpty); expect(result.exitCode, 0); expect(result.stdout, contains('I love executables')); - }, skip: isRunningOnIA32); + } + + test('Compile and run executable', basicCompileTest, skip: isRunningOnIA32); test('Compile to executable disabled on IA32', () async { final p = project(mainSrc: 'void main() { print("I love executables"); }'); @@ -1112,4 +1114,33 @@ void main() { expect(result.stderr, contains('Warning:')); expect(result.exitCode, 0); }); + + if (Platform.isMacOS) { + test('Compile and run executable from signed dartaotruntime', () async { + // Either the locally built dartaotruntime is already linker signed + // (on M1) or it is unsigned (on X64). For this test, sign the + // dartaotruntime executable with a non-linker signed adhoc signature, + // which won't cause issues with any other tests that use it. This + // ensures the code signing path in dart2native is exercised on X64 + // (macOS <11.0), and also mimics the case for end users that are using + // the published Dart SDK (which is fully signed, not linker signed). + final Directory binDir = File(Platform.resolvedExecutable).parent; + final String originalRuntimePath = + path.join(binDir.path, 'dartaotruntime'); + final codeSigningProcess = await Process.start('codesign', [ + '-o', + 'runtime', + '-s', + '-', + originalRuntimePath, + ]); + + final signingResult = await codeSigningProcess.exitCode; + expect(signingResult, 0); + + // Now perform the same basic compile and run test with the signed + // dartaotruntime. + await basicCompileTest(); + }, skip: isRunningOnIA32); + } }