371 lines
12 KiB
TypeScript
371 lines
12 KiB
TypeScript
import type {
|
|
BinaryArchitecture,
|
|
ElfFamilyMetadata,
|
|
HeaderArchitecture,
|
|
MipsElfMetadata,
|
|
RiscvElfMetadata,
|
|
UniversalMachOSlice,
|
|
} from "./binaryTargetTypes.js";
|
|
import { err, ok, type Result } from "./result.js";
|
|
import { readElfFixedHeader } from "./elfFixedHeader.js";
|
|
import { observeMzWindowsHeader, parseDosMzHeader } from "./dosMz.js";
|
|
import { decodePeMachine } from "./peMachine.js";
|
|
import {
|
|
classifyFatContainerIdentity,
|
|
readFatContainerHeader,
|
|
} from "./apple/machoContainer.js";
|
|
|
|
/** Format and architecture facts recovered from an executable header. */
|
|
export type ExecutableMetadata =
|
|
| {
|
|
readonly format: "elf";
|
|
readonly architecture: "mips";
|
|
readonly availableArchitectures: readonly "mips"[];
|
|
readonly mips: MipsElfMetadata;
|
|
}
|
|
| {
|
|
readonly format: "elf";
|
|
readonly architecture: "riscv";
|
|
readonly availableArchitectures: readonly "riscv"[];
|
|
readonly riscv: RiscvElfMetadata;
|
|
}
|
|
| {
|
|
readonly format: "mach-o" | "elf" | "dos-mz" | "dos-com";
|
|
readonly architecture: HeaderArchitecture;
|
|
readonly availableArchitectures: readonly HeaderArchitecture[];
|
|
readonly universalSlice?: never;
|
|
}
|
|
| {
|
|
readonly format: "mach-o-universal";
|
|
readonly architecture: HeaderArchitecture;
|
|
readonly availableArchitectures: readonly HeaderArchitecture[];
|
|
readonly universalSlice: UniversalMachOSlice;
|
|
}
|
|
| {
|
|
readonly format: "pe";
|
|
readonly architecture: HeaderArchitecture;
|
|
readonly availableArchitectures: readonly HeaderArchitecture[];
|
|
readonly executableRole:
|
|
| "application"
|
|
| "shared-library"
|
|
| "non-executable";
|
|
readonly managed: boolean;
|
|
};
|
|
|
|
/**
|
|
* Parse supported executable headers without I/O. The caller supplies the host
|
|
* architecture used for deterministic FAT Mach-O slice selection.
|
|
*/
|
|
export const parseExecutableHeader = (
|
|
bytes: Buffer,
|
|
hostArchitecture: NodeJS.Architecture,
|
|
fileSize = bytes.length,
|
|
mzWindowsHeaderSignature?: Buffer,
|
|
): Result<ExecutableMetadata, string> => {
|
|
if (
|
|
bytes.length >= 4 &&
|
|
bytes.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46]))
|
|
)
|
|
return parseElf(bytes, fileSize);
|
|
if (bytes.length >= 2 && bytes[0] === 0x4d && bytes[1] === 0x5a)
|
|
return parseMz(bytes, fileSize, mzWindowsHeaderSignature);
|
|
if (bytes.length < 8) return err("truncated or unsupported binary header");
|
|
const magic = bytes.readUInt32BE(0);
|
|
if ([0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe].includes(magic))
|
|
return parseThinMachO(bytes, magic, fileSize);
|
|
const fatHeader = readFatContainerHeader(bytes, fileSize);
|
|
if (fatHeader.state !== "not-fat")
|
|
return parseFatMachO(bytes, hostArchitecture, fatHeader);
|
|
return err("unsupported binary format");
|
|
};
|
|
|
|
const parseThinMachO = (
|
|
bytes: Buffer,
|
|
magic: number,
|
|
fileSize: number,
|
|
): Result<ExecutableMetadata, string> => {
|
|
const is64 = magic === 0xfeedfacf || magic === 0xcffaedfe;
|
|
const headerSize = is64 ? 32 : 28;
|
|
if (bytes.length < headerSize)
|
|
return err(
|
|
`truncated Mach-O header: a ${is64 ? 64 : 32}-bit header needs ${headerSize} bytes; the file has ${fileSize}`,
|
|
);
|
|
const little = magic === 0xcefaedfe || magic === 0xcffaedfe;
|
|
const read = (offset: number): number =>
|
|
little ? bytes.readUInt32LE(offset) : bytes.readUInt32BE(offset);
|
|
const architecture = machArchitecture(read(4));
|
|
if (architecture === undefined) return err("unsupported Mach-O architecture");
|
|
const commandBytes = read(20);
|
|
if (headerSize + commandBytes > fileSize)
|
|
return err(
|
|
`truncated Mach-O load commands: the header declares ${commandBytes} bytes after its ${headerSize}-byte header; the file has ${fileSize}`,
|
|
);
|
|
return ok({
|
|
format: "mach-o",
|
|
architecture,
|
|
availableArchitectures: [architecture],
|
|
});
|
|
};
|
|
|
|
const parseFatMachO = (
|
|
bytes: Buffer,
|
|
host: NodeJS.Architecture,
|
|
header: Exclude<
|
|
ReturnType<typeof readFatContainerHeader>,
|
|
{ state: "not-fat" }
|
|
>,
|
|
): Result<ExecutableMetadata, string> => {
|
|
if (header.state === "needs-data")
|
|
return err("truncated or invalid FAT architecture table");
|
|
if (header.state === "malformed") return err(header.reason);
|
|
const identityError = fatIdentityError(bytes, header);
|
|
if (identityError !== null) return err(identityError);
|
|
const architectures: HeaderArchitecture[] = [];
|
|
const slices = new Map<
|
|
HeaderArchitecture,
|
|
{ readonly offset: number; readonly size: number }
|
|
>();
|
|
for (const declaration of header.slices) {
|
|
const architecture = machArchitecture(declaration.cpuType);
|
|
if (architecture === undefined || architectures.includes(architecture))
|
|
continue;
|
|
architectures.push(architecture);
|
|
slices.set(architecture, {
|
|
offset: declaration.offset,
|
|
size: declaration.size,
|
|
});
|
|
}
|
|
const preferred = fatHostArchitecture(host);
|
|
const slice = preferred === undefined ? undefined : slices.get(preferred);
|
|
if (preferred === undefined || slice === undefined)
|
|
return err(`FAT binary has no host-compatible ${host} architecture`);
|
|
const minimumHeaderSize =
|
|
preferred === "arm64" || preferred === "x86_64" ? 32 : 28;
|
|
if (slice.size < minimumHeaderSize)
|
|
return err(`FAT slice for ${preferred} is shorter than its Mach-O header`);
|
|
return ok({
|
|
format: "mach-o-universal",
|
|
architecture: preferred,
|
|
availableArchitectures: architectures,
|
|
universalSlice: { offset: slice.offset, size: slice.size },
|
|
});
|
|
};
|
|
|
|
const fatHostArchitecture = (
|
|
host: NodeJS.Architecture,
|
|
): HeaderArchitecture | undefined => {
|
|
switch (host) {
|
|
case "arm64":
|
|
return "arm64";
|
|
case "x64":
|
|
return "x86_64";
|
|
case "ia32":
|
|
return "x86";
|
|
case "arm":
|
|
return "arm";
|
|
default:
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
const fatIdentityError = (
|
|
bytes: Buffer,
|
|
header: Extract<
|
|
ReturnType<typeof readFatContainerHeader>,
|
|
{ state: "parsed" }
|
|
>,
|
|
): string | null => {
|
|
const identity = classifyFatContainerIdentity(header, (offset) => {
|
|
if (offset >= bytes.length) return undefined;
|
|
if (offset + 4 > bytes.length) return null;
|
|
return bytes.readUInt32BE(offset);
|
|
});
|
|
if (identity.state === "needs-slice-header")
|
|
return "FAT slice identity is outside the bounded prefix";
|
|
if (identity.state === "not-mach-o")
|
|
return "FAT table does not identify a Mach-O image";
|
|
if (identity.state === "malformed") return identity.reason;
|
|
return null;
|
|
};
|
|
|
|
const parseElf = (
|
|
bytes: Buffer,
|
|
fileSize: number,
|
|
): Result<ExecutableMetadata, string> => {
|
|
const fixedHeader = readElfFixedHeader(bytes, fileSize);
|
|
if (!fixedHeader.ok) return fixedHeader;
|
|
const { elfClass, byteOrder } = fixedHeader.value;
|
|
const little = byteOrder === "little";
|
|
const machine = little ? bytes.readUInt16LE(18) : bytes.readUInt16BE(18);
|
|
const architecture = elfArchitecture(machine);
|
|
if (architecture === undefined) return err("unsupported ELF architecture");
|
|
if (architecture === "mips" || architecture === "riscv") {
|
|
const flagsOffset = elfClass === 32 ? 36 : 48;
|
|
const metadata: ElfFamilyMetadata = {
|
|
elfClass,
|
|
byteOrder: little ? "little" : "big",
|
|
type: little ? bytes.readUInt16LE(16) : bytes.readUInt16BE(16),
|
|
flags: little
|
|
? bytes.readUInt32LE(flagsOffset)
|
|
: bytes.readUInt32BE(flagsOffset),
|
|
};
|
|
return architecture === "mips"
|
|
? ok({
|
|
format: "elf",
|
|
architecture,
|
|
availableArchitectures: [architecture],
|
|
mips: metadata,
|
|
})
|
|
: ok({
|
|
format: "elf",
|
|
architecture,
|
|
availableArchitectures: [architecture],
|
|
riscv: metadata,
|
|
});
|
|
}
|
|
return ok({
|
|
format: "elf",
|
|
architecture,
|
|
availableArchitectures: [architecture],
|
|
});
|
|
};
|
|
|
|
const parseMz = (
|
|
bytes: Buffer,
|
|
fileSize: number,
|
|
signatureAtOffset?: Buffer,
|
|
): Result<ExecutableMetadata, string> => {
|
|
const windowsHeader = observeMzWindowsHeader(
|
|
bytes,
|
|
fileSize,
|
|
signatureAtOffset,
|
|
);
|
|
if (
|
|
windowsHeader.state === "header-needed" ||
|
|
windowsHeader.state === "signature-needed"
|
|
)
|
|
return err(
|
|
"Windows new-header pointer or signature is outside the bounded prefix",
|
|
);
|
|
if (windowsHeader.state === "declared") {
|
|
const { offset } = windowsHeader;
|
|
if (offset < 64 || offset > bytes.length - 24)
|
|
return err("invalid or truncated Windows executable header in MZ image");
|
|
if (["NE", "LE", "LX"].includes(windowsHeader.signature))
|
|
return err(
|
|
`unsupported ${windowsHeader.signature} executable in MZ image`,
|
|
);
|
|
if (windowsHeader.signature !== "PE")
|
|
return err("invalid or truncated Windows executable header in MZ image");
|
|
return parsePeRecord(bytes.subarray(offset));
|
|
}
|
|
const parsed = parseDosMzHeader(bytes, fileSize, signatureAtOffset);
|
|
return parsed.ok
|
|
? ok({
|
|
format: "dos-mz",
|
|
architecture: "x86",
|
|
availableArchitectures: ["x86"],
|
|
})
|
|
: parsed;
|
|
};
|
|
|
|
/** Parse a bounded PE signature, COFF record, and optional header without I/O. */
|
|
export const parsePeRecord = (
|
|
record: Buffer,
|
|
): Result<ExecutableMetadata, string> => {
|
|
if (
|
|
record.length < 24 ||
|
|
record.toString("binary", 0, 4) !== "PE\u0000\u0000"
|
|
)
|
|
return err("invalid or truncated PE header");
|
|
const machineType = record.readUInt16LE(4);
|
|
const machine = decodePeMachine(machineType);
|
|
if (machine === undefined)
|
|
return err(
|
|
`unsupported PE architecture (COFF machine 0x${machineType.toString(16).padStart(4, "0")})`,
|
|
);
|
|
const { architecture } = machine;
|
|
const optionalHeaderSize = record.readUInt16LE(20);
|
|
if (optionalHeaderSize > 4096 || record.length < 24 + optionalHeaderSize)
|
|
return err("invalid or truncated PE optional header");
|
|
const optionalHeader = record.subarray(24, 24 + optionalHeaderSize);
|
|
const managed = peManagedStatus(optionalHeader, architecture);
|
|
if (!managed.ok) return managed;
|
|
if (machine.nativeOsOverride && !managed.value)
|
|
return err("unsupported PE architecture");
|
|
const characteristics = record.readUInt16LE(22);
|
|
return ok({
|
|
format: "pe",
|
|
architecture,
|
|
availableArchitectures: [architecture],
|
|
executableRole:
|
|
(characteristics & 0x0002) === 0
|
|
? "non-executable"
|
|
: (characteristics & 0x2000) === 0
|
|
? "application"
|
|
: "shared-library",
|
|
managed: managed.value,
|
|
});
|
|
};
|
|
|
|
const peManagedStatus = (
|
|
optionalHeader: Buffer,
|
|
architecture: BinaryArchitecture,
|
|
): Result<boolean, string> => {
|
|
if (optionalHeader.length < 2) return err("truncated PE optional header");
|
|
const magic = optionalHeader.readUInt16LE(0);
|
|
const expectedMagic =
|
|
architecture === "x86_64" || architecture === "arm64" ? 0x20b : 0x10b;
|
|
if (magic !== expectedMagic)
|
|
return err("PE optional-header magic does not match its architecture");
|
|
const directoryOffset = magic === 0x20b ? 112 : 96;
|
|
const directoryCountOffset = directoryOffset - 4;
|
|
if (optionalHeader.length < directoryCountOffset + 4)
|
|
return err("truncated PE optional header");
|
|
const directoryCount = optionalHeader.readUInt32LE(directoryCountOffset);
|
|
const requiredBytes = directoryOffset + directoryCount * 8;
|
|
if (
|
|
!Number.isSafeInteger(requiredBytes) ||
|
|
requiredBytes > optionalHeader.length
|
|
)
|
|
return err("truncated PE data-directory table");
|
|
if (directoryCount <= 14) return ok(false);
|
|
const cliDirectory = directoryOffset + 14 * 8;
|
|
return ok(
|
|
optionalHeader.readUInt32LE(cliDirectory) !== 0 ||
|
|
optionalHeader.readUInt32LE(cliDirectory + 4) !== 0,
|
|
);
|
|
};
|
|
|
|
const machArchitecture = (cpu: number): HeaderArchitecture | undefined => {
|
|
switch (cpu) {
|
|
case 7:
|
|
return "x86";
|
|
case 0x01000007:
|
|
return "x86_64";
|
|
case 12:
|
|
return "arm";
|
|
case 0x0100000c:
|
|
return "arm64";
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
const elfArchitecture = (machine: number): BinaryArchitecture | undefined => {
|
|
switch (machine) {
|
|
case 3:
|
|
return "x86";
|
|
case 62:
|
|
return "x86_64";
|
|
case 8: // EM_MIPS: ISA, ABI and byte order remain separate ELF facts.
|
|
return "mips";
|
|
case 40:
|
|
return "arm";
|
|
case 183:
|
|
return "arm64";
|
|
case 243: // EM_RISCV: XLEN, byte order and e_flags remain separate ELF facts.
|
|
return "riscv";
|
|
}
|
|
return undefined;
|
|
};
|