Files
rea/bridge/ghidra/ReaGhidraBridge.java
T
Giridharandmorluto 6e0d5d64d3 feat(ghidra): admit set_address_name and set_addresses_names (#1635)
* feat(ghidra): admit set_address_name and set_addresses_names

Ghidra sessions rejected the existing naming tools with "operation is not declared by this provider", so agents had to use annotate_native_function, which returns a complete function dossier per rename and cannot name data addresses.

The Java bridge now names a local function entry (namespace-aware, like annotate_native_function) or creates/renames the primary label at any other mapped address, verifying readback. set_addresses_names applies each address in its own transaction and reports per-address booleans. Both are mutating capabilities: excluded from read-only (Windows P0) handshakes and from snapshot replay.

* fix(ghidra): normalize qualified symbol names and reject empty leaves

Share namespace-aware leaf normalization across functions and primary labels. Invalid qualified leaves now return false for naming operations without marking the provider unavailable; function annotations retain typed invalid-input errors.

Extend real CLI/MCP namespace verification with namespaced globals, readback replay, failed-name rollback, mixed batches, and capability checks.

Validation: 75 focused tests and npm run check pass. Ghidra 12.1.4/JDK 21 namespace verification passes on macOS arm64 with unchanged source bytes and complete cleanup. The full host lane passes this workflow before the existing entry-alias fixture fails because the compiler places its interior label at the function entry.

---------

Co-authored-by: morluto <[email protected]>
2026-10-11 04:35:38 +08:00

3503 lines
164 KiB
Java

// 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.AddressIterator;
import ghidra.program.model.address.AddressSet;
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<String> DESCRIPTOR_KEYS = Set.of(
"transport",
"endpoint_path",
"token",
"run_id",
"target_sha256",
"provider_version",
"profile_digest"
);
private static final Set<String> REQUEST_KEYS = Set.of(
"id",
"token",
"method",
"params"
);
private static final String[] CAPABILITIES = {
"annotate_native_function",
"set_address_name",
"set_addresses_names",
"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<InventoryItem> nameInventory;
private List<FunctionEntry> procedureInventory;
private List<InventoryItem> 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 "set_address_name", "set_addresses_names" -> {
if (!descriptor.transport.equals("unix-socket"))
throw new RequestFailure("method_unavailable", "Windows P0 does not admit database mutation");
yield request.method.equals("set_address_name")
? setAddressName(request.params) : setAddressesNames(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")
|| value.equals("set_address_name") || value.equals("set_addresses_names")))
.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<FileBytes> 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<Relocation> 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<Address> 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<Function> observed = callers
? function.getCallingFunctions(monitor)
: function.getCalledFunctions(monitor);
List<Function> 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");
}
List<Reference> references = collectReferences(function, 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 : scanInstructions(function, Integer.MAX_VALUE).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();
JsonObject kind = referenceKind(reference);
item.addProperty("source_address", canonicalAddress(reference.getFromAddress()));
item.addProperty("target_address", canonicalAddress(reference.getToAddress()));
for (String field : List.of("type", "call", "jump", "indirect", "computed",
"operand_index", "data", "read", "write", "primary", "provenance"))
item.add(field, kind.get(field));
item.addProperty("source", normalizedSource(reference.getSource()));
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();
JsonArray flowEvidence = 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();
JsonObject listingFlow = new JsonObject();
listingFlow.addProperty("source", "ghidra-listing-flow-type");
listingFlow.addProperty("value", type.getName());
flowEvidence.add(listingFlow);
boolean decodedReturn = false;
for (PcodeOp operation : instruction.getPcode()) {
if (operation.getOpcode() != PcodeOp.RETURN) continue;
decodedReturn = true;
JsonObject returnEvidence = new JsonObject();
returnEvidence.addProperty("source", "ghidra-instruction-pcode");
returnEvidence.addProperty("value", operation.getMnemonic());
flowEvidence.add(returnEvidence);
}
flow.addProperty("kind", type.isCall() ? "call" : type.isJump() ? "jump" : decodedReturn ? "return" : 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);
flow.add("classification_evidence", flowEvidence);
result.add("operands", operands);
result.add("flow", flow);
result.add("references", references);
List<String> limitations = new ArrayList<>(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."));
if (flow.get("kind").getAsString().equals("terminal")) limitations.add("No RETURN operation was observed in the provider's instruction p-code; terminal classification remains generic.");
result.add("limitations", GSON.toJsonTree(limitations));
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<String, JsonArray> 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<String, JsonArray> 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<Address> sources = new HashSet<>();
while (references.hasNext()) {
monitor.checkCancelled();
Reference reference = references.next();
if (reference.isEntryPointReference()) {
continue;
}
sources.add(reference.getFromAddress());
}
List<Address> 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));
for (String field : List.of("name", "comment", "inline_comment")) {
if (params.has(field)) validateAnnotationText(requireText(params, field), field);
}
int transaction = currentProgram.startTransaction("REA function annotations");
boolean commit = false;
try {
String leafName = params.has("name")
? symbolLeafName(function.getSymbol(), requireString(params, "name")) : null;
// 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(leafName, 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") ? leafName : 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 String symbolLeafName(Symbol symbol, String requested) throws ghidra.util.exception.InvalidInputException {
// Fully qualified readback must be usable as an idempotent rename input.
if (requested.equals(symbol.getName(true))) return symbol.getName();
String namespace = symbol.getParentNamespace().isGlobal()
? "" : symbol.getParentNamespace().getName(true);
String prefix = namespace + "::";
if (!namespace.isEmpty() && requested.startsWith(prefix)) {
String leaf = requested.substring(prefix.length());
if (!leaf.isEmpty()) return leaf;
throw new ghidra.util.exception.InvalidInputException(
"Symbol name has an empty leaf within namespace " + namespace + " at " +
canonicalAddress(symbol.getAddress()) + ": " + requested);
}
return requested;
}
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 static void validateAnnotationText(String text, String field) {
for (int index = 0; index < text.length(); index++) {
char character = text.charAt(index);
if (character == 0)
throw new RequestFailure("invalid_request", "Annotation " + field + " contains NUL at UTF-16 index " + index + "; Ghidra's decompiler cannot marshal NUL text");
if (Character.isHighSurrogate(character) && index + 1 < text.length() &&
Character.isLowSurrogate(text.charAt(index + 1))) {
index++;
continue;
}
if (Character.isSurrogate(character))
throw new RequestFailure("invalid_request", "Annotation " + field + " contains an unpaired Unicode surrogate at UTF-16 index " + index + "; Ghidra cannot preserve it as annotation text");
}
}
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<Reference> incomingReferences = collectReferences(function, "incoming")
.stream()
.filter(reference -> !function.getBody().contains(reference.getFromAddress()))
.toList();
List<Reference> outgoingReferences = collectReferences(function, "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<PcodeOpAST> selected = new ArrayList<>();
int omittedOperationsLowerBound = 0;
Iterator<PcodeOpAST> 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<PcodeOpAST, String> 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));
}
// Analyst naming: a function entry renames the function, other addresses get a primary label.
private boolean applyAddressName(Address address, String name) throws Exception {
if (!currentProgram.getMemory().contains(address))
throw new RequestFailure("invalid_request", "Address is not mapped: " + canonicalAddress(address));
validateAnnotationText(name, "name");
int transaction = currentProgram.startTransaction("REA address name");
boolean commit = false;
try {
Function function = currentProgram.getFunctionManager().getFunctionAt(address);
String expected = name;
if (function != null) {
expected = symbolLeafName(function.getSymbol(), name);
function.setName(expected, SourceType.USER_DEFINED);
} else {
var table = currentProgram.getSymbolTable();
Symbol primary = table.getPrimarySymbol(address);
if (primary != null && !primary.isDynamic()) {
expected = symbolLeafName(primary, name);
primary.setName(expected, SourceType.USER_DEFINED);
} else {
Symbol created = table.createLabel(address, name, SourceType.USER_DEFINED);
created.setPrimary();
}
}
invalidateAnalysisCaches();
Symbol readback = currentProgram.getSymbolTable().getPrimarySymbol(address);
commit = readback != null && expected.equals(readback.getName());
return commit;
}
catch (ghidra.util.exception.InvalidInputException | ghidra.util.exception.DuplicateNameException exception) {
return false;
}
finally {
currentProgram.endTransaction(transaction, commit);
}
}
private JsonElement setAddressName(JsonObject params) throws Exception {
requireKeys(params, Set.of("document", "address", "name"));
requireDocument(params);
return GSON.toJsonTree(applyAddressName(requireAddress(params, "address"), requireString(params, "name")));
}
private JsonElement setAddressesNames(JsonObject params) throws Exception {
requireKeys(params, Set.of("document", "names"));
requireDocument(params);
if (!params.has("names") || !params.get("names").isJsonObject())
throw new RequestFailure("invalid_request", "names must be an object of address -> name");
JsonObject result = new JsonObject();
for (Map.Entry<String, JsonElement> entry : params.getAsJsonObject("names").entrySet()) {
boolean ok;
try {
ok = applyAddressName(parseReaAddress(entry.getKey()), entry.getValue().getAsString());
}
catch (RequestFailure failure) {
ok = false;
}
result.addProperty(entry.getKey(), ok);
}
return result;
}
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<InventoryItem> 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<InventoryItem> 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<MemoryBlock> 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<InventoryItem> 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<InventoryItem> 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) {
monitor.checkCancelled();
if (!matcher.matches(item.value)) {
continue;
}
items.add(resultItem(item, null));
}
return items;
}
private List<InventoryItem> names() throws Exception {
if (nameInventory != null) {
return nameInventory;
}
List<InventoryItem> 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<FunctionEntry> procedures() throws Exception {
if (procedureInventory != null) {
return procedureInventory;
}
List<FunctionEntry> result = new ArrayList<>();
Set<Address> 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<FunctionEntry> destination,
Set<Address> 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<InventoryItem> strings() throws Exception {
if (stringInventory != null) {
return stringInventory;
}
List<InventoryItem> 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 {
// Explicit address syntax wins; an unprefixed identifier is first an
// exact symbol name, even when every character is a hexadecimal digit.
boolean explicitAddress = isExplicitReaAddress(value);
if (!explicitAddress) {
List<Function> matches = new ArrayList<>();
for (FunctionEntry entry : procedures()) {
monitor.checkCancelled();
Function function = entry.function;
Symbol symbol = function.getSymbol();
String qualified = symbol == null ? function.getName() : symbol.getName(true);
boolean matchesName = function.getName().equals(value) || qualified.equals(value);
if (!matchesName) {
// Imported labels (for example Mach-O _main) can share a
// function entry without becoming its primary symbol. Only
// exact entries qualify; an interior label is not a selector.
for (Symbol entrySymbol : currentProgram.getSymbolTable().getSymbols(function.getEntryPoint())) {
monitor.checkCancelled();
if (entrySymbol.getName().equals(value) || entrySymbol.getName(true).equals(value)) {
matchesName = true;
break;
}
}
}
if (matchesName) matches.add(function);
}
if (matches.size() > 1)
throw new RequestFailure("ambiguous", "Ghidra procedure name is ambiguous: " + value +
"; select an exact entry address from " + matches.stream()
.map(function -> canonicalAddress(function.getEntryPoint())).toList());
if (matches.size() == 1) return matches.get(0);
}
// Retain legacy bare-hex address input only when no symbol matches.
Address address = explicitAddress ? parseReaAddress(value) : tryParseAddress(value);
if (address == null)
throw new RequestFailure("not_found", "Unknown Ghidra procedure name or address: " + value);
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: " + value);
return function;
}
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<ClangNode> 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<ClangNode> 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<Address> 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<Address> 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<Long, Address> 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<Address> 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<Long, Address> cases = new TreeMap<>();
private final Set<Address> defaults = new java.util.TreeSet<>();
private final Set<Address> unknownTargets = new HashSet<>();
private final Set<Long> ambiguousValues = new HashSet<>();
private final Set<String> limitations = new java.util.LinkedHashSet<>();
}
private TypedJumpTableCases typedJumpTableCases(
JumpTable jumpTable,
List<ClangNode> tokens
) throws Exception {
TypedJumpTableCases result = new TypedJumpTableCases();
Set<Address> 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<RecoveredAArch64Mapping> mappings;
private RecoveredAArch64RelativeTable(
Address tableAddress,
Address branchBase,
int entrySize,
List<RecoveredAArch64Mapping> 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<Instruction> 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<String> recoveredTargetSet = new HashSet<>();
for (Address target : recoveredTargets) recoveredTargetSet.add(canonicalAddress(target));
List<Address> 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<String> selectorAliases = new HashSet<>();
selectorAliases.add(comparedRegister);
Set<String> 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<RecoveredAArch64Mapping> mappings = new ArrayList<>();
Set<String> 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<String> 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<String> 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<Address> 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<CodeBlock> 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<Address> 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<Address> 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<Instruction> 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<Instruction> instructions) {
return String.join("\n", renderAssemblyLines(instructions));
}
private static List<String> renderAssemblyLines(List<Instruction> instructions) {
List<String> 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<Reference> collectReferences(
Function function,
String direction) throws Exception {
// References can target instruction interiors or originate in embedded
// data. Use actual body ownership, including an external's exact entry.
AddressSet scope = new AddressSet(function.getBody());
scope.add(function.getEntryPoint());
boolean outgoing = direction.equals("outgoing");
AddressIterator addresses = outgoing
? currentProgram.getReferenceManager().getReferenceSourceIterator(scope, true)
: currentProgram.getReferenceManager().getReferenceDestinationIterator(scope, true);
TreeMap<String, Reference> observed = new TreeMap<>();
while (addresses.hasNext()) {
monitor.checkCancelled();
Address address = addresses.next();
if (outgoing) {
for (Reference reference : currentProgram.getReferenceManager()
.getReferencesFrom(address)) {
monitor.checkCancelled();
addReference(observed, reference);
}
}
else {
ReferenceIterator iterator = currentProgram.getReferenceManager()
.getReferencesTo(address);
while (iterator.hasNext()) {
monitor.checkCancelled();
addReference(observed, iterator.next());
}
}
}
List<Reference> result = new ArrayList<>(observed.values());
result.sort(REFERENCE_ORDER);
return result;
}
private static void addReference(TreeMap<String, Reference> 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<Reference> 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<Variable> 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<Function> functions) {
List<Function> 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<CodeBlock> basicBlocks(Function function) throws Exception {
BasicBlockModel model = new BasicBlockModel(currentProgram, false);
CodeBlockIterator iterator = model.getCodeBlocksContaining(function.getBody(), monitor);
List<CodeBlock> 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<Address> 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<Address> 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<Instruction> instructions) {
TreeMap<String, JsonObject> 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<Reference> references) {
TreeMap<String, JsonObject> 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<Reference> references) {
TreeMap<String, JsonObject> 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<InventoryItem> 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 static boolean isExplicitReaAddress(String value) {
if (value.regionMatches(true, 0, "0x", 0, 2) &&
isHexadecimal(value, 2, value.length()))
return true;
int separator = value.toLowerCase(Locale.ROOT).lastIndexOf(":0x");
if (separator <= 0 || !isHexadecimal(value, separator + 3, value.length()))
return false;
// Classify the encoded space iteratively. A repeated regex alternation
// can exhaust Java's stack on a long ordinary symbol name.
for (int index = 0; index < separator; index++) {
char item = value.charAt(index);
if ((item >= 'A' && item <= 'Z') ||
(item >= 'a' && item <= 'z') ||
(item >= '0' && item <= '9') ||
item == '.' || item == '_' || item == '~' || item == '-')
continue;
if (item != '%' || index + 2 >= separator ||
!isHexadecimal(value, index + 1, index + 3))
return false;
index += 2;
}
return true;
}
private static boolean isHexadecimal(String value, int start, int end) {
if (start >= end) return false;
for (int index = start; index < end; index++) {
char item = value.charAt(index);
if (!((item >= '0' && item <= '9') ||
(item >= 'a' && item <= 'f') ||
(item >= 'A' && item <= 'F')))
return false;
}
return true;
}
private Address parseReaAddress(String value) {
Address address = tryParseAddress(value);
if (address == null) {
throw new RequestFailure("invalid_request", "Unknown or invalid Ghidra address: " + value);
}
return address;
}
private Address tryParseAddress(String value) {
try {
String spaceName = null;
String offset = value;
int separator = value.toLowerCase(Locale.ROOT).lastIndexOf(":0x");
if (separator >= 0) {
spaceName = decodeAddressSpace(value.substring(0, separator));
offset = value.substring(separator + 3);
}
else if (value.startsWith("0x") || value.startsWith("0X")) {
offset = value.substring(2);
}
if (!isHexadecimal(offset, 0, offset.length())) {
return null;
}
AddressSpace space = spaceName == null
? currentProgram.getAddressFactory().getDefaultAddressSpace()
: currentProgram.getAddressFactory().getAddressSpace(spaceName);
if (space == null) return null;
Address address = space.getAddress(offset);
BigInteger requestedOffset = new BigInteger(offset, 16);
BigInteger parsedOffset = new BigInteger(Long.toUnsignedString(address.getOffset()));
if (!requestedOffset.equals(parsedOffset))
throw new RequestFailure("invalid_request",
"Ghidra address offset cannot be represented without truncation in space " +
space.getName() + ": requested " + value + ", parsed " + canonicalAddress(address));
return address;
}
catch (RequestFailure failure) { throw failure; }
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) {
// Java wraps compiler stack exhaustion in PatternSyntaxException
// without retaining its cause. This producer reason is not bad syntax.
if (exception.getDescription().equals("Stack overflow during pattern compilation"))
throw regexStackExhausted(expression, "compiling");
throw new RequestFailure("invalid_request", "Invalid regex pattern " + expression + ": " + exception.getDescription() + " at index " + exception.getIndex());
}
catch (StackOverflowError exhausted) {
throw regexStackExhausted(expression, "compiling");
}
return value -> {
try {
return pattern.matcher(value).find();
}
catch (StackOverflowError exhausted) {
throw regexStackExhausted(expression, "matching");
}
};
}
private static RequestFailure regexStackExhausted(String expression, String stage) {
return new RequestFailure("regex_stack_exhausted",
"Ghidra's Java regex engine exhausted its stack while " + stage +
" pattern " + expression + ". Use literal mode or simplify the regex; the session remains usable.");
}
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<String> 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<String> 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<String> 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: " + document + "; active program: " + currentProgram.getName());
}
}
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);
// Gson leaves UTF-16 surrogates literal. Escape them in JSON so even
// diagnostics quoting ill-formed caller text remain lossless UTF-8.
int start = 0;
for (int index = 0; index < encoded.length(); index++) {
if (!Character.isSurrogate(encoded.charAt(index))) continue;
writer.write(encoded, start, index - start);
writer.write("\\u" + Integer.toHexString(encoded.charAt(index)));
start = index + 1;
}
writer.write(encoded, start, encoded.length() - start);
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<InventoryItem> INVENTORY_ORDER =
Comparator.comparing(InventoryItem::address)
.thenComparing(InventoryItem::value)
.thenComparing(item -> GSON.toJson(item.facts));
private static final Comparator<Reference> 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<Instruction> 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;
}
}
}