From 2e86294c860710af933ba7c996d8f0f35e6dc48d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E4=BD=B3=E6=B5=A9Alben?= <35291372+XiaoBinGan@users.noreply.github.com> Date: Sat, 24 Oct 2026 20:35:30 +0800 Subject: [PATCH] fix(firmware): stop recommending a raise at the staging budget ceiling (#6971) The staging refusal told callers to increase max_output_files or max_output_bytes within its schema maximum. At the schema ceiling that recovery does not exist, so the refusal was unactionable exactly where the least room remains. firmwareStagingBudgetRemediation now branches on the selected value against the schema maximum: below the ceiling it names the room and the control, at the ceiling it states that a further raise is unsupported and points at a smaller firmware byte range. The schema maxima move into named constants so the advice and the accepted range cannot drift apart. Verified with a module test for both budgets at the ceiling and one below it (red on the previous single-form remediation), plus the existing firmware suites. --- src/domain/firmware/firmwareAnalysis.ts | 8 ++- src/firmware/FirmwareFiles.ts | 27 +++++++++- src/firmware/FirmwareStagingBudget.test.ts | 58 ++++++++++++++++++++++ 3 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 src/firmware/FirmwareStagingBudget.test.ts diff --git a/src/domain/firmware/firmwareAnalysis.ts b/src/domain/firmware/firmwareAnalysis.ts index 086c297118..91f01f7243 100644 --- a/src/domain/firmware/firmwareAnalysis.ts +++ b/src/domain/firmware/firmwareAnalysis.ts @@ -9,6 +9,10 @@ const range = z.strictObject({ offset: bytes, length: bytes.positive() }); export const DEFAULT_FIRMWARE_ANALYSIS_TIMEOUT_MS = 120_000; /** Largest delay supported without overflowing Node timers. */ export const MAX_FIRMWARE_TIMEOUT_MS = 2_147_483_647; +/** Largest staging file-byte budget the schema accepts. */ +export const MAX_FIRMWARE_STAGING_BYTES = 512 * 1024 * 1024; +/** Largest staging entry budget the schema accepts. */ +export const MAX_FIRMWARE_STAGING_ENTRIES = 100_000; const timeout = z.number().int().min(1).max(MAX_FIRMWARE_TIMEOUT_MS); const target = { path: absoluteLocalPathStringSchema.describe( @@ -43,7 +47,7 @@ export const firmwareInputSchemas = { .number() .int() .min(1) - .max(512 * 1024 * 1024) + .max(MAX_FIRMWARE_STAGING_BYTES) .default(256 * 1024 * 1024) .describe( "Staging file-byte budget, checked during extraction; transient overshoot is possible", @@ -52,7 +56,7 @@ export const firmwareInputSchemas = { .number() .int() .min(1) - .max(100_000) + .max(MAX_FIRMWARE_STAGING_ENTRIES) .default(10_000) .describe("Staging entry budget, including directories and links"), publication_timeout_ms: timeout diff --git a/src/firmware/FirmwareFiles.ts b/src/firmware/FirmwareFiles.ts index 9bb5d915e8..27128e78d9 100644 --- a/src/firmware/FirmwareFiles.ts +++ b/src/firmware/FirmwareFiles.ts @@ -12,6 +12,10 @@ import { AnalysisOutputError, } from "../domain/analysisErrorCore.js"; import type { FirmwareRequest } from "../domain/firmware/firmwareAnalysis.js"; +import { + MAX_FIRMWARE_STAGING_BYTES, + MAX_FIRMWARE_STAGING_ENTRIES, +} from "../domain/firmware/firmwareAnalysis.js"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; import { readLinuxResourceLimits } from "../process/LinuxResourceLimits.js"; import { readBoundedFileBytes } from "../process/BoundedFileBytes.js"; @@ -442,6 +446,27 @@ export const inventoryFirmwareOutput = async ( ); }; +/** + * Recovery for one staging budget refusal. At the schema ceiling a larger + * selection is impossible, so that case must not recommend raising the value. + */ +export const firmwareStagingBudgetRemediation = ( + budget: "max_output_files" | "max_output_bytes", + selectedLimit: number, +): string => { + const maximum = + budget === "max_output_files" + ? MAX_FIRMWARE_STAGING_ENTRIES + : MAX_FIRMWARE_STAGING_BYTES; + const description = + budget === "max_output_files" + ? "staging entry budget, which counts extracted directories and links" + : "staging file-byte budget, checked during extraction"; + if (selectedLimit < maximum) + return `The ${description} was exceeded (${budget}=${String(selectedLimit)}; supported maximum ${String(maximum)}). Increase ${budget} up to ${String(maximum)} if the larger extraction is required, or select a materially narrower firmware byte range. analysis_timeout_ms does not raise this staging allowance.`; + return `The ${description} reached its supported maximum (${budget}=${String(selectedLimit)}; maximum ${String(maximum)}). Raising ${budget} further is not supported. Select a smaller firmware byte range or analyze a smaller image. analysis_timeout_ms does not raise this staging allowance.`; +}; + /** The same selected aggregate budget is enforced during and after extraction. */ const firmwareStagingLimit = ( operation: string, @@ -455,7 +480,7 @@ const firmwareStagingLimit = ( { [field]: maximum }, { retryable: false, - remediationAction: `Increase ${field} within its schema maximum if the larger extraction is required, or select a materially narrower range. analysis_timeout_ms does not raise this staging allowance. Repeating the unchanged request fails the same way.`, + remediationAction: firmwareStagingBudgetRemediation(field, maximum), }, ); diff --git a/src/firmware/FirmwareStagingBudget.test.ts b/src/firmware/FirmwareStagingBudget.test.ts new file mode 100644 index 0000000000..8a61dccfc0 --- /dev/null +++ b/src/firmware/FirmwareStagingBudget.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from "vitest"; + +import { + MAX_FIRMWARE_STAGING_BYTES, + MAX_FIRMWARE_STAGING_ENTRIES, +} from "../domain/firmware/firmwareAnalysis.js"; +import { firmwareStagingBudgetRemediation } from "./FirmwareFiles.js"; + +describe("firmware staging budget recovery", () => { + it("offers room to raise a budget that is below its supported maximum", () => { + const remediation = firmwareStagingBudgetRemediation("max_output_files", 1); + + expect(remediation).toContain( + `supported maximum ${String(MAX_FIRMWARE_STAGING_ENTRIES)}`, + ); + expect(remediation).toContain("Increase max_output_files up to"); + expect(remediation).not.toContain("not supported"); + }); + + it("does not recommend raising a budget already at its supported maximum", () => { + const entries = firmwareStagingBudgetRemediation( + "max_output_files", + MAX_FIRMWARE_STAGING_ENTRIES, + ); + const bytes = firmwareStagingBudgetRemediation( + "max_output_bytes", + MAX_FIRMWARE_STAGING_BYTES, + ); + for (const remediation of [entries, bytes]) { + // Raising a value the schema already caps is not an available recovery. + expect(remediation).toContain("not supported"); + expect(remediation).not.toContain("Increase"); + expect(remediation).toContain("smaller firmware byte range"); + } + }); + + it("names the exhausted budget, the selected value and the ceiling", () => { + expect(firmwareStagingBudgetRemediation("max_output_bytes", 4)).toContain( + "max_output_bytes=4", + ); + expect( + firmwareStagingBudgetRemediation( + "max_output_bytes", + MAX_FIRMWARE_STAGING_BYTES, + ), + ).toContain(`maximum ${String(MAX_FIRMWARE_STAGING_BYTES)}`); + }); + + it("keeps the ceiling advice off the schema limit for both budgets", () => { + for (const [budget, maximum] of [ + ["max_output_files", MAX_FIRMWARE_STAGING_ENTRIES], + ["max_output_bytes", MAX_FIRMWARE_STAGING_BYTES], + ] as const) + expect(firmwareStagingBudgetRemediation(budget, maximum - 1)).toContain( + `Increase ${budget} up to`, + ); + }); +});