mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
3f168af2e8
Specialize the buffer-backed directory driver for its actual ELF consumers and remove the unused PE branch. Share PE subject architecture interpretation at the existing machine owner, preserving unknowns for conflicting formats and unproven managed overrides. Validation: runtime build and 108 focused service, native parser, GPU, and CLI/MCP tests passed using local fixtures. Live descriptor-backed PE readers remain supported.
655 lines
22 KiB
TypeScript
655 lines
22 KiB
TypeScript
import {
|
|
BinaryFormatFailure,
|
|
BinaryReader,
|
|
BinaryResourceFailure,
|
|
mappedFileOffset,
|
|
imageFileMappings,
|
|
readMappedCString,
|
|
} from "../metadata/BinaryContainer.js";
|
|
import { architectureForElfMachine } from "../metadata/ElfImage.js";
|
|
import { isUtf8 } from "node:buffer";
|
|
import { readElfStringBytes, readElfSymbol } from "../metadata/ElfSymbol.js";
|
|
import {
|
|
readElfDirectoryFromBuffer,
|
|
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) affect reported rows or metadata; 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 = imageFileMappings(image);
|
|
const map = (rva: number, size: number): number =>
|
|
mappedFileOffset(mappings, base + BigInt(rva), size, image.zero_fills);
|
|
const cString = (rva: number): string => {
|
|
const value = readMappedCString(
|
|
bytes,
|
|
image,
|
|
base + BigInt(rva),
|
|
MAXIMUM_NAME_BYTES,
|
|
);
|
|
if (value.length === 0)
|
|
throw new BinaryFormatFailure(
|
|
"malformed",
|
|
`Export name at RVA ${hex(rva)} is empty`,
|
|
);
|
|
return value.toString("latin1");
|
|
};
|
|
|
|
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");
|
|
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`. */
|
|
type DecodedElfString = {
|
|
readonly name: string;
|
|
readonly name_bytes_base64: string | null;
|
|
readonly name_utf8_decodable: boolean;
|
|
};
|
|
|
|
const decodeElfString = (bytes: Buffer): DecodedElfString => {
|
|
const utf8 = isUtf8(bytes);
|
|
return {
|
|
name: bytes.toString(utf8 ? "utf8" : "latin1"),
|
|
name_bytes_base64: bytes.some((byte) => byte >= 0x80)
|
|
? bytes.toString("base64")
|
|
: null,
|
|
name_utf8_decodable: utf8,
|
|
};
|
|
};
|
|
|
|
const readVersionNames = (
|
|
reader: BinaryReader,
|
|
sections: readonly NativeSection[],
|
|
issues: NativeExportIssue[],
|
|
): Map<number, DecodedElfString | null> => {
|
|
const namesByIndex = new Map<number, DecodedElfString | null>();
|
|
const addName = (index: number, name: Buffer): void => {
|
|
namesByIndex.set(index, name.length === 0 ? null : decodeElfString(name));
|
|
};
|
|
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,
|
|
readElfStringBytes(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 = readElfStringBytes(
|
|
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 = readElfDirectoryFromBuffer(bytes);
|
|
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;
|
|
const dynsym = sections.find(({ type }) => type === SHT_DYNSYM);
|
|
if (
|
|
sections.length === 0 ||
|
|
(dynsym === undefined && directory.programs.some(({ type }) => type === 2))
|
|
)
|
|
return {
|
|
...result,
|
|
exports: [],
|
|
coverage: {
|
|
status: "unsupported",
|
|
reason:
|
|
sections.length === 0
|
|
? "The ELF has no section headers; enumerating dynamic symbols from the dynamic segment is not implemented"
|
|
: "The ELF declares a dynamic segment but has no dynamic symbol section; enumerating dynamic symbols from the dynamic segment is not implemented",
|
|
examined_entries: 0,
|
|
exports: 0,
|
|
},
|
|
issues: [],
|
|
limitations: ELF_LIMITATIONS,
|
|
};
|
|
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 decodedName = versionNames.get(versionIndex) ?? null;
|
|
if (decodedName === 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: decodedName?.name ?? null,
|
|
name_bytes_base64: decodedName?.name_bytes_base64 ?? null,
|
|
name_utf8_decodable: decodedName?.name_utf8_decodable ?? null,
|
|
hidden: (raw & 0x8000) !== 0,
|
|
};
|
|
}
|
|
}
|
|
let name: string;
|
|
let nameUtf8Decodable: boolean;
|
|
let rawNameBase64: string | null;
|
|
try {
|
|
const rawName = readElfStringBytes(names, symbol.name_offset);
|
|
const decodedName = decodeElfString(rawName);
|
|
nameUtf8Decodable = decodedName.name_utf8_decodable;
|
|
name = decodedName.name;
|
|
rawNameBase64 = decodedName.name_bytes_base64;
|
|
} 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,
|
|
rawNameBase64,
|
|
typeof version === "object" ? (version?.name_bytes_base64 ?? null) : null,
|
|
);
|
|
exports.push({
|
|
name,
|
|
name_bytes_base64: rawNameBase64,
|
|
name_utf8_decodable: nameUtf8Decodable,
|
|
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",
|
|
);
|
|
};
|