From cf7efda45640ebfe493d58474f32d0ea3b86f867 Mon Sep 17 00:00:00 2001 From: N0zoM1z0 <161784452+N0zoM1z0@users.noreply.github.com> Date: Wed, 14 Oct 2026 04:11:48 +0800 Subject: [PATCH] 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. --- src/application/WorkflowEvidence.test.ts | 69 +++++++++++++++++++++++- src/application/WorkflowEvidence.ts | 24 +++------ src/ghidra/GhidraPeProfile.test.ts | 29 ++++++++-- src/ghidra/GhidraPeProfile.ts | 11 ++-- 4 files changed, 105 insertions(+), 28 deletions(-) diff --git a/src/application/WorkflowEvidence.test.ts b/src/application/WorkflowEvidence.test.ts index 39fcfdcce..b374efa01 100644 --- a/src/application/WorkflowEvidence.test.ts +++ b/src/application/WorkflowEvidence.test.ts @@ -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((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"] }), ); }); diff --git a/src/application/WorkflowEvidence.ts b/src/application/WorkflowEvidence.ts index d79ff8405..5bbd75455 100644 --- a/src/application/WorkflowEvidence.ts +++ b/src/application/WorkflowEvidence.ts @@ -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(); 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), }); }; diff --git a/src/ghidra/GhidraPeProfile.test.ts b/src/ghidra/GhidraPeProfile.test.ts index 4509d3ef7..1780b9eb1 100644 --- a/src/ghidra/GhidraPeProfile.test.ts +++ b/src/ghidra/GhidraPeProfile.test.ts @@ -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); diff --git a/src/ghidra/GhidraPeProfile.ts b/src/ghidra/GhidraPeProfile.ts index 08ed11d81..019b1a2d3 100644 --- a/src/ghidra/GhidraPeProfile.ts +++ b/src/ghidra/GhidraPeProfile.ts @@ -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]; };