blob: 052d62a7d20c11f49696499aa85214e26b72c005 [file] [log] [blame]
// Copyright (c) 2022, 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.
import 'dart:io';
import 'dart:math' as math;
import './macho.dart';
import 'src/generate_utils.dart';
/// Pipe from one file stream into another.
///
/// This is done in chunks to avoid excessive memory load.
Future<int> pipeStream(RandomAccessFile from, RandomAccessFile to,
{int? numToWrite, int chunkSize = 1 << 30}) async {
var numWritten = 0;
final fileLength = from.lengthSync();
while (from.positionSync() != fileLength) {
final availableBytes = fileLength - from.positionSync();
final numToRead = numToWrite == null
? math.min(availableBytes, chunkSize)
: math.min(
numToWrite - numWritten,
math.min(availableBytes, chunkSize),
);
final buffer = await from.read(numToRead);
await to.writeFrom(buffer);
numWritten += numToRead;
if (numToWrite != null && numWritten >= numToWrite) {
break;
}
}
return numWritten;
}
class _MacOSVersion {
final int? _major;
final int? _minor;
static final _regexp = RegExp(r'Version (?<major>\d+).(?<minor>\d+)');
static const _parseFailure =
FormatException('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.
//
// WARNING: this method is used within google3, so don't try to refactor so
// [dartaotruntimePath] is a constant inside this file.
Future<void> writeAppendedMachOExecutable(
String dartaotruntimePath, String payloadPath, String outputPath) async {
final aotRuntimeFile = File(dartaotruntimePath);
final aotRuntimeHeaders = MachOFile.fromFile(aotRuntimeFile);
final oldLinkEdit = aotRuntimeHeaders.linkEditSegment;
if (oldLinkEdit == null) {
throw const FormatException('__LINKEDIT segment not found');
}
// Get the length of the contents of the section to be added.
final snapshotFile = File(payloadPath);
final payloadLength = snapshotFile.lengthSync();
// Add the header information for where the snapshot will live, and retrieve
// the needed parts back out of the new headers.
final outputHeaders =
aotRuntimeHeaders.adjustHeaderForSnapshot(payloadLength);
final snapshotNote = outputHeaders.snapshotNote!;
final newLinkEdit = outputHeaders.linkEditSegment!;
final output = await File(outputPath).open(mode: FileMode.write);
void addPadding(int start, int end) {
assert(end >= start);
output.writeFromSync(List.filled(end - start, 0));
}
// First, write the new headers.
outputHeaders.writeSync(output);
// If the newer headers are smaller, add appropriate padding to fit.
addPadding(outputHeaders.size, aotRuntimeHeaders.size);
// Now write the original contents from the header to the __LINKEDIT segment
// contents.
final aotRuntimeStream = await aotRuntimeFile.open();
await aotRuntimeStream.setPosition(aotRuntimeHeaders.size);
await pipeStream(aotRuntimeStream, output,
numToWrite: oldLinkEdit.fileOffset - aotRuntimeHeaders.size);
// Now insert the snapshot contents at this position in the file.
// There may be additional padding needed between the old __LINKEDIT file
// offset and the start of the new snapshot.
addPadding(oldLinkEdit.fileOffset, snapshotNote.fileOffset);
final snapshotStream = await snapshotFile.open();
await pipeStream(snapshotStream, output);
// Now add appropriate padding after the snapshot to reach the expected offset
// of the __LINKEDIT segment in the new file.
final snapshotEnd = snapshotNote.fileOffset + snapshotNote.fileSize;
addPadding(snapshotEnd, newLinkEdit.fileOffset);
// Copy the rest of the file from the original to the new one.
await pipeStream(aotRuntimeStream, output);
await output.close();
if (outputHeaders.hasCodeSignature) {
if (!Platform.isMacOS) {
throw UnsupportedError('Cannot sign MachO binary on non-macOS platform');
}
// After writing the modified file, we perform ad-hoc signing (no identity)
// 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 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) {
stderr
..write('Failed to replace the dartaotruntime signature, ')
..write('subcommand terminated with exit code ')
..write(signingProcess.exitCode)
..writeln('.');
if (signingProcess.stdout case final signingOutput
when processOutputIsNotEmpty(signingOutput)) {
stderr
..writeln('Subcommand stdout:')
..writeln(signingOutput);
}
if (signingProcess.stderr case final signingErrorOutput
when processOutputIsNotEmpty(signingErrorOutput)) {
stderr
..writeln('Subcommand stderr:')
..writeln(signingErrorOutput);
}
throw Exception('Could not sign the new executable');
}
}
}