diff --git a/scripts/lib/native-value-e2e.mjs b/scripts/lib/native-value-e2e.mjs index 10e226908..1249ed327 100644 --- a/scripts/lib/native-value-e2e.mjs +++ b/scripts/lib/native-value-e2e.mjs @@ -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 }; diff --git a/src/ghidra/GhidraAnalysisProfile.ts b/src/ghidra/GhidraAnalysisProfile.ts index 1c2ec4d7f..418136f74 100644 --- a/src/ghidra/GhidraAnalysisProfile.ts +++ b/src/ghidra/GhidraAnalysisProfile.ts @@ -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 diff --git a/src/ghidra/GhidraJavaLaunch.ts b/src/ghidra/GhidraJavaLaunch.ts index 6f50ddf89..869574b8a 100644 --- a/src/ghidra/GhidraJavaLaunch.ts +++ b/src/ghidra/GhidraJavaLaunch.ts @@ -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", diff --git a/src/ghidra/GhidraLauncher.ts b/src/ghidra/GhidraLauncher.ts index f030c8559..de833d007 100644 --- a/src/ghidra/GhidraLauncher.ts +++ b/src/ghidra/GhidraLauncher.ts @@ -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; diff --git a/src/ghidra/GhidraLauncherEnvironment.test.ts b/src/ghidra/GhidraLauncherEnvironment.test.ts index a66037515..6319d44ae 100644 --- a/src/ghidra/GhidraLauncherEnvironment.test.ts +++ b/src/ghidra/GhidraLauncherEnvironment.test.ts @@ -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: "", }); }, ); diff --git a/tests/boundary/providers/ghidra/ghidraLauncher.test.ts b/tests/boundary/providers/ghidra/ghidraLauncher.test.ts index 01b84aee6..66672324a 100644 --- a/tests/boundary/providers/ghidra/ghidraLauncher.test.ts +++ b/tests/boundary/providers/ghidra/ghidraLauncher.test.ts @@ -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(); - }); + }, + ); });