fix(ghidra): preserve POSIX runtime paths with spaces

This commit is contained in:
morluto
2026-10-09 07:29:17 +08:00
parent c715af8058
commit 80c3d36dfa
6 changed files with 38 additions and 43 deletions
+1 -1
View File
@@ -80,7 +80,7 @@ export async function verifyNativeValueE2e(
const workspace = await mkdtemp(join(tmpdir(), "rea-native-values-e2e-"));
const runtime = join(
workspace,
`runtime${process.platform === "darwin" ? " with spaces" : ""}-${"x".repeat(100)}`,
`runtime${process.platform === "win32" ? "" : " with spaces"}-${"x".repeat(100)}`,
);
await mkdir(runtime);
const env = { ...environment(), TMPDIR: runtime };
+3 -3
View File
@@ -45,9 +45,9 @@ export const resolveGhidraAnalysisProfile = (
function_references: "complete-body-and-entry-reference-manager-v2",
location_resolution: "explicit-address-exact-name-first-v2",
process_launch:
installation.platform === "darwin"
? "inspected-jvm-launch-support-v1"
: "official-headless-script-v1",
installation.platform === "win32"
? "official-headless-script-v1"
: "inspected-jvm-launch-support-v1",
...(dos
? {
load_image_evidence: dosCom
+2 -1
View File
@@ -3,6 +3,7 @@ import { execFileOutput } from "../process/ExecFileOutput.js";
/** Launch coordinates obtained from Ghidra's own release configuration helper. */
export const ghidraJavaLaunch = async (options: {
readonly platform: NodeJS.Platform;
readonly analyzeHeadlessPath: string;
readonly javaHome: string;
readonly homeRoot: string;
@@ -71,7 +72,7 @@ export const ghidraJavaLaunch = async (options: {
environment,
arguments: [
...vmArguments,
"-Xdock:name=Ghidra-Headless",
...(options.platform === "darwin" ? ["-Xdock:name=Ghidra-Headless"] : []),
`-Xmx${environment.GHIDRA_HEADLESS_MAXMEM || environment.GHIDRA_MAXMEM || "2G"}`,
"-XX:ParallelGCThreads=2",
"-XX:CICompilerCount=2",
+18 -21
View File
@@ -139,13 +139,14 @@ export class GhidraHeadlessLauncher implements GhidraLauncher {
platform,
scriptCommand.command,
);
if (platform === "darwin" && this.options.javaHome === undefined)
if (platform !== "win32" && this.options.javaHome === undefined)
throw new GhidraLaunchError(
"macOS Ghidra requires its inspected JDK home",
"POSIX Ghidra requires its inspected JDK home",
);
const command =
platform === "darwin" && this.options.javaHome !== undefined
platform !== "win32" && this.options.javaHome !== undefined
? await ghidraJavaLaunch({
platform,
analyzeHeadlessPath: this.options.analyzeHeadlessPath,
javaHome: this.options.javaHome,
homeRoot: paths.homeRoot,
@@ -161,9 +162,9 @@ export class GhidraHeadlessLauncher implements GhidraLauncher {
command: command.command,
arguments: command.arguments,
runId: session.runId,
// macOS launches the inspected JVM directly: platform shell wrappers
// with withheld environments cannot establish run-token ownership.
expectedCommand: platform === "darwin" ? command.command : null,
// POSIX launches the inspected JVM directly; script wrappers cannot
// preserve JVM property paths containing spaces as single arguments.
expectedCommand: platform !== "win32" ? command.command : null,
windowsVerbatimArguments: platform === "win32",
platform,
env: command.environment,
@@ -441,7 +442,7 @@ const ghidraLaunchEnvironment = (
};
};
/** Encode provider-owned JVM paths for the parser that actually consumes them. */
/** Encode provider-owned JVM paths for the platform launcher that consumes them. */
export const ghidraHeadlessJavaOptions = (
homeRoot: string,
tempRoot: string,
@@ -451,20 +452,16 @@ export const ghidraHeadlessJavaOptions = (
"JDK_JAVA_OPTIONS" | "GHIDRA_HEADLESS_JAVA_OPTIONS"
> => {
const options = [`-Duser.home=${homeRoot}`, `-Djava.io.tmpdir=${tempRoot}`];
// analyzeHeadless.bat quotes VMARG_LIST, then launch.bat expands it again.
// Inner quotes fail in cmd.exe; unquoted paths with spaces split at the JVM.
// JDK_JAVA_OPTIONS is parsed by Java itself, outside that batch expansion.
return platform === "win32"
? {
JDK_JAVA_OPTIONS: [...options, "-XX:-UsePerfData"]
.map(quoteWindowsBatchToken)
.join(" "),
GHIDRA_HEADLESS_JAVA_OPTIONS: "",
}
: {
JDK_JAVA_OPTIONS: "",
GHIDRA_HEADLESS_JAVA_OPTIONS: options.join(" "),
};
if (platform === "win32")
return {
JDK_JAVA_OPTIONS: [...options, "-XX:-UsePerfData"]
.map(quoteWindowsBatchToken)
.join(" "),
GHIDRA_HEADLESS_JAVA_OPTIONS: "",
};
// POSIX launches the JVM directly, so these properties travel as separate
// argv items instead of the shell-split analyzeHeadless option list.
return { JDK_JAVA_OPTIONS: "", GHIDRA_HEADLESS_JAVA_OPTIONS: "" };
};
const isAborted = (signal?: AbortSignal): boolean => signal?.aborted === true;
+2 -3
View File
@@ -18,14 +18,13 @@ describe("Ghidra headless JVM environment", () => {
});
it.each(["linux", "darwin"] as const)(
"keeps the %s headless-script option list and clears inherited JDK options",
"passes %s isolated JVM paths through direct argv instead of script options",
(platform) => {
expect(
ghidraHeadlessJavaOptions("/tmp/rea/home", "/tmp/rea/tmp", platform),
).toEqual({
JDK_JAVA_OPTIONS: "",
GHIDRA_HEADLESS_JAVA_OPTIONS:
"-Duser.home=/tmp/rea/home -Djava.io.tmpdir=/tmp/rea/tmp",
GHIDRA_HEADLESS_JAVA_OPTIONS: "",
});
},
);
@@ -73,10 +73,8 @@ const expectIsolatedEnvironment = (
JAVA_HOME: javaHome,
REA_PROCESS_RUN_ID: "d6fcbb66-e829-4ff6-a535-0035aec63139",
});
expect(environment.PATH).toMatch(
process.platform === "win32"
? /^C:\\Java\\jdk-21\\bin;/u
: /^\/opt\/jdk-21\/bin:/u,
expect(environment.PATH).toBe(
`${join(javaHome, "bin")}${process.platform === "win32" ? ";" : ":"}${process.env.PATH ?? ""}`,
);
};
@@ -200,7 +198,9 @@ describe("Ghidra headless launcher", () => {
).toThrow(/metacharacters/u);
});
it("keeps authority in a private descriptor and isolates Ghidra state", async () => {
it.skipIf(process.platform !== "win32")(
"keeps authority in a private descriptor and isolates Ghidra state through the batch launcher",
async () => {
vi.stubEnv("GHIDRA_JAVA_OPTIONS", "-javaagent:/unapproved/agent.jar");
vi.stubEnv("JAVA_TOOL_OPTIONS", "-Duser.home=/unapproved/home");
vi.stubEnv("JDK_JAVA_OPTIONS", "-XX:MaxRAMPercentage=99");
@@ -210,13 +210,12 @@ describe("Ghidra headless launcher", () => {
runtimes.push(runtime);
const runtimeRoot = runtime.path;
const token = "secret-token-that-must-not-leak";
const javaHome =
process.platform === "win32" ? "C:\\Java\\jdk-21" : "/opt/jdk-21";
const javaHome = "C:\\Java\\jdk-21";
const launcher = new GhidraHeadlessLauncher({
analyzeHeadlessPath: fixturePath,
// This fixture exercises the official script route. Native macOS JVM
// launching is covered by the real Ghidra acceptance lane.
platform: process.platform === "win32" ? "win32" : "linux",
// POSIX follows the real inspected-JVM route; Windows uses the official
// batch script because its launcher contract differs.
platform: "win32",
javaHome,
bridgeScriptPath: "/package/bridge/ReaGhidraBridge.java",
});
@@ -247,9 +246,7 @@ describe("Ghidra headless launcher", () => {
});
expectIsolatedEnvironment(capture.environment, runtimeRoot, javaHome);
expect(capture.environment.GHIDRA_HEADLESS_JAVA_OPTIONS).toBe(
process.platform === "win32"
? ""
: `-Duser.home=${join(runtimeRoot, "home")} -Djava.io.tmpdir=${join(runtimeRoot, "tmp")}`,
"",
);
if (process.platform !== "win32")
expect(
@@ -259,5 +256,6 @@ describe("Ghidra headless launcher", () => {
const cleaned = await launched.value.cleanup?.();
expect(cleaned).toMatchObject({ cleaned: true });
await expect(access(join(runtimeRoot, "project"))).resolves.toBeUndefined();
});
},
);
});