fix(providers): preserve bounded artifact reads and cleanup owners

Refs #4373, #4455, #4457, #4458.

Use canonical artifact ownership in Go, Rust, SQLite and PE readers and their failure consumers. Admit PE read budgets before allocation and ignore zero-extent sections in interval ownership. Close Rust resources through CLI and MCP lifecycles.
This commit is contained in:
morluto
2026-10-17 00:33:10 +08:00
parent 542fa84957
commit effb71f59c
23 changed files with 1068 additions and 231 deletions
+8 -18
View File
@@ -584,22 +584,6 @@ const hardLinksUnsupported = (cause: unknown): boolean =>
const describeAdbFailure = (cause: unknown): string =>
cause instanceof Error ? cause.message : String(cause);
const cleanupLeasesForRetryFailure = (
cause: unknown,
): readonly AnalysisCleanupLease[] | undefined => {
if (!(cause instanceof ArtifactResourceRetryFailure)) return undefined;
const leases = new Set<AnalysisCleanupLease>();
for (const owner of cause.failedOwners) {
if (
owner.kind !== "lease" ||
!(owner.lease instanceof AnalysisCleanupLease)
)
return undefined;
leases.add(owner.lease);
}
return leases.size > 0 ? [...leases] : undefined;
};
interface ScreenPublicationCloseFailure {
readonly cause: unknown;
readonly resource: string;
@@ -1046,7 +1030,10 @@ export class AdbProvider {
const leases =
(cause instanceof AnalysisError
? this.#outputDirectoryRetryLeases.get(cause)
: undefined) ?? cleanupLeasesForRetryFailure(cause);
: undefined) ??
(cause instanceof ArtifactResourceRetryFailure
? cause.cleanupLeases
: undefined);
if (leases === undefined) allFailuresHaveExactLeaseOwners = false;
for (const lease of leases ?? []) failedLeases.add(lease);
}
@@ -1390,7 +1377,10 @@ export class AdbProvider {
[cause],
this.#cleanupResources(cause, "ADB provider-owned resource"),
);
const leases = cleanupLeasesForRetryFailure(cause);
const leases =
cause instanceof ArtifactResourceRetryFailure
? cause.cleanupLeases
: undefined;
if (leases !== undefined) new AnalysisCleanupLeaseGroup(error, leases);
return err(error);
}
+1
View File
@@ -12,6 +12,7 @@ import type { Result } from "../../domain/result.js";
/** Replaceable offline dependency reader with no target-execution authority. */
export interface RustBinaryPort {
readonly identity: ProviderIdentity;
close?(): Promise<void>;
inspect(
input: InspectRustBinaryInput,
options?: ExecutionOptions,
@@ -36,6 +36,10 @@ const architectureForSubject = (architecture: string) => {
export class RustBinaryService {
constructor(readonly provider: RustBinaryPort) {}
close(): Promise<void> {
return this.provider.close?.() ?? Promise.resolve();
}
/** Inspect one explicitly selected file without acquiring a binary session. */
async inspect(
rawInput: unknown,
+6 -2
View File
@@ -68,7 +68,9 @@ export const webArtifactReadError = (
if (cause instanceof ArtifactReaderFailure && cause.cleanup !== undefined) {
const primary =
signal?.aborted === true
? new AnalysisCancelledError(context.operation, { cause })
? new AnalysisCancelledError(context.operation, {
cause: signal.reason,
})
: new ArtifactOperationError(
context.operation,
cause.reason,
@@ -88,7 +90,9 @@ export const webArtifactReadError = (
);
}
if (signal?.aborted === true)
return new AnalysisCancelledError(context.operation, { cause });
return new AnalysisCancelledError(context.operation, {
cause: signal.reason,
});
const selection = context.callerSelected
? selectedFileError(cause, context)
: undefined;
+8 -2
View File
@@ -1,5 +1,6 @@
import type { ExecutionOptions } from "../../application/AnalysisProvider.js";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import { artifactFileCancellationCause } from "../../artifacts/ArtifactFileOperation.js";
import { AnalysisError } from "../../domain/analysisErrorBase.js";
import {
AnalysisAccessDeniedError,
@@ -90,7 +91,10 @@ export const historicalCaptureFailure = (
cause,
);
}
if (options?.signal?.aborted) return new AnalysisCancelledError(OPERATION);
if (options?.signal?.aborted)
return new AnalysisCancelledError(OPERATION, {
cause: options.signal.reason,
});
if (
phase === "capture-read" &&
cause instanceof ArtifactReaderFailure &&
@@ -107,7 +111,9 @@ export const historicalCaptureFailure = (
cause instanceof ArtifactReaderFailure &&
cause.reason === "cancelled"
)
return new AnalysisCancelledError(OPERATION);
return new AnalysisCancelledError(OPERATION, {
cause: artifactFileCancellationCause(cause),
});
if (
phase === "capture-read" &&
cause instanceof ArtifactReaderFailure &&
+18 -11
View File
@@ -2,7 +2,7 @@ import { z } from "incur";
import { createRustBinaryService } from "../composition/rust.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { logCliCommand } from "../cliLogging.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
import type { Logger } from "pino";
import type { CliInstance } from "./types.js";
import { withCommandCancellation } from "./commandCancellation.js";
@@ -13,7 +13,6 @@ export const registerRustCommands = (
cli: CliInstance,
logger: Logger,
): void => {
const service = createRustBinaryService();
cli.command(CLI_COMMANDS.inspectRustBinary, {
description:
"Inspect an embedded cargo-auditable Rust dependency list without target execution",
@@ -21,14 +20,22 @@ export const registerRustCommands = (
path: z.string().describe("Local ELF, PE or thin Mach-O image"),
}),
run: ({ args }) =>
withCommandCancellation((signal) =>
logCliCommand(logger, CLI_COMMANDS.inspectRustBinary, async () => {
const result = await service.inspect(
{ path: absoluteCallerPath(args.path) },
{ signal },
);
return result.ok ? result.value : projectAnalysisError(result.error);
}),
),
withCommandCancellation((signal) => {
const service = createRustBinaryService();
return logCliCommand(logger, CLI_COMMANDS.inspectRustBinary, () =>
finishAnalysisCommand(
() =>
service.inspect(
{ path: absoluteCallerPath(args.path) },
{ signal },
),
service,
"rust-auditable",
"inspect_rust_binary",
undefined,
signal,
),
);
}),
});
};
+1 -1
View File
@@ -341,7 +341,7 @@ export class EvmoleInterfaceProvider implements EvmInterfacePort {
cause instanceof ArtifactReaderFailure ? cause.cleanup : undefined;
if (options?.signal?.aborted)
throw new AnalysisCancelledError(OPERATION, {
cause,
cause: options.signal.reason,
capturedOutput,
...(cleanup === undefined ? {} : { cleanup }),
});
+83 -56
View File
@@ -1,4 +1,10 @@
import { readStableArtifact } from "../artifacts/readStableArtifact.js";
import {
ArtifactFileCleanupFailure,
assertArtifactReadActive,
bindArtifactFileCleanup,
artifactFileCancellationCause,
} from "../artifacts/ArtifactFileOperation.js";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
@@ -40,19 +46,16 @@ const OPERATION = "inspect_go_binary";
const INPUT_BYTES = 256 * 1024 * 1024;
const OUTPUT_BYTES = 8 * 1024 * 1024;
const failureForPrimary = (
cause: unknown,
path: string,
signal?: AbortSignal,
): AnalysisError => {
const failureForPrimary = (cause: unknown, path: string): AnalysisError => {
if (cause instanceof AnalysisError) return cause;
if (cause instanceof ArtifactReaderFailure && cause.reason === "integrity")
return new AnalysisArtifactChangedError(OPERATION, path, cause.message, {
cause,
});
if (cause instanceof ArtifactReaderFailure && cause.reason === "cancelled")
return new AnalysisCancelledError(OPERATION, { cause });
if (signal?.aborted) return new AnalysisCancelledError(OPERATION, { cause });
return new AnalysisCancelledError(OPERATION, {
cause: artifactFileCancellationCause(cause),
});
if (cause instanceof GoBinaryResourceFailure)
return new AnalysisResourceConstraintError(
OPERATION,
@@ -119,7 +122,13 @@ const failureFor = (
path: string,
signal?: AbortSignal,
): AnalysisError => {
const primary = failureForPrimary(cause, path, signal);
const primary =
cause instanceof ArtifactFileCleanupFailure &&
cause.fileCleanupFailure.outcome.kind === "failed"
? failureForPrimary(cause.fileCleanupFailure.outcome.cause, path)
: cause instanceof ArtifactFileCleanupFailure && signal?.aborted
? new AnalysisCancelledError(OPERATION, { cause: signal.reason })
: failureForPrimary(cause, path);
if (!(cause instanceof ArtifactReaderFailure) || cause.cleanup === undefined)
return primary;
const cleanup = new ProviderCleanupError(
@@ -128,7 +137,9 @@ const failureFor = (
{ reason: cause.cleanup.reason },
{ operation: OPERATION, cause },
);
return analysisErrorWithCleanupFailure(primary, cleanup, OPERATION);
const error = analysisErrorWithCleanupFailure(primary, cleanup, OPERATION);
bindArtifactFileCleanup(cause, error);
return error;
};
const outputLimit = (path: string) =>
@@ -174,8 +185,24 @@ export class GoBinaryProvider implements GoBinaryPort {
readonly identity = GO_BINARY_PROVIDER_IDENTITY;
readonly #resources = new ArtifactResourceScope();
close(): Promise<void> {
return this.#resources.close();
async close(): Promise<void> {
try {
await this.#resources.close();
} catch (cause: unknown) {
if (
!(cause instanceof ArtifactReaderFailure) ||
cause.cleanup === undefined
)
throw cause;
const error = new ProviderCleanupError(
GO_BINARY_PROVIDER_IDENTITY.id,
cause.cleanup.resources,
{ reason: cause.cleanup.reason },
{ cause, operation: OPERATION },
);
bindArtifactFileCleanup(cause, error);
throw error;
}
}
/** Read complete compiler/module metadata while preserving original byte identity and source ranges. */
@@ -184,57 +211,57 @@ export class GoBinaryProvider implements GoBinaryPort {
options?: ExecutionOptions,
): Promise<Result<GoBinary, AnalysisError>> {
try {
const snapshot = await this.#resources.run(() =>
readStableArtifact(
return await this.#resources.run(async () => {
const snapshot = await readStableArtifact(
input.path,
INPUT_BYTES,
options?.signal,
undefined,
this.#resources,
),
);
options?.signal?.throwIfAborted();
const image = readGoBinaryImage(snapshot.bytes);
const info = image.build_info;
if (
info !== null &&
((info.module_text !== null &&
Buffer.byteLength(info.module_text) > OUTPUT_BYTES) ||
Buffer.byteLength(info.module_bytes_base64) > OUTPUT_BYTES)
)
throw outputLimit(input.path);
const module =
info === null
? null
: info.module_text === null
? parseGoModuleBytes(
Buffer.from(info.module_bytes_base64, "base64").subarray(
16,
-16,
),
)
: parseGoModuleText(info.module_text);
const report = {
...image,
artifact: {
path: input.path,
sha256: snapshot.sha256,
bytes: snapshot.bytes.length,
},
build_info: info === null ? null : { ...info, module },
limitations: limitationsFor(info, module),
};
if (Buffer.byteLength(JSON.stringify(report), "utf8") > OUTPUT_BYTES)
throw outputLimit(input.path);
const parsed = goBinarySchema.safeParse(report);
if (!parsed.success)
throw new AnalysisOutputError(
OPERATION,
"Go reader produced invalid source ranges or report values.",
{ cause: parsed.error },
);
options?.signal?.throwIfAborted();
return ok(parsed.data);
assertArtifactReadActive(options?.signal);
const image = readGoBinaryImage(snapshot.bytes);
const info = image.build_info;
if (
info !== null &&
((info.module_text !== null &&
Buffer.byteLength(info.module_text) > OUTPUT_BYTES) ||
Buffer.byteLength(info.module_bytes_base64) > OUTPUT_BYTES)
)
throw outputLimit(input.path);
const module =
info === null
? null
: info.module_text === null
? parseGoModuleBytes(
Buffer.from(info.module_bytes_base64, "base64").subarray(
16,
-16,
),
)
: parseGoModuleText(info.module_text);
const report = {
...image,
artifact: {
path: input.path,
sha256: snapshot.sha256,
bytes: snapshot.bytes.length,
},
build_info: info === null ? null : { ...info, module },
limitations: limitationsFor(info, module),
};
if (Buffer.byteLength(JSON.stringify(report), "utf8") > OUTPUT_BYTES)
throw outputLimit(input.path);
const parsed = goBinarySchema.safeParse(report);
if (!parsed.success)
throw new AnalysisOutputError(
OPERATION,
"Go reader produced invalid source ranges or report values.",
{ cause: parsed.error },
);
assertArtifactReadActive(options?.signal);
return ok(parsed.data);
});
} catch (cause: unknown) {
return err(failureFor(cause, input.path, options?.signal));
}
+5 -4
View File
@@ -1,5 +1,6 @@
import { createHash } from "node:crypto";
import { setImmediate } from "node:timers/promises";
import { assertArtifactReadActive } from "../../artifacts/ArtifactFileOperation.js";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import type { ArtifactReaderBoundary } from "../../artifacts/ArtifactReader.js";
import { jsonParts } from "../../domain/jsonSerialization.js";
@@ -41,7 +42,7 @@ export const parsePeResources = async (
input: InspectPeResourcesInput,
signal?: AbortSignal,
): Promise<PeResources> => {
signal?.throwIfAborted();
assertArtifactReadActive(signal);
const layout = readPeResourceLayout(bytes);
const directories: PeResources["directories"] = [];
const resources: PeResources["resources"] = [];
@@ -145,7 +146,7 @@ export const parsePeResources = async (
for (let index = 0; index < count; index++) {
if (index % 128 === 0) {
await setImmediate();
signal?.throwIfAborted();
assertArtifactReadActive(signal);
}
const entry = header + 16 + index * 8;
const encoded = bytes.readUInt32LE(entry);
@@ -212,7 +213,7 @@ export const parsePeResources = async (
const hash = createHash("sha256");
for (let offset = 0; offset < range.bytes; offset += 1048576) {
await setImmediate();
signal?.throwIfAborted();
assertArtifactReadActive(signal);
hash.update(
bytes.subarray(
range.offset + offset,
@@ -252,7 +253,7 @@ export const parsePeResources = async (
"Exact shared byte ranges are retained; non-identical overlapping resource structures or payloads are rejected.",
],
};
signal?.throwIfAborted();
assertArtifactReadActive(signal);
if (jsonBytes(report, METADATA_BYTES) > METADATA_BYTES)
limit("PE resource result exceeds the 8 MiB allocation budget.", "memory");
return report;
+54 -23
View File
@@ -1,5 +1,10 @@
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import { readStableArtifact } from "../../artifacts/readStableArtifact.js";
import {
ArtifactFileCleanupFailure,
bindArtifactFileCleanup,
artifactFileCancellationCause,
} from "../../artifacts/ArtifactFileOperation.js";
import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js";
import type { PeResourcesPort } from "../../application/binaryDiagnostics/PeResourcesPort.js";
@@ -27,33 +32,49 @@ export class PeResourcesProvider implements PeResourcesPort {
readonly identity = PE_RESOURCES_PROVIDER;
readonly #resources = new ArtifactResourceScope();
close(): Promise<void> {
return this.#resources.close();
async close(): Promise<void> {
try {
await this.#resources.close();
} catch (cause: unknown) {
if (
!(cause instanceof ArtifactReaderFailure) ||
cause.cleanup === undefined
)
throw cause;
const error = new ProviderCleanupError(
PE_RESOURCES_PROVIDER.id,
cause.cleanup.resources,
{ reason: cause.cleanup.reason },
{ cause, operation: "inspect_pe_resources" },
);
bindArtifactFileCleanup(cause, error);
throw error;
}
}
async inspect(input: InspectPeResourcesInput, options?: ExecutionOptions) {
try {
const snapshot = await this.#resources.run(() =>
readStableArtifact(
return await this.#resources.run(async () => {
const snapshot = await readStableArtifact(
input.path,
input.max_file_bytes,
options?.signal,
undefined,
this.#resources,
),
);
return ok(
await parsePeResources(
snapshot.bytes,
{
path: input.path,
sha256: snapshot.sha256,
bytes: snapshot.bytes.length,
},
input,
options?.signal,
),
);
);
return ok(
await parsePeResources(
snapshot.bytes,
{
path: input.path,
sha256: snapshot.sha256,
bytes: snapshot.bytes.length,
},
input,
options?.signal,
),
);
});
} catch (cause) {
return err(peResourcesFailure(cause, input, options?.signal));
}
@@ -80,7 +101,6 @@ export const inputBudgetRemediation = (
const peResourcesFailurePrimary = (
cause: unknown,
input: InspectPeResourcesInput,
signal: AbortSignal | undefined,
) => {
const operation = "inspect_pe_resources";
if (cause instanceof ArtifactReaderFailure && cause.reason === "integrity")
@@ -91,8 +111,9 @@ const peResourcesFailurePrimary = (
{ cause },
);
if (cause instanceof ArtifactReaderFailure && cause.reason === "cancelled")
return new AnalysisCancelledError(operation, { cause });
if (signal?.aborted) return new AnalysisCancelledError(operation, { cause });
return new AnalysisCancelledError(operation, {
cause: artifactFileCancellationCause(cause),
});
if (cause instanceof ArtifactReaderFailure) {
// An input byte budget is a file-size bound, not a memory constraint, and
@@ -160,7 +181,15 @@ const peResourcesFailure = (
input: InspectPeResourcesInput,
signal: AbortSignal | undefined,
) => {
const primary = peResourcesFailurePrimary(cause, input, signal);
const primary =
cause instanceof ArtifactFileCleanupFailure &&
cause.fileCleanupFailure.outcome.kind === "failed"
? peResourcesFailurePrimary(cause.fileCleanupFailure.outcome.cause, input)
: cause instanceof ArtifactFileCleanupFailure && signal?.aborted
? new AnalysisCancelledError("inspect_pe_resources", {
cause: signal.reason,
})
: peResourcesFailurePrimary(cause, input);
if (!(cause instanceof ArtifactReaderFailure) || cause.cleanup === undefined)
return primary;
const cleanup = new ProviderCleanupError(
@@ -169,9 +198,11 @@ const peResourcesFailure = (
{ reason: cause.cleanup.reason },
{ operation: "inspect_pe_resources", cause },
);
return analysisErrorWithCleanupFailure(
const error = analysisErrorWithCleanupFailure(
primary,
cleanup,
"inspect_pe_resources",
);
bindArtifactFileCleanup(cause, error);
return error;
};
+31 -4
View File
@@ -1,3 +1,8 @@
import {
bindArtifactFileCleanup,
ArtifactFileCleanupFailure,
artifactFileCancellationCause,
} from "../../artifacts/ArtifactFileOperation.js";
import { access, mkdir, stat, writeFile } from "node:fs/promises";
import { constants } from "node:fs";
import { randomUUID } from "node:crypto";
@@ -17,6 +22,7 @@ import {
type RecordedCrash,
} from "../../domain/native/recordedCrash.js";
import { readStableArtifact } from "../../artifacts/readStableArtifact.js";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import type { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
import { AnalysisError } from "../../domain/analysisErrorBase.js";
import {
@@ -186,19 +192,40 @@ export const inspectPwndbgCore = async ({
);
reply = replySchema.parse(JSON.parse(raw.bytes.toString("utf8")));
} catch (cause: unknown) {
if (options?.signal?.aborted)
throw new AnalysisCancelledError(OPERATION, {
cause,
if (
cause instanceof ArtifactReaderFailure &&
(cause.reason === "cancelled" ||
(cause instanceof ArtifactFileCleanupFailure &&
cause.fileCleanupFailure.outcome.kind === "completed" &&
options?.signal?.aborted))
) {
const error = new AnalysisCancelledError(OPERATION, {
cause:
cause.reason === "cancelled"
? artifactFileCancellationCause(cause)
: options?.signal?.reason,
capturedOutput: recordedCrashStageOutput(diagnostics, output),
...(cause instanceof ArtifactReaderFailure &&
cause.cleanup !== undefined
? { cleanup: cause.cleanup }
: {}),
});
throw new AnalysisOutputError(
bindArtifactFileCleanup(cause, error);
throw error;
}
const error = new AnalysisOutputError(
OPERATION,
`Core-only debugger reply failed: ${cause instanceof Error ? cause.message : String(cause)}`,
{
cause,
...(cause instanceof ArtifactReaderFailure
? { cleanup: cause.cleanup }
: {}),
capturedOutput: recordedCrashStageOutput(diagnostics, output),
},
);
bindArtifactFileCleanup(cause, error);
throw error;
}
if (!reply.ok) {
if (reply.reason === "unavailable" || reply.reason === "unsupported")
+34 -5
View File
@@ -1,3 +1,8 @@
import {
bindArtifactFileCleanup,
ArtifactFileCleanupFailure,
artifactFileCancellationCause,
} from "../../artifacts/ArtifactFileOperation.js";
import {
pwntoolsDecoderFailure,
pwntoolsUnavailable,
@@ -322,16 +327,40 @@ export class PwntoolsDecoder<
);
reply = replySchema.parse(JSON.parse(file.bytes.toString("utf8")));
} catch (cause: unknown) {
if (options?.signal?.aborted)
throw new AnalysisCancelledError(OPERATION, {
cause,
if (
cause instanceof ArtifactReaderFailure &&
(cause.reason === "cancelled" ||
(cause instanceof ArtifactFileCleanupFailure &&
cause.fileCleanupFailure.outcome.kind === "completed" &&
options?.signal?.aborted))
) {
const error = new AnalysisCancelledError(OPERATION, {
cause:
cause.reason === "cancelled"
? artifactFileCancellationCause(cause)
: options?.signal?.reason,
capturedOutput,
...(cause instanceof ArtifactReaderFailure &&
cause.cleanup !== undefined
? { cleanup: cause.cleanup }
: {}),
});
throw new AnalysisOutputError(
bindArtifactFileCleanup(cause, error);
throw error;
}
const error = new AnalysisOutputError(
OPERATION,
`Owned pwntools decoder reply failed for ${input.path}: ${cause instanceof z.ZodError ? cause.issues[0]?.message + " at " + (cause.issues[0]?.path.map(String).join(".") ?? "root") : cause instanceof Error ? cause.message : String(cause)}`,
{ cause, capturedOutput },
{
cause,
capturedOutput,
...(cause instanceof ArtifactReaderFailure
? { cleanup: cause.cleanup }
: {}),
},
);
bindArtifactFileCleanup(cause, error);
throw error;
}
if (!reply.ok) {
if (reply.reason === "unsupported-target")
+88 -33
View File
@@ -1,4 +1,12 @@
import { readStableArtifact } from "../artifacts/readStableArtifact.js";
import {
ArtifactFileCleanupFailure,
assertArtifactReadActive,
bindArtifactFileCleanup,
} from "../artifacts/ArtifactFileOperation.js";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import type { ExecutionOptions } from "../application/AnalysisProvider.js";
import type { RustBinaryPort } from "../application/rust/RustBinaryPort.js";
@@ -39,6 +47,27 @@ const OPERATION = "inspect_rust_binary";
/** Stable in-process dependency inspection without subprocesses, temporary files, or a Rust toolchain. */
export class RustBinaryProvider implements RustBinaryPort {
readonly identity = RUST_BINARY_PROVIDER_IDENTITY;
readonly #resources = new ArtifactResourceScope();
async close(): Promise<void> {
try {
await this.#resources.close();
} catch (cause: unknown) {
if (
!(cause instanceof ArtifactReaderFailure) ||
cause.cleanup === undefined
)
throw cause;
const error = new ProviderCleanupError(
RUST_BINARY_PROVIDER_IDENTITY.id,
cause.cleanup.resources,
{ reason: cause.cleanup.reason },
{ cause, operation: OPERATION },
);
bindArtifactFileCleanup(cause, error);
throw error;
}
}
/** Read one dependency section and any inferred rustc commit from the selected file. */
async inspect(
@@ -46,33 +75,39 @@ export class RustBinaryProvider implements RustBinaryPort {
options?: ExecutionOptions,
): Promise<Result<RustBinary, AnalysisError>> {
try {
const snapshot = await readStableArtifact(
input.path,
RUST_BINARY_INPUT_BYTES,
options?.signal,
);
options?.signal?.throwIfAborted();
const image = readRustBinaryImage(snapshot.bytes);
const report = {
...image,
artifact: {
path: input.path,
sha256: snapshot.sha256,
bytes: snapshot.bytes.length,
},
limitations: limitationsFor(image),
};
if (Buffer.byteLength(JSON.stringify(report), "utf8") > RUST_OUTPUT_BYTES)
throw outputLimit(input.path);
const parsed = rustBinarySchema.safeParse(report);
if (!parsed.success)
throw new AnalysisOutputError(
OPERATION,
"Rust reader produced an invalid dependency report.",
{ cause: parsed.error },
return await this.#resources.run(async () => {
const snapshot = await readStableArtifact(
input.path,
RUST_BINARY_INPUT_BYTES,
options?.signal,
undefined,
this.#resources,
);
options?.signal?.throwIfAborted();
return ok(parsed.data);
assertArtifactReadActive(options?.signal);
const image = readRustBinaryImage(snapshot.bytes);
const report = {
...image,
artifact: {
path: input.path,
sha256: snapshot.sha256,
bytes: snapshot.bytes.length,
},
limitations: limitationsFor(image),
};
if (
Buffer.byteLength(JSON.stringify(report), "utf8") > RUST_OUTPUT_BYTES
)
throw outputLimit(input.path);
const parsed = rustBinarySchema.safeParse(report);
if (!parsed.success)
throw new AnalysisOutputError(
OPERATION,
"Rust reader produced an invalid dependency report.",
{ cause: parsed.error },
);
assertArtifactReadActive(options?.signal);
return ok(parsed.data);
});
} catch (cause: unknown) {
return err(failureFor(cause, input.path, options?.signal));
}
@@ -143,13 +178,8 @@ const rustcLimitation = (
];
};
const failureFor = (
cause: unknown,
path: string,
signal?: AbortSignal,
): AnalysisError => {
const failureForPrimary = (cause: unknown, path: string): AnalysisError => {
if (cause instanceof AnalysisError) return cause;
if (signal?.aborted) return new AnalysisCancelledError(OPERATION);
if (cause instanceof RustAuditableResourceFailure)
return new AnalysisResourceConstraintError(
OPERATION,
@@ -173,6 +203,31 @@ const failureFor = (
return artifactFailure(cause, path);
};
const failureFor = (
cause: unknown,
path: string,
signal?: AbortSignal,
): AnalysisError => {
const primary =
cause instanceof ArtifactFileCleanupFailure &&
cause.fileCleanupFailure.outcome.kind === "failed"
? failureForPrimary(cause.fileCleanupFailure.outcome.cause, path)
: cause instanceof ArtifactFileCleanupFailure && signal?.aborted
? new AnalysisCancelledError(OPERATION, { cause: signal.reason })
: failureForPrimary(cause, path);
if (!(cause instanceof ArtifactReaderFailure) || cause.cleanup === undefined)
return primary;
const cleanup = new ProviderCleanupError(
RUST_BINARY_PROVIDER_IDENTITY.id,
cause.cleanup.resources,
{ reason: cause.cleanup.reason },
{ cause, operation: OPERATION },
);
const error = analysisErrorWithCleanupFailure(primary, cleanup, OPERATION);
bindArtifactFileCleanup(cause, error);
return error;
};
const artifactFailure = (cause: unknown, path: string): AnalysisError => {
if (cause instanceof ArtifactReaderFailure)
return artifactReaderFailure(cause, path);
@@ -212,7 +267,7 @@ const artifactReaderFailure = (
cause,
});
if (cause.reason === "cancelled")
return new AnalysisCancelledError(OPERATION);
return new AnalysisCancelledError(OPERATION, { cause: cause.cause });
if (cause.reason === "limit")
return new AnalysisResourceConstraintError(
OPERATION,
+6 -1
View File
@@ -310,6 +310,7 @@ export const createServer = (
const webModuleTrace =
options.webModuleTrace ?? createWebModuleTraceService(environment);
const goBinary = options.goBinary ?? createGoBinaryService();
const rustBinary = options.rustBinary ?? createRustBinaryService();
const peResources = createPeResourcesService();
const webNetworkCapture =
options.webNetworkCapture ?? createWebNetworkCaptureService(environment);
@@ -387,6 +388,7 @@ export const createServer = (
webSourceLocation.close(),
webModuleTrace.close(),
goBinary.close(),
rustBinary.close(),
peResources.close(),
webNetworkCapture.close(),
javascriptRecovery.close?.() ?? Promise.resolve(),
@@ -444,6 +446,7 @@ export const createServer = (
withAdmittedAnalysis: analysisAdmission,
artifactResources,
goBinary,
rustBinary,
peResources,
webNetworkCapture,
};
@@ -642,6 +645,7 @@ const registerConfiguredAnalysisTools = (
recordEvidence,
withAdmittedAnalysis,
goBinary,
rustBinary,
peResources,
webNetworkCapture,
}: ServerToolContext,
@@ -698,7 +702,7 @@ const registerConfiguredAnalysisTools = (
);
registerRustTools(
server,
options.rustBinary ?? createRustBinaryService(),
rustBinary,
toolLogger,
recordEvidence,
withAdmittedAnalysis,
@@ -809,6 +813,7 @@ const createSessionRecorders = (
interface ServerToolContext extends ReturnType<typeof createSessionRecorders> {
readonly goBinary: GoBinaryService;
readonly rustBinary: RustBinaryService;
readonly peResources: PeResourcesService;
readonly webNetworkCapture: WebNetworkCaptureService;
readonly artifactResources: ArtifactResourceScope;
+26 -12
View File
@@ -1,4 +1,9 @@
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import {
ArtifactFileCleanupFailure,
bindArtifactFileCleanup,
artifactFileCancellationCause,
} from "../artifacts/ArtifactFileOperation.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import {
AnalysisError,
@@ -49,25 +54,32 @@ export const sqliteDatabaseFailure = (
const operation = "inspect_sqlite_database";
if (cause instanceof AnalysisError) return cause;
if (cause instanceof ArtifactReaderFailure) {
if (cause.cleanup !== undefined)
return new ProviderCleanupError(
if (cause.cleanup !== undefined) {
const physicalRead =
cause instanceof ArtifactFileCleanupFailure
? cause.fileCleanupFailure.outcome
: undefined;
const previous =
physicalRead?.kind === "failed"
? sqliteDatabaseFailure(physicalRead.cause, path, phase)
: signal?.aborted
? new AnalysisCancelledError(operation, { cause: signal.reason })
: physicalRead?.kind === "completed" || cause.cause === undefined
? undefined
: sqliteDatabaseFailure(cause.cause, path, phase);
const error = new ProviderCleanupError(
SQLITE_PROVIDER_IDENTITY.id,
cause.cleanup.resources,
{
reason: cause.cleanup.reason,
previous_error:
cause.cause === undefined
? null
: projectAnalysisError(
signal?.aborted
? new AnalysisCancelledError(operation, {
cause: cause.cause,
})
: sqliteDatabaseFailure(cause.cause, path, phase),
),
previous === undefined ? null : projectAnalysisError(previous),
},
{ operation, cause },
);
bindArtifactFileCleanup(cause, error);
return error;
}
if (cause.reason === "io" && cause.cause !== undefined)
return sqliteDatabaseFailure(cause.cause, path, phase, signal);
if (cause.reason === "integrity")
@@ -75,7 +87,9 @@ export const sqliteDatabaseFailure = (
cause,
});
if (cause.reason === "cancelled")
return new AnalysisCancelledError(operation, { cause });
return new AnalysisCancelledError(operation, {
cause: artifactFileCancellationCause(cause),
});
return new AnalysisInputError(operation, { cause }, [
{
path: ["path"],
+47 -54
View File
@@ -9,7 +9,10 @@ import {
AnalysisResourceConstraintError,
} from "../domain/analysisErrorCore.js";
import { SQLITE_PROVIDER_IDENTITY } from "../domain/sqlite/sqliteDatabase.js";
import { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js";
import {
withArtifactFileHandle,
assertArtifactReadActive,
} from "../artifacts/ArtifactFileOperation.js";
import {
readStableArtifact,
type StableArtifactFileSystem,
@@ -180,61 +183,51 @@ const verifyInactiveJournal = async (
constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK,
),
);
const owner = new OwnedFileHandle(file);
let failure: unknown;
try {
if (!sameState(before, await file.stat({ bigint: true })))
throw new ArtifactReaderFailure(
"integrity",
`SQLite journal changed before open: ${path}`,
);
// TRUNCATE commits leave an empty file; PERSIST commits zero the complete
// 28-byte fixed header. A new active journal may zero only its magic bytes.
const header = Buffer.alloc(28);
let bytesRead = 0;
if (before.size > 0n) {
while (bytesRead < header.length) {
signal?.throwIfAborted();
const read = await file.read(
header,
bytesRead,
header.length - bytesRead,
bytesRead,
await withArtifactFileHandle(
path,
file,
resourceScope,
signal,
async (file) => {
if (!sameState(before, await file.stat({ bigint: true })))
throw new ArtifactReaderFailure(
"integrity",
`SQLite journal changed before open: ${path}`,
);
if (read.bytesRead === 0) break;
bytesRead += read.bytesRead;
// TRUNCATE commits leave an empty file; PERSIST commits zero the complete
// 28-byte fixed header. A new active journal may zero only its magic bytes.
const header = Buffer.alloc(28);
let bytesRead = 0;
if (before.size > 0n) {
while (bytesRead < header.length) {
assertArtifactReadActive(signal);
const read = await file.read(
header,
bytesRead,
header.length - bytesRead,
bytesRead,
);
if (read.bytesRead === 0) break;
bytesRead += read.bytesRead;
}
}
}
if (!sameState(before, await file.stat({ bigint: true })))
throw new ArtifactReaderFailure(
"integrity",
`SQLite journal changed during inspection: ${path}`,
);
if (
before.size !== 0n &&
(bytesRead !== header.length || header.some((byte) => byte !== 0))
)
throw new ArtifactReaderFailure(
phase === "initial" ? "format" : "integrity",
phase === "initial"
? `Rollback journal may require recovery: ${path}. Supply a clean offline snapshot; only empty journals or a fully zeroed header are supported.`
: `Rollback journal became active or malformed during database acquisition: ${path}`,
);
} catch (cause: unknown) {
failure = cause;
throw cause;
} finally {
const cleanup = await resourceScope.release({
kind: "file-handle",
resource: path,
handle: owner,
});
if (cleanup.kind === "failed")
throw ArtifactReaderFailure.withCleanup(
failure ?? cleanup.cause,
ArtifactReaderFailure.cleanupObservation(cleanup.cause, path),
);
}
if (!sameState(before, await file.stat({ bigint: true })))
throw new ArtifactReaderFailure(
"integrity",
`SQLite journal changed during inspection: ${path}`,
);
if (
before.size !== 0n &&
(bytesRead !== header.length || header.some((byte) => byte !== 0))
)
throw new ArtifactReaderFailure(
phase === "initial" ? "format" : "integrity",
phase === "initial"
? `Rollback journal may require recovery: ${path}. Supply a clean offline snapshot; only empty journals or a fully zeroed header are supported.`
: `Rollback journal became active or malformed during database acquisition: ${path}`,
);
},
);
};
// Absence is a caller value to correct, not malformed path or content.
+25 -3
View File
@@ -1,3 +1,8 @@
import {
bindArtifactFileCleanup,
ArtifactFileCleanupFailure,
artifactFileCancellationCause,
} from "../artifacts/ArtifactFileOperation.js";
import { randomUUID } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
@@ -176,13 +181,30 @@ export const executeSqliteDatabaseWorker = async (request: {
? { cleanup: cause.cleanup }
: {}),
};
if (request.signal?.aborted)
throw new AnalysisCancelledError(operation, options);
throw new AnalysisOutputError(
if (
cause instanceof ArtifactReaderFailure &&
(cause.reason === "cancelled" ||
(cause instanceof ArtifactFileCleanupFailure &&
cause.fileCleanupFailure.outcome.kind === "completed" &&
request.signal?.aborted))
) {
const error = new AnalysisCancelledError(operation, {
...options,
cause:
cause.reason === "cancelled"
? artifactFileCancellationCause(cause)
: request.signal?.reason,
});
bindArtifactFileCleanup(cause, error);
throw error;
}
const error = new AnalysisOutputError(
operation,
`Owned SQLite worker reply is missing or malformed: ${cause instanceof Error ? cause.message : String(cause)}`,
options,
);
bindArtifactFileCleanup(cause, error);
throw error;
}
if (!reply.ok) throw rejectedReply(reply, capturedOutput);
return reply.inspection;
+1 -1
View File
@@ -164,7 +164,7 @@ export class WabtArtifactProvider implements WasmArtifactPort {
cause instanceof ArtifactReaderFailure ? cause.cleanup : undefined;
if (options?.signal?.aborted)
throw new AnalysisCancelledError(OPERATION, {
cause,
cause: options.signal.reason,
...(cleanup === undefined ? {} : { cleanup }),
});
throw new AnalysisArtifactChangedError(
+5 -1
View File
@@ -1,4 +1,5 @@
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import { artifactFileCancellationCause } from "../artifacts/ArtifactFileOperation.js";
import { boundedUtf8Text } from "../process/boundedUtf8Text.js";
import type { ProviderProcessSnapshot } from "../process/ProviderProcess.js";
import { AnalysisError } from "../domain/analysisErrorBase.js";
@@ -48,7 +49,10 @@ export const wabtFailure = (
(cause instanceof Error && cause.name === "AbortError")
)
return new AnalysisCancelledError("inspect_wasm_artifact", {
cause,
cause:
cause instanceof ArtifactReaderFailure
? artifactFileCancellationCause(cause)
: cause,
...cleanupOptions,
});
if (cause instanceof OwnedCommandFailure) {
@@ -92,6 +92,103 @@ const expectPeInputFailuresAcrossAdapters = async (input: {
const contract = toolContract("inspect_pe_resources");
const expectPeMappingOwnershipAcrossAdapters = async (
cli: TestCli,
client: Client,
root: string,
): Promise<void> => {
for (const { name, payloadRva, payloadBytes, section, expectedOffset } of [
{
name: "empty-header",
payloadRva: 0x300,
payloadBytes: 0,
section: null,
expectedOffset: 0x300,
},
{
name: "header-section-collision",
payloadRva: 0x300,
payloadBytes: 0,
section: { rva: 0x300, bytes: 4 },
expectedOffset: null,
},
{
name: "empty-section-in-header",
payloadRva: 0x300,
payloadBytes: 4,
section: { rva: 0x301, bytes: 0 },
expectedOffset: 0x300,
},
{
name: "empty-section-in-resources",
payloadRva: null,
payloadBytes: 4,
section: { rva: 0x2001, bytes: 0 },
expectedOffset: "original",
},
]) {
const fixture = peResourceFixture(false, [
{ type: 10, name: 1, language: 1033, payload: Buffer.from("DATA") },
]);
const data = fixture.dataOffsets[0] ?? 0;
let bytes = fixture.bytes;
const originalPayloadOffset = bytes.readUInt32LE(data) - 0x1c00;
if (payloadRva !== null) bytes.writeUInt32LE(payloadRva, data);
bytes.writeUInt32LE(payloadBytes, data + 4);
if (section !== null) {
bytes.writeUInt16LE(2, 0x86);
const at = fixture.sectionAt + 40;
bytes.writeUInt32LE(section.bytes, at + 8);
bytes.writeUInt32LE(section.rva, at + 12);
bytes.writeUInt32LE(section.bytes, at + 16);
bytes.writeUInt32LE(bytes.length, at + 20);
bytes = Buffer.concat([bytes, Buffer.alloc(section.bytes)]);
}
const path = join(root, `${name}.exe`);
await writeFile(path, bytes);
if (expectedOffset === null) {
await expectPeInputFailureAcrossAdapters({
cli,
client,
toolName: contract.name,
path,
reason: "invalid_format",
});
continue;
}
const result = await cli.run({
arguments: ["inspect-pe-resources", path, "--json"],
});
expect(result.exitCode, name).toBe(0);
const evidence = contract.outputSchema.parse(result.json);
const response = await client.callTool({
name: contract.name,
arguments: { path },
});
expect(response.isError, name).not.toBe(true);
const mcp = contract.outputSchema.parse(response.structuredContent);
expect(mcp.evidence_id).toBe(evidence.evidence_id);
expect(mcp.normalized_result).toEqual(evidence.normalized_result);
expect(evidence.normalized_result).toMatchObject({
coverage: { status: "complete", resources: 1 },
resources: [
{
payload: {
location: {
offset:
expectedOffset === "original"
? originalPayloadOffset
: expectedOffset,
bytes: payloadBytes,
},
},
},
],
});
expect(await readFile(path)).toEqual(bytes);
}
};
const expectPeInventoriesAcrossAdapters = async (
cli: TestCli,
client: Client,
@@ -201,6 +298,7 @@ cliTest(
"inspect_pe_resources",
);
await expectPeInventoriesAcrossAdapters(cli, client, root);
await expectPeMappingOwnershipAcrossAdapters(cli, client, root);
await expectPePaddingFailureAcrossAdapters({
cli,
client,
@@ -308,6 +308,10 @@ it("retains reply-read cancellation and retries the failed reply descriptor clos
descriptorCleanup.denied = false;
await selected.provider.close();
expect(handle.fd).toBe(-1);
expect(result.error.cleanupIncomplete).toBe(false);
expect(projectAnalysisError(result.error)).toMatchObject({
code: "cancelled",
});
for (const root of selected.roots)
await expect(access(root.path)).rejects.toMatchObject({ code: "ENOENT" });
});
@@ -0,0 +1,94 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { setImmediate } from "node:timers/promises";
import { beforeEach, expect, it, vi } from "vitest";
const parsing = vi.hoisted(() => {
const gate = () => {
let resolve!: () => void;
const promise = new Promise<void>((complete) => {
resolve = complete;
});
return { promise, resolve };
};
return { gate, entered: gate(), release: gate() };
});
vi.mock("../../src/native/pe/PeResourceParser.js", async (importOriginal) => {
const original =
await importOriginal<
typeof import("../../src/native/pe/PeResourceParser.js")
>();
return {
...original,
parsePeResources: async (
...arguments_: Parameters<typeof original.parsePeResources>
) => {
parsing.entered.resolve();
await parsing.release.promise;
return original.parsePeResources(...arguments_);
},
};
});
import { inspectPeResourcesInputSchema } from "../../src/domain/native/peResources.js";
import { PeResourcesProvider } from "../../src/native/pe/PeResourcesProvider.js";
import { peResourceFixture } from "../../src/native/pe/PeResources.fixture.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
beforeEach(() => {
parsing.entered = parsing.gate();
parsing.release = parsing.gate();
});
it.each(["completion", "cancellation"])(
"waits for admitted PE parsing through %s before closing",
async (outcome) => {
const root = await createTestTempDirectory("rea-pe-parsing-lifetime-");
const path = join(root, "image.exe");
await writeFile(path, peResourceFixture().bytes);
const provider = new PeResourcesProvider();
const controller = new AbortController();
const reason = new Error("cancelled during admitted parsing");
const inspection = provider.inspect(
inspectPeResourcesInputSchema.parse({ path }),
{ signal: controller.signal },
);
await parsing.entered.promise;
let closed = false;
const closing = provider.close().then(() => {
closed = true;
});
try {
if (outcome === "cancellation") controller.abort(reason);
await setImmediate();
expect(
closed,
"close reported release while parsing was still admitted",
).toBe(false);
parsing.release.resolve();
const result = await inspection;
if (outcome === "cancellation") {
expect(result).toMatchObject({
ok: false,
error: { _tag: "AnalysisCancelledError" },
});
expect(result.ok ? undefined : result.error.cause).toBe(reason);
} else
expect(result).toMatchObject({
ok: true,
value: { coverage: { status: "complete" } },
});
await closing;
expect(closed).toBe(true);
expect(
(await provider.inspect(inspectPeResourcesInputSchema.parse({ path })))
.ok,
).toBe(false);
} finally {
parsing.release.resolve();
await inspection;
await closing;
}
},
);
@@ -0,0 +1,421 @@
import { writeFile, type FileHandle } from "node:fs/promises";
import { join } from "node:path";
import { afterEach, beforeEach, expect, it, vi } from "vitest";
const fault = vi.hoisted(() => ({
path: "",
blockNew: true,
blocked: new Set<FileHandle>(),
handles: [] as FileHandle[],
onClose: undefined as (() => void) | undefined,
readFailure: undefined as { cause: unknown } | undefined,
}));
vi.mock("node:fs/promises", async (importOriginal) => {
const original = await importOriginal<typeof import("node:fs/promises")>();
return {
...original,
open: async (...arguments_: Parameters<typeof original.open>) => {
const handle = await original.open(...arguments_);
if (String(arguments_[0]) !== fault.path) return handle;
const close = handle.close.bind(handle);
fault.handles.push(handle);
if (fault.blockNew) fault.blocked.add(handle);
if (fault.readFailure !== undefined) {
const failure = fault.readFailure;
handle.stat = async () => {
throw failure.cause;
};
}
handle.close = async () => {
fault.onClose?.();
if (fault.blocked.has(handle))
throw new Error("controlled Rust descriptor close failure");
await close();
};
return handle;
},
};
});
import { RustBinaryProvider } from "../../src/rust/RustBinaryProvider.js";
import { RustBinaryService } from "../../src/application/rust/RustBinaryService.js";
import { AnalysisError } from "../../src/domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisArtifactChangedError,
AnalysisAccessDeniedError,
AnalysisResourceConstraintError,
} from "../../src/domain/analysisErrorCore.js";
import {
ArtifactFileCleanupFailure,
withArtifactRegularFile,
} from "../../src/artifacts/ArtifactFileOperation.js";
import { ArtifactResourceScope } from "../../src/artifacts/ArtifactResourceScope.js";
import { ArtifactReaderFailure } from "../../src/artifacts/ArtifactReader.js";
import { GoBinaryProvider } from "../../src/go/GoBinaryProvider.js";
import { PeResourcesProvider } from "../../src/native/pe/PeResourcesProvider.js";
import { inspectPeResourcesInputSchema } from "../../src/domain/native/peResources.js";
import { peResourceFixture } from "../../src/native/pe/PeResources.fixture.js";
import { createServer } from "../../src/server/createServer.js";
import { createTestBinarySession } from "../fixtures/binarySession.js";
import { createRustImage } from "../fixtures/rust/image.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { parseMcpToolError } from "../fixtures/mcpToolError.js";
import { mcpTest } from "../support/mcp/mcpFixture.js";
beforeEach(() => {
fault.path = "";
fault.blockNew = true;
fault.blocked.clear();
fault.handles = [];
fault.onClose = undefined;
fault.readFailure = undefined;
});
const staticProviders = [
["Rust", () => new RustBinaryProvider()],
["Go", () => new GoBinaryProvider()],
["PE", () => new PeResourcesProvider()],
] as const;
it.each(staticProviders)(
"preserves the exact late-close cancellation cause through %s inspection",
async (_name, makeProvider) => {
const path = await selectedImage();
const provider = makeProvider();
if (provider instanceof PeResourcesProvider)
await writeFile(path, peResourceFixture().bytes);
const controller = new AbortController();
const reason = new Error("cancel during confirmed descriptor close");
fault.blockNew = false;
fault.onClose = () => controller.abort(reason);
try {
const result =
provider instanceof PeResourcesProvider
? await provider.inspect(
inspectPeResourcesInputSchema.parse({ path }),
{ signal: controller.signal },
)
: await provider.inspect({ path }, { signal: controller.signal });
if (result.ok) throw new Error("Expected late cancellation");
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toBe(reason);
expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true);
} finally {
fault.blocked.clear();
await provider.close();
}
},
);
it.each(
staticProviders.flatMap(([name, makeProvider]) => [
[
name,
makeProvider,
new ArtifactReaderFailure("integrity", "source replacement"),
AnalysisArtifactChangedError,
] as const,
[
name,
makeProvider,
Object.assign(new Error("read denied"), { code: "EACCES" }),
AnalysisAccessDeniedError,
] as const,
[
name,
makeProvider,
new ArtifactReaderFailure("limit", "read limit"),
AnalysisResourceConstraintError,
] as const,
]),
)(
"preserves %s read failure after successful close observes cancellation",
async (_name, makeProvider, primary, expected) => {
const path = await selectedImage();
const provider = makeProvider();
fault.blockNew = false;
fault.readFailure = { cause: primary };
const controller = new AbortController();
fault.onClose = () => controller.abort(new Error("later cancellation"));
try {
const result =
provider instanceof PeResourcesProvider
? await provider.inspect(
inspectPeResourcesInputSchema.parse({ path }),
{ signal: controller.signal },
)
: await provider.inspect({ path }, { signal: controller.signal });
if (result.ok) throw new Error("Expected primary read failure");
expect(result.error).toBeInstanceOf(expected);
expect(result.error.cause).toBe(primary);
expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true);
} finally {
await provider.close();
}
},
);
it.each(staticProviders)(
"keeps the primary %s read failure when cancellation arrives during failed close",
async (_name, makeProvider) => {
const path = await selectedImage();
const provider = makeProvider();
const primary = new ArtifactReaderFailure(
"integrity",
"controlled source replacement",
);
fault.readFailure = { cause: primary };
const controller = new AbortController();
fault.onClose = () => controller.abort(new Error("later cancellation"));
try {
const result =
provider instanceof PeResourcesProvider
? await provider.inspect(
inspectPeResourcesInputSchema.parse({ path }),
{ signal: controller.signal },
)
: await provider.inspect({ path }, { signal: controller.signal });
if (result.ok)
throw new Error("Expected primary failure and retained descriptor");
expect(result.error.cleanupIncomplete).toBe(true);
const aggregate = result.error.cause;
if (!(aggregate instanceof AggregateError))
throw new Error("Missing primary/cleanup aggregate");
const [original] = aggregate.errors;
expect(original).toBeInstanceOf(AnalysisArtifactChangedError);
expect((original as AnalysisArtifactChangedError).cause).toBe(primary);
fault.blocked.clear();
await provider.close();
expect(result.error.cleanupIncomplete).toBe(false);
} finally {
fault.blocked.clear();
await provider.close();
}
},
);
afterEach(async () => {
fault.blockNew = false;
fault.blocked.clear();
fault.onClose = undefined;
for (const handle of fault.handles) if (handle.fd >= 0) await handle.close();
});
const selectedImage = async () => {
const root = await createTestTempDirectory("rea-rust-artifact-owner-");
const path = join(root, "image.elf");
await writeFile(path, createRustImage({ format: "elf" }));
fault.path = path;
return path;
};
mcpTest(
"retains Rust descriptor cleanup and recovers in the same MCP connection",
async ({ mcp }) => {
const path = await selectedImage();
const service = new RustBinaryService(new RustBinaryProvider());
const failures: AnalysisError[] = [];
const inspect = service.inspect.bind(service);
vi.spyOn(service, "inspect").mockImplementation(async (...arguments_) => {
const result = await inspect(...arguments_);
if (!result.ok) failures.push(result.error);
return result;
});
const session = createTestBinarySession(() => {
throw new Error("Unexpected native provider");
});
const server = createServer(
{ kind: "session", session },
{ rustBinary: service },
);
const client = await mcp.connect(server);
try {
for (let attempt = 0; attempt < 2; attempt++) {
const response = await client.callTool({
name: "inspect_rust_binary",
arguments: { path },
});
expect(response.isError).toBe(true);
expect(parseMcpToolError(response).error).toMatchObject({
code: "cleanup_incomplete",
details: { resources: [path] },
});
}
expect(fault.handles).toHaveLength(1);
expect(fault.handles[0]?.fd).toBeGreaterThanOrEqual(0);
expect(failures.map((error) => error.cleanupIncomplete)).toEqual([
true,
true,
]);
fault.blockNew = false;
fault.blocked.clear();
const recovered = await client.callTool({
name: "inspect_rust_binary",
arguments: { path },
});
expect(recovered.isError).not.toBe(true);
expect(failures.map((error) => error.cleanupIncomplete)).toEqual([
false,
false,
]);
expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true);
} finally {
fault.blockNew = false;
fault.blocked.clear();
await server.close();
await session.close();
}
},
);
it("keeps mixed descriptor cleanup and its aggregate bound to the exact unresolved owners", async () => {
const path = await selectedImage();
const service = new RustBinaryService(new RustBinaryProvider());
try {
const inspections = await Promise.all([
service.inspect({ path }),
service.inspect({ path }),
]);
expect(fault.handles).toHaveLength(2);
const errors = inspections.map((result) => {
if (result.ok) throw new Error("Expected retained cleanup");
return result.error;
});
const firstClose = await service.close().catch((cause: unknown) => cause);
if (!(firstClose instanceof AnalysisError))
throw new Error("Missing typed shutdown failure");
expect(firstClose.cleanupIncomplete).toBe(true);
const handlesByError = errors.map((error) => {
const aggregate = error.cause;
if (!(aggregate instanceof AggregateError))
throw new Error("Missing cleanup aggregate");
const cleanup = aggregate.errors[1];
if (
!(cleanup instanceof AnalysisError) ||
!(cleanup.cause instanceof ArtifactFileCleanupFailure)
)
throw new Error("Missing exact descriptor owner");
return cleanup.cause.fileCleanupFailure.owner.handle;
});
expect(new Set(handlesByError)).toEqual(new Set(fault.handles));
const [first, second] = handlesByError;
if (first === undefined || second === undefined)
throw new Error("Missing distinct descriptors");
fault.blocked.delete(first);
const mixedClose = await service.close().catch((cause: unknown) => cause);
if (!(mixedClose instanceof AnalysisError))
throw new Error("Missing mixed shutdown failure");
expect(first.fd).toBe(-1);
expect(second.fd).toBeGreaterThanOrEqual(0);
expect(errors.map((error) => error.cleanupIncomplete)).toEqual([
false,
true,
]);
expect(firstClose.cleanupIncomplete).toBe(true);
expect(mixedClose.cleanupIncomplete).toBe(true);
fault.blocked.clear();
await service.close();
expect(errors.map((error) => error.cleanupIncomplete)).toEqual([
false,
false,
]);
expect(firstClose.cleanupIncomplete).toBe(false);
expect(mixedClose.cleanupIncomplete).toBe(false);
} finally {
fault.blockNew = false;
fault.blocked.clear();
await service.close();
}
});
mcpTest(
"server shutdown releases Rust descriptors retained by a failed tool call",
async ({ mcp }) => {
const path = await selectedImage();
const session = createTestBinarySession(() => {
throw new Error("Unexpected native provider");
});
const server = createServer({ kind: "session", session });
const client = await mcp.connect(server);
try {
const response = await client.callTool({
name: "inspect_rust_binary",
arguments: { path },
});
expect(parseMcpToolError(response).error.code).toBe("cleanup_incomplete");
expect(fault.handles[0]?.fd).toBeGreaterThanOrEqual(0);
fault.blockNew = false;
fault.blocked.clear();
await server.close();
expect(fault.handles[0]?.fd).toBe(-1);
} finally {
fault.blockNew = false;
fault.blocked.clear();
await server.close();
await session.close();
}
},
);
it("preserves the caller's cancellation cause when descriptor close also fails", async () => {
const path = await selectedImage();
const provider = new RustBinaryProvider();
const controller = new AbortController();
const reason = new Error("cancel during Rust descriptor close");
fault.onClose = () => controller.abort(reason);
try {
const result = await provider.inspect(
{ path },
{ signal: controller.signal },
);
if (result.ok)
throw new Error("Expected cancellation with retained cleanup");
expect(result.error.cleanupIncomplete).toBe(true);
const aggregate = result.error.cause;
if (!(aggregate instanceof AggregateError))
throw new Error("Missing primary/cleanup aggregate");
const [primary] = aggregate.errors;
expect(primary).toBeInstanceOf(AnalysisCancelledError);
expect((primary as AnalysisCancelledError).cause).toBe(reason);
fault.blockNew = false;
fault.blocked.clear();
await provider.close();
expect(result.error.cleanupIncomplete).toBe(false);
} finally {
fault.blockNew = false;
fault.blocked.clear();
await provider.close();
fault.onClose = undefined;
}
});
it("preserves upstream resource-bound facts through an unconfirmed descriptor close", async () => {
const path = await selectedImage();
const scope = new ArtifactResourceScope();
const primary = new ArtifactReaderFailure(
"limit",
"Controlled input budget refusal",
{ boundary: "file-size" },
);
try {
const failure = await scope
.run(() =>
withArtifactRegularFile(path, scope, undefined, async () => {
throw primary;
}),
)
.catch((cause: unknown) => cause);
expect(failure).toBeInstanceOf(ArtifactFileCleanupFailure);
expect(failure).toMatchObject({ reason: "limit", boundary: "file-size" });
if (!(failure instanceof ArtifactFileCleanupFailure))
throw new Error("Expected exact descriptor cleanup failure");
expect(failure.fileCleanupFailure.outcome).toEqual({
kind: "failed",
cause: primary,
});
} finally {
fault.blocked.clear();
await scope.close();
}
});