// REA's packaged, read-only Ghidra headless bridge. // @category REA import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.math.BigInteger; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.StandardProtocolFamily; import java.net.UnixDomainSocketAddress; import java.nio.channels.Channels; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.nio.charset.StandardCharsets; import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.PosixFilePermissions; import java.security.MessageDigest; import java.util.ArrayList; import java.util.Comparator; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import ghidra.app.util.headless.HeadlessScript; import ghidra.app.decompiler.DecompInterface; import ghidra.app.decompiler.DecompileOptions; import ghidra.app.decompiler.DecompileResults; import ghidra.app.decompiler.DecompiledFunction; import ghidra.app.decompiler.ClangCaseToken; import ghidra.app.decompiler.ClangNode; import ghidra.framework.Application; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressSpace; import ghidra.program.model.address.AddressRange; import ghidra.program.model.address.AddressRangeIterator; import ghidra.program.model.block.BasicBlockModel; import ghidra.program.model.block.CodeBlock; import ghidra.program.model.block.CodeBlockIterator; import ghidra.program.model.block.CodeBlockReference; import ghidra.program.model.block.CodeBlockReferenceIterator; 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.CommentType; import ghidra.program.model.listing.Data; import ghidra.program.model.listing.DataIterator; import ghidra.program.model.listing.Function; import ghidra.program.model.listing.FunctionIterator; import ghidra.program.model.listing.Instruction; import ghidra.program.model.listing.InstructionIterator; import ghidra.program.model.listing.Variable; import ghidra.program.model.mem.MemoryBlock; import ghidra.program.model.mem.MemoryBlockSourceInfo; import ghidra.program.database.mem.FileBytes; import ghidra.program.model.address.SegmentedAddress; import ghidra.program.model.reloc.Relocation; import java.io.ByteArrayOutputStream; import ghidra.program.model.symbol.RefType; import ghidra.program.model.symbol.Reference; import ghidra.program.model.symbol.ReferenceIterator; import ghidra.program.model.symbol.SourceType; import ghidra.program.model.symbol.Symbol; import ghidra.program.model.symbol.SymbolIterator; import ghidra.program.model.pcode.FunctionPrototype; import ghidra.program.model.pcode.HighFunction; import ghidra.program.model.pcode.HighParam; import ghidra.program.model.pcode.HighSymbol; import ghidra.program.model.pcode.JumpTable; import ghidra.program.model.pcode.PcodeOp; import ghidra.program.model.pcode.PcodeOpAST; import ghidra.program.model.pcode.PcodeBlock; import ghidra.program.model.pcode.PcodeBlockBasic; import ghidra.program.model.pcode.Varnode; import ghidra.program.model.pcode.VarnodeAST; public final class ReaGhidraBridge extends HeadlessScript { private static final int MAX_HIGH_PCODE_OPS = 3000; private static final int MAX_HIGH_PCODE_INPUTS_PER_OP = 64; private static final int MAX_HIGH_PCODE_DEF_USE_EDGES = 12000; private static final Gson GSON = new GsonBuilder().serializeNulls().create(); private static final Set DESCRIPTOR_KEYS = Set.of( "transport", "endpoint_path", "token", "run_id", "target_sha256", "provider_version", "profile_digest" ); private static final Set REQUEST_KEYS = Set.of( "id", "token", "method", "params" ); private static final String[] CAPABILITIES = { "annotate_native_function", "inspect_native_load_image", "read_bytes", "address_to_file_offset", "ping", "shutdown", "address_name", "list_documents", "list_names", "list_procedures", "list_segments", "list_strings", "procedure_address", "resolve_containing_procedure", "search_procedures", "search_strings", "analyze_function", "procedure_assembly", "procedure_callees", "procedure_callers", "procedure_info", "procedure_pseudo_code", "read_function_instructions", "inspect_native_instruction", "inspect_native_data_type", "resolve_native_call_targets", "procedure_references", "xrefs" }; private List nameInventory; private List procedureInventory; private List stringInventory; private DecompInterface decompiler; private final ReaGhidraExtensions analysisExtensions = new ReaGhidraExtensions(); private static AddressSpace sessionDefaultAddressSpace; @Override public void run() throws Exception { String[] arguments = getScriptArgs(); if (arguments.length != 1) { throw new IllegalArgumentException("REA bridge requires one session descriptor"); } Path descriptorPath = Path.of(arguments[0]); SessionDescriptor descriptor = readDescriptor(descriptorPath); // Windows retains a native immutable bearer descriptor lease until // runtime_close; POSIX removes it after this read. Normal cleanup removes // the Windows file, while abrupt owner death can leave private residue. // Its protected runtime DACL also protects the bearer token while the // bridge is running. POSIX keeps its existing consume-and-delete flow. if (!isWindowsHost()) { Files.deleteIfExists(descriptorPath); } if (currentProgram == null) { throw new IllegalStateException("REA bridge requires an imported program"); } sessionDefaultAddressSpace = currentProgram.getAddressFactory().getDefaultAddressSpace(); if (currentProgram.getExecutableFormat().equals("Raw Binary") && !currentProgram.getOptions(ghidra.program.model.listing.Program.PROGRAM_INFO) .getBoolean("REA DOS COM prepared", false)) { throw new IllegalStateException("REA DOS COM entry/context preparation failed"); } if (!Application.getApplicationVersion().equals(descriptor.providerVersion)) { throw new IllegalStateException("Ghidra provider version does not match the session"); } String importedSha256 = currentProgram.getExecutableSHA256(); if (importedSha256 == null || !constantTimeEquals( importedSha256.toLowerCase(Locale.ROOT), descriptor.targetSha256 )) { throw new IllegalStateException( "Ghidra imported-byte digest does not match the admitted target" ); } // GhidraScript wraps run() in a transaction. End it before serving so // each edit owns a real outer transaction and can roll back immediately. end(true); try { if (!descriptor.transport.equals("unix-socket") && descriptor.analysisExtensions.size() > 0) throw new IOException("Windows Ghidra P0 does not admit metadata recovery or database mutation."); analysisExtensions.analyze(descriptor.analysisExtensions, currentProgram, monitor); initializeDecompiler(); serve(descriptor); } finally { if (decompiler != null) { decompiler.dispose(); decompiler = null; } analysisExtensions.close(); } } private void serve(SessionDescriptor descriptor) throws Exception { if (descriptor.transport.equals("unix-socket")) { serveUnixSocket(descriptor); return; } if (descriptor.transport.equals("authenticated-loopback-tcp")) { serveLoopbackTcp(descriptor); return; } throw new IllegalArgumentException("REA bridge transport is invalid"); } private void serveUnixSocket(SessionDescriptor descriptor) throws Exception { Path socketPath = Path.of(descriptor.endpointPath); Files.deleteIfExists(socketPath); try (ServerSocketChannel server = ServerSocketChannel.open(StandardProtocolFamily.UNIX)) { server.bind(UnixDomainSocketAddress.of(socketPath)); Files.setPosixFilePermissions(socketPath, PosixFilePermissions.fromString("rw-------")); acceptClient(server, descriptor); } finally { Files.deleteIfExists(socketPath); } } private void serveLoopbackTcp(SessionDescriptor descriptor) throws Exception { Path endpointPath = Path.of(descriptor.endpointPath); Path pendingPath = endpointPath.resolveSibling(endpointPath.getFileName() + ".pending"); Files.deleteIfExists(endpointPath); Files.deleteIfExists(pendingPath); try (ServerSocketChannel server = ServerSocketChannel.open()) { server.bind(new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 0)); InetSocketAddress address = (InetSocketAddress) server.getLocalAddress(); JsonObject endpoint = new JsonObject(); endpoint.addProperty("host", "127.0.0.1"); endpoint.addProperty("port", address.getPort()); // The Windows native reader refuses a file while a writer still // owns it. Closing CREATE_NEW publishes the complete record without // requesting rename rights on the retained private directory guard. Files.writeString( isWindowsHost() ? endpointPath : pendingPath, GSON.toJson(endpoint) + "\n", StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE ); if (!isWindowsHost()) { try { Files.move(pendingPath, endpointPath, StandardCopyOption.ATOMIC_MOVE); } catch (AtomicMoveNotSupportedException exception) { Files.move(pendingPath, endpointPath); } } acceptClient(server, descriptor); } finally { Files.deleteIfExists(pendingPath); Files.deleteIfExists(endpointPath); } } private static boolean isWindowsHost() { return System.getProperty("os.name", "").startsWith("Windows"); } private void acceptClient( ServerSocketChannel server, SessionDescriptor descriptor) throws Exception { try (SocketChannel client = server.accept(); BufferedReader reader = new BufferedReader( Channels.newReader(client, StandardCharsets.UTF_8) ); BufferedWriter writer = new BufferedWriter( Channels.newWriter(client, StandardCharsets.UTF_8) )) { serveClient(descriptor, reader, writer); } } private void serveClient( SessionDescriptor descriptor, BufferedReader reader, BufferedWriter writer) throws Exception { while (true) { String line = reader.readLine(); if (line == null) { return; } Request request; try { request = parseRequest(line, descriptor.token); } catch (RuntimeException exception) { writeFailure(writer, 1, "invalid_request", "Bridge request is invalid"); return; } try { if (request.method.equals("shutdown")) { requireKeys(request.params, Set.of()); JsonObject result = new JsonObject(); result.addProperty("shutdown", true); result.addProperty("project_ephemeral", true); writeSuccess(writer, request.id, result); return; } writeSuccess(writer, request.id, handleRequest(request, descriptor)); } catch (RequestFailure failure) { writeFailure(writer, request.id, failure.code, failure.getMessage()); } catch (Exception exception) { writeFailure( writer, request.id, "bridge_exception", exception.getClass().getSimpleName() + ": " + safeMessage(exception) ); } } } private JsonElement handleRequest(Request request, SessionDescriptor descriptor) throws Exception { if (request.method.equals("ping")) { requireKeys(request.params, Set.of()); return sessionInfo(descriptor); } if (analysisTimeoutOccurred()) { throw new RequestFailure( "analysis_incomplete", "Ghidra auto-analysis did not complete for this Program" ); } return switch (request.method) { case "inspect_native_load_image" -> inspectNativeLoadImage(request.params); case "read_bytes" -> readMemoryBytes(request.params); case "address_to_file_offset" -> addressToFileOffset(request.params); case "address_name" -> addressName(request.params); case "list_documents" -> listDocuments(request.params); case "list_names" -> listNames(request.params); case "list_procedures" -> listProcedures(request.params); case "list_segments" -> listSegments(request.params); case "list_strings" -> listStrings(request.params); case "procedure_address" -> procedureAddress(request.params); case "procedure_assembly" -> procedureAssembly(request.params); case "procedure_callees" -> procedureCalls(request.params, false); case "procedure_callers" -> procedureCalls(request.params, true); case "procedure_info" -> procedureInfo(request.params); case "procedure_pseudo_code" -> procedurePseudocode(request.params); case "read_function_instructions" -> readFunctionInstructions(request.params); case "procedure_references" -> procedureReferences(request.params); case "resolve_containing_procedure" -> containingProcedure(request.params); case "search_procedures" -> search(request.params, true); case "search_strings" -> search(request.params, false); case "inspect_native_data_type" -> inspectNativeDataType(request.params); case "inspect_native_instruction" -> inspectNativeInstruction(request.params); case "resolve_native_call_targets" -> resolveNativeCallTargets(request.params); case "xrefs" -> xrefs(request.params); case "analyze_function" -> analyzeFunction(request.params); case "annotate_native_function" -> { if (!descriptor.transport.equals("unix-socket")) throw new RequestFailure("method_unavailable", "Windows P0 does not admit database mutation"); yield annotateNativeFunction(request.params); } default -> throw new RequestFailure( "method_unavailable", "Bridge method is unavailable" ); }; } private JsonObject sessionInfo(SessionDescriptor descriptor) throws Exception { boolean timedOut = analysisTimeoutOccurred(); JsonObject provider = new JsonObject(); provider.addProperty("id", "ghidra"); provider.addProperty("version", Application.getApplicationVersion()); JsonObject target = new JsonObject(); target.addProperty("name", currentProgram.getName()); target.addProperty("language_id", currentProgram.getLanguageID().getIdAsString()); target.addProperty( "compiler_spec_id", currentProgram.getCompilerSpec().getCompilerSpecID().getIdAsString() ); target.addProperty("image_base", canonicalAddress(currentProgram.getImageBase())); target.addProperty( "default_address_space", currentProgram.getAddressFactory().getDefaultAddressSpace().getName() ); target.addProperty("sha256", descriptor.targetSha256); JsonObject result = new JsonObject(); result.addProperty("name", "REA Ghidra bridge"); result.addProperty("run_id", descriptor.runId); result.addProperty("profile_digest", descriptor.profileDigest); result.add("provider", provider); boolean readOnly = !descriptor.transport.equals("unix-socket"); result.addProperty("read_only", readOnly); result.addProperty("analysis_complete", !timedOut); result.addProperty("analysis_timed_out", timedOut); result.add("capabilities", GSON.toJsonTree( java.util.Arrays.stream(CAPABILITIES) .filter(value -> !readOnly || !value.equals("annotate_native_function")) .toArray(String[]::new) )); result.add("target", target); JsonArray extensions = analysisExtensions.reports(); if (extensions.size() > 0) result.add("analysis_extensions", extensions); return result; } private static String bytesHex(byte[] bytes) { return java.util.HexFormat.of().formatHex(bytes); } private String hashFileBytes(FileBytes source, boolean modified) throws Exception { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] chunk = new byte[65536]; for (long offset = 0; offset < source.getSize();) { monitor.checkCancelled(); int length = (int) Math.min(chunk.length, source.getSize() - offset); int read = modified ? source.getModifiedBytes(offset, chunk, 0, length) : source.getOriginalBytes(offset, chunk, 0, length); if (read != length) throw new IOException("Incomplete FileBytes read at " + offset); digest.update(chunk, 0, read); offset += read; } return bytesHex(digest.digest()); } private String hashMemory(Address start, long length) throws Exception { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] chunk = new byte[65536]; for (long offset = 0; offset < length;) { monitor.checkCancelled(); int count = (int) Math.min(chunk.length, length - offset); int read = currentProgram.getMemory().getBytes(start.add(offset), chunk, 0, count); if (read != count) throw new IOException("Incomplete memory read at " + start.add(offset)); digest.update(chunk, 0, read); offset += read; } return bytesHex(digest.digest()); } private JsonObject inspectNativeLoadImage(JsonObject params) throws Exception { requireKeys(params, Set.of()); JsonObject result = new JsonObject(); JsonArray recovery = analysisExtensions.inspectImage(); if (recovery.size() > 0) result.add("metadata_recovery", recovery); result.addProperty("executable_format", currentProgram.getExecutableFormat()); result.addProperty("language_id", currentProgram.getLanguageID().getIdAsString()); result.addProperty("compiler_spec_id", currentProgram.getCompilerSpec().getCompilerSpecID().getIdAsString()); result.addProperty("image_base", canonicalAddress(currentProgram.getImageBase())); result.addProperty("default_address_space", sessionDefaultAddressSpace.getName()); List files = currentProgram.getMemory().getAllFileBytes(); JsonArray sources = new JsonArray(); for (FileBytes file : files) { JsonObject source = new JsonObject(); source.addProperty("name", file.getFilename()); source.addProperty("size", file.getSize()); source.addProperty("original_sha256", hashFileBytes(file, false)); source.addProperty("modified_sha256", hashFileBytes(file, true)); sources.add(source); } result.add("source_files", sources); JsonArray mappings = new JsonArray(); for (MemoryBlock block : currentProgram.getMemory().getBlocks()) { for (MemoryBlockSourceInfo source : block.getSourceInfos()) { monitor.checkCancelled(); JsonObject mapping = new JsonObject(); mapping.addProperty("block", block.getName()); mapping.addProperty("start", canonicalAddress(source.getMinAddress())); mapping.addProperty("end", canonicalAddress(source.getMaxAddress())); mapping.addProperty("address_space", source.getMinAddress().getAddressSpace().getName()); mapping.addProperty("initialized", block.isInitialized()); mapping.addProperty("loaded", block.isLoaded()); mapping.addProperty("overlay", block.isOverlay()); mapping.addProperty("length", source.getLength()); FileBytes backing = source.getFileBytes().orElse(null); int sourceIndex = backing == null ? -1 : files.indexOf(backing); if (sourceIndex < 0) mapping.add("source_file_index", JsonNull.INSTANCE); else mapping.addProperty("source_file_index", sourceIndex); long offset = source.getFileBytesOffset(); if (offset < 0) mapping.add("file_offset", JsonNull.INSTANCE); else mapping.addProperty("file_offset", offset); if (block.isInitialized()) mapping.addProperty("sha256", hashMemory(source.getMinAddress(), source.getLength())); else mapping.add("sha256", JsonNull.INSTANCE); mappings.add(mapping); } } result.add("mappings", mappings); JsonArray relocations = new JsonArray(); Iterator iterator = currentProgram.getRelocationTable().getRelocations(); while (iterator.hasNext()) { monitor.checkCancelled(); Relocation relocation = iterator.next(); Address location = relocation.getAddress(); JsonObject row = new JsonObject(); row.addProperty("address", canonicalAddress(location)); if (location instanceof SegmentedAddress segmented) { row.addProperty("segment", segmented.getSegment()); row.addProperty("offset", segmented.getSegmentOffset()); } else { row.add("segment", JsonNull.INSTANCE); row.add("offset", JsonNull.INSTANCE); } row.addProperty("status", relocation.getStatus().toString()); row.addProperty("type", relocation.getType()); row.add("values", GSON.toJsonTree(relocation.getValues() == null ? new long[0] : relocation.getValues())); byte[] original = relocation.getBytes(); row.add("original_bytes_hex", original == null ? JsonNull.INSTANCE : GSON.toJsonTree(bytesHex(original))); if (original != null && original.length > 0) { byte[] memory = new byte[original.length]; int read = currentProgram.getMemory().getBytes(location, memory); row.add("memory_bytes_hex", read == memory.length ? GSON.toJsonTree(bytesHex(memory)) : JsonNull.INSTANCE); } else row.add("memory_bytes_hex", JsonNull.INSTANCE); relocations.add(row); } result.add("relocations", relocations); List
entries = new ArrayList<>(); var entryIterator = currentProgram.getSymbolTable().getExternalEntryPointIterator(); while (entryIterator.hasNext()) entries.add(entryIterator.next()); entries.sort(Address::compareTo); JsonArray entryPoints = new JsonArray(); for (Address entry : entries) entryPoints.add(canonicalAddress(entry)); result.add("entry_points", entryPoints); JsonArray contexts = new JsonArray(); for (Address entry : entries) { JsonObject context = new JsonObject(); context.addProperty("address", canonicalAddress(entry)); JsonArray registers = new JsonArray(); for (String name : new String[] { "CS", "DS", "ES", "SS" }) { Register register = currentProgram.getRegister(name); if (register == null) continue; JsonObject row = new JsonObject(); row.addProperty("name", name); BigInteger value = currentProgram.getProgramContext().getValue(register, entry, false); row.add("value_hex", value == null ? JsonNull.INSTANCE : GSON.toJsonTree("0x" + value.toString(16))); registers.add(row); } context.add("registers", registers); contexts.add(context); } result.add("entry_context", contexts); return result; } private JsonObject readMemoryBytes(JsonObject params) throws Exception { requireKeys(params, Set.of("document", "address", "length")); requireDocument(params); Address start = requireAddress(params, "address"); JsonElement requested = params.get("length"); long length; try { if (requested == null || !requested.isJsonPrimitive() || !requested.getAsJsonPrimitive().isNumber()) throw new IllegalArgumentException(); length = requested.getAsBigDecimal().longValueExact(); if (length < 1 || length > 9007199254740991L) throw new IllegalArgumentException(); } catch (RuntimeException failure) { throw new RequestFailure("invalid_request", "Byte length must be a positive safe integer"); } ByteArrayOutputStream bytes = new ByteArrayOutputStream(); byte[] chunk = new byte[65536]; for (long offset = 0; offset < length;) { monitor.checkCancelled(); Address at; try { at = start.addNoWrap(offset); } catch (Exception endOfSpace) { break; } MemoryBlock block = currentProgram.getMemory().getBlock(at); if (block == null || !block.isInitialized()) break; int count = (int) Math.min(chunk.length, Math.min(length - offset, block.getEnd().subtract(at) + 1)); int read = currentProgram.getMemory().getBytes(at, chunk, 0, count); bytes.write(chunk, 0, read); if (read != count) break; offset += read; } JsonObject result = new JsonObject(); result.addProperty("address", canonicalAddress(start)); result.addProperty("requested_bytes", length); result.addProperty("returned_bytes", bytes.size()); result.addProperty("bytes_hex", bytesHex(bytes.toByteArray())); result.addProperty("complete", bytes.size() == length); return result; } private JsonObject addressToFileOffset(JsonObject params) { requireKeys(params, Set.of("document", "address")); requireDocument(params); Address address = requireAddress(params, "address"); MemoryBlock block = currentProgram.getMemory().getBlock(address); if (block == null || !block.isInitialized()) throw new RequestFailure("invalid_request", "Address has no initialized file-backed memory: " + canonicalAddress(address)); Long fileOffset = null; int mappings = 0; for (MemoryBlockSourceInfo source : block.getSourceInfos()) { if (!source.contains(address) || source.getFileBytes().isEmpty()) continue; long offset = source.getFileBytesOffset(address); if (offset < 0) continue; fileOffset = offset; mappings++; } if (mappings != 1) throw new RequestFailure("invalid_request", "Address has " + mappings + " authoritative source mappings: " + canonicalAddress(address)); JsonObject result = new JsonObject(); result.addProperty("address", canonicalAddress(address)); result.addProperty("file_offset", fileOffset); return result; } private void initializeDecompiler() { DecompileOptions options = new DecompileOptions(); options.setMaxInstructions(Integer.MAX_VALUE); decompiler = new DecompInterface(); decompiler.setOptions(options); decompiler.toggleCCode(true); decompiler.toggleSyntaxTree(true); decompiler.toggleJumpLoads(true); if (!decompiler.openProgram(currentProgram)) { throw new IllegalStateException("Ghidra decompiler could not open the imported Program"); } } private JsonElement procedurePseudocode(JsonObject params) throws Exception { requireKeys(params, Set.of("document", "procedure")); requireDocument(params); String value = decompiledText( decompile(resolveProcedure(requireString(params, "procedure"))) ); return value == null ? JsonNull.INSTANCE : GSON.toJsonTree(value); } private JsonElement procedureAssembly(JsonObject params) throws Exception { requireKeys(params, Set.of("document", "procedure")); requireDocument(params); Function function = resolveProcedure(requireString(params, "procedure")); InstructionScan scan = scanInstructions(function, Integer.MAX_VALUE); return GSON.toJsonTree(renderAssembly(scan.instructions)); } private JsonObject readFunctionInstructions(JsonObject params) throws Exception { requireKeys(params, Set.of("document", "procedure")); requireDocument(params); Function function = resolveProcedure(requireString(params, "procedure")); InstructionScan scan = scanInstructions(function, Integer.MAX_VALUE); JsonArray items = new JsonArray(); for (String line : renderAssemblyLines(scan.instructions)) { items.add(line); } JsonArray limitations = new JsonArray(); limitations.add("Instruction text and ordering are Ghidra-specific representations."); limitations.add( "The fast path does not invoke the decompiler or scan whole-program names and strings." ); JsonObject result = new JsonObject(); result.add("procedure", procedureIdentity(function)); result.add("instructions", items); result.add("limitations", limitations); return result; } private JsonArray procedureCalls(JsonObject params, boolean callers) throws Exception { requireKeys(params, Set.of("document", "procedure")); requireDocument(params); Function function = resolveProcedure(requireString(params, "procedure")); Set observed = callers ? function.getCallingFunctions(monitor) : function.getCalledFunctions(monitor); List ordered = new ArrayList<>(observed); ordered.sort(Comparator.comparing(Function::getEntryPoint)); JsonArray result = new JsonArray(); for (Function item : ordered) { result.add(canonicalAddress(item.getEntryPoint())); } return result; } private JsonObject procedureInfo(JsonObject params) throws Exception { requireKeys(params, Set.of("document", "procedure")); requireDocument(params); Function function = resolveProcedure(requireString(params, "procedure")); JsonObject result = new JsonObject(); result.addProperty("name", procedureName(function)); result.addProperty("entrypoint", canonicalAddress(function.getEntryPoint())); result.addProperty("basicblock_count", basicBlocks(function).size()); JsonObject body = functionBody(function); result.addProperty("length", body.get("total_bytes").getAsLong()); result.add("body", body); result.addProperty("signature", function.getPrototypeString(false, true)); result.add("locals", functionLocals(function)); result.add("classification", functionClassification(function)); return result; } private JsonObject procedureReferences(JsonObject params) throws Exception { requireKeys( params, Set.of( "document", "procedure", "direction" ) ); requireDocument(params); Function function = resolveProcedure(requireString(params, "procedure")); String direction = requireString(params, "direction"); if (!direction.equals("incoming") && !direction.equals("outgoing")) { throw new RequestFailure("invalid_request", "direction must be incoming or outgoing"); } InstructionScan scan = scanInstructions(function, Integer.MAX_VALUE); List references = collectReferences(scan.instructions, direction); JsonArray edges = new JsonArray(); for (Reference reference : references) { edges.add(referenceEdge(reference)); } JsonObject result = new JsonObject(); result.add("procedure", procedureIdentity(function)); result.addProperty("direction", direction); result.add("references", edges); result.addProperty("reference_kinds_available", true); JsonArray unresolvedCalls = new JsonArray(); if (direction.equals("outgoing")) for (Instruction instruction : scan.instructions) { if (!instruction.getFlowType().isCall()) continue; boolean resolved = false; for (Reference reference : instruction.getReferencesFrom()) if (reference.getReferenceType().isCall()) resolved = true; if (!resolved) { JsonObject site = new JsonObject(); site.addProperty("address", canonicalAddress(instruction.getAddress())); site.addProperty("reason", "Instruction decoder identifies a call but ReferenceManager supplies no call destination"); unresolvedCalls.add(site); } } result.add("unresolved_calls", unresolvedCalls); return result; } private JsonObject inspectNativeDataType(JsonObject params) throws Exception { requireKeys(params, Set.of("document", "type", "address")); requireDocument(params); boolean byType = params.has("type") && !params.get("type").isJsonNull(); boolean byAddress = params.has("address") && !params.get("address").isJsonNull(); if (byType == byAddress) throw new RequestFailure("invalid_request", "Supply exactly one of type or address"); DataType type; Address address = null; if (byType) type = currentProgram.getDataTypeManager().getDataType(requireString(params, "type")); else { address = requireAddress(params, "address"); Data data = currentProgram.getListing().getDefinedDataAt(address); type = data == null ? null : data.getDataType(); } JsonObject result = new JsonObject(); result.addProperty("status", type == null ? "unavailable" : "available"); result.add("reason", type == null ? GSON.toJsonTree("No exact database type or defined typed data exists at the selected identity") : JsonNull.INSTANCE); for (String key : List.of("id", "name", "source_archive", "size_bytes", "alignment_bytes", "packing_enabled", "referenced_type", "array_count", "array_stride_bytes")) result.add(key, JsonNull.INSTANCE); result.add("address", address == null ? JsonNull.INSTANCE : GSON.toJsonTree(canonicalAddress(address))); result.addProperty("source", "analysis-database"); result.addProperty("kind", "unavailable"); JsonArray fields = new JsonArray(); JsonArray members = new JsonArray(); int totalFields = 0; if (type != null) { result.addProperty("id", type.getPathName()); result.addProperty("name", type.getName()); if (type.getSourceArchive() != null) result.addProperty("source_archive", type.getSourceArchive().getName()); if (type.getLength() >= 0) result.addProperty("size_bytes", type.getLength()); if (type.getAlignment() > 0) result.addProperty("alignment_bytes", type.getAlignment()); String kind = type instanceof ghidra.program.model.data.Structure ? "struct" : type instanceof ghidra.program.model.data.Union ? "union" : type instanceof ghidra.program.model.data.Enum ? "enum" : type instanceof ghidra.program.model.data.Pointer ? "pointer" : type instanceof ghidra.program.model.data.Array ? "array" : type instanceof ghidra.program.model.data.TypeDef ? "typedef" : type instanceof ghidra.program.model.data.BuiltInDataType ? "scalar" : "unsupported"; result.addProperty("kind", kind); if (type instanceof ghidra.program.model.data.Composite composite) { result.addProperty("packing_enabled", composite.isPackingEnabled()); ghidra.program.model.data.DataTypeComponent[] components = composite.getComponents(); totalFields = components.length; for (int index = 0; index < Math.min(components.length, 20_000); index++) { monitor.checkCancelled(); ghidra.program.model.data.DataTypeComponent component = components[index]; JsonObject field = new JsonObject(); field.addProperty("ordinal", component.getOrdinal()); field.add("name", component.getFieldName() == null ? JsonNull.INSTANCE : GSON.toJsonTree(component.getFieldName())); field.addProperty("offset_bytes", component.getOffset()); field.add("size_bytes", component.getLength() < 0 ? JsonNull.INSTANCE : GSON.toJsonTree(component.getLength())); field.addProperty("type_id", component.getDataType().getPathName()); field.add("bit_size", JsonNull.INSTANCE); field.add("bit_offset", JsonNull.INSTANCE); if (component.getDataType() instanceof ghidra.program.model.data.BitFieldDataType bitfield) { field.addProperty("bit_size", bitfield.getBitSize()); field.addProperty("bit_offset", bitfield.getBitOffset()); } field.addProperty("status", "observed"); field.addProperty("flexible_tail", "unknown"); fields.add(field); } } else if (type instanceof ghidra.program.model.data.Enum enumeration) { String[] names = enumeration.getNames(); totalFields = names.length; java.util.Arrays.sort(names); for (int index = 0; index < Math.min(names.length, 20_000); index++) { monitor.checkCancelled(); JsonObject member = new JsonObject(); member.addProperty("name", names[index]); member.addProperty("value", Long.toString(enumeration.getValue(names[index]))); members.add(member); } } else if (type instanceof ghidra.program.model.data.Pointer pointer) { if (pointer.getDataType() != null) result.addProperty("referenced_type", pointer.getDataType().getPathName()); } else if (type instanceof ghidra.program.model.data.Array array) { result.addProperty("referenced_type", array.getDataType().getPathName()); result.addProperty("array_count", array.getNumElements()); result.addProperty("array_stride_bytes", array.getElementLength()); } else if (type instanceof ghidra.program.model.data.TypeDef alias) { result.addProperty("referenced_type", alias.getDataType().getPathName()); } } result.add("fields", fields); result.add("members", members); result.addProperty("total_fields", totalFields); result.addProperty("truncated", totalFields > 20_000); result.add("limitations", GSON.toJsonTree(List.of("Layouts are Ghidra data-type database observations; original debug/source authority and flexible-tail semantics are not inferred. Nested and recursive types retain category-path identities for separate inspection.", "At most 20000 fields or enum members are serialized per type to bound provider output. total_fields and truncated report omissions. Enum values preserve Ghidra's signed long representation."))); if (type != null) { JsonObject metadata = analysisExtensions.inspectDataType(currentProgram, type, address, monitor); if (metadata != null) result.add("metadata_recovery", metadata); } return result; } private JsonObject instructionReference(Reference reference) { JsonObject item = new JsonObject(); RefType type = reference.getReferenceType(); item.addProperty("target_address", canonicalAddress(reference.getToAddress())); item.addProperty("type", type.getName()); item.addProperty("call", type.isCall()); item.addProperty("jump", type.isJump()); item.addProperty("indirect", type.isIndirect()); item.addProperty("computed", type.isComputed()); item.addProperty("operand_index", reference.getOperandIndex()); return item; } private JsonObject inspectNativeInstruction(JsonObject params) throws Exception { requireKeys(params, Set.of("document", "address")); requireDocument(params); Address address = requireAddress(params, "address"); Instruction instruction = currentProgram.getListing().getInstructionAt(address); Function function = currentProgram.getFunctionManager().getFunctionContaining(address); JsonObject result = new JsonObject(); result.addProperty("address", canonicalAddress(address)); result.add("procedure", function == null ? JsonNull.INSTANCE : GSON.toJsonTree(canonicalAddress(function.getEntryPoint()))); result.addProperty("architecture", currentProgram.getLanguageID().getIdAsString()); result.addProperty("mode", currentProgram.getLanguage().getLanguageDescription().getVariant()); JsonArray operands = new JsonArray(); JsonArray references = new JsonArray(); JsonArray destinations = new JsonArray(); JsonObject flow = new JsonObject(); flow.addProperty("kind", "unavailable"); flow.addProperty("conditional", false); flow.addProperty("computed", false); if (instruction == null) { result.addProperty("status", !currentProgram.getMemory().contains(address) ? "outside-memory" : currentProgram.getListing().getInstructionContaining(address) != null ? "not-instruction-boundary" : currentProgram.getListing().getDefinedDataContaining(address) != null ? "data" : "undecodable"); for (String key : List.of("bytes", "length", "mnemonic", "raw_disassembly")) result.add(key, JsonNull.INSTANCE); } else { result.addProperty("status", "decoded"); result.addProperty("bytes", java.util.HexFormat.of().formatHex(instruction.getBytes())); result.addProperty("length", instruction.getLength()); result.addProperty("mnemonic", instruction.getMnemonicString()); result.addProperty("raw_disassembly", instruction.toString()); RefType type = instruction.getFlowType(); flow.addProperty("kind", type.isCall() ? "call" : type.isJump() ? "jump" : type.isTerminal() ? "terminal" : "fallthrough"); flow.addProperty("conditional", type.isConditional()); flow.addProperty("computed", type.isComputed()); if (!type.isComputed()) for (Address target : instruction.getFlows()) destinations.add(canonicalAddress(target)); for (Reference reference : instruction.getReferencesFrom()) references.add(instructionReference(reference)); for (int index = 0; index < instruction.getNumOperands(); index++) { JsonObject operand = new JsonObject(); operand.addProperty("index", index); operand.addProperty("raw", instruction.getDefaultOperandRepresentation(index)); operand.addProperty("provider_type", instruction.getOperandType(index)); operand.addProperty("memory_addressing", "unavailable"); JsonArray components = new JsonArray(); for (Object object : instruction.getOpObjects(index)) { JsonObject component = new JsonObject(); component.addProperty("text", object.toString()); component.add("value", JsonNull.INSTANCE); component.add("bit_width", JsonNull.INSTANCE); if (object instanceof Register register) { component.addProperty("kind", "register"); component.addProperty("value", register.getName()); component.addProperty("bit_width", register.getBitLength()); } else if (object instanceof Scalar scalar) { component.addProperty("kind", "immediate"); component.addProperty("value", "0x" + Long.toUnsignedString(scalar.getUnsignedValue(), 16)); component.addProperty("bit_width", scalar.bitLength()); } else if (object instanceof Address target) { component.addProperty("kind", "address"); component.addProperty("value", canonicalAddress(target)); } else component.addProperty("kind", "unknown"); components.add(component); } operand.add("components", components); operands.add(operand); } } flow.add("direct_destinations", destinations); result.add("operands", operands); result.add("flow", flow); result.add("references", references); result.add("limitations", GSON.toJsonTree(List.of("Operand components are Ghidra Listing decoder tokens; effective memory base/index/displacement roles and per-instruction context mode are unavailable. The mode field is the program language variant.", "Terminal flow is not classified as a return without provider evidence."))); return result; } private JsonObject resolveNativeCallTargets(JsonObject params) throws Exception { JsonObject instruction = inspectNativeInstruction(params); JsonObject result = new JsonObject(); result.add("call_site", instruction.get("address")); result.add("procedure", instruction.get("procedure")); JsonArray targets = new JsonArray(); JsonObject flow = instruction.getAsJsonObject("flow"); boolean decoded = instruction.get("status").getAsString().equals("decoded"); boolean call = flow.get("kind").getAsString().equals("call"); boolean computed = flow.get("computed").getAsBoolean(); Map byTarget = new TreeMap<>(); if (call) for (JsonElement element : instruction.getAsJsonArray("references")) { JsonObject reference = element.getAsJsonObject(); if (reference.get("call").getAsBoolean()) byTarget.computeIfAbsent(reference.get("target_address").getAsString(), ignored -> new JsonArray()).add(reference); } boolean ambiguous = byTarget.size() > 1; for (Map.Entry entry : byTarget.entrySet()) { JsonObject target = new JsonObject(); target.addProperty("address", entry.getKey()); Address address = tryParseAddress(entry.getKey()); Function function = address == null ? null : currentProgram.getFunctionManager().getFunctionAt(address); target.add("procedure", function == null ? JsonNull.INSTANCE : GSON.toJsonTree(canonicalAddress(function.getEntryPoint()))); target.addProperty("status", ambiguous ? "candidate" : computed ? "resolved-indirect" : "direct"); target.addProperty("basis", "provider-reference"); target.add("references", entry.getValue()); targets.add(target); } result.addProperty("status", !decoded ? "unavailable" : !call ? "not-call" : ambiguous ? "ambiguous" : targets.size() == 0 ? "unresolved" : computed ? "resolved-indirect" : "direct"); result.addProperty("mechanism", !call ? "unavailable" : computed ? "computed" : "direct"); result.add("targets", targets); result.add("limitations", GSON.toJsonTree(List.of("Targets are static Ghidra ReferenceManager call-reference observations. Computed calls without call references remain unresolved; a resolved reference is not proof of runtime execution.", "Objective-C, Swift, vtable and closure mechanisms are not identified by this reference-only boundary."))); return result; } private JsonArray xrefs(JsonObject params) throws Exception { requireKeys(params, Set.of("document", "address")); requireDocument(params); Address address = requireAddress(params, "address"); ReferenceIterator references = currentProgram.getReferenceManager().getReferencesTo(address); Set
sources = new HashSet<>(); while (references.hasNext()) { monitor.checkCancelled(); Reference reference = references.next(); if (reference.isEntryPointReference()) { continue; } sources.add(reference.getFromAddress()); } List
ordered = new ArrayList<>(sources); ordered.sort(Address::compareTo); JsonArray result = new JsonArray(); for (Address source : ordered) { result.add(canonicalAddress(source)); } return result; } private void invalidateAnalysisCaches() { nameInventory = null; procedureInventory = null; stringInventory = null; if (decompiler != null) decompiler.flushCache(); } private JsonObject annotationReadback(Function function) { Address entry = function.getEntryPoint(); JsonObject value = new JsonObject(); value.addProperty("address", canonicalAddress(entry)); value.addProperty("name", procedureName(function)); value.addProperty("comment", currentProgram.getListing().getComment(CommentType.PRE, entry)); value.addProperty("inline_comment", currentProgram.getListing().getComment(CommentType.EOL, entry)); return value; } private JsonObject annotateNativeFunction(JsonObject params) throws Exception { if (!Set.of("procedure", "name", "comment", "inline_comment").containsAll(params.keySet())) throw new RequestFailure("invalid_request", "Unknown function annotation field"); if (!params.has("name") && !params.has("comment") && !params.has("inline_comment")) throw new RequestFailure("invalid_request", "Supply at least one annotation change"); Function function = resolveProcedure(requireString(params, "procedure")); Address entry = function.getEntryPoint(); if (function.isExternal() || !currentProgram.getMemory().contains(entry)) throw new RequestFailure("invalid_request", "Annotations require a local function entry: " + canonicalAddress(entry)); int transaction = currentProgram.startTransaction("REA function annotations"); boolean commit = false; try { // Validate edits through Ghidra's own setters, including name rules. // A later invalid name must roll back earlier comment writes. for (String field : List.of("comment", "inline_comment")) { if (params.has(field)) { String text = requireText(params, field); CommentType type = field.equals("comment") ? CommentType.PRE : CommentType.EOL; currentProgram.getListing().setComment(entry, type, text.isEmpty() ? null : text); } } if (params.has("name")) function.setName(requireString(params, "name"), SourceType.USER_DEFINED); monitor.checkCancelled(); invalidateAnalysisCaches(); JsonObject readback = annotationReadback(function); for (String field : List.of("name", "comment", "inline_comment")) { if (!params.has(field)) continue; String requested = requireText(params, field); JsonElement measured = readback.get(field); String expected = !field.equals("name") && requested.isEmpty() ? null : requested; String observed = field.equals("name") ? function.getName() : (measured.isJsonNull() ? null : measured.getAsString()); if (!java.util.Objects.equals(expected, observed)) throw new RequestFailure("annotation_readback_mismatch", "Annotation readback differs for " + field + " at " + canonicalAddress(entry)); } JsonObject query = new JsonObject(); query.addProperty("procedure", canonicalAddress(entry)); JsonObject result = new JsonObject(); result.add("annotations", readback); result.add("dossier", analyzeFunction(query)); JsonObject effects = new JsonObject(); effects.addProperty("scope", "session-analysis-database"); effects.addProperty("source_bytes_modified", false); effects.addProperty("persists_after_close", false); result.add("effects", effects); monitor.checkCancelled(); commit = true; return result; } catch (ghidra.util.exception.InvalidInputException | ghidra.util.exception.DuplicateNameException exception) { throw new RequestFailure("invalid_function_name", "Invalid function name at " + canonicalAddress(entry) + ": " + safeMessage(exception)); } finally { currentProgram.endTransaction(transaction, commit); // Rollback also invalidates inventory and decompiler views. invalidateAnalysisCaches(); } } private static String requireText(JsonObject params, String field) { JsonElement value = params.get(field); if (value == null || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString()) throw new RequestFailure("invalid_request", "Annotation " + field + " must be text"); return value.getAsString(); } private JsonObject analyzeFunction(JsonObject params) throws Exception { requireKeys(params, Set.of("procedure")); Function function = resolveProcedure(requireString(params, "procedure")); InstructionScan scan = scanInstructions(function, Integer.MAX_VALUE); DecompileResults decompilation = decompile(function); String pseudocode = decompiledText(decompilation); if (pseudocode == null) { pseudocode = ""; } List incomingReferences = collectReferences(scan.instructions, "incoming") .stream() .filter(reference -> !function.getBody().contains(reference.getFromAddress())) .toList(); List outgoingReferences = collectReferences(scan.instructions, "outgoing"); JsonArray incoming = referenceEdges(incomingReferences); JsonArray outgoing = referenceEdges(outgoingReferences); JsonArray comments = comments(scan.instructions); JsonArray callers = procedureIdentities(function.getCallingFunctions(monitor)); JsonArray callees = procedureIdentities(function.getCalledFunctions(monitor)); JsonArray referencedStrings = referencedStrings(outgoingReferences); JsonArray referencedNames = referencedNames(outgoingReferences); JsonArray blocks = basicBlockValues(function); JsonArray assembly = GSON.toJsonTree(renderAssemblyLines(scan.instructions)) .getAsJsonArray(); JsonObject procedure = procedureIdentity(function); procedure.addProperty("signature", function.getPrototypeString(false, true)); procedure.add("locals", functionLocals(function)); JsonObject result = new JsonObject(); result.add("procedure", procedure); result.addProperty("pseudocode", pseudocode); result.add("assembly", assembly); result.add("comments", comments); result.add("callers", callers); result.add("callees", callees); result.add("incoming_references", incoming); result.add("outgoing_references", outgoing); result.add("referenced_strings", referencedStrings); result.add("referenced_names", referencedNames); result.add("basic_blocks", blocks); result.add( "native_api", nativeApiBoundary(function, decompilation) ); result.add("native_value_flow", nativeValueFlow(decompilation)); JsonArray limitations = new JsonArray(); limitations.add( "Unresolved computed or indirect flows without target addresses are not represented as reference edges." ); limitations.add( "Thunk and external classifications are Ghidra FunctionManager observations; they do not resolve targetless calls." ); limitations.add( "Pseudocode and assembly are Ghidra-specific representations, not original source or Hopper-equivalent text." ); limitations.add( "Synthetic Ghidra entry-point references without actionable memory sources are omitted." ); if (decompilation != null && decompilation.getErrorMessage() != null && !decompilation.getErrorMessage().isBlank()) { limitations.add("Ghidra decompiler diagnostic: " + decompilation.getErrorMessage()); } result.add("limitations", limitations); return result; } private JsonObject nativeValueFlow(DecompileResults decompilation) throws Exception { HighFunction highFunction = decompilation == null ? null : decompilation.getHighFunction(); if (highFunction == null) { JsonObject unavailable = new JsonObject(); unavailable.addProperty("available", false); unavailable.addProperty( "reason", "Ghidra has no HighFunction p-code model for this procedure" ); JsonArray limitations = new JsonArray(); limitations.add( "No p-code operations or def-use relationships were recovered for this procedure." ); unavailable.add("limitations", limitations); return unavailable; } List selected = new ArrayList<>(); int omittedOperationsLowerBound = 0; Iterator operations = highFunction.getPcodeOps(); while (operations.hasNext()) { monitor.checkCancelled(); PcodeOpAST operation = operations.next(); if (selected.size() >= MAX_HIGH_PCODE_OPS) { omittedOperationsLowerBound = 1; break; } selected.add(operation); } Map operationIds = new IdentityHashMap<>(); for (PcodeOpAST operation : selected) { operationIds.put(operation, pcodeOperationId(operation)); } JsonArray parameterUses = new JsonArray(); JsonArray parameterValues = new JsonArray(); FunctionPrototype prototype = highFunction.getFunctionPrototype(); for (int ordinal = 0; ordinal < prototype.getNumParams(); ordinal++) { HighSymbol parameter = prototype.getParam(ordinal); if (parameter == null) continue; JsonObject value = new JsonObject(); value.addProperty("ordinal", ordinal); value.addProperty("name", parameter.getName()); value.addProperty("data_type", parameter.getDataType().getPathName()); parameterValues.add(value); } JsonArray operationValues = new JsonArray(); JsonArray defUse = new JsonArray(); JsonArray effects = new JsonArray(); int omittedInputs = 0; int omittedEdges = 0; for (PcodeOpAST operation : selected) { monitor.checkCancelled(); String operationId = operationIds.get(operation); JsonObject value = new JsonObject(); value.addProperty("id", operationId); value.addProperty( "address", canonicalAddress(operation.getSeqnum().getTarget()) ); value.addProperty("sequence", operation.getSeqnum().getTime()); value.addProperty("opcode", operation.getMnemonic()); value.addProperty("is_dead", operation.isDead()); value.addProperty("block_membership", operation.getParent() == null ? "detached" : "member"); JsonArray inputs = new JsonArray(); int inputCount = operation.getNumInputs(); int capturedInputs = Math.min(inputCount, MAX_HIGH_PCODE_INPUTS_PER_OP); for (int inputIndex = 0; inputIndex < capturedInputs; inputIndex += 1) { Varnode input = operation.getInput(inputIndex); inputs.add(varnodeValue(input)); if (input instanceof VarnodeAST astInput) { if (astInput.getHigh() instanceof HighParam parameter) { if (parameterUses.size() >= MAX_HIGH_PCODE_DEF_USE_EDGES) omittedEdges++; else { JsonObject use = new JsonObject(); use.addProperty("ordinal", parameter.getSlot()); use.addProperty("use", operationId); use.addProperty("input_index", inputIndex); parameterUses.add(use); } } PcodeOp definition = astInput.getDef(); if (definition instanceof PcodeOpAST astDefinition) { String definitionId = operationIds.get(astDefinition); if (definitionId == null || defUse.size() >= MAX_HIGH_PCODE_DEF_USE_EDGES) { omittedEdges += 1; } else { JsonObject edge = new JsonObject(); edge.addProperty("definition", definitionId); edge.addProperty("use", operationId); edge.addProperty("input_index", inputIndex); defUse.add(edge); } } } } omittedInputs += inputCount - capturedInputs; value.add("inputs", inputs); Varnode output = operation.getOutput(); value.add("output", output == null ? JsonNull.INSTANCE : varnodeValue(output)); operationValues.add(value); addPcodeEffect(effects, operation, operationId); } JsonObject result = new JsonObject(); result.addProperty("available", true); result.addProperty("provenance", "ghidra-high-pcode"); result.add("operations", operationValues); result.add("def_use", defUse); result.add("parameters", parameterValues); result.add("parameter_uses", parameterUses); result.add("effects", effects); boolean truncated = omittedOperationsLowerBound > 0 || omittedInputs > 0 || omittedEdges > 0; result.addProperty("truncated", truncated); result.addProperty( "omitted_operations_lower_bound", omittedOperationsLowerBound ); result.addProperty("known_omitted_inputs", omittedInputs); result.addProperty("known_omitted_edges", omittedEdges); JsonArray limitations = new JsonArray(); limitations.add( "High p-code is decompiler-derived, not original source or runtime behavior. is_dead preserves the Java AST flag; decoded block members can retain that flag, so block_membership separately reports actual syntax-tree membership." ); limitations.add( "Def-use links do not cross function boundaries and may omit memory aliasing, call side effects, or unresolved indirect flow." ); limitations.add( "Memory read/write effects include stack and temporary memory operations; they do not identify persistent state or business meaning." ); limitations.add( "Operation, input, and def-use limits can truncate large functions; inspect truncation, omitted lower bounds, and known omitted counts before drawing conclusions." ); limitations.add( "Known omitted-edge counts cover links omitted from retained operations; omitted operations may contain additional uncounted links." ); result.add("limitations", limitations); return result; } private String pcodeOperationId(PcodeOpAST operation) { return canonicalAddress(operation.getSeqnum().getTarget()) + "#" + operation.getSeqnum().getTime(); } private JsonObject varnodeValue(Varnode varnode) { JsonObject result = new JsonObject(); String kind = varnode.isConstant() ? "constant" : varnode.isAddress() ? "address" : varnode.isRegister() ? "register" : varnode.isUnique() ? "unique" : "other"; result.addProperty("kind", kind); result.addProperty("size_bytes", varnode.getSize()); result.add( "location", varnode.isConstant() ? JsonNull.INSTANCE : GSON.toJsonTree(canonicalAddress(varnode.getAddress())) ); result.add( "constant_hex", varnode.isConstant() ? GSON.toJsonTree(Long.toUnsignedString(varnode.getOffset(), 16)) : JsonNull.INSTANCE ); return result; } private void addPcodeEffect( JsonArray destination, PcodeOp operation, String operationId ) { String kind = null; Integer operandInput = null; Integer valueInput = null; switch (operation.getOpcode()) { case PcodeOp.LOAD -> { kind = "memory_read"; operandInput = 1; } case PcodeOp.STORE -> { kind = "memory_write"; operandInput = 1; valueInput = 2; } case PcodeOp.CBRANCH -> { kind = "conditional_branch"; valueInput = 1; } case PcodeOp.BRANCHIND -> { kind = "indirect_branch"; operandInput = 0; } case PcodeOp.CALL -> { kind = "direct_call"; operandInput = 0; } case PcodeOp.CALLIND -> { kind = "indirect_call"; operandInput = 0; } default -> { } } if (kind == null) return; JsonObject effect = new JsonObject(); effect.addProperty("operation", operationId); effect.addProperty("kind", kind); effect.add( "operand_input", operandInput == null ? JsonNull.INSTANCE : GSON.toJsonTree(operandInput) ); effect.add( "value_input", valueInput == null ? JsonNull.INSTANCE : GSON.toJsonTree(valueInput) ); destination.add(effect); } private JsonElement addressName(JsonObject params) { requireKeys(params, Set.of("document", "address")); requireDocument(params); Address address = requireAddress(params, "address"); Symbol symbol = currentProgram.getSymbolTable().getPrimarySymbol(address); return symbol == null ? JsonNull.INSTANCE : GSON.toJsonTree(symbol.getName(true)); } private JsonArray listDocuments(JsonObject params) { requireKeys(params, Set.of()); JsonArray result = new JsonArray(); result.add(currentProgram.getName()); return result; } private JsonArray listNames(JsonObject params) throws Exception { requireKeys(params, Set.of("document", "address")); requireDocument(params); List inventory = names(); String requested = optionalString(params, "address"); if (requested != null) { Address address = parseReaAddress(requested); inventory = inventory.stream() .filter(item -> item.address.equals(address)) .toList(); } return inventory(inventory, "symbol"); } private JsonArray listProcedures(JsonObject params) throws Exception { requireKeys(params, Set.of("document")); requireDocument(params); List inventory = procedures().stream() .map(FunctionEntry::item) .toList(); return inventory(inventory, "procedure"); } private JsonArray listSegments(JsonObject params) { requireKeys(params, Set.of("document")); requireDocument(params); JsonArray result = new JsonArray(); String imageBase = canonicalAddress(currentProgram.getImageBase()); MemoryBlock[] blocks = currentProgram.getMemory().getBlocks(); List ordered = new ArrayList<>(List.of(blocks)); ordered.sort(Comparator.comparing(MemoryBlock::getStart)); for (MemoryBlock block : ordered) { JsonObject item = memoryRegion(block, imageBase); item.add("sections", new JsonArray()); result.add(item); } return result; } private JsonArray listStrings(JsonObject params) throws Exception { requireKeys(params, Set.of("document", "address")); requireDocument(params); List inventory = strings(); String requested = optionalString(params, "address"); if (requested != null) { Address address = parseReaAddress(requested); inventory = inventory.stream() .filter(item -> item.address.equals(address)) .toList(); } return inventory(inventory, "string"); } private JsonElement procedureAddress(JsonObject params) throws Exception { requireKeys(params, Set.of("document", "procedure")); requireDocument(params); return GSON.toJsonTree(canonicalAddress(resolveProcedure(requireString(params, "procedure")).getEntryPoint())); } private JsonObject containingProcedure(JsonObject params) { requireKeys(params, Set.of("document", "address")); requireDocument(params); Address query = requireAddress(params, "address"); Function function = currentProgram.getFunctionManager().getFunctionAt(query); if (function == null && query.isMemoryAddress()) { function = currentProgram.getFunctionManager().getFunctionContaining(query); } JsonObject result = new JsonObject(); result.addProperty("query_address", canonicalAddress(query)); if (function != null) { result.addProperty("found", true); result.add("procedure", procedureIdentity(function)); return result; } result.addProperty("found", false); result.add("procedure", JsonNull.INSTANCE); boolean inside = query.isMemoryAddress() && currentProgram.getMemory().contains(query); result.addProperty("reason", inside ? "not_in_procedure" : "outside_segments"); return result; } private JsonArray search(JsonObject params, boolean procedureSearch) throws Exception { requireKeys( params, Set.of("pattern", "mode", "case_sensitive", "document") ); requireDocument(params); String expression = requireString(params, "pattern"); String mode = requireString(params, "mode"); if (!mode.equals("literal") && !mode.equals("regex")) { throw new RequestFailure("invalid_request", "mode must be literal or regex"); } boolean caseSensitive = requireBoolean(params, "case_sensitive"); List inventory = procedureSearch ? procedures().stream().map(FunctionEntry::item).toList() : strings(); ValueMatcher matcher = mode.equals("literal") ? literalMatcher(expression, caseSensitive) : regexMatcher(expression, caseSensitive); JsonArray items = new JsonArray(); for (InventoryItem item : inventory) { if (!matcher.matches(item.value)) { continue; } items.add(resultItem(item, null)); } return items; } private List names() throws Exception { if (nameInventory != null) { return nameInventory; } List result = new ArrayList<>(); SymbolIterator symbols = currentProgram.getSymbolTable().getAllSymbols(true); while (symbols.hasNext()) { monitor.checkCancelled(); Symbol symbol = symbols.next(); Address address = symbol.getAddress(); if (!address.isMemoryAddress() && !address.isExternalAddress()) { continue; } JsonObject facts = new JsonObject(); facts.addProperty("primary", symbol.isPrimary()); facts.addProperty("dynamic", symbol.isDynamic()); facts.addProperty("external", symbol.isExternal()); facts.addProperty("type", normalizedSymbolType(symbol)); facts.addProperty("source", normalizedSource(symbol.getSource())); result.add(new InventoryItem(address, symbol.getName(true), facts)); } result.sort(INVENTORY_ORDER); nameInventory = List.copyOf(result); return nameInventory; } private List procedures() throws Exception { if (procedureInventory != null) { return procedureInventory; } List result = new ArrayList<>(); Set
seen = new HashSet<>(); appendFunctions(result, seen, currentProgram.getFunctionManager().getFunctions(true)); appendFunctions(result, seen, currentProgram.getFunctionManager().getExternalFunctions()); result.sort((left, right) -> INVENTORY_ORDER.compare(left.item, right.item)); procedureInventory = List.copyOf(result); return procedureInventory; } private void appendFunctions( List destination, Set
seen, FunctionIterator functions) throws Exception { while (functions.hasNext()) { monitor.checkCancelled(); Function function = functions.next(); if (!seen.add(function.getEntryPoint())) { continue; } JsonObject facts = new JsonObject(); facts.addProperty("external", function.isExternal()); facts.addProperty("thunk", function.isThunk()); Function target = function.isThunk() ? function.getThunkedFunction(false) : null; if (target == null) { facts.add("thunk_target", JsonNull.INSTANCE); } else { facts.addProperty("thunk_target", canonicalAddress(target.getEntryPoint())); } Symbol symbol = function.getSymbol(); InventoryItem item = new InventoryItem( function.getEntryPoint(), symbol == null ? function.getName() : symbol.getName(true), facts ); destination.add(new FunctionEntry(function, item)); } } private List strings() throws Exception { if (stringInventory != null) { return stringInventory; } List result = new ArrayList<>(); DataIterator dataItems = currentProgram.getListing().getDefinedData(true); while (dataItems.hasNext()) { monitor.checkCancelled(); Data data = dataItems.next(); if (!data.hasStringValue()) { continue; } StringDataInstance instance = StringDataInstance.getStringDataInstance(data); String value = instance.getStringValue(); if (value == null) { continue; } JsonObject facts = new JsonObject(); String charset = instance.getCharsetName(); facts.addProperty("encoding", charset == null || charset.isEmpty() ? "unknown" : charset); facts.addProperty( "termination", instance.isMissingNullTerminator() ? "missing" : "present_or_not_required" ); facts.addProperty("byte_length", Math.max(0, data.getLength())); result.add(new InventoryItem(data.getAddress(), value, facts)); } result.sort(INVENTORY_ORDER); stringInventory = List.copyOf(result); return stringInventory; } private Function resolveProcedure(String value) throws Exception { Address address = tryParseAddress(value); if (address != null) { Function function = currentProgram.getFunctionManager().getFunctionAt(address); if (function == null && address.isMemoryAddress()) { function = currentProgram.getFunctionManager().getFunctionContaining(address); } if (function == null) { throw new RequestFailure("not_found", "No procedure exists at the requested address"); } return function; } List matches = new ArrayList<>(); for (FunctionEntry entry : procedures()) { Function function = entry.function; Symbol symbol = function.getSymbol(); String qualified = symbol == null ? function.getName() : symbol.getName(true); if (function.getName().equals(value) || qualified.equals(value)) { matches.add(function); } } if (matches.isEmpty()) { throw new RequestFailure("not_found", "Unknown Ghidra procedure name"); } if (matches.size() != 1) { throw new RequestFailure("ambiguous", "Ghidra procedure name is ambiguous"); } return matches.get(0); } private JsonObject nativeApiBoundary( Function function, DecompileResults decompilation ) throws Exception { if (decompilation == null || decompilation.getHighFunction() == null) { return unavailableNativeApiBoundary( "Ghidra has no HighFunction model for an external or bodyless procedure" ); } HighFunction highFunction = decompilation.getHighFunction(); FunctionPrototype prototype = highFunction.getFunctionPrototype(); String signatureSource = function .getSignatureSource() .name() .toLowerCase(Locale.ROOT); JsonObject result = new JsonObject(); result.addProperty("available", true); result.addProperty("provenance", "ghidra-high-function"); result.addProperty("signature_source", signatureSource); String callingConvention = prototype.getModelName(); result.addProperty( "calling_convention", callingConvention == null || callingConvention.isBlank() ? "unknown" : callingConvention ); result.add( "return_type", inferredBoundaryType( "return", null, null, prototype.getReturnType(), prototype.getReturnStorage().toString(), false, signatureSource ) ); JsonArray parameters = new JsonArray(); result.add("parameters", parameters); JsonArray jumpTables = new JsonArray(); result.add("jump_tables", jumpTables); JsonObject pseudocode = new JsonObject(); pseudocode.addProperty("classification", "decompiler-generated-non-source"); pseudocode.addProperty("compilable", false); result.add("pseudocode", pseudocode); JsonArray artifacts = new JsonArray(); artifacts.add("recovered-signature"); artifacts.add("register-and-stack-variables"); artifacts.add("compiler-generated-control-flow"); artifacts.add("pointer-arithmetic"); artifacts.add("pseudocode"); result.add("decompiler_artifacts", artifacts); JsonArray limitations = new JsonArray(); limitations.add( "Types and calling conventions are Ghidra decompiler observations, not source declarations." ); limitations.add( "Register variables, compiler-generated blocks, and pointer arithmetic remain decompiler artifacts." ); limitations.add( "Pseudocode is neither original source nor guaranteed to compile." ); result.add("limitations", limitations); for (int index = 0; index < prototype.getNumParams(); index += 1) { HighSymbol parameter = prototype.getParam(index); if (parameter == null) { continue; } parameters.add( inferredBoundaryType( "parameter", index, parameter.getName(), parameter.getDataType(), parameter.getStorage().toString(), parameter.isTypeLocked(), signatureSource ) ); } JumpTable[] recoveredJumpTables = highFunction.getJumpTables(); List caseTokens = new ArrayList<>(); if (decompilation.getCCodeMarkup() != null) { decompilation.getCCodeMarkup().flatten(caseTokens); } for (int index = 0; index < recoveredJumpTables.length; index += 1) { jumpTables.add(inferredJumpTable(recoveredJumpTables[index], caseTokens)); } return result; } private static JsonObject unavailableNativeApiBoundary(String reason) { JsonObject unavailable = new JsonObject(); unavailable.addProperty("available", false); unavailable.addProperty("reason", reason); JsonArray unknowns = new JsonArray(); unknowns.add("Return and parameter boundary types are unavailable."); unknowns.add("Jump-table mappings are unavailable."); unavailable.add("residual_unknowns", unknowns); return unavailable; } private static JsonObject inferredBoundaryType( String role, Integer ordinal, String name, DataType dataType, String storage, boolean typeLocked, String signatureSource ) { JsonObject result = new JsonObject(); result.addProperty("role", role); if (ordinal == null) { result.add("ordinal", JsonNull.INSTANCE); } else { result.addProperty("ordinal", ordinal); } if (name == null || name.isBlank()) { result.add("name", JsonNull.INSTANCE); } else { result.addProperty("name", name); } result.addProperty("data_type", dataType.getDisplayName()); int dataTypeLength = dataType.getLength(); if (dataTypeLength < 0) { result.add("size_bytes", JsonNull.INSTANCE); } else { result.addProperty("size_bytes", dataTypeLength); } if (storage.isBlank()) { result.add("storage", JsonNull.INSTANCE); } else { result.addProperty("storage", storage); } result.addProperty( "confidence", inferenceConfidence(signatureSource, typeLocked) ); JsonArray evidence = new JsonArray(); evidence.add( inferenceEvidence( "signature-source", "ghidra-function-manager", "Function signature source is " + signatureSource + "." ) ); evidence.add( inferenceEvidence( "decompiler-type", "ghidra-high-function", "HighFunction recovered data type " + dataType.getDisplayName() + "." ) ); if (typeLocked) { evidence.add( inferenceEvidence( "type-lock", "ghidra-high-symbol", "The decompiler marks this parameter type as locked." ) ); } result.add("evidence", evidence); JsonArray artifacts = new JsonArray(); artifacts.add("recovered-signature"); if (!storage.isBlank()) { artifacts.add("register-or-stack-storage"); } if (name != null && (name.startsWith("param_") || name.startsWith("unaff_"))) { artifacts.add("compiler-generated-variable"); } result.add("decompiler_artifacts", artifacts); return result; } private JsonObject inferredJumpTable( JumpTable jumpTable, List caseTokens ) throws Exception { JsonObject result = new JsonObject(); result.addProperty( "dispatch_address", canonicalAddress(jumpTable.getSwitchAddress()) ); JumpTable.LoadTable[] loadTables = jumpTable.getLoadTables(); RecoveredAArch64RelativeTable recoveredAArch64 = loadTables.length == 0 ? recoverAArch64RelativeTable(jumpTable) : null; List
referencedDataAddresses = loadTables.length == 0 ? dispatchPathDataReferences(jumpTable) : List.of(); JsonArray dataSources = new JsonArray(); for (int index = 0; index < loadTables.length; index += 1) { monitor.checkCancelled(); JumpTable.LoadTable loadTable = loadTables[index]; JsonObject source = new JsonObject(); source.addProperty("address", canonicalAddress(loadTable.getAddress())); source.addProperty("provenance", "ghidra-decompiler-load-table"); int entrySize = loadTable.getSize(); int entryCount = loadTable.getNum(); if (entrySize > 0) source.addProperty("entry_size_bytes", entrySize); else source.add("entry_size_bytes", JsonNull.INSTANCE); if (entryCount > 0) source.addProperty("entry_count", entryCount); else source.add("entry_count", JsonNull.INSTANCE); source.addProperty( "confidence", entrySize > 0 && entryCount > 0 ? "high" : "medium" ); JsonArray evidence = new JsonArray(); evidence.add( inferenceEvidence( "jump-table", "ghidra-high-function", "Decompiler load table starts at " + canonicalAddress(loadTable.getAddress()) + "." ) ); source.add("evidence", evidence); dataSources.add(source); } if (recoveredAArch64 != null) { JsonObject source = new JsonObject(); source.addProperty( "address", canonicalAddress(recoveredAArch64.tableAddress) ); source.addProperty("provenance", recoveredAArch64.entrySize == 1 ? "ghidra-aarch64-byte-table" : "ghidra-aarch64-halfword-table"); source.addProperty("entry_size_bytes", recoveredAArch64.entrySize); source.addProperty("entry_count", recoveredAArch64.mappings.size()); source.addProperty("confidence", "high"); JsonArray evidence = new JsonArray(); evidence.add( inferenceEvidence( "jump-table", "ghidra-instruction-and-memory", "Decoded " + recoveredAArch64.mappings.size() + " entries of size " + recoveredAArch64.entrySize + " bytes from " + canonicalAddress(recoveredAArch64.tableAddress) + "; the AArch64 LDRB-or-LDRH/ADR/ADD/BR chain uses a selector guarded by an unsigned upper-bound branch." ) ); source.add("evidence", evidence); dataSources.add(source); } for (int index = 0; index < referencedDataAddresses.size(); index += 1) { Address address = referencedDataAddresses.get(index); if ( recoveredAArch64 != null && recoveredAArch64.tableAddress.equals(address) ) { continue; } JsonObject source = new JsonObject(); source.addProperty("address", canonicalAddress(address)); source.addProperty( "provenance", "ghidra-dispatch-block-data-reference" ); source.add("entry_size_bytes", JsonNull.INSTANCE); source.add("entry_count", JsonNull.INSTANCE); source.addProperty("confidence", "medium"); JsonArray evidence = new JsonArray(); evidence.add( inferenceEvidence( "jump-table", "ghidra-reference-manager", "The dispatch block or an immediate predecessor contains a typed data reference to " + canonicalAddress(address) + "." ) ); source.add("evidence", evidence); dataSources.add(source); } result.add("data_sources", dataSources); Address[] targets = jumpTable.getCases(); TypedJumpTableCases typedCases = typedJumpTableCases(jumpTable, caseTokens); JsonArray mappings = new JsonArray(); JsonArray defaultTargets = new JsonArray(); for (Address target : typedCases.defaults) { defaultTargets.add(typedJumpTableTarget(target, jumpTable, "Typed default label")); } Set
coveredTargets = new HashSet<>(typedCases.cases.values()); coveredTargets.addAll(typedCases.defaults); if (recoveredAArch64 != null) { for (int index = 0; index < recoveredAArch64.mappings.size(); index += 1) { monitor.checkCancelled(); RecoveredAArch64Mapping recovered = recoveredAArch64.mappings.get(index); if ( typedCases.unknownTargets.contains(recovered.targetAddress) || typedCases.ambiguousValues.contains((long) recovered.caseValue) ) continue; Address typedTarget = typedCases.cases.get((long) recovered.caseValue); if (typedTarget != null && !typedTarget.equals(recovered.targetAddress)) { typedCases.cases.remove((long) recovered.caseValue); typedCases.ambiguousValues.add((long) recovered.caseValue); typedCases.unknownTargets.add(typedTarget); typedCases.unknownTargets.add(recovered.targetAddress); typedCases.limitations.add( "Typed case evidence and the verified relative table disagree on a destination; both destinations remain unknown for that case." ); continue; } JsonObject mapping = new JsonObject(); mapping.addProperty("case_value", recovered.caseValue); String targetAddress = canonicalAddress(recovered.targetAddress); mapping.addProperty("target_address", targetAddress); mapping.addProperty("confidence", "high"); JsonArray evidence = new JsonArray(); evidence.add( inferenceEvidence( "jump-table", "ghidra-instruction-and-memory", "Case value " + recovered.caseValue + (recoveredAArch64.entrySize == 1 ? " indexes byte " : " indexes halfword at byte offset ") + (recovered.caseValue * recoveredAArch64.entrySize) + " at " + canonicalAddress(recoveredAArch64.tableAddress) + "; the loaded unsigned entry selects a four-byte instruction offset from " + canonicalAddress(recoveredAArch64.branchBase) + ", reaching " + targetAddress + "." ) ); mapping.add("evidence", evidence); if (!typedCases.cases.containsKey((long) recovered.caseValue)) { mappings.add(mapping); } coveredTargets.add(recovered.targetAddress); } } for (Map.Entry entry : typedCases.cases.entrySet()) { JsonObject mapping = typedJumpTableTarget( entry.getValue(), jumpTable, "Typed case value " + entry.getKey() ); mapping.addProperty("case_value", entry.getKey()); mappings.add(mapping); } Set
emittedUnknownTargets = new HashSet<>(); for (int index = 0; index < targets.length; index += 1) { monitor.checkCancelled(); boolean unresolvedToken = typedCases.unknownTargets.contains(targets[index]); if (coveredTargets.contains(targets[index]) && !unresolvedToken) continue; if (!emittedUnknownTargets.add(targets[index])) continue; JsonObject mapping = new JsonObject(); mapping.add("case_value", JsonNull.INSTANCE); String targetAddress = canonicalAddress(targets[index]); mapping.addProperty("target_address", targetAddress); // Ghidra's recovered case-to-target relation does not identify // which backing load table produced an individual case. Keep the // table inventory and recovered mappings separate instead of // attaching every table address to every target. mapping.addProperty("confidence", "medium"); JsonArray evidence = new JsonArray(); evidence.add( inferenceEvidence( "jump-table", "ghidra-high-function", "Ghidra recovered target " + targetAddress + " at dispatch " + canonicalAddress(jumpTable.getSwitchAddress()) + ", but no unambiguous, exactly representable case label or default relationship was available." ) ); mapping.add("evidence", evidence); mappings.add(mapping); } result.add("mappings", mappings); result.add("default_targets", defaultTargets); JsonArray limitations = new JsonArray(); for (String limitation : typedCases.limitations) limitations.add(limitation); if (defaultTargets.isEmpty()) { limitations.add( "Ghidra exposed no typed default label with an unambiguous relationship to a recovered destination of this dispatch; default or out-of-range handling remains unknown." ); } if (loadTables.length == 0 && referencedDataAddresses.isEmpty()) { limitations.add( "Ghidra recovered targets but exposed no backing load-table or dispatch-block data address." ); } if ( recoveredAArch64 == null && loadTables.length == 0 && !referencedDataAddresses.isEmpty() ) { limitations.add( "Load-table metadata was absent; table-level candidate ownership is inferred from typed data references in the dispatch block and its immediate control-flow predecessors, but candidates are not assigned to individual mappings." ); } if (!emittedUnknownTargets.isEmpty()) { limitations.add( "Some recovered destinations lack an unambiguous, exactly representable case/default relationship; their case values remain unknown. Unequal label and destination arrays are never paired by index." ); } if (recoveredAArch64 == null && loadTables.length == 0 && currentProgram.getLanguageID().getIdAsString().startsWith("AARCH64:")) { limitations.add("REA did not verify a supported byte/halfword relative dispatch chain with exact unsigned bounds and register definitions; unsupported encodings and ambiguous bounds remain unknown."); } if (targets.length == 0) { limitations.add( "Ghidra exposed a jump-table dispatch without recovered case targets." ); } result.add("limitations", limitations); return result; } private static final class TypedJumpTableCases { private final Map cases = new TreeMap<>(); private final Set
defaults = new java.util.TreeSet<>(); private final Set
unknownTargets = new HashSet<>(); private final Set ambiguousValues = new HashSet<>(); private final Set limitations = new java.util.LinkedHashSet<>(); } private TypedJumpTableCases typedJumpTableCases( JumpTable jumpTable, List tokens ) throws Exception { TypedJumpTableCases result = new TypedJumpTableCases(); Set
targets = new HashSet<>(); for (Address target : jumpTable.getCases()) targets.add(target); for (ClangNode node : tokens) { monitor.checkCancelled(); if (!(node instanceof ClangCaseToken token)) continue; PcodeOp operation = token.getPcodeOp(); if (operation == null || operation.getParent() == null) continue; PcodeBlockBasic block = operation.getParent(); Address target = block.getStart(); if (!targets.contains(target)) continue; PcodeOp dispatch = null; boolean ambiguousDispatch = false; boolean includesDispatch = false; for (int index = 0; index < block.getInSize(); index += 1) { PcodeBlock predecessor = block.getIn(index); if (!(predecessor instanceof PcodeBlockBasic basic)) continue; PcodeOp last = basic.getLastOp(); if (last == null || last.getOpcode() != PcodeOp.BRANCHIND) continue; if (last.getSeqnum().getTarget().equals(jumpTable.getSwitchAddress())) { includesDispatch = true; } if (dispatch != null && dispatch != last) ambiguousDispatch = true; dispatch = last; } if (!includesDispatch) continue; if (ambiguousDispatch) { result.unknownTargets.add(target); result.limitations.add( "A typed case/default block has multiple indirect dispatch predecessors; REA cannot attribute its label to one switch." ); continue; } // Default is a typed label role, not an OFF sentinel: Ghidra can // encode a default token with an ordinary in-range case value. if ("default".equals(token.getText())) { result.defaults.add(target); continue; } Long value = exactTypedCaseValue(token); if (value == null) { result.unknownTargets.add(target); result.limitations.add( "Typed case label " + token.getText() + " has encoded unsigned magnitude " + Long.toUnsignedString(token.getValue()) + " at recovered target " + canonicalAddress(target) + " for dispatch " + canonicalAddress(jumpTable.getSwitchAddress()) + "; the label is nonliteral, inconsistent with its encoded magnitude, or outside the exact JSON integer range, so its numeric case value remains unknown." ); continue; } if (result.ambiguousValues.contains(value)) { result.unknownTargets.add(target); continue; } Address previous = result.cases.putIfAbsent(value, target); if (previous != null && !previous.equals(target)) { result.cases.remove(value); result.ambiguousValues.add(value); result.unknownTargets.add(previous); result.unknownTargets.add(target); result.limitations.add( "Typed tokens assign one case value to different recovered destinations; that case remains unknown." ); } } return result; } private static Long exactTypedCaseValue(ClangCaseToken token) { String text = token.getText(); if (text == null || !text.matches("-?(?:0[xX][0-9a-fA-F]+|[0-9]+)")) return null; boolean negative = text.startsWith("-"); String literal = negative ? text.substring(1) : text; BigInteger magnitude = literal.startsWith("0x") || literal.startsWith("0X") ? new BigInteger(literal.substring(2), 16) : new BigInteger(literal, 10); // Negative numeric tokens carry an unsigned magnitude in OFF, so // getValue()/getScalar() alone would incorrectly emit positive cases. if (!magnitude.equals(new BigInteger(Long.toUnsignedString(token.getValue())))) return null; BigInteger value = negative ? magnitude.negate() : magnitude; if (value.abs().compareTo(BigInteger.valueOf(9007199254740991L)) > 0) return null; return value.longValueExact(); } private static JsonObject typedJumpTableTarget( Address target, JumpTable table, String label ) { JsonObject result = new JsonObject(); String address = canonicalAddress(target); result.addProperty("target_address", address); result.addProperty("confidence", "high"); JsonArray evidence = new JsonArray(); evidence.add(inferenceEvidence( "jump-table", "ghidra-clang-case-token", label + " belongs to the p-code block starting at recovered target " + address + "; its unique indirect dispatch predecessor is " + canonicalAddress(table.getSwitchAddress()) + "." )); result.add("evidence", evidence); return result; } private static final class RecoveredAArch64Mapping { private final int caseValue; private final Address targetAddress; private RecoveredAArch64Mapping(int caseValue, Address targetAddress) { this.caseValue = caseValue; this.targetAddress = targetAddress; } } private static final class RecoveredAArch64RelativeTable { private final Address tableAddress; private final Address branchBase; private final int entrySize; private final List mappings; private RecoveredAArch64RelativeTable( Address tableAddress, Address branchBase, int entrySize, List mappings ) { this.tableAddress = tableAddress; this.branchBase = branchBase; this.entrySize = entrySize; this.mappings = mappings; } } /** * Recover only the directly verified AArch64 byte/halfword-indexed relative branch * table form. Other instruction sequences keep their case labels unknown. */ private RecoveredAArch64RelativeTable recoverAArch64RelativeTable( JumpTable jumpTable ) throws Exception { if (!currentProgram.getLanguageID().getIdAsString().startsWith("AARCH64:")) { return null; } Function function = containingFunction(jumpTable.getSwitchAddress()); if (function == null) return null; List instructions = new ArrayList<>(); InstructionIterator iterator = currentProgram.getListing().getInstructions( function.getBody(), true ); while (iterator.hasNext()) { monitor.checkCancelled(); Instruction instruction = iterator.next(); instructions.add(instruction); if (instruction.getAddress().equals(jumpTable.getSwitchAddress())) break; } int branchIndex = instructions.size() - 1; if (branchIndex < 1 || !instructions.get(branchIndex).getMnemonicString().equalsIgnoreCase("BR")) { return null; } Address[] recoveredTargets = jumpTable.getCases(); Set recoveredTargetSet = new HashSet<>(); for (Address target : recoveredTargets) recoveredTargetSet.add(canonicalAddress(target)); List
dataAddresses = dispatchPathDataReferences(jumpTable); for (int loadIndex = 0; loadIndex < branchIndex; loadIndex += 1) { Instruction load = instructions.get(loadIndex); int entrySize = load.getMnemonicString().equalsIgnoreCase("LDRB") ? 1 : load.getMnemonicString().equalsIgnoreCase("LDRH") ? 2 : 0; if (entrySize == 0 || branchIndex != loadIndex + 2 || loadIndex < 3 || !instructions.get(loadIndex - 1).getMnemonicString().equalsIgnoreCase("ADR") || !instructions.get(loadIndex - 2).getMnemonicString().equalsIgnoreCase("ADD") || !instructions.get(loadIndex - 3).getMnemonicString().equalsIgnoreCase("ADRP")) continue; String loadOperands = operands(load); java.util.regex.Matcher loadMatcher = java.util.regex.Pattern .compile(entrySize == 1 ? "(?i)^[wx]\\d+,\\s*\\[([wx]\\d+),\\s*([wx]\\d+)\\]$" : "(?i)^[wx]\\d+,\\s*\\[([wx]\\d+),\\s*([wx]\\d+),\\s*LSL\\s*#(?:0x)?1\\]$") .matcher(loadOperands); if (!loadMatcher.matches()) continue; String loadedRegister = normalizedRegister(loadOperands.substring(0, loadOperands.indexOf(','))); String tableBaseRegister = normalizedRegister(loadMatcher.group(1)); String indexRegister = normalizedRegister(loadMatcher.group(2)); Address resolvedTableAddress = null; for (int index = loadIndex - 3; index < loadIndex - 1; index += 1) { Instruction instruction = instructions.get(index); String mnemonic = instruction.getMnemonicString().toUpperCase(Locale.ROOT); String text = operands(instruction); if (mnemonic.equals("ADRP") && text.matches("(?i)^[wx]\\d+,\\s*0x[0-9a-f]+$")) { String[] parts = text.split(","); if (normalizedRegister(parts[0]).equals(tableBaseRegister)) { resolvedTableAddress = addressFromOperand(parts[1]); } } else if (mnemonic.equals("ADD") && resolvedTableAddress != null && text.matches("(?i)^[wx]\\d+,\\s*[wx]\\d+,\\s*#?(?:0x[0-9a-f]+|[0-9]+)$")) { String[] parts = text.split(","); if (normalizedRegister(parts[0]).equals(tableBaseRegister) && normalizedRegister(parts[1]).equals(tableBaseRegister)) { resolvedTableAddress = resolvedTableAddress.add(parseInteger(parts[2].trim().replaceFirst("^#", ""))); } } } if (resolvedTableAddress == null || !currentProgram.getMemory().contains(resolvedTableAddress) || (!dataAddresses.isEmpty() && !dataAddresses.contains(resolvedTableAddress))) continue; Address tableAddress = resolvedTableAddress; Address branchBase = null; boolean addressChain = false; for (int index = loadIndex - 1; index < branchIndex; index += 1) { Instruction instruction = instructions.get(index); String mnemonic = instruction.getMnemonicString().toUpperCase(Locale.ROOT); String text = operands(instruction); if (index < loadIndex && mnemonic.equals("ADR") && text.matches("(?i)^[wx]\\d+,\\s*0x[0-9a-f]+$")) { int comma = text.indexOf(','); String destination = normalizedRegister(text.substring(0, comma)); if (!destination.equals(normalizedRegister(operands(instructions.get(branchIndex))))) continue; branchBase = addressFromOperand(text.substring(comma + 1)); } if (index > loadIndex && mnemonic.equals("ADD") && branchBase != null && text.matches("(?i)^[wx]\\d+,\\s*[wx]\\d+,\\s*[wx]\\d+,\\s*LSL\\s*#(?:0x)?2$")) { String[] parts = text.split(","); String destination = normalizedRegister(parts[0]); String base = normalizedRegister(parts[1]); String offset = normalizedRegister(parts[2]); if (destination.equals(base) && base.equals(normalizedRegister(operands(instructions.get(branchIndex)))) && offset.equals(loadedRegister)) { addressChain = true; } } } if (!addressChain || branchBase == null) continue; int entryCount = -1; Address defaultTarget = null; for (int compareIndex = 0; compareIndex < loadIndex; compareIndex += 1) { Instruction compare = instructions.get(compareIndex); if (!compare.getMnemonicString().equalsIgnoreCase("CMP")) continue; java.util.regex.Matcher compareMatcher = java.util.regex.Pattern .compile("(?i)^([wx]\\d+),\\s*#?(0x[0-9a-f]+|[0-9]+)$") .matcher(operands(compare)); if (!compareMatcher.matches()) continue; String comparedRegister = normalizedRegister(compareMatcher.group(1)); long upperBound = parseInteger(compareMatcher.group(2)); if (upperBound < 0 || upperBound >= 4096) continue; for (int guardIndex = compareIndex + 1; guardIndex < loadIndex; guardIndex += 1) { Instruction guard = instructions.get(guardIndex); String mnemonic = guard.getMnemonicString().toUpperCase(Locale.ROOT); if (!mnemonic.equals("B.HI")) continue; Address[] flows = guard.getFlows(); if (flows.length != 1 || !recoveredTargetSet.contains(canonicalAddress(flows[0]))) continue; boolean supportedSequence = true; for (int sequenceIndex = compareIndex + 1; sequenceIndex < loadIndex; sequenceIndex += 1) { String sequenceMnemonic = instructions.get(sequenceIndex) .getMnemonicString().toUpperCase(Locale.ROOT); if (!Set.of("B.HI", "MOV", "ADRP", "ADD", "ADR").contains(sequenceMnemonic)) { supportedSequence = false; break; } } Set selectorAliases = new HashSet<>(); selectorAliases.add(comparedRegister); Set boundedIndexes = new HashSet<>(); if (compareMatcher.group(1).toLowerCase(Locale.ROOT).startsWith("x")) boundedIndexes.add(comparedRegister); for (int sequenceIndex = compareIndex + 1; sequenceIndex < loadIndex; sequenceIndex++) { Instruction instruction = instructions.get(sequenceIndex); String[] parts = operands(instruction).split(","); if (parts.length == 0) continue; if (instruction.getMnemonicString().equalsIgnoreCase("MOV")) { String destination = normalizedRegister(parts[0]); String source = parts.length == 2 ? normalizedRegister(parts[1]) : ""; boolean aliasesSelector = selectorAliases.contains(source); boolean fullWidthBounded = boundedIndexes.contains(source) || (parts[0].trim().toLowerCase(Locale.ROOT).startsWith("w") && aliasesSelector); if (aliasesSelector) selectorAliases.add(destination); else selectorAliases.remove(destination); if (fullWidthBounded) boundedIndexes.add(destination); else boundedIndexes.remove(destination); } else if (Set.of("ADRP", "ADD", "ADR").contains(instruction.getMnemonicString().toUpperCase(Locale.ROOT))) { selectorAliases.remove(normalizedRegister(parts[0])); boundedIndexes.remove(normalizedRegister(parts[0])); } } boolean selectorFeedsTable = boundedIndexes.contains(indexRegister); if (compareIndex < loadIndex - 3 && selectorFeedsTable && supportedSequence) { entryCount = (int) upperBound + 1; defaultTarget = flows[0]; break; } } if (entryCount > 0) break; } if (entryCount <= 0 || entryCount > 4096 || defaultTarget == null) continue; List mappings = new ArrayList<>(); Set mappedTargets = new HashSet<>(); boolean valid = true; for (int caseValue = 0; caseValue < entryCount; caseValue += 1) { Address entry = tableAddress.add((long) caseValue * entrySize); int encodedOffset = entrySize == 1 ? currentProgram.getMemory().getByte(entry) & 0xff : currentProgram.getMemory().getShort(entry) & 0xffff; Address target = branchBase.add((long) encodedOffset * 4L); String canonicalTarget = canonicalAddress(target); if (!recoveredTargetSet.contains(canonicalTarget)) { valid = false; break; } mappedTargets.add(canonicalTarget); mappings.add(new RecoveredAArch64Mapping(caseValue, target)); } if (!valid || mappedTargets.contains(canonicalAddress(defaultTarget))) continue; Set accountedTargets = new HashSet<>(mappedTargets); accountedTargets.add(canonicalAddress(defaultTarget)); if (!accountedTargets.equals(recoveredTargetSet)) continue; return new RecoveredAArch64RelativeTable(tableAddress, branchBase, entrySize, List.copyOf(mappings)); } return null; } private static String operands(Instruction instruction) { List values = new ArrayList<>(); for (int index = 0; index < instruction.getNumOperands(); index += 1) { values.add(instruction.getDefaultOperandRepresentation(index)); } return String.join(", ", values); } private static String normalizedRegister(String value) { String register = value.trim().toLowerCase(Locale.ROOT); return register.matches("[wx]\\d+") ? "r" + register.substring(1) : register; } private static long parseInteger(String value) { String number = value.toLowerCase(Locale.ROOT); return number.startsWith("0x") ? Long.parseUnsignedLong(number.substring(2), 16) : Long.parseLong(number); } private Address addressFromOperand(String value) { String hex = value.trim().replaceFirst("(?i)^0x", ""); return currentProgram.getAddressFactory().getDefaultAddressSpace() .getAddress(Long.parseUnsignedLong(hex, 16)); } private List
dispatchPathDataReferences(JumpTable jumpTable) throws Exception { BasicBlockModel model = new BasicBlockModel(currentProgram, false); CodeBlock dispatchBlock = model.getFirstCodeBlockContaining( jumpTable.getSwitchAddress(), monitor ); if (dispatchBlock == null) { return List.of(); } List blocks = new ArrayList<>(); blocks.add(dispatchBlock); CodeBlockReferenceIterator sources = dispatchBlock.getSources(monitor); while (sources.hasNext()) { monitor.checkCancelled(); CodeBlock sourceBlock = sources.next().getSourceBlock(); if (sourceBlock != null) { blocks.add(sourceBlock); } } Set
addresses = new HashSet<>(); for (CodeBlock block : blocks) { InstructionIterator instructions = currentProgram .getListing() .getInstructions(block, true); while (instructions.hasNext()) { monitor.checkCancelled(); Instruction instruction = instructions.next(); for ( Reference reference : currentProgram .getReferenceManager() .getReferencesFrom(instruction.getAddress()) ) { if ( reference.getReferenceType().isData() && currentProgram .getMemory() .getLoadedAndInitializedAddressSet() .contains(reference.getToAddress()) ) { addresses.add(reference.getToAddress()); } } } } List
ordered = new ArrayList<>(addresses); ordered.sort(Address::compareTo); return List.copyOf(ordered); } private static JsonObject inferenceEvidence( String kind, String source, String detail ) { JsonObject evidence = new JsonObject(); evidence.addProperty("kind", kind); evidence.addProperty("source", source); evidence.addProperty("detail", detail); return evidence; } private static String inferenceConfidence( String signatureSource, boolean typeLocked ) { if ( typeLocked || signatureSource.equals("user_defined") || signatureSource.equals("imported") ) { return "high"; } return signatureSource.equals("analysis") ? "medium" : "low"; } private DecompileResults decompile(Function function) { if (function.isExternal() || function.getBody().isEmpty()) { return null; } DecompileResults results = decompiler.decompileFunction( function, 0, monitor ); if (results.isCancelled()) { throw new RequestFailure("decompile_cancelled", "Ghidra decompilation was cancelled"); } if (!results.decompileCompleted()) { throw new RequestFailure( "decompile_failed", "Ghidra decompilation failed: " + diagnosticMessage(results.getErrorMessage()) ); } return results; } private static String decompiledText(DecompileResults results) { if (results == null) { return null; } DecompiledFunction value = results.getDecompiledFunction(); return value == null ? null : value.getC(); } private InstructionScan scanInstructions(Function function, int maximum) throws Exception { InstructionIterator iterator = currentProgram.getListing().getInstructions( function.getBody(), true ); List instructions = new ArrayList<>(); while (iterator.hasNext() && instructions.size() < maximum) { monitor.checkCancelled(); instructions.add(iterator.next()); } return new InstructionScan(List.copyOf(instructions), iterator.hasNext()); } private static String renderAssembly(List instructions) { return String.join("\n", renderAssemblyLines(instructions)); } private static List renderAssemblyLines(List instructions) { List result = new ArrayList<>(); for (Instruction instruction : instructions) { StringBuilder line = new StringBuilder(); line.append(canonicalAddress(instruction.getAddress())); line.append(": "); line.append(instruction.getMnemonicString()); for (int index = 0; index < instruction.getNumOperands(); index += 1) { line.append(index == 0 ? " " : ", "); line.append(instruction.getDefaultOperandRepresentation(index)); } result.add(line.toString()); } return result; } private List collectReferences( List instructions, String direction) throws Exception { TreeMap observed = new TreeMap<>(); for (Instruction instruction : instructions) { monitor.checkCancelled(); if (direction.equals("outgoing")) { for (Reference reference : currentProgram.getReferenceManager() .getReferencesFrom(instruction.getAddress())) { addReference(observed, reference); } } else { ReferenceIterator iterator = currentProgram.getReferenceManager() .getReferencesTo(instruction.getAddress()); while (iterator.hasNext()) { addReference(observed, iterator.next()); } } } List result = new ArrayList<>(observed.values()); result.sort(REFERENCE_ORDER); return result; } private static void addReference(TreeMap observed, Reference reference) { if (reference.isEntryPointReference()) { return; } String key = canonicalAddress(reference.getFromAddress()) + "\u0000" + canonicalAddress(reference.getToAddress()) + "\u0000" + reference.getReferenceType().getName() + "\u0000" + reference.getOperandIndex() + "\u0000" + reference.isPrimary(); observed.putIfAbsent(key, reference); } private JsonArray referenceEdges(List references) { JsonArray result = new JsonArray(); for (Reference reference : references) { result.add(referenceEdge(reference)); } return result; } private JsonObject referenceEdge(Reference reference) { JsonObject result = new JsonObject(); result.addProperty("source_address", canonicalAddress(reference.getFromAddress())); result.addProperty("target_address", canonicalAddress(reference.getToAddress())); Function source = containingFunction(reference.getFromAddress()); Function target = containingFunction(reference.getToAddress()); result.add( "source_procedure", source == null ? JsonNull.INSTANCE : procedureIdentity(source) ); result.add( "target_procedure", target == null ? JsonNull.INSTANCE : procedureIdentity(target) ); result.add("kind", referenceKind(reference)); return result; } private static JsonObject referenceKind(Reference reference) { RefType type = reference.getReferenceType(); JsonObject result = new JsonObject(); result.addProperty("available", true); result.addProperty("provenance", "ghidra-reference-manager"); result.addProperty("type", type.getName()); result.addProperty("flow", type.isFlow()); result.addProperty("call", type.isCall()); result.addProperty("jump", type.isJump()); result.addProperty("data", type.isData()); result.addProperty("read", type.isRead()); result.addProperty("write", type.isWrite()); result.addProperty("indirect", type.isIndirect()); result.addProperty("computed", type.isComputed()); result.addProperty("conditional", type.isConditional()); result.addProperty("terminal", type.isTerminal()); result.addProperty("primary", reference.isPrimary()); result.addProperty("operand_index", reference.getOperandIndex()); result.addProperty("external", reference.isExternalReference()); return result; } private Function containingFunction(Address address) { Function result = currentProgram.getFunctionManager().getFunctionAt(address); if (result == null && address.isMemoryAddress()) { result = currentProgram.getFunctionManager().getFunctionContaining(address); } return result; } private static String procedureName(Function function) { Symbol symbol = function.getSymbol(); return symbol == null ? function.getName() : symbol.getName(true); } private static JsonObject functionClassification(Function function) { JsonObject result = new JsonObject(); result.addProperty("external", function.isExternal()); result.addProperty("thunk", function.isThunk()); Function target = function.isThunk() ? function.getThunkedFunction(false) : null; if (target == null) { result.add("thunk_target", JsonNull.INSTANCE); } else { result.addProperty("thunk_target", canonicalAddress(target.getEntryPoint())); } result.addProperty("provenance", "ghidra-function-manager"); return result; } private static JsonArray functionLocals(Function function) { Variable[] variables = function.getLocalVariables(); List ordered = new ArrayList<>(List.of(variables)); ordered.sort( Comparator.comparing(Variable::getName) .thenComparing(variable -> variable.getVariableStorage().toString()) ); JsonArray result = new JsonArray(); for (Variable variable : ordered) { JsonObject item = new JsonObject(); item.addProperty( "description", variable.getDataType().getDisplayName() + " " + variable.getName() + " @ " + variable.getVariableStorage() ); item.addProperty("provenance", "ghidra-function-database"); result.add(item); } return result; } private JsonArray procedureIdentities(Set functions) { List ordered = new ArrayList<>(functions); ordered.sort(Comparator.comparing(Function::getEntryPoint)); JsonArray result = new JsonArray(); for (Function function : ordered) { result.add(procedureIdentity(function)); } return result; } private List basicBlocks(Function function) throws Exception { BasicBlockModel model = new BasicBlockModel(currentProgram, false); CodeBlockIterator iterator = model.getCodeBlocksContaining(function.getBody(), monitor); List result = new ArrayList<>(); while (iterator.hasNext()) { monitor.checkCancelled(); result.add(iterator.next()); } result.sort(Comparator.comparing(CodeBlock::getFirstStartAddress)); return result; } private JsonArray basicBlockValues(Function function) throws Exception { JsonArray result = new JsonArray(); for (CodeBlock block : basicBlocks(function)) { JsonObject item = new JsonObject(); item.addProperty("start", canonicalAddress(block.getFirstStartAddress())); item.addProperty("end", exclusiveAddress(block.getMaxAddress())); Set
successors = new HashSet<>(); CodeBlockReferenceIterator destinations = block.getDestinations(monitor); while (destinations.hasNext()) { CodeBlockReference destination = destinations.next(); CodeBlock destinationBlock = destination.getDestinationBlock(); if (destinationBlock == null) { continue; } Address address = destinationBlock.getFirstStartAddress(); if ( !destination.getFlowType().isCall() && function.getBody().contains(address) ) { successors.add(address); } } List
ordered = new ArrayList<>(successors); ordered.sort(Address::compareTo); JsonArray values = new JsonArray(); for (Address address : ordered) { values.add(canonicalAddress(address)); } item.add("successors", values); result.add(item); } return result; } private static JsonArray comments(List instructions) { TreeMap observed = new TreeMap<>(); for (Instruction instruction : instructions) { for (CommentType type : CommentType.values()) { String text = instruction.getComment(type); if (text == null || text.isEmpty()) { continue; } String kind = type == CommentType.EOL ? "inline" : "comment"; JsonObject item = new JsonObject(); item.addProperty("address", canonicalAddress(instruction.getAddress())); item.addProperty("kind", kind); item.addProperty("text", text); String key = canonicalAddress(instruction.getAddress()) + "\u0000" + kind + "\u0000" + type.name() + "\u0000" + text; observed.put(key, item); } } JsonArray result = new JsonArray(); observed.values().forEach(result::add); return result; } private JsonArray referencedStrings(List references) { TreeMap observed = new TreeMap<>(); for (Reference reference : references) { Data data = currentProgram.getListing().getDefinedDataContaining( reference.getToAddress() ); if (data == null || !data.hasStringValue()) { continue; } String value = StringDataInstance.getStringDataInstance(data).getStringValue(); if (value == null) { continue; } JsonObject item = new JsonObject(); item.addProperty("address", canonicalAddress(data.getAddress())); item.addProperty("value", value); item.addProperty("source_address", canonicalAddress(reference.getFromAddress())); String key = canonicalAddress(data.getAddress()) + "\u0000" + value + "\u0000" + canonicalAddress(reference.getFromAddress()); observed.put(key, item); } JsonArray result = new JsonArray(); observed.values().forEach(result::add); return result; } private JsonArray referencedNames(List references) { TreeMap observed = new TreeMap<>(); for (Reference reference : references) { Symbol symbol = currentProgram.getSymbolTable().getPrimarySymbol( reference.getToAddress() ); if (symbol == null) { continue; } JsonObject item = new JsonObject(); item.addProperty("address", canonicalAddress(reference.getToAddress())); item.addProperty("value", symbol.getName(true)); item.addProperty("source_address", canonicalAddress(reference.getFromAddress())); String key = canonicalAddress(reference.getToAddress()) + "\u0000" + symbol.getName(true) + "\u0000" + canonicalAddress(reference.getFromAddress()); observed.put(key, item); } JsonArray result = new JsonArray(); observed.values().forEach(result::add); return result; } private static String exclusiveAddress(Address inclusive) { Address next = inclusive.next(); if (next != null && next.getAddressSpace().equals(inclusive.getAddressSpace())) { return canonicalAddress(next); } BigInteger offset = new BigInteger(Long.toUnsignedString(inclusive.getOffset())); return canonicalOffset(inclusive.getAddressSpace(), offset.add(BigInteger.ONE)); } private static String diagnosticMessage(String value) { if (value == null || value.isBlank()) { return "native decompiler returned no detail"; } return value.replaceAll("[\\r\\n]+", " "); } private JsonArray inventory( List inventory, String factsName) throws Exception { JsonArray result = new JsonArray(); for (InventoryItem item : inventory) { monitor.checkCancelled(); result.add(resultItem(item, factsName)); } return result; } private static JsonObject resultItem(InventoryItem item, String factsName) { JsonObject result = new JsonObject(); result.addProperty("address", canonicalAddress(item.address)); result.addProperty("value", item.value); if (factsName != null) { result.add(factsName, item.facts.deepCopy()); } return result; } private static JsonObject memoryRegion(MemoryBlock block, String imageBase) { JsonObject permissions = new JsonObject(); permissions.addProperty("available", true); permissions.addProperty("source", "ghidra-memory-block"); JsonObject result = new JsonObject(); result.addProperty("name", block.getName()); result.addProperty("start", canonicalAddress(block.getStart())); result.addProperty("end", exclusiveEnd(block)); result.addProperty("readable", block.isRead()); result.addProperty("writable", block.isWrite()); result.addProperty("executable", block.isExecute()); result.add("permissions", permissions); result.addProperty("provenance", "ghidra-memory-block"); result.addProperty("address_space", block.getStart().getAddressSpace().getName()); result.addProperty("image_base", imageBase); result.addProperty("initialized", block.isInitialized()); result.addProperty("overlay", block.isOverlay()); return result; } private static JsonObject procedureIdentity(Function function) { JsonObject result = new JsonObject(); result.addProperty("address", canonicalAddress(function.getEntryPoint())); result.addProperty("name", procedureName(function)); result.add("classification", functionClassification(function)); result.add("body", functionBody(function)); return result; } /** Observe the entire AddressSet; range endpoints are inclusive, not a guessed span. */ private static JsonObject functionBody(Function function) { JsonArray ranges = new JsonArray(); BigInteger total = BigInteger.ZERO; Address first = null; Address last = null; AddressSpace space = null; boolean multipleSpaces = false; AddressRangeIterator iterator = function.getBody().getAddressRanges(true); while (iterator.hasNext()) { AddressRange range = iterator.next(); Address start = range.getMinAddress(); Address end = range.getMaxAddress(); if (start.getAddressSpace().getAddressableUnitSize() != 1) { throw new IllegalStateException("Function body byte coverage requires a byte-addressed space"); } JsonObject value = new JsonObject(); value.addProperty("start", canonicalAddress(start)); value.addProperty("end", canonicalAddress(end)); ranges.add(value); BigInteger begin = new BigInteger(Long.toUnsignedString(start.getOffset())); BigInteger finish = new BigInteger(Long.toUnsignedString(end.getOffset())); total = total.add(finish.subtract(begin).add(BigInteger.ONE)); if (first == null) { first = start; space = start.getAddressSpace(); } else if (!space.equals(start.getAddressSpace())) { multipleSpaces = true; } last = end; } JsonObject result = new JsonObject(); result.addProperty("available", true); result.addProperty("provenance", "ghidra-function-body-address-set"); result.add("ranges", ranges); result.addProperty("total_bytes", safeBodyByteCount(total)); if (multipleSpaces) { result.add("span_bytes", JsonNull.INSTANCE); } else { BigInteger span = first == null ? BigInteger.ZERO : new BigInteger(Long.toUnsignedString(last.getOffset())) .subtract(new BigInteger(Long.toUnsignedString(first.getOffset()))) .add(BigInteger.ONE); result.addProperty("span_bytes", safeBodyByteCount(span)); } result.addProperty("non_contiguous", ranges.size() > 1); result.addProperty("contains_entry", function.getBody().contains(function.getEntryPoint())); return result; } private static long safeBodyByteCount(BigInteger value) { if (value.signum() < 0 || value.compareTo(BigInteger.valueOf(9007199254740991L)) > 0) { throw new IllegalStateException("Function body byte coverage exceeds exact JSON integer range"); } return value.longValueExact(); } private static String exclusiveEnd(MemoryBlock block) { Address next = block.getEnd().next(); if (next != null && next.getAddressSpace().equals(block.getStart().getAddressSpace())) { return canonicalAddress(next); } BigInteger start = new BigInteger(Long.toUnsignedString(block.getStart().getOffset())); return canonicalOffset( block.getStart().getAddressSpace(), start.add(BigInteger.valueOf(block.getSize())) ); } private static String canonicalAddress(Address address) { return canonicalOffset( address.getAddressSpace(), new BigInteger(Long.toUnsignedString(address.getOffset())) ); } private static String canonicalOffset(AddressSpace space, BigInteger offset) { String value = "0x" + offset.toString(16); if (space.equals(sessionDefaultAddressSpace)) { return value; } return encodeAddressSpace(space.getName()) + ":" + value; } private Address requireAddress(JsonObject params, String name) { return parseReaAddress(requireString(params, name)); } private Address parseReaAddress(String value) { Address address = tryParseAddress(value); if (address == null) { throw new RequestFailure("invalid_request", "Unknown or invalid Ghidra address"); } return address; } private Address tryParseAddress(String value) { try { String spaceName = null; String offset = value; int separator = value.lastIndexOf(":0x"); if (separator >= 0) { spaceName = decodeAddressSpace(value.substring(0, separator)); offset = value.substring(separator + 3); } else if (value.startsWith("0x")) { offset = value.substring(2); } if (!offset.matches("[0-9A-Fa-f]+")) { return null; } AddressSpace space = spaceName == null ? currentProgram.getAddressFactory().getDefaultAddressSpace() : currentProgram.getAddressFactory().getAddressSpace(spaceName); return space == null ? null : space.getAddress(offset); } catch (Exception exception) { return null; } } private static String encodeAddressSpace(String value) { StringBuilder result = new StringBuilder(); for (byte item : value.getBytes(StandardCharsets.UTF_8)) { int unsigned = Byte.toUnsignedInt(item); if ((unsigned >= 'A' && unsigned <= 'Z') || (unsigned >= 'a' && unsigned <= 'z') || (unsigned >= '0' && unsigned <= '9') || unsigned == '.' || unsigned == '_' || unsigned == '~' || unsigned == '-') { result.append((char) unsigned); } else { result.append('%'); result.append(Character.toUpperCase(Character.forDigit(unsigned >>> 4, 16))); result.append(Character.toUpperCase(Character.forDigit(unsigned & 0xf, 16))); } } return result.toString(); } private static String decodeAddressSpace(String value) { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); for (int index = 0; index < value.length();) { char item = value.charAt(index); if (item == '%') { if (index + 2 >= value.length()) { throw new RequestFailure("invalid_request", "Address space encoding is invalid"); } int high = Character.digit(value.charAt(index + 1), 16); int low = Character.digit(value.charAt(index + 2), 16); if (high < 0 || low < 0) { throw new RequestFailure("invalid_request", "Address space encoding is invalid"); } bytes.write((high << 4) | low); index += 3; } else if ((item >= 'A' && item <= 'Z') || (item >= 'a' && item <= 'z') || (item >= '0' && item <= '9') || item == '.' || item == '_' || item == '~' || item == '-') { bytes.write((byte) item); index += 1; } else { throw new RequestFailure("invalid_request", "Address space encoding is invalid"); } } return bytes.toString(StandardCharsets.UTF_8); } private static String normalizedSymbolType(Symbol symbol) { String value = symbol.getSymbolType().toString() .toLowerCase(Locale.ROOT) .replaceAll("[^a-z0-9]+", "_") .replaceAll("^_+|_+$", ""); return value.isEmpty() ? "unknown" : value; } private static String normalizedSource(SourceType source) { return source.name().toLowerCase(Locale.ROOT); } private static ValueMatcher literalMatcher(String pattern, boolean caseSensitive) { String needle = caseSensitive ? pattern : pattern.toLowerCase(Locale.ROOT); return value -> { String candidate = caseSensitive ? value : value.toLowerCase(Locale.ROOT); return candidate.contains(needle); }; } private static ValueMatcher regexMatcher(String expression, boolean caseSensitive) { Pattern pattern; try { pattern = Pattern.compile( expression, caseSensitive ? 0 : Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE ); } catch (PatternSyntaxException exception) { throw new RequestFailure("invalid_request", "Invalid regex pattern"); } return value -> pattern.matcher(value).find(); } private static SessionDescriptor readDescriptor(Path path) throws IOException { if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) { throw new IllegalArgumentException("REA session descriptor is invalid"); } JsonElement parsed = JsonParser.parseString(Files.readString(path, StandardCharsets.UTF_8)); Set keys = new HashSet<>(DESCRIPTOR_KEYS); if (parsed.isJsonObject() && parsed.getAsJsonObject().has("analysis_extensions")) keys.add("analysis_extensions"); JsonObject object = requireObject(parsed, keys); return new SessionDescriptor( requireString(object, "transport"), requireString(object, "endpoint_path"), requireString(object, "token"), requireString(object, "run_id"), requireSha256(object, "target_sha256"), requireString(object, "provider_version"), requireString(object, "profile_digest"), object.has("analysis_extensions") ? object.getAsJsonArray("analysis_extensions") : new JsonArray() ); } private static Request parseRequest(String line, String expectedToken) { JsonObject object = requireObject(JsonParser.parseString(line), REQUEST_KEYS); int id = requireInteger(object, "id"); if (id <= 0 || !constantTimeEquals(requireString(object, "token"), expectedToken)) { throw new IllegalArgumentException("Bridge request authentication failed"); } JsonElement params = object.get("params"); if (params == null || !params.isJsonObject()) { throw new IllegalArgumentException("Bridge request parameters are invalid"); } return new Request(id, requireString(object, "method"), params.getAsJsonObject()); } private static JsonObject requireObject(JsonElement element, Set keys) { if (!element.isJsonObject()) { throw new IllegalArgumentException("JSON object required"); } JsonObject object = element.getAsJsonObject(); if (!object.keySet().equals(keys)) { throw new IllegalArgumentException("JSON object fields are invalid"); } return object; } private static void requireKeys(JsonObject object, Set keys) { if (!object.keySet().equals(keys)) { throw new RequestFailure("invalid_request", "Bridge request fields are invalid"); } } private void requireDocument(JsonObject params) { String document = optionalString(params, "document"); if (document != null && !document.equals(currentProgram.getName())) { throw new RequestFailure("not_found", "Unknown Ghidra Program identity"); } } private static String optionalString(JsonObject object, String name) { JsonElement value = object.get(name); if (value == null || value.isJsonNull()) { return null; } if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString()) { throw new RequestFailure("invalid_request", name + " must be a string or null"); } String result = value.getAsString(); if (result.isEmpty()) { throw new RequestFailure("invalid_request", name + " cannot be empty"); } return result; } private static String requireString(JsonObject object, String name) { JsonElement value = object.get(name); if (value == null || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString()) { throw new RequestFailure("invalid_request", name + " must be a string"); } String result = value.getAsString(); if (result.isEmpty()) { throw new RequestFailure("invalid_request", name + " cannot be empty"); } return result; } private static String requireSha256(JsonObject object, String name) { String value = requireString(object, name); if (!value.matches("[a-f0-9]{64}")) { throw new IllegalArgumentException(name + " must be lowercase SHA-256"); } return value; } private static int requireInteger(JsonObject object, String name) { JsonElement value = object.get(name); if (value == null || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isNumber()) { throw new IllegalArgumentException("JSON integer required"); } int result = value.getAsInt(); if (value.getAsDouble() != result) { throw new IllegalArgumentException("JSON integer required"); } return result; } private static int requireBoundedInteger( JsonObject object, String name, int minimum, int maximum) { try { int value = requireInteger(object, name); if (value < minimum || value > maximum) { throw new RequestFailure( "invalid_request", name + " is outside its supported range" ); } return value; } catch (IllegalArgumentException exception) { throw new RequestFailure("invalid_request", name + " must be an integer"); } } private static boolean requireBoolean(JsonObject object, String name) { JsonElement value = object.get(name); if (value == null || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isBoolean()) { throw new RequestFailure("invalid_request", name + " must be a boolean"); } return value.getAsBoolean(); } private static boolean constantTimeEquals(String provided, String expected) { return MessageDigest.isEqual( provided.getBytes(StandardCharsets.UTF_8), expected.getBytes(StandardCharsets.UTF_8) ); } private static void writeSuccess(BufferedWriter writer, int id, JsonElement result) throws IOException { JsonObject response = new JsonObject(); response.addProperty("id", id); response.addProperty("ok", true); response.add("result", result); writeResponse(writer, response); } private static void writeFailure( BufferedWriter writer, int id, String code, String message) throws IOException { JsonObject error = new JsonObject(); error.addProperty("code", code); error.addProperty("message", message); JsonObject response = new JsonObject(); response.addProperty("id", id); response.addProperty("ok", false); response.add("error", error); writeResponse(writer, response); } private static void writeResponse(BufferedWriter writer, JsonObject response) throws IOException { String encoded = GSON.toJson(response); writer.write(encoded); writer.newLine(); writer.flush(); } private static String safeMessage(Exception exception) { String message = exception.getMessage(); return message == null || message.isBlank() ? "operation failed" : message; } private interface ValueMatcher { boolean matches(String value); } private static final Comparator INVENTORY_ORDER = Comparator.comparing(InventoryItem::address) .thenComparing(InventoryItem::value) .thenComparing(item -> GSON.toJson(item.facts)); private static final Comparator REFERENCE_ORDER = Comparator.comparing(Reference::getFromAddress) .thenComparing(Reference::getToAddress) .thenComparing(reference -> reference.getReferenceType().getName()) .thenComparingInt(Reference::getOperandIndex) .thenComparing(Reference::isPrimary); private record InventoryItem(Address address, String value, JsonObject facts) {} private record FunctionEntry(Function function, InventoryItem item) {} private record InstructionScan(List instructions, boolean truncated) {} private record SessionDescriptor( String transport, String endpointPath, String token, String runId, String targetSha256, String providerVersion, String profileDigest, JsonArray analysisExtensions ) {} private record Request(int id, String method, JsonObject params) {} private static final class RequestFailure extends RuntimeException { private final String code; RequestFailure(String code, String message) { super(message); this.code = code; } } }