mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(go): accept file-backed unmapped Mach-O debug segments
Validate unmapped segment file ranges without adding virtual mappings or fallback metadata addresses. Keep mapped-segment bounds and reject build-info sections declared in unmapped segments. Validation: real unstripped Go 1.26.5 Darwin arm64 and amd64 artifacts passed CLI/MCP oracle, Evidence, source-range and unchanged-byte checks. Retained malformed-input regressions; removed the weaker synthetic acceptance test. Full cross-platform verifier not run. Fixes #2167.
This commit is contained in:
@@ -115,16 +115,25 @@ try {
|
||||
],
|
||||
{ GOOS: os, GOARCH: arch },
|
||||
);
|
||||
await verify(path, target, oracle);
|
||||
await verify(path, target, oracle, null, "stripped");
|
||||
if (os === "linux" && arch === "amd64") {
|
||||
const sectionless = join(root.path, "linux-amd64-sectionless");
|
||||
const bytes = Buffer.from(await readFile(path));
|
||||
bytes.fill(0, 40, 48); // ELF64 e_shoff.
|
||||
bytes.fill(0, 58, 64); // e_shentsize, e_shnum and e_shstrndx.
|
||||
await writeFile(sectionless, bytes);
|
||||
await verify(sectionless, target, oracle);
|
||||
await verify(sectionless, target, oracle, null, "sectionless");
|
||||
}
|
||||
}
|
||||
for (const target of targets.filter(([os]) => os === "darwin")) {
|
||||
const [os, arch] = target;
|
||||
const path = join(root.path, `${os}-${arch}-unstripped`);
|
||||
await go(["build", "-trimpath", "-buildvcs=false", "-o", path, "."], {
|
||||
GOOS: os,
|
||||
GOARCH: arch,
|
||||
});
|
||||
await verify(path, target, oracle, null, "unstripped");
|
||||
}
|
||||
if (process.platform === "win32")
|
||||
omittedCoverage.push(
|
||||
"Compiler-produced non-UTF-8 argv strings were not verified on Windows; Windows argv cannot preserve arbitrary byte arguments.",
|
||||
@@ -317,7 +326,13 @@ async function inspect(mode, path) {
|
||||
return { evidence, report };
|
||||
}
|
||||
|
||||
async function verify(path, target, oracle, byteExpected = null) {
|
||||
async function verify(
|
||||
path,
|
||||
target,
|
||||
oracle,
|
||||
byteExpected = null,
|
||||
fileKind = "stripped",
|
||||
) {
|
||||
const bytes = await readFile(path);
|
||||
const expected =
|
||||
byteExpected ??
|
||||
@@ -387,12 +402,7 @@ async function verify(path, target, oracle, byteExpected = null) {
|
||||
);
|
||||
verified.push({
|
||||
target: `${target[0]}/${target[1]}`,
|
||||
file:
|
||||
byteExpected === null
|
||||
? path.endsWith("sectionless")
|
||||
? "sectionless"
|
||||
: "stripped"
|
||||
: "byte-valued-strings",
|
||||
file: byteExpected === null ? fileKind : "byte-valued-strings",
|
||||
go_version: expected.GoVersion,
|
||||
...(byteExpected === null
|
||||
? {}
|
||||
|
||||
+26
-9
@@ -63,7 +63,11 @@ export const readGoMachoImage = (bytes: Buffer): GoBinaryContainer => {
|
||||
const maxProtection = reader.u32(offset + (wide ? 56 : 40));
|
||||
const protection = reader.u32(offset + (wide ? 60 : 44));
|
||||
const sectionCount = reader.u32(offset + (wide ? 64 : 48));
|
||||
if (fileSize > memorySize || address + memorySize > 1n << BigInt(bits))
|
||||
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",
|
||||
@@ -73,16 +77,19 @@ export const readGoMachoImage = (bytes: Buffer): GoBinaryContainer => {
|
||||
"malformed",
|
||||
"Mach-O section table exceeds its segment command",
|
||||
);
|
||||
const mapping = {
|
||||
...(fileSize === 0n
|
||||
? { offset: 0, size: 0 }
|
||||
: reader.fileRange(fileOffset, fileSize, "Mach-O segment")),
|
||||
address,
|
||||
};
|
||||
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.size !== 0 && name !== "__PAGEZERO") mappings.push(mapping);
|
||||
if (mapping !== undefined && name !== "__PAGEZERO")
|
||||
mappings.push(mapping);
|
||||
if (memorySize > fileSize && name !== "__PAGEZERO")
|
||||
zeroFills.push({
|
||||
address: address + fileSize,
|
||||
@@ -90,8 +97,8 @@ export const readGoMachoImage = (bytes: Buffer): GoBinaryContainer => {
|
||||
});
|
||||
if (
|
||||
fallback === null &&
|
||||
mapping !== undefined &&
|
||||
address !== 0n &&
|
||||
mapping.size !== 0 &&
|
||||
protection === 3 &&
|
||||
maxProtection === 3
|
||||
)
|
||||
@@ -103,6 +110,16 @@ export const readGoMachoImage = (bytes: Buffer): GoBinaryContainer => {
|
||||
"__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(
|
||||
|
||||
@@ -436,6 +436,59 @@ it("does not treat Mach-O __PAGEZERO as a module string header", () => {
|
||||
expect(() => readGoBinaryImage(fixture.bytes)).toThrowError(/mapped|range/i);
|
||||
});
|
||||
|
||||
it("rejects out-of-file bytes in an unmapped Mach-O segment", () => {
|
||||
const fixture = createGoBinaryFixture({ format: "macho" });
|
||||
addLegacyMapping(fixture.bytes, "macho", {
|
||||
address: 0x20000n,
|
||||
fileOffset: 3072,
|
||||
fileSize: 1025,
|
||||
memorySize: 0,
|
||||
protection: 0,
|
||||
});
|
||||
expect(() => readGoBinaryImage(fixture.bytes)).toThrowError(
|
||||
/outside the file/i,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects oversized Mach-O file contents when initial protection is set", () => {
|
||||
const fixture = createGoBinaryFixture({ format: "macho" });
|
||||
addLegacyMapping(fixture.bytes, "macho", {
|
||||
address: 0x20000n,
|
||||
fileOffset: 3072,
|
||||
fileSize: 512,
|
||||
memorySize: 0,
|
||||
protection: 3,
|
||||
});
|
||||
expect(() => readGoBinaryImage(fixture.bytes)).toThrowError(
|
||||
/addresses or sizes are invalid/i,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects build-info declared inside an unmapped Mach-O segment", () => {
|
||||
const fixture = createGoBinaryFixture({ format: "macho" });
|
||||
const command = 32 + 72 + 80;
|
||||
const size = 72;
|
||||
fixture.bytes.writeUInt32LE(2, 16);
|
||||
fixture.bytes.writeUInt32LE(304, 20);
|
||||
fixture.bytes.writeUInt32LE(0x19, command);
|
||||
fixture.bytes.writeUInt32LE(size + 80, command + 4);
|
||||
fixture.bytes.write("__DWARF", command + 8);
|
||||
fixture.bytes.writeBigUInt64LE(0x10000n, command + 24);
|
||||
fixture.bytes.writeBigUInt64LE(0n, command + 32);
|
||||
fixture.bytes.writeBigUInt64LE(3072n, command + 40);
|
||||
fixture.bytes.writeBigUInt64LE(512n, command + 48);
|
||||
fixture.bytes.writeUInt32LE(3, command + 56);
|
||||
fixture.bytes.writeUInt32LE(0, command + 60);
|
||||
fixture.bytes.writeUInt32LE(1, command + 64);
|
||||
const section = command + size;
|
||||
fixture.bytes.write("__go_buildinfo", section);
|
||||
fixture.bytes.write("__DWARF", section + 16);
|
||||
fixture.bytes.writeBigUInt64LE(0x10000n, section + 32);
|
||||
fixture.bytes.writeBigUInt64LE(0n, section + 40);
|
||||
fixture.bytes.writeUInt32LE(3072, section + 48);
|
||||
expect(() => readGoBinaryImage(fixture.bytes)).toThrowError(/unmapped/i);
|
||||
});
|
||||
|
||||
it.each(["elf", "pe", "macho"] as const)(
|
||||
"refuses file aliases overlapping only part of a zero-filled %s module header",
|
||||
(format) => {
|
||||
@@ -541,11 +594,13 @@ const addLegacyMapping = (
|
||||
fileOffset,
|
||||
fileSize,
|
||||
memorySize,
|
||||
protection = 3,
|
||||
}: {
|
||||
address: bigint;
|
||||
fileOffset: number;
|
||||
fileSize: number;
|
||||
memorySize: number;
|
||||
protection?: number;
|
||||
},
|
||||
{ bits, little }: { bits: 32 | 64; little: boolean } = {
|
||||
bits: 64,
|
||||
@@ -589,7 +644,7 @@ const addLegacyMapping = (
|
||||
word(entry + (wide ? 32 : 28), BigInt(memorySize));
|
||||
word(entry + (wide ? 40 : 32), BigInt(fileOffset));
|
||||
word(entry + (wide ? 48 : 36), BigInt(fileSize));
|
||||
u32(entry + (wide ? 56 : 40), 3);
|
||||
u32(entry + (wide ? 60 : 44), 3);
|
||||
u32(entry + (wide ? 56 : 40), protection);
|
||||
u32(entry + (wide ? 60 : 44), protection);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user