mirror of
https://github.com/morluto/rea.git
synced 2026-10-28 10:25:42 +00:00
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.
342 lines
10 KiB
TypeScript
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}`;
|
|
};
|