diff --git a/.agents/skills/reverse-engineer-anything/references/native-and-artifacts.md b/.agents/skills/reverse-engineer-anything/references/native-and-artifacts.md index 7fa6a44d2..0a2810676 100644 --- a/.agents/skills/reverse-engineer-anything/references/native-and-artifacts.md +++ b/.agents/skills/reverse-engineer-anything/references/native-and-artifacts.md @@ -152,7 +152,7 @@ context. PSP/stack/device state remains unmodeled. See [DOS guide](https://githu ### Function annotations in Ghidra -Use `annotate_native_function` for one function name and/or entry comments, with at least one explicit change. Review its annotation readback and refreshed dossier inline. Changes are atomic and session-scoped; empty comments clear them, omitted fields preserve them. Later MCP calls observe edits until close. CLI `annotate-native-function` returns the updated analysis before discarding the session. Original executable bytes are unchanged; edits invalidate immutable snapshots. Windows P0 does not admit database mutations. +Use `annotate_native_function` for one function name and/or entry comments, with at least one explicit change. Review its annotation readback and refreshed dossier inline. Changes are atomic and session-scoped; empty comments clear them, omitted fields preserve them. Later MCP calls observe edits until close. CLI `annotate-native-function` returns the updated analysis before discarding the session. Original executable bytes are unchanged; edits invalidate immutable snapshots. Windows P0 does not admit database mutations. For naming only, `set_address_name` / `set_addresses_names` also work with Ghidra: they name function entries or data addresses (primary labels) and return compact per-address success booleans instead of a dossier. ## Offline PE resource inventory diff --git a/bridge/ghidra/ReaGhidraBridge.java b/bridge/ghidra/ReaGhidraBridge.java index fce8a995b..7ec7a205c 100644 --- a/bridge/ghidra/ReaGhidraBridge.java +++ b/bridge/ghidra/ReaGhidraBridge.java @@ -120,6 +120,8 @@ public final class ReaGhidraBridge extends HeadlessScript { ); private static final String[] CAPABILITIES = { "annotate_native_function", + "set_address_name", + "set_addresses_names", "inspect_native_load_image", "read_bytes", "address_to_file_offset", @@ -374,6 +376,12 @@ public final class ReaGhidraBridge extends HeadlessScript { case "resolve_native_call_targets" -> resolveNativeCallTargets(request.params); case "xrefs" -> xrefs(request.params); case "analyze_function" -> analyzeFunction(request.params); + case "set_address_name", "set_addresses_names" -> { + if (!descriptor.transport.equals("unix-socket")) + throw new RequestFailure("method_unavailable", "Windows P0 does not admit database mutation"); + yield request.method.equals("set_address_name") + ? setAddressName(request.params) : setAddressesNames(request.params); + } case "annotate_native_function" -> { if (!descriptor.transport.equals("unix-socket")) throw new RequestFailure("method_unavailable", "Windows P0 does not admit database mutation"); @@ -417,7 +425,8 @@ public final class ReaGhidraBridge extends HeadlessScript { result.addProperty("analysis_timed_out", timedOut); result.add("capabilities", GSON.toJsonTree( java.util.Arrays.stream(CAPABILITIES) - .filter(value -> !readOnly || !value.equals("annotate_native_function")) + .filter(value -> !readOnly || !(value.equals("annotate_native_function") + || value.equals("set_address_name") || value.equals("set_addresses_names"))) .toArray(String[]::new) )); result.add("target", target); @@ -1027,11 +1036,11 @@ public final class ReaGhidraBridge extends HeadlessScript { for (String field : List.of("name", "comment", "inline_comment")) { if (params.has(field)) validateAnnotationText(requireText(params, field), field); } - String leafName = params.has("name") - ? annotationLeafName(function, requireString(params, "name")) : null; int transaction = currentProgram.startTransaction("REA function annotations"); boolean commit = false; try { + String leafName = params.has("name") + ? symbolLeafName(function.getSymbol(), requireString(params, "name")) : null; // Validate edits through Ghidra's own setters, including name rules. // A later invalid name must roll back earlier comment writes. for (String field : List.of("comment", "inline_comment")) { @@ -1080,18 +1089,18 @@ public final class ReaGhidraBridge extends HeadlessScript { } } - private String annotationLeafName(Function function, String requested) { + private String symbolLeafName(Symbol symbol, String requested) throws ghidra.util.exception.InvalidInputException { // Fully qualified readback must be usable as an idempotent rename input. - if (requested.equals(procedureName(function))) return function.getName(); - String namespace = function.getParentNamespace().isGlobal() - ? "" : function.getParentNamespace().getName(true); + if (requested.equals(symbol.getName(true))) return symbol.getName(); + String namespace = symbol.getParentNamespace().isGlobal() + ? "" : symbol.getParentNamespace().getName(true); String prefix = namespace + "::"; if (!namespace.isEmpty() && requested.startsWith(prefix)) { String leaf = requested.substring(prefix.length()); if (!leaf.isEmpty()) return leaf; - throw new RequestFailure("invalid_function_name", - "Function name has an empty leaf within namespace " + namespace + " at " + - canonicalAddress(function.getEntryPoint()) + ": " + requested); + throw new ghidra.util.exception.InvalidInputException( + "Symbol name has an empty leaf within namespace " + namespace + " at " + + canonicalAddress(symbol.getAddress()) + ": " + requested); } return requested; } @@ -1416,6 +1425,68 @@ public final class ReaGhidraBridge extends HeadlessScript { return symbol == null ? JsonNull.INSTANCE : GSON.toJsonTree(symbol.getName(true)); } + // Analyst naming: a function entry renames the function, other addresses get a primary label. + private boolean applyAddressName(Address address, String name) throws Exception { + if (!currentProgram.getMemory().contains(address)) + throw new RequestFailure("invalid_request", "Address is not mapped: " + canonicalAddress(address)); + validateAnnotationText(name, "name"); + int transaction = currentProgram.startTransaction("REA address name"); + boolean commit = false; + try { + Function function = currentProgram.getFunctionManager().getFunctionAt(address); + String expected = name; + if (function != null) { + expected = symbolLeafName(function.getSymbol(), name); + function.setName(expected, SourceType.USER_DEFINED); + } else { + var table = currentProgram.getSymbolTable(); + Symbol primary = table.getPrimarySymbol(address); + if (primary != null && !primary.isDynamic()) { + expected = symbolLeafName(primary, name); + primary.setName(expected, SourceType.USER_DEFINED); + } else { + Symbol created = table.createLabel(address, name, SourceType.USER_DEFINED); + created.setPrimary(); + } + } + invalidateAnalysisCaches(); + Symbol readback = currentProgram.getSymbolTable().getPrimarySymbol(address); + commit = readback != null && expected.equals(readback.getName()); + return commit; + } + catch (ghidra.util.exception.InvalidInputException | ghidra.util.exception.DuplicateNameException exception) { + return false; + } + finally { + currentProgram.endTransaction(transaction, commit); + } + } + + private JsonElement setAddressName(JsonObject params) throws Exception { + requireKeys(params, Set.of("document", "address", "name")); + requireDocument(params); + return GSON.toJsonTree(applyAddressName(requireAddress(params, "address"), requireString(params, "name"))); + } + + private JsonElement setAddressesNames(JsonObject params) throws Exception { + requireKeys(params, Set.of("document", "names")); + requireDocument(params); + if (!params.has("names") || !params.get("names").isJsonObject()) + throw new RequestFailure("invalid_request", "names must be an object of address -> name"); + JsonObject result = new JsonObject(); + for (Map.Entry entry : params.getAsJsonObject("names").entrySet()) { + boolean ok; + try { + ok = applyAddressName(parseReaAddress(entry.getKey()), entry.getValue().getAsString()); + } + catch (RequestFailure failure) { + ok = false; + } + result.addProperty(entry.getKey(), ok); + } + return result; + } + private JsonArray listDocuments(JsonObject params) { requireKeys(params, Set.of()); JsonArray result = new JsonArray(); diff --git a/docs/installation.md b/docs/installation.md index 1463d7aec..b23bd3825 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -610,8 +610,10 @@ dossiers, instructions, recovered data types, measured load mappings, loaded memory bytes, and observed file offsets. Independent load-image attestation supports DOS MZ and explicitly selected COM; PE returns its measurements with that limitation. On Linux and macOS, `annotate_native_function` also edits a function name and/or -entry comments atomically and returns refreshed analysis. These session metadata -edits leave executable bytes unchanged and are discarded on close. GUI controls +entry comments atomically and returns refreshed analysis, and `set_address_name` +and `set_addresses_names` name a function entry or any other mapped address +(creating or renaming its primary label) with per-address success results. These +session metadata edits leave executable bytes unchanged and are discarded on close. GUI controls require Hopper; Windows P0 remains read-only. Windows P0 admits native x86 and x86-64 PE applications and DLLs on fixed local NTFS volumes. diff --git a/docs/native-investigation.md b/docs/native-investigation.md index 12ea1718a..93492137a 100644 --- a/docs/native-investigation.md +++ b/docs/native-investigation.md @@ -397,6 +397,12 @@ unpaired Unicode surrogate; a rejection identifies the field and UTF-16 index and leaves every annotation unchanged. CRLF, supplementary Unicode characters, and combining characters are preserved. Windows P0 remains read-only. Ghidra has no GUI authority, and REA never falls back automatically to Hopper. +`set_address_name` and `set_addresses_names` give Ghidra sessions the same +compact naming results as Hopper: a local function entry renames that function, +and any other mapped address, such as a global, receives a new or renamed primary +label. A batch applies each address separately and reports one success boolean +per address. Use them to name many globals and helpers cheaply; use +`annotate_native_function` when you also want comments and a refreshed dossier. Ghidra name edits preserve the existing namespace. Supply either a leaf name such as `renamed` or a fully qualified name in that namespace, such as `alpha::renamed`. The returned qualified name can be reused as an idempotent diff --git a/scripts/lib/real-ghidra-namespace-annotations.mjs b/scripts/lib/real-ghidra-namespace-annotations.mjs index 43e9acdba..f67343526 100644 --- a/scripts/lib/real-ghidra-namespace-annotations.mjs +++ b/scripts/lib/real-ghidra-namespace-annotations.mjs @@ -54,6 +54,14 @@ export async function verifyGhidraNamespaceAnnotations({ ); return { namespace, address: procedure.address }; }); + const names = await call("list_names"); + const globals = ["alpha", "outer::inner"].map((namespace) => { + const symbol = names.find( + (item) => item.value === `${namespace}::value` && item.symbol.primary, + ); + assert.ok(symbol, `Missing demangled fixture global ${namespace}::value`); + return { namespace, address: symbol.address }; + }); const ambiguous = await reject("procedure_address", { procedure: "same" }); assert.equal(ambiguous.code, "invalid_request"); for (const { address } of entries) @@ -125,6 +133,16 @@ export async function verifyGhidraNamespaceAnnotations({ }); assert.deepEqual(unchanged.annotations, qualified.annotations); } + await verifyAddressNames(call, [entries[0], entries[2], ...globals]); + for (const { namespace, address } of entries) { + assert.equal( + await call("set_address_name", { + address, + name: `${namespace}::qualified`, + }), + true, + ); + } const { stdout } = await exec( process.execPath, [ @@ -159,6 +177,96 @@ export async function verifyGhidraNamespaceAnnotations({ } } +async function verifyAddressNames(call, symbols) { + for (const { namespace, address } of symbols) { + assert.equal( + await call("set_address_name", { address, name: "address_renamed" }), + true, + ); + const qualified = `${namespace}::address_qualified`; + assert.equal( + await call("set_address_name", { address, name: qualified }), + true, + ); + assert.equal(await call("address_name", { address }), qualified); + assert.equal( + await call("set_address_name", { address, name: "other::literal" }), + true, + ); + const literal = await call("address_name", { address }); + assert.equal(literal, `${namespace}::other::literal`); + assert.equal( + await call("set_address_name", { address, name: literal }), + true, + ); + assert.equal(await call("address_name", { address }), literal); + assert.equal( + await call("set_address_name", { address, name: qualified }), + true, + ); + for (let repeat = 0; repeat < 2; repeat++) { + const readback = await call("address_name", { address }); + assert.deepEqual( + await call("set_addresses_names", { names: { [address]: readback } }), + { [address]: true }, + ); + assert.equal(await call("address_name", { address }), qualified); + } + assert.equal( + await call("set_address_name", { address, name: `${namespace}::` }), + false, + ); + const session = await call("binary_session"); + for (const operation of [ + "set_address_name", + "set_addresses_names", + "address_name", + "list_names", + ]) + assert.equal( + session.capabilities.find((item) => item.operation === operation) + ?.available, + true, + `An invalid name must not mark ${operation} unavailable`, + ); + assert.equal(await call("address_name", { address }), qualified); + } + const [valid, invalid] = symbols; + assert.deepEqual( + await call("set_addresses_names", { + names: { + [valid.address]: `${valid.namespace}::batch_renamed`, + [invalid.address]: `${invalid.namespace}::`, + }, + }), + { [valid.address]: true, [invalid.address]: false }, + ); + assert.equal( + await call("address_name", { address: valid.address }), + `${valid.namespace}::batch_renamed`, + ); + assert.equal( + await call("address_name", { address: invalid.address }), + `${invalid.namespace}::address_qualified`, + ); + const names = await call("list_names"); + for (const { namespace, address } of symbols) { + const expected = + address === valid.address + ? `${namespace}::batch_renamed` + : `${namespace}::address_qualified`; + assert.ok( + names.some( + (item) => + item.address === address && + item.value === expected && + item.symbol.primary, + ), + `Missing refreshed primary symbol ${expected}`, + ); + } +} + async function requireNamespaceDemangler(env) { const inspection = inspectGhidraInstallation({ environment: process.env, diff --git a/src/ghidra/GhidraInventoryValues.ts b/src/ghidra/GhidraInventoryValues.ts index ebb4c30cc..86c17a7fb 100644 --- a/src/ghidra/GhidraInventoryValues.ts +++ b/src/ghidra/GhidraInventoryValues.ts @@ -13,7 +13,10 @@ import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js"; import { err, ok, type Result } from "../domain/result.js"; import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js"; -/** Read-only direct inventory operations admitted by the Ghidra adapter. */ +/** + * Direct inventory operations admitted by the Ghidra adapter. All are reads + * except the naming operations, which edit the ephemeral session database. + */ export const GHIDRA_INVENTORY_OPERATIONS = [ "inspect_native_load_image", "read_bytes", @@ -28,8 +31,16 @@ export const GHIDRA_INVENTORY_OPERATIONS = [ "resolve_containing_procedure", "search_procedures", "search_strings", + "set_address_name", + "set_addresses_names", ] as const; +/** Inventory operations that write analyst names into the session database. */ +export const GHIDRA_NAMING_OPERATIONS = [ + "set_address_name", + "set_addresses_names", +] as const satisfies readonly GhidraInventoryOperation[]; + /** One direct inventory operation implemented by the packaged Java bridge. */ export type GhidraInventoryOperation = (typeof GHIDRA_INVENTORY_OPERATIONS)[number]; @@ -83,6 +94,19 @@ const inputSchemas = { .strict(), search_procedures: z.object(searchInput).strict(), search_strings: z.object(searchInput).strict(), + set_address_name: z + .object({ document, address: explicitAddress, name: z.string().min(1) }) + .strict(), + set_addresses_names: z + .object({ + document, + names: z.record(explicitAddress, z.string().min(1)), + }) + .strict() + .refine( + (value) => Object.keys(value.names).length > 0, + "Supply at least one address name", + ), } satisfies Readonly>; /** Validate and default one provider request before it crosses the socket. */ @@ -281,6 +305,8 @@ const resultSchemas = { resolve_containing_procedure: containingProcedure, search_procedures: z.array(searchItem), search_strings: z.array(searchItem), + set_address_name: z.boolean(), + set_addresses_names: z.record(z.string(), z.boolean()), } satisfies Readonly>; /** Validate exact Java-bridge output before creating Evidence. */ diff --git a/src/ghidra/GhidraProviderCapabilities.ts b/src/ghidra/GhidraProviderCapabilities.ts index 5b9a6ca8b..943380789 100644 --- a/src/ghidra/GhidraProviderCapabilities.ts +++ b/src/ghidra/GhidraProviderCapabilities.ts @@ -8,6 +8,7 @@ import { } from "../process/WindowsAuthority.js"; import { GHIDRA_FUNCTION_OPERATIONS } from "./GhidraFunctionValues.js"; import { GHIDRA_INVENTORY_OPERATIONS } from "./GhidraInventoryValues.js"; +import { GHIDRA_MUTATING_OPERATIONS } from "./GhidraSessionValues.js"; /** Public identity committed by every Ghidra-backed observation. */ export const GHIDRA_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({ @@ -50,6 +51,14 @@ export const limitationsFor = (operation: string): readonly string[] => { "Annotation text rejects NUL and unpaired Unicode surrogates before mutation, with the field and UTF-16 index in the error. Supported Unicode and line endings are preserved.", "Metadata edits invalidate immutable analysis snapshots and are discarded on close. CLI returns the updated dossier before session cleanup; this is not a saved Ghidra project.", ]; + case "set_address_name": + case "set_addresses_names": + return [ + ...common, + "Names use Ghidra USER_DEFINED source. A local function entry renames that function; any other mapped address receives a new or renamed primary label. Existing symbols retain their namespace and accept leaf names or qualified names within that namespace; qualified readback can be reused without adding namespace prefixes. Readback compares the primary symbol name.", + "set_addresses_names applies each address in its own transaction and reports per-address success; invalid names, unmapped addresses, and readback mismatches roll back only that address.", + "Metadata edits invalidate immutable analysis snapshots and are discarded on close; this is not a saved Ghidra project.", + ]; case "inspect_native_load_image": return [ ...common, @@ -162,12 +171,12 @@ export const CAPABILITIES: readonly CapabilityDescriptor[] = Object.freeze( reason: null, // Project writes are private import machinery. Analysis reads are bound // to the artifact/profile; session annotations disable snapshot replay. - ...(operation === "annotate_native_function" || + ...(GHIDRA_MUTATING_OPERATIONS.has(operation) || operation === "list_documents" ? {} : { cachePolicy: "snapshot" as const }), effects: Object.freeze({ - mutatesArtifact: operation === "annotate_native_function", + mutatesArtifact: GHIDRA_MUTATING_OPERATIONS.has(operation), launchesProcess: true, mayShowUi: false, mayAccessNetwork: false, diff --git a/src/ghidra/GhidraSessionValues.test.ts b/src/ghidra/GhidraSessionValues.test.ts index 1de80268c..25a864f8d 100644 --- a/src/ghidra/GhidraSessionValues.test.ts +++ b/src/ghidra/GhidraSessionValues.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { GHIDRA_SESSION_CAPABILITIES, + GHIDRA_MUTATING_OPERATIONS, parseGhidraSessionInfo, } from "./GhidraSessionValues.js"; @@ -81,7 +82,7 @@ describe("Ghidra mutation handshake", () => { ...value, read_only: true, capabilities: value.capabilities.filter( - (c) => c !== "annotate_native_function", + (c) => !GHIDRA_MUTATING_OPERATIONS.has(c), ), }; expect( @@ -92,4 +93,24 @@ describe("Ghidra mutation handshake", () => { parseGhidraSessionInfo(value, { ...expected, expectedReadOnly: true }).ok, ).toBe(false); }); + it("treats address naming as database mutation", () => { + expect([...GHIDRA_MUTATING_OPERATIONS].sort()).toEqual([ + "annotate_native_function", + "set_address_name", + "set_addresses_names", + ]); + const value = session(); + for (const naming of ["set_address_name", "set_addresses_names"]) { + expect(value.capabilities).toContain(naming); + expect( + parseGhidraSessionInfo( + { + ...value, + capabilities: value.capabilities.filter((c) => c !== naming), + }, + expected, + ).ok, + ).toBe(false); + } + }); }); diff --git a/src/ghidra/GhidraSessionValues.ts b/src/ghidra/GhidraSessionValues.ts index 64bbe74c8..81fc273a4 100644 --- a/src/ghidra/GhidraSessionValues.ts +++ b/src/ghidra/GhidraSessionValues.ts @@ -2,7 +2,10 @@ import { z } from "zod"; import type { JsonValue } from "../domain/jsonValue.js"; import { err, ok, type Result } from "../domain/result.js"; -import { GHIDRA_INVENTORY_OPERATIONS } from "./GhidraInventoryValues.js"; +import { + GHIDRA_INVENTORY_OPERATIONS, + GHIDRA_NAMING_OPERATIONS, +} from "./GhidraInventoryValues.js"; import { GHIDRA_FUNCTION_OPERATIONS } from "./GhidraFunctionValues.js"; import { ghidraExtensionResultSchema } from "./extensions/GhidraExtensions.js"; @@ -14,6 +17,12 @@ export const GHIDRA_SESSION_CAPABILITIES = [ ...GHIDRA_FUNCTION_OPERATIONS, ] as const; +/** Methods that edit the session database; read-only bridges omit them. */ +export const GHIDRA_MUTATING_OPERATIONS: ReadonlySet = new Set([ + "annotate_native_function", + ...GHIDRA_NAMING_OPERATIONS, +]); + const capabilitySchema = z.enum(GHIDRA_SESSION_CAPABILITIES); const sessionInfoSchema = z @@ -64,7 +73,7 @@ export const parseGhidraSessionInfo = ( const parsed = sessionInfoSchema.safeParse(value); const readOnly = expected.expectedReadOnly ?? false; const expectedCapabilities = GHIDRA_SESSION_CAPABILITIES.filter( - (capability) => !readOnly || capability !== "annotate_native_function", + (capability) => !readOnly || !GHIDRA_MUTATING_OPERATIONS.has(capability), ); if ( !parsed.success || diff --git a/tests/boundary/mcp/ghidraEvidenceMcp.test.ts b/tests/boundary/mcp/ghidraEvidenceMcp.test.ts index 4d6d3da23..9fa1f3b97 100644 --- a/tests/boundary/mcp/ghidraEvidenceMcp.test.ts +++ b/tests/boundary/mcp/ghidraEvidenceMcp.test.ts @@ -62,6 +62,66 @@ it("rejects contradictory annotation readback across the provider and MCP bounda } }); +it("routes address naming to Ghidra with compact per-address results", async () => { + const requests: { operation: string; input: unknown }[] = []; + let output: unknown = null; + const harness = await connectGhidraMcp( + "ghidra-address-naming", + (operation, input) => { + requests.push({ operation, input }); + return Promise.resolve(ok(jsonValueSchema.parse(output))); + }, + ); + try { + output = true; + const single = await harness.mcp.callTool({ + name: "set_address_name", + arguments: { address: "0x1000", name: "fixture_entry" }, + }); + expect(single.isError).not.toBe(true); + expect(single.structuredContent).toMatchObject({ normalized_result: true }); + output = { "0x1000": true, "0x2000": false }; + const batch = await harness.mcp.callTool({ + name: "set_addresses_names", + arguments: { names: { "0x1000": "fixture_entry", "0x2000": "g_value" } }, + }); + expect(batch.isError).not.toBe(true); + expect(batch.structuredContent).toMatchObject({ + normalized_result: { "0x1000": true, "0x2000": false }, + }); + expect(requests).toEqual([ + { + operation: "set_address_name", + input: { document: null, address: "0x1000", name: "fixture_entry" }, + }, + { + operation: "set_addresses_names", + input: { + document: null, + names: { "0x1000": "fixture_entry", "0x2000": "g_value" }, + }, + }, + ]); + const empty = await harness.mcp.callTool({ + name: "set_addresses_names", + arguments: { names: {} }, + }); + expect(empty.isError).toBe(true); + expect(requests).toHaveLength(2); + output = "fixture_entry"; + const malformed = await harness.mcp.callTool({ + name: "set_address_name", + arguments: { address: "0x1000", name: "fixture_entry" }, + }); + expect(malformed.isError).toBe(true); + expect(parseMcpToolError(malformed)).toMatchObject({ + error: { code: "unreadable_output" }, + }); + } finally { + await harness.close(); + } +}); + describe("Ghidra MCP evidence parity", () => { it("preserves provider evidence, composed parity, and capability routing", async () => { const harness = await connectGhidraMcp("ghidra-parity"); diff --git a/tests/boundary/providers/ghidra/ghidraClient.test.ts b/tests/boundary/providers/ghidra/ghidraClient.test.ts index bcb4de53f..bbc851c2b 100644 --- a/tests/boundary/providers/ghidra/ghidraClient.test.ts +++ b/tests/boundary/providers/ghidra/ghidraClient.test.ts @@ -19,7 +19,10 @@ import type { GhidraLauncher, } from "../../../../src/ghidra/GhidraLauncher.js"; import type { GhidraTransportKind } from "../../../../src/ghidra/GhidraTransport.js"; -import { GHIDRA_SESSION_CAPABILITIES } from "../../../../src/ghidra/GhidraSessionValues.js"; +import { + GHIDRA_MUTATING_OPERATIONS, + GHIDRA_SESSION_CAPABILITIES, +} from "../../../../src/ghidra/GhidraSessionValues.js"; import { waitForExit as waitForChildExit } from "../../../support/process/processFixture.js"; import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; @@ -177,7 +180,7 @@ describe("GhidraClient", () => { capabilities: GHIDRA_SESSION_CAPABILITIES.filter( (value) => HOST_TRANSPORT === "unix-socket" || - value !== "annotate_native_function", + !GHIDRA_MUTATING_OPERATIONS.has(value), ), target: { image_base: "0x1000", diff --git a/tests/conformance/ghidra/namespaces.c b/tests/conformance/ghidra/namespaces.c index 3dc12faa0..c672697c3 100644 --- a/tests/conformance/ghidra/namespaces.c +++ b/tests/conformance/ghidra/namespaces.c @@ -5,6 +5,11 @@ #define REA_CPP_SYMBOL(name) name #endif +volatile int rea_namespace_alpha_value + __asm__(REA_CPP_SYMBOL("_ZN5alpha5valueE")) = 3; +volatile int rea_namespace_nested_value + __asm__(REA_CPP_SYMBOL("_ZN5outer5inner5valueE")) = 1; + __attribute__((noinline, used)) int rea_namespace_alpha(int value) __asm__(REA_CPP_SYMBOL("_ZN5alpha4sameEi")); __attribute__((noinline, used)) int rea_namespace_beta(int value) @@ -12,9 +17,9 @@ __attribute__((noinline, used)) int rea_namespace_beta(int value) __attribute__((noinline, used)) int rea_namespace_nested(int value) __asm__(REA_CPP_SYMBOL("_ZN5outer5inner4sameEi")); -int rea_namespace_alpha(int value) { return value + 3; } +int rea_namespace_alpha(int value) { return value + rea_namespace_alpha_value; } int rea_namespace_beta(int value) { return value * 2; } -int rea_namespace_nested(int value) { return value - 1; } +int rea_namespace_nested(int value) { return value - rea_namespace_nested_value; } int main(void) { return rea_namespace_alpha(1) + rea_namespace_beta(2) + diff --git a/tests/fixtures/fakeGhidra.mjs b/tests/fixtures/fakeGhidra.mjs index a1255a22c..92f698001 100644 --- a/tests/fixtures/fakeGhidra.mjs +++ b/tests/fixtures/fakeGhidra.mjs @@ -231,6 +231,8 @@ const sessionInfo = ({ "resolve_containing_procedure", "search_procedures", "search_strings", + "set_address_name", + "set_addresses_names", "annotate_native_function", "inspect_native_data_type", "inspect_native_instruction", @@ -246,7 +248,12 @@ const sessionInfo = ({ "xrefs", ].filter( (value) => - transport === "unix-socket" || value !== "annotate_native_function", + transport === "unix-socket" || + ![ + "annotate_native_function", + "set_address_name", + "set_addresses_names", + ].includes(value), ), target: { name: "fixture",