mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 09:52:27 +00:00
fix(settings): parse runtime launch seeds at the operation boundary
Startup no longer rejects a runtime-owned REA_ seed, so update_analysis_settings can replace it. Debugger construction keeps only the restart-only caps, Kuna stores the launch baseline rather than a cached override, and the advertised operations include GDB close, Rizin close, Kuna list_procedures, and the stable-file ChangeTime callers.
This commit is contained in:
1 parent
6ebd033dff
commit
b9be864801
17 files changed
+452
-298
No files matched your search
@@ -1,8 +1,13 @@
|
||||
import { launchAnalysisSetting } from "../config/analysisSettings.js";
|
||||
import {
|
||||
parseEnvironment,
|
||||
parseEnvironmentValues,
|
||||
type Environment,
|
||||
} from "../config/environment.js";
|
||||
import {
|
||||
DEFAULT_GHIDRA_STARTUP_TIMEOUT_MS,
|
||||
DEFAULT_HOPPER_STARTUP_TIMEOUT_MS,
|
||||
} from "../domain/analysisResourceLimits.js";
|
||||
import { ConfigurationError } from "../domain/configurationErrors.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import {
|
||||
@@ -318,6 +323,56 @@ const validatedDetails = (
|
||||
return ok(environmentDetails(environment, validated.value, unresolved));
|
||||
};
|
||||
|
||||
const admittedTimeout = (
|
||||
id: Parameters<typeof launchAnalysisSetting>[0],
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
fallback: number,
|
||||
): number => {
|
||||
const parsed = launchAnalysisSetting(id, environment);
|
||||
return parsed.ok && parsed.value.value !== null
|
||||
? parsed.value.value
|
||||
: fallback;
|
||||
};
|
||||
|
||||
const registrationTimeoutInputs = (
|
||||
environment: Readonly<Record<string, string>>,
|
||||
): Pick<
|
||||
McpToolTimeoutInputs,
|
||||
| "ghidraStartupTimeoutMs"
|
||||
| "hopperStartupTimeoutMs"
|
||||
| "ghidraDecompileTimeoutMs"
|
||||
| "jebMcpTimeoutMs"
|
||||
> => {
|
||||
const decompile = launchAnalysisSetting(
|
||||
"ghidra.decompile_timeout_seconds",
|
||||
environment,
|
||||
);
|
||||
const jeb = launchAnalysisSetting("jeb.mcp_timeout_ms", environment);
|
||||
return {
|
||||
ghidraStartupTimeoutMs: admittedTimeout(
|
||||
"ghidra.startup_timeout_ms",
|
||||
environment,
|
||||
DEFAULT_GHIDRA_STARTUP_TIMEOUT_MS,
|
||||
),
|
||||
hopperStartupTimeoutMs: admittedTimeout(
|
||||
"hopper.startup_timeout_ms",
|
||||
environment,
|
||||
DEFAULT_HOPPER_STARTUP_TIMEOUT_MS,
|
||||
),
|
||||
...(decompile.ok &&
|
||||
decompile.value.source === "setting" &&
|
||||
decompile.value.value !== null
|
||||
? { ghidraDecompileTimeoutMs: decompile.value.value * 1000 }
|
||||
: {}),
|
||||
...(jeb.ok &&
|
||||
jeb.value.source === "setting" &&
|
||||
jeb.value.value !== null &&
|
||||
environment.REA_JEB_MCP_TIMEOUT_MS !== undefined
|
||||
? { jebMcpTimeoutMs: jeb.value.value }
|
||||
: {}),
|
||||
};
|
||||
};
|
||||
|
||||
const environmentDetails = (
|
||||
environment: Readonly<Record<string, string>>,
|
||||
configuration: Environment,
|
||||
@@ -335,17 +390,7 @@ const environmentDetails = (
|
||||
? undefined
|
||||
: {
|
||||
analysisProvider: configuration.REA_ANALYSIS_PROVIDER,
|
||||
ghidraStartupTimeoutMs: configuration.REA_GHIDRA_STARTUP_TIMEOUT_MS,
|
||||
...(configuration.REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS === undefined
|
||||
? {}
|
||||
: {
|
||||
ghidraDecompileTimeoutMs:
|
||||
configuration.REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS * 1000,
|
||||
}),
|
||||
hopperStartupTimeoutMs: configuration.REA_HOPPER_STARTUP_TIMEOUT_MS,
|
||||
...(configuration.REA_JEB_MCP_TIMEOUT_MS === undefined
|
||||
? {}
|
||||
: { jebMcpTimeoutMs: configuration.REA_JEB_MCP_TIMEOUT_MS }),
|
||||
...registrationTimeoutInputs(environment),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+46
-50
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { launchAnalysisSetting } from "./config/analysisSettings.js";
|
||||
import {
|
||||
defaultHopperLauncherPath,
|
||||
inspectNativeAnalysisConfiguration,
|
||||
@@ -21,17 +22,31 @@ it("still rejects a NUL in an unresolved expression", () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
["REA_ASAR_MAX_LISTING_BYTES", "9007199254740992"],
|
||||
["REA_GHIDRA_VALUE_FLOW_MAX_OPERATIONS", "0"],
|
||||
["REA_GHIDRA_VALUE_FLOW_MAX_INPUTS_PER_OPERATION", "2147483648"],
|
||||
["REA_GHIDRA_VALUE_FLOW_MAX_EDGES", "1.5"],
|
||||
["REA_GHIDRA_DECOMPILE_MAX_PAYLOAD_MIB", "2048"],
|
||||
])(
|
||||
"rejects invalid %s at launch with its exact setting identity",
|
||||
(setting, value) => {
|
||||
["asar.max_listing_bytes", "REA_ASAR_MAX_LISTING_BYTES", "9007199254740992"],
|
||||
[
|
||||
"ghidra.value_flow_max_operations",
|
||||
"REA_GHIDRA_VALUE_FLOW_MAX_OPERATIONS",
|
||||
"0",
|
||||
],
|
||||
[
|
||||
"ghidra.value_flow_max_inputs_per_operation",
|
||||
"REA_GHIDRA_VALUE_FLOW_MAX_INPUTS_PER_OPERATION",
|
||||
"2147483648",
|
||||
],
|
||||
["ghidra.value_flow_max_edges", "REA_GHIDRA_VALUE_FLOW_MAX_EDGES", "1.5"],
|
||||
[
|
||||
"ghidra.decompile_max_payload_mib",
|
||||
"REA_GHIDRA_DECOMPILE_MAX_PAYLOAD_MIB",
|
||||
"2048",
|
||||
],
|
||||
] as const)(
|
||||
"defers invalid %s until the operation reads it",
|
||||
(id, setting, value) => {
|
||||
const parsed = parseConfig({ [setting]: value });
|
||||
if (parsed.ok) throw new Error("Invalid resource policy accepted");
|
||||
expect(parsed.error.settings).toContainEqual(
|
||||
if (!parsed.ok) throw new Error("runtime seed rejected startup");
|
||||
const boundary = launchAnalysisSetting(id, { [setting]: value });
|
||||
if (boundary.ok) throw new Error("invalid runtime seed was admitted");
|
||||
expect(boundary.error.settings).toContainEqual(
|
||||
expect.objectContaining({ setting }),
|
||||
);
|
||||
},
|
||||
@@ -200,7 +215,6 @@ describe("runtime configuration", () => {
|
||||
});
|
||||
expect(rejected.issues.map(({ setting }) => setting)).toEqual([
|
||||
"REA_ANALYSIS_PROVIDER",
|
||||
"REA_GHIDRA_STARTUP_TIMEOUT_MS",
|
||||
]);
|
||||
expect(
|
||||
parseConfig({
|
||||
@@ -377,22 +391,16 @@ describe("configuration failure diagnostics", () => {
|
||||
);
|
||||
|
||||
it.each(["", "0", "-1", "1.5", "1e3", "invalid", "2147483648"])(
|
||||
"rejects malformed Hopper startup deadline %j with its setting",
|
||||
"defers a malformed Hopper startup deadline %j until the operation reads it",
|
||||
(value) => {
|
||||
const result = parseConfig({ REA_HOPPER_STARTUP_TIMEOUT_MS: value });
|
||||
if (result.ok) throw new Error("Expected invalid startup deadline");
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "configuration_invalid",
|
||||
category: "invalid_input",
|
||||
details: {
|
||||
settings: [
|
||||
{
|
||||
setting: "REA_HOPPER_STARTUP_TIMEOUT_MS",
|
||||
constraint: expect.any(String),
|
||||
},
|
||||
],
|
||||
},
|
||||
if (!result.ok) throw new Error("runtime seed rejected startup");
|
||||
expect(result.value.hopperStartupTimeoutMs).toBe(120_000);
|
||||
expect(result.value.hopperStartupTimeoutSource).toBe("default");
|
||||
const boundary = launchAnalysisSetting("hopper.startup_timeout_ms", {
|
||||
REA_HOPPER_STARTUP_TIMEOUT_MS: value,
|
||||
});
|
||||
if (boundary.ok) throw new Error("invalid Hopper deadline was admitted");
|
||||
},
|
||||
);
|
||||
|
||||
@@ -411,19 +419,12 @@ describe("configuration failure diagnostics", () => {
|
||||
"rejects malformed Ghidra startup deadline %j with its setting and constraint",
|
||||
(value) => {
|
||||
const result = parseConfig({ REA_GHIDRA_STARTUP_TIMEOUT_MS: value });
|
||||
if (result.ok) throw new Error("Expected invalid startup deadline");
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "configuration_invalid",
|
||||
category: "invalid_input",
|
||||
details: {
|
||||
settings: [
|
||||
{
|
||||
setting: "REA_GHIDRA_STARTUP_TIMEOUT_MS",
|
||||
constraint: expect.any(String),
|
||||
},
|
||||
],
|
||||
},
|
||||
if (!result.ok) throw new Error("runtime seed rejected startup");
|
||||
expect(result.value.ghidraStartupTimeoutMs).toBe(330_000);
|
||||
const boundary = launchAnalysisSetting("ghidra.startup_timeout_ms", {
|
||||
REA_GHIDRA_STARTUP_TIMEOUT_MS: value,
|
||||
});
|
||||
if (boundary.ok) throw new Error("invalid Ghidra deadline was admitted");
|
||||
},
|
||||
);
|
||||
|
||||
@@ -524,8 +525,8 @@ describe("JEB MCP timeout configuration", () => {
|
||||
value: 900_000,
|
||||
});
|
||||
expect(
|
||||
parseEnvironmentValues({ REA_JEB_MCP_TIMEOUT_MS: "900000" }),
|
||||
).toMatchObject({ ok: true, value: { REA_JEB_MCP_TIMEOUT_MS: 900_000 } });
|
||||
parseEnvironmentValues({ REA_JEB_MCP_TIMEOUT_MS: "900000" }).ok,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it.each(["", "0", "-1", "1.5", "1e3", "invalid", "2147483648"])(
|
||||
@@ -641,18 +642,13 @@ describe("Ghidra decompile limit configuration", () => {
|
||||
const result = parseConfig({
|
||||
REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS: value,
|
||||
});
|
||||
if (result.ok) throw new Error("Expected invalid decompile limit");
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "configuration_invalid",
|
||||
details: {
|
||||
settings: [
|
||||
{
|
||||
setting: "REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS",
|
||||
constraint: expect.any(String),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
if (!result.ok) throw new Error("runtime seed rejected startup");
|
||||
expect(result.value.ghidraDecompileTimeoutSeconds).toBeUndefined();
|
||||
const boundary = launchAnalysisSetting(
|
||||
"ghidra.decompile_timeout_seconds",
|
||||
{ REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS: value },
|
||||
);
|
||||
if (boundary.ok) throw new Error("invalid decompile limit was admitted");
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -49,7 +49,7 @@ it("keeps the 16 MiB ceiling when the setting is unset", () => {
|
||||
);
|
||||
const environment = parseEnvironment({});
|
||||
if (!environment.ok) throw new Error("doctor rejected an unset ceiling");
|
||||
expect(environment.value.REA_BRIDGE_MAX_REQUEST_BYTES).toBeUndefined();
|
||||
expect(environment.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts a caller-selected ceiling inside the floor and maximum", () => {
|
||||
@@ -62,7 +62,7 @@ it("accepts a caller-selected ceiling inside the floor and maximum", () => {
|
||||
[BRIDGE_MAX_REQUEST_BYTES_SETTING]: "8192",
|
||||
});
|
||||
if (!environment.ok) throw new Error("doctor rejected a valid ceiling");
|
||||
expect(environment.value.REA_BRIDGE_MAX_REQUEST_BYTES).toBe(8192);
|
||||
expect(environment.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("sets the maximum to the serialized string bound minus the framing LF", () => {
|
||||
@@ -153,16 +153,14 @@ it.each([
|
||||
const doctor = parseEnvironment({
|
||||
[BRIDGE_MAX_REQUEST_BYTES_SETTING]: value,
|
||||
});
|
||||
if (startup.ok) throw new Error(`Startup accepted ${JSON.stringify(value)}`);
|
||||
expect(startup.error._tag).toBe("ConfigurationError");
|
||||
expect(startup.error.settings).toContainEqual({
|
||||
setting: BRIDGE_MAX_REQUEST_BYTES_SETTING,
|
||||
constraint: parsed.error.message,
|
||||
});
|
||||
if (doctor.ok) throw new Error(`Doctor accepted ${JSON.stringify(value)}`);
|
||||
expect(doctor.error._tag).toBe("ConfigurationError");
|
||||
expect(doctor.error.settings).toContainEqual({
|
||||
setting: BRIDGE_MAX_REQUEST_BYTES_SETTING,
|
||||
constraint: parsed.error.message,
|
||||
});
|
||||
if (!startup.ok)
|
||||
throw new Error(`Startup rejected a runtime seed ${JSON.stringify(value)}`);
|
||||
expect(startup.value.bridgeMaxRequestBytes).toEqual(
|
||||
DEFAULT_BRIDGE_MAX_REQUEST,
|
||||
);
|
||||
if (!doctor.ok)
|
||||
throw new Error(`Doctor rejected a runtime seed ${JSON.stringify(value)}`);
|
||||
expect(() => parseBridgeMaxRequestBytes(value)).not.toThrow();
|
||||
if (parseBridgeMaxRequestBytes(value).ok)
|
||||
throw new Error(`Bridge parser accepted ${JSON.stringify(value)}`);
|
||||
});
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
CAPTURE_DECODER_OLD_GENERATION_SETTING,
|
||||
DEFAULT_CAPTURE_DECODER_HEAP,
|
||||
parseCaptureDecoderHeap,
|
||||
resolveCaptureDecoderHeap,
|
||||
} from "./captureDecoderHeap.js";
|
||||
import { MAXIMUM_CAPTURE_DECODER_OLD_GENERATION_MIB } from "../domain/analysisResourceLimits.js";
|
||||
|
||||
@@ -258,20 +259,20 @@ it.each([
|
||||
const startup = parseEnvironment({
|
||||
REA_CAPTURE_DECODER_OLD_GENERATION_MIB: value,
|
||||
});
|
||||
if (startup.ok) throw new Error(`Startup accepted ${JSON.stringify(value)}`);
|
||||
expect(startup.error.settings).toContainEqual({
|
||||
setting: CAPTURE_DECODER_OLD_GENERATION_SETTING,
|
||||
constraint: parsed.error.message,
|
||||
});
|
||||
if (!startup.ok)
|
||||
throw new Error(`Startup rejected a runtime seed ${JSON.stringify(value)}`);
|
||||
const config = parseConfig({
|
||||
REA_CAPTURE_DECODER_OLD_GENERATION_MIB: value,
|
||||
});
|
||||
if (config.ok)
|
||||
throw new Error(`parseConfig accepted ${JSON.stringify(value)}`);
|
||||
expect(projectAnalysisError(config.error)).toMatchObject({
|
||||
code: "configuration_invalid",
|
||||
message: expect.stringContaining("REA_CAPTURE_DECODER_OLD_GENERATION_MIB"),
|
||||
});
|
||||
if (!config.ok)
|
||||
throw new Error(
|
||||
`parseConfig rejected a runtime seed ${JSON.stringify(value)}`,
|
||||
);
|
||||
expect(() =>
|
||||
resolveCaptureDecoderHeap({
|
||||
REA_CAPTURE_DECODER_OLD_GENERATION_MIB: value,
|
||||
}),
|
||||
).toThrow(ConfigurationError);
|
||||
expect(
|
||||
() =>
|
||||
new NodeCaptureAdapter("har", {
|
||||
|
||||
@@ -139,11 +139,18 @@ it.each([
|
||||
);
|
||||
expect(detail.constraint).toContain(`default is ${defaultValue} ${unit}`);
|
||||
const startup = parseEnvironment({ [setting]: value });
|
||||
if (startup.ok) throw new Error(`${setting} was accepted at startup`);
|
||||
expect(projectAnalysisError(startup.error)).toMatchObject({
|
||||
code: "configuration_invalid",
|
||||
details: { settings: [detail] },
|
||||
});
|
||||
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] },
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -280,30 +287,21 @@ it("does not let one Rizin manager consume another manager's session cap", async
|
||||
}
|
||||
});
|
||||
|
||||
it("refuses an invalid GDB deadline before a session starts", () => {
|
||||
it("defers an invalid GDB deadline until a command reads it", () => {
|
||||
expect(
|
||||
() =>
|
||||
new GdbSessionManager({
|
||||
environment: { [DEBUGGER_COMMAND_DEADLINE_SETTING]: "0" },
|
||||
}),
|
||||
).toThrow(ConfigurationError);
|
||||
).not.toThrow();
|
||||
const parsed = parseDebuggerSessionLimits({
|
||||
[DEBUGGER_COMMAND_DEADLINE_SETTING]: "0",
|
||||
});
|
||||
if (parsed.ok) throw new Error("invalid deadline was accepted");
|
||||
try {
|
||||
new GdbSessionManager({
|
||||
environment: { [DEBUGGER_COMMAND_DEADLINE_SETTING]: "0" },
|
||||
});
|
||||
} catch (error) {
|
||||
if (!(error instanceof ConfigurationError)) throw error;
|
||||
expect(projectAnalysisError(error)).toMatchObject({
|
||||
code: "configuration_invalid",
|
||||
details: {
|
||||
settings: parsed.error.settings,
|
||||
},
|
||||
});
|
||||
}
|
||||
expect(projectAnalysisError(parsed.error)).toMatchObject({
|
||||
code: "configuration_invalid",
|
||||
details: { settings: parsed.error.settings },
|
||||
});
|
||||
expect(
|
||||
resolveDebuggerSessionLimits({
|
||||
[DEBUGGER_COMMAND_DEADLINE_SETTING]: "1500",
|
||||
|
||||
@@ -133,14 +133,11 @@ export const debuggerMaxCommandOutputBytesSchema = positiveDecimalIntegerSchema(
|
||||
);
|
||||
|
||||
/** Schemas startup validation spreads. The setting names are not restated. */
|
||||
/** Restart-only debugger caps. Runtime deadline and output are parsed per call. */
|
||||
export const debuggerSessionLimitEnvironmentShape = {
|
||||
[DEBUGGER_COMMAND_DEADLINE_SETTING]:
|
||||
debuggerCommandDeadlineMsSchema.optional(),
|
||||
[DEBUGGER_MAX_SESSIONS_SETTING]: debuggerMaxSessionsSchema.optional(),
|
||||
[DEBUGGER_MAX_PENDING_COMMANDS_SETTING]:
|
||||
debuggerMaxPendingCommandsSchema.optional(),
|
||||
[DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING]:
|
||||
debuggerMaxCommandOutputBytesSchema.optional(),
|
||||
};
|
||||
|
||||
export interface DebuggerSessionLimits {
|
||||
@@ -236,6 +233,32 @@ export const parseDebuggerSessionLimits = (
|
||||
});
|
||||
};
|
||||
|
||||
const RUNTIME_DEBUGGER_SETTINGS = new Set<string>([
|
||||
DEBUGGER_COMMAND_DEADLINE_SETTING,
|
||||
DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Session and pending caps still fail construction. An invalid runtime deadline
|
||||
* or output cap does not: the next command reads the snapshot or the launch seed.
|
||||
*/
|
||||
export const debuggerConstructionLimits = (
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
): DebuggerSessionLimits => {
|
||||
const parsed = parseDebuggerSessionLimits(environment);
|
||||
if (parsed.ok) return parsed.value;
|
||||
const blockers = parsed.error.settings.filter(
|
||||
(setting) => !RUNTIME_DEBUGGER_SETTINGS.has(setting.setting),
|
||||
);
|
||||
if (blockers.length > 0) throw parsed.error;
|
||||
const startup = { ...environment };
|
||||
delete startup[DEBUGGER_COMMAND_DEADLINE_SETTING];
|
||||
delete startup[DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING];
|
||||
const again = parseDebuggerSessionLimits(startup);
|
||||
if (!again.ok) throw again.error;
|
||||
return again.value;
|
||||
};
|
||||
|
||||
/** Refuse an invalid launch cap instead of falling back to the default. */
|
||||
export const resolveDebuggerSessionLimits = (
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
|
||||
@@ -19,34 +19,13 @@ import {
|
||||
import { ghidraDisabledAnalyzersSchema } from "./ghidraDisabledAnalyzers.js";
|
||||
import {
|
||||
DEFAULT_SWIFTC_COMPILE_TIMEOUT_MS,
|
||||
ghidraDecompileTimeoutSchema,
|
||||
providerStartupTimeoutSchema,
|
||||
} from "./providerStartupTimeout.js";
|
||||
import {
|
||||
DEFAULT_GHIDRA_STARTUP_TIMEOUT_MS,
|
||||
DEFAULT_HOPPER_CALLER_SCAN_BUDGET_MS,
|
||||
DEFAULT_HOPPER_STARTUP_TIMEOUT_MS,
|
||||
} from "../domain/analysisResourceLimits.js";
|
||||
import { mcpInputSchemaProfileSchema } from "./mcpInputSchemaProfile.js";
|
||||
import { mcpOutputSchemaProfileSchema } from "./mcpOutputSchemaProfile.js";
|
||||
import { mcpToolCatalogProfileSchema } from "./mcpToolCatalogProfile.js";
|
||||
import { mcpResponseBudgetSchema } from "./mcpResponseBudget.js";
|
||||
import { asarMaxHeaderBytesSchema } from "./asarHeaderAdmission.js";
|
||||
import { asarMaxListingBytesSchema } from "./asarListingAdmission.js";
|
||||
import {
|
||||
ghidraValueFlowCountSchema,
|
||||
ghidraDecompilePayloadMiBSchema,
|
||||
} from "./ghidraValueFlowLimits.js";
|
||||
import { bridgeMaxRequestBytesSchema } from "./bridgeMaxRequestBytes.js";
|
||||
import { captureDecoderOldGenerationMiBSchema } from "./captureDecoderHeap.js";
|
||||
import { debuggerSessionLimitEnvironmentShape } from "./debuggerSessionLimits.js";
|
||||
import { regexMatchingDeadlineMsSchema } from "./regexMatchingDeadline.js";
|
||||
import { regexWorkerHeapRequestMiBSchema } from "./regexWorkerHeapRequest.js";
|
||||
import {
|
||||
windowsChangeTimeDriftMsSchema,
|
||||
windowsChangeTimeSettleMaxPollsSchema,
|
||||
windowsChangeTimeSettlePollMsSchema,
|
||||
} from "./windowsChangeTimeSettle.js";
|
||||
import {
|
||||
mcpDoctorDeadlineMsSchema,
|
||||
mcpFirstCatalogBudgetMsSchema,
|
||||
@@ -61,26 +40,8 @@ import { parseJebMcpEndpoint } from "./jebMcpEndpoint.js";
|
||||
const environmentSchema = z.object({
|
||||
...debuggerSessionLimitEnvironmentShape,
|
||||
REA_ANALYSIS_PROVIDER: analysisProviderSelectorSchema.default("auto"),
|
||||
REA_BRIDGE_MAX_REQUEST_BYTES: bridgeMaxRequestBytesSchema.optional(),
|
||||
REA_MCP_MAX_RESPONSE_BYTES: mcpResponseBudgetSchema.optional(),
|
||||
REA_MCP_MAX_INPUT_ELEMENTS: mcpInputBudgetSchema.optional(),
|
||||
REA_ASAR_MAX_HEADER_BYTES: asarMaxHeaderBytesSchema.optional(),
|
||||
REA_ASAR_MAX_LISTING_BYTES: asarMaxListingBytesSchema.optional(),
|
||||
REA_GHIDRA_VALUE_FLOW_MAX_OPERATIONS: ghidraValueFlowCountSchema.optional(),
|
||||
REA_GHIDRA_VALUE_FLOW_MAX_INPUTS_PER_OPERATION:
|
||||
ghidraValueFlowCountSchema.optional(),
|
||||
REA_GHIDRA_VALUE_FLOW_MAX_EDGES: ghidraValueFlowCountSchema.optional(),
|
||||
REA_GHIDRA_DECOMPILE_MAX_PAYLOAD_MIB:
|
||||
ghidraDecompilePayloadMiBSchema.optional(),
|
||||
REA_CAPTURE_DECODER_OLD_GENERATION_MIB:
|
||||
captureDecoderOldGenerationMiBSchema.optional(),
|
||||
REA_REGEX_MATCHING_DEADLINE_MS: regexMatchingDeadlineMsSchema.optional(),
|
||||
REA_REGEX_WORKER_HEAP_REQUEST_MIB: regexWorkerHeapRequestMiBSchema.optional(),
|
||||
REA_WINDOWS_CHANGE_TIME_DRIFT_MS: windowsChangeTimeDriftMsSchema.optional(),
|
||||
REA_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS:
|
||||
windowsChangeTimeSettlePollMsSchema.optional(),
|
||||
REA_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS:
|
||||
windowsChangeTimeSettleMaxPollsSchema.optional(),
|
||||
REA_MCP_INITIALIZE_BUDGET_MS: mcpInitializeBudgetMsSchema.optional(),
|
||||
REA_MCP_TOOLS_LIST_BUDGET_MS: mcpToolsListBudgetMsSchema.optional(),
|
||||
REA_MCP_FIRST_CATALOG_BUDGET_MS: mcpFirstCatalogBudgetMsSchema.optional(),
|
||||
@@ -94,7 +55,6 @@ const environmentSchema = z.object({
|
||||
.min(1)
|
||||
.refine(isAbsolute, "REA_IDA_MCP_CONFIG must be absolute")
|
||||
.optional(),
|
||||
REA_JEB_MCP_TIMEOUT_MS: providerStartupTimeoutSchema.optional(),
|
||||
REA_JEB_MCP_URL: z
|
||||
.string()
|
||||
.superRefine((value, context) => {
|
||||
@@ -124,19 +84,9 @@ const environmentSchema = z.object({
|
||||
.min(1)
|
||||
.refine(isAbsolute, "REA_GHIDRA_NATIVEAOT_JAR must be absolute")
|
||||
.optional(),
|
||||
REA_GHIDRA_STARTUP_TIMEOUT_MS: providerStartupTimeoutSchema.default(
|
||||
DEFAULT_GHIDRA_STARTUP_TIMEOUT_MS,
|
||||
),
|
||||
REA_HOPPER_STARTUP_TIMEOUT_MS: providerStartupTimeoutSchema.default(
|
||||
DEFAULT_HOPPER_STARTUP_TIMEOUT_MS,
|
||||
),
|
||||
REA_HOPPER_CALLER_SCAN_BUDGET_MS: providerStartupTimeoutSchema.default(
|
||||
DEFAULT_HOPPER_CALLER_SCAN_BUDGET_MS,
|
||||
),
|
||||
REA_SWIFTC_COMPILE_TIMEOUT_MS: providerStartupTimeoutSchema.default(
|
||||
DEFAULT_SWIFTC_COMPILE_TIMEOUT_MS,
|
||||
),
|
||||
REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS: ghidraDecompileTimeoutSchema.optional(),
|
||||
REA_GHIDRA_PROJECT_PATH: ghidraProjectPathSchema.optional(),
|
||||
REA_GHIDRA_CHECKPOINT_DIR: z
|
||||
.string()
|
||||
|
||||
+61
-37
@@ -5,9 +5,16 @@ import {
|
||||
type ConfigurationSettingIssue,
|
||||
} from "../domain/configurationErrors.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { launchAnalysisSetting } from "./analysisSettings.js";
|
||||
import type { AnalysisSettingId } from "../domain/analysisSettings.js";
|
||||
import { parseEnvironment } from "./environment.js";
|
||||
import { providerStartupTimeoutSchema } from "./providerStartupTimeout.js";
|
||||
import { homeDirectoryFromEnvironment } from "./homeDirectory.js";
|
||||
import {
|
||||
DEFAULT_GHIDRA_STARTUP_TIMEOUT_MS,
|
||||
DEFAULT_HOPPER_CALLER_SCAN_BUDGET_MS,
|
||||
DEFAULT_HOPPER_STARTUP_TIMEOUT_MS,
|
||||
} from "../domain/analysisResourceLimits.js";
|
||||
import { DEFAULT_BRIDGE_MAX_REQUEST } from "./bridgeMaxRequestBytes.js";
|
||||
import { hopperCallerScanBudgetFromEnvironment } from "./hopperCallerScanBudget.js";
|
||||
import { DEFAULT_REGEX_MATCHING_DEADLINE } from "./regexMatchingDeadline.js";
|
||||
@@ -70,12 +77,14 @@ export const inspectNativeAnalysisConfiguration = (
|
||||
const issues = parsed.ok ? [] : parsed.error.settings;
|
||||
const configuredTimeout = environment.REA_HOPPER_STARTUP_TIMEOUT_MS;
|
||||
if (configuredTimeout === undefined) return { issues };
|
||||
if (parsed.ok)
|
||||
const admitted = launchAnalysisSetting(
|
||||
"hopper.startup_timeout_ms",
|
||||
environment,
|
||||
);
|
||||
if (parsed.ok && admitted.ok && admitted.value.value !== null)
|
||||
return {
|
||||
issues,
|
||||
configuredHopperStartupTimeout: ok(
|
||||
parsed.value.REA_HOPPER_STARTUP_TIMEOUT_MS,
|
||||
),
|
||||
configuredHopperStartupTimeout: ok(admitted.value.value),
|
||||
};
|
||||
// Another setting may reject the aggregate before this supplied timeout's
|
||||
// facts are available. Its dedicated diagnostic still uses the same schema.
|
||||
@@ -103,6 +112,23 @@ export const parseConfig = (
|
||||
const parsed = parseEnvironment(nativeAnalysisEnvironment(environment));
|
||||
if (!parsed.ok) return parsed;
|
||||
const env = parsed.value;
|
||||
const admitted = (id: AnalysisSettingId): number | null | undefined => {
|
||||
const result = launchAnalysisSetting(id, environment);
|
||||
return result.ok ? result.value.value : undefined;
|
||||
};
|
||||
const ghidraStartup = admitted("ghidra.startup_timeout_ms");
|
||||
const hopperStartup = admitted("hopper.startup_timeout_ms");
|
||||
const decompileTimeout = admitted("ghidra.decompile_timeout_seconds");
|
||||
const decompilePayload = admitted("ghidra.decompile_max_payload_mib");
|
||||
const valueFlowOperations = admitted("ghidra.value_flow_max_operations");
|
||||
const valueFlowInputs = admitted(
|
||||
"ghidra.value_flow_max_inputs_per_operation",
|
||||
);
|
||||
const valueFlowEdges = admitted("ghidra.value_flow_max_edges");
|
||||
const callerScan = admitted("hopper.caller_scan_budget_ms");
|
||||
const bridgeBytes = admitted("bridge.max_request_bytes");
|
||||
const regexDeadline = admitted("regex.matching_deadline_ms");
|
||||
const regexHeap = admitted("regex.worker_heap_request_mib");
|
||||
return ok({
|
||||
...(env.REA_MCP_TOOL_FAMILIES === undefined
|
||||
? {}
|
||||
@@ -141,34 +167,22 @@ export const parseConfig = (
|
||||
? {}
|
||||
: { ghidraCheckpointDirectory: env.REA_GHIDRA_CHECKPOINT_DIR }),
|
||||
ghidraJavaHome: env.JAVA_HOME,
|
||||
ghidraStartupTimeoutMs: env.REA_GHIDRA_STARTUP_TIMEOUT_MS,
|
||||
...(env.REA_GHIDRA_DECOMPILE_MAX_PAYLOAD_MIB === undefined
|
||||
ghidraStartupTimeoutMs: ghidraStartup ?? DEFAULT_GHIDRA_STARTUP_TIMEOUT_MS,
|
||||
...(decompilePayload === undefined || decompilePayload === null
|
||||
? {}
|
||||
: {
|
||||
ghidraDecompileMaxPayloadMiB:
|
||||
env.REA_GHIDRA_DECOMPILE_MAX_PAYLOAD_MIB,
|
||||
}),
|
||||
...(env.REA_GHIDRA_VALUE_FLOW_MAX_OPERATIONS === undefined
|
||||
: { ghidraDecompileMaxPayloadMiB: decompilePayload }),
|
||||
...(valueFlowOperations === undefined || valueFlowOperations === null
|
||||
? {}
|
||||
: {
|
||||
ghidraValueFlowMaxOperations:
|
||||
env.REA_GHIDRA_VALUE_FLOW_MAX_OPERATIONS,
|
||||
}),
|
||||
...(env.REA_GHIDRA_VALUE_FLOW_MAX_INPUTS_PER_OPERATION === undefined
|
||||
: { ghidraValueFlowMaxOperations: valueFlowOperations }),
|
||||
...(valueFlowInputs === undefined || valueFlowInputs === null
|
||||
? {}
|
||||
: {
|
||||
ghidraValueFlowMaxInputsPerOperation:
|
||||
env.REA_GHIDRA_VALUE_FLOW_MAX_INPUTS_PER_OPERATION,
|
||||
}),
|
||||
...(env.REA_GHIDRA_VALUE_FLOW_MAX_EDGES === undefined
|
||||
: { ghidraValueFlowMaxInputsPerOperation: valueFlowInputs }),
|
||||
...(valueFlowEdges === undefined || valueFlowEdges === null
|
||||
? {}
|
||||
: { ghidraValueFlowMaxEdges: env.REA_GHIDRA_VALUE_FLOW_MAX_EDGES }),
|
||||
...(env.REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS === undefined
|
||||
: { ghidraValueFlowMaxEdges: valueFlowEdges }),
|
||||
...(decompileTimeout === undefined || decompileTimeout === null
|
||||
? {}
|
||||
: {
|
||||
ghidraDecompileTimeoutSeconds:
|
||||
env.REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS,
|
||||
}),
|
||||
: { ghidraDecompileTimeoutSeconds: decompileTimeout }),
|
||||
...(env.REA_GHIDRA_LANGUAGE_ID === undefined
|
||||
? {}
|
||||
: {
|
||||
@@ -195,14 +209,18 @@ export const parseConfig = (
|
||||
process.platform,
|
||||
homeDirectoryFromEnvironment(environment, process.platform),
|
||||
),
|
||||
hopperStartupTimeoutMs: env.REA_HOPPER_STARTUP_TIMEOUT_MS,
|
||||
hopperStartupTimeoutMs: hopperStartup ?? DEFAULT_HOPPER_STARTUP_TIMEOUT_MS,
|
||||
hopperStartupTimeoutSource:
|
||||
environment.REA_HOPPER_STARTUP_TIMEOUT_MS === undefined
|
||||
environment.REA_HOPPER_STARTUP_TIMEOUT_MS === undefined ||
|
||||
hopperStartup === undefined
|
||||
? "default"
|
||||
: "setting",
|
||||
hopperCallerScanBudget: hopperCallerScanBudgetFromEnvironment(
|
||||
environment.REA_HOPPER_CALLER_SCAN_BUDGET_MS,
|
||||
env.REA_HOPPER_CALLER_SCAN_BUDGET_MS,
|
||||
callerScan === undefined ||
|
||||
environment.REA_HOPPER_CALLER_SCAN_BUDGET_MS === undefined
|
||||
? undefined
|
||||
: environment.REA_HOPPER_CALLER_SCAN_BUDGET_MS,
|
||||
callerScan ?? DEFAULT_HOPPER_CALLER_SCAN_BUDGET_MS,
|
||||
),
|
||||
hopperTargetPath: env.HOPPER_TARGET_PATH,
|
||||
hopperTargetKind: env.HOPPER_TARGET_KIND,
|
||||
@@ -212,24 +230,30 @@ export const parseConfig = (
|
||||
secretPatterns: env.referenceSecretPatterns,
|
||||
},
|
||||
bridgeMaxRequestBytes:
|
||||
env.REA_BRIDGE_MAX_REQUEST_BYTES === undefined
|
||||
bridgeBytes === undefined ||
|
||||
bridgeBytes === null ||
|
||||
environment.REA_BRIDGE_MAX_REQUEST_BYTES === undefined
|
||||
? DEFAULT_BRIDGE_MAX_REQUEST
|
||||
: {
|
||||
maximumBytes: env.REA_BRIDGE_MAX_REQUEST_BYTES,
|
||||
maximumBytes: bridgeBytes,
|
||||
source: "setting",
|
||||
},
|
||||
regexMatchingDeadline:
|
||||
env.REA_REGEX_MATCHING_DEADLINE_MS === undefined
|
||||
regexDeadline === undefined ||
|
||||
regexDeadline === null ||
|
||||
environment.REA_REGEX_MATCHING_DEADLINE_MS === undefined
|
||||
? DEFAULT_REGEX_MATCHING_DEADLINE
|
||||
: {
|
||||
deadlineMs: env.REA_REGEX_MATCHING_DEADLINE_MS,
|
||||
deadlineMs: regexDeadline,
|
||||
source: "setting",
|
||||
},
|
||||
regexWorkerHeapRequest:
|
||||
env.REA_REGEX_WORKER_HEAP_REQUEST_MIB === undefined
|
||||
regexHeap === undefined ||
|
||||
regexHeap === null ||
|
||||
environment.REA_REGEX_WORKER_HEAP_REQUEST_MIB === undefined
|
||||
? DEFAULT_REGEX_WORKER_HEAP_REQUEST
|
||||
: {
|
||||
heapMiB: env.REA_REGEX_WORKER_HEAP_REQUEST_MIB,
|
||||
heapMiB: regexHeap,
|
||||
source: "setting",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { parseConfig } from "./parseConfig.js";
|
||||
import {
|
||||
DEFAULT_REGEX_MATCHING_DEADLINE,
|
||||
parseRegexMatchingDeadline,
|
||||
resolveRegexMatchingDeadline,
|
||||
REGEX_MATCHING_DEADLINE_SETTING,
|
||||
regexMatchingDeadlineContinuingRecovery,
|
||||
regexMatchingDeadlineRecovery,
|
||||
@@ -153,19 +154,17 @@ it.each([
|
||||
},
|
||||
]);
|
||||
const startup = parseEnvironment({ REA_REGEX_MATCHING_DEADLINE_MS: value });
|
||||
if (startup.ok) throw new Error(`Startup accepted ${JSON.stringify(value)}`);
|
||||
expect(startup.error.settings).toContainEqual({
|
||||
setting: REGEX_MATCHING_DEADLINE_SETTING,
|
||||
constraint: parsed.error.message,
|
||||
});
|
||||
if (!startup.ok)
|
||||
throw new Error(`Startup rejected a runtime seed ${JSON.stringify(value)}`);
|
||||
const config = parseConfig({ REA_REGEX_MATCHING_DEADLINE_MS: value });
|
||||
if (config.ok)
|
||||
throw new Error(`parseConfig accepted ${JSON.stringify(value)}`);
|
||||
expect(projectAnalysisError(config.error)).toMatchObject({
|
||||
code: "configuration_invalid",
|
||||
message: expect.stringContaining("REA_REGEX_MATCHING_DEADLINE_MS"),
|
||||
});
|
||||
expect(
|
||||
() => new KunaProvider({ REA_REGEX_MATCHING_DEADLINE_MS: value }),
|
||||
if (!config.ok)
|
||||
throw new Error(
|
||||
`parseConfig rejected a runtime seed ${JSON.stringify(value)}`,
|
||||
);
|
||||
expect(config.value.regexMatchingDeadline).toEqual(
|
||||
DEFAULT_REGEX_MATCHING_DEADLINE,
|
||||
);
|
||||
expect(() =>
|
||||
resolveRegexMatchingDeadline({ REA_REGEX_MATCHING_DEADLINE_MS: value }),
|
||||
).toThrow(ConfigurationError);
|
||||
});
|
||||
@@ -11,6 +11,7 @@ import { parseConfig } from "./parseConfig.js";
|
||||
import {
|
||||
DEFAULT_REGEX_WORKER_HEAP_REQUEST,
|
||||
parseRegexWorkerHeapRequest,
|
||||
resolveRegexWorkerHeapRequest,
|
||||
REGEX_WORKER_HEAP_REQUEST_SETTING,
|
||||
regexWorkerHeapRequestContinuingRecovery,
|
||||
regexWorkerHeapRequestRecovery,
|
||||
@@ -168,18 +169,21 @@ it.each([
|
||||
const startup = parseEnvironment({
|
||||
REA_REGEX_WORKER_HEAP_REQUEST_MIB: value,
|
||||
});
|
||||
if (startup.ok) throw new Error(`Startup accepted ${JSON.stringify(value)}`);
|
||||
expect(startup.error.settings).toContainEqual({
|
||||
setting: REGEX_WORKER_HEAP_REQUEST_SETTING,
|
||||
constraint: parsed.error.message,
|
||||
});
|
||||
if (!startup.ok)
|
||||
throw new Error(`Startup rejected a runtime seed ${JSON.stringify(value)}`);
|
||||
const config = parseConfig({ REA_REGEX_WORKER_HEAP_REQUEST_MIB: value });
|
||||
if (config.ok)
|
||||
throw new Error(`parseConfig accepted ${JSON.stringify(value)}`);
|
||||
expect(projectAnalysisError(config.error)).toMatchObject({
|
||||
code: "configuration_invalid",
|
||||
message: expect.stringContaining("REA_REGEX_WORKER_HEAP_REQUEST_MIB"),
|
||||
});
|
||||
if (!config.ok)
|
||||
throw new Error(
|
||||
`parseConfig rejected a runtime seed ${JSON.stringify(value)}`,
|
||||
);
|
||||
expect(config.value.regexWorkerHeapRequest).toEqual(
|
||||
DEFAULT_REGEX_WORKER_HEAP_REQUEST,
|
||||
);
|
||||
expect(() =>
|
||||
resolveRegexWorkerHeapRequest({
|
||||
REA_REGEX_WORKER_HEAP_REQUEST_MIB: value,
|
||||
}),
|
||||
).toThrow(ConfigurationError);
|
||||
expect(
|
||||
() => new KunaProvider({ REA_REGEX_WORKER_HEAP_REQUEST_MIB: value }),
|
||||
).toThrow(ConfigurationError);
|
||||
|
||||
@@ -46,11 +46,7 @@ it("keeps today's settle defaults when the settings are unset", () => {
|
||||
});
|
||||
const startup = parseEnvironment({});
|
||||
if (!startup.ok) throw new Error("unset settle settings were rejected");
|
||||
expect(startup.value.REA_WINDOWS_CHANGE_TIME_DRIFT_MS).toBeUndefined();
|
||||
expect(startup.value.REA_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS).toBeUndefined();
|
||||
expect(
|
||||
startup.value.REA_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS,
|
||||
).toBeUndefined();
|
||||
expect(startup.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("uses valid settle overrides", () => {
|
||||
@@ -77,9 +73,7 @@ it("uses valid settle overrides", () => {
|
||||
REA_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS: "1000",
|
||||
});
|
||||
if (!startup.ok) throw new Error("valid settle settings were rejected");
|
||||
expect(startup.value.REA_WINDOWS_CHANGE_TIME_DRIFT_MS).toBe(60_000);
|
||||
expect(startup.value.REA_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS).toBe(1);
|
||||
expect(startup.value.REA_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS).toBe(1_000);
|
||||
expect(startup.ok).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -113,12 +107,8 @@ it.each([
|
||||
details: { settings: [facts] },
|
||||
});
|
||||
const startup = parseEnvironment({ [bound.setting]: value });
|
||||
if (startup.ok) throw new Error(`Startup accepted ${bound.setting}=${value}`);
|
||||
expect(startup.error.settings).toContainEqual(facts);
|
||||
expect(startup.error.settings[0]).not.toHaveProperty("pending_restart");
|
||||
expect(projectAnalysisError(startup.error).details).toMatchObject({
|
||||
settings: [facts],
|
||||
});
|
||||
if (!startup.ok)
|
||||
throw new Error(`Startup rejected runtime seed ${bound.setting}`);
|
||||
});
|
||||
|
||||
it("applies a Windows settle override before rejecting the launch value", () => {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import {
|
||||
analysisSettingDefinition,
|
||||
runtimeAnalysisEnvironmentNames,
|
||||
} from "./analysisSettingDefinitions.js";
|
||||
|
||||
const source = (path: string) =>
|
||||
readFileSync(join(import.meta.dirname, path), "utf8");
|
||||
|
||||
it("does not validate runtime-owned launch seeds at startup", () => {
|
||||
const startup = [
|
||||
source("../config/environment.ts"),
|
||||
source("../config/debuggerSessionLimits.ts"),
|
||||
].join("\n");
|
||||
const mentioned = [...runtimeAnalysisEnvironmentNames()].filter(
|
||||
(name) => startup.includes(`${name}:`) || startup.includes(`[${name}]`),
|
||||
);
|
||||
expect(mentioned).toEqual([]);
|
||||
});
|
||||
|
||||
it("advertises the operations that consume each migrated setting", () => {
|
||||
const operations = (id: Parameters<typeof analysisSettingDefinition>[0]) =>
|
||||
analysisSettingDefinition(id).operations;
|
||||
expect(operations("debugger.command_deadline_ms")).toEqual(
|
||||
expect.arrayContaining(["close_gdb_session", "close_rizin_debug_session"]),
|
||||
);
|
||||
expect(operations("kuna.function_timeout_ms")).toContain("list_procedures");
|
||||
expect(operations("windows.change_time_drift_ms")).toEqual(
|
||||
expect.arrayContaining(["inspect_binary_layout", "inspect_recorded_crash"]),
|
||||
);
|
||||
expect(
|
||||
operations("pwntools.max_input_bytes").includes("inspect_binary_layout"),
|
||||
).toBe(true);
|
||||
expect(analysisSettingDefinition("pwntools.max_input_bytes").parameter).toBe(
|
||||
"producer_limits.input_bytes",
|
||||
);
|
||||
});
|
||||
@@ -406,8 +406,10 @@ export const ANALYSIS_SETTINGS = {
|
||||
operations: [
|
||||
"start_gdb_session",
|
||||
"gdb_console",
|
||||
"close_gdb_session",
|
||||
"start_rizin_debug_session",
|
||||
"rizin_debug_command",
|
||||
"close_rizin_debug_session",
|
||||
],
|
||||
},
|
||||
"debugger.max_command_output_bytes": {
|
||||
@@ -472,6 +474,7 @@ export const ANALYSIS_SETTINGS = {
|
||||
"procedure_pseudo_code",
|
||||
"procedure_assembly",
|
||||
"analyze_function",
|
||||
"list_procedures",
|
||||
],
|
||||
},
|
||||
"pwntools.max_input_bytes": {
|
||||
@@ -483,7 +486,7 @@ export const ANALYSIS_SETTINGS = {
|
||||
appliesAt: "next_operation",
|
||||
provider: "pwntools",
|
||||
operations: ["inspect_binary_layout", "inspect_recorded_crash"],
|
||||
parameter: "input_bytes",
|
||||
parameter: "producer_limits.input_bytes",
|
||||
},
|
||||
"windows.change_time_drift_ms": {
|
||||
environment: "REA_WINDOWS_CHANGE_TIME_DRIFT_MS",
|
||||
@@ -493,7 +496,13 @@ export const ANALYSIS_SETTINGS = {
|
||||
unit: "ms",
|
||||
appliesAt: "next_operation",
|
||||
provider: "rea-artifact-graph",
|
||||
operations: ["inventory_artifact", "inspect_artifact", "extract_artifact"],
|
||||
operations: [
|
||||
"inventory_artifact",
|
||||
"inspect_artifact",
|
||||
"extract_artifact",
|
||||
"inspect_binary_layout",
|
||||
"inspect_recorded_crash",
|
||||
],
|
||||
},
|
||||
"windows.change_time_settle_poll_ms": {
|
||||
environment: "REA_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS",
|
||||
@@ -503,7 +512,13 @@ export const ANALYSIS_SETTINGS = {
|
||||
unit: "ms",
|
||||
appliesAt: "next_operation",
|
||||
provider: "rea-artifact-graph",
|
||||
operations: ["inventory_artifact", "inspect_artifact", "extract_artifact"],
|
||||
operations: [
|
||||
"inventory_artifact",
|
||||
"inspect_artifact",
|
||||
"extract_artifact",
|
||||
"inspect_binary_layout",
|
||||
"inspect_recorded_crash",
|
||||
],
|
||||
},
|
||||
"windows.change_time_settle_max_polls": {
|
||||
environment: "REA_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS",
|
||||
@@ -513,9 +528,25 @@ export const ANALYSIS_SETTINGS = {
|
||||
unit: "count",
|
||||
appliesAt: "next_operation",
|
||||
provider: "rea-artifact-graph",
|
||||
operations: ["inventory_artifact", "inspect_artifact", "extract_artifact"],
|
||||
operations: [
|
||||
"inventory_artifact",
|
||||
"inspect_artifact",
|
||||
"extract_artifact",
|
||||
"inspect_binary_layout",
|
||||
"inspect_recorded_crash",
|
||||
],
|
||||
},
|
||||
} as const satisfies Record<AnalysisSettingId, SettingDefinition>;
|
||||
/** Launch seeds `update_analysis_settings` can replace. Startup must not reject them. */
|
||||
export const runtimeAnalysisEnvironmentNames = (): ReadonlySet<string> =>
|
||||
new Set(
|
||||
Object.values(ANALYSIS_SETTINGS).flatMap((definition) =>
|
||||
definition.updateMode === "runtime" && "environment" in definition
|
||||
? [definition.environment]
|
||||
: [],
|
||||
),
|
||||
);
|
||||
|
||||
export const analysisSettingIds = Object.keys(
|
||||
ANALYSIS_SETTINGS,
|
||||
) as AnalysisSettingId[];
|
||||
|
||||
@@ -32,7 +32,8 @@ import {
|
||||
type SpawnedOwnedProviderProcess,
|
||||
} from "../process/ProviderProcess.js";
|
||||
import {
|
||||
resolveDebuggerSessionLimits,
|
||||
debuggerConstructionLimits,
|
||||
parseDebuggerSessionLimits,
|
||||
type DebuggerAdmissionLimit,
|
||||
} from "../config/debuggerSessionLimits.js";
|
||||
import {
|
||||
@@ -617,9 +618,15 @@ export class GdbSessionManager {
|
||||
currentAnalysisOperationSnapshot()?.overrides[
|
||||
"debugger.command_deadline_ms"
|
||||
];
|
||||
return override === undefined
|
||||
? this.#launchCommandDeadline
|
||||
: { ...this.#launchCommandDeadline, value: override, source: "runtime" };
|
||||
if (override !== undefined)
|
||||
return {
|
||||
...this.#launchCommandDeadline,
|
||||
value: override,
|
||||
source: "runtime",
|
||||
};
|
||||
const parsed = parseDebuggerSessionLimits(this.#environment);
|
||||
if (!parsed.ok) throw parsed.error;
|
||||
return parsed.value.commandDeadline;
|
||||
}
|
||||
get #commandTimeoutMs(): number {
|
||||
return this.#commandDeadline.value;
|
||||
@@ -629,9 +636,15 @@ export class GdbSessionManager {
|
||||
currentAnalysisOperationSnapshot()?.overrides[
|
||||
"debugger.max_command_output_bytes"
|
||||
];
|
||||
return override === undefined
|
||||
? this.#launchCommandOutput
|
||||
: { ...this.#launchCommandOutput, value: override, source: "runtime" };
|
||||
if (override !== undefined)
|
||||
return {
|
||||
...this.#launchCommandOutput,
|
||||
value: override,
|
||||
source: "runtime",
|
||||
};
|
||||
const parsed = parseDebuggerSessionLimits(this.#environment);
|
||||
if (!parsed.ok) throw parsed.error;
|
||||
return parsed.value.maxCommandOutputBytes;
|
||||
}
|
||||
get #maxCommandOutputBytes(): number {
|
||||
return this.#commandOutput.value;
|
||||
@@ -664,7 +677,7 @@ export class GdbSessionManager {
|
||||
) {
|
||||
this.#environment = { ...(options.environment ?? process.env) };
|
||||
this.#command = this.#environment.REA_GDB_COMMAND ?? "gdb";
|
||||
const limits = resolveDebuggerSessionLimits(this.#environment);
|
||||
const limits = debuggerConstructionLimits(this.#environment);
|
||||
this.#launchCommandDeadline = limits.commandDeadline;
|
||||
this.#startupTimeoutMs = options.startupTimeoutMs ?? STARTUP_TIMEOUT_MS;
|
||||
this.#platform = options.platform ?? process.platform;
|
||||
|
||||
@@ -48,6 +48,9 @@ export interface KunaConfiguration {
|
||||
/** Deadline for one single-function `kuna decompile` run. */
|
||||
readonly functionTimeoutMs: number;
|
||||
readonly functionTimeoutSource: "default" | "setting" | "runtime";
|
||||
/** False when the launch seed was invalid and only a runtime override admitted the open. */
|
||||
readonly projectTimeoutLaunchValid?: boolean;
|
||||
readonly functionTimeoutLaunchValid?: boolean;
|
||||
readonly jobs: number;
|
||||
}
|
||||
|
||||
@@ -59,38 +62,49 @@ export interface KunaDeadline {
|
||||
}
|
||||
|
||||
/** Single-function decompiles and the standalone inventory. */
|
||||
export const kunaFunctionDeadline = (
|
||||
const kunaDeadline = (
|
||||
configuration: KunaConfiguration,
|
||||
setting: KunaDeadline["setting"],
|
||||
overrideSetting: "kuna.function_timeout_ms" | "kuna.project_timeout_ms",
|
||||
ms: number,
|
||||
source: KunaDeadline["source"],
|
||||
launchValid: boolean | undefined,
|
||||
): KunaDeadline => {
|
||||
const override =
|
||||
currentAnalysisOperationSnapshot()?.overrides["kuna.function_timeout_ms"];
|
||||
return override === undefined
|
||||
? {
|
||||
setting: "REA_KUNA_TIMEOUT_MS",
|
||||
ms: configuration.functionTimeoutMs,
|
||||
source: configuration.functionTimeoutSource,
|
||||
}
|
||||
: { setting: "REA_KUNA_TIMEOUT_MS", ms: override, source: "runtime" };
|
||||
currentAnalysisOperationSnapshot()?.overrides[overrideSetting];
|
||||
if (override !== undefined)
|
||||
return { setting, ms: override, source: "runtime" };
|
||||
if (launchValid === false)
|
||||
throw new Error(
|
||||
`${setting} is not a valid launch seed and no runtime override is active.`,
|
||||
);
|
||||
return { setting, ms, source };
|
||||
};
|
||||
|
||||
export const kunaFunctionDeadline = (
|
||||
configuration: KunaConfiguration,
|
||||
): KunaDeadline =>
|
||||
kunaDeadline(
|
||||
configuration,
|
||||
"REA_KUNA_TIMEOUT_MS",
|
||||
"kuna.function_timeout_ms",
|
||||
configuration.functionTimeoutMs,
|
||||
configuration.functionTimeoutSource,
|
||||
configuration.functionTimeoutLaunchValid,
|
||||
);
|
||||
|
||||
/** The project export and whole-program string, graph and xref scans. */
|
||||
export const kunaProjectDeadline = (
|
||||
configuration: KunaConfiguration,
|
||||
): KunaDeadline => {
|
||||
const override =
|
||||
currentAnalysisOperationSnapshot()?.overrides["kuna.project_timeout_ms"];
|
||||
return override === undefined
|
||||
? {
|
||||
setting: "REA_KUNA_PROJECT_TIMEOUT_MS",
|
||||
ms: configuration.projectTimeoutMs,
|
||||
source: configuration.projectTimeoutSource,
|
||||
}
|
||||
: {
|
||||
setting: "REA_KUNA_PROJECT_TIMEOUT_MS",
|
||||
ms: override,
|
||||
source: "runtime",
|
||||
};
|
||||
};
|
||||
): KunaDeadline =>
|
||||
kunaDeadline(
|
||||
configuration,
|
||||
"REA_KUNA_PROJECT_TIMEOUT_MS",
|
||||
"kuna.project_timeout_ms",
|
||||
configuration.projectTimeoutMs,
|
||||
configuration.projectTimeoutSource,
|
||||
configuration.projectTimeoutLaunchValid,
|
||||
);
|
||||
|
||||
/** Why the configured environment cannot select a Kuna installation. */
|
||||
export interface KunaConfigurationFailure {
|
||||
@@ -130,26 +144,28 @@ const configuredTimeout = (
|
||||
raw: string | undefined,
|
||||
setting: string,
|
||||
fallback: number,
|
||||
override: number | undefined,
|
||||
): Result<
|
||||
{
|
||||
readonly ms: number;
|
||||
readonly source: "default" | "setting" | "runtime";
|
||||
readonly source: "default" | "setting";
|
||||
readonly launchValid: boolean;
|
||||
},
|
||||
KunaConfigurationFailure
|
||||
> => {
|
||||
if (raw === undefined)
|
||||
return ok({
|
||||
ms: override ?? fallback,
|
||||
source: override === undefined ? "default" : "runtime",
|
||||
});
|
||||
return ok({ ms: fallback, source: "default", launchValid: true });
|
||||
const parsed = providerStartupTimeoutSchema.safeParse(raw);
|
||||
if (parsed.success)
|
||||
return ok({
|
||||
ms: override ?? parsed.data,
|
||||
source: override === undefined ? "setting" : "runtime",
|
||||
});
|
||||
if (override !== undefined) return ok({ ms: override, source: "runtime" });
|
||||
return ok({ ms: parsed.data, source: "setting", launchValid: true });
|
||||
return ok({ ms: fallback, source: "default", launchValid: false });
|
||||
};
|
||||
|
||||
const refuseInvalidLaunch = (
|
||||
raw: string | undefined,
|
||||
setting: string,
|
||||
parsed: { readonly launchValid: boolean },
|
||||
): Result<void, KunaConfigurationFailure> => {
|
||||
if (parsed.launchValid) return ok(undefined);
|
||||
return failure(
|
||||
"open_options_invalid",
|
||||
`${setting} must be a decimal millisecond count from 1 to 2147483647; received ${String(raw)}.`,
|
||||
@@ -199,16 +215,30 @@ export const readKunaConfiguration = (
|
||||
environment.REA_KUNA_PROJECT_TIMEOUT_MS,
|
||||
"REA_KUNA_PROJECT_TIMEOUT_MS",
|
||||
DEFAULT_PROJECT_TIMEOUT_MS,
|
||||
overrides?.["kuna.project_timeout_ms"],
|
||||
);
|
||||
if (!projectTimeout.ok) return projectTimeout;
|
||||
if (overrides?.["kuna.project_timeout_ms"] === undefined) {
|
||||
const refused = refuseInvalidLaunch(
|
||||
environment.REA_KUNA_PROJECT_TIMEOUT_MS,
|
||||
"REA_KUNA_PROJECT_TIMEOUT_MS",
|
||||
projectTimeout.value,
|
||||
);
|
||||
if (!refused.ok) return refused;
|
||||
}
|
||||
const functionTimeout = configuredTimeout(
|
||||
environment.REA_KUNA_TIMEOUT_MS,
|
||||
"REA_KUNA_TIMEOUT_MS",
|
||||
DEFAULT_FUNCTION_TIMEOUT_MS,
|
||||
overrides?.["kuna.function_timeout_ms"],
|
||||
);
|
||||
if (!functionTimeout.ok) return functionTimeout;
|
||||
if (overrides?.["kuna.function_timeout_ms"] === undefined) {
|
||||
const refused = refuseInvalidLaunch(
|
||||
environment.REA_KUNA_TIMEOUT_MS,
|
||||
"REA_KUNA_TIMEOUT_MS",
|
||||
functionTimeout.value,
|
||||
);
|
||||
if (!refused.ok) return refused;
|
||||
}
|
||||
|
||||
const rawJobs = environment.REA_KUNA_JOBS;
|
||||
const jobs = rawJobs === undefined ? DEFAULT_JOBS : positiveInteger(rawJobs);
|
||||
@@ -231,8 +261,10 @@ export const readKunaConfiguration = (
|
||||
cacheMaxBytes,
|
||||
projectTimeoutMs: projectTimeout.value.ms,
|
||||
projectTimeoutSource: projectTimeout.value.source,
|
||||
projectTimeoutLaunchValid: projectTimeout.value.launchValid,
|
||||
functionTimeoutMs: functionTimeout.value.ms,
|
||||
functionTimeoutSource: functionTimeout.value.source,
|
||||
functionTimeoutLaunchValid: functionTimeout.value.launchValid,
|
||||
jobs,
|
||||
});
|
||||
};
|
||||
@@ -37,7 +37,8 @@ import {
|
||||
} from "../process/ProviderProcess.js";
|
||||
import {
|
||||
debuggerControlReplyOutput,
|
||||
resolveDebuggerSessionLimits,
|
||||
debuggerConstructionLimits,
|
||||
parseDebuggerSessionLimits,
|
||||
type DebuggerAdmissionLimit,
|
||||
} from "../config/debuggerSessionLimits.js";
|
||||
import {
|
||||
@@ -285,7 +286,7 @@ export class RizinDebugSessionManager {
|
||||
) {
|
||||
this.#environment = { ...environment };
|
||||
this.#command = environment.REA_RIZIN_COMMAND ?? "rizin";
|
||||
const limits = resolveDebuggerSessionLimits(this.#environment);
|
||||
const limits = debuggerConstructionLimits(this.#environment);
|
||||
this.#launchCommandDeadline = limits.commandDeadline;
|
||||
this.#startupTimeoutMs = options.startupTimeoutMs ?? STARTUP_TIMEOUT_MS;
|
||||
this.#maxSessions = limits.maxSessions;
|
||||
@@ -301,9 +302,15 @@ export class RizinDebugSessionManager {
|
||||
currentAnalysisOperationSnapshot()?.overrides[
|
||||
"debugger.command_deadline_ms"
|
||||
];
|
||||
return override === undefined
|
||||
? this.#launchCommandDeadline
|
||||
: { ...this.#launchCommandDeadline, value: override, source: "runtime" };
|
||||
if (override !== undefined)
|
||||
return {
|
||||
...this.#launchCommandDeadline,
|
||||
value: override,
|
||||
source: "runtime",
|
||||
};
|
||||
const parsed = parseDebuggerSessionLimits(this.#environment);
|
||||
if (!parsed.ok) throw parsed.error;
|
||||
return parsed.value.commandDeadline;
|
||||
}
|
||||
get #frameTimeoutMs(): number {
|
||||
return this.#commandDeadline.value;
|
||||
@@ -313,9 +320,15 @@ export class RizinDebugSessionManager {
|
||||
currentAnalysisOperationSnapshot()?.overrides[
|
||||
"debugger.max_command_output_bytes"
|
||||
];
|
||||
return override === undefined
|
||||
? this.#launchCommandOutput
|
||||
: { ...this.#launchCommandOutput, value: override, source: "runtime" };
|
||||
if (override !== undefined)
|
||||
return {
|
||||
...this.#launchCommandOutput,
|
||||
value: override,
|
||||
source: "runtime",
|
||||
};
|
||||
const parsed = parseDebuggerSessionLimits(this.#environment);
|
||||
if (!parsed.ok) throw parsed.error;
|
||||
return parsed.value.maxCommandOutputBytes;
|
||||
}
|
||||
get #maxCommandOutputBytes(): number {
|
||||
return this.#commandOutput.value;
|
||||
|
||||
@@ -363,12 +363,10 @@ it("refuses invalid settings at startup validation and at each owner", async ()
|
||||
},
|
||||
]);
|
||||
const startup = parseEnvironment({ REA_ASAR_MAX_HEADER_BYTES: value });
|
||||
if (startup.ok)
|
||||
throw new Error(`Startup accepted ${JSON.stringify(value)}`);
|
||||
expect(startup.error.settings).toContainEqual({
|
||||
setting: "REA_ASAR_MAX_HEADER_BYTES",
|
||||
constraint: parsed.error.message,
|
||||
});
|
||||
if (!startup.ok)
|
||||
throw new Error(
|
||||
`Startup rejected a runtime seed ${JSON.stringify(value)}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Owners refuse rather than falling back to the default.
|
||||
|
||||
Reference in new issue
Block a user