Files
rea/tests/conformance/ghidra/ReaDecompilerFailureProbe.java
T
Syed Rishad c374c2fe72 fix(ghidra): preserve partial dossiers on decompiler failure (#4650)
Preserve listing-backed facts and explicit unavailable decompiler facets when one function fails. Retain Evidence while excluding transient failures and derived workflows from snapshot reuse; keep healthy cache reuse and owned cleanup semantics.

Fixes #4644.
2026-10-20 12:08:08 +00:00

214 lines
12 KiB
Java

//@category REA Verification
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import generic.jar.ResourceFile;
import ghidra.app.decompiler.DecompInterface;
import ghidra.app.decompiler.DecompileProcess.DisposeState;
import ghidra.app.decompiler.DecompileResults;
import ghidra.app.script.GhidraScript;
import ghidra.app.script.GhidraScriptUtil;
import ghidra.app.util.headless.HeadlessAnalyzer;
import ghidra.app.util.headless.HeadlessScript;
import ghidra.framework.Application;
import ghidra.program.model.listing.Function;
import ghidra.util.task.TaskMonitor;
import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.util.HexFormat;
import java.util.List;
/**
* Replaces the bridge decompiler after real import and analysis so one procedure's
* decompilation fails. Verifies that analyze_function still returns the listing-backed
* facts. This injection does not establish which natural inputs make Ghidra fail.
*/
public class ReaDecompilerFailureProbe extends GhidraScript {
private static final String INJECTED = "Injected backend failure";
private static final List<String> LISTING_FACETS = List.of(
"assembly", "comments", "callers", "callees", "incoming_references",
"outgoing_references", "referenced_strings", "referenced_names", "basic_blocks",
"unresolved_calls");
private final JsonArray checks = new JsonArray();
private void require(String name, boolean condition) {
if (!condition) throw new IllegalStateException("Decompiler failure assertion failed: " + name);
checks.add(name);
}
private Function exportedFunction(String name) {
Function selected = null;
for (Function f : currentProgram.getFunctionManager().getFunctions(true)) {
if (!f.isThunk() && (f.getName().equals(name) || f.getName().equals("_" + name)) &&
currentProgram.getSymbolTable().isExternalEntryPoint(f.getEntryPoint())) {
if (selected != null) throw new IllegalStateException("Ambiguous fixture export " + name);
selected = f;
}
}
if (selected == null) throw new IllegalStateException("Missing fixture export " + name);
return selected;
}
private GhidraScript script(String name, File expected) throws Exception {
ResourceFile source = GhidraScriptUtil.findScriptByName(name);
if (source == null || !expected.equals(new File(source.getAbsolutePath()).getCanonicalFile()))
throw new IllegalStateException("Fixture resolved an unexpected script: " + name);
GhidraScript instance = GhidraScriptUtil.getProvider(source).getScriptInstance(source, errorWriter);
instance.set(getState(), getControls());
// Match HeadlessScript.runScript's nested-script setup before invoking
// the real request handler, which checks the current analysis status.
Method headless = HeadlessScript.class.getDeclaredMethod("setHeadlessInstance", HeadlessAnalyzer.class);
headless.setAccessible(true);
headless.invoke(instance, HeadlessAnalyzer.getInstance());
return instance;
}
private static DecompInterface failing(Function expected, DisposeState disposition) {
return new DecompInterface() {
@Override
public DecompileResults decompileFunction(Function function, int timeout, TaskMonitor taskMonitor) {
if (!function.equals(expected))
throw new IllegalStateException("Unexpected decompilation of " + function.getName());
return new DecompileResults(function, function.getProgram().getLanguage(),
function.getProgram().getCompilerSpec(), null, INJECTED, null, disposition);
}
};
}
private static String failureCode(Throwable failure) throws Exception {
Field code = failure.getClass().getDeclaredField("code");
code.setAccessible(true);
return (String) code.get(failure);
}
private static String reason(JsonObject value) {
JsonElement reason = value.get("reason");
return reason == null ? null : reason.getAsString();
}
@Override
public void run() throws Exception {
String[] args = getScriptArgs();
if (args.length != 1) throw new IllegalArgumentException("Expected exact bridge directory");
File directory = new File(args[0]).getCanonicalFile();
GhidraScript bridge = script("ReaGhidraBridge.java", new File(directory, "ReaGhidraBridge.java"));
Field space = bridge.getClass().getDeclaredField("sessionDefaultAddressSpace");
space.setAccessible(true);
space.set(null, currentProgram.getAddressFactory().getDefaultAddressSpace());
Method initialize = bridge.getClass().getDeclaredMethod("initializeDecompiler");
initialize.setAccessible(true);
initialize.invoke(bridge);
Class<?> requestType = Class.forName(bridge.getClass().getName() + "$Request", true, bridge.getClass().getClassLoader());
Class<?> descriptorType = Class.forName(bridge.getClass().getName() + "$SessionDescriptor", true, bridge.getClass().getClassLoader());
Constructor<?> requestConstructor = requestType.getDeclaredConstructor(int.class, String.class, JsonObject.class);
requestConstructor.setAccessible(true);
Method analyze = bridge.getClass().getDeclaredMethod("handleRequest", requestType, descriptorType);
analyze.setAccessible(true);
Method pseudocode = bridge.getClass().getDeclaredMethod("procedurePseudocode", JsonObject.class);
pseudocode.setAccessible(true);
Field decompiler = bridge.getClass().getDeclaredField("decompiler");
decompiler.setAccessible(true);
DecompInterface real = (DecompInterface) decompiler.get(bridge);
Function target = exportedFunction("rea_cross_branch");
String procedure = "0x" + Long.toUnsignedString(target.getEntryPoint().getOffset(), 16);
JsonObject query = new JsonObject();
query.addProperty("procedure", procedure);
// Exercise the actual wire response producer, including cache metadata.
Object request = requestConstructor.newInstance(1, "analyze_function", query);
JsonObject pseudocodeQuery = new JsonObject();
pseudocodeQuery.add("document", JsonNull.INSTANCE);
pseudocodeQuery.addProperty("procedure", procedure);
JsonObject baseline;
JsonObject degraded;
try {
baseline = (JsonObject) analyze.invoke(bridge, request, null);
JsonObject value = baseline.getAsJsonObject("value");
require("healthy_snapshot_policy_preserved", !baseline.has("cache_policy"));
require("baseline_pseudocode_available", !value.get("pseudocode").getAsString().isEmpty() &&
value.getAsJsonObject("facet_availability").getAsJsonObject("pseudocode").get("available").getAsBoolean());
require("baseline_native_api_available", value.getAsJsonObject("native_api").get("available").getAsBoolean());
require("baseline_assembly_present", value.getAsJsonArray("assembly").size() > 0);
require("baseline_callers_present", value.getAsJsonArray("callers").size() > 0);
require("baseline_callees_present", value.getAsJsonArray("callees").size() > 0);
require("baseline_multiple_blocks_present", value.getAsJsonArray("basic_blocks").size() > 1);
decompiler.set(bridge, failing(target, DisposeState.NOT_DISPOSED));
degraded = (JsonObject) analyze.invoke(bridge, request, null);
JsonObject dossier = degraded.getAsJsonObject("value");
require("decompiler_failure_is_live", degraded.get("cache_policy").getAsString().equals("live"));
String expected = "Ghidra decompilation failed: " + INJECTED;
require("dossier_returned", dossier.getAsJsonObject("procedure").get("address").getAsString()
.equals(value.getAsJsonObject("procedure").get("address").getAsString()));
JsonObject facets = dossier.getAsJsonObject("facet_availability");
require("pseudocode_unavailable_with_failure", dossier.get("pseudocode").getAsString().isEmpty() &&
!facets.getAsJsonObject("pseudocode").get("available").getAsBoolean() &&
expected.equals(reason(facets.getAsJsonObject("pseudocode"))));
require("native_api_unavailable_with_failure",
!dossier.getAsJsonObject("native_api").get("available").getAsBoolean() &&
expected.equals(reason(dossier.getAsJsonObject("native_api"))));
require("native_value_flow_unavailable_with_failure",
!dossier.getAsJsonObject("native_value_flow").get("available").getAsBoolean() &&
expected.equals(reason(dossier.getAsJsonObject("native_value_flow"))));
for (String facet : LISTING_FACETS)
require("listing_facet_unchanged_" + facet, dossier.get(facet).equals(value.get(facet)));
for (String facet : List.of("assembly", "comments", "callers", "callees", "references", "strings_names", "cfg"))
require("facet_availability_unchanged_" + facet,
facets.get(facet).equals(value.getAsJsonObject("facet_availability").get(facet)));
require("relationship_coverage_unchanged",
degraded.get("relationship_coverage").equals(baseline.get("relationship_coverage")));
boolean named = false;
for (JsonElement item : dossier.getAsJsonArray("limitations"))
named |= item.getAsString().startsWith(expected + ". Pseudocode, the native API boundary") &&
item.getAsString().contains("were still collected");
require("failure_limitation_named", named);
try {
pseudocode.invoke(bridge, pseudocodeQuery);
throw new IllegalStateException("Pseudocode request unexpectedly succeeded");
} catch (InvocationTargetException failure) {
require("pseudocode_request_still_fails",
failureCode(failure.getCause()).equals("decompile_failed") &&
failure.getCause().getMessage().equals(expected));
}
decompiler.set(bridge, failing(target, DisposeState.DISPOSED_ON_CANCEL));
try {
analyze.invoke(bridge, request, null);
throw new IllegalStateException("Cancelled decompilation unexpectedly returned a dossier");
} catch (InvocationTargetException failure) {
require("cancellation_still_ends_request",
failureCode(failure.getCause()).equals("decompile_cancelled"));
}
decompiler.set(bridge, real);
JsonObject recovered = (JsonObject) analyze.invoke(bridge, request, null);
require("session_recovers_after_failure", recovered.getAsJsonObject("value")
.getAsJsonObject("facet_availability").getAsJsonObject("pseudocode").get("available").getAsBoolean());
} finally {
decompiler.set(bridge, real);
real.dispose();
}
JsonObject report = new JsonObject();
report.addProperty("status", "passed");
report.addProperty("ghidra_version", Application.getApplicationVersion());
report.addProperty("target_sha256", currentProgram.getExecutableSHA256());
byte[] bytes = Files.readAllBytes(new File(directory, "ReaGhidraBridge.java").toPath());
report.addProperty("ReaGhidraBridge.java",
HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes)));
report.add("checks", checks);
report.add("baseline", baseline);
report.add("degraded", degraded);
println("REA_DECOMPILER_FAILURE_PROBE_JSON " + report);
}
}