Validate ELF identification/header versions and declared header bounds at the shared owner. Reuse parsed facts in target selection, Go, Dart and MIPS readers while preserving their stricter format policies and x32 support. Repair synthetic ELF fixtures and remove a superseded Go error-text assertion. Validation: 74 source tests, 202 filesystem/Go/MIPS boundary tests, typecheck and lint. No new real-provider claims.
367 lines
12 KiB
TypeScript
367 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 machine = decodePeMachine(record.readUInt16LE(4));
|
|
if (machine === undefined) return err("unsupported PE architecture");
|
|
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;
|
|
};
|