fix(gpu): distinguish ELF string scan limits from corruption (#8955)

Co-authored-by: hhang <169027046+xu91102@users.noreply.github.com>
This commit is contained in:
hhangandhhang authored and GitHub committed 2026-11-05 15:38:19 +08:00
1 parent 0dcb456bbe
commit 8e3d794834
6 files changed
+126 -6

No files matched your search

+3 -1
View File
@@ -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"
+13 -1
View File
@@ -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,
),
}),
);
+1 -1
View File
@@ -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`,
);
}
}
@@ -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,
);
});
+9 -3
View File
@@ -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;
};
@@ -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 }) => {