fix(ghidra): admit Linux arm64 with matching native tools

This commit is contained in:
morluto 2026-10-09 07:09:41 +08:00
parent 1f79002497
commit 3e598b2d38
11 changed files with 139 additions and 66 deletions

View File

@ -2,7 +2,7 @@
## Product Direction
REA exposes reverse-engineering tools through a CLI and MCP server. Hopper, the bring-your-own Ghidra adapter, and the bring-your-own IDA MCP adapter are operation-capable deep binary-analysis providers. Ghidra is supported on Linux x64 and macOS x64/arm64 with matching native decompiler tools, and has an experimental Windows x64 P0 boundary for approved native x86 and x86-64 PE applications; on Linux and macOS it supplies inventory, function analysis, and atomic function annotation edits in an ephemeral database, without modifying executable bytes or controlling a GUI. Windows P0 has no mutation authority. IDA adapts upstream legacy attached GUI and modern headless database-supervisor profiles for live read-only analysis; initial real verification covers Windows, and an attached GUI database is never saved or closed. Keep provider-specific code out of the domain and application layers.
REA exposes reverse-engineering tools through a CLI and MCP server. Hopper, the bring-your-own Ghidra adapter, and the bring-your-own IDA MCP adapter are operation-capable deep binary-analysis providers. Ghidra is supported on Linux x64/arm64 and macOS x64/arm64 with matching native decompiler tools, and has an experimental Windows x64 P0 boundary for approved native x86 and x86-64 PE applications; on Linux and macOS it supplies inventory, function analysis, and atomic function annotation edits in an ephemeral database, without modifying executable bytes or controlling a GUI. Windows P0 has no mutation authority. IDA adapts upstream legacy attached GUI and modern headless database-supervisor profiles for live read-only analysis; initial real verification covers Windows, and an attached GUI database is never saved or closed. Keep provider-specific code out of the domain and application layers.
Prioritize:

View File

@ -415,14 +415,17 @@ copying license secrets into logs, or killing unrelated Hopper processes.
## Ghidra
REA connects to an existing Ghidra installation on Linux x64, macOS x64/arm64,
REA connects to an existing Ghidra installation on Linux x64/arm64, macOS x64/arm64,
or experimental Windows x64 P0.
It accepts Ghidra 12.1.x and the 64-bit full JDK declared by that installation's
`application.java.min` and `application.java.max`. Current 12.1 releases require
JDK 21 or newer and set no maximum. The bridge is verified with Ghidra 12.1.4
and JDK 21. On macOS, the installation
must include the native decompiler for the host architecture; REA does not
build it or change Gatekeeper quarantine settings.
and JDK 21. Each installation must include the native decompiler for the host
architecture in `Ghidra/Features/Decompiler/os/<platform>/` or the corresponding
`build/os/<platform>/` directory. Linux ARM64 uses `linux_arm_64`; official
release archives may require you to build that native component separately.
REA checks the executable prerequisite and does not build or install native
tools, or change Gatekeeper quarantine settings.
The adapter exposes 25 read-only operations: thirteen inventory/name/search
operations and twelve function-analysis operations. These cover metadata,
@ -525,7 +528,7 @@ continues. These messages redact known bridge authentication tokens and
preserve local paths and other analysis context.
Run `GHIDRA_INSTALL_DIR=... npm run verify:ghidra` from a source checkout to
compile and analyze debug and stripped host-native fixtures (ELF on Linux x64
compile and analyze debug and stripped host-native fixtures (ELF on Linux x64/arm64
or Mach-O on macOS), plus a native DWARF 4 type-layout object. This lane needs a
host C compiler in addition to Ghidra and its JDK.

View File

@ -339,7 +339,7 @@ the tool fails with a `debugger-attach-denied` reason.
## Provider and verification boundaries
Install a Ghidra 12.1.x release and the 64-bit full JDK it declares, then configure REA to
use them. Ghidra analysis supports Linux x64 and macOS x64/arm64; macOS requires
use them. Ghidra analysis supports Linux x64/arm64 and macOS x64/arm64; each installation requires
the matching native decompiler. Experimental Windows x64 P0 admits native
x86-64 PE applications on local NTFS using bundled Job Object ownership,
protected runtime DACLs, and handle-based path admission. See [Windows Ghidra P0](windows-ghidra-p0.md) and

View File

@ -2,11 +2,11 @@
This guide describes repository main. Check the [released package boundary](installation.md#released-package-and-main) when using npm.
Ghidra read-only analysis is available on Linux x64 and macOS x64/arm64.
Ghidra read-only analysis is available on Linux x64/arm64 and macOS x64/arm64.
Install a Ghidra 12.1.x release and the 64-bit full JDK that release declares,
then configure REA to use them. Current 12.1 releases require JDK 21 or newer
and set no maximum. The bridge is verified with Ghidra 12.1.4 and JDK 21.
macOS installations need the matching native decompiler.
Each installation needs the matching native decompiler.
REA imports one target into a temporary project and exposes 25 read-only
operations after analysis completes. They cover inventories, search,
@ -121,7 +121,7 @@ capability from a successful import.
| Result extent | Function instruction scans and native API boundary observations remain complete. P-code flow is the explicit bounded exception; its count fields distinguish exact retained counts from lower-bound or known-omitted counts. Caller cancellation and provider failures remain distinct. |
`npm run verify:ghidra` compiles the versioned C oracle into debug and stripped
host-native targets (x86-64 ELF on Linux x64 or Mach-O on macOS), plus a native
host-native targets (x86-64/AArch64 ELF on Linux or Mach-O on macOS), plus a native
DWARF 4 type-layout object. It proves the admitted operations, external functions, resolved thunks, exports, stripped-name
behavior, direct and targetless indirect calls, typed references, strings/xrefs,
multi-block CFG, bounded p-code def-use/effect links, semantic enhanced workflows, cancellation, startup deadlines,

View File

@ -69,10 +69,10 @@ verified paths for an existing Ghidra installation. It configures Claude Code,
Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin, OpenCode, Antigravity,
GitHub Copilot CLI, Command Code, VS Code, and Grok Build using each client's configuration format. Grok Bot is detected locally, but its connector is added from the Grok Bot chat because the signed-in account store is not a data-directory file.
Ghidra analysis supports Linux x64 and macOS x64/arm64 with Ghidra 12.1.x and the
Ghidra analysis supports Linux x64/arm64 and macOS x64/arm64 with Ghidra 12.1.x and the
64-bit full JDK that installation declares. Current 12.1 releases require JDK 21
or newer and set no maximum; the bridge is verified with Ghidra 12.1.4 and JDK 21.
macOS also requires the matching native decompiler. The
Each installation requires the matching native decompiler. The
adapter exposes thirteen inventory operations and twelve function-analysis
operations, for 25 read-only operations total. Linux and macOS additionally support
atomic function-name and entry-comment edits with refreshed analysis; metadata

View File

@ -307,12 +307,18 @@ prove behavior on the verified Ghidra 12.1.4 and JDK 21 build.
| Ghidra lane | Supported runner/target | Additional local tools |
| ------------------------------------------ | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| `npm run verify:ghidra` | Linux x64 ELF or macOS x64/arm64 Mach-O | Host C compiler, Ghidra 12.1.4, and full JDK 21 |
| `npm run verify:ghidra` | Linux x64/arm64 ELF or macOS x64/arm64 Mach-O | Host C compiler, Ghidra 12.1.4, and full JDK 21 |
| `npm run verify:ghidra:switch` | Linux x64 ELF; GCC/Clang optimized and stripped switch fixtures | GCC, Clang, GNU nm/objdump/strip, Ghidra 12.1.4, and full JDK 21 |
| `npm run verify:ghidra:aarch64-jump-table` | Any supported Ghidra host; AArch64 ELF; byte/halfword tables; host ARM64 Mach-O | Clang with AArch64 target support, Ghidra 12.1.4, and full JDK 21 |
| `npm run verify:ghidra:cross-format` | Any supported Ghidra host; also analyzes AArch64 ELF, x86-64 PE, and x86-64 Mach-O | `clang`, LLD, and `lld-link` in addition to host-lane prerequisites |
| `npm run verify:ghidra:windows` | Controlled Windows x64 with native x86-64 PE; `-- --x86` selects native x86 PE | Ghidra 12.1.4, full JDK 21, and the matching native artifact |
The host lane also verifies namespaced C++ ABI symbols and annotation identity.
That facet requires Ghidra's matching `demangler_gnu_v2_41` native component in
`GPL/DemanglerGnu/os/<platform>/` or `build/os/<platform>/`, in addition to the
native decompiler. The namespace verifier reports missing or non-executable
components before testing the fixture. REA does not build or install them.
Windows native conformance runs with `npm run verify:windows-native` and does
not require Ghidra or Java. An optional independently compiled Windows fixture
adds in-place reparse, breakaway, environment, and token observations.

View File

@ -1,11 +1,13 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { constants } from "node:fs";
import { access, mkdtemp, readFile, rm, stat } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { basename, dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { inspectGhidraInstallation } from "../../dist/ghidra/GhidraInstallation.js";
/** Exercise qualified annotation names against real demangled C++ ABI symbols. */
export async function verifyGhidraNamespaceAnnotations({
@ -15,6 +17,7 @@ export async function verifyGhidraNamespaceAnnotations({
entrypoint,
env,
}) {
await requireNamespaceDemangler(env);
const exec = promisify(execFile);
const compiler = process.env.REA_CC ?? "cc";
try {
@ -155,3 +158,45 @@ export async function verifyGhidraNamespaceAnnotations({
await rm(workspace, { recursive: true, force: true });
}
}
async function requireNamespaceDemangler(env) {
const inspection = inspectGhidraInstallation({
installDir: env.GHIDRA_INSTALL_DIR,
...(env.JAVA_HOME === undefined ? {} : { javaHome: env.JAVA_HOME }),
});
if (
inspection.status !== "available" ||
inspection.nativeDecompilerPath === null
)
throw new Error(
`Ghidra namespace verification requires an available installation: ${JSON.stringify(inspection)}`,
);
const nativeDirectory = basename(dirname(inspection.nativeDecompilerPath));
const executable =
process.platform === "win32"
? "demangler_gnu_v2_41.exe"
: "demangler_gnu_v2_41";
const failures = [];
for (const location of ["os", "build/os"]) {
const path = join(
inspection.installDir,
"GPL",
"DemanglerGnu",
location,
nativeDirectory,
executable,
);
try {
await access(path, constants.X_OK);
if ((await stat(path)).isFile()) return;
failures.push(`${path}: not a regular file`);
} catch (cause) {
failures.push(
`${path}: ${cause instanceof Error ? cause.message : String(cause)}`,
);
}
}
throw new Error(
`Ghidra namespace annotation verification requires the matching GNU demangler native component. Provide ${executable} for ${nativeDirectory} in GPL/DemanglerGnu/os or its build/os equivalent; REA does not build it. Checked: ${failures.join("; ")}`,
);
}

View File

@ -299,11 +299,12 @@ function nativeFixtureTarget(platform, architecture) {
"darwin-arm64": { format: "mach-o", architecture: "arm64" },
"darwin-x64": { format: "mach-o", architecture: "x86_64" },
"linux-x64": { format: "elf", architecture: "x86_64" },
"linux-arm64": { format: "elf", architecture: "arm64" },
};
const target = supportedTargets[`${platform}-${architecture}`];
if (target === undefined)
throw new Error(
`The Ghidra verifier does not support a host-native fixture on ${platform}/${architecture}; use a supported Linux x64 or macOS x64/arm64 host.`,
`The Ghidra verifier does not support a host-native fixture on ${platform}/${architecture}; use a supported Linux or macOS x64/arm64 host.`,
);
return target;
}

View File

@ -138,7 +138,7 @@ describe("Ghidra doctor integration", () => {
],
[
"unsupported architecture",
inspection({ installDir: INSTALL, architecture: "arm64" }),
inspection({ installDir: INSTALL, architecture: "arm" }),
"ghidra-architecture",
],
] as const)("distinguishes %s", async (_label, ghidra, checkName) => {

View File

@ -12,10 +12,12 @@ import {
const INSTALL = "/opt/ghidra";
const PROPERTIES = `${INSTALL}/Ghidra/application.properties`;
const HEADLESS = `${INSTALL}/support/analyzeHeadless`;
const LINUX_DECOMPILER = `${INSTALL}/Ghidra/Features/Decompiler/os/linux_x86_64/decompile`;
const MAC_DECOMPILER = `${INSTALL}/Ghidra/Features/Decompiler/os/mac_arm_64/decompile`;
const WINDOWS_INSTALL = "C:\\tools\\ghidra_12.1.4_PUBLIC";
const WINDOWS_PROPERTIES = `${WINDOWS_INSTALL}\\Ghidra\\application.properties`;
const WINDOWS_HEADLESS = `${WINDOWS_INSTALL}\\support\\analyzeHeadless.bat`;
const WINDOWS_DECOMPILER = `${WINDOWS_INSTALL}\\Ghidra\\Features\\Decompiler\\os\\win_x86_64\\decompile.exe`;
const JAVA: GhidraJavaObservation = {
version: "21.0.11",
major: SUPPORTED_GHIDRA_JAVA_MAJOR,
@ -31,7 +33,8 @@ const host = (
path === PROPERTIES
? `application.version=${SUPPORTED_GHIDRA_VERSION}\n`
: undefined,
executable: (path) => path === HEADLESS || path === MAC_DECOMPILER,
executable: (path) =>
path === HEADLESS || path === MAC_DECOMPILER || path === LINUX_DECOMPILER,
probeJava: () => JAVA,
...overrides,
});
@ -108,28 +111,29 @@ describe("Ghidra installation inspection", () => {
},
);
it.each(["x64", "arm64"] as const)(
"accepts macOS %s with matching native tools",
(architecture) => {
const decompiler = `${INSTALL}/Ghidra/Features/Decompiler/os/${architecture === "arm64" ? "mac_arm_64" : "mac_x86_64"}/decompile`;
const macHost: GhidraInstallationHost = {
...host({
platform: "darwin",
it.each([
["darwin", "x64", "mac_x86_64"],
["darwin", "arm64", "mac_arm_64"],
["linux", "arm64", "linux_arm_64"],
] as const)(
"accepts %s/%s with the matching distribution or locally built native tool",
(platform, architecture, nativeDirectory) => {
for (const location of ["os", "build/os"]) {
const decompiler = `${INSTALL}/Ghidra/Features/Decompiler/${location}/${nativeDirectory}/decompile`;
expect(
inspectGhidraInstallation(
{ installDir: INSTALL, platform, architecture },
host({
executable: (path) => path === HEADLESS || path === decompiler,
}),
),
).toMatchObject({
status: "available",
platform,
architecture,
executable: (path) => path === HEADLESS || path === decompiler,
}),
};
expect(
inspectGhidraInstallation(
{ installDir: INSTALL, platform: "darwin", architecture },
macHost,
),
).toMatchObject({
status: "available",
platform: "darwin",
architecture,
nativeDecompilerPath: decompiler,
});
nativeDecompilerPath: decompiler,
});
}
},
);
@ -141,7 +145,8 @@ describe("Ghidra installation inspection", () => {
path === WINDOWS_PROPERTIES
? `application.version=${SUPPORTED_GHIDRA_VERSION}\n`
: undefined,
executable: (path) => path === WINDOWS_HEADLESS,
executable: (path) =>
path === WINDOWS_HEADLESS || path === WINDOWS_DECOMPILER,
probeJava: (command, environment) => {
expect(command).toBe("C:\\Java\\jdk-21\\bin\\java.exe");
expect(environment.PATH).toMatch(/^C:\\Java\\jdk-21\\bin;/u);
@ -198,7 +203,7 @@ describe("Ghidra installation rejection diagnostics", () => {
options: {
installDir: INSTALL,
platform: "linux" as const,
architecture: "arm64" as const,
architecture: "arm" as const,
},
override: {},
failed: "architecture",
@ -237,6 +242,17 @@ describe("Ghidra installation rejection diagnostics", () => {
failed: "headless",
code: "executable_missing",
},
{
name: "only a foreign-architecture decompiler on Linux arm64",
options: {
installDir: INSTALL,
platform: "linux" as const,
architecture: "arm64" as const,
},
override: {},
failed: "native_decompiler",
code: "executable_missing",
},
{
name: "missing Java",
options: {

View File

@ -22,6 +22,19 @@ import {
export { SUPPORTED_GHIDRA_JAVA_MAJOR, SUPPORTED_GHIDRA_VERSION };
const NATIVE_PLATFORMS: Readonly<
Partial<
Record<
NodeJS.Platform,
Readonly<Partial<Record<NodeJS.Architecture, string>>>
>
>
> = {
linux: { x64: "linux_x86_64", arm64: "linux_arm_64" },
darwin: { x64: "mac_x86_64", arm64: "mac_arm_64" },
win32: { x64: "win_x86_64" },
};
/** Caller-owned paths and host coordinates used for one installation probe. */
export interface GhidraInstallationOptions {
readonly installDir?: string;
@ -228,14 +241,11 @@ const installationCoordinates = (
"support",
platform === "win32" ? "analyzeHeadless.bat" : "analyzeHeadless",
);
const decompilerPlatform =
platform === "darwin"
? architecture === "arm64"
? "mac_arm_64"
: "mac_x86_64"
: null;
const decompilerPlatform = NATIVE_PLATFORMS[platform]?.[architecture];
const decompilerExecutable =
platform === "win32" ? "decompile.exe" : "decompile";
const nativeDecompilerPath =
installDir === null || decompilerPlatform === null
installDir === null || decompilerPlatform === undefined
? null
: ([
path.join(
@ -245,7 +255,7 @@ const installationCoordinates = (
"Decompiler",
"os",
decompilerPlatform,
"decompile",
decompilerExecutable,
),
path.join(
installDir,
@ -255,7 +265,7 @@ const installationCoordinates = (
"build",
"os",
decompilerPlatform,
"decompile",
decompilerExecutable,
),
].find((candidate) => host.executable(candidate)) ?? null);
const properties =
@ -305,23 +315,18 @@ const installationChecks = ({
}),
installationCheck({
name: "platform",
passed:
platform === "linux" || platform === "win32" || platform === "darwin",
passed: NATIVE_PLATFORMS[platform] !== undefined,
code: "unsupported_host",
detail: platform,
remediation:
"Use Linux or Windows x64, or macOS x64/arm64 with matching native Ghidra tools.",
"Use Linux or macOS x64/arm64, or Windows x64 with matching native Ghidra tools.",
}),
installationCheck({
name: "architecture",
passed:
((platform === "linux" || platform === "win32") &&
architecture === "x64") ||
(platform === "darwin" &&
(architecture === "x64" || architecture === "arm64")),
passed: NATIVE_PLATFORMS[platform]?.[architecture] !== undefined,
code: "unsupported_host",
detail: architecture,
remediation: "Use x64 on Linux or Windows, or x64/arm64 on macOS.",
remediation: "Use x64/arm64 on Linux or macOS, or x64 on Windows.",
}),
installationCheck({
name: "installation",
@ -346,15 +351,12 @@ const installationChecks = ({
}),
installationCheck({
name: "native_decompiler",
passed: platform !== "darwin" || coordinates.nativeDecompilerPath !== null,
passed: coordinates.nativeDecompilerPath !== null,
code: "executable_missing",
detail:
platform !== "darwin"
? "not required on this platform"
: (coordinates.nativeDecompilerPath ??
"matching macOS native decompiler was not found"),
remediation:
"REA does not build native tools. Build Ghidra's native components for this macOS architecture or provide an installation containing Ghidra/Features/Decompiler/os/<platform>/decompile.",
coordinates.nativeDecompilerPath ??
`Matching native decompiler for ${platform}/${architecture} was not found`,
remediation: `Provide an executable Ghidra/Features/Decompiler/os/${NATIVE_PLATFORMS[platform]?.[architecture] ?? "<platform>"}/${platform === "win32" ? "decompile.exe" : "decompile"}, or its build/os equivalent. REA does not build or install native tools.`,
}),
javaCheck(
coordinates.java,