mirror of
https://github.com/morluto/rea.git
synced 2026-11-09 16:26:36 +00:00
fix(erlang): validate table framing before row capacity (#9606)
fix(erlang): validate BEAM table extents before capacity
This commit is contained in:
1 parent
7c821cfea1
commit
97fe58d3fe
5 files changed
+94
-27
No files matched your search
@@ -98,6 +98,18 @@ export const erlangBeamDeclaredRowsFixture = (
|
||||
return concat(Buffer.from("FOR1", "latin1"), u32(form.length), form);
|
||||
};
|
||||
|
||||
/** BEAM table frames with all declared fixed-width row bytes present. */
|
||||
export const erlangBeamRowsFixture = (
|
||||
counts: readonly (readonly ["ExpT" | "ImpT" | "FunT", number])[],
|
||||
): Buffer =>
|
||||
erlangBeamChunksFixture(
|
||||
counts.map(([name, count]) => {
|
||||
const payload = Buffer.alloc(4 + count * (name === "FunT" ? 24 : 12));
|
||||
payload.writeUInt32BE(count);
|
||||
return [name, payload] as const;
|
||||
}),
|
||||
);
|
||||
|
||||
/** BEAM form whose atom table exceeds a caller-selected atom budget. */
|
||||
export const erlangBeamManyAtomsFixture = (count: number): Buffer => {
|
||||
const names = Array.from(
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
|
||||
import {
|
||||
erlangBeamChunksFixture,
|
||||
erlangBeamDeclaredRowsFixture,
|
||||
erlangBeamRowsFixture,
|
||||
erlangBeamFixture,
|
||||
erlangBeamLegacyAtomsFixture,
|
||||
erlangBeamManyAtomsFixture,
|
||||
@@ -280,7 +280,7 @@ it("rejects combined table rows before allocating them", () => {
|
||||
const overflow = MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS - below * 2 + 1;
|
||||
expect(() =>
|
||||
parse(
|
||||
erlangBeamDeclaredRowsFixture([
|
||||
erlangBeamRowsFixture([
|
||||
["ExpT", below],
|
||||
["ImpT", below],
|
||||
["FunT", overflow],
|
||||
@@ -293,10 +293,22 @@ it("rejects combined table rows before allocating them", () => {
|
||||
message: expect.stringContaining("row budget"),
|
||||
}),
|
||||
);
|
||||
const complete = Buffer.alloc(
|
||||
4 + (MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS + 1) * 12,
|
||||
);
|
||||
complete.writeUInt32BE(MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS + 1);
|
||||
expect(() =>
|
||||
parse(
|
||||
erlangBeamChunksFixture([
|
||||
["ExpT", complete],
|
||||
["ImpT", Buffer.alloc(1)],
|
||||
]),
|
||||
),
|
||||
).toThrow(/import table is truncated/);
|
||||
const count = Buffer.alloc(4);
|
||||
count.writeUInt32BE(MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS + 1);
|
||||
expect(() => parse(erlangBeamChunksFixture([["ExpT", count]]))).toThrow(
|
||||
/row budget/,
|
||||
/truncated/,
|
||||
);
|
||||
count.writeUInt32BE(MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS);
|
||||
expect(() => parse(erlangBeamChunksFixture([["ExpT", count]]))).toThrow(
|
||||
@@ -359,7 +371,7 @@ it.each([
|
||||
},
|
||||
{
|
||||
resource: "table-rows",
|
||||
bytes: erlangBeamDeclaredRowsFixture([
|
||||
bytes: erlangBeamRowsFixture([
|
||||
["ExpT", MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS + 1],
|
||||
]),
|
||||
settings: {},
|
||||
|
||||
+32
-20
@@ -172,15 +172,19 @@ const parseAtoms = (
|
||||
return atoms;
|
||||
};
|
||||
|
||||
const tableCount = (payload: Buffer, width: number, label: string): number => {
|
||||
if (payload.length < 4) fail(`BEAM ${label} table is truncated.`);
|
||||
const count = u32(payload, 0);
|
||||
if (count > Math.floor((payload.length - 4) / (width * 4)))
|
||||
fail(`BEAM ${label} table is truncated.`);
|
||||
return count;
|
||||
};
|
||||
|
||||
const parseRows = (
|
||||
payload: Buffer,
|
||||
width: number,
|
||||
label: string,
|
||||
count: number,
|
||||
): number[][] => {
|
||||
if (payload.length < 4) fail(`BEAM ${label} table is truncated.`);
|
||||
const count = u32(payload, 0);
|
||||
const need = 4 + count * width * 4;
|
||||
if (payload.length < need) fail(`BEAM ${label} table is truncated.`);
|
||||
const rows: number[][] = [];
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const row: number[] = [];
|
||||
@@ -278,47 +282,55 @@ export const parseErlangBeam = (
|
||||
atomChunk === undefined || atomFormat === null
|
||||
? null
|
||||
: parseAtoms(atomChunk.payload, input.max_atoms, atomFormat);
|
||||
const declaredRows = (name: string): number => {
|
||||
const payload = payloads.get(name)?.payload;
|
||||
return payload !== undefined && payload.length >= 4 ? u32(payload, 0) : 0;
|
||||
};
|
||||
const exportChunk = payloads.get("ExpT");
|
||||
const importChunk = payloads.get("ImpT");
|
||||
const lambdaChunk = payloads.get("FunT");
|
||||
const exportCount =
|
||||
exportChunk === undefined
|
||||
? 0
|
||||
: tableCount(exportChunk.payload, 3, "export");
|
||||
const importCount =
|
||||
importChunk === undefined
|
||||
? 0
|
||||
: tableCount(importChunk.payload, 3, "import");
|
||||
// OTP skips the optional lambda parser for a zero-byte selected chunk.
|
||||
const lambdaCount =
|
||||
lambdaChunk === undefined || lambdaChunk.payload.length === 0
|
||||
? 0
|
||||
: tableCount(lambdaChunk.payload, 6, "lambda");
|
||||
if (
|
||||
declaredRows("ExpT") + declaredRows("ImpT") + declaredRows("FunT") >
|
||||
exportCount + importCount + lambdaCount >
|
||||
MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS
|
||||
)
|
||||
throw new BeamCapacityFailure(
|
||||
"table-rows",
|
||||
MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS,
|
||||
`BEAM tables exceed the ${String(
|
||||
MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS,
|
||||
)}-row budget.`,
|
||||
`BEAM tables exceed the ${String(MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS)}-row budget.`,
|
||||
);
|
||||
const exports =
|
||||
payloads.get("ExpT") === undefined
|
||||
exportChunk === undefined
|
||||
? null
|
||||
: parseRows(payloads.get("ExpT")!.payload, 3, "export").map((row) => ({
|
||||
: parseRows(exportChunk.payload, 3, exportCount).map((row) => ({
|
||||
function_index: row[0]!,
|
||||
function: atomAt(atoms, row[0]!),
|
||||
arity: row[1]!,
|
||||
label: row[2]!,
|
||||
}));
|
||||
const imports =
|
||||
payloads.get("ImpT") === undefined
|
||||
importChunk === undefined
|
||||
? null
|
||||
: parseRows(payloads.get("ImpT")!.payload, 3, "import").map((row) => ({
|
||||
: parseRows(importChunk.payload, 3, importCount).map((row) => ({
|
||||
module_index: row[0]!,
|
||||
module: atomAt(atoms, row[0]!),
|
||||
function_index: row[1]!,
|
||||
function: atomAt(atoms, row[1]!),
|
||||
arity: row[2]!,
|
||||
}));
|
||||
const lambdaChunk = payloads.get("FunT");
|
||||
// OTP skips the optional lambda parser for a zero-byte selected chunk.
|
||||
// A four-byte zero count remains a decoded, explicitly empty table.
|
||||
const lambdas =
|
||||
lambdaChunk === undefined || lambdaChunk.payload.length === 0
|
||||
? null
|
||||
: parseRows(lambdaChunk.payload, 6, "lambda").map((row) => ({
|
||||
: parseRows(lambdaChunk.payload, 6, lambdaCount).map((row) => ({
|
||||
function_index: row[0]!,
|
||||
function: atomAt(atoms, row[0]!),
|
||||
arity: row[1]!,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { createBeamService } from "../../../src/composition/erlang.js";
|
||||
import { MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS } from "../../../src/domain/erlang/beam.js";
|
||||
import {
|
||||
erlangBeamDeclaredRowsFixture,
|
||||
erlangBeamRowsFixture,
|
||||
erlangBeamFixture,
|
||||
} from "../../../src/erlang/Beam.fixture.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
@@ -66,7 +66,7 @@ describe("Erlang BEAM stable filesystem boundary", () => {
|
||||
const below = Math.floor(MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS / 2);
|
||||
await writeFile(
|
||||
path,
|
||||
erlangBeamDeclaredRowsFixture([
|
||||
erlangBeamRowsFixture([
|
||||
["ExpT", below],
|
||||
["ImpT", below],
|
||||
["FunT", MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS - below * 2 + 1],
|
||||
|
||||
@@ -17,6 +17,7 @@ import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
||||
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
||||
import {
|
||||
erlangBeamDeclaredRowsFixture,
|
||||
erlangBeamRowsFixture,
|
||||
erlangBeamFixture,
|
||||
} from "../../../src/erlang/Beam.fixture.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
@@ -106,7 +107,7 @@ mcpTest(
|
||||
],
|
||||
[
|
||||
"rows",
|
||||
erlangBeamDeclaredRowsFixture([
|
||||
erlangBeamRowsFixture([
|
||||
["ExpT", MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS + 1],
|
||||
]),
|
||||
{},
|
||||
@@ -177,3 +178,33 @@ mcpTest(
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
mcpTest(
|
||||
"reports impossible BEAM table counts as malformed input",
|
||||
async ({ mcp }) => {
|
||||
const { client, session } = await connect(mcp);
|
||||
const root = await createTestTempDirectory("rea-beam-malformed-rows-");
|
||||
try {
|
||||
for (const chunk of ["ExpT", "ImpT", "FunT"]) {
|
||||
const path = join(root, `${chunk}.beam`);
|
||||
await writeFile(
|
||||
path,
|
||||
erlangBeamDeclaredRowsFixture([
|
||||
[chunk, MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS + 1],
|
||||
]),
|
||||
);
|
||||
const response = await client.callTool({
|
||||
name: "inspect_erlang_beam",
|
||||
arguments: { path },
|
||||
});
|
||||
expect(response.isError).toBe(true);
|
||||
expect(parseMcpToolError(response).error).toMatchObject({
|
||||
code: "invalid_request",
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
},
|
||||
60_000,
|
||||
);
|
||||
Reference in new issue
Block a user