From 57dd7f26b15df2bf495f59f9f90bc4f93301df3a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 11 Nov 2026 10:08:05 +0000 Subject: [PATCH] fix(limits): carry runtime ceilings into Hopper and pwntools recovery A runtime bridge ceiling is injected into the Hopper bootstrap. Pwntools input-limit guidance names the layer that selected the ceiling. --- src/hopper/BridgeLauncher.ts | 2 +- src/hopper/bridgeBootstrapLimit.test.ts | 13 +++++++ src/native/pwntools/PwntoolsFailures.test.ts | 39 ++++++++++++++++++++ src/native/pwntools/PwntoolsFailures.ts | 37 ++++++++++++++++++- 4 files changed, 88 insertions(+), 3 deletions(-) diff --git a/src/hopper/BridgeLauncher.ts b/src/hopper/BridgeLauncher.ts index a8631ab21d..ce9dd6a050 100644 --- a/src/hopper/BridgeLauncher.ts +++ b/src/hopper/BridgeLauncher.ts @@ -671,7 +671,7 @@ export const bridgeBootstrapSource = ( ): string => { const limit = options.bridgeMaxRequestBytes; const injectedLimit = - limit?.source === "setting" && + (limit?.source === "setting" || limit?.source === "runtime") && Number.isSafeInteger(limit.maximumBytes) && limit.maximumBytes >= MINIMUM_BRIDGE_MAX_REQUEST_BYTES && limit.maximumBytes <= MAXIMUM_BRIDGE_MAX_REQUEST_BYTES diff --git a/src/hopper/bridgeBootstrapLimit.test.ts b/src/hopper/bridgeBootstrapLimit.test.ts index 63f6ec44b2..996fb64012 100644 --- a/src/hopper/bridgeBootstrapLimit.test.ts +++ b/src/hopper/bridgeBootstrapLimit.test.ts @@ -39,3 +39,16 @@ it("injects a caller-selected ceiling before the bridge is executed", () => { expect(injected).toBeGreaterThanOrEqual(0); expect(injected).toBeLessThan(source.indexOf("exec(compile")); }); + +it("injects a runtime ceiling before the bridge is executed", () => { + const source = bridgeBootstrapSource( + session, + { + ...options, + bridgeMaxRequestBytes: { maximumBytes: 65_536, source: "runtime" }, + }, + true, + "/target", + ); + expect(source).toContain("REA_BRIDGE_MAX_REQUEST_BYTES = 65536"); +}); diff --git a/src/native/pwntools/PwntoolsFailures.test.ts b/src/native/pwntools/PwntoolsFailures.test.ts index 1d9e91352c..cb11a88aab 100644 --- a/src/native/pwntools/PwntoolsFailures.test.ts +++ b/src/native/pwntools/PwntoolsFailures.test.ts @@ -56,7 +56,46 @@ it("keeps the caller-selected pwntools input-limit source", () => { source: "request", update_mode: "parameter", pending_restart: false, + parameter: "producer_limits.input_bytes", }); + expect(projected.remediation.action).toContain( + "producer_limits.input_bytes is 4096 bytes", + ); + expect(projected.remediation.action).not.toContain("launch default of"); +}); + +it("names a runtime pwntools ceiling instead of the built-in default", () => { + const projected = projectAnalysisError( + pwntoolsDecoderFailure( + new ArtifactReaderFailure( + "limit", + "artifact exceeds the input allowance", + {}, + ), + "artifact-read", + "/selected", + "/python", + {}, + { + operation: "inspect_binary_layout", + providerId: "pwntools", + producerLimits: resolvePwntoolsLimits(undefined, 67_108_864), + inputBytesSource: "runtime", + }, + ), + ); + expect(projected.details?.limit).toMatchObject({ + effective_value: 67_108_864, + source: "runtime", + update_mode: "runtime", + }); + expect(projected.remediation.action).toContain( + "67108864 bytes (source runtime)", + ); + expect(projected.remediation.action).toContain("update_analysis_settings"); + expect(projected.message).not.toContain( + "Selected producer_limits.input_bytes is", + ); }); it("distinguishes the wall deadline from caller-selected producer resource budgets", () => { diff --git a/src/native/pwntools/PwntoolsFailures.ts b/src/native/pwntools/PwntoolsFailures.ts index 2dd843a5a4..cdcaeb973e 100644 --- a/src/native/pwntools/PwntoolsFailures.ts +++ b/src/native/pwntools/PwntoolsFailures.ts @@ -53,6 +53,36 @@ export interface PwntoolsFailureContext { | undefined; } +const pwntoolsInputLimitFact = ( + bytes: number, + source: PwntoolsFailureContext["inputBytesSource"], +): string => { + const effective = String(bytes); + if (source === "request") + return `Selected producer_limits.input_bytes is ${effective} bytes.`; + if (source === "runtime") + return `The runtime pwntools.max_input_bytes ceiling is ${effective} bytes.`; + if (source === "setting") + return `REA_PWNTOOLS_MAX_INPUT_BYTES selected ${effective} bytes.`; + return `The launch default pwntools.max_input_bytes ceiling is ${effective} bytes.`; +}; + +const pwntoolsInputLimitRecovery = ( + bytes: number, + source: PwntoolsFailureContext["inputBytesSource"], +): string => { + const effective = String(bytes); + const shared = + "CPU, memory, output and transport allowances remain independent."; + if (source === "request") + return `producer_limits.input_bytes is ${effective} bytes for this call. Raise it on the next call (CLI --producer-limits JSON). ${shared}`; + if (source === "runtime") + return `pwntools.max_input_bytes is ${effective} bytes (source runtime) and can be changed with update_analysis_settings. The launch seed is REA_PWNTOOLS_MAX_INPUT_BYTES. producer_limits.input_bytes overrides that ceiling on the next call. ${shared}`; + if (source === "setting") + return `REA_PWNTOOLS_MAX_INPUT_BYTES is ${effective} bytes (source setting). Change pwntools.max_input_bytes with update_analysis_settings, or set producer_limits.input_bytes on the next call. The built-in default is ${String(PWNTOOLS_LIMITS.input_bytes)} bytes. ${shared}`; + return `pwntools.max_input_bytes is the launch default of ${effective} bytes and can be changed with update_analysis_settings. The launch seed is REA_PWNTOOLS_MAX_INPUT_BYTES. producer_limits.input_bytes overrides that default on the next call. ${shared}`; +}; + /** Recovery identifies the selected controls rather than unrelated wall deadlines. */ export const pwntoolsBudgetRecovery = ( limits: PwntoolsProducerLimits = resolvePwntoolsLimits(), @@ -302,8 +332,11 @@ const classifyPwntoolsDecoderFailure = ( ...stableArtifactInputRecovery(cause), ...(cause.reason === "limit" ? { - userMessage: `${cause.message} Selected producer_limits.input_bytes is ${String(limits.input_bytes)} bytes. ${budgetRecovery}`, - 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.`, + userMessage: `${cause.message} ${pwntoolsInputLimitFact(limits.input_bytes, context.inputBytesSource)} ${budgetRecovery}`, + remediationAction: pwntoolsInputLimitRecovery( + limits.input_bytes, + context.inputBytesSource, + ), limit: analysisSettingLimit( "pwntools.max_input_bytes", limits.input_bytes,