fix(evidence): stabilize caveats and retain partial PDB identity
Canonicalize composed upstream limitations independently of concurrent provider timing. Preserve declared CodeView identity when Program PDB identity fields are omitted, alongside loaded-state and resource-coverage observations. Validation: 171 focused tests across 12 suites and runtime build pass. Add controlled dependent-branch ordering and partial PDB attribute regressions; use the declared 4096 UTF-16 wire budget.
This commit is contained in:
parent
7570215ad9
commit
cf7efda456
|
|
@ -20,7 +20,72 @@ import { ghidraFunctionDossier } from "../domain/ghidraValues.fixture.js";
|
|||
|
||||
const provider = { id: "fixture", name: "Fixture", version: "1" };
|
||||
|
||||
it("retains unique upstream limits in invocation order across concurrent observations", async () => {
|
||||
it("keeps Evidence identity stable when dependent branches finish in opposite orders", async () => {
|
||||
const run = async (order: readonly number[]) => {
|
||||
const gate = () => {
|
||||
let resolve: () => void = () => {
|
||||
throw new Error("gate not initialized");
|
||||
};
|
||||
const promise = new Promise<void>((release) => {
|
||||
resolve = release;
|
||||
});
|
||||
return { promise, resolve };
|
||||
};
|
||||
const parents = [gate(), gate()];
|
||||
const children = [gate(), gate()];
|
||||
const analysis: AnalysisOperationPort = {
|
||||
execute: async (operation, parameters) => {
|
||||
const index = Number(parameters.index);
|
||||
if (operation === "procedure_address") {
|
||||
await parents[index]!.promise;
|
||||
return ok(createAnalysisExecution(String(index), provider));
|
||||
}
|
||||
children[index]!.resolve();
|
||||
return ok(
|
||||
createAnalysisExecution(String(index), provider, {
|
||||
limitations: [
|
||||
index === 0 ? "first child caveat" : "second child caveat",
|
||||
],
|
||||
}),
|
||||
);
|
||||
},
|
||||
};
|
||||
const composed = await executeComposedWorkflow(
|
||||
analysis,
|
||||
async (observed) => {
|
||||
const branches = Promise.all(
|
||||
[0, 1].map(async (index) => {
|
||||
await observed.execute("procedure_address", { index });
|
||||
const child = await observed.execute("address_name", { index });
|
||||
if (!child.ok) throw child.error;
|
||||
return child.value.result;
|
||||
}),
|
||||
);
|
||||
for (const index of order) {
|
||||
parents[index]!.resolve();
|
||||
await children[index]!.promise;
|
||||
}
|
||||
return ok(await branches);
|
||||
},
|
||||
);
|
||||
if (!composed.ok) throw composed.error;
|
||||
return createWorkflowEvidence({
|
||||
target: undefined,
|
||||
operation: "batch_decompile",
|
||||
parameters: {},
|
||||
result: composed.value.result,
|
||||
upstreamProfile: undefined,
|
||||
upstreamLimitations: composed.value.limitations,
|
||||
});
|
||||
};
|
||||
const left = await run([0, 1]);
|
||||
const right = await run([1, 0]);
|
||||
expect(left.normalized_result).toEqual(right.normalized_result);
|
||||
expect(left.limitations).toEqual(right.limitations);
|
||||
expect(left.evidence_id).toBe(right.evidence_id);
|
||||
});
|
||||
|
||||
it("retains unique upstream limits in canonical order across concurrent observations", async () => {
|
||||
let release: () => void = () => {
|
||||
throw new Error("gate not initialized");
|
||||
};
|
||||
|
|
@ -52,7 +117,7 @@ it("retains unique upstream limits in invocation order across concurrent observa
|
|||
});
|
||||
expect(calls).toBe(2);
|
||||
expect(result).toEqual(
|
||||
ok({ result: { count: 0 }, limitations: ["first", "shared", "second"] }),
|
||||
ok({ result: { count: 0 }, limitations: ["first", "second", "shared"] }),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import {
|
|||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import type { UnknownRegistryPort } from "./investigation/InvestigationRecordPort.js";
|
||||
import { UnknownRegistryError } from "../domain/unknownRegistryError.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
/** Retain the limits of exactly the observations used by one composed call. */
|
||||
export const executeComposedWorkflow = async (
|
||||
|
|
@ -36,34 +37,21 @@ export const executeComposedWorkflow = async (
|
|||
AnalysisError
|
||||
>
|
||||
> => {
|
||||
let nextObservation = 0;
|
||||
const limitations = new Map<
|
||||
string,
|
||||
{ observation: number; position: number }
|
||||
>();
|
||||
const limitations = new Set<string>();
|
||||
const result = await execute({
|
||||
execute: async (operation, parameters, options) => {
|
||||
const observation = nextObservation++;
|
||||
const execution = await analysis.execute(operation, parameters, options);
|
||||
if (execution.ok)
|
||||
execution.value.limitations.forEach((limitation, position) => {
|
||||
const previous = limitations.get(limitation);
|
||||
if (previous === undefined || observation < previous.observation)
|
||||
limitations.set(limitation, { observation, position });
|
||||
});
|
||||
execution.value.limitations.forEach((limitation) =>
|
||||
limitations.add(limitation),
|
||||
);
|
||||
return execution;
|
||||
},
|
||||
});
|
||||
if (!result.ok) return result;
|
||||
return ok({
|
||||
result: result.value,
|
||||
limitations: [...limitations]
|
||||
.sort(
|
||||
([, left], [, right]) =>
|
||||
left.observation - right.observation ||
|
||||
left.position - right.position,
|
||||
)
|
||||
.map(([limitation]) => limitation),
|
||||
limitations: [...limitations].sort(compareUnicodeCodePoints),
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,27 @@ const absentAttributes = {
|
|||
loaded: null,
|
||||
};
|
||||
|
||||
it.each(["file", "guid", "age"] as const)(
|
||||
"retains declared identity when only reported %s is omitted",
|
||||
(attribute) => {
|
||||
const pdb: GhidraSessionInfo["pdb"] = {
|
||||
file: "program.pdb",
|
||||
guid: "reported-guid",
|
||||
age: "a",
|
||||
signature: null,
|
||||
version: "RSDS",
|
||||
loaded: null,
|
||||
resource_limit: { omitted_attributes: [attribute], utf16_budget: 4096 },
|
||||
[attribute]: null,
|
||||
};
|
||||
const caveats = ghidraPePdbLimitations(target, pdb).join(" ");
|
||||
expect(caveats).toContain("omitted");
|
||||
expect(caveats).toContain('PDB "public.pdb"');
|
||||
expect(caveats).toContain("01234567-89ab-cdef-0123-456789abcdef");
|
||||
expect(caveats).toContain("load state unknown");
|
||||
},
|
||||
);
|
||||
|
||||
it.each([undefined, absentAttributes])(
|
||||
"retains declared identity and unknown load state when reported attributes are %j",
|
||||
(pdb) => {
|
||||
|
|
@ -77,9 +98,11 @@ it("retains a loaded-state attestation caveat even when reference attributes are
|
|||
...absentAttributes,
|
||||
loaded: true,
|
||||
});
|
||||
expect(caveats).toEqual(
|
||||
ghidraPdbLimitations({ ...absentAttributes, loaded: true }),
|
||||
expect(caveats[0]).toEqual(
|
||||
ghidraPdbLimitations({ ...absentAttributes, loaded: true })[0],
|
||||
);
|
||||
expect(caveats).toHaveLength(2);
|
||||
expect(caveats[1]).toContain('PDB "public.pdb"');
|
||||
expect(caveats[0]).toContain("not an independent PDB identity");
|
||||
expect(caveats[0]).not.toContain("reports not loaded");
|
||||
});
|
||||
|
|
@ -87,7 +110,7 @@ it("retains a loaded-state attestation caveat even when reference attributes are
|
|||
it("preserves omitted-attribute resource coverage with declared fallback identity", () => {
|
||||
const pdb: GhidraSessionInfo["pdb"] = {
|
||||
...absentAttributes,
|
||||
resource_limit: { omitted_attributes: ["file"], utf16_budget: 2048 },
|
||||
resource_limit: { omitted_attributes: ["file"], utf16_budget: 4096 },
|
||||
};
|
||||
const caveats = ghidraPePdbLimitations(target, pdb);
|
||||
expect(caveats).toHaveLength(2);
|
||||
|
|
|
|||
|
|
@ -13,11 +13,10 @@ export const ghidraPePdbLimitations = (
|
|||
const reported = ghidraPdbLimitations(pdb);
|
||||
if (
|
||||
target.codeView === undefined ||
|
||||
pdb?.loaded === true ||
|
||||
(pdb !== undefined &&
|
||||
[pdb.file, pdb.guid, pdb.age, pdb.signature, pdb.version].some(
|
||||
(attribute) => attribute !== null,
|
||||
))
|
||||
pdb.file !== null &&
|
||||
pdb.age !== null &&
|
||||
(target.codeView.guid === null || pdb.guid !== null))
|
||||
)
|
||||
return reported;
|
||||
const { pdbFileName, guid, age } = target.codeView;
|
||||
|
|
@ -35,7 +34,9 @@ export const ghidraPePdbLimitations = (
|
|||
? "Ghidra did not report PDB attributes; load state unknown"
|
||||
: pdb.loaded === false
|
||||
? "Ghidra reports not loaded"
|
||||
: "Ghidra reports load state unknown";
|
||||
: pdb.loaded === true
|
||||
? "Ghidra reports a loaded PDB; this declared identity is not an independent matching-PDB attestation"
|
||||
: "Ghidra reports load state unknown";
|
||||
const declared = `The PE CodeView record references PDB ${locator} (${identity}); ${state}. Original PDB names and types may be missing. Use list_procedures and address-based analysis to inspect recovered functions. REA does not admit a caller-selected PDB companion.`;
|
||||
return pdb === undefined ? [declared] : [...reported, declared];
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue