fix(managed): share source identity and input failure ownership
This commit is contained in:
1 parent
e76f308d26
commit
4345a45982
7 files changed
+160
-40
No files matched your search
@@ -323,6 +323,14 @@ when matching declared native calls to supplied native Evidence. Static inspecti
|
||||
does not load or execute the assembly. Reconstructed structure and unresolved
|
||||
native/runtime questions retain their own authority and coverage.
|
||||
|
||||
When the selected source changes during acquisition or no longer matches its
|
||||
admitted digest, inspection and comparison report retryable `artifact_changed`;
|
||||
any completed hash retains the observed digest and byte count. Retry against a
|
||||
stable source. This differs from corrupted retained Evidence, which remains an
|
||||
Evidence-integrity failure. Malformed selected PE input has the same input-format
|
||||
classification in inspection and comparison, and a failed right-side comparison
|
||||
retains the completed left observation.
|
||||
|
||||
## Member comparison
|
||||
|
||||
Call edges and field accesses retain `il_offset` as their instruction site within
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
AnalysisInputError,
|
||||
AnalysisOutputError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { EvidenceIntegrityError } from "../../domain/evidenceErrors.js";
|
||||
import { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import {
|
||||
@@ -20,7 +19,11 @@ import {
|
||||
} from "../BinaryTargetResolver.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
import { inspectManagedMembersBytes } from "../../dotnet/ManagedMemberInspector.js";
|
||||
import { readManagedSnapshot } from "../../dotnet/ManagedSnapshot.js";
|
||||
import {
|
||||
readManagedSnapshot,
|
||||
verifyManagedSourceIdentity,
|
||||
} from "../../dotnet/ManagedSnapshot.js";
|
||||
import { managedInspectionInputFailure } from "../../dotnet/ManagedReaderFailure.js";
|
||||
import {
|
||||
MANAGED_STATIC_PROVIDER,
|
||||
MANAGED_WORKFLOW_PROVIDER,
|
||||
@@ -130,15 +133,19 @@ export const compareManagedMemberPaths = async (
|
||||
if (failure !== undefined) throw failure;
|
||||
throw cause;
|
||||
}
|
||||
if (snapshot.sha256 !== target.sha256)
|
||||
throw new EvidenceIntegrityError(
|
||||
`Managed artifact digest changed after open: expected ${target.sha256}, observed ${snapshot.sha256} at ${target.path}`,
|
||||
const admitted = verifyManagedSourceIdentity(target, snapshot, operation);
|
||||
if (!admitted.ok) throw admitted.error;
|
||||
// Member comparison never reads literals or spends their decode budget.
|
||||
let inspection: ReturnType<typeof inspectManagedMembersBytes>;
|
||||
try {
|
||||
inspection = inspectManagedMembersBytes(snapshot.bytes, target, {
|
||||
userStrings: false,
|
||||
});
|
||||
} catch (cause: unknown) {
|
||||
throw (
|
||||
managedInspectionInputFailure(operation, selectedPath, cause) ?? cause
|
||||
);
|
||||
// Member comparison never reads literals, so it does not spend the
|
||||
// decode budget on them.
|
||||
const inspection = inspectManagedMembersBytes(snapshot.bytes, target, {
|
||||
userStrings: false,
|
||||
});
|
||||
}
|
||||
const evidence = createEvidence(target, MANAGED_STATIC_PROVIDER, {
|
||||
operation: "inspect_managed_members",
|
||||
parameters: {},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AnalysisInputError } from "../domain/analysisErrorCore.js";
|
||||
import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js";
|
||||
|
||||
/** Bounded format failure retained as caller-visible managed coverage. */
|
||||
@@ -10,6 +11,22 @@ export class ManagedReaderFailure extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/** Malformed selected metadata is input failure for every inspection workflow. */
|
||||
export const managedInspectionInputFailure = (
|
||||
operation: string,
|
||||
path: string,
|
||||
cause: unknown,
|
||||
): AnalysisInputError | undefined =>
|
||||
cause instanceof ManagedReaderFailure && cause.issue.code !== "resource-limit"
|
||||
? new AnalysisInputError(operation, { cause }, [
|
||||
{
|
||||
path: ["path"],
|
||||
reason: "invalid_format",
|
||||
message: `Selected artifact ${path}: ${cause.issue.detail}`,
|
||||
},
|
||||
])
|
||||
: undefined;
|
||||
|
||||
/** Construct a typed managed parse failure at an exact byte location. */
|
||||
export const managedFailure = (
|
||||
code: ManagedParseIssue["code"],
|
||||
|
||||
@@ -17,10 +17,11 @@ import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
|
||||
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
|
||||
import { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import {
|
||||
AnalysisArtifactChangedError,
|
||||
AnalysisCancelledError,
|
||||
AnalysisResourceConstraintError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { EvidenceIntegrityError } from "../domain/evidenceErrors.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { fixedAllowanceOptions } from "../domain/fixedAllowanceRemediation.js";
|
||||
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
||||
import { RegularFileChangedError } from "../filesystem/RegularFile.js";
|
||||
@@ -39,6 +40,29 @@ interface ManagedSnapshot extends ManagedSourceObservation {
|
||||
readonly bytes: Buffer;
|
||||
}
|
||||
|
||||
/** Admit one observed source digest before any parser or cache consumes its bytes. */
|
||||
export const verifyManagedSourceIdentity = (
|
||||
target: { readonly path: string; readonly sha256: string },
|
||||
observed: Pick<ManagedSourceObservation, "sha256" | "byteLength">,
|
||||
operation: string,
|
||||
): Result<null, AnalysisArtifactChangedError> =>
|
||||
observed.sha256 === target.sha256
|
||||
? ok(null)
|
||||
: err(
|
||||
new AnalysisArtifactChangedError(
|
||||
operation,
|
||||
target.path,
|
||||
`Managed artifact digest changed after open: expected ${target.sha256}, observed ${observed.sha256} at ${target.path}`,
|
||||
{
|
||||
diagnostics: {
|
||||
expected_sha256: target.sha256,
|
||||
observed_sha256: observed.sha256,
|
||||
observed_bytes: observed.byteLength,
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const managedSnapshotCapacityError = (
|
||||
operation: string,
|
||||
): AnalysisResourceConstraintError =>
|
||||
@@ -233,10 +257,9 @@ const normalizeReadFailure = (
|
||||
if (signal?.aborted === true && cause === signal.reason)
|
||||
return new AnalysisCancelledError(operation, { cause });
|
||||
if (cause instanceof RegularFileChangedError)
|
||||
return new EvidenceIntegrityError(
|
||||
`Managed source changed while ${operation} was reading ${path}.`,
|
||||
{ cause },
|
||||
);
|
||||
return new AnalysisArtifactChangedError(operation, path, cause.message, {
|
||||
cause,
|
||||
});
|
||||
if (cause instanceof AnalysisError) return cause;
|
||||
return (
|
||||
classifyBinaryTargetFilesystemFailure(path, cause) ??
|
||||
|
||||
@@ -22,10 +22,8 @@ import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisInputError,
|
||||
AnalysisUnsupportedTargetError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { EvidenceIntegrityError } from "../domain/evidenceErrors.js";
|
||||
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
||||
import type { EvidenceLocation } from "../domain/evidence.js";
|
||||
@@ -40,11 +38,12 @@ import { inspectManagedArtifactBytes } from "./ManagedArtifactInspector.js";
|
||||
import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js";
|
||||
import { inspectManagedNativeBoundariesBytes } from "./ManagedNativeBoundaryInspector.js";
|
||||
import { RegularFileCleanupFailure } from "../filesystem/OwnedRegularFile.js";
|
||||
import { ManagedReaderFailure } from "./ManagedReaderFailure.js";
|
||||
import { managedInspectionInputFailure } from "./ManagedReaderFailure.js";
|
||||
import {
|
||||
hashManagedSource,
|
||||
managedCleanupError,
|
||||
readManagedSnapshot,
|
||||
verifyManagedSourceIdentity,
|
||||
} from "./ManagedSnapshot.js";
|
||||
|
||||
/** Execution-free managed PE/CLI auxiliary provider. */
|
||||
@@ -114,12 +113,12 @@ class ManagedStaticClient implements AnalysisClient {
|
||||
signal: options?.signal,
|
||||
resources: this.#resources,
|
||||
});
|
||||
if (observed.sha256 !== this.target.sha256)
|
||||
return err(
|
||||
new EvidenceIntegrityError(
|
||||
`Managed artifact digest changed after open: expected ${this.target.sha256}, observed ${observed.sha256} at ${this.target.path}`,
|
||||
),
|
||||
);
|
||||
const admitted = verifyManagedSourceIdentity(
|
||||
this.target,
|
||||
observed,
|
||||
operation,
|
||||
);
|
||||
if (!admitted.ok) return admitted;
|
||||
const bytes = snapshot ?? observed.bytes;
|
||||
if (bytes === undefined)
|
||||
throw new TypeError("Managed snapshot bytes are unavailable");
|
||||
@@ -148,20 +147,12 @@ class ManagedStaticClient implements AnalysisClient {
|
||||
cause: options.signal.reason,
|
||||
}),
|
||||
);
|
||||
// A PE header or section table the reader cannot map is malformed input.
|
||||
if (
|
||||
cause instanceof ManagedReaderFailure &&
|
||||
cause.issue.code !== "resource-limit"
|
||||
)
|
||||
return err(
|
||||
new AnalysisInputError(operation, { cause }, [
|
||||
{
|
||||
path: ["path"],
|
||||
reason: "invalid_format",
|
||||
message: `Selected artifact ${this.target.path}: ${cause.issue.detail}`,
|
||||
},
|
||||
]),
|
||||
);
|
||||
const inputFailure = managedInspectionInputFailure(
|
||||
operation,
|
||||
this.target.path,
|
||||
cause,
|
||||
);
|
||||
if (inputFailure !== undefined) return err(inputFailure);
|
||||
return err(
|
||||
new ProviderAdapterError(MANAGED_STATIC_PROVIDER.id, operation, {
|
||||
cause,
|
||||
|
||||
@@ -888,6 +888,41 @@ describe("managed comparison snapshot admission and cleanup", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("managed comparison malformed artifact admission", () => {
|
||||
it.each(["left", "right"] as const)(
|
||||
"retains malformed %s source classification and completed peer Evidence",
|
||||
async (side) => {
|
||||
const directory = await createTestTempDirectory("rea-managed-malformed-");
|
||||
const good = join(directory, "good.dll");
|
||||
const bad = join(directory, "bad.dll");
|
||||
const bytes = buildManagedPeFixture();
|
||||
await writeFile(good, bytes);
|
||||
bytes.writeUInt16LE(0, bytes.readUInt32LE(0x3c) + 6);
|
||||
await writeFile(bad, bytes);
|
||||
const result = await compareManagedMemberPaths({
|
||||
leftPath: side === "left" ? bad : good,
|
||||
rightPath: side === "right" ? bad : good,
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.error._tag).toBe("AnalysisInputError");
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "invalid_request",
|
||||
details: {
|
||||
issues: [
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining("PE section count"),
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(result.error.partialObservation !== undefined).toBe(
|
||||
side === "right",
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("managed comparison target identity", () => {
|
||||
it("rejects a file that changes after target identity is resolved", async () => {
|
||||
const directory = await createTestTempDirectory(
|
||||
@@ -915,7 +950,7 @@ describe("managed comparison target identity", () => {
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.error._tag).toBe("EvidenceIntegrityError");
|
||||
expect(result.error._tag).toBe("AnalysisArtifactChangedError");
|
||||
expect(result.error.message).toContain(leftPath);
|
||||
});
|
||||
});
|
||||
@@ -25,6 +25,39 @@ import { RegularFileCleanupFailure } from "../../../../src/filesystem/OwnedRegul
|
||||
import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js";
|
||||
import * as boundedFileBytes from "../../../../src/process/BoundedFileBytes.js";
|
||||
|
||||
it("classifies a source changed during managed snapshot acquisition as artifact_changed", async () => {
|
||||
const directory = await createTestTempDirectory("rea-managed-source-change-");
|
||||
const path = join(directory, "fixture.dll");
|
||||
const bytes = buildManagedPeFixture();
|
||||
await writeFile(path, bytes);
|
||||
const target = await parseBinaryTarget(path);
|
||||
if (!target.ok) throw target.error;
|
||||
const client = new ManagedStaticProvider().createClient(target.value);
|
||||
const original = boundedFileBytes.readBoundedFileBytes;
|
||||
const read = vi
|
||||
.spyOn(boundedFileBytes, "readBoundedFileBytes")
|
||||
.mockImplementation(async (...args) => {
|
||||
const observed = await original(...args);
|
||||
await writeFile(
|
||||
path,
|
||||
buildManagedPeFixture({ methodName: "ChangedAfterRead" }),
|
||||
);
|
||||
return observed;
|
||||
});
|
||||
try {
|
||||
const result = await client.execute("inspect_managed_members", {});
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok)
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "artifact_changed",
|
||||
retryable: true,
|
||||
});
|
||||
} finally {
|
||||
read.mockRestore();
|
||||
await client.close();
|
||||
}
|
||||
});
|
||||
|
||||
describe("managed static snapshot capacity failure", () => {
|
||||
it("preserves a typed capacity failure when closing the admitted file also fails", async () => {
|
||||
const directory = await createTestTempDirectory(
|
||||
@@ -330,9 +363,15 @@ describe("managed static provider path boundary", () => {
|
||||
const staleDigest = await client.execute("inspect_managed_artifact", {});
|
||||
expect(staleDigest).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "EvidenceIntegrityError" },
|
||||
error: { _tag: "AnalysisArtifactChangedError" },
|
||||
});
|
||||
|
||||
if (!staleDigest.ok)
|
||||
expect(staleDigest.error.diagnostics).toMatchObject({
|
||||
expected_sha256: parsed.value.sha256,
|
||||
observed_sha256: createHash("sha256").update(changed).digest("hex"),
|
||||
});
|
||||
|
||||
await client.close();
|
||||
const nativeClient = new ManagedStaticProvider().createClient({
|
||||
path: parsed.value.path,
|
||||
|
||||
Reference in new issue
Block a user