diff --git a/src/application/ClientRegistrationEnvironment.ts b/src/application/ClientRegistrationEnvironment.ts index 849f6f95b9..23bec6cf88 100644 --- a/src/application/ClientRegistrationEnvironment.ts +++ b/src/application/ClientRegistrationEnvironment.ts @@ -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[0], + environment: Readonly>, + fallback: number, +): number => { + const parsed = launchAnalysisSetting(id, environment); + return parsed.ok && parsed.value.value !== null + ? parsed.value.value + : fallback; +}; + +const registrationTimeoutInputs = ( + environment: Readonly>, +): 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>, 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), }, }); diff --git a/src/config.test.ts b/src/config.test.ts index 2594788877..ed2289e327 100644 --- a/src/config.test.ts +++ b/src/config.test.ts @@ -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"); }, ); }); diff --git a/src/config/bridgeMaxRequestBytes.test.ts b/src/config/bridgeMaxRequestBytes.test.ts index e960132c15..3ab2151c24 100644 --- a/src/config/bridgeMaxRequestBytes.test.ts +++ b/src/config/bridgeMaxRequestBytes.test.ts @@ -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)}`); }); diff --git a/src/config/captureDecoderHeap.test.ts b/src/config/captureDecoderHeap.test.ts index daa7af5a84..aeb353e0dc 100644 --- a/src/config/captureDecoderHeap.test.ts +++ b/src/config/captureDecoderHeap.test.ts @@ -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", { diff --git a/src/config/debuggerSessionLimits.test.ts b/src/config/debuggerSessionLimits.test.ts index e7ea0cb41e..747e22e37c 100644 --- a/src/config/debuggerSessionLimits.test.ts +++ b/src/config/debuggerSessionLimits.test.ts @@ -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", diff --git a/src/config/debuggerSessionLimits.ts b/src/config/debuggerSessionLimits.ts index 2405b2e27e..570f178dee 100644 --- a/src/config/debuggerSessionLimits.ts +++ b/src/config/debuggerSessionLimits.ts @@ -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([ + 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>, +): 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>, diff --git a/src/config/environment.ts b/src/config/environment.ts index d2405c3373..38655da367 100644 --- a/src/config/environment.ts +++ b/src/config/environment.ts @@ -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() diff --git a/src/config/parseConfig.ts b/src/config/parseConfig.ts index 57230a5d47..74e9190bcb 100644 --- a/src/config/parseConfig.ts +++ b/src/config/parseConfig.ts @@ -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", }, }); diff --git a/src/config/regexMatchingDeadline.test.ts b/src/config/regexMatchingDeadline.test.ts index 4ed7918d46..e85a90c60b 100644 --- a/src/config/regexMatchingDeadline.test.ts +++ b/src/config/regexMatchingDeadline.test.ts @@ -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); }); diff --git a/src/config/regexWorkerHeapRequest.test.ts b/src/config/regexWorkerHeapRequest.test.ts index d24583eac4..2af15037e5 100644 --- a/src/config/regexWorkerHeapRequest.test.ts +++ b/src/config/regexWorkerHeapRequest.test.ts @@ -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); diff --git a/src/config/windowsChangeTimeSettle.test.ts b/src/config/windowsChangeTimeSettle.test.ts index 6fb64a5cdc..269a65bab7 100644 --- a/src/config/windowsChangeTimeSettle.test.ts +++ b/src/config/windowsChangeTimeSettle.test.ts @@ -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", () => { diff --git a/src/domain/analysisSettingAdmission.test.ts b/src/domain/analysisSettingAdmission.test.ts new file mode 100644 index 0000000000..24d21779ab --- /dev/null +++ b/src/domain/analysisSettingAdmission.test.ts @@ -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[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", + ); +}); diff --git a/src/domain/analysisSettingDefinitions.ts b/src/domain/analysisSettingDefinitions.ts index 1118e7c9f0..72b4ed41ca 100644 --- a/src/domain/analysisSettingDefinitions.ts +++ b/src/domain/analysisSettingDefinitions.ts @@ -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; +/** Launch seeds `update_analysis_settings` can replace. Startup must not reject them. */ +export const runtimeAnalysisEnvironmentNames = (): ReadonlySet => + 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[]; diff --git a/src/gdb/GdbSessionManager.ts b/src/gdb/GdbSessionManager.ts index 2bb1289091..2d30fc6053 100644 --- a/src/gdb/GdbSessionManager.ts +++ b/src/gdb/GdbSessionManager.ts @@ -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; diff --git a/src/kuna/KunaConfiguration.ts b/src/kuna/KunaConfiguration.ts index add432703c..da3f92eb7c 100644 --- a/src/kuna/KunaConfiguration.ts +++ b/src/kuna/KunaConfiguration.ts @@ -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 => { + 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, }); }; diff --git a/src/rizin/RizinDebugSessionManager.ts b/src/rizin/RizinDebugSessionManager.ts index 3af2f00ecf..42b61b7627 100644 --- a/src/rizin/RizinDebugSessionManager.ts +++ b/src/rizin/RizinDebugSessionManager.ts @@ -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; diff --git a/tests/boundary/filesystem/asarHeaderAdmission.test.ts b/tests/boundary/filesystem/asarHeaderAdmission.test.ts index dfa814a87e..4204733706 100644 --- a/tests/boundary/filesystem/asarHeaderAdmission.test.ts +++ b/tests/boundary/filesystem/asarHeaderAdmission.test.ts @@ -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.