diff --git a/docs/native-investigation.md b/docs/native-investigation.md index 2c785eedca..ebdff7fbf1 100644 --- a/docs/native-investigation.md +++ b/docs/native-investigation.md @@ -1256,7 +1256,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 67787e5d2d..7bd4aa10f6 100644 --- a/scripts/lib/real-ghidra-boundaries.mjs +++ b/scripts/lib/real-ghidra-boundaries.mjs @@ -13,6 +13,7 @@ import { promisify } from "node:util"; import { GHIDRA_ANNOTATIONS_RETAINED } from "../../dist/ghidra/GhidraProviderClient.js"; import Ajv from "ajv/dist/2020.js"; import addFormats from "ajv-formats"; +import { OFFICIAL_TOOL_CONTRACTS } from "../../dist/contracts/officialToolContracts.js"; import { verifyLegacyGhidraReferenceSnapshot } from "./ghidra-reference-snapshot-e2e.mjs"; import { verifyGhidraSnapshotLifecycle } from "./real-ghidra-snapshot-lifecycle.mjs"; @@ -30,6 +31,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", + ); + const annotationContract = OFFICIAL_TOOL_CONTRACTS.find( + (tool) => tool.name === "annotate_native_function", + ); + assert.ok(annotationContract, "Annotation contract is missing"); + assert.equal(annotation?.description, annotationContract.description); 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 a3ebd76d27..189553e1be 100644 --- a/src/cli/coreNativeCommands.ts +++ b/src/cli/coreNativeCommands.ts @@ -482,7 +482,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 9b14990c7f..ae9d52a1e0 100644 --- a/src/contracts/officialToolContracts.ts +++ b/src/contracts/officialToolContracts.ts @@ -57,7 +57,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 edits owned Program metadata; original executable bytes remain unchanged. Empty comment text clears that comment. With REA_GHIDRA_CHECKPOINT_DIR configured at startup, normal close saves edits for the next process; ephemeral mode discards them. CLI follows the same setting after returning its updated dossier. Check binary_session capabilities for the current mode. 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 edits owned Program metadata; original executable bytes remain unchanged. Empty comment text clears that comment. With REA_GHIDRA_CHECKPOINT_DIR configured at startup, normal close saves edits for the next process; ephemeral mode discards them. CLI follows the same setting after returning its updated dossier. Check binary_session capabilities for the current mode. 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()