mirror of
https://github.com/morluto/rea.git
synced 2026-11-14 18:52:48 +00:00
feat(settings): read per-call REA_ limits from the operation snapshot
Part of #9881. Move the limits checked on each call into update_analysis_settings, keep their REA_ seeds and bounds, and reject updates outside the allowed range.
This commit is contained in:
1 parent
9924bf3445
commit
b0541c80b3
34 files changed
+704
-254
No files matched your search
@@ -1,5 +1,11 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { AnalysisSettings } from "./AnalysisSettings.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import {
|
||||
applyWindowsChangeTimeSnapshot,
|
||||
resolveWindowsChangeTimeSettle,
|
||||
} from "../config/windowsChangeTimeSettle.js";
|
||||
import { pwntoolsInputBytes } from "../native/pwntools/PwntoolsInputLimit.js";
|
||||
import {
|
||||
runtimeAnalysisConfig,
|
||||
runtimeAsarHeaderAdmission,
|
||||
@@ -159,7 +165,7 @@ it("exposes shared provider controls and preserves the explicit unbounded decomp
|
||||
store
|
||||
.inspect({ provider: "ghidra", operation: "search_strings" })
|
||||
.settings.map(({ setting }) => setting),
|
||||
).toEqual(["ghidra.startup_timeout_ms"]);
|
||||
).toEqual(["ghidra.startup_timeout_ms", "bridge.max_request_bytes"]);
|
||||
expect(
|
||||
store
|
||||
.inspect({ provider: "ida", operation: "search_strings" })
|
||||
@@ -169,7 +175,13 @@ it("exposes shared provider controls and preserves the explicit unbounded decomp
|
||||
store
|
||||
.inspect({ operation: "inventory_artifact" })
|
||||
.settings.map(({ setting }) => setting),
|
||||
).toEqual(["asar.max_listing_bytes", "asar.max_header_bytes"]);
|
||||
).toEqual([
|
||||
"asar.max_listing_bytes",
|
||||
"asar.max_header_bytes",
|
||||
"windows.change_time_drift_ms",
|
||||
"windows.change_time_settle_poll_ms",
|
||||
"windows.change_time_settle_max_polls",
|
||||
]);
|
||||
expect(
|
||||
store.update({
|
||||
changes: [
|
||||
@@ -217,3 +229,73 @@ it("exposes shared provider controls and preserves the explicit unbounded decomp
|
||||
).toBe(true);
|
||||
expect(config.ghidraDecompileTimeoutSeconds).toBe(7);
|
||||
});
|
||||
|
||||
it("rejects an out-of-range per-call limit and reads an accepted value from the operation snapshot", () => {
|
||||
const store = new AnalysisSettings({});
|
||||
const rejected = store.update({
|
||||
changes: [
|
||||
{ setting: "windows.change_time_drift_ms", action: "set", value: 0 },
|
||||
],
|
||||
});
|
||||
expect(rejected.ok).toBe(false);
|
||||
if (!rejected.ok)
|
||||
expect(projectAnalysisError(rejected.error)).toMatchObject({
|
||||
code: "invalid_request",
|
||||
});
|
||||
expect(
|
||||
store.update({
|
||||
changes: [
|
||||
{ setting: "bridge.max_request_bytes", action: "set", value: 1 },
|
||||
],
|
||||
}).ok,
|
||||
).toBe(false);
|
||||
expect(
|
||||
store.update({
|
||||
changes: [
|
||||
{
|
||||
setting: "windows.change_time_settle_max_polls",
|
||||
action: "set",
|
||||
value: 1001,
|
||||
},
|
||||
],
|
||||
}).ok,
|
||||
).toBe(false);
|
||||
const accepted = store.update({
|
||||
changes: [
|
||||
{ setting: "windows.change_time_drift_ms", action: "set", value: 5000 },
|
||||
{ setting: "pwntools.max_input_bytes", action: "set", value: 65536 },
|
||||
{ setting: "bridge.max_request_bytes", action: "set", value: 33554432 },
|
||||
],
|
||||
});
|
||||
expect(accepted.ok).toBe(true);
|
||||
if (!accepted.ok) return;
|
||||
expect(accepted.value).toMatchObject({
|
||||
effect: "immediate",
|
||||
rea_restart_required: false,
|
||||
});
|
||||
const parsed = parseConfig({});
|
||||
if (!parsed.ok) throw parsed.error;
|
||||
const live = runtimeAnalysisConfig(parsed.value, store.read);
|
||||
store.run(() => {
|
||||
const windows = resolveWindowsChangeTimeSettle({});
|
||||
if (!windows.ok) throw windows.error;
|
||||
expect(applyWindowsChangeTimeSnapshot(windows.value)).toMatchObject({
|
||||
driftMs: 5000,
|
||||
driftSource: "runtime",
|
||||
pollSource: "default",
|
||||
});
|
||||
expect(pwntoolsInputBytes({})).toMatchObject({
|
||||
configured: true,
|
||||
bytes: 65536,
|
||||
source: "runtime",
|
||||
});
|
||||
expect(live.bridgeMaxRequestBytes).toMatchObject({
|
||||
maximumBytes: 33554432,
|
||||
source: "runtime",
|
||||
});
|
||||
});
|
||||
expect(pwntoolsInputBytes({})).toMatchObject({
|
||||
bytes: 32 * 1024 * 1024,
|
||||
source: "default",
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { runWithAnalysisOperationSnapshot } from "../domain/analysisOperationSnapshot.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { launchAnalysisSetting } from "../config/analysisSettings.js";
|
||||
import { AsyncLocalStorage } from "node:async_hooks";
|
||||
@@ -42,7 +43,9 @@ export class AnalysisSettings {
|
||||
readonly read = (): AnalysisSettingsSnapshot =>
|
||||
this.#accepted.getStore() ?? this.#current;
|
||||
run<Value>(operation: () => Value): Value {
|
||||
return this.#accepted.run(this.#current, operation);
|
||||
return this.#accepted.run(this.#current, () =>
|
||||
runWithAnalysisOperationSnapshot(this.#current, operation),
|
||||
);
|
||||
}
|
||||
inspect(input: z.infer<typeof inspectAnalysisSettingsInputSchema> = {}) {
|
||||
return {
|
||||
|
||||
@@ -17,6 +17,17 @@ import { regexMatchingDeadlineMsSchema } from "./regexMatchingDeadline.js";
|
||||
import { regexWorkerHeapRequestMiBSchema } from "./regexWorkerHeapRequest.js";
|
||||
import { captureDecoderOldGenerationMiBSchema } from "./captureDecoderHeap.js";
|
||||
import { parseNativeMatchingAllowance } from "./nativeMatchingAllowance.js";
|
||||
import { bridgeMaxRequestBytesSchema } from "./bridgeMaxRequestBytes.js";
|
||||
import {
|
||||
debuggerCommandDeadlineMsSchema,
|
||||
debuggerMaxCommandOutputBytesSchema,
|
||||
} from "./debuggerSessionLimits.js";
|
||||
import {
|
||||
windowsChangeTimeDriftMsSchema,
|
||||
windowsChangeTimeSettleMaxPollsSchema,
|
||||
windowsChangeTimeSettlePollMsSchema,
|
||||
} from "./windowsChangeTimeSettle.js";
|
||||
import { pwntoolsInputBytesSchema } from "../native/pwntools/PwntoolsInputLimit.js";
|
||||
const environmentSchemas: Readonly<Record<string, z.ZodType<number, string>>> =
|
||||
{
|
||||
REA_ASAR_MAX_HEADER_BYTES: asarMaxHeaderBytesSchema,
|
||||
@@ -33,6 +44,18 @@ const environmentSchemas: Readonly<Record<string, z.ZodType<number, string>>> =
|
||||
REA_REGEX_WORKER_HEAP_REQUEST_MIB: regexWorkerHeapRequestMiBSchema,
|
||||
REA_CAPTURE_DECODER_OLD_GENERATION_MIB:
|
||||
captureDecoderOldGenerationMiBSchema,
|
||||
REA_BRIDGE_MAX_REQUEST_BYTES: bridgeMaxRequestBytesSchema,
|
||||
REA_DEBUGGER_COMMAND_DEADLINE_MS: debuggerCommandDeadlineMsSchema,
|
||||
REA_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES: debuggerMaxCommandOutputBytesSchema,
|
||||
REA_HOPPER_CALLER_SCAN_BUDGET_MS: providerStartupTimeoutSchema,
|
||||
REA_JEB_MCP_TIMEOUT_MS: providerStartupTimeoutSchema,
|
||||
REA_KUNA_PROJECT_TIMEOUT_MS: providerStartupTimeoutSchema,
|
||||
REA_KUNA_TIMEOUT_MS: providerStartupTimeoutSchema,
|
||||
REA_PWNTOOLS_MAX_INPUT_BYTES: pwntoolsInputBytesSchema,
|
||||
REA_WINDOWS_CHANGE_TIME_DRIFT_MS: windowsChangeTimeDriftMsSchema,
|
||||
REA_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS: windowsChangeTimeSettlePollMsSchema,
|
||||
REA_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS:
|
||||
windowsChangeTimeSettleMaxPollsSchema,
|
||||
};
|
||||
export const launchAnalysisSetting = (
|
||||
id: AnalysisSettingId,
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
import { positiveDecimalIntegerSchema } from "./positiveDecimalInteger.js";
|
||||
|
||||
import { analysisSettingLimit } from "../domain/analysisSettingDefinitions.js";
|
||||
import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js";
|
||||
import { ConfigurationError } from "../domain/configurationErrors.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
|
||||
import {
|
||||
DEFAULT_BRIDGE_MAX_REQUEST_BYTES,
|
||||
MAXIMUM_BRIDGE_MAX_REQUEST_BYTES,
|
||||
MINIMUM_BRIDGE_MAX_REQUEST_BYTES,
|
||||
} from "../domain/analysisResourceLimits.js";
|
||||
|
||||
export const BRIDGE_MAX_REQUEST_BYTES_SETTING = "REA_BRIDGE_MAX_REQUEST_BYTES";
|
||||
|
||||
/** Request payload ceiling used when the setting is unset. Today's fixed constant. */
|
||||
export const DEFAULT_BRIDGE_MAX_REQUEST_BYTES = 16 * 1024 * 1024;
|
||||
export {
|
||||
DEFAULT_BRIDGE_MAX_REQUEST_BYTES,
|
||||
MAXIMUM_BRIDGE_MAX_REQUEST_BYTES,
|
||||
MINIMUM_BRIDGE_MAX_REQUEST_BYTES,
|
||||
};
|
||||
|
||||
/**
|
||||
* Smallest ceiling that still admits the largest bounded bridge request.
|
||||
@@ -22,10 +32,7 @@ export const DEFAULT_BRIDGE_MAX_REQUEST_BYTES = 16 * 1024 * 1024;
|
||||
* Comments, search text, and Cutter command text are unbounded; this ceiling
|
||||
* is what limits them. The floor stays below the 16 MiB default so a caller
|
||||
* can lower the ceiling.
|
||||
*/
|
||||
export const MINIMUM_BRIDGE_MAX_REQUEST_BYTES = 7464;
|
||||
|
||||
/**
|
||||
*
|
||||
* Largest configurable request, in encoded payload bytes.
|
||||
*
|
||||
* The ceiling is the final JSON payload, not the raw command. Callers measure
|
||||
@@ -39,15 +46,11 @@ export const MINIMUM_BRIDGE_MAX_REQUEST_BYTES = 7464;
|
||||
* then parses that string. A fitting response does not keep the paged wire
|
||||
* frame, a second concatenated copy, and an outer JSON string.
|
||||
*/
|
||||
const NODE_MAX_STRING_LENGTH = 536_870_888;
|
||||
const FRAMING_LF_CODE_UNITS = 1;
|
||||
export const MAXIMUM_BRIDGE_MAX_REQUEST_BYTES =
|
||||
NODE_MAX_STRING_LENGTH - FRAMING_LF_CODE_UNITS;
|
||||
|
||||
/** Effective bridge request ceiling and where its value came from. */
|
||||
export interface BridgeMaxRequestBytes {
|
||||
readonly maximumBytes: number;
|
||||
readonly source: "default" | "setting";
|
||||
readonly source: "default" | "setting" | "runtime";
|
||||
}
|
||||
|
||||
export const DEFAULT_BRIDGE_MAX_REQUEST: BridgeMaxRequestBytes = {
|
||||
@@ -107,16 +110,22 @@ export const resolveBridgeMaxRequestBytes = (
|
||||
};
|
||||
|
||||
/** How to read and change the effective bridge request ceiling. */
|
||||
export const bridgeRequestLimit = (limit: BridgeMaxRequestBytes) =>
|
||||
analysisSettingLimit(
|
||||
"bridge.max_request_bytes",
|
||||
limit.maximumBytes,
|
||||
limit.source,
|
||||
);
|
||||
|
||||
export const bridgeMaxRequestBytesRecovery = (
|
||||
limit: BridgeMaxRequestBytes,
|
||||
peer: BridgePeer = "Hopper",
|
||||
): string =>
|
||||
[
|
||||
`The effective ${BRIDGE_MAX_REQUEST_BYTES_SETTING} value is ${String(limit.maximumBytes)} bytes (source ${limit.source}; default ${String(DEFAULT_BRIDGE_MAX_REQUEST_BYTES)}, floor ${String(MINIMUM_BRIDGE_MAX_REQUEST_BYTES)}, maximum ${String(MAXIMUM_BRIDGE_MAX_REQUEST_BYTES)}).`,
|
||||
`Set ${BRIDGE_MAX_REQUEST_BYTES_SETTING} in the environment that starts REA to a decimal integer from ${String(MINIMUM_BRIDGE_MAX_REQUEST_BYTES)} to ${String(MAXIMUM_BRIDGE_MAX_REQUEST_BYTES)}, then restart REA.`,
|
||||
`The effective bridge.max_request_bytes value is ${String(limit.maximumBytes)} bytes (source ${limit.source}; default ${String(DEFAULT_BRIDGE_MAX_REQUEST_BYTES)}, floor ${String(MINIMUM_BRIDGE_MAX_REQUEST_BYTES)}, maximum ${String(MAXIMUM_BRIDGE_MAX_REQUEST_BYTES)}). It can be changed at runtime with update_analysis_settings. The launch seed is ${BRIDGE_MAX_REQUEST_BYTES_SETTING}.`,
|
||||
peer === "Ghidra"
|
||||
? `The Ghidra bridge enforces no request ceiling of its own, so only the REA client applies ${BRIDGE_MAX_REQUEST_BYTES_SETTING} to Ghidra requests.`
|
||||
: "Restart Hopper or Cutter from that same environment so the bridge uses this ceiling.",
|
||||
? `The Ghidra bridge enforces no request ceiling of its own, so only the REA client applies bridge.max_request_bytes to Ghidra requests.`
|
||||
: "The Hopper or Cutter bridge keeps the ceiling it started with until that bridge starts again.",
|
||||
].join(" ");
|
||||
|
||||
const limitFacts = (
|
||||
@@ -142,7 +151,11 @@ export const bridgeRequestOversizeError = (
|
||||
"transport",
|
||||
`${refusal} ${recovery}`,
|
||||
{ ...limitFacts(limit), payload_bytes: payloadBytes },
|
||||
{ remediationAction: recovery, retryable: false },
|
||||
{
|
||||
remediationAction: recovery,
|
||||
retryable: false,
|
||||
limit: bridgeRequestLimit(limit),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -163,7 +176,11 @@ export const bridgeRequestLimitMismatchError = (
|
||||
bridge_max_request_bytes: bridgeBytes,
|
||||
client_max_request_bytes: limit.maximumBytes,
|
||||
},
|
||||
{ remediationAction: recovery, retryable: false },
|
||||
{
|
||||
remediationAction: recovery,
|
||||
retryable: false,
|
||||
limit: bridgeRequestLimit(limit),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -177,6 +194,10 @@ export const bridgeResponseOversizeError = (
|
||||
"transport",
|
||||
`Cutter bridge response exceeded its framed transport limit of ${String(limit.maximumBytes)} bytes. ${recovery}`,
|
||||
limitFacts(limit),
|
||||
{ remediationAction: recovery, retryable: false },
|
||||
{
|
||||
remediationAction: recovery,
|
||||
retryable: false,
|
||||
limit: bridgeRequestLimit(limit),
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -34,7 +34,7 @@ export const DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING =
|
||||
export interface DebuggerAdmissionLimit {
|
||||
readonly setting: string;
|
||||
readonly value: number;
|
||||
readonly source: "default" | "setting";
|
||||
readonly source: "default" | "setting" | "runtime";
|
||||
readonly defaultValue: number;
|
||||
readonly minimum: number;
|
||||
readonly maximum: number;
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { parseConfig } from "./parseConfig.js";
|
||||
import {
|
||||
HOPPER_CALLER_SCAN_BUDGET_SETTING,
|
||||
hopperCallerScanLimit,
|
||||
hopperCallerScanRecovery,
|
||||
} from "./hopperCallerScanBudget.js";
|
||||
|
||||
@@ -57,21 +58,16 @@ it("explains the time budget and that fixed byte allowances have no override", (
|
||||
const parsed = parseConfig({ [HOPPER_CALLER_SCAN_BUDGET_SETTING]: "250" });
|
||||
if (!parsed.ok) throw parsed.error;
|
||||
const budget = parsed.value.hopperCallerScanBudget;
|
||||
const time = hopperCallerScanRecovery(
|
||||
facts({ stop: { code: "work_budget", budget_ms: 250 } }),
|
||||
budget,
|
||||
);
|
||||
expect(time).toContain(`${HOPPER_CALLER_SCAN_BUDGET_SETTING} is 250 ms`);
|
||||
expect(time).toContain(
|
||||
`The default is ${String(DEFAULT_HOPPER_CALLER_SCAN_BUDGET_MS)} ms`,
|
||||
);
|
||||
expect(time).toContain(
|
||||
`from ${String(MINIMUM_HOPPER_CALLER_SCAN_BUDGET_MS)} to ${String(MAX_NODE_TIMER_DELAY_MS)} ms`,
|
||||
);
|
||||
expect(time).toContain("no runtime override");
|
||||
expect(time).toContain(
|
||||
`${HOPPER_CALLER_SCAN_BUDGET_SETTING} does not raise them`,
|
||||
);
|
||||
expect(hopperCallerScanLimit(budget)).toMatchObject({
|
||||
setting: "hopper.caller_scan_budget_ms",
|
||||
effective_value: 250,
|
||||
source: "setting",
|
||||
minimum: MINIMUM_HOPPER_CALLER_SCAN_BUDGET_MS,
|
||||
maximum: MAX_NODE_TIMER_DELAY_MS,
|
||||
effect: "immediate",
|
||||
rea_restart_required: false,
|
||||
});
|
||||
expect(DEFAULT_HOPPER_CALLER_SCAN_BUDGET_MS).toBe(2000);
|
||||
const page = hopperCallerScanRecovery(
|
||||
facts({
|
||||
stop: {
|
||||
@@ -82,9 +78,7 @@ it("explains the time budget and that fixed byte allowances have no override", (
|
||||
budget,
|
||||
);
|
||||
expect(page).toContain("no runtime override");
|
||||
expect(page).toContain(
|
||||
`${HOPPER_CALLER_SCAN_BUDGET_SETTING} does not raise them`,
|
||||
);
|
||||
expect(page).toContain("hopper.caller_scan_budget_ms does not raise them");
|
||||
expect(page).not.toContain("is 250 ms");
|
||||
expect(
|
||||
hopperCallerScanRecovery(facts({ stop: { code: "complete" } }), budget),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { analysisSettingLimit } from "../domain/analysisSettingDefinitions.js";
|
||||
import {
|
||||
DEFAULT_HOPPER_CALLER_SCAN_BUDGET_MS,
|
||||
MAX_NODE_TIMER_DELAY_MS,
|
||||
@@ -16,7 +17,7 @@ export const HOPPER_CALLER_SCAN_BUDGET_SETTING =
|
||||
export interface HopperCallerScanBudget {
|
||||
readonly setting: typeof HOPPER_CALLER_SCAN_BUDGET_SETTING;
|
||||
readonly milliseconds: number;
|
||||
readonly source: "default" | "setting";
|
||||
readonly source: "default" | "setting" | "runtime";
|
||||
readonly defaultMilliseconds: number;
|
||||
readonly floorMilliseconds: number;
|
||||
readonly maximumMilliseconds: number;
|
||||
@@ -52,7 +53,15 @@ export const hopperCallerScanBudgetForMilliseconds = (
|
||||
milliseconds,
|
||||
);
|
||||
|
||||
const fixedAllowanceRecovery = `This release has no runtime override for the ${String(HOPPER_CALLER_PAGE_ALLOWANCE_BYTES)} byte instruction, native-caller, and page allowances or the ${String(HOPPER_CALLER_SCAN_RETENTION_BYTES)} byte retention budget. ${HOPPER_CALLER_SCAN_BUDGET_SETTING} does not raise them.`;
|
||||
const fixedAllowanceRecovery = `This release has no runtime override for the ${String(HOPPER_CALLER_PAGE_ALLOWANCE_BYTES)} byte instruction, native-caller, and page allowances or the ${String(HOPPER_CALLER_SCAN_RETENTION_BYTES)} byte retention budget. hopper.caller_scan_budget_ms does not raise them.`;
|
||||
|
||||
/** Structured facts for the scan time budget. The sentence is display text. */
|
||||
export const hopperCallerScanLimit = (budget: HopperCallerScanBudget) =>
|
||||
analysisSettingLimit(
|
||||
"hopper.caller_scan_budget_ms",
|
||||
budget.milliseconds,
|
||||
budget.source,
|
||||
);
|
||||
|
||||
/** Environment recovery for a caller-scan limit. Domain facts stay separate. */
|
||||
export const hopperCallerScanRecovery = (
|
||||
@@ -64,7 +73,9 @@ export const hopperCallerScanRecovery = (
|
||||
facts.stop.code === "retention_budget" ||
|
||||
!facts.instructions_complete ||
|
||||
facts.native_callers_complete === false;
|
||||
if (facts.stop.code === "work_budget")
|
||||
return `${budget.setting} is ${String(budget.milliseconds)} ms (source: ${budget.source}). The default is ${String(budget.defaultMilliseconds)} ms. Allowed values are integers from ${String(budget.floorMilliseconds)} to ${String(budget.maximumMilliseconds)} ms. Set it in the environment that starts REA and restart REA. ${fixedAllowanceRecovery}`;
|
||||
if (facts.stop.code === "work_budget") {
|
||||
const limit = hopperCallerScanLimit(budget);
|
||||
return `hopper.caller_scan_budget_ms is ${String(limit.effective_value)} ms (source ${limit.source ?? "unknown"}; default ${String(budget.defaultMilliseconds)} ms, floor ${String(limit.minimum)} ms, maximum ${String(limit.maximum)} ms). It can be changed at runtime with update_analysis_settings. The launch seed is ${budget.setting}. ${fixedAllowanceRecovery}`;
|
||||
}
|
||||
return fixedLimit ? fixedAllowanceRecovery : "";
|
||||
};
|
||||
@@ -29,6 +29,8 @@ export const runtimeAnalysisConfig = (
|
||||
if (read === undefined) return config;
|
||||
const deadline = config.regexMatchingDeadline;
|
||||
const heap = config.regexWorkerHeapRequest;
|
||||
const scan = config.hopperCallerScanBudget;
|
||||
const bridge = config.bridgeMaxRequestBytes;
|
||||
return {
|
||||
...config,
|
||||
get ghidraStartupTimeoutMs() {
|
||||
@@ -104,6 +106,31 @@ export const runtimeAnalysisConfig = (
|
||||
: "runtime";
|
||||
},
|
||||
},
|
||||
hopperCallerScanBudget: {
|
||||
...scan,
|
||||
get milliseconds() {
|
||||
return (
|
||||
read().overrides["hopper.caller_scan_budget_ms"] ?? scan.milliseconds
|
||||
);
|
||||
},
|
||||
get source() {
|
||||
return read().overrides["hopper.caller_scan_budget_ms"] === undefined
|
||||
? scan.source
|
||||
: "runtime";
|
||||
},
|
||||
},
|
||||
bridgeMaxRequestBytes: {
|
||||
get maximumBytes() {
|
||||
return (
|
||||
read().overrides["bridge.max_request_bytes"] ?? bridge.maximumBytes
|
||||
);
|
||||
},
|
||||
get source() {
|
||||
return read().overrides["bridge.max_request_bytes"] === undefined
|
||||
? bridge.source
|
||||
: "runtime";
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
export const runtimeAsarHeaderAdmission = (
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { currentAnalysisOperationSnapshot } from "../domain/analysisOperationSnapshot.js";
|
||||
import {
|
||||
DEFAULT_WINDOWS_CHANGE_TIME_DRIFT_MS,
|
||||
DEFAULT_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS,
|
||||
DEFAULT_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS,
|
||||
MAXIMUM_WINDOWS_CHANGE_TIME_DRIFT_MS,
|
||||
MAXIMUM_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS,
|
||||
MAXIMUM_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS,
|
||||
} from "../domain/analysisResourceLimits.js";
|
||||
import { ConfigurationError } from "../domain/configurationErrors.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
|
||||
@@ -19,18 +28,18 @@ interface WindowsChangeTimeBound {
|
||||
*/
|
||||
export const WINDOWS_CHANGE_TIME_DRIFT_MS = {
|
||||
setting: "REA_WINDOWS_CHANGE_TIME_DRIFT_MS",
|
||||
defaultValue: 2_000,
|
||||
defaultValue: DEFAULT_WINDOWS_CHANGE_TIME_DRIFT_MS,
|
||||
floor: 1,
|
||||
maximum: 60_000,
|
||||
maximum: MAXIMUM_WINDOWS_CHANGE_TIME_DRIFT_MS,
|
||||
unit: "ms",
|
||||
} as const satisfies WindowsChangeTimeBound;
|
||||
|
||||
/** Delay between ChangeTime observations. Unset keeps the 25 ms poll. */
|
||||
export const WINDOWS_CHANGE_TIME_SETTLE_POLL_MS = {
|
||||
setting: "REA_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS",
|
||||
defaultValue: 25,
|
||||
defaultValue: DEFAULT_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS,
|
||||
floor: 1,
|
||||
maximum: 1_000,
|
||||
maximum: MAXIMUM_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS,
|
||||
unit: "ms",
|
||||
} as const satisfies WindowsChangeTimeBound;
|
||||
|
||||
@@ -40,9 +49,9 @@ export const WINDOWS_CHANGE_TIME_SETTLE_POLL_MS = {
|
||||
*/
|
||||
export const WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS = {
|
||||
setting: "REA_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS",
|
||||
defaultValue: 40,
|
||||
defaultValue: DEFAULT_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS,
|
||||
floor: 1,
|
||||
maximum: 1_000,
|
||||
maximum: MAXIMUM_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS,
|
||||
unit: "polls",
|
||||
} as const satisfies WindowsChangeTimeBound;
|
||||
|
||||
@@ -61,11 +70,11 @@ export interface WindowsChangeTimeSettingFailure {
|
||||
/** Effective settle budget and whether each part came from the default or a setting. */
|
||||
export interface WindowsChangeTimeSettleLimits {
|
||||
readonly driftMs: number;
|
||||
readonly driftSource: "default" | "setting";
|
||||
readonly driftSource: "default" | "setting" | "runtime";
|
||||
readonly pollMs: number;
|
||||
readonly pollSource: "default" | "setting";
|
||||
readonly pollSource: "default" | "setting" | "runtime";
|
||||
readonly maxPolls: number;
|
||||
readonly maxPollsSource: "default" | "setting";
|
||||
readonly maxPollsSource: "default" | "setting" | "runtime";
|
||||
}
|
||||
|
||||
const windowsChangeTimeSettingFailure = (
|
||||
@@ -176,3 +185,22 @@ export const resolveWindowsChangeTimeSettle = (
|
||||
maxPollsSource: maxPolls === undefined ? "default" : "setting",
|
||||
});
|
||||
};
|
||||
|
||||
/** Apply the accepted operation snapshot. An explicit caller budget is left alone. */
|
||||
export const applyWindowsChangeTimeSnapshot = (
|
||||
limits: WindowsChangeTimeSettleLimits,
|
||||
): WindowsChangeTimeSettleLimits => {
|
||||
const overrides = currentAnalysisOperationSnapshot()?.overrides;
|
||||
if (overrides === undefined) return limits;
|
||||
const driftMs = overrides["windows.change_time_drift_ms"];
|
||||
const pollMs = overrides["windows.change_time_settle_poll_ms"];
|
||||
const maxPolls = overrides["windows.change_time_settle_max_polls"];
|
||||
return {
|
||||
driftMs: driftMs ?? limits.driftMs,
|
||||
driftSource: driftMs === undefined ? limits.driftSource : "runtime",
|
||||
pollMs: pollMs ?? limits.pollMs,
|
||||
pollSource: pollMs === undefined ? limits.pollSource : "runtime",
|
||||
maxPolls: maxPolls ?? limits.maxPolls,
|
||||
maxPollsSource: maxPolls === undefined ? limits.maxPollsSource : "runtime",
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { AsyncLocalStorage } from "node:async_hooks";
|
||||
import type { AnalysisSettingsSnapshot } from "./analysisSettings.js";
|
||||
|
||||
const snapshots = new AsyncLocalStorage<AnalysisSettingsSnapshot>();
|
||||
|
||||
/**
|
||||
* Bind the snapshot accepted for one operation. Providers and filesystem
|
||||
* helpers read it at the point they enforce a limit. A later update does not
|
||||
* change work already inside this call.
|
||||
*/
|
||||
export const runWithAnalysisOperationSnapshot = <Value>(
|
||||
snapshot: AnalysisSettingsSnapshot,
|
||||
operation: () => Value,
|
||||
): Value => snapshots.run(snapshot, operation);
|
||||
|
||||
/** The snapshot for the operation on this async stack, when one was accepted. */
|
||||
export const currentAnalysisOperationSnapshot = ():
|
||||
| AnalysisSettingsSnapshot
|
||||
| undefined => snapshots.getStore();
|
||||
@@ -191,3 +191,28 @@ export const MAXIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES = Math.floor(
|
||||
* a caller cap below this budget must not truncate those required replies.
|
||||
*/
|
||||
export const DEBUGGER_CONTROL_REPLY_BYTES = 16 * 1024 * 1024;
|
||||
|
||||
/** Hopper, Ghidra, and Cutter request payload ceiling. Same bound as the bridge owner. */
|
||||
export const DEFAULT_BRIDGE_MAX_REQUEST_BYTES = 16 * 1024 * 1024;
|
||||
export const MINIMUM_BRIDGE_MAX_REQUEST_BYTES = 7464;
|
||||
export const MAXIMUM_BRIDGE_MAX_REQUEST_BYTES = 536_870_888 - 1;
|
||||
|
||||
/** MCP client SDK default request timeout. JEB uses it when the setting is unset. */
|
||||
export const DEFAULT_JEB_MCP_TIMEOUT_MS = 60_000;
|
||||
|
||||
/** Kuna project export and whole-program scans. */
|
||||
export const DEFAULT_KUNA_PROJECT_TIMEOUT_MS = 30 * 60 * 1000;
|
||||
/** One Kuna function decompile. */
|
||||
export const DEFAULT_KUNA_FUNCTION_TIMEOUT_MS = 120_000;
|
||||
|
||||
/** Pwntools artifact admission when producer_limits.input_bytes is omitted. */
|
||||
export const DEFAULT_PWNTOOLS_MAX_INPUT_BYTES = 32 * 1024 * 1024;
|
||||
export const MAXIMUM_PWNTOOLS_MAX_INPUT_BYTES = 1024 * 1024 * 1024;
|
||||
|
||||
/** Windows ChangeTime settle window, poll, and poll count. */
|
||||
export const DEFAULT_WINDOWS_CHANGE_TIME_DRIFT_MS = 2_000;
|
||||
export const MAXIMUM_WINDOWS_CHANGE_TIME_DRIFT_MS = 60_000;
|
||||
export const DEFAULT_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS = 25;
|
||||
export const MAXIMUM_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS = 1_000;
|
||||
export const DEFAULT_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS = 40;
|
||||
export const MAXIMUM_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS = 1_000;
|
||||
@@ -29,6 +29,28 @@ import {
|
||||
DEFAULT_JADX_OPERATION_TIMEOUT_MS,
|
||||
DEFAULT_NATIVE_MATCHING_REPRESENTATION_BYTES,
|
||||
MAXIMUM_NATIVE_MATCHING_REPRESENTATION_BYTES,
|
||||
DEFAULT_BRIDGE_MAX_REQUEST_BYTES,
|
||||
MINIMUM_BRIDGE_MAX_REQUEST_BYTES,
|
||||
MAXIMUM_BRIDGE_MAX_REQUEST_BYTES,
|
||||
DEFAULT_DEBUGGER_COMMAND_DEADLINE_MS,
|
||||
MINIMUM_DEBUGGER_COMMAND_DEADLINE_MS,
|
||||
MAXIMUM_DEBUGGER_COMMAND_DEADLINE_MS,
|
||||
DEFAULT_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES,
|
||||
MINIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES,
|
||||
MAXIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES,
|
||||
DEFAULT_HOPPER_CALLER_SCAN_BUDGET_MS,
|
||||
MINIMUM_HOPPER_CALLER_SCAN_BUDGET_MS,
|
||||
DEFAULT_JEB_MCP_TIMEOUT_MS,
|
||||
DEFAULT_KUNA_FUNCTION_TIMEOUT_MS,
|
||||
DEFAULT_KUNA_PROJECT_TIMEOUT_MS,
|
||||
DEFAULT_PWNTOOLS_MAX_INPUT_BYTES,
|
||||
MAXIMUM_PWNTOOLS_MAX_INPUT_BYTES,
|
||||
DEFAULT_WINDOWS_CHANGE_TIME_DRIFT_MS,
|
||||
MAXIMUM_WINDOWS_CHANGE_TIME_DRIFT_MS,
|
||||
DEFAULT_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS,
|
||||
MAXIMUM_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS,
|
||||
DEFAULT_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS,
|
||||
MAXIMUM_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS,
|
||||
} from "./analysisResourceLimits.js";
|
||||
import {
|
||||
JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS,
|
||||
@@ -313,6 +335,139 @@ export const ANALYSIS_SETTINGS = {
|
||||
provider: "rea-function-comparison",
|
||||
operations: ["match_native_functions"],
|
||||
},
|
||||
"bridge.max_request_bytes": {
|
||||
environment: "REA_BRIDGE_MAX_REQUEST_BYTES",
|
||||
defaultValue: DEFAULT_BRIDGE_MAX_REQUEST_BYTES,
|
||||
minimum: MINIMUM_BRIDGE_MAX_REQUEST_BYTES,
|
||||
maximum: MAXIMUM_BRIDGE_MAX_REQUEST_BYTES,
|
||||
unit: "bytes",
|
||||
appliesAt: "next_operation",
|
||||
provider: "hopper",
|
||||
relatedProviders: ["ghidra", "cutter"],
|
||||
operations: [
|
||||
"procedure_pseudo_code",
|
||||
"analyze_function",
|
||||
"search_strings",
|
||||
"set_address_name",
|
||||
],
|
||||
},
|
||||
"debugger.command_deadline_ms": {
|
||||
...timer,
|
||||
environment: "REA_DEBUGGER_COMMAND_DEADLINE_MS",
|
||||
defaultValue: DEFAULT_DEBUGGER_COMMAND_DEADLINE_MS,
|
||||
minimum: MINIMUM_DEBUGGER_COMMAND_DEADLINE_MS,
|
||||
maximum: MAXIMUM_DEBUGGER_COMMAND_DEADLINE_MS,
|
||||
provider: "gdb",
|
||||
relatedProviders: ["rizin"],
|
||||
operations: [
|
||||
"start_gdb_session",
|
||||
"gdb_console",
|
||||
"start_rizin_debug_session",
|
||||
"rizin_debug_command",
|
||||
],
|
||||
},
|
||||
"debugger.max_command_output_bytes": {
|
||||
environment: "REA_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES",
|
||||
defaultValue: DEFAULT_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES,
|
||||
minimum: MINIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES,
|
||||
maximum: MAXIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES,
|
||||
unit: "bytes",
|
||||
appliesAt: "next_operation",
|
||||
provider: "gdb",
|
||||
relatedProviders: ["rizin"],
|
||||
operations: ["gdb_console", "rizin_debug_command"],
|
||||
},
|
||||
"hopper.caller_scan_budget_ms": {
|
||||
...timer,
|
||||
environment: "REA_HOPPER_CALLER_SCAN_BUDGET_MS",
|
||||
defaultValue: DEFAULT_HOPPER_CALLER_SCAN_BUDGET_MS,
|
||||
minimum: MINIMUM_HOPPER_CALLER_SCAN_BUDGET_MS,
|
||||
provider: "hopper",
|
||||
operations: [
|
||||
"procedure_callers",
|
||||
"analyze_function",
|
||||
"procedure_references",
|
||||
],
|
||||
},
|
||||
"jeb.mcp_timeout_ms": {
|
||||
...timer,
|
||||
environment: "REA_JEB_MCP_TIMEOUT_MS",
|
||||
defaultValue: DEFAULT_JEB_MCP_TIMEOUT_MS,
|
||||
provider: "jeb",
|
||||
operations: [
|
||||
"inspect_jeb_client",
|
||||
"open_jeb_project",
|
||||
"list_jeb_units",
|
||||
"decompile_jeb_item",
|
||||
],
|
||||
},
|
||||
"kuna.project_timeout_ms": {
|
||||
...timer,
|
||||
environment: "REA_KUNA_PROJECT_TIMEOUT_MS",
|
||||
defaultValue: DEFAULT_KUNA_PROJECT_TIMEOUT_MS,
|
||||
provider: "kuna",
|
||||
operations: [
|
||||
"list_strings",
|
||||
"search_strings",
|
||||
"list_procedures",
|
||||
"search_procedures",
|
||||
"xrefs",
|
||||
"procedure_callers",
|
||||
"procedure_callees",
|
||||
],
|
||||
},
|
||||
"kuna.function_timeout_ms": {
|
||||
...timer,
|
||||
environment: "REA_KUNA_TIMEOUT_MS",
|
||||
defaultValue: DEFAULT_KUNA_FUNCTION_TIMEOUT_MS,
|
||||
provider: "kuna",
|
||||
operations: [
|
||||
"procedure_pseudo_code",
|
||||
"procedure_assembly",
|
||||
"analyze_function",
|
||||
],
|
||||
},
|
||||
"pwntools.max_input_bytes": {
|
||||
environment: "REA_PWNTOOLS_MAX_INPUT_BYTES",
|
||||
defaultValue: DEFAULT_PWNTOOLS_MAX_INPUT_BYTES,
|
||||
minimum: 1,
|
||||
maximum: MAXIMUM_PWNTOOLS_MAX_INPUT_BYTES,
|
||||
unit: "bytes",
|
||||
appliesAt: "next_operation",
|
||||
provider: "pwntools",
|
||||
operations: ["inspect_binary_layout", "inspect_recorded_crash"],
|
||||
parameter: "input_bytes",
|
||||
},
|
||||
"windows.change_time_drift_ms": {
|
||||
environment: "REA_WINDOWS_CHANGE_TIME_DRIFT_MS",
|
||||
defaultValue: DEFAULT_WINDOWS_CHANGE_TIME_DRIFT_MS,
|
||||
minimum: 1,
|
||||
maximum: MAXIMUM_WINDOWS_CHANGE_TIME_DRIFT_MS,
|
||||
unit: "ms",
|
||||
appliesAt: "next_operation",
|
||||
provider: "rea-artifact-graph",
|
||||
operations: ["inventory_artifact", "inspect_artifact", "extract_artifact"],
|
||||
},
|
||||
"windows.change_time_settle_poll_ms": {
|
||||
environment: "REA_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS",
|
||||
defaultValue: DEFAULT_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS,
|
||||
minimum: 1,
|
||||
maximum: MAXIMUM_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS,
|
||||
unit: "ms",
|
||||
appliesAt: "next_operation",
|
||||
provider: "rea-artifact-graph",
|
||||
operations: ["inventory_artifact", "inspect_artifact", "extract_artifact"],
|
||||
},
|
||||
"windows.change_time_settle_max_polls": {
|
||||
environment: "REA_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS",
|
||||
defaultValue: DEFAULT_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS,
|
||||
minimum: 1,
|
||||
maximum: MAXIMUM_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS,
|
||||
unit: "count",
|
||||
appliesAt: "next_operation",
|
||||
provider: "rea-artifact-graph",
|
||||
operations: ["inventory_artifact", "inspect_artifact", "extract_artifact"],
|
||||
},
|
||||
} as const satisfies Record<AnalysisSettingId, SettingDefinition>;
|
||||
export const analysisSettingIds = Object.keys(
|
||||
ANALYSIS_SETTINGS,
|
||||
|
||||
@@ -17,6 +17,17 @@ export interface AnalysisSettingValues {
|
||||
readonly "javascript.max_heap_mb": number;
|
||||
readonly "wasm.command_timeout_ms": number;
|
||||
readonly "native.matching_representation_bytes": number;
|
||||
readonly "bridge.max_request_bytes": number;
|
||||
readonly "debugger.command_deadline_ms": number;
|
||||
readonly "debugger.max_command_output_bytes": number;
|
||||
readonly "hopper.caller_scan_budget_ms": number;
|
||||
readonly "jeb.mcp_timeout_ms": number;
|
||||
readonly "kuna.project_timeout_ms": number;
|
||||
readonly "kuna.function_timeout_ms": number;
|
||||
readonly "pwntools.max_input_bytes": number;
|
||||
readonly "windows.change_time_drift_ms": number;
|
||||
readonly "windows.change_time_settle_poll_ms": number;
|
||||
readonly "windows.change_time_settle_max_polls": number;
|
||||
}
|
||||
export type AnalysisSettingId = keyof AnalysisSettingValues;
|
||||
export interface AnalysisSettingsSnapshot {
|
||||
|
||||
@@ -39,22 +39,14 @@ it("gives every numeric exception a reason and a matching inventory row", () =>
|
||||
for (const entry of NUMERIC_LIMIT_REGISTRY) {
|
||||
expect(entry.update_mode).toBe("restart");
|
||||
expect(entry.reason.trim().length).toBeGreaterThanOrEqual(40);
|
||||
if (entry.policy === "restart-only")
|
||||
expect(["startup", "construction"]).toContain(entry.read);
|
||||
expect(entry.read).not.toBe(
|
||||
entry.policy === "per-call" ? "startup" : "per-call",
|
||||
);
|
||||
expect(entry.policy).toBe("restart-only");
|
||||
expect(["startup", "construction"]).toContain(entry.read);
|
||||
expect(runtimeEnvironments.has(entry.name)).toBe(false);
|
||||
const row = exceptionRow(entry.name);
|
||||
expect(row.length).toBeGreaterThanOrEqual(5);
|
||||
expect(row[1]?.length ?? 0).toBeGreaterThan(0);
|
||||
}
|
||||
expect(
|
||||
NUMERIC_LIMIT_REGISTRY.filter((entry) => entry.policy === "restart-only"),
|
||||
).toHaveLength(15);
|
||||
expect(
|
||||
NUMERIC_LIMIT_REGISTRY.filter((entry) => entry.policy === "per-call"),
|
||||
).toHaveLength(11);
|
||||
expect(NUMERIC_LIMIT_REGISTRY).toHaveLength(15);
|
||||
for (const definition of Object.values(ANALYSIS_SETTINGS)) {
|
||||
if (!("environment" in definition) || definition.environment === undefined)
|
||||
continue;
|
||||
|
||||
@@ -4,9 +4,8 @@
|
||||
* The static check in `scripts/verify/rea-numeric-limits.mjs` checks recognized
|
||||
* TypeScript reads against this list and `ANALYSIS_SETTINGS`. External readers
|
||||
* and generated controls declare separate behavioral witnesses, which the
|
||||
* Node-only check cannot verify. `restart-only` is the durable exception. `per-call`
|
||||
* names a limit still read from the launch environment on each use; the
|
||||
* runtime migration deletes that entry instead of leaving it here.
|
||||
* Node-only check cannot verify. Every remaining entry is restart-only; limits
|
||||
* checked on each call belong in `ANALYSIS_SETTINGS` instead of this list.
|
||||
*
|
||||
* Narrative bounds, owners, and the non-numeric settings live in
|
||||
* `docs/design/rea-limit-inventory.md`.
|
||||
@@ -14,8 +13,8 @@
|
||||
export interface NumericLimitRegistryEntry {
|
||||
readonly name: string;
|
||||
readonly update_mode: "restart";
|
||||
readonly policy: "restart-only" | "per-call";
|
||||
readonly read: "startup" | "per-call" | "construction";
|
||||
readonly policy: "restart-only";
|
||||
readonly read: "startup" | "construction";
|
||||
readonly reason: string;
|
||||
/** Read outside the TypeScript scan or generated into a bridge; execution needs its behavioral witness. */
|
||||
readonly provenance?: {
|
||||
@@ -28,30 +27,6 @@ export interface NumericLimitRegistryEntry {
|
||||
}
|
||||
|
||||
export const NUMERIC_LIMIT_REGISTRY = [
|
||||
{
|
||||
name: "REA_BRIDGE_MAX_REQUEST_BYTES",
|
||||
update_mode: "restart",
|
||||
policy: "per-call",
|
||||
read: "per-call",
|
||||
reason:
|
||||
"The Hopper, Ghidra, and Cutter clients measure each request against this ceiling. The value is still taken from the launch environment, so a running process does not see a new value until the runtime migration removes this entry.",
|
||||
},
|
||||
{
|
||||
name: "REA_DEBUGGER_COMMAND_DEADLINE_MS",
|
||||
update_mode: "restart",
|
||||
policy: "per-call",
|
||||
read: "construction",
|
||||
reason:
|
||||
"GDB and Rizin apply this deadline to each debugger command. The session manager currently copies it at construction from the launch environment; the runtime migration reads the accepted operation snapshot instead.",
|
||||
},
|
||||
{
|
||||
name: "REA_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES",
|
||||
update_mode: "restart",
|
||||
policy: "per-call",
|
||||
read: "construction",
|
||||
reason:
|
||||
"Each debugger command retains output up to this ceiling. The session manager currently copies it at construction from the launch environment; the runtime migration reads the accepted operation snapshot instead.",
|
||||
},
|
||||
{
|
||||
name: "REA_DEBUGGER_MAX_PENDING_COMMANDS",
|
||||
update_mode: "restart",
|
||||
@@ -68,14 +43,6 @@ export const NUMERIC_LIMIT_REGISTRY = [
|
||||
reason:
|
||||
"Caps how many debugger sessions the session manager created at startup will admit. Changing the pool size of a manager that already exists requires a new REA process.",
|
||||
},
|
||||
{
|
||||
name: "REA_HOPPER_CALLER_SCAN_BUDGET_MS",
|
||||
update_mode: "restart",
|
||||
policy: "per-call",
|
||||
read: "per-call",
|
||||
reason:
|
||||
"Forward caller reconciliation reads this budget for each scan. It is still resolved from the launch environment; the runtime migration reads the accepted operation snapshot.",
|
||||
},
|
||||
{
|
||||
name: "REA_HOPPER_DEMO_DIALOG_TIMEOUT_MS",
|
||||
update_mode: "restart",
|
||||
@@ -105,14 +72,6 @@ export const NUMERIC_LIMIT_REGISTRY = [
|
||||
reason:
|
||||
"Passed to the JVM as -Xmx when the JADX worker process starts. A running JVM does not adopt a new heap size.",
|
||||
},
|
||||
{
|
||||
name: "REA_JEB_MCP_TIMEOUT_MS",
|
||||
update_mode: "restart",
|
||||
policy: "per-call",
|
||||
read: "construction",
|
||||
reason:
|
||||
"Bounds each JEB tools/call and catalog discovery. The connection currently captures the launch value when it is created; the runtime migration reads the accepted snapshot for each request.",
|
||||
},
|
||||
{
|
||||
name: "REA_KUNA_CACHE_MAX_BYTES",
|
||||
update_mode: "restart",
|
||||
@@ -129,22 +88,6 @@ export const NUMERIC_LIMIT_REGISTRY = [
|
||||
reason:
|
||||
"Selects the Kuna worker-pool width when the provider builds its run slots. The pool is not resized for a process that is already running.",
|
||||
},
|
||||
{
|
||||
name: "REA_KUNA_PROJECT_TIMEOUT_MS",
|
||||
update_mode: "restart",
|
||||
policy: "per-call",
|
||||
read: "per-call",
|
||||
reason:
|
||||
"Bounds a Kuna project export and the whole-program scans that use that deadline. The configuration object still comes from the launch environment; the runtime migration reads the accepted snapshot at each use.",
|
||||
},
|
||||
{
|
||||
name: "REA_KUNA_TIMEOUT_MS",
|
||||
update_mode: "restart",
|
||||
policy: "per-call",
|
||||
read: "per-call",
|
||||
reason:
|
||||
"Bounds one Kuna function decompile. The configuration object still comes from the launch environment; the runtime migration reads the accepted snapshot at each use.",
|
||||
},
|
||||
{
|
||||
name: "REA_MCP_DOCTOR_DEADLINE_MS",
|
||||
update_mode: "restart",
|
||||
@@ -193,14 +136,6 @@ export const NUMERIC_LIMIT_REGISTRY = [
|
||||
reason:
|
||||
"Bounds the MCP tools/list phase while the server is starting. It shapes server startup, not a later analysis call.",
|
||||
},
|
||||
{
|
||||
name: "REA_PWNTOOLS_MAX_INPUT_BYTES",
|
||||
update_mode: "restart",
|
||||
policy: "per-call",
|
||||
read: "per-call",
|
||||
reason:
|
||||
"Default artifact-size admission for a pwntools decode. A per-call producer_limits.input_bytes override already exists; the launch default is still read from the environment and the runtime migration moves that default into the snapshot.",
|
||||
},
|
||||
{
|
||||
name: "REA_RECONNECT_TIMEOUT_SECONDS",
|
||||
update_mode: "restart",
|
||||
@@ -224,28 +159,4 @@ export const NUMERIC_LIMIT_REGISTRY = [
|
||||
reason:
|
||||
"Captured when the Darwin ownership-helper reader is constructed at process start. That helper compile uses the captured deadline, so a later value needs a new REA process.",
|
||||
},
|
||||
{
|
||||
name: "REA_WINDOWS_CHANGE_TIME_DRIFT_MS",
|
||||
update_mode: "restart",
|
||||
policy: "per-call",
|
||||
read: "per-call",
|
||||
reason:
|
||||
"Each Windows ChangeTime observation compares the file against this drift window. The observation still parses the launch environment; the runtime migration reads the accepted operation snapshot.",
|
||||
},
|
||||
{
|
||||
name: "REA_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS",
|
||||
update_mode: "restart",
|
||||
policy: "per-call",
|
||||
read: "per-call",
|
||||
reason:
|
||||
"Each Windows ChangeTime settle loop stops after this many polls. The observation still parses the launch environment; the runtime migration reads the accepted operation snapshot.",
|
||||
},
|
||||
{
|
||||
name: "REA_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS",
|
||||
update_mode: "restart",
|
||||
policy: "per-call",
|
||||
read: "per-call",
|
||||
reason:
|
||||
"Each Windows ChangeTime settle loop waits this long between reads. The observation still parses the launch environment; the runtime migration reads the accepted operation snapshot.",
|
||||
},
|
||||
] as const satisfies readonly NumericLimitRegistryEntry[];
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Stats } from "node:fs";
|
||||
|
||||
import {
|
||||
applyWindowsChangeTimeSnapshot,
|
||||
resolveWindowsChangeTimeSettle,
|
||||
type WindowsChangeTimeSettleLimits,
|
||||
} from "../config/windowsChangeTimeSettle.js";
|
||||
@@ -49,7 +50,7 @@ const limitsFrom = (
|
||||
seams.environment ?? process.env,
|
||||
);
|
||||
if (!resolved.ok) throw resolved.error;
|
||||
return resolved.value;
|
||||
return applyWindowsChangeTimeSnapshot(resolved.value);
|
||||
};
|
||||
|
||||
/** Stop settling as soon as the owning operation is cancelled. */
|
||||
|
||||
@@ -23,6 +23,7 @@ import type {
|
||||
GdbOutputLimit,
|
||||
} from "../domain/commandObservation.js";
|
||||
import { DEBUGGER_CONTROL_REPLY_BYTES } from "../domain/analysisResourceLimits.js";
|
||||
import { currentAnalysisOperationSnapshot } from "../domain/analysisOperationSnapshot.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import {
|
||||
ProviderProcessSupervisor,
|
||||
@@ -596,8 +597,7 @@ export class GdbSessionManager {
|
||||
>();
|
||||
readonly #command: string;
|
||||
readonly #environment: NodeJS.ProcessEnv;
|
||||
readonly #commandDeadline: DebuggerAdmissionLimit;
|
||||
readonly #commandTimeoutMs: number;
|
||||
readonly #launchCommandDeadline: DebuggerAdmissionLimit;
|
||||
readonly #startupTimeoutMs: number;
|
||||
readonly #platform: NodeJS.Platform;
|
||||
readonly #stopProcess: typeof stopOwnedDebuggerProcess;
|
||||
@@ -610,9 +610,32 @@ export class GdbSessionManager {
|
||||
readonly #readInferiorProcessStartTime: ProcessStartTimeReader | undefined;
|
||||
readonly #maxSessions: DebuggerAdmissionLimit;
|
||||
readonly #maxPendingCommands: DebuggerAdmissionLimit;
|
||||
readonly #commandOutput: DebuggerAdmissionLimit;
|
||||
readonly #maxCommandOutputBytes: number;
|
||||
readonly #launchCommandOutput: DebuggerAdmissionLimit;
|
||||
readonly #controlReplyBytes: number;
|
||||
get #commandDeadline(): DebuggerAdmissionLimit {
|
||||
const override =
|
||||
currentAnalysisOperationSnapshot()?.overrides[
|
||||
"debugger.command_deadline_ms"
|
||||
];
|
||||
return override === undefined
|
||||
? this.#launchCommandDeadline
|
||||
: { ...this.#launchCommandDeadline, value: override, source: "runtime" };
|
||||
}
|
||||
get #commandTimeoutMs(): number {
|
||||
return this.#commandDeadline.value;
|
||||
}
|
||||
get #commandOutput(): DebuggerAdmissionLimit {
|
||||
const override =
|
||||
currentAnalysisOperationSnapshot()?.overrides[
|
||||
"debugger.max_command_output_bytes"
|
||||
];
|
||||
return override === undefined
|
||||
? this.#launchCommandOutput
|
||||
: { ...this.#launchCommandOutput, value: override, source: "runtime" };
|
||||
}
|
||||
get #maxCommandOutputBytes(): number {
|
||||
return this.#commandOutput.value;
|
||||
}
|
||||
#sessionReservations = 0;
|
||||
#closing = false;
|
||||
#closePromise: Promise<void> | undefined;
|
||||
@@ -642,8 +665,7 @@ export class GdbSessionManager {
|
||||
this.#environment = { ...(options.environment ?? process.env) };
|
||||
this.#command = this.#environment.REA_GDB_COMMAND ?? "gdb";
|
||||
const limits = resolveDebuggerSessionLimits(this.#environment);
|
||||
this.#commandDeadline = limits.commandDeadline;
|
||||
this.#commandTimeoutMs = this.#commandDeadline.value;
|
||||
this.#launchCommandDeadline = limits.commandDeadline;
|
||||
this.#startupTimeoutMs = options.startupTimeoutMs ?? STARTUP_TIMEOUT_MS;
|
||||
this.#platform = options.platform ?? process.platform;
|
||||
this.#stopProcess = options.stopProcess ?? stopOwnedDebuggerProcess;
|
||||
@@ -653,8 +675,7 @@ export class GdbSessionManager {
|
||||
this.#readInferiorProcessStartTime = options.readInferiorProcessStartTime;
|
||||
this.#maxSessions = limits.maxSessions;
|
||||
this.#maxPendingCommands = limits.maxPendingCommands;
|
||||
this.#commandOutput = limits.maxCommandOutputBytes;
|
||||
this.#maxCommandOutputBytes = this.#commandOutput.value;
|
||||
this.#launchCommandOutput = limits.maxCommandOutputBytes;
|
||||
this.#controlReplyBytes =
|
||||
options.controlReplyBytes ?? DEBUGGER_CONTROL_REPLY_BYTES;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
mcpTextBlocks,
|
||||
parseMcpJsonOrText,
|
||||
} from "../providers/McpToolResult.js";
|
||||
import { currentAnalysisOperationSnapshot } from "../domain/analysisOperationSnapshot.js";
|
||||
import { jsonObjectSchema, type JsonValue } from "../domain/jsonValue.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
@@ -99,10 +100,14 @@ export const createStreamableHttpJebMcpConnection: JebMcpConnectionFactory = (
|
||||
endpoint,
|
||||
timeouts,
|
||||
) => {
|
||||
const discoveryTimeoutMs =
|
||||
timeouts?.discoveryTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MSEC;
|
||||
const requestTimeoutMs =
|
||||
timeouts?.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MSEC;
|
||||
const discoveryTimeoutMs = () =>
|
||||
currentAnalysisOperationSnapshot()?.overrides["jeb.mcp_timeout_ms"] ??
|
||||
timeouts?.discoveryTimeoutMs ??
|
||||
DEFAULT_REQUEST_TIMEOUT_MSEC;
|
||||
const requestTimeoutMs = () =>
|
||||
currentAnalysisOperationSnapshot()?.overrides["jeb.mcp_timeout_ms"] ??
|
||||
timeouts?.requestTimeoutMs ??
|
||||
DEFAULT_REQUEST_TIMEOUT_MSEC;
|
||||
let state: JebMcpConnectionOwner | undefined;
|
||||
let sessionGeneration = 0;
|
||||
let closing: Promise<void> | undefined;
|
||||
@@ -211,7 +216,7 @@ export const createStreamableHttpJebMcpConnection: JebMcpConnectionFactory = (
|
||||
Result<readonly string[], unknown>
|
||||
>(async (startupSignal) => {
|
||||
const deadline = new JebMcpDeadline(
|
||||
discoveryTimeoutMs,
|
||||
discoveryTimeoutMs(),
|
||||
startupSignal,
|
||||
"discovery",
|
||||
);
|
||||
@@ -277,7 +282,7 @@ export const createStreamableHttpJebMcpConnection: JebMcpConnectionFactory = (
|
||||
? owner.signal.signal
|
||||
: AbortSignal.any([signal, owner.signal.signal]);
|
||||
const deadline = new JebMcpDeadline(
|
||||
discoveryTimeoutMs,
|
||||
discoveryTimeoutMs(),
|
||||
discoverySignal,
|
||||
"discovery",
|
||||
);
|
||||
@@ -307,7 +312,7 @@ export const createStreamableHttpJebMcpConnection: JebMcpConnectionFactory = (
|
||||
signal === undefined
|
||||
? owner.signal.signal
|
||||
: AbortSignal.any([signal, owner.signal.signal]);
|
||||
const deadline = new JebMcpDeadline(requestTimeoutMs, requestSignal);
|
||||
const deadline = new JebMcpDeadline(requestTimeoutMs(), requestSignal);
|
||||
let result: unknown;
|
||||
try {
|
||||
result = await owner.client.callTool(
|
||||
|
||||
@@ -7,20 +7,35 @@ import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisTimeoutError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { MAX_NODE_TIMER_DELAY_MS } from "../domain/analysisResourceLimits.js";
|
||||
import { analysisSettingLimit } from "../domain/analysisSettingDefinitions.js";
|
||||
import { currentAnalysisOperationSnapshot } from "../domain/analysisOperationSnapshot.js";
|
||||
import {
|
||||
DEFAULT_JEB_MCP_TIMEOUT_MS,
|
||||
MAX_NODE_TIMER_DELAY_MS,
|
||||
} from "../domain/analysisResourceLimits.js";
|
||||
import { ProviderStartupDeadline } from "../process/ProviderDeadline.js";
|
||||
|
||||
/** Which JEB MCP phase consumed `REA_JEB_MCP_TIMEOUT_MS`. */
|
||||
export type JebMcpDeadlinePhase = "discovery" | "tools/call";
|
||||
|
||||
/** Name the launch-environment deadline that both JEB phases share. */
|
||||
/** Name the deadline that both JEB phases share. */
|
||||
export const jebMcpTimeoutGuidance = (
|
||||
phase: JebMcpDeadlinePhase,
|
||||
timeoutMs: number,
|
||||
): { readonly userMessage: string; readonly remediationAction: string } => ({
|
||||
userMessage: `This request reached REA's configured JEB ${phase === "discovery" ? "catalog discovery" : phase} deadline of ${String(timeoutMs)} ms (deadlineExpired).`,
|
||||
remediationAction: `Increase REA_JEB_MCP_TIMEOUT_MS in REA's launch environment to an integer from 1 to ${String(MAX_NODE_TIMER_DELAY_MS)} milliseconds (omitted means ${String(DEFAULT_REQUEST_TIMEOUT_MSEC)} ms) and restart and reconnect REA to apply it. The same setting raises catalog discovery and each tools/call. Allow execution, cleanup and delivery headroom in the outer MCP client deadline. Confirm outstanding work and cleanup before retrying.`,
|
||||
});
|
||||
) => {
|
||||
const override =
|
||||
currentAnalysisOperationSnapshot()?.overrides["jeb.mcp_timeout_ms"];
|
||||
const limit = analysisSettingLimit(
|
||||
"jeb.mcp_timeout_ms",
|
||||
timeoutMs,
|
||||
override === undefined ? undefined : "runtime",
|
||||
);
|
||||
return {
|
||||
userMessage: `This request reached REA's configured JEB ${phase === "discovery" ? "catalog discovery" : phase} deadline of ${String(timeoutMs)} ms (deadlineExpired).`,
|
||||
remediationAction: `jeb.mcp_timeout_ms is ${String(timeoutMs)} ms (source ${limit.source ?? "unknown"}; default ${String(DEFAULT_JEB_MCP_TIMEOUT_MS)} ms, floor ${String(limit.minimum)} ms, maximum ${String(limit.maximum)} ms). It can be changed at runtime with update_analysis_settings. The launch seed is REA_JEB_MCP_TIMEOUT_MS. The same setting raises catalog discovery and each tools/call. Allow execution, cleanup and delivery headroom in the outer MCP client deadline. Confirm outstanding work and cleanup before retrying.`,
|
||||
limit,
|
||||
};
|
||||
};
|
||||
|
||||
/** SDK 2.3.1 preserves an SdkError abort reason by identity. */
|
||||
export class JebMcpDeadline {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
AnalysisError,
|
||||
type AnalysisCapturedOutput,
|
||||
} from "../domain/analysisErrorBase.js";
|
||||
import { analysisSettingLimit } from "../domain/analysisSettingDefinitions.js";
|
||||
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
@@ -413,10 +414,15 @@ const commandFailure = (
|
||||
});
|
||||
else if (cause.reason === "timeout") {
|
||||
const { setting, ms } = request.deadline;
|
||||
const analysisSetting =
|
||||
setting === "REA_KUNA_TIMEOUT_MS"
|
||||
? "kuna.function_timeout_ms"
|
||||
: "kuna.project_timeout_ms";
|
||||
primary = new AnalysisTimeoutError(operation, ms, {
|
||||
...details,
|
||||
userMessage: `kuna ${subcommand} did not finish within ${setting} (${String(ms)} ms).`,
|
||||
remediationAction: `Raise ${setting} in the environment that starts REA, then restart REA; it reads Kuna settings when it starts.`,
|
||||
limit: analysisSettingLimit(analysisSetting, ms, undefined),
|
||||
userMessage: `kuna ${subcommand} did not finish within ${analysisSetting} (${String(ms)} ms).`,
|
||||
remediationAction: `${analysisSetting} is ${String(ms)} ms. It can be changed at runtime with update_analysis_settings. The launch seed is ${setting}.`,
|
||||
});
|
||||
}
|
||||
// The same input overflows the same fixed buffer; waiting cannot help.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { isAbsolute } from "node:path";
|
||||
|
||||
import { currentAnalysisOperationSnapshot } from "../domain/analysisOperationSnapshot.js";
|
||||
import { defaultKunaCacheDirectory } from "../config/kunaCacheDirectory.js";
|
||||
import { homeDirectoryFromEnvironment } from "../config/homeDirectory.js";
|
||||
import { providerStartupTimeoutSchema } from "../config/providerStartupTimeout.js";
|
||||
@@ -59,7 +60,9 @@ export const kunaFunctionDeadline = (
|
||||
configuration: KunaConfiguration,
|
||||
): KunaDeadline => ({
|
||||
setting: "REA_KUNA_TIMEOUT_MS",
|
||||
ms: configuration.functionTimeoutMs,
|
||||
ms:
|
||||
currentAnalysisOperationSnapshot()?.overrides["kuna.function_timeout_ms"] ??
|
||||
configuration.functionTimeoutMs,
|
||||
});
|
||||
|
||||
/** The project export and whole-program string, graph and xref scans. */
|
||||
@@ -67,7 +70,9 @@ export const kunaProjectDeadline = (
|
||||
configuration: KunaConfiguration,
|
||||
): KunaDeadline => ({
|
||||
setting: "REA_KUNA_PROJECT_TIMEOUT_MS",
|
||||
ms: configuration.projectTimeoutMs,
|
||||
ms:
|
||||
currentAnalysisOperationSnapshot()?.overrides["kuna.project_timeout_ms"] ??
|
||||
configuration.projectTimeoutMs,
|
||||
});
|
||||
|
||||
/** Why the configured environment cannot select a Kuna installation. */
|
||||
|
||||
@@ -215,7 +215,7 @@ export const exportKunaProject = async (
|
||||
): Promise<KunaExportOutcome> => {
|
||||
const { installation, signal } = request;
|
||||
const { configuration } = installation;
|
||||
const deadline = Date.now() + configuration.projectTimeoutMs;
|
||||
const deadline = Date.now() + kunaProjectDeadline(configuration).ms;
|
||||
const input = join(request.workspace, "input", basename(request.targetPath));
|
||||
await mkdir(join(request.workspace, "input"), { mode: 0o700 });
|
||||
await mkdir(request.output, { recursive: true, mode: 0o700 });
|
||||
|
||||
@@ -39,6 +39,7 @@ import { combineFunctionRelationshipCoverage } from "../domain/functionCallCover
|
||||
import { functionDossierSchema } from "../domain/native/functionDossier.js";
|
||||
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import { analysisSettingLimit } from "../domain/analysisSettingDefinitions.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import type { RelationshipCoverage } from "../domain/relationshipCoverage.js";
|
||||
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
@@ -278,8 +279,9 @@ const exportUnavailableMessage = (
|
||||
/**
|
||||
* How a request that needs the export reports the export's failure. A session
|
||||
* never rebuilds a failed export, so repeating the request cannot succeed; the
|
||||
* error says what to change before reopening the target. Kuna settings are read
|
||||
* when REA starts, so changing one also needs a restart. Cancellation and
|
||||
* error says what to change before reopening the target. A failed export is
|
||||
* not rebuilt in the same session. The project deadline can change at runtime
|
||||
* for the next export. Cancellation and
|
||||
* unproven cleanup keep their own errors, which callers must still observe.
|
||||
*/
|
||||
const exportUnavailable = (
|
||||
@@ -292,9 +294,17 @@ const exportUnavailable = (
|
||||
const exported = projectAnalysisError(failure);
|
||||
const restart =
|
||||
"REA reads Kuna settings when it starts, so restart REA, then reopen the target.";
|
||||
if (failure instanceof AnalysisTimeoutError)
|
||||
failure.bindLimit(
|
||||
analysisSettingLimit(
|
||||
"kuna.project_timeout_ms",
|
||||
failure.timeoutMs,
|
||||
undefined,
|
||||
),
|
||||
);
|
||||
const next =
|
||||
failure instanceof AnalysisTimeoutError
|
||||
? `To retry, set REA_KUNA_PROJECT_TIMEOUT_MS above ${String(failure.timeoutMs)} ms in the environment that starts REA. ${restart}`
|
||||
? `To retry, set kuna.project_timeout_ms above ${String(failure.timeoutMs)} ms with update_analysis_settings. It can be changed at runtime. The launch seed is REA_KUNA_PROJECT_TIMEOUT_MS. Then reopen the target so the export runs again.`
|
||||
: failure instanceof AnalysisResourceConstraintError &&
|
||||
failure.remediationAction !== undefined
|
||||
? // A fixed buffer (`memory`) is no setting, so a restart cannot help.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { analysisSettingLimit } from "../../domain/analysisSettingDefinitions.js";
|
||||
import {
|
||||
PWNTOOLS_LIMITS,
|
||||
resolvePwntoolsLimits,
|
||||
@@ -296,7 +297,12 @@ const classifyPwntoolsDecoderFailure = (
|
||||
...(cause.reason === "limit"
|
||||
? {
|
||||
userMessage: `${cause.message} Selected producer_limits.input_bytes is ${String(limits.input_bytes)} bytes. ${budgetRecovery}`,
|
||||
remediationAction: `Increase producer_limits.input_bytes above ${String(limits.input_bytes)} on the next call (CLI --producer-limits JSON). No restart is required. Changing REA_PWNTOOLS_MAX_INPUT_BYTES affects the launch default instead. CPU, memory, output and transport allowances remain independent.`,
|
||||
remediationAction: `pwntools.max_input_bytes is the launch default of ${String(PWNTOOLS_LIMITS.input_bytes)} bytes and can be changed at runtime with update_analysis_settings. The launch seed is REA_PWNTOOLS_MAX_INPUT_BYTES. producer_limits.input_bytes overrides that default on the next call (CLI --producer-limits JSON). The effective input_bytes for this call is ${String(limits.input_bytes)}. CPU, memory, output and transport allowances remain independent.`,
|
||||
limit: analysisSettingLimit(
|
||||
"pwntools.max_input_bytes",
|
||||
limits.input_bytes,
|
||||
undefined,
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
|
||||
@@ -14,6 +14,7 @@ describe("pwntools input bound", () => {
|
||||
expect(pwntoolsInputBytes({})).toEqual({
|
||||
configured: true,
|
||||
bytes: PWNTOOLS_LIMITS.input_bytes,
|
||||
source: "default",
|
||||
});
|
||||
expect(PWNTOOLS_LIMITS.input_bytes).toBe(32 * 1024 * 1024);
|
||||
});
|
||||
@@ -25,7 +26,7 @@ describe("pwntools input bound", () => {
|
||||
["1073741824", 1024 * 1024 * 1024],
|
||||
] as const)("accepts %s as %s bytes", (value, expected) => {
|
||||
expect(pwntoolsInputBytes({ REA_PWNTOOLS_MAX_INPUT_BYTES: value })).toEqual(
|
||||
{ configured: true, bytes: expected },
|
||||
{ configured: true, bytes: expected, source: "setting" },
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { currentAnalysisOperationSnapshot } from "../../domain/analysisOperationSnapshot.js";
|
||||
import { PWNTOOLS_LIMITS } from "../../domain/native/pwntoolsResourceLimits.js";
|
||||
|
||||
// The artifact is read into one in-memory buffer and staged again as a private
|
||||
@@ -20,7 +21,11 @@ const CONSTRAINT =
|
||||
|
||||
/** Resolved input bound, or the reason the caller's override cannot be used. */
|
||||
export type PwntoolsInputBytes =
|
||||
| { readonly configured: true; readonly bytes: number }
|
||||
| {
|
||||
readonly configured: true;
|
||||
readonly bytes: number;
|
||||
readonly source: "default" | "setting" | "runtime";
|
||||
}
|
||||
| { readonly configured: false; readonly error: string };
|
||||
|
||||
/**
|
||||
@@ -30,11 +35,19 @@ export type PwntoolsInputBytes =
|
||||
export const pwntoolsInputBytes = (
|
||||
environment: Readonly<NodeJS.ProcessEnv>,
|
||||
): PwntoolsInputBytes => {
|
||||
const override =
|
||||
currentAnalysisOperationSnapshot()?.overrides["pwntools.max_input_bytes"];
|
||||
if (override !== undefined)
|
||||
return { configured: true, bytes: override, source: "runtime" };
|
||||
const value = environment.REA_PWNTOOLS_MAX_INPUT_BYTES;
|
||||
if (value === undefined)
|
||||
return { configured: true, bytes: PWNTOOLS_LIMITS.input_bytes };
|
||||
return {
|
||||
configured: true,
|
||||
bytes: PWNTOOLS_LIMITS.input_bytes,
|
||||
source: "default",
|
||||
};
|
||||
const parsed = pwntoolsInputBytesSchema.safeParse(value);
|
||||
return parsed.success
|
||||
? { configured: true, bytes: parsed.data }
|
||||
? { configured: true, bytes: parsed.data, source: "setting" }
|
||||
: { configured: false, error: CONSTRAINT };
|
||||
};
|
||||
@@ -1,6 +1,12 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
import type { DebuggerAdmissionLimit } from "../config/debuggerSessionLimits.js";
|
||||
import {
|
||||
DEBUGGER_COMMAND_DEADLINE_SETTING,
|
||||
DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING,
|
||||
type DebuggerAdmissionLimit,
|
||||
} from "../config/debuggerSessionLimits.js";
|
||||
import { analysisSettingLimit } from "../domain/analysisSettingDefinitions.js";
|
||||
import type { AnalysisSettingId } from "../domain/analysisSettings.js";
|
||||
import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import { parseLinuxProcessStat } from "./LinuxProcessStat.js";
|
||||
@@ -11,36 +17,64 @@ export type OwnedDebuggerStopResult =
|
||||
| { readonly status: "stopped" | "already_exited" }
|
||||
| { readonly status: "incomplete"; readonly reason: string };
|
||||
|
||||
const RUNTIME_DEBUGGER_SETTINGS = {
|
||||
[DEBUGGER_COMMAND_DEADLINE_SETTING]: "debugger.command_deadline_ms",
|
||||
[DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING]:
|
||||
"debugger.max_command_output_bytes",
|
||||
} as const satisfies Partial<Record<string, AnalysisSettingId>>;
|
||||
|
||||
const runtimeDebuggerSetting = (
|
||||
admission: DebuggerAdmissionLimit,
|
||||
): AnalysisSettingId | undefined =>
|
||||
RUNTIME_DEBUGGER_SETTINGS[
|
||||
admission.setting as keyof typeof RUNTIME_DEBUGGER_SETTINGS
|
||||
];
|
||||
|
||||
/** Structured limit for a debugger budget that update_analysis_settings can change. */
|
||||
export const debuggerAnalysisLimit = (admission: DebuggerAdmissionLimit) => {
|
||||
const setting = runtimeDebuggerSetting(admission);
|
||||
return setting === undefined
|
||||
? undefined
|
||||
: analysisSettingLimit(setting, admission.value, admission.source);
|
||||
};
|
||||
|
||||
/** Structured facts a limit error exposes. The sentence is display text. */
|
||||
export const debuggerAdmissionDiagnostics = (
|
||||
admission: DebuggerAdmissionLimit,
|
||||
): {
|
||||
readonly setting: string;
|
||||
readonly effective_value: number;
|
||||
readonly source: DebuggerAdmissionLimit["source"];
|
||||
readonly default_value: number;
|
||||
readonly minimum: number;
|
||||
readonly maximum: number;
|
||||
readonly unit: DebuggerAdmissionLimit["unit"];
|
||||
readonly update_mode: "restart";
|
||||
readonly pending_restart: false;
|
||||
} => ({
|
||||
setting: admission.setting,
|
||||
effective_value: admission.value,
|
||||
source: admission.source,
|
||||
default_value: admission.defaultValue,
|
||||
minimum: admission.minimum,
|
||||
maximum: admission.maximum,
|
||||
unit: admission.unit,
|
||||
update_mode: "restart",
|
||||
pending_restart: false,
|
||||
});
|
||||
) => {
|
||||
const analysisSetting = runtimeDebuggerSetting(admission);
|
||||
const update_mode = analysisSetting === undefined ? "restart" : "runtime";
|
||||
return {
|
||||
setting: admission.setting,
|
||||
effective_value: admission.value,
|
||||
source: admission.source,
|
||||
default_value: admission.defaultValue,
|
||||
minimum: admission.minimum,
|
||||
maximum: admission.maximum,
|
||||
unit: admission.unit,
|
||||
update_mode,
|
||||
pending_restart: false as const,
|
||||
...(analysisSetting === undefined
|
||||
? {}
|
||||
: { analysis_setting: analysisSetting }),
|
||||
};
|
||||
};
|
||||
|
||||
/** Name the setting, the effective value, its source, the range, and a restart. */
|
||||
/** Name the setting, the effective value, its source, and the range. */
|
||||
export const debuggerAdmissionRecovery = (
|
||||
admission: DebuggerAdmissionLimit,
|
||||
): string =>
|
||||
`The effective ${admission.setting} value is ${String(admission.value)} ${admission.unit} (source ${admission.source}; default ${String(admission.defaultValue)} ${admission.unit}, floor ${String(admission.minimum)} ${admission.unit}, maximum ${String(admission.maximum)} ${admission.unit}). Set ${admission.setting} in the environment that starts REA to a positive decimal integer from ${String(admission.minimum)} to ${String(admission.maximum)}, then restart REA.`;
|
||||
): string => {
|
||||
const facts = debuggerAdmissionDiagnostics(admission);
|
||||
const identity =
|
||||
facts.analysis_setting === undefined
|
||||
? admission.setting
|
||||
: `${facts.analysis_setting} (launch seed ${admission.setting})`;
|
||||
const change =
|
||||
facts.update_mode === "runtime"
|
||||
? `It can be changed at runtime with update_analysis_settings.`
|
||||
: `Set ${admission.setting} in the environment that starts REA to a positive decimal integer from ${String(admission.minimum)} to ${String(admission.maximum)}, then restart REA.`;
|
||||
return `The effective ${identity} value is ${String(admission.value)} ${admission.unit} (source ${admission.source}; default ${String(admission.defaultValue)} ${admission.unit}, floor ${String(admission.minimum)} ${admission.unit}, maximum ${String(admission.maximum)} ${admission.unit}). ${change}`;
|
||||
};
|
||||
|
||||
/** Fixed startup deadline. Distinct from the shared command deadline setting. */
|
||||
export const fixedDeadlineGuidance = (
|
||||
@@ -61,10 +95,12 @@ export const configurableDeadlineGuidance = (
|
||||
readonly diagnostics: ReturnType<typeof debuggerAdmissionDiagnostics>;
|
||||
} => {
|
||||
const recovery = debuggerAdmissionRecovery(admission);
|
||||
const limit = debuggerAnalysisLimit(admission);
|
||||
return {
|
||||
userMessage: `${what} reached its ${String(admission.value)} ms deadline (deadlineExpired). ${recovery}`,
|
||||
remediationAction: recovery,
|
||||
diagnostics: debuggerAdmissionDiagnostics(admission),
|
||||
...(limit === undefined ? {} : { limit }),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -79,6 +115,7 @@ export const debuggerResourceConstraintError = (
|
||||
): AnalysisResourceConstraintError => {
|
||||
const facts = debuggerAdmissionDiagnostics(admission);
|
||||
const recovery = debuggerAdmissionRecovery(admission);
|
||||
const limit = debuggerAnalysisLimit(admission);
|
||||
return new AnalysisResourceConstraintError(
|
||||
operation,
|
||||
resource,
|
||||
@@ -87,6 +124,7 @@ export const debuggerResourceConstraintError = (
|
||||
{
|
||||
remediationAction: `${remediation} ${recovery}`,
|
||||
diagnostics: facts,
|
||||
...(limit === undefined ? {} : { limit }),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -28,6 +28,7 @@ import type {
|
||||
} from "../domain/analysisErrorBase.js";
|
||||
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
|
||||
import { createEvidence, type Evidence } from "../domain/evidence.js";
|
||||
import { currentAnalysisOperationSnapshot } from "../domain/analysisOperationSnapshot.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import {
|
||||
ProviderProcessSupervisor,
|
||||
@@ -263,13 +264,11 @@ export class RizinDebugSessionManager {
|
||||
#closingAll = false;
|
||||
readonly #startDrainWaiters: Array<() => void> = [];
|
||||
readonly #command: string;
|
||||
readonly #commandDeadline: DebuggerAdmissionLimit;
|
||||
readonly #frameTimeoutMs: number;
|
||||
readonly #launchCommandDeadline: DebuggerAdmissionLimit;
|
||||
readonly #startupTimeoutMs: number;
|
||||
readonly #maxSessions: DebuggerAdmissionLimit;
|
||||
readonly #maxPendingCommands: DebuggerAdmissionLimit;
|
||||
readonly #commandOutput: DebuggerAdmissionLimit;
|
||||
readonly #maxCommandOutputBytes: number;
|
||||
readonly #launchCommandOutput: DebuggerAdmissionLimit;
|
||||
readonly #platform: NodeJS.Platform;
|
||||
readonly #stopProcess: typeof stopOwnedDebuggerProcess;
|
||||
readonly #spawnProcess: typeof spawnOwnedProviderProcess;
|
||||
@@ -287,18 +286,41 @@ export class RizinDebugSessionManager {
|
||||
this.#environment = { ...environment };
|
||||
this.#command = environment.REA_RIZIN_COMMAND ?? "rizin";
|
||||
const limits = resolveDebuggerSessionLimits(this.#environment);
|
||||
this.#commandDeadline = limits.commandDeadline;
|
||||
this.#frameTimeoutMs = this.#commandDeadline.value;
|
||||
this.#launchCommandDeadline = limits.commandDeadline;
|
||||
this.#startupTimeoutMs = options.startupTimeoutMs ?? STARTUP_TIMEOUT_MS;
|
||||
this.#maxSessions = limits.maxSessions;
|
||||
this.#maxPendingCommands = limits.maxPendingCommands;
|
||||
this.#commandOutput = limits.maxCommandOutputBytes;
|
||||
this.#maxCommandOutputBytes = this.#commandOutput.value;
|
||||
this.#launchCommandOutput = limits.maxCommandOutputBytes;
|
||||
this.#platform = options.platform ?? process.platform;
|
||||
this.#stopProcess = options.stopProcess ?? stopOwnedDebuggerProcess;
|
||||
this.#spawnProcess = options.spawnProcess ?? spawnOwnedProviderProcess;
|
||||
}
|
||||
|
||||
get #commandDeadline(): DebuggerAdmissionLimit {
|
||||
const override =
|
||||
currentAnalysisOperationSnapshot()?.overrides[
|
||||
"debugger.command_deadline_ms"
|
||||
];
|
||||
return override === undefined
|
||||
? this.#launchCommandDeadline
|
||||
: { ...this.#launchCommandDeadline, value: override, source: "runtime" };
|
||||
}
|
||||
get #frameTimeoutMs(): number {
|
||||
return this.#commandDeadline.value;
|
||||
}
|
||||
get #commandOutput(): DebuggerAdmissionLimit {
|
||||
const override =
|
||||
currentAnalysisOperationSnapshot()?.overrides[
|
||||
"debugger.max_command_output_bytes"
|
||||
];
|
||||
return override === undefined
|
||||
? this.#launchCommandOutput
|
||||
: { ...this.#launchCommandOutput, value: override, source: "runtime" };
|
||||
}
|
||||
get #maxCommandOutputBytes(): number {
|
||||
return this.#commandOutput.value;
|
||||
}
|
||||
|
||||
async start(
|
||||
input: {
|
||||
readonly path: string;
|
||||
|
||||
Reference in new issue
Block a user