mirror of
https://github.com/morluto/rea.git
synced 2026-11-13 18:22:42 +00:00
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:
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.
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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],
|
||||
);
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
|
||||
@@ -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";
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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];
|
||||
@@ -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",
|
||||
],
|
||||
|
||||
Reference in new issue
Block a user