[GH-ISSUE #2259] [Bug] Ghidra provider never applies a PE's sidecar PDB; name lookups fail and no limitation is reported #748

Closed
opened 2026-10-15 18:27:57 +00:00 by bsakaguchi · 3 comments
Owner

Originally created by @BeLit-Development-2 on GitHub (Oct 12, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/2259

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Native analysis or decompilation

Observed behavior

With the Ghidra provider, a PE executable whose matching PDB sits beside it is analyzed as if it had no debug information. Every user function appears as FUN_<address>, so name-based operations fail:

  • MCP search_procedures {"pattern":"check_license"} returns [].
  • MCP procedure_address {"procedure":"main"} and CLI rea decompile <exe> add_numbers fail with invalid_request: Unknown Ghidra procedure name or address: ….
  • list_procedures names only the entry, imports and a few FID matches (strlen, FID_conflict:printf_s).

No Evidence limitations entry says that the PE's CodeView record references a PDB that was not applied, so the caller gets no signal that symbol coverage is missing.

Running Ghidra 12.1.4 analyzeHeadless directly on the same executable, with default analyzers, applies the sidecar PDB (PDB analyzer parsing file: …\fixture.pdb, PDB Loaded = true) and names every function. REA's analysis profile reports analyzer_preset: ghidra-default, so the difference is in which inputs Ghidra can see, not in which analyzers run.

Address-based analysis is otherwise correct: pseudocode, constants, callers/callees and string xrefs for the unnamed functions matched source ground truth.

Reproduction

  1. Build the reduced fixture below with MSVC x64 (VS 2026, cl 19.51):
    #include <stdio.h>
    
    __declspec(noinline) int add_numbers(int a, int b)
    {
        return a + b;
    }
    
    int main(int argc, char **argv)
    {
        (void)argv;
        printf("%d\n", add_numbers(argc, 41));
        return 0;
    }
    
    cl /nologo /O1 /GS- /Zi fixture.c /link /INCREMENTAL:NO /OPT:REF /PDB:fixture.pdb
    This produces fixture.exe with fixture.pdb beside it.
  2. With GHIDRA_INSTALL_DIR and JAVA_HOME set: rea decompile C:\repro\fixture.exe add_numbers --provider ghidra --format json
  3. Observe exit code 1 and Unknown Ghidra procedure name or address: add_numbers.
  4. Control: analyzeHeadless.bat <tmpProject> p -import C:\repro\fixture.exe -postScript <script listing functions> -deleteProject names add_numbers at 0x140001008 and main at 0x14000100c, with PDB Loaded = true.

The same happens over MCP (open_binary then search_procedures / procedure_address / procedure_pseudo_code by name) on a larger 6-function fixture.

Expected behavior

Either:

  • a matching PDB beside the selected PE is admitted (identity checked by the CodeView GUID and age, and recorded in the analysis profile/Evidence), so recovered names and types are available; or
  • if REA intentionally does not admit companions (as for dSYM in #1193), the Evidence says so: a limitation such as "the target references debug symbols fixture.pdb (GUID …, age …) that were not applied; procedure names are analysis-generated".

Impact and scope

  • Affects every name-based native workflow on Windows PE targets that ship with or are built with PDBs: search, procedure_address, decompile <name>, find_xrefs_to_name, call-path seeds, and any agent prompt that names a function. Agents must fall back to string or xref hunting, as if the binary were stripped.
  • Evidence integrity: results are not wrong, but coverage is silently narrower than what the analyst supplied, and nothing in the limitations reveals it.
  • Not affected: address-based analysis, decompilation quality for the unnamed functions, imports and exports.
  • Workaround: REA_GHIDRA_SEED_FILE with address<TAB>function<TAB>name lines restores name lookups. I verified this: decompile check_license succeeds and callees show their seeded names. It is global to the server process, though, and needs the addresses produced out of band (for example from a linker /MAP).

REA version

6.4.0 (npm, global install)

Runtime and platform

Node 24.20.0; Windows 11 x64 (10.0.26300); Ghidra 12.1.4 PUBLIC; Temurin JDK 21.0.12

Target and artifact context

PE32+ x86-64 console executable built by MSVC 14.51 with /Zi; matching PDB (/PDB:) in the same directory; static CRT.

Interface

CLI and MCP (both)

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

# REA (no seed file)
$ rea decompile C:\repro\fixture.exe add_numbers --provider ghidra --format json
{"error":"Analysis failed","code":"invalid_request","category":"invalid_input",
 "message":"Analysis input is invalid. Check the arguments and try again.","retryable":true,
 "details":{"operation":"procedure_pseudo_code","issues":[{"path":[],"reason":"invalid_value",
 "message":"Unknown Ghidra procedure name or address: add_numbers"}]}}

# MCP, 6-function fixture (open_binary then:)
search_procedures {"pattern":"check_license"}  -> normalized_result: []
procedure_address {"procedure":"main"}         -> invalid_request "Unknown Ghidra procedure name or address: main"
binary_overview -> "document": "target-c6839f0c7bb2.exe"

# Plain Ghidra 12.1.4 analyzeHeadless on the same file
INFO  PDB analyzer parsing file: <dir>\fixture.pdb (PdbUniversalAnalyzer)
INFO  REA_FN 140001008 add_numbers
INFO  REA_FN 14000100c main
INFO  REA_PDB_LOADED true

Typed outcome or recovery expected

When the PE's debug directory has a CodeView (RSDS) record and no matching PDB was applied, add an Evidence limitation naming the referenced PDB file name, GUID and age, and suggest a next probe or remediation. If companion admission is added, record the admitted PDB's identity in the analysis profile, so snapshots and Evidence for symbol-assisted and symbol-less analysis cannot be confused (the same identity concerns as #1193).

Analysis or suspected cause

Source-derived hypothesis, not verified by instrumenting REA:

  • dist/ghidra/GhidraTargetSnapshot.js copies the target into a private runtime root as target-<sha256[0:12]><ext> (import_mode: ephemeral-source-immutable), and Ghidra imports that copy. Ghidra's PDB Universal analyzer searches the program's own directory by default. The renamed copy has no sibling PDB there, so nothing is found.
  • dist/ghidra/GhidraLauncher.js / GhidraJavaLaunch.js launch Ghidra with -Duser.home=<runtimeRoot>/home. A symbol-server or search-path configuration the user set up in their own Ghidra preferences is therefore also unreachable, so there is no user-side workaround through Ghidra settings.
  • The CodeView record's absolute PDB path still points at the original file, but it was not used (observed: no PDB analyzer parsing file outcome, all functions FUN_*).

Possible directions (maintainers' call): snapshot a matching sidecar PDB beside the private copy after verifying GUID and age; or pass an explicit PDB location to the PDB analyzer; or, at minimum, emit the limitation above.

Additional context

  • Related: #1193, the same private-copy constraint for dSYM companions on Mach-O. This report is the PE/PDB counterpart. The concrete defect, even if companion admission is treated as a feature, is the missing limitation.
  • Found during a ground-truth test run of REA 6.4.0 on Windows with MSVC-built fixtures. Other results in the same run: Ghidra startup about 55 s for a 140 KB static-CRT exe, and address-based results matched source exactly.
Originally created by @BeLit-Development-2 on GitHub (Oct 12, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/2259 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Native analysis or decompilation ### Observed behavior With the Ghidra provider, a PE executable whose matching PDB sits beside it is analyzed as if it had no debug information. Every user function appears as `FUN_<address>`, so name-based operations fail: - MCP `search_procedures {"pattern":"check_license"}` returns `[]`. - MCP `procedure_address {"procedure":"main"}` and CLI `rea decompile <exe> add_numbers` fail with `invalid_request`: `Unknown Ghidra procedure name or address: …`. - `list_procedures` names only the entry, imports and a few FID matches (`strlen`, `FID_conflict:printf_s`). No Evidence `limitations` entry says that the PE's CodeView record references a PDB that was not applied, so the caller gets no signal that symbol coverage is missing. Running Ghidra 12.1.4 `analyzeHeadless` directly on the same executable, with default analyzers, applies the sidecar PDB (`PDB analyzer parsing file: …\fixture.pdb`, `PDB Loaded = true`) and names every function. REA's analysis profile reports `analyzer_preset: ghidra-default`, so the difference is in which inputs Ghidra can see, not in which analyzers run. Address-based analysis is otherwise correct: pseudocode, constants, callers/callees and string xrefs for the unnamed functions matched source ground truth. ### Reproduction 1. Build the reduced fixture below with MSVC x64 (VS 2026, cl 19.51): ```c #include <stdio.h> __declspec(noinline) int add_numbers(int a, int b) { return a + b; } int main(int argc, char **argv) { (void)argv; printf("%d\n", add_numbers(argc, 41)); return 0; } ``` `cl /nologo /O1 /GS- /Zi fixture.c /link /INCREMENTAL:NO /OPT:REF /PDB:fixture.pdb` This produces `fixture.exe` with `fixture.pdb` beside it. 2. With `GHIDRA_INSTALL_DIR` and `JAVA_HOME` set: `rea decompile C:\repro\fixture.exe add_numbers --provider ghidra --format json` 3. Observe exit code 1 and `Unknown Ghidra procedure name or address: add_numbers`. 4. Control: `analyzeHeadless.bat <tmpProject> p -import C:\repro\fixture.exe -postScript <script listing functions> -deleteProject` names `add_numbers` at `0x140001008` and `main` at `0x14000100c`, with `PDB Loaded = true`. The same happens over MCP (`open_binary` then `search_procedures` / `procedure_address` / `procedure_pseudo_code` by name) on a larger 6-function fixture. ### Expected behavior Either: - a matching PDB beside the selected PE is admitted (identity checked by the CodeView GUID and age, and recorded in the analysis profile/Evidence), so recovered names and types are available; or - if REA intentionally does not admit companions (as for dSYM in #1193), the Evidence says so: a limitation such as "the target references debug symbols `fixture.pdb` (GUID …, age …) that were not applied; procedure names are analysis-generated". ### Impact and scope - Affects every name-based native workflow on Windows PE targets that ship with or are built with PDBs: search, `procedure_address`, `decompile <name>`, `find_xrefs_to_name`, call-path seeds, and any agent prompt that names a function. Agents must fall back to string or xref hunting, as if the binary were stripped. - Evidence integrity: results are not wrong, but coverage is silently narrower than what the analyst supplied, and nothing in the limitations reveals it. - Not affected: address-based analysis, decompilation quality for the unnamed functions, imports and exports. - Workaround: `REA_GHIDRA_SEED_FILE` with `address<TAB>function<TAB>name` lines restores name lookups. I verified this: `decompile check_license` succeeds and callees show their seeded names. It is global to the server process, though, and needs the addresses produced out of band (for example from a linker `/MAP`). ### REA version 6.4.0 (npm, global install) ### Runtime and platform Node 24.20.0; Windows 11 x64 (10.0.26300); Ghidra 12.1.4 PUBLIC; Temurin JDK 21.0.12 ### Target and artifact context PE32+ x86-64 console executable built by MSVC 14.51 with `/Zi`; matching PDB (`/PDB:`) in the same directory; static CRT. ### Interface CLI and MCP (both) ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence ```text # REA (no seed file) $ rea decompile C:\repro\fixture.exe add_numbers --provider ghidra --format json {"error":"Analysis failed","code":"invalid_request","category":"invalid_input", "message":"Analysis input is invalid. Check the arguments and try again.","retryable":true, "details":{"operation":"procedure_pseudo_code","issues":[{"path":[],"reason":"invalid_value", "message":"Unknown Ghidra procedure name or address: add_numbers"}]}} # MCP, 6-function fixture (open_binary then:) search_procedures {"pattern":"check_license"} -> normalized_result: [] procedure_address {"procedure":"main"} -> invalid_request "Unknown Ghidra procedure name or address: main" binary_overview -> "document": "target-c6839f0c7bb2.exe" # Plain Ghidra 12.1.4 analyzeHeadless on the same file INFO PDB analyzer parsing file: <dir>\fixture.pdb (PdbUniversalAnalyzer) INFO REA_FN 140001008 add_numbers INFO REA_FN 14000100c main INFO REA_PDB_LOADED true ``` ### Typed outcome or recovery expected When the PE's debug directory has a CodeView (RSDS) record and no matching PDB was applied, add an Evidence limitation naming the referenced PDB file name, GUID and age, and suggest a next probe or remediation. If companion admission is added, record the admitted PDB's identity in the analysis profile, so snapshots and Evidence for symbol-assisted and symbol-less analysis cannot be confused (the same identity concerns as #1193). ### Analysis or suspected cause *Source-derived hypothesis, not verified by instrumenting REA:* - `dist/ghidra/GhidraTargetSnapshot.js` copies the target into a private runtime root as `target-<sha256[0:12]><ext>` (`import_mode: ephemeral-source-immutable`), and Ghidra imports that copy. Ghidra's PDB Universal analyzer searches the program's own directory by default. The renamed copy has no sibling PDB there, so nothing is found. - `dist/ghidra/GhidraLauncher.js` / `GhidraJavaLaunch.js` launch Ghidra with `-Duser.home=<runtimeRoot>/home`. A symbol-server or search-path configuration the user set up in their own Ghidra preferences is therefore also unreachable, so there is no user-side workaround through Ghidra settings. - The CodeView record's absolute PDB path still points at the original file, but it was not used (observed: no `PDB analyzer parsing file` outcome, all functions `FUN_*`). Possible directions (maintainers' call): snapshot a matching sidecar PDB beside the private copy after verifying GUID and age; or pass an explicit PDB location to the PDB analyzer; or, at minimum, emit the limitation above. ### Additional context - Related: #1193, the same private-copy constraint for dSYM companions on Mach-O. This report is the PE/PDB counterpart. The concrete defect, even if companion admission is treated as a feature, is the missing limitation. - Found during a ground-truth test run of REA 6.4.0 on Windows with MSVC-built fixtures. Other results in the same run: Ghidra startup about 55 s for a 140 KB static-CRT exe, and address-based results matched source exactly.
bsakaguchi 2026-10-15 18:27:57 +00:00
Author
Owner

@akram1089 commented on GitHub (Oct 12, 2026):

I will take this. I'll work on adding detection for referenced PE CodeView (RSDS) records so the Ghidra provider emits the Evidence limitation naming the unapplied PDB (GUID and age) when symbols are not loaded, and open a PR.

<!-- gh-comment-id:6129571914 --> @akram1089 commented on GitHub (Oct 12, 2026): I will take this. I'll work on adding detection for referenced PE CodeView (RSDS) records so the Ghidra provider emits the Evidence limitation naming the unapplied PDB (GUID and age) when symbols are not loaded, and open a PR.
Author
Owner

@matbrgz commented on GitHub (Oct 12, 2026):

Triage & Sidecar PDB Discovery Notes

When analyzing PE binaries accompanied by sidecar PDB files, Ghidra headless requires explicit symbol search path configuration (-scriptPath / PDB analyzer flags) to bind local .pdb files:

  • Root Cause: REA's ephemeral Ghidra headless launcher copies or isolates the target binary into an ephemeral workspace without staging the accompanying sidecar .pdb or without passing -import <exe> -postScript LoadPdbScript.java <pdb>.
  • Limitation Invariant: If the PE contains a CodeView debug directory entry pointing to a PDB filename that is absent or unapplied, REA must emit an explicit limitation (pdb_symbols_unapplied) rather than silently returning generic FUN_<address> labels with complete coverage claims.
<!-- gh-comment-id:6134362719 --> @matbrgz commented on GitHub (Oct 12, 2026): ### Triage & Sidecar PDB Discovery Notes When analyzing PE binaries accompanied by sidecar PDB files, Ghidra headless requires explicit symbol search path configuration (`-scriptPath` / PDB analyzer flags) to bind local `.pdb` files: - **Root Cause:** REA's ephemeral Ghidra headless launcher copies or isolates the target binary into an ephemeral workspace without staging the accompanying sidecar `.pdb` or without passing `-import <exe> -postScript LoadPdbScript.java <pdb>`. - **Limitation Invariant:** If the PE contains a CodeView debug directory entry pointing to a PDB filename that is absent or unapplied, REA must emit an explicit limitation (`pdb_symbols_unapplied`) rather than silently returning generic `FUN_<address>` labels with complete coverage claims.
Author
Owner

@WraithFallen commented on GitHub (Oct 13, 2026):

This also reproduces on the Windows P0 lane with a PE DLL, not only an executable.

  • rea-agents@6.4.0 (npm), Windows 10 x64, Ghidra 12.1.2, JDK 25.0.3, --provider ghidra.
  • Target: a real-world x64 Skyrim SKSE plugin DLL (AltTabFix.dll), with its own matching PDB copied beside it. The DLL's CodeView record names that same PDB file.
  • rea search <dll> FUN_ --kind procedures: 1,111 local functions, all FUN_<address>. The only real names are the two exports (SKSEPlugin_Load, SKSEPlugin_Query) plus 272 imports.
  • Name searches for symbols that are in the PDB return [], and the string pdb appears nowhere in the Evidence, so there is no limitation either. Same as the original report.

Our interim workaround is to read the matching PDB's S_PUB32 publics ourselves, after a GUID+age check, and pass them in through REA_GHIDRA_SEED_FILE. That works only because 6.4.0 added seeds, and it's no substitute for applying the PDB, which would also give us types. +1 on the pdb_symbols_unapplied limitation as a first step.

<!-- gh-comment-id:6138843755 --> @WraithFallen commented on GitHub (Oct 13, 2026): This also reproduces on the **Windows P0 lane with a PE DLL**, not only an executable. - `rea-agents@6.4.0` (npm), Windows 10 x64, Ghidra 12.1.2, JDK 25.0.3, `--provider ghidra`. - Target: a real-world x64 Skyrim SKSE plugin DLL (`AltTabFix.dll`), with its own matching PDB copied beside it. The DLL's CodeView record names that same PDB file. - `rea search <dll> FUN_ --kind procedures`: **1,111** local functions, all `FUN_<address>`. The only real names are the two exports (`SKSEPlugin_Load`, `SKSEPlugin_Query`) plus 272 imports. - Name searches for symbols that are in the PDB return `[]`, and the string `pdb` appears nowhere in the Evidence, so there is no limitation either. Same as the original report. Our interim workaround is to read the matching PDB's S_PUB32 publics ourselves, after a GUID+age check, and pass them in through `REA_GHIDRA_SEED_FILE`. That works only because 6.4.0 added seeds, and it's no substitute for applying the PDB, which would also give us types. +1 on the `pdb_symbols_unapplied` limitation as a first step.
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#748
No description provided.