fix(artifacts): name the ASAR header admission budget (#6172)

Keep the 16 MiB ASAR header/index ceiling fixed and report that budget,
the lack of a runtime override, and that source selection does not bypass
header admission.

Refs #5910

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: morluto <morluto@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-22 06:33:09 +08:00
1 parent 82e4e032df
commit d7ca93cc2c
11 files changed
+202 -15

No files matched your search

@@ -188,6 +188,7 @@ const artifactFailureToOperationError = (
...(cause.partialObservation === undefined
? {}
: { partialObservation: cause.partialObservation }),
...(cause.admission === undefined ? {} : { admission: cause.admission }),
},
);
@@ -32,6 +32,9 @@ export const translateArtifactFailure = (
...(cause.partialObservation === undefined
? {}
: { partialObservation: cause.partialObservation }),
...(cause.admission === undefined
? {}
: { admission: cause.admission }),
},
);
bindArtifactFileCleanup(cause, error);
+9
View File
@@ -1,3 +1,4 @@
import type { ArtifactAdmissionLimit } from "../domain/artifactOperationError.js";
import type {
ArtifactIntegrityClaim,
ArtifactIntegrityDetails,
@@ -28,6 +29,8 @@ export interface ArtifactReaderFailureOptions extends ErrorOptions {
readonly partialObservation?: AnalysisPartialObservation | undefined;
/** The constrained resource bound for a limit failure. */
readonly boundary?: ArtifactReaderBoundary;
/** Named admission whose recovery must not be inferred from other limit text. */
readonly admission?: ArtifactAdmissionLimit;
}
/** Archive-neutral entry metadata. Reader adapters never choose output paths. */
@@ -192,6 +195,8 @@ export class ArtifactReaderFailure extends Error {
readonly partialObservation: AnalysisPartialObservation | undefined;
/** The constrained bound for a `limit` reason, when the reader knows it. */
readonly boundary: ArtifactReaderBoundary | undefined;
/** Named admission for a limit whose public recovery is owned by the producer. */
readonly admission: ArtifactAdmissionLimit | undefined;
constructor(
readonly reason:
@@ -217,6 +222,7 @@ export class ArtifactReaderFailure extends Error {
options?.cleanupLeases ?? (this.cleanup === undefined ? [] : undefined);
this.partialObservation = options?.partialObservation;
this.boundary = options?.boundary;
this.admission = options?.admission;
}
/** Preserve the primary failure and observation. Supplied leases must cover all merged cleanup. */
@@ -261,6 +267,9 @@ export class ArtifactReaderFailure extends Error {
...(primary.boundary === undefined
? {}
: { boundary: primary.boundary }),
...(primary.admission === undefined
? {}
: { admission: primary.admission }),
...(mergedObservation === undefined
? {}
: { partialObservation: mergedObservation }),
+30 -5
View File
@@ -5,9 +5,37 @@ import { assertArtifactReadActive } from "./ArtifactFileOperation.js";
import { ArtifactReaderFailure } from "./ArtifactReader.js";
import { err, ok, type Result } from "../domain/result.js";
/** Shared resource budget for the eagerly decoded ASAR header, not member bytes. */
/**
* Fixed header/index admission for this release. The header is decoded before
* any member path can be selected. There is no caller or configuration override.
*/
export const MAX_ASAR_HEADER_BYTES = 16 * 1024 * 1024;
const MEBIBYTE = 1024 * 1024;
/** User-visible recovery for the fixed ASAR header/index admission budget. */
export const asarHeaderAdmissionRecovery = (): string => {
const bytes = MAX_ASAR_HEADER_BYTES;
const mebibytes = bytes / MEBIBYTE;
const allowance = Number.isInteger(mebibytes)
? `${String(mebibytes)} MiB / ${String(bytes)} bytes`
: `${String(bytes)} bytes`;
return [
`ASAR header/index admission is limited to ${allowance}.`,
"This release has no runtime override.",
"Selecting fewer sources or narrowing an internal path does not bypass header admission, because the archive index is read before any member can be selected.",
"Raising max_heap_mb or available host memory does not raise it.",
"Analyze an extracted application directory, which does not admit an ASAR header, or supply an archive whose header fits this allowance.",
"Repeating the unchanged request fails the same way.",
].join(" ");
};
/** Refuse an ASAR whose header/index exceeds the fixed admission budget. */
export const asarHeaderAdmissionFailure = (): ArtifactReaderFailure =>
new ArtifactReaderFailure("limit", asarHeaderAdmissionRecovery(), {
admission: { kind: "asar-header", maximumBytes: MAX_ASAR_HEADER_BYTES },
});
/** Absolute archive prefix required to inspect an admitted header. */
export const MAX_ASAR_HEADER_PROBE_BYTES = MAX_ASAR_HEADER_BYTES + 8;
@@ -345,10 +373,7 @@ export const admitAsarHeader = async (
const parsed = parseAsarHeaderExtents(prefix, observed.size);
if (!parsed.ok) throw parsed.error;
if (parsed.value.headerBytes > MAX_ASAR_HEADER_BYTES)
throw new ArtifactReaderFailure(
"limit",
`ASAR header exceeds the ${String(MAX_ASAR_HEADER_BYTES)} byte decode budget`,
);
throw asarHeaderAdmissionFailure();
return {
archiveBytes: observed.size,
...parsed.value,
@@ -1,6 +1,6 @@
import { hasGodotPckHeader } from "../../domain/godotPckFormat.js";
import type { ArtifactClassificationEvidence } from "../ArtifactClassificationProbe.js";
import { ArtifactReaderFailure } from "../ArtifactReader.js";
import { asarHeaderAdmissionFailure } from "../AsarHeader.js";
import { hasDestinationPath } from "../ArtifactPaths.js";
import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
import { isJavaScriptSourcePath } from "../../domain/javascript/javascriptArtifactFiles.js";
@@ -529,11 +529,7 @@ export const classifyArtifactContent = (
path.toLowerCase().endsWith(".asar") &&
asarHeader.state !== "rejected"
) {
if (asarHeader.state === "limit")
throw new ArtifactReaderFailure(
"limit",
"ASAR header exceeds the bounded classification or decode budget",
);
if (asarHeader.state === "limit") throw asarHeaderAdmissionFailure();
if (asarHeader.state === "matched")
return { kind: "container", format: "asar" };
}
+6
View File
@@ -374,6 +374,12 @@ const artifactStateErrorDetails = (
reason: error.reason,
...(error.reason === "limit" ? { truncated: true } : {}),
...(error.detail === undefined ? {} : { detail: error.detail }),
...(error.admission === undefined
? {}
: {
admission: error.admission.kind,
maximum_header_bytes: error.admission.maximumBytes,
}),
};
if (error instanceof UnknownRegistryError) return { reason: error.reason };
if (error instanceof EvidenceFileError)
+16 -1
View File
@@ -7,9 +7,17 @@ import {
type AnalysisErrorOptions,
} from "./analysisErrorBase.js";
/** Fixed admission named by the producing reader, with its effective byte ceiling. */
export interface ArtifactAdmissionLimit {
readonly kind: "asar-header";
readonly maximumBytes: number;
}
/** Artifact inventory or extraction failed a typed safety boundary. */
export class ArtifactOperationError extends AnalysisError {
readonly _tag = "ArtifactOperationError";
override readonly userMessage: string | undefined;
readonly admission: ArtifactAdmissionLimit | undefined;
constructor(
readonly operation:
@@ -37,7 +45,10 @@ export class ArtifactOperationError extends AnalysisError {
readonly artifactDetails?: ArtifactIntegrityDetails,
/** The specific constraint that failed, such as the colliding path. */
readonly detail?: string,
options?: AnalysisErrorOptions,
options?: AnalysisErrorOptions & {
readonly userMessage?: string;
readonly admission?: ArtifactAdmissionLimit;
},
) {
super(
artifactDetails === undefined
@@ -45,6 +56,10 @@ export class ArtifactOperationError extends AnalysisError {
: artifactDetailsMessage(operation, reason, artifactDetails),
options,
);
this.admission = options?.admission;
this.userMessage =
options?.userMessage ??
(options?.admission === undefined ? undefined : detail);
}
}