Keep ordinary Ghidra facts in normalized Evidence without repeating complete raw replies. Preserve distinct overview, attestation, file-offset and pre-overlay observations through shared CLI/MCP execution. Record real application measurements, equivalent selected facts, response admission and explicit latency/coverage limits. Fixes #5337. Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
10 KiB
Ghidra Evidence projection
Continues #5337. The bridge
adapter supplied a complete rawResult for every successful operation, even
when strict parsing preserved the same value in normalized_result. Function
observation envelopes repeated their value and already-projected qualifications.
The generic Evidence contract already permits raw_result: null in this case.
Owning change
The Ghidra inventory and function parsers now declare whether they retain an independent upstream representation. Execution consumes those parsed facts and named metadata, replacing the positional helper argument that required every caller to supply another complete report. CLI and MCP use the same projection.
Ordinary inventories and function observations omit the redundant copy. Limitations, relationship coverage and locations remain canonical Evidence fields. The following representations remain available:
- The overview's upstream segment permissions, sections and provenance.
- Load-image observations before local attestation and enrichment.
- File-offset selection roles, which generic locations alone do not express.
- The database type reply before adding a locally derived NativeAOT overlay.
No tool, schema, query default, provider protocol, cache, target execution or
annotation authority is added. Existing source Evidence is not rewritten.
New records have the identities appropriate to their new representation;
artifact and procedure identities remain unchanged. Consumers should read
function results from normalized_result, rather than raw_result.value.
Real inputs and completed question
Both ELF x86-64 targets are read without executing them:
| Target | Bytes | SHA-256 | Provenance |
|---|---|---|---|
true |
35,664 | c79bf44242829108e323378531f4ac839513ca1fba45efd6583643526e1e9fd2 |
Installed Debian coreutils 9.1-1; package metadata is a local observation. |
jq-linux-amd64 |
2,267,912 | b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f |
Official jq 1.8.2 asset; matches both the corpus and the release API's asset digest/size. |
Each fresh production stdio MCP session discovers the complete catalog, opens the target, reads documents and the complete observed procedure inventory, selects the first internal non-thunk procedure, reads overview/load image and six function operations, repeats inventory/pseudocode, exports Evidence and closes. This is one catalog discovery and 15 RPCs, including cleanup. It qualifies this inventory/function-read question, rather than application feature reconstruction or complete native-code discovery.
The live named SDK contracts validate replies. Each selected record binds to
the original target digest and Ghidra profile. The candidate's complete selected
fact vector equals the baseline after excluding only evidence_id and
raw_result: subject, parameters, normalized facts, provider/profile, authority,
limitations, relationships, locations and Evidence links remain equal. Exported
records match returned Evidence, repeated facts agree, and target hashes remain
unchanged. This is representation equivalence, not an independent qualification
of every Ghidra interpretation.
Measurements and limits
Linux x86-64, Node 24.18.0/npm 11.16.0, MCP SDK 2.3.1, Ghidra 12.1.4 and Eclipse Adoptium 21.0.12.1+1-LTS. Sessions and Java analysis are fresh; OS caches are warm and unflushed. Configured Java heap is 768 MiB with one active processor. The whole process family uses one logical CPU, 4 GiB hard/3 GiB early-stop memory, no swap and 192 tasks. No completed query task has an OOM/limit event or cleanup failure at verified payload exit. Final whole-scope counters after scope removal remain unavailable. Post-GC retained heap is unmeasured.
The small pair uses main 4e716b06078d725a53234503cbefbb340731e809 and its
candidate. Main advanced during the experiment; the large pair uses
3fca2e0ca6e54ba700c286d9a85a9600dee6a19d on both sides. Existing timing
samples keep their original runtime fingerprints. The curated receipt
retains individual observations and incomplete attempts; raw data stays outside Git.
| Recording / metric | Before | After |
|---|---|---|
true: observed procedures |
231 | 231 |
true: all RPC result bytes |
473,344 | 326,642 |
true: catalog + request arguments + results |
3,729,626 | 3,582,925 |
true: complete scope wall |
34.369 s | 34.901 s |
true: payload-family CPU |
33.150 s | 33.834 s |
true: kernel family peak including file cache |
947.9 MiB | 977.7 MiB |
jq: observed procedures |
2,279 | 2,279 |
jq: all RPC result bytes |
2,375,466 | 1,264,884 |
jq: catalog + request arguments + results |
5,639,932 | 4,529,351 |
jq: complete scope wall |
279.699 s | 399.097 s |
jq: payload-family CPU |
277.696 s | 352.660 s |
jq: kernel family peak including file cache |
1060.0 MiB | 1038.3 MiB |
RPC result bytes include the SDK's text and structured representations. Request objects and the complete catalog are included in the combined byte count; initialization, wire framing and model rendering are unmeasured. Actual model tokens and tokenizer estimates are unmeasured. The observed byte reduction does not establish a token saving or better agent investigation outcome.
The large result envelopes shrink by 46.8%; combined serialization shrinks by
19.7%, with unchanged catalog and call count. Small combined serialization
shrinks by 3.9%. These are single pairs, not distributions. No whole-task latency,
CPU or peak-memory improvement is established. The large candidate's first
list_documents costs 391.136 s versus 270.160 s before; this phase includes
startup, analysis wait and client work, not isolated engine time. Changed
projection code executes after the bridge returns its observation, but the
cause of the preparation-time variation is unestablished. One warm inventory
read costs 338.93 → 197.59 ms; this is not a general latency qualification.
A subsequent unchanged-main repeat with the same target/profile returned 2,276
procedures instead of 2,279, omitting 0x4f9930, 0x5260b0 and 0x529640
relative to its earlier inventory. The strict oracle rejected that control
before export. Its cause is unestablished; it is retained as an incomplete
attempt, not added to performance averages or treated as a projection defect.
Verification scope
The production-boundary capacity regression supplies 30,000 valid simulated
Ghidra procedure rows through GhidraProvider, BinarySession and MCP. On
unchanged production main it fails with resource_constraint at the existing
10 MiB response budget. The candidate delivers every row exactly, without a
budget increase or truncation. This qualifies response admission, not a real
30,000-function Ghidra analysis or a universal maximum procedure count.
Source/parser tests cover strict rejection, sidecars, distinct file-offset mapping roles, overview details, NativeAOT enrichment and unchanged observations. Executed checks and real-provider verification are recorded in the receipt. NativeAOT overlay tests use producer fixtures; real Windows/macOS and other engine versions remain unqualified here.
The existing real large-result helper passed through a private stdio harness:
oversized lookup/read/annotation/dossier replies retained complete Evidence,
subsequent queries recovered, complete CLI readback matched the original bytes
with raw_result: null, and generated target bytes remained unchanged.
The public verify-real-ghidra-address-context.mjs also passed with real
Ghidra/CLI/MCP, including file-offset selection, address round trips, recovery,
unchanged source and verified cleanup. These capacity/address fixtures use a
separate 6 GiB family cap; they are correctness checks, not latency pairs.
Three early true harness attempts used an incorrect subject-digest selector,
assumed the composed overview's provider was Ghidra, or misread the existing
finalizer's boolean result. The last completed its facts but exited nonzero;
none is a completed performance sample. A first jq candidate hit the SDK's
five-minute deadline while awaiting readiness, then cleaned up successfully.
The final driver's startup/RPC deadlines are ten minutes on both sides; product
defaults are unchanged. Earlier successful warm facts were rechecked from their
archived production replies when explicit warm-result assertions were added.
Reproduce
Build and retain separate baseline/candidate runtimes. Use the same absolute target path and caller-supplied Ghidra/JDK for both:
python3 experiments/analysis-efficiency/resource-run.py --observe-payload-exit -- \
node experiments/analysis-efficiency/ghidra-evidence-projection.mjs \
--runtime /absolute/baseline --target /absolute/application \
--ghidra /absolute/ghidra --java /absolute/jdk \
--expect duplicate --output /absolute/new-before
python3 experiments/analysis-efficiency/resource-run.py --observe-payload-exit -- \
node experiments/analysis-efficiency/ghidra-evidence-projection.mjs \
--runtime /absolute/candidate --target /absolute/application \
--ghidra /absolute/ghidra --java /absolute/jdk \
--expect compact --control /absolute/new-before/facts.json \
--output /absolute/new-after
The candidate's independent baseline comparison is included in harness costs; its isolated verification overhead is unmeasured. Static checks use a separate 8 GiB memory cap. All heavy operations run serially.