fix(apk): classify fixed inspection allowances as nonretryable (#7784)

This commit is contained in:
gensokyo-dev authored and GitHub committed 2026-10-28 20:13:58 +08:00
1 parent 9593bc536b
commit ac545adae2
5 files changed
+191 -3

No files matched your search

+7
View File
@@ -239,6 +239,13 @@ serialized size of the report together with its limitations (which repeat
archive names) is then checked against an 8 MiB output budget (the exact check
is authoritative); an over-budget report is refused, not truncated.
Signing blocks admit at most 4096 ID/value pairs; at most 4096 signing-relevant
META-INF entries are retained. These fixed counts, signer/lineage counts and the
report byte budget return nonretryable `resource_constraint` errors naming the
effective allowance. This release has no runtime override for them. Waiting for
an active scan cannot raise a fixed count, and complete CLI JSON cannot raise the
report budget. Occupied scan-queue capacity remains retryable after a scan finishes.
The whole-file reader allocates the selected snapshot and bounded read scratch;
total process memory exceeds the selected file's size. It also computes the complete
file SHA-256, so inspection is not header-only. Content digests are recomputed
+15 -3
View File
@@ -1,3 +1,5 @@
import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { expect, it } from "vitest";
import { apkSignatureAdmission } from "./ApkSignatureAdmission.js";
import { APK_SIGNATURE_LIMITS } from "./ApkSignatureRelease.js";
@@ -21,9 +23,19 @@ it("bounds process-wide scans and waiting requests, cancels queued work and reco
() => apkSignatureAdmission.acquire(),
);
try {
await expect(apkSignatureAdmission.acquire()).rejects.toMatchObject({
_tag: "AnalysisResourceConstraintError",
resource: "capacity",
const refused = await apkSignatureAdmission
.acquire()
.catch((cause: unknown) => {
if (!(cause instanceof AnalysisResourceConstraintError)) throw cause;
return projectAnalysisError(cause);
});
expect(refused).toMatchObject({
code: "resource_constraint",
retryable: true,
remediation: {
action:
"Wait for an active analysis to complete, then retry within the reported admission limits.",
},
});
expect(granted).toBe(false);
controller.abort(reason);
@@ -0,0 +1,66 @@
import { expect, it } from "vitest";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import {
apkOutputBudgetError,
mapApkBlockError,
mapApkSchemeError,
mapCentralDirectoryError,
v1SignerCapacityError,
} from "./apkSignatureFailures.js";
import { APK_SIGNATURE_LIMITS } from "./ApkSignatureRelease.js";
it.each([
[
"block pairs",
mapApkBlockError(
{ kind: "budget-exceeded", resource: "signing-block-pairs", limit: 4096 },
"/selected.apk",
),
"max_signing_block_pairs",
4096,
],
[
"scheme signers",
mapApkSchemeError(
{ kind: "budget-exceeded", resource: "signers", limit: 32 },
"v2",
),
"max_signers",
32,
],
[
"v1 entries",
mapCentralDirectoryError({
kind: "budget-exceeded",
resource: "v1-signature-entries",
limit: 4096,
}),
"max_v1_signature_entries",
4096,
],
["v1 signers", v1SignerCapacityError(32), "max_v1_signers", 32],
[
"report bytes",
apkOutputBudgetError(APK_SIGNATURE_LIMITS.outputBytes + 1),
"max_output_bytes",
APK_SIGNATURE_LIMITS.outputBytes,
],
] as const)(
"reports %s as a deterministic fixed allowance",
(_name, error, key, limit) => {
const projected = projectAnalysisError(error);
expect(projected).toMatchObject({
code: "resource_constraint",
retryable: false,
details: { reported_limits: { [key]: limit } },
});
expect(projected.remediation.action).toContain(String(limit));
expect(projected.remediation.action).toContain("no runtime override");
expect(projected.remediation.action).toContain(
"Repeating the unchanged request fails the same way",
);
expect(projected.remediation.action).not.toContain(
"Wait for an active analysis to complete",
);
},
);
+21
View File
@@ -1,3 +1,4 @@
import { fixedAllowanceOptions } from "../domain/fixedAllowanceRemediation.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import {
artifactFilePrimaryFacts,
@@ -66,6 +67,10 @@ export const mapApkBlockError = (
"capacity",
`APK Signing Block holds more ${error.resource} than the ${String(error.limit)} limit.`,
{ max_signing_block_pairs: error.limit },
fixedAllowanceOptions(
`The APK Signing Block allowance of ${String(error.limit)} pairs (this release has no runtime override)`,
"count",
),
);
}
};
@@ -84,6 +89,10 @@ export const mapApkSchemeError = (
"capacity",
`APK Signature Scheme ${scheme} holds more ${error.resource} than the ${String(error.limit)} limit.`,
{ [`max_${error.resource.replace(/-/g, "_")}`]: error.limit },
fixedAllowanceOptions(
`The APK Signature Scheme ${scheme} allowance of ${String(error.limit)} ${error.resource} (this release has no runtime override)`,
"count",
),
);
/** A central directory that cannot be walked leaves v1 presence unknown: refuse. */
@@ -96,6 +105,10 @@ export const mapCentralDirectoryError = (
"capacity",
`The central directory holds more than ${String(error.limit)} META-INF v1 signing entries.`,
{ max_v1_signature_entries: error.limit },
fixedAllowanceOptions(
`The APK v1 allowance of ${String(error.limit)} META-INF signing entries (this release has no runtime override)`,
"count",
),
)
: inputError(
`Malformed ZIP central directory at byte ${String(error.offset)}: ${error.detail}.`,
@@ -108,6 +121,10 @@ export const v1SignerCapacityError = (limit: number): AnalysisError =>
"capacity",
`The archive pairs more than ${String(limit)} v1 signature blocks with .SF files.`,
{ max_v1_signers: limit },
fixedAllowanceOptions(
`The APK v1 allowance of ${String(limit)} signers (this release has no runtime override)`,
"count",
),
);
/** The decoded result would exceed the inline output budget. */
@@ -117,6 +134,10 @@ export const apkOutputBudgetError = (projectedBytes: number): AnalysisError =>
"capacity",
`The decoded signing report (${String(projectedBytes)} bytes) exceeds the ${String(APK_SIGNATURE_LIMITS.outputBytes)}-byte output budget.`,
{ max_output_bytes: APK_SIGNATURE_LIMITS.outputBytes },
fixedAllowanceOptions(
`The decoded APK signing report allowance of ${String(APK_SIGNATURE_LIMITS.outputBytes)} bytes (this release has no runtime override)`,
"report",
),
);
/**
@@ -0,0 +1,82 @@
import { createHash } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { parseEvidence } from "../../src/domain/evidence.js";
import { analysisErrorProjectionSchema } from "../../src/contracts/errorSchemas.js";
import { apkSignatureSummaryViewResultSchema } from "../../src/domain/analysisView/analysisView.js";
import { parseMcpToolError } from "../fixtures/mcpToolError.js";
import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import {
withSigningBlock,
zipWith,
} from "../fixtures/apkSignature/v1SignatureBlock.js";
import { cliTest } from "../support/cli/cliFixture.js";
cliTest(
"keeps fixed APK pair refusals nonretryable through CLI and MCP, then accepts a valid input",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-apk-pair-limit-");
const archive = await zipWith([
{ name: "AndroidManifest.xml", data: Uint8Array.of(3, 0, 8, 0) },
]);
const paths = new Map<number, string>();
const digests = new Map<number, string>();
for (const count of [1, 4096, 4097]) {
const path = join(root, `pairs-${count}.apk`);
const bytes = withSigningBlock(
archive,
Array.from({ length: count }, () => ({
id: 0xffffffff,
value: new Uint8Array(),
})),
);
await writeFile(path, bytes);
digests.set(count, createHash("sha256").update(bytes).digest("hex"));
paths.set(count, path);
}
const expected = {
code: "resource_constraint",
category: "resource_constraint",
retryable: false,
details: {
operation: "inspect_apk_signature",
resource: "capacity",
reported_limits: { max_signing_block_pairs: 4096 },
},
};
for (let attempt = 0; attempt < 2; attempt++) {
const reply = await cli.run({
arguments: ["inspect-apk-signature", paths.get(4097)!, "--json"],
});
expect(reply.exitCode, reply.stderr).toBe(1);
const error = analysisErrorProjectionSchema.parse(reply.json);
expect(error).toMatchObject(expected);
expect(error.remediation.action).toContain("4096");
expect(error.remediation.action).toContain("no runtime override");
expect(error.remediation.action).toContain(
"Waiting for an active analysis to finish does not raise it",
);
}
const { call } = await connectLocalToolsMcp({
REA_MCP_OUTPUT_SCHEMA_PROFILE: "off",
});
const rejected = await call("inspect_apk_signature", {
path: paths.get(4097),
});
expect(parseMcpToolError(rejected).error).toMatchObject(expected);
for (const count of [4096, 1]) {
const path = paths.get(count)!;
const reply = await call("inspect_apk_signature", { path });
expect(reply.isError, JSON.stringify(reply.content)).not.toBe(true);
const report = apkSignatureSummaryViewResultSchema.parse(
parseEvidence(reply.structuredContent).normalized_result,
).summary;
expect(report.artifact.path).toBe(path);
expect(report.artifact.sha256).toBe(digests.get(count));
expect(report.posture.strongest_scheme).toBe("none");
}
},
);