| // 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:convert'; |
| import 'dart:io'; |
| import 'dart:math'; |
| import 'dart:typed_data'; |
| |
| import 'package:dart2native/macho.dart'; |
| import 'package:dart2native/macho_parser.dart'; |
| |
| const String kSnapshotSegmentName = "__CUSTOM"; |
| const String kSnapshotSectionName = "__dart_app_snap"; |
| const int kMinimumSegmentSize = 0x4000; |
| // Since arm64 macOS has 16K pages, which is larger than the 4K pages on x64 |
| // macOS, we use this larger page size to ensure the MachO file is aligned |
| // properly on all architectures. |
| const int kSegmentAlignment = 0x4000; |
| |
| int align(int size, int base) { |
| final int over = size % base; |
| if (over != 0) { |
| return size + (base - over); |
| } |
| return size; |
| } |
| |
| // Utility for aligning parts of MachO headers to the defined sizes. |
| int vmSizeAlign(int size) { |
| return align(max(size, kMinimumSegmentSize), kSegmentAlignment); |
| } |
| |
| // Returns value + amount only if the original value is within the bounds |
| // defined by [withinStart, withinStart + withinSize). |
| Uint32 addIfWithin( |
| Uint32 value, Uint64 amount, Uint64 withinStart, Uint64 withinSize) { |
| final intWithinStart = withinStart.asInt(); |
| final intWithinSize = withinSize.asInt(); |
| |
| if (value >= intWithinStart && value < (intWithinStart + intWithinSize)) { |
| return (value.asUint64() + amount).asUint32(); |
| } else { |
| return value; |
| } |
| } |
| |
| // Trims a bytestring that an arbitrary number of null characters on the end of |
| // it. |
| String trimmedBytestring(Uint8List bytestring) { |
| return String.fromCharCodes(bytestring.takeWhile((value) => value != 0)); |
| } |
| |
| // Simplifies casting so we get null values back instead of exceptions. |
| T? cast<T>(x) => x is T ? x : null; |
| |
| // Inserts a segment definition into a MachOFile. This does NOT insert the |
| // actual segment into the file. It only inserts the definition of that segment |
| // into the MachO header. |
| // |
| // In addition to simply specifying the definition for the segment, this |
| // function also moves the existing __LINKEDIT segment to the end of the header |
| // definition as is required by the MachO specification (or at least MacOS's |
| // implementation of it). In doing so there are several offsets in the original |
| // __LINKEDIT segment that must be updated to point to their new location |
| // because the __LINKEDIT segment and sections are now in a different |
| // place. This function takes care of those shifts as well. |
| // |
| // Returns the original, unmodified, __LINKEDIT segment. |
| Future<MachOSegmentCommand64> insertSegmentDefinition(MachOFile file, |
| File segment, String segmentName, String sectionName) async { |
| // Load in the data to be inserted. |
| final segmentData = await segment.readAsBytes(); |
| |
| // Find the existing __LINKEDIT segment |
| final linkedit = cast<MachOSegmentCommand64>(file.commands |
| .where((segment) => |
| segment.asType() is MachOSegmentCommand64 && |
| MachOConstants.SEG_LINKEDIT == |
| trimmedBytestring((segment as MachOSegmentCommand64).segname)) |
| .first); |
| |
| final linkeditIndex = file.commands.indexWhere((segment) => |
| segment.asType() is MachOSegmentCommand64 && |
| MachOConstants.SEG_LINKEDIT == |
| trimmedBytestring((segment as MachOSegmentCommand64).segname)); |
| |
| if (linkedit == null) { |
| throw FormatException( |
| "Could not find a __LINKEDIT section in the specified binary."); |
| } else { |
| // Create the new segment. |
| final Uint8List segname = Uint8List(16); |
| segname.setRange(0, segmentName.length, ascii.encode(segmentName)); |
| segname.fillRange(segmentName.length, 16, 0); |
| |
| final Uint64 vmaddr = linkedit.vmaddr; |
| final Uint64 vmsize = Uint64(vmSizeAlign(segmentData.length)); |
| final Uint64 fileoff = linkedit.fileoff; |
| final Uint64 filesize = vmsize; |
| final Int32 maxprot = MachOConstants.VM_PROT_READ; |
| final Int32 initprot = maxprot; |
| final Uint32 nsects = Uint32(1); |
| |
| final Uint8List sectname = Uint8List(16); |
| sectname.setRange(0, sectionName.length, ascii.encode(sectionName)); |
| sectname.fillRange(sectionName.length, 16, 0); |
| |
| final Uint64 addr = vmaddr; |
| final Uint64 size = Uint64(segmentData.length); |
| final Uint32 offset = fileoff.asUint32(); |
| final Uint32 flags = MachOConstants.S_REGULAR; |
| |
| final Uint32 zero = Uint32(0); |
| |
| final loadCommandDefinitionSize = 4 * 2; |
| final sectionDefinitionSize = 16 * 2 + 8 * 2 + 4 * 8; |
| final segmentDefinitionSize = 16 + 8 * 4 + 4 * 4; |
| final commandSize = loadCommandDefinitionSize + |
| segmentDefinitionSize + |
| sectionDefinitionSize; |
| |
| final loadCommand = |
| MachOLoadCommand(MachOConstants.LC_SEGMENT_64, Uint32(commandSize)); |
| |
| final section = MachOSection64(sectname, segname, addr, size, offset, zero, |
| zero, zero, flags, zero, zero, zero); |
| |
| final segment = MachOSegmentCommand64(Uint32(commandSize), segname, vmaddr, |
| vmsize, fileoff, filesize, maxprot, initprot, nsects, zero, [section]); |
| |
| // Setup the new linkedit command. |
| final shiftedLinkeditVmaddr = linkedit.vmaddr + segment.vmsize; |
| final shiftedLinkeditFileoff = linkedit.fileoff + segment.filesize; |
| final shiftedLinkedit = MachOSegmentCommand64( |
| linkedit.cmdsize, |
| linkedit.segname, |
| shiftedLinkeditVmaddr, |
| linkedit.vmsize, |
| shiftedLinkeditFileoff, |
| linkedit.filesize, |
| linkedit.maxprot, |
| linkedit.initprot, |
| linkedit.nsects, |
| linkedit.flags, |
| linkedit.sections); |
| |
| // Shift all of the related commands that need to reference the new file |
| // position of the linkedit segment. |
| for (var i = 0; i < file.commands.length; i++) { |
| final command = file.commands[i]; |
| |
| final offsetAmount = segment.filesize; |
| final withinStart = linkedit.fileoff; |
| final withinSize = linkedit.filesize; |
| |
| // For the specific command that we need to adjust, we need to move the |
| // commands' various offsets forward by the new segment's size in the file |
| // (segment.filesize). However, we need to ensure that when we move the |
| // offset forward, we exclude cases where the offset was originally |
| // outside of the linkedit segment (i.e. offset < linkedit.fileoff or |
| // offset >= linkedit.fileoff + linkedit.filesize). The DRY-ing function |
| // addIfWithin takes care of that repeated logic. |
| if (command is MachODyldInfoCommand) { |
| file.commands[i] = MachODyldInfoCommand( |
| command.cmd, |
| command.cmdsize, |
| addIfWithin( |
| command.rebase_off, offsetAmount, withinStart, withinSize), |
| command.rebase_size, |
| addIfWithin( |
| command.bind_off, offsetAmount, withinStart, withinSize), |
| command.bind_size, |
| addIfWithin( |
| command.weak_bind_off, offsetAmount, withinStart, withinSize), |
| command.weak_bind_size, |
| addIfWithin( |
| command.lazy_bind_off, offsetAmount, withinStart, withinSize), |
| command.lazy_bind_size, |
| addIfWithin( |
| command.export_off, offsetAmount, withinStart, withinSize), |
| command.export_size); |
| } else if (command is MachOSymtabCommand) { |
| file.commands[i] = MachOSymtabCommand( |
| command.cmdsize, |
| addIfWithin(command.symoff, offsetAmount, withinStart, withinSize), |
| command.nsyms, |
| addIfWithin(command.stroff, offsetAmount, withinStart, withinSize), |
| command.strsize); |
| } else if (command is MachODysymtabCommand) { |
| file.commands[i] = MachODysymtabCommand( |
| command.cmdsize, |
| command.ilocalsym, |
| command.nlocalsym, |
| command.iextdefsym, |
| command.nextdefsym, |
| command.iundefsym, |
| command.nundefsym, |
| addIfWithin(command.tocoff, offsetAmount, withinStart, withinSize), |
| command.ntoc, |
| addIfWithin( |
| command.modtaboff, offsetAmount, withinStart, withinSize), |
| command.nmodtab, |
| addIfWithin( |
| command.extrefsymoff, offsetAmount, withinStart, withinSize), |
| command.nextrefsyms, |
| addIfWithin( |
| command.indirectsymoff, offsetAmount, withinStart, withinSize), |
| command.nindirectsyms, |
| addIfWithin( |
| command.extreloff, offsetAmount, withinStart, withinSize), |
| command.nextrel, |
| addIfWithin( |
| command.locreloff, offsetAmount, withinStart, withinSize), |
| command.nlocrel); |
| } else if (command is MachOLinkeditDataCommand) { |
| file.commands[i] = MachOLinkeditDataCommand( |
| command.cmd, |
| command.cmdsize, |
| addIfWithin(command.dataoff, offsetAmount, withinStart, withinSize), |
| command.datasize); |
| } |
| } |
| |
| // Now we need to build the new header from these modified pieces. |
| file.header = MachOHeader( |
| file.header!.magic, |
| file.header!.cputype, |
| file.header!.cpusubtype, |
| file.header!.filetype, |
| file.header!.ncmds + Uint32(1), |
| file.header!.sizeofcmds + loadCommand.cmdsize, |
| file.header!.flags, |
| file.header!.reserved); |
| |
| file.commands[linkeditIndex] = shiftedLinkedit; |
| file.commands.insert(linkeditIndex, segment); |
| } |
| |
| return linkedit; |
| } |
| |
| // Pipe from one file stream into another. We do this in chunks to avoid |
| // excessive memory load. |
| Future<int> pipeStream(RandomAccessFile from, RandomAccessFile to, |
| {int? numToWrite, int chunkSize = 1 << 30}) async { |
| int numWritten = 0; |
| final int fileLength = from.lengthSync(); |
| while (from.positionSync() != fileLength) { |
| final int availableBytes = fileLength - from.positionSync(); |
| final int numToRead = numToWrite == null |
| ? min(availableBytes, chunkSize) |
| : min(numToWrite - numWritten, 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 = '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( |
| String dartaotruntimePath, String payloadPath, String outputPath) async { |
| File originalExecutableFile = File(dartaotruntimePath); |
| |
| MachOFile machOFile = MachOFile(); |
| await machOFile.loadFromFile(originalExecutableFile); |
| |
| // Insert the new segment that contains our snapshot data. |
| File newSegmentFile = File(payloadPath); |
| |
| // Note that these two values MUST match the ones in |
| // runtime/bin/snapshot_utils.cc, which looks specifically for the snapshot in |
| // this segment/section. |
| final linkeditCommand = await insertSegmentDefinition( |
| machOFile, newSegmentFile, kSnapshotSegmentName, kSnapshotSectionName); |
| |
| // Write out the new executable, with the same contents except the new header. |
| File outputFile = File(outputPath); |
| RandomAccessFile stream = await outputFile.open(mode: FileMode.write); |
| |
| // Write the MachO header. |
| machOFile.writeSync(stream); |
| final int headerBytesWritten = stream.positionSync(); |
| |
| RandomAccessFile newSegmentFileStream = await newSegmentFile.open(); |
| RandomAccessFile originalFileStream = await originalExecutableFile.open(); |
| await originalFileStream.setPosition(headerBytesWritten); |
| |
| // Write the unchanged data from the original file. |
| await pipeStream(originalFileStream, stream, |
| numToWrite: linkeditCommand.fileoff.asInt() - headerBytesWritten); |
| |
| // Write the inserted section data, ensuring that the data is padded to the |
| // segment size. |
| await pipeStream(newSegmentFileStream, stream); |
| final int newSegmentLength = newSegmentFileStream.lengthSync(); |
| final int alignedSegmentSize = vmSizeAlign(newSegmentLength); |
| await stream.writeFrom(List.filled(alignedSegmentSize - newSegmentLength, 0)); |
| |
| // Copy the rest of the file from the original to the new one. |
| await pipeStream(originalFileStream, stream); |
| |
| 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) |
| // 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.isNotEmpty) { |
| stderr |
| ..writeln('Subcommand stdout:') |
| ..writeln(signingProcess.stdout); |
| } |
| if (signingProcess.stderr.isNotEmpty) { |
| stderr |
| ..writeln('Subcommand stderr:') |
| ..writeln(signingProcess.stderr); |
| } |
| throw 'Could not sign the new executable'; |
| } |
| } |
| } |