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.
214 lines
12 KiB
Java
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);
|
|
}
|
|
}
|