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.
267 lines
8.7 KiB
TypeScript
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");
|
|
};
|