Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions pkgs/code_assets/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
## 1.3.0-wip

- Validate bundled dynamic libraries by reading their header: when the file is
a recognized ELF, Mach-O, or PE binary, check that it was built for the
target architecture. Reject multi-architecture Mach-O output because hooks
run once per target architecture. A file the built-in validators do not
recognize, or a recognized library built for an architecture they do not
know, warns on the supplied logger instead of failing.
- Bump the dependency on `package:hooks` to `^2.2.0-wip`.

## 1.2.1

- Avoid throwing a null-assertion error/exception when `input.config.code` is
Expand Down
112 changes: 112 additions & 0 deletions pkgs/code_assets/lib/src/code_assets/elf_validator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Copyright (c) 2026, 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:typed_data';

import 'architecture.dart';
import 'native_library_validation.dart';
import 'os.dart';

/// Validates that a bundled dynamic library is an ELF file built for the target
/// architecture.
///
/// ELF is the container format for Android, Fuchsia, and Linux targets. For any
/// other target operating system this validator returns
/// [NativeLibraryValidation.notRecognized] without opening the file. A file
/// that is not an ELF binary is also [NativeLibraryValidation.notRecognized],
/// leaving it to another validator or to a warning.
final class ElfValidator implements NativeLibraryValidator {
/// Creates an [ElfValidator].
const ElfValidator();

// The identification and machine values come from the System V ABI machine
// table and the RISC-V ELF psABI:
// https://gabi.xinuos.com/elf/a-emachine.html
// https://github.com/riscv-non-isa/riscv-elf-psabi-doc/blob/master/riscv-elf.adoc
static const _magic = [0x7f, 0x45, 0x4c, 0x46];
static const _classOffset = 4;
static const _class32 = 1;
static const _class64 = 2;
static const _dataOffset = 5;
static const _dataLittleEndian = 1;
static const _dataBigEndian = 2;
static const _machineOffset = 18;
static const _machineX86 = 3;
static const _machineArm = 40;
static const _machineX64 = 62;
static const _machineArm64 = 183;
static const _machineRiscV = 243;

// The identification and the machine field occupy the first 20 bytes.
static const _minLength = 20;

@override
Future<NativeLibraryValidation> validate(
NativeLibraryValidationContext context,
) async {
final targetOS = context.config.targetOS;
if (targetOS != OS.android &&
targetOS != OS.fuchsia &&
targetOS != OS.linux) {
return const NativeLibraryValidation.notRecognized();
}

final Uint8List head;
try {
final raf = File.fromUri(context.file).openSync();
try {
head = raf.readSync(_minLength);
} finally {
raf.closeSync();
}
} on FileSystemException {
return const NativeLibraryValidation.notRecognized();
}

if (head.length < _minLength || !_hasMagic(head)) {
return const NativeLibraryValidation.notRecognized();
}
final elfClass = head[_classOffset];
final elfData = head[_dataOffset];
if ((elfClass != _class32 && elfClass != _class64) ||
(elfData != _dataLittleEndian && elfData != _dataBigEndian)) {
return const NativeLibraryValidation.notRecognized();
}

final machine = ByteData.sublistView(head).getUint16(
_machineOffset,
elfData == _dataLittleEndian ? Endian.little : Endian.big,
);
final architecture = switch (machine) {
_machineX86 => Architecture.ia32,
_machineArm => Architecture.arm,
_machineX64 => Architecture.x64,
_machineArm64 => Architecture.arm64,
_machineRiscV =>
elfClass == _class32 ? Architecture.riscv32 : Architecture.riscv64,
_ => null,
};

final target = context.config.targetArchitecture;
if (architecture == target) {
return const NativeLibraryValidation.matched();
}
if (architecture == null) {
return NativeLibraryValidation.inconclusive(
'The built-in validator does not recognize machine value '
'0x${machine.toRadixString(16)}.',
);
}
return NativeLibraryValidation.rejected([
'is built for $architecture, but the target architecture is $target.',
]);
}

static bool _hasMagic(Uint8List head) =>
head[0] == _magic[0] &&
head[1] == _magic[1] &&
head[2] == _magic[2] &&
head[3] == _magic[3];
}
4 changes: 2 additions & 2 deletions pkgs/code_assets/lib/src/code_assets/extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,13 @@ final class CodeAssetExtension extends ProtocolExtension {
Future<ValidationErrors> validateBuildOutput(
BuildInput input,
BuildOutput output,
) => validateCodeAssetBuildOutput(input, output);
) => validateCodeAssetBuildOutput(input, output, logger: logger);

@override
Future<ValidationErrors> validateLinkOutput(
LinkInput input,
LinkOutput output,
) => validateCodeAssetLinkOutput(input, output);
) => validateCodeAssetLinkOutput(input, output, logger: logger);

@override
Future<ValidationErrors> validateApplicationAssets(
Expand Down
171 changes: 171 additions & 0 deletions pkgs/code_assets/lib/src/code_assets/mach_o_validator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// Copyright (c) 2026, 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:typed_data';

import 'architecture.dart';
import 'native_library_validation.dart';
import 'os.dart';

/// Validates that a bundled dynamic library is a thin Mach-O file built for the
/// target architecture.
///
/// Mach-O is the container format for iOS and macOS targets. For any other
/// target operating system this validator returns
/// [NativeLibraryValidation.notRecognized] without opening the file. A file
/// that is not a Mach-O binary is also [NativeLibraryValidation.notRecognized],
/// leaving it to another validator or to a warning.
///
/// The validator recognizes files according to the Mach-O format specified by
/// Apple.
final class MachOValidator implements NativeLibraryValidator {
/// Creates a [MachOValidator].
const MachOValidator();

// The magic and CPU type values come from Apple's loader.h, fat.h, and
// machine.h:
// https://github.com/apple-oss-distributions/xnu/blob/main/EXTERNAL_HEADERS/mach-o/loader.h
// https://github.com/apple-oss-distributions/cctools/blob/main/include/mach-o/fat.h
// https://github.com/apple-oss-distributions/xnu/blob/main/osfmk/mach/machine.h
static const _magic32 = 0xfeedface;
static const _magic64 = 0xfeedfacf;
static const _swappedMagic32 = 0xcefaedfe;
static const _swappedMagic64 = 0xcffaedfe;
static const _fatMagic32 = 0xcafebabe;
static const _fatMagic64 = 0xcafebabf;
static const _fatEntrySize32 = 20;
static const _fatEntrySize64 = 32;
static const _cpuTypeX86 = 0x00000007;
static const _cpuTypeX64 = 0x01000007;
static const _cpuTypeArm = 0x0000000c;
static const _cpuTypeArm64 = 0x0100000c;

// The magic number and the 32-bit field after it (the CPU type for a thin
// binary, or the slice count for a fat binary) occupy the first 8 bytes. The
// fat path re-reads the architecture entries from the file directly, so
// nothing past this is read from the header here.
static const _minLength = 8;

@override
Future<NativeLibraryValidation> validate(
NativeLibraryValidationContext context,
) async {
final targetOS = context.config.targetOS;
if (targetOS != OS.iOS && targetOS != OS.macOS) {
return const NativeLibraryValidation.notRecognized();
}

final RandomAccessFile raf;
try {
raf = File.fromUri(context.file).openSync();
} on FileSystemException {
return const NativeLibraryValidation.notRecognized();
}
try {
final head = raf.readSync(_minLength);
if (head.length < _minLength) {
return const NativeLibraryValidation.notRecognized();
}
final data = ByteData.sublistView(head);

// The fat header and its arch entries are always big-endian.
final fatMagic = data.getUint32(0, Endian.big);
if (fatMagic == _fatMagic32 || fatMagic == _fatMagic64) {
return _validateFat(
raf,
data,
context.config.targetArchitecture,
is64: fatMagic == _fatMagic64,
);
}

final thinMagic = data.getUint32(0, Endian.little);
final Endian endian;
if (thinMagic == _magic32 || thinMagic == _magic64) {
endian = Endian.little;
} else if (thinMagic == _swappedMagic32 || thinMagic == _swappedMagic64) {
endian = Endian.big;
} else {
return const NativeLibraryValidation.notRecognized();
}
final cpuType = data.getUint32(4, endian);
return _checkArchitecture(
_architectureForCpuType(cpuType),
cpuType,
context.config.targetArchitecture,
);
} on FileSystemException {
return const NativeLibraryValidation.notRecognized();
} finally {
raf.closeSync();
}
}

NativeLibraryValidation _validateFat(
RandomAccessFile raf,
ByteData head,
Architecture target, {
required bool is64,
}) {
final sliceCount = head.getUint32(4, Endian.big);
if (sliceCount == 0) return const NativeLibraryValidation.notRecognized();
final entrySize = is64 ? _fatEntrySize64 : _fatEntrySize32;
final entriesLength = sliceCount * entrySize;
if (raf.lengthSync() < 8 + entriesLength) {
return const NativeLibraryValidation.notRecognized();
}
raf.setPositionSync(8);
final entries = raf.readSync(entriesLength);
if (entries.length < entriesLength) {
return const NativeLibraryValidation.notRecognized();
}
if (sliceCount > 1) return _multiArchitecture();

final cpuType = ByteData.sublistView(entries).getUint32(0, Endian.big);
return _checkArchitecture(
_architectureForCpuType(cpuType),
cpuType,
target,
);
}

static Architecture? _architectureForCpuType(
int cpuType,
) => switch (cpuType) {
_cpuTypeX86 => Architecture.ia32,
_cpuTypeX64 => Architecture.x64,
_cpuTypeArm => Architecture.arm,
// arm64e uses CPU_TYPE_ARM64 and identifies the ABI in cpusubtype. Until
// TODO(https://github.com/dart-lang/native/issues/3379) reads that subtype,
// treat arm64e as arm64 rather than claiming to validate its ABI.
_cpuTypeArm64 => Architecture.arm64,
_ => null,
};

NativeLibraryValidation _checkArchitecture(
Architecture? architecture,
int machineValue,
Architecture target,
) {
if (architecture == target) {
return const NativeLibraryValidation.matched();
}
if (architecture == null) {
return NativeLibraryValidation.inconclusive(
'The built-in validator does not recognize machine value '
'0x${machineValue.toRadixString(16)}.',
);
}
return NativeLibraryValidation.rejected([
'is built for $architecture, but the target architecture is $target.',
]);
}

static NativeLibraryValidation _multiArchitecture() =>
NativeLibraryValidation.rejected([
'is a multi-architecture Mach-O binary. Hooks run once per target '
'architecture and must output a thin Mach-O binary.',
]);
}
Loading
Loading