From 1ff5a655bc8d97738c4e5a1ff7a6786126e6e3fe Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Thu, 5 Nov 2026 02:32:15 +0800 Subject: [PATCH] fix(native)!: preserve value-flow absence semantics (#8785) * fix(native)!: preserve value-flow absence semantics * test(native): compare value-flow reasons with their owners Stop freezing the unrecorded and Kuna unavailable sentences. Assert the procedure-scoped unknown and that an explicit provider reason is preserved. --------- Co-authored-by: Cursor Agent --- docs/native-investigation.md | 6 ++- src/application/WorkflowAnalysisProfile.ts | 3 ++ .../binary/AnalysisSnapshotCache.test.ts | 12 +++++ .../native/NativeValueTrace.test.ts | 46 +++++++++++++++++-- src/application/native/NativeValueTrace.ts | 6 ++- src/kuna/KunaProvider.ts | 1 + src/kuna/KunaSessionClient.ts | 10 +++- .../providers/kuna/kunaProvider.test.ts | 5 ++ 8 files changed, 81 insertions(+), 8 deletions(-) diff --git a/docs/native-investigation.md b/docs/native-investigation.md index bd585131b1..1d84257fdd 100644 --- a/docs/native-investigation.md +++ b/docs/native-investigation.md @@ -466,8 +466,10 @@ resources it owns stops the trace: the result is that provider's own `cleanup_incomplete` failure with its resources and any bound cleanup retry, and the work admitted before the stop is retained as partial Evidence with a located unknown at the stopping read. A successful seed without a native def-use -model still returns its procedure and an explicit coverage unknown. A new trace -can retry failed selectors. +model still returns its procedure and an explicit coverage unknown. An unrecorded +facet stays unknown; when the provider explicitly reports unavailability, the trace +retains its reason. REA's Kuna provider explicitly reports that it does not supply +native def-use observations. A new trace can retry failed selectors. Paging and traversal completeness are separate facts. `page_truncated` means the page omits retained nodes, and the edges they source, before `offset` or after diff --git a/src/application/WorkflowAnalysisProfile.ts b/src/application/WorkflowAnalysisProfile.ts index eaf1b18d41..5c54417703 100644 --- a/src/application/WorkflowAnalysisProfile.ts +++ b/src/application/WorkflowAnalysisProfile.ts @@ -13,6 +13,9 @@ export const workflowAnalysisProfile = ( createAnalysisProfile(REA_WORKFLOW_PROVIDER, { upstream_analysis_profile: upstream, source_limitations: "provider-operation-caveats-v1", + ...(operation === "trace_native_values" + ? { value_flow_absence: "recorded-versus-unavailable-v1" } + : {}), ...(operation === "binary_overview" ? { workflow_revision: 5 } : operation === "batch_decompile" diff --git a/src/application/binary/AnalysisSnapshotCache.test.ts b/src/application/binary/AnalysisSnapshotCache.test.ts index 59544d1f04..40e823c817 100644 --- a/src/application/binary/AnalysisSnapshotCache.test.ts +++ b/src/application/binary/AnalysisSnapshotCache.test.ts @@ -1357,3 +1357,15 @@ it("revises the trace_feature workflow profile so earlier results are not replay "other workflows keep their profile", ).toEqual(workflowAnalysisProfile(profile)); }); + +it("binds native value-trace absence semantics without changing other workflows", () => { + const profile = ANALYSIS_SNAPSHOT_PROFILE; + const valueTrace = workflowAnalysisProfile(profile, "trace_native_values"); + expect(valueTrace.parameters.value_flow_absence).toBe( + "recorded-versus-unavailable-v1", + ); + expect(valueTrace.digest).not.toBe(workflowAnalysisProfile(profile).digest); + expect(workflowAnalysisProfile(profile, "trace_call_path")).toEqual( + workflowAnalysisProfile(profile), + ); +}); diff --git a/src/application/native/NativeValueTrace.test.ts b/src/application/native/NativeValueTrace.test.ts index 88b4cb0bfb..d2b1e930bc 100644 --- a/src/application/native/NativeValueTrace.test.ts +++ b/src/application/native/NativeValueTrace.test.ts @@ -1,6 +1,9 @@ import { createAnalysisExecutionEvidence } from "../../domain/analysisObservation.js"; import { describe, expect, it } from "vitest"; -import { traceNativeValues as traceNativeValuesCore } from "./NativeValueTrace.js"; +import { + traceNativeValues as traceNativeValuesCore, + UNRECORDED_NATIVE_VALUE_FLOW, +} from "./NativeValueTrace.js"; import { executeComposedWorkflow } from "../WorkflowEvidence.js"; import { createAnalysisExecution, @@ -190,12 +193,47 @@ describe("native value trace seed admission", () => { expect.objectContaining({ address: base.procedure.address }), ], unknowns: [ - expect.objectContaining({ - reason: "Provider has no native def-use model", - }), + { + procedure: base.procedure.address, + address: null, + reason: UNRECORDED_NATIVE_VALUE_FLOW, + }, ], }); }); + + it("preserves an explicit unavailable reason for that procedure", async () => { + const base = functionDossierSchema.parse(ghidraFunctionDossier()); + const providerReason = "selected procedure has no p-code model"; + const result = await traceNativeValues( + { + execute: async () => + ok( + createAnalysisExecution( + { + ...base, + native_value_flow: { + available: false, + reason: providerReason, + limitations: [], + }, + }, + provider, + { subject }, + ), + ), + }, + { procedure: base.procedure.address }, + ); + if (!result.ok) throw result.error; + expect(nativeValueTraceSchema.parse(result.value).unknowns).toEqual([ + { + procedure: base.procedure.address, + address: null, + reason: providerReason, + }, + ]); + }); }); describe("native value parameter-use node membership", () => { diff --git a/src/application/native/NativeValueTrace.ts b/src/application/native/NativeValueTrace.ts index c80aaad55a..bb2c03d9ac 100644 --- a/src/application/native/NativeValueTrace.ts +++ b/src/application/native/NativeValueTrace.ts @@ -28,6 +28,10 @@ import { createValueTraceBudget } from "./NativeValueTraceBudget.js"; export const CLEANUP_STOPPED_VALUE_TRACE = "Trace stopped because the provider did not confirm cleanup of resources it owns"; +/** Fallback when a dossier has no native value-flow facet for this procedure. */ +export const UNRECORDED_NATIVE_VALUE_FLOW = + "Native def-use was not recorded for this procedure."; + /** Compose bounded high-p-code def-use graphs across exactly referenced call destinations. */ export const traceNativeValues = async ( analysis: AnalysisOperationPort, @@ -181,7 +185,7 @@ export const traceNativeValues = async ( unknowns.push({ procedure: address, address: null, - reason: flow?.reason ?? "Provider has no native def-use model", + reason: flow?.reason ?? UNRECORDED_NATIVE_VALUE_FLOW, }); continue; } diff --git a/src/kuna/KunaProvider.ts b/src/kuna/KunaProvider.ts index 1b0b91981e..c7aba09c46 100644 --- a/src/kuna/KunaProvider.ts +++ b/src/kuna/KunaProvider.ts @@ -281,6 +281,7 @@ export class KunaProvider implements AnalysisProviderCandidate { termination: "any", xrefs: false, }, + value_flow: "explicitly-unavailable-v1", cache_policy: "live", }, ), diff --git a/src/kuna/KunaSessionClient.ts b/src/kuna/KunaSessionClient.ts index f868fef86b..8424be370c 100644 --- a/src/kuna/KunaSessionClient.ts +++ b/src/kuna/KunaSessionClient.ts @@ -298,6 +298,10 @@ const kunaVerificationLimitations = ( /** Kuna names a procedure by its inventory entry or by an exact entry address. */ const procedureSelectorSchema = z.string().min(1); +/** REA's Kuna integration does not supply a def-use model for any procedure. */ +export const KUNA_NATIVE_VALUE_FLOW_UNAVAILABLE = + "REA's Kuna provider does not report native def-use observations."; + /** * One Kuna analysis session for one target. * @@ -1523,7 +1527,11 @@ export class KunaSessionClient implements AnalysisClient { referenced_names: [], basic_blocks: [], native_api: null, - native_value_flow: null, + native_value_flow: { + available: false, + reason: KUNA_NATIVE_VALUE_FLOW_UNAVAILABLE, + limitations: [], + }, facet_availability: { pseudocode: text === null diff --git a/tests/boundary/providers/kuna/kunaProvider.test.ts b/tests/boundary/providers/kuna/kunaProvider.test.ts index 499e0c3a46..32b20e9004 100644 --- a/tests/boundary/providers/kuna/kunaProvider.test.ts +++ b/tests/boundary/providers/kuna/kunaProvider.test.ts @@ -20,6 +20,7 @@ import { OwnedFileHandle } from "../../../../src/filesystem/OwnedFileHandle.js"; import { AnalysisProviderRegistry } from "../../../../src/application/binary/AnalysisProviderRegistry.js"; import { composeBinarySession } from "../../../../src/application/binary/BinarySessionComposition.js"; import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js"; +import { KUNA_NATIVE_VALUE_FLOW_UNAVAILABLE } from "../../../../src/kuna/KunaSessionClient.js"; import { KunaProvider } from "../../../../src/kuna/KunaProvider.js"; import { KUNA_FUNCTION_TEXT_BYTES } from "../../../../src/kuna/KunaLimits.js"; import { KunaProjectCache } from "../../../../src/kuna/KunaProjectCache.js"; @@ -509,6 +510,10 @@ it("builds a dossier with text and call relationships, leaving unsupported facet }, pseudocode: "int main(void)\n{\n return 0;\n}", assembly: ["main: ; 0x1000", " 00001000: c3 RET"], + native_value_flow: { + available: false, + reason: KUNA_NATIVE_VALUE_FLOW_UNAVAILABLE, + }, facet_availability: { pseudocode: { available: true }, assembly: { available: true },