fix(go): read inline build strings from ELF mappings

Fixes #6716
This commit is contained in:
morluto
2026-10-23 21:02:31 +08:00
parent aa52ff6f4b
commit 386359060b
3 changed files with 47 additions and 1 deletions
+5
View File
@@ -132,6 +132,11 @@ includes sectionless PIE images with metadata in a later data segment. Named
sections retain priority, and alignment, file mapping and record validation
remain required.
A named ELF build-info section selects the record's discovery range. For inline
encoding, the following version and module strings may continue past that
section into its file-backed load mapping; their bytes are still checked
against the mapping and the existing aggregate string limit.
Historical moduleless Go binaries can store their empty module-string header in
declared zero-initialized virtual memory rather than file-backed bytes. The reader
recognizes this only when the complete optional module header lies in that memory
+24 -1
View File
@@ -104,10 +104,10 @@ export const readGoBinaryImage = (bytes: Buffer): GoBinaryImage => {
const encoding = (flags & 2) !== 0 ? "inline" : "pointer";
const readInline = (offset: number, remaining: number) =>
readInlineString(reader, offset, remaining);
const sourceEnd = search.offset + search.size;
let version: LocatedString;
let module: LocatedString;
if (encoding === "inline") {
const sourceEnd = inlineDataEnd(header, search, container);
version = readInline(header + 32, sourceEnd - header - 32);
const next = version.location.offset + version.location.bytes;
module = readInline(next, sourceEnd - next);
@@ -178,6 +178,29 @@ export const readGoBinaryImage = (bytes: Buffer): GoBinaryImage => {
};
};
/** Inline strings follow the header in mapped Go build-info data, even when
* the named ELF discovery section ends immediately after the header.
*/
const inlineDataEnd = (
header: number,
search: FileMapping,
container: BinaryContainer,
): number => {
if (container.format !== "elf") return search.offset + search.size;
const address = search.address + BigInt(header - search.offset);
let end = search.offset + search.size;
for (const { mapping } of container.load_regions ?? []) {
const delta = address - mapping.address;
if (
delta >= 0n &&
delta < BigInt(mapping.size) &&
mapping.offset + Number(delta) === header
)
end = Math.max(end, mapping.offset + mapping.size);
}
return Math.min(end, header + 32 + MAX_BUILD_INFO_BYTES);
};
const observedText = (bytes: Buffer): string | null => {
try {
return observedUtf8(bytes, "Go build string");
+18
View File
@@ -191,6 +191,24 @@ it.each([
},
);
it("reads inline build strings past the ELF build-info discovery section", () => {
const fixture = createGoBinaryFixture();
const buildInfoSection = 256 + 64 * 2;
fixture.bytes.writeBigUInt64LE(32n, buildInfoSection + 32);
expect(readGoBinaryImage(fixture.bytes).build_info).toMatchObject({
header_offset: fixture.headerOffset,
encoding: "inline",
go_version: "go1.26.0",
module_text: GO_MODULE_TEXT,
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) => {