diff --git a/docs/go-binaries.md b/docs/go-binaries.md index 7946520034..fd2014a816 100644 --- a/docs/go-binaries.md +++ b/docs/go-binaries.md @@ -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 diff --git a/src/go/GoBinaryImage.ts b/src/go/GoBinaryImage.ts index 4cec34c949..f5df25c4ac 100644 --- a/src/go/GoBinaryImage.ts +++ b/src/go/GoBinaryImage.ts @@ -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"); diff --git a/tests/boundary/go/goBinaryImage.test.ts b/tests/boundary/go/goBinaryImage.test.ts index bcaa8c619d..c65e2aa02b 100644 --- a/tests/boundary/go/goBinaryImage.test.ts +++ b/tests/boundary/go/goBinaryImage.test.ts @@ -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) => {