fix(ghidra): pre-create the launch-support config directory (#2470)
* fix(ghidra): pre-create the launch-support config directory
Ghidra's launch support resolves its JDK before the headless run starts and
persists that choice under `<config>/ghidra/<release directory>`. Inside REA's
private runtime a child JVM cannot create those directories, so the first
Windows Ghidra query aborted at ghidra.launch.AppConfig.saveJavaHome with:
java.io.IOException: Failed to create directory:
...\config\ghidra\ghidra_12.1.4_PUBLIC
at ghidra.launch.AppConfig.saveJavaHome(AppConfig.java:145)
at LaunchSupport.findJavaHome(LaunchSupport.java:210)
LaunchSupport then fell back to its interactive prompt, read EOF, and reported
"JDK 21+ (64-bit) could not be found and must be manually chosen!", so the
provider failed with "Ghidra exited before bridge startup" even though
`rea doctor` reported a valid Ghidra installation and JDK.
Create the release directory while this process still owns the runtime, leaving
Ghidra only a file write inside it. The release coordinate comes from the
installation's Ghidra/application.properties (application.version plus
application.release.name) with the extraction directory name as a fallback. On
Windows each level goes through the native private-runtime owner, which opens
every existing component and creates only the final one.
Verified on Windows x64 with Ghidra 12.1.4, a 64-bit Temurin JDK 21, and REA's
packaged native addon: `rea inspect <x64 PE DLL> --provider ghidra` and the
equivalent MCP `binary_overview` call both complete and report 21 segments,
1436 procedures, and 2761 strings. Windows Ghidra remains read-only P0; this
change does not extend that boundary.
* style(ghidra): format launch configuration regressions
* fix(ghidra): reuse Java Properties parsing for launch release identity
---------
Co-authored-by: AbdulAzizShanShi <441650742@QQ.COM>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
2 files changed
+114
-3
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import { parseGhidraJavaProperties } from "./GhidraJavaProperties.js";
|
||||
import {
|
||||
GHIDRA_SEED_SCRIPT,
|
||||
snapshotGhidraAnalysisSeeds,
|
||||
@@ -5,7 +6,7 @@ import {
|
||||
} from "./GhidraAnalysisSeeds.js";
|
||||
import type { GhidraLanguageOverride } from "../config/ghidraLanguageOverride.js";
|
||||
import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
|
||||
import { mkdir, realpath } from "node:fs/promises";
|
||||
import { mkdir, readFile, realpath } from "node:fs/promises";
|
||||
import { basename, dirname, join, posix, win32 } from "node:path";
|
||||
|
||||
import writeFileAtomic from "write-file-atomic";
|
||||
@@ -219,7 +220,11 @@ export class GhidraHeadlessLauncher implements GhidraLauncher {
|
||||
"Installed Allegrex content changed since profile resolution; reopen the target to resolve a fresh profile.",
|
||||
);
|
||||
}
|
||||
await createGhidraRuntimeDirectories(paths, platform);
|
||||
await createGhidraRuntimeDirectories(
|
||||
paths,
|
||||
platform,
|
||||
await ghidraConfigReleaseDirectory(this.options.analyzeHeadlessPath),
|
||||
);
|
||||
const seedPath =
|
||||
this.options.analysisSeeds === undefined
|
||||
? undefined
|
||||
@@ -721,9 +726,44 @@ const ghidraRuntimePaths = (runtimeRoot: string) => ({
|
||||
scriptLogPath: join(runtimeRoot, "script.log"),
|
||||
});
|
||||
|
||||
/**
|
||||
* Ghidra's launch support resolves its JDK before the headless run and then
|
||||
* persists that choice under `<config>/ghidra/<release directory>`. Inside
|
||||
* REA's Windows private runtime a child JVM cannot create those directories
|
||||
* itself, so startup aborted with "Failed to create directory" and reported
|
||||
* that no supported JDK was found. Create the release directory while this
|
||||
* process still owns the runtime, leaving Ghidra only a file write inside it.
|
||||
*/
|
||||
const ghidraConfigReleaseDirectory = async (
|
||||
analyzeHeadlessPath: string,
|
||||
): Promise<string> => {
|
||||
const installRoot = dirname(dirname(analyzeHeadlessPath));
|
||||
try {
|
||||
const properties = await readFile(
|
||||
join(installRoot, "Ghidra", "application.properties"),
|
||||
"latin1",
|
||||
);
|
||||
const parsed = parseGhidraJavaProperties(properties);
|
||||
if (!parsed.ok) return basename(installRoot);
|
||||
const version = parsed.value.get("application.version");
|
||||
const release = parsed.value.get("application.release.name");
|
||||
return version === undefined ||
|
||||
release === undefined ||
|
||||
release.length === 0
|
||||
? basename(installRoot)
|
||||
: `ghidra_${version}_${release}`;
|
||||
} catch (cause: unknown) {
|
||||
// An unreadable properties file leaves the release coordinate unknown; the
|
||||
// conventional extraction directory carries the same name.
|
||||
void cause;
|
||||
return basename(installRoot);
|
||||
}
|
||||
};
|
||||
|
||||
const createGhidraRuntimeDirectories = async (
|
||||
paths: ReturnType<typeof ghidraRuntimePaths>,
|
||||
platform: NodeJS.Platform,
|
||||
configReleaseDirectory: string,
|
||||
): Promise<void> => {
|
||||
if (platform === "win32") {
|
||||
const runtime = windowsPrivateRuntime(dirname(paths.projectRoot));
|
||||
@@ -736,6 +776,11 @@ const createGhidraRuntimeDirectories = async (
|
||||
paths.dataRoot,
|
||||
])
|
||||
runtime.mkdir(basename(path));
|
||||
// Each level needs its parent first: the native owner opens every existing
|
||||
// component and creates only the final one.
|
||||
const configRoot = basename(paths.configRoot);
|
||||
runtime.mkdir(win32.join(configRoot, "ghidra"));
|
||||
runtime.mkdir(win32.join(configRoot, "ghidra", configReleaseDirectory));
|
||||
return;
|
||||
}
|
||||
await Promise.all(
|
||||
@@ -746,6 +791,7 @@ const createGhidraRuntimeDirectories = async (
|
||||
paths.cacheRoot,
|
||||
paths.configRoot,
|
||||
paths.dataRoot,
|
||||
join(paths.configRoot, "ghidra", configReleaseDirectory),
|
||||
].map((path) => mkdir(path, { recursive: true, mode: 0o700 })),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { access, chmod, readFile, stat } from "node:fs/promises";
|
||||
import {
|
||||
access,
|
||||
chmod,
|
||||
mkdir,
|
||||
readFile,
|
||||
stat,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { join, posix, win32 } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
@@ -480,6 +487,64 @@ describe("Ghidra Windows launcher", () => {
|
||||
).toThrow(/metacharacters/u);
|
||||
});
|
||||
|
||||
it.skipIf(process.platform !== "win32").each([
|
||||
{
|
||||
properties:
|
||||
"application.version=12.1.4\napplication.release.name=PUBLIC\n",
|
||||
release: "ghidra_12.1.4_PUBLIC",
|
||||
},
|
||||
{ properties: undefined, release: "renamed-install" },
|
||||
{ properties: "application.version=12.1.4\n", release: "renamed-install" },
|
||||
])(
|
||||
"prepares the release directory through the Windows runtime ($release)",
|
||||
async ({ properties, release }) => {
|
||||
const parent = await createTestTempDirectory("rea-launch-config-");
|
||||
const installRoot = join(parent, "renamed-install");
|
||||
if (properties !== undefined) {
|
||||
await mkdir(join(installRoot, "Ghidra"), { recursive: true });
|
||||
await writeFile(
|
||||
join(installRoot, "Ghidra", "application.properties"),
|
||||
properties,
|
||||
);
|
||||
}
|
||||
const runtime = await createGhidraTestRuntime(parent);
|
||||
runtimes.push(runtime);
|
||||
const spawnProcess = vi.fn(async () => {
|
||||
throw new Error("fixture stops before process creation");
|
||||
});
|
||||
const launcher = new GhidraHeadlessLauncher({
|
||||
environment: process.env,
|
||||
platform: "win32",
|
||||
analyzeHeadlessPath: join(
|
||||
installRoot,
|
||||
"support",
|
||||
"analyzeHeadless.bat",
|
||||
),
|
||||
javaHome: "C:\\Java\\jdk-21",
|
||||
bridgeScriptPath: join(parent, "ReaGhidraBridge.java"),
|
||||
spawnProcess,
|
||||
});
|
||||
const launched = await launcher.launch({
|
||||
runtimeRoot: runtime.path,
|
||||
transport: "unix-socket",
|
||||
endpointPath: join(runtime.path, "bridge.sock"),
|
||||
token: "fixture-token",
|
||||
runId: "d6fcbb66-e829-4ff6-a535-0035aec63139",
|
||||
targetPath: join(parent, "fixture.exe"),
|
||||
targetSha256: "b".repeat(64),
|
||||
providerVersion: "12.1.4",
|
||||
profileDigest: "a".repeat(64),
|
||||
});
|
||||
expect(launched.ok).toBe(false);
|
||||
expect(spawnProcess).toHaveBeenCalledOnce();
|
||||
expect(
|
||||
(
|
||||
await stat(join(runtime.path, "config", "ghidra", release))
|
||||
).isDirectory(),
|
||||
).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(process.platform !== "win32")(
|
||||
"keeps authority in a private descriptor and isolates Ghidra state through the batch launcher",
|
||||
async () => {
|
||||
|
||||
Reference in new issue
Block a user