feat(native): read PE and ELF export tables

Add a host-independent export-table reader on the shared native
container layer. PE images report each used export-address-table slot
with its ordinal, one row per name, its address or forwarder text, and
the export directory identity; the PE image reader now exposes its data
directories. ELF images report defined default/protected global, weak
and unique .dynsym symbols with type, binding and .gnu.version_d
version (hidden versions kept distinct, unreadable version tables
"unknown"). Unreadable entries are counted in issues and make coverage
partial; an ELF without section headers is unsupported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822andClaude Opus 5.5 committed 2026-10-22 06:58:12 +00:00
1 parent c8ed328538
commit 2dee400f93
6 files changed
+1165

No files matched your search

+128
View File
@@ -0,0 +1,128 @@
import { z } from "zod";
import { absoluteLocalPathStringSchema } from "../localPath.js";
const uint = z.number().int().min(0).max(0xffffffff);
const address = z.string().regex(/^0x[0-9a-f]+$/u);
/** Hard ceiling on the caller-selected input read budget. */
export const MAX_INSPECT_NATIVE_EXPORTS_FILE_BYTES = 536870912;
/** Fixed bound on reported export rows, so one library cannot flood a reply. */
export const MAX_NATIVE_EXPORT_ROWS = 262144;
/** One explicitly selected PE or ELF library or executable. */
export const inspectNativeExportsInputSchema = z.strictObject({
path: absoluteLocalPathStringSchema.describe(
"Absolute filesystem path to a PE or ELF image",
),
max_file_bytes: z
.number()
.int()
.min(64)
.max(MAX_INSPECT_NATIVE_EXPORTS_FILE_BYTES)
.default(268435456)
.describe("Maximum stable artifact bytes to read (default 256 MiB)"),
});
export type InspectNativeExportsInput = z.infer<
typeof inspectNativeExportsInputSchema
>;
/**
* One PE export-address-table slot as the export directory states it. A
* forwarder has no address in this image; an ordinal-only export has no name.
*/
const peExportSchema = z.strictObject({
name: z.string().min(1).nullable(),
ordinal: z.number().int().min(0).max(0xffff),
address: address.nullable(),
forwarder: z.string().min(1).nullable(),
});
/**
* One defined, default- or protected-visibility global, weak or unique
* dynamic symbol. `version` is null when the symbol carries no version and
* "unknown" when the version table could not be read; a hidden version is not
* the default one a by-name lookup selects.
*/
const elfExportSchema = z.strictObject({
name: z.string().min(1),
/** `st_value` as stored; on 32-bit ARM, bit 0 of a function marks Thumb code. */
value: address,
size: z.string().regex(/^[0-9]+$/u),
type: z.enum(["function", "ifunc", "object", "tls", "other"]),
binding: z.enum(["global", "weak", "unique"]),
version: z.union([
z.null(),
z.literal("unknown"),
z.strictObject({
index: z.number().int().min(2).max(0x7fff),
name: z.string().min(1).nullable(),
hidden: z.boolean(),
}),
]),
});
/** Parsed table issue; rows it affects are counted, not silently dropped. */
const nativeExportIssueSchema = z.strictObject({
code: z.enum([
"malformed_entry",
"name_unreadable",
"version_unresolved",
"table_unavailable",
]),
location: z.string().min(1).nullable(),
message: z.string().min(1),
});
const artifactSchema = z.strictObject({
path: z.string().min(1),
sha256: z.string().regex(/^[a-f0-9]{64}$/u),
bytes: uint,
});
const coverageSchema = z.strictObject({
/** complete: every table entry was read; partial: issues name the gaps. */
status: z.enum(["complete", "partial", "unsupported"]),
reason: z.string().min(1).nullable(),
examined_entries: uint,
exports: uint,
});
/** Static export inventory of one PE or ELF image, read without execution. */
export const nativeExportsSchema = z.discriminatedUnion("format", [
z.strictObject({
artifact: artifactSchema,
format: z.literal("pe"),
architecture: z.string().min(1),
bits: z.union([z.literal(32), z.literal(64)]),
/** Null when the image declares no export directory. */
export_directory: z
.strictObject({
rva: uint,
size: uint,
dll_name: z.string().nullable(),
ordinal_base: uint,
timestamp: uint,
})
.nullable(),
exports: z.array(peExportSchema).max(MAX_NATIVE_EXPORT_ROWS),
coverage: coverageSchema,
issues: z.array(nativeExportIssueSchema),
limitations: z.array(z.string().min(1)),
}),
z.strictObject({
artifact: artifactSchema,
format: z.literal("elf"),
architecture: z.string().min(1),
bits: z.union([z.literal(32), z.literal(64)]),
byte_order: z.enum(["little", "big"]),
exports: z.array(elfExportSchema).max(MAX_NATIVE_EXPORT_ROWS),
coverage: coverageSchema,
issues: z.array(nativeExportIssueSchema),
limitations: z.array(z.string().min(1)),
}),
]);
export type NativeExports = z.infer<typeof nativeExportsSchema>;
export type PeExport = z.infer<typeof peExportSchema>;
export type ElfExport = z.infer<typeof elfExportSchema>;
export type NativeExportIssue = z.infer<typeof nativeExportIssueSchema>;
@@ -0,0 +1,291 @@
import { describe, expect, it } from "vitest";
import {
elfExportImage,
peExportImage,
} from "../../../tests/fixtures/binaryDiagnostics/nativeExports.js";
import { nativeExportsSchema } from "../../domain/native/nativeExports.js";
import {
BinaryFormatFailure,
BinaryResourceFailure,
} from "../metadata/BinaryContainer.js";
import { parseNativeExports } from "./NativeExportParser.js";
const artifact = { path: "/fixture/lib", sha256: "a".repeat(64), bytes: 1 };
const parse = (bytes: Buffer) =>
nativeExportsSchema.parse(parseNativeExports(bytes, artifact));
describe("PE export directories", () => {
it.each([32, 64] as const)(
"reports named, aliased, ordinal-only and forwarded PE%d exports",
(bits) => {
const result = parse(
peExportImage({
bits,
ordinalBase: 5,
functions: [
{ rva: 0x2000 },
{ rva: 0x2010 },
null,
{ forwarder: "OTHER.Target" },
],
names: [
{ name: "Alias", index: 0 },
{ name: "Forwarded", index: 3 },
{ name: "Primary", index: 0 },
],
}),
);
const base = bits === 64 ? 0x180000000 : 0x10000000;
expect(result).toMatchObject({
format: "pe",
bits,
export_directory: {
rva: 0x1000,
size: 0x200,
dll_name: "fixture.dll",
ordinal_base: 5,
},
coverage: {
status: "complete",
examined_entries: 4,
exports: 4,
},
issues: [],
});
expect(result.exports).toEqual([
{
name: "Alias",
ordinal: 5,
address: `0x${(base + 0x2000).toString(16)}`,
forwarder: null,
},
{
name: "Primary",
ordinal: 5,
address: `0x${(base + 0x2000).toString(16)}`,
forwarder: null,
},
{
name: null,
ordinal: 6,
address: `0x${(base + 0x2010).toString(16)}`,
forwarder: null,
},
{
name: "Forwarded",
ordinal: 8,
address: null,
forwarder: "OTHER.Target",
},
]);
},
);
it("counts unreadable and out-of-range names without inventing exports", () => {
const result = parse(
peExportImage({
functions: [{ rva: 0x2000 }, { rva: 0x2010 }],
names: [
{ name: "Unmapped", index: 0 },
{ name: "Stray", index: 9 },
{ name: "Kept", index: 1 },
],
nameRvaOverride: { position: 0, rva: 0x9000 },
}),
);
expect(result.exports.map(({ name }) => name)).toEqual(["Kept"]);
expect(result.coverage).toMatchObject({ status: "partial", exports: 1 });
expect(result.issues.map(({ code }) => code)).toEqual([
"name_unreadable",
"malformed_entry",
]);
});
it("reports an image without an export directory as exporting nothing", () => {
const result = parse(peExportImage({ exportDirectory: false }));
expect(result).toMatchObject({
export_directory: null,
exports: [],
coverage: { status: "complete", examined_entries: 0 },
});
expect(result.limitations[0]).toContain("declares no export directory");
});
it("refuses ordinals beyond the 16-bit ordinal space", () => {
expect(() =>
parseNativeExports(
peExportImage({
functions: [{ rva: 0x2000 }],
ordinalBase: 0xffff + 1,
}),
artifact,
),
).toThrow("16-bit ordinals");
});
it("refuses an export table above the inventory allowance", () => {
expect(() =>
parseNativeExports(
peExportImage({ functions: [], declaredFunctions: 0x40001 }),
artifact,
),
).toThrow(BinaryResourceFailure);
});
});
describe("ELF dynamic symbol exports", () => {
const symbols = [
{ name: "plain_fn" },
{ name: "versioned_fn", version: 2 },
{ name: "old_fn", version: 0x8003 },
{ name: "weak_obj", binding: 2, type: 1 },
{ name: "ifunc_sym", type: 10 },
{ name: "protected_fn", visibility: 3 },
{ name: "hidden_fn", visibility: 2 },
{ name: "undefined_fn", section: 0 },
{ name: "local_fn", binding: 0 },
{ name: "local_version_fn", version: 0 },
];
it("lists defined default and protected globals with their versions", () => {
const result = parse(
elfExportImage({
symbols,
versionDefinitions: ["libfixture.so", "VER_1", "VER_0"],
}),
);
expect(result).toMatchObject({
format: "elf",
bits: 64,
byte_order: "little",
architecture: "amd64",
coverage: { status: "complete", examined_entries: 10, exports: 6 },
issues: [],
});
expect(
result.format === "elf" &&
result.exports.map(({ name, type, binding, version }) => ({
name,
type,
binding,
version,
})),
).toEqual([
{ name: "plain_fn", type: "function", binding: "global", version: null },
{
name: "versioned_fn",
type: "function",
binding: "global",
version: { index: 2, name: "VER_1", hidden: false },
},
{
name: "old_fn",
type: "function",
binding: "global",
version: { index: 3, name: "VER_0", hidden: true },
},
{ name: "weak_obj", type: "object", binding: "weak", version: null },
{ name: "ifunc_sym", type: "ifunc", binding: "global", version: null },
{
name: "protected_fn",
type: "function",
binding: "global",
version: null,
},
]);
});
it("reads a big-endian ELF32 table", () => {
const result = parse(
elfExportImage({
bits: 32,
little: false,
machine: 20,
symbols: [{ name: "be_fn", value: 0x10000n, size: 8n }],
}),
);
expect(result).toMatchObject({
bits: 32,
byte_order: "big",
architecture: "ppc",
exports: [{ name: "be_fn", value: "0x10000", size: "8" }],
});
});
it("marks versions unknown when the version table does not fit", () => {
const result = parse(
elfExportImage({
symbols: [{ name: "a" }, { name: "b", version: 2 }],
versionDefinitions: ["lib.so", "V1"],
truncatedVersions: true,
}),
);
expect(
result.format === "elf" && result.exports.map(({ version }) => version),
).toEqual(["unknown", "unknown"]);
expect(result.coverage.status).toBe("partial");
expect(result.issues).toMatchObject([{ code: "version_unresolved" }]);
});
it("reports one issue per undefined version index", () => {
const result = parse(
elfExportImage({
symbols: [
{ name: "a", version: 7 },
{ name: "b", version: 7 },
],
versionDefinitions: ["lib.so"],
}),
);
expect(
result.format === "elf" && result.exports.map(({ version }) => version),
).toEqual([
{ index: 7, name: null, hidden: false },
{ index: 7, name: null, hidden: false },
]);
expect(result.issues).toHaveLength(1);
});
it("counts a symbol whose name is outside the string table", () => {
const result = parse(
elfExportImage({
symbols: [{ name: "kept" }, { name: "lost", nameOffset: 0x7000 }],
}),
);
expect(result.exports.map(({ name }) => name)).toEqual(["kept"]);
expect(result.issues).toMatchObject([{ code: "name_unreadable" }]);
expect(result.coverage.status).toBe("partial");
});
it("separates no dynamic symbols from unread section headers", () => {
expect(
parse(elfExportImage({ symbols: [], dynamicSymbols: false })),
).toMatchObject({ exports: [], coverage: { status: "complete" } });
expect(
parse(elfExportImage({ symbols: [], sectionHeaders: false })),
).toMatchObject({
exports: [],
coverage: {
status: "unsupported",
reason: expect.stringContaining("no section headers"),
},
});
});
});
it("refuses formats other than PE and ELF", () => {
const macho = Buffer.alloc(64);
macho.writeUInt32LE(0xfeedfacf, 0);
expect(() => parseNativeExports(macho, artifact)).toThrow(
BinaryFormatFailure,
);
});
+468
View File
@@ -0,0 +1,468 @@
import {
BinaryFormatFailure,
BinaryReader,
BinaryResourceFailure,
mappedFileOffset,
} from "../metadata/BinaryContainer.js";
import { readElfImage } 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_VERSYM = 0x6fffffff;
const PE_EXPORT_DIRECTORY = 0;
const PE_EXPORT_DIRECTORY_BYTES = 40;
const MAXIMUM_NAME_BYTES = 4096;
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 map = (rva: number, size: number): number =>
mappedFileOffset(
image.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 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)
exports.push({ name, ordinal, ...target });
// A slot whose only name was unreadable is reported through its issue.
else if (!namedFunctions.has(index))
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",
};
/** Version definitions by index, read from `.gnu.version_d`. */
const readVersionDefinitions = (
reader: BinaryReader,
sections: readonly NativeSection[],
issues: NativeExportIssue[],
): Map<number, string> => {
const definitions = new Map<number, string>();
const verdef = sections.find(({ type }) => type === SHT_GNU_VERDEF);
if (verdef === undefined) return definitions;
try {
const strings = sections[verdef.link];
if (strings?.type !== SHT_STRTAB)
throw new BinaryFormatFailure(
"malformed",
"Version definitions do not link to a string table",
);
const names = reader.fileRange(
strings.offset,
strings.size,
"Version strings",
);
const nameBytes = reader.bytes.subarray(
names.offset,
names.offset + names.size,
);
const table = reader.fileRange(
verdef.offset,
verdef.size,
"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);
if (aux < 20 || offset + aux + 8 > table.size)
throw new BinaryFormatFailure(
"malformed",
"Version definition name entry leaves its section",
);
definitions.set(
versionIndex,
readElfString(nameBytes, reader.u32(at + aux)),
);
if (next === 0) break;
offset += next;
}
} catch (cause) {
issues.push({
code: "version_unresolved",
location: hex(verdef.offset),
message: `Version definitions: ${failureMessage(cause)}`,
});
}
return definitions;
};
/** Read defined exported entries of `.dynsym` with their symbol versions. */
const parseElfExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
const image = readElfImage(bytes);
const directory = readNativeDirectoryFromBuffer(bytes, "elf");
const reader = new BinaryReader(bytes, directory.little);
const result = {
artifact,
format: "elf" as const,
architecture: image.architecture,
bits: image.bits,
byte_order: image.byte_order,
};
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 ||
strings?.type !== SHT_STRTAB
)
throw new BinaryFormatFailure(
"malformed",
"ELF dynamic symbol table entry size, length 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) {
if (versym.size === BigInt(count * 2))
versions = reader.table(versym.offset, count, 2, "ELF symbol versions");
else
issues.push({
code: "version_unresolved",
location: hex(versym.offset),
message: `Symbol version table has ${String(versym.size)} bytes for ${String(count)} symbols`,
});
}
const definitions = readVersionDefinitions(reader, sections, issues);
const unresolvedVersions = new Set<number>();
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 = definitions.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)}, is named by no version definition`,
});
}
version = {
index: versionIndex,
name: name === "" ? null : 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;
}
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",
);
};
+5
View File
@@ -95,6 +95,11 @@ export interface BinaryContainer {
readonly byte_order: "little" | "big";
/** Declared PE preferred base, when available. */
readonly image_base?: bigint;
/** PE optional-header data directories, indexed as declared. */
readonly pe_data_directories?: readonly {
readonly virtual_address: number;
readonly size: number;
}[];
/** Mach-O load-command observation, when available. */
readonly macho_chained_fixups?: boolean;
readonly mappings: readonly FileMapping[];
+5
View File
@@ -82,6 +82,10 @@ export const readPeImage = (bytes: Buffer): BinaryContainer => {
"malformed",
"PE data directories extend beyond the optional header",
);
const dataDirectories = Array.from({ length: directories }, (_, index) => {
const entry = optional + directoriesOffset + index * 8;
return { virtual_address: reader.u32(entry), size: reader.u32(entry + 4) };
});
const imageBase = reader.word(optional + (bits === 64 ? 24 : 28), bits);
const count = reader.u16(pe + 6);
const start = reader.table(
@@ -152,6 +156,7 @@ export const readPeImage = (bytes: Buffer): BinaryContainer => {
return {
format: "pe",
image_base: imageBase,
pe_data_directories: dataDirectories,
architecture: names.get(machine) ?? `pe-machine-${machine}`,
bits,
byte_order: "little",
+268
View File
@@ -0,0 +1,268 @@
/** One PE export-address-table slot: code RVA, forwarder text, or unused. */
export type PeFixtureFunction =
| { readonly rva: number }
| { readonly forwarder: string }
| null;
const SECTION_RVA = 0x1000;
const SECTION_OFFSET = 0x400;
const SECTION_BYTES = 0x400;
/**
* Minimal PE32/PE32+ image whose only section holds an export directory at
* RVA 0x1000. Name entries point at `functions` by index.
*/
export const peExportImage = (options: {
readonly bits?: 32 | 64;
readonly ordinalBase?: number;
readonly functions?: readonly PeFixtureFunction[];
readonly names?: readonly { readonly name: string; readonly index: number }[];
/** Replace one name's RVA, e.g. with an unmapped one. */
readonly nameRvaOverride?: {
readonly position: number;
readonly rva: number;
};
readonly exportDirectory?: boolean;
readonly declaredFunctions?: number;
}): Buffer => {
const bits = options.bits ?? 64;
const bytes = Buffer.alloc(SECTION_OFFSET + SECTION_BYTES);
const wide = bits === 64;
bytes.write("MZ", 0, "ascii");
bytes.writeUInt32LE(128, 60);
bytes.write("PE\0\0", 128, "ascii");
bytes.writeUInt16LE(wide ? 0x8664 : 0x14c, 132);
bytes.writeUInt16LE(1, 134);
const directoriesAt = wide ? 112 : 96;
const optionalSize = directoriesAt + 16 * 8;
bytes.writeUInt16LE(optionalSize, 148);
const optional = 152;
bytes.writeUInt16LE(wide ? 0x20b : 0x10b, optional);
if (wide) bytes.writeBigUInt64LE(0x180000000n, optional + 24);
else bytes.writeUInt32LE(0x10000000, optional + 28);
bytes.writeUInt32LE(16, optional + directoriesAt - 4);
const section = optional + optionalSize;
bytes.write(".edata", section, "ascii");
bytes.writeUInt32LE(SECTION_BYTES, section + 8);
bytes.writeUInt32LE(SECTION_RVA, section + 12);
bytes.writeUInt32LE(SECTION_BYTES, section + 16);
bytes.writeUInt32LE(SECTION_OFFSET, section + 20);
bytes.writeUInt32LE(0x40000040, section + 36);
if (options.exportDirectory === false) return bytes;
const at = (rva: number) => rva - SECTION_RVA + SECTION_OFFSET;
const functions = options.functions ?? [];
const names = options.names ?? [];
// Directory, address table, name table, ordinals, then strings; forwarders
// stay inside the 0x200-byte declared directory range.
const eat = SECTION_RVA + 0x40;
const nameTable = SECTION_RVA + 0x80;
const ordinalTable = SECTION_RVA + 0xc0;
let forwarderText = SECTION_RVA + 0x100;
let text = SECTION_RVA + 0x200;
const string = (value: string, inside: boolean): number => {
const rva = inside ? forwarderText : text;
bytes.write(`${value}\0`, at(rva), "latin1");
if (inside) forwarderText += value.length + 1;
else text += value.length + 1;
return rva;
};
bytes.writeUInt32LE(SECTION_RVA, optional + directoriesAt);
bytes.writeUInt32LE(0x200, optional + directoriesAt + 4);
const directory = at(SECTION_RVA);
bytes.writeUInt32LE(0x5f5e0ff, directory + 4);
bytes.writeUInt32LE(string("fixture.dll", false), directory + 12);
bytes.writeUInt32LE(options.ordinalBase ?? 1, directory + 16);
bytes.writeUInt32LE(
options.declaredFunctions ?? functions.length,
directory + 20,
);
bytes.writeUInt32LE(names.length, directory + 24);
bytes.writeUInt32LE(eat, directory + 28);
bytes.writeUInt32LE(nameTable, directory + 32);
bytes.writeUInt32LE(ordinalTable, directory + 36);
functions.forEach((entry, index) => {
const value =
entry === null
? 0
: "rva" in entry
? entry.rva
: string(entry.forwarder, true);
bytes.writeUInt32LE(value, at(eat) + index * 4);
});
names.forEach(({ name, index }, position) => {
const rva =
options.nameRvaOverride?.position === position
? options.nameRvaOverride.rva
: string(name, false);
bytes.writeUInt32LE(rva, at(nameTable) + position * 4);
bytes.writeUInt16LE(index, at(ordinalTable) + position * 2);
});
return bytes;
};
/** One `.dynsym` entry; `version` is its raw `.gnu.version` half-word. */
export interface ElfFixtureSymbol {
readonly name: string;
readonly binding?: number;
readonly type?: number;
readonly visibility?: number;
readonly section?: number;
readonly value?: bigint;
readonly size?: bigint;
readonly version?: number;
/** Replace the symbol's string-table offset. */
readonly nameOffset?: number;
}
/**
* Minimal ELF shared object with `.dynsym`, `.dynstr`, `.gnu.version` and
* `.gnu.version_d`. Version definitions are indexed from 1 in order.
*/
export const elfExportImage = (options: {
readonly bits?: 32 | 64;
readonly little?: boolean;
readonly machine?: number;
readonly symbols: readonly ElfFixtureSymbol[];
readonly versionDefinitions?: readonly string[];
/** Declare a `.gnu.version` table that is one entry short. */
readonly truncatedVersions?: boolean;
readonly sectionHeaders?: boolean;
readonly dynamicSymbols?: boolean;
}): Buffer => {
const bits = options.bits ?? 64;
const little = options.little ?? true;
const wide = bits === 64;
const bytes = Buffer.alloc(0x1000);
const u16 = (offset: number, value: number) =>
little
? bytes.writeUInt16LE(value, offset)
: bytes.writeUInt16BE(value, offset);
const u32 = (offset: number, value: number) =>
little
? bytes.writeUInt32LE(value, offset)
: bytes.writeUInt32BE(value, offset);
const word = (offset: number, value: bigint) => {
if (!wide) u32(offset, Number(value));
else if (little) bytes.writeBigUInt64LE(value, offset);
else bytes.writeBigUInt64BE(value, offset);
};
bytes.set([0x7f, 0x45, 0x4c, 0x46, wide ? 2 : 1, little ? 1 : 2, 1]);
u16(16, 3);
u16(18, options.machine ?? (wide ? 62 : 40));
u32(20, 1);
const headerSize = wide ? 64 : 52;
u16(wide ? 52 : 40, headerSize);
u16(wide ? 54 : 42, wide ? 56 : 32);
u16(wide ? 58 : 46, wide ? 64 : 40);
if (options.sectionHeaders === false) return bytes;
const dynstr = ["\0"];
let dynstrBytes = 1;
const intern = (value: string): number => {
const offset = dynstrBytes;
dynstr.push(`${value}\0`);
dynstrBytes += value.length + 1;
return offset;
};
const definitions = options.versionDefinitions ?? [];
const definitionNames = definitions.map(intern);
const symbolWidth = wide ? 24 : 16;
const symbols = [null, ...options.symbols];
const dynsymAt = 0x200;
symbols.forEach((symbol, index) => {
if (symbol === null) return;
const at = dynsymAt + index * symbolWidth;
const info = ((symbol.binding ?? 1) << 4) | (symbol.type ?? 2);
const other = symbol.visibility ?? 0;
const name = symbol.nameOffset ?? intern(symbol.name);
u32(at, name);
if (wide) {
bytes[at + 4] = info;
bytes[at + 5] = other;
u16(at + 6, symbol.section ?? 1);
word(at + 8, symbol.value ?? BigInt(0x1000 + index * 16));
word(at + 16, symbol.size ?? 16n);
} else {
word(at + 4, symbol.value ?? BigInt(0x1000 + index * 16));
word(at + 8, symbol.size ?? 16n);
bytes[at + 12] = info;
bytes[at + 13] = other;
u16(at + 14, symbol.section ?? 1);
}
});
const dynstrAt = 0x600;
Buffer.from(dynstr.join(""), "latin1").copy(bytes, dynstrAt);
const versymAt = 0x900;
symbols.forEach((symbol, index) =>
u16(versymAt + index * 2, symbol?.version ?? (index === 0 ? 0 : 1)),
);
const verdefAt = 0xa00;
definitions.forEach((_, index) => {
const at = verdefAt + index * 28;
u16(at, 1);
u16(at + 2, index === 0 ? 1 : 0);
u16(at + 4, index + 1);
u16(at + 6, 1);
u32(at + 12, 20);
u32(at + 16, index === definitions.length - 1 ? 0 : 28);
u32(at + 20, definitionNames[index]!);
});
const shstrtab =
"\0.dynsym\0.dynstr\0.gnu.version\0.gnu.version_d\0.shstrtab\0";
const shstrtabAt = 0xc00;
bytes.write(shstrtab, shstrtabAt, "latin1");
const sections: (readonly [
number,
number,
number,
number,
number,
number,
number,
])[] = [
// name, type, offset, size, link, info, entsize
[0, 0, 0, 0, 0, 0, 0],
...(options.dynamicSymbols === false
? []
: ([
[1, 11, dynsymAt, symbols.length * symbolWidth, 2, 1, symbolWidth],
[9, 3, dynstrAt, dynstrBytes, 0, 0, 0],
[
17,
0x6fffffff,
versymAt,
(symbols.length - (options.truncatedVersions === true ? 1 : 0)) * 2,
1,
0,
2,
],
[
30,
0x6ffffffd,
verdefAt,
definitions.length * 28,
2,
definitions.length,
0,
],
] as const)),
[45, 3, shstrtabAt, shstrtab.length, 0, 0, 0],
];
const sectionAt = 0xd00;
const sectionWidth = wide ? 64 : 40;
sections.forEach(([name, type, offset, size, link, info, entsize], index) => {
const at = sectionAt + index * sectionWidth;
u32(at, name);
u32(at + 4, type);
word(at + (wide ? 24 : 16), BigInt(offset));
word(at + (wide ? 32 : 20), BigInt(size));
u32(at + (wide ? 40 : 24), link);
u32(at + (wide ? 44 : 28), info);
word(at + (wide ? 56 : 36), BigInt(entsize));
});
word(wide ? 40 : 32, BigInt(sectionAt));
u16(wide ? 60 : 48, sections.length);
u16(wide ? 62 : 50, sections.length - 1);
return bytes;
};