feat(analysis): make resource budgets recoverable across CLI and MCP (#9706)

* refactor(analysis): centralize resource budgets and recovery facts

* feat(analysis): expose ASAR and Ghidra runtime admission controls

* feat(artifacts): expose reader admission and preserve limited recognition

* feat(javascript): expose recoverable semantic precision budgets

* feat(web): add recoverable source-map budgets and compact retained views

* feat(web): make historical capture decoding budgets recoverable

* fix(desktop): enforce selected view policies across capture and export

* feat(runtime): make Frida and LLDB retention budgets selectable

* feat(native): expose diagnostic budgets and preserve exhaustion causes

* feat(managed): expose recoverable snapshot and representation budgets

* feat(metadata): make Go and Rust inspection budgets recoverable

* feat(wasm): expose representation and producer admissions

* feat(evm): expose per-call worker resource budgets

* feat(android): make resource and string-pool omissions recoverable

* feat(producers): expose command and recorded-capture budgets

* docs(analysis): record recovery contracts and integrated validation

* fix(docs): link calibration oracle from published verification pages

* fix(docs): restore desktop task anchors and receipt links

* test(analysis): align fixtures with selectable budget contracts

* fix(analysis): remove source-location projector dependency cycle

* test(analysis): refresh budget recovery and verifier regressions

* test(analysis): preserve format and publication recovery expectations

* test(windows): align conformance with owned runtime admission

* fix(desktop): preserve selected alias kinds in exported state

* test(analysis): align provider fixtures with recoverable budgets

* test(desktop): validate emitted alias metadata structurally

* fix(ghidra): classify bounded Windows endpoint refusals

* test(cli): assert selectable artifact admission recovery

* fix(cli): preserve capture provenance in environment builds
This commit is contained in:
morluto authored and GitHub committed 2026-11-10 05:11:20 -05:00
1 parent 55b6d2ca20
commit cb4e5cfbff
515 files changed
+21066 -3637

No files matched your search

@@ -348,6 +348,9 @@ export class JavaScriptAnalysisWorker {
id,
node_budget: capacity.nodes,
unknown_budget: capacity.unknowns,
...(this.limits.semanticPrecision === undefined
? {}
: { semantic_precision: this.limits.semanticPrecision }),
text_sha256: createHash("sha256").update(file.text.value).digest("hex"),
file: metadata,
package_type: packageType,
@@ -1,3 +1,4 @@
import { javaScriptSemanticPrecisionSchema } from "../../domain/javascript/javascriptSemanticPrecision.js";
import { z } from "zod";
import { ANALYSIS_RESOURCE_KINDS } from "../../domain/analysisErrorCore.js";
import { jsonValueSchema } from "../../domain/jsonValue.js";
@@ -25,6 +26,7 @@ export const javaScriptCheckpointCeilingRemediation = (
/** Caller-selected artifact identity, separate from the decoded source-text digest. */
export const javaScriptWorkerRequestSchema = z.strictObject({
id: z.uuid(),
semantic_precision: javaScriptSemanticPrecisionSchema.optional(),
unknown_budget: z.int().min(0).max(JAVASCRIPT_SEMANTIC_ITEM_MAXIMUM),
node_budget: z.int().min(0).max(JAVASCRIPT_SEMANTIC_ITEM_MAXIMUM),
text_sha256: digestSchema,
@@ -1,3 +1,7 @@
import {
resolveJavaScriptSemanticPrecision,
javaScriptSemanticPrecisionSchema,
} from "../../domain/javascript/javascriptSemanticPrecision.js";
import {
javaScriptSemanticUnknownOmissionsSchema,
javaScriptSemanticNodeSchema,
@@ -609,18 +613,27 @@ const coverage = z
status: z.literal("complete"),
omittedCount: z.literal(0),
resourceLimits: z.array(javaScriptSemanticResourceLimitSchema).optional(),
precision: javaScriptSemanticPrecisionSchema
.transform((value) => resolveJavaScriptSemanticPrecision(value))
.exactOptional(),
widenedReturnedReferences: z.array(range).optional(),
}),
z.strictObject({
status: z.literal("partial"),
omittedCount: z.union([z.literal(0), z.null()]),
resourceLimits: z.array(javaScriptSemanticResourceLimitSchema).optional(),
precision: javaScriptSemanticPrecisionSchema
.transform((value) => resolveJavaScriptSemanticPrecision(value))
.exactOptional(),
widenedReturnedReferences: z.array(range).optional(),
}),
z.strictObject({
status: z.literal("failed"),
omittedCount: z.null(),
resourceLimits: z.array(javaScriptSemanticResourceLimitSchema).optional(),
precision: javaScriptSemanticPrecisionSchema
.transform((value) => resolveJavaScriptSemanticPrecision(value))
.exactOptional(),
widenedReturnedReferences: z.array(range).optional(),
}),
])
@@ -1,3 +1,4 @@
import type { JavaScriptSemanticPrecisionInput } from "../../domain/javascript/javascriptSemanticPrecision.js";
import { fitJavaScriptRouteSourceFacts } from "../../domain/javascript/javascriptRouteResultBudget.js";
import { jsonValueByteLength } from "../../domain/jsonValueByteLength.js";
import type { JsonValue } from "../../domain/jsonValue.js";
@@ -46,6 +47,7 @@ export interface JavaScriptSourceWorkerAnalyzer {
},
packageType: JavaScriptPackageType,
purpose: "analysis" | "preload-discovery",
precision?: JavaScriptSemanticPrecisionInput,
): Promise<{
readonly module: JavaScriptModuleSemanticIr;
readonly projection: JavaScriptSemanticFileProjection;
@@ -191,6 +193,7 @@ const analyzeJob = async (
},
request.package_type,
request.purpose,
request.semantic_precision,
);
await send({ kind: "phase", id: request.id, phase: "transfer" });
const descriptor =
+1 -1
View File
@@ -1,4 +1,4 @@
import { BOUNDED_PRODUCER_LIMITS } from "../../process/LinuxResourceLimits.js";
import { BOUNDED_PRODUCER_LIMITS } from "../../domain/producerResourceLimits.js";
/**
* Releases the report parser accepts. Wakaru changes `--version`, `--json`
@@ -1,3 +1,7 @@
import { webSourceLocationInputSchema } from "../../domain/webSourceLocation.js";
import { analyzeWebBundleInputSchema } from "../../domain/webBundleAnalysis.js";
import { webSourceLocationArgs } from "../../../tests/fixtures/webSourceLocation.js";
import { selectedSourceMapLimits } from "../../domain/sourceMapLimits.js";
import { expect, it } from "vitest";
import { inspectSourceMapEnvelope } from "../../domain/sourceMapEnvelope.js";
@@ -49,3 +53,76 @@ it("admits numeric version 3 through the same consumers", () => {
}).matches,
).toMatchObject([{ section_path: [0, 0], reported_source: "original.ts" }]);
});
it("recovers the same finite map by raising the exact decoder record admission", () => {
const text = JSON.stringify({
version: 3,
sources: ["a.ts"],
sourcesContent: ["original"],
names: [],
mappings: "AAAA",
});
const position = { line: 1, column: 0 };
expect(() =>
traceSourceMap(
text,
"https://example.test/app.map",
position,
selectedSourceMapLimits({ decoded_records: 4 }),
),
).toThrow(
expect.objectContaining({
reason: "limit",
message: expect.stringContaining(
"source_map_limits.decoded_records is 4",
),
}),
);
expect(
traceSourceMap(
text,
"https://example.test/app.map",
position,
selectedSourceMapLimits({ decoded_records: 5 }),
).matches,
).toMatchObject([{ reported_source: "a.ts", content: { text: "original" } }]);
});
it("rejects invalid and inapplicable budget selections at the public owning contracts", () => {
for (const selection of [
{ map_bytes: 0 },
{ output_bytes: 536870888 },
{ decoded_records: 0 },
{ decoded_records: undefined },
{ codec_heap_mib: 127 },
{ fetch_bytes: 1024 },
]) {
expect(
webSourceLocationInputSchema.safeParse({
...webSourceLocationArgs,
source_map_limits: selection,
}).success,
).toBe(false);
}
const bundle = {
cdp_endpoint: "http://127.0.0.1:9222",
allowed_origins: ["https://app.test"],
target_id: "page",
fetch_source_maps: true,
};
expect(
analyzeWebBundleInputSchema.safeParse({
...bundle,
fetch_source_maps: false,
source_map_limits: { decoded_records: 5 },
}).success,
).toBe(false);
for (const selection of [{ map_bytes: 1024 }, { codec_heap_mib: 256 }]) {
expect(
analyzeWebBundleInputSchema.safeParse({
...bundle,
source_map_limits: selection,
}).success,
).toBe(false);
}
});
@@ -1,5 +1,9 @@
import { readStableArtifact } from "../../artifacts/readStableArtifact.js";
import { WEB_SOURCE_MAP_LIMITS } from "../../domain/webSourceLocation.js";
import {
selectedSourceMapLimits,
sourceMapLimitRecovery,
sourceMapLimitsSchema,
} from "../../domain/sourceMapLimits.js";
import { traceSourceMap } from "./TraceSourceMap.js";
import { SourceMapFormatFailure } from "./SourceMapFormat.js";
import {
@@ -17,26 +21,32 @@ const execute = async (): Promise<string> => {
"format",
"Codec request path is missing.",
);
const data = await readStableArtifact(
path,
WEB_SOURCE_MAP_LIMITS.outputBytes,
const selected = sourceMapLimitsSchema.parse(
JSON.parse(process.argv[3] ?? "{}"),
);
const limits = selectedSourceMapLimits(selected);
const data = await readStableArtifact(path, limits.outputBytes);
const text = new TextDecoder("utf-8", { fatal: true }).decode(data.bytes);
const value: unknown = JSON.parse(text);
const input = sourceMapCodecInputSchema.parse(value);
if (Buffer.byteLength(input.text) > WEB_SOURCE_MAP_LIMITS.mapBytes)
if (Buffer.byteLength(input.text) > limits.mapBytes)
throw new SourceMapFormatFailure(
"limit",
"Source map exceeds the 4 MiB codec input budget.",
`Source map exceeds codec input admission. ${sourceMapLimitRecovery("map_bytes", limits.mapBytes)}`,
);
const report = traceSourceMap(input.text, input.url, input.position);
const report = traceSourceMap(
input.text,
input.url,
input.position,
limits,
);
const reply = JSON.stringify(
sourceMapCodecReplySchema.parse({ state: "success", report }),
);
if (Buffer.byteLength(reply) > WEB_SOURCE_MAP_LIMITS.outputBytes)
if (Buffer.byteLength(reply) > limits.outputBytes)
throw new SourceMapFormatFailure(
"limit",
"Complete source-map point evidence exceeds the 32 MiB output budget; no partial evidence was returned.",
`Complete source-map point evidence exceeds output admission; no partial evidence was returned. ${sourceMapLimitRecovery("output_bytes", limits.outputBytes)}`,
);
return reply;
} catch (cause: unknown) {
@@ -21,10 +21,14 @@ import {
import { traceSourceMap } from "./TraceSourceMap.js";
import {
runSourceMapCommand,
SOURCE_MAP_CODEC_MEMORY_LIMIT_BYTES,
type SourceMapCleanupRetention,
} from "./SourceMapCommand.js";
const expectedProcessMemoryLimitBytes =
process.platform === "win32" && process.versions.bun !== undefined
? 704 * 1024 * 1024
: null;
const report = traceSourceMap(
JSON.stringify({ version: 3, sources: [], names: [], mappings: "" }),
"https://app.test/app.map",
@@ -37,7 +41,7 @@ const completedReport: WebSourceMapReport = {
? { ...report.runtime, v8_heap_limit_bytes: 200 * 1024 * 1024 }
: {
...report.runtime,
process_memory_limit_bytes: SOURCE_MAP_CODEC_MEMORY_LIMIT_BYTES,
process_memory_limit_bytes: expectedProcessMemoryLimitBytes,
},
};
const completedCodec = `process.stdout.write(${JSON.stringify(JSON.stringify({ state: "success", report: completedReport }))})`;
@@ -361,7 +365,7 @@ it("preserves an observed process-memory limit over the codec exit status", asyn
expect(result.error).toMatchObject({
resource: "memory",
reportedLimits: {
codec_process_memory_limit_bytes: SOURCE_MAP_CODEC_MEMORY_LIMIT_BYTES,
codec_process_memory_limit_bytes: expectedProcessMemoryLimitBytes,
},
capturedOutput: { stderr: "memory limit reached" },
});
+47 -20
View File
@@ -1,3 +1,8 @@
import {
sourceMapLimitSelection,
sourceMapLimitRecovery,
type SourceMapLimits,
} from "../../domain/sourceMapLimits.js";
import { randomUUID } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
@@ -15,7 +20,6 @@ import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
import type { JsonValue } from "../../domain/jsonValue.js";
import { err, type Result } from "../../domain/result.js";
import { noRuntimeOverrideSentence } from "../../domain/browserDeadline.js";
import {
DEFAULT_WEB_SOURCE_MAP_DECODE_TIMEOUT_MS,
WEB_SOURCE_MAP_LIMITS,
@@ -43,9 +47,6 @@ import { prepareProcessOwnershipInspection } from "../../process/ProcessOwnershi
const OPERATION = "trace_web_source_location";
const PROVIDER = "source-map-decoder";
/** Process commit limit enforced on the codec; null where V8 heap flags apply. */
export const SOURCE_MAP_CODEC_MEMORY_LIMIT_BYTES =
javaScriptWorkerMemoryLimitBytes(WEB_SOURCE_MAP_LIMITS.codecHeapMiB);
/** Launcher seam retains the actual codec protocol and ownership supervisor. */
export type SourceMapCodecLauncher = (
options: OwnedProviderProcessSpawnOptions,
@@ -83,7 +84,7 @@ type CodecPhysicalObservation =
readonly message: string;
readonly cause: Error;
}
| { readonly kind: "output-limit" };
| { readonly kind: "output-limit"; readonly stream: "stdout" | "stderr" };
type CodecOutcome =
| {
readonly kind: "physical";
@@ -109,6 +110,7 @@ export const runSourceMapCommand = async (
dependencies: SourceMapDecoderDependencies,
retainedCleanup: SourceMapCleanupRetention,
decodeTimeoutMs: number = DEFAULT_WEB_SOURCE_MAP_DECODE_TIMEOUT_MS,
limits: SourceMapLimits = WEB_SOURCE_MAP_LIMITS,
): Promise<Result<WebSourceMapReport, AnalysisError>> => {
try {
await (dependencies.prepareOwnership ?? prepareProcessOwnershipInspection)(
@@ -136,14 +138,21 @@ export const runSourceMapCommand = async (
decodeTimeoutMs,
options?.signal,
);
const command = new OwnedCodec(path, dependencies, deadline, decodeTimeoutMs);
const command = new OwnedCodec(
path,
dependencies,
deadline,
decodeTimeoutMs,
limits,
);
let result: Result<WebSourceMapReport, AnalysisError>;
try {
await command.start(request);
result = parseSourceMapReply(
await command.collect(),
path,
SOURCE_MAP_CODEC_MEMORY_LIMIT_BYTES,
javaScriptWorkerMemoryLimitBytes(limits.codecHeapMiB),
limits.codecHeapMiB,
);
} catch (cause: unknown) {
result = err(command.failure(cause));
@@ -189,6 +198,7 @@ class OwnedCodec implements SourceMapCleanupOwner {
readonly dependencies: SourceMapDecoderDependencies,
readonly deadline: ProviderStartupDeadline,
readonly decodeTimeoutMs: number,
readonly limits: SourceMapLimits,
) {
this.#onInterruption = () => this.#observeInterruption();
if (deadline.signal.aborted) this.#observeInterruption();
@@ -227,20 +237,25 @@ class OwnedCodec implements SourceMapCleanupOwner {
)({
command: process.execPath,
arguments: [
`--max-old-space-size=${String(WEB_SOURCE_MAP_LIMITS.codecHeapMiB)}`,
`--max-old-space-size=${String(this.limits.codecHeapMiB)}`,
"--max-semi-space-size=8",
"--v8-pool-size=1",
"--single-threaded",
"--stack_size=2048",
fileURLToPath(new URL("./SourceMapCodecProcess.js", import.meta.url)),
requestPath,
JSON.stringify(sourceMapLimitSelection(this.limits)),
],
runId: this.#runId,
cwd: this.#runtime.path,
hostEnvironment: environment,
...(SOURCE_MAP_CODEC_MEMORY_LIMIT_BYTES === null
...(javaScriptWorkerMemoryLimitBytes(this.limits.codecHeapMiB) === null
? {}
: { memoryLimitBytes: SOURCE_MAP_CODEC_MEMORY_LIMIT_BYTES }),
: {
memoryLimitBytes: javaScriptWorkerMemoryLimitBytes(
this.limits.codecHeapMiB,
)!,
}),
signal: this.deadline.signal,
});
this.#supervisor = new ProviderProcessSupervisor(
@@ -252,7 +267,7 @@ class OwnedCodec implements SourceMapCleanupOwner {
(() => cleanupOwnedProcessGroup(spawned.ownership)),
},
{
maxDiagnosticBytes: WEB_SOURCE_MAP_LIMITS.outputBytes + 1024 * 1024,
maxDiagnosticBytes: this.limits.outputBytes + 1024 * 1024,
onDiagnostic: (event) => {
if (
event.type === "exit" &&
@@ -274,10 +289,13 @@ class OwnedCodec implements SourceMapCleanupOwner {
(event.truncated === true ||
event.totalBytes >
(event.stream === "stdout"
? WEB_SOURCE_MAP_LIMITS.outputBytes
? this.limits.outputBytes
: 1024 * 1024))
) {
this.#recordPhysicalFailure({ kind: "output-limit" });
this.#recordPhysicalFailure({
kind: "output-limit",
stream: event.stream,
});
}
},
},
@@ -375,7 +393,7 @@ class OwnedCodec implements SourceMapCleanupOwner {
}
if (observation.kind === "process-error")
return this.#processErrorFailure(observation, snapshot);
return this.#outputLimitFailure(snapshot);
return this.#outputLimitFailure(snapshot, observation.stream);
}
#exitFailure(
@@ -425,6 +443,7 @@ class OwnedCodec implements SourceMapCleanupOwner {
#outputLimitFailure(
snapshot: ProviderProcessSnapshot | undefined,
stream: "stdout" | "stderr",
): AnalysisError {
const outputComplete = this.#supervisor?.observedOutputClose() ?? false;
const capturedOutput = this.#capturedOutput(
@@ -434,8 +453,14 @@ class OwnedCodec implements SourceMapCleanupOwner {
);
return new AnalysisOutputError(
OPERATION,
"Codec output exceeds its complete-evidence or diagnostic budget.",
stream === "stdout"
? "Codec output exceeds its complete-evidence budget."
: "Codec diagnostic output exceeds its 1048576-byte allowance.",
{
remediationAction:
stream === "stdout"
? sourceMapLimitRecovery("output_bytes", this.limits.outputBytes)
: "The codec stderr diagnostic allowance is fixed at 1048576 bytes; no source_map_limits field raises it. Inspect retained diagnostics before another call.",
capturedOutput: {
stdout: capturedOutput?.stdout ?? "",
stderr: capturedOutput?.stderr ?? "",
@@ -469,9 +494,11 @@ class OwnedCodec implements SourceMapCleanupOwner {
return new AnalysisResourceConstraintError(
OPERATION,
"memory",
`Source-map codec reached its ${SOURCE_MAP_CODEC_MEMORY_LIMIT_BYTES === null ? "" : `${String(SOURCE_MAP_CODEC_MEMORY_LIMIT_BYTES)}-byte `}process memory limit for ${this.path}; no partial evidence was returned. ${noRuntimeOverrideSentence(`The ${String(WEB_SOURCE_MAP_LIMITS.codecHeapMiB)} MiB codec heap stays fixed.`, "decode_timeout_ms")}`,
`Source-map codec reached its ${javaScriptWorkerMemoryLimitBytes(this.limits.codecHeapMiB) === null ? "" : `${String(javaScriptWorkerMemoryLimitBytes(this.limits.codecHeapMiB))}-byte `}process memory limit for ${this.path}; no partial evidence was returned. ${sourceMapLimitRecovery("codec_heap_mib", this.limits.codecHeapMiB)}`,
{
codec_process_memory_limit_bytes: SOURCE_MAP_CODEC_MEMORY_LIMIT_BYTES,
codec_process_memory_limit_bytes: javaScriptWorkerMemoryLimitBytes(
this.limits.codecHeapMiB,
),
},
{
capturedOutput: {
@@ -481,9 +508,9 @@ class OwnedCodec implements SourceMapCleanupOwner {
snapshot.diagnosticTruncated === true ||
!(this.#supervisor?.observedOutputClose() ?? false),
},
remediationAction: noRuntimeOverrideSentence(
`The ${String(WEB_SOURCE_MAP_LIMITS.codecHeapMiB)} MiB codec heap, ${String(WEB_SOURCE_MAP_LIMITS.mapBytes)}-byte map budget, and ${String(WEB_SOURCE_MAP_LIMITS.outputBytes)}-byte reply budget stay fixed.`,
"decode_timeout_ms",
remediationAction: sourceMapLimitRecovery(
"codec_heap_mib",
this.limits.codecHeapMiB,
),
},
);
+23 -9
View File
@@ -1,3 +1,8 @@
import {
selectedSourceMapLimits,
localSourceMapLimitsSchema,
sourceMapLimitRecovery,
} from "../../domain/sourceMapLimits.js";
import type { ExecutionOptions } from "../../application/AnalysisProvider.js";
import type { WebSourceMapPort } from "../../application/WebSourceLocationPorts.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
@@ -7,10 +12,8 @@ import {
} from "../../domain/analysisErrorCore.js";
import { ArtifactOperationError } from "../../domain/artifactOperationError.js";
import { err, type Result } from "../../domain/result.js";
import { noRuntimeOverrideSentence } from "../../domain/browserDeadline.js";
import {
DEFAULT_WEB_SOURCE_MAP_DECODE_TIMEOUT_MS,
WEB_SOURCE_MAP_LIMITS,
type WebSourceMapReport,
} from "../../domain/webSourceLocation.js";
import { sourceMapCodecInputSchema } from "./SourceMapCodecProtocol.js";
@@ -84,21 +87,31 @@ export class SourceMapDecoder implements WebSourceMapPort {
});
if (!request.success)
return err(new AnalysisInputError(OPERATION, { cause: request.error }));
const capacityFailure = () =>
new ArtifactOperationError(
const selected = localSourceMapLimitsSchema.safeParse(input.limits ?? {});
if (!selected.success)
return err(new AnalysisInputError(OPERATION, { cause: selected.error }));
const limits = selectedSourceMapLimits(selected.data);
const capacityFailure = (
field: "map_bytes" | "output_bytes",
value: number,
) => {
const recovery = sourceMapLimitRecovery(field, value);
return new ArtifactOperationError(
OPERATION,
"limit",
undefined,
`${input.path}: Source-map input exceeds the ${String(WEB_SOURCE_MAP_LIMITS.mapBytes)}-byte map or ${String(WEB_SOURCE_MAP_LIMITS.outputBytes)}-byte encoded request budget. ${noRuntimeOverrideSentence("Those budgets stay fixed.", "decode_timeout_ms")}`,
`${input.path}: Source-map admission exhausted. ${recovery}`,
{ remediationAction: recovery },
);
if (Buffer.byteLength(input.text) > WEB_SOURCE_MAP_LIMITS.mapBytes)
return err(capacityFailure());
};
if (Buffer.byteLength(input.text) > limits.mapBytes)
return err(capacityFailure("map_bytes", limits.mapBytes));
const encoded: string[] = [];
let encodedBytes = 0;
for (const part of jsonParts(request.data)) {
const bytes = Buffer.byteLength(part);
if (bytes > WEB_SOURCE_MAP_LIMITS.outputBytes - encodedBytes)
return err(capacityFailure());
if (bytes > limits.outputBytes - encodedBytes)
return err(capacityFailure("output_bytes", limits.outputBytes));
encoded.push(part);
encodedBytes += bytes;
}
@@ -115,6 +128,7 @@ export class SourceMapDecoder implements WebSourceMapPort {
this.dependencies,
retainedCleanup,
input.decodeTimeoutMs ?? DEFAULT_WEB_SOURCE_MAP_DECODE_TIMEOUT_MS,
limits,
);
if (retainedCleanup.owner !== undefined)
this.#pendingCleanup = retainedCleanup.owner;
+51 -28
View File
@@ -1,5 +1,9 @@
import { z } from "zod";
import { WEB_SOURCE_MAP_LIMITS } from "../../domain/webSourceLocation.js";
import {
WEB_SOURCE_MAP_LIMITS,
sourceMapLimitRecovery,
type SourceMapLimits,
} from "../../domain/sourceMapLimits.js";
import {
classifySourceMapSection,
classifySourceMapSections,
@@ -41,13 +45,14 @@ interface PendingMap {
readonly stop: Offset | null;
}
/** Shared structural constraints configurable only for stricter consumers. */
/** Aggregate structural work shared by maps in one selected inspection. */
export interface SourceMapStructureBudget {
records: number;
}
export interface SourceMapStructureOptions {
readonly profile?: "browser-collection";
readonly limits?: SourceMapLimits;
readonly budget?: SourceMapStructureBudget;
readonly check?: () => void;
}
@@ -98,7 +103,11 @@ const codecLeaf = (
const compare = (a: Offset, b: Offset): number =>
a.line - b.line || a.column - b.column;
const addOffset = (base: Offset, offset: Offset): Offset => {
const addOffset = (
base: Offset,
offset: Offset,
limits: SourceMapLimits,
): Offset => {
const result = {
line: base.line + offset.line,
column: offset.column + (offset.line === 0 ? base.column : 0),
@@ -106,20 +115,24 @@ const addOffset = (base: Offset, offset: Offset): Offset => {
if (
!Number.isSafeInteger(result.line) ||
!Number.isSafeInteger(result.column) ||
result.line >= WEB_SOURCE_MAP_LIMITS.decodedRows
result.line >= limits.decodedRows
)
throw new SourceMapFormatFailure(
"limit",
"Indexed offset exceeds the 262144 generated-row decoder budget.",
`Indexed offset exceeds the generated-row decoder budget. ${sourceMapLimitRecovery("decoded_rows", limits.decodedRows)}`,
);
return result;
};
const consumeRecords = (budget: { records: number }, count: number): void => {
if (count > WEB_SOURCE_MAP_LIMITS.decodedRecords - budget.records)
const consumeRecords = (
budget: { records: number },
limits: SourceMapLimits,
count: number,
): void => {
if (count > limits.decodedRecords - budget.records)
throw new SourceMapFormatFailure(
"limit",
`Source-map structure exceeds the ${WEB_SOURCE_MAP_LIMITS.decodedRecords}-record decoder budget.`,
`Source-map structure exceeds the decoder budget. ${sourceMapLimitRecovery("decoded_records", limits.decodedRecords)}`,
);
budget.records += count;
};
@@ -137,8 +150,9 @@ const addSections = ({
readonly budget: { records: number };
readonly options: SourceMapStructureOptions;
}): void => {
const limits = options.limits ?? WEB_SOURCE_MAP_LIMITS;
options.check?.();
consumeRecords(budget, rawSections.length);
consumeRecords(budget, limits, rawSections.length);
const sections = z.array(sectionSchema).parse(rawSections);
const embeddedMaps: Extract<
SourceMapSection,
@@ -176,14 +190,14 @@ const addSections = ({
);
const next = sections[index + 1];
const nextOffset =
next === undefined ? null : addOffset(node.offset, next.offset);
next === undefined ? null : addOffset(node.offset, next.offset, limits);
const stop =
nextOffset === null
? node.stop
: node.stop === null || compare(nextOffset, node.stop) < 0
? nextOffset
: node.stop;
const offset = addOffset(node.offset, section.offset);
const offset = addOffset(node.offset, section.offset, limits);
pending.push({
value: map,
path: [...node.path, index],
@@ -195,6 +209,7 @@ const addSections = ({
const inspectMappingLayout = (
mappings: string,
limits: SourceMapLimits,
check?: () => void,
): { lines: number; segments: number } => {
let lines = 1;
@@ -241,13 +256,10 @@ const inspectMappingLayout = (
digits = 0;
}
}
if (
segments > WEB_SOURCE_MAP_LIMITS.decodedSegments ||
lines > WEB_SOURCE_MAP_LIMITS.decodedRows
)
if (segments > limits.decodedSegments || lines > limits.decodedRows)
throw new SourceMapFormatFailure(
"limit",
"Source-map layout exceeds the 262144 segment/generated-row decoder budget.",
`Source-map layout exceeds decoder admission. ${sourceMapLimitRecovery(segments > limits.decodedSegments ? "decoded_segments" : "decoded_rows", segments > limits.decodedSegments ? limits.decodedSegments : limits.decodedRows)}`,
);
}
finish();
@@ -259,8 +271,9 @@ const inspectLeaf = (
budget: { records: number },
options: SourceMapStructureOptions,
): z.output<typeof leafSchema> => {
const limits = options.limits ?? WEB_SOURCE_MAP_LIMITS;
options.check?.();
consumeRecords(budget, 1);
consumeRecords(budget, limits, 1);
if (
typeof node.value !== "object" ||
node.value === null ||
@@ -271,20 +284,28 @@ const inspectLeaf = (
const rawSources = rawMap.sources;
const rawNames = rawMap.names;
const rawContents = rawMap.sourcesContent;
consumeRecords(budget, Array.isArray(rawSources) ? rawSources.length : 0);
consumeRecords(budget, Array.isArray(rawNames) ? rawNames.length : 0);
consumeRecords(budget, Array.isArray(rawContents) ? rawContents.length : 0);
consumeRecords(
budget,
limits,
Array.isArray(rawSources) ? rawSources.length : 0,
);
consumeRecords(budget, limits, Array.isArray(rawNames) ? rawNames.length : 0);
consumeRecords(
budget,
limits,
Array.isArray(rawContents) ? rawContents.length : 0,
);
options.check?.();
const leaf = leafSchema.parse(node.value);
const layout = inspectMappingLayout(leaf.mappings, options.check);
consumeRecords(budget, layout.segments + layout.lines);
const layout = inspectMappingLayout(leaf.mappings, limits, options.check);
consumeRecords(budget, limits, layout.segments + layout.lines);
if (
layout.segments > WEB_SOURCE_MAP_LIMITS.decodedSegments ||
node.offset.line + layout.lines > WEB_SOURCE_MAP_LIMITS.decodedRows
layout.segments > limits.decodedSegments ||
node.offset.line + layout.lines > limits.decodedRows
)
throw new SourceMapFormatFailure(
"limit",
"Source-map layout exceeds the 262144 segment/generated-row decoder budget.",
`Source-map layout exceeds decoder admission. ${sourceMapLimitRecovery(layout.segments > limits.decodedSegments ? "decoded_segments" : "decoded_rows", layout.segments > limits.decodedSegments ? limits.decodedSegments : limits.decodedRows)}`,
);
return leaf;
};
@@ -292,10 +313,11 @@ const inspectLeaf = (
/** Validate structure before allocation, retaining raw declarations and section boundaries. */
export const inspectSourceMap = (
text: string,
options: SourceMapStructureOptions = {},
): ReturnType<typeof inspectSourceMapValue> => {
const jsonText = sourceMapJsonText(text);
const value: unknown = JSON.parse(jsonText);
return inspectSourceMapValue(value);
return inspectSourceMapValue(value, options);
};
/** Return validated leaves when a consumer needs decoded rows after preflight. */
@@ -308,6 +330,7 @@ export const inspectSourceMapValue = (
readonly declarations: readonly SourceMapSourceDeclaration[];
readonly leaves: readonly SourceMapLeaf[];
} => {
const limits = options.limits ?? WEB_SOURCE_MAP_LIMITS;
options.check?.();
const root = mapHeaderSchema.parse(value);
const declarations: SourceMapSourceDeclaration[] = [];
@@ -320,10 +343,10 @@ export const inspectSourceMapValue = (
options.check?.();
const node = pending.pop();
if (node === undefined) break;
if (node.path.length > WEB_SOURCE_MAP_LIMITS.sectionDepth)
if (node.path.length > limits.sectionDepth)
throw new SourceMapFormatFailure(
"limit",
"Indexed source-map nesting exceeds the 64-level decoder stack budget.",
`Indexed source-map nesting exceeds decoder admission. ${sourceMapLimitRecovery("section_depth", limits.sectionDepth)}`,
);
const sections = classifySourceMapSections(node.value);
if (sections.kind === "invalid")
+6 -1
View File
@@ -1,3 +1,4 @@
import { WEB_SOURCE_MAP_LIMITS } from "../../domain/sourceMapLimits.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import {
AnalysisCapabilityUnavailableError,
@@ -17,6 +18,7 @@ export const parseSourceMapReply = (
text: string,
path: string,
processMemoryLimitBytes: number | null,
heapMiB: number = WEB_SOURCE_MAP_LIMITS.codecHeapMiB,
): Result<WebSourceMapReport, AnalysisError> => {
let value: unknown;
try {
@@ -42,6 +44,7 @@ export const parseSourceMapReply = (
const runtimeFailure = codecRuntimeFailure(
reply.data.report.runtime,
processMemoryLimitBytes,
heapMiB,
);
return runtimeFailure === undefined
? ok(reply.data.report)
@@ -52,6 +55,7 @@ export const parseSourceMapReply = (
const codecRuntimeFailure = (
runtime: WebSourceMapReport["runtime"],
processMemoryLimitBytes: number | null,
heapMiB: number,
): string | undefined => {
if (
runtime.id !==
@@ -60,7 +64,7 @@ const codecRuntimeFailure = (
return `Codec reported runtime ${runtime.id}, not the runtime that launched it.`;
if (
runtime.id === "node-v8" &&
runtime.v8_heap_limit_bytes > 256 * 1024 * 1024
runtime.v8_heap_limit_bytes > (heapMiB + 64) * 1024 * 1024
)
return "Codec reported a V8 heap limit exceeding its independent process budget.";
if (
@@ -83,6 +87,7 @@ const codecFailure = (
"limit",
undefined,
`${path}: ${detail}`,
{ remediationAction: detail },
)
: reason === "unsupported"
? new AnalysisCapabilityUnavailableError(
+10 -5
View File
@@ -1,3 +1,8 @@
import {
WEB_SOURCE_MAP_LIMITS,
sourceMapLimitRecovery,
type SourceMapLimits,
} from "../../domain/sourceMapLimits.js";
import { createHash } from "node:crypto";
import { createRequire } from "node:module";
import { getHeapStatistics } from "node:v8";
@@ -15,7 +20,6 @@ import { resolveJavaScriptSourceMapUrl } from "../../domain/javascript/javascrip
import {
webSourceMapReportSchema,
type WebSourceMapReport,
WEB_SOURCE_MAP_LIMITS,
} from "../../domain/webSourceLocation.js";
import type { WebSourcePosition } from "../../domain/webSourcePosition.js";
import {
@@ -30,8 +34,9 @@ export const traceSourceMap = (
text: string,
url: string,
position: WebSourcePosition,
limits: SourceMapLimits = WEB_SOURCE_MAP_LIMITS,
): WebSourceMapReport => {
const inspected = inspectSourceMap(text);
const inspected = inspectSourceMap(text, { limits });
const decodedLeaves = decodeValidatedSourceMapLeaves(inspected.leaves, url);
let map: TraceMap;
if (inspected.format === "regular") {
@@ -71,10 +76,10 @@ export const traceSourceMap = (
for (const segment of segments) {
const match = originalMatch(segment, map, url, inspected.declarations);
bytes += Buffer.byteLength(JSON.stringify(match)) + 1;
if (bytes > WEB_SOURCE_MAP_LIMITS.outputBytes)
if (bytes > limits.outputBytes)
throw new SourceMapFormatFailure(
"limit",
"Complete source-map point evidence exceeds the 32 MiB output budget; no partial evidence was returned.",
`Complete source-map point evidence exceeds output admission; no partial evidence was returned. ${sourceMapLimitRecovery("output_bytes", limits.outputBytes)}`,
);
matches.push(match);
}
@@ -104,7 +109,7 @@ export const traceSourceMap = (
node_compatibility_version: runtime.nodeCompatibilityVersion,
v8_heap_limit_bytes: null,
process_memory_limit_bytes: javaScriptWorkerMemoryLimitBytes(
WEB_SOURCE_MAP_LIMITS.codecHeapMiB,
limits.codecHeapMiB,
),
},
format: inspected.format,