From 2a17f77b1c8eaecba26d3ee561d77ab3dbaabd22 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Mon, 2 Nov 2026 02:15:31 +0800 Subject: [PATCH] fix(ghidra): clarify function annotation selectors --- docs/native-investigation.md | 4 +++- scripts/lib/real-ghidra-boundaries.mjs | 8 ++++++++ scripts/lib/real-ghidra-entry-aliases.mjs | 9 +++++++++ src/cli/coreNativeCommands.ts | 4 +++- src/contracts/officialToolContracts.ts | 2 +- src/domain/native/nativeFunctionAnnotations.ts | 7 ++++++- 6 files changed, 30 insertions(+), 4 deletions(-) diff --git a/docs/native-investigation.md b/docs/native-investigation.md index 1bf77b8d87..cefbe8230d 100644 --- a/docs/native-investigation.md +++ b/docs/native-investigation.md @@ -1137,7 +1137,9 @@ handle-based path admission. See [Windows Ghidra P0](windows-ghidra-p0.md) and [issue #527](https://github.com/morluto/rea/issues/527). On Linux and macOS, `annotate_native_function` atomically edits a function name and entry comments in the ephemeral database, returning refreshed analysis -without changing executable bytes. Annotation text must contain no NUL or +without changing executable bytes. A Ghidra address inside a function selects +the containing function; the name and comments are changed at its entry, not +at the selected instruction. Annotation text must contain no NUL or 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 diff --git a/scripts/lib/real-ghidra-boundaries.mjs b/scripts/lib/real-ghidra-boundaries.mjs index e6abb17a26..b5836526b4 100644 --- a/scripts/lib/real-ghidra-boundaries.mjs +++ b/scripts/lib/real-ghidra-boundaries.mjs @@ -29,6 +29,14 @@ export async function verifyGhidraBoundaries( ) { const options = { timeout: 180000 }; const catalog = await client.listTools(); + const annotation = catalog.tools.find( + (tool) => tool.name === "annotate_native_function", + ); + assert.match( + annotation?.description ?? "", + /interior address.*containing function.*function entry.*procedure_address/u, + "The annotation catalog must explain where an interior selector edits", + ); const ajv = new Ajv({ strict: false }); addFormats(ajv); const validators = new Map(); diff --git a/scripts/lib/real-ghidra-entry-aliases.mjs b/scripts/lib/real-ghidra-entry-aliases.mjs index 7b87ec9bb3..55f10fe7c7 100644 --- a/scripts/lib/real-ghidra-entry-aliases.mjs +++ b/scripts/lib/real-ghidra-entry-aliases.mjs @@ -156,6 +156,15 @@ export async function verifyGhidraEntryAliases({ alias.address, "Explicit interior addresses keep their existing containment semantics", ); + const interiorEdit = await call("annotate_native_function", { + procedure: interior.address, + comment: "Selected by interior address", + }); + assert.equal(interiorEdit.annotations.address, alias.address); + assert.equal( + interiorEdit.annotations.comment, + "Selected by interior address", + ); const edited = await call("annotate_native_function", { procedure: alias.value, comment: "Selected by imported entry alias", diff --git a/src/cli/coreNativeCommands.ts b/src/cli/coreNativeCommands.ts index d438e4bb39..5161644363 100644 --- a/src/cli/coreNativeCommands.ts +++ b/src/cli/coreNativeCommands.ts @@ -469,7 +469,9 @@ const registerAnnotationCommand = ( path: localPathStringSchema.describe("Local executable path"), procedure: z .string() - .describe("Exact function entry address or unique name"), + .describe( + "Function name or address; Ghidra resolves interior addresses to the containing function and edits its entry", + ), }), options: z.object({ name: z.string().min(1).optional().describe("Analyst function name"), diff --git a/src/contracts/officialToolContracts.ts b/src/contracts/officialToolContracts.ts index 9d1ab1a899..a5a2fa4ae9 100644 --- a/src/contracts/officialToolContracts.ts +++ b/src/contracts/officialToolContracts.ts @@ -50,7 +50,7 @@ const official = < export const OFFICIAL_TOOL_CONTRACTS = [ official( "annotate_native_function", - "Atomically update one function name and/or entry comments, read them back, and return a refreshed complete function dossier. Ghidra changes its ephemeral session analysis database only; original executable bytes remain unchanged. Empty comment text clears that comment. Edits remain available to subsequent MCP calls until close; CLI returns the updated dossier before discarding its session. Invalid edits roll back the entire operation. Windows P0 and providers without this capability are unsupported.", + "Atomically update one function name and/or entry comments, read them back, and return a refreshed complete function dossier. Ghidra resolves an interior address to its containing function and applies edits at the function entry, not at the selected instruction; use procedure_address first to find that entry. Ghidra changes its ephemeral session analysis database only; original executable bytes remain unchanged. Empty comment text clears that comment. Edits remain available to subsequent MCP calls until close; CLI returns the updated dossier before discarding its session. Invalid edits roll back the entire operation. Windows P0 and providers without this capability are unsupported.", nativeFunctionAnnotationsInputSchema, ), official( diff --git a/src/domain/native/nativeFunctionAnnotations.ts b/src/domain/native/nativeFunctionAnnotations.ts index 0e5d5f12b8..3d49d1054d 100644 --- a/src/domain/native/nativeFunctionAnnotations.ts +++ b/src/domain/native/nativeFunctionAnnotations.ts @@ -4,7 +4,12 @@ import { functionDossierSchema } from "../native/functionDossier.js"; /** One explicit function's analyst-authored metadata changes. */ export const nativeFunctionAnnotationsInputSchema = z .strictObject({ - procedure: z.string().min(1), + procedure: z + .string() + .min(1) + .describe( + "Function name or address selecting a function; edits apply at its entry.", + ), name: z.string().min(1).optional(), comment: z .string()