Files
rea/tests/boundary/go/goBinaryImage.test.ts
T
morluto c19c2d6723 fix(native): separate container facts from Go metadata admission
Move ELF, PE and Mach-O container parsing into a shared native owner. Go alone selects and validates Go build-info sections, so unrelated malformed Go metadata no longer blocks Rust dependency or IL2CPP inspection. Remove the superseded Go-specific container representation and migrate all consumers. Adapted from draft PR #5510 without asserting human PR readiness.

Validation: 51 compiled CLI/MCP acceptance tests across Rust, Go and IL2CPP; 186 shared-reader tests; full check:ci passed. Three public synthetic-file probes reproduce the baseline Rust refusal and validate the fixed SDK-advertised MCP output, artifact digests and unchanged bytes. The exact zero-address nonallocated ELF case now succeeds for Rust while Go still reports invalid_format. No target execution or real compiler-artifact qualification was performed; no performance improvement is claimed.

Closes #3398
2026-10-21 11:58:15 +08:00

836 lines
29 KiB
TypeScript

import { expect, it } from "vitest";
import { javaClassFile } from "../../fixtures/javaClassFile.js";
import {
CPU,
fatImage,
machoImage,
} from "../../../src/artifacts/apple/MachoImage.fixture.js";
import { readGoBinaryImage } from "../../../src/go/GoBinaryImage.js";
import { goBuildInfoSearch } from "../../../src/go/GoBuildInfoSearch.js";
import { BinaryResourceFailure } from "../../../src/native/metadata/BinaryContainer.js";
import { readMachoImage } from "../../../src/native/metadata/MachoImage.js";
import { readBinaryContainer } from "../../../src/native/metadata/readBinaryContainer.js";
import {
createGoBinaryFixture,
createGoSplitElfFixture,
createGoOverlappingElfFixture,
GO_MODULE_TEXT,
} from "../../fixtures/go/image.js";
it("preserves exact Mach-O protection masks for Go fallback selection", () => {
const { bytes } = createGoBinaryFixture({
format: "macho",
sectionless: true,
});
const firstSegment = 32;
const secondSegment = 104;
bytes.writeUInt32LE(2, 16);
bytes.writeUInt32LE(144, 20);
bytes.writeUInt32LE(11, firstSegment + 56);
bytes.writeUInt32LE(11, firstSegment + 60);
bytes.writeUInt32LE(0x19, secondSegment);
bytes.writeUInt32LE(72, secondSegment + 4);
bytes.write("__DATA2", secondSegment + 8, "ascii");
bytes.writeBigUInt64LE(0x20000n, secondSegment + 24);
bytes.writeBigUInt64LE(BigInt(bytes.length), secondSegment + 32);
bytes.writeBigUInt64LE(0n, secondSegment + 40);
bytes.writeBigUInt64LE(BigInt(bytes.length), secondSegment + 48);
bytes.writeUInt32LE(3, secondSegment + 56);
bytes.writeUInt32LE(3, secondSegment + 60);
const container = readBinaryContainer(bytes);
expect(
container.load_regions?.map((region) => region.protection_masks),
).toEqual([
{ current: 11, maximum: 11 },
{ current: 3, maximum: 3 },
]);
expect(goBuildInfoSearch(bytes, container)).toMatchObject([
{ address: 0x20000n, offset: 0, size: bytes.length },
]);
expect(readGoBinaryImage(bytes).build_info?.go_version).toBe("go1.26.0");
});
it("bounds cumulative header searches across overlapping ELF file spans", () => {
const bytes = createGoOverlappingElfFixture();
expect(() => readGoBinaryImage(bytes)).toThrow(BinaryResourceFailure);
expect(() => readGoBinaryImage(bytes)).toThrow(/256 MiB.*search/);
});
it("does not charge duplicate ELF search spans repeatedly", () => {
expect(
readGoBinaryImage(createGoOverlappingElfFixture(true)).build_info,
).toBeNull();
});
it("accepts a search exactly at the cumulative byte limit", () => {
const bytes = createGoOverlappingElfFixture();
bytes.writeUInt16LE(256, 56);
expect(readGoBinaryImage(bytes).build_info).toBeNull();
});
it("keeps different virtual alignments distinct when file spans match", () => {
const fixture = createGoBinaryFixture({ sectionless: true });
fixture.bytes.copy(fixture.bytes, 184, 128, 184);
fixture.bytes.writeUInt16LE(2, 56);
fixture.bytes.writeBigUInt64LE(0x200001n, 128 + 16);
expect(readGoBinaryImage(fixture.bytes).build_info?.header_offset).toBe(
fixture.headerOffset,
);
});
it.each([
[32, "little", "inline"],
[32, "little", "pointer"],
[32, "big", "inline"],
[32, "big", "pointer"],
[64, "little", "inline"],
[64, "little", "pointer"],
[64, "big", "inline"],
[64, "big", "pointer"],
] as const)(
"retains later-load ELF metadata without sections (%d %s %s)",
(bits, byteOrder, encoding) => {
const original = createGoSplitElfFixture({ bits, byteOrder, encoding });
const sectionless = createGoSplitElfFixture({
bits,
byteOrder,
encoding,
sectionless: true,
});
const expected = readGoBinaryImage(original.bytes).build_info;
expect(expected).not.toBeNull();
expect(readGoBinaryImage(sectionless.bytes).build_info).toEqual(expected);
},
);
it("continues past an empty file-backed ELF data load", () => {
const fixture = createGoSplitElfFixture({ sectionless: true });
const expected = readGoBinaryImage(fixture.bytes).build_info;
fixture.bytes.writeBigUInt64LE(0n, 128 + 32);
expect(readGoBinaryImage(fixture.bytes).build_info).toEqual(expected);
});
it.each([4, 5, 7])(
"does not search a later non-data ELF load with flags %d",
(flags) => {
const fixture = createGoSplitElfFixture({ sectionless: true });
fixture.bytes.writeUInt32LE(flags, 184 + 4);
expect(readGoBinaryImage(fixture.bytes).build_info).toBeNull();
},
);
it("prefers the named ELF section to an earlier writable-load marker", () => {
const fixture = createGoSplitElfFixture();
const expected = readGoBinaryImage(fixture.bytes).build_info;
fixture.bytes.copy(
fixture.bytes,
512,
fixture.headerOffset,
fixture.headerOffset + 32,
);
expect(readGoBinaryImage(fixture.bytes).build_info).toEqual(expected);
});
it("does not skip malformed earlier metadata to report a later valid record", () => {
const fixture = createGoSplitElfFixture({ sectionless: true });
fixture.bytes.copy(
fixture.bytes,
512,
fixture.headerOffset,
fixture.headerOffset + 32,
);
fixture.bytes[512 + 15] = 0xff;
expect(() => readGoBinaryImage(fixture.bytes)).toThrow(
/unsupported format bit/,
);
});
it.each([
["elf", 32, "little"],
["elf", 32, "big"],
["elf", 64, "little"],
["elf", 64, "big"],
["pe", 32, "little"],
["pe", 64, "little"],
["macho", 32, "little"],
["macho", 32, "big"],
["macho", 64, "little"],
["macho", 64, "big"],
] as const)(
"reads %s %d-bit %s inline build metadata",
(format, bits, byteOrder) => {
const fixture = createGoBinaryFixture({ format, bits, byteOrder });
expect(readGoBinaryImage(fixture.bytes)).toMatchObject({
format,
bits,
byte_order: byteOrder,
build_info: {
header_offset: fixture.headerOffset,
encoding: "inline",
go_version: "go1.26.0",
module_text: GO_MODULE_TEXT,
module_bytes_base64: fixture.moduleBytes.toString("base64"),
version_location: { offset: fixture.versionOffset, bytes: 8 },
module_location: {
offset: fixture.moduleOffset,
bytes: fixture.moduleBytes.length,
},
},
});
},
);
it.each(["elf", "pe", "macho"] as const)(
"follows mapped pointers in legacy %s metadata",
(format) => {
const fixture = createGoBinaryFixture({
format,
encoding: "pointer",
goVersion: "go1.17",
});
expect(readGoBinaryImage(fixture.bytes).build_info).toMatchObject({
encoding: "pointer",
go_version: "go1.17",
module_text: GO_MODULE_TEXT,
version_location: { offset: fixture.versionOffset, bytes: 6 },
});
},
);
it.each(["elf", "macho"] as const)(
"finds sectionless %s metadata in its writable data mapping",
(format) => {
const fixture = createGoBinaryFixture({ format, sectionless: true });
expect(readGoBinaryImage(fixture.bytes).build_info?.go_version).toBe(
"go1.26.0",
);
},
);
it("does not treat a code/overlay decoy or unaligned marker as Go metadata", () => {
const absent = createGoBinaryFixture({ omitBuildInfo: true });
const decoy = createGoBinaryFixture();
decoy.bytes
.subarray(decoy.headerOffset, decoy.headerOffset + 512)
.copy(absent.bytes, 3072);
expect(readGoBinaryImage(absent.bytes).build_info).toBeNull();
const unaligned = createGoBinaryFixture({
sectionless: true,
omitBuildInfo: true,
});
decoy.bytes
.subarray(decoy.headerOffset, decoy.headerOffset + 512)
.copy(unaligned.bytes, 1025);
expect(readGoBinaryImage(unaligned.bytes).build_info).toBeNull();
});
it("refuses mapped pointer lengths outside the file instead of rounding 64-bit addresses", () => {
const fixture = createGoBinaryFixture({ encoding: "pointer" });
fixture.bytes.writeBigUInt64LE(0x20000000000001n, 1536);
expect(() => readGoBinaryImage(fixture.bytes)).toThrowError(
/mapped|address/i,
);
const length = createGoBinaryFixture({ encoding: "pointer" });
length.bytes.writeBigUInt64LE(0xffffffffffffffffn, 1544);
expect(() => readGoBinaryImage(length.bytes)).toThrowError(/limit|length/i);
});
it("refuses overlong or truncated varints before decoding or allocating strings", () => {
const fixture = createGoBinaryFixture();
fixture.bytes.fill(
0xff,
fixture.headerOffset + 32,
fixture.headerOffset + 42,
);
expect(() => readGoBinaryImage(fixture.bytes)).toThrowError(/varint/i);
const truncated = createGoBinaryFixture();
truncated.bytes.writeBigUInt64LE(33n, 256 + 64 * 2 + 32);
truncated.bytes[truncated.headerOffset + 32] = 0x80;
expect(() => readGoBinaryImage(truncated.bytes)).toThrowError(
/truncated|range|varint/i,
);
});
it("refuses invalid module framing and preserves BOMs without replacement decoding", () => {
const fixture = createGoBinaryFixture();
fixture.bytes[fixture.moduleOffset] = 0;
expect(() => readGoBinaryImage(fixture.bytes)).toThrowError(/framing/i);
const bom = createGoBinaryFixture({
goVersion: "\ufeffgo1.26.0",
moduleText: "\ufeffpath\tx\n",
});
expect(readGoBinaryImage(bom.bytes).build_info).toMatchObject({
go_version: "\ufeffgo1.26.0",
module_text: "\ufeffpath\tx\n",
});
});
it("rejects future encodings and invalid pointer widths explicitly", () => {
const flags = createGoBinaryFixture();
flags.bytes[flags.headerOffset + 15] = 0x82;
expect(() => readGoBinaryImage(flags.bytes)).toThrowError(/flags/i);
const width = createGoBinaryFixture({ encoding: "pointer" });
width.bytes[width.headerOffset + 14] = 16;
expect(() => readGoBinaryImage(width.bytes)).toThrowError(/pointer/i);
});
it("preserves non-UTF8 compiler and module bytes without rejecting other observed metadata", () => {
const version = createGoBinaryFixture();
version.bytes[version.versionOffset] = 0xff;
expect(readGoBinaryImage(version.bytes).build_info).toMatchObject({
go_version: null,
go_version_bytes_base64: version.bytes
.subarray(version.versionOffset, version.versionOffset + 8)
.toString("base64"),
module_text: GO_MODULE_TEXT,
});
const module = createGoBinaryFixture({ moduleText: "build\t-tags=x\n" });
module.bytes[module.moduleOffset + 16 + Buffer.byteLength("build\t-tags=")] =
0xff;
expect(readGoBinaryImage(module.bytes).build_info).toMatchObject({
go_version: "go1.26.0",
module_text: null,
module_bytes_base64: module.bytes
.subarray(
module.moduleOffset,
module.moduleOffset + module.moduleBytes.length,
)
.toString("base64"),
});
});
it.each([0, 4, 16])("ignores unused inline pointer width %s", (width) => {
const fixture = createGoBinaryFixture();
fixture.bytes[fixture.headerOffset + 14] = width;
expect(readGoBinaryImage(fixture.bytes).build_info?.go_version).toBe(
"go1.26.0",
);
});
it.each(["pe", "macho"] as const)(
"refuses ambiguous inline %s header mappings",
(format) => {
const fixture = createGoBinaryFixture({ format });
if (format === "pe") {
fixture.bytes.writeUInt16LE(2, 134);
fixture.bytes.copy(fixture.bytes, 304, 264, 304);
fixture.bytes.writeUInt32LE(512, 324);
} else {
fixture.bytes.writeUInt32LE(2, 16);
fixture.bytes.writeUInt32LE(224, 20);
fixture.bytes.writeUInt32LE(0x19, 184);
fixture.bytes.writeUInt32LE(72, 188);
fixture.bytes.write("__ALIAS", 192);
fixture.bytes.writeBigUInt64LE(0x10000n, 208);
fixture.bytes.writeBigUInt64LE(4096n, 216);
fixture.bytes.writeBigUInt64LE(16n, 224);
fixture.bytes.writeBigUInt64LE(4080n, 232);
}
expect(() => readGoBinaryImage(fixture.bytes)).toThrowError(/ambiguous/i);
},
);
it("refuses a contradictory mapping that overlaps only inline string bytes", () => {
const fixture = createGoBinaryFixture({ format: "pe" });
fixture.bytes.writeUInt16LE(2, 134);
fixture.bytes.copy(fixture.bytes, 304, 264, 304);
fixture.bytes.writeUInt32LE(16, 312);
fixture.bytes.writeUInt32LE(fixture.moduleOffset + 32, 316);
fixture.bytes.writeUInt32LE(16, 320);
fixture.bytes.writeUInt32LE(512, 324);
expect(() => readGoBinaryImage(fixture.bytes)).toThrowError(/ambiguous/i);
});
it("ignores the endian flag for pointer-free strings in a big-endian image", () => {
const fixture = createGoBinaryFixture({ byteOrder: "big" });
fixture.bytes[fixture.headerOffset + 15] = 2;
expect(readGoBinaryImage(fixture.bytes).build_info?.go_version).toBe(
"go1.26.0",
);
});
it("distinguishes unsupported containers from malformed recognized images", () => {
expect(() =>
readGoBinaryImage(Buffer.from([0xca, 0xfe, 0xba, 0xbe])),
).toThrowError(/universal|fat/i);
expect(() => readGoBinaryImage(Buffer.from("\x7fELF"))).toThrowError(
/truncated/i,
);
const pe = createGoBinaryFixture({ format: "pe" });
pe.bytes.writeUInt32LE(0xfffffff0, 60);
expect(() => readGoBinaryImage(pe.bytes)).toThrowError(/range|truncated/i);
});
it("reports JVM class files as unsupported images instead of fat Mach-O containers", () => {
expect(() => readGoBinaryImage(javaClassFile())).toThrowError(
/Only ELF, PE, and thin Mach-O native images are supported/,
);
expect(() => readGoBinaryImage(javaClassFile(4000))).toThrowError(
/Only ELF, PE, and thin Mach-O native images are supported/,
);
expect(() => readGoBinaryImage(javaClassFile(4000, true))).toThrowError(
/Only ELF, PE, and thin Mach-O native images are supported/,
);
});
it("keeps the thin-slice failure for confirmed universal Mach-O containers", () => {
const slices = [
{ cpu: CPU.x86_64, bytes: machoImage({ cpu: CPU.x86_64 }) },
{ cpu: CPU.arm64, bytes: machoImage({ cpu: CPU.arm64 }) },
];
for (const universal of [
fatImage(slices),
fatImage(slices, true),
fatImage(slices, false, true),
fatImage(slices, true, true),
]) {
expect(() => readGoBinaryImage(Buffer.from(universal))).toThrowError(
/fat Mach-O containers require selecting a thin slice/,
);
}
});
it("rejects ELF header tables overlapping the native header", () => {
const fixture = createGoBinaryFixture({ sectionless: true });
fixture.bytes.writeBigUInt64LE(0n, 32);
expect(() => readGoBinaryImage(fixture.bytes)).toThrowError(
/header|overlap/i,
);
});
it("rejects contradictory aliases when a metadata address maps to different file bytes", () => {
const fixture = createGoBinaryFixture();
fixture.bytes.writeUInt16LE(2, 56);
fixture.bytes.writeUInt32LE(1, 184);
fixture.bytes.writeUInt32LE(6, 188);
fixture.bytes.writeBigUInt64LE(16n, 192);
fixture.bytes.writeBigUInt64LE(0x10000n, 200);
fixture.bytes.writeBigUInt64LE(4080n, 216);
fixture.bytes.writeBigUInt64LE(4080n, 224);
expect(() => readGoBinaryImage(fixture.bytes)).toThrowError(/ambiguous/i);
});
it.each(["pe", "elf"] as const)(
"ignores a marker extending past the selected %s data boundary",
(format) => {
const fixture = createGoBinaryFixture({ format, omitBuildInfo: true });
if (format === "pe") fixture.bytes.writeUInt32LE(2040, 264 + 8);
else fixture.bytes.writeBigUInt64LE(2040n, 256 + 64 * 2 + 32);
Buffer.from("ff20476f206275696c64696e663a", "hex").copy(
fixture.bytes,
3056,
);
expect(readGoBinaryImage(fixture.bytes).build_info).toBeNull();
},
);
it.each([0n, 3n])(
"rejects reserved extended ELF section counts containing %s",
(count) => {
const fixture = createGoBinaryFixture();
fixture.bytes.writeUInt16LE(0, 60);
fixture.bytes.writeBigUInt64LE(count, 256 + 32);
expect(() => readGoBinaryImage(fixture.bytes)).toThrowError(
/extended|section count/i,
);
},
);
it.each(["names", "programs"] as const)(
"rejects extended ELF %s values below their reserved ranges",
(field) => {
const fixture = createGoBinaryFixture();
fixture.bytes.writeUInt16LE(0xffff, field === "names" ? 62 : 56);
fixture.bytes.writeUInt32LE(1, 256 + (field === "names" ? 40 : 44));
expect(() => readGoBinaryImage(fixture.bytes)).toThrowError(/extended/i);
},
);
it("requires SHT_NULL for the initial section carrying extended ELF numbering", () => {
const bytes = extendedElfFixture();
bytes.writeUInt32LE(1, 4096 + 4);
expect(() => readGoBinaryImage(bytes)).toThrowError(
/initial|section zero|SHT_NULL/i,
);
});
it("reads a valid 65,536-entry ELF section table with an extended name-table index", () => {
const bytes = extendedElfFixture();
expect(readGoBinaryImage(bytes).build_info).toMatchObject({
go_version: "go1.26.0",
module_text: GO_MODULE_TEXT,
});
});
const extendedElfFixture = (): Buffer => {
const fixture = createGoBinaryFixture();
const tableOffset = 4096;
const count = 65536;
const bytes = Buffer.alloc(tableOffset + count * 64);
fixture.bytes.copy(bytes);
bytes.writeBigUInt64LE(BigInt(tableOffset), 40);
bytes.writeUInt16LE(0, 60);
bytes.writeUInt16LE(0xffff, 62);
bytes.writeBigUInt64LE(BigInt(count), tableOffset + 32);
bytes.writeUInt32LE(count - 1, tableOffset + 40);
fixture.bytes.subarray(384, 448).copy(bytes, tableOffset + 64);
fixture.bytes.subarray(320, 384).copy(bytes, tableOffset + (count - 1) * 64);
return bytes;
};
const legacyLayouts = [
["elf", 32, "little"],
["elf", 32, "big"],
["elf", 64, "little"],
["elf", 64, "big"],
["pe", 32, "little"],
["pe", 64, "little"],
["macho", 32, "little"],
["macho", 32, "big"],
["macho", 64, "little"],
["macho", 64, "big"],
] as const;
it.each(legacyLayouts)(
"reads moduleless legacy %s %d-bit %s metadata with a zero-filled module header",
(format, bits, byteOrder) => {
const fixture = zeroFilledModuleFixture(format, bits, byteOrder);
expect(readGoBinaryImage(fixture.bytes).build_info).toMatchObject({
encoding: "pointer",
go_version: "go1.17",
module_text: "",
module_bytes_base64: "",
module_location: { offset: fixture.moduleField, bytes: 0 },
});
},
);
it.each(["elf", "pe", "macho"] as const)(
"recognizes a module header in a %s segment containing only zero-filled memory",
(format) => {
const fixture = zeroFilledModuleFixture(format, 64, "little", true);
expect(readGoBinaryImage(fixture.bytes).build_info).toMatchObject({
go_version: "go1.17",
module_text: "",
module_location: { offset: fixture.moduleField, bytes: 0 },
});
},
);
it.each(["elf", "pe", "macho"] as const)(
"keeps unmapped and partial zero-filled %s module headers invalid",
(format) => {
const fixture = zeroFilledModuleFixture(format, 64, "little");
fixture.bytes.writeBigUInt64LE(
fixture.moduleAddress + 8n,
fixture.moduleField,
);
expect(() => readGoBinaryImage(fixture.bytes)).toThrowError(
/mapped|range/i,
);
fixture.bytes.writeBigUInt64LE(0x30000n, fixture.moduleField);
expect(() => readGoBinaryImage(fixture.bytes)).toThrowError(
/mapped|range/i,
);
},
);
it.each(["elf", "pe", "macho"] as const)(
"keeps compiler headers in zero-filled %s memory invalid",
(format) => {
const fixture = zeroFilledModuleFixture(format, 64, "little");
fixture.bytes.writeBigUInt64LE(
fixture.moduleAddress,
fixture.headerOffset + 16,
);
expect(() => readGoBinaryImage(fixture.bytes)).toThrowError(
/mapped|range/i,
);
},
);
it.each(["elf", "pe", "macho"] as const)(
"keeps nonempty string contents in zero-filled %s memory invalid",
(format) => {
const version = zeroFilledModuleFixture(format, 64, "little");
version.bytes.writeBigUInt64LE(version.moduleAddress, 1536);
expect(() => readGoBinaryImage(version.bytes)).toThrowError(
/mapped|range/i,
);
const module = zeroFilledModuleFixture(format, 64, "little");
module.bytes.writeBigUInt64LE(0x10000n + 1552n, module.moduleField);
module.bytes.writeBigUInt64LE(module.moduleAddress, 1552);
expect(() => readGoBinaryImage(module.bytes)).toThrowError(/mapped|range/i);
},
);
it.each(["elf", "pe", "macho"] as const)(
"rejects zero-filled %s extents overflowing the image address width",
(format) => {
const fixture = zeroFilledModuleFixture(format, 64, "little", true);
if (format === "elf")
fixture.bytes.writeBigUInt64LE(0xfffffffffffffff8n, 184 + 16);
else if (format === "pe")
fixture.bytes.writeBigUInt64LE(0xfffffffffffeffffn, 152 + 24);
else fixture.bytes.writeBigUInt64LE(0xfffffffffffffff8n, 184 + 24);
expect(() => readGoBinaryImage(fixture.bytes)).toThrowError(
/overflow|addresses/i,
);
},
);
it.each(["elf", "pe", "macho"] as const)(
"ignores unused file offsets for %s memory with no file bytes",
(format) => {
const fixture = zeroFilledModuleFixture(format, 64, "little", true);
if (format === "elf")
fixture.bytes.writeBigUInt64LE(0xffffffffffffffffn, 184 + 8);
else if (format === "pe") fixture.bytes.writeUInt32LE(0xffffffff, 304 + 20);
else fixture.bytes.writeBigUInt64LE(0xffffffffffffffffn, 184 + 40);
expect(readGoBinaryImage(fixture.bytes).build_info?.go_version).toBe(
"go1.17",
);
},
);
it("does not treat Mach-O __PAGEZERO as a module string header", () => {
const fixture = zeroFilledModuleFixture("macho", 64, "little", true);
fixture.bytes.fill(0, 184 + 8, 184 + 24);
fixture.bytes.write("__PAGEZERO", 184 + 8);
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("does not mark Mach-O section bytes outside their owning segment as read-only", () => {
const fixture = createGoBinaryFixture({ format: "macho" });
const command = 32;
const section = command + 72;
fixture.bytes.write("__TEXT", command + 8, "ascii");
fixture.bytes.writeBigUInt64LE(1024n, command + 48);
fixture.bytes.writeUInt32LE(1, command + 56);
fixture.bytes.writeUInt32LE(1, command + 60);
fixture.bytes.write("__debug_info", section, "ascii");
fixture.bytes.write("__TEXT", section + 16, "ascii");
fixture.bytes.writeBigUInt64LE(0x10c00n, section + 32);
fixture.bytes.writeBigUInt64LE(128n, section + 40);
fixture.bytes.writeUInt32LE(3072, section + 48);
const image = readMachoImage(fixture.bytes);
expect(image.sections[0]).toMatchObject({
file_backed: false,
read_only: false,
});
});
it.each(["elf", "pe", "macho"] as const)(
"refuses file aliases overlapping only part of a zero-filled %s module header",
(format) => {
const fixture = zeroFilledModuleFixture(format, 64, "little");
addLegacyMapping(fixture.bytes, format, {
address: fixture.moduleAddress + 8n,
fileOffset: 512,
fileSize: 8,
memorySize: 8,
});
expect(() => readGoBinaryImage(fixture.bytes)).toThrowError(/ambiguous/i);
},
);
it.each(["elf", "pe", "macho"] as const)(
"refuses a file alias covering a zero-filled %s module header even when file bytes are zero",
(format) => {
const fixture = zeroFilledModuleFixture(format, 64, "little");
addLegacyMapping(fixture.bytes, format, {
address: fixture.moduleAddress,
fileOffset: 512,
fileSize: 16,
memorySize: 16,
});
expect(() => readGoBinaryImage(fixture.bytes)).toThrowError(/ambiguous/i);
},
);
it("does not infer PE zero-fill from raw padding beyond a smaller virtual size", () => {
const fixture = createGoBinaryFixture({ format: "pe", encoding: "pointer" });
fixture.bytes.writeUInt32LE(1024, 264 + 8);
expect(readGoBinaryImage(fixture.bytes).build_info?.go_version).toBe(
"go1.26.0",
);
});
it.each(["inline", "pointer"] as const)(
"refuses zero-filled aliases overlapping only %s string bytes",
(encoding) => {
const fixture = createGoBinaryFixture({ format: "pe", encoding });
addLegacyMapping(fixture.bytes, "pe", {
address: 0x10000n + BigInt(fixture.moduleOffset + 32),
fileOffset: 0,
fileSize: 0,
memorySize: 8,
});
expect(() => readGoBinaryImage(fixture.bytes)).toThrowError(/ambiguous/i);
},
);
const zeroFilledModuleFixture = (
format: "elf" | "pe" | "macho",
bits: 32 | 64,
byteOrder: "little" | "big",
separateSegment = false,
) => {
const fixture = createGoBinaryFixture({
format,
bits,
byteOrder,
encoding: "pointer",
goVersion: "go1.17",
});
const view = new DataView(
fixture.bytes.buffer,
fixture.bytes.byteOffset,
fixture.bytes.byteLength,
);
const little = byteOrder === "little";
const moduleAddress = separateSegment
? 0x20000n
: format === "pe"
? 0x10c00n
: 0x11000n;
const moduleField = fixture.headerOffset + 16 + bits / 8;
if (bits === 64) view.setBigUint64(moduleField, moduleAddress, little);
else view.setUint32(moduleField, Number(moduleAddress), little);
if (separateSegment) {
addLegacyMapping(
fixture.bytes,
format,
{ address: moduleAddress, fileOffset: 0, fileSize: 0, memorySize: 16 },
{ bits, little },
);
} else if (format === "elf") {
if (bits === 64) view.setBigUint64(168, 4112n, little);
else view.setUint32(148, 4112, little);
} else if (format === "pe") {
view.setUint32(152 + (bits === 64 ? 112 : 96) + 8, 2064, little);
} else if (bits === 64) {
view.setBigUint64(32 + 32, 4112n, little);
} else {
view.setUint32(28 + 28, 4112, little);
}
return { ...fixture, moduleAddress, moduleField };
};
const addLegacyMapping = (
bytes: Buffer,
format: "elf" | "pe" | "macho",
{
address,
fileOffset,
fileSize,
memorySize,
protection = 3,
}: {
address: bigint;
fileOffset: number;
fileSize: number;
memorySize: number;
protection?: number;
},
{ bits, little }: { bits: 32 | 64; little: boolean } = {
bits: 64,
little: true,
},
): void => {
const wide = bits === 64;
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const u32 = (offset: number, value: number) =>
view.setUint32(offset, value, little);
const word = (offset: number, value: bigint) => {
if (wide) view.setBigUint64(offset, value, little);
else u32(offset, Number(value));
};
if (format === "elf") {
view.setUint16(wide ? 56 : 44, 2, little);
const entry = 128 + (wide ? 56 : 32);
u32(entry, 1);
u32(entry + (wide ? 4 : 24), 6);
word(entry + (wide ? 8 : 4), BigInt(fileOffset));
word(entry + (wide ? 16 : 8), address);
word(entry + (wide ? 32 : 16), BigInt(fileSize));
word(entry + (wide ? 40 : 20), BigInt(memorySize));
} else if (format === "pe") {
view.setUint16(134, 2, little);
const entry = 152 + (wide ? 112 : 96) + 40;
u32(entry + 8, memorySize);
u32(entry + 12, Number(address - 0x10000n));
u32(entry + 16, fileSize);
u32(entry + 20, fileOffset);
u32(entry + 36, 0xc0000040);
} else {
const entry = wide ? 32 + 72 + 80 : 28 + 56 + 68;
const size = wide ? 72 : 56;
u32(16, 2);
u32(20, entry + size - (wide ? 32 : 28));
u32(entry, wide ? 0x19 : 1);
u32(entry + 4, size);
bytes.write("__EXTRA", entry + 8);
word(entry + 24, address);
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), protection);
u32(entry + (wide ? 60 : 44), protection);
}
};