diff --git a/bridge/ghidra/ReaGhidraBridge.java b/bridge/ghidra/ReaGhidraBridge.java index 498a99e002..4635a183a3 100644 --- a/bridge/ghidra/ReaGhidraBridge.java +++ b/bridge/ghidra/ReaGhidraBridge.java @@ -27,6 +27,7 @@ import java.util.Comparator; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -1504,16 +1505,33 @@ public final class ReaGhidraBridge extends HeadlessScript { requireDocument(params); if (!params.has("names") || !params.get("names").isJsonObject()) throw new RequestFailure("invalid_request", "names must be an object of address -> name"); + Map> requestsByAddress = new LinkedHashMap<>(); JsonObject result = new JsonObject(); for (Map.Entry entry : params.getAsJsonObject("names").entrySet()) { - boolean ok; try { - ok = applyAddressName(parseReaAddress(entry.getKey()), entry.getValue().getAsString()); + Address address = parseReaAddress(entry.getKey()); + requestsByAddress.computeIfAbsent(address, ignored -> new ArrayList<>()) + .add(new AddressNameRequest(entry.getKey(), address, entry.getValue().getAsString())); } catch (RequestFailure failure) { - ok = false; + result.addProperty(entry.getKey(), false); } - result.addProperty(entry.getKey(), ok); + } + for (List requests : requestsByAddress.values()) { + String name = requests.get(0).name; + boolean conflictingAliasNames = requests.stream() + .anyMatch(request -> !request.name.equals(name)); + boolean success = false; + if (!conflictingAliasNames) { + try { + success = applyAddressName(requests.get(0).address, name); + } + catch (RequestFailure failure) { + // Keep per-address failures in the existing batch result shape. + } + } + for (AddressNameRequest request : requests) + result.addProperty(request.inputAddress, success); } return result; } @@ -3564,6 +3582,7 @@ public final class ReaGhidraBridge extends HeadlessScript { .thenComparing(Reference::isPrimary); private record InventoryItem(Address address, String value, JsonObject facts) {} + private record AddressNameRequest(String inputAddress, Address address, String name) {} private record FunctionEntry(Function function, InventoryItem item) {} private record FunctionAnalysisObservation(JsonObject dossier, List limitations) {} private record InstructionScan(List instructions, boolean truncated) {} diff --git a/scripts/lib/real-ghidra-address-boundaries.mjs b/scripts/lib/real-ghidra-address-boundaries.mjs index 1fae2bbe10..0835ecec39 100644 --- a/scripts/lib/real-ghidra-address-boundaries.mjs +++ b/scripts/lib/real-ghidra-address-boundaries.mjs @@ -62,6 +62,36 @@ export async function verifyGhidraAddressBoundaries(client, entry, cli) { await query("read_bytes", { address: padded, length: 1 }), await query("read_bytes", { address: entry, length: 1 }), ); + const aliasKeys = [entry, padded]; + assert.deepEqual( + await query("set_addresses_names", { + names: { + [entry]: "rea_same_address_alias", + [padded]: "rea_same_address_alias", + }, + }), + Object.fromEntries(aliasKeys.map((address) => [address, true])), + "Equivalent address spellings with one requested name share one edit", + ); + const nameBeforeAliasedBatch = await query("address_name", { + address: entry, + }); + assert.equal(nameBeforeAliasedBatch, "rea_same_address_alias"); + assert.deepEqual( + await query("set_addresses_names", { + names: { + [entry]: "rea_first_address_alias", + [padded]: "rea_second_address_alias", + }, + }), + Object.fromEntries(aliasKeys.map((address) => [address, false])), + "Conflicting spellings of one Ghidra address must be rejected together", + ); + assert.equal( + await query("address_name", { address: entry }), + nameBeforeAliasedBatch, + "A rejected alias collision must leave the preexisting target name unchanged", + ); for (const [command, flags, operation] of [ ["read-bytes", ["--length", "1"], "read_bytes"], [