Merge branch 'feat/4057-native-export-inventory' into feat/4057-verify-native-exports

This commit is contained in:
Kaoru0822 committed 2026-10-22 08:10:02 +00:00
commit 198aa6e4aa
7 files changed
+295 -102

No files matched your search

+10 -6
View File
@@ -334,13 +334,17 @@ count and SHA-256.
`"unknown"` when the version table cannot be read. A hidden version is not `"unknown"` when the version table cannot be read. A hidden version is not
the default version that a by-name `dlsym` lookup selects. the default version that a by-name `dlsym` lookup selects.
Unreadable names, out-of-range name ordinals, unreadable forwarders and Unreadable names, out-of-range name ordinals and unreadable forwarders are
unresolved version indexes are listed in `issues`, the affected entries are listed in `issues`, their entries are not reported, and coverage is `partial`.
not reported, and coverage is `partial`. An ELF without section headers is Unresolved symbol versions keep their rows: the version name is null for an
`unsupported`: enumerating dynamic symbols from the dynamic segment is not index no definition names, and `version` is `"unknown"` when the version table
cannot be read; coverage is likewise `partial`. A dynamic symbol table without
its null entry is malformed. An ELF without section headers is `unsupported`:
enumerating dynamic symbols from the dynamic segment is not
implemented. Mach-O exports are reported by `inspect_macho`. implemented. Mach-O exports are reported by `inspect_macho`.
`max_file_bytes` defaults to 256 MiB (maximum 512 MiB; CLI `--max-file-bytes`). `max_file_bytes` defaults to 256 MiB (maximum 512 MiB; CLI `--max-file-bytes`).
An export or dynamic symbol table above 262144 entries fails without partial Export directories in PE headers are read like those in sections. An export or
success. Export rows do not establish that a symbol is reachable, safe to call dynamic symbol table above 262144 entries, more than 262144 rows, or more than
16 MiB of retained names fails without partial success. Export rows do not establish that a symbol is reachable, safe to call
or loaded at runtime. or loaded at runtime.
@@ -282,6 +282,82 @@ describe("ELF dynamic symbol exports", () => {
}); });
}); });
describe("export inventory review regressions", () => {
it("reads an export directory stored in the PE headers", () => {
const result = parse(
peExportImage({
inHeaders: true,
functions: [{ rva: 0x2000 }, { forwarder: "OTHER.Target" }],
names: [
{ name: "HeaderExport", index: 0 },
{ name: "HeaderForward", index: 1 },
],
}),
);
expect(result).toMatchObject({
export_directory: { rva: 0x200, dll_name: "fixture.dll" },
coverage: { status: "complete", exports: 2 },
});
expect(result.exports.map(({ name }) => name)).toEqual([
"HeaderExport",
"HeaderForward",
]);
});
it("bounds retained name text before storing rows", () => {
// 300 rows naming one 60000-byte string retain about 18 MB of text.
const long = "n".repeat(60000);
const bytes = elfExportImage({
symbols: Array.from({ length: 300 }, () => ({ name: long })),
});
expect(bytes.length).toBeLessThan(100_000);
expect(() => parseNativeExports(bytes, artifact)).toThrow(
BinaryResourceFailure,
);
});
it("keeps exports when the version table lies outside the file", () => {
const result = parse(
elfExportImage({
symbols: [{ name: "a" }, { name: "b", version: 2 }],
versionDefinitions: ["lib.so", "V1"],
versionsOutsideFile: true,
}),
);
expect(
result.format === "elf" && result.exports.map(({ version }) => version),
).toEqual(["unknown", "unknown"]);
expect(result.coverage.status).toBe("partial");
});
it("reports an empty version name as unresolved", () => {
const result = parse(
elfExportImage({
symbols: [{ name: "a", version: 2 }],
versionDefinitions: ["lib.so", ""],
}),
);
expect(
result.format === "elf" && result.exports.map(({ version }) => version),
).toEqual([{ index: 2, name: null, hidden: false }]);
expect(result.issues).toMatchObject([{ code: "version_unresolved" }]);
expect(result.coverage.status).toBe("partial");
});
it("rejects a dynamic symbol table without its null entry", () => {
expect(() =>
parseNativeExports(
elfExportImage({ symbols: [], emptyDynamicSymbols: true }),
artifact,
),
).toThrow(BinaryFormatFailure);
});
});
it("refuses formats other than PE and ELF", () => { it("refuses formats other than PE and ELF", () => {
const macho = Buffer.alloc(64); const macho = Buffer.alloc(64);
macho.writeUInt32LE(0xfeedfacf, 0); macho.writeUInt32LE(0xfeedfacf, 0);
+54 -20
View File
@@ -4,7 +4,7 @@ import {
BinaryResourceFailure, BinaryResourceFailure,
mappedFileOffset, mappedFileOffset,
} from "../metadata/BinaryContainer.js"; } from "../metadata/BinaryContainer.js";
import { readElfImage } from "../metadata/ElfImage.js"; import { architectureForElfMachine } from "../metadata/ElfImage.js";
import { readElfString, readElfSymbol } from "../metadata/ElfSymbol.js"; import { readElfString, readElfSymbol } from "../metadata/ElfSymbol.js";
import { import {
readNativeDirectoryFromBuffer, readNativeDirectoryFromBuffer,
@@ -28,6 +28,25 @@ const SHT_GNU_VERSYM = 0x6fffffff;
const PE_EXPORT_DIRECTORY = 0; const PE_EXPORT_DIRECTORY = 0;
const PE_EXPORT_DIRECTORY_BYTES = 40; const PE_EXPORT_DIRECTORY_BYTES = 40;
const MAXIMUM_NAME_BYTES = 4096; 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 hex = (value: bigint | number) => `0x${value.toString(16)}`;
@@ -88,13 +107,12 @@ const parsePeExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
], ],
}; };
const reader = new BinaryReader(bytes, true); 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 => const map = (rva: number, size: number): number =>
mappedFileOffset( mappedFileOffset(mappings, base + BigInt(rva), size, image.zero_fills);
image.mappings,
base + BigInt(rva),
size,
image.zero_fills,
);
const cString = (rva: number): string => { const cString = (rva: number): string => {
const start = map(rva, 1); const start = map(rva, 1);
const end = bytes const end = bytes
@@ -167,6 +185,7 @@ const parsePeExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
} }
const forwarderEnd = directory.virtual_address + directory.size; const forwarderEnd = directory.virtual_address + directory.size;
const retain = createRetention();
const exports: PeExport[] = []; const exports: PeExport[] = [];
for (let index = 0; index < functionCount; index++) { for (let index = 0; index < functionCount; index++) {
const rva = reader.u32(functions + index * 4); const rva = reader.u32(functions + index * 4);
@@ -188,11 +207,15 @@ const parsePeExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
} else target = { address: hex(base + BigInt(rva)), forwarder: null }; } else target = { address: hex(base + BigInt(rva)), forwarder: null };
const exportNames = namesByFunction.get(index); const exportNames = namesByFunction.get(index);
if (exportNames !== undefined) if (exportNames !== undefined)
for (const name of exportNames) for (const name of exportNames) {
retain(name, target.forwarder);
exports.push({ name, ordinal, ...target }); exports.push({ name, ordinal, ...target });
}
// A slot whose only name was unreadable is reported through its issue. // A slot whose only name was unreadable is reported through its issue.
else if (!namedFunctions.has(index)) else if (!namedFunctions.has(index)) {
retain(target.forwarder);
exports.push({ name: null, ordinal, ...target }); exports.push({ name: null, ordinal, ...target });
}
} }
return { return {
...result, ...result,
@@ -293,15 +316,14 @@ const readVersionDefinitions = (
/** Read defined exported entries of `.dynsym` with their symbol versions. */ /** Read defined exported entries of `.dynsym` with their symbol versions. */
const parseElfExports = (bytes: Buffer, artifact: Artifact): NativeExports => { const parseElfExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
const image = readElfImage(bytes);
const directory = readNativeDirectoryFromBuffer(bytes, "elf"); const directory = readNativeDirectoryFromBuffer(bytes, "elf");
const reader = new BinaryReader(bytes, directory.little); const reader = new BinaryReader(bytes, directory.little);
const result = { const result = {
artifact, artifact,
format: "elf" as const, format: "elf" as const,
architecture: image.architecture, architecture: architectureForElfMachine(directory.machine),
bits: image.bits, bits: directory.bits,
byte_order: image.byte_order, byte_order: directory.little ? ("little" as const) : ("big" as const),
}; };
const { sections } = directory; const { sections } = directory;
if (sections.length === 0) if (sections.length === 0)
@@ -335,11 +357,13 @@ const parseElfExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
if ( if (
dynsym.entry_size !== BigInt(width) || dynsym.entry_size !== BigInt(width) ||
dynsym.size % BigInt(width) !== 0n || dynsym.size % BigInt(width) !== 0n ||
// The table must hold at least its mandatory null symbol.
dynsym.size < BigInt(width) ||
strings?.type !== SHT_STRTAB strings?.type !== SHT_STRTAB
) )
throw new BinaryFormatFailure( throw new BinaryFormatFailure(
"malformed", "malformed",
"ELF dynamic symbol table entry size, length or string-table link is invalid", "ELF dynamic symbol table entry size, length, null entry or string-table link is invalid",
); );
const count = Number(dynsym.size / BigInt(width)); const count = Number(dynsym.size / BigInt(width));
rowBudget(count, "ELF dynamic symbol table"); rowBudget(count, "ELF dynamic symbol table");
@@ -364,17 +388,25 @@ const parseElfExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
); );
let versions: number | null = null; let versions: number | null = null;
if (versym !== undefined) { if (versym !== undefined) {
if (versym.size === BigInt(count * 2)) 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"); versions = reader.table(versym.offset, count, 2, "ELF symbol versions");
else } catch (cause) {
// Keep every export; their versions become unknown.
issues.push({ issues.push({
code: "version_unresolved", code: "version_unresolved",
location: hex(versym.offset), location: hex(versym.offset),
message: `Symbol version table has ${String(versym.size)} bytes for ${String(count)} symbols`, message: failureMessage(cause),
}); });
}
} }
const definitions = readVersionDefinitions(reader, sections, issues); const definitions = readVersionDefinitions(reader, sections, issues);
const unresolvedVersions = new Set<number>(); const unresolvedVersions = new Set<number>();
const retain = createRetention();
const exports: ElfExport[] = []; const exports: ElfExport[] = [];
for (let index = 1; index < count; index++) { for (let index = 1; index < count; index++) {
const at = table + index * width; const at = table + index * width;
@@ -399,18 +431,19 @@ const parseElfExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
const versionIndex = raw & 0x7fff; const versionIndex = raw & 0x7fff;
if (versionIndex === 0) continue; if (versionIndex === 0) continue;
if (versionIndex >= 2) { if (versionIndex >= 2) {
const name = definitions.get(versionIndex) ?? null; const defined = definitions.get(versionIndex);
const name = defined === undefined || defined === "" ? null : defined;
if (name === null && !unresolvedVersions.has(versionIndex)) { if (name === null && !unresolvedVersions.has(versionIndex)) {
unresolvedVersions.add(versionIndex); unresolvedVersions.add(versionIndex);
issues.push({ issues.push({
code: "version_unresolved", code: "version_unresolved",
location: hex(symbol.value), location: hex(symbol.value),
message: `Version index ${String(versionIndex)}, first used by symbol ${String(index)}, is named by no version definition`, message: `Version index ${String(versionIndex)}, first used by symbol ${String(index)}, has no named version definition`,
}); });
} }
version = { version = {
index: versionIndex, index: versionIndex,
name: name === "" ? null : name, name,
hidden: (raw & 0x8000) !== 0, hidden: (raw & 0x8000) !== 0,
}; };
} }
@@ -434,6 +467,7 @@ const parseElfExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
}); });
continue; continue;
} }
retain(name, typeof version === "object" ? (version?.name ?? null) : null);
exports.push({ exports.push({
name, name,
value: hex(symbol.value), value: hex(symbol.value),
+5
View File
@@ -95,6 +95,11 @@ export interface BinaryContainer {
readonly byte_order: "little" | "big"; readonly byte_order: "little" | "big";
/** Declared PE preferred base, when available. */ /** Declared PE preferred base, when available. */
readonly image_base?: bigint; readonly image_base?: bigint;
/**
* File-backed PE headers (`SizeOfHeaders`, clipped to the file), which the
* loader maps at the image base. Kept apart from section `mappings`.
*/
readonly pe_header_mapping?: FileMapping;
/** PE optional-header data directories, indexed as declared. */ /** PE optional-header data directories, indexed as declared. */
readonly pe_data_directories?: readonly { readonly pe_data_directories?: readonly {
readonly virtual_address: number; readonly virtual_address: number;
+1 -1
View File
@@ -104,7 +104,7 @@ export const readElfImage = (bytes: Buffer): BinaryContainer => {
}; };
/** Map ELF machine identifiers to stable architecture names. */ /** Map ELF machine identifiers to stable architecture names. */
const architectureForElfMachine = (machine: number): string => { export const architectureForElfMachine = (machine: number): string => {
const names = new Map([ const names = new Map([
[3, "386"], [3, "386"],
[8, "mips"], [8, "mips"],
+6
View File
@@ -87,6 +87,11 @@ export const readPeImage = (bytes: Buffer): BinaryContainer => {
return { virtual_address: reader.u32(entry), size: reader.u32(entry + 4) }; return { virtual_address: reader.u32(entry), size: reader.u32(entry + 4) };
}); });
const imageBase = reader.word(optional + (bits === 64 ? 24 : 28), bits); const imageBase = reader.word(optional + (bits === 64 ? 24 : 28), bits);
const headerMapping: FileMapping = {
address: imageBase,
offset: 0,
size: Math.min(reader.u32(optional + 60), bytes.length),
};
const count = reader.u16(pe + 6); const count = reader.u16(pe + 6);
const start = reader.table( const start = reader.table(
BigInt(optional + optionalSize), BigInt(optional + optionalSize),
@@ -157,6 +162,7 @@ export const readPeImage = (bytes: Buffer): BinaryContainer => {
format: "pe", format: "pe",
image_base: imageBase, image_base: imageBase,
pe_data_directories: dataDirectories, pe_data_directories: dataDirectories,
pe_header_mapping: headerMapping,
architecture: names.get(machine) ?? `pe-machine-${machine}`, architecture: names.get(machine) ?? `pe-machine-${machine}`,
bits, bits,
byte_order: "little", byte_order: "little",
+143 -75
View File
@@ -7,6 +7,8 @@ export type PeFixtureFunction =
const SECTION_RVA = 0x1000; const SECTION_RVA = 0x1000;
const SECTION_OFFSET = 0x400; const SECTION_OFFSET = 0x400;
const SECTION_BYTES = 0x400; const SECTION_BYTES = 0x400;
/** SizeOfHeaders; header RVAs map to the same file offsets. */
const HEADER_BYTES = 0x400;
/** /**
* Minimal PE32/PE32+ image whose only section holds an export directory at * Minimal PE32/PE32+ image whose only section holds an export directory at
@@ -24,6 +26,8 @@ export const peExportImage = (options: {
}; };
readonly exportDirectory?: boolean; readonly exportDirectory?: boolean;
readonly declaredFunctions?: number; readonly declaredFunctions?: number;
/** Store the export directory and its tables in the PE headers. */
readonly inHeaders?: boolean;
}): Buffer => { }): Buffer => {
const bits = options.bits ?? 64; const bits = options.bits ?? 64;
const bytes = Buffer.alloc(SECTION_OFFSET + SECTION_BYTES); const bytes = Buffer.alloc(SECTION_OFFSET + SECTION_BYTES);
@@ -41,6 +45,7 @@ export const peExportImage = (options: {
if (wide) bytes.writeBigUInt64LE(0x180000000n, optional + 24); if (wide) bytes.writeBigUInt64LE(0x180000000n, optional + 24);
else bytes.writeUInt32LE(0x10000000, optional + 28); else bytes.writeUInt32LE(0x10000000, optional + 28);
bytes.writeUInt32LE(16, optional + directoriesAt - 4); bytes.writeUInt32LE(16, optional + directoriesAt - 4);
bytes.writeUInt32LE(HEADER_BYTES, optional + 60);
const section = optional + optionalSize; const section = optional + optionalSize;
bytes.write(".edata", section, "ascii"); bytes.write(".edata", section, "ascii");
bytes.writeUInt32LE(SECTION_BYTES, section + 8); bytes.writeUInt32LE(SECTION_BYTES, section + 8);
@@ -50,16 +55,18 @@ export const peExportImage = (options: {
bytes.writeUInt32LE(0x40000040, section + 36); bytes.writeUInt32LE(0x40000040, section + 36);
if (options.exportDirectory === false) return bytes; if (options.exportDirectory === false) return bytes;
const at = (rva: number) => rva - SECTION_RVA + SECTION_OFFSET; const at = (rva: number) =>
rva < HEADER_BYTES ? rva : rva - SECTION_RVA + SECTION_OFFSET;
const base = options.inHeaders === true ? 0x200 : SECTION_RVA;
const functions = options.functions ?? []; const functions = options.functions ?? [];
const names = options.names ?? []; const names = options.names ?? [];
// Directory, address table, name table, ordinals, then strings; forwarders // Directory, address table, name table, ordinals, then strings; forwarders
// stay inside the 0x200-byte declared directory range. // stay inside the 0x200-byte declared directory range.
const eat = SECTION_RVA + 0x40; const eat = base + 0x40;
const nameTable = SECTION_RVA + 0x80; const nameTable = base + 0x80;
const ordinalTable = SECTION_RVA + 0xc0; const ordinalTable = base + 0xc0;
let forwarderText = SECTION_RVA + 0x100; let forwarderText = base + 0x100;
let text = SECTION_RVA + 0x200; let text = base + (options.inHeaders === true ? 0x180 : 0x200);
const string = (value: string, inside: boolean): number => { const string = (value: string, inside: boolean): number => {
const rva = inside ? forwarderText : text; const rva = inside ? forwarderText : text;
bytes.write(`${value}\0`, at(rva), "latin1"); bytes.write(`${value}\0`, at(rva), "latin1");
@@ -67,9 +74,9 @@ export const peExportImage = (options: {
else text += value.length + 1; else text += value.length + 1;
return rva; return rva;
}; };
bytes.writeUInt32LE(SECTION_RVA, optional + directoriesAt); bytes.writeUInt32LE(base, optional + directoriesAt);
bytes.writeUInt32LE(0x200, optional + directoriesAt + 4); bytes.writeUInt32LE(0x200, optional + directoriesAt + 4);
const directory = at(SECTION_RVA); const directory = at(base);
bytes.writeUInt32LE(0x5f5e0ff, directory + 4); bytes.writeUInt32LE(0x5f5e0ff, directory + 4);
bytes.writeUInt32LE(string("fixture.dll", false), directory + 12); bytes.writeUInt32LE(string("fixture.dll", false), directory + 12);
bytes.writeUInt32LE(options.ordinalBase ?? 1, directory + 16); bytes.writeUInt32LE(options.ordinalBase ?? 1, directory + 16);
@@ -117,7 +124,7 @@ export interface ElfFixtureSymbol {
/** /**
* Minimal ELF shared object with `.dynsym`, `.dynstr`, `.gnu.version` and * Minimal ELF shared object with `.dynsym`, `.dynstr`, `.gnu.version` and
* `.gnu.version_d`. Version definitions are indexed from 1 in order. * `.gnu.version_d`, laid out in order. Version definitions are indexed from 1.
*/ */
export const elfExportImage = (options: { export const elfExportImage = (options: {
readonly bits?: 32 | 64; readonly bits?: 32 | 64;
@@ -127,13 +134,53 @@ export const elfExportImage = (options: {
readonly versionDefinitions?: readonly string[]; readonly versionDefinitions?: readonly string[];
/** Declare a `.gnu.version` table that is one entry short. */ /** Declare a `.gnu.version` table that is one entry short. */
readonly truncatedVersions?: boolean; readonly truncatedVersions?: boolean;
/** Point `.gnu.version` past the end of the file. */
readonly versionsOutsideFile?: boolean;
/** Declare a zero-byte `.dynsym`, without its null entry. */
readonly emptyDynamicSymbols?: boolean;
readonly sectionHeaders?: boolean; readonly sectionHeaders?: boolean;
readonly dynamicSymbols?: boolean; readonly dynamicSymbols?: boolean;
}): Buffer => { }): Buffer => {
const bits = options.bits ?? 64; const bits = options.bits ?? 64;
const little = options.little ?? true; const little = options.little ?? true;
const wide = bits === 64; const wide = bits === 64;
const bytes = Buffer.alloc(0x1000); const symbolWidth = wide ? 24 : 16;
const sectionWidth = wide ? 64 : 40;
const symbols = [null, ...options.symbols];
const definitions = options.versionDefinitions ?? [];
const dynstr: string[] = ["\0"];
let dynstrBytes = 1;
const strings = new Map<string, number>();
const intern = (value: string): number => {
const known = strings.get(value);
if (known !== undefined) return known;
const offset = dynstrBytes;
strings.set(value, offset);
dynstr.push(`${value}\0`);
dynstrBytes += value.length + 1;
return offset;
};
const definitionNames = definitions.map(intern);
const symbolNames = symbols.map((symbol) =>
symbol === null ? 0 : (symbol.nameOffset ?? intern(symbol.name)),
);
const shstrtab =
"\0.dynsym\0.dynstr\0.gnu.version\0.gnu.version_d\0.shstrtab\0";
let cursor = 0x100;
const place = (size: number): number => {
const offset = cursor;
cursor = Math.ceil((cursor + size) / 16) * 16;
return offset;
};
const dynsymAt = place(symbols.length * symbolWidth);
const dynstrAt = place(dynstrBytes);
const versymAt = place(symbols.length * 2);
const verdefAt = place(definitions.length * 28);
const shstrtabAt = place(shstrtab.length);
const sectionAt = place(6 * sectionWidth);
const bytes = Buffer.alloc(cursor);
const u16 = (offset: number, value: number) => const u16 = (offset: number, value: number) =>
little little
? bytes.writeUInt16LE(value, offset) ? bytes.writeUInt16LE(value, offset)
@@ -151,53 +198,36 @@ export const elfExportImage = (options: {
u16(16, 3); u16(16, 3);
u16(18, options.machine ?? (wide ? 62 : 40)); u16(18, options.machine ?? (wide ? 62 : 40));
u32(20, 1); u32(20, 1);
const headerSize = wide ? 64 : 52; u16(wide ? 52 : 40, wide ? 64 : 52);
u16(wide ? 52 : 40, headerSize);
u16(wide ? 54 : 42, wide ? 56 : 32); u16(wide ? 54 : 42, wide ? 56 : 32);
u16(wide ? 58 : 46, wide ? 64 : 40); u16(wide ? 58 : 46, sectionWidth);
if (options.sectionHeaders === false) return bytes; 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) => { symbols.forEach((symbol, index) => {
if (symbol === null) return; if (symbol === null) return;
const at = dynsymAt + index * symbolWidth; const at = dynsymAt + index * symbolWidth;
const info = ((symbol.binding ?? 1) << 4) | (symbol.type ?? 2); const info = ((symbol.binding ?? 1) << 4) | (symbol.type ?? 2);
const other = symbol.visibility ?? 0; const other = symbol.visibility ?? 0;
const name = symbol.nameOffset ?? intern(symbol.name); const value = symbol.value ?? BigInt(0x1000 + index * 16);
u32(at, name); u32(at, symbolNames[index]!);
if (wide) { if (wide) {
bytes[at + 4] = info; bytes[at + 4] = info;
bytes[at + 5] = other; bytes[at + 5] = other;
u16(at + 6, symbol.section ?? 1); u16(at + 6, symbol.section ?? 1);
word(at + 8, symbol.value ?? BigInt(0x1000 + index * 16)); word(at + 8, value);
word(at + 16, symbol.size ?? 16n); word(at + 16, symbol.size ?? 16n);
} else { } else {
word(at + 4, symbol.value ?? BigInt(0x1000 + index * 16)); word(at + 4, value);
word(at + 8, symbol.size ?? 16n); word(at + 8, symbol.size ?? 16n);
bytes[at + 12] = info; bytes[at + 12] = info;
bytes[at + 13] = other; bytes[at + 13] = other;
u16(at + 14, symbol.section ?? 1); u16(at + 14, symbol.section ?? 1);
} }
}); });
const dynstrAt = 0x600;
Buffer.from(dynstr.join(""), "latin1").copy(bytes, dynstrAt); Buffer.from(dynstr.join(""), "latin1").copy(bytes, dynstrAt);
const versymAt = 0x900;
symbols.forEach((symbol, index) => symbols.forEach((symbol, index) =>
u16(versymAt + index * 2, symbol?.version ?? (index === 0 ? 0 : 1)), u16(versymAt + index * 2, symbol?.version ?? (index === 0 ? 0 : 1)),
); );
const verdefAt = 0xa00;
definitions.forEach((_, index) => { definitions.forEach((_, index) => {
const at = verdefAt + index * 28; const at = verdefAt + index * 28;
u16(at, 1); u16(at, 1);
@@ -208,49 +238,21 @@ export const elfExportImage = (options: {
u32(at + 16, index === definitions.length - 1 ? 0 : 28); u32(at + 16, index === definitions.length - 1 ? 0 : 28);
u32(at + 20, definitionNames[index]!); 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"); bytes.write(shstrtab, shstrtabAt, "latin1");
const sections: (readonly [ const sections = elfSectionRows(options, {
number, dynsymAt,
number, dynsym: symbols.length * symbolWidth,
number, symbolWidth,
number, dynstrAt,
number, dynstrBytes,
number, versymAt,
number, versym: symbols.length * 2,
])[] = [ verdefAt,
// name, type, offset, size, link, info, entsize definitions: definitions.length,
[0, 0, 0, 0, 0, 0, 0], shstrtabAt,
...(options.dynamicSymbols === false shstrtab: shstrtab.length,
? [] end: cursor,
: ([ });
[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) => { sections.forEach(([name, type, offset, size, link, info, entsize], index) => {
const at = sectionAt + index * sectionWidth; const at = sectionAt + index * sectionWidth;
u32(at, name); u32(at, name);
@@ -266,3 +268,69 @@ export const elfExportImage = (options: {
u16(wide ? 62 : 50, sections.length - 1); u16(wide ? 62 : 50, sections.length - 1);
return bytes; return bytes;
}; };
type ElfSectionRow = readonly [
name: number,
type: number,
offset: number,
size: number,
link: number,
info: number,
entsize: number,
];
/** Section header rows for the fixture layout, with the requested defects. */
const elfSectionRows = (
options: Parameters<typeof elfExportImage>[0],
layout: {
readonly dynsymAt: number;
readonly dynsym: number;
readonly symbolWidth: number;
readonly dynstrAt: number;
readonly dynstrBytes: number;
readonly versymAt: number;
readonly versym: number;
readonly verdefAt: number;
readonly definitions: number;
readonly shstrtabAt: number;
readonly shstrtab: number;
readonly end: number;
},
): readonly ElfSectionRow[] => [
[0, 0, 0, 0, 0, 0, 0],
...(options.dynamicSymbols === false
? []
: ([
[
1,
11,
layout.dynsymAt,
options.emptyDynamicSymbols === true ? 0 : layout.dynsym,
2,
1,
layout.symbolWidth,
],
[9, 3, layout.dynstrAt, layout.dynstrBytes, 0, 0, 0],
[
17,
0x6fffffff,
options.versionsOutsideFile === true
? layout.end + 64
: layout.versymAt,
layout.versym - (options.truncatedVersions === true ? 2 : 0),
1,
0,
2,
],
[
30,
0x6ffffffd,
layout.verdefAt,
layout.definitions * 28,
2,
layout.definitions,
0,
],
] satisfies ElfSectionRow[])),
[45, 3, layout.shstrtabAt, layout.shstrtab, 0, 0, 0],
];