fix(ghidra): say when an inferred no-return call cuts a function

Ghidra's Non-Returning Functions - Discovered analyzer can mark a
returning callee, such as an imported operator new thunk, as
non-returning. Ghidra then clears the fallthrough at each call site and
refits the callers, so a constructor decompiles to a few plausible
lines. REA reported the terminal call with the FunctionManager flag but
did not say the result omits the code after it or how to compare.

That analyzer bookmarks each entry it marks with the Analysis comment
"Non-Returning Function Found" (Ghidra 11.0 through 12.1.4 source); the
name-list Known analyzer writes "Non-Returning Function Identified".
When a terminal call's callee, or the function a thunk forwards to,
carries the Discovered bookmark, the call's limitation now says the
flag was inferred, and the function adds one address-free limitation
that its body and pseudocode end at that call and omit the following
code if the callee returns, with the REA_GHIDRA_DISABLED_ANALYZERS
comparison. A flag without the bookmark keeps an unknown origin.

The function-boundary observation contract moves to v5 so snapshots
from earlier bridges are not replayed without the qualification.

Refs #10365

Co-Authored-By: gensokyo-dev <gensokyo-dev@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
gensokyo-devandClaude Opus 5.5 authored and Ysopwnable committed 2026-11-14 05:34:39 +01:00
1 parent 9d082b144a
commit ad2111fac6
6 files changed
+65 -14

No files matched your search

+46 -6
View File
@@ -77,6 +77,8 @@ import ghidra.program.model.data.StringDataInstance;
import ghidra.program.model.data.DataType;
import ghidra.program.model.lang.Register;
import ghidra.program.model.scalar.Scalar;
import ghidra.program.model.listing.Bookmark;
import ghidra.program.model.listing.BookmarkType;
import ghidra.program.model.listing.CommentType;
import ghidra.program.model.listing.Data;
import ghidra.program.model.listing.DataIterator;
@@ -3987,6 +3989,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
private final class FunctionBoundaryLimitations {
private final Function function;
private final List<String> limitations = new ArrayList<>();
private boolean inferredNoReturnCall;
private Instruction beforePrevious;
private Instruction previous;
@@ -3996,8 +3999,11 @@ public final class ReaGhidraBridge extends HeadlessScript {
private void observe(Instruction instruction) throws Exception {
monitor.checkCancelled();
String limitation = terminalCallLimitation(instruction);
if (limitation != null) limitations.add(limitation);
TerminalCall terminal = terminalCall(instruction);
if (terminal != null) {
limitations.add(terminal.limitation());
if (terminal.inferredNoReturn()) inferredNoReturnCall = true;
}
if (beforePrevious != null && previous != null) {
String seh = borlandSehHandlerLimitation(
function, beforePrevious, previous, instruction);
@@ -4008,10 +4014,39 @@ public final class ReaGhidraBridge extends HeadlessScript {
}
private List<String> values() {
return List.copyOf(limitations);
if (!inferredNoReturnCall) return List.copyOf(limitations);
List<String> result = new ArrayList<>(limitations);
// One consequence and recovery per function; each call keeps its own fact. The text
// names no address, so bounded summaries deduplicate it across functions.
result.add("A terminal call in this function targets a callee that " +
"Ghidra's Non-Returning Functions - Discovered analyzer inferred cannot return, so Ghidra " +
"ended the analyzed body, and the pseudocode and facts derived from it, at that call. If the " +
"callee returns, this result omits the code after the call. To compare, analyze a fresh import " +
"with REA_GHIDRA_DISABLED_ANALYZERS='Non-Returning Functions - Discovered'.");
return result;
}
}
/** A terminal call's limitation and whether its no-return flag was analyzer-inferred. */
private record TerminalCall(String limitation, boolean inferredNoReturn) {}
/**
* Ghidra's Non-Returning Functions - Discovered analyzer bookmarks each entry it marks with
* this Analysis comment; the name-list Known analyzer uses "Non-Returning Function Identified".
* A missing bookmark leaves the flag's origin unknown, never declared.
*/
private boolean discoveredNoReturn(Function target) {
Function thunked = target.isThunk() ? target.getThunkedFunction(true) : null;
return hasDiscoveredNoReturnBookmark(target) ||
(thunked != null && hasDiscoveredNoReturnBookmark(thunked));
}
private boolean hasDiscoveredNoReturnBookmark(Function function) {
Bookmark bookmark = currentProgram.getBookmarkManager().getBookmark(
function.getEntryPoint(), BookmarkType.ANALYSIS, "Non-Returning Function");
return bookmark != null && "Non-Returning Function Found".equals(bookmark.getComment());
}
private String borlandSehHandlerLimitation(
Function function, Instruction handlerPush, Instruction framePush,
Instruction frameLink) {
@@ -4104,9 +4139,10 @@ public final class ReaGhidraBridge extends HeadlessScript {
}
}
private String terminalCallLimitation(Instruction instruction) {
private TerminalCall terminalCall(Instruction instruction) {
if (!instruction.getFlowType().isCall() || !instruction.getFlowType().isTerminal())
return null;
boolean inferred = false;
Address[] destinations = instruction.getFlows();
StringBuilder limitation = new StringBuilder();
limitation.append("Ghidra Listing reports a terminal call at ")
@@ -4135,14 +4171,18 @@ public final class ReaGhidraBridge extends HeadlessScript {
Function target = destinations.length == 1
? currentProgram.getFunctionManager().getFunctionAt(destinations[0])
: null;
if (target != null && target.hasNoReturn()) {
if (target != null && target.hasNoReturn() && discoveredNoReturn(target)) {
inferred = true;
limitation.append(" Ghidra's Non-Returning Functions - Discovered analyzer bookmarked this callee as a non-returning function it found; the flag is an analysis inference, not an independent verification that the callee cannot return.");
}
else if (target != null && target.hasNoReturn()) {
limitation.append(" This records Ghidra's FunctionManager flag, not an independent verification that the callee cannot return.");
}
else {
limitation.append(" The reason for the terminal flow is not established by this observation.");
}
}
return limitation.toString();
return new TerminalCall(limitation.toString(), inferred);
}
private static String renderAssembly(List<Instruction> instructions) {
+12 -1
View File
@@ -338,7 +338,18 @@ handler execution or change the body. Instruction windows and dossiers also
retain these warnings in their result limitations. An unresolved callee or
unestablished terminal-flow basis remains explicit. These observations qualify
the analysis; they do not prove that a no-return flag is incorrect or recover
excluded code. Analysis profiles version this observation contract so older
excluded code. When the callee's entry, or the function a thunk forwards to,
carries the `Non-Returning Function Found` analysis bookmark that Ghidra's
`Non-Returning Functions - Discovered` analyzer writes, the call's limitation
says the flag was inferred, and the function's limitations add one statement
that the body and pseudocode end at that call and omit the following code if
the callee returns. That analyzer can mark returning callees, such as an
imported `operator new` thunk, so a constructor may decompile to a few lines. To
compare, open a fresh import with
`REA_GHIDRA_DISABLED_ANALYZERS='Non-Returning Functions - Discovered'`; an
existing project (`REA_GHIDRA_PROJECT_PATH`) keeps the flags its analysis
already stored. A flag without that bookmark keeps an unknown origin; REA does
not treat it as declared. Analysis profiles version this observation contract so older
snapshots cannot replay unqualified results.
On non-Windows hosts, the ephemeral import repair clears no-return flags on the
+1 -1
View File
@@ -74,7 +74,7 @@ const sharedAnalysisSemantics = {
instruction_status: "unlisted-and-containing-code-data-v3",
instruction_references: "exact-address-incoming-without-synthetic-entry-v3",
byte_search_coverage: "separate-loaded-overlays-v1",
function_boundary_observations: "ghidra-function-boundary-limitations-v4",
function_boundary_observations: "ghidra-function-boundary-limitations-v5",
} as const;
/**
+3 -3
View File
@@ -8,12 +8,12 @@ import { GhidraProvider } from "./GhidraProvider.js";
// Digests commit to the current shared policy, including the instruction
// status, load-image relocation-symbol, procedure search, byte-search and
// bit-field base-type, runtime value-flow budgets and inventory-search revisions that prevent reuse of snapshots with earlier
// bit-field base-type, runtime value-flow budgets, function-boundary and inventory-search revisions that prevent reuse of snapshots with earlier
// results.
const RAW_DIGESTS = {
"6502": "51160ff6e94cb32eaeb87ac248915afb3bc17e33b11a1a101222344dd3907c6f",
"6502": "6c4e88435526e8f0907b072600e3b121577962a36554e023dde4cf85113dd9f6",
"x86-disabled-analyzer":
"94e93961a699cf9250844d53fe95802c72063c08656fc308b3fed523dd86ea1c",
"b2dc33d1932cbe71d72819428634927f3a2d8a2bbd54b6893f823ccb6b795d0b",
} as const;
const configFor = (environment: NodeJS.ProcessEnv) => {
+2 -2
View File
@@ -351,8 +351,8 @@ describe("Ghidra snapshot profile admission", () => {
},
{
parameter: "function_boundary_observations",
current: "ghidra-function-boundary-limitations-v4",
legacy: "ghidra-function-boundary-limitations-v3",
current: "ghidra-function-boundary-limitations-v5",
legacy: "ghidra-function-boundary-limitations-v4",
},
{
parameter: "instruction_references",
+1 -1
View File
@@ -190,7 +190,7 @@ it("retains terminal-call qualifications across live dossiers and snapshot-backe
expect(evidence.limitations, name).toContain(limitation);
expect(evidence.analysis_profile?.parameters).toMatchObject({
function_boundary_observations:
"ghidra-function-boundary-limitations-v4",
"ghidra-function-boundary-limitations-v5",
});
const replay = await harness.session.execute(name, evidence.parameters);
if (!replay.ok) throw replay.error;