Files
rea/src/application/native/NativeValueTrace.ts
T
morluto c7345512ea perf(native): index admitted value-trace nodes
Replace a graph-wide node scan for each parameter use with membership in the set of admitted node IDs. Preserve node-budget omission, pagination, and existing ID-collision behavior.

Flameox profiles identify the eliminated scan. Six-run medians on a bounded synthetic four-procedure workload improved from 2538.17 ms to 220.32 ms (11.52x); full JSON responses have identical SHA-256 4071fc7ec17be01e6a23d53814a47b3b1bb0ebd80099d21ef8431da487801725. This measures production trace composition with a synthetic provider, not real Ghidra target speed.

External design research through DeepWiki was checked against Ghidra 12.1.4 DataGraph: https://github.com/NationalSecurityAgency/ghidra/blob/8b6bbb857accdfa20dc5b2f5dea471178c2e9fbc/Ghidra/Features/CodeCompare/src/main/java/ghidra/features/codecompare/graphanalysis/DataGraph.java
2026-10-11 06:10:10 +08:00

394 lines
13 KiB
TypeScript

import { functionDossierSchema } from "../../domain/hopperValues.js";
import { nativeCallTargetsSchema } from "../../domain/native/nativeInstruction.js";
import {
nativeValueTraceInputSchema,
nativeValueTraceSchema,
} from "../../domain/native/nativeValueTrace.js";
import { createEvidence } from "../../domain/evidence.js";
import {
AnalysisCancelledError,
AnalysisOutputError,
} from "../../domain/analysisErrorCore.js";
import { err, ok } from "../../domain/result.js";
import type { AnalysisOperationPort } from "../AnalysisProvider.js";
import type { EnhancedResult } from "../EnhancedToolTypes.js";
/** Compose bounded high-p-code def-use graphs across exactly referenced call destinations. */
export const traceNativeValues = async (
analysis: AnalysisOperationPort,
parameters: unknown,
signal?: AbortSignal,
): EnhancedResult => {
const input = nativeValueTraceInputSchema.parse(parameters);
const nodes: typeof nativeValueTraceSchema._output.nodes = [];
const nodeIds = new Set<string>();
const edges: typeof nativeValueTraceSchema._output.edges = [];
const procedures: typeof nativeValueTraceSchema._output.procedures = [];
const unknowns: typeof nativeValueTraceSchema._output.unknowns = [];
const callBindings: {
caller: string;
callee: string;
call: string;
argument_count: number;
output: boolean;
evidence_id: string;
}[] = [];
const pending = [{ procedure: input.procedure, depth: 0 }];
const visited = new Set<string>();
let targetSha256: string | undefined;
let truncated = false;
let decompilations = 0;
let callSites = 0;
let bytes = 0;
while (pending.length > 0) {
if (signal?.aborted)
return err(new AnalysisCancelledError("trace_native_values"));
const next = pending.shift();
if (next === undefined || visited.has(next.procedure)) continue;
if (decompilations >= input.max_functions) {
truncated = true;
unknowns.push({
procedure: next.procedure,
address: null,
reason: "decompilation budget reached",
});
break;
}
const execution = await analysis.execute(
"analyze_function",
{ procedure: next.procedure },
signal === undefined ? {} : { signal },
);
decompilations++;
if (!execution.ok) {
if (execution.error._tag === "AnalysisCancelledError")
return err(execution.error);
unknowns.push({
procedure: next.procedure,
address: null,
reason: execution.error.message,
});
continue;
}
const parsed = functionDossierSchema.safeParse(execution.value.result);
if (!parsed.success || execution.value.subject === null)
return err(
new AnalysisOutputError(
"trace_native_values",
"Function dossier lacks validated facts or artifact identity",
),
);
const dossier = parsed.data;
const address = dossier.procedure.address;
if (visited.has(address)) continue;
visited.add(next.procedure);
visited.add(address);
targetSha256 ??= execution.value.subject.sha256;
if (execution.value.subject.sha256 !== targetSha256)
return err(
new AnalysisOutputError(
"trace_native_values",
"Cross-function target digest mismatch",
),
);
const evidence = createEvidence(
execution.value.subject,
execution.value.provider,
{
operation: "analyze_function",
parameters: { procedure: next.procedure },
result: execution.value.result,
rawResult: execution.value.rawResult,
limitations: execution.value.limitations,
locations: execution.value.locations,
...(execution.value.analysisProfile === undefined
? {}
: { analysisProfile: execution.value.analysisProfile }),
},
);
const evidenceId = evidence.evidence_id;
procedures.push({
address,
depth: next.depth,
evidence_id: evidenceId,
provider: execution.value.provider.id,
analysis_profile_digest: execution.value.analysisProfile?.digest ?? null,
});
const flow = dossier.native_value_flow;
if (flow === null || !flow.available) {
unknowns.push({
procedure: address,
address: null,
reason: flow?.reason ?? "Provider has no native def-use model",
});
continue;
}
if (flow.truncated) {
truncated = true;
unknowns.push({
procedure: address,
address: null,
reason: "Provider p-code was truncated",
});
}
const ids = new Set<string>();
const qualified = (id: string) => `${address}/${id}`;
for (const operation of flow.operations) {
if (
operation.block_membership === "detached" ||
(operation.block_membership === "unavailable" && operation.is_dead)
)
continue;
const node = {
id: qualified(operation.id),
procedure: address,
evidence_id: evidenceId,
operation,
kind: "operation" as const,
parameter: null,
};
const size = Buffer.byteLength(JSON.stringify(node));
if (nodes.length >= input.max_nodes || bytes + size > 8 * 1024 * 1024) {
truncated = true;
break;
}
bytes += size;
ids.add(operation.id);
nodes.push(node);
nodeIds.add(node.id);
}
for (const parameter of flow.parameters) {
if (nodes.length >= input.max_nodes) {
truncated = true;
break;
}
const id = `${address}/parameter:${parameter.ordinal}`;
nodes.push({
id,
procedure: address,
evidence_id: evidenceId,
kind: "parameter",
operation: null,
parameter,
});
nodeIds.add(id);
}
for (const use of flow.parameter_uses) {
if (
!ids.has(use.use) ||
!nodeIds.has(`${address}/parameter:${use.ordinal}`)
)
continue;
if (edges.length >= input.max_edges) {
truncated = true;
break;
}
edges.push({
source: `${address}/parameter:${use.ordinal}`,
target: qualified(use.use),
input_index: use.input_index,
kind: "parameter-use",
status: "derived",
evidence_id: evidenceId,
});
}
for (const edge of flow.def_use) {
if (!ids.has(edge.definition) || !ids.has(edge.use)) continue;
if (edges.length >= input.max_edges) {
truncated = true;
break;
}
edges.push({
source: qualified(edge.definition),
target: qualified(edge.use),
input_index: edge.input_index,
kind: "def-use",
status: "derived",
evidence_id: evidenceId,
});
}
for (const operation of flow.operations) {
if (
!ids.has(operation.id) ||
!["CALL", "CALLIND"].includes(operation.opcode)
)
continue;
if (callSites >= input.max_call_sites) {
truncated = true;
unknowns.push({
procedure: address,
address: operation.address,
reason: "Call-site resolution budget reached",
});
break;
}
callSites++;
const resolved = await analysis.execute(
"resolve_native_call_targets",
{ address: operation.address },
signal === undefined ? {} : { signal },
);
if (!resolved.ok) {
if (resolved.error._tag === "AnalysisCancelledError")
return err(resolved.error);
unknowns.push({
procedure: address,
address: operation.address,
reason: resolved.error.message,
});
continue;
}
const targets = nativeCallTargetsSchema.safeParse(resolved.value.result);
if (!targets.success)
return err(
new AnalysisOutputError(
"resolve_native_call_targets",
"Invalid static call resolution",
),
);
if (targets.data.targets.length === 0)
unknowns.push({
procedure: address,
address: operation.address,
reason: `Call target ${targets.data.status}`,
});
for (const target of targets.data.targets) {
if (edges.length >= input.max_edges) {
truncated = true;
break;
}
const destination = target.procedure ?? target.address;
edges.push({
source: qualified(operation.id),
target: destination,
input_index: null,
kind: "call",
status: target.status,
evidence_id: evidenceId,
});
if (target.status !== "candidate")
callBindings.push({
caller: address,
callee: destination,
call: qualified(operation.id),
argument_count: Math.max(0, operation.inputs.length - 1),
output: operation.output !== null,
evidence_id: evidenceId,
});
if (next.depth < input.max_depth && target.status !== "candidate")
pending.push({ procedure: destination, depth: next.depth + 1 });
else if (!visited.has(destination)) {
truncated = true;
unknowns.push({
procedure: address,
address: operation.address,
reason:
target.status === "candidate"
? "Ambiguous call candidates were not traversed"
: "Call depth budget reached",
});
}
}
}
if (
truncated &&
(nodes.length >= input.max_nodes ||
edges.length >= input.max_edges ||
bytes >= 8 * 1024 * 1024)
)
break;
}
if (targetSha256 === undefined)
return err(
new AnalysisOutputError(
"trace_native_values",
"No seed function could be analyzed",
),
);
for (const binding of callBindings) {
const parameters = nodes.filter(
(node) => node.procedure === binding.callee && node.kind === "parameter",
);
if (parameters.length === 0)
unknowns.push({
procedure: binding.caller,
address: null,
reason: `Parameter bindings for ${binding.callee} are unavailable or omitted`,
});
for (const node of parameters) {
const ordinal = node.parameter?.ordinal;
if (ordinal === undefined || ordinal >= binding.argument_count) {
unknowns.push({
procedure: binding.caller,
address: null,
reason: `Argument ${ordinal ?? "unknown"} for ${binding.callee} was not recovered`,
});
continue;
}
if (edges.length >= input.max_edges) {
truncated = true;
break;
}
edges.push({
source: binding.call,
target: node.id,
input_index: ordinal + 1,
kind: "argument-binding",
status: "derived",
evidence_id: binding.evidence_id,
});
}
if (binding.output)
for (const node of nodes.filter(
(node) =>
node.procedure === binding.callee &&
node.operation?.opcode === "RETURN" &&
node.operation.inputs.length > 1,
)) {
if (edges.length >= input.max_edges) {
truncated = true;
break;
}
edges.push({
source: node.id,
target: binding.call,
input_index: 1,
kind: "return-binding",
status: "derived",
evidence_id: node.evidence_id,
});
}
}
const page = nodes.slice(input.offset, input.offset + input.limit);
const pageIds = new Set(page.map(({ id }) => id));
const result = nativeValueTraceSchema.parse({
seed: input.procedure,
target_sha256: targetSha256,
nodes: page,
edges: edges.filter(({ source }) => pageIds.has(source)),
procedures,
unknowns,
total_nodes: nodes.length,
total_edges: edges.length,
decompilations,
next_offset:
input.offset + page.length < nodes.length
? input.offset + page.length
: null,
truncated: truncated || page.length !== nodes.length,
limitations: [
"Static decompiler def-use graphs cover recovered block members; when membership is unavailable they exclude operations marked dead. Logical CALL argument positions bind to recovered HighParam ordinals and RETURN values bind to recovered CALL outputs; these are decompiler-derived dependencies, not original source or runtime observations. Missing parameters, variadic arguments and alias dependencies remain unknown.",
"LOAD and STORE are memory operations; object fields, persistent state and RNG semantics are not inferred from names or pseudocode.",
"Traversal caps depth, decompilations, call-site resolutions, nodes, edges and node payloads (8 MiB). Complete serialized output is bounded to 32 MiB. Pagination reruns analysis and returns edges whose source is on the node page; endpoints outside the page retain stable identities.",
],
});
if (Buffer.byteLength(JSON.stringify(result)) > 32 * 1024 * 1024)
return err(
new AnalysisOutputError(
"trace_native_values",
"Graph output exceeds 32 MiB; lower max_nodes, max_edges or page limit",
),
);
return ok(result);
};