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
836 lines
29 KiB
TypeScript
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);
|
|
}
|
|
};
|