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"],
[