mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(native): account export and version text in UTF-8 bytes
Count retained UTF-8 text rather than UTF-16 code units, including exact-byte base64 spellings. Bound the decoded version-name map independently from output expansion, releasing replaced entries from its live storage charge. Add real service/provider boundary controls for accepted and over-budget symbol names, version names and unused version metadata. Existing per-string bounds remain the allocation owner. Validation: native parser, filesystem and CLI/MCP views passed; build, typecheck, lint and formatting passed. No serialized-report size cap, OOM reproduction or performance improvement is claimed.
This commit is contained in:
1 parent
3d74c480e0
commit
74d5cab372
2 files changed
+134
-8
No files matched your search
@@ -36,18 +36,29 @@ const MAXIMUM_NAME_BYTES = 4096;
|
||||
/** Text retained across all rows; names can repeat one long string table entry. */
|
||||
const MAXIMUM_RETAINED_TEXT_BYTES = 16 * 1024 * 1024;
|
||||
|
||||
const retainedTextBytes = (...strings: readonly (string | null)[]): number =>
|
||||
strings.reduce(
|
||||
(bytes, value) =>
|
||||
bytes + (value === null ? 0 : Buffer.byteLength(value, "utf8")),
|
||||
0,
|
||||
);
|
||||
|
||||
const checkRetainedText = (bytes: number): void => {
|
||||
if (bytes > MAXIMUM_RETAINED_TEXT_BYTES)
|
||||
throw new BinaryResourceFailure(
|
||||
"structure",
|
||||
MAXIMUM_RETAINED_TEXT_BYTES,
|
||||
`Export names exceed the ${String(MAXIMUM_RETAINED_TEXT_BYTES)}-byte retained-text allowance`,
|
||||
);
|
||||
};
|
||||
|
||||
/** Charge retained text and rows at their owning storage boundary. */
|
||||
const createRetention = () => {
|
||||
let rows = 0;
|
||||
let text = 0;
|
||||
const retainText = (...strings: readonly (string | null)[]): void => {
|
||||
for (const value of strings) text += value?.length ?? 0;
|
||||
if (text > MAXIMUM_RETAINED_TEXT_BYTES)
|
||||
throw new BinaryResourceFailure(
|
||||
"structure",
|
||||
MAXIMUM_RETAINED_TEXT_BYTES,
|
||||
`Export names exceed the ${String(MAXIMUM_RETAINED_TEXT_BYTES)}-byte retained-text allowance`,
|
||||
);
|
||||
text += retainedTextBytes(...strings);
|
||||
checkRetainedText(text);
|
||||
};
|
||||
return {
|
||||
text: retainText,
|
||||
@@ -316,8 +327,25 @@ const readVersionNames = (
|
||||
issues: NativeExportIssue[],
|
||||
): Map<number, DecodedElfString | null> => {
|
||||
const namesByIndex = new Map<number, DecodedElfString | null>();
|
||||
// This intermediate map owns its storage independently of repeated text in
|
||||
// output rows. Replacing an index releases its previous decoded name.
|
||||
let textBytes = 0;
|
||||
const addName = (index: number, name: Buffer): void => {
|
||||
namesByIndex.set(index, name.length === 0 ? null : decodeElfString(name));
|
||||
const decoded = name.length === 0 ? null : decodeElfString(name);
|
||||
const previous = namesByIndex.get(index);
|
||||
const nextTextBytes =
|
||||
textBytes -
|
||||
retainedTextBytes(
|
||||
previous?.name ?? null,
|
||||
previous?.name_bytes_base64 ?? null,
|
||||
) +
|
||||
retainedTextBytes(
|
||||
decoded?.name ?? null,
|
||||
decoded?.name_bytes_base64 ?? null,
|
||||
);
|
||||
checkRetainedText(nextTextBytes);
|
||||
namesByIndex.set(index, decoded);
|
||||
textBytes = nextTextBytes;
|
||||
};
|
||||
const verneed = sections.find(({ type }) => type === SHT_GNU_VERNEED);
|
||||
if (verneed !== undefined) {
|
||||
|
||||
@@ -87,6 +87,104 @@ describe("native export stable filesystem boundary", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("native export text capacity", () => {
|
||||
it.each(["symbol", "version"] as const)(
|
||||
"bounds UTF-8 bytes of retained ELF %s text and its exact-byte spelling",
|
||||
async (kind) => {
|
||||
const root = await createTestTempDirectory("rea-native-export-text-");
|
||||
const service = createNativeExportsService();
|
||||
const text = "€".repeat(20_000);
|
||||
const rawName = Buffer.from(text, "utf8").toString("latin1");
|
||||
try {
|
||||
for (const count of [119, 160]) {
|
||||
const path = join(root, `${kind}-${String(count)}.so`);
|
||||
const bytes = elfExportImage({
|
||||
symbols: Array.from({ length: count }, () =>
|
||||
kind === "symbol" ? { name: rawName } : { name: "a", version: 2 },
|
||||
),
|
||||
...(kind === "version"
|
||||
? { versionDefinitions: ["lib.so", rawName] }
|
||||
: {}),
|
||||
});
|
||||
expect(bytes.length).toBeLessThan(70_000);
|
||||
await writeFile(path, bytes);
|
||||
const result = await service.inspect({ path });
|
||||
if (count === 160) {
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "AnalysisResourceConstraintError" },
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (!result.ok) throw new Error(JSON.stringify(result.error));
|
||||
const report = nativeExportsSchema.parse(
|
||||
result.value.normalized_result,
|
||||
);
|
||||
if (report.format !== "elf") throw new Error("Expected ELF report");
|
||||
expect(report.coverage).toMatchObject({
|
||||
status: "complete",
|
||||
exports: count,
|
||||
});
|
||||
const row = report.exports[0]!;
|
||||
const name = kind === "symbol" ? row : row.version;
|
||||
expect(name).toMatchObject({
|
||||
name: text,
|
||||
name_bytes_base64: Buffer.from(text, "utf8").toString("base64"),
|
||||
name_utf8_decodable: true,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
await service.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("bounds intermediate ELF version names even when no symbols use them", async () => {
|
||||
const root = await createTestTempDirectory("rea-native-export-versions-");
|
||||
const service = createNativeExportsService();
|
||||
try {
|
||||
for (const count of [279, 280]) {
|
||||
const path = join(root, `versions-${String(count)}.so`);
|
||||
const bytes = elfExportImage({
|
||||
symbols: [],
|
||||
versionDefinitions: Array<string>(count).fill("v".repeat(60_000)),
|
||||
// Definitions replace requirements at the same indices; released
|
||||
// names must not remain charged against the live map's allowance.
|
||||
...(count === 279
|
||||
? {
|
||||
versionRequirements: Array.from(
|
||||
{ length: count },
|
||||
(_, index) => ({
|
||||
index: index + 1,
|
||||
name: "w".repeat(60_000),
|
||||
}),
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
expect(bytes.length).toBeLessThan(140_000);
|
||||
await writeFile(path, bytes);
|
||||
const result = await service.inspect({ path });
|
||||
if (count === 280)
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "AnalysisResourceConstraintError" },
|
||||
});
|
||||
else {
|
||||
if (!result.ok) throw new Error(JSON.stringify(result.error));
|
||||
expect(result.value.normalized_result).toMatchObject({
|
||||
exports: [],
|
||||
issues: [],
|
||||
coverage: { status: "complete", exports: 0 },
|
||||
});
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await service.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("native export encoding and failures", () => {
|
||||
it("keeps ELF symbol bytes while exposing valid UTF-8 text", async () => {
|
||||
const root = await createTestTempDirectory("rea-native-export-unicode-");
|
||||
|
||||
Reference in new issue
Block a user