diff --git a/bridge/ghidra/ReaGhidraNoReturnFix.java b/bridge/ghidra/ReaGhidraNoReturnFix.java index d7c8c3000..ebeb0b95b 100644 --- a/bridge/ghidra/ReaGhidraNoReturnFix.java +++ b/bridge/ghidra/ReaGhidraNoReturnFix.java @@ -3,20 +3,25 @@ import ghidra.app.script.GhidraScript; import ghidra.program.model.address.Address; import ghidra.program.model.listing.*; import ghidra.program.model.symbol.FlowType; +import ghidra.program.model.symbol.Reference; import ghidra.program.model.pcode.PcodeOp; -import java.util.ArrayList; +import java.util.ArrayDeque; +import java.util.Deque; import java.util.HashSet; -import java.util.List; import java.util.Set; import java.util.TreeSet; /** * Undo wrong no-return flags from auto-analysis. A function flagged no-return that contains a - * real RET is an ordinary function (stream readers, cxa_guard_acquire, ...). Callers treated the - * call as a terminator and the decompiler truncated them. Clear the flag, then re-disassemble only + * real RET is an ordinary function (stream readers, cxa_guard_acquire, ...). Known returning + * libc/TLS imports have no body to inspect, so clear their flags by exact external ABI name. + * Callers treated the call as a terminator and the decompiler truncated them. Clear the flag, then re-disassemble only * the call sites of the functions that were cleared and refit their owning functions in place. */ public final class ReaGhidraNoReturnFix extends GhidraScript { + private static final Set RETURNING_IMPORTS = Set.of( + "malloc", "__tls_get_addr", "memcpy", "memset", "strlen", "__errno_location"); + @Override public void run() throws Exception { // Windows P0 does not admit analysis-database mutation. @@ -24,66 +29,87 @@ public final class ReaGhidraNoReturnFix extends GhidraScript { FunctionManager fm = currentProgram.getFunctionManager(); Listing li = currentProgram.getListing(); Set
cleared = new HashSet<>(); - for (Function f : fm.getFunctions(true)) { + Deque
pending = new ArrayDeque<>(); + for (Function f : fm.getExternalFunctions()) { monitor.checkCancelled(); - if (!f.hasNoReturn() || f.isThunk()) continue; - for (Instruction ins : li.getInstructions(f.getBody(), true)) { - FlowType ft = ins.getFlowType(); - boolean decodedReturn = false; - for (PcodeOp operation : ins.getPcode()) { - if (operation.getOpcode() == PcodeOp.RETURN) decodedReturn = true; - } - if (decodedReturn && !ft.isJump() && !ft.isCall()) { - f.setNoReturn(false); - cleared.add(f.getEntryPoint()); - break; - } - } + // Do not apply import-name assumptions to local functions or to arbitrary externals. + if (!f.hasNoReturn() || !RETURNING_IMPORTS.contains(f.getName())) continue; + f.setNoReturn(false); + enqueueReturning(f, cleared, pending); } - // A thunk inherits the flag of the function it forwards to. for (Function f : fm.getFunctions(true)) { monitor.checkCancelled(); - if (!f.isThunk()) continue; - Function target = f.getThunkedFunction(true); - if (target != null && cleared.contains(target.getEntryPoint())) { - cleared.add(f.getEntryPoint()); + if (clearDecodedReturn(f, li)) { + enqueueReturning(f, cleared, pending); } } int redone = 0; TreeSet
owners = new TreeSet<>(); - if (!cleared.isEmpty()) { - List
sites = new ArrayList<>(); - for (Instruction ins : li.getInstructions(true)) { + while (!pending.isEmpty()) { + Address target = pending.removeFirst(); + TreeSet
sites = new TreeSet<>(); + // References include computed import calls after Ghidra resolves their destination. + // Collect before redisassembly mutates the reference table. + for (Reference reference : currentProgram.getReferenceManager().getReferencesTo(target)) { monitor.checkCancelled(); - FlowType ft = ins.getFlowType(); - if (!(ft.isCall() && ft.isTerminal())) continue; - Address[] flows = ins.getFlows(); - if (flows.length == 0 || !cleared.contains(flows[0])) continue; - // Do not redisassemble into an independently identified function. - Address next = ins.getMaxAddress().add(1); - if (fm.getFunctionAt(next) != null || !currentProgram.getMemory().contains(next)) continue; - sites.add(ins.getAddress()); + sites.add(reference.getFromAddress()); } for (Address s : sites) { monitor.checkCancelled(); Instruction ins = li.getInstructionAt(s); if (ins == null) continue; + FlowType ft = ins.getFlowType(); + if (!(ft.isCall() && ft.isTerminal())) continue; + Address[] flows = ins.getFlows(); + boolean callsTarget = false; + for (Address flow : flows) { + if (flow.equals(target)) callsTarget = true; + } + if (!callsTarget) continue; + // Do not redisassemble into an independently identified function. + Address next = ins.getMaxAddress().add(1); + if (fm.getFunctionAt(next) != null || !currentProgram.getMemory().contains(next)) continue; Function owner = fm.getFunctionContaining(s); if (owner != null && !owner.isThunk()) owners.add(owner.getEntryPoint()); clearListing(ins.getMinAddress(), ins.getMaxAddress()); disassemble(s); redone++; - } - for (Address e : owners) { - monitor.checkCancelled(); - Function f = fm.getFunctionAt(e); // Refit the body in place so the name, signature and calling convention survive. - if (f != null) CreateFunctionCmd.fixupFunctionBody(currentProgram, f, monitor); + if (owner != null && !owner.isThunk()) { + CreateFunctionCmd.fixupFunctionBody(currentProgram, owner, monitor); + // Auto-analysis may have propagated the false flag to this truncated caller. + // Its RET becomes visible only now; repair its callers in the same pass. + if (clearDecodedReturn(owner, li)) enqueueReturning(owner, cleared, pending); + } } - // No analyzeChanges here: on libpl2.so it adds about 70 s, which pushes startup past the 330 s deadline. } + // No analyzeChanges here: on libpl2.so it adds about 70 s, which pushes startup past the 330 s deadline. println( "REA no-return fix: cleared=" + cleared.size() + " sites=" + redone + " refit=" + owners.size()); } + private void enqueueReturning(Function function, Set
cleared, Deque
pending) { + if (cleared.add(function.getEntryPoint())) pending.addLast(function.getEntryPoint()); + // A thunk inherits the flag of the function it forwards to, including through other thunks. + Address[] thunks = function.getFunctionThunkAddresses(true); + if (thunks == null) return; + for (Address thunk : thunks) { + if (cleared.add(thunk)) pending.addLast(thunk); + } + } + + private boolean clearDecodedReturn(Function function, Listing listing) throws Exception { + if (!function.hasNoReturn() || function.isThunk()) return false; + for (Instruction instruction : listing.getInstructions(function.getBody(), true)) { + monitor.checkCancelled(); + FlowType flow = instruction.getFlowType(); + if (flow.isJump() || flow.isCall()) continue; + for (PcodeOp operation : instruction.getPcode()) { + if (operation.getOpcode() != PcodeOp.RETURN) continue; + function.setNoReturn(false); + return true; + } + } + return false; + } } diff --git a/docs/ghidra-provider.md b/docs/ghidra-provider.md index 3c1dbf5b2..b1e87a188 100644 --- a/docs/ghidra-provider.md +++ b/docs/ghidra-provider.md @@ -66,6 +66,16 @@ 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 snapshots cannot replay unqualified results. +On non-Windows hosts, the ephemeral import repair clears no-return flags on the +known returning external ABI names `malloc`, `__tls_get_addr`, `memcpy`, `memset`, +`strlen`, and `__errno_location`. This relies on their expected ABI behavior; +external implementations are not inspected. The repair re-disassembles affected +terminal calls and refits their owning functions, preserving independent function +entries. If a recovered caller reveals a decoded return, its propagated no-return +flag and affected callers are repaired in the same pass. Unknown imports and +genuine no-return imports keep their flags. Analysis profiles version this repair +so snapshots produced before it are not reused. Windows P0 does not run it. + `npm run verify:ghidra` compiles the versioned C oracle into debug and stripped host-native targets (x86-64/AArch64 ELF on Linux or Mach-O on macOS), plus a native DWARF 4 type-layout object. It proves the admitted operations, external functions, resolved thunks, exports, stripped-name diff --git a/knip.json b/knip.json index bfd7783d6..8d38b4ef1 100644 --- a/knip.json +++ b/knip.json @@ -8,6 +8,7 @@ "src/evaluation/AgentEvaluationFixtures.ts", "src/application/RealHopperAssertions.ts", "scripts/verify/termux/browser.mjs", + "scripts/verify-real-ghidra-noreturn.mjs", "tests/processOwnershipRunner.ts" ], "project": ["src/**/*.ts", "tests/**/*.ts", "scripts/**/*.mjs"], diff --git a/scripts/verify-real-ghidra-noreturn.mjs b/scripts/verify-real-ghidra-noreturn.mjs new file mode 100644 index 000000000..3d7caafb6 --- /dev/null +++ b/scripts/verify-real-ghidra-noreturn.mjs @@ -0,0 +1,206 @@ +import assert from "node:assert/strict"; +import { execFile } from "node:child_process"; +import { createHash } from "node:crypto"; +import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; + +import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js"; +import { parseConfig } from "../dist/config/parseConfig.js"; +import { GhidraClient } from "../dist/ghidra/GhidraClient.js"; +import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js"; +import { SUPPORTED_GHIDRA_VERSION } from "../dist/ghidra/GhidraInstallationPolicy.js"; +import { GhidraProvider } from "../dist/ghidra/GhidraProvider.js"; +import { silentLogger } from "../dist/logger.js"; +import { cleanupOwnedProcessGroup } from "../dist/process/ProcessOwnership.js"; +import { + ProviderProcessSupervisor, + spawnOwnedProviderProcess, +} from "../dist/process/ProviderProcess.js"; +import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs"; + +const exec = promisify(execFile); +const run = createVerifierRun(); +assert.equal(process.platform, "linux", "This regression lane requires Linux"); +assert.equal(process.arch, "x64", "This regression lane requires x86-64"); +const config = parseConfig(process.env); +if (!config.ok) throw config.error; +const installation = inspectGhidraInstallation({ + environment: process.env, + installDir: config.value.ghidraInstallDir, + javaHome: config.value.ghidraJavaHome, +}); +assert.equal(installation.status, "available", JSON.stringify(installation)); +assert.equal(installation.providerVersion, SUPPORTED_GHIDRA_VERSION); +const fixtures = fileURLToPath( + new URL("../tests/conformance/ghidra", import.meta.url), +); +const bridge = fileURLToPath(new URL("../bridge/ghidra", import.meta.url)); +const workspace = await mkdtemp(join(tmpdir(), "rea-ghidra-noreturn-")); +let cleaned = true; +let report; +const digest = (bytes) => createHash("sha256").update(bytes).digest("hex"); +try { + const targetPath = join(workspace, "noreturn.so"); + await exec(process.env.REA_CC ?? "cc", [ + "-shared", + "-fPIC", + "-O0", + "-fno-builtin", + join(fixtures, "no-return.c"), + "-o", + targetPath, + ]); + const targetDigest = digest(await readFile(targetPath)); + const sourceDigests = Object.fromEntries( + await Promise.all( + ["ReaGhidraBridge.java", "ReaGhidraNoReturnFix.java"].map( + async (name) => [name, digest(await readFile(join(bridge, name)))], + ), + ), + ); + const project = join(workspace, "project"); + await mkdir(project); + const launch = await spawnOwnedProviderProcess({ + command: installation.analyzeHeadlessPath, + arguments: [ + project, + "NoReturn", + "-import", + targetPath, + "-readOnly", + "-deleteProject", + "-scriptPath", + `${fixtures};${bridge}`, + "-postScript", + join(fixtures, "ReaNoReturnProbe.java"), + bridge, + ], + runId: run.run_id, + expectedCommand: null, + }); + const supervisor = new ProviderProcessSupervisor({ + ...launch, + ownsProcessLifetime: true, + cleanup: () => cleanupOwnedProcessGroup(launch.ownership), + }); + let probe; + try { + assert.ok( + await supervisor.waitForExit(120000), + "No-return probe timed out", + ); + assert.ok( + await supervisor.waitForOutputClose(1000), + "No-return probe output did not close", + ); + const snapshot = supervisor.snapshot(); + const output = `${snapshot.stdout.text}\n${snapshot.stderr.text}`; + assert.equal(snapshot.exitCode, 0, output); + const reports = [ + ...output.matchAll(/REA_NO_RETURN_PROBE_JSON (\{[^\r\n]*\})/gu), + ]; + assert.equal(reports.length, 1, output); + probe = JSON.parse(reports[0][1]); + assert.equal(probe.status, "passed"); + assert.equal(probe.ghidra_version, SUPPORTED_GHIDRA_VERSION); + assert.equal(probe.target_sha256, targetDigest); + for (const [name, expected] of Object.entries(sourceDigests)) + assert.equal(probe[name], expected); + assert.ok(probe.checks.includes("independent_entry_preserved")); + assert.ok(probe.checks.includes("propagated_flag_repaired_rea_tls_outer")); + assert.ok(probe.checks.includes("recovered_pseudocode")); + } finally { + const stopped = await supervisor.stop(); + cleaned = stopped.status === "verified-cleanup"; + assert.ok( + cleaned, + `Owned cleanup incomplete; retained ${workspace}: ${JSON.stringify(stopped)}`, + ); + } + + // Exercise the actual Java server, authenticated transport, request queue and Evidence + // projection. This read-only TCP lane also runs where AF_UNIX is unavailable; it does + // not claim Windows authority or validation of the default Linux Unix-socket transport. + const provider = new GhidraProvider( + config.value, + silentLogger, + process.env, + undefined, + (options) => + new GhidraClient({ ...options, transport: "authenticated-loopback-tcp" }), + ); + const target = await parseBinaryTarget(targetPath); + if (!target.ok) throw target.error; + const profile = await provider.resolveAnalysisProfile(target.value); + if (!profile.ok) throw profile.error; + assert.ok(profile.value.profile); + assert.equal( + profile.value.profile.parameters.no_return_repair, + "returning-imports-decoded-return-v2", + ); + const client = provider.createClient(target.value, profile.value.profile); + cleaned = false; + const observations = []; + const failures = []; + try { + for (const operation of [ + "procedure_pseudo_code", + "procedure_assembly", + "procedure_info", + "read_function_instructions", + "analyze_function", + ]) { + const result = await client.execute(operation, { + procedure: "rea_abort", + }); + if (!result.ok) throw result.error; + const warnings = result.value.limitations.filter((value) => + /terminal call/u.test(value), + ); + assert.ok( + warnings.some( + (value) => /abort/u.test(value) && /fallthrough/u.test(value), + ), + operation, + ); + observations.push({ operation, warnings }); + } + const healthy = await client.execute("procedure_pseudo_code", { + procedure: "rea_tls", + }); + if (!healthy.ok) throw healthy.error; + assert.ok( + !healthy.value.limitations.some((value) => /terminal call/u.test(value)), + ); + } catch (cause) { + failures.push(cause); + } + const closed = await client.close(); + cleaned = closed.ok; + if (!closed.ok) failures.push(closed.error); + if (failures.length > 0) + throw new AggregateError( + failures, + "No-return Evidence verification failed", + ); + assert.equal(digest(await readFile(targetPath)), targetDigest); + report = { + status: "passed", + probe, + observations, + scope: + "Real Ghidra ELF analysis with injected flags; real read-only TCP Evidence projection. No target execution; exact Amethyst artifact and Windows are unverified.", + }; +} finally { + if (cleaned) await rm(workspace, { recursive: true, force: true }); +} +console.log( + JSON.stringify({ + ...report, + cleanup: "complete", + verifier_run: await completeVerifierRun(run), + }), +); diff --git a/src/ghidra/GhidraAnalysisProfile.ts b/src/ghidra/GhidraAnalysisProfile.ts index ed0a2f734..4cd801ad0 100644 --- a/src/ghidra/GhidraAnalysisProfile.ts +++ b/src/ghidra/GhidraAnalysisProfile.ts @@ -84,6 +84,10 @@ export const resolveGhidraAnalysisProfile = ( location_resolution: "explicit-address-exact-entry-symbol-first-v3", instruction_flow_evidence: "decoded-return-pcode-v1", function_boundary_observations: "ghidra-terminal-call-limitations-v1", + no_return_repair: + installation.platform === "win32" + ? "disabled-windows-p0" + : "returning-imports-decoded-return-v2", process_launch: installation.platform === "win32" ? "official-headless-script-v1" diff --git a/tests/conformance/ghidra/README.md b/tests/conformance/ghidra/README.md index 8f9148d4b..bb4ae4cac 100644 --- a/tests/conformance/ghidra/README.md +++ b/tests/conformance/ghidra/README.md @@ -14,6 +14,15 @@ binaries and Ghidra projects are never committed. x86-64 PE, and x86-64 Mach-O targets from the same semantics. It preserves an exported entry, direct and indirect calls, a volatile string reference, and a multi-block branch across loaders. +- `no-return.c` and `ReaNoReturnProbe.java` inject incorrect import flags into a + real ELF analysis database, verify caller recovery, propagated flag repair and signature preservation, + and retain genuine/unknown no-return calls and independently identified entries. + Run `npm run build:cached`, then `node scripts/verify-real-ghidra-noreturn.mjs` + with the Ghidra/JDK environment below on Linux x64 and a procps-compatible process + table that exposes REA's current process for owned cleanup. It also checks named + limitations through the real read-only TCP bridge and provider Evidence + projection; default Unix-socket transport, Windows, and the reported Amethyst + artifact need separate verification. Fixture target code is never executed. `scripts/verify-real-ghidra.mjs` uses `cc`, `clang`, and `lld-link` by default; `REA_CC`, `REA_CLANG`, and `REA_LLD_LINK` can select alternate commands. The diff --git a/tests/conformance/ghidra/ReaNoReturnProbe.java b/tests/conformance/ghidra/ReaNoReturnProbe.java new file mode 100644 index 000000000..970cf99a7 --- /dev/null +++ b/tests/conformance/ghidra/ReaNoReturnProbe.java @@ -0,0 +1,179 @@ +//@category REA Verification + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import generic.jar.ResourceFile; +import ghidra.app.script.GhidraScript; +import ghidra.app.script.GhidraScriptUtil; +import ghidra.framework.Application; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSet; +import ghidra.program.model.listing.FlowOverride; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.Instruction; +import ghidra.program.model.symbol.SourceType; +import java.io.File; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.security.MessageDigest; +import java.util.HexFormat; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Injects the reported analysis flags into real imported ELF functions; never executes ELF code. */ +public class ReaNoReturnProbe extends GhidraScript { + private final JsonArray checks = new JsonArray(); + private final Map originalEnds = new LinkedHashMap<>(); + private final Map originalSignatures = new LinkedHashMap<>(); + + private void require(String name, boolean condition) { + if (!condition) throw new IllegalStateException("No-return assertion failed: " + name); + checks.add(name); + } + + private Function function(String name) { + for (Function f : currentProgram.getFunctionManager().getFunctions(true)) + if (!f.isThunk() && f.getName().equals(name)) return f; + throw new IllegalStateException("Missing fixture function " + name); + } + + private Function external(String name) { + for (Function f : currentProgram.getFunctionManager().getExternalFunctions()) + if (f.getName().equals(name)) return f; + throw new IllegalStateException("Missing imported function " + name); + } + + private void truncate(Function caller, String callee) throws Exception { + truncate(caller, external(callee)); + } + + private void truncate(Function caller, Function callee) throws Exception { + for (Instruction ins : currentProgram.getListing().getInstructions(caller.getBody(), true)) { + if (!ins.getFlowType().isCall()) continue; + Address[] flows = ins.getFlows(); + if (flows.length != 1) continue; + Function target = currentProgram.getFunctionManager().getFunctionAt(flows[0]); + if (target == null) continue; + Function root = target.isThunk() ? target.getThunkedFunction(true) : target; + if (root == null || !root.equals(callee)) continue; + originalEnds.put(caller, caller.getBody().getMaxAddress()); + originalSignatures.put(caller, caller.getPrototypeString(false, true)); + callee.setNoReturn(true); + ins.setFlowOverride(FlowOverride.CALL_RETURN); + Address end = ins.getMaxAddress(); + clearListing(end.add(1), originalEnds.get(caller)); + caller.setBody(new AddressSet(caller.getEntryPoint(), end)); + require("truncated_" + callee.getName(), ins.getFlowType().isTerminal()); + return; + } + throw new IllegalStateException("Missing fixture call to " + callee.getName()); + } + + 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()); + return instance; + } + + @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(); + Map imports = Map.of( + "rea_malloc", "malloc", "rea_tls", "__tls_get_addr", + "rea_memcpy", "memcpy", "rea_memset", "memset", + "rea_strlen", "strlen", "rea_errno", "__errno_location"); + for (var item : imports.entrySet()) truncate(function(item.getKey()), item.getValue()); + truncate(function("rea_tls_chain"), function("rea_tls")); + truncate(function("rea_tls_outer"), function("rea_tls_chain")); + function("rea_tls_outer").setNoReturn(true); + Function adjacentCaller = function("rea_malloc_adjacent"); + truncate(adjacentCaller, "malloc"); + Address adjacentEntry = adjacentCaller.getBody().getMaxAddress().add(1); + Function adjacent = currentProgram.getFunctionManager().createFunction( + "rea_independent_entry", adjacentEntry, + new AddressSet(adjacentEntry, originalEnds.get(adjacentCaller)), SourceType.USER_DEFINED); + AddressSet adjacentBody = new AddressSet(adjacent.getBody()); + truncate(function("rea_unknown"), "rea_unknown_stop"); + external("abort").setNoReturn(true); + external("_Unwind_Resume").setNoReturn(true); + Function localStop = function("rea_local_stop"); + localStop.setParentNamespace(currentProgram.getSymbolTable().createNameSpace( + currentProgram.getGlobalNamespace(), "REA_LOCAL_CONTROL", SourceType.USER_DEFINED)); + localStop.setName("malloc", SourceType.USER_DEFINED); + localStop.setNoReturn(true); + Function localReturn = function("rea_local_return"); + localReturn.setNoReturn(true); + + GhidraScript fix = script("ReaGhidraNoReturnFix.java", new File(directory, "ReaGhidraNoReturnFix.java")); + Method repair = fix.getClass().getMethod("run"); + repair.invoke(fix); + for (String name : List.of("rea_tls", "rea_tls_chain", "rea_tls_outer")) { + Function caller = function(name); + require("propagated_flag_repaired_" + name, !caller.hasNoReturn()); + require("propagated_body_recovered_" + name, caller.getBody().contains(originalEnds.get(caller))); + require("propagated_signature_preserved_" + name, caller.getPrototypeString(false, true).equals(originalSignatures.get(caller))); + } + for (var item : imports.entrySet()) { + Function caller = function(item.getKey()); + require("returning_import_" + item.getValue(), !external(item.getValue()).hasNoReturn()); + require("recovered_body_" + item.getValue(), caller.getBody().contains(originalEnds.get(caller))); + require("preserved_signature_" + item.getValue(), caller.getPrototypeString(false, true).equals(originalSignatures.get(caller))); + } + require("abort_preserved", external("abort").hasNoReturn()); + require("unwind_preserved", external("_Unwind_Resume").hasNoReturn()); + require("unknown_import_preserved", external("rea_unknown_stop").hasNoReturn()); + require("local_name_not_whitelisted", localStop.hasNoReturn()); + require("local_return_repaired", !localReturn.hasNoReturn()); + require("independent_entry_preserved", adjacent.getBody().equals(adjacentBody) && + !adjacentCaller.getBody().contains(adjacentEntry)); + + GhidraScript bridge = script("ReaGhidraBridge.java", new File(directory, "ReaGhidraBridge.java")); + Method warnings = bridge.getClass().getDeclaredMethod("noReturnCallLimitations", Iterable.class); + warnings.setAccessible(true); + Object reported = warnings.invoke(bridge, currentProgram.getListing().getInstructions(function("rea_unknown").getBody(), true)); + require("unknown_boundary_named", reported instanceof List values && + values.stream().anyMatch(value -> value.toString().contains("rea_unknown_stop") && value.toString().contains("fallthrough"))); + Object healthy = warnings.invoke(bridge, currentProgram.getListing().getInstructions(function("rea_tls").getBody(), true)); + require("healthy_boundary_no_warning", healthy instanceof List values && values.isEmpty()); + Object guarded = warnings.invoke(bridge, currentProgram.getListing().getInstructions(adjacentCaller.getBody(), true)); + require("unrepaired_boundary_named", guarded instanceof List values && + values.stream().anyMatch(value -> value.toString().contains("malloc"))); + Method initialize = bridge.getClass().getDeclaredMethod("initializeDecompiler"); + initialize.setAccessible(true); + initialize.invoke(bridge); + Method pseudocode = bridge.getClass().getDeclaredMethod("procedurePseudocode", JsonObject.class); + pseudocode.setAccessible(true); + JsonObject query = new JsonObject(); + query.addProperty("document", currentProgram.getName()); + query.addProperty("procedure", "rea_tls"); + var field = bridge.getClass().getDeclaredField("decompiler"); + field.setAccessible(true); + try { + Object output = pseudocode.invoke(bridge, query); + require("recovered_pseudocode", output instanceof JsonObject value && + value.get("value").getAsString().contains("rea_after_call")); + } finally { + ((ghidra.app.decompiler.DecompInterface) field.get(bridge)).dispose(); + } + Map bodies = new LinkedHashMap<>(); + for (var item : imports.entrySet()) bodies.put(function(item.getKey()), new AddressSet(function(item.getKey()).getBody())); + repair.invoke(fix); + require("idempotent_body_repair", bodies.entrySet().stream().allMatch(item -> item.getValue().equals(item.getKey().getBody()))); + JsonObject report = new JsonObject(); + report.addProperty("status", "passed"); + report.addProperty("ghidra_version", Application.getApplicationVersion()); + report.addProperty("target_sha256", currentProgram.getExecutableSHA256()); + for (String name : List.of("ReaGhidraBridge.java", "ReaGhidraNoReturnFix.java")) { + byte[] bytes = Files.readAllBytes(new File(directory, name).toPath()); + report.addProperty(name, HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes))); + } + report.add("checks", checks); + println("REA_NO_RETURN_PROBE_JSON " + report); + } +} diff --git a/tests/conformance/ghidra/no-return.c b/tests/conformance/ghidra/no-return.c new file mode 100644 index 000000000..a2f270281 --- /dev/null +++ b/tests/conformance/ghidra/no-return.c @@ -0,0 +1,62 @@ +#include +#include +#include + +extern void *__tls_get_addr(void *); +extern void rea_unknown_stop(void); +extern void _Unwind_Resume(void *) __attribute__((noreturn)); +volatile unsigned long rea_after_call; + +void *rea_malloc(size_t n) { + void *p = malloc(n); + rea_after_call++; + return p; +} +void *rea_malloc_adjacent(size_t n) { + void *p = malloc(n); + rea_after_call++; + return p; +} +__attribute__((visibility("hidden"))) void *rea_tls(void *p) { + void *value = __tls_get_addr(p); + rea_after_call++; + return value; +} +__attribute__((visibility("hidden"))) void *rea_tls_chain(void *p) { + void *value = rea_tls(p); + rea_after_call++; + return value; +} +void *rea_tls_outer(void *p) { + void *value = rea_tls_chain(p); + rea_after_call++; + return value; +} +void *rea_memcpy(void *out, const void *in, size_t n) { + void *value = memcpy(out, in, n); + rea_after_call++; + return value; +} +void *rea_memset(void *out, int c, size_t n) { + void *value = memset(out, c, n); + rea_after_call++; + return value; +} +size_t rea_strlen(const char *p) { + size_t value = strlen(p); + rea_after_call++; + return value; +} +int *rea_errno(void) { + int *p = __errno_location(); + rea_after_call++; + return p; +} +void rea_unknown(void) { + rea_unknown_stop(); + rea_after_call++; +} +void rea_abort(void) { abort(); } +void rea_unwind(void *p) { _Unwind_Resume(p); } +void rea_local_stop(void) { __builtin_trap(); } +int rea_local_return(void) { return 1658; }