mirror of
https://github.com/morluto/rea.git
synced 2026-11-08 07:46:28 +00:00
feat(kuna): preserve stored pointer references (#8851)
* feat(kuna): verify release 1.990 * feat(kuna): preserve stored pointer references * fix(kuna): group image-wide reference coverage gaps * fix(evidence): retain producer-required xref observations * fix(mcp): clarify xref address page scope * fix(kuna): preserve reference target aliases --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
1 parent
3e8a5f76d0
commit
f7dc5489b3
17 files changed
+692
-29
No files matched your search
@@ -318,6 +318,8 @@ Retained `xrefs` Evidence also supports `native-observation` address pages and
|
||||
default and does not accept `detail`; when its MCP response exceeds the budget,
|
||||
follow the returned continuation to page the retained result. These addresses
|
||||
remain untyped; paging does not add reference kinds or establish completeness.
|
||||
The page tells callers that any additional provider detail recorded in the parent
|
||||
requires retrieving or exporting the complete Evidence.
|
||||
|
||||
These views reference complete source context through `source_context.digest`
|
||||
and caveat counts. Read it using the same parent with
|
||||
|
||||
@@ -82,6 +82,22 @@ export interface AnalysisExecution extends AnalysisObservation {
|
||||
* output.
|
||||
*/
|
||||
readonly referencesTruncated?: boolean;
|
||||
/** The producer needs this observation retained as source Evidence for composed workflows. */
|
||||
readonly requiresSourceEvidence?: true;
|
||||
/** Known reference-discovery gaps; absent means coverage was not reported. */
|
||||
readonly referenceCoverageGaps?: readonly {
|
||||
readonly code: string;
|
||||
readonly scope: "image" | "observation";
|
||||
readonly description: string;
|
||||
}[];
|
||||
/** Typed source facts for this live xref observation; absent means unreported. */
|
||||
readonly referenceClassifications?: readonly {
|
||||
readonly sourceAddress: string;
|
||||
readonly targetAddress: string;
|
||||
readonly kind: "stored_pointer";
|
||||
readonly provenance: string;
|
||||
readonly width: number;
|
||||
}[];
|
||||
/**
|
||||
* Sibling operations known to be unavailable at the provider level when this
|
||||
* operation answered from a standalone or fallback path.
|
||||
@@ -97,6 +113,9 @@ interface CreateAnalysisExecutionOptions {
|
||||
readonly limitations?: readonly string[];
|
||||
readonly relationshipCoverage?: RelationshipCoverage;
|
||||
readonly referencesTruncated?: boolean;
|
||||
readonly requiresSourceEvidence?: true;
|
||||
readonly referenceClassifications?: AnalysisExecution["referenceClassifications"];
|
||||
readonly referenceCoverageGaps?: AnalysisExecution["referenceCoverageGaps"];
|
||||
readonly siblingUnavailability?: AnalysisCapabilityUnavailableError;
|
||||
readonly locations?: readonly EvidenceLocation[];
|
||||
readonly sourceEvidence?: readonly Evidence[];
|
||||
@@ -156,6 +175,15 @@ export function createAnalysisExecution(
|
||||
...(options.referencesTruncated === undefined
|
||||
? {}
|
||||
: { referencesTruncated: options.referencesTruncated }),
|
||||
...(options.requiresSourceEvidence === undefined
|
||||
? {}
|
||||
: { requiresSourceEvidence: options.requiresSourceEvidence }),
|
||||
...(options.referenceCoverageGaps === undefined
|
||||
? {}
|
||||
: { referenceCoverageGaps: options.referenceCoverageGaps }),
|
||||
...(options.referenceClassifications === undefined
|
||||
? {}
|
||||
: { referenceClassifications: options.referenceClassifications }),
|
||||
...(options.siblingUnavailability === undefined
|
||||
? {}
|
||||
: { siblingUnavailability: options.siblingUnavailability }),
|
||||
|
||||
@@ -25,8 +25,10 @@ import {
|
||||
retainAnalysisResultSources,
|
||||
type AnalysisResultWithSources,
|
||||
} from "./investigation/recordEvidenceSources.js";
|
||||
import { resolveInvestigationEvidenceClosure } from "./investigation/InvestigationEvidence.js";
|
||||
import { createInvestigationEvidenceClosureChecker } from "./investigation/InvestigationEvidence.js";
|
||||
import {
|
||||
resolveInvestigationEvidenceClosure,
|
||||
createInvestigationEvidenceClosureChecker,
|
||||
} from "./investigation/InvestigationEvidence.js";
|
||||
import type { Evidence } from "../domain/evidence.js";
|
||||
import type { NativeToolName } from "../contracts/native/nativeToolContracts.js";
|
||||
import type { EnhancedToolName } from "../contracts/enhancedInputs.js";
|
||||
|
||||
@@ -137,6 +137,7 @@ const traceReferences = async (
|
||||
const tracedTargets = new Set<string>();
|
||||
// A source address is resolved once however many matches reference it.
|
||||
const containing = new Map<string, TraceContainingProcedure>();
|
||||
const imageCoverageGaps = new Set<string>();
|
||||
for (const match of request.matches) {
|
||||
if (tracedTargets.has(match.address)) continue;
|
||||
tracedTargets.add(match.address);
|
||||
@@ -154,6 +155,17 @@ const traceReferences = async (
|
||||
`References to ${match.address} were truncated by the provider; additional references may exist.`,
|
||||
);
|
||||
}
|
||||
for (const gap of observed.value.referenceCoverageGaps ?? []) {
|
||||
if (gap.scope === "observation") {
|
||||
residual.push(`References to ${match.address}: ${gap.description}`);
|
||||
} else {
|
||||
const key = JSON.stringify([observed.value.provider.id, gap.code]);
|
||||
if (!imageCoverageGaps.has(key)) {
|
||||
imageCoverageGaps.add(key);
|
||||
residual.push(gap.description);
|
||||
}
|
||||
}
|
||||
}
|
||||
const sources = observed.value.result;
|
||||
if (!Array.isArray(sources))
|
||||
return err(
|
||||
@@ -162,6 +174,20 @@ const traceReferences = async (
|
||||
"provider returned a non-array result",
|
||||
),
|
||||
);
|
||||
const classifications = new Map<
|
||||
string,
|
||||
{ kind: "stored_pointer"; provenance: string; width: number }[]
|
||||
>();
|
||||
for (const fact of observed.value.referenceClassifications ?? []) {
|
||||
if (fact.targetAddress !== match.address) continue;
|
||||
const values = classifications.get(fact.sourceAddress) ?? [];
|
||||
values.push({
|
||||
kind: fact.kind,
|
||||
provenance: fact.provenance,
|
||||
width: fact.width,
|
||||
});
|
||||
classifications.set(fact.sourceAddress, values);
|
||||
}
|
||||
for (const source of sources) {
|
||||
if (typeof source !== "string")
|
||||
return err(
|
||||
@@ -211,6 +237,9 @@ const traceReferences = async (
|
||||
target_address: match.address,
|
||||
source_address: source,
|
||||
containing_procedure: containingProcedure,
|
||||
...(classifications.has(source)
|
||||
? { source_classifications: classifications.get(source)! }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ export const workflowAnalysisProfile = (
|
||||
: operation === "batch_decompile"
|
||||
? { workflow_revision: 4 }
|
||||
: operation === "trace_feature"
|
||||
? { workflow_revision: 4 }
|
||||
? { workflow_revision: 8 }
|
||||
: operation === "get_call_graph"
|
||||
? { workflow_revision: 3 }
|
||||
: operation === "trace_native_values"
|
||||
|
||||
@@ -864,6 +864,73 @@ describe("document-scoped snapshot admission", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("retains producer-required xrefs without replaying a filesystem-writing query", () => {
|
||||
const descriptor: CapabilityDescriptor = {
|
||||
operation: "xrefs",
|
||||
provider: ANALYSIS_SNAPSHOT_PROVIDER,
|
||||
available: true,
|
||||
reason: null,
|
||||
effects: {
|
||||
mutatesArtifact: false,
|
||||
launchesProcess: true,
|
||||
mayShowUi: false,
|
||||
mayAccessNetwork: false,
|
||||
mayWriteFilesystem: false,
|
||||
changesPermissions: false,
|
||||
requiresRoot: false,
|
||||
},
|
||||
limitations: [],
|
||||
};
|
||||
const xrefs = descriptor;
|
||||
expect(
|
||||
isSourceEvidenceRetainable("xrefs", xrefs, { address: "0x1000" }),
|
||||
).toBe(false);
|
||||
expect(isSnapshotCacheable("xrefs", xrefs, { address: "0x1000" })).toBe(
|
||||
false,
|
||||
);
|
||||
expect(isSourceEvidenceRetainable("xrefs", xrefs, {})).toBe(false);
|
||||
const writingXrefs = {
|
||||
...xrefs,
|
||||
effects: { ...xrefs.effects, mayWriteFilesystem: true },
|
||||
};
|
||||
expect(
|
||||
isSourceEvidenceRetainable("xrefs", writingXrefs, { address: "0x1000" }),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isSourceEvidenceRetainable(
|
||||
"xrefs",
|
||||
writingXrefs,
|
||||
{ address: "0x1000" },
|
||||
{ requiresSourceEvidence: true },
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isSourceEvidenceRetainable(
|
||||
"xrefs",
|
||||
writingXrefs,
|
||||
{},
|
||||
{ requiresSourceEvidence: true },
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isSourceEvidenceRetainable(
|
||||
"xrefs",
|
||||
{
|
||||
...writingXrefs,
|
||||
effects: { ...writingXrefs.effects, mutatesArtifact: true },
|
||||
},
|
||||
{ address: "0x1000" },
|
||||
{ requiresSourceEvidence: true },
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isSnapshotCacheable("xrefs", writingXrefs, {
|
||||
address: "0x1000",
|
||||
document: "fixture",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
describe("analysis snapshot cache partitioning", () => {
|
||||
it.each<{
|
||||
operation: Exclude<AnalysisOperation, "health">;
|
||||
@@ -1490,13 +1557,43 @@ describe("executions that report truncated references", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not replay references when the snapshot would lose their classifications", () => {
|
||||
const cache = new AnalysisSnapshotCache();
|
||||
const parameters = { address: "0x1000", document: "fixture" };
|
||||
cache.record({
|
||||
target: ANALYSIS_SNAPSHOT_TARGET,
|
||||
profile: ANALYSIS_SNAPSHOT_PROFILE,
|
||||
operation: "xrefs",
|
||||
parameters,
|
||||
execution: createAnalysisExecution(["0x2000"], ANALYSIS_SNAPSHOT_PROVIDER, {
|
||||
referenceClassifications: [
|
||||
{
|
||||
sourceAddress: "0x2000",
|
||||
targetAddress: "0x1000",
|
||||
kind: "stored_pointer",
|
||||
provenance: "relocation",
|
||||
width: 8,
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
expect(
|
||||
cache.lookup(
|
||||
ANALYSIS_SNAPSHOT_TARGET,
|
||||
ANALYSIS_SNAPSHOT_PROFILE,
|
||||
"xrefs",
|
||||
parameters,
|
||||
),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("revises the trace_feature workflow profile so earlier results are not replayed", () => {
|
||||
const profile = ANALYSIS_SNAPSHOT_PROFILE;
|
||||
|
||||
expect(
|
||||
workflowAnalysisProfile(profile, "trace_feature").parameters
|
||||
.workflow_revision,
|
||||
).toBe(4);
|
||||
).toBe(8);
|
||||
|
||||
expect(workflowAnalysisProfile(profile, "trace_feature")).not.toEqual(
|
||||
workflowAnalysisProfile(profile),
|
||||
|
||||
@@ -117,25 +117,30 @@ const DOCUMENTLESS_SOURCE_EVIDENCE_OPERATIONS: ReadonlySet<AnalysisOperation> =
|
||||
|
||||
/**
|
||||
* Whether a source observation is safe to retain as Evidence.
|
||||
* A missing document selector blocks snapshot replay. Only caller and callee
|
||||
* walks stay in the ledger without one.
|
||||
* A missing document selector blocks snapshot replay. A producer can require
|
||||
* its observation as Evidence independently of whether it can be replayed.
|
||||
*/
|
||||
export const isSourceEvidenceRetainable = (
|
||||
operation: AnalysisOperation,
|
||||
descriptor: CapabilityDescriptor | undefined,
|
||||
parameters: Readonly<Record<string, JsonValue>>,
|
||||
context?: ExecutionOptions["snapshotContext"],
|
||||
options: {
|
||||
readonly snapshotContext?: ExecutionOptions["snapshotContext"];
|
||||
readonly requiresSourceEvidence?: boolean;
|
||||
} = {},
|
||||
): descriptor is CapabilityDescriptor =>
|
||||
!STATEFUL_OPERATIONS.has(operation) &&
|
||||
!RESOURCE_SCOPED_OPERATIONS.has(operation) &&
|
||||
(!DOCUMENT_SCOPED_OPERATIONS.has(operation) ||
|
||||
typeof parameters.document === "string" ||
|
||||
DOCUMENTLESS_SOURCE_EVIDENCE_OPERATIONS.has(operation) ||
|
||||
isOneShotPseudocode(operation, descriptor, context)) &&
|
||||
options.requiresSourceEvidence === true ||
|
||||
isOneShotPseudocode(operation, descriptor, options.snapshotContext)) &&
|
||||
(!CURSOR_DEFAULT_OPERATIONS.has(operation) ||
|
||||
typeof parameters.address === "string") &&
|
||||
descriptor?.effects.mutatesArtifact === false &&
|
||||
(descriptor.cachePolicy === "snapshot" ||
|
||||
(options.requiresSourceEvidence === true ||
|
||||
descriptor.cachePolicy === "snapshot" ||
|
||||
descriptor.effects.mayWriteFilesystem === false) &&
|
||||
descriptor.effects.changesPermissions === false;
|
||||
|
||||
@@ -146,7 +151,9 @@ export const isSnapshotCacheable = (
|
||||
parameters: Readonly<Record<string, JsonValue>>,
|
||||
context?: ExecutionOptions["snapshotContext"],
|
||||
): descriptor is CapabilityDescriptor =>
|
||||
isSourceEvidenceRetainable(operation, descriptor, parameters, context) &&
|
||||
isSourceEvidenceRetainable(operation, descriptor, parameters, {
|
||||
snapshotContext: context,
|
||||
}) &&
|
||||
(replayDocumentSelected(operation, parameters) ||
|
||||
isOneShotPseudocode(operation, descriptor, context)) &&
|
||||
descriptor.cachePolicy !== "live";
|
||||
@@ -589,9 +596,15 @@ export class AnalysisSnapshotCache {
|
||||
): void {
|
||||
const { target, profile, operation, parameters, execution } = input;
|
||||
this.select(target, profile);
|
||||
// The snapshot format does not carry reference truncation; a replay would
|
||||
// present a capped read as complete, so it is not recorded.
|
||||
if (execution.referencesTruncated === true) return;
|
||||
// The snapshot format does not carry reference truncation or stored-pointer
|
||||
// classifications. Replaying either would present a capped or untyped read
|
||||
// as the live observation.
|
||||
if (
|
||||
execution.referencesTruncated === true ||
|
||||
execution.referenceClassifications !== undefined ||
|
||||
execution.referenceCoverageGaps !== undefined
|
||||
)
|
||||
return;
|
||||
let sourceBinding: SnapshotReplayBinding | undefined;
|
||||
if ((execution.sourceEvidence?.length ?? 0) > 0) {
|
||||
if (execution.evidenceId === undefined || lookup === undefined) return;
|
||||
|
||||
@@ -1539,12 +1539,10 @@ export class BinarySession
|
||||
}
|
||||
if (
|
||||
execution.ok &&
|
||||
isSourceEvidenceRetainable(
|
||||
name,
|
||||
capability,
|
||||
arguments_,
|
||||
options?.snapshotContext,
|
||||
) &&
|
||||
isSourceEvidenceRetainable(name, capability, arguments_, {
|
||||
snapshotContext: options?.snapshotContext,
|
||||
requiresSourceEvidence: execution.value.requiresSourceEvidence === true,
|
||||
}) &&
|
||||
profile !== undefined
|
||||
) {
|
||||
const evidence = createAnalysisExecutionEvidence(
|
||||
@@ -1561,7 +1559,11 @@ export class BinarySession
|
||||
"observed",
|
||||
);
|
||||
const recorded = this.recordEvidence(evidence);
|
||||
if (!recorded.ok) return recorded;
|
||||
if (!recorded.ok) {
|
||||
// The provider finished. A full ledger must not discard that observation.
|
||||
recorded.error.retainPartialObservation(evidence);
|
||||
return recorded;
|
||||
}
|
||||
const retained = { ...execution.value, evidenceId: evidence.evidence_id };
|
||||
if (cacheable && execution.value.cachePolicy !== "live")
|
||||
this.recordSnapshot({
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import type { ElectronBoundarySummary } from "../../domain/javascript/javascriptApplicationAnalysis.js";
|
||||
import { electronSemanticsState } from "./ElectronBoundaryAnalysis.js";
|
||||
import {
|
||||
electronSemanticsState,
|
||||
createElectronIpcContext,
|
||||
summarizeElectronBoundaries,
|
||||
type ElectronIpcContext,
|
||||
} from "./ElectronBoundaryAnalysis.js";
|
||||
import { ELECTRON_IDENTITY_LIMITATION } from "../../domain/javascript/javascriptElectronMemberWrites.js";
|
||||
import type { ArtifactInventorySnapshot } from "../../domain/artifactInventorySnapshot.js";
|
||||
import {
|
||||
@@ -47,11 +52,6 @@ import {
|
||||
} from "./JavaScriptArtifactGraphStructure.js";
|
||||
import { addJavaScriptBundlerNodes } from "./JavaScriptArtifactGraphBundlers.js";
|
||||
import { addElectronBoundaries } from "./ElectronBoundaryGraph.js";
|
||||
import {
|
||||
createElectronIpcContext,
|
||||
summarizeElectronBoundaries,
|
||||
type ElectronIpcContext,
|
||||
} from "./ElectronBoundaryAnalysis.js";
|
||||
|
||||
/**
|
||||
* Build a validated application graph with cooperative immutable ownership.
|
||||
|
||||
@@ -30,6 +30,10 @@ import {
|
||||
} from "./nativeObservationSchemas.js";
|
||||
import { projectNativeGraphRows } from "./nativeGraphView.js";
|
||||
|
||||
/** What an address page leaves in the complete parent, when recorded. */
|
||||
export const XREFS_ADDRESS_PAGE_LIMITATION =
|
||||
"This page contains source addresses only, not reference kinds or provenance. Retrieve or export the complete parent Evidence for any additional provider details it recorded.";
|
||||
|
||||
const readAddresses = createAnalysisViewResultReader(nativeAddressListSchema);
|
||||
const readAddress = createAnalysisViewResultReader(
|
||||
nativeObservationSourceSchemas.procedure_address,
|
||||
@@ -347,6 +351,9 @@ export const projectNativeObservationView = (
|
||||
const { total, ...payload } = page;
|
||||
const observation = {
|
||||
...common,
|
||||
...(source.value.operation === "xrefs"
|
||||
? { unknowns: [...common.unknowns, XREFS_ADDRESS_PAGE_LIMITATION] }
|
||||
: {}),
|
||||
...payload,
|
||||
summary: { observations: total },
|
||||
coverage: pageViewCoverage(selection.offset, page.items.rows.length, total),
|
||||
|
||||
@@ -7,6 +7,18 @@ export const literalFeatureReferenceSchema = z.object({
|
||||
target_address: z.string(),
|
||||
source_address: z.string(),
|
||||
containing_procedure: traceContainingProcedureSchema,
|
||||
source_classifications: z
|
||||
.array(
|
||||
z.strictObject({
|
||||
kind: z.literal("stored_pointer"),
|
||||
provenance: z.string(),
|
||||
width: z.int().positive(),
|
||||
}),
|
||||
)
|
||||
.exactOptional()
|
||||
.describe(
|
||||
"Provider-reported source classifications; omission means none were supplied. The list need not exhaust all kinds.",
|
||||
),
|
||||
});
|
||||
|
||||
/** Complete result of a native literal feature trace. */
|
||||
|
||||
@@ -273,7 +273,12 @@ export class KunaProvider implements AnalysisProviderCandidate {
|
||||
bodies: false,
|
||||
edge_kind: "call",
|
||||
},
|
||||
xrefs: { command: "xrefs", direction: "to", exact_address: true },
|
||||
xrefs: {
|
||||
command: "xrefs",
|
||||
direction: "to",
|
||||
exact_address: true,
|
||||
stored_pointers: true,
|
||||
},
|
||||
strings: {
|
||||
command: "strings",
|
||||
encoding: "all",
|
||||
|
||||
@@ -224,6 +224,32 @@ const xrefRow = z.object({
|
||||
to_function: xrefFunction.nullable(),
|
||||
instruction: z.string(),
|
||||
});
|
||||
const xrefPointer = z.object({
|
||||
kind: z.literal("pointer"),
|
||||
from_address: address,
|
||||
from_address_hex: address,
|
||||
from_name: z.string().nullable(),
|
||||
to_address: address,
|
||||
to_address_hex: address,
|
||||
to_name: z.string().nullable(),
|
||||
width: z.int().positive(),
|
||||
source: z.string().min(1),
|
||||
});
|
||||
const xrefWalkCoverage = z.object({
|
||||
decoded: z.boolean(),
|
||||
section: z.string().nullable(),
|
||||
code_section: z.boolean().nullable(),
|
||||
executable: z.boolean().nullable(),
|
||||
unwalked: z.array(
|
||||
z.object({
|
||||
name: z.string().nullable(),
|
||||
address,
|
||||
address_hex: address,
|
||||
section: z.string(),
|
||||
}),
|
||||
),
|
||||
notes: z.array(z.string()),
|
||||
});
|
||||
const xrefsDocument = z.object({
|
||||
binary: z.string(),
|
||||
direction: z.literal("to"),
|
||||
@@ -235,6 +261,10 @@ const xrefsDocument = z.object({
|
||||
aliases: z.array(xrefFunction),
|
||||
}),
|
||||
xrefs: z.array(z.unknown()),
|
||||
coverage: xrefWalkCoverage.optional(),
|
||||
data_refs_coverage: z.string().optional(),
|
||||
data_refs_count: count.optional(),
|
||||
data_refs: z.array(xrefPointer).optional(),
|
||||
});
|
||||
|
||||
export interface KunaIncomingReferences {
|
||||
@@ -243,6 +273,15 @@ export interface KunaIncomingReferences {
|
||||
readonly bytes: number;
|
||||
readonly rows: number;
|
||||
readonly excludedAliasRows: number;
|
||||
readonly targetAliases: readonly {
|
||||
readonly name: string;
|
||||
readonly address: string;
|
||||
}[];
|
||||
readonly walkCoverage: z.infer<typeof xrefWalkCoverage> | null;
|
||||
readonly pointerCoverage: string | null;
|
||||
readonly pointers: Array<
|
||||
Omit<z.infer<typeof xrefPointer>, "from_address_hex" | "to_address_hex">
|
||||
> | null;
|
||||
}
|
||||
|
||||
/** Code from incoming-reference admission, plus the address field when one failed. */
|
||||
@@ -275,8 +314,34 @@ export const parseKunaIncomingReferences = (
|
||||
throw new KunaDocumentError("xref_alias_hex");
|
||||
aliases.add(alias.address);
|
||||
}
|
||||
if (
|
||||
(document.data_refs === undefined) !==
|
||||
(document.data_refs_count === undefined)
|
||||
)
|
||||
throw new KunaDocumentError("xref_count");
|
||||
if (
|
||||
document.data_refs !== undefined &&
|
||||
document.data_refs.length !== document.data_refs_count
|
||||
)
|
||||
throw new KunaDocumentError("xref_count");
|
||||
for (const item of document.coverage?.unwalked ?? [])
|
||||
if (item.address !== item.address_hex)
|
||||
throw new KunaDocumentError("xref_endpoints");
|
||||
for (const pointer of document.data_refs ?? []) {
|
||||
if (
|
||||
pointer.from_address !== pointer.from_address_hex ||
|
||||
pointer.to_address !== pointer.to_address_hex
|
||||
)
|
||||
throw new KunaDocumentError("xref_endpoints");
|
||||
if (!aliases.has(pointer.to_address))
|
||||
throw new KunaDocumentError("xref_target");
|
||||
}
|
||||
const addresses = new Set<string>();
|
||||
let excludedAliasRows = 0;
|
||||
for (const pointer of document.data_refs ?? []) {
|
||||
if (pointer.to_address === target) addresses.add(pointer.from_address);
|
||||
else excludedAliasRows += 1;
|
||||
}
|
||||
for (const raw of document.xrefs) {
|
||||
const row = xrefRow.parse(raw);
|
||||
if (
|
||||
@@ -296,8 +361,19 @@ export const parseKunaIncomingReferences = (
|
||||
}
|
||||
return ok({
|
||||
addresses: [...addresses].sort(compareKunaAddresses),
|
||||
rows: document.xrefs.length,
|
||||
rows: document.xrefs.length + (document.data_refs?.length ?? 0),
|
||||
excludedAliasRows,
|
||||
targetAliases: document.target.aliases.map(({ name, address }) => ({
|
||||
name,
|
||||
address,
|
||||
})),
|
||||
walkCoverage: document.coverage ?? null,
|
||||
pointerCoverage: document.data_refs_coverage ?? null,
|
||||
pointers:
|
||||
document.data_refs?.map(
|
||||
({ from_address_hex: _from, to_address_hex: _to, ...pointer }) =>
|
||||
pointer,
|
||||
) ?? null,
|
||||
bytes: Buffer.byteLength(text),
|
||||
sha256: createHash("sha256").update(text).digest("hex"),
|
||||
});
|
||||
|
||||
@@ -178,8 +178,11 @@ interface Answer {
|
||||
readonly locations: readonly EvidenceLocation[];
|
||||
readonly limitations: readonly string[];
|
||||
readonly referenceSource?: JsonValue;
|
||||
readonly requiresSourceEvidence?: true;
|
||||
readonly stringSource?: JsonValue;
|
||||
readonly relationshipCoverage?: RelationshipCoverage;
|
||||
readonly referenceClassifications?: AnalysisExecution["referenceClassifications"];
|
||||
readonly referenceCoverageGaps?: AnalysisExecution["referenceCoverageGaps"];
|
||||
readonly siblingUnavailability?: AnalysisCapabilityUnavailableError;
|
||||
}
|
||||
|
||||
@@ -725,6 +728,15 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
...(answer.relationshipCoverage === undefined
|
||||
? {}
|
||||
: { relationshipCoverage: answer.relationshipCoverage }),
|
||||
...(answer.requiresSourceEvidence === undefined
|
||||
? {}
|
||||
: { requiresSourceEvidence: answer.requiresSourceEvidence }),
|
||||
...(answer.referenceCoverageGaps === undefined
|
||||
? {}
|
||||
: { referenceCoverageGaps: answer.referenceCoverageGaps }),
|
||||
...(answer.referenceClassifications === undefined
|
||||
? {}
|
||||
: { referenceClassifications: answer.referenceClassifications }),
|
||||
...(answer.siblingUnavailability === undefined
|
||||
? {}
|
||||
: { siblingUnavailability: answer.siblingUnavailability }),
|
||||
@@ -1005,11 +1017,59 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
options.signal,
|
||||
);
|
||||
if (!references.ok) return references;
|
||||
const coverageGaps = [
|
||||
...(references.value.walkCoverage !== null &&
|
||||
references.value.walkCoverage.unwalked.length > 0
|
||||
? [
|
||||
{
|
||||
code: "unwalked-code",
|
||||
scope: "observation" as const,
|
||||
description:
|
||||
"Kuna reports unwalked code candidates whose outgoing references are not included. Their presence does not establish a reference to the selected target; the source observation identifies them.",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(references.value.pointerCoverage === "none"
|
||||
? [
|
||||
{
|
||||
code: "pointer-relocations-unavailable",
|
||||
scope: "image" as const,
|
||||
description:
|
||||
"Kuna reports no pointer-relocation coverage for this image; stored pointers may still exist.",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
return ok({
|
||||
result: [...references.value.addresses],
|
||||
source: null,
|
||||
locations,
|
||||
limitations,
|
||||
limitations: [
|
||||
...limitations,
|
||||
...(references.value.pointerCoverage === null
|
||||
? []
|
||||
: [
|
||||
"Kuna pointer references come from relocation metadata. The reported metadata source does not establish successful or complete enumeration.",
|
||||
]),
|
||||
...coverageGaps.map(({ description }) => description),
|
||||
],
|
||||
...(coverageGaps.length === 0
|
||||
? {}
|
||||
: { referenceCoverageGaps: coverageGaps }),
|
||||
...(references.value.pointers === null
|
||||
? {}
|
||||
: {
|
||||
referenceClassifications: references.value.pointers
|
||||
.filter((pointer) => pointer.to_address === address)
|
||||
.map((pointer) => ({
|
||||
sourceAddress: pointer.from_address,
|
||||
targetAddress: pointer.to_address,
|
||||
kind: "stored_pointer" as const,
|
||||
provenance: pointer.source,
|
||||
width: pointer.width,
|
||||
})),
|
||||
}),
|
||||
requiresSourceEvidence: true,
|
||||
referenceSource: {
|
||||
kind: "kuna-xrefs",
|
||||
direction: "to",
|
||||
@@ -1018,6 +1078,12 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
bytes: references.value.bytes,
|
||||
examined_rows: references.value.rows,
|
||||
excluded_alias_rows: references.value.excludedAliasRows,
|
||||
target_aliases: references.value.targetAliases.map(
|
||||
({ name, address }) => ({ name, address }),
|
||||
),
|
||||
walk_coverage: references.value.walkCoverage,
|
||||
pointer_coverage: references.value.pointerCoverage,
|
||||
pointers: references.value.pointers,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -140,6 +140,106 @@ const trace = async (analysis: AnalysisOperationPort) => {
|
||||
return result.structuredContent as { normalized_result: unknown };
|
||||
};
|
||||
|
||||
it("reports known reference-discovery gaps without claiming truncation or a missing edge", async () => {
|
||||
const baseline = provider();
|
||||
const gap =
|
||||
"An unwalked candidate may have outgoing references; its presence does not establish a reference to the selected target.";
|
||||
const output = await trace({
|
||||
execute: (operation, input, options) =>
|
||||
operation === "xrefs" && input.address === "0x10"
|
||||
? Promise.resolve(
|
||||
ok(
|
||||
createAnalysisExecution([], PROVIDER, {
|
||||
referenceCoverageGaps: [
|
||||
{ code: "unwalked", scope: "observation", description: gap },
|
||||
],
|
||||
}),
|
||||
),
|
||||
)
|
||||
: baseline.execute(operation, input, options),
|
||||
});
|
||||
expect(output.normalized_result).toMatchObject({
|
||||
truncated: false,
|
||||
residual_unknowns: [`References to 0x10: ${gap}`],
|
||||
references: [{ target_address: "0x11", source_address: "0x21" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("deduplicates an image-wide gap without merging target-specific gaps", async () => {
|
||||
const baseline = provider();
|
||||
const output = await trace({
|
||||
execute: async (operation, input, options) => {
|
||||
const result = await baseline.execute(operation, input, options);
|
||||
if (operation !== "xrefs" || !result.ok) return result;
|
||||
return ok({
|
||||
...result.value,
|
||||
referenceCoverageGaps: [
|
||||
{
|
||||
code: "missing-relocations",
|
||||
scope: "image",
|
||||
description: "Image relocation coverage unavailable.",
|
||||
},
|
||||
{
|
||||
code: "unwalked",
|
||||
scope: "observation",
|
||||
description: "This observation has unwalked sources.",
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
});
|
||||
expect(output.normalized_result).toMatchObject({
|
||||
truncated: false,
|
||||
residual_unknowns: [
|
||||
"Image relocation coverage unavailable.",
|
||||
"References to 0x10: This observation has unwalked sources.",
|
||||
"References to 0x11: This observation has unwalked sources.",
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("carries reported pointer classifications without inferring other reference kinds", async () => {
|
||||
const baseline = provider();
|
||||
const output = await trace({
|
||||
execute: (operation, input, options) =>
|
||||
operation === "xrefs" && input.address === "0x10"
|
||||
? Promise.resolve(
|
||||
ok(
|
||||
createAnalysisExecution(["0x20"], PROVIDER, {
|
||||
referenceClassifications: [
|
||||
{
|
||||
sourceAddress: "0x20",
|
||||
targetAddress: "0x10",
|
||||
kind: "stored_pointer",
|
||||
provenance: "chained-fixups",
|
||||
width: 8,
|
||||
},
|
||||
{
|
||||
sourceAddress: "0x20",
|
||||
targetAddress: "0x99",
|
||||
kind: "stored_pointer",
|
||||
provenance: "other",
|
||||
width: 4,
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
)
|
||||
: baseline.execute(operation, input, options),
|
||||
});
|
||||
expect(output.normalized_result).toMatchObject({
|
||||
references: [
|
||||
{
|
||||
source_address: "0x20",
|
||||
source_classifications: [
|
||||
{ kind: "stored_pointer", provenance: "chained-fixups", width: 8 },
|
||||
],
|
||||
},
|
||||
{ source_address: "0x21" },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves references and explicitly unknown owners without claiming truncation", async () => {
|
||||
const baseline = provider();
|
||||
const output = await trace({
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
analysisViewResultSchema,
|
||||
inspectAnalysisViewInputSchema,
|
||||
} from "../../../src/domain/analysisView/analysisView.js";
|
||||
import { XREFS_ADDRESS_PAGE_LIMITATION } from "../../../src/domain/analysisView/nativeObservationView.js";
|
||||
import { NATIVE_PRIMITIVE_OBSERVATION_OPERATIONS } from "../../../src/domain/native/nativeObservation.js";
|
||||
import { connectGhidraMcp } from "./ghidraMcpHarness.js";
|
||||
import { createMcpSchemaValidation } from "../../fixtures/mcpSchemaValidation.js";
|
||||
@@ -138,6 +139,9 @@ it("delivers native primitive and graph summaries with exact retained parents an
|
||||
},
|
||||
coverage: { total: 1, exhausted: true },
|
||||
});
|
||||
expect(
|
||||
analysisViewResultSchema.parse(page.normalized_result).unknowns,
|
||||
).toContain(XREFS_ADDRESS_PAGE_LIMITATION);
|
||||
const caveats = await call("inspect_analysis_view", {
|
||||
source,
|
||||
view: { kind: "native-context", facet: "limitations" },
|
||||
|
||||
@@ -286,6 +286,226 @@ it("keeps exact endpoints, deduplicates sites and counts excluded forwarding ali
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves pointer and walk coverage separately and rejects inconsistent pointer identity", () => {
|
||||
const pointer = {
|
||||
kind: "pointer",
|
||||
from_address: "@16384",
|
||||
from_address_hex: "0x4000",
|
||||
from_name: "slot",
|
||||
to_address: "@8192",
|
||||
to_address_hex: "0x2000",
|
||||
to_name: "callee",
|
||||
width: 8,
|
||||
source: "chained-fixups",
|
||||
};
|
||||
const source = {
|
||||
...xrefs(),
|
||||
coverage: {
|
||||
decoded: true,
|
||||
section: "__TEXT,__text",
|
||||
code_section: true,
|
||||
executable: true,
|
||||
unwalked: [
|
||||
{
|
||||
name: "middle",
|
||||
address: "@20480",
|
||||
address_hex: "0x5000",
|
||||
section: "__TEXT,__const",
|
||||
},
|
||||
],
|
||||
notes: ["middle was not walked"],
|
||||
},
|
||||
data_refs_coverage: "chained-fixups",
|
||||
data_refs_count: 1,
|
||||
data_refs: [pointer],
|
||||
};
|
||||
expect(
|
||||
parseKunaIncomingReferences(wire(source), input, "0x2000"),
|
||||
).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
addresses: ["0x1004", "0x4000"],
|
||||
pointerCoverage: "chained-fixups",
|
||||
pointers: [
|
||||
{
|
||||
from_address: "0x4000",
|
||||
to_address: "0x2000",
|
||||
source: "chained-fixups",
|
||||
},
|
||||
],
|
||||
walkCoverage: { unwalked: [{ address: "0x5000" }] },
|
||||
},
|
||||
});
|
||||
pointer.to_address_hex = "0x2001";
|
||||
expect(
|
||||
parseKunaIncomingReferences(wire(source), input, "0x2000"),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "xref_endpoints" },
|
||||
});
|
||||
});
|
||||
|
||||
it("counts pointer rows excluded because they target a forwarding alias", () => {
|
||||
const pointer = (from: number, to: number) => ({
|
||||
kind: "pointer",
|
||||
from_address: `@${from}`,
|
||||
from_address_hex: `0x${from.toString(16)}`,
|
||||
from_name: null,
|
||||
to_address: `@${to}`,
|
||||
to_address_hex: `0x${to.toString(16)}`,
|
||||
to_name: null,
|
||||
width: 8,
|
||||
source: "dynamic-relocations",
|
||||
});
|
||||
const source = {
|
||||
...xrefs(),
|
||||
data_refs_coverage: "dynamic-relocations",
|
||||
data_refs_count: 2,
|
||||
data_refs: [pointer(16384, 8192), pointer(16392, 12288)],
|
||||
};
|
||||
expect(
|
||||
parseKunaIncomingReferences(wire(source), input, "0x2000"),
|
||||
).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
addresses: ["0x1004", "0x4000"],
|
||||
rows: 5,
|
||||
excludedAliasRows: 2,
|
||||
targetAliases: [{ name: "slot", address: "0x3000" }],
|
||||
pointers: [
|
||||
{ from_address: "0x4000", to_address: "0x2000" },
|
||||
{ from_address: "0x4008", to_address: "0x3000" },
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves full-width pointer and unwalked addresses without numeric rounding", () => {
|
||||
const source = {
|
||||
binary: input,
|
||||
direction: "to",
|
||||
count: 0,
|
||||
target: {
|
||||
name: null,
|
||||
address: "@18446744073709551615",
|
||||
address_hex: "0xffffffffffffffff",
|
||||
aliases: [],
|
||||
},
|
||||
xrefs: [],
|
||||
coverage: {
|
||||
decoded: false,
|
||||
section: null,
|
||||
code_section: null,
|
||||
executable: null,
|
||||
unwalked: [
|
||||
{
|
||||
name: "candidate",
|
||||
address: "@9007199254740993",
|
||||
address_hex: "0x20000000000001",
|
||||
section: ".text",
|
||||
},
|
||||
],
|
||||
notes: [],
|
||||
},
|
||||
data_refs_coverage: "dynamic-relocations",
|
||||
data_refs_count: 1,
|
||||
data_refs: [
|
||||
{
|
||||
kind: "pointer",
|
||||
from_address: "@9007199254740995",
|
||||
from_address_hex: "0x20000000000003",
|
||||
from_name: null,
|
||||
to_address: "@18446744073709551615",
|
||||
to_address_hex: "0xffffffffffffffff",
|
||||
to_name: null,
|
||||
width: 8,
|
||||
source: "dynamic-relocations",
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(
|
||||
parseKunaIncomingReferences(wire(source), input, "0xffffffffffffffff"),
|
||||
).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
addresses: ["0x20000000000003"],
|
||||
rows: 1,
|
||||
excludedAliasRows: 0,
|
||||
pointers: [
|
||||
{ from_address: "0x20000000000003", to_address: "0xffffffffffffffff" },
|
||||
],
|
||||
walkCoverage: { unwalked: [{ address: "0x20000000000001" }] },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("distinguishes unreported pointer coverage from an explicitly empty pointer list", () => {
|
||||
const source = xrefs();
|
||||
expect(
|
||||
parseKunaIncomingReferences(wire(source), input, "0x2000"),
|
||||
).toMatchObject({
|
||||
ok: true,
|
||||
value: { pointerCoverage: null, pointers: null, walkCoverage: null },
|
||||
});
|
||||
expect(
|
||||
parseKunaIncomingReferences(
|
||||
wire({
|
||||
...source,
|
||||
data_refs_coverage: "none",
|
||||
data_refs_count: 0,
|
||||
data_refs: [],
|
||||
}),
|
||||
input,
|
||||
"0x2000",
|
||||
),
|
||||
).toMatchObject({
|
||||
ok: true,
|
||||
value: { pointerCoverage: "none", pointers: [], walkCoverage: null },
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects partial or inconsistent pointer inventories rather than reporting them empty", () => {
|
||||
const source = xrefs();
|
||||
for (const fields of [
|
||||
{ data_refs: [] },
|
||||
{ data_refs_count: 0 },
|
||||
{ data_refs_count: 1, data_refs: [] },
|
||||
]) {
|
||||
expect(
|
||||
parseKunaIncomingReferences(
|
||||
wire({ ...source, ...fields }),
|
||||
input,
|
||||
"0x2000",
|
||||
),
|
||||
).toEqual({
|
||||
ok: false,
|
||||
error: { code: "xref_count" },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves unknown coverage for an address outside known sections", () => {
|
||||
const source = {
|
||||
...xrefs(),
|
||||
coverage: {
|
||||
decoded: false,
|
||||
section: null,
|
||||
code_section: null,
|
||||
executable: null,
|
||||
unwalked: [],
|
||||
notes: [],
|
||||
},
|
||||
};
|
||||
expect(
|
||||
parseKunaIncomingReferences(wire(source), input, "0x2000"),
|
||||
).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
walkCoverage: { section: null, code_section: null, executable: null },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
"count",
|
||||
|
||||
Reference in new issue
Block a user