Compare commits

...
Author SHA1 Message Date
Kaoru0822andClaude Opus 5.5 beaf4040bc Merge origin/main into fix/fixed-budget-refusal-advice
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 15:44:09 +00:00
Kaoru0822andClaude Opus 5.5 7d97187ec4 fix(diagnostics): name fixed inspection budgets in refusal recovery
Fixed Go, Rust, PE resource, managed snapshot, Flutter ABI inventory and
Apple dispatch snapshot budgets reported resource "memory" without a
producer-specific recovery action, so projection fell back to advice to
raise worker or host memory, which cannot admit the unchanged input.
Each producer now marks the refusal non-retryable and names the fixed
reported limit. The PE metadata refusal also reports its exceeded 8 MiB
budget as max_metadata_bytes.

Fixes #5556

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 15:19:30 +00:00
14 changed files with 191 additions and 17 deletions

No files matched your search

+2 -1
View File
@@ -210,7 +210,8 @@ and language. Other resource payload formats remain opaque.
`max_file_bytes` defaults to 64 MiB (maximum 512 MiB); `max_entries` defaults to
16384, the largest stock Windows resource image measured with headroom
(maximum 65536). CLI options are `--max-file-bytes` and `--max-entries`.
Metadata is bounded to 8 MiB; exceeding any budget fails without truncation.
Metadata is bounded to a fixed 8 MiB, reported as `max_metadata_bytes`;
exceeding any budget fails without truncation.
Large Evidence envelopes may require a caller-configured MCP transport receive
buffer because envelope copies add overhead. Requests honor cancellation during
snapshot reading, directory traversal and payload hashing. The operation reads
+7
View File
@@ -42,6 +42,13 @@ const managedSnapshotCapacityError = (
"memory",
"Managed static inspection requires an eagerly retained PE snapshot within its input byte budget.",
{ max_snapshot_bytes: MAX_MANAGED_SNAPSHOT_BYTES },
{
// The snapshot budget is fixed, so neither waiting nor more worker or
// host memory admits the same input.
retryable: false,
remediationAction:
"max_snapshot_bytes is a fixed managed-inspection snapshot budget; worker limits and available host memory do not raise it. Select a complete managed assembly within the budget; retrying the unchanged artifact will fail again.",
},
);
export const readManagedSnapshot = async (
+7
View File
@@ -296,6 +296,13 @@ const collectFlutterLibraries = async (
max_flutter_abis: MAX_FLUTTER_ABIS,
max_library_path_bytes: MAX_FLUTTER_LIBRARY_PATH_BYTES,
},
{
// Both inventory bounds are fixed; neither waiting nor more worker
// or host memory admits the same package.
retryable: false,
remediationAction:
"max_flutter_abis and max_library_path_bytes are fixed Flutter library inventory budgets; worker limits and available host memory do not raise them. Select a complete APK within those budgets, such as a per-ABI split APK; retrying the unchanged package will fail again.",
},
);
libraries.push({
entry,
+8 -1
View File
@@ -62,7 +62,14 @@ const failureForPrimary = (cause: unknown, path: string): AnalysisError => {
cause.boundary === "search" ? "cpu" : "memory",
`Selected artifact ${path}: ${cause.message}`,
{ boundary: cause.boundary, maximum_bytes: cause.maximum_bytes },
{ cause },
{
cause,
// Every boundary is a fixed parser budget with no caller override, so
// neither waiting nor more worker or host memory admits the same file.
retryable: false,
remediationAction:
"The reported boundary's maximum_bytes is a fixed Go build-info parser budget; worker limits and available host memory do not raise it. Select another complete Go binary whose metadata fits the budget; retrying the unchanged artifact will fail again.",
},
);
if (cause instanceof GoBinaryFormatFailure)
return cause.kind === "unsupported"
+41 -1
View File
@@ -1,7 +1,16 @@
import { describe, expect, it } from "vitest";
import { truncate, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it, onTestFinished } from "vitest";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import {
NativeFixtureRunner,
nativeMachoTarget,
} from "../../tests/fixtures/nativeCommands.js";
import { machoWithPropertyAttributes } from "../../tests/fixtures/objcPropertyMacho.js";
import { objcRecordBudgetMacho } from "../../tests/fixtures/objcRecordBudgetMacho.js";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
import { decodeAppleDispatchMetadata } from "./AppleDispatchMetadata.js";
import { NativeMacOSProvider } from "./NativeMacOSProvider.js";
const base = 0x100000000n;
const fixture = (
@@ -1375,3 +1384,34 @@ describe("malformed Objective-C protocol method lists", () => {
});
});
});
describe("Apple dispatch snapshot budget", () => {
it("refuses a target above the fixed snapshot budget without worker memory advice", async () => {
const root = await createTestTempDirectory("rea-dispatch-snapshot-");
const path = join(root, "large.macho");
await writeFile(path, Buffer.alloc(0));
await truncate(path, 64 * 1024 * 1024 + 1);
const client = new NativeMacOSProvider(
{},
new NativeFixtureRunner({}),
"darwin",
).createClient(nativeMachoTarget(path));
onTestFinished(async () => {
await client.close();
});
const result = await client.execute("inspect_native_dispatch_metadata", {});
if (result.ok) throw new Error("Expected a snapshot budget refusal");
const projected = projectAnalysisError(result.error);
expect(projected).toMatchObject({
code: "resource_constraint",
retryable: false,
details: {
resource: "memory",
reported_limits: { input_bytes: 64 * 1024 * 1024 },
},
});
// The snapshot budget is fixed; worker or host memory cannot admit it.
expect(projected.remediation.action).toContain("input_bytes");
expect(projected.remediation.action).not.toMatch(/sufficient memory/iu);
});
});
+7
View File
@@ -217,6 +217,13 @@ export const inspectAppleDispatchMetadata = async (
"memory",
"Apple static metadata input exceeds its 64 MiB snapshot budget",
{ input_bytes: maximumBytes },
{
// The snapshot budget is fixed, so neither waiting nor more worker or
// host memory admits the same target.
retryable: false,
remediationAction:
"input_bytes is the fixed 64 MiB static dispatch-metadata snapshot budget; worker limits and available host memory do not raise it. Select a complete Mach-O within the budget; retrying the unchanged target will fail again.",
},
);
const cancelled = () =>
new AnalysisCancelledError("inspect_native_dispatch_metadata", {
+9 -4
View File
@@ -19,7 +19,8 @@ type Range = { offset: number; bytes: number };
type OccupiedRange = Range & {
kind: "directory" | "name" | "data-entry" | "payload";
};
const METADATA_BYTES = 8 * 1024 * 1024;
/** Fixed complete-result metadata budget; callers cannot raise it. */
export const PE_RESOURCE_METADATA_BYTES = 8 * 1024 * 1024;
const limit = (message: string, boundary: ArtifactReaderBoundary): never => {
throw new ArtifactReaderFailure("limit", message, { boundary });
};
@@ -51,7 +52,7 @@ export const parsePeResources = async (
let examined = 0;
let metadataBytes = 0;
const ensureCapacity = (projectedBytes: number): void => {
if (metadataBytes + projectedBytes > METADATA_BYTES)
if (metadataBytes + projectedBytes > PE_RESOURCE_METADATA_BYTES)
limit(
"PE resource metadata exceeds the 8 MiB complete-result allocation budget.",
"memory",
@@ -63,7 +64,9 @@ export const parsePeResources = async (
};
const account = (value: unknown): void => {
// Rows are retained in arrays; reserve a separator along with each row.
accountBytes(jsonBytes(value, METADATA_BYTES - metadataBytes - 1) + 1);
accountBytes(
jsonBytes(value, PE_RESOURCE_METADATA_BYTES - metadataBytes - 1) + 1,
);
};
const reserveDecodedName = (utf16Units: number): void => {
// A JSON string can need six ASCII bytes per UTF-16 code unit (for example,
@@ -254,7 +257,9 @@ export const parsePeResources = async (
],
};
assertArtifactReadActive(signal);
if (jsonBytes(report, METADATA_BYTES) > METADATA_BYTES)
if (
jsonBytes(report, PE_RESOURCE_METADATA_BYTES) > PE_RESOURCE_METADATA_BYTES
)
limit("PE resource result exceeds the 8 MiB allocation budget.", "memory");
return report;
};
+32
View File
@@ -172,3 +172,35 @@ describe("PE resource provider budget reporting", () => {
});
});
});
describe("PE resource provider fixed metadata budget", () => {
it("reports the fixed metadata budget without worker memory advice", async () => {
const root = await createTestTempDirectory("rea-pe-metadata-budget-");
const path = join(root, "resource.exe");
const name = "n".repeat(200);
const resources = Array.from({ length: 6000 }, (_, index) => ({
type: 10,
name: `${name}${String(index).padStart(4, "0")}`,
language: 1033,
payload: Buffer.alloc(0),
}));
await writeFile(
path,
peResourceFixture(false, resources, 10 * 1024 * 1024).bytes,
);
const result = await inspect(path);
if (result.ok) throw new Error("Expected a refusal");
const projected = projectAnalysisError(result.error);
expect(projected).toMatchObject({
code: "resource_constraint",
retryable: false,
details: {
resource: "memory",
reported_limits: { max_metadata_bytes: 8 * 1024 * 1024 },
},
});
// Neither caller bounds nor worker or host memory raise the fixed budget.
expect(projected.remediation.action).toContain("max_metadata_bytes");
expect(projected.remediation.action).not.toMatch(/sufficient memory/iu);
});
});
+14 -8
View File
@@ -25,7 +25,10 @@ import {
import { err, ok } from "../../domain/result.js";
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
import { PE_RESOURCES_PROVIDER } from "../../application/InvestigationProviders.js";
import { parsePeResources } from "./PeResourceParser.js";
import {
parsePeResources,
PE_RESOURCE_METADATA_BYTES,
} from "./PeResourceParser.js";
/** Portable parser over one bounded, identity-checked regular file. */
export class PeResourcesProvider implements PeResourcesPort {
@@ -126,19 +129,22 @@ const peResourcesFailurePrimary = (
{
max_file_bytes: input.max_file_bytes,
max_entries: input.max_entries,
...(cause.boundary === "memory"
? { max_metadata_bytes: PE_RESOURCE_METADATA_BYTES }
: {}),
},
{
cause,
// Both bounds are caller-selected and deterministic for the same
// input, so waiting for an active analysis cannot help.
...(cause.boundary === undefined || cause.boundary === "memory"
// Every bound is deterministic for the same input, so waiting for an
// active analysis or adding worker or host memory cannot help.
...(cause.boundary === undefined
? {}
: {
retryable: false,
remediationAction: inputBudgetRemediation(
cause.boundary,
input,
),
remediationAction:
cause.boundary === "memory"
? "max_metadata_bytes is a fixed PE resource metadata budget; max_entries, max_file_bytes, worker limits, and available host memory do not raise it. Select a complete PE image whose resource metadata fits the budget; retrying the unchanged image will fail again."
: inputBudgetRemediation(cause.boundary, input),
}),
},
);
+12 -1
View File
@@ -186,7 +186,18 @@ const failureForPrimary = (cause: unknown, path: string): AnalysisError => {
cause.constraint.resource,
`Selected artifact ${path}: ${cause.message}`,
cause.constraint.reported_limits,
{ cause },
{
cause,
// Section and inflated-JSON sizes are fixed reader budgets; worker
// limits and host memory cannot admit the same dependency section.
...(cause.constraint.resource === "memory"
? {
retryable: false,
remediationAction:
"maximum_bytes is a fixed Rust dependency-section budget; worker limits and available host memory do not raise it. Select another complete Rust binary whose dependency section fits the budget; retrying the unchanged artifact will fail again.",
}
: {}),
},
);
if (cause instanceof RustAuditableFormatFailure)
return cause.kind === "unsupported"
+32
View File
@@ -164,6 +164,38 @@ cliTest("reports malformed zlib as invalid input", async ({ cli }) => {
});
});
cliTest(
"refuses an oversized dependency section with fixed-budget advice",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-rust-cli-section-");
const path = join(root, "oversized-section.elf");
// Keep the ELF headers and declare a file-backed .dep-v0 one byte above
// the fixed 1 MiB compressed-section budget.
const limit = 1024 * 1024;
const bytes = Buffer.alloc(1024 + limit + 1);
createRustImage({ format: "elf" }).copy(bytes);
bytes.writeBigUInt64LE(BigInt(limit + 1), 384 + 32);
await writeFile(path, bytes);
const result = await cli.run({
arguments: ["inspect-rust-binary", path, "--json"],
});
expect(result.exitCode).toBe(1);
expect(result.json).toMatchObject({
code: "resource_constraint",
retryable: false,
remediation: { action: expect.stringContaining("maximum_bytes") },
details: {
resource: "memory",
reported_limits: { maximum_bytes: 1024 * 1024 },
},
});
// The section budget is fixed; worker or host memory cannot admit it.
expect(result.json).not.toMatchObject({
remediation: { action: expect.stringMatching(/sufficient memory/iu) },
});
},
);
cliTest(
"releases a failed Rust descriptor close before projecting the CLI failure",
async ({ cli }) => {
@@ -1245,7 +1245,15 @@ it.each(["identify_flutter_build", "inspect_dart_aot"] as const)(
onTestFinished(() => provider.close());
const result = await provider.execute({ operation, input: { path } });
if (result.ok) throw new Error("ABI expansion must be refused");
expect(projectAnalysisError(result.error).code).toBe("resource_constraint");
const projected = projectAnalysisError(result.error);
expect(projected).toMatchObject({
code: "resource_constraint",
retryable: false,
details: { reported_limits: { max_flutter_abis: 32 } },
});
// The inventory bound is fixed; worker or host memory cannot admit it.
expect(projected.remediation.action).toContain("max_flutter_abis");
expect(projected.remediation.action).not.toMatch(/sufficient memory/iu);
expect(extract).not.toHaveBeenCalled();
},
);
@@ -7,6 +7,7 @@ import {
AnalysisResourceConstraintError,
AnalysisUnsupportedTargetError,
} from "../../../src/domain/analysisErrorCore.js";
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
import {
createGoBinaryDiagnosticFixtures,
createGoBinaryFixture,
@@ -67,6 +68,11 @@ it("preserves the actual build-info byte guard as a resource failure", async ()
boundary: "build-info",
maximum_bytes: 1024 * 1024,
});
const projected = projectAnalysisError(result.error);
expect(projected.retryable).toBe(false);
// The parser budget is fixed; worker or host memory cannot admit it.
expect(projected.remediation.action).toContain("maximum_bytes");
expect(projected.remediation.action).not.toMatch(/sufficient memory/iu);
});
it.skipIf(process.platform === "win32")(
@@ -664,6 +664,11 @@ describe("managed comparison snapshot admission and cleanup", () => {
max_snapshot_bytes: MAX_MANAGED_SNAPSHOT_BYTES,
},
});
const projected = projectAnalysisError(result.error);
expect(projected.retryable).toBe(false);
// The snapshot budget is fixed; worker or host memory cannot admit it.
expect(projected.remediation.action).toContain("max_snapshot_bytes");
expect(projected.remediation.action).not.toMatch(/sufficient memory/iu);
expect(read).not.toHaveBeenCalled();
} finally {
read.mockRestore();