VERDEF entries replace VERNEED names for shared indices, matching glibc lookup behavior.
620 lines
20 KiB
TypeScript
620 lines
20 KiB
TypeScript
import {
|
|
BinaryFormatFailure,
|
|
BinaryReader,
|
|
BinaryResourceFailure,
|
|
mappedFileOffset,
|
|
} from "../metadata/BinaryContainer.js";
|
|
import { architectureForElfMachine } from "../metadata/ElfImage.js";
|
|
import { readElfString, readElfSymbol } from "../metadata/ElfSymbol.js";
|
|
import {
|
|
readNativeDirectoryFromBuffer,
|
|
type NativeSection,
|
|
} from "../metadata/NativeSectionDirectory.js";
|
|
import { readPeImage } from "../metadata/PeImage.js";
|
|
import {
|
|
MAX_NATIVE_EXPORT_ROWS,
|
|
type ElfExport,
|
|
type NativeExportIssue,
|
|
type NativeExports,
|
|
type PeExport,
|
|
} from "../../domain/native/nativeExports.js";
|
|
|
|
type Artifact = NativeExports["artifact"];
|
|
|
|
const SHT_STRTAB = 3;
|
|
const SHT_DYNSYM = 11;
|
|
const SHT_GNU_VERDEF = 0x6ffffffd;
|
|
const SHT_GNU_VERNEED = 0x6ffffffe;
|
|
const SHT_GNU_VERSYM = 0x6fffffff;
|
|
const PE_EXPORT_DIRECTORY = 0;
|
|
const PE_EXPORT_DIRECTORY_BYTES = 40;
|
|
const MAXIMUM_NAME_BYTES = 4096;
|
|
/** Text retained across all rows; names can repeat one long string table entry. */
|
|
const MAXIMUM_RETAINED_TEXT_BYTES = 16 * 1024 * 1024;
|
|
|
|
/** Charge every retained row and its text before it is stored. */
|
|
const createRetention = () => {
|
|
let rows = 0;
|
|
let text = 0;
|
|
return (...strings: readonly (string | null)[]): void => {
|
|
rows++;
|
|
rowBudget(rows, "Export inventory");
|
|
for (const value of strings) text += value?.length ?? 0;
|
|
if (text > MAXIMUM_RETAINED_TEXT_BYTES)
|
|
throw new BinaryResourceFailure(
|
|
"structure",
|
|
MAXIMUM_RETAINED_TEXT_BYTES,
|
|
`Export names exceed the ${String(MAXIMUM_RETAINED_TEXT_BYTES)}-byte retained-text allowance`,
|
|
);
|
|
};
|
|
};
|
|
|
|
const hex = (value: bigint | number) => `0x${value.toString(16)}`;
|
|
|
|
const rowBudget = (count: number, label: string): void => {
|
|
if (count > MAX_NATIVE_EXPORT_ROWS)
|
|
throw new BinaryResourceFailure(
|
|
"structure",
|
|
MAX_NATIVE_EXPORT_ROWS,
|
|
`${label} declares ${String(count)} entries, above the ${String(MAX_NATIVE_EXPORT_ROWS)}-entry export inventory allowance`,
|
|
);
|
|
};
|
|
|
|
const coverage = (
|
|
issues: readonly NativeExportIssue[],
|
|
examined: number,
|
|
exports: number,
|
|
): NativeExports["coverage"] =>
|
|
issues.length === 0
|
|
? { status: "complete", reason: null, examined_entries: examined, exports }
|
|
: {
|
|
status: "partial",
|
|
reason: `${String(issues.length)} export table issue(s) left entries unreported; see issues`,
|
|
examined_entries: examined,
|
|
exports,
|
|
};
|
|
|
|
const failureMessage = (cause: unknown): string => {
|
|
if (cause instanceof BinaryFormatFailure) return cause.message;
|
|
throw cause;
|
|
};
|
|
|
|
const PE_LIMITATIONS = [
|
|
"Rows reproduce the PE export directory; they do not establish that an export is reachable, safe to call or loaded at runtime.",
|
|
"Forwarded exports resolve in another DLL, which is not opened; ordinal-only exports have no name in this image.",
|
|
];
|
|
|
|
/** Read the PE export directory: every used address-table slot and its names. */
|
|
const parsePeExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
|
|
const image = readPeImage(bytes);
|
|
const base = image.image_base ?? 0n;
|
|
const directory = image.pe_data_directories?.[PE_EXPORT_DIRECTORY];
|
|
const result = {
|
|
artifact,
|
|
format: "pe" as const,
|
|
architecture: image.architecture,
|
|
bits: image.bits,
|
|
};
|
|
if (directory === undefined || directory.virtual_address === 0)
|
|
return {
|
|
...result,
|
|
export_directory: null,
|
|
exports: [],
|
|
coverage: coverage([], 0, 0),
|
|
issues: [],
|
|
limitations: [
|
|
"The image declares no export directory, so it exports nothing by name or ordinal.",
|
|
...PE_LIMITATIONS,
|
|
],
|
|
};
|
|
const reader = new BinaryReader(bytes, true);
|
|
const mappings =
|
|
image.pe_header_mapping === undefined
|
|
? image.mappings
|
|
: [image.pe_header_mapping, ...image.mappings];
|
|
const map = (rva: number, size: number): number =>
|
|
mappedFileOffset(mappings, base + BigInt(rva), size, image.zero_fills);
|
|
const cString = (rva: number): string => {
|
|
const start = map(rva, 1);
|
|
const end = bytes
|
|
.subarray(start, start + MAXIMUM_NAME_BYTES + 1)
|
|
.indexOf(0);
|
|
if (end <= 0)
|
|
throw new BinaryFormatFailure(
|
|
"malformed",
|
|
`Export name at RVA ${hex(rva)} is empty, unterminated or longer than ${String(MAXIMUM_NAME_BYTES)} bytes`,
|
|
);
|
|
// The whole string must stay inside one file-backed mapping.
|
|
map(rva, end + 1);
|
|
return bytes.toString("latin1", start, start + end);
|
|
};
|
|
|
|
const header = map(directory.virtual_address, PE_EXPORT_DIRECTORY_BYTES);
|
|
const ordinalBase = reader.u32(header + 16);
|
|
const functionCount = reader.u32(header + 20);
|
|
const nameCount = reader.u32(header + 24);
|
|
rowBudget(functionCount, "PE export address table");
|
|
if (ordinalBase + functionCount > 0x10000)
|
|
throw new BinaryFormatFailure(
|
|
"malformed",
|
|
"PE export ordinal base and address-table size exceed 16-bit ordinals",
|
|
);
|
|
rowBudget(nameCount, "PE export name table");
|
|
const functions =
|
|
functionCount === 0 ? 0 : map(reader.u32(header + 28), functionCount * 4);
|
|
const names =
|
|
nameCount === 0 ? 0 : map(reader.u32(header + 32), nameCount * 4);
|
|
const ordinals =
|
|
nameCount === 0 ? 0 : map(reader.u32(header + 36), nameCount * 2);
|
|
const issues: NativeExportIssue[] = [];
|
|
let dllName: string | null = null;
|
|
try {
|
|
dllName = cString(reader.u32(header + 12));
|
|
} catch (cause) {
|
|
issues.push({
|
|
code: "name_unreadable",
|
|
location: hex(reader.u32(header + 12)),
|
|
message: `Export directory DLL name: ${failureMessage(cause)}`,
|
|
});
|
|
}
|
|
|
|
const namesByFunction = new Map<number, string[]>();
|
|
const namedFunctions = new Set<number>();
|
|
for (let index = 0; index < nameCount; index++) {
|
|
const functionIndex = reader.u16(ordinals + index * 2);
|
|
const nameRva = reader.u32(names + index * 4);
|
|
if (functionIndex >= functionCount) {
|
|
issues.push({
|
|
code: "malformed_entry",
|
|
location: hex(nameRva),
|
|
message: `Export name ${String(index)} selects address-table slot ${String(functionIndex)}, outside its ${String(functionCount)} slots`,
|
|
});
|
|
continue;
|
|
}
|
|
namedFunctions.add(functionIndex);
|
|
try {
|
|
const list = namesByFunction.get(functionIndex) ?? [];
|
|
list.push(cString(nameRva));
|
|
namesByFunction.set(functionIndex, list);
|
|
} catch (cause) {
|
|
issues.push({
|
|
code: "name_unreadable",
|
|
location: hex(nameRva),
|
|
message: failureMessage(cause),
|
|
});
|
|
}
|
|
}
|
|
|
|
const forwarderEnd = directory.virtual_address + directory.size;
|
|
const retain = createRetention();
|
|
const exports: PeExport[] = [];
|
|
for (let index = 0; index < functionCount; index++) {
|
|
const rva = reader.u32(functions + index * 4);
|
|
// A zero slot is an unused ordinal, not an export.
|
|
if (rva === 0) continue;
|
|
const ordinal = ordinalBase + index;
|
|
let target: Pick<PeExport, "address" | "forwarder">;
|
|
if (rva >= directory.virtual_address && rva < forwarderEnd) {
|
|
try {
|
|
target = { address: null, forwarder: cString(rva) };
|
|
} catch (cause) {
|
|
issues.push({
|
|
code: "malformed_entry",
|
|
location: hex(rva),
|
|
message: `Forwarder of ordinal ${String(ordinal)}: ${failureMessage(cause)}`,
|
|
});
|
|
continue;
|
|
}
|
|
} else target = { address: hex(base + BigInt(rva)), forwarder: null };
|
|
const exportNames = namesByFunction.get(index);
|
|
if (exportNames !== undefined)
|
|
for (const name of exportNames) {
|
|
retain(name, target.forwarder);
|
|
exports.push({ name, ordinal, ...target });
|
|
}
|
|
// A slot whose only name was unreadable is reported through its issue.
|
|
else if (!namedFunctions.has(index)) {
|
|
retain(target.forwarder);
|
|
exports.push({ name: null, ordinal, ...target });
|
|
}
|
|
}
|
|
return {
|
|
...result,
|
|
export_directory: {
|
|
rva: directory.virtual_address,
|
|
size: directory.size,
|
|
dll_name: dllName,
|
|
ordinal_base: ordinalBase,
|
|
timestamp: reader.u32(header + 4),
|
|
},
|
|
exports,
|
|
coverage: coverage(issues, functionCount, exports.length),
|
|
issues,
|
|
limitations: PE_LIMITATIONS,
|
|
};
|
|
};
|
|
|
|
const ELF_LIMITATIONS = [
|
|
"Rows list defined default- or protected-visibility global, weak and unique dynamic symbols; local, hidden, internal and undefined symbols are not exports.",
|
|
"A hidden symbol version is not the default version that a by-name dlsym lookup selects.",
|
|
"Rows reproduce the dynamic symbol table; they do not establish that a symbol is reachable, safe to call or loaded at runtime.",
|
|
];
|
|
|
|
const ELF_TYPES: Readonly<Record<number, ElfExport["type"]>> = {
|
|
1: "object",
|
|
2: "function",
|
|
6: "tls",
|
|
10: "ifunc",
|
|
};
|
|
const ELF_BINDINGS: Readonly<Record<number, ElfExport["binding"]>> = {
|
|
1: "global",
|
|
2: "weak",
|
|
10: "unique",
|
|
};
|
|
|
|
const versionSectionData = (
|
|
reader: BinaryReader,
|
|
sections: readonly NativeSection[],
|
|
section: NativeSection,
|
|
label: string,
|
|
): {
|
|
table: { readonly offset: number; readonly size: number };
|
|
names: Buffer;
|
|
} => {
|
|
const strings = sections[section.link];
|
|
if (strings?.type !== SHT_STRTAB)
|
|
throw new BinaryFormatFailure(
|
|
"malformed",
|
|
`${label} do not link to a string table`,
|
|
);
|
|
const stringRange = reader.fileRange(
|
|
strings.offset,
|
|
strings.size,
|
|
`${label} strings`,
|
|
);
|
|
const table = reader.fileRange(section.offset, section.size, label);
|
|
return {
|
|
table,
|
|
names: reader.bytes.subarray(
|
|
stringRange.offset,
|
|
stringRange.offset + stringRange.size,
|
|
),
|
|
};
|
|
};
|
|
|
|
/** Version names by index, read from `.gnu.version_d` and `.gnu.version_r`. */
|
|
const readVersionNames = (
|
|
reader: BinaryReader,
|
|
sections: readonly NativeSection[],
|
|
issues: NativeExportIssue[],
|
|
): Map<number, string | null> => {
|
|
const namesByIndex = new Map<number, string | null>();
|
|
const addName = (index: number, name: string): void => {
|
|
namesByIndex.set(index, name || null);
|
|
};
|
|
const verneed = sections.find(({ type }) => type === SHT_GNU_VERNEED);
|
|
if (verneed !== undefined) {
|
|
try {
|
|
const { table, names } = versionSectionData(
|
|
reader,
|
|
sections,
|
|
verneed,
|
|
"Version requirements",
|
|
);
|
|
let offset = 0;
|
|
for (let record = 0; record < verneed.info; record++) {
|
|
if (offset + 16 > table.size)
|
|
throw new BinaryFormatFailure(
|
|
"malformed",
|
|
"Version requirement chain leaves its section",
|
|
);
|
|
const at = table.offset + offset;
|
|
const auxiliaryCount = reader.u16(at + 2);
|
|
const auxiliaryOffset = reader.u32(at + 8);
|
|
const next = reader.u32(at + 12);
|
|
if (auxiliaryCount > 0 && auxiliaryOffset < 16)
|
|
throw new BinaryFormatFailure(
|
|
"malformed",
|
|
"Version requirement auxiliary chain starts inside its header",
|
|
);
|
|
let auxiliary = offset + auxiliaryOffset;
|
|
for (let item = 0; item < auxiliaryCount; item++) {
|
|
if (auxiliary + 16 > table.size)
|
|
throw new BinaryFormatFailure(
|
|
"malformed",
|
|
"Version requirement auxiliary chain leaves its section",
|
|
);
|
|
const auxAt = table.offset + auxiliary;
|
|
const versionIndex = reader.u16(auxAt + 6) & 0x7fff;
|
|
const auxiliaryNext = reader.u32(auxAt + 12);
|
|
try {
|
|
addName(versionIndex, readElfString(names, reader.u32(auxAt + 8)));
|
|
} catch (cause) {
|
|
issues.push({
|
|
code: "version_unresolved",
|
|
location: hex(verneed.offset + BigInt(auxiliary)),
|
|
message: `Version requirement name: ${failureMessage(cause)}`,
|
|
});
|
|
}
|
|
if (item + 1 < auxiliaryCount) {
|
|
if (auxiliaryNext < 16)
|
|
throw new BinaryFormatFailure(
|
|
"malformed",
|
|
"Version requirement auxiliary chain ends before its declared count",
|
|
);
|
|
auxiliary += auxiliaryNext;
|
|
}
|
|
}
|
|
if (record + 1 < verneed.info) {
|
|
if (next < 16)
|
|
throw new BinaryFormatFailure(
|
|
"malformed",
|
|
"Version requirement chain ends before its declared count",
|
|
);
|
|
offset += next;
|
|
}
|
|
}
|
|
} catch (cause) {
|
|
issues.push({
|
|
code: "version_unresolved",
|
|
location: hex(verneed.offset),
|
|
message: `Version requirements: ${failureMessage(cause)}`,
|
|
});
|
|
}
|
|
}
|
|
// GNU lookup installs requirement names first, then definition names.
|
|
const verdef = sections.find(({ type }) => type === SHT_GNU_VERDEF);
|
|
if (verdef !== undefined) {
|
|
try {
|
|
const { table, names } = versionSectionData(
|
|
reader,
|
|
sections,
|
|
verdef,
|
|
"Version definitions",
|
|
);
|
|
let offset = 0;
|
|
for (let index = 0; index < verdef.info; index++) {
|
|
if (offset + 20 > table.size)
|
|
throw new BinaryFormatFailure(
|
|
"malformed",
|
|
"Version definition chain leaves its section",
|
|
);
|
|
const at = table.offset + offset;
|
|
const versionIndex = reader.u16(at + 4) & 0x7fff;
|
|
const aux = reader.u32(at + 12);
|
|
const next = reader.u32(at + 16);
|
|
const auxiliaryCount = reader.u16(at + 6);
|
|
if (auxiliaryCount === 0 || aux < 20 || offset + aux + 8 > table.size)
|
|
throw new BinaryFormatFailure(
|
|
"malformed",
|
|
"Version definition name entry leaves its section",
|
|
);
|
|
let auxiliary = offset + aux;
|
|
const versionName = readElfString(
|
|
names,
|
|
reader.u32(table.offset + auxiliary),
|
|
);
|
|
addName(versionIndex, versionName);
|
|
for (let item = 0; item < auxiliaryCount; item++) {
|
|
if (auxiliary + 8 > table.size)
|
|
throw new BinaryFormatFailure(
|
|
"malformed",
|
|
"Version definition auxiliary chain leaves its section",
|
|
);
|
|
if (item + 1 < auxiliaryCount) {
|
|
const auxiliaryNext = reader.u32(table.offset + auxiliary + 4);
|
|
if (auxiliaryNext < 8)
|
|
throw new BinaryFormatFailure(
|
|
"malformed",
|
|
"Version definition auxiliary chain ends before its declared count",
|
|
);
|
|
auxiliary += auxiliaryNext;
|
|
}
|
|
}
|
|
if (index + 1 < verdef.info) {
|
|
if (next < 20)
|
|
throw new BinaryFormatFailure(
|
|
"malformed",
|
|
"Version definition chain ends before its declared count",
|
|
);
|
|
offset += next;
|
|
}
|
|
}
|
|
} catch (cause) {
|
|
issues.push({
|
|
code: "version_unresolved",
|
|
location: hex(verdef.offset),
|
|
message: `Version definitions: ${failureMessage(cause)}`,
|
|
});
|
|
}
|
|
}
|
|
|
|
return namesByIndex;
|
|
};
|
|
|
|
/** Read defined exported entries of `.dynsym` with their symbol versions. */
|
|
const parseElfExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
|
|
const directory = readNativeDirectoryFromBuffer(bytes, "elf");
|
|
const reader = new BinaryReader(bytes, directory.little);
|
|
const result = {
|
|
artifact,
|
|
format: "elf" as const,
|
|
architecture: architectureForElfMachine(directory.machine),
|
|
bits: directory.bits,
|
|
byte_order: directory.little ? ("little" as const) : ("big" as const),
|
|
};
|
|
const { sections } = directory;
|
|
if (sections.length === 0)
|
|
return {
|
|
...result,
|
|
exports: [],
|
|
coverage: {
|
|
status: "unsupported",
|
|
reason:
|
|
"The ELF has no section headers; enumerating dynamic symbols from the dynamic segment is not implemented",
|
|
examined_entries: 0,
|
|
exports: 0,
|
|
},
|
|
issues: [],
|
|
limitations: ELF_LIMITATIONS,
|
|
};
|
|
const dynsym = sections.find(({ type }) => type === SHT_DYNSYM);
|
|
if (dynsym === undefined)
|
|
return {
|
|
...result,
|
|
exports: [],
|
|
coverage: coverage([], 0, 0),
|
|
issues: [],
|
|
limitations: [
|
|
"The ELF has no dynamic symbol table, so it exports no dynamic symbols.",
|
|
...ELF_LIMITATIONS,
|
|
],
|
|
};
|
|
const width = directory.bits === 64 ? 24 : 16;
|
|
const strings = sections[dynsym.link];
|
|
if (
|
|
dynsym.entry_size !== BigInt(width) ||
|
|
dynsym.size % BigInt(width) !== 0n ||
|
|
// The table must hold at least its mandatory null symbol.
|
|
dynsym.size < BigInt(width) ||
|
|
strings?.type !== SHT_STRTAB
|
|
)
|
|
throw new BinaryFormatFailure(
|
|
"malformed",
|
|
"ELF dynamic symbol table entry size, length, null entry or string-table link is invalid",
|
|
);
|
|
const count = Number(dynsym.size / BigInt(width));
|
|
rowBudget(count, "ELF dynamic symbol table");
|
|
const table = reader.table(
|
|
dynsym.offset,
|
|
count,
|
|
width,
|
|
"ELF dynamic symbols",
|
|
);
|
|
const nameRange = reader.fileRange(
|
|
strings.offset,
|
|
strings.size,
|
|
"ELF dynamic strings",
|
|
);
|
|
const names = bytes.subarray(
|
|
nameRange.offset,
|
|
nameRange.offset + nameRange.size,
|
|
);
|
|
const issues: NativeExportIssue[] = [];
|
|
const versym = sections.find(
|
|
({ type, link }) => type === SHT_GNU_VERSYM && link === dynsym.index,
|
|
);
|
|
let versions: number | null = null;
|
|
if (versym !== undefined) {
|
|
try {
|
|
if (versym.size !== BigInt(count * 2))
|
|
throw new BinaryFormatFailure(
|
|
"malformed",
|
|
`Symbol version table has ${String(versym.size)} bytes for ${String(count)} symbols`,
|
|
);
|
|
versions = reader.table(versym.offset, count, 2, "ELF symbol versions");
|
|
} catch (cause) {
|
|
// Keep every export; their versions become unknown.
|
|
issues.push({
|
|
code: "version_unresolved",
|
|
location: hex(versym.offset),
|
|
message: failureMessage(cause),
|
|
});
|
|
}
|
|
}
|
|
const versionNames = readVersionNames(reader, sections, issues);
|
|
const unresolvedVersions = new Set<number>();
|
|
const retain = createRetention();
|
|
const exports: ElfExport[] = [];
|
|
for (let index = 1; index < count; index++) {
|
|
const at = table + index * width;
|
|
const symbol = readElfSymbol(
|
|
bytes.subarray(at, at + width),
|
|
directory.bits,
|
|
directory.little,
|
|
);
|
|
const binding = ELF_BINDINGS[symbol.binding];
|
|
const visibility = symbol.other & 3;
|
|
// Undefined, local and hidden/internal symbols are not exports.
|
|
if (
|
|
binding === undefined ||
|
|
symbol.section_index === 0 ||
|
|
(visibility !== 0 && visibility !== 3)
|
|
)
|
|
continue;
|
|
let version: ElfExport["version"] = null;
|
|
if (versym !== undefined && versions === null) version = "unknown";
|
|
else if (versions !== null) {
|
|
const raw = reader.u16(versions + index * 2);
|
|
const versionIndex = raw & 0x7fff;
|
|
if (versionIndex === 0) continue;
|
|
if (versionIndex >= 2) {
|
|
const name = versionNames.get(versionIndex) ?? null;
|
|
if (name === null && !unresolvedVersions.has(versionIndex)) {
|
|
unresolvedVersions.add(versionIndex);
|
|
issues.push({
|
|
code: "version_unresolved",
|
|
location: hex(symbol.value),
|
|
message: `Version index ${String(versionIndex)}, first used by symbol ${String(index)}, has no unambiguous version name`,
|
|
});
|
|
}
|
|
version = {
|
|
index: versionIndex,
|
|
name,
|
|
hidden: (raw & 0x8000) !== 0,
|
|
};
|
|
}
|
|
}
|
|
let name: string;
|
|
try {
|
|
name = readElfString(names, symbol.name_offset);
|
|
} catch (cause) {
|
|
issues.push({
|
|
code: "name_unreadable",
|
|
location: hex(symbol.value),
|
|
message: `Dynamic symbol ${String(index)}: ${failureMessage(cause)}`,
|
|
});
|
|
continue;
|
|
}
|
|
if (name === "") {
|
|
issues.push({
|
|
code: "name_unreadable",
|
|
location: hex(symbol.value),
|
|
message: `Defined global dynamic symbol ${String(index)} has an empty name`,
|
|
});
|
|
continue;
|
|
}
|
|
retain(name, typeof version === "object" ? (version?.name ?? null) : null);
|
|
exports.push({
|
|
name,
|
|
value: hex(symbol.value),
|
|
size: symbol.size.toString(),
|
|
type: ELF_TYPES[symbol.type] ?? "other",
|
|
binding,
|
|
version,
|
|
});
|
|
}
|
|
return {
|
|
...result,
|
|
exports,
|
|
coverage: coverage(issues, count - 1, exports.length),
|
|
issues,
|
|
limitations: ELF_LIMITATIONS,
|
|
};
|
|
};
|
|
|
|
/** Static export inventory of one PE or ELF image; other formats are unsupported. */
|
|
export const parseNativeExports = (
|
|
bytes: Buffer,
|
|
artifact: Artifact,
|
|
): NativeExports => {
|
|
if (bytes.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46])))
|
|
return parseElfExports(bytes, artifact);
|
|
if (bytes.subarray(0, 2).equals(Buffer.from("MZ")))
|
|
return parsePeExports(bytes, artifact);
|
|
throw new BinaryFormatFailure(
|
|
"unsupported",
|
|
"Only PE and ELF images are supported; inspect Mach-O exports with inspect_macho",
|
|
);
|
|
};
|