refactor: remove redundant seams and preserve cleanup ownership (#10375)

* refactor: remove redundant seams and preserve cleanup ownership

* fix: validate PE resource extents before budget checks
This commit is contained in:
morluto authored and GitHub committed 2026-11-13 10:11:06 -05:00
1 parent 4b26f38d6f
commit cdc4541ef7
369 files changed
+3847 -3662

No files matched your search

+2 -1
View File
@@ -5,7 +5,8 @@ import { fileURLToPath, pathToFileURL } from "node:url";
/**
* Reduce filesystem resolution during CLI and MCP startup. Keep dependencies
* external, dynamic imports lazy, and original compiled worker entrypoints.
* external and original compiled worker entrypoints. Dynamic imports can split
* into lazy chunks unless the same module is also statically reachable.
*/
export const bundleRuntime = async (root) => {
const compiledRoot = resolve(root, "dist");
+1 -1
View File
@@ -1,5 +1,5 @@
import { execFile } from "node:child_process";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { mkdir, writeFile } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
-2
View File
@@ -1,4 +1,3 @@
import { deepStrictEqual } from "node:assert";
import {
lstat,
mkdir,
@@ -13,7 +12,6 @@ import { dirname, isAbsolute, join } from "node:path";
import { parse as parseToml } from "smol-toml";
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
import { PRODUCT_IDENTITY } from "../dist/identity.js";
import { pathExists } from "./lib/verify-package-core.mjs";
import { parse as parseJsonc } from "jsonc-parser";
import { skillReferenceIssues } from "./lib/docs-facts.mjs";
-4
View File
@@ -1,7 +1,6 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { readFile, mkdtemp, mkdir, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
@@ -13,7 +12,6 @@ import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
import { ArtifactResourceScope } from "../dist/artifacts/ArtifactResourceScope.js";
import { readGhidraLanguageCatalog } from "../dist/ghidra/GhidraLanguageCatalog.js";
import { parseEvidence } from "../dist/domain/evidence.js";
import { parseAnalysisSnapshot } from "../dist/domain/analysisSnapshot.js";
import { functionDossierSchema } from "../dist/domain/native/functionDossier.js";
import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs";
import { buildGameCubeDolFixture } from "../tests/conformance/ghidra/dol-fixture.mjs";
@@ -96,8 +94,6 @@ async function languageInstalled(languageId) {
}
}
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
/** One MCP session plus CLI check. `languageOverride` is set only for the Gekko pass. */
async function runPass({ fixture, languageOverride, label, layout }) {
const env = { ...baseEnv };
+2 -2
View File
@@ -185,7 +185,7 @@ const stringBindings = (sourceFile, symbols, unresolved) => {
)
return undefined;
if ((declaration.parent.flags & ts.NodeFlags.Const) === 0) {
unknown(identifier, "mutable setting-key or value alias");
unknown(declaration.name, "mutable setting-key or value alias");
return undefined;
}
if (declaration.pos >= identifier.pos) {
@@ -919,7 +919,7 @@ if (direct) {
const entries = parseNumericLimitRegistry(registry);
const listed = new Map();
for (const entry of entries) {
const { name, policy, read, reason } = entry;
const { name, read, reason } = entry;
if (listed.has(name)) failures.push(`${name} is listed twice`);
listed.set(name, entry);
if (reason.trim().length < 40)
+31 -6
View File
@@ -20,6 +20,7 @@ it.each([
source:
'process.stdout.write("partial-雪"); process.stderr.write("waiting\\n"); setInterval(() => {}, 1000)',
code: "provider_timeout",
timeoutMs: 500,
stdout: "partial-雪",
stderr: "waiting\n",
},
@@ -28,6 +29,7 @@ it.each([
source:
'process.stdout.write("partial-雪"); process.stderr.write("adb: error: device offline\\n"); process.exitCode = 1',
code: "unsupported_target",
expectedPath: "caller-device",
stdout: "partial-雪",
stderr: "adb: error: device offline\n",
},
@@ -36,6 +38,7 @@ it.each([
source:
'process.stdout.write("partial-雪"); process.stderr.write("adb: error: cannot stat \'/host/token file\': Permission denied\\n"); process.exitCode = 1',
code: "unsupported_target",
expectedPath: "/host/token file",
stdout: "partial-雪",
stderr: "adb: error: cannot stat '/host/token file': Permission denied\n",
},
@@ -54,7 +57,7 @@ it.each([
},
["-e", testCase.source],
{
timeoutMs: testCase.name === "timeout" ? 500 : 5_000,
timeoutMs: testCase.timeoutMs ?? 5_000,
maxOutputBytes: testCase.maxOutputBytes ?? 1024,
},
undefined,
@@ -75,12 +78,34 @@ it.each([
},
});
if (testCase.code === "unsupported_target")
expect(projected.details?.path).toBe(
testCase.name === "local path refusal"
? "/host/token file"
: "caller-device",
);
expect(projected.details?.path).toBe(testCase.expectedPath);
if (testCase.code === "provider_timeout") {
expect(projected.details?.timeout_ms).toBe(testCase.timeoutMs);
expect(projected.details?.limit).toBeUndefined();
}
expect(JSON.stringify(projected)).not.toContain(
"ambient-value-must-not-be-persisted",
);
});
it.each([true, false])(
"projects a truncated adb capture as retryable only for volatile output (%s)",
async (volatileOutput) => {
const result = await runAdbCapture(
volatileOutput ? "read_adb_logcat" : "install_adb_package",
{ device: "device" },
{ binary: process.execPath, pathSource: "environment", environment: {} },
["-e", 'process.stdout.write("X".repeat(4096))'],
{ timeoutMs: 5_000, maxOutputBytes: 1024, volatileOutput },
undefined,
);
if (result.ok) throw new Error("Expected a capture allowance refusal");
const projected = projectAnalysisError(result.error);
expect(projected.retryable).toBe(volatileOutput);
expect(projected).toMatchObject({
code: "resource_constraint",
details: { reported_limits: { max_output_bytes: 1024 } },
});
expect(projected.details?.limit).toBeUndefined();
},
);
-60
View File
@@ -1,5 +1,4 @@
import { expect, it, vi } from "vitest";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import {
ExecFileOutputCancelledError,
execFileBytes,
@@ -212,65 +211,6 @@ it("does not infer cancellation from a foreign ABORT_ERR code", async () => {
});
});
it.each([true, false])(
"projects a truncated adb capture as retryable only for volatile output (%s)",
async (volatileOutput) => {
vi.mocked(execFileBytes).mockRejectedValueOnce(
Object.assign(new Error("too much"), {
code: 1,
stdout: "",
stderr: "",
signal: null,
killed: false,
outputTruncated: true,
}),
);
const result = await runAdbCapture(
volatileOutput ? "read_adb_logcat" : "install_adb_package",
{ device: "device" },
{ binary: "/host/adb", pathSource: "environment", environment: {} },
volatileOutput ? ["logcat", "-d"] : ["install", "app.apk"],
{ timeoutMs: 100, maxOutputBytes: 1024, volatileOutput },
undefined,
);
if (result.ok) throw new Error("Expected a capture allowance refusal");
const projected = projectAnalysisError(result.error);
expect(projected.retryable).toBe(volatileOutput);
expect(projected).toMatchObject({
code: "resource_constraint",
details: { reported_limits: { max_output_bytes: 1024 } },
});
expect(projected.details?.limit).toBeUndefined();
},
);
it("reports the effective fixed adb deadline", async () => {
vi.mocked(execFileBytes).mockRejectedValueOnce(
Object.assign(new Error("deadline"), {
code: "ETIMEDOUT",
stdout: "",
stderr: "",
signal: null,
killed: true,
}),
);
const result = await runAdbCapture(
"pull_adb_file",
{ device: "device" },
{ binary: "/host/adb", pathSource: "environment", environment: {} },
["pull", "/remote"],
{ timeoutMs: 600_000, maxOutputBytes: 1024 },
undefined,
);
if (result.ok) throw new Error("Expected an adb deadline");
const projected = projectAnalysisError(result.error);
expect(projected).toMatchObject({
code: "provider_timeout",
details: { timeout_ms: 600_000 },
});
expect(projected.details?.limit).toBeUndefined();
});
it("keeps a physical failure even when the later abort reason is that same error", async () => {
const controller = new AbortController();
const failure = new Error("physical command failure");
+48 -32
View File
@@ -47,19 +47,32 @@ it("reports the fixed adb version-probe deadline", async () => {
});
it("reports the fixed adb version-output cap", async () => {
vi.mocked(execFileOutput).mockRejectedValueOnce(
Object.assign(new Error("too much"), {
code: "ERR_CHILD_PROCESS_STDIO_MAXBUFFER",
stdout: "",
stderr: "",
signal: null,
killed: false,
outputTruncated: true,
}),
);
const failure = await inspectAdbClient(selection).catch(
(cause: unknown) => cause,
);
let selected: string = selection.binary;
if (process.platform === "win32") {
vi.mocked(execFileOutput).mockRejectedValueOnce(
Object.assign(new Error("too much"), {
code: "ERR_CHILD_PROCESS_STDIO_MAXBUFFER",
stdout: "",
stderr: "",
signal: null,
killed: false,
outputTruncated: true,
}),
);
} else {
const root = await mkdtemp(join(tmpdir(), "rea-adb-version-cap-"));
onTestFinished(() => rm(root, { recursive: true, force: true }));
selected = join(root, "adb");
await writeFile(
selected,
`#!${process.execPath}\nprocess.stdout.write("x".repeat(100_000));\n`,
{ mode: 0o755 },
);
}
const failure = await inspectAdbClient({
...selection,
binary: selected,
}).catch((cause: unknown) => cause);
const projected = projectAnalysisError(
failure as Parameters<typeof projectAnalysisError>[0],
);
@@ -74,25 +87,28 @@ it("reports the fixed adb version-output cap", async () => {
expect(projected.details?.limit).toBeUndefined();
});
it("distinguishes execute denial from missing or unreadable version output", async () => {
vi.mocked(execFileOutput).mockRejectedValueOnce(
Object.assign(new Error("denied"), {
code: "EACCES",
spawnFailed: true,
stdout: "",
stderr: "",
signal: null,
killed: false,
}),
);
await expect(inspectAdbClient(selection)).rejects.toMatchObject({
code: "binary_not_executable",
diagnostics: {
selected_binary: selection.binary,
filesystem_error: "EACCES",
},
});
});
it.skipIf(process.platform !== "win32")(
"classifies an EACCES execute refusal on Windows",
async () => {
vi.mocked(execFileOutput).mockRejectedValueOnce(
Object.assign(new Error("denied"), {
code: "EACCES",
spawnFailed: true,
stdout: "",
stderr: "",
signal: null,
killed: false,
}),
);
await expect(inspectAdbClient(selection)).rejects.toMatchObject({
code: "binary_not_executable",
diagnostics: {
selected_binary: selection.binary,
filesystem_error: "EACCES",
},
});
},
);
it("names an EPERM execute refusal separately from missing permission", async () => {
vi.mocked(execFileOutput).mockRejectedValueOnce(
+4 -4
View File
@@ -313,10 +313,10 @@ export class JadxProvider implements AndroidAnalysisPort {
: AbortSignal.any([signal, this.#shutdown.signal]),
});
this.#inspections.add(pending);
void pending.then(
() => this.#inspections.delete(pending),
() => this.#inspections.delete(pending),
);
const remove = (): void => {
this.#inspections.delete(pending);
};
void pending.then(remove, remove);
return pending;
}
+16 -8
View File
@@ -1,5 +1,6 @@
import * as commandOwner from "../process/OwnedCommand.js";
import { failedInspectionDescriptor } from "../../tests/fixtures/processInspectionCleanup.js";
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
import {
AnalysisError,
primaryAnalysisError,
@@ -486,6 +487,7 @@ it("cancels a pending filesystem lookup and owns its eventual rejection", async
const pendingStat = new Promise((_resolve, reject) => {
rejectLookup = reject;
});
void pendingStat.catch(() => undefined);
const observed = new Promise<void>((resolve) => {
started = resolve;
});
@@ -502,14 +504,20 @@ it("cancels a pending filesystem lookup and owns its eventual rejection", async
"win32",
controller.signal,
);
await observed;
controller.abort();
await expect(pending).rejects.toMatchObject({
_tag: "AnalysisCancelledError",
});
rejectLookup(new Error("filesystem completed after cancellation"));
await Promise.resolve();
expect(lookup.stat).toHaveBeenCalledTimes(1);
try {
await awaitAcknowledgement(observed, pending, "apktool filesystem lookup");
controller.abort();
await expect(pending).rejects.toMatchObject({
_tag: "AnalysisCancelledError",
});
rejectLookup(new Error("filesystem completed after cancellation"));
await Promise.resolve();
expect(lookup.stat).toHaveBeenCalledTimes(1);
} finally {
controller.abort();
rejectLookup(new Error("fixture cleanup"));
await pending.catch(() => undefined);
}
});
it("bounds the entire pending PATH lookup by the selection deadline", async () => {
+23 -4
View File
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, onTestFinished } from "vitest";
import { createDeferred } from "../../tests/fixtures/binarySession.js";
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
import {
getNavigationContext,
@@ -500,7 +501,16 @@ it("waits for late facet cleanup after cancellation and retains its owner", asyn
settled = true;
return result;
});
await started.promise;
onTestFinished(async () => {
controller.abort();
late.resolve(err(failure));
try {
await pending;
} finally {
await lease.release();
}
});
await awaitAcknowledgement(started.promise, pending, "facet execution");
controller.abort();
await Promise.resolve();
expect(settled).toBe(false);
@@ -530,7 +540,12 @@ it("reports cancellation after a late successful facet that ignored its signal",
{ address: "0x401000", document: "fixture" },
controller.signal,
);
await started.promise;
onTestFinished(async () => {
controller.abort();
late.resolve(ok(createAnalysisExecution("finished", provider)));
await pending;
});
await awaitAcknowledgement(started.promise, pending, "facet execution");
controller.abort();
late.resolve(ok(createAnalysisExecution("finished", provider)));
const result = await pending;
@@ -586,7 +601,11 @@ it("preserves a provider's cancellation for the same signal", async () => {
{ address: "0x401000", document: "fixture" },
controller.signal,
);
await started.promise;
onTestFinished(async () => {
controller.abort(reason);
await pending;
});
await awaitAcknowledgement(started.promise, pending, "provider execution");
controller.abort(reason);
const result = await pending;
if (result.ok) throw new Error("expected cancellation");
+1 -4
View File
@@ -67,10 +67,7 @@ import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js";
import { profilelessNativeDispatchProfile } from "./binary/ProfilelessNativeWorkflowSnapshot.js";
import { err, ok, type Result } from "../domain/result.js";
import type { AnalysisSnapshot } from "../domain/analysisSnapshot.js";
import type {
AnalysisExecution,
ProviderIdentity,
} from "./AnalysisProvider.js";
import type { AnalysisExecution } from "./AnalysisProvider.js";
import { createAnalysisExecutionEvidence } from "../domain/analysisObservation.js";
import { REA_WORKFLOW_PROVIDER } from "../domain/investigationProviders.js";
import { workflowAnalysisProfile } from "./WorkflowAnalysisProfile.js";
-1
View File
@@ -1,7 +1,6 @@
import { expect, it } from "vitest";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { RegularFileCleanupFailure } from "../filesystem/OwnedRegularFile.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import { MANAGED_SKILL_FILES } from "./SetupSkill.js";
import { searchGuidanceOutputSchema } from "../contracts/guidanceContracts.js";
import { RetainedRegularFileCleanupFailure } from "./RegularFileRead.js";
+17 -11
View File
@@ -12,6 +12,7 @@ import { join, sep } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
import { ok } from "../domain/result.js";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
let resources: ArtifactResourceScope;
@@ -612,17 +613,22 @@ describe("Registration cancellation and sibling outcomes", () => {
entered();
await blocked;
});
await admitted;
const controller = new AbortController();
const queued = register(path, launcher, controller.signal);
controller.abort("queued cancellation");
unblock();
await first;
expect(await queued).toMatchObject({
ok: false,
error: { _tag: "AnalysisCancelledError" },
});
expect(await readFile(path, "utf8")).toBe("{}");
try {
await awaitAcknowledgement(admitted, first, "registration transaction");
const controller = new AbortController();
const queued = register(path, launcher, controller.signal);
controller.abort("queued cancellation");
unblock();
await first;
expect(await queued).toMatchObject({
ok: false,
error: { _tag: "AnalysisCancelledError" },
});
expect(await readFile(path, "utf8")).toBe("{}");
} finally {
unblock();
await first;
}
});
it("keeps successful siblings after ordinary failures and marks work not started after cancellation", async () => {
-1
View File
@@ -23,7 +23,6 @@ import { PRODUCT_IDENTITY, SDK_IDENTITY } from "../identity.js";
import { DEFAULT_CATALOG_IDENTITY } from "../catalogIdentity.js";
import type { ClientRegistrationStatus } from "./ClientRegistrationStatus.js";
import { currentClientRegistrationCommand } from "./ClientRegistrationIdentity.js";
import { CURRENT_ENTRYPOINT_PATH } from "../process/CurrentEntrypointPath.js";
/** Recording setup host for service-level planning and recovery tests. */
export class FakeSetupHost implements SetupHost {
@@ -31,7 +31,6 @@ import type {
} from "../AnalysisProvider.js";
import {
AnalysisSnapshotCache,
SNAPSHOT_CACHE_ENTRY_CEILING,
isSnapshotCacheable,
isSourceEvidenceRetainable,
} from "./AnalysisSnapshotCache.js";
@@ -595,7 +594,7 @@ describe("analysis snapshot cache capacity", () => {
parameters: { query: "existing" },
execution,
};
for (let index = 0; index < SNAPSHOT_CACHE_ENTRY_CEILING - 1; index += 1)
for (let index = 0; index < 9_999; index += 1)
cache.recordWorkflow({
...workflowInput,
parameters: { query: String(index) },
@@ -620,9 +619,7 @@ describe("analysis snapshot cache capacity", () => {
});
cache.recordWorkflow(workflowInput);
expect(cache.entries()).toHaveLength(1);
expect(cache.workflowEntries()).toHaveLength(
SNAPSHOT_CACHE_ENTRY_CEILING - 1,
);
expect(cache.workflowEntries()).toHaveLength(9_999);
cache.record({
...directInput,
@@ -669,9 +666,7 @@ describe("analysis snapshot cache capacity", () => {
),
"a capacity-refused workflow binding is not counted",
).toBe(0);
expect(cache.workflowEntries()).toHaveLength(
SNAPSHOT_CACHE_ENTRY_CEILING - 1,
);
expect(cache.workflowEntries()).toHaveLength(9_999);
const otherTarget = { ...target, sha256: "b".repeat(64) };
cache.recordWorkflow({
@@ -87,7 +87,7 @@ const DOCUMENT_SCOPED_OPERATIONS: ReadonlySet<string> = new Set(
);
/** Maximum duplicated result bindings retained across direct and workflow queries. */
export const SNAPSHOT_CACHE_ENTRY_CEILING = 10_000;
const SNAPSHOT_CACHE_ENTRY_CEILING = 10_000;
const replayDocumentSelected = (
operation: AnalysisOperation,
+19 -38
View File
@@ -1754,22 +1754,16 @@ export class BinarySession
this.#transitionPending += 1;
// Later calls wait for this transition and must not be included in its drain.
const admittedThrough = this.#transitionGeneration++;
const result = this.#transition.then(
() => operation(admittedThrough),
() => operation(admittedThrough),
);
const perform = (): Promise<T> => operation(admittedThrough);
const result = this.#transition.then(perform, perform);
this.#transition = result.then(
() => undefined,
() => undefined,
);
void result.then(
() => {
this.#transitionPending -= 1;
},
() => {
this.#transitionPending -= 1;
},
);
const finish = (): void => {
this.#transitionPending -= 1;
};
void result.then(finish, finish);
return result;
}
@@ -1864,32 +1858,19 @@ export class BinarySession
);
};
signal.addEventListener("abort", onAbort, { once: true });
transition.then(
() => {
signal.removeEventListener("abort", onAbort);
resolve(
signal.aborted
? err(
new AnalysisCancelledError(operation, {
cause: signal.reason,
}),
)
: ok(undefined),
);
},
() => {
signal.removeEventListener("abort", onAbort);
resolve(
signal.aborted
? err(
new AnalysisCancelledError(operation, {
cause: signal.reason,
}),
)
: ok(undefined),
);
},
);
const finish = (): void => {
signal.removeEventListener("abort", onAbort);
resolve(
signal.aborted
? err(
new AnalysisCancelledError(operation, {
cause: signal.reason,
}),
)
: ok(undefined),
);
};
transition.then(finish, finish);
});
}
}
@@ -1,3 +1,6 @@
import { createHash } from "node:crypto";
import { parseAppleIps } from "../../native/apple/AppleIpsParser.js";
import { inspectAppleIpsInputSchema } from "../../domain/native/appleIps.js";
import { parseEvidence } from "../../domain/evidence.js";
import { expect, it } from "vitest";
import { AppleIpsService } from "./AppleIpsService.js";
@@ -10,52 +13,19 @@ import {
} from "../../domain/analysisErrorCore.js";
const identity = APPLE_IPS_PROVIDER;
const report: AppleIps = {
artifact: {
path: "/artifacts/report.ips",
sha256: "a".repeat(64),
bytes: 128,
const bytes = Buffer.from(
'{"bug_type":"309"}\n{"procName":"rea-crash-fixture","threads":[],"usedImages":[]}',
);
const path = "/artifacts/report.ips";
const report = parseAppleIps(
bytes,
{
path,
sha256: createHash("sha256").update(bytes).digest("hex"),
bytes: bytes.length,
},
metadata: {
bug_type: "309",
timestamp: null,
os_version: null,
bundle_id: null,
incident_id: null,
name: null,
build_version: null,
platform: null,
unlisted_keys: [],
},
process: {
name: "rea-crash-fixture",
path: null,
pid: null,
parent: null,
parent_pid: null,
cpu_type: null,
translated: null,
os_version: null,
},
exception: null,
termination: null,
faulting_thread: null,
threads: [],
images: [],
classification: { status: "unclassified", matches: [] },
coverage: {
status: "complete",
reason: null,
threads: 0,
frames: 0,
images: 0,
registers: 0,
type_mismatches: [],
},
unlisted_body_keys: [],
join: null,
limitations: ["Opened Mach-O and dSYM UUID joins remain unobserved."],
};
inspectAppleIpsInputSchema.parse({ path }),
);
it.each([0, 5])(
"rejects an unresolved faulting thread %s without its mismatch",
@@ -1,11 +1,13 @@
import type { ExecutionOptions } from "../AnalysisProvider.js";
import type {
ExecutionOptions,
ProviderIdentity,
} from "../AnalysisProvider.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import type {
BinaryLayout,
InspectBinaryLayoutInput,
} from "../../domain/native/binaryLayout.js";
import type { Result } from "../../domain/result.js";
import type { ProviderIdentity } from "../AnalysisProvider.js";
/** Replaceable offline binary-layout provider; owns its parser and process protocol. */
export interface BinaryLayoutPort {
@@ -5,10 +5,7 @@ import { expect, it, vi } from "vitest";
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
import { byteEntropySchema } from "../../domain/native/byteEntropy.js";
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
import {
ByteEntropyService,
MAX_BYTE_ENTROPY_BLOCKS,
} from "./ByteEntropyService.js";
import { ByteEntropyService } from "./ByteEntropyService.js";
it("keeps whole-file identity and entropy while selecting unaligned blocks and a partial tail", async () => {
const root = await createTestTempDirectory("rea-entropy-");
@@ -421,7 +418,7 @@ it("reports a changed artifact when its parent directory moves during streaming"
it("refuses a block series above the output-row limit before scanning", async () => {
const root = await createTestTempDirectory("rea-entropy-block-limit-");
const path = join(root, "bytes");
await writeFile(path, Buffer.alloc(MAX_BYTE_ENTROPY_BLOCKS * 256 + 1));
await writeFile(path, Buffer.alloc(32_768 * 256 + 1));
let reads = 0;
const service = new ByteEntropyService({
stat,
@@ -443,8 +440,8 @@ it("refuses a block series above the output-row limit before scanning", async ()
_tag: "AnalysisResourceConstraintError",
resource: "capacity",
reportedLimits: {
max_blocks: MAX_BYTE_ENTROPY_BLOCKS,
requested_blocks: MAX_BYTE_ENTROPY_BLOCKS + 1,
max_blocks: 32_768,
requested_blocks: 32_769,
},
},
});
@@ -44,7 +44,7 @@ import { completeProviderEvidence } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
/** Bound retained result rows before constructing the Evidence representation. */
export const MAX_BYTE_ENTROPY_BLOCKS = 32_768;
const MAX_BYTE_ENTROPY_BLOCKS = 32_768;
/** Generated files such as Linux /proc entries report size 0 but still hold bytes. */
const ZERO_SIZE_LIMITATION =
@@ -2,7 +2,10 @@ import { parseEvidence } from "../../domain/evidence.js";
import { expect, it } from "vitest";
import { ElfNotesService } from "./ElfNotesService.js";
import { ELF_NOTES_PROVIDER } from "../../domain/investigationProviders.js";
import type { ElfNotes } from "../../domain/native/elfNotes.js";
import { createHash } from "node:crypto";
import { inspectElfNotesInputSchema } from "../../domain/native/elfNotes.js";
import { elfNoteFixture } from "../../native/elf/ElfNotes.fixture.js";
import { parseElfNotes } from "../../native/elf/ElfNoteParser.js";
import { ok } from "../../domain/result.js";
import {
AnalysisCancelledError,
@@ -11,22 +14,17 @@ import {
} from "../../domain/analysisErrorCore.js";
const identity = ELF_NOTES_PROVIDER;
const report: ElfNotes = {
artifact: {
path: "/artifacts/app",
sha256: "a".repeat(64),
bytes: 512,
const path = "/artifacts/app";
const bytes = elfNoteFixture({ ownership: "none" });
const report = parseElfNotes(
bytes,
{
path,
sha256: createHash("sha256").update(bytes).digest("hex"),
bytes: bytes.length,
},
format: "elf64",
byte_order: "little",
machine: 62,
type: 3,
type_name: "shared",
tables: [],
entries: [],
coverage: { status: "complete", examined_tables: 0, examined_entries: 0 },
limitations: ["No PT_NOTE program header or SHT_NOTE section was declared."],
};
inspectElfNotesInputSchema.parse({ path }),
);
it("rejects a relative path and extra arguments before reading", async () => {
let calls = 0;
@@ -27,57 +27,6 @@ const undecodableHeaderPointer = (): Buffer => {
bytes[Number(bytes.readBigUInt64LE(64 + 56 + 8)) + 1] = 0x0f;
return bytes;
};
const report: ElfUnwindRanges = {
artifact: {
path: "/artifacts/app",
sha256: "a".repeat(64),
bytes: 512,
},
format: "elf64",
byte_order: "little",
machine: 62,
type: 3,
type_name: "shared",
eh_frame: { status: "absent", source: null, location: null },
eh_frame_hdr: {
status: "absent",
location: null,
version: null,
eh_frame_pointer_encoding: null,
fde_count_encoding: null,
table_encoding: null,
eh_frame_pointer: null,
fde_count: null,
table_entries: 0,
},
debug_frame: { status: "absent", location: null },
cies: [],
ranges: [],
page: {
offset: 0,
limit: 256,
returned: 0,
total: 0,
next_offset: null,
},
coverage: {
status: "complete",
total_ranges: 0,
total_cies: 0,
ranges_with_symbols: 0,
ranges_without_symbols: 0,
eh_frame_hdr_agreement: "unavailable",
examined_records: 0,
},
limitations: [
"No .eh_frame section or PT_GNU_EH_FRAME segment was declared.",
"No .eh_frame_hdr section or PT_GNU_EH_FRAME segment was declared.",
"CFA instructions are counted but not evaluated.",
".debug_frame is absent; only .eh_frame was examined.",
"Symbol names, section names, and augmentation strings are target-derived and untrusted.",
],
};
const parsed = (
bytes: Buffer,
raw: Readonly<Record<string, unknown>> = {},
@@ -97,6 +46,8 @@ const parsed = (
);
};
const report = parsed(elfUnwindAbsentFixture());
const accept = async (
value: ElfUnwindRanges,
raw: Readonly<Record<string, unknown>> = {},
@@ -1,7 +1,10 @@
import { parseEvidence } from "../../domain/evidence.js";
import { createHash } from "node:crypto";
import { inspectPeCertificatesInputSchema } from "../../domain/native/peCertificates.js";
import { peCertificateFixture } from "../../native/pe/PeCertificates.fixture.js";
import {
peCertificateFixture,
peWithoutCertificateTableFixture,
} from "../../native/pe/PeCertificates.fixture.js";
import { parsePeCertificates } from "../../native/pe/PeCertificateParser.js";
import { expect, it } from "vitest";
import { PeCertificatesService } from "./PeCertificatesService.js";
@@ -15,19 +18,17 @@ import {
} from "../../domain/analysisErrorCore.js";
const identity = PE_CERTIFICATES_PROVIDER;
const report: PeCertificates = {
artifact: {
path: "/artifacts/app.exe",
sha256: "a".repeat(64),
bytes: 512,
const path = "/artifacts/app.exe";
const bytes = peWithoutCertificateTableFixture();
const report = parsePeCertificates(
bytes,
{
path,
sha256: createHash("sha256").update(bytes).digest("hex"),
bytes: bytes.length,
},
format: "pe32",
machine: 0x14c,
table: null,
entries: [],
coverage: { status: "complete", examined_entries: 0, certificates: 0 },
limitations: ["IMAGE_DIRECTORY_ENTRY_SECURITY is absent."],
};
inspectPeCertificatesInputSchema.parse({ path }),
);
it("rejects a relative path and extra arguments before reading", async () => {
let calls = 0;
@@ -1,9 +1,15 @@
import { createHash } from "node:crypto";
import { peWithoutImportTableFixture } from "../../../tests/fixtures/binaryDiagnostics/peImports.js";
import { parsePeImports } from "../../native/pe/PeImportParser.js";
import {
inspectPeImportsInputSchema,
peImportsSchema,
type PeImports,
} from "../../domain/native/peImports.js";
import { parseEvidence } from "../../domain/evidence.js";
import { expect, it } from "vitest";
import { PeImportsService } from "./PeImportsService.js";
import { PE_IMPORTS_PROVIDER } from "../../domain/investigationProviders.js";
import type { PeImports } from "../../domain/native/peImports.js";
import { peImportsSchema } from "../../domain/native/peImports.js";
import { ok } from "../../domain/result.js";
import {
AnalysisCancelledError,
@@ -12,35 +18,17 @@ import {
} from "../../domain/analysisErrorCore.js";
const identity = PE_IMPORTS_PROVIDER;
const report: PeImports = {
artifact: {
path: "/artifacts/app.exe",
sha256: "a".repeat(64),
bytes: 512,
const path = "/artifacts/app.exe";
const bytes = peWithoutImportTableFixture();
const report = await parsePeImports(
bytes,
{
path,
sha256: createHash("sha256").update(bytes).digest("hex"),
bytes: bytes.length,
},
format: "pe32-plus",
machine: 0x8664,
bits: 64,
architecture: "amd64",
import_directory: null,
delay_import_directory: null,
imports: [],
delay_imports: [],
imphash: {
algorithm: "pefile",
md5: null,
reason: "IMAGE_DIRECTORY_ENTRY_IMPORT is absent.",
},
coverage: {
status: "complete",
reason: null,
examined_dlls: 0,
imports: 0,
delay_imports: 0,
},
issues: [],
limitations: ["IMAGE_DIRECTORY_ENTRY_IMPORT is absent."],
};
inspectPeImportsInputSchema.parse({ path }),
);
it("rejects a relative path and extra arguments before reading", async () => {
let calls = 0;
@@ -189,7 +177,7 @@ it("publishes every consumed directory span as its own Evidence location", async
size: 40,
locations: [
{ offset: 0x100, bytes: 20 },
{ offset: 0x1f8, bytes: 20 },
{ offset: report.artifact.bytes - 8, bytes: 20 },
],
},
}),
@@ -11,7 +11,7 @@ import {
type PeResourceIdentity,
} from "../../domain/native/peResources.js";
import { peSubjectArchitecture } from "../../domain/peMachine.js";
import { err, ok, type Result } from "../../domain/result.js";
import { err, type Result } from "../../domain/result.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
@@ -2,7 +2,9 @@ import { parseEvidence } from "../../domain/evidence.js";
import { expect, it } from "vitest";
import { WindowsShortcutService } from "./WindowsShortcutService.js";
import { WINDOWS_SHORTCUT_PROVIDER } from "../../domain/investigationProviders.js";
import type { WindowsShortcut } from "../../domain/native/windowsShortcut.js";
import { createHash } from "node:crypto";
import { inspectWindowsShortcutInputSchema } from "../../domain/native/windowsShortcut.js";
import { parseWindowsShortcut } from "../../native/windows/WindowsShortcutParser.js";
import { ok } from "../../domain/result.js";
import {
AnalysisCancelledError,
@@ -12,64 +14,20 @@ import {
const identity = WINDOWS_SHORTCUT_PROVIDER;
const report: WindowsShortcut = {
artifact: {
path: "/artifacts/shortcut.lnk",
sha256: "a".repeat(64),
bytes: 128,
const path = "/artifacts/shortcut.lnk";
const bytes = Buffer.alloc(80);
bytes.writeUInt32LE(76, 0);
Buffer.from("0114020000000000c000000000000046", "hex").copy(bytes, 4);
bytes.writeUInt32LE(1, 60);
const report = await parseWindowsShortcut(
bytes,
{
path,
sha256: createHash("sha256").update(bytes).digest("hex"),
bytes: bytes.length,
},
header: {
location: { offset: 0, bytes: 76 },
header_size: 76,
clsid: "00021401-0000-0000-C000-000000000046",
link_flags: { raw: 0, decoded: [] },
file_attributes: { raw: 0, decoded: [] },
creation_time: { raw: "0", iso8601: null },
access_time: { raw: "0", iso8601: null },
write_time: { raw: "0", iso8601: null },
file_size: 0,
icon_index: 0,
show_command: { raw: 1, decoded: "normal" },
hotkey: {
raw: 0,
modifiers: [],
virtual_key: null,
virtual_key_name: null,
},
},
id_list: null,
link_info: null,
string_data: {
name: null,
relative_path: null,
working_dir: null,
arguments: null,
icon_location: null,
},
extra_data: { blocks: [], terminal_offset: null },
observations: [
{ kind: "arguments_present", evidence_kind: "derived", value: false },
{ kind: "argument_length", evidence_kind: "derived", characters: 0 },
{ kind: "target_extension", evidence_kind: "derived", extension: null },
{ kind: "show_command_minimized", evidence_kind: "derived", value: false },
{
kind: "icon_location_differs_from_target",
evidence_kind: "derived",
value: null,
},
],
coverage: {
status: "complete",
reason: null,
examined_id_list_items: 0,
examined_extra_data_blocks: 0,
failing_offset: null,
},
issues: [],
limitations: [
"Rows reproduce the selected Shell Link bytes; they do not resolve, open, or execute the link target.",
],
};
inspectWindowsShortcutInputSchema.parse({ path }),
);
it("rejects a relative path and extra arguments before reading", async () => {
let calls = 0;
@@ -7,7 +7,6 @@ import {
READER_RUNNER_SLACK_S,
readerTimeout,
} from "../../domain/desktop/appTaskReaders.js";
import { BUILD_REA_TIMEOUT_S } from "../../desktop/BuildComputerController.js";
import { BuildComputerLongRuns } from "../../desktop/BuildComputerLongRuns.js";
import { AnalysisError } from "../../domain/analysisErrorBase.js";
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
@@ -32,6 +31,7 @@ import { jsonValueSchema, type JsonValue } from "../../domain/jsonValue.js";
import { err, ok, type Result } from "../../domain/result.js";
import { AGENTENV_PROVIDER } from "../../desktop/AgentEnvComputerHelper.js";
import {
BUILD_REA_TIMEOUT_S,
fetchHomeFolder,
type ControlledComputer,
} from "../../desktop/BuildComputerController.js";
@@ -8,7 +8,7 @@ import {
} from "../../domain/electron/electronFuses.js";
import type { Evidence } from "../../domain/evidence.js";
import { createElectronFuseEvidence } from "../../domain/electron/electronFuseEvidence.js";
import { err, ok, type Result } from "../../domain/result.js";
import { err, type Result } from "../../domain/result.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
@@ -14,7 +14,6 @@ import {
TEMPLATE_RULES,
type TemplateRule,
} from "../../domain/analysisView/environmentManifestRules.js";
import type { Evidence } from "../../domain/evidence.js";
import type { EnvironmentBuildReport } from "../../domain/environmentBundleExport.js";
import { err, ok, type Result } from "../../domain/result.js";
import { inspectAnalysisView } from "../analysisView/AnalysisViewService.js";
@@ -1,3 +1,4 @@
import { escapeJsonPointerToken } from "../../domain/jsonPointer.js";
import { z } from "zod";
import { basename } from "node:path";
import { isIP } from "node:net";
@@ -83,9 +84,6 @@ const SCAN_BODY_LIMIT_BYTES = 4 * 1024 * 1024;
/** Pointers the report lists; the count covers every match. */
const REPORTED_POINTERS = 20;
const pointerToken = (name: string): string =>
name.replaceAll("~", "~0").replaceAll("/", "~1");
/** A held value: a non-empty string, or an array holding one. */
const holdsValue = (value: unknown): boolean =>
(typeof value === "string" && value !== "") ||
@@ -198,7 +196,7 @@ const credentialTally = () => {
const entries = Object.entries(value);
for (let index = entries.length - 1; index >= 0; index--) {
const [name, child] = entries[index]!;
const path = `${pointer}/${pointerToken(name)}`;
const path = `${pointer}/${escapeJsonPointerToken(name)}`;
field(`${label} ${path}`, name, child);
stack.push([child, path]);
}
@@ -209,7 +207,7 @@ const credentialTally = () => {
else if (scanned?.kind === "json") walk(scanned.value, label);
else if (scanned?.kind === "form")
for (const [name, value] of scanned.fields)
field(`${label} /${pointerToken(name)}`, name, value);
field(`${label} /${escapeJsonPointerToken(name)}`, name, value);
};
return {
field,
@@ -1,3 +1,4 @@
import { escapeJsonPointerToken } from "../../domain/jsonPointer.js";
import type { JsonObject } from "../../domain/analysisView/environmentStateRules.js";
import type { FieldRule } from "../../domain/analysisView/environmentStateWriteRules.js";
import { matchesClockFormat } from "../../domain/analysisView/environmentStateWriteClockFormats.js";
@@ -74,9 +75,6 @@ const shown = (value: JsonValue | undefined): string => {
return text.length > VALUE_CHARS ? `${text.slice(0, VALUE_CHARS)}…` : text;
};
const escapePointer = (key: string): string =>
key.replaceAll("~", "~0").replaceAll("/", "~1");
const formatKey = (format: ClockFormat): string =>
format.kind === "epoch"
? `epoch-${format.unit}`
@@ -427,7 +425,7 @@ class Differ {
? { agreed: new Set<string>(), fields: new Map<string, string>() }
: undefined;
for (const [key, value] of Object.entries(live)) {
const child = `${pointer}/${escapePointer(key)}`;
const child = `${pointer}/${escapeJsonPointerToken(key)}`;
const other = envKey(key);
// A translated key and a verbatim key may name the same entry; an
// environment member can account for only one live member.
@@ -460,7 +458,7 @@ class Differ {
? undefined
: {
...generation,
pointer: `${generation.pointer}/${escapePointer(key)}`,
pointer: `${generation.pointer}/${escapeJsonPointerToken(key)}`,
},
);
if (this.differences.length === before) composed?.agreed.add(key);
@@ -489,7 +487,7 @@ class Differ {
} else
this.add({
category: "value",
pointer: `${pointer}/${escapePointer(rule.field)}`,
pointer: `${pointer}/${escapeJsonPointerToken(rule.field)}`,
live: shown(live[rule.field]),
env: shown(env[other]),
});
@@ -499,7 +497,7 @@ class Differ {
if (!matched.has(key))
this.add({
category: "extra-field",
pointer: `${pointer}/${escapePointer(key)}`,
pointer: `${pointer}/${escapeJsonPointerToken(key)}`,
env: shown(value),
});
}
@@ -1,3 +1,4 @@
import { escapeJsonPointerToken } from "../../domain/jsonPointer.js";
import type { JsonObject } from "../../domain/analysisView/environmentStateRules.js";
import type { BodyAddress } from "../../domain/analysisView/environmentStateWriteRules.js";
import { isUuid } from "../../domain/analysisView/environmentStateWriteValues.js";
@@ -315,7 +316,7 @@ const idPair = (
: undefined;
const childPointer = (at: string, key: string | number): string =>
`${at}/${String(key).replaceAll("~", "~0").replaceAll("/", "~1")}`;
`${at}/${escapeJsonPointerToken(String(key))}`;
const learnAt = (
live: JsonValue | undefined,
@@ -1,3 +1,4 @@
import { jsonPointerTokens } from "../../domain/jsonPointer.js";
import {
createdFields,
documentBodyProblem,
@@ -53,7 +54,6 @@ import {
collectionMatch,
decodedSegment,
isObject,
pointerTokens,
scalarText,
type JsonObject,
type StateEntity,
@@ -435,7 +435,7 @@ export const writeAnswer = (
// A member the call sent and the plan stores as sent is answered as sent; any other keeps the recorded answer.
unloaded: (plan.observation?.read?.unloaded ?? []).filter(
(path) => {
const [field] = pointerTokens(path);
const [field] = jsonPointerTokens(path);
return !(
rendered.sent?.includes(field!) === true &&
plan.rules.some(
@@ -1,3 +1,4 @@
import { jsonPointerTokens } from "../../domain/jsonPointer.js";
import {
ownMember,
isObject,
@@ -5,7 +6,6 @@ import {
isSafeHttpMethod,
listItems,
locateList,
pointerTokens,
type JsonObject,
} from "../../domain/analysisView/environmentStateRules.js";
import { AnalysisResourceConstraintError } from "../../domain/analysisErrorCore.js";
@@ -170,7 +170,7 @@ const describeJson = (value: JsonValue): string => {
const pointer = locateList(value, undefined);
const items = pointer === undefined ? [] : (listItems(value, pointer) ?? []);
if (pointer !== undefined && items.length > 0) {
const [member] = pointerTokens(pointer);
const [member] = jsonPointerTokens(pointer);
return member === undefined
? `a JSON array of objects ${withFields(items)}`
: `a JSON object whose ${describeText(member, "member", "capture Evidence", JSON.stringify)} lists objects ${withFields(items)}`;
@@ -1,3 +1,4 @@
import { escapeJsonPointerToken } from "../../domain/jsonPointer.js";
import type { AnalysisSettingsReader } from "../../domain/analysisSettings.js";
import {
ASAR_HEADER_ADMISSION_SETTING,
@@ -145,7 +146,8 @@ export const analyzeJavaScriptApplicationValidated = async (
});
assertJavaScriptAnalysisActive(options.signal);
return ok(evidence);
} catch (cause: unknown) {
} catch (caught: unknown) {
let cause = caught;
if (
cause instanceof ArtifactReaderFailure &&
cause.reason === "cancelled" &&
@@ -246,13 +248,10 @@ const describeResultSchemaFailure = (cause: z.ZodError): string => {
const pointer =
path.length === 0
? "/"
: `/${path.map((part) => escapePointerPart(String(part))).join("/")}`;
: `/${path.map((part) => escapeJsonPointerToken(String(part))).join("/")}`;
return `Result schema rejected ${String(cause.issues.length)} ${cause.issues.length === 1 ? "issue" : "issues"} at ${pointer} (${issue.code})`;
};
const escapePointerPart = (part: string): string =>
part.replaceAll("~", "~0").replaceAll("/", "~1");
const FILESYSTEM_ERROR_CODES = new Set([
"ENOENT",
"EACCES",
@@ -214,8 +214,11 @@ export const reconstructJavaScriptArtifact = async (
).length,
{ scope },
);
let outcome;
let cleanupFailure: AnalysisError | undefined;
let outcome!: Awaited<
ReturnType<typeof analyzeAndAdoptJavaScriptArtifactFiles>
>;
let analysisFailure: { readonly cause: unknown } | undefined;
let cleanupAttempt: Awaited<ReturnType<ArtifactResourceScope["release"]>>;
try {
outcome = await analyzeAndAdoptJavaScriptArtifactFiles(
files,
@@ -250,18 +253,28 @@ export const reconstructJavaScriptArtifact = async (
preloads.paths,
preloads.limitations,
);
} catch (cause: unknown) {
analysisFailure = { cause };
} finally {
try {
const cleanup = await scope.release({
kind: "javascript-source-analysis",
analysis: worker,
resource: `JavaScript source analyzer for ${path}`,
});
if (cleanup.kind === "failed") throw cleanup.cause;
} catch (cause: unknown) {
if (!(cause instanceof AnalysisError)) throw cause;
cleanupFailure = cause;
}
cleanupAttempt = await scope.release({
kind: "javascript-source-analysis",
analysis: worker,
resource: `JavaScript source analyzer for ${path}`,
});
}
if (analysisFailure !== undefined) {
if (cleanupAttempt.kind === "failed")
throw scope.cleanupFailure(
{ kind: "failed", cause: analysisFailure.cause },
[cleanupAttempt],
);
throw analysisFailure.cause;
}
let cleanupFailure: AnalysisError | undefined;
if (cleanupAttempt.kind === "failed") {
if (!(cleanupAttempt.cause instanceof AnalysisError))
throw cleanupAttempt.cause;
cleanupFailure = cleanupAttempt.cause;
}
const analysis = outcome.analysis;
let interruption = outcome.interruption;
@@ -1,10 +1,7 @@
import { expect, it } from "vitest";
import { BrowserObservationError } from "../../domain/browserObservationError.js";
import {
AnalysisCancelledError,
AnalysisOutputError,
} from "../../domain/analysisErrorCore.js";
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
import { err, ok } from "../../domain/result.js";
import { parseEvidence } from "../../domain/evidence.js";
import type { JavaScriptRuntimeTargetList } from "../../domain/javascript/javascriptRuntimeObservation.js";
@@ -16,6 +16,7 @@ import { parseEvidence } from "../../domain/evidence.js";
import { ntfsJournalResultSchema } from "../../domain/windows/ntfsJournalQuery.js";
import { inspectNtfsJournalEvidence } from "./NtfsJournalService.js";
import { createDeferred } from "../../../tests/fixtures/binarySession.js";
import { awaitAcknowledgement } from "../../../tests/fixtures/lifecycleAcknowledgement.js";
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
const acquisitions = async () => {
@@ -170,7 +171,11 @@ it("keeps an active acquisition independent of a cancelled peer, bounds concurre
});
try {
const active = inspectNtfsJournalEvidence(fixture.input, resources);
await entered.promise;
await awaitAcknowledgement(
entered.promise,
active,
"provider lifecycle boundary",
);
const cancelled = new AbortController();
cancelled.abort("peer cancelled");
const peer = await inspectNtfsJournalEvidence(
+2 -1
View File
@@ -2,6 +2,7 @@ import { open, writeFile, type FileHandle } from "node:fs/promises";
import { join } from "node:path";
import { expect, it, vi } from "vitest";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
import { ArtifactFileSource } from "./ArtifactFileSource.js";
it("retries an admission cancelled after opening and closing the selected descriptor", async () => {
@@ -132,7 +133,7 @@ it("lets another caller continue when shared admission is cancelled", async () =
},
(cause: unknown) => cause,
);
await started;
await awaitAcknowledgement(started, first, "artifact source admission");
const second = source.read();
await Promise.resolve();
controller.abort(reason);
@@ -5,6 +5,17 @@ import { translateArtifactFailure } from "./ArtifactOperationFailure.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { artifactReaderLimitFailure } from "./ArtifactReaderLimits.js";
import { constants as bufferConstants } from "node:buffer";
import { imageFileSizeRemediation } from "./StableArtifactFailure.js";
// The provider ceiling test uses a sparse file and is skipped on Windows.
it.skipIf(process.platform !== "win32")(
"names the fixed image ceiling when the selected budget is already maximal",
() => {
expect(imageFileSizeRemediation(1024, 1024)).toContain(
"no runtime override",
);
},
);
it.each([
{ effective: 1, required: bufferConstants.MAX_STRING_LENGTH + 1 },
@@ -94,6 +105,18 @@ it.each(["capacity", "file-size", "memory"] as const)(
message: failure.message,
details: { boundary, detail: failure.message },
});
const wrapped = ArtifactReaderFailure.withCleanup(failure, {
reason: "close failed",
resources: ["handle"],
});
expect(
projectAnalysisError(
translateArtifactFailure("inventory_artifact", wrapped),
),
).toMatchObject({
code: "cleanup_incomplete",
details: { boundary, cleanup: "incomplete", resources: ["handle"] },
});
},
);
+27 -18
View File
@@ -1,6 +1,7 @@
import { open, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
import {
withArtifactFileHandle,
bindArtifactFileCleanup,
@@ -36,25 +37,33 @@ it("waits for admitted operations and shares concurrent close", async () => {
started.resolve();
await finish.promise;
});
await started.promise;
try {
await awaitAcknowledgement(started.promise, operation, "scope admission");
const close = scope.close();
expect(scope.close()).toBe(close);
await expect(scope.run(async () => undefined)).rejects.toMatchObject({
reason: "unavailable",
message: expect.stringContaining("closed"),
});
let closed = false;
void close.then(() => {
closed = true;
});
await Promise.resolve();
expect(closed).toBe(false);
const close = scope.close();
expect(scope.close()).toBe(close);
await expect(scope.run(async () => undefined)).rejects.toMatchObject({
reason: "unavailable",
message: expect.stringContaining("closed"),
});
let closed = false;
void close.then(() => {
closed = true;
});
await Promise.resolve();
expect(closed).toBe(false);
finish.resolve();
await operation;
await close;
expect(closed).toBe(true);
finish.resolve();
await operation;
await close;
expect(closed).toBe(true);
} finally {
finish.resolve();
try {
await operation;
} finally {
await scope.close();
}
}
});
it("stops a close hook even when retained cleanup is still failing", async () => {
@@ -89,7 +89,13 @@ it("fails closed on over-limit records", async () => {
scanCanonicalArtifactInventory(
await writeImage("runs.hex", `${runs.join("\n")}\n`),
),
).rejects.toMatchObject({ reason: "limit", boundary: "capacity" });
).rejects.toMatchObject({
reason: "limit",
boundary: "capacity",
message: expect.stringContaining(
"more than 65536 address-discontiguous record runs",
),
});
});
it("leaves an invalid first record to plain file classification", async () => {
+66 -54
View File
@@ -1,5 +1,6 @@
import { EventEmitter, once } from "node:events";
import { PassThrough } from "node:stream";
import { buffer } from "node:stream/consumers";
import { truncate, writeFile } from "node:fs/promises";
import { join } from "node:path";
@@ -14,6 +15,7 @@ import { ProviderProcessSupervisor } from "../process/ProviderProcess.js";
import type { NativeCommandRunner } from "../native/CommandRunner.js";
import { NativeCommandFailure } from "../native/CommandRunner.js";
import { createDeferred } from "../../tests/fixtures/binarySession.js";
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
import { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js";
import type { ArtifactEntry } from "./ArtifactReader.js";
@@ -157,56 +159,60 @@ describe("Mach-O command cleanup lifecycle", () => {
const reader = new MachOSliceArtifactReader(binary, {}, runner);
const iteration = reader.entries();
const firstEntry = iteration[Symbol.asyncIterator]().next();
await started;
const closing = reader.close();
await Promise.resolve();
expect(cleanupRetries).toBe(0);
finishCommand(
err(
new NativeCommandFailure(
"lipo",
"timeout",
null,
undefined,
{
stdout: "partial lipo output",
stderr: "partial lipo diagnostic",
stdoutBytes: 19,
stderrBytes: 23,
exitCode: null,
signal: null,
truncated: false,
},
"capture process group remains unverified",
["capture-group:9999"],
),
const commandResult = err(
new NativeCommandFailure(
"lipo",
"timeout",
null,
undefined,
{
stdout: "partial lipo output",
stderr: "partial lipo diagnostic",
stdoutBytes: 19,
stderrBytes: 23,
exitCode: null,
signal: null,
truncated: false,
},
"capture process group remains unverified",
["capture-group:9999"],
),
);
await expect(firstEntry).rejects.toMatchObject({
reason: "unavailable",
cause: expect.objectContaining({
cleanupFailure: "capture process group remains unverified",
}),
cleanup: {
reason: "capture process group remains unverified",
resources: ["capture-group:9999"],
},
});
await expect(closing).rejects.toMatchObject({
cleanup: {
reason:
"run:first, process-group:1234: first process group remains live; run:second, process-group:5678: second process group remains live",
resources: [
"run:first",
"process-group:1234",
"run:second",
"process-group:5678",
],
},
});
await reader.close();
expect(cleanupRetries).toBe(2);
try {
await awaitAcknowledgement(started, firstEntry, "lipo capture start");
const closing = reader.close();
await Promise.resolve();
expect(cleanupRetries).toBe(0);
finishCommand(commandResult);
await expect(firstEntry).rejects.toMatchObject({
reason: "unavailable",
cause: expect.objectContaining({
cleanupFailure: "capture process group remains unverified",
}),
cleanup: {
reason: "capture process group remains unverified",
resources: ["capture-group:9999"],
},
});
await expect(closing).rejects.toMatchObject({
cleanup: {
reason:
"run:first, process-group:1234: first process group remains live; run:second, process-group:5678: second process group remains live",
resources: [
"run:first",
"process-group:1234",
"run:second",
"process-group:5678",
],
},
});
await reader.close();
expect(cleanupRetries).toBe(2);
} finally {
finishCommand(commandResult);
await firstEntry.catch(() => undefined);
await reader.close().catch(() => undefined);
}
});
});
@@ -633,6 +639,7 @@ describe("Mach-O slice stream ownership", () => {
const release = createDeferred<void>();
const nativeRead = source.handle.read.bind(source.handle);
let reads: { mockRestore(): void } | undefined;
let consumed: Promise<Buffer> | undefined;
try {
const [entry] = await sliceEntries(reader);
reads = vi.spyOn(source.handle, "read").mockImplementation((async (
@@ -643,8 +650,9 @@ describe("Mach-O slice stream ownership", () => {
return Reflect.apply(nativeRead, source.handle, args);
}) as typeof source.handle.read);
const stream = await reader.open(entry!);
stream.read(0);
await reached.promise;
consumed = buffer(stream);
void consumed.catch(() => undefined);
await awaitAcknowledgement(reached.promise, consumed, "slice read");
let closed = false;
const closing = reader.close().then(() => {
closed = true;
@@ -656,9 +664,13 @@ describe("Mach-O slice stream ownership", () => {
expect(stream.closed).toBe(true);
} finally {
release.resolve();
await reader.close();
reads?.mockRestore();
await source.handle.close();
try {
await reader.close();
} finally {
await consumed?.catch(() => undefined);
reads?.mockRestore();
await source.handle.close();
}
}
});
@@ -729,7 +741,7 @@ it("joins Mach-O structural enumeration reads admitted before close", async () =
const traversal = reader.entries()[Symbol.asyncIterator]();
const next = traversal.next();
try {
await reached.promise;
await awaitAcknowledgement(reached.promise, next, "slice enumeration read");
let closed = false;
const closing = reader.close().then(() => {
closed = true;
+6 -3
View File
@@ -7,8 +7,11 @@ import { ArtifactFileSource } from "./ArtifactFileSource.js";
import { assertArtifactReadActive } from "./ArtifactFileOperation.js";
import { NtfsMftArtifactReader } from "./NtfsMftArtifactReader.js";
import { MFT_DEFAULT_RECORD_BYTES } from "../domain/windows/ntfsMft.js";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
// The constructed FILE records intentionally use the 1024-byte default.
const MFT_DEFAULT_RECORD_BYTES = 1024;
import { linkedMftFixture } from "../../tests/fixtures/ntfsMftExtension.js";
import { scanCanonicalArtifactInventory } from "../../tests/fixtures/artifactInventory.js";
@@ -212,7 +215,7 @@ it("retries a shared preparation cancelled by another traversal", async () => {
const cancelledResult = expect(cancelled).rejects.toMatchObject({
reason: "cancelled",
});
await started;
await awaitAcknowledgement(started, cancelled, "NTFS traversal read");
const other = reader.entries()[Symbol.asyncIterator]().next();
controller.abort("cancelled first traversal");
await cancelledResult;
@@ -319,7 +322,7 @@ it("does not finish a traversal when close begins during final source verificati
const rejected = expect(traversal).rejects.toMatchObject({
reason: "unavailable",
});
await started;
await awaitAcknowledgement(started, traversal, "NTFS close read");
const closing = reader.close();
resume();
await rejected;
+5 -3
View File
@@ -201,6 +201,8 @@ vi.mock("node:fs/promises", async () => {
const PLANTED_SOURCE = "release-exposure-planted-secret-7c2e9a";
const PLANTED_KEY = "AKIAPLANT9EXPOSUREXX";
const ignoreText = (): void => undefined;
// Keep the marker separate so Vite does not treat fixture text as this file's source map.
const INLINE_MAP_PREFIX = "# sourceMappingURL=data:application/json;base64,";
describe("ReleaseExposureReader review regressions", () => {
it("keeps inline CSS source-map coverage unknown in files and packages", async () => {
@@ -2067,7 +2069,7 @@ const plistObject = (type: number, length: number, body: Buffer): Buffer =>
describe("ReleaseExposureReader text encodings", () => {
const marker = "-----BEGIN PRIVATE KEY-----\n";
const inlineMap = `//# sourceMappingURL=data:application/json;base64,${Buffer.from(
const inlineMap = `//${INLINE_MAP_PREFIX}${Buffer.from(
JSON.stringify({
version: 3,
sources: ["src/app.ts"],
@@ -4328,7 +4330,7 @@ describe("ReleaseExposureReader script roles over a .map name", () => {
});
describe("ReleaseExposureReader HTML script references", () => {
const inlineMap = `//# sourceMappingURL=data:application/json;base64,${Buffer.from(
const inlineMap = `//${INLINE_MAP_PREFIX}${Buffer.from(
JSON.stringify({
version: 3,
sources: ["src/app.ts"],
@@ -4620,7 +4622,7 @@ describe("ReleaseExposureReader cumulative text allowance", () => {
describe("ReleaseExposureReader unfollowed map links", () => {
const SCRIPT = "const value = 1;\n//# sourceMappingURL=app.js.map\n";
const EMBEDDED_PAYLOAD = `const value = 1;\n//# sourceMappingURL=data:application/json;base64,${Buffer.from(
const EMBEDDED_PAYLOAD = `const value = 1;\n//${INLINE_MAP_PREFIX}${Buffer.from(
JSON.stringify({
version: 3,
sources: ["a.ts"],
+38
View File
@@ -5,6 +5,44 @@ import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import { ArtifactReaderFailure } from "./ArtifactReader.js";
import { bindArtifactFileCleanup } from "./ArtifactFileOperation.js";
import { completedStableArtifactObservation } from "./StableArtifactObservation.js";
import { isSelectedRegularFileError } from "../filesystem/RegularFile.js";
import {
AnalysisAccessDeniedError,
AnalysisInputError,
} from "../domain/analysisErrorCore.js";
/** Project filesystem errors that identify a refused or unreadable selected file. */
export const projectSelectedArtifactFileFailure = (
cause: unknown,
operation: string,
path: string,
description: string,
field = "path",
): AnalysisError | undefined => {
if (!(cause instanceof Error) || !("code" in cause)) return undefined;
if (cause.code === "EACCES" || cause.code === "EPERM")
return new AnalysisAccessDeniedError(operation, path, cause.code, {
cause,
});
if (isSelectedRegularFileError(cause))
return new AnalysisInputError(operation, { cause }, [
{
path: [field],
reason: "invalid_value",
message: `Selected ${description} could not be read (${String(cause.code)}): ${path}.`,
},
]);
return undefined;
};
/** Explain the selected image read budget and its fixed upper bound. */
export const imageFileSizeRemediation = (
selectedBytes: number,
maximumBytes: number,
): string =>
selectedBytes >= maximumBytes
? `The image exceeds the ${String(maximumBytes)}-byte input ceiling this operation accepts. This release has no runtime override. Select a smaller artifact.`
: `Raise max_file_bytes above ${String(selectedBytes)} bytes or select a smaller artifact; the limit is the caller-selected input read budget.`;
/** Name an observed regular-file retry that admission will validate again. */
export const regularFileSymlinkRecovery = (resolvedPath: string): string =>
@@ -103,9 +103,12 @@ it("preserves cancellation during payload CRC admission", async () => {
),
).toBe(true);
} finally {
await reader.close();
reads.mockRestore();
await admitted.handle.close();
try {
await reader.close();
} finally {
reads.mockRestore();
await admitted.handle.close();
}
}
});
@@ -149,9 +152,12 @@ it("admits a multi-image count before scanning the remaining payload", async ()
results.reduce((sum, result) => sum + result.bytesRead, 0),
).toBeLessThanOrEqual(UIMAGE_HEADER_SIZE + UIMAGE_MULTI_TABLE_WINDOW);
} finally {
await reader.close();
reads.mockRestore();
await admitted.handle.close();
try {
await reader.close();
} finally {
reads.mockRestore();
await admitted.handle.close();
}
}
});
@@ -7,6 +7,7 @@ import { setImmediate } from "node:timers/promises";
import { describe, expect, it, vi } from "vitest";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
import { createDeferred } from "../../tests/fixtures/binarySession.js";
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
import { buildUnityFs } from "../../tests/fixtures/unityfs.js";
import { openRegularFile } from "../filesystem/RegularFile.js";
import { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js";
@@ -185,9 +186,12 @@ describe("UnityFS allocation and malformed-input boundaries", () => {
});
expect(reads).toHaveBeenCalledTimes(1); // bounded header only
} finally {
await reader.close();
reads.mockRestore();
await admitted.handle.close();
try {
await reader.close();
} finally {
reads.mockRestore();
await admitted.handle.close();
}
}
},
);
@@ -223,6 +227,19 @@ describe("UnityFS allocation and malformed-input boundaries", () => {
size,
data: index === 0 ? Buffer.alloc(size, 0x61) : Buffer.alloc(0),
}));
const control = new UnityFsArtifactReader(
await fixture(
buildUnityFs(members.slice(0, 256), {
blockSize: size,
encodeBlock: repeatedBlock,
}),
),
);
try {
expect(await entries(control)).toHaveLength(256);
} finally {
await control.close();
}
const reader = new UnityFsArtifactReader(
await fixture(
buildUnityFs(members, {
@@ -232,7 +249,9 @@ describe("UnityFS allocation and malformed-input boundaries", () => {
),
);
try {
await expect(entries(reader)).rejects.toMatchObject({ reason: "limit" });
await expect(entries(reader)).rejects.toMatchObject({
reason: "limit",
});
} finally {
await reader.close();
}
@@ -354,7 +373,7 @@ describe("UnityFS stream ownership and immutable cache", () => {
let consumed: Promise<Buffer> | undefined;
try {
consumed = buffer(await reader.open(entry!, controller.signal));
await reached.promise;
await awaitAcknowledgement(reached.promise, consumed, "member read");
controller.abort(new Error("caller cancelled"));
release.resolve();
await expect(consumed).rejects.toBe(failure);
@@ -405,10 +424,12 @@ describe("UnityFS stream ownership and immutable cache", () => {
await release.promise;
return Reflect.apply(nativeRead, admitted.handle, args);
}) as typeof admitted.handle.read);
let consumed: Promise<Buffer> | undefined;
try {
const stream = await reader.open(entry!);
stream.read(0);
await reached.promise;
consumed = buffer(stream);
void consumed.catch(() => undefined);
await awaitAcknowledgement(reached.promise, consumed, "member read");
let closed = false;
const closing = reader.close().then(() => {
closed = true;
@@ -420,9 +441,13 @@ describe("UnityFS stream ownership and immutable cache", () => {
expect(stream.closed).toBe(true);
} finally {
release.resolve();
await reader.close();
reads.mockRestore();
await admitted.handle.close();
try {
await reader.close();
} finally {
await consumed?.catch(() => undefined);
reads.mockRestore();
await admitted.handle.close();
}
}
});
@@ -454,9 +479,12 @@ describe("UnityFS stream ownership and immutable cache", () => {
buffer(await reader.open(entry!, controller.signal)),
).rejects.toMatchObject({ reason: "cancelled", cause: reason });
} finally {
await reader.close();
reads.mockRestore();
await admitted.handle.close();
try {
await reader.close();
} finally {
reads.mockRestore();
await admitted.handle.close();
}
}
});
});
@@ -474,12 +502,19 @@ describe("UnityFS stream capacity and cache ownership", () => {
await fixture(buildUnityFs(members, { blockSize: 1 })),
);
try {
let completed = 0;
await expect(
(async () => {
for (const entry of await entries(reader))
for (const entry of await entries(reader)) {
await buffer(await reader.open(entry));
completed += 1;
}
})(),
).rejects.toMatchObject({ reason: "limit", boundary: "capacity" });
).rejects.toMatchObject({
reason: "limit",
boundary: "capacity",
});
expect(completed).toBe(128);
} finally {
await reader.close();
}
@@ -156,7 +156,10 @@ describe("wxapkg allocation and malformed-input boundaries", () => {
await fixture(buildWxapkg(members, { aliasOffsets: true })),
);
try {
await expect(entries(reader)).rejects.toMatchObject({ reason: "limit" });
await expect(entries(reader)).rejects.toMatchObject({
reason: "limit",
message: expect.stringContaining("wxapkg members declare"),
});
} finally {
await reader.close();
}
@@ -418,29 +421,25 @@ describe("wxapkg stream capacity and freshness", () => {
}
});
it("bounds repeated member I/O operations, including empty members", async () => {
it.each([
{ path: "x", data: "b" },
{ path: "empty.wxss", data: "" },
])("bounds repeated I/O operations for $path", async (member) => {
const reader = new WxapkgArtifactReader(
await fixture(
buildWxapkg([
{ path: "x", data: "b" },
{ path: "empty.wxss", data: "" },
]),
),
await fixture(buildWxapkg([member])),
);
try {
const [entry, emptyEntry] = await entries(reader);
const [entry] = await entries(reader);
await expect(
(async () => {
for (let index = 0; index < 5000; index++)
await buffer(await reader.open(entry!));
})(),
).rejects.toMatchObject({ reason: "limit", boundary: "capacity" });
await expect(
(async () => {
for (let index = 0; index < 5000; index++)
await buffer(await reader.open(emptyEntry!));
})(),
).rejects.toMatchObject({ reason: "limit", boundary: "capacity" });
).rejects.toMatchObject({
reason: "limit",
boundary: "capacity",
message: expect.stringContaining("wxapkg directory-and-chunk I/O work"),
});
} finally {
await reader.close();
}
+2 -1
View File
@@ -6,6 +6,7 @@ import {
} from "../../tests/fixtures/zipZstandard.js";
import { ArtifactReaderFailure, type ArtifactEntry } from "./ArtifactReader.js";
import { createDeferred } from "../../tests/fixtures/binarySession.js";
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
import { once } from "node:events";
import { randomBytes } from "node:crypto";
import { constants, zstdCompressSync } from "node:zlib";
@@ -836,7 +837,7 @@ it("joins an admitted ZIP enumeration before reader close completes", async () =
const traversal = reader.entries()[Symbol.asyncIterator]();
const next = traversal.next();
try {
await reached.promise;
await awaitAcknowledgement(reached.promise, next, "ZIP enumeration read");
let closed = false;
const closing = reader.close().then(() => {
closed = true;
@@ -9,7 +9,6 @@ import { AnalysisAccessDeniedError } from "../../domain/analysisErrorCore.js";
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
import { ArtifactReaderFailure } from "../ArtifactReader.js";
import { translateArtifactFailure } from "../ArtifactOperationFailure.js";
import { DirectoryArtifactEntryAccessDenied } from "../DirectoryArtifactReader.js";
import { inspectBundleKeyedArchive } from "./KeyedArchiveReader.js";
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
@@ -24,8 +24,6 @@ import {
versionMinCommand,
} from "./MachoImage.fixture.js";
import {
MAX_LAZY_INFO_DEPENDENCIES,
MAX_LAZY_INFO_SYMBOL_REFERENCES,
MAX_LOAD_COMMAND_BYTES,
readMachoImage,
} from "./MachoLoadCommandReader.js";
@@ -35,6 +33,10 @@ import { DirectoryArtifactReader } from "../DirectoryArtifactReader.js";
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
import { javaClassFile } from "../../../tests/fixtures/javaClassFile.js";
// Independently exercise the dependency and reference budgets.
const MAX_LAZY_INFO_DEPENDENCIES = 1024;
const MAX_LAZY_INFO_SYMBOL_REFERENCES = 64 * 1024;
const read = (bytes: Uint8Array) =>
readMachoImage(readerOf(bytes), bytes.length);
@@ -19,10 +19,7 @@ import type {
MachoImageFacts,
MachoSlice,
} from "../../domain/apple/dylibResolution.js";
import {
machoArchitectureName,
machoCpuSubtypeBase,
} from "../../domain/apple/machoCpuIdentity.js";
import { machoArchitectureName } from "../../domain/apple/machoCpuIdentity.js";
import {
legacyMachoPlatform,
type MachoVersionMinPlatform,
@@ -52,13 +49,13 @@ export const MAX_LOAD_COMMAND_BYTES = 16 * 1024 * 1024;
* byte cap alone would retain about a million dependency results. Stop before
* those results are kept or traced.
*/
export const MAX_LAZY_INFO_DEPENDENCIES = 1024;
const MAX_LAZY_INFO_DEPENDENCIES = 1024;
/**
* Symbol names are validated and then discarded. Distinct one-byte strings
* stay under the 16 MiB byte budgets while retaining one cache entry per
* offset, so stop before that map and those decodes grow.
*/
export const MAX_LAZY_INFO_SYMBOL_REFERENCES = 64 * 1024;
const MAX_LAZY_INFO_SYMBOL_REFERENCES = 64 * 1024;
const LC_SEGMENT = 0x1;
const LC_SEGMENT_64 = 0x19;
@@ -5,6 +5,7 @@ import { join } from "node:path";
import { buffer } from "node:stream/consumers";
import { expect, it, vi } from "vitest";
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
import { awaitAcknowledgement } from "../../../tests/fixtures/lifecycleAcknowledgement.js";
import * as artifactPaths from "../ArtifactPaths.js";
import { BunStandaloneArtifactReader } from "../BunStandaloneArtifactReader.js";
import { inventoryArtifact } from "../inventory/ArtifactInventory.js";
@@ -284,7 +285,7 @@ it("keeps another caller's in-flight inventory independent of cancelled preparat
},
(cause: unknown) => cause,
);
await started;
await awaitAcknowledgement(started, first, "Bun artifact read");
const second = collect(reader.entries());
controller.abort(reason);
resume();
+3 -7
View File
@@ -1,10 +1,6 @@
import { expect, it } from "vitest";
import {
MAX_DENO_METADATA_JSON_BYTES,
MAX_DENO_TABLE_COUNT,
parseDenoSection,
} from "./DenoSectionLayout.js";
import { parseDenoSection } from "./DenoSectionLayout.js";
import {
buildDenoSection,
defaultDenoMetadata,
@@ -130,7 +126,7 @@ it("treats a missing opening marker as an unsupported layout", async () => {
});
it("refuses metadata JSON above the named allowance before reading it as JSON", async () => {
const claimed = MAX_DENO_METADATA_JSON_BYTES + 1;
const claimed = 8 * 1024 * 1024 + 1;
const bytes = Buffer.alloc(DENO_FIXTURE_SECTION_MAGIC.length + 8 + claimed);
DENO_FIXTURE_SECTION_MAGIC.copy(bytes, 0);
bytes.writeBigUInt64LE(BigInt(claimed), DENO_FIXTURE_SECTION_MAGIC.length);
@@ -145,7 +141,7 @@ it("refuses a specifier table above the named record allowance", async () => {
parseDenoSectionBytes(
buildDenoSection({
specifiers: [],
specifierCount: MAX_DENO_TABLE_COUNT + 1,
specifierCount: 100_001,
}),
),
).rejects.toMatchObject({
+2 -2
View File
@@ -9,9 +9,9 @@ import { walkDenoVirtualFs, type DenoVfsWalk } from "./DenoVirtualFs.js";
/** Opening and closing marker of a Deno compile data section. */
export const DENO_SECTION_MAGIC = Buffer.from("d3n0l4nd", "latin1");
export const MAX_DENO_METADATA_JSON_BYTES = 8 * 1024 * 1024;
const MAX_DENO_METADATA_JSON_BYTES = 8 * 1024 * 1024;
export const MAX_DENO_VFS_HEADER_JSON_BYTES = 32 * 1024 * 1024;
export const MAX_DENO_TABLE_COUNT = 100_000;
const MAX_DENO_TABLE_COUNT = 100_000;
export const MAX_DENO_SPECIFIER_TEXT_BYTES = 8 * 1024 * 1024;
export const MAX_DENO_REMOTE_BODY_BYTES = 256 * 1024 * 1024;
@@ -5,6 +5,7 @@ import { open, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it, vi } from "vitest";
import { awaitAcknowledgement } from "../../../tests/fixtures/lifecycleAcknowledgement.js";
import { elfNote, elfNoteFixture } from "../../native/elf/ElfNotes.fixture.js";
import {
@@ -19,10 +20,7 @@ import { artifactFormatDescriptor } from "../artifactFormats.js";
import { ArtifactPathRegistry } from "../ArtifactPaths.js";
import { DenoStandaloneArtifactReader } from "../DenoStandaloneArtifactReader.js";
import { cArchiveExecutable } from "../pyinstaller/CArchive.fixture.js";
import {
locateDenoStandalone,
MAX_DENO_INTEL_TRAILER_SENTINELS,
} from "./DenoStandaloneLocation.js";
import { locateDenoStandalone } from "./DenoStandaloneLocation.js";
import {
buildDenoSection,
DENO_FIXTURE_SECTION_MAGIC,
@@ -34,9 +32,11 @@ import {
denoUniversal,
machOWithSegment,
} from "./DenoStandalone.fixture.js";
import { MAX_DENO_PATH_COMPONENT_BYTES } from "./DenoVirtualFs.js";
import { resolveArtifactReaderLimits } from "../../domain/artifactReaderLimits.js";
const MAX_DENO_PATH_COMPONENT_BYTES = 255;
it("keeps a confirmed Deno inventory when another pack recognizer is limited", async () => {
const original = denoElf();
// Add a valid empty section table without changing the existing PT_NOTE.
@@ -1071,7 +1071,7 @@ it("rejects packed Intel sentinels without rereading a chunk per sentinel", asyn
it("leaves an x64 Mach-O absent when sentinels pass the allowance", async () => {
const packed = Buffer.concat([
denoMachO([], { architecture: "x64" }),
rejectedIntelSentinels(MAX_DENO_INTEL_TRAILER_SENTINELS + 1),
rejectedIntelSentinels(65),
]);
let readBytes = 0;
const probe = await locateDenoStandalone(
@@ -1307,7 +1307,7 @@ it("accepts side-by-side Intel trailers with identical bytes", async () => {
it("refuses a confirmed Intel trailer when the allowance ends the outer search", async () => {
const bytes = Buffer.concat([
denoMachO([], { architecture: "x64" }),
rejectedIntelSentinels(MAX_DENO_INTEL_TRAILER_SENTINELS),
rejectedIntelSentinels(64),
intelTrailerRecord(buildDenoSection()),
]);
await expect(
@@ -1876,7 +1876,7 @@ it.each(["retry", "independent caller", "physical failure"] as const)(
(cause: unknown) => cause,
);
pending.push(failure);
await entered;
await awaitAcknowledgement(entered, first, "Deno artifact read");
const concurrent =
scenario === "independent caller"
? collect(reader.entries())
+1 -1
View File
@@ -64,7 +64,7 @@ const ELF_NOTE_TYPE = 0x53554901;
const PE_HEADER_PROBE_BYTES = 64 * 1024;
const INTEL_CHUNK_BYTES = 1024 * 1024;
/** Trailer candidates examined before the search stops. */
export const MAX_DENO_INTEL_TRAILER_SENTINELS = 64;
const MAX_DENO_INTEL_TRAILER_SENTINELS = 64;
const INTEL_SENTINEL = Buffer.concat([
Buffer.from("<~sui-data~>", "latin1"),
Buffer.from([0xef, 0xbe, 0xad, 0xde]),
+7 -5
View File
@@ -2,15 +2,17 @@ import { expect, it } from "vitest";
import { ArtifactReaderFailure } from "../ArtifactReader.js";
import {
MAX_DENO_LOGICAL_PATH_BYTES,
MAX_DENO_LOGICAL_PATH_PARTS,
MAX_DENO_PATH_COMPONENT_BYTES,
MAX_DENO_VFS_DEPTH,
MAX_DENO_VFS_ENTRIES,
MAX_DENO_VFS_NAME_UNITS,
walkDenoVirtualFs,
} from "./DenoVirtualFs.js";
// Independent format and admission examples.
const MAX_DENO_VFS_ENTRIES = 100_000;
const MAX_DENO_VFS_NAME_UNITS = 4_096;
const MAX_DENO_VFS_DEPTH = 512;
const MAX_DENO_PATH_COMPONENT_BYTES = 255;
const MAX_DENO_LOGICAL_PATH_BYTES = 512;
const file = (
name: string,
offset: number,
+6 -6
View File
@@ -4,32 +4,32 @@ import { ArtifactReaderFailure } from "../ArtifactReader.js";
import { normalizeArtifactPath } from "../ArtifactPaths.js";
/** Directory, file, and symlink records admitted from one virtual filesystem. */
export const MAX_DENO_VFS_ENTRIES = 100_000;
const MAX_DENO_VFS_ENTRIES = 100_000;
/** Logical path parts admitted before an indexed fallback. */
export const MAX_DENO_LOGICAL_PATH_PARTS = 256;
/**
* UTF-16 code units retained for one virtual name. Longer names are indexed
* and the original spelling is not copied into members.
*/
export const MAX_DENO_VFS_NAME_UNITS = 4_096;
const MAX_DENO_VFS_NAME_UNITS = 4_096;
/**
* Nested `walkEntries` calls. This is the recursion budget, separate from
* the logical-path part fallback.
*/
export const MAX_DENO_VFS_DEPTH = 512;
const MAX_DENO_VFS_DEPTH = 512;
/**
* UTF-16 code units of the source path built for each entry, directories
* included, plus retained symlink parts. Each path is rebuilt and normalized
* once, so this also bounds the synchronous walk.
*/
export const MAX_DENO_VFS_RETAINED_UNITS = 32 * 1024 * 1024;
const MAX_DENO_VFS_RETAINED_UNITS = 32 * 1024 * 1024;
/** POSIX NAME_MAX. A longer component makes extraction fail with ENAMETOOLONG. */
export const MAX_DENO_PATH_COMPONENT_BYTES = 255;
const MAX_DENO_PATH_COMPONENT_BYTES = 255;
/**
* Natural logical path bytes. Half of the 1,024-byte Darwin PATH_MAX, leaving
* the rest for the output root, so a long path does not abort extraction.
*/
export const MAX_DENO_LOGICAL_PATH_BYTES = 512;
const MAX_DENO_LOGICAL_PATH_BYTES = 512;
/** Logical path suffixes of the regions stored beside an original file. */
export const DENO_CHILD_SUFFIXES = {
transpiled: ".deno-transpiled",
@@ -106,10 +106,13 @@ it.each(cases)(
).toHaveLength(2);
expect(scope.hasPending).toBe(false);
} finally {
await scope.close();
observation.mockRestore();
close.mockRestore();
release.mockRestore();
try {
await scope.close();
} finally {
observation.mockRestore();
close.mockRestore();
release.mockRestore();
}
}
},
);
@@ -146,8 +149,11 @@ it.each(cases)(
await expect(stat(output)).rejects.toMatchObject({ code: "ENOENT" });
expect(scope.hasPending).toBe(false);
} finally {
await scope.close();
close.mockRestore();
try {
await scope.close();
} finally {
close.mockRestore();
}
}
},
);
@@ -295,8 +301,11 @@ it.each(cases)(
expect(closes).toBe(2);
expect(scope.hasPending).toBe(false);
} finally {
await scope.close();
close.mockRestore();
try {
await scope.close();
} finally {
close.mockRestore();
}
}
},
);
@@ -378,8 +387,11 @@ it.each(
}
expect(scope.hasPending).toBe(false);
} finally {
await scope.close();
close.mockRestore();
try {
await scope.close();
} finally {
close.mockRestore();
}
}
},
);
+65 -72
View File
@@ -12,13 +12,7 @@ import {
fitFixture,
type FitFixtureImage,
} from "./FitImage.fixture.js";
import {
FIT_MAX_CONFIGURATION_REFERENCES,
FIT_MAX_CONFIGURATIONS,
FIT_MAX_HASH_NODES_PER_IMAGE,
FIT_MAX_IMAGES,
readFitStructure,
} from "./FitStructure.js";
import { readFitStructure } from "./FitStructure.js";
const read = (file: Buffer) =>
readFitStructure(
@@ -487,75 +481,74 @@ describe("FIT configurations", () => {
describe("FIT fixed limits", () => {
it("fails more images than the fixed allowance", () => {
expect(
failureOf(
fitFixture({
images: Array.from({ length: FIT_MAX_IMAGES + 1 }, (_, index) => ({
name: `image-${String(index)}`,
data: Buffer.from([index & 0xff]),
})),
}),
),
).toMatchObject({ reason: "limit", boundary: "capacity" });
const make = (count: number) =>
fitFixture({
images: Array.from({ length: count }, (_, index) => ({
name: `image-${String(index)}`,
data: Buffer.from([index & 0xff]),
})),
});
expect(read(make(1024)).images).toHaveLength(1024);
expect(failureOf(make(1025))).toMatchObject({
reason: "limit",
boundary: "capacity",
});
});
it("fails more hash nodes than the fixed allowance", () => {
expect(
failureOf(
fitFixture({
images: [
{
...kernel(),
hashes: Array.from(
{ length: FIT_MAX_HASH_NODES_PER_IMAGE + 1 },
() => "crc32",
),
},
],
}),
),
).toMatchObject({ reason: "limit", boundary: "capacity" });
const make = (count: number) =>
fitFixture({
images: [
{
...kernel(),
hashes: Array.from({ length: count }, () => "crc32"),
},
],
});
expect(read(make(8)).images[0]?.hashes).toHaveLength(8);
expect(failureOf(make(9))).toMatchObject({
reason: "limit",
boundary: "capacity",
});
});
it("fails more configurations than the fixed allowance", () => {
expect(
failureOf(
fitFixture({
images: [kernel()],
configurations: {
name: "configurations",
children: Array.from(
{ length: FIT_MAX_CONFIGURATIONS + 1 },
(_, index) => ({ name: `conf-${String(index)}` }),
),
},
}),
),
).toMatchObject({ reason: "limit", boundary: "capacity" });
const make = (count: number) =>
fitFixture({
images: [kernel()],
configurations: {
name: "configurations",
children: Array.from({ length: count }, (_, index) => ({
name: `conf-${String(index)}`,
})),
},
});
expect(read(make(1024)).metadata.configurations).toHaveLength(1024);
expect(failureOf(make(1025))).toMatchObject({
reason: "limit",
boundary: "capacity",
});
});
it("bounds configuration references before decoding them", () => {
expect(
failureOf(
fitFixture({
images: [kernel()],
configurations: {
name: "configurations",
children: [
{
name: "conf-1",
properties: {
loadables: Buffer.alloc(FIT_MAX_CONFIGURATION_REFERENCES + 1),
},
},
],
},
}),
),
).toMatchObject({ reason: "limit", boundary: "capacity" });
const make = (count: number) =>
fitFixture({
images: [kernel()],
configurations: {
name: "configurations",
children: [
{ name: "conf-1", properties: { loadables: Buffer.alloc(count) } },
],
},
});
expect(read(make(16_384)).metadata.configurations).toHaveLength(1);
expect(failureOf(make(16_385))).toMatchObject({
reason: "limit",
boundary: "capacity",
});
});
it("maps a parser limit to a fixed allowance without a caller limit", () => {
it("does not attach caller-adjustable admission to a fixed parser limit", () => {
let nested: FixtureNode = { name: "leaf" };
for (let depth = 0; depth < 64; depth += 1)
nested = { name: `n${String(depth)}`, children: [nested] };
@@ -565,9 +558,12 @@ describe("FIT fixed limits", () => {
children: [{ name: "images" }, nested],
}),
);
expect(failure).toMatchObject({ reason: "limit", boundary: "capacity" });
// The parser's caller-limit recovery does not apply to fixed FIT reads.
expect(String((failure as Error).message)).not.toContain("caller-selected");
expect(failure).toMatchObject({
reason: "limit",
boundary: "capacity",
readerLimit: undefined,
admission: undefined,
});
});
it("fails a structure without /images", () => {
@@ -580,10 +576,7 @@ describe("FIT fixed limits", () => {
it("bounds dangling-reference diagnostics while preserving every declared reference", () => {
const name = "c".repeat(4096);
const references = Array.from(
{ length: FIT_MAX_CONFIGURATION_REFERENCES },
() => "missing",
);
const references = Array.from({ length: 16_384 }, () => "missing");
const file = fitFixture({
images: [{ name: "kernel", data: Buffer.from("K") }],
configurations: {
+4 -4
View File
@@ -18,11 +18,11 @@ import { BinaryFormatFailure } from "../../native/metadata/BinaryContainer.js";
import { ArtifactReaderFailure } from "../ArtifactReader.js";
// Fixed resource bounds; configurable ceilings are tracked in #6176.
export const FIT_MAX_IMAGES = 1024;
export const FIT_MAX_HASH_NODES_PER_IMAGE = 8;
export const FIT_MAX_CONFIGURATIONS = 1024;
const FIT_MAX_IMAGES = 1024;
const FIT_MAX_HASH_NODES_PER_IMAGE = 8;
const FIT_MAX_CONFIGURATIONS = 1024;
/** Bounds configuration references and their limitations across the FIT. */
export const FIT_MAX_CONFIGURATION_REFERENCES = 16_384;
const FIT_MAX_CONFIGURATION_REFERENCES = 16_384;
type FitNode = Devicetree["nodes"][number];
type FitProperty = FitNode["properties"][number];
-2
View File
@@ -3,12 +3,10 @@ import {
nesContainerFormat,
parseInesLayout,
} from "../../domain/inesContainer.js";
import type { GodotPckAdmission } from "../GodotPckLocation.js";
import {
resolveArtifactReaderLimits,
type ArtifactReaderLimits,
} from "../../domain/artifactReaderLimits.js";
import type { PyInstallerAdmission } from "../pyinstaller/PyInstallerAdmission.js";
import type {
ArtifactReaderAdmission,
ArtifactFormatAdmission,
+1 -3
View File
@@ -81,9 +81,7 @@ export const buildNodeSeaBlob = (
return Buffer.concat(parts);
};
export const nodeSeaFuseBytes = (
state: NodeSeaFuseFixture = "absent",
): Buffer => {
const nodeSeaFuseBytes = (state: NodeSeaFuseFixture = "absent"): Buffer => {
if (state === "absent") return Buffer.alloc(0);
const sentinel = (digit: string, hash = NODE_SEA_FUSE_HASH): Buffer =>
Buffer.from(`\0NODE_SEA_FUSE_${hash}:${digit}\0`, "latin1");
@@ -1,6 +1,7 @@
import type { FileHandle } from "node:fs/promises";
import { expect, it, vi } from "vitest";
import { withPyInstallerReader } from "../../../tests/fixtures/pyinstallerReader.js";
import { awaitAcknowledgement } from "../../../tests/fixtures/lifecycleAcknowledgement.js";
import { OwnedFileHandle } from "../../filesystem/OwnedFileHandle.js";
import type { ArtifactEntry } from "../ArtifactReader.js";
import { PyInstallerPyzStore } from "./PyInstallerPyzStore.js";
@@ -114,7 +115,11 @@ it.each([false, true])(
const opening = reader.open(selected, signal.signal);
void opening.catch(() => undefined);
try {
await started;
await awaitAcknowledgement(
started,
opening,
"PyInstaller image admission",
);
signal.abort(cancelled);
release();
await expect(opening).rejects.toMatchObject({
@@ -1,4 +1,5 @@
import { withPyInstallerReader } from "../../../tests/fixtures/pyinstallerReader.js";
import { awaitAcknowledgement } from "../../../tests/fixtures/lifecycleAcknowledgement.js";
import { dirname, join } from "node:path";
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
import { createHash, randomBytes } from "node:crypto";
@@ -392,7 +393,11 @@ it.each([false, true])(
void consumed.catch(() => undefined);
let closing: Promise<void> | undefined;
try {
await entered.promise;
await awaitAcknowledgement(
entered.promise,
consumed,
"PyInstaller operation",
);
controller.abort(caller);
let closed = false;
closing = reader.close().then(() => {
@@ -616,7 +621,11 @@ it("joins a late private-file close and retries its failed owner after cancellat
.next()
.catch((cause: unknown) => cause);
try {
await entered.promise;
await awaitAcknowledgement(
entered.promise,
listing,
"PyInstaller operation",
);
let settled = false;
const closing = reader.close().then(() => {
settled = true;
@@ -690,7 +699,11 @@ for (const cancel of [false, true]) {
(cause: unknown) => ({ ok: false as const, cause }),
);
try {
await entered.promise;
await awaitAcknowledgement(
entered.promise,
reading,
"PyInstaller operation",
);
expect(output.readableEnded).toBe(false);
expect(owner?.handle.fd).toBeGreaterThanOrEqual(0);
if (cancel) {
@@ -825,7 +838,11 @@ it.each([false, true])(
.catch((cause: unknown) => cause);
let closing: Promise<void> | undefined;
try {
await entered.promise;
await awaitAcknowledgement(
entered.promise,
first,
"PyInstaller operation",
);
await setImmediate();
expect(handles.size).toBe(1);
caller.abort(reason);
@@ -928,7 +945,11 @@ it.each(
);
let second: Promise<ArtifactEntry[]> | undefined;
try {
await entered.promise;
await awaitAcknowledgement(
entered.promise,
first,
"PyInstaller operation",
);
expect(reader.formatMetadata()).toBeUndefined();
if (peer) {
second = members(reader);
@@ -1031,7 +1052,11 @@ it("keeps a physical parse failure primary despite simultaneous caller cancellat
);
let second: Promise<unknown> | undefined;
try {
await entered.promise;
await awaitAcknowledgement(
entered.promise,
first,
"PyInstaller operation",
);
second = members(reader).catch((cause: unknown) => cause);
release.resolve();
const original = await first;
@@ -1042,8 +1067,7 @@ it("keeps a physical parse failure primary despite simultaneous caller cancellat
expect(reader.formatMetadata()).toBeUndefined();
} finally {
release.resolve();
await first;
await second;
await Promise.allSettled([first, ...(second ? [second] : [])]);
reads.mockRestore();
}
},
+2 -2
View File
@@ -6,7 +6,7 @@ import { open, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import { readPyzArchive, MAX_PYZ_TABLE_BYTES } from "./PyzArchive.js";
import { readPyzArchive } from "./PyzArchive.js";
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
import { PYTHON_39_MAGIC, pyzArchive } from "./CArchive.fixture.js";
@@ -82,7 +82,7 @@ it.each(["PYZ\0", "BAD\0"])(
await writeFile(path, header);
const handle = await open(path, "r+");
try {
const size = 12 + MAX_PYZ_TABLE_BYTES + 1;
const size = 12 + 16 * 1024 * 1024 + 1;
await handle.truncate(size);
expect(await readPyzArchive(handle, 0, size)).toMatchObject({
ok: false,
+1 -1
View File
@@ -44,7 +44,7 @@ export type PyzArchive =
};
/** Bound the transient marshal table allocation independently of image expansion. */
export const MAX_PYZ_TABLE_BYTES = 16 * 1024 * 1024;
const MAX_PYZ_TABLE_BYTES = 16 * 1024 * 1024;
interface PyzHeaderFacts {
readonly signatureMatches: boolean;
+3 -10
View File
@@ -1,3 +1,4 @@
import { jsonPointerTokens } from "../domain/jsonPointer.js";
import { visit, type JSONPath } from "jsonc-parser";
import { isExactJsonNumber } from "../domain/exactJsonNumber.js";
import { safeParseJson } from "../domain/safeJson.js";
@@ -22,14 +23,6 @@ export interface ResponseValue {
readonly value: string | number;
}
const pointerTokens = (pointer: string): readonly string[] =>
pointer === ""
? []
: pointer
.slice(1)
.split("/")
.map((token) => token.replaceAll("~1", "/").replaceAll("~0", "~"));
/** Keep unrelated fields usable while marking numbers that cannot be selected exactly. */
export const parseScenarioValueJson = (text: string) =>
safeParseJson(text, (_key, value, context) =>
@@ -44,7 +37,7 @@ export const responsePointerValue = (
pointer: string,
): unknown => {
let current = value;
for (const key of pointerTokens(pointer)) {
for (const key of jsonPointerTokens(pointer)) {
if (Array.isArray(current)) {
if (!/^(?:0|[1-9][0-9]*)(?![\s\S])/u.test(key)) return undefined;
current = current[Number(key)];
@@ -191,7 +184,7 @@ export const redactResponseSource = (
): Result<Buffer | undefined, ResponseValueIssue> => {
const markers = new Map(
values.map(({ entry, value }) => [
JSON.stringify(pointerTokens(entry.json_pointer)),
JSON.stringify(jsonPointerTokens(entry.json_pointer)),
redact(String(value)),
]),
);
-2
View File
@@ -2,11 +2,9 @@ import {
cleanupOwnedProcessGroup,
cleanupWindowsProcessTree,
verifyNoTokenOwnedProcesses,
type OwnedProcessGroup,
type PersistentOwnedProcessGroup,
type ProcessCleanupResult,
type ProcessLineageObservation,
type OwnershipSweepUnverifiedProcess,
type ProcessOwnershipHost,
} from "../process/ProcessOwnership.js";
import { systemProcessOwnershipHost } from "../process/ProcessOwnershipObservation.js";
+14 -4
View File
@@ -1,3 +1,4 @@
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
import { expect, it } from "vitest";
import { createWebPageInspection } from "../../tests/support/browser/webPageInspection.js";
@@ -233,10 +234,19 @@ it("carries the exact caller cancellation reason and completed prefix facts", as
input,
controller.signal,
);
await started.promise;
controller.abort(exactReason);
const result = await pending;
analyzer.releaseReservation();
let result: Awaited<typeof pending>;
try {
await awaitAcknowledgement(started.promise, pending, "script analysis");
controller.abort(exactReason);
result = await pending;
} finally {
controller.abort(exactReason);
try {
await pending;
} finally {
analyzer.releaseReservation();
}
}
if (result.ok) throw new Error("Expected cancellation");
expect(result.error._tag).toBe("AnalysisCancelledError");
@@ -5,6 +5,7 @@ import { afterEach, expect, it, vi } from "vitest";
import type { ElectronApplication } from "playwright-core";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { createDeferred } from "../../tests/fixtures/binarySession.js";
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
import {
electronActiveObservationInputSchema,
@@ -791,20 +792,33 @@ it.each(["acquisition", "identity"] as const)(
{ signal: controller.signal },
resources,
);
await started.promise;
controller.abort(new Error("stop during selected window read"));
await Promise.resolve();
finish.resolve();
const outcome = await running;
expect(outcome.actions[0]).toMatchObject({
status: "cancelled",
target: null,
});
expect(page.locator).not.toHaveBeenCalled();
expect(dispose).toHaveBeenCalledOnce();
expect(resources.hasPending).toBe(false);
await resources.close();
expect(dispose).toHaveBeenCalledOnce();
try {
await awaitAcknowledgement(
started.promise,
running,
"selected window read",
);
controller.abort(new Error("stop during selected window read"));
await Promise.resolve();
finish.resolve();
const outcome = await running;
expect(outcome.actions[0]).toMatchObject({
status: "cancelled",
target: null,
});
expect(page.locator).not.toHaveBeenCalled();
expect(dispose).toHaveBeenCalledOnce();
expect(resources.hasPending).toBe(false);
await resources.close();
expect(dispose).toHaveBeenCalledOnce();
} finally {
finish.resolve();
try {
await running;
} finally {
await resources.close();
}
}
},
);
+151 -75
View File
@@ -6,6 +6,7 @@ import { afterEach, expect, it, vi } from "vitest";
import { mkdir, writeFile, rm, access, symlink } from "node:fs/promises";
import { join } from "node:path";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
import type {
Browser,
BrowserContext,
@@ -242,34 +243,42 @@ it.each([false, true])(
(value) => ({ ok: true as const, value }),
(cause: unknown) => ({ ok: false as const, cause }),
);
await started.promise;
if (cancelled) controller.abort(reason);
launched.resolve({
const context = {
close,
browser: () => ({}) as Browser,
pages: () => [],
newPage,
} as unknown as BrowserContext);
const result = await opening;
if (result.ok) {
try {
expect(cancelled).toBe(false);
expect(newPage).toHaveBeenCalledOnce();
expect(close).not.toHaveBeenCalled();
expect(result.value.profilePath).toBe(profile);
} finally {
await result.value.cleanup.close();
} as unknown as BrowserContext;
try {
await awaitAcknowledgement(started.promise, opening, "browser launch");
if (cancelled) controller.abort(reason);
launched.resolve(context);
const result = await opening;
if (result.ok) {
try {
expect(cancelled).toBe(false);
expect(newPage).toHaveBeenCalledOnce();
expect(close).not.toHaveBeenCalled();
expect(result.value.profilePath).toBe(profile);
} finally {
await result.value.cleanup.close();
}
} else {
expect(cancelled).toBe(true);
expect(result.cause).toMatchObject({ reason: "cancelled" });
if (!(result.cause instanceof AnalysisError))
throw new Error("Expected typed cancellation");
expect(result.cause.cause).toBe(reason);
expect(newPage).not.toHaveBeenCalled();
}
} else {
expect(cancelled).toBe(true);
expect(result.cause).toMatchObject({ reason: "cancelled" });
if (!(result.cause instanceof AnalysisError))
throw new Error("Expected typed cancellation");
expect(result.cause.cause).toBe(reason);
expect(newPage).not.toHaveBeenCalled();
expect(close).toHaveBeenCalledOnce();
await expect(access(profile)).rejects.toMatchObject({ code: "ENOENT" });
} finally {
controller.abort(reason);
launched.resolve(context);
const outcome = await opening;
if (outcome.ok) await outcome.value.cleanup.close();
}
expect(close).toHaveBeenCalledOnce();
await expect(access(profile)).rejects.toMatchObject({ code: "ENOENT" });
},
);
@@ -428,16 +437,27 @@ it("does not configure a late attached connection after cancellation", async ()
signal: controller.signal,
},
);
await connectionStarted.promise;
controller.abort();
connection.resolve(connectedBrowser);
try {
await awaitAcknowledgement(
connectionStarted.promise,
opening,
"CDP connection",
);
controller.abort();
connection.resolve(connectedBrowser);
await expect(opening).rejects.toMatchObject({
_tag: "BrowserObservationError",
reason: "cancelled",
});
expect(connectedBrowser.contexts).not.toHaveBeenCalled();
expect(connectedBrowser.close).toHaveBeenCalledOnce();
await expect(opening).rejects.toMatchObject({
_tag: "BrowserObservationError",
reason: "cancelled",
});
expect(connectedBrowser.contexts).not.toHaveBeenCalled();
expect(connectedBrowser.close).toHaveBeenCalledOnce();
} finally {
controller.abort();
connection.resolve(connectedBrowser);
const opened = await opening.catch(() => undefined);
await opened?.cleanup.close();
}
});
it("closes an attached connection when cancellation starts target discovery", async () => {
@@ -651,10 +671,19 @@ it("checks a late stop after attached restoration and before transport disconnec
},
});
await restorationStarted.promise;
revision += 1;
finishRestoration.resolve(undefined);
await release;
try {
await awaitAcknowledgement(
restorationStarted.promise,
release,
"browser restoration",
);
revision += 1;
finishRestoration.resolve(undefined);
await release;
} finally {
finishRestoration.resolve(undefined);
await release.catch(() => undefined);
}
expect(order).toEqual([
"restore-started",
@@ -738,25 +767,36 @@ it("waits for a cancelled environment mutation before restoring it", async () =>
});
const controller = new AbortController();
const opening = fixture.open(controller.signal);
await localeStarted.promise;
const cancelled = expect(opening).rejects.toMatchObject({
_tag: "AnalysisCancelledError",
});
controller.abort();
await new Promise((resolve) => setImmediate(resolve));
expect(fixture.environmentSession.send).not.toHaveBeenCalledWith(
"Emulation.setLocaleOverride",
{ locale: "" },
);
try {
await awaitAcknowledgement(
localeStarted.promise,
opening,
"locale mutation",
);
const cancelled = expect(opening).rejects.toMatchObject({
_tag: "AnalysisCancelledError",
});
controller.abort();
await new Promise((resolve) => setImmediate(resolve));
expect(fixture.environmentSession.send).not.toHaveBeenCalledWith(
"Emulation.setLocaleOverride",
{ locale: "" },
);
localeResult.resolve(undefined);
await cancelled;
expect(fixture.environmentSession.send).toHaveBeenCalledWith(
"Emulation.setLocaleOverride",
{ locale: "" },
);
expect(fixture.environmentSession.detach).toHaveBeenCalledOnce();
expect(fixture.browser.close).toHaveBeenCalledOnce();
localeResult.resolve(undefined);
await cancelled;
expect(fixture.environmentSession.send).toHaveBeenCalledWith(
"Emulation.setLocaleOverride",
{ locale: "" },
);
expect(fixture.environmentSession.detach).toHaveBeenCalledOnce();
expect(fixture.browser.close).toHaveBeenCalledOnce();
} finally {
controller.abort();
localeResult.resolve(undefined);
const opened = await opening.catch(() => undefined);
await opened?.cleanup.close();
}
});
it("releases cleanup ownership when the attached target is destroyed", async () => {
@@ -1055,12 +1095,26 @@ it("rechecks the browser release gate after finalization changes its revision",
},
);
await eventsStarted.promise;
finishEvents.resolve(undefined);
await gateStarted.promise;
revision += 1;
finishGate.resolve(undefined);
await release;
try {
await awaitAcknowledgement(
eventsStarted.promise,
release,
"browser event finalization",
);
finishEvents.resolve(undefined);
await awaitAcknowledgement(
gateStarted.promise,
release,
"browser release gate",
);
revision += 1;
finishGate.resolve(undefined);
await release;
} finally {
finishEvents.resolve(undefined);
finishGate.resolve(undefined);
await release.catch(() => undefined);
}
expect(gateRuns).toEqual([0, 1]);
expect(closeBrowser).toHaveBeenCalledTimes(1);
@@ -1092,10 +1146,19 @@ it("runs the latest stop gate after attached restoration and before disconnect",
},
});
await restorationStarted.promise;
revision += 1;
finishRestoration.resolve(undefined);
await release;
try {
await awaitAcknowledgement(
restorationStarted.promise,
release,
"browser restoration",
);
revision += 1;
finishRestoration.resolve(undefined);
await release;
} finally {
finishRestoration.resolve(undefined);
await release.catch(() => undefined);
}
expect(gateRevisions).toEqual([1]);
expect(disconnected).toBe(true);
@@ -1143,19 +1206,32 @@ it.each([false, true])(
() => ({ ok: true as const }),
(cause: unknown) => ({ ok: false as const, cause }),
);
await gateStarted.promise;
if (changeRevision) revision += 1;
finishGate.resolve(undefined);
await expect(release).resolves.toEqual({ ok: false, cause: failure });
expect(closeBrowser).toHaveBeenCalledTimes(1);
expect(transportClosed).toBe(false);
expect(owner.browserClosed).toBe(false);
expect(owner.cleanupResources).toContain("browser_transport");
try {
await awaitAcknowledgement(
gateStarted.promise,
release,
"browser release gate",
);
if (changeRevision) revision += 1;
finishGate.resolve(undefined);
await expect(release).resolves.toEqual({ ok: false, cause: failure });
expect(closeBrowser).toHaveBeenCalledTimes(1);
expect(transportClosed).toBe(false);
expect(owner.browserClosed).toBe(false);
expect(owner.cleanupResources).toContain("browser_transport");
await owner.release(undefined, gate);
expect(closeBrowser).toHaveBeenCalledTimes(2);
expect(transportClosed).toBe(true);
expect(owner.browserClosed).toBe(true);
await owner.release(undefined, gate);
expect(closeBrowser).toHaveBeenCalledTimes(2);
expect(transportClosed).toBe(true);
expect(owner.browserClosed).toBe(true);
} finally {
finishGate.resolve(undefined);
try {
await release;
} finally {
await owner.close();
}
}
},
);
@@ -1076,6 +1076,12 @@ describe("saved response JSON pointers", () => {
vi.unstubAllGlobals();
});
it("selects the root and decodes escaped response keys once", () => {
const value = { "a/b": { "~1": "literal", "/": "slash" } };
expect(responsePointerValue(value, "")).toBe(value);
expect(responsePointerValue(value, "/a~1b/~01")).toBe("literal");
});
it("does not coerce a terminal newline in an array index", () => {
expect(responsePointerValue({ items: ["record-7"] }, "/items/0\n")).toBe(
undefined,
@@ -389,14 +389,10 @@ export class PlaywrightScenarioServiceWorkers {
// Abort discovery/handshakes and reads, never an already-sent page mutation.
this.#setup.abort();
const restoration = (this.#restoration ??= this.restore());
void restoration.then(
() => {
if (this.#restoration === restoration) this.#restoration = undefined;
},
() => {
if (this.#restoration === restoration) this.#restoration = undefined;
},
);
const clearRestoration = (): void => {
if (this.#restoration === restoration) this.#restoration = undefined;
};
void restoration.then(clearRestoration, clearRestoration);
// Restoration completion does not discharge a stop request that arrived
// after that restoration began. Both paths share the owner's stop latch.
const completion = restoration.then(() => this.fulfillStopRequest());
@@ -427,14 +423,10 @@ export class PlaywrightScenarioServiceWorkers {
this.#loadingStopped = true;
})();
this.#stopPromise = stopping;
void stopping.then(
() => {
if (this.#stopPromise === stopping) this.#stopPromise = undefined;
},
() => {
if (this.#stopPromise === stopping) this.#stopPromise = undefined;
},
);
const clearStop = (): void => {
if (this.#stopPromise === stopping) this.#stopPromise = undefined;
};
void stopping.then(clearStop, clearStop);
return stopping;
}
+114 -73
View File
@@ -1,3 +1,4 @@
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
import {
AnalysisCleanupLeaseGroup,
AnalysisCleanupLease,
@@ -550,24 +551,34 @@ it("transfers cancellation to a no-stop cleanup already in progress", async () =
});
const firstClose = session.close(false);
await started;
cancellation.abort();
const retry = session.close();
finishRestoration();
await expect(Promise.all([firstClose, retry])).resolves.toEqual([
"disconnected-external",
"disconnected-external",
]);
try {
await awaitAcknowledgement(started, firstClose, "policy restoration");
cancellation.abort();
const retry = session.close();
finishRestoration();
await expect(Promise.all([firstClose, retry])).resolves.toEqual([
"disconnected-external",
"disconnected-external",
]);
const policyStops = mocks.policySend.mock.calls.filter(
([method]) => method === "Page.stopLoading",
).length;
const frameStops = page.evaluate.mock.calls.filter(
([script]) => script === "globalThis.stop()",
).length;
expect(policyStops + frameStops).toBe(1);
expect(cleanup.close).toHaveBeenCalledTimes(1);
expect(browserWasOpenAtStop).toBe(true);
const policyStops = mocks.policySend.mock.calls.filter(
([method]) => method === "Page.stopLoading",
).length;
const frameStops = page.evaluate.mock.calls.filter(
([script]) => script === "globalThis.stop()",
).length;
expect(policyStops + frameStops).toBe(1);
expect(cleanup.close).toHaveBeenCalledTimes(1);
expect(browserWasOpenAtStop).toBe(true);
} finally {
cancellation.abort();
finishRestoration();
try {
await firstClose;
} finally {
await session.close();
}
}
});
it("does not repeat a stop confirmed by storage during a late retry", async () => {
@@ -591,27 +602,37 @@ it("does not repeat a stop confirmed by storage during a late retry", async () =
});
const firstClose = session.close(false);
await started;
cancellation.abort();
const retry = session.close();
finishRestoration();
await expect(Promise.all([firstClose, retry])).resolves.toEqual([
"disconnected-external",
"disconnected-external",
]);
try {
await awaitAcknowledgement(started, firstClose, "policy restoration");
cancellation.abort();
const retry = session.close();
finishRestoration();
await expect(Promise.all([firstClose, retry])).resolves.toEqual([
"disconnected-external",
"disconnected-external",
]);
expect(storage?.close).toHaveBeenCalledWith(false);
expect(
mocks.policySend.mock.calls.filter(
([method]) => method === "Page.stopLoading",
),
).toHaveLength(0);
expect(
page.evaluate.mock.calls.filter(
([script]) => script === "globalThis.stop()",
),
).toHaveLength(0);
expect(cleanup.close).toHaveBeenCalledTimes(1);
expect(storage?.close).toHaveBeenCalledWith(false);
expect(
mocks.policySend.mock.calls.filter(
([method]) => method === "Page.stopLoading",
),
).toHaveLength(0);
expect(
page.evaluate.mock.calls.filter(
([script]) => script === "globalThis.stop()",
),
).toHaveLength(0);
expect(cleanup.close).toHaveBeenCalledTimes(1);
} finally {
cancellation.abort();
finishRestoration();
try {
await firstClose;
} finally {
await session.close();
}
}
});
it.each(["block", "allow"] as const)(
@@ -938,18 +959,19 @@ it("does not await policy initialization after its child target detaches", async
},
sessionId: "policy-session",
});
await started;
const closing = session.close();
mocks.policyEvent?.({
method: "Target.detachedFromTarget",
params: { sessionId: "child-session" },
});
try {
await awaitAcknowledgement(started, held, "child policy admission");
const closing = session.close();
mocks.policyEvent?.({
method: "Target.detachedFromTarget",
params: { sessionId: "child-session" },
});
await expect(closing).resolves.toBe("disconnected-external");
expect(cleanup.close).toHaveBeenCalledTimes(1);
} finally {
releaseChild();
await session.close();
}
expect(cleanup.close).toHaveBeenCalledTimes(1);
});
it("skips policy restoration for a child without a committed document", async () => {
@@ -1222,7 +1244,11 @@ it.each([false, true])(
signal: controller.signal,
});
try {
await started.promise;
await awaitAcknowledgement(
started.promise,
result,
"cookie initialization",
);
controller.abort(reason);
await new Promise<void>((resolve) => setImmediate(resolve));
expect(cleanup.close).not.toHaveBeenCalled();
@@ -1396,14 +1422,21 @@ it("cancels a stalled policy preflight without allowing late setup", async () =>
const outcome = expect(opening).rejects.toMatchObject({
_tag: "AnalysisCancelledError",
});
await started;
cancellation.abort();
await outcome;
expect(cleanup.close).toHaveBeenCalledTimes(1);
expect(cleanup.close.mock.calls[0]).toHaveLength(1);
release();
await new Promise<void>((resolve) => setImmediate(resolve));
expect(mocks.policySend).not.toHaveBeenCalled();
try {
await awaitAcknowledgement(started, opening, "policy preflight");
cancellation.abort();
await outcome;
expect(cleanup.close).toHaveBeenCalledTimes(1);
expect(cleanup.close.mock.calls[0]).toHaveLength(1);
release();
await new Promise<void>((resolve) => setImmediate(resolve));
expect(mocks.policySend).not.toHaveBeenCalled();
} finally {
cancellation.abort();
release();
await opening.catch(() => undefined);
await outcome.catch(() => undefined);
}
});
it("waits for a pending registration installation before cancelled cleanup", async () => {
@@ -1427,25 +1460,32 @@ it("waits for a pending registration installation before cancelled cleanup", asy
const outcome = expect(opening).rejects.toMatchObject({
_tag: "AnalysisCancelledError",
});
await started;
cancellation.abort();
await new Promise<void>((resolve) => setImmediate(resolve));
expect(cleanup.close).not.toHaveBeenCalled();
expect(page.evaluate).toHaveBeenCalledTimes(1);
releaseInstallation();
await outcome;
const restoration = mocks.policySend.mock.calls.find(
([method]) => method === "Runtime.evaluate",
)?.[1];
expect(restoration).toMatchObject({
expression: expect.stringContaining("policy_owned"),
});
expect(mocks.policySend).toHaveBeenCalledWith(
"Page.stopLoading",
{},
"policy-session",
);
expect(cleanup.close).toHaveBeenCalledTimes(1);
try {
await awaitAcknowledgement(started, opening, "policy installation");
cancellation.abort();
await new Promise<void>((resolve) => setImmediate(resolve));
expect(cleanup.close).not.toHaveBeenCalled();
expect(page.evaluate).toHaveBeenCalledTimes(1);
releaseInstallation();
await outcome;
const restoration = mocks.policySend.mock.calls.find(
([method]) => method === "Runtime.evaluate",
)?.[1];
expect(restoration).toMatchObject({
expression: expect.stringContaining("policy_owned"),
});
expect(mocks.policySend).toHaveBeenCalledWith(
"Page.stopLoading",
{},
"policy-session",
);
expect(cleanup.close).toHaveBeenCalledTimes(1);
} finally {
cancellation.abort();
releaseInstallation();
await opening.catch(() => undefined);
await outcome.catch(() => undefined);
}
});
it("rejects attached block when caller-owned registrations already exist", async () => {
@@ -1578,7 +1618,7 @@ it("retains late opening failure cleanup after cancellation until the exact prof
const pending = provider.captureScenario(scenario, {
signal: caller.signal,
});
await started;
await awaitAcknowledgement(started, pending, "browser launch start");
caller.abort(reason);
const result = await pending;
if (result.ok) throw new Error("Expected cancelled opening");
@@ -1600,6 +1640,7 @@ it("retains late opening failure cleanup after cancellation until the exact prof
});
expect(failure.cleanupIncomplete).toBe(false);
} finally {
caller.abort(reason);
denied = false;
finishOpening();
await provider.close();
@@ -120,11 +120,14 @@ it.for([
}
}
} finally {
await worker.close();
preparation.mockRestore();
child.stdin.destroy();
child.stdout.destroy();
child.stderr.destroy();
try {
await worker.close();
} finally {
preparation.mockRestore();
child.stdin.destroy();
child.stdout.destroy();
child.stderr.destroy();
}
}
},
);
+44 -77
View File
@@ -601,84 +601,51 @@ it("does not mistake an independent body AbortError for a falsy caller cancellat
).toBe(true);
});
it("keeps an actual stalled HTTP header deadline local to its map", async () => {
const server = createServer(() => {});
server.listen(0, "127.0.0.1");
await once(server, "listening");
const address = server.address();
if (address === null || typeof address === "string")
throw new Error("Expected TCP address");
const url = `http://127.0.0.1:${address.port}/map`;
try {
const result = await fetchWebSourceMaps(
[{ ...request, declaredUrl: url, fetchUrl: url }],
{
...input,
allowed_origins: [new URL(url).origin],
source_map_fetch_timeout_ms: 1000,
},
undefined,
{ fetch },
);
expect(result).toMatchObject({
ok: true,
value: {
items: [
{
status: "fetch_failed",
limitation: expect.stringContaining("deadline"),
},
],
},
it.each(["headers", "body"] as const)(
"keeps an actual stalled HTTP %s deadline local to its map",
async (stage) => {
const server = createServer((_request, response) => {
if (stage === "body") {
response.writeHead(200, { "content-type": "application/json" });
response.write('{"version":3,');
}
});
} finally {
server.closeAllConnections();
await new Promise<void>((resolve, reject) =>
server.close((error) => (error ? reject(error) : resolve())),
);
}
});
it("keeps an actual stalled HTTP response deadline local to its map", async () => {
const server = createServer((_request, response) => {
response.writeHead(200, { "content-type": "application/json" });
response.write('{"version":3,');
});
server.listen(0, "127.0.0.1");
await once(server, "listening");
const address = server.address();
if (address === null || typeof address === "string")
throw new Error("Expected TCP address");
const url = `http://127.0.0.1:${address.port}/map`;
try {
const result = await fetchWebSourceMaps(
[{ ...request, declaredUrl: url, fetchUrl: url }],
{
...input,
allowed_origins: [new URL(url).origin],
source_map_fetch_timeout_ms: 1000,
},
undefined,
{ fetch },
);
expect(result).toMatchObject({
ok: true,
value: {
items: [
{
status: "fetch_failed",
limitation: expect.stringContaining("deadline"),
},
],
},
});
} finally {
server.closeAllConnections();
await new Promise<void>((resolve, reject) =>
server.close((error) => (error ? reject(error) : resolve())),
);
}
});
server.listen(0, "127.0.0.1");
await once(server, "listening");
const address = server.address();
if (address === null || typeof address === "string")
throw new Error("Expected TCP address");
const url = `http://127.0.0.1:${address.port}/map`;
try {
const result = await fetchWebSourceMaps(
[{ ...request, declaredUrl: url, fetchUrl: url }],
{
...input,
allowed_origins: [new URL(url).origin],
source_map_fetch_timeout_ms: 1000,
},
undefined,
{ fetch },
);
expect(result).toMatchObject({
ok: true,
value: {
items: [
{
status: "fetch_failed",
limitation: expect.stringContaining("deadline"),
},
],
},
});
} finally {
server.closeAllConnections();
await new Promise<void>((resolve, reject) =>
server.close((error) => (error ? reject(error) : resolve())),
);
}
},
);
it.each([302, 503])(
"retains HTTP %s response cleanup without hanging on an independent tee branch",
+4 -2
View File
@@ -706,7 +706,8 @@ const fetchOne = async (
budget: budget.decodedRecords,
},
);
} catch (caught: unknown) {
} catch (failure: unknown) {
let caught = failure;
if (caught instanceof SourceMapPendingFetch) {
if (caught.cause instanceof SourceMapDeadlineError) {
// Native fetch reports abort completion asynchronously. Check the next
@@ -870,7 +871,8 @@ const readBoundedText = async (
if (value !== undefined) chunks.push(value);
bytes += nextBytes;
}
} catch (cause: unknown) {
} catch (caught: unknown) {
let cause = caught;
// Fetch can error its body with the abort reason before read cancellation.
// A confirmed errored stream is already terminal and needs only its lock released.
await Promise.resolve();
@@ -2,7 +2,6 @@ import { expect, it } from "vitest";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import {
captureDecoderHeapContinuingRecovery,
captureDecoderHeapRecovery,
DEFAULT_CAPTURE_DECODER_HEAP,
} from "../../config/captureDecoderHeap.js";
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
+5 -7
View File
@@ -1,3 +1,4 @@
import { javaScriptWorkerMemoryLimitBytes } from "../../process/JavaScriptWorkerMemoryLimit.js";
import { captureBudgets } from "./CaptureBudgets.js";
import { analysisSettingLimit } from "../../domain/analysisSettingDefinitions.js";
import type { ExecutionOptions } from "../../application/AnalysisProvider.js";
@@ -36,11 +37,8 @@ import {
type CaptureDecoderHeap,
} from "../../config/captureDecoderHeap.js";
import { OwnedCommandFailure } from "../../process/OwnedCommand.js";
import {
NODE_CAPTURE_HEAP_ENFORCED,
NODE_CAPTURE_HEAP_LIMITS,
captureDecoderProcessMemoryLimitBytes,
} from "./CaptureRelease.js";
import { NODE_CAPTURE_HEAP_ENFORCED } from "./CaptureRelease.js";
import { CAPTURE_DECODER_SEMI_SPACE_MIB } from "../../domain/analysisResourceLimits.js";
const OPERATION = "inspect_web_network_capture";
/** Reason for a decoder whose owned process tree reached its Job Object commit limit. */
@@ -296,7 +294,7 @@ export const captureMemoryFailure = (
cause?: unknown,
heap: CaptureDecoderHeap = DEFAULT_CAPTURE_DECODER_HEAP,
): AnalysisResourceConstraintError => {
const processMemoryLimitBytes = captureDecoderProcessMemoryLimitBytes(
const processMemoryLimitBytes = javaScriptWorkerMemoryLimitBytes(
heap.oldGenerationMiB,
);
const recovery = captureDecoderHeapRecovery(heap);
@@ -315,7 +313,7 @@ export const captureMemoryFailure = (
? {
old_generation_heap_mib: heap.oldGenerationMiB,
old_generation_heap_source: heap.source,
semi_space_heap_mib: NODE_CAPTURE_HEAP_LIMITS.semiSpaceMiB,
semi_space_heap_mib: CAPTURE_DECODER_SEMI_SPACE_MIB,
}
: {}),
...(input.format !== "mitmproxy" && processMemoryLimitBytes !== null
+2 -1
View File
@@ -1,3 +1,4 @@
import { escapeJsonPointerToken } from "../../domain/jsonPointer.js";
import { captureBudgets } from "./CaptureBudgets.js";
import { visit } from "jsonc-parser";
import { LosslessNumber } from "lossless-json";
@@ -58,7 +59,7 @@ export const parseCaptureJson = (
return `${parent.pointer}/${parent.items.length}`;
return parent.pendingKey === undefined
? parent.pointer
: `${parent.pointer}/${parent.pendingKey.replaceAll("~", "~0").replaceAll("/", "~1")}`;
: `${parent.pointer}/${escapeJsonPointerToken(parent.pendingKey)}`;
};
// The document root is container level 1. Admit a container only while
// its own level stays within the budget; values inside an admitted
+2 -1
View File
@@ -1,3 +1,4 @@
import { escapeJsonPointerToken } from "../../domain/jsonPointer.js";
import { isSafeNumber, type LosslessNumber } from "lossless-json";
import type { WebNetworkCaptureRecord } from "../../domain/webNetworkCapture.js";
import { parseRecordedTransportUrl } from "../../domain/recordedTransportUrl.js";
@@ -149,7 +150,7 @@ export class CaptureRedaction {
/** Escape a producer property for an RFC 6901 location. */
export const capturePointer = (parent: string, key: string): string =>
`${parent}/${key.replaceAll("~", "~0").replaceAll("/", "~1")}`;
`${parent}/${escapeJsonPointerToken(key)}`;
/** Structural record guard does not duck-type number/byte sidecar markers. */
export const captureObject = (
-22
View File
@@ -1,27 +1,5 @@
import {
CAPTURE_DECODER_SEMI_SPACE_MIB,
DEFAULT_CAPTURE_DECODER_OLD_GENERATION_MIB,
} from "../../domain/analysisResourceLimits.js";
import { javaScriptRuntime } from "../../domain/runtimeVersion.js";
import { javaScriptWorkerMemoryLimitBytes } from "../../process/JavaScriptWorkerMemoryLimit.js";
/**
* Default V8 heap settings for the owned Node capture decoder process.
* The old generation is `REA_CAPTURE_DECODER_OLD_GENERATION_MIB`; the
* semi-space stays fixed.
*/
export const NODE_CAPTURE_HEAP_LIMITS = {
oldGenerationMiB: DEFAULT_CAPTURE_DECODER_OLD_GENERATION_MIB,
semiSpaceMiB: CAPTURE_DECODER_SEMI_SPACE_MIB,
} as const;
/** Windows Job Object commit limit for one resolved old-generation budget. */
export const captureDecoderProcessMemoryLimitBytes = (
oldGenerationMiB: number,
): number | null => javaScriptWorkerMemoryLimitBytes(oldGenerationMiB);
/** The Node capture decoder runs on this executable; Bun accepts but ignores V8 heap flags. */
export const NODE_CAPTURE_HEAP_ENFORCED =
javaScriptRuntime(process.versions).id === "node";
/** Fixed offline interpretation profile, not an authenticated producer claim. */
@@ -1,3 +1,4 @@
import { javaScriptWorkerMemoryLimitBytes } from "../../process/JavaScriptWorkerMemoryLimit.js";
import { captureBudgets, withCaptureBudgets } from "./CaptureBudgets.js";
import type { FileSnapshotPartialObservation } from "../../domain/fileSnapshotObservation.js";
import {
@@ -16,10 +17,7 @@ import {
resolveCaptureDecoderHeap,
type CaptureDecoderHeap,
} from "../../config/captureDecoderHeap.js";
import {
NODE_CAPTURE_HEAP_ENFORCED,
captureDecoderProcessMemoryLimitBytes,
} from "./CaptureRelease.js";
import { NODE_CAPTURE_HEAP_ENFORCED } from "./CaptureRelease.js";
import type { HistoricalCaptureFormatAdapter } from "./HistoricalCaptureFormatAdapter.js";
import { randomUUID } from "node:crypto";
import { writeFile } from "node:fs/promises";
@@ -613,9 +611,7 @@ const nodeMemoryLimitation = (heap: CaptureDecoderHeap): string => {
const recovery = captureDecoderHeapRecovery(heap);
if (NODE_CAPTURE_HEAP_ENFORCED)
return `HAR/SAZ/qlog use a ${String(heap.oldGenerationMiB)} MiB Node old-generation heap (source ${heap.source}). ${recovery}`;
const commitLimit = captureDecoderProcessMemoryLimitBytes(
heap.oldGenerationMiB,
);
const commitLimit = javaScriptWorkerMemoryLimitBytes(heap.oldGenerationMiB);
return commitLimit === null
? `HAR/SAZ/qlog run on Bun, which does not enforce REA's V8 heap flags. ${recovery}`
: `HAR/SAZ/qlog run on Bun, which does not enforce REA's V8 heap flags, under a ${String(commitLimit)}-byte Windows Job Object commit limit. ${recovery}`;
+14 -11
View File
@@ -1,4 +1,4 @@
import { expect, it, vi } from "vitest";
import { expect, it } from "vitest";
import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js";
import { NodeCaptureAdapter } from "./NodeCaptureAdapter.js";
@@ -9,17 +9,20 @@ it.each(["har", "qlog"] as const)(
},
);
vi.mock("./CaptureRelease.js", async (original) => ({
...(await original<typeof import("./CaptureRelease.js")>()),
NODE_CAPTURE_HEAP_ENFORCED: false,
captureDecoderProcessMemoryLimitBytes: () => null,
}));
it.each(["saz", "qlog"] as const)(
"refuses a %s worker when the runtime has no enforceable memory bound",
"uses an enforceable memory bound for %s on the current runtime",
async (format) => {
await expect(
new NodeCaptureAdapter(format).command("/owned/request.json"),
).rejects.toBeInstanceOf(AnalysisCapabilityUnavailableError);
const adapter = new NodeCaptureAdapter(format);
const bun = process.versions.bun !== undefined;
if (bun && process.platform !== "win32") {
await expect(
adapter.command("/owned/request.json"),
).rejects.toBeInstanceOf(AnalysisCapabilityUnavailableError);
} else {
const command = await adapter.command("/owned/request.json");
expect(command.command).toBe(process.execPath);
if (bun) expect(command.memoryLimitBytes).toBeGreaterThan(0);
else expect(command.memoryLimitBytes).toBeUndefined();
}
},
);
+3 -5
View File
@@ -1,3 +1,4 @@
import { javaScriptWorkerMemoryLimitBytes } from "../../process/JavaScriptWorkerMemoryLimit.js";
import { fileURLToPath } from "node:url";
import type { HistoricalCaptureFormatAdapter } from "./HistoricalCaptureFormatAdapter.js";
import {
@@ -6,10 +7,7 @@ import {
type CaptureDecoderHeap,
} from "../../config/captureDecoderHeap.js";
import { CAPTURE_DECODER_SEMI_SPACE_MIB } from "../../domain/analysisResourceLimits.js";
import {
NODE_CAPTURE_HEAP_ENFORCED,
captureDecoderProcessMemoryLimitBytes,
} from "./CaptureRelease.js";
import { NODE_CAPTURE_HEAP_ENFORCED } from "./CaptureRelease.js";
import {
QLOG_CAPTURE_PROVIDER_IDENTITY,
HAR_CAPTURE_PROVIDER_IDENTITY,
@@ -54,7 +52,7 @@ export class NodeCaptureAdapter implements HistoricalCaptureFormatAdapter {
: [];
}
async command(requestPath: string) {
const memoryLimitBytes = captureDecoderProcessMemoryLimitBytes(
const memoryLimitBytes = javaScriptWorkerMemoryLimitBytes(
this.heap.oldGenerationMiB,
);
if (
@@ -1,4 +1,5 @@
import { createDeferred } from "../../../../tests/fixtures/binarySession.js";
import { awaitAcknowledgement } from "../../../../tests/fixtures/lifecycleAcknowledgement.js";
import { readFile, rename, copyFile } from "node:fs/promises";
import { expect, it, vi } from "vitest";
import { ArtifactResourceScope } from "../../../artifacts/ArtifactResourceScope.js";
@@ -240,7 +241,7 @@ it("bounds active imports without an unbounded queue and releases admission afte
);
const first = port.inspect(fixture.path);
try {
await reached.promise;
await awaitAcknowledgement(reached.promise, first, "trace import");
const second = await port.inspect(fixture.path);
if (second.ok) throw new Error("Expected capacity refusal");
expect(second.error._tag).toBe("AnalysisResourceConstraintError");
@@ -1,3 +1,4 @@
import { unescapeJsonPointerToken } from "../../../domain/jsonPointer.js";
import { captureBudgets } from "../CaptureBudgets.js";
import { isAbsolute } from "node:path";
import { CaptureFormatError } from "../CaptureFormatError.js";
@@ -63,10 +64,7 @@ const excludeBody = (
sha256: null,
},
);
const parts = pointer
.split("/")
.slice(1)
.map((part) => part.replaceAll("~1", "/").replaceAll("~0", "~"));
const parts = pointer.split("/").slice(1).map(unescapeJsonPointerToken);
let parent: JsonValue | undefined = record.reported;
for (const part of parts.slice(0, -1)) {
if (
+1 -1
View File
@@ -13,7 +13,7 @@ import {
import { evidenceSchema, type Evidence } from "../domain/evidence.js";
import type { JsonValue } from "../domain/jsonValue.js";
import type { Result } from "../domain/result.js";
import { err, ok } from "../domain/result.js";
import { ok } from "../domain/result.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import {
OBJDUMP_ARTIFACT_OPERATIONS,
+5 -9
View File
@@ -49,11 +49,7 @@ export const registerUtilityCommands = (
registerReferenceSourceCommand(cli, logger, environment);
};
/** Decimal argv text; the shared deadline field owns the bounds and help. */
const cliNativeDeadline = (field: typeof nativeUiDeadlineField) =>
cliDecimalField(field);
// Decimal tokens only, for the same reason as cliNativeDeadline.
// Decimal tokens only; shared fields own their bounds and help.
const nativeUiScopeOptions = {
pid: cliIntegerArgument(1).describe("Existing application PID"),
windowId: cliIntegerArgument(1).describe(
@@ -70,7 +66,7 @@ const nativeUiScopeOptions = {
maxNodes: cliIntegerArgument(1)
.default(500)
.describe("Maximum accessibility nodes per capture"),
nativeUiDeadlineMs: cliNativeDeadline(nativeUiDeadlineField),
nativeUiDeadlineMs: cliDecimalField(nativeUiDeadlineField),
};
const nativeUiScope = (options: {
@@ -208,7 +204,7 @@ const registerNativeCommands = (
path: localPathStringSchema.describe("Mach-O or app path"),
}),
options: z.object({
nativeCommandTimeoutMs: cliNativeDeadline(nativeCommandTimeoutField),
nativeCommandTimeoutMs: cliDecimalField(nativeCommandTimeoutField),
}),
run: ({ args, options }) =>
logCliCommand(logger, command, () =>
@@ -235,7 +231,7 @@ const registerNativeCommands = (
.describe(
"Plist path relative to the app root, or an absolute path (default: the bundle's Info.plist)",
),
nativeCommandTimeoutMs: cliNativeDeadline(nativeCommandTimeoutField),
nativeCommandTimeoutMs: cliDecimalField(nativeCommandTimeoutField),
}),
run: ({ args, options }) =>
logCliCommand(logger, "inspect-plist", () =>
@@ -263,7 +259,7 @@ const registerNativeCommands = (
symbols: swiftSymbolsSchema.describe("Swift mangled symbols to demangle"),
}),
options: z.object({
nativeCommandTimeoutMs: cliNativeDeadline(nativeCommandTimeoutField),
nativeCommandTimeoutMs: cliDecimalField(nativeCommandTimeoutField),
}),
run: ({ args, options }) =>
logCliCommand(logger, "demangle-swift", () =>
-1
View File
@@ -4,7 +4,6 @@ import { createWebRuntimeService } from "../composition/webRuntime.js";
import { cliDomainInput } from "./domainInput.js";
import { observeWebExecutionInputSchema } from "../domain/webExecution.js";
import { inspectWebEventListenersInputSchema } from "../domain/webEventListeners.js";
import { browserCliError } from "./browserCliError.js";
import {
browserScopeOptions,
selectedCommandTimeout,
+1 -3
View File
@@ -35,7 +35,6 @@ export const parseCliJsonInput = async (
options?: {
readonly snapshot?: boolean;
readonly stdin?: Readable;
readonly maxStdinBytes?: number;
},
): Promise<
| { readonly ok: true; readonly value: unknown }
@@ -132,13 +131,12 @@ const readCliJsonStdin = async (
options?: {
readonly snapshot?: boolean;
readonly stdin?: Readable;
readonly maxStdinBytes?: number;
},
): Promise<
Result<unknown, AnalysisInputError | AnalysisResourceConstraintError>
> => {
const stream = options?.stdin ?? process.stdin;
const maxBytes = options?.maxStdinBytes ?? DEFAULT_MAX_STDIN_BYTES;
const maxBytes = DEFAULT_MAX_STDIN_BYTES;
const chunks: Buffer[] = [];
let totalBytes = 0;
const requireHeadroom = (): void => {
@@ -1,4 +1,4 @@
import { existsSync, statSync } from "node:fs";
import { statSync } from "node:fs";
import { win32 } from "node:path";
import { windowsPathEntries } from "../process/WindowsExecutable.js";
@@ -11,7 +11,7 @@ export interface WindowsExecutableLookup {
const isRegularFile = (path: string): boolean => {
try {
return existsSync(path) && statSync(path).isFile();
return statSync(path).isFile();
} catch {
return false;
}
@@ -1,14 +1,8 @@
import { analyzeCapturedWebScript } from "../domain/webBundleAnalyzer.js";
import {
serveWebBundleAnalysisWorker,
type WebBundleScriptAnalyzer,
} from "../browser/WebBundleAnalysisWorkerServer.js";
const analyze: WebBundleScriptAnalyzer = (script, precision) =>
analyzeCapturedWebScript(script, precision);
import { serveWebBundleAnalysisWorker } from "../browser/WebBundleAnalysisWorkerServer.js";
try {
await serveWebBundleAnalysisWorker(analyze);
await serveWebBundleAnalysisWorker(analyzeCapturedWebScript);
} catch (cause: unknown) {
process.stderr.write(
`${cause instanceof Error ? cause.message : String(cause)}\n`,
@@ -16,7 +16,6 @@ import {
resolveRegexWorkerHeapRequest,
REGEX_WORKER_HEAP_REQUEST_SETTING,
regexWorkerHeapRequestContinuingRecovery,
regexWorkerHeapRequestRecovery,
} from "./regexWorkerHeapRequest.js";
import {
MAXIMUM_REGEX_WORKER_HEAP_REQUEST_MIB,
@@ -4,16 +4,73 @@ import { expect, it } from "vitest";
import { toolContract } from "./toolContracts.js";
import { toolInputSchemaWithMetadata } from "./toolSchemaMetadata.js";
const view = {
kind: "beam",
collection: "exports",
fields: ["function"],
const beamView = { kind: "beam", collection: "exports", fields: ["function"] };
const pycView = {
kind: "python-pyc",
collection: "code_objects",
fields: ["name"],
};
it.each(["draft-2020-12", "draft-07"] as const)(
"advertises BEAM delivery combinations in %s",
(target) => {
const contract = toolContract("inspect_erlang_beam");
const cases = [
{
name: "inspect_erlang_beam",
path: "/tmp/module.beam",
view: beamView,
extra: [
[{ detail: "summary", view: { ...beamView, fields: ["name"] } }, false],
[
{ detail: "summary", view: { ...beamView, modules: ["erlang"] } },
false,
],
[
{
detail: "summary",
view: { ...beamView, collection: "imports", modules: ["erlang"] },
},
true,
],
[
{
detail: "summary",
view: { ...beamView, collection: "chunks", fields: ["payload"] },
},
true,
],
[
{
detail: "summary",
view: { ...beamView, collection: "atoms", fields: ["index", "name"] },
},
true,
],
[
{
detail: "summary",
view: { ...beamView, collection: "lambdas", fields: ["free_vars"] },
},
true,
],
],
},
{
name: "inspect_python_pyc",
path: "/tmp/module.pyc",
view: pycView,
extra: [],
},
] as const;
it.each(
cases.flatMap((testCase) =>
(["draft-2020-12", "draft-07"] as const).map((target) => ({
...testCase,
target,
})),
),
)(
"advertises $name delivery combinations in $target",
({ name, path, view, extra, target }) => {
const contract = toolContract(name);
const projected = toolInputSchemaWithMetadata(contract)[
"~standard"
].jsonSchema.input({ target });
@@ -43,38 +100,9 @@ it.each(["draft-2020-12", "draft-07"] as const)(
[{ detail: "summary", view: { ...view, fields: ["missing"] } }, false],
[{ detail: "summary", view: { ...view, kind: "native-exports" } }, false],
[{ detail: "summary", view, unexpected: true }, false],
[{ detail: "summary", view: { ...view, fields: ["name"] } }, false],
[{ detail: "summary", view: { ...view, modules: ["erlang"] } }, false],
[
{
detail: "summary",
view: { ...view, collection: "imports", modules: ["erlang"] },
},
true,
],
[
{
detail: "summary",
view: { ...view, collection: "chunks", fields: ["payload"] },
},
true,
],
[
{
detail: "summary",
view: { ...view, collection: "atoms", fields: ["index", "name"] },
},
true,
],
[
{
detail: "summary",
view: { ...view, collection: "lambdas", fields: ["free_vars"] },
},
true,
],
...extra,
] as const) {
const input = { path: "/tmp/module.beam", ...delivery };
const input = { path, ...delivery };
expect(
contract.inputSchema.safeParse(input).success,
JSON.stringify(input),
Loaded 100 of 369 files, more files were not shown because too many files have changed in this diff. Show more