mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
9fd78a74f8
Closes #6792. ELF images with `PT_DYNAMIC` but no `SHT_DYNSYM` section incorrectly reported a complete, empty export inventory. The shared ELF directory reader now owns the dynamic-segment observation; CLI/MCP report `unsupported` with a reason. Existing symbol-table results remain intact. Read-only format correction; no new execution authority or public schema. Validation on the current main integration: - `npm run check`, test discovery, and 166 selected Linux tests pass, including actual CLI/stdio MCP cases and PE/IL2CPP/GPU/Rust reader consumers. Original author validation: 113 tests on macOS, including regressions that failed before the fix; test discovery passed. - Real Debian libc (1,926,232 bytes) and libLLVM 14 (109,967,296 bytes), with only section-header-count/name-index bytes changed, reproduce the old false absence in both public adapters. The reviewed implementation returns unsupported in both adapters. Independent Python maps unchanged `PT_DYNAMIC` declarations and selected symbol bytes; original `readelf` agrees. The reduced files are not loaded or executed; `readelf -D` itself cannot enumerate these reduced layouts, and that failed oracle attempt is retained. - Unmodified libc still returns all 3,025 exports, byte-identical complete Evidence and the same Evidence ID as the baseline. Resource-guarded public probes completed without stops or payload leftovers. No speed, memory or model-token improvement is claimed for this coverage fix. Codex review completed with no findings. CI failed: all 61 observed failures (including 47 omitted from the aggregate report by canceled shards) reproduce with identical complete failure diagnostics on the exact CI base and merge trees. Baseline-only bypass is explicitly authorized for this maintenance session; canceled/skipped coverage remains unverified. See the final CI attribution comment. The original author's readiness declaration below is preserved. ## Author readiness Human owner: @leeguooooo - [x] I reviewed the final diff and relevant existing code, understand the change and its edge cases, and take responsibility for the complete PR, including AI-assisted work. - [x] I checked the evidence above, confirmed it supports the current change, and recorded meaningful validation gaps. - [x] I declare this PR ready for initial maintainer review.
486 lines
16 KiB
TypeScript
486 lines
16 KiB
TypeScript
/** 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;
|
|
/** 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
|
|
* 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;
|
|
/** Store the export directory and its tables in the PE headers. */
|
|
readonly inHeaders?: boolean;
|
|
}): 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);
|
|
bytes.writeUInt32LE(HEADER_BYTES, optional + 60);
|
|
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 < HEADER_BYTES ? rva : rva - SECTION_RVA + SECTION_OFFSET;
|
|
const base = options.inHeaders === true ? 0x200 : SECTION_RVA;
|
|
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 = base + 0x40;
|
|
const nameTable = base + 0x80;
|
|
const ordinalTable = base + 0xc0;
|
|
let forwarderText = base + 0x100;
|
|
let text = base + (options.inHeaders === true ? 0x180 : 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(base, optional + directoriesAt);
|
|
bytes.writeUInt32LE(0x200, optional + directoriesAt + 4);
|
|
const directory = at(base);
|
|
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;
|
|
}
|
|
|
|
export interface ElfFixtureVersionRequirement {
|
|
readonly index: number;
|
|
readonly name: string;
|
|
}
|
|
|
|
const elfStringHash = (value: string): number => {
|
|
let hash = 0;
|
|
for (const character of value) {
|
|
hash = (hash << 4) + character.charCodeAt(0);
|
|
const high = hash & 0xf0000000;
|
|
if (high !== 0) hash ^= high >>> 24;
|
|
hash &= ~high;
|
|
}
|
|
return hash >>> 0;
|
|
};
|
|
|
|
const writeVersionRequirementTable = (input: {
|
|
readonly offset: number;
|
|
readonly requirements: readonly ElfFixtureVersionRequirement[];
|
|
readonly nameOffsets: readonly number[];
|
|
readonly fileOffset: number;
|
|
readonly writeU16: (offset: number, value: number) => void;
|
|
readonly writeU32: (offset: number, value: number) => void;
|
|
}): void => {
|
|
const { offset, requirements, nameOffsets, fileOffset, writeU16, writeU32 } =
|
|
input;
|
|
if (requirements.length === 0) return;
|
|
writeU16(offset, 1);
|
|
writeU16(offset + 2, requirements.length);
|
|
writeU32(offset + 4, fileOffset);
|
|
writeU32(offset + 8, 16);
|
|
requirements.forEach((requirement, index) => {
|
|
const at = offset + 16 + index * 16;
|
|
writeU32(at, elfStringHash(requirement.name));
|
|
writeU16(at + 6, requirement.index);
|
|
writeU32(at + 8, nameOffsets[index]!);
|
|
writeU32(at + 12, index + 1 < requirements.length ? 16 : 0);
|
|
});
|
|
};
|
|
|
|
const writeVersionDefinitionTable = (input: {
|
|
readonly offset: number;
|
|
readonly definitions: readonly string[];
|
|
readonly nameOffsets: readonly number[];
|
|
readonly truncated: boolean;
|
|
readonly writeU16: (offset: number, value: number) => void;
|
|
readonly writeU32: (offset: number, value: number) => void;
|
|
}): void => {
|
|
const { offset, definitions, nameOffsets, truncated, writeU16, writeU32 } =
|
|
input;
|
|
definitions.forEach((_, index) => {
|
|
const at = offset + index * 28;
|
|
writeU16(at, 1);
|
|
writeU16(at + 2, index === 0 ? 1 : 0);
|
|
writeU16(at + 4, index + 1);
|
|
writeU16(at + 6, 1);
|
|
writeU32(at + 12, 20);
|
|
writeU32(
|
|
at + 16,
|
|
truncated && index === 0 ? 0 : index + 1 === definitions.length ? 0 : 28,
|
|
);
|
|
writeU32(at + 20, nameOffsets[index]!);
|
|
});
|
|
};
|
|
|
|
const writeElfSymbol = (input: {
|
|
readonly bytes: Buffer;
|
|
readonly symbol: ElfFixtureSymbol;
|
|
readonly index: number;
|
|
readonly at: number;
|
|
readonly wide: boolean;
|
|
readonly nameOffset: number;
|
|
readonly writeU16: (offset: number, value: number) => void;
|
|
readonly writeU32: (offset: number, value: number) => void;
|
|
readonly writeWord: (offset: number, value: bigint) => void;
|
|
}): void => {
|
|
const {
|
|
bytes,
|
|
symbol,
|
|
index,
|
|
at,
|
|
wide,
|
|
nameOffset,
|
|
writeU16,
|
|
writeU32,
|
|
writeWord,
|
|
} = input;
|
|
const info = ((symbol.binding ?? 1) << 4) | (symbol.type ?? 2);
|
|
const value = symbol.value ?? BigInt(0x1000 + index * 16);
|
|
writeU32(at, nameOffset);
|
|
if (wide) {
|
|
bytes[at + 4] = info;
|
|
bytes[at + 5] = symbol.visibility ?? 0;
|
|
writeU16(at + 6, symbol.section ?? 1);
|
|
writeWord(at + 8, value);
|
|
writeWord(at + 16, symbol.size ?? 16n);
|
|
} else {
|
|
writeWord(at + 4, value);
|
|
writeWord(at + 8, symbol.size ?? 16n);
|
|
bytes[at + 12] = info;
|
|
bytes[at + 13] = symbol.visibility ?? 0;
|
|
writeU16(at + 14, symbol.section ?? 1);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Minimal ELF image with `.dynsym`, `.dynstr`, `.gnu.version`, definitions and
|
|
* requirements, laid out in order.
|
|
*/
|
|
export const elfExportImage = (options: {
|
|
readonly bits?: 32 | 64;
|
|
readonly little?: boolean;
|
|
readonly machine?: number;
|
|
readonly symbols: readonly ElfFixtureSymbol[];
|
|
readonly versionDefinitions?: readonly string[];
|
|
readonly versionRequirements?: readonly ElfFixtureVersionRequirement[];
|
|
/** End the definition chain before its declared record count. */
|
|
readonly truncatedVersionDefinitions?: boolean;
|
|
/** Declare a `.gnu.version` table that is one entry short. */
|
|
readonly truncatedVersions?: boolean;
|
|
/** Shorten the GNU version-requirement section by one byte. */
|
|
readonly truncatedVersionRequirements?: 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 | "null-only";
|
|
readonly dynamicSegment?: boolean;
|
|
readonly dynamicSymbols?: boolean;
|
|
}): Buffer => {
|
|
const bits = options.bits ?? 64;
|
|
const little = options.little ?? true;
|
|
const wide = bits === 64;
|
|
const symbolWidth = wide ? 24 : 16;
|
|
const sectionWidth = wide ? 64 : 40;
|
|
const symbols = [null, ...options.symbols];
|
|
const definitions = options.versionDefinitions ?? [];
|
|
const requirements = options.versionRequirements ?? [];
|
|
|
|
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 requirementNames = requirements.map(({ name }) => intern(name));
|
|
const requirementFile = requirements.length === 0 ? 0 : intern("libc.so.6");
|
|
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.gnu.version_r\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 verneedAt = place(
|
|
requirements.length === 0 ? 0 : 16 + requirements.length * 16,
|
|
);
|
|
const shstrtabAt = place(shstrtab.length);
|
|
const sectionAt = place(7 * sectionWidth);
|
|
const bytes = Buffer.alloc(cursor);
|
|
|
|
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);
|
|
u16(wide ? 52 : 40, wide ? 64 : 52);
|
|
u16(wide ? 54 : 42, wide ? 56 : 32);
|
|
u16(wide ? 58 : 46, sectionWidth);
|
|
if (options.dynamicSegment === true)
|
|
writeElfDynamicSegment(bytes, bits, little);
|
|
if (options.sectionHeaders === false) return bytes;
|
|
|
|
symbols.forEach((symbol, index) => {
|
|
if (symbol !== null)
|
|
writeElfSymbol({
|
|
bytes,
|
|
symbol,
|
|
index,
|
|
at: dynsymAt + index * symbolWidth,
|
|
wide,
|
|
nameOffset: symbolNames[index]!,
|
|
writeU16: u16,
|
|
writeU32: u32,
|
|
writeWord: word,
|
|
});
|
|
});
|
|
Buffer.from(dynstr.join(""), "latin1").copy(bytes, dynstrAt);
|
|
symbols.forEach((symbol, index) =>
|
|
u16(versymAt + index * 2, symbol?.version ?? (index === 0 ? 0 : 1)),
|
|
);
|
|
writeVersionDefinitionTable({
|
|
offset: verdefAt,
|
|
definitions,
|
|
nameOffsets: definitionNames,
|
|
truncated: options.truncatedVersionDefinitions === true,
|
|
writeU16: u16,
|
|
writeU32: u32,
|
|
});
|
|
writeVersionRequirementTable({
|
|
offset: verneedAt,
|
|
requirements,
|
|
nameOffsets: requirementNames,
|
|
fileOffset: requirementFile,
|
|
writeU16: u16,
|
|
writeU32: u32,
|
|
});
|
|
bytes.write(shstrtab, shstrtabAt, "latin1");
|
|
const sections = elfSectionRows(options, {
|
|
dynsymAt,
|
|
dynsym: symbols.length * symbolWidth,
|
|
symbolWidth,
|
|
dynstrAt,
|
|
dynstrBytes,
|
|
versymAt,
|
|
versym: symbols.length * 2,
|
|
verdefAt,
|
|
definitions: definitions.length,
|
|
verneedAt,
|
|
requirements: requirements.length,
|
|
shstrtabAt,
|
|
shstrtab: shstrtab.length,
|
|
end: cursor,
|
|
});
|
|
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));
|
|
const nullOnly = options.sectionHeaders === "null-only";
|
|
u16(wide ? 60 : 48, nullOnly ? 1 : sections.length);
|
|
u16(wide ? 62 : 50, nullOnly ? 0 : sections.length - 1);
|
|
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 verneedAt: number;
|
|
readonly requirements: 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,
|
|
],
|
|
[
|
|
45,
|
|
0x6ffffffe,
|
|
layout.verneedAt,
|
|
(layout.requirements === 0 ? 0 : 16 + layout.requirements * 16) -
|
|
(options.truncatedVersionRequirements === true ? 1 : 0),
|
|
2,
|
|
layout.requirements === 0 ? 0 : 1,
|
|
0,
|
|
],
|
|
] satisfies ElfSectionRow[])),
|
|
[60, 3, layout.shstrtabAt, layout.shstrtab, 0, 0, 0],
|
|
];
|
|
|
|
/** Two program headers: an empty load segment followed by a dynamic segment. */
|
|
const writeElfDynamicSegment = (
|
|
bytes: Buffer,
|
|
bits: 32 | 64,
|
|
little: boolean,
|
|
): void => {
|
|
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
const wide = bits === 64;
|
|
const programAt = wide ? 64 : 52;
|
|
if (wide) view.setBigUint64(32, BigInt(programAt), little);
|
|
else view.setUint32(28, programAt, little);
|
|
view.setUint16(wide ? 56 : 44, 2, little);
|
|
view.setUint32(programAt, 1, little);
|
|
view.setUint32(programAt + (wide ? 56 : 32), 2, little);
|
|
};
|