[PR #3817] feat(ghidra): select an existing-project program inside a project folder #5920

Open
opened 2026-10-31 01:10:26 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/3817
Author: @whalemenace
Created: 10/15/2026
Status: 🔄 Open

Base: main ← Head: feat/ghidra-persistent-project


📝 Commits (1)

  • f8abf28 feat(ghidra): select an existing-project program inside a project folder

📊 Changes

12 files changed (+344 additions, -28 deletions)

View changed files

📝 .agents/skills/reverse-engineer-anything/references/native-and-artifacts.md (+3 -2)
📝 docs/installation.md (+14 -6)
📝 src/application/SetupRegistrationEnvironment.ts (+1 -0)
📝 src/config/environment.ts (+6 -3)
📝 src/config/ghidraExistingProject.ts (+33 -2)
📝 src/config/parseConfig.ts (+25 -11)
📝 src/ghidra/GhidraLauncher.ts (+14 -2)
📝 src/ghidra/GhidraProjectSnapshot.ts (+16 -1)
📝 src/ghidra/GhidraProvider.behavior.test.ts (+72 -0)
📝 src/ghidra/GhidraProviderClient.ts (+6 -1)
📝 tests/boundary/providers/ghidra/ghidraLauncher.test.ts (+35 -0)
📝 tests/boundary/providers/ghidra/ghidraProjectSnapshot.test.ts (+119 -0)

📄 Description

Problem

#7236 reuses a caller-selected Ghidra project through a verified read-only copy. Its program selection is limited to the project root: REA_GHIDRA_PROGRAM_NAME refuses /, and the launcher passes the bare project name, so -process only searches /. Projects that keep programs in folders (for example /firmware/v2/app) cannot be reused without first moving the program to the root in Ghidra.

Fix

  • New optional REA_GHIDRA_PROGRAM_FOLDER (requires REA_GHIDRA_PROJECT_PATH): one absolute, non-root project folder such as /firmware/v2. It refuses /, relative paths, trailing slashes, empty, . and .. segments, padded segments, *, ?, backslashes and control characters. The root is selected by omitting the setting, so each folder has one spelling. REA_GHIDRA_PROGRAM_NAME is unchanged and still refuses paths.
  • resolveGhidraProject commits the folder as program_folder in the existing project commitment. The key is absent for the root, so root profiles and their digests do not change.
  • The launcher opens the owned copy as <project>/<folder> with -process <program>, following analyzeHeadless's <project_name>[/<folder_path>] argument. The folder is checked again before arguments are built.
  • The provider client refuses a profile whose committed folder differs from the configured one, as it already does for the path and program name.
  • rea setup keeps the new setting in saved registrations. Installation docs and the skill reference describe it.

Assessment against #7236

  • What the copy/identity owner already does: GhidraProjectSnapshot.ts owns selection and identity. resolveGhidraProject commits the configured path, the resolved project directory and name, the program name and a SHA-256 over the whole .gpr/.rep pair (fingerprintGhidraProject, sorted 64 KiB streaming reads; symlinks and non-regular entries refused). copyExistingGhidraProject copies only that pair into the owned runtime, refuses overlap with the runtime, and re-fingerprints the copy against the committed digest. The launcher runs -process <program> -noanalysis -readOnly on the copy. The bridge still checks the program's recorded import SHA-256 against the admitted target and blocks database edits.
  • What folder selection adds: only a coordinate inside the copy. Folders live inside the .rep database, so the copied content, its fingerprint and the import-digest check are the same for every folder. The folder only changes the project argument passed to analyzeHeadless and becomes part of the committed selection.
  • Why it belongs inside that owner: the folder is part of "which program in which project", which ghidraProjectCommitmentSchema already records. Putting it there makes it part of the profile digest (different folders cannot share cached observations) and reuses the copy, verification, cleanup, Windows refusal and import-override refusals unchanged. No second open path is needed.
  • What was dropped from the original PR, and why: the in-place read-only open of the caller's project, its separate config module, digest handshake, bridge changes, capability rewrite, real-Ghidra script and docs. A tester's real Ghidra 12.1.4 run showed that opening the project in place still rewrote Fixture.rep/idata/~index.bak and ~journal.bak, so it could not promise an untouched original. #7236's copy does, and it already covers the reuse, identity, read-only and historical-report parts of the original scope.

Tested

Run on a Windows x64 host with Node 24.22.1. Existing-project mode is unavailable on Windows, so the launcher tests that copy and launch a project skip on this host.

  • npm run typecheck: exit 0.
  • Focused suites, run with node scripts/run-exact-tests.mjs run (the test:focused build step needs the packaged Windows native addon, which this checkout does not have): tests/boundary/providers/ghidra/ghidraProjectSnapshot.test.ts, tests/boundary/providers/ghidra/ghidraLauncher.test.ts, src/ghidra/GhidraProvider.behavior.test.ts: 232 passed, 12 skipped, 6 failed. The 6 failures are launcher tests that need that native addon (PrivateRuntimeRootUnavailableError). They fail the same way with main's source.
  • 11 new tests (4 snapshot/config, 6 launcher argument, 1 provider) all pass. With main's source and only the new tests applied, 10 of them fail; the 11th guards the copy identity check and passes either way.
  • Wider run over 55 files (src/ghidra, src/config, tests/boundary/providers/ghidra, plus the doctor, startup-policy and project-read-cancellation suites): main 1085 passed / 107 failed / 102 skipped; this branch 1096 passed / 107 failed / 102 skipped. The failing test lists are identical (Windows host and native addon).
  • oxfmt --check on changed files: clean. oxlint on changed TypeScript files: no new warnings.
  • Real Ghidra 12.1.4 on the same Windows host, on a project with one app.exe at the root and a different app.exe in /sub/dir. The provider refuses existing projects on Windows, so a small script drove this branch's pieces directly: parseConfig with REA_GHIDRA_PROGRAM_FOLDER=/sub/dir, resolveGhidraProject, copyExistingGhidraProject with the committed SHA-256, and ghidraHeadlessArguments (POSIX argument form), then ran analyzeHeadless on the copy with a probe post-script in place of the bridge. It opened /sub/dir/app.exe from the copy (the program's executable SHA-256 matched the folder input; functions and decompilation present). With the folder unset it opened /app.exe; with /sub/nope, analyzeHeadless stopped with Specified project folder not found: /sub/nope. Ghidra rewrote ~index.bak and removed ~journal.bak in each copy, while every file of the original project, those two included, kept its size, mtime and SHA-256.
  • Not run: npm run docs:check (same native-addon build step), a Linux run, and the full provider and bridge on a folder program.

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/morluto/rea/pull/3817 **Author:** [@whalemenace](https://github.com/whalemenace) **Created:** 10/15/2026 **Status:** 🔄 Open **Base:** `main` ← **Head:** `feat/ghidra-persistent-project` --- ### 📝 Commits (1) - [`f8abf28`](https://github.com/morluto/rea/commit/f8abf28cc9a62f23520166bc90696ae5b42c12e0) feat(ghidra): select an existing-project program inside a project folder ### 📊 Changes **12 files changed** (+344 additions, -28 deletions) <details> <summary>View changed files</summary> 📝 `.agents/skills/reverse-engineer-anything/references/native-and-artifacts.md` (+3 -2) 📝 `docs/installation.md` (+14 -6) 📝 `src/application/SetupRegistrationEnvironment.ts` (+1 -0) 📝 `src/config/environment.ts` (+6 -3) 📝 `src/config/ghidraExistingProject.ts` (+33 -2) 📝 `src/config/parseConfig.ts` (+25 -11) 📝 `src/ghidra/GhidraLauncher.ts` (+14 -2) 📝 `src/ghidra/GhidraProjectSnapshot.ts` (+16 -1) 📝 `src/ghidra/GhidraProvider.behavior.test.ts` (+72 -0) 📝 `src/ghidra/GhidraProviderClient.ts` (+6 -1) 📝 `tests/boundary/providers/ghidra/ghidraLauncher.test.ts` (+35 -0) 📝 `tests/boundary/providers/ghidra/ghidraProjectSnapshot.test.ts` (+119 -0) </details> ### 📄 Description ## Problem #7236 reuses a caller-selected Ghidra project through a verified read-only copy. Its program selection is limited to the project root: `REA_GHIDRA_PROGRAM_NAME` refuses `/`, and the launcher passes the bare project name, so `-process` only searches `/`. Projects that keep programs in folders (for example `/firmware/v2/app`) cannot be reused without first moving the program to the root in Ghidra. ## Fix - New optional `REA_GHIDRA_PROGRAM_FOLDER` (requires `REA_GHIDRA_PROJECT_PATH`): one absolute, non-root project folder such as `/firmware/v2`. It refuses `/`, relative paths, trailing slashes, empty, `.` and `..` segments, padded segments, `*`, `?`, backslashes and control characters. The root is selected by omitting the setting, so each folder has one spelling. `REA_GHIDRA_PROGRAM_NAME` is unchanged and still refuses paths. - `resolveGhidraProject` commits the folder as `program_folder` in the existing project commitment. The key is absent for the root, so root profiles and their digests do not change. - The launcher opens the owned copy as `<project>/<folder>` with `-process <program>`, following analyzeHeadless's `<project_name>[/<folder_path>]` argument. The folder is checked again before arguments are built. - The provider client refuses a profile whose committed folder differs from the configured one, as it already does for the path and program name. - `rea setup` keeps the new setting in saved registrations. Installation docs and the skill reference describe it. ## Assessment against #7236 - **What the copy/identity owner already does:** `GhidraProjectSnapshot.ts` owns selection and identity. `resolveGhidraProject` commits the configured path, the resolved project directory and name, the program name and a SHA-256 over the whole `.gpr`/`.rep` pair (`fingerprintGhidraProject`, sorted 64 KiB streaming reads; symlinks and non-regular entries refused). `copyExistingGhidraProject` copies only that pair into the owned runtime, refuses overlap with the runtime, and re-fingerprints the copy against the committed digest. The launcher runs `-process <program> -noanalysis -readOnly` on the copy. The bridge still checks the program's recorded import SHA-256 against the admitted target and blocks database edits. - **What folder selection adds:** only a coordinate inside the copy. Folders live inside the `.rep` database, so the copied content, its fingerprint and the import-digest check are the same for every folder. The folder only changes the project argument passed to analyzeHeadless and becomes part of the committed selection. - **Why it belongs inside that owner:** the folder is part of "which program in which project", which `ghidraProjectCommitmentSchema` already records. Putting it there makes it part of the profile digest (different folders cannot share cached observations) and reuses the copy, verification, cleanup, Windows refusal and import-override refusals unchanged. No second open path is needed. - **What was dropped from the original PR, and why:** the in-place read-only open of the caller's project, its separate config module, digest handshake, bridge changes, capability rewrite, real-Ghidra script and docs. A tester's real Ghidra 12.1.4 run showed that opening the project in place still rewrote `Fixture.rep/idata/~index.bak` and `~journal.bak`, so it could not promise an untouched original. #7236's copy does, and it already covers the reuse, identity, read-only and historical-report parts of the original scope. ## Tested Run on a Windows x64 host with Node 24.22.1. Existing-project mode is unavailable on Windows, so the launcher tests that copy and launch a project skip on this host. - `npm run typecheck`: exit 0. - Focused suites, run with `node scripts/run-exact-tests.mjs run` (the `test:focused` build step needs the packaged Windows native addon, which this checkout does not have): `tests/boundary/providers/ghidra/ghidraProjectSnapshot.test.ts`, `tests/boundary/providers/ghidra/ghidraLauncher.test.ts`, `src/ghidra/GhidraProvider.behavior.test.ts`: 232 passed, 12 skipped, 6 failed. The 6 failures are launcher tests that need that native addon (`PrivateRuntimeRootUnavailableError`). They fail the same way with main's source. - 11 new tests (4 snapshot/config, 6 launcher argument, 1 provider) all pass. With main's source and only the new tests applied, 10 of them fail; the 11th guards the copy identity check and passes either way. - Wider run over 55 files (`src/ghidra`, `src/config`, `tests/boundary/providers/ghidra`, plus the doctor, startup-policy and project-read-cancellation suites): main 1085 passed / 107 failed / 102 skipped; this branch 1096 passed / 107 failed / 102 skipped. The failing test lists are identical (Windows host and native addon). - `oxfmt --check` on changed files: clean. `oxlint` on changed TypeScript files: no new warnings. - Real Ghidra 12.1.4 on the same Windows host, on a project with one `app.exe` at the root and a different `app.exe` in `/sub/dir`. The provider refuses existing projects on Windows, so a small script drove this branch's pieces directly: `parseConfig` with `REA_GHIDRA_PROGRAM_FOLDER=/sub/dir`, `resolveGhidraProject`, `copyExistingGhidraProject` with the committed SHA-256, and `ghidraHeadlessArguments` (POSIX argument form), then ran `analyzeHeadless` on the copy with a probe post-script in place of the bridge. It opened `/sub/dir/app.exe` from the copy (the program's executable SHA-256 matched the folder input; functions and decompilation present). With the folder unset it opened `/app.exe`; with `/sub/nope`, analyzeHeadless stopped with `Specified project folder not found: /sub/nope`. Ghidra rewrote `~index.bak` and removed `~journal.bak` in each copy, while every file of the original project, those two included, kept its size, mtime and SHA-256. - Not run: `npm run docs:check` (same native-addon build step), a Linux run, and the full provider and bridge on a folder program. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#5920
No description provided.