[GH-ISSUE #2326] [Bug] Callee and caller reads give no warning when Ghidra's function body stops short #770

Closed
opened 2026-10-15 18:28:11 +00:00 by bsakaguchi · 2 comments
Owner

Originally created by @CassieFox23 on GitHub (Oct 12, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/2326

TL;DR: When Ghidra's function body stops short, the callee and caller reads give no warning. In OpenMW 0.51.0 (GPL-3.0), 22 of 50 checked functions lost real calls, though 20 were picked for losing the most. All 100 reads said complete with no note, and I am not asking to change that word, only to tell the reader.

What I ran

I ran the Ghidra provider over MCP on a stripped copy of esmtool.x86_64 from OpenMW 0.51.0, on main 0365454a5. An earlier run on 69e097a8c (#2026) gave the same results, so no recent commit changed this. The fold below says how I picked the 50 functions.

The smallest example

The function is boost::wrapexcept<boost::bad_any_cast>::rethrow() at 0x112cc0. Ghidra loads this file at 0x100000, so the file address is 0x12cc0.

  • The function is 196 bytes. Ghidra's body is 164 bytes, from 0x112cc0 to 0x112d63.
  • procedure_callees returns 2 callees. The code makes 5 direct calls.
  • The 3 missing calls are at 0x112d6f, 0x112d77 and 0x112d7f. They sit in the block that starts at 0x112d64, just past the body.
  • That block is exception cleanup. The file's own exception table names 0x112d64 as a landing pad. A landing pad is a spot that only the C++ unwinder jumps to.
  • The reply says complete, lists four generic limitations, and says nothing about the body.
  • procedure_info has one terminal-call line, for the call at 0x112d57. That call is inside the body. It does not explain the lost block.
  • trace_call_path from 0x112cc0 to 0x112930 (__cxa_free_exception) says not_reached, truncated: false, and no unknowns. The real call to it is at 0x112d77.

Counts

Check Result
Callee and caller reads that say complete 100 of 100
Those reads with any note about the body 0 of 100
Functions that lost real calls 22 of 50 (20 picked for losing the most)
Missing calls: edges, then call sites 269, at 618 sites
Missing call sites inside the reported body 0 of 618
Cause A: right after a terminal call that procedure_info lists 539 sites
Cause B: right after ret or jmp, and no view lists it 79 sites, in 4 functions

22 of 50 is not a rate, since 20 were picked for losing the most. In the earlier whole-file run, 391 of 1,995 functions had at least one missing call.

Callers have the same gap, and a read on the target can't see it. For 10 targets, procedure_callers left out a real caller, and all 10 replies said complete with no note.

How I picked the functions, and more on the counts
  • Thirty of the 50 functions are spread across the file; 2 of those lost calls. The other 20 lost the most calls in my earlier whole-file run on 69e097a8c (#2026).
  • For all 50, that run gave the same bodies, callee lists and coverage. The limitation text also matches for the 30 where that run kept it.
  • A function can call one target from several places, so there are more sites than edges.
  • For cause B, I checked the exception table for the example above. For 17 of the 79 sites, the table also names a landing pad earlier in the same stretch of left-out code. I did not trace the other 62.
  • Callers example: 0x1dd750 has 2 real direct callers, and REA returned none.

What the docs say

docs/tool-design.md, lines 60 to 64, says that complete "means that set was returned without a known read limit". It also says that provider and execution caveats "stay in limitations". So the word complete is by design, and a caveat belongs in limitations.

docs/ghidra-provider.md, lines 58 to 67, says an inferred no-return call "can terminate that body before subsequent bytes". It lists the views that report this: pseudocode, assembly, procedure info, instruction windows and dossiers. Callers and callees are not on that list. In the bridge, procedureCalls (line 721 of bridge/ghidra/ReaGhidraBridge.java) returns a bare list and does not call noReturnCallLimitations. procedureInfo does. I only read this; I changed nothing.

What I ask

  • Tell the reader somewhere in the reply. I do not mind where. By the design doc, limitations fits and complete can stay. If you read "known read limit" as covering this, partial fits too. In src/application/CallPathTracing.ts, trace_call_path only turns unknown on partial or unknown coverage. So a note in limitations alone will not change not_reached. I read that code; I did not run it.
  • For cause B, one line would help: the body leaves out code that only the unwinder reaches. No view says this today, not even procedure_info.

A kind no is fine. I know the design doc keeps caveats out of coverage on purpose.

Related: my earlier issue #1658 asked for this warning, and 69a8b3a95 added it to five views. Callers and callees were left out. #2222 and #2223 are about readers who trust an incomplete label. This report is about whether the label has a warning to read.

Not run

  • The whole file on 0365454a5. I ran the 50 functions only.
  • Hopper, IDA, Windows, REA on the unstripped file, and other programs.
  • REA's tests and npm run verify:ghidra.
  • compare_functions, verify_reconstruction and build_call_path.
  • A check that each of the 62 other cause B sites is cleanup code.

Thanks, morluto, for the notes in 69a8b3a95. They made this easy to chase.

Table of the 50 functions

The Symbol, Body, Real and Got columns are the same on 69e097a8c and 0365454a5. The Terminal column was kept for 30 functions only. It matches there. S is a function from the spread sample. T is one of the 20 with the most missing calls. Symbol is the size in the symbol table, in bytes. Body is the size of Ghidra's body, in bytes. Real is the count of direct calls in the function's own symbol range, found with objdump on the unstripped file. Got is the count of callees REA returned. Terminal is the count of terminal-call lines in procedure_info.

# Address Symbol Body Real Got Missing Terminal
S0 0x112cc0 196 164 5 2 3 1
S1 0x12a890 1 1 0 0 0 0
S2 0x12f0f0 1 1 0 0 0 0
S3 0x130120 202 193 1 1 0 1
S4 0x132c50 471 368 11 7 4 3
S5 0x139090 42 42 0 0 0 0
S6 0x13e480 183 179 0 0 0 0
S7 0x150ba0 580 557 11 12 0 2
S8 0x1586d0 18 18 1 1 0 0
S9 0x158b40 16 16 0 1 0 1
S10 0x158f20 457 448 2 2 0 0
S11 0x15aab0 166 166 1 1 0 1
S12 0x15f2b0 627 611 9 9 0 0
S13 0x16bd10 210 209 1 1 0 1
S14 0x16fe90 43 36 1 1 0 0
S15 0x175670 71 71 1 1 0 0
S16 0x17a770 7 7 0 0 0 0
S17 0x180cf0 358 335 4 6 0 2
S18 0x18c2b0 457 448 2 2 0 0
S19 0x194860 75 75 0 0 0 0
S20 0x19c2c0 990 931 9 9 0 3
S21 0x1a7360 291 246 0 0 0 0
S22 0x1accb0 74 74 1 2 0 1
S23 0x1bab40 399 383 5 6 0 2
S24 0x1c9330 8570 8139 19 26 0 44
S25 0x1d36f0 104 94 1 1 0 0
S26 0x1de600 253 238 3 3 0 0
S27 0x1e63d0 51 46 0 0 0 0
S28 0x1ea490 555 543 3 3 0 1
S29 0x1f67d0 132 131 2 2 0 0
T 0x14a000 4487 171 34 1 33 1
T 0x122ff0 1231 12 19 1 18 1
T 0x115fb0 509 12 16 1 15 1
T 0x122dae 417 12 15 1 14 1
T 0x124a43 670 69 18 4 14 1
T 0x11f06c 897 12 14 1 13 1
T 0x120dc6 658 12 14 1 13 1
T 0x11d8a0 3577 50 15 3 12 1
T 0x1228de 959 114 16 4 12 1
T 0x123da2 313 12 13 1 12 1
T 0x123edc 1144 258 18 6 12 1
T 0x1133b2 812 167 21 10 11 1
T 0x115d09 610 26 13 2 11 1
T 0x11a948 281 12 12 1 11 1
T 0x12031c 541 17 12 1 11 1
T 0x116d4c 346 12 11 1 10 1
T 0x119296 763 12 11 1 10 1
T 0x11a1f4 524 17 11 1 10 1
T 0x11cf16 416 12 11 1 10 1
T 0x122c9e 272 12 11 1 10 1
Earlier run, whole file

These numbers are from the earlier run on 69e097a8c. I did not repeat them on 0365454a5. Recall is the share of real calls that REA returned. Precision is the share of returned calls that are real.

What Result
Function starts found 1,543 of 1,543
Cold pieces found 452 of 452
Starts that are real 94.59%
Body size exact 569 of 1,543
Body size within 16 bytes 401 of 1,543
Body size within 10% 406 of 1,543
Body size worse 167 of 1,543
Callee recall, sample of 30 92.47%
Callee precision, sample of 30 86.87%
Callee recall, all 1,995 84.13%
Callee precision, all 1,995 90.38%
Functions with a missing call, all 1,995 391
Real calls missing, all 1,995 1,363
Of those, call site outside the body 1,363
Caller recall, sample of 30 88.89%
Caller precision, sample of 30 80.0%
Commands
Step Command Result
Main gh api repos/morluto/rea/branches/main --jq .commit.sha 0
Build npm ci, npm run build, npm run test:prepare 0, 0, 0
Strip strip --strip-all esmtool.x86_64 0
Run systemd-run --user --scope -p MemoryMax=8G -- node mcp_client.mjs client quit normally
Calls open_binary, procedure_info, procedure_callees, procedure_callers, get_call_graph, trace_call_path no tool errors
Real calls objdump -d --no-show-raw-insn -j .text -j .plt -j .plt.got 0
Exception table lsda_check.py and lsda_all.py, using pyelftools 0, 0

The run was one MCP session with 168 tool calls. open_binary took provider_id set to ghidra. The Node line in Test setup is the version that built and ran REA.

Test setup
  • Box: Nobara 44 (Fedora 44 base), Linux 7.2.9
  • CPU: AMD Ryzen 9 5900X, 12 cores / 24 threads
  • RAM: 32 GB DDR4-3600
  • Node: 24.18.0
  • Head / base: 0365454a5 / 69e097a8c (#2026)
  • Other: Ghidra 12.1.4 (Apache-2.0) with JDK 21.0.12.1; GNU objdump 2.46.1; pyelftools 0.33
  • Target: esmtool.x86_64 from the OpenMW 0.51.0 Linux tarball (GPL-3.0); stripped copy sha256 68510563bcb9026f0554712307be115d56d3e3f7167968df3b832886f28ff585
  • Shared box: another job ran at the same time. Ghidra's startup took about 130 s here.

🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊

Originally created by @CassieFox23 on GitHub (Oct 12, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/2326 **TL;DR:** When Ghidra's function body stops short, the callee and caller reads give no warning. In OpenMW 0.51.0 ([GPL-3.0](https://github.com/OpenMW/openmw/blob/openmw-0.51.0/LICENSE)), 22 of 50 checked functions lost real calls, though 20 were picked for losing the most. All 100 reads said `complete` with no note, and I am not asking to change that word, only to tell the reader. ### What I ran I ran the Ghidra provider over MCP on a stripped copy of `esmtool.x86_64` from OpenMW 0.51.0, on main 0365454a5. An earlier run on 69e097a8c (#2026) gave the same results, so no recent commit changed this. The fold below says how I picked the 50 functions. ### The smallest example The function is `boost::wrapexcept<boost::bad_any_cast>::rethrow()` at `0x112cc0`. Ghidra loads this file at 0x100000, so the file address is `0x12cc0`. - The function is 196 bytes. Ghidra's body is 164 bytes, from `0x112cc0` to `0x112d63`. - `procedure_callees` returns 2 callees. The code makes 5 direct calls. - The 3 missing calls are at `0x112d6f`, `0x112d77` and `0x112d7f`. They sit in the block that starts at `0x112d64`, just past the body. - That block is exception cleanup. The file's own exception table names `0x112d64` as a landing pad. A landing pad is a spot that only the C++ unwinder jumps to. - The reply says `complete`, lists four generic limitations, and says nothing about the body. - `procedure_info` has one terminal-call line, for the call at `0x112d57`. That call is inside the body. It does not explain the lost block. - `trace_call_path` from `0x112cc0` to `0x112930` (`__cxa_free_exception`) says `not_reached`, `truncated: false`, and no unknowns. The real call to it is at `0x112d77`. ### Counts | Check | Result | |---|---| | Callee and caller reads that say `complete` | 100 of 100 | | Those reads with any note about the body | 0 of 100 | | Functions that lost real calls | 22 of 50 (20 picked for losing the most) | | Missing calls: edges, then call sites | 269, at 618 sites | | Missing call sites inside the reported body | 0 of 618 | | Cause A: right after a terminal call that `procedure_info` lists | 539 sites | | Cause B: right after `ret` or `jmp`, and no view lists it | 79 sites, in 4 functions | 22 of 50 is not a rate, since 20 were picked for losing the most. In the earlier whole-file run, 391 of 1,995 functions had at least one missing call. Callers have the same gap, and a read on the target can't see it. For 10 targets, `procedure_callers` left out a real caller, and all 10 replies said `complete` with no note. <details> <summary>How I picked the functions, and more on the counts</summary> - Thirty of the 50 functions are spread across the file; 2 of those lost calls. The other 20 lost the most calls in my earlier whole-file run on 69e097a8c (#2026). - For all 50, that run gave the same bodies, callee lists and coverage. The limitation text also matches for the 30 where that run kept it. - A function can call one target from several places, so there are more sites than edges. - For cause B, I checked the exception table for the example above. For 17 of the 79 sites, the table also names a landing pad earlier in the same stretch of left-out code. I did not trace the other 62. - Callers example: `0x1dd750` has 2 real direct callers, and REA returned none. </details> ### What the docs say `docs/tool-design.md`, lines 60 to 64, says that `complete` "means that set was returned without a known read limit". It also says that provider and execution caveats "stay in `limitations`". So the word `complete` is by design, and a caveat belongs in `limitations`. `docs/ghidra-provider.md`, lines 58 to 67, says an inferred no-return call "can terminate that body before subsequent bytes". It lists the views that report this: pseudocode, assembly, procedure info, instruction windows and dossiers. Callers and callees are not on that list. In the bridge, `procedureCalls` (line 721 of `bridge/ghidra/ReaGhidraBridge.java`) returns a bare list and does not call `noReturnCallLimitations`. `procedureInfo` does. I only read this; I changed nothing. ### What I ask - Tell the reader somewhere in the reply. I do not mind where. By the design doc, `limitations` fits and `complete` can stay. If you read "known read limit" as covering this, `partial` fits too. In `src/application/CallPathTracing.ts`, `trace_call_path` only turns `unknown` on `partial` or `unknown` coverage. So a note in `limitations` alone will not change `not_reached`. I read that code; I did not run it. - For cause B, one line would help: the body leaves out code that only the unwinder reaches. No view says this today, not even `procedure_info`. A kind no is fine. I know the design doc keeps caveats out of coverage on purpose. Related: my earlier issue #1658 asked for this warning, and 69a8b3a95 added it to five views. Callers and callees were left out. #2222 and #2223 are about readers who trust an incomplete label. This report is about whether the label has a warning to read. ### Not run - The whole file on 0365454a5. I ran the 50 functions only. - Hopper, IDA, Windows, REA on the unstripped file, and other programs. - REA's tests and `npm run verify:ghidra`. - `compare_functions`, `verify_reconstruction` and `build_call_path`. - A check that each of the 62 other cause B sites is cleanup code. Thanks, morluto, for the notes in 69a8b3a95. They made this easy to chase. <details> <summary>Table of the 50 functions</summary> The Symbol, Body, Real and Got columns are the same on 69e097a8c and 0365454a5. The Terminal column was kept for 30 functions only. It matches there. S is a function from the spread sample. T is one of the 20 with the most missing calls. Symbol is the size in the symbol table, in bytes. Body is the size of Ghidra's body, in bytes. Real is the count of direct calls in the function's own symbol range, found with objdump on the unstripped file. Got is the count of callees REA returned. Terminal is the count of terminal-call lines in `procedure_info`. | # | Address | Symbol | Body | Real | Got | Missing | Terminal | |---|---|---|---|---|---|---|---| | S0 | `0x112cc0` | 196 | 164 | 5 | 2 | 3 | 1 | | S1 | `0x12a890` | 1 | 1 | 0 | 0 | 0 | 0 | | S2 | `0x12f0f0` | 1 | 1 | 0 | 0 | 0 | 0 | | S3 | `0x130120` | 202 | 193 | 1 | 1 | 0 | 1 | | S4 | `0x132c50` | 471 | 368 | 11 | 7 | 4 | 3 | | S5 | `0x139090` | 42 | 42 | 0 | 0 | 0 | 0 | | S6 | `0x13e480` | 183 | 179 | 0 | 0 | 0 | 0 | | S7 | `0x150ba0` | 580 | 557 | 11 | 12 | 0 | 2 | | S8 | `0x1586d0` | 18 | 18 | 1 | 1 | 0 | 0 | | S9 | `0x158b40` | 16 | 16 | 0 | 1 | 0 | 1 | | S10 | `0x158f20` | 457 | 448 | 2 | 2 | 0 | 0 | | S11 | `0x15aab0` | 166 | 166 | 1 | 1 | 0 | 1 | | S12 | `0x15f2b0` | 627 | 611 | 9 | 9 | 0 | 0 | | S13 | `0x16bd10` | 210 | 209 | 1 | 1 | 0 | 1 | | S14 | `0x16fe90` | 43 | 36 | 1 | 1 | 0 | 0 | | S15 | `0x175670` | 71 | 71 | 1 | 1 | 0 | 0 | | S16 | `0x17a770` | 7 | 7 | 0 | 0 | 0 | 0 | | S17 | `0x180cf0` | 358 | 335 | 4 | 6 | 0 | 2 | | S18 | `0x18c2b0` | 457 | 448 | 2 | 2 | 0 | 0 | | S19 | `0x194860` | 75 | 75 | 0 | 0 | 0 | 0 | | S20 | `0x19c2c0` | 990 | 931 | 9 | 9 | 0 | 3 | | S21 | `0x1a7360` | 291 | 246 | 0 | 0 | 0 | 0 | | S22 | `0x1accb0` | 74 | 74 | 1 | 2 | 0 | 1 | | S23 | `0x1bab40` | 399 | 383 | 5 | 6 | 0 | 2 | | S24 | `0x1c9330` | 8570 | 8139 | 19 | 26 | 0 | 44 | | S25 | `0x1d36f0` | 104 | 94 | 1 | 1 | 0 | 0 | | S26 | `0x1de600` | 253 | 238 | 3 | 3 | 0 | 0 | | S27 | `0x1e63d0` | 51 | 46 | 0 | 0 | 0 | 0 | | S28 | `0x1ea490` | 555 | 543 | 3 | 3 | 0 | 1 | | S29 | `0x1f67d0` | 132 | 131 | 2 | 2 | 0 | 0 | | T | `0x14a000` | 4487 | 171 | 34 | 1 | 33 | 1 | | T | `0x122ff0` | 1231 | 12 | 19 | 1 | 18 | 1 | | T | `0x115fb0` | 509 | 12 | 16 | 1 | 15 | 1 | | T | `0x122dae` | 417 | 12 | 15 | 1 | 14 | 1 | | T | `0x124a43` | 670 | 69 | 18 | 4 | 14 | 1 | | T | `0x11f06c` | 897 | 12 | 14 | 1 | 13 | 1 | | T | `0x120dc6` | 658 | 12 | 14 | 1 | 13 | 1 | | T | `0x11d8a0` | 3577 | 50 | 15 | 3 | 12 | 1 | | T | `0x1228de` | 959 | 114 | 16 | 4 | 12 | 1 | | T | `0x123da2` | 313 | 12 | 13 | 1 | 12 | 1 | | T | `0x123edc` | 1144 | 258 | 18 | 6 | 12 | 1 | | T | `0x1133b2` | 812 | 167 | 21 | 10 | 11 | 1 | | T | `0x115d09` | 610 | 26 | 13 | 2 | 11 | 1 | | T | `0x11a948` | 281 | 12 | 12 | 1 | 11 | 1 | | T | `0x12031c` | 541 | 17 | 12 | 1 | 11 | 1 | | T | `0x116d4c` | 346 | 12 | 11 | 1 | 10 | 1 | | T | `0x119296` | 763 | 12 | 11 | 1 | 10 | 1 | | T | `0x11a1f4` | 524 | 17 | 11 | 1 | 10 | 1 | | T | `0x11cf16` | 416 | 12 | 11 | 1 | 10 | 1 | | T | `0x122c9e` | 272 | 12 | 11 | 1 | 10 | 1 | </details> <details> <summary>Earlier run, whole file</summary> These numbers are from the earlier run on 69e097a8c. I did not repeat them on 0365454a5. Recall is the share of real calls that REA returned. Precision is the share of returned calls that are real. | What | Result | |---|---| | Function starts found | 1,543 of 1,543 | | Cold pieces found | 452 of 452 | | Starts that are real | 94.59% | | Body size exact | 569 of 1,543 | | Body size within 16 bytes | 401 of 1,543 | | Body size within 10% | 406 of 1,543 | | Body size worse | 167 of 1,543 | | Callee recall, sample of 30 | 92.47% | | Callee precision, sample of 30 | 86.87% | | Callee recall, all 1,995 | 84.13% | | Callee precision, all 1,995 | 90.38% | | Functions with a missing call, all 1,995 | 391 | | Real calls missing, all 1,995 | 1,363 | | Of those, call site outside the body | 1,363 | | Caller recall, sample of 30 | 88.89% | | Caller precision, sample of 30 | 80.0% | </details> <details> <summary>Commands</summary> | Step | Command | Result | |---|---|---| | Main | `gh api repos/morluto/rea/branches/main --jq .commit.sha` | 0 | | Build | `npm ci`, `npm run build`, `npm run test:prepare` | 0, 0, 0 | | Strip | `strip --strip-all esmtool.x86_64` | 0 | | Run | `systemd-run --user --scope -p MemoryMax=8G -- node mcp_client.mjs` | client quit normally | | Calls | `open_binary`, `procedure_info`, `procedure_callees`, `procedure_callers`, `get_call_graph`, `trace_call_path` | no tool errors | | Real calls | `objdump -d --no-show-raw-insn -j .text -j .plt -j .plt.got` | 0 | | Exception table | `lsda_check.py` and `lsda_all.py`, using pyelftools | 0, 0 | The run was one MCP session with 168 tool calls. `open_binary` took `provider_id` set to `ghidra`. The Node line in Test setup is the version that built and ran REA. </details> <details> <summary>Test setup</summary> - Box: Nobara 44 (Fedora 44 base), Linux 7.2.9 - CPU: AMD Ryzen 9 5900X, 12 cores / 24 threads - RAM: 32 GB DDR4-3600 - Node: 24.18.0 - Head / base: 0365454a5 / 69e097a8c (#2026) - Other: Ghidra 12.1.4 (Apache-2.0) with JDK 21.0.12.1; GNU objdump 2.46.1; pyelftools 0.33 - Target: `esmtool.x86_64` from the OpenMW 0.51.0 Linux tarball ([GPL-3.0](https://github.com/OpenMW/openmw/blob/openmw-0.51.0/LICENSE)); stripped copy sha256 `68510563bcb9026f0554712307be115d56d3e3f7167968df3b832886f28ff585` - Shared box: another job ran at the same time. Ghidra's startup took about 130 s here. </details> 🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊
bsakaguchi 2026-10-15 18:28:11 +00:00
Author
Owner

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

Triage & Implementation Review

Reporting complete coverage when Ghidra's decompiler or auto-analysis delineates a truncated function body misleads agent reasoning by hiding real call edges.

  • Limitation Reporting: When the boundary of a function body terminates abruptly before return/unwind or has unresolved fallthrough, emitting an explicit limitation warning maintains Evidence provenance.
  • Tracking: Implemented in PR #2360 (fix(ghidra): warn when callee/caller reads stop at a truncated function body).
<!-- gh-comment-id:6134254336 --> @matbrgz commented on GitHub (Oct 12, 2026): ### Triage & Implementation Review Reporting complete coverage when Ghidra's decompiler or auto-analysis delineates a truncated function body misleads agent reasoning by hiding real call edges. - **Limitation Reporting:** When the boundary of a function body terminates abruptly before return/unwind or has unresolved fallthrough, emitting an explicit limitation warning maintains Evidence provenance. - **Tracking:** Implemented in PR #2360 (`fix(ghidra): warn when callee/caller reads stop at a truncated function body`).
Author
Owner

@destinal commented on GitHub (Oct 21, 2026):

A likely root cause on the Ghidra side, for anyone who lands here. I couldn't check it against your binary, but it matches both causes in this report.

Ghidra 12.1.x mis-decodes .gcc_except_table for ELF images that the loader rebased, such as a PIE at the default 0x100000. GccExceptionAnalyzer puts every call-site start and landing pad at its true address plus the image-base adjustment (filed as https://github.com/NationalSecurityAgency/ghidra/issues/9979). The effects:

  • Real landing pads are never disassembled. The analyzer disassembles 1 MB away instead, so code reachable only through the unwinder stays outside function bodies (your cause B).
  • The misplaced disassembly lands mid-instruction in unrelated code. That leaves junk after ordinary calls, which can push "Non-Returning Functions - Discovered" into flagging normal callees (e.g. memcpy) as no-return and ending bodies early at those calls (cause A). Your listed terminal callees would be worth checking for that.
  • The analysis log fills with Function body does not contain the landing pad (LSDACallSiteTable) and Failed to disassemble at … (GccExceptionAnalyzer).

The 0x112cc0 addresses in this report suggest a rebased PIE at 0x100000, which is the affected case.

Workaround: import with the image base equal to the ELF's (headless -loader ElfLoader -loader-imagebase 0; same option in the GUI import dialog). The adjustment is then 0. Ghidra master appears to fix it in b23a95e1e (GP-6913), but that hasn't reached a 12.1.x release yet. So the provider might want to import PIEs at their ELF base, or note this limitation, until a patched release is out.

<!-- gh-comment-id:6305212928 --> @destinal commented on GitHub (Oct 21, 2026): A likely root cause on the Ghidra side, for anyone who lands here. I couldn't check it against your binary, but it matches both causes in this report. Ghidra 12.1.x mis-decodes `.gcc_except_table` for ELF images that the loader rebased, such as a PIE at the default `0x100000`. `GccExceptionAnalyzer` puts every call-site start and landing pad at its true address **plus the image-base adjustment** (filed as https://github.com/NationalSecurityAgency/ghidra/issues/9979). The effects: - **Real landing pads are never disassembled.** The analyzer disassembles 1 MB away instead, so code reachable only through the unwinder stays outside function bodies (your cause B). - **The misplaced disassembly lands mid-instruction in unrelated code.** That leaves junk after ordinary calls, which can push "Non-Returning Functions - Discovered" into flagging normal callees (e.g. `memcpy`) as no-return and ending bodies early at those calls (cause A). Your listed terminal callees would be worth checking for that. - The analysis log fills with `Function body does not contain the landing pad (LSDACallSiteTable)` and `Failed to disassemble at … (GccExceptionAnalyzer)`. The `0x112cc0` addresses in this report suggest a rebased PIE at `0x100000`, which is the affected case. Workaround: import with the image base equal to the ELF's (headless `-loader ElfLoader -loader-imagebase 0`; same option in the GUI import dialog). The adjustment is then 0. Ghidra master appears to fix it in `b23a95e1e` (GP-6913), but that hasn't reached a 12.1.x release yet. So the provider might want to import PIEs at their ELF base, or note this limitation, until a patched release is out.
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#770
No description provided.