From feca30c72cfc0355fb3a2652be09aefd6b9ed8da Mon Sep 17 00:00:00 2001 From: Kaoru0822 Date: Thu, 12 Nov 2026 20:40:05 -0800 Subject: [PATCH] fix(go): validate string extents before budget admission (#10231) --- src/go/GoBinaryImage.ts | 25 +++++++------ tests/acceptance/goBinaryCli.test.ts | 17 +++++++++ tests/boundary/go/goBinaryImage.test.ts | 42 ++++++++++++++++++++++ tests/boundary/go/goBinaryProvider.test.ts | 4 +-- 4 files changed, 76 insertions(+), 12 deletions(-) diff --git a/src/go/GoBinaryImage.ts b/src/go/GoBinaryImage.ts index 9f86ee3581..e89856b93b 100644 --- a/src/go/GoBinaryImage.ts +++ b/src/go/GoBinaryImage.ts @@ -360,6 +360,11 @@ const readInlineExtent = ( ); length |= BigInt(byte & 127) << BigInt(index * 7); if ((byte & 128) === 0) { + if (length > BigInt(remaining - index - 1)) + throw new BinaryFormatFailure( + "malformed", + "Go build-info string range is truncated", + ); if (length > BigInt(maximumBytes)) throw new BinaryResourceFailure( "build-info", @@ -367,11 +372,6 @@ const readInlineExtent = ( `Go build-info string length exceeds max_build_info_bytes=${String(maximumBytes)}`, ); const size = Number(length); - if (size > remaining - index - 1) - throw new BinaryFormatFailure( - "malformed", - "Go build-info string range is truncated", - ); const start = offset + index + 1; return { location: { offset: start, bytes: size }, @@ -401,11 +401,10 @@ const readPointerString = ( const header = mappedFileOffset(mappings, address, width * 2, zeroFills); const valueAddress = reader.word(header, pointerBits); const length = reader.word(header + width, pointerBits); - if (length > BigInt(maximumBytes)) - throw new BinaryResourceFailure( - "build-info", - maximumBytes, - `Go build-info pointer string length exceeds max_build_info_bytes=${String(maximumBytes)}`, + if (length > BigInt(reader.bytes.length)) + throw new BinaryFormatFailure( + "malformed", + "Go build-info pointer string length exceeds the file extent", ); const size = Number(length); // Zero-length Go strings may carry a zero data pointer. @@ -413,6 +412,12 @@ const readPointerString = ( size === 0 && valueAddress === 0n ? header : mappedFileOffset(mappings, valueAddress, size, zeroFills); + if (length > BigInt(maximumBytes)) + throw new BinaryResourceFailure( + "build-info", + maximumBytes, + `Go build-info pointer string length exceeds max_build_info_bytes=${String(maximumBytes)}`, + ); return { bytes: reader.range(offset, size, "Go build-info pointer string"), location: { offset, bytes: size }, diff --git a/tests/acceptance/goBinaryCli.test.ts b/tests/acceptance/goBinaryCli.test.ts index e1d0fc6156..33d65fe226 100644 --- a/tests/acceptance/goBinaryCli.test.ts +++ b/tests/acceptance/goBinaryCli.test.ts @@ -88,6 +88,13 @@ cliTest( const root = await createTestTempDirectory("rea-go-inline-budget-"); const { client, call } = await connectLocalToolsMcp(); const maximum = 1024 * 1024; + const impossibleInline = createGoBinaryFixture(); + impossibleInline.bytes.set( + [0x81, 0x80, 0x40], + impossibleInline.headerOffset + 32, + ); + const impossiblePointer = createGoBinaryFixture({ encoding: "pointer" }); + impossiblePointer.bytes.writeBigUInt64LE(BigInt(maximum + 1), 1544); const cases = [ { name: "exact", @@ -104,6 +111,16 @@ cliTest( fixture: createGoInlineAggregateFixture(512), outcome: "malformed", }, + { + name: "impossible-inline", + fixture: impossibleInline, + outcome: "malformed", + }, + { + name: "impossible-pointer", + fixture: impossiblePointer, + outcome: "malformed", + }, ] as const; for (const item of cases) { diff --git a/tests/boundary/go/goBinaryImage.test.ts b/tests/boundary/go/goBinaryImage.test.ts index 51eaeb9ab3..b7e0c00467 100644 --- a/tests/boundary/go/goBinaryImage.test.ts +++ b/tests/boundary/go/goBinaryImage.test.ts @@ -285,6 +285,48 @@ it("refuses mapped pointer lengths outside the file instead of rounding 64-bit a expect(() => readGoBinaryImage(length.bytes)).toThrowError(/limit|length/i); }); +it.each(["inline", "pointer"] as const)( + "classifies impossible %s string extents before the decoding allowance", + (encoding) => { + for (const length of [1048577n, 0xffffffffffffffffn]) { + const fixture = createGoBinaryFixture({ encoding }); + if (encoding === "pointer") fixture.bytes.writeBigUInt64LE(length, 1544); + else { + let value = length; + let at = fixture.headerOffset + 32; + do { + const byte = Number(value & 127n); + value >>= 7n; + fixture.bytes[at++] = byte | (value === 0n ? 0 : 128); + } while (value !== 0n); + } + expect(() => readGoBinaryImage(fixture.bytes)).toThrowError( + expect.objectContaining({ kind: "malformed" }), + ); + } + }, +); + +it("validates pointer mapping before a smaller caller allowance", () => { + const fixture = createGoBinaryFixture({ encoding: "pointer" }); + fixture.bytes.writeBigUInt64LE(0x10fffn, 1536); + expect(() => + readGoBinaryImage(fixture.bytes, { budgets: { max_build_info_bytes: 1 } }), + ).toThrowError(expect.objectContaining({ kind: "malformed" })); +}); + +it.each(["inline", "pointer"] as const)( + "preserves resource refusal for physically valid %s strings", + (encoding) => { + const fixture = createGoBinaryFixture({ encoding }); + expect(() => + readGoBinaryImage(fixture.bytes, { + budgets: { max_build_info_bytes: 1 }, + }), + ).toThrowError(BinaryResourceFailure); + }, +); + it.each(["file bytes", "zero-fill"])( "refuses a header-backed PE module descriptor contradicted by %s", (alias) => { diff --git a/tests/boundary/go/goBinaryProvider.test.ts b/tests/boundary/go/goBinaryProvider.test.ts index 2403308689..d6696fde2d 100644 --- a/tests/boundary/go/goBinaryProvider.test.ts +++ b/tests/boundary/go/goBinaryProvider.test.ts @@ -12,6 +12,7 @@ import { projectAnalysisError } from "../../../src/domain/analysisErrorProjectio import { createGoBinaryDiagnosticFixtures, createGoBinaryFixture, + createGoInlineAggregateFixture, createGoOverlappingElfFixture, } from "../../fixtures/go/image.js"; import { createGoRodataStringFixture } from "../../fixtures/go/rodataStringImage.js"; @@ -55,8 +56,7 @@ it.each([32, 64] as const)( ); it("preserves the actual build-info byte guard as a resource failure", async () => { - const fixture = createGoBinaryFixture(); - fixture.bytes.set([0x81, 0x80, 0x40], fixture.headerOffset + 32); + const fixture = createGoInlineAggregateFixture(1024 * 1024 + 2); const root = await createTestTempDirectory("rea-go-resource-"); const path = join(root, "oversized-metadata.elf"); await writeFile(path, fixture.bytes);