fix(ghidra): parse long procedure selectors without recursion

This commit is contained in:
morluto
2026-10-09 06:39:12 +08:00
parent e762e32afb
commit 480b16b9c1
3 changed files with 64 additions and 4 deletions
+38 -3
View File
@@ -1621,8 +1621,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
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 = value.matches("(?i)^0x[0-9a-f]+$") ||
value.matches("(?i)^(?:[a-z0-9._~-]|%[0-9a-f]{2})+:0x[0-9a-f]+$");
boolean explicitAddress = isExplicitReaAddress(value);
if (!explicitAddress) {
List<Function> matches = new ArrayList<>();
for (FunctionEntry entry : procedures()) {
@@ -2997,6 +2996,42 @@ public final class ReaGhidraBridge extends HeadlessScript {
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) {
@@ -3017,7 +3052,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
else if (value.startsWith("0x") || value.startsWith("0X")) {
offset = value.substring(2);
}
if (!offset.matches("[0-9A-Fa-f]+")) {
if (!isHexadecimal(offset, 0, offset.length())) {
return null;
}
AddressSpace space = spaceName == null
+19
View File
@@ -210,6 +210,24 @@ export async function verifyGhidraBoundaries(
entrypoint,
env,
});
const longSelector = "REA_MISSING_" + "x".repeat(32768);
for (const selected of [
longSelector,
`${longSelector}:0x1`,
`${"%78".repeat(12000)}:0x1`,
`${longSelector}%GG:0x1`,
]) {
await invalid(
"procedure_address",
{ procedure: selected },
/Unknown|invalid/u,
);
assert.equal(
await call("procedure_address", { procedure: address }),
address,
"Invalid selectors must preserve the live provider connection",
);
}
const baseline = await call("inspect_native_instruction", { address });
assert.equal(baseline.status, "decoded");
const bytes = await call("read_bytes", { address, length: baseline.length });
@@ -724,6 +742,7 @@ export async function verifyGhidraBoundaries(
equivalent_instruction_address_spellings: true,
qualified_annotation_name_roundtrip: true,
oversized_result_retention_and_complete_export: true,
long_selector_rejection_and_provider_recovery: true,
source_immutable: true,
reopen_discards_edits: true,
long_tmpdir_private_socket_cleanup: true,
@@ -82,7 +82,13 @@ export async function verifyGhidraNamespaceAnnotations({
}),
address,
);
for (const name of ["other::literal", `${namespace}::child::literal`]) {
for (const name of [
"other::literal",
`${namespace}::child::literal`,
"0xnothex",
"address%GG:0x1",
"address:0xnothex",
]) {
const literal = await call("annotate_native_function", {
procedure: address,
name,