mirror of
https://github.com/morluto/rea.git
synced 2026-11-08 07:46:28 +00:00
* feat(native): report binary hardening for ELF, PE and Mach-O Documents inspect_binary_hardening and its CLI equivalent, including per-property states, supporting evidence, read budgets, and one report per Mach-O slice. Explains that missing protection imports remain unknown, signature presence does not establish validity, and hardened runtime requires inspect_signature. Adds hardening links throughout the documentation. The supplied diff also removes the advanced CodeQL workflow and configuration, along with snapshot-binding mismatch guidance. * fix(native): validate declared payloads and dynamic tables before hardening rows Run the shared range checks for ELF, read imports from the dynamic tables rather than section metadata, and keep an unresolved symbol count apart from an absent table. Check every Mach-O command before building rows and retain only the offsets the rows need. Make the verifier's readelf output wide, check the emitted format before parsing Mach-O, and place the sources before the libraries in the link. * fix(native): share Mach-O segment admission with hardening reads A segment with a section count its cmdsize cannot hold passed the hardening reader and could enable restrict_segment. Move the size, section count and file range checks to a shared admission that readMachoImage and the hardening reader both use, and pin the ELF load-size case with a regression test. * fix(hardening): reject Mach-O section payloads past the slice and empty ELF interpreters Segment admission never checked each section's payload range, so a file-backed section past the slice still produced rows. Decode sections with the section header owner and mark the slice malformed when a payload leaves it; zero-fill and empty sections own no file bytes and are exempt. admitInterp accepted a NUL first byte in a longer extent, which counted an empty interpreter as PIE evidence. It now requires a nonempty path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KuWHGvzJkrhCqjtEHUwqyH * fix(native): read CFG instrumentation from decoded GuardFlags Use the IMAGE_GUARD_CF_INSTRUMENTED flag that guardFlagRecord already decodes instead of masking the raw GuardFlags word a second time. * fix(native): reject ELF table spans a second PT_LOAD remaps The hardening reader mapped DT_STRTAB, DT_SYMTAB and hash tables through the one PT_LOAD containing the table's first address. A second PT_LOAD starting inside the table could map the rest of it from other file bytes and the reader still reported parsed import evidence. Cap each mapping at the next PT_LOAD that starts inside it, reject a requested range that crosses that boundary, and stop hash reads at the same limit, matching the conflicting-mapping refusal in readElfSymbols. * fix(native): dedupe ELF note tables only on shared boundaries The hardening reader dropped any note table whose bytes another table already covered. A covering SHT_NOTE can hold a non-GNU note whose descriptor is itself a complete GNU property note, while PT_NOTE names that inner note directly. Walking only the outer stream missed the GNU marker and reported CET disabled. Drop an alias only when it starts and ends on boundaries of admitted tables, which are note boundaries of the walked streams. Any other covered alias leaves the feature rows unknown, as a partial overlap already does. * fix(native): admit every Mach-O symbol record after a canary match The Mach-O stack protector scan returned as soon as it found an undefined ___stack_chk_fail, so a later undefined symbol whose n_strx lay outside the string table was never checked. The same damaged table parsed or failed as malformed depending only on record order. Keep the match and finish the bounded record walk. Records after the match are only checked against the string table bounds, so their names are not read or charged to the scan budget. The ELF reader already admits its whole symbol table before matching names. * fix(native): accept ELF load aliases with the same file translation The hardening reader treated every PT_LOAD that starts inside a table, or that also contains the table's address, as a conflicting mapping. A load with the same address-to-file translation names the same file bytes, so tables it overlaps still have one interpretation, and the canonical ElfLoadMap already merges such spans. Treat equivalent loads as one interpretation. A load with a different translation, or the zero-filled tail of an equivalent load, still ends the usable range, so a remapped table is still rejected. * fix(native): keep same-span ELF note tables with different alignments unknown A section and a program header that share endpoints were treated as one note stream even when their effective note alignments differed. Walking the same bytes at 4 and 8 can frame different notes, so dropping the second table could hide a GNU property word and report CET or BTI as disabled. Require matching effective alignment at both endpoints, and no alignment change inside the covered span, before treating a table as an alias. Otherwise mark the feature rows unknown. The fixture's .note.gnu.property section now declares the class's note alignment, as toolchains emit it. * refactor(native): read LC_SYMTAB through the shared fixed-word decoder Move the Mach-O fixed uint32 command layouts out of MachoCommandNames into domain/apple/machoLoadCommands so there is one owner of those offsets. MachoCommandNames and the hardening reader now both decode through it; the hardening reader keeps its own bounds and import policy. * fix(native): skip Mach-O common definitions in canary imports A common definition sets N_UNDF|N_EXT like an import but stores its allocation size in n_value. An object holding only `int __stack_chk_fail;` therefore reported stack_protector: enabled. Require a zero n_value before an undefined external counts as an import, for both nlist widths. * refactor(native): read linkedit data commands through the shared fixed-word decoder readMachoLinkeditDataCommand reproduced the dataoff/datasize offsets that FIXED_WORD_FIELDS already owns. Take the command and decode through decodeMachoFixedWordFields, as readMachoSymtabCommand does, keeping the truncation and endianness behavior. * fix(native): skip empty SHT_NOTE sections in ELF hardening A zero-sized SHT_NOTE owns no bytes, so the section directory marks it not file-backed and the hardening reader rejected the whole image as malformed, while readElfImage accepted the same bytes. Skip empty note sections before the file-backing check; they have nothing to walk. * fix(scripts): run hardening verifier commands through the shared owner The binary hardening verifier started its probes and compiler drivers with raw spawnSync, which bypassed the src/process/ supervision that AGENTS.md requires. Every launch now goes through createVerifierCommands, and the temporary root is removed only after the owner closes cleanly. * fix(scripts): clean up hardening verifier commands on Ctrl-C SIGINT or SIGTERM ended Node before withVerifierCleanup ran, so detached compiler trees and the temporary root could outlive the verifier. The verifier now turns those signals into an abort that every owned command receives, so cleanup closes the owner and removes the root before exit. * fix(scripts): fail the hardening verifier when a signal lands outside a command Only the owned commands read the interruption signal, so a SIGINT or SIGTERM during the final cleanup or between commands was recorded and then dropped, and the verifier exited 0. After cleanup settles, the verifier now throws the interruption reason. A failure the operation or its cleanup already raised still leaves first and is not replaced. * fix(native): fail Mach-O slices with a malformed encryption command The hardening reader turned every malformed status from the shared encryption reader into an unknown `encrypted` row, so a short LC_ENCRYPTION_INFO_64 or an encrypted range past the slice still left the slice parsed with a full report. Only two conflicting encryption commands stay an unknown row now. Any other encryption defect fails the slice like other structural command failures. * fix(native): ignore STB_LOCAL undefined ELF symbols as imports A local undefined __stack_chk_fail or __memcpy_chk is never resolved against another object, so it does not establish an external import. Skip STB_LOCAL bindings when reading protector signs, and give the fixture's positive symbols STB_GLOBAL binding. --------- Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1275 lines
46 KiB
TypeScript
1275 lines
46 KiB
TypeScript
import { PeLoadConfigProvider } from "../../src/native/pe/PeLoadConfigProvider.js";
|
|
import { BinaryHardeningProvider } from "../../src/native/hardening/BinaryHardeningProvider.js";
|
|
import { inspectPeLoadConfigInputSchema } from "../../src/domain/native/peLoadConfig.js";
|
|
import { inspectBinaryHardeningInputSchema } from "../../src/domain/native/binaryHardening.js";
|
|
import { open, writeFile, type FileHandle } from "node:fs/promises";
|
|
import { createHash } from "node:crypto";
|
|
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,
|
|
laterReadFailure: undefined as
|
|
| { position: number; 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;
|
|
};
|
|
}
|
|
if (fault.laterReadFailure !== undefined) {
|
|
const failure = fault.laterReadFailure;
|
|
const read = handle.read.bind(handle) as (
|
|
buffer: Buffer,
|
|
offset: number,
|
|
length: number,
|
|
position: number,
|
|
) => Promise<{ bytesRead: number }>;
|
|
const instrumented = handle as unknown as { read: typeof read };
|
|
let observedStart = false;
|
|
instrumented.read = async (buffer, offset, length, position) => {
|
|
if (observedStart && position > failure.position) throw failure.cause;
|
|
const result = await read(buffer, offset, length, position);
|
|
if (position === failure.position && result.bytesRead > 0)
|
|
observedStart = true;
|
|
return result;
|
|
};
|
|
}
|
|
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,
|
|
resolvedAnalysisError,
|
|
} from "../../src/domain/analysisErrorBase.js";
|
|
import { createEvidence, parseEvidence } from "../../src/domain/evidence.js";
|
|
import { projectAnalysisError } from "../../src/domain/analysisErrorProjection.js";
|
|
import {
|
|
AnalysisCancelledError,
|
|
AnalysisInputError,
|
|
AnalysisArtifactChangedError,
|
|
AnalysisAccessDeniedError,
|
|
AnalysisResourceConstraintError,
|
|
} from "../../src/domain/analysisErrorCore.js";
|
|
import {
|
|
ArtifactFileCleanupFailure,
|
|
mapArtifactFileFailure,
|
|
artifactFilePrimaryFacts,
|
|
withArtifactRegularFile,
|
|
} from "../../src/artifacts/ArtifactFileOperation.js";
|
|
import { AnalysisCleanupLease } from "../../src/artifacts/AnalysisCleanupLease.js";
|
|
import { translateArtifactFailure } from "../../src/artifacts/ArtifactOperationFailure.js";
|
|
import { OwnedFileHandle } from "../../src/filesystem/OwnedFileHandle.js";
|
|
import {
|
|
OwnedRegularFileOperationFailure,
|
|
RegularFileCleanupFailure,
|
|
regularFileCancellation,
|
|
} from "../../src/filesystem/OwnedRegularFile.js";
|
|
import { RegularFileOperationCancelled } from "../../src/filesystem/RegularFile.js";
|
|
import { ArtifactResourceScope } from "../../src/artifacts/ArtifactResourceScope.js";
|
|
import { readStableArtifact } from "../../src/artifacts/readStableArtifact.js";
|
|
import { ArtifactAnalysisResources } from "../../src/process/ArtifactAnalysisResources.js";
|
|
import { readLinuxResourceLimits } from "../../src/process/LinuxResourceLimits.js";
|
|
import { LocalWebSourceLocationArtifacts } from "../../src/browser/assets/WebSourceLocationArtifacts.js";
|
|
import { ok } from "../../src/domain/result.js";
|
|
import {
|
|
webSourceLocationArgs,
|
|
webSourceLocationFixture,
|
|
} from "../fixtures/webSourceLocation.js";
|
|
import { ArtifactReaderFailure } from "../../src/artifacts/ArtifactReader.js";
|
|
import { GoBinaryProvider } from "../../src/go/GoBinaryProvider.js";
|
|
import { PythonPycProvider } from "../../src/python/PythonPycProvider.js";
|
|
import { inspectPythonPycInputSchema } from "../../src/domain/python/pythonPyc.js";
|
|
import { PeResourcesProvider } from "../../src/native/pe/PeResourcesProvider.js";
|
|
import { Il2cppMetadataProvider } from "../../src/il2cpp/Il2cppMetadataProvider.js";
|
|
import { ElfNotesProvider } from "../../src/native/elf/ElfNotesProvider.js";
|
|
import { AppleIpsProvider } from "../../src/native/apple/AppleIpsProvider.js";
|
|
import { KernelModuleProvider } from "../../src/native/elf/KernelModuleProvider.js";
|
|
import { PeCertificatesProvider } from "../../src/native/pe/PeCertificatesProvider.js";
|
|
import { PeImportsProvider } from "../../src/native/pe/PeImportsProvider.js";
|
|
import { NativeExportsProvider } from "../../src/native/exports/NativeExportsProvider.js";
|
|
import { BeamProvider } from "../../src/erlang/BeamProvider.js";
|
|
import { inspectElfNotesInputSchema } from "../../src/domain/native/elfNotes.js";
|
|
import { inspectAppleIpsInputSchema } from "../../src/domain/native/appleIps.js";
|
|
import { inspectKernelModuleInputSchema } from "../../src/domain/native/kernelModule.js";
|
|
import { inspectPeCertificatesInputSchema } from "../../src/domain/native/peCertificates.js";
|
|
import { inspectPeImportsInputSchema } from "../../src/domain/native/peImports.js";
|
|
import { inspectNativeExportsInputSchema } from "../../src/domain/native/nativeExports.js";
|
|
import { inspectErlangBeamInputSchema } from "../../src/domain/erlang/beam.js";
|
|
import { ModelArtifactProvider } from "../../src/model/ModelArtifactProvider.js";
|
|
import { ggufFixture } from "../fixtures/model/model.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 { createIl2cppMetadataFixture } from "../fixtures/il2cpp/metadata.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;
|
|
fault.laterReadFailure = undefined;
|
|
});
|
|
|
|
const staticProviders = [
|
|
["Rust", () => new RustBinaryProvider()],
|
|
["Go", () => new GoBinaryProvider()],
|
|
["PE", () => new PeResourcesProvider()],
|
|
["IL2CPP metadata", () => new Il2cppMetadataProvider()],
|
|
] as const;
|
|
|
|
it.each(
|
|
["Rust service", "selected producer input", "source map"].flatMap(
|
|
(consumer) =>
|
|
["close failure", "close cancellation", "physical failure"].map(
|
|
(outcome) => ({ consumer, outcome }),
|
|
),
|
|
),
|
|
)(
|
|
"retains completed caller input for $consumer through $outcome",
|
|
async ({ consumer, outcome }) => {
|
|
const path = await selectedImage();
|
|
const bytes = Buffer.from("selected completed input");
|
|
await writeFile(path, bytes);
|
|
const controller = new AbortController();
|
|
const reason = { source: "caller input finalization" };
|
|
const physical = Object.assign(new Error("input metadata denied"), {
|
|
code: "EACCES",
|
|
});
|
|
if (outcome !== "close failure")
|
|
fault.onClose = () => controller.abort(reason);
|
|
if (outcome === "physical failure") fault.readFailure = { cause: physical };
|
|
const { sourceMap: _sourceMap, ...script } = webSourceLocationFixture();
|
|
const rust = new RustBinaryService(new RustBinaryProvider());
|
|
const web = new LocalWebSourceLocationArtifacts({
|
|
load: () => Promise.resolve(ok(script)),
|
|
});
|
|
const resources = new ArtifactAnalysisResources(
|
|
"selected-test",
|
|
"inspect_signal_recording",
|
|
readStableArtifact,
|
|
readLinuxResourceLimits,
|
|
{ message: "busy", details: {} },
|
|
);
|
|
const close = () =>
|
|
consumer === "Rust service"
|
|
? rust.close()
|
|
: consumer === "source map"
|
|
? web.close()
|
|
: resources.close();
|
|
try {
|
|
const result =
|
|
consumer === "Rust service"
|
|
? await rust.inspect({ path }, { signal: controller.signal })
|
|
: consumer === "source map"
|
|
? await web.load(
|
|
{
|
|
...webSourceLocationArgs,
|
|
source_map: { ...webSourceLocationArgs.source_map, path },
|
|
},
|
|
{ signal: controller.signal },
|
|
)
|
|
: await resources.run(async () =>
|
|
ok(
|
|
await resources.readSelectedArtifact({
|
|
path,
|
|
maximumBytes: 1024,
|
|
allowanceName: "fixture input",
|
|
observedOutput: undefined,
|
|
signal: controller.signal,
|
|
}),
|
|
),
|
|
);
|
|
if (result.ok)
|
|
throw new Error("Expected descriptor finalization failure");
|
|
expect(result.error.cleanupIncomplete).toBe(true);
|
|
const snapshot = {
|
|
path,
|
|
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
bytes: bytes.length,
|
|
};
|
|
expect(result.error.partialObservation).toEqual(
|
|
consumer === "source map"
|
|
? {
|
|
kind: "web-artifact-acquisition",
|
|
analysis: "not-performed",
|
|
files: [
|
|
{
|
|
role: "manifest",
|
|
file: script.manifestFile,
|
|
admission: "manifest-validated",
|
|
},
|
|
{
|
|
role: "source",
|
|
file: script.sourceFile,
|
|
admission: "source-verified",
|
|
},
|
|
...(outcome === "physical failure"
|
|
? []
|
|
: [
|
|
{ role: "source-map", file: snapshot, admission: "read" },
|
|
]),
|
|
],
|
|
}
|
|
: outcome === "physical failure"
|
|
? undefined
|
|
: { kind: "file-snapshot-hash", ...snapshot },
|
|
);
|
|
const beforeCleanup = projectAnalysisError(result.error).details
|
|
?.partial_observation;
|
|
if (outcome === "physical failure")
|
|
expect(result.error.cleanupPrimary?.cause).toBe(physical);
|
|
if (outcome === "close cancellation")
|
|
expect(result.error.cleanupPrimary?.cause).toBe(reason);
|
|
await expect(close()).rejects.toBeDefined();
|
|
expect(result.error.cleanupIncomplete).toBe(true);
|
|
fault.blocked.clear();
|
|
await close();
|
|
expect(result.error.cleanupIncomplete).toBe(false);
|
|
if (consumer === "source map")
|
|
expect(
|
|
projectAnalysisError(resolvedAnalysisError(result.error)).details
|
|
?.partial_observation,
|
|
).toEqual(beforeCleanup);
|
|
expect(fault.handles).toHaveLength(1);
|
|
expect(fault.handles[0]?.fd).toBe(-1);
|
|
} finally {
|
|
fault.onClose = undefined;
|
|
fault.blocked.clear();
|
|
await close();
|
|
}
|
|
},
|
|
);
|
|
|
|
const expectIl2cppSource = (
|
|
error: AnalysisError,
|
|
path: string,
|
|
bytes: Buffer,
|
|
) => {
|
|
const source = parseEvidence(error.partialObservation);
|
|
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
expect(source.operation).toBe("observe_il2cpp_metadata_source");
|
|
expect(source.subject).toMatchObject({
|
|
local_path: path,
|
|
digest: { sha256 },
|
|
});
|
|
expect(source.normalized_result).toEqual({
|
|
artifact: { path, sha256, bytes: bytes.length },
|
|
inventory_status: "not-returned-as-complete",
|
|
});
|
|
};
|
|
|
|
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);
|
|
if (provider instanceof Il2cppMetadataProvider)
|
|
await writeFile(path, createIl2cppMetadataFixture().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);
|
|
if (provider instanceof Il2cppMetadataProvider)
|
|
expectIl2cppSource(
|
|
result.error,
|
|
path,
|
|
createIl2cppMetadataFixture().bytes,
|
|
);
|
|
expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true);
|
|
} finally {
|
|
fault.blocked.clear();
|
|
await provider.close();
|
|
}
|
|
},
|
|
);
|
|
|
|
it.each([false, true])(
|
|
"retains completed IL2CPP source facts through failed close (cancel: %s)",
|
|
async (cancel) => {
|
|
const path = await selectedImage();
|
|
const { bytes } = createIl2cppMetadataFixture();
|
|
await writeFile(path, bytes);
|
|
const provider = new Il2cppMetadataProvider();
|
|
const controller = new AbortController();
|
|
const reason = new Error("cancel during failed close");
|
|
if (cancel) fault.onClose = () => controller.abort(reason);
|
|
try {
|
|
const result = await provider.inspect(
|
|
{ path },
|
|
{ signal: controller.signal },
|
|
);
|
|
if (result.ok) throw new Error("Expected failed close");
|
|
if (cancel) {
|
|
expect(result.error.cleanupPrimary).toBeInstanceOf(
|
|
AnalysisCancelledError,
|
|
);
|
|
expect(result.error.cleanupPrimary?.cause).toBe(reason);
|
|
}
|
|
expect(result.error.cleanupIncomplete).toBe(true);
|
|
expectIl2cppSource(result.error, path, bytes);
|
|
expect(fault.handles).toHaveLength(1);
|
|
expect(fault.handles[0]?.fd).toBeGreaterThanOrEqual(0);
|
|
fault.blocked.clear();
|
|
await provider.close();
|
|
expect(result.error.cleanupIncomplete).toBe(false);
|
|
expect(fault.handles[0]?.fd).toBe(-1);
|
|
expect(
|
|
projectAnalysisError(result.error).details?.partial_observation,
|
|
).toEqual(result.error.partialObservation);
|
|
} 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);
|
|
if (provider instanceof Il2cppMetadataProvider)
|
|
expect(result.error.partialObservation).toBeUndefined();
|
|
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);
|
|
if (provider instanceof Il2cppMetadataProvider)
|
|
expect(result.error.partialObservation).toBeUndefined();
|
|
fault.blocked.clear();
|
|
await provider.close();
|
|
expect(result.error.cleanupIncomplete).toBe(false);
|
|
} finally {
|
|
fault.blocked.clear();
|
|
await provider.close();
|
|
}
|
|
},
|
|
);
|
|
it("clears the Python pyc cleanup flag once a retained descriptor closes", async () => {
|
|
const path = await selectedImage();
|
|
const provider = new PythonPycProvider();
|
|
const primary = new ArtifactReaderFailure(
|
|
"integrity",
|
|
"controlled source replacement",
|
|
);
|
|
fault.readFailure = { cause: primary };
|
|
try {
|
|
const result = await provider.inspect(
|
|
inspectPythonPycInputSchema.parse({ path }),
|
|
);
|
|
if (result.ok)
|
|
throw new Error("Expected primary failure and retained descriptor");
|
|
expect(result.error.cleanupIncomplete).toBe(true);
|
|
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;
|
|
};
|
|
|
|
it.each([
|
|
"confirmed-close-cancel",
|
|
"failed-close",
|
|
"failed-close-cancel",
|
|
] as const)(
|
|
"retains complete scoped model observations through %s and retries the exact file owner",
|
|
async (mode) => {
|
|
const path = await selectedImage();
|
|
await writeFile(path, ggufFixture().bytes);
|
|
const provider = new ModelArtifactProvider();
|
|
const controller = new AbortController();
|
|
const reason = new Error("model close cancellation");
|
|
fault.blockNew = mode !== "confirmed-close-cancel";
|
|
if (mode !== "failed-close") fault.onClose = () => controller.abort(reason);
|
|
try {
|
|
const result = await provider.inspect(
|
|
{ path, hash: "header" },
|
|
{ signal: controller.signal },
|
|
);
|
|
if (result.ok) throw new Error("Expected finalization failure");
|
|
const evidence = parseEvidence(result.error.partialObservation);
|
|
expect(evidence.operation).toBe("inspect_model_artifact");
|
|
expect(evidence.subject).toBeNull();
|
|
expect(evidence.normalized_result).toMatchObject({
|
|
artifact: { path, sha256: null, read_scope: "header" },
|
|
tensors: [{ name: "weight" }, { name: "quant" }],
|
|
});
|
|
if (mode === "confirmed-close-cancel") {
|
|
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
|
|
expect(result.error.cause).toBe(reason);
|
|
expect(fault.handles[0]?.fd).toBe(-1);
|
|
} else {
|
|
expect(result.error.cleanupIncomplete).toBe(true);
|
|
if (mode === "failed-close-cancel")
|
|
expect(result.error.cleanupPrimary?.cause).toBe(reason);
|
|
expect(fault.handles[0]?.fd).toBeGreaterThanOrEqual(0);
|
|
fault.blocked.clear();
|
|
await provider.close();
|
|
expect(result.error.cleanupIncomplete).toBe(false);
|
|
expect(fault.handles[0]?.fd).toBe(-1);
|
|
}
|
|
} finally {
|
|
fault.onClose = undefined;
|
|
fault.blocked.clear();
|
|
await provider.close();
|
|
}
|
|
},
|
|
);
|
|
|
|
it("preserves a model permission failure when cancellation arrives during close", async () => {
|
|
const path = await selectedImage();
|
|
const provider = new ModelArtifactProvider();
|
|
const denied = Object.assign(new Error("model read denied"), {
|
|
code: "EACCES",
|
|
});
|
|
fault.readFailure = { cause: denied };
|
|
fault.blockNew = false;
|
|
const controller = new AbortController();
|
|
fault.onClose = () => controller.abort(new Error("later stop"));
|
|
try {
|
|
const result = await provider.inspect(
|
|
{ path, hash: "header" },
|
|
{ signal: controller.signal },
|
|
);
|
|
if (result.ok) throw new Error("Read failure disappeared");
|
|
expect(result.error).toBeInstanceOf(AnalysisAccessDeniedError);
|
|
expect(result.error.cause).toBe(denied);
|
|
expect(result.error.partialObservation).toBeUndefined();
|
|
} finally {
|
|
fault.onClose = undefined;
|
|
await provider.close();
|
|
}
|
|
});
|
|
|
|
it("retains earlier GGUF rows and physical failure while retrying failed descriptor cleanup", async () => {
|
|
const path = await selectedImage();
|
|
const seed = ggufFixture();
|
|
const count = 200_000;
|
|
const fixture = ggufFixture({
|
|
tensors: [],
|
|
metadata: [
|
|
{ key: "general.name", type: 8, value: seed.text("First row") },
|
|
{
|
|
key: "custom.bools",
|
|
type: 9,
|
|
value: Buffer.concat([
|
|
seed.u32(7),
|
|
seed.u64(BigInt(count)),
|
|
Buffer.alloc(count),
|
|
]),
|
|
},
|
|
],
|
|
});
|
|
await writeFile(path, fixture.bytes);
|
|
const arrayStart =
|
|
24 +
|
|
8 +
|
|
Buffer.byteLength("general.name") +
|
|
4 +
|
|
8 +
|
|
Buffer.byteLength("First row") +
|
|
8 +
|
|
Buffer.byteLength("custom.bools") +
|
|
4 +
|
|
4 +
|
|
8;
|
|
const denied = Object.assign(new Error("controlled later GGUF read denial"), {
|
|
code: "EACCES",
|
|
});
|
|
fault.laterReadFailure = { position: arrayStart, cause: denied };
|
|
const provider = new ModelArtifactProvider();
|
|
try {
|
|
const result = await provider.inspect({ path, hash: "header" });
|
|
if (result.ok) throw new Error("Expected physical read failure");
|
|
expect(result.error.cleanupPrimary).toBeInstanceOf(
|
|
AnalysisAccessDeniedError,
|
|
);
|
|
expect(result.error.cleanupPrimary?.cause).toBe(denied);
|
|
expect(result.error.cleanupIncomplete).toBe(true);
|
|
const partial = parseEvidence(result.error.partialObservation);
|
|
expect(partial.normalized_result).toMatchObject({
|
|
kind: "model-artifact-partial",
|
|
metadata: [{ index: 0, key: "general.name", value: "First row" }],
|
|
artifact: {
|
|
path,
|
|
header_sha256: null,
|
|
sha256: null,
|
|
file_identity: "unverified",
|
|
},
|
|
});
|
|
expect(fault.handles).toHaveLength(1);
|
|
expect(fault.handles[0]?.fd).toBeGreaterThanOrEqual(0);
|
|
fault.blocked.clear();
|
|
await provider.close();
|
|
expect(result.error.cleanupIncomplete).toBe(false);
|
|
expect(parseEvidence(result.error.partialObservation)).toEqual(partial);
|
|
} finally {
|
|
fault.blocked.clear();
|
|
await provider.close();
|
|
}
|
|
});
|
|
|
|
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();
|
|
}
|
|
});
|
|
|
|
it.each([
|
|
"physical cancellation",
|
|
"mapped foreign artifact",
|
|
"foreign nested artifact",
|
|
] as const)(
|
|
"preserves %s provenance while retrying only the owned descriptor",
|
|
async (shape) => {
|
|
const path = await selectedImage();
|
|
const foreignPath = join(path, "..", "foreign.txt");
|
|
await writeFile(foreignPath, "foreign owner bytes");
|
|
const foreign = new OwnedFileHandle(await open(foreignPath, "r"));
|
|
const foreignLease = AnalysisCleanupLease.forCloseOwner(
|
|
foreign,
|
|
foreignPath,
|
|
);
|
|
const foreignPartial = createEvidence(
|
|
undefined,
|
|
{ id: "foreign", name: "foreign owner", version: null },
|
|
{
|
|
operation: "inspect_artifact",
|
|
parameters: {},
|
|
result: { foreign: true },
|
|
confidence: "observed",
|
|
},
|
|
);
|
|
const foreignReason = { source: "foreign cancellation" };
|
|
const foreignFailure = new ArtifactReaderFailure(
|
|
"cancelled",
|
|
"foreign artifact failure",
|
|
{
|
|
cause: foreignReason,
|
|
cancellation: { reason: foreignReason },
|
|
cleanup: { resources: [foreignPath], reason: "foreign cleanup" },
|
|
cleanupLeases: [foreignLease],
|
|
partialObservation: foreignPartial,
|
|
},
|
|
);
|
|
const raw =
|
|
shape === "physical cancellation"
|
|
? new RegularFileOperationCancelled(foreignReason)
|
|
: shape === "foreign nested artifact"
|
|
? new ArtifactFileCleanupFailure(
|
|
new RegularFileCleanupFailure({
|
|
path: foreignPath,
|
|
owner: foreign,
|
|
outcome: { kind: "failed", cause: foreignFailure },
|
|
cleanupCause: new Error("foreign descriptor close failure"),
|
|
}),
|
|
foreignLease,
|
|
)
|
|
: foreignFailure;
|
|
const marker = new OwnedRegularFileOperationFailure(raw);
|
|
const controller = new AbortController();
|
|
const lateReason = { source: "late owned-close abort" };
|
|
fault.onClose = () => controller.abort(lateReason);
|
|
const scope = new ArtifactResourceScope();
|
|
try {
|
|
const failure = await scope
|
|
.run(() =>
|
|
withArtifactRegularFile(path, scope, controller.signal, async () => {
|
|
throw marker;
|
|
}),
|
|
)
|
|
.catch((cause: unknown) => cause);
|
|
if (!(failure instanceof ArtifactFileCleanupFailure))
|
|
throw new Error("Expected the actual input descriptor's failed close");
|
|
expect(artifactFilePrimaryFacts(failure)).toEqual({
|
|
cause: raw,
|
|
origin: "opaque-operation",
|
|
});
|
|
const projected = mapArtifactFileFailure(failure, (primary) => {
|
|
expect(primary).toBe(raw);
|
|
return shape === "mapped foreign artifact"
|
|
? new ArtifactReaderFailure(
|
|
"unavailable",
|
|
"authored permission classification",
|
|
{ cause: raw },
|
|
)
|
|
: primary;
|
|
});
|
|
if (!(projected instanceof ArtifactFileCleanupFailure))
|
|
throw new Error(
|
|
"Expected the same owned descriptor envelope after projection",
|
|
);
|
|
for (const envelope of [failure, projected]) {
|
|
expect(envelope.reason).toBe(
|
|
envelope === projected && shape === "mapped foreign artifact"
|
|
? "unavailable"
|
|
: "io",
|
|
);
|
|
expect(envelope.cancellation).toBeUndefined();
|
|
expect(envelope.partialObservation).toBeUndefined();
|
|
expect(envelope.cleanup?.resources).toEqual([path]);
|
|
expect(
|
|
regularFileCancellation(envelope.fileCleanupFailure),
|
|
).toBeUndefined();
|
|
}
|
|
expect(failure.fileCleanupFailure.outcome).toEqual({
|
|
kind: "failed",
|
|
cause: raw,
|
|
});
|
|
expect(projected.fileCleanupFailure.operationFailure).toBe(marker);
|
|
expect(artifactFilePrimaryFacts(projected).cause).toBe(
|
|
projected.fileCleanupFailure.outcome.kind === "failed"
|
|
? projected.fileCleanupFailure.outcome.cause
|
|
: undefined,
|
|
);
|
|
expect(artifactFilePrimaryFacts(projected).origin).toBe(
|
|
shape === "mapped foreign artifact"
|
|
? "owned-or-classified"
|
|
: "opaque-operation",
|
|
);
|
|
expect(marker.operationCause).toBe(raw);
|
|
expect(projected.cleanupLease).toBe(failure.cleanupLease);
|
|
expect(projected.cleanupLeases).toEqual([failure.cleanupLease]);
|
|
const error = translateArtifactFailure("inspect_artifact", projected);
|
|
expect(error.cleanupResources).toEqual([path]);
|
|
expect(error.cleanupIncomplete).toBe(true);
|
|
expect(error.partialObservation).toBeUndefined();
|
|
expect(controller.signal.reason).toBe(lateReason);
|
|
expect(fault.handles).toHaveLength(1);
|
|
expect(fault.handles[0]!.fd).toBeGreaterThanOrEqual(0);
|
|
await expect(scope.close()).rejects.toThrow();
|
|
expect(foreign.handle.fd).toBeGreaterThanOrEqual(0);
|
|
fault.blocked.clear();
|
|
await scope.close();
|
|
expect(fault.handles[0]!.fd).toBe(-1);
|
|
expect(error.cleanupIncomplete).toBe(false);
|
|
expect(error.cleanupResources).toEqual([]);
|
|
expect(foreign.handle.fd).toBeGreaterThanOrEqual(0);
|
|
expect(foreignLease.released).toBe(false);
|
|
expect(foreignFailure.cleanup?.resources).toEqual([foreignPath]);
|
|
expect(foreignFailure.partialObservation).toBe(foreignPartial);
|
|
} finally {
|
|
fault.onClose = undefined;
|
|
fault.blocked.clear();
|
|
await scope.close();
|
|
await foreign.close();
|
|
}
|
|
},
|
|
);
|
|
|
|
const stableArtifactProviders = [
|
|
...staticProviders,
|
|
["ELF notes", () => new ElfNotesProvider()],
|
|
["Apple IPS", () => new AppleIpsProvider()],
|
|
["kernel module", () => new KernelModuleProvider()],
|
|
["PE certificates", () => new PeCertificatesProvider()],
|
|
["PE load configuration", () => new PeLoadConfigProvider()],
|
|
["binary hardening", () => new BinaryHardeningProvider()],
|
|
["PE imports", () => new PeImportsProvider()],
|
|
["native exports", () => new NativeExportsProvider()],
|
|
["Python pyc", () => new PythonPycProvider()],
|
|
] as const;
|
|
|
|
const fileFailureProviders = [
|
|
...stableArtifactProviders,
|
|
["BEAM", () => new BeamProvider()],
|
|
] as const;
|
|
|
|
const inspectFileFailure = (
|
|
provider: ReturnType<(typeof fileFailureProviders)[number][1]>,
|
|
path: string,
|
|
signal?: AbortSignal,
|
|
) => {
|
|
const options = signal === undefined ? undefined : { signal };
|
|
if (provider instanceof PythonPycProvider)
|
|
return provider.inspect(
|
|
inspectPythonPycInputSchema.parse({ path }),
|
|
options,
|
|
);
|
|
if (provider instanceof ElfNotesProvider)
|
|
return provider.inspect(
|
|
inspectElfNotesInputSchema.parse({ path }),
|
|
options,
|
|
);
|
|
if (provider instanceof AppleIpsProvider)
|
|
return provider.inspect(
|
|
inspectAppleIpsInputSchema.parse({ path }),
|
|
options,
|
|
);
|
|
if (provider instanceof KernelModuleProvider)
|
|
return provider.inspect(
|
|
inspectKernelModuleInputSchema.parse({ path }),
|
|
options,
|
|
);
|
|
if (provider instanceof PeCertificatesProvider)
|
|
return provider.inspect(
|
|
inspectPeCertificatesInputSchema.parse({ path }),
|
|
options,
|
|
);
|
|
if (provider instanceof PeLoadConfigProvider)
|
|
return provider.inspect(
|
|
inspectPeLoadConfigInputSchema.parse({ path }),
|
|
options,
|
|
);
|
|
if (provider instanceof BinaryHardeningProvider)
|
|
return provider.inspect(
|
|
inspectBinaryHardeningInputSchema.parse({ path }),
|
|
options,
|
|
);
|
|
if (provider instanceof PeImportsProvider)
|
|
return provider.inspect(
|
|
inspectPeImportsInputSchema.parse({ path }),
|
|
options,
|
|
);
|
|
if (provider instanceof NativeExportsProvider)
|
|
return provider.inspect(
|
|
inspectNativeExportsInputSchema.parse({ path }),
|
|
options,
|
|
);
|
|
if (provider instanceof BeamProvider)
|
|
return provider.inspect(
|
|
inspectErlangBeamInputSchema.parse({ path }),
|
|
options,
|
|
);
|
|
if (provider instanceof PeResourcesProvider)
|
|
return provider.inspect(
|
|
inspectPeResourcesInputSchema.parse({ path }),
|
|
options,
|
|
);
|
|
return provider.inspect({ path }, options);
|
|
};
|
|
|
|
it.each(
|
|
stableArtifactProviders.flatMap(([name, makeProvider]) =>
|
|
[
|
|
["ESTALE", "AnalysisInputError"],
|
|
["ENOTFILE", "AnalysisInputError"],
|
|
["EISDIR", "AnalysisInputError"],
|
|
["ELOOP", "AnalysisInputError"],
|
|
["EACCES", "AnalysisAccessDeniedError"],
|
|
["EIO", undefined],
|
|
].map(([code, expectedTag]) => ({ name, makeProvider, code, expectedTag })),
|
|
),
|
|
)(
|
|
"preserves $name descriptor failure classification for $code",
|
|
async ({ makeProvider, code, expectedTag }) => {
|
|
const path = await selectedImage();
|
|
const provider = makeProvider();
|
|
const physical = Object.assign(new Error("descriptor metadata failed"), {
|
|
code,
|
|
});
|
|
fault.blockNew = false;
|
|
fault.readFailure = { cause: physical };
|
|
try {
|
|
const result = await inspectFileFailure(provider, path);
|
|
if (result.ok) throw new Error("Expected physical read failure");
|
|
expect(result.error.cause).toBe(physical);
|
|
expect(result.error._tag).toBe(
|
|
expectedTag ??
|
|
(provider instanceof ElfNotesProvider ||
|
|
provider instanceof KernelModuleProvider ||
|
|
provider instanceof PeCertificatesProvider ||
|
|
provider instanceof PeResourcesProvider
|
|
? "ArtifactOperationError"
|
|
: "ProviderAdapterError"),
|
|
);
|
|
if (result.error instanceof AnalysisInputError)
|
|
expect(result.error.issues).toEqual([
|
|
{
|
|
path: ["path"],
|
|
reason: "invalid_value",
|
|
message: expect.stringContaining(path),
|
|
},
|
|
]);
|
|
expect(fault.handles).toHaveLength(1);
|
|
expect(fault.handles[0]!.fd).toBe(-1);
|
|
} finally {
|
|
await provider.close();
|
|
}
|
|
},
|
|
);
|
|
|
|
it.each(fileFailureProviders)(
|
|
"uses the original %s physical failure across nested descriptor owners",
|
|
async (_name, makeProvider) => {
|
|
const path = await selectedImage();
|
|
const scope = new ArtifactResourceScope();
|
|
const provider = makeProvider();
|
|
const primary = new ArtifactReaderFailure(
|
|
"integrity",
|
|
"original source replacement",
|
|
);
|
|
try {
|
|
const nested = await scope
|
|
.run(() =>
|
|
withArtifactRegularFile(path, scope, undefined, async () => {
|
|
throw primary;
|
|
}),
|
|
)
|
|
.catch((cause: unknown) => cause);
|
|
expect(nested).toBeInstanceOf(ArtifactFileCleanupFailure);
|
|
fault.readFailure = { cause: nested };
|
|
const result = await inspectFileFailure(provider, path);
|
|
if (result.ok) throw new Error("Expected nested physical failure");
|
|
expect(result.error.cleanupPrimary).toBeInstanceOf(
|
|
AnalysisArtifactChangedError,
|
|
);
|
|
expect(result.error.cleanupPrimary?.cause).toBe(primary);
|
|
expect(result.error.cleanupIncomplete).toBe(true);
|
|
expect(fault.handles).toHaveLength(2);
|
|
fault.blocked.delete(fault.handles[1]!);
|
|
await provider.close();
|
|
expect(fault.handles[1]?.fd).toBe(-1);
|
|
expect(fault.handles[0]?.fd).toBeGreaterThanOrEqual(0);
|
|
expect(result.error.cleanupIncomplete).toBe(true);
|
|
fault.blocked.clear();
|
|
await scope.close();
|
|
expect(result.error.cleanupIncomplete).toBe(false);
|
|
} finally {
|
|
fault.blocked.clear();
|
|
await provider.close();
|
|
await scope.close();
|
|
}
|
|
},
|
|
);
|
|
|
|
it.each(
|
|
fileFailureProviders.flatMap(([name, makeProvider]) =>
|
|
["recorded", "later"].map((timing) => ({ name, makeProvider, timing })),
|
|
),
|
|
)(
|
|
"classifies $name finalization using the owner with $timing cancellation",
|
|
async ({ makeProvider, timing }) => {
|
|
const path = await selectedImage();
|
|
const scope = new ArtifactResourceScope();
|
|
const provider = makeProvider();
|
|
const controller = new AbortController();
|
|
const reason = new Error("file owner cancellation");
|
|
if (timing === "recorded") fault.onClose = () => controller.abort(reason);
|
|
let run: ReturnType<typeof vi.spyOn> | undefined;
|
|
try {
|
|
const failure = await scope
|
|
.run(() =>
|
|
readStableArtifact(
|
|
path,
|
|
1024 * 1024,
|
|
timing === "recorded" ? controller.signal : undefined,
|
|
undefined,
|
|
scope,
|
|
),
|
|
)
|
|
.catch((cause: unknown) => cause);
|
|
expect(failure).toBeInstanceOf(ArtifactFileCleanupFailure);
|
|
// Inject a real completed-close failure at the provider's acquisition
|
|
// boundary, after the owner has recorded its outcome and cancellation.
|
|
run = vi
|
|
.spyOn(ArtifactResourceScope.prototype, "run")
|
|
.mockImplementationOnce(async () => {
|
|
if (timing === "later") controller.abort(reason);
|
|
throw failure;
|
|
});
|
|
const result = await inspectFileFailure(
|
|
provider,
|
|
path,
|
|
timing === "later" ? controller.signal : undefined,
|
|
);
|
|
run.mockRestore();
|
|
run = undefined;
|
|
if (result.ok) throw new Error("Expected retained close failure");
|
|
expect(result.error.cleanupIncomplete).toBe(true);
|
|
if (timing === "recorded") {
|
|
expect(result.error.cleanupPrimary).toBeInstanceOf(
|
|
AnalysisCancelledError,
|
|
);
|
|
expect(result.error.cleanupPrimary?.cause).toBe(reason);
|
|
} else
|
|
expect(result.error.cleanupPrimary).not.toBeInstanceOf(
|
|
AnalysisCancelledError,
|
|
);
|
|
fault.blocked.clear();
|
|
await scope.close();
|
|
expect(result.error.cleanupIncomplete).toBe(false);
|
|
} finally {
|
|
run?.mockRestore();
|
|
fault.onClose = undefined;
|
|
fault.blocked.clear();
|
|
await provider.close();
|
|
await scope.close();
|
|
}
|
|
},
|
|
);
|
|
|
|
it.each([false, true])(
|
|
"preserves a pyc permission failure through late cancellation (failed close: %s)",
|
|
async (failedClose) => {
|
|
const path = await selectedImage();
|
|
const provider = new PythonPycProvider();
|
|
const controller = new AbortController();
|
|
const physical = Object.assign(new Error("pyc descriptor stat denied"), {
|
|
code: "EACCES",
|
|
});
|
|
fault.blockNew = failedClose;
|
|
fault.readFailure = { cause: physical };
|
|
fault.onClose = () => controller.abort(physical);
|
|
try {
|
|
const result = await provider.inspect(
|
|
inspectPythonPycInputSchema.parse({ path }),
|
|
{ signal: controller.signal },
|
|
);
|
|
if (result.ok) throw new Error("Expected physical read failure");
|
|
const primary = result.error.cleanupPrimary ?? result.error;
|
|
expect(primary).toBeInstanceOf(AnalysisAccessDeniedError);
|
|
expect(primary.cause).toBe(physical);
|
|
expect(result.error.cleanupIncomplete).toBe(failedClose);
|
|
expect(fault.handles).toHaveLength(1);
|
|
expect(fault.handles[0]!.fd >= 0).toBe(failedClose);
|
|
fault.blocked.clear();
|
|
await provider.close();
|
|
expect(result.error.cleanupIncomplete).toBe(false);
|
|
expect(fault.handles[0]!.fd).toBe(-1);
|
|
} finally {
|
|
fault.onClose = undefined;
|
|
fault.blocked.clear();
|
|
await provider.close();
|
|
}
|
|
},
|
|
);
|