mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 11:13:21 +00:00
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:
1 parent
0dcb456bbe
commit
8e3d794834
6 files changed
+126
-6
No files matched your search
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
@@ -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 }) => {
|
||||
|
||||
Reference in new issue
Block a user