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.
This commit is contained in:
Cursor Agent authored and morluto committed 2026-11-11 20:16:24 +08:00
1 parent b9be864801
commit 57dd7f26b1
4 files changed
+88 -3

No files matched your search

+1 -1
View File
@@ -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
+13
View File
@@ -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");
});
@@ -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", () => {
+35 -2
View File
@@ -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,