fix(settings): advertise every bridge operation and prefer accepted overrides

The bridge ceiling lists every Hopper and Ghidra bridge operation plus the Cutter calls, and a test fails when a registry entry is missing. ChangeTime observation and the other launch-seed parsers read an accepted override before rejecting an invalid value.
This commit is contained in:
Cursor Agent authored and morluto committed 2026-11-11 20:16:24 +08:00
1 parent 2bac42cde5
commit e0dcdab1a9
11 files changed
+209 -14

No files matched your search

@@ -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.
+57
View File
@@ -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;
+24
View File
@@ -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);
});
+18
View File
@@ -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,
+4
View File
@@ -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<Record<string, string | undefined>>,
): 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],
);
+6
View File
@@ -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<Record<string, string | undefined>>,
): 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],
);
+4
View File
@@ -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<Record<string, string | undefined>>,
): RegexMatchingDeadline => {
const override = currentAnalysisSettingOverride("regex.matching_deadline_ms");
if (override !== undefined)
return { deadlineMs: override, source: "runtime" };
const parsed = parseRegexMatchingDeadline(
environment[REGEX_MATCHING_DEADLINE_SETTING],
);
+5
View File
@@ -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<Record<string, string | undefined>>,
): 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],
);
+29 -7
View File
@@ -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";
},
},
+12 -1
View File
@@ -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<AnalysisSettingsSnapshot>();
@@ -17,3 +20,11 @@ export const runWithAnalysisOperationSnapshot = <Value>(
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];
+43 -1
View File
@@ -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",
],