fix(go): reuse PE content spans for build metadata search

This commit is contained in:
morluto committed 2026-10-20 19:05:24 +08:00
1 parent cac12ae334
commit 266dd1d958
4 files changed
+125 -1

No files matched your search

+63
View File
@@ -0,0 +1,63 @@
import { expect, it } from "vitest";
import { readGoBinaryImage } from "./GoBinaryImage.js";
import { readGoPeImage } from "./GoPeImage.js";
import {
createGoBinaryFixture,
setGoPeVirtualSize,
} from "../../tests/fixtures/go/image.js";
it.each([32, 64] as const)(
"uses raw-size fallback for PE%d Go build-info and keeps RVA mappings intact",
(bits) => {
for (const encoding of ["inline", "pointer"] as const) {
const fixture = createGoBinaryFixture({ format: "pe", bits, encoding });
const expected = readGoBinaryImage(fixture.bytes).build_info;
expect(expected).not.toBeNull();
setGoPeVirtualSize(fixture.bytes, 0);
const container = readGoPeImage(fixture.bytes);
expect(container.sections[0]).toMatchObject({
name: ".data",
offset: 1024,
size: 2048,
file_backed: true,
});
expect(container.mappings[0]).toMatchObject({
address: 0x10400n,
offset: 1024,
size: 2048,
});
expect(container.search[0]).toMatchObject({
address: 0x10400n,
offset: 1024,
size: 2048,
});
expect(readGoBinaryImage(fixture.bytes).build_info).toEqual(expected);
}
},
);
it.each(["inline", "pointer"] as const)(
"does not search PE raw alignment padding beyond a nonzero VirtualSize (%s)",
(encoding) => {
const fixture = createGoBinaryFixture({ format: "pe", bits: 64, encoding });
// Put an otherwise valid, aligned record in the raw-only tail.
fixture.bytes.copy(
fixture.bytes,
fixture.headerOffset + 512,
fixture.headerOffset,
fixture.headerOffset + 512,
);
fixture.bytes.fill(0, fixture.headerOffset, fixture.headerOffset + 512);
setGoPeVirtualSize(fixture.bytes, 512);
const container = readGoPeImage(fixture.bytes);
expect(container.sections[0]).toMatchObject({
size: 512,
file_backed: true,
});
expect(container.mappings[0]).toMatchObject({ size: 2048, offset: 1024 });
expect(container.search[0]).toMatchObject({ size: 512, offset: 1024 });
expect(readGoBinaryImage(fixture.bytes).build_info).toBeNull();
},
);
+1 -1
View File
@@ -137,7 +137,7 @@ export const readGoPeImage = (bytes: Buffer): GoBinaryContainer => {
size !== 0 &&
(characteristics & ~0x00600000) >>> 0 === 0xc0000040
) {
search = { ...mapping, size: Math.min(size, virtualSize) };
search = { ...mapping, size: contentSize };
}
}
const machine = reader.u16(pe + 4);
+51
View File
@@ -10,6 +10,7 @@ import {
createGoSplitElfFixture,
createGoOverlappingElfFixture,
createGoBinaryPathFailureFixture,
setGoPeVirtualSize,
GO_MODULE_TEXT,
} from "../fixtures/go/image.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
@@ -76,6 +77,56 @@ cliTest(
30000,
);
for (const bits of [32, 64] as const) {
for (const encoding of ["inline", "pointer"] as const) {
cliTest(
`preserves PE ${bits}-bit ${encoding} build info at zero VirtualSize through CLI and stdio MCP`,
async ({ cli }) => {
const root = await createTestTempDirectory("rea-go-pe-zero-vsize-");
const fixture = createGoBinaryFixture({ format: "pe", bits, encoding });
const ordinaryBytes = Buffer.from(fixture.bytes);
const zeroSizeBytes = Buffer.from(fixture.bytes);
setGoPeVirtualSize(zeroSizeBytes, 0);
const { client, call } = await connectLocalToolsMcp();
const buildInfos = [];
for (const [label, bytes] of [
["ordinary", ordinaryBytes],
["zero-vsize", zeroSizeBytes],
] as const) {
const path = join(root, `${label}.exe`);
await writeFile(path, bytes);
const result = await cli.run({
arguments: ["inspect-go-binary", path, "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
const evidence = toolContract("inspect_go_binary").outputSchema.parse(
result.json,
);
expect(evidence.subject?.digest.sha256).toBe(
createHash("sha256").update(bytes).digest("hex"),
);
expect(evidence.normalized_result.build_info).toMatchObject({
go_version: "go1.26.0",
encoding,
header_offset: fixture.headerOffset,
module_text: GO_MODULE_TEXT,
});
buildInfos.push(evidence.normalized_result.build_info);
const response = await call("inspect_go_binary", { path });
expect(response.isError, JSON.stringify(response)).not.toBe(true);
expect(response.structuredContent).toEqual(evidence);
}
expect(buildInfos[1]).toEqual(buildInfos[0]);
await client.ping();
},
30000,
);
}
}
for (const expectedErrno of ["ELOOP", "ENAMETOOLONG"] as const) {
cliTest.skipIf(expectedErrno === "ELOOP" && process.platform === "win32")(
`reports native ${expectedErrno} path failure as invalid selected-path input through the public Go CLI`,
+10
View File
@@ -135,6 +135,16 @@ export const createGoBinaryFixture = (
return { bytes, headerOffset, versionOffset, moduleOffset, moduleBytes };
};
/** Set the first PE section's declared virtual content size in an authored fixture. */
export const setGoPeVirtualSize = (
bytes: Buffer,
virtualSize: number,
): void => {
const pe = bytes.readUInt32LE(60);
const section = pe + 24 + bytes.readUInt16LE(pe + 20);
bytes.writeUInt32LE(virtualSize, section + 8);
};
/** Two writable ELF loads, as in a PIE whose build info is in the later load. */
export const createGoSplitElfFixture = (
options: Parameters<typeof createGoBinaryFixture>[0] = {},