Files
reverse-engineering-anything/src/go/GoElfImage.ts
T
morluto 55dd21a5e2 fix(native): retain partial format facts and explicit coverage
Preserve XML and Interface Builder identity at their parsing boundary, retain usable Objective-C slots with precise coverage, and keep native trace failures distinct from absence. Add caller-selected Interface Builder summaries backed by complete retained Evidence, and preserve comparison limitations.

Reviewed fixes: #1969, #2088, #2108, #2513, #2639, #2684, #2731, #2759, #2875, #2888, #2889, #2970, #2971, #3002.

Validation: relevant native/domain focused lane passed 142 tests; earlier Interface Builder/MCP checks passed 24 tests. Real native UI and Ghidra producer coverage was not repeated.
2026-10-14 08:29:16 +08:00

342 lines
10 KiB
TypeScript

import {
declaredFileRange,
GoBinaryFormatFailure,
GoBinaryReader,
mappedFileOffset,
type GoBinaryContainer,
type GoFileMapping,
type GoImageSection,
type GoZeroFillMapping,
} from "./GoBinaryContainer.js";
import { readElfFixedHeader } from "../domain/elfFixedHeader.js";
/** Read ELF build-info sections and file-backed program mappings. */
export const readGoElfImage = (bytes: Buffer): GoBinaryContainer => {
const fixedHeader = readElfFixedHeader(bytes);
if (!fixedHeader.ok)
throw new GoBinaryFormatFailure("malformed", fixedHeader.error);
const { elfClass: bits, headerSize } = fixedHeader.value;
const reader = new GoBinaryReader(
bytes,
fixedHeader.value.byteOrder === "little",
);
const wide = bits === 64;
const phOffset = reader.word(wide ? 32 : 28, bits);
const shOffset = reader.word(wide ? 40 : 32, bits);
const phWidth = reader.u16(wide ? 54 : 42);
const shWidth = reader.u16(wide ? 58 : 46);
let phCount = reader.u16(wide ? 56 : 44);
let shCount = reader.u16(wide ? 60 : 48);
let namesIndex = reader.u16(wide ? 62 : 50);
if (
(phCount !== 0 && phOffset < BigInt(headerSize)) ||
(shOffset !== 0n && shOffset < BigInt(headerSize))
)
throw new GoBinaryFormatFailure(
"malformed",
"ELF header table overlaps the native header",
);
if (
shOffset !== 0n &&
(shCount === 0 || phCount === 0xffff || namesIndex === 0xffff)
) {
if (shWidth < (wide ? 64 : 40))
throw new GoBinaryFormatFailure(
"malformed",
"ELF extended section header width is invalid",
);
const first = reader.fileRange(
shOffset,
BigInt(shWidth),
"ELF section zero",
).offset;
if (reader.u32(first + 4) !== 0)
throw new GoBinaryFormatFailure(
"malformed",
"ELF initial section for extended numbering must have SHT_NULL type",
);
if (shCount === 0) {
const count = reader.word(first + (wide ? 32 : 20), bits);
if (count < 0xff00n || count > BigInt(Number.MAX_SAFE_INTEGER))
throw new GoBinaryFormatFailure(
"malformed",
"ELF extended section count is invalid",
);
shCount = Number(count);
}
if (phCount === 0xffff) {
phCount = reader.u32(first + (wide ? 44 : 28));
if (phCount < 0xffff)
throw new GoBinaryFormatFailure(
"malformed",
"ELF extended program count is below its reserved range",
);
}
if (namesIndex === 0xffff) {
namesIndex = reader.u32(first + (wide ? 40 : 24));
if (namesIndex < 0xff00)
throw new GoBinaryFormatFailure(
"malformed",
"ELF extended section name index is below its reserved range",
);
}
} else if (
shOffset === 0n &&
(shCount !== 0 || namesIndex !== 0 || phCount === 0xffff)
) {
throw new GoBinaryFormatFailure(
"malformed",
"ELF section table is missing",
);
}
if (phCount !== 0 && phWidth < (wide ? 56 : 32))
throw new GoBinaryFormatFailure(
"malformed",
"ELF program header width is invalid",
);
const phStart = reader.table(
phOffset,
phCount,
phWidth,
"ELF program headers",
);
const mappings: GoFileMapping[] = [];
const zeroFills: GoZeroFillMapping[] = [];
const sections: GoImageSection[] = [];
const fallback: GoFileMapping[] = [];
for (let index = 0; index < phCount; index++) {
const offset = phStart + index * phWidth;
if (reader.u32(offset) !== 1) continue;
const flags = reader.u32(offset + (wide ? 4 : 24));
const fileOffset = reader.word(offset + (wide ? 8 : 4), bits);
const address = reader.word(offset + (wide ? 16 : 8), bits);
const size = reader.word(offset + (wide ? 32 : 16), bits);
const memorySize = reader.word(offset + (wide ? 40 : 20), bits);
if (size > memorySize || address + memorySize > 1n << BigInt(bits))
throw new GoBinaryFormatFailure(
"malformed",
"ELF load segment sizes or addresses are invalid",
);
const mapping = {
...(size === 0n
? { offset: 0, size: 0 }
: reader.fileRange(fileOffset, size, "ELF load segment")),
address,
};
if (mapping.size !== 0) mappings.push(mapping);
if (memorySize > size)
zeroFills.push({ address: address + size, size: memorySize - size });
if (mapping.size !== 0 && (flags & 3) === 2) fallback.push(mapping);
}
let search: GoFileMapping | null = null;
if (shCount !== 0) {
if (shWidth < (wide ? 64 : 40) || namesIndex >= shCount)
throw new GoBinaryFormatFailure(
"malformed",
"ELF section width or name-table index is invalid",
);
const shStart = reader.table(
shOffset,
shCount,
shWidth,
"ELF section headers",
);
if (namesIndex !== 0) {
const namesHeader = shStart + namesIndex * shWidth;
if (reader.u32(namesHeader + 4) !== 3)
throw new GoBinaryFormatFailure(
"malformed",
"ELF section name table is not a string table",
);
const names = reader.fileRange(
reader.word(namesHeader + (wide ? 24 : 16), bits),
reader.word(namesHeader + (wide ? 32 : 20), bits),
"ELF section names",
);
for (let index = 0; index < shCount; index++) {
const offset = shStart + index * shWidth;
const nameOffset = reader.u32(offset);
if (nameOffset >= names.size)
throw new GoBinaryFormatFailure(
"malformed",
"ELF section name offset is outside its string table",
);
recordElfSection(sections, {
bytes,
names,
nameOffset,
reader,
bits,
wide,
header: offset,
});
// Comparing the complete fixed name avoids quadratic scans through malicious string tables.
const expectedName = Buffer.from(".go.buildinfo\0");
if (
expectedName.length > names.size - nameOffset ||
!bytes
.subarray(
names.offset + nameOffset,
names.offset + nameOffset + expectedName.length,
)
.equals(expectedName)
)
continue;
if (search !== null)
throw new GoBinaryFormatFailure(
"malformed",
"ELF contains ambiguous build-info sections",
);
if (reader.u32(offset + 4) !== 1)
throw new GoBinaryFormatFailure(
"malformed",
"ELF build-info section is not file-backed data",
);
if ((reader.word(offset + 8, bits) & 0x800n) !== 0n)
throw new GoBinaryFormatFailure(
"unsupported",
"Compressed ELF build-info sections are unsupported",
);
const address = reader.word(offset + (wide ? 16 : 12), bits);
search = {
...reader.fileRange(
reader.word(offset + (wide ? 24 : 16), bits),
reader.word(offset + (wide ? 32 : 20), bits),
"ELF build-info section",
),
address,
};
if (
mappedFileOffset(mappings, address, search.size, zeroFills) !==
search.offset
)
throw new GoBinaryFormatFailure(
"malformed",
"ELF build-info section disagrees with its load mapping",
);
}
}
}
const machine = reader.u16(18);
return {
format: "elf",
architecture: elfArchitecture(machine),
bits,
byte_order: reader.little ? "little" : "big",
mappings,
zero_fills: zeroFills,
sections,
search: search === null ? fallback : [search],
};
};
const recordElfSection = (
sections: GoImageSection[],
input: {
readonly bytes: Buffer;
readonly names: { readonly offset: number; readonly size: number };
readonly nameOffset: number;
readonly reader: GoBinaryReader;
readonly bits: 32 | 64;
readonly wide: boolean;
readonly header: number;
},
): void => {
const decoded = elfSectionName(input.bytes, input.names, input.nameOffset);
const header = {
reader: input.reader,
bits: input.bits,
wide: input.wide,
header: input.header,
};
switch (decoded.status) {
case "named":
sections.push(elfImageSection({ ...header, name: decoded.name }));
return;
case "truncated": {
// Names past this window stay out of .dep-v0 matching. Read-only
// bytes can still hold the only compiler path.
const section = elfImageSection({ ...header, name: "" });
if (section.read_only) sections.push(section);
return;
}
case "unnamed":
return;
default: {
const unreachable: never = decoded;
return unreachable;
}
}
};
const elfSectionName = (
bytes: Buffer,
names: { readonly offset: number; readonly size: number },
nameOffset: number,
):
| { readonly status: "named"; readonly name: string }
| { readonly status: "unnamed" }
| { readonly status: "truncated" } => {
const start = names.offset + nameOffset;
const limit = Math.min(start + 64, names.offset + names.size);
const nul = bytes.subarray(start, limit).indexOf(0);
if (nul === 0) return { status: "unnamed" };
if (nul < 0) return { status: "truncated" };
return {
status: "named",
name: bytes.toString("latin1", start, start + nul),
};
};
const elfImageSection = ({
reader,
bits,
wide,
header,
name,
}: {
readonly reader: GoBinaryReader;
readonly bits: 32 | 64;
readonly wide: boolean;
readonly header: number;
readonly name: string;
}): GoImageSection => {
const type = reader.u32(header + 4);
const flags = reader.word(header + 8, bits);
const declared = declaredFileRange(
reader.bytes.length,
reader.word(header + (wide ? 24 : 16), bits),
reader.word(header + (wide ? 32 : 20), bits),
);
// SHT_NOBITS offsets and sizes do not describe stored bytes.
const range =
type === SHT_NOBITS ? { ...declared, file_backed: false } : declared;
const compressed = (flags & 0x800n) !== 0n;
const readOnly =
type === 1 &&
range.file_backed &&
!compressed &&
(flags & 1n) === 0n &&
(flags & 4n) === 0n &&
(flags & 2n) !== 0n;
return { name, ...range, read_only: readOnly, compressed };
};
const SHT_NOBITS = 8;
const elfArchitecture = (machine: number): string => {
const names = new Map([
[3, "386"],
[8, "mips"],
[20, "ppc"],
[21, "ppc64"],
[22, "s390"],
[40, "arm"],
[62, "amd64"],
[183, "arm64"],
[243, "riscv"],
[258, "loongarch"],
]);
return names.get(machine) ?? `elf-machine-${machine}`;
};