diff --git a/.agents/skills/reverse-engineer-anything/references/connection-and-recovery.md b/.agents/skills/reverse-engineer-anything/references/connection-and-recovery.md index e2e767411f..a98808414a 100644 --- a/.agents/skills/reverse-engineer-anything/references/connection-and-recovery.md +++ b/.agents/skills/reverse-engineer-anything/references/connection-and-recovery.md @@ -108,11 +108,13 @@ is not a recommendation to change it. Do not invent a setting. Host memory and - Capacity, when the result is `resource_constraint` and `retryable` is true: wait for the active admission to finish, then retry within the reported limits. Do not restart REA to free the slot. -- `configuration_invalid`: correct the named setting in the environment that - starts REA (the shell or the MCP registration), then restart REA. Run - `rea doctor` to see the `runtime-configuration:` check. Re-run `rea setup` - for that client when the registration must carry the new setting, then - reconnect. +- `configuration_invalid`: when the error reports `update_mode: "runtime"`, + call `update_analysis_settings` for that setting. When it reports + `update_mode: "parameter"`, change that call's argument; the setter does not + accept it. Otherwise correct the named setting in the environment that starts + REA (the shell or the MCP registration) and start REA again. Run `rea doctor` + to see the `runtime-configuration:` check. Re-run `rea setup` for that client + when the registration must carry the new setting, then reconnect. - `invalid_request`: correct the reported fields from the current schema. Do not repeat the unchanged call. diff --git a/src/application/AnalysisSettings.test.ts b/src/application/AnalysisSettings.test.ts index d4ed9bbd5f..2b3b3eb3ad 100644 --- a/src/application/AnalysisSettings.test.ts +++ b/src/application/AnalysisSettings.test.ts @@ -6,6 +6,11 @@ import { resolveWindowsChangeTimeSettle, } from "../config/windowsChangeTimeSettle.js"; import { pwntoolsInputBytes } from "../native/pwntools/PwntoolsInputLimit.js"; +import { resolveBridgeMaxRequestBytes } from "../config/bridgeMaxRequestBytes.js"; +import { resolveCaptureDecoderHeap } from "../config/captureDecoderHeap.js"; +import { resolveRegexMatchingDeadline } from "../config/regexMatchingDeadline.js"; +import { resolveRegexWorkerHeapRequest } from "../config/regexWorkerHeapRequest.js"; +import { runWithAnalysisOperationSnapshot } from "../domain/analysisOperationSnapshot.js"; import { runtimeAnalysisConfig, runtimeAsarHeaderAdmission, @@ -317,6 +322,58 @@ it("rejects an out-of-range per-call limit and reads an accepted value from the }); }); +it("reads an accepted override before an invalid launch seed", () => { + const resolved = runWithAnalysisOperationSnapshot( + { + revision: 1, + overrides: { + "bridge.max_request_bytes": 8192, + "capture.decoder_old_generation_mib": 64, + "regex.matching_deadline_ms": 250, + "regex.worker_heap_request_mib": 128, + "asar.max_header_bytes": 8192, + "asar.max_listing_bytes": 4096, + }, + }, + () => ({ + bridge: resolveBridgeMaxRequestBytes({ + REA_BRIDGE_MAX_REQUEST_BYTES: "nope", + }), + capture: resolveCaptureDecoderHeap({ + REA_CAPTURE_DECODER_OLD_GENERATION_MIB: "0", + }), + deadline: resolveRegexMatchingDeadline({ + REA_REGEX_MATCHING_DEADLINE_MS: "nope", + }), + heap: resolveRegexWorkerHeapRequest({ + REA_REGEX_WORKER_HEAP_REQUEST_MIB: "1", + }), + asar: runtimeAsarHeaderAdmission({ + REA_ASAR_MAX_HEADER_BYTES: "nope", + REA_ASAR_MAX_LISTING_BYTES: "0", + }), + }), + ); + expect(resolved.bridge).toMatchObject({ + maximumBytes: 8192, + source: "runtime", + }); + expect(resolved.capture).toMatchObject({ + oldGenerationMiB: 64, + source: "runtime", + }); + expect(resolved.deadline).toMatchObject({ + deadlineMs: 250, + source: "runtime", + }); + expect(resolved.heap).toMatchObject({ heapMiB: 128, source: "runtime" }); + expect(resolved.asar).toMatchObject({ + maximumBytes: 8192, + source: "runtime", + listing: { maximumBytes: 4096, source: "runtime" }, + }); +}); + it("keeps the Hopper startup deadline source through a runtime override", () => { const parsed = parseConfig({ REA_HOPPER_STARTUP_TIMEOUT_MS: "45000" }); if (!parsed.ok) throw parsed.error; diff --git a/src/artifacts/ChangeTimeBaseline.test.ts b/src/artifacts/ChangeTimeBaseline.test.ts index 49e5baf624..beaddc79a1 100644 --- a/src/artifacts/ChangeTimeBaseline.test.ts +++ b/src/artifacts/ChangeTimeBaseline.test.ts @@ -2,6 +2,7 @@ import type { Stats } from "node:fs"; import { expect, it } from "vitest"; +import { runWithAnalysisOperationSnapshot } from "../domain/analysisOperationSnapshot.js"; import { settleChangeTimeBaseline } from "../filesystem/ChangeTimeBaseline.js"; const DRIFT_MS = 2_000; @@ -229,3 +230,26 @@ it("throws the caller's typed cancellation rather than the raw reason", async () }), ).rejects.toBeInstanceOf(Cancelled); }); + +it("uses a Windows settle override before rejecting an invalid launch seed", async () => { + const observed = await runWithAnalysisOperationSnapshot( + { + revision: 1, + overrides: { + "windows.change_time_drift_ms": 50, + "windows.change_time_settle_poll_ms": 10, + "windows.change_time_settle_max_polls": 4, + }, + }, + () => + settleChangeTimeBaseline(async () => statsAt(1_000), { + environment: { + REA_WINDOWS_CHANGE_TIME_DRIFT_MS: "nope", + REA_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS: "0", + REA_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS: "1001", + }, + platform: "linux", + }), + ); + expect(observed.ctimeMs).toBe(1_000); +}); diff --git a/src/config/bridgeMaxRequestBytes.test.ts b/src/config/bridgeMaxRequestBytes.test.ts index 02a6811487..e960132c15 100644 --- a/src/config/bridgeMaxRequestBytes.test.ts +++ b/src/config/bridgeMaxRequestBytes.test.ts @@ -1,6 +1,9 @@ import { expect, it } from "vitest"; +import { analysisSettingDefinition } from "../domain/analysisSettingDefinitions.js"; import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; +import { GHIDRA_OPERATIONS } from "../ghidra/GhidraProviderMetadata.js"; +import { HOPPER_OPERATIONS } from "../hopper/HopperProviderMetadata.js"; import { parseEnvironment } from "./environment.js"; import { parseConfig } from "./parseConfig.js"; import { @@ -19,6 +22,21 @@ const symbolName = "\u0001".repeat(1024); const session = "00000000-0000-4000-8000-000000000001"; const token = "a".repeat(64); +it("advertises the bridge ceiling for every bridge-backed operation", () => { + const advertised = new Set( + analysisSettingDefinition("bridge.max_request_bytes").operations, + ); + const required = [ + ...HOPPER_OPERATIONS, + ...GHIDRA_OPERATIONS, + "list_cutter_sessions", + "cutter_command", + ]; + expect(required.filter((operation) => !advertised.has(operation))).toEqual( + [], + ); +}); + it("keeps the 16 MiB ceiling when the setting is unset", () => { expect(parseBridgeMaxRequestBytes(undefined)).toEqual({ ok: true, diff --git a/src/config/bridgeMaxRequestBytes.ts b/src/config/bridgeMaxRequestBytes.ts index e92f080b6b..4116a1276a 100644 --- a/src/config/bridgeMaxRequestBytes.ts +++ b/src/config/bridgeMaxRequestBytes.ts @@ -1,6 +1,7 @@ import { positiveDecimalIntegerSchema } from "./positiveDecimalInteger.js"; import { analysisSettingLimit } from "../domain/analysisSettingDefinitions.js"; +import { currentAnalysisSettingOverride } from "../domain/analysisOperationSnapshot.js"; import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js"; import { ConfigurationError } from "../domain/configurationErrors.js"; import { err, ok, type Result } from "../domain/result.js"; @@ -102,6 +103,9 @@ export const parseBridgeMaxRequestBytes = ( export const resolveBridgeMaxRequestBytes = ( environment: Readonly>, ): BridgeMaxRequestBytes => { + const override = currentAnalysisSettingOverride("bridge.max_request_bytes"); + if (override !== undefined) + return { maximumBytes: override, source: "runtime" }; const parsed = parseBridgeMaxRequestBytes( environment[BRIDGE_MAX_REQUEST_BYTES_SETTING], ); diff --git a/src/config/captureDecoderHeap.ts b/src/config/captureDecoderHeap.ts index fff78278f2..84205f047c 100644 --- a/src/config/captureDecoderHeap.ts +++ b/src/config/captureDecoderHeap.ts @@ -6,6 +6,7 @@ import { import { positiveDecimalIntegerSchema } from "./positiveDecimalInteger.js"; import { ConfigurationError } from "../domain/configurationErrors.js"; +import { currentAnalysisSettingOverride } from "../domain/analysisOperationSnapshot.js"; import { err, ok, type Result } from "../domain/result.js"; export const CAPTURE_DECODER_OLD_GENERATION_SETTING = @@ -60,6 +61,11 @@ export const parseCaptureDecoderHeap = ( export const resolveCaptureDecoderHeap = ( environment: Readonly>, ): CaptureDecoderHeap => { + const override = currentAnalysisSettingOverride( + "capture.decoder_old_generation_mib", + ); + if (override !== undefined) + return { oldGenerationMiB: override, source: "runtime" }; const parsed = parseCaptureDecoderHeap( environment[CAPTURE_DECODER_OLD_GENERATION_SETTING], ); diff --git a/src/config/regexMatchingDeadline.ts b/src/config/regexMatchingDeadline.ts index 73ac67dda2..0afdd56f46 100644 --- a/src/config/regexMatchingDeadline.ts +++ b/src/config/regexMatchingDeadline.ts @@ -5,6 +5,7 @@ import { import { positiveDecimalIntegerSchema } from "./positiveDecimalInteger.js"; import { ConfigurationError } from "../domain/configurationErrors.js"; +import { currentAnalysisSettingOverride } from "../domain/analysisOperationSnapshot.js"; import { err, ok, type Result } from "../domain/result.js"; export const REGEX_MATCHING_DEADLINE_SETTING = "REA_REGEX_MATCHING_DEADLINE_MS"; @@ -57,6 +58,9 @@ export const parseRegexMatchingDeadline = ( export const resolveRegexMatchingDeadline = ( environment: Readonly>, ): RegexMatchingDeadline => { + const override = currentAnalysisSettingOverride("regex.matching_deadline_ms"); + if (override !== undefined) + return { deadlineMs: override, source: "runtime" }; const parsed = parseRegexMatchingDeadline( environment[REGEX_MATCHING_DEADLINE_SETTING], ); diff --git a/src/config/regexWorkerHeapRequest.ts b/src/config/regexWorkerHeapRequest.ts index 8103970901..2dce7cebec 100644 --- a/src/config/regexWorkerHeapRequest.ts +++ b/src/config/regexWorkerHeapRequest.ts @@ -6,6 +6,7 @@ import { import { positiveDecimalIntegerSchema } from "./positiveDecimalInteger.js"; import { ConfigurationError } from "../domain/configurationErrors.js"; +import { currentAnalysisSettingOverride } from "../domain/analysisOperationSnapshot.js"; import { err, ok, type Result } from "../domain/result.js"; export const REGEX_WORKER_HEAP_REQUEST_SETTING = @@ -59,6 +60,10 @@ export const parseRegexWorkerHeapRequest = ( export const resolveRegexWorkerHeapRequest = ( environment: Readonly>, ): RegexWorkerHeapRequest => { + const override = currentAnalysisSettingOverride( + "regex.worker_heap_request_mib", + ); + if (override !== undefined) return { heapMiB: override, source: "runtime" }; const parsed = parseRegexWorkerHeapRequest( environment[REGEX_WORKER_HEAP_REQUEST_SETTING], ); diff --git a/src/config/runtimeAnalysisSettings.ts b/src/config/runtimeAnalysisSettings.ts index 907cd8e19d..e6a582c128 100644 --- a/src/config/runtimeAnalysisSettings.ts +++ b/src/config/runtimeAnalysisSettings.ts @@ -1,4 +1,5 @@ import type { AnalysisSettingsReader } from "../domain/analysisSettings.js"; +import { currentAnalysisSettingOverride } from "../domain/analysisOperationSnapshot.js"; import type { ConfigurationError } from "../domain/configurationErrors.js"; import { ok, type Result } from "../domain/result.js"; import type { AppConfig } from "./types.js"; @@ -7,8 +8,12 @@ import { DEFAULT_GHIDRA_VALUE_FLOW_MAX_INPUTS_PER_OPERATION, DEFAULT_GHIDRA_VALUE_FLOW_MAX_EDGES, } from "../domain/analysisResourceLimits.js"; -import { parseAsarListingAdmission } from "./asarListingAdmission.js"; import { + parseAsarListingAdmission, + DEFAULT_ASAR_LISTING_ADMISSION, +} from "./asarListingAdmission.js"; +import { + DEFAULT_ASAR_HEADER_ADMISSION, parseAsarHeaderAdmission, type AsarHeaderAdmission, } from "./asarHeaderAdmission.js"; @@ -145,25 +150,42 @@ export const runtimeAsarHeaderAdmission = ( const parsed = parseAsarHeaderAdmission( environment.REA_ASAR_MAX_HEADER_BYTES, ); - if (!parsed.ok) throw parsed.error; + const headerOverride = + read?.().overrides["asar.max_header_bytes"] ?? + currentAnalysisSettingOverride("asar.max_header_bytes"); + if (!parsed.ok && headerOverride === undefined) throw parsed.error; const listing = parseAsarListingAdmission( environment.REA_ASAR_MAX_LISTING_BYTES, ); - if (!listing.ok) throw listing.error; - const baseline = parsed.value; + const listingOverride = + read?.().overrides["asar.max_listing_bytes"] ?? + currentAnalysisSettingOverride("asar.max_listing_bytes"); + if (!listing.ok && listingOverride === undefined) throw listing.error; + const baseline = parsed.ok ? parsed.value : DEFAULT_ASAR_HEADER_ADMISSION; + const listingBaseline = listing.ok + ? listing.value + : DEFAULT_ASAR_LISTING_ADMISSION; return read === undefined - ? { ...baseline, listing: listing.value } + ? { + maximumBytes: headerOverride ?? baseline.maximumBytes, + source: headerOverride === undefined ? baseline.source : "runtime", + listing: { + maximumBytes: listingOverride ?? listingBaseline.maximumBytes, + source: + listingOverride === undefined ? listingBaseline.source : "runtime", + }, + } : { listing: { get maximumBytes() { return ( read().overrides["asar.max_listing_bytes"] ?? - listing.value.maximumBytes + listingBaseline.maximumBytes ); }, get source() { return read().overrides["asar.max_listing_bytes"] === undefined - ? listing.value.source + ? listingBaseline.source : "runtime"; }, }, diff --git a/src/domain/analysisOperationSnapshot.ts b/src/domain/analysisOperationSnapshot.ts index c49173f79a..19d5bfb457 100644 --- a/src/domain/analysisOperationSnapshot.ts +++ b/src/domain/analysisOperationSnapshot.ts @@ -1,5 +1,8 @@ import { AsyncLocalStorage } from "node:async_hooks"; -import type { AnalysisSettingsSnapshot } from "./analysisSettings.js"; +import type { + AnalysisSettingValues, + AnalysisSettingsSnapshot, +} from "./analysisSettings.js"; const snapshots = new AsyncLocalStorage(); @@ -17,3 +20,11 @@ export const runWithAnalysisOperationSnapshot = ( export const currentAnalysisOperationSnapshot = (): | AnalysisSettingsSnapshot | undefined => snapshots.getStore(); + +/** Accepted override for one setting, when this operation has a snapshot. */ +export const currentAnalysisSettingOverride = < + Setting extends keyof AnalysisSettingValues, +>( + setting: Setting, +): AnalysisSettingValues[Setting] | undefined => + currentAnalysisOperationSnapshot()?.overrides[setting]; diff --git a/src/domain/analysisSettingDefinitions.ts b/src/domain/analysisSettingDefinitions.ts index f93e53d57a..1118e7c9f0 100644 --- a/src/domain/analysisSettingDefinitions.ts +++ b/src/domain/analysisSettingDefinitions.ts @@ -344,11 +344,53 @@ export const ANALYSIS_SETTINGS = { appliesAt: "next_operation", provider: "hopper", relatedProviders: ["ghidra", "cutter"], + // Every Hopper bridge operation, every Ghidra bridge operation, and the + // Cutter bridge calls. bridgeMaxRequestBytes.test.ts fails when a registry + // gains an operation that is missing here. operations: [ + "address_name", + "comment", + "current_address", + "current_procedure", + "current_document", + "goto_address", + "inline_comment", + "list_bookmarks", + "list_documents", + "list_names", + "list_procedures", + "list_segments", + "list_strings", + "next_address", + "prev_address", + "procedure_address", + "procedure_assembly", + "procedure_callees", + "procedure_callers", + "procedure_info", + "read_function_instructions", + "read_bytes", + "address_to_file_offset", + "procedure_references", "procedure_pseudo_code", - "analyze_function", + "resolve_containing_procedure", + "search_procedures", "search_strings", "set_address_name", + "set_addresses_names", + "set_bookmark", + "set_comment", + "set_inline_comment", + "unset_bookmark", + "xrefs", + "analyze_function", + "binary_overview", + "inspect_native_load_image", + "search_bytes", + "annotate_native_function", + "inspect_native_data_type", + "inspect_native_instruction", + "resolve_native_call_targets", "list_cutter_sessions", "cutter_command", ],