mirror of
https://github.com/morluto/rea.git
synced 2026-10-27 01:45:32 +00:00
fix(nativeaot): retain snapshot digest mismatch and recovery
This commit is contained in:
1 parent
fc05f68406
commit
d8e90fe621
6 files changed
+99
-8
No files matched your search
@@ -131,6 +131,11 @@ establishing their absence. Performance on the large bflat report has not been
|
||||
measured; this change removes repeated closure scans rather than resolving
|
||||
that separate discovery limitation.
|
||||
|
||||
If captured snapshot bytes no longer match their admitted digest, the parser
|
||||
retains both digests and the Ghidra owner reports `artifact_changed`, with the
|
||||
captured byte count and admitted identity still available. Reopen acquisition
|
||||
against a stable artifact; this is distinct from corrupted retained Evidence.
|
||||
|
||||
The optional adapter performs its annotations in an ephemeral Program
|
||||
transaction. That workflow may change analysis-memory bytes, never the original
|
||||
executable file. It is unavailable on Windows P0, which has no database
|
||||
|
||||
@@ -120,11 +120,18 @@ describe("read-only NativeAOT PE parser", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("fails closed on snapshot identity mismatch", () => {
|
||||
it("fails closed on snapshot identity mismatch while preserving observed identity", () => {
|
||||
const bytes = nativeAotPeFixture();
|
||||
expect(() =>
|
||||
parseNativeAotPe(bytes, "0".repeat(64), SUMMARY_BUDGET),
|
||||
).toThrow(/digest/iu);
|
||||
try {
|
||||
parseNativeAotPe(bytes, "0".repeat(64), SUMMARY_BUDGET);
|
||||
throw new Error("Expected source identity mismatch");
|
||||
} catch (cause: unknown) {
|
||||
expect(cause).toMatchObject({
|
||||
name: "NativeAotSnapshotIdentityError",
|
||||
expectedSha256: "0".repeat(64),
|
||||
observedSha256: nativeAotPeDigest(bytes),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("reports a detected but unsupported RTR version as partial without guessed types", () => {
|
||||
|
||||
@@ -73,6 +73,19 @@ class NativeAotParserBudget {
|
||||
}
|
||||
}
|
||||
|
||||
/** Source-byte identity mismatch retained for adapter-specific recovery. */
|
||||
export class NativeAotSnapshotIdentityError extends Error {
|
||||
constructor(
|
||||
readonly expectedSha256: string,
|
||||
readonly observedSha256: string,
|
||||
) {
|
||||
super(
|
||||
`NativeAOT snapshot digest does not match the admitted artifact identity: expected ${expectedSha256}, observed ${observedSha256}`,
|
||||
);
|
||||
this.name = "NativeAotSnapshotIdentityError";
|
||||
}
|
||||
}
|
||||
|
||||
/** Hydration stops by throwing; other scans record truncation and continue. */
|
||||
class NativeAotParserBudgetError extends Error {
|
||||
constructor(readonly budget: "work" | "working-memory") {
|
||||
@@ -1272,9 +1285,7 @@ export const parseNativeAotPe = (
|
||||
);
|
||||
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
||||
if (sha256 !== expectedSha256)
|
||||
throw new Error(
|
||||
"NativeAOT snapshot digest does not match the admitted artifact identity",
|
||||
);
|
||||
throw new NativeAotSnapshotIdentityError(expectedSha256, sha256);
|
||||
let image: PeImage;
|
||||
try {
|
||||
image = new PeImage(bytes);
|
||||
|
||||
@@ -4,9 +4,13 @@ import type {
|
||||
AnalysisPartialObservation,
|
||||
} from "../domain/analysisErrorBase.js";
|
||||
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
|
||||
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
|
||||
import {
|
||||
AnalysisArtifactChangedError,
|
||||
AnalysisCancelledError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import {
|
||||
NativeAotSnapshotIdentityError,
|
||||
NATIVE_AOT_MAX_REPORT_BYTES,
|
||||
NATIVE_AOT_MAX_SOURCE_BYTES,
|
||||
nativeAotSnapshotOverCapacity,
|
||||
@@ -41,6 +45,7 @@ export class GhidraNativeAotSnapshotOwner {
|
||||
constructor(
|
||||
private readonly input: {
|
||||
readonly targetSha256: string;
|
||||
readonly targetPath: string;
|
||||
readonly readSnapshot: GhidraClient["readTargetSnapshot"] | undefined;
|
||||
readonly projectFailure: (
|
||||
operation: AnalysisOperation,
|
||||
@@ -198,6 +203,29 @@ export class GhidraNativeAotSnapshotOwner {
|
||||
imageBase: string,
|
||||
observations: JsonValue,
|
||||
): AnalysisError {
|
||||
if (failure.cause instanceof NativeAotSnapshotIdentityError) {
|
||||
const mismatch = failure.cause;
|
||||
const primary = new AnalysisArtifactChangedError(
|
||||
operation,
|
||||
this.input.targetPath,
|
||||
mismatch.message,
|
||||
{
|
||||
cause: mismatch,
|
||||
diagnostics: {
|
||||
expected_sha256: mismatch.expectedSha256,
|
||||
observed_sha256: mismatch.observedSha256,
|
||||
},
|
||||
partialObservation: failure.partialObservation,
|
||||
},
|
||||
);
|
||||
return failure.cleanupFailure?.cleanupIncomplete
|
||||
? analysisErrorWithCleanupFailure(
|
||||
primary,
|
||||
failure.cleanupFailure,
|
||||
operation,
|
||||
).bindCleanupDependencies([primary, failure.cleanupFailure])
|
||||
: primary;
|
||||
}
|
||||
if (failure.kind !== "cancelled" || this.#conflict === undefined)
|
||||
return this.input.projectFailure(
|
||||
operation,
|
||||
|
||||
@@ -2166,6 +2166,44 @@ it.each(["captured", "cancelled-with-facts"] as const)(
|
||||
},
|
||||
);
|
||||
|
||||
it("preserves the observed NativeAOT snapshot digest with source-change recovery", async () => {
|
||||
const bytes = nativeAotPeFixture();
|
||||
const expected = "0".repeat(64);
|
||||
const owner = new GhidraNativeAotSnapshotOwner({
|
||||
targetSha256: expected,
|
||||
targetPath: "/fixtures/admitted.exe",
|
||||
readSnapshot: async () => ok({ kind: "captured", bytes }),
|
||||
projectFailure: () => {
|
||||
throw new Error("Identity mismatch must retain source-change recovery");
|
||||
},
|
||||
});
|
||||
try {
|
||||
const result = await owner.derive(
|
||||
"0x140000000",
|
||||
"inspect_native_load_image",
|
||||
undefined,
|
||||
{},
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.error).toMatchObject({
|
||||
_tag: "AnalysisArtifactChangedError",
|
||||
path: "/fixtures/admitted.exe",
|
||||
diagnostics: {
|
||||
expected_sha256: expected,
|
||||
observed_sha256: nativeAotPeDigest(bytes),
|
||||
},
|
||||
partialObservation: {
|
||||
kind: "native-aot-snapshot",
|
||||
analysis_artifact_sha256: expected,
|
||||
capture: { status: "captured", bytes: bytes.length },
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
await owner.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves NativeAOT source facts and cleanup when a later caller contradicts identity", async () => {
|
||||
const partial = {
|
||||
kind: "file-snapshot-hash" as const,
|
||||
@@ -2181,6 +2219,7 @@ it("preserves NativeAOT source facts and cleanup when a later caller contradicts
|
||||
onTestFinished(() => lease.release());
|
||||
const owner = new GhidraNativeAotSnapshotOwner({
|
||||
targetSha256: partial.sha256,
|
||||
targetPath: partial.path,
|
||||
readSnapshot: async () =>
|
||||
err(
|
||||
new GhidraSessionError(
|
||||
|
||||
@@ -532,6 +532,7 @@ export const createGhidraProviderClient = (input: {
|
||||
target.format === "pe"
|
||||
? new GhidraNativeAotSnapshotOwner({
|
||||
targetSha256: target.sha256,
|
||||
targetPath: target.path,
|
||||
readSnapshot: client.readTargetSnapshot?.bind(client),
|
||||
projectFailure: (operation, failure) =>
|
||||
projectSessionError(
|
||||
|
||||
Reference in new issue
Block a user