diff --git a/src/gpu/GpuInventory.ts b/src/gpu/GpuInventory.ts index f7870c095c..9615fe4003 100644 --- a/src/gpu/GpuInventory.ts +++ b/src/gpu/GpuInventory.ts @@ -20,6 +20,7 @@ import type { import { BinaryFormatFailure, BinaryResourceFailure, + BinaryStringReadLimitFailure, } from "../native/metadata/BinaryContainer.js"; import { readElfSectionDirectory, @@ -295,7 +296,8 @@ export class GpuInventory { ) throw cause; row.inspection = - cause instanceof BinaryResourceFailure + cause instanceof BinaryResourceFailure || + cause instanceof BinaryStringReadLimitFailure ? "capacity" : cause.kind === "unsupported" ? "unsupported" diff --git a/src/native/exports/NativeExportParser.test.ts b/src/native/exports/NativeExportParser.test.ts index 22e0268b2c..5058dd3675 100644 --- a/src/native/exports/NativeExportParser.test.ts +++ b/src/native/exports/NativeExportParser.test.ts @@ -9,6 +9,7 @@ import * as nativeExportContracts from "../../domain/native/nativeExports.js"; import { BinaryFormatFailure, BinaryResourceFailure, + BinaryStringReadLimitFailure, } from "../metadata/BinaryContainer.js"; import { parseNativeExports, @@ -564,6 +565,17 @@ describe("ELF export version and name coverage", () => { expect(result.coverage.status).toBe("partial"); }); + it("keeps unrelated exports when a symbol name exceeds the string scan allowance", () => { + const result = parse( + elfExportImage({ + symbols: [{ name: "kept" }, { name: "k".repeat(65_536) }], + }), + ); + expect(result.exports.map(({ name }) => name)).toEqual(["kept"]); + expect(result.issues).toMatchObject([{ code: "name_unreadable" }]); + expect(result.coverage.status).toBe("partial"); + }); + it("separates an examined empty symbol table from unavailable symbol declarations", () => { expect(parse(elfExportImage({ symbols: [] }))).toMatchObject({ exports: [], @@ -833,7 +845,7 @@ describe("ELF symbol-name text", () => { expect(result.issues).toContainEqual( expect.objectContaining({ message: expect.stringContaining( - "unterminated or exceeds the string budget", + new BinaryStringReadLimitFailure(65_536).message, ), }), ); diff --git a/src/native/metadata/BinaryContainer.ts b/src/native/metadata/BinaryContainer.ts index cc24e1b556..05f7073f47 100644 --- a/src/native/metadata/BinaryContainer.ts +++ b/src/native/metadata/BinaryContainer.ts @@ -40,7 +40,7 @@ export class BinaryStringReadLimitFailure extends BinaryFormatFailure { // allowing coverage-aware consumers to distinguish a limit from corruption. super( "malformed", - `String is unterminated or longer than ${String(maximumBytes)} bytes`, + `String termination was not established within its ${String(maximumBytes)}-byte read allowance`, ); } } diff --git a/src/native/metadata/ElfStringTable.test.ts b/src/native/metadata/ElfStringTable.test.ts new file mode 100644 index 0000000000..f06fedaeec --- /dev/null +++ b/src/native/metadata/ElfStringTable.test.ts @@ -0,0 +1,64 @@ +import { expect, it } from "vitest"; +import { + BinaryFormatFailure, + BinaryStringReadLimitFailure, +} from "./BinaryContainer.js"; +import { readElfString, readElfStringBytes } from "./ElfStringTable.js"; + +it("preserves empty and non-UTF8 string-table bytes", () => { + const table = Buffer.from([0, 0xff, 0x61, 0]); + expect(readElfStringBytes(table, 0).equals(Buffer.alloc(0))).toBe(true); + expect(readElfStringBytes(table, 1).equals(Buffer.from([0xff, 0x61]))).toBe( + true, + ); + expect(readElfString(table, 1)).toBe("\xffa"); +}); + +it("reads a terminator inside the scan allowance at a nonzero table offset", () => { + const table = Buffer.concat([ + Buffer.from([0]), + Buffer.alloc(65_535, 0x61), + Buffer.from([0]), + ]); + expect(readElfStringBytes(table, 1)).toHaveLength(65_535); +}); + +it.each([65_536, 70_000])( + "reports unknown termination when a %i-byte string exceeds the scan allowance", + (size) => { + const table = Buffer.concat([ + Buffer.from([0]), + Buffer.alloc(size, 0x61), + Buffer.from([0]), + ]); + expect(() => readElfStringBytes(table, 1)).toThrow( + BinaryStringReadLimitFailure, + ); + try { + readElfStringBytes(table, 1); + } catch (cause) { + expect(cause).toBeInstanceOf(BinaryFormatFailure); + expect(cause).toMatchObject({ maximumBytes: 65_536 }); + } + }, +); + +it.each([3, 65_536])( + "keeps a fully examined %i-byte unterminated table malformed", + (size) => { + const table = Buffer.alloc(size, 0x61); + expect(() => readElfStringBytes(table, 0)).toThrow(BinaryFormatFailure); + expect(() => readElfStringBytes(table, 0)).not.toThrow( + BinaryStringReadLimitFailure, + ); + }, +); + +it.each([-1, 1])("keeps an out-of-table offset %i malformed", (offset) => { + expect(() => readElfStringBytes(Buffer.from([0]), offset)).toThrow( + BinaryFormatFailure, + ); + expect(() => readElfStringBytes(Buffer.from([0]), offset)).not.toThrow( + BinaryStringReadLimitFailure, + ); +}); diff --git a/src/native/metadata/ElfStringTable.ts b/src/native/metadata/ElfStringTable.ts index c60a179a57..9f02f84a38 100644 --- a/src/native/metadata/ElfStringTable.ts +++ b/src/native/metadata/ElfStringTable.ts @@ -1,5 +1,8 @@ import { observeElfStringTableEntry } from "../../domain/elfStringTable.js"; -import { BinaryFormatFailure } from "./BinaryContainer.js"; +import { + BinaryFormatFailure, + BinaryStringReadLimitFailure, +} from "./BinaryContainer.js"; export const readElfString = (bytes: Buffer, offset: number): string => { return readElfStringBytes(bytes, offset).toString("latin1"); @@ -13,10 +16,13 @@ export const readElfStringBytes = (bytes: Buffer, offset: number): Buffer => { "malformed", "ELF string offset is outside its string table", ); - if (observed.state === "truncated") + if (observed.state === "truncated") { + if (offset + observed.examinedBytes < bytes.length) + throw new BinaryStringReadLimitFailure(observed.examinedBytes); throw new BinaryFormatFailure( "malformed", - "ELF string is unterminated or exceeds the string budget", + "ELF string is unterminated inside its string table", ); + } return observed.bytes; }; diff --git a/tests/acceptance/gpuCodeObjectsCli.test.ts b/tests/acceptance/gpuCodeObjectsCli.test.ts index c48600d867..09ca561735 100644 --- a/tests/acceptance/gpuCodeObjectsCli.test.ts +++ b/tests/acceptance/gpuCodeObjectsCli.test.ts @@ -69,6 +69,42 @@ cliTest( }, ); +cliTest( + "reports an exhausted ELF symbol string scan as capacity instead of corruption", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-cubin-string-limit-"); + const bytes = cubinFixture({ name: "k".repeat(65_536) }); + const path = join(root, "long-symbol.cubin"); + await writeFile(path, bytes); + const response = await runFileCli(cli, { + arguments: [ + "inspect-gpu-code-objects", + path, + "--analysis", + "kernels", + "--detail", + "complete", + "--json", + ], + }); + expect(response.exitCode, response.stderr).toBe(0); + const evidence = parseEvidence( + toolContract("inspect_gpu_code_objects").outputSchema.parse( + response.json, + ), + ); + const report = gpuCodeObjectsSchema.parse(evidence.normalized_result); + expect(report.objects[0]).toMatchObject({ + kind: "cubin", + inspection: "capacity", + }); + expect(report.artifact.sha256).toBe( + createHash("sha256").update(bytes).digest("hex"), + ); + expect(report.coverage.payloads).toBe("partial"); + }, +); + cliTest( "inventories a complete CCOB stream containing ordinary CCOB payload text", async ({ cli }) => {