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:
N0zoM1z0 2026-10-14 04:11:48 +08:00
parent 7570215ad9
commit cf7efda456
4 changed files with 105 additions and 28 deletions

View File

@ -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"] }),
);
});

View File

@ -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),
});
};

View File

@ -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);

View File

@ -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];
};