1633 lines
64 KiB
Markdown
1633 lines
64 KiB
Markdown
# Product Requirements Document
|
|
|
|
## ghidra-cli
|
|
|
|
- Status: Draft
|
|
- Revision: 0.1
|
|
- Audience: maintainers and early users
|
|
- Repository name: `ghidra-cli`
|
|
- Executable name: `ghidr`
|
|
- License: Apache-2.0
|
|
|
|
## Summary
|
|
|
|
`ghidra-cli` is a small Rust command-line interface for reproducible, read-only
|
|
analysis and decompilation through Ghidra. It hides Ghidra project management
|
|
and Java process details behind commands that accept a sample path directly.
|
|
|
|
The repository retains the descriptive `ghidra-cli` name even though another
|
|
public project uses the same name for a similar purpose. The installed
|
|
executable will use a shorter distinct name. This implementation remains
|
|
independent and does not copy source from that GPL-3.0 project.
|
|
|
|
The project is developed with AI assistance and human review. AI assistance
|
|
does not relax provenance, licensing, security, testing, or maintainership
|
|
requirements. Maintainers are responsible for every accepted contribution.
|
|
|
|
The first release favors a narrow, reliable workflow over a broad command
|
|
surface. Automation agents are the primary consumers of its interface
|
|
contract. Human-readable terminal output is a first-class presentation of the
|
|
same underlying results, but it does not take priority over predictable
|
|
machine behavior.
|
|
|
|
## Problem
|
|
|
|
Ghidra supports headless analysis, but its native interface exposes concepts
|
|
such as project directories, project names, import and process modes, script
|
|
paths, and JVM configuration. Answering a simple question about a sample
|
|
therefore requires orchestration knowledge unrelated to reverse engineering.
|
|
|
|
Existing wrappers commonly address this by becoming persistent services with
|
|
background JVMs, sockets, job queues, project lifecycle commands, custom query
|
|
languages, and mutating operations. Those capabilities are useful in mature
|
|
interactive systems, but they increase the number of states that can fail and
|
|
make a small tool harder to trust.
|
|
|
|
## Product principles
|
|
|
|
1. A sample path is enough. Users do not manage Ghidra projects.
|
|
2. Read-only by default and throughout the first release.
|
|
3. Content determines identity. Moving or renaming a sample does not invalidate
|
|
compatible analysis.
|
|
4. Structured output is a public interface, not formatted log text.
|
|
5. Every failure is explicit, typed, and actionable.
|
|
6. No background process is required for the initial command set.
|
|
7. Concurrency is introduced only with a concrete concurrent workflow.
|
|
8. Ghidra remains the analysis engine; Rust owns orchestration and policy.
|
|
9. Automation behavior takes priority when machine and terminal conveniences
|
|
conflict.
|
|
10. Bounded output must disclose its bounds; truncation is never implicit.
|
|
11. Existing Unix tools perform ad hoc filtering; the CLI does not invent a
|
|
query language.
|
|
|
|
## Users
|
|
|
|
### Automation agent
|
|
|
|
The primary consumer. It needs deterministic commands, bounded output, stable
|
|
JSON schemas, meaningful exit codes, zero ambiguous resolution, and enough
|
|
provenance to explain where a result came from. It can compose JSON output with
|
|
standard tools such as `jq` before placing selected results in model context.
|
|
|
|
### Human analyst
|
|
|
|
Wants to inspect an unfamiliar executable, discover functions, and decompile a
|
|
selected function without opening the Ghidra GUI or learning its project model.
|
|
Human-readable output presents the same semantics as the automation contract.
|
|
|
|
### Maintainer
|
|
|
|
Needs a small dependency graph, reproducible fixtures, a clear Java boundary,
|
|
and compatibility failures that are discovered by tests rather than users.
|
|
|
|
## Goals for the first release
|
|
|
|
- Diagnose whether the local Ghidra and JDK installation is usable.
|
|
- Inspect basic sample and analysis metadata.
|
|
- List discovered functions with stable identifiers.
|
|
- Decompile one selected function.
|
|
- Reuse compatible analysis without requiring user-managed project names.
|
|
- Inspect and explicitly clean tool-owned stored data.
|
|
- Emit concise terminal output and versioned JSON.
|
|
- Bound Ghidra execution by configurable time and memory limits.
|
|
- Operate without network access after dependencies are installed.
|
|
- Run on x86-64 Linux through a pinned Nix package.
|
|
|
|
## Non-goals for the first release
|
|
|
|
- Replacing Ghidra's decompiler or analysis engine.
|
|
- A daemon, TCP bridge, HTTP service, or MCP server.
|
|
- Multiple concurrent Ghidra analyses.
|
|
- An interactive shell.
|
|
- Binary patching or exporting modified executables.
|
|
- Renaming symbols, applying types, or changing function signatures.
|
|
- Arbitrary Java or Python script execution.
|
|
- A custom filtering or query language.
|
|
- Manual Ghidra loader, processor-language, or compiler-specification
|
|
overrides.
|
|
- Raw Samples that require manual Target Specification.
|
|
- Automatically downloading Ghidra or a JDK.
|
|
- Managing user-visible Ghidra projects.
|
|
- Automatically evicting Analyses or Artifacts.
|
|
- Claiming support for untested host platforms.
|
|
- Supporting every historical Ghidra version.
|
|
|
|
## Initial platform and distribution
|
|
|
|
Version 0.1 officially supports `x86_64-linux`. The implementation should keep
|
|
host-specific behavior isolated, but successful compilation on another host
|
|
does not constitute support without passing the real Ghidra integration suite.
|
|
|
|
The supported installation is a pinned Nix flake package containing compatible
|
|
versions of `ghidra-cli`, Ghidra 12.1.2, and JDK 21. The flake pins a nixpkgs
|
|
revision that provides those versions; it does not float with a registry or
|
|
channel. Users can run the package without a global installation:
|
|
|
|
```console
|
|
nix run <flake-reference> -- doctor
|
|
```
|
|
|
|
macOS is the next intended platform. It becomes supported only after CI or a
|
|
maintainer-owned Mac runs the same integration fixtures used on Linux. Other
|
|
Linux architectures and non-Nix installation methods are deferred.
|
|
|
|
Version 0.1 has no persistent configuration file. Runtime policy is expressed
|
|
through documented CLI flags and narrowly scoped environment variables. A
|
|
configuration file is deferred until repeated real-world usage demonstrates
|
|
which settings deserve persistence.
|
|
|
|
## Verified Sample targets
|
|
|
|
Version 0.1 verifies these Sample targets through real Ghidra integration
|
|
fixtures and reviewed golden outputs:
|
|
|
|
- ELF, x86-64 little-endian.
|
|
- PE32+, x86-64.
|
|
|
|
The supported host and verified Sample target are independent: the x86-64 Linux
|
|
host analyzes both Linux and Windows executable formats. Mach-O, ARM64,
|
|
firmware images, raw binaries, and other Ghidra targets are unverified in
|
|
version 0.1.
|
|
|
|
A Target Specification that Ghidra recognizes but the version 0.1 matrix does
|
|
not cover is analyzed on a best-effort basis. Success remains success, but
|
|
response provenance includes:
|
|
|
|
```json
|
|
{
|
|
"target": {
|
|
"loader": "<loader-name>",
|
|
"format": "<format-name>",
|
|
"processor_language": "<language-id>",
|
|
"compiler_specification": "<compiler-spec-id>",
|
|
"verification": "unverified"
|
|
},
|
|
"warnings": [
|
|
{
|
|
"code": "unverified_target",
|
|
"message": "target is outside the verified integration-test matrix",
|
|
"details": {
|
|
"verified_targets": [
|
|
"elf-x86_64-little-endian",
|
|
"pe32plus-x86_64"
|
|
]
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
`verification` describes this project's integration coverage, not Ghidra's
|
|
confidence in its own result. Failure by Ghidra to recognize a Target
|
|
Specification remains a structured error. Ambiguous auto-detection also fails
|
|
and returns bounded candidate Target Specifications; the CLI never chooses one
|
|
silently.
|
|
|
|
Ghidra upgrades are deliberate reviewed changes. An upgrade must change the
|
|
pinned nixpkgs revision, pass the complete real-Ghidra integration suite, and
|
|
explain any protocol schema or golden-fixture differences before merge.
|
|
|
|
## Command-line experience
|
|
|
|
### Installation diagnosis
|
|
|
|
```console
|
|
$ ghidr --format human doctor
|
|
Ghidra 12.1.2: ready
|
|
JDK 21: ready
|
|
Analysis store: writable
|
|
```
|
|
|
|
`doctor` must not modify a Sample or create an Analysis. It may create and
|
|
remove a temporary file inside the Analysis Store to verify writability.
|
|
It reports the store path and total disk usage.
|
|
|
|
Successful `doctor` response data has this version 0.1 shape:
|
|
|
|
```json
|
|
{
|
|
"ready": true,
|
|
"components": {
|
|
"ghidra": {
|
|
"status": "ready",
|
|
"version": "12.1.2",
|
|
"launcher": {
|
|
"encoding": "utf8",
|
|
"value": "/nix/store/.../bin/ghidra-analyzeHeadless"
|
|
}
|
|
},
|
|
"java": {
|
|
"status": "ready",
|
|
"version": "21",
|
|
"executable": {
|
|
"encoding": "utf8",
|
|
"value": "/nix/store/.../bin/java"
|
|
}
|
|
},
|
|
"analysis_store": {
|
|
"status": "ready",
|
|
"path": {
|
|
"encoding": "utf8",
|
|
"value": "/home/user/ghidr-store"
|
|
},
|
|
"source": "home_fallback",
|
|
"usage": {
|
|
"logical_bytes": 1048576,
|
|
"allocated_bytes": 1114112
|
|
}
|
|
},
|
|
"sandbox": {
|
|
"status": "ready",
|
|
"backend": "bubblewrap",
|
|
"verification": "verified"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Each component status is exactly one of `ready`, `missing`, `incompatible`,
|
|
`invalid`, or `unwritable`. The aggregate `ready` is true only when every
|
|
required component is ready. `doctor` runs every safe check instead of stopping
|
|
at the first failure, and it removes its Analysis Store write probe before
|
|
returning.
|
|
|
|
Storage usage is always represented by both `logical_bytes`, the sum of file
|
|
lengths, and `allocated_bytes`, the sum of Linux `st_blocks * 512` including
|
|
directory allocation. Store scans use `lstat` and never follow symlinks.
|
|
|
|
A healthy report is a success document on stdout with exit status 0. If any
|
|
required component is not ready, `doctor` emits a `doctor_failed` error on
|
|
stderr with exit status 1; `error.details.report` contains the same complete
|
|
shape with `ready` set to false. This preserves strict JSON framing while
|
|
allowing one invocation to diagnose every component.
|
|
|
|
### Inspect a sample
|
|
|
|
```console
|
|
$ ghidr --format human inspect ./sample
|
|
Sample: 8d969eef...
|
|
Format: ELF
|
|
Language: x86:LE:64
|
|
Functions: 42
|
|
```
|
|
|
|
If no compatible Analysis exists, the command creates one automatically. The
|
|
same bytes at another path reuse the Analysis. Different bytes at the same path
|
|
create a different Analysis.
|
|
|
|
Successful `inspect` response data has this minimal version 0.1 shape:
|
|
|
|
```json
|
|
{
|
|
"sample": {
|
|
"sha256": "<64-character digest>",
|
|
"size_bytes": 123456
|
|
},
|
|
"program": {
|
|
"image_base": {
|
|
"space": "ram",
|
|
"offset": "0x0000000000400000"
|
|
},
|
|
"minimum_address": {
|
|
"space": "ram",
|
|
"offset": "0x0000000000400000"
|
|
},
|
|
"maximum_address": {
|
|
"space": "ram",
|
|
"offset": "0x0000000000404fff"
|
|
},
|
|
"function_count": 42
|
|
},
|
|
"analysis": {
|
|
"disposition": "created"
|
|
}
|
|
}
|
|
```
|
|
|
|
The three Address fields are nullable when Ghidra has no applicable memory
|
|
Address. `function_count` includes memory, external, and thunk Functions: the
|
|
same population returned by `functions`. `analysis.disposition` is exactly one
|
|
of `created`, `reused`, or `rebuilt`. Executable format, processor language,
|
|
compiler specification, Ghidra version, and Analysis Profile digest live in
|
|
the common response provenance and are not duplicated here.
|
|
|
|
### List functions
|
|
|
|
```console
|
|
$ ghidr --format human functions ./sample
|
|
ADDRESS SIZE NAME
|
|
0x00401000 112 _start
|
|
0x00401120 248 main
|
|
```
|
|
|
|
The command returns a bounded result set and discloses whether additional
|
|
results exist. It does not implement a filtering expression language in the
|
|
first release; JSON consumers can use existing tools such as `jq`.
|
|
|
|
`functions` includes every Ghidra Function, including external/imported
|
|
functions and thunks. Each item explicitly states `location`, `is_external`,
|
|
`is_thunk`, and `decompilable`. External functions remain visible for program
|
|
structure but are not valid decompilation targets.
|
|
|
|
The minimal version 0.1 function-list item is:
|
|
|
|
```json
|
|
{
|
|
"name": "parse",
|
|
"qualified_name": "Widget::parse",
|
|
"entry": {
|
|
"space": "ram",
|
|
"offset": "0x0000000000401120"
|
|
},
|
|
"body_address_count": 248,
|
|
"location": "memory",
|
|
"is_external": false,
|
|
"is_thunk": false,
|
|
"thunk_target_entry": null,
|
|
"decompilable": true
|
|
}
|
|
```
|
|
|
|
`thunk_target_entry` is an Address when Ghidra resolves a thunk target and is
|
|
otherwise `null`. Version 0.1 deliberately omits signatures, parameters,
|
|
locals, call counts, and analyzer-derived heuristics from this collection.
|
|
Those details require targeted Queries rather than making every list item
|
|
larger and less stable.
|
|
|
|
The `functions` Query uses this deterministic total order:
|
|
|
|
1. Functions whose `location` is `memory`, followed by functions whose
|
|
`location` is `external`.
|
|
2. `entry.space` in byte-for-byte ascending order.
|
|
3. `entry.offset` in unsigned numeric ascending order.
|
|
|
|
Ghidra permits only one Function at an entry Address within an Analysis, so the
|
|
Address completes the ordering and no symbol-name tie-breaker is required. The
|
|
Query reports the order identifier `location_then_entry_ascending`.
|
|
|
|
`name` is Ghidra's exact Function basename (`getName(false)`).
|
|
`qualified_name` is Ghidra's exact fully qualified Function name
|
|
(`getName(true)`), including its `::` namespace spelling. The CLI preserves
|
|
case, UTF-8 bytes, generated names such as `FUN_00401120`, and analyzer-applied
|
|
demangling. It does not independently demangle, simplify templates, strip
|
|
namespaces, or Unicode-normalize names.
|
|
|
|
Function Selector comparison uses those exact strings. Even a qualified name
|
|
that resolves to multiple Functions produces the normal ambiguity error with
|
|
entry Addresses. Structured namespaces and alternate symbol names are deferred
|
|
to a targeted symbol or Function-detail Query.
|
|
|
|
Collection Queries share these flags:
|
|
|
|
```console
|
|
ghidr functions ./sample --limit 100
|
|
ghidr functions ./sample --limit 100 --offset 100
|
|
ghidr functions ./sample --all
|
|
ghidr decompile ./sample --name main --max-inline-bytes 1048576
|
|
```
|
|
|
|
- `--limit <COUNT>` sets the maximum number of returned items.
|
|
- `--offset <COUNT>` skips that many items in the documented stable order.
|
|
- `--all` explicitly requests every item.
|
|
- Omitting both `--limit` and `--all` applies a default limit of 100 items.
|
|
- `--all` cannot be combined with `--limit` or `--offset`.
|
|
- A zero `--limit` is invalid; `--all` is the only spelling for unbounded
|
|
collection output.
|
|
- Invalid combinations fail as invalid arguments rather than choosing an
|
|
implicit precedence.
|
|
- `--max-inline-bytes <BYTES>` overrides the 65,536-byte inline budget with a
|
|
positive integer measured strictly in bytes.
|
|
- `--max-inline-bytes 0`, unit suffixes, and an unbounded-inline sentinel are
|
|
invalid.
|
|
|
|
Collection JSON includes explicit page metadata:
|
|
|
|
```json
|
|
{
|
|
"page": {
|
|
"order": "location_then_entry_ascending",
|
|
"offset": 0,
|
|
"limit": 100,
|
|
"returned": 100,
|
|
"total": 342,
|
|
"has_more": true
|
|
},
|
|
"items": []
|
|
}
|
|
```
|
|
|
|
The ordering is part of the Query contract. Repeating a page against the same
|
|
immutable Analysis returns the same items. Human-readable output ends with a
|
|
concise notice when more results exist.
|
|
|
|
### Oversized results
|
|
|
|
Item limits alone do not bound response size because one item, such as a
|
|
decompiled function or extracted string, may be very large. Every response is
|
|
therefore also subject to a serialized-byte policy. The default successful
|
|
inline budget is 65,536 bytes of serialized output.
|
|
|
|
The CLI never truncates a JSON document or silently removes fields to meet that
|
|
budget. When a complete result would exceed it, the CLI writes the result
|
|
atomically as an Artifact in the Analysis Store and returns a successful,
|
|
bounded descriptor:
|
|
|
|
```json
|
|
{
|
|
"schema_version": 1,
|
|
"kind": "decompilation",
|
|
"provenance": {
|
|
"ghidr_version": "0.1.0",
|
|
"adapter_protocol_version": 1,
|
|
"ghidra_version": "12.1.2",
|
|
"java_version": "21",
|
|
"sample_sha256": "<digest>",
|
|
"source_path": {
|
|
"encoding": "utf8",
|
|
"value": "./sample"
|
|
},
|
|
"analysis_profile_sha256": "<digest>",
|
|
"target": {
|
|
"loader": "<loader-name>",
|
|
"format": "<format-name>",
|
|
"processor_language": "<language-id>",
|
|
"compiler_specification": "<compiler-spec-id>",
|
|
"verification": "verified"
|
|
},
|
|
"analysis_store": {
|
|
"path": {
|
|
"encoding": "utf8",
|
|
"value": "/home/user/ghidr-store"
|
|
},
|
|
"source": "home_fallback"
|
|
},
|
|
"sandbox": {
|
|
"backend": "bubblewrap",
|
|
"verification": "verified"
|
|
},
|
|
"limits": {
|
|
"max_heap_mib": 2048,
|
|
"max_cpu": 2,
|
|
"analysis_timeout_seconds": 600,
|
|
"decompile_timeout_seconds": 60,
|
|
"child_watchdog_seconds": 780
|
|
}
|
|
},
|
|
"data": {
|
|
"spilled": true,
|
|
"artifact": {
|
|
"path": {
|
|
"encoding": "utf8",
|
|
"value": "/home/user/ghidr-store/artifacts/<sample>/<profile>/<digest>.json"
|
|
},
|
|
"bytes": 2097152,
|
|
"sha256": "<artifact-digest>",
|
|
"media_type": "application/json",
|
|
"contains": "complete_success_response"
|
|
}
|
|
},
|
|
"warnings": []
|
|
}
|
|
```
|
|
|
|
Spilling is not an error: the requested result was produced in full and remains
|
|
available to local tools such as `jq` and `rg`. Human-readable output reports
|
|
the Artifact path, size, and digest instead of printing partial content.
|
|
|
|
The Artifact contains the complete success response that would otherwise have
|
|
been written to stdout, including `schema_version`, `kind`, `provenance`,
|
|
command `data`, and `warnings`. It uses UTF-8 JSON without a byte-order mark,
|
|
the same compact serializer as stdout, and exactly one trailing LF. The
|
|
reported `bytes` and `sha256` cover the exact file bytes, including that LF.
|
|
|
|
Artifact creation is atomic. A completed Artifact is immutable until explicit
|
|
cleanup and never recursively spills regardless of its own size. Version 0.1
|
|
does not add an Artifact-reading command: local automation reads the tagged
|
|
absolute path directly with tools such as `jq` and may verify the SHA-256
|
|
before use. `contains` is exactly `complete_success_response`.
|
|
|
|
The caller's inline budget applies strictly to successful stdout documents.
|
|
Before publishing an Artifact, the CLI serializes its prospective descriptor.
|
|
If that descriptor exceeds the selected budget, the CLI removes the temporary
|
|
Artifact and fails with `inline_budget_too_small`. Typed error details contain
|
|
`configured_bytes` and the exact `required_bytes`; retrying at that value or
|
|
higher can return the descriptor.
|
|
|
|
Error documents use an independent fixed 65,536-byte serialized safety cap.
|
|
This guarantees that a bounded stderr response can explain why no valid success
|
|
document fit. A successful document never exceeds `--max-inline-bytes`.
|
|
|
|
`--all` removes the collection item bound; it does not remove the serialized
|
|
byte budget. Only `--max-inline-bytes <BYTES>` changes that budget.
|
|
|
|
### Decompile one function
|
|
|
|
Successful `decompile` response data has this minimal version 0.1 shape:
|
|
|
|
```json
|
|
{
|
|
"requested_selector": {
|
|
"kind": "name",
|
|
"value": "main"
|
|
},
|
|
"function": {
|
|
"name": "main",
|
|
"qualified_name": "main",
|
|
"entry": {
|
|
"space": "ram",
|
|
"offset": "0x0000000000401120"
|
|
}
|
|
},
|
|
"decompilation": {
|
|
"syntax": "ghidra_c",
|
|
"text": "int main(void) {\n return 0;\n}\n"
|
|
}
|
|
}
|
|
```
|
|
|
|
`requested_selector` records the exact selector kind and value supplied by the
|
|
caller. `function` records the unique Function that selector resolved to.
|
|
`syntax` is `ghidra_c` because the text is Ghidra's C-like representation and
|
|
is not guaranteed to be valid ISO C. Decompiler warnings use the response
|
|
envelope's normal `warnings` collection rather than being embedded into the
|
|
text. A timeout or failure never returns partial decompilation text.
|
|
|
|
The adapter normalizes CRLF and bare CR line endings in `decompilation.text` to
|
|
LF. It preserves every other character exactly as returned by Ghidra,
|
|
including indentation, comments, blank lines, trailing spaces, and the
|
|
presence or absence of a final newline. Version 0.1 does not reformat generated
|
|
code. A future formatter or richer representation may be added in response to
|
|
demonstrated usage, but it must be an explicit contract change rather than an
|
|
unannounced alteration of `text`.
|
|
|
|
### Clean stored data
|
|
|
|
```console
|
|
ghidr clean ./sample
|
|
ghidr clean ./sample --dry-run
|
|
ghidr clean --digest <64-character-sha256>
|
|
ghidr clean --all
|
|
ghidr clean --all --dry-run
|
|
ghidr clean --all --yes
|
|
```
|
|
|
|
The first release never evicts an Analysis or Artifact automatically. Cleanup
|
|
is an explicit user request. `clean --all` requires interactive confirmation;
|
|
non-interactive callers must provide `--yes` deliberately.
|
|
|
|
`clean <SAMPLE>` hashes the current Sample bytes, then removes every Analysis
|
|
and Artifact for that Sample digest across all Analysis Profiles. It never
|
|
removes data for another Sample. Cleanup reports the affected Analysis Profiles
|
|
and the logical and allocated usage associated with removed targets.
|
|
|
|
`clean --digest <SHA256>` provides the same Sample-wide behavior when the
|
|
original Sample is no longer available. The digest must contain exactly 64
|
|
hexadecimal characters. `<SAMPLE>`, `--digest`, and `--all` are mutually
|
|
exclusive cleanup targets; specifying none or more than one is an invalid
|
|
argument.
|
|
|
|
Cleanup executes by default. `--dry-run` reports the exact targets and their
|
|
logical and allocated usage without deleting anything. Sample- and digest-specific
|
|
cleanup requires no confirmation because the destructive verb and target are
|
|
both explicit.
|
|
|
|
Store-wide cleanup follows the selected output format:
|
|
|
|
- JSON mode never prompts. Without `--yes`, it fails with
|
|
`confirmation_required`; the typed error details include the prospective item
|
|
counts and usage. The caller may then repeat the request with `--yes`.
|
|
- Human mode prompts only when attached to a terminal.
|
|
- Human mode without a terminal requires `--yes`.
|
|
- `--dry-run` never prompts and never deletes in either format.
|
|
|
|
Successful cleanup response data has this version 0.1 shape:
|
|
|
|
```json
|
|
{
|
|
"mode": "executed",
|
|
"target": {
|
|
"kind": "sample",
|
|
"source_path": {
|
|
"encoding": "utf8",
|
|
"value": "./sample"
|
|
},
|
|
"sha256": "<sample digest>"
|
|
},
|
|
"matched": {
|
|
"analyses": 2,
|
|
"quarantined_analyses": 1,
|
|
"artifacts": 3,
|
|
"diagnostic_logs": 4,
|
|
"usage": {
|
|
"logical_bytes": 5242880,
|
|
"allocated_bytes": 5308416
|
|
},
|
|
"analysis_profile_sha256": [
|
|
"<digest-a>",
|
|
"<digest-b>"
|
|
]
|
|
},
|
|
"removed": {
|
|
"analyses": 2,
|
|
"quarantined_analyses": 1,
|
|
"artifacts": 3,
|
|
"diagnostic_logs": 4,
|
|
"usage": {
|
|
"logical_bytes": 5242880,
|
|
"allocated_bytes": 5308416
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
`mode` is exactly `executed` or `dry_run`. `target` is a tagged union:
|
|
`sample` contains the caller's source Path and resolved SHA-256, `digest`
|
|
contains the supplied SHA-256, and `all` has no additional fields. `matched`
|
|
records the preflight snapshot. Its Analysis Profile digests are unique and
|
|
bytewise sorted. For a dry run, `removed` is `null`.
|
|
|
|
A successful execution reports exact removed counts, including Quarantined
|
|
Analyses and Diagnostic Logs. Matching nothing is a successful result with
|
|
zero counts. A large cleanup report uses the normal Artifact-spill behavior.
|
|
The reported allocation is the preflight allocation associated with the
|
|
successfully deleted targets, not a claim about the filesystem's observed
|
|
free-space delta; compression, reflinks, metadata, and concurrent activity can
|
|
make those values differ.
|
|
|
|
Cleanup uses a recoverable two-phase transaction:
|
|
|
|
1. Resolve every target and acquire its lock in deterministic digest order.
|
|
2. If any lock is held, fail with `analysis_busy` before changing store data.
|
|
3. Atomically rename each target into a tool-owned cleanup transaction
|
|
directory on the same filesystem.
|
|
4. If any staging rename fails, roll back completed renames and return an
|
|
error.
|
|
5. Delete staged targets only after every rename succeeds.
|
|
6. Report success only after every staged target has actually been deleted.
|
|
|
|
If deletion fails after partial progress, return retryable
|
|
`cleanup_incomplete`. Typed details include exact removed and remaining item
|
|
counts, logical and allocated usage, and the cleanup transaction's tagged
|
|
absolute Path.
|
|
Transaction contents can never satisfy a Query. A later matching `clean`
|
|
resumes deletion of the transaction before cleaning newly matched active data.
|
|
This provides recoverable interruption behavior even though deleting multiple
|
|
filesystem objects cannot be made truly atomic.
|
|
|
|
### Decompile a function
|
|
|
|
```console
|
|
$ ghidr --format human decompile ./sample --name main
|
|
int main(int argc, char **argv)
|
|
{
|
|
...
|
|
}
|
|
```
|
|
|
|
Exactly one explicit Function Selector is required:
|
|
|
|
```console
|
|
ghidr decompile ./sample --name main
|
|
ghidr decompile ./sample --address ram:0x0000000000401120
|
|
```
|
|
|
|
`--name` performs exact symbol resolution. An ambiguous symbol is an error and
|
|
returns bounded candidates instead of silently choosing one. `--address`
|
|
requires an explicit address space and canonical hexadecimal offset. The flags
|
|
are mutually exclusive; positional selector guessing is unsupported.
|
|
|
|
`--address` must equal a function entry Address. An interior Address fails with
|
|
`function_entry_required`; typed error details include the containing
|
|
function's entry when Ghidra identifies one. Version 0.1 does not provide a
|
|
containing-address selector.
|
|
|
|
`--name` is exact and case-sensitive. A qualified name matches only that full
|
|
qualified name. An unqualified name matches exact basenames across namespaces:
|
|
one match succeeds, zero fails with `function_not_found`, and multiple fail
|
|
with `function_selector_ambiguous`. Version 0.1 performs no fuzzy, glob, regex,
|
|
or demangled-substring matching.
|
|
|
|
### Structured output
|
|
|
|
```console
|
|
$ ghidr inspect ./sample
|
|
```
|
|
|
|
JSON is the unconditional default. Human-readable output is explicit:
|
|
|
|
```console
|
|
$ ghidr --format human inspect ./sample
|
|
```
|
|
|
|
JSON uses a versioned envelope:
|
|
|
|
```json
|
|
{
|
|
"schema_version": 1,
|
|
"kind": "inspection",
|
|
"provenance": {
|
|
"ghidr_version": "0.1.0",
|
|
"adapter_protocol_version": 1,
|
|
"ghidra_version": "12.1.2",
|
|
"java_version": "21",
|
|
"sample_sha256": "<digest>",
|
|
"source_path": {
|
|
"encoding": "utf8",
|
|
"value": "./sample"
|
|
},
|
|
"analysis_profile_sha256": "<digest>",
|
|
"target": {
|
|
"loader": "<loader-name>",
|
|
"format": "<format-name>",
|
|
"processor_language": "<language-id>",
|
|
"compiler_specification": "<compiler-spec-id>",
|
|
"verification": "verified"
|
|
},
|
|
"analysis_store": {
|
|
"path": {
|
|
"encoding": "utf8",
|
|
"value": "/home/user/ghidr-store"
|
|
},
|
|
"source": "xdg"
|
|
},
|
|
"sandbox": {
|
|
"backend": "bubblewrap",
|
|
"verification": "verified"
|
|
},
|
|
"limits": {
|
|
"max_heap_mib": 2048,
|
|
"max_cpu": 2,
|
|
"analysis_timeout_seconds": 600,
|
|
"decompile_timeout_seconds": null,
|
|
"child_watchdog_seconds": 720
|
|
}
|
|
},
|
|
"data": {},
|
|
"warnings": []
|
|
}
|
|
```
|
|
|
|
Every Sample-backed Query (`inspect`, `functions`, and `decompile`) uses this
|
|
fixed provenance shape. `analysis_store.source` is exactly one of `cli`,
|
|
`environment`, `xdg`, or `home_fallback`. `target.verification` is exactly
|
|
`verified` or `unverified`. `sandbox.backend` is exactly `bubblewrap`,
|
|
`external`, or `off`, and its `verification` is `verified`, `unverified`, or
|
|
`disabled`. `decompile_timeout_seconds` is `null` for a Query with no
|
|
decompilation phase. Limits report selected policy even when a compatible
|
|
Analysis is reused; the derived watchdog records the bound for the current
|
|
command. Command-specific facts remain in `data`.
|
|
|
|
Every path exposed through JSON uses a tagged Path object. A valid UTF-8 path
|
|
is represented without alteration:
|
|
|
|
```json
|
|
{
|
|
"encoding": "utf8",
|
|
"value": "/tmp/sample"
|
|
}
|
|
```
|
|
|
|
A Linux path containing non-UTF-8 bytes uses standard padded Base64 over its
|
|
exact Unix byte sequence:
|
|
|
|
```json
|
|
{
|
|
"encoding": "unix_bytes_base64",
|
|
"value": "L3RtcC9zYW1wbGX/"
|
|
}
|
|
```
|
|
|
|
Caller-supplied relative Sample paths remain relative and byte-exact in
|
|
provenance. Tool-resolved locations, including the Analysis Store, Artifacts,
|
|
and Diagnostic Logs, are absolute. Human output renders non-UTF-8 bytes with
|
|
an explicit escaped representation.
|
|
|
|
JSON mode is a strict framing protocol:
|
|
|
|
- On success, stdout contains exactly one JSON document and stderr is empty.
|
|
- On failure, stdout is empty and stderr contains exactly one JSON error
|
|
document.
|
|
- Progress is suppressed.
|
|
- Ghidra and adapter diagnostics are written as Diagnostic Logs in the Analysis
|
|
Store rather than mixed into either stream.
|
|
- An error includes the path, byte size, and content digest of its Diagnostic
|
|
Log when one is available.
|
|
|
|
CLI discovery is the sole framing exception. `ghidr --help`, every
|
|
`ghidr <command> --help`, and `ghidr --version` are successful metadata
|
|
requests rather than operations: they emit plain UTF-8 text on stdout, leave
|
|
stderr empty, exit with status 0, and never initialize Ghidra, access the
|
|
Analysis Store, or execute a command.
|
|
|
|
Human-readable mode may present progress and concise diagnostics on stderr.
|
|
|
|
Every Ghidra/adapter invocation captures diagnostics into a private temporary
|
|
log. An ordinary successful invocation deletes that capture. Failure, timeout,
|
|
forced termination, protocol violation, or a successful warning that
|
|
explicitly references diagnostics causes the CLI to atomically retain it as a
|
|
Diagnostic Log.
|
|
|
|
Sample-backed Diagnostic Logs remain until cleanup of that Sample or
|
|
`clean --all`. Logs from non-Sample commands such as `doctor` remain until
|
|
`clean --all`. Version 0.1 performs no automatic log expiry. Store directories
|
|
use mode `0700` and log files use mode `0600`.
|
|
|
|
The CLI never intentionally records Sample bytes, the caller's source Path,
|
|
the full environment, or secrets from environment variables. However, Ghidra
|
|
may echo symbols, strings, or other Sample-derived content. Diagnostic Logs are
|
|
therefore sensitive rather than claimed to be fully redacted. Every retained
|
|
log descriptor includes `"sensitive": true`, its tagged absolute Path, exact
|
|
file-byte count, SHA-256, and media type.
|
|
|
|
A retained Diagnostic Log is an immutable directory bundle:
|
|
|
|
```text
|
|
diagnostics/<id>/
|
|
├── manifest.json
|
|
├── stdout.log
|
|
└── stderr.log
|
|
```
|
|
|
|
`manifest.json` is schema-versioned JSON containing invocation metadata,
|
|
termination reason, timestamps, stream byte counts, truncation flags,
|
|
encodings, and SHA-256 digests. `stdout.log` and `stderr.log` contain the exact
|
|
raw child-process bytes; the manifest states whether each stream is valid
|
|
UTF-8 or arbitrary bytes. Agents can query metadata with `jq` and search
|
|
ordinary text diagnostics directly with `rg`, `tail`, or `sed` without JSON
|
|
escaping or Base64 overhead.
|
|
|
|
The entire bundle is published atomically by renaming its private temporary
|
|
directory. Its descriptor points to the tagged absolute Path of
|
|
`manifest.json`. Version 0.1 uses no SQLite diagnostic database; a future index
|
|
may be built over immutable manifests without changing the bundle format.
|
|
|
|
The CLI drains child stdout and stderr concurrently so a full pipe cannot block
|
|
Ghidra. Version 0.1 applies a fixed 8 MiB capture limit independently to each
|
|
stream. A stream within the limit is retained completely as `stdout.log` or
|
|
`stderr.log`. For a larger stream, the bundle retains its first 4 MiB and last
|
|
4 MiB as separate segment files and inserts no synthetic bytes into either.
|
|
|
|
The manifest records total observed bytes, captured bytes, `truncated: true`,
|
|
and each segment's original byte offset, length, tagged relative Path, and
|
|
SHA-256. Diagnostic truncation does not turn an otherwise successful operation
|
|
into failure. Any retained result referencing that bundle emits the
|
|
`diagnostics_truncated` warning. Version 0.1 exposes no diagnostic-size flag;
|
|
real usage must justify adding one.
|
|
|
|
Every warning has exactly three fields:
|
|
|
|
```json
|
|
{
|
|
"code": "unverified_target",
|
|
"message": "target is outside the verified integration-test matrix",
|
|
"details": {
|
|
"verified_targets": [
|
|
"elf-x86_64-little-endian",
|
|
"pe32plus-x86_64"
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
`code` is a stable machine identifier. `message` is concise human context and
|
|
must not be parsed for control flow. `details` is always an object with a
|
|
warning-code-specific schema and is `{}` when no structured context applies.
|
|
Warnings have no `severity` or `retryable` field because every member of the
|
|
collection is already non-fatal. A warning never disguises incomplete required
|
|
data; that condition is an error.
|
|
|
|
The encoder deduplicates warnings that have the same code and canonicalized
|
|
details, then sorts the collection in bytewise ascending `code` order followed
|
|
by canonicalized-details order. Warning collections remain subject to the
|
|
normal serialized-byte bound.
|
|
|
|
### Schema compatibility
|
|
|
|
Reviewed JSON Schema documents are published under `schemas/v1/`. Every
|
|
response carries `"schema_version": 1` and validates against its corresponding
|
|
schema.
|
|
|
|
Within schema v1:
|
|
|
|
- New optional fields are compatible.
|
|
- Consumers must ignore unknown fields.
|
|
- Existing fields cannot be removed.
|
|
- Field types and meanings cannot change.
|
|
|
|
Removing a field or changing its type or meaning requires a new major schema
|
|
version. The CLI emits only its current schema in the first release; maintaining
|
|
multiple output encoders is deferred until real compatibility demand justifies
|
|
it. Golden fixtures and schema-validation tests gate every response change.
|
|
This policy is recorded in
|
|
[ADR 0001](./docs/adr/0001-version-the-json-contract.md).
|
|
|
|
## Functional requirements
|
|
|
|
### Sample identity
|
|
|
|
- Compute a SHA-256 digest from the Sample bytes.
|
|
- Open the caller's path read-only and record source metadata before copying.
|
|
- Reject regular Samples larger than 1,073,741,824 bytes before copying.
|
|
- Allow `--max-sample-bytes <BYTES>` to replace that limit with a positive
|
|
integer.
|
|
- Reject zero and unlimited sentinel values for maximum Sample size.
|
|
- Accept regular files and symlinks whose opened target is a regular file.
|
|
- Accept non-UTF-8 Linux Sample paths because Ghidra receives only the
|
|
tool-generated private snapshot path.
|
|
- Reject directories, FIFOs, sockets, and device files.
|
|
- Enforce maximum Sample size while streaming even when initial file metadata
|
|
reports a smaller size.
|
|
- Before copying, require available Analysis Store filesystem space of at least
|
|
the observed Sample size plus 1,073,741,824 reserve bytes.
|
|
- If later Analysis exhausts disk space, fail the Analysis, preserve
|
|
diagnostics where possible, and remove staging.
|
|
- Copy bytes into a private staging snapshot while computing SHA-256.
|
|
- Record source metadata again after copying and fail with `sample_changed` if
|
|
it changed.
|
|
- Pass only the staged snapshot to Ghidra; Java never reads the caller's path.
|
|
- Remove the snapshot after successful import or any failure.
|
|
- Treat identical bytes as the same Sample regardless of path.
|
|
- Never write to the Sample.
|
|
- Record the observed path as provenance, not identity.
|
|
|
|
The digest describes the exact staged bytes used for Analysis. This boundary is
|
|
recorded in
|
|
[ADR 0005](./docs/adr/0005-analyze-a-staged-sample-snapshot.md).
|
|
|
|
### Analysis compatibility
|
|
|
|
An Analysis is reusable only when its Sample digest and Analysis Profile match.
|
|
At minimum, the Analysis Profile includes:
|
|
|
|
- Ghidra version.
|
|
- Java version.
|
|
- Analysis-adapter version.
|
|
- Ghidra language and compiler specification.
|
|
- Fully resolved loader options.
|
|
- Enabled analyzer configuration.
|
|
- Maximum analysis CPU count.
|
|
|
|
The canonical Profile document has this version 0.1 shape:
|
|
|
|
```json
|
|
{
|
|
"profile_version": 1,
|
|
"ghidra_version": "12.1.2",
|
|
"java_version": "21",
|
|
"analysis_adapter_version": 1,
|
|
"target": {
|
|
"loader": "<loader-id>",
|
|
"processor_language": "<language-id>",
|
|
"compiler_specification": "<compiler-spec-id>"
|
|
},
|
|
"loader_options": [],
|
|
"analyzer_options": [],
|
|
"max_cpu": 2
|
|
}
|
|
```
|
|
|
|
Option collections contain fully resolved typed entries rather than only user
|
|
overrides. Entries sort bytewise by canonical option name and reject duplicate
|
|
names. Canonical JSON uses UTF-8, lexicographically ordered object keys, no
|
|
insignificant whitespace, decimal integers, no floating-point values, and no
|
|
trailing LF. Strings remain byte-exact UTF-8 without Unicode normalization.
|
|
The Analysis Profile digest is SHA-256 over those exact canonical bytes, and
|
|
its schema is published under `schemas/v1/`.
|
|
|
|
Sample digest, source Path, Analysis Store Path, timestamps, inline and adapter
|
|
response limits, Java heap limit, and timeouts do not affect Profile identity.
|
|
Target Verification is also excluded because changing the project's
|
|
integration-test coverage does not change Ghidra's Analysis. `max_cpu` is
|
|
included conservatively because parallel analysis may influence ordering or
|
|
tie resolution. The Sample digest and Profile digest together identify an
|
|
Analysis.
|
|
|
|
An incompatible Analysis is preserved until normal retention policy removes it;
|
|
it is not overwritten in place.
|
|
|
|
Version 0.1 uses one analyzer policy: Ghidra 12.1.2's default auto-analysis
|
|
options. The Java adapter records the fully resolved option set in canonical
|
|
form, and its digest is part of the Analysis Profile. Users cannot enable,
|
|
disable, or tune individual analyzers in version 0.1.
|
|
|
|
### Analysis execution
|
|
|
|
- Invoke the official Ghidra headless launcher as a child process.
|
|
- Pass requests to a small, bundled Java adapter.
|
|
- Exchange structured request and response files rather than scrape logs.
|
|
- Capture Ghidra logs separately for diagnostics.
|
|
- Build new Analyses in a staging location.
|
|
- Promote a staged Analysis atomically only after full successful analysis and
|
|
adapter validation.
|
|
- Treat Ghidra's native partial results after timeout as unusable.
|
|
- On timeout, interruption, or analysis failure, preserve the Diagnostic Log
|
|
and remove the partial staging project.
|
|
- Pass a 600-second native analysis timeout to Ghidra by default.
|
|
- Allow `--analysis-timeout-seconds <SECONDS>` to replace that default with a
|
|
positive integer.
|
|
- Reject zero and unlimited sentinel values for analysis timeout.
|
|
- Apply a hard child-process watchdog equal to the sum of enabled native phase
|
|
timeouts plus 120 seconds.
|
|
- Give a cached metadata Query with no separately bounded native phase a
|
|
120-second child-process watchdog.
|
|
- Derive the watchdog automatically; do not expose a competing total-timeout
|
|
flag.
|
|
- Terminate the child process after each command in the first release.
|
|
- Report whether termination was graceful or forced.
|
|
|
|
Bubblewrap/Ghidra starts in its own process group. On watchdog timeout, the
|
|
harness marks the invocation timed out, sends `SIGTERM` to the entire group,
|
|
continues draining diagnostics for a fixed 10-second grace period, then sends
|
|
`SIGKILL` and reaps the group if any process remains. A first user interrupt
|
|
uses the same sequence but retains exit status 130; a second interrupt during
|
|
the grace period escalates immediately to `SIGKILL`.
|
|
|
|
Bubblewrap uses parent-death handling so a crashed harness cannot leave the
|
|
worker running. Once cancellation begins, no response or Analysis data can be
|
|
promoted even if the worker races to report success. The harness retains
|
|
diagnostics, removes staging, and reports termination as `graceful` or
|
|
`forced`. Watchdog timeout exits 124 and user interruption exits 130. The grace
|
|
period is not configurable in version 0.1.
|
|
|
|
The harness performs one worker attempt per invocation. It does not hide
|
|
additional attempts behind transport fallback, timeout escalation, or
|
|
automatic retry. A caller may explicitly repeat a failed read-only command
|
|
when its typed error is retryable.
|
|
|
|
Each adapter invocation uses a private directory containing `request.json`,
|
|
`response.json.tmp`, and `response.json`. Rust atomically writes mode-`0600`
|
|
`request.json` before launching Ghidra. The request contains
|
|
`protocol_version: 1`, a random 128-bit invocation ID, an operation enum,
|
|
validated tool-owned Paths, selected limits, and operation-specific arguments.
|
|
Only the generated request and response Paths are passed as adapter arguments;
|
|
Sample selectors and other untrusted values remain inside JSON.
|
|
|
|
Version 0.1 limits serialized `request.json` to 1,048,576 bytes (1 MiB).
|
|
Rust enforces the ceiling before writing the request or launching Ghidra. Java
|
|
independently bounds the bytes read from the request Path and rejects an
|
|
oversized request before JSON parsing. An oversized request reaching Java
|
|
indicates a harness defect or tampering and is reported by Rust as
|
|
`protocol_violation`; the limit is internal and has no CLI override. Sample
|
|
bytes never appear in `request.json`.
|
|
|
|
The Java adapter strictly rejects unknown or missing protocol fields because
|
|
this is an internal boundary whose two sides ship together. It writes
|
|
`response.json.tmp`, flushes it, and atomically renames it to `response.json`
|
|
only after serialization completes. The response echoes the protocol version,
|
|
invocation ID, and operation.
|
|
|
|
Rust accepts only the final regular response file and validates all three
|
|
echoed values plus the operation-specific schema. A missing, malformed,
|
|
mismatched, or merely temporary response is a `protocol_violation`; child
|
|
output remains diagnostic data and is never parsed as a fallback result. The
|
|
private invocation directory is removed after response validation or after its
|
|
Diagnostic Log has been retained.
|
|
|
|
Version 0.1 limits a serialized adapter response to 268,435,456 bytes
|
|
(256 MiB). Java enforces the ceiling while serializing, and Rust independently
|
|
rejects an oversized final response. On overflow, Java discards the partial
|
|
temporary response and returns a small `result_too_large` protocol response
|
|
containing `limit_bytes`, `observed_at_least_bytes`, and the operation kind.
|
|
|
|
No partial Query data or Artifact is published. The completed Analysis remains
|
|
reusable because only the Query failed. `result_too_large` is non-retryable
|
|
with identical arguments, although a narrower collection page may succeed.
|
|
Version 0.1 exposes no response-size override flag; observed usage must justify
|
|
raising or configuring the limit.
|
|
|
|
### Decompilation execution
|
|
|
|
- Apply a 60-second Ghidra decompiler timeout per selected Function by default.
|
|
- Allow `--decompile-timeout-seconds <SECONDS>` to replace that default with a
|
|
positive integer.
|
|
- Reject zero and unlimited sentinel values for decompilation timeout.
|
|
- Include the selected decompiler timeout in the derived child-process
|
|
watchdog.
|
|
|
|
### JVM memory
|
|
|
|
- Retain Ghidra headless's default 2,048 MiB maximum Java heap.
|
|
- Allow `--max-heap-mib <MIB>` to set a positive integer heap limit.
|
|
- Describe this value as a Java heap limit, not a total process-memory or RSS
|
|
limit.
|
|
- Treat an out-of-memory failure as an Analysis or Query failure, preserve its
|
|
Diagnostic Log, and never promote staged Analysis data.
|
|
|
|
### CPU usage
|
|
|
|
- Pass `-max-cpu 2` to Ghidra by default.
|
|
- Allow `--max-cpu <COUNT>` to set a positive integer core limit.
|
|
- Reject zero and negative values rather than inheriting Ghidra's implicit
|
|
coercion behavior.
|
|
- Record the selected core limit in response provenance.
|
|
|
|
### Analysis Store
|
|
|
|
- Use this version 0.1 layout:
|
|
|
|
```text
|
|
<store>/
|
|
├── store.json
|
|
├── analyses/<sample-sha256>/<profile-sha256>/
|
|
│ ├── manifest.json
|
|
│ └── project/
|
|
│ ├── analysis.gpr
|
|
│ └── analysis.rep/
|
|
├── artifacts/<sample-sha256>/<profile-sha256>/<artifact-sha256>.json
|
|
├── diagnostics/
|
|
│ ├── <sample-sha256>/<invocation-id>/
|
|
│ └── global/<invocation-id>/
|
|
├── quarantine/<sample-sha256>/<profile-sha256>/<invocation-id>/
|
|
├── locks/<sample-sha256>/<profile-sha256>.lock
|
|
├── staging/<invocation-id>/
|
|
└── cleanup/<transaction-id>/
|
|
```
|
|
|
|
- Use only lowercase ASCII labels, digests, and random IDs for tool-generated
|
|
path components; no generated component starts with `.`.
|
|
- Record the store-layout version in `store.json`.
|
|
- Give every complete Analysis one `manifest.json` and one fixed Ghidra project
|
|
named `analysis`.
|
|
- Promote the whole Analysis directory from `staging` through a same-filesystem
|
|
atomic rename.
|
|
- Move the whole invalid Analysis directory into its unique quarantine path.
|
|
- Name Artifacts by the SHA-256 of their exact file bytes.
|
|
- Scope Diagnostic Log bundles to their Sample digest when available and to
|
|
`global` otherwise.
|
|
- Keep locks outside Analysis directories so promotion and quarantine cannot
|
|
move an active lock.
|
|
- Reject symlinks anywhere inside tool-owned storage as corruption.
|
|
- Treat directory traversal as authoritative; version 0.1 has no SQLite or
|
|
global mutable index.
|
|
- Record these fields in every Analysis `manifest.json`:
|
|
|
|
```json
|
|
{
|
|
"manifest_version": 1,
|
|
"state": "complete",
|
|
"sample": {
|
|
"sha256": "<digest>",
|
|
"size_bytes": 123456
|
|
},
|
|
"analysis_profile": {
|
|
"sha256": "<digest>",
|
|
"ghidra_version": "12.1.2",
|
|
"adapter_protocol_version": 1,
|
|
"target": {},
|
|
"analyzer_options_sha256": "<digest>"
|
|
},
|
|
"created_by": {
|
|
"ghidr_version": "0.1.0",
|
|
"invocation_id": "<id>",
|
|
"completed_at": "<RFC3339 UTC>"
|
|
},
|
|
"project": {
|
|
"name": "analysis",
|
|
"path": "project",
|
|
"files": [
|
|
{
|
|
"path": "analysis.gpr",
|
|
"size_bytes": 1234,
|
|
"sha256": "<digest>"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
- Inventory project files in bytewise relative-Path order.
|
|
- Reject symlinks, special files, missing files, unexpected files, and recorded
|
|
size or SHA-256 mismatches.
|
|
- Recompute Sample and Analysis Profile digests and require them to match their
|
|
directory names.
|
|
- Hash the complete project tree before reusing an Analysis.
|
|
- Open cached Queries through Ghidra's read-only mode.
|
|
- Hash the complete tree again before accepting the Query result, proving the
|
|
read-only invocation did not mutate it.
|
|
- Treat any validation mismatch as corruption and apply the accepted quarantine
|
|
and single-rebuild policy.
|
|
- Prefer this correctness-first double validation in version 0.1; optimize it
|
|
only after measurement demonstrates material cost.
|
|
- Resolve the store through this fixed precedence:
|
|
1. `--store <ABSOLUTE_PATH>`.
|
|
2. `$GHIDR_STORE`.
|
|
3. `$XDG_CACHE_HOME/ghidr` when `XDG_CACHE_HOME` is explicitly set and the
|
|
resulting path passes Ghidra path validation.
|
|
4. `~/ghidr-store`.
|
|
- Fail when an explicitly configured store path is invalid.
|
|
- Require a UTF-8 Analysis Store path because it crosses the Java/Ghidra
|
|
boundary. Reject a non-UTF-8 resolved store with `invalid_store_path`; typed
|
|
error details retain its exact bytes using the tagged Path representation.
|
|
- Never select the working directory automatically.
|
|
- Report the resolved path and its source as `cli`, `environment`, `xdg`, or
|
|
`home_fallback` through `doctor` and response provenance.
|
|
- Keep the store local to one machine in the first release; synchronized or
|
|
shared Ghidra project storage is unsupported.
|
|
- Use atomic writes for manifests, Artifacts, and Diagnostic Logs.
|
|
- Store oversized Query results as complete, content-digested Artifacts.
|
|
- Detect incomplete Analysis creation after interruption.
|
|
- Never expose or reuse an incomplete Analysis.
|
|
- Acquire an OS-backed per-Analysis lock before staging.
|
|
- Fail immediately with retryable `analysis_busy` when another process holds
|
|
the same Analysis lock; include the Analysis Profile digest and a suggested
|
|
retry delay.
|
|
- Never start duplicate Ghidra work for the same Sample and Analysis Profile.
|
|
- Allow concurrent reads of a completed immutable Analysis.
|
|
- Fail cleanup with `analysis_busy` rather than remove targeted data that is in
|
|
use.
|
|
- Atomically move a structurally corrupt cached Analysis into quarantine.
|
|
- Never use a Quarantined Analysis to satisfy a Query.
|
|
- Attempt one fresh rebuild after quarantining corrupt data.
|
|
- Return a `corrupt_analysis_rebuilt` warning when that rebuild succeeds and a
|
|
structured failure when it does not.
|
|
- Preserve quarantined manifests and Diagnostic Logs until explicit cleanup.
|
|
- Retain Analyses and Artifacts until an explicit cleanup request.
|
|
- Never apply automatic age- or size-based eviction in the first release.
|
|
- Treat the entire store as disposable.
|
|
|
|
### Output behavior
|
|
|
|
- Default to JSON for terminals, pipes, and non-interactive execution.
|
|
- Support explicit `--format json` and `--format human` values.
|
|
- Never select a format through terminal detection.
|
|
- Include schema version and provenance in every JSON response.
|
|
- Bound collection responses and state the applied bound.
|
|
- Apply the selected serialized-byte budget to every successful response and
|
|
the fixed 65,536-byte safety cap to every error response.
|
|
- Never omit undisclosed results from a successful response.
|
|
- Never truncate a JSON document or field to satisfy the byte budget.
|
|
- Spill oversized results atomically and return a bounded Artifact descriptor.
|
|
- Support `--limit` and `--offset` for bounded collection pages.
|
|
- Require `--all` for unbounded collection output.
|
|
- Reject ambiguous or conflicting collection flags.
|
|
- Document a stable order for every pageable Query.
|
|
- Use hexadecimal addresses with an explicit address space where necessary.
|
|
- Represent every JSON Address as an object containing `space` and `offset`.
|
|
- Preserve the exact case-sensitive UTF-8 name returned by Ghidra in
|
|
`Address.space`; do not case-fold or Unicode-normalize it.
|
|
- Compare and order address-space names by their exact UTF-8 bytes.
|
|
- Require `--address SPACE:0x...` to match an address-space name exactly.
|
|
- Return `address_space_not_found` with bounded valid candidates for an unknown
|
|
space, allowing an emitted Address to round-trip into a selector unchanged.
|
|
- Encode an Address offset as a lowercase, `0x`-prefixed hexadecimal string
|
|
padded to the Address-space width; never encode it as a JSON number.
|
|
- Keep JSON composable with standard tools such as `jq`.
|
|
- Keep JSON-mode stdout and stderr free of all content outside their single
|
|
success or error document.
|
|
- Suppress progress in JSON mode and retain full diagnostics as Diagnostic
|
|
Logs.
|
|
- Provide concise human-readable presentation only when explicitly requested.
|
|
|
|
### Errors and exit status
|
|
|
|
Distinct error categories must cover:
|
|
|
|
- Invalid arguments.
|
|
- Missing or unreadable Sample.
|
|
- Unsupported or ambiguous format/language.
|
|
- Missing or incompatible Ghidra/JDK installation.
|
|
- Analysis timeout.
|
|
- Ghidra analysis failure.
|
|
- Function not found or function selector ambiguous.
|
|
- Invalid or incompatible cached Analysis.
|
|
- Required destructive confirmation not supplied.
|
|
- Internal protocol violation.
|
|
|
|
In the default JSON format, every failure emits no stdout and emits one
|
|
versioned error document on stderr:
|
|
|
|
```json
|
|
{
|
|
"schema_version": 1,
|
|
"kind": "error",
|
|
"error": {
|
|
"code": "function_selector_ambiguous",
|
|
"message": "symbol 'parse' resolves to more than one function",
|
|
"retryable": false,
|
|
"details": {
|
|
"candidates": []
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
`code` is a stable machine identifier. `message` is concise human context and
|
|
must not be parsed for control flow. `retryable` states whether repeating the
|
|
same logical request may reasonably succeed without changing its arguments.
|
|
`details` has an error-code-specific schema and remains subject to collection
|
|
and serialized-byte bounds.
|
|
|
|
Argument-parsing failures follow the same contract when the caller requested
|
|
JSON. Ambiguous Function Selector resolution is a failure with bounded
|
|
candidates in `details`, never a partial success.
|
|
|
|
Exit statuses are deliberately coarse:
|
|
|
|
- `0`: success.
|
|
- `1`: runtime failure.
|
|
- `2`: invalid invocation or arguments.
|
|
- `124`: timeout.
|
|
- `130`: interrupted by the user.
|
|
|
|
The versioned JSON `error.code` provides precise diagnosis within those broad
|
|
classes. This allocation is recorded in
|
|
[ADR 0002](./docs/adr/0002-keep-exit-statuses-coarse.md).
|
|
|
|
## Architecture boundary
|
|
|
|
### Rust CLI responsibilities
|
|
|
|
- Argument parsing and validation.
|
|
- Sample hashing and stable identity.
|
|
- Analysis Profile construction.
|
|
- Analysis Store layout, locking, and atomicity.
|
|
- Child-process lifecycle, timeouts, and log capture.
|
|
- Typed request and response schemas.
|
|
- Published JSON Schema documents and compatibility checks.
|
|
- Human and JSON rendering.
|
|
- Exit-code policy.
|
|
|
|
Rust owns the authoritative compile-time operation registry. Each operation
|
|
definition includes its request and response types, public command kind,
|
|
published schema name, stable category, and one-sentence agent description.
|
|
The category and description are internal metadata in version 0.1, retained so
|
|
a future adapter can generate capability discovery without duplicating command
|
|
definitions. Runtime reflection and dynamic operation registration are out of
|
|
scope.
|
|
|
|
### Java adapter responsibilities
|
|
|
|
- Use supported Ghidra APIs.
|
|
- Import and analyze a Sample.
|
|
- Resolve Function Selectors.
|
|
- Extract program and function metadata.
|
|
- Invoke the Ghidra decompiler.
|
|
- Serialize only the response defined by the protocol.
|
|
|
|
The Java adapter must not listen on a network socket, download dependencies, or
|
|
execute user-provided scripts.
|
|
|
|
A conformance test compares the authoritative Rust operation registry with the
|
|
Java dispatch set, published schemas, and golden fixtures. It fails when Rust
|
|
declares an operation Java cannot handle, Java accepts an undeclared operation,
|
|
or an operation lacks its required schema or golden coverage.
|
|
|
|
## Rust implementation policy
|
|
|
|
Version 0.1 starts as one library-plus-binary Cargo package rather than a
|
|
multi-crate workspace:
|
|
|
|
```text
|
|
src/
|
|
├── main.rs
|
|
├── lib.rs
|
|
├── cli.rs
|
|
├── commands/
|
|
├── domain/
|
|
├── output/
|
|
├── process/
|
|
├── protocol/
|
|
├── sandbox/
|
|
└── store/
|
|
```
|
|
|
|
Supporting source lives under `java/`, `schemas/v1/`, `tests/`, and
|
|
`fixtures/`. Domain and protocol types belong in the Rust library so
|
|
integration tests exercise the same implementation as the executable.
|
|
|
|
The initial dependency set is deliberately small:
|
|
|
|
- `clap` for typed CLI parsing.
|
|
- `serde` and `serde_json` for protocol and public JSON.
|
|
- `schemars` for schemas generated from the same Rust types.
|
|
- `sha2`, `hex`, and `base64` for identities and byte representations.
|
|
- `thiserror` for typed internal errors.
|
|
- `rustix` for Linux locks, process groups, signals, and filesystem metadata.
|
|
- `getrandom` for 128-bit invocation and transaction IDs.
|
|
- A small RFC3339 time implementation selected during scaffolding.
|
|
- Test-only `assert_cmd`, `tempfile`, and `jsonschema`.
|
|
|
|
Version 0.1 has no async runtime, plugin framework, database, general logging
|
|
framework, or multi-crate workspace. The repository commits `Cargo.lock`, pins
|
|
Rust through Nix, and checks dependency licenses, advisories, and sources with
|
|
`cargo-deny`. New dependencies require concrete behavior, compatible
|
|
permissive licensing, and review of their transitive trees.
|
|
|
|
## Synchronous execution decision
|
|
|
|
The first release uses a synchronous command lifecycle because each invocation
|
|
coordinates one Ghidra child process and returns one result. Ghidra performs the
|
|
CPU-intensive work; an asynchronous Rust runtime would not make that work
|
|
faster.
|
|
|
|
The design must not prevent a future asynchronous frontend. A server, parallel
|
|
batch analysis, interactive session, or responsive cancellation channel would
|
|
constitute a concrete reason to add an async runtime.
|
|
|
|
## Security and safety
|
|
|
|
- Treat every Sample and every Ghidra-produced value as untrusted input.
|
|
- Do not execute the Sample.
|
|
- Do not provide arbitrary script execution.
|
|
- Do not require network access during analysis.
|
|
- Do not emit telemetry or send Sample-derived data, metadata, or diagnostics
|
|
to a remote destination.
|
|
- Avoid including Sample bytes or extracted secrets in routine logs.
|
|
- Validate all paths crossing the Rust/Java boundary.
|
|
- Apply bounded output, memory, and execution-time policies.
|
|
- Document the supported Ghidra version and relevant security advisories.
|
|
|
|
The trusted Rust harness remains outside the narrow Ghidra worker sandbox. It
|
|
owns Sample snapshotting, locks, limits, validation, the Analysis Store, and
|
|
atomic promotion. The sandboxed Ghidra/JVM child receives only:
|
|
|
|
- The tool-generated staged Sample as read-only.
|
|
- The current project or staging directory with the minimum required access.
|
|
- The private request/response invocation directory.
|
|
- The read-only pinned Nix closure.
|
|
- A private home and temporary directory.
|
|
|
|
It receives no caller source Path, broader workspace, unrelated Analysis data,
|
|
or network access. The harness validates all worker output before promotion.
|
|
This boundary is recorded in
|
|
[ADR 0006](./docs/adr/0006-sandbox-the-ghidra-worker.md).
|
|
|
|
Version 0.1 supports three explicit sandbox modes:
|
|
|
|
- `--sandbox bubblewrap` is the default. Bubblewrap is a separately maintained
|
|
dependency, not part of Ghidra, and is included in the pinned Nix package.
|
|
It isolates mount, network, PID, IPC, and UTS namespaces and uses
|
|
parent-death handling.
|
|
- `--sandbox external` declares that the calling agent/tool harness already
|
|
provides equivalent isolation. `ghidr` cannot verify that policy, so
|
|
provenance reports `verification: unverified` and emits
|
|
`external_sandbox_unverified`.
|
|
- `--sandbox off` is an explicit debugging escape hatch. Provenance reports
|
|
`verification: disabled` and emits `sandbox_disabled`.
|
|
|
|
The CLI never auto-detects or silently changes sandbox mode. `doctor` verifies
|
|
an actual Java/Ghidra probe for the Bubblewrap backend. External providers
|
|
integrate through the surrounding harness in version 0.1; the CLI does not
|
|
accept an arbitrary sandbox-runner command. A provider-neutral internal
|
|
backend boundary permits reviewed native integrations later. Sandbox policy is
|
|
response provenance, not Analysis Profile identity.
|
|
|
|
## Performance expectations
|
|
|
|
Correctness and reproducibility take priority over startup latency in the first
|
|
release.
|
|
|
|
- A compatible cached Analysis should avoid repeating auto-analysis.
|
|
- Metadata Queries should reuse exported Artifacts where correctness permits.
|
|
- Targeted decompilation may start a new JVM.
|
|
- The CLI should explain whether it reused or created an Analysis when verbose
|
|
diagnostics are enabled.
|
|
|
|
Persistent workers and interactive sessions are deferred until measured usage
|
|
shows JVM startup to be the dominant usability problem.
|
|
|
|
## Test strategy
|
|
|
|
- Run `cargo fmt --check`.
|
|
- Run `cargo clippy --all-targets --all-features -- -D warnings`.
|
|
- Run `cargo test --all-targets`.
|
|
- Run `cargo deny check`.
|
|
- Make `nix flake check` the complete local and Forgejo CI entry point.
|
|
- Forbid Rust `unsafe` in version 0.1.
|
|
- Deny `unwrap`, `expect`, `panic`, `todo`, and `unimplemented` in production
|
|
code while permitting narrowly scoped use in tests.
|
|
- Select useful strict Clippy lints explicitly rather than enabling every
|
|
pedantic lint.
|
|
- Compile Java with applicable lint warnings enabled and treated as errors.
|
|
- Apply a deterministic Java formatter.
|
|
- Format-check Nix and run `statix`.
|
|
- Unit-test Sample identity, profile identity, store layout, schema validation,
|
|
selector parsing, and error mapping without Ghidra.
|
|
- Validate every structured response against its published JSON Schema.
|
|
- Validate structured errors and their typed details against published schemas.
|
|
- Verify the Rust operation registry, Java dispatch set, published schemas, and
|
|
golden fixtures contain exactly the same operations.
|
|
- Verify Rust rejects a request larger than 1 MiB before worker launch and Java
|
|
independently rejects one before JSON parsing.
|
|
- Test process lifecycle against a controllable fake child process.
|
|
- Run a purpose-built probe through the production Bubblewrap launcher and
|
|
prove it cannot reach a test listener outside the worker network namespace.
|
|
- Regress against default data egress: the shipped CLI, environment-variable
|
|
surface, and Java protocol expose no remote-destination or telemetry setting.
|
|
- Maintain tiny, source-controlled fixture programs with known properties.
|
|
- Compile ELF x86-64 and PE32+ x86-64 fixtures reproducibly.
|
|
- Run integration tests against pinned Ghidra 12.1.2 and JDK 21.
|
|
- Assert verified status for the ELF and PE fixtures and unverified status for
|
|
at least one recognized target outside the matrix.
|
|
- Run the complete real-Ghidra integration suite before accepting a Ghidra or
|
|
JDK upgrade.
|
|
- Compare JSON against reviewed golden fixtures after normalizing unstable
|
|
values.
|
|
- Never rewrite a golden fixture in CI; updates require an explicit local
|
|
command and reviewed diff.
|
|
- Add a regression test for every corrected bug.
|
|
- Verify interruption leaves no Analysis that can be mistaken for complete.
|
|
- Verify Ghidra timeout results are never promoted or reused.
|
|
- Verify corruption causes quarantine and at most one automatic rebuild.
|
|
- Verify identical bytes at different paths reuse one compatible Analysis.
|
|
- Verify changed bytes at the same path do not reuse the old Analysis.
|
|
- Verify a source change during snapshot creation aborts before Ghidra starts.
|
|
- Verify Ghidra receives the staged snapshot path rather than the caller path.
|
|
|
|
## Delivery milestones
|
|
|
|
### Milestone 0: design
|
|
|
|
- Review this PRD and the domain language.
|
|
- Use `ghidr` as the short executable name.
|
|
- Use the Apache-2.0 license.
|
|
- Pin a nixpkgs revision providing Ghidra 12.1.2 and JDK 21.
|
|
- Define the remaining pinned Nix inputs and package closure.
|
|
- Define protocol schemas and use the accepted coarse exit-status allocation.
|
|
- Publish the initial `schemas/v1/` documents.
|
|
|
|
### Milestone 1: vertical inspection slice
|
|
|
|
- Create the Rust crate and minimal Java adapter.
|
|
- Implement `doctor`.
|
|
- Implement explicit Analysis Store cleanup.
|
|
- Implement `inspect`.
|
|
- Prove Rust to `analyzeHeadless` to Java to JSON end to end.
|
|
|
|
### Milestone 2: function discovery
|
|
|
|
- Implement `functions`.
|
|
- Define stable address and Function Selector representations.
|
|
- Add reproducible native fixtures.
|
|
|
|
### Milestone 3: targeted decompilation
|
|
|
|
- Implement `decompile`.
|
|
- Add ambiguity handling and decompiler time limits.
|
|
- Stabilize the first public JSON schema.
|
|
|
|
### Milestone 4: packaging
|
|
|
|
- Provide a reproducible x86-64 Linux Nix development and test environment.
|
|
- Publish a Nix flake package usable through `nix run`.
|
|
- Document installation and sandboxed use.
|
|
- Publish the first pre-release.
|
|
|
|
### Post-version-0.1: agent tracing
|
|
|
|
After the reliable inspection-to-decompilation slice is established, add
|
|
separate bounded Queries for relationships and targeted detail in this
|
|
priority order:
|
|
|
|
1. `strings`: a stably ordered page containing exact Address, byte length,
|
|
explicit encoding, decoded representation, and bounded reference links.
|
|
2. `xrefs`: structured source and destination targets, reference type, operand
|
|
index, direction, and containing source Function when present.
|
|
3. `calls`: flat caller, callsite, and resolved or unresolved callee edges.
|
|
4. `disassemble`: instructions for one explicit, bounded Address range or
|
|
selected Function.
|
|
5. `function`: signatures, parameters, locals, body ranges, thunk resolution,
|
|
and alternate symbols for one selected Function.
|
|
|
|
These relationships do not belong in every `functions` item. Exact names and
|
|
entry Addresses provide the stable identifiers that later Queries compose
|
|
with, while separate schemas can represent relationship-specific ambiguity and
|
|
provenance. Every Query retains explicit bounds, stable ordering, typed
|
|
per-record fields, and the public byte-spill contract. Version 0.1's rejection
|
|
of zero or implicit unlimited sentinels continues to apply. Whole-program graph
|
|
renderers, recursive graph algorithms, prose records, and a custom filter
|
|
language remain out of scope; downstream tools can transform bounded facts.
|
|
|
|
Batch Query execution is considered only after measurements show that repeated
|
|
JVM startup materially dominates agent workflows. A batch design must define
|
|
maximum item and aggregate byte counts, deterministic order, per-item typed
|
|
success or error, cancellation, and explicit fail-fast versus continue
|
|
behavior. It must not introduce hidden parallelism.
|
|
|
|
After the CLI schemas stabilize, an MCP integration may be built as a separate
|
|
thin adapter. It invokes `ghidr` as a process, exposes at most one tool per
|
|
stable Query, and forwards the CLI's versioned JSON unchanged. It may generate
|
|
capability descriptions and categories from the Rust operation metadata. It
|
|
must not call Ghidra directly, own Analysis Store policy, add a second result
|
|
contract, depend on mutable current-Program state, or introduce a persistent
|
|
listener into the core package. Stdio remains the default MCP transport; any
|
|
future network transport requires a separate threat model.
|
|
|
|
## Release acceptance criteria
|
|
|
|
The first release is acceptable when:
|
|
|
|
- A new user can run `doctor`, `inspect`, `functions`, and `decompile` without
|
|
manually creating a Ghidra project.
|
|
- A user can inspect store usage and explicitly remove tool-owned data.
|
|
- Repeating a Query against an unchanged Sample reuses compatible analysis.
|
|
- All commands provide versioned JSON and documented exit behavior.
|
|
- An interrupted analysis cannot be mistaken for a complete one.
|
|
- A timed-out Analysis attempt cannot satisfy a later Query.
|
|
- Tests exercise a real pinned Ghidra release and known fixture binaries.
|
|
- No first-release command mutates the Sample or Analysis.
|
|
- No analysis command requires a network connection.
|
|
|
|
## Open decisions
|
|
|
|
- No product-level decision currently blocks implementation.
|