Files
rea/src/go/GoMachoImage.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

267 lines
8.7 KiB
TypeScript

import {
declaredFileRange,
GoBinaryFormatFailure,
GoBinaryReader,
mappedFileOffset,
type GoBinaryContainer,
type GoFileMapping,
type GoImageSection,
type GoZeroFillMapping,
} from "./GoBinaryContainer.js";
/** Read thin Mach-O images, retaining the slice's exact file-backed mappings. */
export const readGoMachoImage = (bytes: Buffer): GoBinaryContainer => {
const magic = bytes.readUInt32BE(0);
const little = magic === 0xcefaedfe || magic === 0xcffaedfe;
const bits = magic === 0xfeedfacf || magic === 0xcffaedfe ? 64 : 32;
const reader = new GoBinaryReader(bytes, little);
const headerSize = bits === 64 ? 32 : 28;
reader.range(0, headerSize, "Mach-O header");
const count = reader.u32(16);
const commandBytes = reader.u32(20);
const commandStart = reader.table(
BigInt(headerSize),
commandBytes,
1,
"Mach-O load commands",
);
if (count > Math.floor(commandBytes / 8))
throw new GoBinaryFormatFailure(
"malformed",
"Mach-O command count exceeds its load-command range",
);
const mappings: GoFileMapping[] = [];
const zeroFills: GoZeroFillMapping[] = [];
const sections: GoImageSection[] = [];
const named: GoFileMapping[] = [];
let fallback: GoFileMapping | null = null;
let offset = commandStart;
const end = commandStart + commandBytes;
for (let index = 0; index < count; index++) {
const command = reader.u32(offset);
const size = reader.u32(offset + 4);
if (size < 8 || size % 4 !== 0 || size > end - offset)
throw new GoBinaryFormatFailure(
"malformed",
"Mach-O load command size is invalid or truncated",
);
if (command === 1 || command === 0x19) {
const wide = command === 0x19;
if (wide !== (bits === 64))
throw new GoBinaryFormatFailure(
"malformed",
"Mach-O segment command does not match the header width",
);
const segmentHeader = wide ? 72 : 56;
const sectionWidth = wide ? 80 : 68;
if (size < segmentHeader)
throw new GoBinaryFormatFailure(
"malformed",
"Mach-O segment header is truncated",
);
const address = reader.word(offset + 24, bits);
const memorySize = reader.word(offset + (wide ? 32 : 28), bits);
const fileOffset = reader.word(offset + (wide ? 40 : 32), bits);
const fileSize = reader.word(offset + (wide ? 48 : 36), bits);
const maxProtection = reader.u32(offset + (wide ? 56 : 40));
const protection = reader.u32(offset + (wide ? 60 : 44));
const sectionCount = reader.u32(offset + (wide ? 64 : 48));
const unmapped = memorySize === 0n && protection === 0;
if (
(!unmapped && fileSize > memorySize) ||
address + memorySize > 1n << BigInt(bits)
)
throw new GoBinaryFormatFailure(
"malformed",
"Mach-O segment addresses or sizes are invalid",
);
if (sectionCount > Math.floor((size - segmentHeader) / sectionWidth))
throw new GoBinaryFormatFailure(
"malformed",
"Mach-O section table exceeds its segment command",
);
const fileRange =
fileSize === 0n
? undefined
: reader.fileRange(fileOffset, fileSize, "Mach-O segment");
const mapping =
unmapped || fileRange === undefined
? undefined
: { ...fileRange, address };
const name = fixedName(
reader.range(offset + 8, 16, "Mach-O segment name"),
);
if (mapping !== undefined && name !== "__PAGEZERO")
mappings.push(mapping);
if (memorySize > fileSize && name !== "__PAGEZERO")
zeroFills.push({
address: address + fileSize,
size: memorySize - fileSize,
});
if (
fallback === null &&
mapping !== undefined &&
address !== 0n &&
protection === 3 &&
maxProtection === 3
)
fallback = mapping;
for (let sectionIndex = 0; sectionIndex < sectionCount; sectionIndex++) {
const section = offset + segmentHeader + sectionIndex * sectionWidth;
const sectionName = fixedName(
reader.range(section, 16, "Mach-O section name"),
);
sections.push(
machoImageSection({
reader,
bits,
wide,
header: section,
name: `${fixedName(reader.range(section + 16, 16, "Mach-O section segment"))},${sectionName}`,
protection,
unmapped: unmapped || mapping === undefined,
segmentAddress: address,
segmentMemorySize: memorySize,
segmentFileOffset: fileOffset,
segmentFileSize: fileSize,
}),
);
if (sectionName !== "__go_buildinfo") continue;
if (unmapped)
throw new GoBinaryFormatFailure(
"malformed",
"Mach-O build-info section is in an unmapped segment",
);
if (mapping === undefined)
throw new GoBinaryFormatFailure(
"malformed",
"Mach-O build-info section is not file mapped",
);
const sectionFlags = reader.u32(section + (wide ? 64 : 56));
if ([1, 0xc, 0x12].includes(sectionFlags & 0xff))
throw new GoBinaryFormatFailure(
"malformed",
"Mach-O build-info section is zero-fill data",
);
const sectionAddress = reader.word(section + 32, bits);
const sectionSize = reader.word(section + (wide ? 40 : 36), bits);
const sectionOffset = BigInt(reader.u32(section + (wide ? 48 : 40)));
const info = {
...reader.fileRange(
sectionOffset,
sectionSize,
"Mach-O build-info section",
),
address: sectionAddress,
};
if (
mappedFileOffset([mapping], sectionAddress, info.size) !== info.offset
)
throw new GoBinaryFormatFailure(
"malformed",
"Mach-O build-info section disagrees with its segment",
);
named.push(info);
}
}
offset += size;
}
if (offset !== end)
throw new GoBinaryFormatFailure(
"malformed",
"Mach-O load-command byte count does not match its commands",
);
if (named.length > 1)
throw new GoBinaryFormatFailure(
"malformed",
"Mach-O has ambiguous build-info sections",
);
const cpu = reader.u32(4);
const names = new Map([
[7, "386"],
[0x01000007, "amd64"],
[12, "arm"],
[0x0100000c, "arm64"],
[18, "ppc"],
[0x01000012, "ppc64"],
]);
return {
format: "macho",
architecture: names.get(cpu) ?? `macho-cpu-${cpu}`,
bits,
byte_order: little ? "little" : "big",
mappings,
zero_fills: zeroFills,
sections,
search: named.length > 0 ? named : fallback === null ? [] : [fallback],
};
};
const machoImageSection = ({
reader,
bits,
wide,
header,
name,
protection,
unmapped,
segmentAddress,
segmentMemorySize,
segmentFileOffset,
segmentFileSize,
}: {
readonly reader: GoBinaryReader;
readonly bits: 32 | 64;
readonly wide: boolean;
readonly header: number;
readonly name: string;
readonly protection: number;
readonly unmapped: boolean;
readonly segmentAddress: bigint;
readonly segmentMemorySize: bigint;
readonly segmentFileOffset: bigint;
readonly segmentFileSize: bigint;
}): GoImageSection => {
const flags = reader.u32(header + (wide ? 64 : 56));
const zeroFill = [1, 0xc, 0x12].includes(flags & 0xff);
const sectionAddress = reader.word(header + 32, bits);
const sectionSize = reader.word(header + (wide ? 40 : 36), bits);
const sectionOffset = BigInt(reader.u32(header + (wide ? 48 : 40)));
const addressDelta = sectionAddress - segmentAddress;
const fileDelta = sectionOffset - segmentFileOffset;
const inSegmentMemory =
addressDelta >= 0n &&
addressDelta <= segmentMemorySize &&
sectionSize <= segmentMemorySize - addressDelta;
const inSegmentFile =
fileDelta >= 0n &&
fileDelta <= segmentFileSize &&
sectionSize <= segmentFileSize - fileDelta;
const range = unmapped
? { offset: 0, size: 0, file_backed: false }
: declaredFileRange(
reader.bytes.length,
BigInt(reader.u32(header + (wide ? 48 : 40))),
reader.word(header + (wide ? 40 : 36), bits),
);
const fileBacked =
range.file_backed && !zeroFill && inSegmentMemory && inSegmentFile;
const readOnly =
fileBacked &&
(protection & 1) !== 0 &&
(protection & 2) === 0 &&
(flags & 0x80000400) === 0;
return {
name,
...range,
file_backed: fileBacked,
read_only: readOnly,
compressed: false,
};
};
const fixedName = (bytes: Buffer): string => {
const nul = bytes.indexOf(0);
return bytes.subarray(0, nul < 0 ? bytes.length : nul).toString("latin1");
};