Files
rea/bridge/ghidra/ReaGhidraBridge.java
T

2681 lines
106 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.io.Reader;
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.List;
import java.util.Locale;
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.framework.Application;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressSpace;
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.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.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.HighSymbol;
import ghidra.program.model.pcode.JumpTable;
public final class ReaGhidraBridge extends HeadlessScript {
private static final int BRIDGE_VERSION = 6;
private static final int MAX_DESCRIPTOR_BYTES = 16 * 1024;
private static final int MAX_REQUEST_CHARACTERS = 256 * 1024;
private static final int MAX_RESPONSE_BYTES = 1024 * 1024;
private static final int RESPONSE_ENVELOPE_RESERVE_BYTES = 32 * 1024;
private static final int MAX_NATIVE_API_RESPONSE_BYTES = 192 * 1024;
private static final int MAX_INVENTORY_ITEMS = 1_000_000;
private static final int MAX_FUNCTION_ITEMS = 100_000;
private static final int MAX_FUNCTION_INSTRUCTIONS = 100_000;
private static final int MAX_NATIVE_API_PARAMETERS = 64;
private static final int MAX_NATIVE_API_JUMP_TABLES = 4;
private static final int MAX_NATIVE_API_LOAD_TABLES = 4;
private static final int MAX_NATIVE_API_MAPPINGS = 32;
private static final int DECOMPILE_TIMEOUT_SECONDS = 30;
private static final int DECOMPILE_PAYLOAD_MBYTES = 8;
private static final int MAX_LIST_VALUE_CODE_POINTS = 1_024;
private static final int MAX_SEARCH_VALUE_CODE_POINTS = 4_096;
private static final int MAX_REGEX_CANDIDATE_CHARACTERS = 4_096;
private static final long MAX_REGEX_BACKTRACKING_PATHS = 10_000;
private static final long MAX_LITERAL_SEARCH_WORK_UNITS = 1_000_000;
private static final long MAX_REGEX_SEARCH_WORK_UNITS = 1_000_000;
private static final Gson GSON = new GsonBuilder().serializeNulls().create();
private static final Set<String> DESCRIPTOR_KEYS = Set.of(
"schema_version",
"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 = {
"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",
"procedure_references",
"xrefs"
};
private List<InventoryItem> nameInventory;
private List<FunctionEntry> procedureInventory;
private List<InventoryItem> stringInventory;
private DecompInterface decompiler;
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);
Files.deleteIfExists(descriptorPath);
if (currentProgram == null) {
throw new IllegalStateException("REA bridge requires an imported program");
}
sessionDefaultAddressSpace =
currentProgram.getAddressFactory().getDefaultAddressSpace();
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"
);
}
try {
initializeDecompiler();
serve(descriptor);
}
finally {
if (decompiler != null) {
decompiler.dispose();
decompiler = null;
}
}
}
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("schema_version", 1);
endpoint.addProperty("host", "127.0.0.1");
endpoint.addProperty("port", address.getPort());
Files.writeString(
pendingPath,
GSON.toJson(endpoint) + "\n",
StandardCharsets.UTF_8,
StandardOpenOption.CREATE_NEW,
StandardOpenOption.WRITE
);
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 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 = readBoundedLine(reader, MAX_REQUEST_CHARACTERS);
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 "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 "xrefs" -> xrefs(request.params);
case "analyze_function" -> analyzeFunction(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("bridge_version", BRIDGE_VERSION);
result.addProperty("run_id", descriptor.runId);
result.addProperty("profile_digest", descriptor.profileDigest);
result.add("provider", provider);
result.addProperty("read_only", true);
result.addProperty("analysis_complete", !timedOut);
result.addProperty("analysis_timed_out", timedOut);
result.add("capabilities", GSON.toJsonTree(CAPABILITIES));
result.add("target", target);
return result;
}
private void initializeDecompiler() {
DecompileOptions options = new DecompileOptions();
options.setDefaultTimeout(DECOMPILE_TIMEOUT_SECONDS);
options.setMaxPayloadMBytes(DECOMPILE_PAYLOAD_MBYTES);
options.setMaxInstructions(MAX_FUNCTION_INSTRUCTIONS);
decompiler = new DecompInterface();
decompiler.setOptions(options);
decompiler.toggleCCode(true);
decompiler.toggleSyntaxTree(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, MAX_FUNCTION_INSTRUCTIONS);
if (scan.truncated) {
throw functionLimit("Procedure assembly exceeds the 100000-instruction limit");
}
return GSON.toJsonTree(renderAssembly(scan.instructions));
}
private JsonObject readFunctionInstructions(JsonObject params) throws Exception {
requireKeys(params, Set.of("document", "procedure", "offset", "limit"));
requireDocument(params);
Function function = resolveProcedure(requireString(params, "procedure"));
int offset = requireBoundedInteger(params, "offset", 0, 100_000);
int limit = requireBoundedInteger(params, "limit", 1, 500);
InstructionScan scan = scanInstructions(function, offset + limit + 1);
int start = Math.min(offset, scan.instructions.size());
int end = Math.min(scan.instructions.size(), start + limit);
JsonArray items = new JsonArray();
for (String line : renderAssemblyLines(scan.instructions.subList(start, end))) {
items.add(line);
}
boolean hasMore = scan.truncated || end < scan.instructions.size();
JsonObject instructions = new JsonObject();
instructions.add("items", items);
if (scan.truncated) {
instructions.add("total", JsonNull.INSTANCE);
}
else {
instructions.addProperty("total", scan.instructions.size());
}
instructions.addProperty("returned", items.size());
instructions.addProperty("truncated", hasMore);
if (hasMore) {
instructions.addProperty("next_offset", end);
}
else {
instructions.add("next_offset", JsonNull.INSTANCE);
}
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", instructions);
result.addProperty("instructions_scanned", scan.instructions.size());
result.addProperty("instruction_scan_truncated", scan.truncated);
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);
if (observed.size() > MAX_FUNCTION_ITEMS) {
throw functionLimit("Resolved call set exceeds the 100000-item limit");
}
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());
result.addProperty("length", function.getBody().getNumAddresses());
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",
"offset",
"limit",
"max_instructions"
)
);
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");
}
int offset = requireBoundedInteger(params, "offset", 0, Integer.MAX_VALUE);
int limit = requireBoundedInteger(params, "limit", 1, 500);
int maximum = requireBoundedInteger(params, "max_instructions", 1, 5_000);
InstructionScan scan = scanInstructions(function, maximum);
List<Reference> references = collectReferences(scan.instructions, direction);
JsonArray edges = new JsonArray();
for (Reference reference : references) {
edges.add(referenceEdge(reference));
}
JsonObject result = new JsonObject();
result.add("procedure", procedureIdentity(function));
result.addProperty("direction", direction);
result.add("references", bounded(edges, offset, limit, !scan.truncated));
result.addProperty("instructions_scanned", scan.instructions.size());
result.addProperty("instruction_scan_truncated", scan.truncated);
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());
if (sources.size() > MAX_FUNCTION_ITEMS) {
throw functionLimit("Cross-reference set exceeds the 100000-item limit");
}
}
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 JsonObject analyzeFunction(JsonObject params) throws Exception {
requireKeys(
params,
Set.of(
"procedure",
"include_assembly",
"limit",
"max_pseudocode_chars",
"max_instructions",
"pseudocode_offset",
"assembly_offset",
"collection_offset"
)
);
Function function = resolveProcedure(requireString(params, "procedure"));
boolean includeAssembly = requireBoolean(params, "include_assembly");
int limit = requireBoundedInteger(params, "limit", 1, 500);
int maximumCharacters = requireBoundedInteger(
params,
"max_pseudocode_chars",
1,
100_000
);
int maximumInstructions = requireBoundedInteger(
params,
"max_instructions",
1,
5_000
);
int pseudocodeOffset = requireBoundedInteger(
params,
"pseudocode_offset",
0,
Integer.MAX_VALUE
);
int assemblyOffset = requireBoundedInteger(
params,
"assembly_offset",
0,
Integer.MAX_VALUE
);
JsonObject offsets = requireCollectionOffsets(params);
InstructionScan scan = scanInstructions(function, maximumInstructions);
DecompileResults decompilation = decompile(function);
String pseudocode = decompiledText(decompilation);
if (pseudocode == null) {
pseudocode = "";
}
List<Reference> incomingReferences = collectReferences(scan.instructions, "incoming")
.stream()
.filter(reference -> !function.getBody().contains(reference.getFromAddress()))
.toList();
List<Reference> outgoingReferences = collectReferences(scan.instructions, "outgoing");
JsonArray incoming = referenceEdges(incomingReferences);
JsonArray outgoing = referenceEdges(outgoingReferences);
JsonArray comments = comments(scan.instructions);
JsonArray callers = procedureIdentities(function.getCallingFunctions(monitor));
JsonArray callees = procedureIdentities(function.getCalledFunctions(monitor));
JsonArray referencedStrings = referencedStrings(outgoingReferences);
JsonArray referencedNames = referencedNames(outgoingReferences);
JsonArray blocks = basicBlockValues(function);
JsonArray assembly = includeAssembly
? GSON.toJsonTree(renderAssemblyLines(scan.instructions)).getAsJsonArray()
: new JsonArray();
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.add(
"pseudocode",
pseudocodePage(pseudocode, pseudocodeOffset, maximumCharacters)
);
result.add(
"assembly",
includeAssembly
? bounded(assembly, assemblyOffset, maximumInstructions, !scan.truncated)
: bounded(assembly, 0, maximumInstructions, true)
);
result.add(
"comments",
bounded(comments, collectionOffset(offsets, "comments"), limit, !scan.truncated)
);
result.add("callers", bounded(callers, collectionOffset(offsets, "callers"), limit, true));
result.add("callees", bounded(callees, collectionOffset(offsets, "callees"), limit, true));
result.add(
"incoming_references",
bounded(
incoming,
collectionOffset(offsets, "incoming_references"),
limit,
!scan.truncated
)
);
result.add(
"outgoing_references",
bounded(
outgoing,
collectionOffset(offsets, "outgoing_references"),
limit,
!scan.truncated
)
);
result.add(
"referenced_strings",
bounded(
referencedStrings,
collectionOffset(offsets, "referenced_strings"),
limit,
!scan.truncated
)
);
result.add(
"referenced_names",
bounded(
referencedNames,
collectionOffset(offsets, "referenced_names"),
limit,
!scan.truncated
)
);
result.add(
"basic_blocks",
bounded(blocks, collectionOffset(offsets, "basic_blocks"), limit, true)
);
JsonObject instructionScan = new JsonObject();
instructionScan.addProperty("scanned", scan.instructions.size());
instructionScan.addProperty("truncated", scan.truncated);
result.add("instruction_scan", instructionScan);
int nativeApiBudget = Math.min(
MAX_NATIVE_API_RESPONSE_BYTES,
Math.max(
0,
MAX_RESPONSE_BYTES -
encodedUtf8Length(result) -
RESPONSE_ENVELOPE_RESERVE_BYTES
)
);
result.add(
"native_api",
nativeApiBoundary(function, decompilation, nativeApiBudget)
);
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."
);
result.add("limitations", limitations);
return result;
}
private JsonElement addressName(JsonObject params) {
requireKeys(params, Set.of("document", "address"));
requireDocument(params);
Address address = requireAddress(params, "address");
Symbol symbol = currentProgram.getSymbolTable().getPrimarySymbol(address);
return symbol == null ? JsonNull.INSTANCE : GSON.toJsonTree(symbol.getName(true));
}
private JsonArray listDocuments(JsonObject params) {
requireKeys(params, Set.of());
JsonArray result = new JsonArray();
result.add(currentProgram.getName());
return result;
}
private JsonObject listNames(JsonObject params) throws Exception {
requireKeys(params, Set.of("document", "address", "offset", "limit"));
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 page(
inventory,
requireBoundedInteger(params, "offset", 0, Integer.MAX_VALUE),
requireBoundedInteger(params, "limit", 1, 500),
"symbol"
);
}
private JsonObject listProcedures(JsonObject params) throws Exception {
requireKeys(params, Set.of("document", "offset", "limit"));
requireDocument(params);
List<InventoryItem> inventory = procedures().stream()
.map(FunctionEntry::item)
.toList();
return page(
inventory,
requireBoundedInteger(params, "offset", 0, Integer.MAX_VALUE),
requireBoundedInteger(params, "limit", 1, 500),
"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();
if (blocks.length > MAX_INVENTORY_ITEMS) {
throw inventoryLimit();
}
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 JsonObject listStrings(JsonObject params) throws Exception {
requireKeys(params, Set.of("document", "address", "offset", "limit"));
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 page(
inventory,
requireBoundedInteger(params, "offset", 0, Integer.MAX_VALUE),
requireBoundedInteger(params, "limit", 1, 500),
"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 JsonObject search(JsonObject params, boolean procedureSearch) throws Exception {
requireKeys(
params,
Set.of("pattern", "mode", "case_sensitive", "offset", "limit", "document")
);
requireDocument(params);
String expression = requireString(params, "pattern");
if (expression.codePointCount(0, expression.length()) > 256) {
throw new RequestFailure(
"invalid_request",
"pattern must contain between 1 and 256 characters"
);
}
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");
int offset = requireBoundedInteger(params, "offset", 0, Integer.MAX_VALUE);
int limit = requireBoundedInteger(params, "limit", 1, 100);
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();
int total = 0;
long pageEnd = (long) offset + limit;
for (InventoryItem item : inventory) {
if (!matcher.matches(item.value)) {
continue;
}
if (total >= offset && total < pageEnd) {
items.add(resultItem(item, null, MAX_SEARCH_VALUE_CODE_POINTS));
}
total += 1;
}
return pageResult(items, offset, limit, total);
}
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()));
addBounded(
result,
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
);
if (destination.size() >= MAX_INVENTORY_ITEMS) {
throw inventoryLimit();
}
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()));
addBounded(result, new InventoryItem(data.getAddress(), value, facts));
}
result.sort(INVENTORY_ORDER);
stringInventory = List.copyOf(result);
return stringInventory;
}
private Function resolveProcedure(String value) throws Exception {
Address address = tryParseAddress(value);
if (address != null) {
Function function = currentProgram.getFunctionManager().getFunctionAt(address);
if (function == null && address.isMemoryAddress()) {
function = currentProgram.getFunctionManager().getFunctionContaining(address);
}
if (function == null) {
throw new RequestFailure("not_found", "No procedure exists at the requested address");
}
return function;
}
List<Function> matches = new ArrayList<>();
for (FunctionEntry entry : procedures()) {
Function function = entry.function;
Symbol symbol = function.getSymbol();
String qualified = symbol == null ? function.getName() : symbol.getName(true);
if (function.getName().equals(value) || qualified.equals(value)) {
matches.add(function);
}
}
if (matches.isEmpty()) {
throw new RequestFailure("not_found", "Unknown Ghidra procedure name");
}
if (matches.size() != 1) {
throw new RequestFailure("ambiguous", "Ghidra procedure name is ambiguous");
}
return matches.get(0);
}
private JsonObject nativeApiBoundary(
Function function,
DecompileResults decompilation,
int maximumBytes
) 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);
result.addProperty("parameters_truncated", false);
JsonArray jumpTables = new JsonArray();
result.add("jump_tables", jumpTables);
result.addProperty("jump_tables_truncated", false);
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);
if (encodedUtf8Length(result) > maximumBytes) {
return unavailableNativeApiBoundary(
"The remaining Ghidra response budget cannot contain a structured native API boundary"
);
}
int parameterCount = Math.min(
prototype.getNumParams(),
MAX_NATIVE_API_PARAMETERS
);
for (int index = 0; index < parameterCount; 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
)
);
if (encodedUtf8Length(result) > maximumBytes) {
parameters.remove(parameters.size() - 1);
break;
}
}
result.addProperty(
"parameters_truncated",
prototype.getNumParams() > parameters.size()
);
JumpTable[] recoveredJumpTables = highFunction.getJumpTables();
int jumpTableCount = Math.min(
recoveredJumpTables.length,
MAX_NATIVE_API_JUMP_TABLES
);
for (int index = 0; index < jumpTableCount; index += 1) {
jumpTables.add(inferredJumpTable(recoveredJumpTables[index]));
if (encodedUtf8Length(result) > maximumBytes) {
jumpTables.remove(jumpTables.size() - 1);
break;
}
}
result.addProperty(
"jump_tables_truncated",
recoveredJumpTables.length > jumpTables.size()
);
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 int encodedUtf8Length(JsonElement value) {
return GSON.toJson(value).getBytes(StandardCharsets.UTF_8).length;
}
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) throws Exception {
JsonObject result = new JsonObject();
result.addProperty(
"dispatch_address",
canonicalAddress(jumpTable.getSwitchAddress())
);
JumpTable.LoadTable[] loadTables = jumpTable.getLoadTables();
List<Address> referencedDataAddresses = loadTables.length == 0
? dispatchPathDataReferences(jumpTable)
: List.of();
JsonArray dataSources = new JsonArray();
int loadTableCount = Math.min(
loadTables.length,
MAX_NATIVE_API_LOAD_TABLES
);
for (int index = 0; index < loadTableCount; index += 1) {
JumpTable.LoadTable loadTable = loadTables[index];
JsonObject source = new JsonObject();
source.addProperty("address", canonicalAddress(loadTable.getAddress()));
source.addProperty("provenance", "ghidra-decompiler-load-table");
source.addProperty("entry_size_bytes", loadTable.getSize());
source.addProperty("entry_count", loadTable.getNum());
source.addProperty("confidence", "high");
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);
}
int referencedDataCount = Math.min(
referencedDataAddresses.size(),
MAX_NATIVE_API_LOAD_TABLES - loadTableCount
);
for (int index = 0; index < referencedDataCount; index += 1) {
Address address = referencedDataAddresses.get(index);
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);
result.addProperty(
"data_sources_truncated",
loadTables.length > loadTableCount ||
referencedDataAddresses.size() > referencedDataCount
);
Address[] targets = jumpTable.getCases();
Integer[] labels = jumpTable.getLabelValues();
JsonArray mappings = new JsonArray();
int mappingCount = Math.min(targets.length, MAX_NATIVE_API_MAPPINGS);
for (int index = 0; index < mappingCount; index += 1) {
JsonObject mapping = new JsonObject();
if (labels == null || index >= labels.length || labels[index] == null) {
mapping.add("case_value", JsonNull.INSTANCE);
}
else {
mapping.addProperty("case_value", labels[index]);
}
String targetAddress = canonicalAddress(targets[index]);
mapping.addProperty("target_address", targetAddress);
JsonArray dataAddresses = new JsonArray();
for (int sourceIndex = 0; sourceIndex < loadTableCount; sourceIndex += 1) {
dataAddresses.add(
canonicalAddress(loadTables[sourceIndex].getAddress())
);
}
mapping.add("data_addresses", dataAddresses);
mapping.addProperty("confidence", "medium");
JsonArray evidence = new JsonArray();
evidence.add(
inferenceEvidence(
"jump-table",
"ghidra-high-function",
"Recovered target " +
targetAddress +
" from dispatch " +
canonicalAddress(jumpTable.getSwitchAddress()) +
"."
)
);
mapping.add("evidence", evidence);
mappings.add(mapping);
}
result.add("mappings", mappings);
result.addProperty("mappings_truncated", targets.length > mappingCount);
JsonArray limitations = new JsonArray();
if (loadTables.length == 0 && referencedDataAddresses.isEmpty()) {
limitations.add(
"Ghidra recovered targets but exposed no backing load-table or dispatch-block data address."
);
}
if (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 (labels == null || labels.length != targets.length) {
limitations.add(
"Case labels were unavailable or incomplete; null labels are preserved."
);
}
if (
loadTables.length > loadTableCount ||
referencedDataAddresses.size() > referencedDataCount ||
targets.length > mappingCount
) {
limitations.add(
"Native API jump-table observations reached REA's bounded output limits."
);
}
result.add("limitations", limitations);
return result;
}
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,
DECOMPILE_TIMEOUT_SECONDS,
monitor
);
if (results.isTimedOut()) {
throw new RequestFailure(
"decompile_timeout",
"Ghidra decompilation reached its 30-second deadline"
);
}
if (results.isCancelled()) {
throw new RequestFailure("decompile_cancelled", "Ghidra decompilation was cancelled");
}
if (!results.decompileCompleted()) {
throw new RequestFailure(
"decompile_failed",
"Ghidra decompilation failed: " + boundedMessage(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(
List<Instruction> instructions,
String direction) throws Exception {
TreeMap<String, Reference> observed = new TreeMap<>();
for (Instruction instruction : instructions) {
monitor.checkCancelled();
if (direction.equals("outgoing")) {
for (Reference reference : currentProgram.getReferenceManager()
.getReferencesFrom(instruction.getAddress())) {
addReference(observed, reference);
}
}
else {
ReferenceIterator iterator = currentProgram.getReferenceManager()
.getReferencesTo(instruction.getAddress());
while (iterator.hasNext()) {
addReference(observed, iterator.next());
}
}
}
List<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);
if (observed.size() > MAX_FUNCTION_ITEMS) {
throw functionLimit("Reference set exceeds the 100000-item limit");
}
}
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();
if (variables.length > MAX_FUNCTION_ITEMS) {
throw functionLimit("Local-variable set exceeds the 100000-item limit");
}
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) {
if (functions.size() > MAX_FUNCTION_ITEMS) {
throw functionLimit("Resolved call set exceeds the 100000-item limit");
}
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();
if (result.size() >= MAX_FUNCTION_ITEMS) {
throw functionLimit("Basic-block set exceeds the 100000-item limit");
}
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);
if (observed.size() > MAX_FUNCTION_ITEMS) {
throw functionLimit("Comment set exceeds the 100000-item limit");
}
}
}
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);
if (observed.size() > MAX_FUNCTION_ITEMS) {
throw functionLimit("Referenced-string set exceeds the 100000-item limit");
}
}
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);
if (observed.size() > MAX_FUNCTION_ITEMS) {
throw functionLimit("Referenced-name set exceeds the 100000-item limit");
}
}
JsonArray result = new JsonArray();
observed.values().forEach(result::add);
return result;
}
private static JsonObject pseudocodePage(String value, int offset, int limit) {
int total = value.codePointCount(0, value.length());
int start = Math.min(offset, total);
int returned = Math.min(limit, total - start);
int startIndex = value.offsetByCodePoints(0, start);
int endIndex = value.offsetByCodePoints(startIndex, returned);
int next = start + returned;
JsonObject result = new JsonObject();
result.addProperty("text", value.substring(startIndex, endIndex));
result.addProperty("total_chars", total);
result.addProperty("returned_chars", returned);
result.addProperty("truncated", next < total);
if (next < total) {
result.addProperty("next_offset", next);
}
else {
result.add("next_offset", JsonNull.INSTANCE);
}
return result;
}
private static JsonObject bounded(
JsonArray values,
int offset,
int limit,
boolean complete) {
int start = Math.min(offset, values.size());
int end = Math.min(values.size(), start + limit);
JsonArray items = new JsonArray();
for (int index = start; index < end; index += 1) {
items.add(values.get(index).deepCopy());
}
boolean hasMore = end < values.size();
JsonObject result = new JsonObject();
result.add("items", items);
if (complete) {
result.addProperty("total", values.size());
}
else {
result.add("total", JsonNull.INSTANCE);
}
result.addProperty("returned", items.size());
result.addProperty("truncated", !complete || hasMore);
if (complete && hasMore) {
result.addProperty("next_offset", end);
}
else {
result.add("next_offset", JsonNull.INSTANCE);
}
return result;
}
private static JsonObject requireCollectionOffsets(JsonObject params) {
JsonElement value = params.get("collection_offset");
if (value == null || !value.isJsonObject()) {
throw new RequestFailure("invalid_request", "collection_offset must be an object");
}
JsonObject result = value.getAsJsonObject();
requireKeys(
result,
Set.of(
"comments",
"callers",
"callees",
"incoming_references",
"outgoing_references",
"referenced_strings",
"referenced_names",
"basic_blocks"
)
);
for (String name : result.keySet()) {
requireBoundedInteger(result, name, 0, Integer.MAX_VALUE);
}
return result;
}
private static int collectionOffset(JsonObject offsets, String name) {
return requireBoundedInteger(offsets, name, 0, Integer.MAX_VALUE);
}
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 boundedMessage(String value) {
if (value == null || value.isBlank()) {
return "native decompiler returned no detail";
}
return truncate(value.replaceAll("[\\r\\n]+", " "), 512).value;
}
private static RequestFailure functionLimit(String message) {
return new RequestFailure("limit_exceeded", message);
}
private JsonObject page(
List<InventoryItem> inventory,
int offset,
int limit,
String factsName) {
int start = Math.min(offset, inventory.size());
int end = Math.min(inventory.size(), start + limit);
JsonArray items = new JsonArray();
for (int index = start; index < end; index += 1) {
items.add(resultItem(inventory.get(index), factsName, MAX_LIST_VALUE_CODE_POINTS));
}
return pageResult(items, offset, limit, inventory.size());
}
private static JsonObject pageResult(JsonArray items, int offset, int limit, int total) {
int next = offset + items.size();
boolean hasMore = next < total;
JsonObject result = new JsonObject();
result.add("items", items);
result.addProperty("offset", offset);
result.addProperty("limit", limit);
result.addProperty("total", total);
if (hasMore) {
result.addProperty("next_offset", next);
}
else {
result.add("next_offset", JsonNull.INSTANCE);
}
result.addProperty("has_more", hasMore);
return result;
}
private static JsonObject resultItem(
InventoryItem item,
String factsName,
int maximumCodePoints) {
TruncatedValue truncated = truncate(item.value, maximumCodePoints);
JsonObject result = new JsonObject();
result.addProperty("address", canonicalAddress(item.address));
result.addProperty("value", truncated.value);
result.addProperty("value_truncated", truncated.truncated);
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));
return result;
}
private static String exclusiveEnd(MemoryBlock block) {
Address next = block.getEnd().next();
if (next != null && next.getAddressSpace().equals(block.getStart().getAddressSpace())) {
return canonicalAddress(next);
}
BigInteger start = new BigInteger(Long.toUnsignedString(block.getStart().getOffset()));
return canonicalOffset(
block.getStart().getAddressSpace(),
start.add(BigInteger.valueOf(block.getSize()))
);
}
private static String canonicalAddress(Address address) {
return canonicalOffset(
address.getAddressSpace(),
new BigInteger(Long.toUnsignedString(address.getOffset()))
);
}
private static String canonicalOffset(AddressSpace space, BigInteger offset) {
String value = "0x" + offset.toString(16);
if (space.equals(sessionDefaultAddressSpace)) {
return value;
}
return encodeAddressSpace(space.getName()) + ":" + value;
}
private Address requireAddress(JsonObject params, String name) {
return parseReaAddress(requireString(params, name));
}
private Address parseReaAddress(String value) {
Address address = tryParseAddress(value);
if (address == null) {
throw new RequestFailure("invalid_request", "Unknown or invalid Ghidra address");
}
return address;
}
private Address tryParseAddress(String value) {
try {
String spaceName = null;
String offset = value;
int separator = value.lastIndexOf(":0x");
if (separator >= 0) {
spaceName = decodeAddressSpace(value.substring(0, separator));
offset = value.substring(separator + 3);
}
else if (value.startsWith("0x")) {
offset = value.substring(2);
}
if (!offset.matches("[0-9A-Fa-f]+")) {
return null;
}
AddressSpace space = spaceName == null
? currentProgram.getAddressFactory().getDefaultAddressSpace()
: currentProgram.getAddressFactory().getAddressSpace(spaceName);
return space == null ? null : space.getAddress(offset);
}
catch (Exception exception) {
return null;
}
}
private static String encodeAddressSpace(String value) {
StringBuilder result = new StringBuilder();
for (byte item : value.getBytes(StandardCharsets.UTF_8)) {
int unsigned = Byte.toUnsignedInt(item);
if ((unsigned >= 'A' && unsigned <= 'Z') ||
(unsigned >= 'a' && unsigned <= 'z') ||
(unsigned >= '0' && unsigned <= '9') ||
unsigned == '.' || unsigned == '_' || unsigned == '~' || unsigned == '-') {
result.append((char) unsigned);
}
else {
result.append('%');
result.append(Character.toUpperCase(Character.forDigit(unsigned >>> 4, 16)));
result.append(Character.toUpperCase(Character.forDigit(unsigned & 0xf, 16)));
}
}
return result.toString();
}
private static String decodeAddressSpace(String value) {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
for (int index = 0; index < value.length();) {
char item = value.charAt(index);
if (item == '%') {
if (index + 2 >= value.length()) {
throw new RequestFailure("invalid_request", "Address space encoding is invalid");
}
int high = Character.digit(value.charAt(index + 1), 16);
int low = Character.digit(value.charAt(index + 2), 16);
if (high < 0 || low < 0) {
throw new RequestFailure("invalid_request", "Address space encoding is invalid");
}
bytes.write((high << 4) | low);
index += 3;
}
else if ((item >= 'A' && item <= 'Z') ||
(item >= 'a' && item <= 'z') ||
(item >= '0' && item <= '9') ||
item == '.' || item == '_' || item == '~' || item == '-') {
bytes.write((byte) item);
index += 1;
}
else {
throw new RequestFailure("invalid_request", "Address space encoding is invalid");
}
}
return bytes.toString(StandardCharsets.UTF_8);
}
private static String normalizedSymbolType(Symbol symbol) {
String value = symbol.getSymbolType().toString()
.toLowerCase(Locale.ROOT)
.replaceAll("[^a-z0-9]+", "_")
.replaceAll("^_+|_+$", "");
return value.isEmpty() ? "unknown" : value;
}
private static String normalizedSource(SourceType source) {
return source.name().toLowerCase(Locale.ROOT);
}
private static void addBounded(List<InventoryItem> destination, InventoryItem item) {
if (destination.size() >= MAX_INVENTORY_ITEMS) {
throw inventoryLimit();
}
destination.add(item);
}
private static RequestFailure inventoryLimit() {
return new RequestFailure(
"limit_exceeded",
"Ghidra inventory exceeds the 1000000-item safety limit"
);
}
private static TruncatedValue truncate(String value, int maximumCodePoints) {
int codePoints = value.codePointCount(0, value.length());
if (codePoints <= maximumCodePoints) {
return new TruncatedValue(value, false);
}
int end = value.offsetByCodePoints(0, maximumCodePoints);
return new TruncatedValue(value.substring(0, end), true);
}
private static ValueMatcher literalMatcher(String pattern, boolean caseSensitive) {
String needle = caseSensitive ? pattern : pattern.toLowerCase(Locale.ROOT);
return new ValueMatcher() {
private long remaining = MAX_LITERAL_SEARCH_WORK_UNITS;
@Override
public boolean matches(String value) {
long required = (long) Math.max(value.length(), 1) + needle.length();
if (required > remaining) {
throw new RequestFailure(
"limit_exceeded",
"Literal search exceeds the 1000000-unit work budget"
);
}
remaining -= required;
String candidate = caseSensitive ? value : value.toLowerCase(Locale.ROOT);
return candidate.contains(needle);
}
};
}
private static ValueMatcher regexMatcher(String expression, boolean caseSensitive) {
RegexMetrics metrics;
Pattern pattern;
try {
metrics = new BoundedRegexParser(expression).parse();
pattern = Pattern.compile(
expression,
caseSensitive ? 0 : Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE
);
}
catch (PatternSyntaxException exception) {
throw new RequestFailure("invalid_request", "Invalid regex pattern");
}
long workPerCharacter = metrics.paths * Math.max(metrics.steps, 1);
return new ValueMatcher() {
private long remaining = MAX_REGEX_SEARCH_WORK_UNITS;
@Override
public boolean matches(String value) {
if (value.length() > MAX_REGEX_CANDIDATE_CHARACTERS) {
throw new RequestFailure(
"limit_exceeded",
"Regex candidate exceeds the 4096-character safety limit"
);
}
long required = workPerCharacter * Math.max(value.length(), 1);
if (required > remaining) {
throw new RequestFailure(
"limit_exceeded",
"Regex search exceeds the 1000000-unit work budget"
);
}
remaining -= required;
return pattern.matcher(value).find();
}
};
}
private static SessionDescriptor readDescriptor(Path path) throws IOException {
if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) ||
Files.size(path) > MAX_DESCRIPTOR_BYTES) {
throw new IllegalArgumentException("REA session descriptor is invalid");
}
JsonObject object = requireObject(
JsonParser.parseString(Files.readString(path, StandardCharsets.UTF_8)),
DESCRIPTOR_KEYS
);
if (requireInteger(object, "schema_version") != 2) {
throw new IllegalArgumentException("REA session descriptor version is invalid");
}
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")
);
}
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");
}
}
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 String readBoundedLine(Reader reader, int maximumCharacters) throws IOException {
StringBuilder line = new StringBuilder();
while (true) {
int character = reader.read();
if (character < 0) {
return line.isEmpty() ? null : line.toString();
}
if (character == '\n') {
return line.toString();
}
if (character != '\r') {
if (line.length() >= maximumCharacters) {
throw new IOException("Bridge request exceeded the maximum line size");
}
line.append((char) character);
}
}
}
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);
if (encoded.getBytes(StandardCharsets.UTF_8).length > MAX_RESPONSE_BYTES) {
int id = response.get("id").getAsInt();
JsonObject error = new JsonObject();
error.addProperty("code", "output_limit");
error.addProperty("message", "Ghidra response exceeds the 1 MiB wire limit");
JsonObject bounded = new JsonObject();
bounded.addProperty("id", id);
bounded.addProperty("ok", false);
bounded.add("error", error);
encoded = GSON.toJson(bounded);
}
writer.write(encoded);
writer.newLine();
writer.flush();
}
private static String safeMessage(Exception exception) {
String message = exception.getMessage();
return message == null || message.isBlank() ? "operation failed" : message;
}
private interface ValueMatcher {
boolean matches(String value);
}
private static final Comparator<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 TruncatedValue(String value, boolean truncated) {}
private record SessionDescriptor(
String transport,
String endpointPath,
String token,
String runId,
String targetSha256,
String providerVersion,
String profileDigest
) {}
private record Request(int id, String method, JsonObject params) {}
private record RegexMetrics(long paths, long steps, boolean containsRepeat) {}
private static final class RequestFailure extends RuntimeException {
private final String code;
RequestFailure(String code, String message) {
super(message);
this.code = code;
}
}
private static final class BoundedRegexParser {
private final String expression;
private int index;
BoundedRegexParser(String expression) {
this.expression = expression;
}
RegexMetrics parse() {
RegexMetrics result = parseExpression(false);
if (index != expression.length()) {
throw invalidRegex();
}
return result;
}
private RegexMetrics parseExpression(boolean grouped) {
RegexMetrics result = parseSequence(grouped);
while (peek('|')) {
index += 1;
RegexMetrics branch = parseSequence(grouped);
result = new RegexMetrics(
checkedPaths(result.paths, branch.paths, false),
Math.max(result.steps, branch.steps),
result.containsRepeat || branch.containsRepeat
);
}
return result;
}
private RegexMetrics parseSequence(boolean grouped) {
RegexMetrics result = new RegexMetrics(1, 0, false);
while (index < expression.length() && !peek('|') && !(grouped && peek(')'))) {
RegexMetrics item = parseAtom();
result = new RegexMetrics(
checkedPaths(result.paths, item.paths, true),
result.steps + item.steps,
result.containsRepeat || item.containsRepeat
);
}
return result;
}
private RegexMetrics parseAtom() {
if (index >= expression.length()) {
throw invalidRegex();
}
char item = expression.charAt(index++);
RegexMetrics atom;
if (item == '(') {
if (peek('?')) {
if (index + 1 >= expression.length() || expression.charAt(index + 1) != ':') {
throw new RequestFailure(
"invalid_request",
"Regex lookarounds and backreferences are not supported"
);
}
index += 2;
}
atom = parseExpression(true);
if (!peek(')')) {
throw invalidRegex();
}
index += 1;
}
else if (item == '[') {
consumeCharacterClass();
atom = leaf();
}
else if (item == '\\') {
consumeEscape();
atom = leaf();
}
else if (item == '*' || item == '+') {
throw unboundedRepeat();
}
else if (item == '?' || item == '{' || item == '}' || item == ')') {
throw invalidRegex();
}
else {
atom = leaf();
}
if (index >= expression.length()) {
return atom;
}
if (peek('*') || peek('+')) {
throw unboundedRepeat();
}
int minimum;
int maximum;
if (peek('?')) {
index += 1;
minimum = 0;
maximum = 1;
}
else if (peek('{')) {
int[] bounds = consumeBounds();
minimum = bounds[0];
maximum = bounds[1];
}
else {
return atom;
}
if (atom.containsRepeat) {
throw new RequestFailure(
"invalid_request",
"Nested regex repetitions are not supported"
);
}
if (maximum > 1_000) {
throw unboundedRepeat();
}
return new RegexMetrics(
repeatPaths(atom.paths, minimum, maximum),
maximum * atom.steps,
true
);
}
private void consumeCharacterClass() {
boolean escaped = false;
boolean hasContent = false;
while (index < expression.length()) {
char item = expression.charAt(index++);
if (escaped) {
escaped = false;
hasContent = true;
}
else if (item == '\\') {
escaped = true;
}
else if (item == ']' && hasContent) {
return;
}
else {
hasContent = true;
}
}
throw invalidRegex();
}
private void consumeEscape() {
if (index >= expression.length()) {
throw invalidRegex();
}
char escaped = expression.charAt(index++);
if (Character.isDigit(escaped) || escaped == 'k') {
throw new RequestFailure(
"invalid_request",
"Regex lookarounds and backreferences are not supported"
);
}
if (escaped == 'Q' || escaped == 'E' || escaped == 'p' || escaped == 'P') {
throw new RequestFailure(
"invalid_request",
"Regex operation is not supported by the bounded matcher"
);
}
if (escaped == 'x') {
consumeHexDigits(2);
}
else if (escaped == 'u') {
consumeHexDigits(4);
}
}
private void consumeHexDigits(int count) {
if (index + count > expression.length()) {
throw invalidRegex();
}
for (int current = 0; current < count; current += 1) {
if (Character.digit(expression.charAt(index + current), 16) < 0) {
throw invalidRegex();
}
}
index += count;
}
private int[] consumeBounds() {
index += 1;
int minimum = consumeDecimal();
int maximum = minimum;
if (peek(',')) {
index += 1;
if (peek('}')) {
throw unboundedRepeat();
}
maximum = consumeDecimal();
}
if (!peek('}') || minimum > maximum) {
throw invalidRegex();
}
index += 1;
return new int[] { minimum, maximum };
}
private int consumeDecimal() {
int start = index;
long value = 0;
while (index < expression.length() && Character.isDigit(expression.charAt(index))) {
value = value * 10 + Character.digit(expression.charAt(index), 10);
if (value > Integer.MAX_VALUE) {
throw unboundedRepeat();
}
index += 1;
}
if (index == start) {
throw invalidRegex();
}
return (int) value;
}
private boolean peek(char expected) {
return index < expression.length() && expression.charAt(index) == expected;
}
private static RegexMetrics leaf() {
return new RegexMetrics(1, 1, false);
}
private static long repeatPaths(long childPaths, int minimum, int maximum) {
long result = 0;
long repeated = 1;
for (int count = 0; count <= maximum; count += 1) {
if (count >= minimum) {
result = checkedPaths(result, repeated, false);
}
if (count < maximum) {
repeated = checkedPaths(repeated, childPaths, true);
}
}
return result;
}
private static long checkedPaths(long left, long right, boolean multiply) {
boolean exceeded = multiply
? right != 0 && left > MAX_REGEX_BACKTRACKING_PATHS / right
: left > MAX_REGEX_BACKTRACKING_PATHS - right;
long result = multiply ? left * right : left + right;
if (exceeded || result > MAX_REGEX_BACKTRACKING_PATHS) {
throw new RequestFailure(
"invalid_request",
"Regex exceeds the 10000-path backtracking budget"
);
}
return result;
}
private static RequestFailure unboundedRepeat() {
return new RequestFailure(
"invalid_request",
"Unbounded or excessive regex repetitions are not supported"
);
}
private static RequestFailure invalidRegex() {
return new RequestFailure("invalid_request", "Invalid regex pattern");
}
}
}