Files
reverse-engineering-anything/src/config/debuggerSessionLimits.test.ts
T

313 lines
10 KiB
TypeScript

import { expect, it } from "vitest";
import { ConfigurationError } from "../domain/configurationErrors.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import {
DEFAULT_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES,
MAXIMUM_DEBUGGER_COMMAND_DEADLINE_MS,
MAXIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES,
MAXIMUM_DEBUGGER_MAX_PENDING_COMMANDS,
MAXIMUM_DEBUGGER_MAX_SESSIONS,
MINIMUM_DEBUGGER_COMMAND_DEADLINE_MS,
MINIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES,
MINIMUM_DEBUGGER_MAX_PENDING_COMMANDS,
MINIMUM_DEBUGGER_MAX_SESSIONS,
} from "../domain/analysisResourceLimits.js";
import { GdbSessionManager } from "../gdb/GdbSessionManager.js";
import { RizinDebugSessionManager } from "../rizin/RizinDebugSessionManager.js";
import { runWithAnalysisOperationSnapshot } from "../domain/analysisOperationSnapshot.js";
import { parseEnvironment } from "./environment.js";
import {
DEBUGGER_COMMAND_DEADLINE_SETTING,
DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING,
DEBUGGER_MAX_PENDING_COMMANDS_SETTING,
DEBUGGER_MAX_SESSIONS_SETTING,
parseDebuggerSessionLimits,
} from "./debuggerSessionLimits.js";
it("uses the shared defaults when the settings are unset", () => {
const parsed = parseDebuggerSessionLimits({});
if (!parsed.ok) throw parsed.error;
expect(parsed.value.commandDeadline).toMatchObject({
setting: DEBUGGER_COMMAND_DEADLINE_SETTING,
value: 120_000,
source: "default",
minimum: MINIMUM_DEBUGGER_COMMAND_DEADLINE_MS,
maximum: MAXIMUM_DEBUGGER_COMMAND_DEADLINE_MS,
unit: "ms",
});
expect(parsed.value.maxSessions).toMatchObject({
setting: DEBUGGER_MAX_SESSIONS_SETTING,
value: 8,
source: "default",
minimum: MINIMUM_DEBUGGER_MAX_SESSIONS,
maximum: MAXIMUM_DEBUGGER_MAX_SESSIONS,
unit: "sessions",
});
expect(parsed.value.maxPendingCommands).toMatchObject({
setting: DEBUGGER_MAX_PENDING_COMMANDS_SETTING,
value: 16,
source: "default",
minimum: MINIMUM_DEBUGGER_MAX_PENDING_COMMANDS,
maximum: MAXIMUM_DEBUGGER_MAX_PENDING_COMMANDS,
unit: "commands",
});
expect(parsed.value.maxCommandOutputBytes).toMatchObject({
setting: DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING,
value: 16 * 1024 * 1024,
source: "default",
minimum: MINIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES,
maximum: MAXIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES,
unit: "bytes",
});
});
it("accepts one in-range override for each shared debugger cap", () => {
const parsed = parseDebuggerSessionLimits({
[DEBUGGER_COMMAND_DEADLINE_SETTING]: "1000",
[DEBUGGER_MAX_SESSIONS_SETTING]: "2",
[DEBUGGER_MAX_PENDING_COMMANDS_SETTING]: "3",
[DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING]: "4096",
});
if (!parsed.ok) throw parsed.error;
expect(parsed.value.commandDeadline).toMatchObject({
value: 1000,
source: "setting",
});
expect(parsed.value.maxSessions).toMatchObject({
value: 2,
source: "setting",
});
expect(parsed.value.maxPendingCommands).toMatchObject({
value: 3,
source: "setting",
});
expect(parsed.value.maxCommandOutputBytes).toMatchObject({
value: 4096,
source: "setting",
});
});
it.each([
[
DEBUGGER_COMMAND_DEADLINE_SETTING,
"0",
MINIMUM_DEBUGGER_COMMAND_DEADLINE_MS,
MAXIMUM_DEBUGGER_COMMAND_DEADLINE_MS,
120_000,
"ms",
],
[
DEBUGGER_MAX_SESSIONS_SETTING,
"65",
MINIMUM_DEBUGGER_MAX_SESSIONS,
MAXIMUM_DEBUGGER_MAX_SESSIONS,
8,
"sessions",
],
[
DEBUGGER_MAX_PENDING_COMMANDS_SETTING,
"00",
MINIMUM_DEBUGGER_MAX_PENDING_COMMANDS,
MAXIMUM_DEBUGGER_MAX_PENDING_COMMANDS,
16,
"commands",
],
[
DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING,
String(MAXIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES + 1),
MINIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES,
MAXIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES,
16 * 1024 * 1024,
"bytes",
],
])(
"rejects invalid %s at the setting boundary",
(setting, value, minimum, maximum, defaultValue, unit) => {
const parsed = parseDebuggerSessionLimits({ [setting]: value });
if (parsed.ok) throw new Error(`${setting} was accepted`);
expect(parsed.error).toBeInstanceOf(ConfigurationError);
expect(parsed.error.settings).toHaveLength(1);
const detail = parsed.error.settings[0];
if (!detail) throw new Error("missing setting guidance");
expect(detail.setting).toBe(setting);
expect(detail.constraint).toContain(setting);
expect(detail.constraint).toMatch(/positive decimal integer/i);
expect(detail.constraint).toContain(
`from ${minimum} to ${maximum} ${unit}`,
);
expect(detail.constraint).toContain(`default is ${defaultValue} ${unit}`);
const startup = parseEnvironment({ [setting]: value });
const runtimeSeed =
setting === DEBUGGER_COMMAND_DEADLINE_SETTING ||
setting === DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING;
if (runtimeSeed) {
if (!startup.ok) throw new Error(`${setting} was rejected at startup`);
} else {
if (startup.ok) throw new Error(`${setting} was accepted at startup`);
expect(projectAnalysisError(startup.error)).toMatchObject({
code: "configuration_invalid",
details: { settings: [detail] },
});
}
},
);
it("applies runtime debugger overrides before rejecting invalid launch values", () => {
const parsed = runWithAnalysisOperationSnapshot(
{
revision: 1,
overrides: {
"debugger.command_deadline_ms": 25,
"debugger.max_command_output_bytes": 4096,
},
},
() =>
parseDebuggerSessionLimits({
[DEBUGGER_COMMAND_DEADLINE_SETTING]: "nope",
[DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING]: "0",
}),
);
if (!parsed.ok) throw parsed.error;
expect(parsed.value.commandDeadline.source).toBe("default");
expect(parsed.value.maxCommandOutputBytes.source).toBe("default");
const sessions = runWithAnalysisOperationSnapshot(
{
revision: 1,
overrides: { "debugger.command_deadline_ms": 25 },
},
() =>
parseDebuggerSessionLimits({
[DEBUGGER_MAX_SESSIONS_SETTING]: "nope",
}),
);
expect(sessions.ok).toBe(false);
});
it("names the session setting on a Rizin capacity refusal", async () => {
const manager = new RizinDebugSessionManager(
{ [DEBUGGER_MAX_SESSIONS_SETTING]: "1" },
{ platform: "linux" },
);
const first = manager.start({
path: "/tmp/rea-debugger-limit",
inferior_tty: "/no/such/rea-debugger-tty",
});
const blocked = await manager.start({
path: "/tmp/rea-debugger-limit-2",
inferior_tty: "/no/such/rea-debugger-tty",
});
if (blocked.ok) throw new Error("expected the session cap to refuse");
expect(projectAnalysisError(blocked.error)).toMatchObject({
code: "resource_constraint",
details: {
resource: "memory",
reported_limits: {
setting: DEBUGGER_MAX_SESSIONS_SETTING,
effective_value: 1,
source: "setting",
minimum: MINIMUM_DEBUGGER_MAX_SESSIONS,
maximum: MAXIMUM_DEBUGGER_MAX_SESSIONS,
unit: "sessions",
update_mode: "restart",
pending_restart: false,
max_sessions: 1,
active_or_reserved_sessions: 1,
},
},
});
await first;
});
it("keeps the command-output maximum inside one Evidence hash string", () => {
const escaped = JSON.stringify("\u0001");
const expansion = escaped.length - 2;
const nodeMaxStringLength = 536_870_888;
const envelope = 1024 * 1024;
expect(expansion).toBe(6);
expect(MAXIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES).toBe(
Math.floor((nodeMaxStringLength - envelope) / expansion),
);
expect(MAXIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES).toBeGreaterThanOrEqual(
DEFAULT_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES,
);
expect(
MAXIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES * expansion + envelope,
).toBeLessThanOrEqual(nodeMaxStringLength);
});
it("does not let one Rizin manager consume another manager's session cap", async () => {
const first = new RizinDebugSessionManager(
{ [DEBUGGER_MAX_SESSIONS_SETTING]: "1" },
{ platform: "linux" },
);
const second = new RizinDebugSessionManager(
{ [DEBUGGER_MAX_SESSIONS_SETTING]: "1" },
{ platform: "linux" },
);
const input = {
path: "/tmp/rea-debugger-limit",
inferior_tty: "/no/such/rea-debugger-tty",
};
const held = first.start(input);
const independent = second.start(input);
const blocked = first.start({
...input,
path: "/tmp/rea-debugger-limit-2",
});
try {
const independentResult = await independent;
if (independentResult.ok)
throw new Error("expected the missing terminal to fail");
expect(projectAnalysisError(independentResult.error)).toMatchObject({
code: "invalid_request",
details: {
issues: [{ path: ["inferior_tty"], reason: "invalid_value" }],
},
});
const blockedResult = await blocked;
if (blockedResult.ok)
throw new Error("expected the first manager to stay capped");
expect(projectAnalysisError(blockedResult.error)).toMatchObject({
code: "resource_constraint",
details: {
reported_limits: {
setting: DEBUGGER_MAX_SESSIONS_SETTING,
effective_value: 1,
source: "setting",
active_or_reserved_sessions: 1,
},
},
});
} finally {
await held;
await independent;
await blocked;
}
});
it("defers an invalid GDB deadline until a command reads it", () => {
expect(
() =>
new GdbSessionManager({
environment: { [DEBUGGER_COMMAND_DEADLINE_SETTING]: "0" },
}),
).not.toThrow();
const parsed = parseDebuggerSessionLimits({
[DEBUGGER_COMMAND_DEADLINE_SETTING]: "0",
});
if (parsed.ok) throw new Error("invalid deadline was accepted");
expect(projectAnalysisError(parsed.error)).toMatchObject({
code: "configuration_invalid",
details: { settings: parsed.error.settings },
});
const valid = parseDebuggerSessionLimits({
[DEBUGGER_COMMAND_DEADLINE_SETTING]: "1500",
});
if (!valid.ok) throw valid.error;
expect(valid.value.commandDeadline).toMatchObject({
value: 1500,
source: "setting",
});
});