Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/gpu/GpuInventory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {
import {
BinaryFormatFailure,
BinaryResourceFailure,
BinaryStringReadLimitFailure,
} from "../native/metadata/BinaryContainer.js";
import {
readElfSectionDirectory,
Expand Down Expand Up @@ -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"
Expand Down
14 changes: 13 additions & 1 deletion src/native/exports/NativeExportParser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as nativeExportContracts from "../../domain/native/nativeExports.js";
import {
BinaryFormatFailure,
BinaryResourceFailure,
BinaryStringReadLimitFailure,
} from "../metadata/BinaryContainer.js";
import {
parseNativeExports,
Expand Down Expand Up @@ -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: [],
Expand Down Expand Up @@ -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,
),
}),
);
Expand Down
2 changes: 1 addition & 1 deletion src/native/metadata/BinaryContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
);
}
}
Expand Down
64 changes: 64 additions & 0 deletions src/native/metadata/ElfStringTable.test.ts
Original file line number Diff line number Diff line change
@@ -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,
);
});
12 changes: 9 additions & 3 deletions src/native/metadata/ElfStringTable.ts
Original file line number Diff line number Diff line change
@@ -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");
Expand All @@ -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;
};
36 changes: 36 additions & 0 deletions tests/acceptance/gpuCodeObjectsCli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down
Loading