fix(ghidra): fail doctor when the JVM os.arch selects an unvalidated decompiler directory (#2792)
* fix(ghidra): fail doctor when the JVM os.arch selects an unvalidated decompiler directory probeJava now also reads the JVM os.arch property. When it maps to a different native decompiler directory than the one validated from the host architecture (for example an x86_64 JDK under Rosetta on Apple silicon selecting os/mac_x86_64 while only mac_arm_64 was built), the native_decompiler check fails with a remediation pointing at a matching-arch JDK instead of reporting a healthy installation that fails every analysis. Unknown os.arch values keep the previous behavior. * fix(ghidra): tighten JVM arch remediation and cover os.arch extraction Address review findings on the JVM os.arch decompiler-directory check: - The remediation no longer suggests building the decompiler for the JVM-selected directory, which could never clear the check; it only advises pointing JAVA_HOME at a matching-architecture JDK. - Add a test running the real Java probe against captured -XshowSettings:properties output, asserting os.arch reaches the installation diagnostics. - Document the JDK-architecture prerequisite in the Ghidra setup guide. * fix(ghidra): refuse unrecognized JVM native architectures * docs(ghidra): align doctor prerequisites with JVM architecture admission * fix(ghidra): reject unreported JVM architecture * test(ghidra): separate JVM and executable admission cases * fix(ghidra): defer decompiler arch validation when Java is absent * fix(ghidra): reject JVM native architecture override * fix(ghidra): validate JVM platform overrides * fix(ghidra): validate JVM os.name and helper-environment platform overrides * test(ci): isolate report fixtures and declare PowerPC Java bounds * test(ghidra): provide observed JVM platform in PowerPC fixture * fix(ghidra): parse quoted JVM environment options before admission --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
parent
97aff28201
commit
20e4f83097
|
|
@ -493,7 +493,13 @@ copying license secrets into logs, or killing unrelated Hopper processes.
|
|||
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
|
||||
`application.java.min` and `application.java.max`. The JDK's reported platform must also
|
||||
map to the same native decompiler directory as the host: a JDK running under
|
||||
emulation (for example x86_64 under Rosetta on Apple silicon) or reporting a
|
||||
non-host `os.name` fails the doctor
|
||||
even when it is an otherwise accepted 64-bit full JDK. Select a matching JDK
|
||||
through `JAVA_HOME`; adding another decompiler in the JVM-selected directory
|
||||
does not satisfy this host-validated admission rule. 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. Each installation must include the native decompiler for the host
|
||||
architecture in `Ghidra/Features/Decompiler/os/<platform>/` or the corresponding
|
||||
|
|
@ -556,6 +562,11 @@ registrations. See [Windows Ghidra P0](windows-ghidra-p0.md) for provider diagno
|
|||
Doctor validates the platform, architecture, Ghidra 12.1.x application version,
|
||||
`support/analyzeHeadless` or `support/analyzeHeadless.bat`, the installation's
|
||||
Java major range, 64-bit JDK bitness, and the presence of `javac`/`javac.exe`.
|
||||
It also checks that the reported JVM `os.arch` and `os.name` map to the same native decompiler
|
||||
directory validated for the host; unrecognized reported architectures or a
|
||||
non-host operating system are refused. LaunchSupport overrides of either
|
||||
property, including through helper-environment JVM option variables, are
|
||||
rejected at launch.
|
||||
When Java is found through `PATH`, setup records its observed JDK home so GUI
|
||||
MCP clients do not depend on an incidental shell path. Setup shows every exact
|
||||
environment entry in its plan, writes only after approval, and never downloads,
|
||||
|
|
|
|||
|
|
@ -15,10 +15,13 @@ import { projectGhidraDoctorInspection } from "./GhidraDoctor.js";
|
|||
const INSTALL = "/opt/ghidra_12.1.4_PUBLIC";
|
||||
const installationHost = (
|
||||
overrides: Partial<GhidraInstallationHost> = {},
|
||||
platform: NodeJS.Platform = "linux",
|
||||
): GhidraInstallationHost => ({
|
||||
readText: () => "application.version=12.1.4\napplication.java.min=21\n",
|
||||
executable: () => true,
|
||||
probeJava: () => ({
|
||||
osArch: "amd64",
|
||||
osName: platform === "win32" ? "Windows 11" : "Linux",
|
||||
version: "21.0.11",
|
||||
major: 21,
|
||||
home: "/usr/lib/jvm/jdk-21",
|
||||
|
|
@ -45,7 +48,7 @@ const inspection = (
|
|||
? {}
|
||||
: { installDir: options.installDir }),
|
||||
},
|
||||
installationHost(overrides),
|
||||
installationHost(overrides, options.platform ?? "linux"),
|
||||
);
|
||||
|
||||
const doctorHost = (
|
||||
|
|
@ -129,6 +132,8 @@ describe("Ghidra doctor integration", () => {
|
|||
{ installDir: INSTALL },
|
||||
{
|
||||
probeJava: () => ({
|
||||
osArch: "amd64",
|
||||
osName: "Linux",
|
||||
version: "17.0.12",
|
||||
major: 17,
|
||||
home: "/usr/lib/jvm/jdk-17",
|
||||
|
|
|
|||
|
|
@ -12,5 +12,7 @@ export const ghidraProfileInstallationHost: GhidraInstallationHost = {
|
|||
home: "/jdk-21",
|
||||
bits: 64,
|
||||
runtime: "jdk",
|
||||
osArch: "amd64",
|
||||
osName: "Linux",
|
||||
}),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
import { elf, fat, thinMach } from "../domain/binaryTarget.fixture.js";
|
||||
import { projectGhidraDoctorInspection } from "./GhidraDoctor.js";
|
||||
import {
|
||||
ghidraInstallationDiagnostics,
|
||||
ghidraJavaEnvironment,
|
||||
inspectGhidraInstallation,
|
||||
type GhidraExecutableHeaderProbe,
|
||||
|
|
@ -16,6 +18,14 @@ import {
|
|||
ghidraJavaRequirement,
|
||||
} from "./GhidraInstallationPolicy.js";
|
||||
|
||||
// The real Java probe shells out via spawnSync; stub it so one test can run
|
||||
// the real probeJava against captured `-XshowSettings:properties` output.
|
||||
// Every other test overrides probeJava, so the stub never fires for them.
|
||||
vi.mock("node:child_process", async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import("node:child_process")>();
|
||||
return { ...original, spawnSync: vi.fn() };
|
||||
});
|
||||
|
||||
const INSTALL = "/opt/ghidra";
|
||||
const PROPERTIES = `${INSTALL}/Ghidra/application.properties`;
|
||||
const HEADLESS = `${INSTALL}/support/analyzeHeadless`;
|
||||
|
|
@ -32,6 +42,8 @@ const headerProbe = (bytes: Buffer): GhidraExecutableHeaderProbe => ({
|
|||
});
|
||||
|
||||
const JAVA: GhidraJavaObservation = {
|
||||
osArch: "amd64",
|
||||
osName: "Linux",
|
||||
version: "21.0.11",
|
||||
major: SUPPORTED_GHIDRA_JAVA_MAJOR,
|
||||
home: "/usr/lib/jvm/jdk-21",
|
||||
|
|
@ -174,6 +186,11 @@ describe("Ghidra installation inspection", () => {
|
|||
{ environment: {}, installDir: INSTALL, platform, architecture },
|
||||
host({
|
||||
executable: (path) => path === HEADLESS || path === decompiler,
|
||||
probeJava: () => ({
|
||||
...JAVA,
|
||||
osArch: architecture === "arm64" ? "aarch64" : "amd64",
|
||||
osName: platform === "darwin" ? "Mac OS X" : "Linux",
|
||||
}),
|
||||
}),
|
||||
),
|
||||
).toMatchObject({
|
||||
|
|
@ -202,6 +219,7 @@ describe("Ghidra installation inspection", () => {
|
|||
return {
|
||||
...JAVA,
|
||||
home: "C:\\Java\\jdk-21",
|
||||
osName: "Windows 11",
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
@ -721,7 +739,10 @@ describe("Ghidra FAT native decompiler admission", () => {
|
|||
platform: "darwin",
|
||||
architecture: "arm64",
|
||||
},
|
||||
host({ executableHeader: () => headerProbe(bytes) }),
|
||||
host({
|
||||
probeJava: () => ({ ...JAVA, osArch: "aarch64", osName: "Mac OS X" }),
|
||||
executableHeader: () => headerProbe(bytes),
|
||||
}),
|
||||
);
|
||||
expect(result.status).toBe(status);
|
||||
expect(
|
||||
|
|
@ -739,7 +760,10 @@ describe("Ghidra FAT native decompiler admission", () => {
|
|||
platform: "darwin",
|
||||
architecture: "arm64",
|
||||
},
|
||||
host({ executableHeader: () => headerProbe(bytes) }),
|
||||
host({
|
||||
probeJava: () => ({ ...JAVA, osArch: "aarch64", osName: "Mac OS X" }),
|
||||
executableHeader: () => headerProbe(bytes),
|
||||
}),
|
||||
);
|
||||
expect(result.status).toBe("available");
|
||||
expect(
|
||||
|
|
@ -788,7 +812,14 @@ describe("Ghidra native decompiler admission", () => {
|
|||
])("distinguishes $name", ({ options, executableHeader }) => {
|
||||
const result = inspectGhidraInstallation(
|
||||
options,
|
||||
host({ executableHeader }),
|
||||
host({
|
||||
probeJava: () => ({
|
||||
...JAVA,
|
||||
osArch: options.architecture === "arm64" ? "aarch64" : "amd64",
|
||||
osName: options.platform === "darwin" ? "Mac OS X" : "Linux",
|
||||
}),
|
||||
executableHeader,
|
||||
}),
|
||||
);
|
||||
expect(result.status).toBe("unavailable");
|
||||
expect(
|
||||
|
|
@ -849,6 +880,7 @@ describe("Ghidra native decompiler admission", () => {
|
|||
architecture: "arm64",
|
||||
},
|
||||
host({
|
||||
probeJava: () => ({ ...JAVA, osArch: "aarch64", osName: "Mac OS X" }),
|
||||
executableHeader: (path: string) =>
|
||||
path === MAC_DECOMPILER ? headerProbe(bytes) : undefined,
|
||||
}),
|
||||
|
|
@ -872,6 +904,7 @@ describe("Ghidra native decompiler admission", () => {
|
|||
architecture: "arm64",
|
||||
},
|
||||
host({
|
||||
probeJava: () => ({ ...JAVA, osArch: "aarch64", osName: "Mac OS X" }),
|
||||
executableHeader: (path: string) =>
|
||||
path === MAC_DECOMPILER ? headerProbe(elf(2, 1, 62)) : undefined,
|
||||
}),
|
||||
|
|
@ -889,3 +922,205 @@ describe("Ghidra native decompiler admission", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Ghidra JVM native directory admission", () => {
|
||||
it("rejects a JDK whose os.arch selects a different native decompiler directory", () => {
|
||||
const result = inspectGhidraInstallation(
|
||||
{
|
||||
environment: {},
|
||||
installDir: INSTALL,
|
||||
platform: "darwin",
|
||||
architecture: "arm64",
|
||||
},
|
||||
host({
|
||||
probeJava: () => ({ ...JAVA, osArch: "x86_64", osName: "Mac OS X" }),
|
||||
}),
|
||||
);
|
||||
expect(result.status).toBe("unavailable");
|
||||
if (result.status !== "unavailable") return;
|
||||
expect(
|
||||
result.checks.find(({ name }) => name === "native_decompiler"),
|
||||
).toMatchObject({
|
||||
status: "failed",
|
||||
code: "executable_missing",
|
||||
detail: expect.stringContaining(
|
||||
"JVM reports os.arch=x86_64, so Ghidra selects its native decompiler from os/mac_x86_64",
|
||||
),
|
||||
remediation: expect.stringContaining("JAVA_HOME"),
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts a JDK whose os.arch selects the validated native decompiler directory", () => {
|
||||
const result = inspectGhidraInstallation(
|
||||
{
|
||||
environment: {},
|
||||
installDir: INSTALL,
|
||||
platform: "darwin",
|
||||
architecture: "arm64",
|
||||
},
|
||||
host({
|
||||
probeJava: () => ({ ...JAVA, osArch: "aarch64", osName: "Mac OS X" }),
|
||||
}),
|
||||
);
|
||||
expect(result.status).toBe("available");
|
||||
});
|
||||
|
||||
it("rejects a successful Java probe that omits os.arch", () => {
|
||||
const { osArch: _observedArchitecture, ...withoutArchitecture } = JAVA;
|
||||
const result = inspectGhidraInstallation(
|
||||
{
|
||||
environment: {},
|
||||
installDir: INSTALL,
|
||||
platform: "darwin",
|
||||
architecture: "arm64",
|
||||
},
|
||||
host({ probeJava: () => withoutArchitecture }),
|
||||
);
|
||||
expect(result.status).toBe("unavailable");
|
||||
expect(
|
||||
result.checks.find(({ name }) => name === "native_decompiler"),
|
||||
).toMatchObject({
|
||||
status: "failed",
|
||||
detail: expect.stringContaining("did not report os.arch"),
|
||||
});
|
||||
expect(projectGhidraDoctorInspection(result).available).toBe(false);
|
||||
});
|
||||
|
||||
it.each(["riscv64", "sparcv9", ""])(
|
||||
"refuses an unrecognized JVM os.arch (%s) instead of assuming alignment",
|
||||
(osArch) => {
|
||||
const result = inspectGhidraInstallation(
|
||||
{
|
||||
environment: {},
|
||||
installDir: INSTALL,
|
||||
platform: "darwin",
|
||||
architecture: "arm64",
|
||||
},
|
||||
host({
|
||||
probeJava: () => ({ ...JAVA, osArch }),
|
||||
}),
|
||||
);
|
||||
expect(result.status).toBe("unavailable");
|
||||
if (result.status !== "unavailable") return;
|
||||
expect(
|
||||
result.checks.find(({ name }) => name === "native_decompiler"),
|
||||
).toMatchObject({
|
||||
status: "failed",
|
||||
detail: expect.stringContaining("unrecognized os.arch="),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects a JDK whose os.name selects a different host OS", () => {
|
||||
const result = inspectGhidraInstallation(
|
||||
{
|
||||
environment: {},
|
||||
installDir: INSTALL,
|
||||
platform: "darwin",
|
||||
architecture: "arm64",
|
||||
},
|
||||
host({
|
||||
probeJava: () => ({
|
||||
...JAVA,
|
||||
osArch: "aarch64",
|
||||
osName: "Windows 11",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(result.status).toBe("unavailable");
|
||||
if (result.status !== "unavailable") return;
|
||||
expect(
|
||||
result.checks.find(({ name }) => name === "native_decompiler"),
|
||||
).toMatchObject({
|
||||
status: "failed",
|
||||
code: "executable_missing",
|
||||
detail: expect.stringContaining("JVM reports os.name=Windows 11"),
|
||||
remediation: expect.stringContaining("JAVA_HOME"),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Ghidra JVM OS admission", () => {
|
||||
it("rejects a successful Java probe that omits os.name", () => {
|
||||
const { osName: _observedOsName, ...withoutOsName } = JAVA;
|
||||
const result = inspectGhidraInstallation(
|
||||
{
|
||||
environment: {},
|
||||
installDir: INSTALL,
|
||||
platform: "linux",
|
||||
architecture: "x64",
|
||||
},
|
||||
host({ probeJava: () => withoutOsName }),
|
||||
);
|
||||
expect(result.status).toBe("unavailable");
|
||||
expect(
|
||||
result.checks.find(({ name }) => name === "native_decompiler"),
|
||||
).toMatchObject({
|
||||
status: "failed",
|
||||
detail: expect.stringContaining("did not report os.name"),
|
||||
});
|
||||
expect(projectGhidraDoctorInspection(result).available).toBe(false);
|
||||
});
|
||||
|
||||
it.each(["SunOS", "FreeBSD", ""])(
|
||||
"refuses an unrecognized JVM os.name (%s) instead of assuming alignment",
|
||||
(osName) => {
|
||||
const result = inspectGhidraInstallation(
|
||||
{
|
||||
environment: {},
|
||||
installDir: INSTALL,
|
||||
platform: "linux",
|
||||
architecture: "x64",
|
||||
},
|
||||
host({
|
||||
probeJava: () => ({ ...JAVA, osName }),
|
||||
}),
|
||||
);
|
||||
expect(result.status).toBe("unavailable");
|
||||
if (result.status !== "unavailable") return;
|
||||
expect(
|
||||
result.checks.find(({ name }) => name === "native_decompiler"),
|
||||
).toMatchObject({
|
||||
status: "failed",
|
||||
detail: expect.stringContaining("os.name="),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("Ghidra Java probe os.arch extraction", () => {
|
||||
it("extracts os.arch through the real Java probe into the installation diagnostics", () => {
|
||||
vi.mocked(spawnSync).mockReturnValue({
|
||||
status: 0,
|
||||
stdout: "",
|
||||
stderr: [
|
||||
"Property settings:",
|
||||
" file.encoding = UTF-8",
|
||||
" java.home = /usr/lib/jvm/jdk-21",
|
||||
" java.version = 21.0.11",
|
||||
" java.vm.name = OpenJDK 64-Bit Server VM",
|
||||
" os.arch = x86_64",
|
||||
" os.name = Linux",
|
||||
" sun.arch.data.model = 64",
|
||||
"",
|
||||
].join("\n"),
|
||||
} as never);
|
||||
try {
|
||||
// No host override: the real system host runs the real probeJava
|
||||
// against the stubbed spawnSync.
|
||||
const inspection = inspectGhidraInstallation({
|
||||
environment: {},
|
||||
installDir: INSTALL,
|
||||
javaHome: "/usr/lib/jvm/jdk-21",
|
||||
platform: "linux",
|
||||
architecture: "x64",
|
||||
});
|
||||
expect(ghidraInstallationDiagnostics(inspection)).toMatchObject({
|
||||
java_os_arch: "x86_64",
|
||||
java_os_name: "Linux",
|
||||
});
|
||||
} finally {
|
||||
vi.mocked(spawnSync).mockReset();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -203,6 +203,17 @@ interface GhidraInstallationObservation {
|
|||
readonly javaCommand: string;
|
||||
/** Observed java.vm.name; omitted when the probe cannot establish VM identity. */
|
||||
readonly javaVmName?: string;
|
||||
/**
|
||||
* Observed JVM os.arch; omitted when the probe cannot establish it. Ghidra
|
||||
* selects its native decompiler directory from this value.
|
||||
*/
|
||||
readonly javaOsArch?: string;
|
||||
/**
|
||||
* Observed JVM os.name; omitted when the probe cannot establish it. Ghidra
|
||||
* selects its native decompiler OS directory from the JVM OS/architecture
|
||||
* pair, so a non-host value selects an unvalidated directory.
|
||||
*/
|
||||
readonly javaOsName?: string;
|
||||
readonly checks: readonly GhidraInstallationCheck[];
|
||||
}
|
||||
|
||||
|
|
@ -244,6 +255,17 @@ export interface GhidraJavaObservation {
|
|||
readonly bits: number;
|
||||
readonly runtime: "jdk" | "jre";
|
||||
readonly vmName?: string;
|
||||
/**
|
||||
* Observed JVM os.arch property, which Ghidra uses to select its native
|
||||
* decompiler directory. Omitted when the probe cannot establish it.
|
||||
*/
|
||||
readonly osArch?: string;
|
||||
/**
|
||||
* Observed JVM os.name property, which Ghidra uses with os.arch to select
|
||||
* its native decompiler OS directory. Omitted when the probe cannot
|
||||
* establish it.
|
||||
*/
|
||||
readonly osName?: string;
|
||||
}
|
||||
|
||||
/** A bounded executable-header probe: a small prefix plus the whole file size. */
|
||||
|
|
@ -324,6 +346,12 @@ export const inspectGhidraInstallation = (
|
|||
...(coordinates.java?.vmName === undefined
|
||||
? {}
|
||||
: { javaVmName: coordinates.java.vmName }),
|
||||
...(coordinates.java?.osArch === undefined
|
||||
? {}
|
||||
: { javaOsArch: coordinates.java.osArch }),
|
||||
...(coordinates.java?.osName === undefined
|
||||
? {}
|
||||
: { javaOsName: coordinates.java.osName }),
|
||||
checks,
|
||||
};
|
||||
if (failed !== undefined)
|
||||
|
|
@ -555,6 +583,8 @@ export const ghidraInstallationDiagnostics = (
|
|||
java_home: inspection.javaHome,
|
||||
java_version: inspection.javaVersion,
|
||||
java_vm_name: inspection.javaVmName ?? null,
|
||||
java_os_arch: inspection.javaOsArch ?? null,
|
||||
java_os_name: inspection.javaOsName ?? null,
|
||||
platform: inspection.platform,
|
||||
architecture: inspection.architecture,
|
||||
checks: inspection.checks.map((check) => ({
|
||||
|
|
@ -632,6 +662,61 @@ const nativeDecompilerCheck = ({
|
|||
architecture,
|
||||
host.executableHeader?.(path),
|
||||
);
|
||||
// Ghidra resolves its native decompiler directory from the JVM's os.arch,
|
||||
// not the host process architecture. A JDK running under emulation (for
|
||||
// example x86_64 under Rosetta on Apple silicon) selects a directory that
|
||||
// was never validated, so every analysis fails after a passing doctor.
|
||||
// Ghidra resolves the OS directory from the JVM os.name/os.arch pair, so a
|
||||
// JVM reporting a non-host os.name (for example through -Dos.name=Windows)
|
||||
// selects a different OS directory even when os.arch matches.
|
||||
const osArch = coordinates.java?.osArch;
|
||||
const osName = coordinates.java?.osName;
|
||||
const selectedDirectory = jvmOsArchNativeDirectory(platform, osArch);
|
||||
const validatedDirectory = NATIVE_PLATFORMS[platform]?.[architecture];
|
||||
// Without a JVM observation the java check reports the missing runtime;
|
||||
// the decompiler directory cannot be selected, so this check defers to it.
|
||||
if (coordinates.java !== undefined) {
|
||||
if (selectedDirectory === undefined)
|
||||
return installationCheck({
|
||||
name: "native_decompiler",
|
||||
passed: false,
|
||||
code: "executable_missing",
|
||||
detail:
|
||||
osArch === undefined
|
||||
? "The Java probe did not report os.arch; the JVM-selected native decompiler directory is unknown and cannot be matched to the validated host directory"
|
||||
: `JVM reports unrecognized os.arch=${osArch}; its selected native decompiler directory is unknown and cannot be matched to the validated host directory`,
|
||||
remediation: `Point JAVA_HOME at a JDK whose reported architecture maps to the validated Ghidra decompiler (${validatedDirectory ?? "unsupported host"}). REA does not build or install native tools.`,
|
||||
});
|
||||
if (
|
||||
validatedDirectory !== undefined &&
|
||||
selectedDirectory !== validatedDirectory &&
|
||||
path !== null
|
||||
)
|
||||
return installationCheck({
|
||||
name: "native_decompiler",
|
||||
passed: false,
|
||||
code: "executable_missing",
|
||||
detail: `JVM reports os.arch=${osArch}, so Ghidra selects its native decompiler from os/${selectedDirectory}, but the validated decompiler targets ${validatedDirectory} (host ${platform}/${architecture})`,
|
||||
remediation: `Point JAVA_HOME at a JDK whose architecture matches the built decompiler (${validatedDirectory}). REA does not build or install native tools.`,
|
||||
});
|
||||
if (osName === undefined)
|
||||
return installationCheck({
|
||||
name: "native_decompiler",
|
||||
passed: false,
|
||||
code: "executable_missing",
|
||||
detail:
|
||||
"The Java probe did not report os.name; the JVM-selected native decompiler OS directory is unknown and cannot be matched to the validated host platform",
|
||||
remediation: `Point JAVA_HOME at a JDK whose reported operating system matches the host platform (${platform}). REA does not build or install native tools.`,
|
||||
});
|
||||
if (!jvmOsNameMatchesPlatform(platform, osName) && path !== null)
|
||||
return installationCheck({
|
||||
name: "native_decompiler",
|
||||
passed: false,
|
||||
code: "executable_missing",
|
||||
detail: `JVM reports os.name=${osName}, which does not match the validated host platform ${platform} (host ${platform}/${architecture})`,
|
||||
remediation: `Point JAVA_HOME at a JDK whose reported operating system matches the host platform (${platform}). REA does not build or install native tools.`,
|
||||
});
|
||||
}
|
||||
return installationCheck({
|
||||
name: "native_decompiler",
|
||||
passed: path !== null && compatibility?.status !== "incompatible",
|
||||
|
|
@ -646,6 +731,43 @@ const nativeDecompilerCheck = ({
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether a JVM os.name value selects the host platform's native OS directory.
|
||||
* Unknown values never align with the validated host directory; admission
|
||||
* must not treat them as matching.
|
||||
*/
|
||||
const jvmOsNameMatchesPlatform = (
|
||||
platform: NodeJS.Platform,
|
||||
osName: string,
|
||||
): boolean => {
|
||||
const normalized = osName.toLowerCase();
|
||||
if (platform === "win32") return normalized.startsWith("windows");
|
||||
if (platform === "darwin")
|
||||
return normalized.startsWith("mac") || normalized.startsWith("darwin");
|
||||
if (platform === "linux") return normalized.startsWith("linux");
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Map a JVM os.arch value to the Ghidra native directory that JVM selects.
|
||||
* Unknown values stay unknown; admission must not treat them as aligned with
|
||||
* the validated host directory.
|
||||
*/
|
||||
const jvmOsArchNativeDirectory = (
|
||||
platform: NodeJS.Platform,
|
||||
osArch: string | undefined,
|
||||
): string | undefined => {
|
||||
if (osArch === undefined) return undefined;
|
||||
const normalized = osArch.toLowerCase();
|
||||
const node: NodeJS.Architecture | undefined =
|
||||
normalized === "x86_64" || normalized === "amd64" || normalized === "x64"
|
||||
? "x64"
|
||||
: normalized === "aarch64" || normalized === "arm64"
|
||||
? "arm64"
|
||||
: undefined;
|
||||
return node === undefined ? undefined : NATIVE_PLATFORMS[platform]?.[node];
|
||||
};
|
||||
|
||||
const versionCheck = (
|
||||
properties: GhidraApplicationProperties | undefined,
|
||||
): GhidraInstallationCheck => {
|
||||
|
|
@ -781,6 +903,8 @@ const systemGhidraInstallationHost = (): GhidraInstallationHost => ({
|
|||
const version = javaProperty(output, "java.version");
|
||||
const home = javaProperty(output, "java.home");
|
||||
const vmName = javaProperty(output, "java.vm.name");
|
||||
const osArch = javaProperty(output, "os.arch");
|
||||
const osName = javaProperty(output, "os.name");
|
||||
const bits = Number.parseInt(
|
||||
javaProperty(output, "sun.arch.data.model") ?? "0",
|
||||
10,
|
||||
|
|
@ -791,6 +915,8 @@ const systemGhidraInstallationHost = (): GhidraInstallationHost => ({
|
|||
major: javaMajor(version),
|
||||
home,
|
||||
...(vmName === null ? {} : { vmName }),
|
||||
...(osArch === null ? {} : { osArch }),
|
||||
...(osName === null ? {} : { osName }),
|
||||
bits,
|
||||
runtime: executablePath(
|
||||
(process.platform === "win32" ? win32 : posix).join(
|
||||
|
|
|
|||
|
|
@ -2,11 +2,43 @@ import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
|
|||
import { dirname, join } from "node:path";
|
||||
import { execFileOutput } from "../process/ExecFileOutput.js";
|
||||
|
||||
/** JVM option environments remove quotes, including quotes inside an option. */
|
||||
const jvmEnvironmentArguments = (value: string): readonly string[] => {
|
||||
const arguments_: string[] = [];
|
||||
let argument = "";
|
||||
let quote: "'" | '"' | undefined;
|
||||
for (const character of value) {
|
||||
if (quote !== undefined) {
|
||||
if (character === quote) quote = undefined;
|
||||
else argument += character;
|
||||
} else if (character === "'" || character === '"') quote = character;
|
||||
else if (/\s/u.test(character)) {
|
||||
if (argument !== "") arguments_.push(argument);
|
||||
argument = "";
|
||||
} else argument += character;
|
||||
}
|
||||
if (quote !== undefined)
|
||||
throw new Error(
|
||||
"Invalid quoted Ghidra LaunchSupport JVM environment option",
|
||||
);
|
||||
if (argument !== "") arguments_.push(argument);
|
||||
return arguments_;
|
||||
};
|
||||
|
||||
const rejectJvmPlatformOverrides = (arguments_: readonly string[]): void => {
|
||||
if (
|
||||
arguments_.some((argument) => /^-Dos\.(?:arch|name)(?:=|$)/u.test(argument))
|
||||
)
|
||||
throw new Error(
|
||||
"Ghidra LaunchSupport may not override os.arch or os.name; the native decompiler was validated for the inspected JVM platform.",
|
||||
);
|
||||
};
|
||||
|
||||
/** 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 javaHome?: string;
|
||||
readonly javaVmName?: string;
|
||||
readonly homeRoot: string;
|
||||
readonly tempRoot: string;
|
||||
|
|
@ -21,7 +53,17 @@ export const ghidraJavaLaunch = async (options: {
|
|||
}> => {
|
||||
const supportRoot = dirname(options.analyzeHeadlessPath);
|
||||
const installRoot = dirname(supportRoot);
|
||||
const command = join(options.javaHome, "bin", "java");
|
||||
const selectedJavaHome = options.javaHome ?? options.environment.JAVA_HOME;
|
||||
const command =
|
||||
selectedJavaHome === undefined
|
||||
? options.platform === "win32"
|
||||
? "java.exe"
|
||||
: "java"
|
||||
: join(
|
||||
selectedJavaHome,
|
||||
"bin",
|
||||
options.platform === "win32" ? "java.exe" : "java",
|
||||
);
|
||||
const isolatedProperties = [
|
||||
`-Duser.home=${options.homeRoot}`,
|
||||
`-Djava.io.tmpdir=${options.tempRoot}`,
|
||||
|
|
@ -48,6 +90,9 @@ export const ghidraJavaLaunch = async (options: {
|
|||
[...supportArguments, "-envvars"],
|
||||
{
|
||||
env: environment,
|
||||
// Wait for the helper's exit on abort so a cancelled or timed-out
|
||||
// startup never leaves the probe running without an owning process.
|
||||
stopSignal: "SIGTERM",
|
||||
...(options.signal === undefined ? {} : { signal: options.signal }),
|
||||
},
|
||||
);
|
||||
|
|
@ -65,11 +110,25 @@ export const ghidraJavaLaunch = async (options: {
|
|||
if ((environment[key] ?? "") === "")
|
||||
environment[key] = line.slice(separator + 1);
|
||||
}
|
||||
// The merged environment becomes the analysis JVM's environment, and the
|
||||
// JVM applies these options variables automatically. An injected
|
||||
// -Dos.arch/-Dos.name there selects an unvalidated native directory without
|
||||
// appearing in vmArguments, so reject the effective values as well.
|
||||
for (const key of [
|
||||
"JAVA_TOOL_OPTIONS",
|
||||
"_JAVA_OPTIONS",
|
||||
"JDK_JAVA_OPTIONS",
|
||||
] as const) {
|
||||
rejectJvmPlatformOverrides(jvmEnvironmentArguments(environment[key] ?? ""));
|
||||
}
|
||||
const configuredArguments = await execFileOutput(
|
||||
command,
|
||||
[...supportArguments, "-vmargs"],
|
||||
{
|
||||
env: environment,
|
||||
// Wait for the helper's exit on abort so a cancelled or timed-out
|
||||
// startup never leaves the probe running without an owning process.
|
||||
stopSignal: "SIGTERM",
|
||||
...(options.signal === undefined ? {} : { signal: options.signal }),
|
||||
},
|
||||
);
|
||||
|
|
@ -83,6 +142,7 @@ export const ghidraJavaLaunch = async (options: {
|
|||
)
|
||||
)
|
||||
throw new Error("Invalid Ghidra LaunchSupport JVM argument");
|
||||
rejectJvmPlatformOverrides(vmArguments);
|
||||
return {
|
||||
command,
|
||||
environment,
|
||||
|
|
|
|||
|
|
@ -172,6 +172,8 @@ export interface GhidraHeadlessLauncherOptions {
|
|||
readonly mipsSoftFloat?: true;
|
||||
/** Spawn seam for provider-boundary lifecycle tests. */
|
||||
readonly spawnProcess?: typeof spawnOwnedProviderProcess;
|
||||
/** LaunchSupport probe seam for Java-free launcher fixtures. */
|
||||
readonly javaLaunch?: typeof ghidraJavaLaunch;
|
||||
}
|
||||
|
||||
/** Launch Ghidra without copying scripts into or modifying its installation. */
|
||||
|
|
@ -303,43 +305,46 @@ export class GhidraHeadlessLauncher implements GhidraLauncher {
|
|||
throw new GhidraLaunchError(
|
||||
"POSIX Ghidra requires its inspected JDK home",
|
||||
);
|
||||
const inspectedJavaLaunch = await (
|
||||
this.options.javaLaunch ?? ghidraJavaLaunch
|
||||
)({
|
||||
platform,
|
||||
analyzeHeadlessPath: this.options.analyzeHeadlessPath,
|
||||
...(this.options.javaHome === undefined
|
||||
? {}
|
||||
: { javaHome: this.options.javaHome }),
|
||||
...(this.options.javaVmName === undefined
|
||||
? {}
|
||||
: { javaVmName: this.options.javaVmName }),
|
||||
homeRoot: paths.homeRoot,
|
||||
tempRoot: paths.tempRoot,
|
||||
arguments: headlessArguments,
|
||||
environment,
|
||||
onStderr: (mode, stderr) => {
|
||||
if (stderr === "") return;
|
||||
const bytes = Buffer.byteLength(stderr);
|
||||
const redacted = redactCapturedTransportCredential(
|
||||
{ text: stderr, bytes, observedBytes: bytes },
|
||||
session.token,
|
||||
"[REDACTED]",
|
||||
);
|
||||
const bounded = boundedUtf8Text(
|
||||
redacted,
|
||||
GHIDRA_LAUNCH_COMMAND_OUTPUT_BYTES,
|
||||
);
|
||||
launchSupport.push({
|
||||
mode,
|
||||
stderr: bounded.text,
|
||||
stderr_bytes: bytes,
|
||||
stderr_truncated: bounded.truncated,
|
||||
});
|
||||
},
|
||||
...(options.signal === undefined ? {} : { signal: options.signal }),
|
||||
});
|
||||
const command =
|
||||
platform !== "win32" && this.options.javaHome !== undefined
|
||||
? await ghidraJavaLaunch({
|
||||
platform,
|
||||
analyzeHeadlessPath: this.options.analyzeHeadlessPath,
|
||||
javaHome: this.options.javaHome,
|
||||
...(this.options.javaVmName === undefined
|
||||
? {}
|
||||
: { javaVmName: this.options.javaVmName }),
|
||||
homeRoot: paths.homeRoot,
|
||||
tempRoot: paths.tempRoot,
|
||||
arguments: headlessArguments,
|
||||
environment,
|
||||
onStderr: (mode, stderr) => {
|
||||
if (stderr === "") return;
|
||||
const bytes = Buffer.byteLength(stderr);
|
||||
const redacted = redactCapturedTransportCredential(
|
||||
{ text: stderr, bytes, observedBytes: bytes },
|
||||
session.token,
|
||||
"[REDACTED]",
|
||||
);
|
||||
const bounded = boundedUtf8Text(
|
||||
redacted,
|
||||
GHIDRA_LAUNCH_COMMAND_OUTPUT_BYTES,
|
||||
);
|
||||
launchSupport.push({
|
||||
mode,
|
||||
stderr: bounded.text,
|
||||
stderr_bytes: bytes,
|
||||
stderr_truncated: bounded.truncated,
|
||||
});
|
||||
},
|
||||
...(options.signal === undefined
|
||||
? {}
|
||||
: { signal: options.signal }),
|
||||
})
|
||||
: { ...scriptCommand, environment };
|
||||
platform === "win32"
|
||||
? { ...scriptCommand, environment }
|
||||
: inspectedJavaLaunch;
|
||||
started = await (this.options.spawnProcess ?? spawnOwnedProviderProcess)({
|
||||
command: command.command,
|
||||
arguments: command.arguments,
|
||||
|
|
|
|||
|
|
@ -37,12 +37,16 @@ import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
|||
import { ProviderStartupDeadline } from "../process/ProviderDeadline.js";
|
||||
|
||||
const INSTALL = "/opt/ghidra_12.1.4_PUBLIC";
|
||||
const installationHost = (): GhidraInstallationHost => ({
|
||||
platform: "linux",
|
||||
const installationHost = (
|
||||
platform: NodeJS.Platform = "linux",
|
||||
): GhidraInstallationHost => ({
|
||||
platform,
|
||||
architecture: "x64",
|
||||
readText: () => "application.version=12.1.4\napplication.java.min=21\n",
|
||||
executable: () => true,
|
||||
probeJava: () => ({
|
||||
osArch: "amd64",
|
||||
osName: platform === "win32" ? "Windows 11" : "Linux",
|
||||
version: "21.0.11",
|
||||
major: 21,
|
||||
home: "/usr/lib/jvm/jdk-21",
|
||||
|
|
@ -102,7 +106,7 @@ describe("Ghidra provider", () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
const windows = provider({ ...installationHost(), platform: "win32" });
|
||||
const windows = provider(installationHost("win32"));
|
||||
expect(windows.inspectTargetSupport(target).status).toBe("unsupported");
|
||||
});
|
||||
it("discovers the exact installation once without launching Ghidra", () => {
|
||||
|
|
@ -112,6 +116,8 @@ describe("Ghidra provider", () => {
|
|||
probeJava: () => {
|
||||
probeCount += 1;
|
||||
return {
|
||||
osArch: "amd64",
|
||||
osName: "Linux",
|
||||
version: "21.0.11",
|
||||
major: 21,
|
||||
home: "/usr/lib/jvm/jdk-21",
|
||||
|
|
@ -193,7 +199,7 @@ describe("Ghidra provider", () => {
|
|||
|
||||
describe("Ghidra raw-binary profile", () => {
|
||||
it("admits raw 6502 on Windows and binds its 16-bit language without mutation authority", async () => {
|
||||
const ghidra = provider({ ...installationHost(), platform: "win32" });
|
||||
const ghidra = provider(installationHost("win32"));
|
||||
const target: BinaryTarget = {
|
||||
path: "/tmp/6502.bin",
|
||||
sha256: "a".repeat(64),
|
||||
|
|
@ -260,7 +266,7 @@ describe("Ghidra raw-binary profile", () => {
|
|||
expect(shifted.value.profile?.digest).not.toBe(
|
||||
resolved.value.profile.digest,
|
||||
);
|
||||
const windows = provider({ ...installationHost(), platform: "win32" });
|
||||
const windows = provider(installationHost("win32"));
|
||||
expect(windows.inspectTargetSupport(target).status).toBe("unsupported");
|
||||
});
|
||||
});
|
||||
|
|
@ -302,7 +308,7 @@ describe("Ghidra Mach-O slice support", () => {
|
|||
|
||||
describe("Ghidra platform support", () => {
|
||||
it("commits the PE role and managed classification to profile identity", async () => {
|
||||
const ghidra = provider({ ...installationHost(), platform: "win32" });
|
||||
const ghidra = provider(installationHost("win32"));
|
||||
const application = peTarget("x86_64");
|
||||
const library = {
|
||||
...application,
|
||||
|
|
@ -343,7 +349,7 @@ describe("Ghidra platform support", () => {
|
|||
});
|
||||
|
||||
it("keeps Windows annotation mutation unavailable independently of native controls", () => {
|
||||
const ghidra = provider({ ...installationHost(), platform: "win32" });
|
||||
const ghidra = provider(installationHost("win32"));
|
||||
expect(
|
||||
ghidra
|
||||
.capabilities()
|
||||
|
|
@ -363,7 +369,7 @@ describe("Ghidra platform support", () => {
|
|||
reason: WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON,
|
||||
});
|
||||
onTestFinished(() => native.mockRestore());
|
||||
const ghidra = provider({ ...installationHost(), platform: "win32" });
|
||||
const ghidra = provider(installationHost("win32"));
|
||||
const nativeApplication = peTarget("x86_64");
|
||||
|
||||
for (const architecture of ["x86", "x86_64"] as const)
|
||||
|
|
@ -454,7 +460,7 @@ describe("Ghidra Windows PE fixture admission", () => {
|
|||
await promisify(execFile)(process.execPath, [
|
||||
join(root, "scripts/create-ghidra-windows-fixture.mjs"),
|
||||
]);
|
||||
const ghidra = provider({ ...installationHost(), platform: "win32" });
|
||||
const ghidra = provider(installationHost("win32"));
|
||||
for (const architecture of ["x86", "x86_64"] as const) {
|
||||
for (const dll of [false, true]) {
|
||||
const path = join(
|
||||
|
|
@ -1818,6 +1824,8 @@ describe("compatible Ghidra builds", () => {
|
|||
readText: () =>
|
||||
"application.version=12.1.2\napplication.java.min=21\napplication.java.max=\n",
|
||||
probeJava: () => ({
|
||||
osArch: "amd64",
|
||||
osName: "Linux",
|
||||
version: "27",
|
||||
major: 27,
|
||||
home: "/usr/lib/jvm/java-27-openjdk",
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ const fixture = (version = "12.1.4") => {
|
|||
`application.version=${version}\napplication.java.min=21\n`,
|
||||
executable: () => true,
|
||||
probeJava: () => ({
|
||||
osArch: "amd64",
|
||||
osName: "Linux",
|
||||
version: "21.0.11",
|
||||
major: 21,
|
||||
home: "/usr/lib/jvm/jdk-21",
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ const host: GhidraInstallationHost = {
|
|||
readText: () => "application.version=12.1.3\napplication.java.min=21\n",
|
||||
executable: () => true,
|
||||
probeJava: () => ({
|
||||
osArch: "amd64",
|
||||
osName: "Linux",
|
||||
version: "21.0.12",
|
||||
major: 21,
|
||||
home: "/jdk-21",
|
||||
|
|
|
|||
|
|
@ -106,6 +106,8 @@ export const ghidraInstallationHost = (): GhidraInstallationHost => ({
|
|||
readText: () => "application.version=12.1.4\napplication.java.min=21\n",
|
||||
executable: () => true,
|
||||
probeJava: () => ({
|
||||
osArch: "amd64",
|
||||
osName: "Linux",
|
||||
version: "21.0.11",
|
||||
major: 21,
|
||||
home: "/usr/lib/jvm/jdk-21",
|
||||
|
|
|
|||
|
|
@ -102,6 +102,8 @@ const installationHost: GhidraInstallationHost = {
|
|||
readText: () => "application.version=12.1.4\napplication.java.min=21\n",
|
||||
executable: () => true,
|
||||
probeJava: () => ({
|
||||
osArch: "amd64",
|
||||
osName: "Linux",
|
||||
version: "21.0.11",
|
||||
major: 21,
|
||||
home: "/fixture/jdk-21",
|
||||
|
|
|
|||
|
|
@ -50,9 +50,16 @@ if (mode === "env-failure" || (probe === "-vmargs" && mode === "vm-failure")) {
|
|||
process.stderr.write("failed " + probe + "\\n");
|
||||
process.exitCode = 3;
|
||||
} else if (probe === "-envvars") {
|
||||
process.stdout.write(mode === "env-malformed" ? "invalid environment\\n" : "GHIDRA_MAXMEM=1G\\n");
|
||||
process.stdout.write(mode === "env-malformed" ? "invalid environment\\n" : mode.startsWith("env-injection:") ? mode.slice("env-injection:".length) + "\\n" : "GHIDRA_MAXMEM=1G\\n");
|
||||
} else {
|
||||
process.stdout.write(mode === "vm-malformed" ? "invalid JVM argument\\n" : "-Dfile.encoding=UTF8\\n");
|
||||
const vmArguments = mode === "vm-malformed"
|
||||
? "invalid JVM argument\\n"
|
||||
: mode === "os-arch-override"
|
||||
? "-Dos.arch=x86_64\\n"
|
||||
: mode === "os-name-override"
|
||||
? "-Dos.name=Windows\\n"
|
||||
: "-Dfile.encoding=UTF8\\n";
|
||||
process.stdout.write(vmArguments);
|
||||
}
|
||||
`,
|
||||
{ mode: 0o755 },
|
||||
|
|
@ -215,6 +222,57 @@ describe.skipIf(process.platform === "win32")(
|
|||
},
|
||||
);
|
||||
|
||||
it.each(["os-arch-override", "os-name-override"])(
|
||||
"rejects LaunchSupport %s before starting analysis",
|
||||
async (mode) => {
|
||||
const { session, launcher, analysisArguments } = await fixture(mode);
|
||||
const launched = await launcher.launch(session);
|
||||
expect(launched.ok).toBe(false);
|
||||
expect(analysisArguments).toEqual([]);
|
||||
if (launched.ok)
|
||||
throw new Error("Expected a JVM platform override failure");
|
||||
expect(launched.error.cause).toMatchObject({
|
||||
message: expect.stringContaining(
|
||||
"may not override os.arch or os.name",
|
||||
),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["JAVA_TOOL_OPTIONS", "-Dos.arch=x86_64"],
|
||||
["JAVA_TOOL_OPTIONS", '"-Dos.arch=x86_64"'],
|
||||
["_JAVA_OPTIONS", "'-Dos.name=Windows'"],
|
||||
["JDK_JAVA_OPTIONS", '-D"os.name"=Windows'],
|
||||
["JDK_JAVA_OPTIONS", "-D'os.arch'=x86_64"],
|
||||
])(
|
||||
"rejects helper-environment platform injection from %s=%s before analysis",
|
||||
async (key, value) => {
|
||||
const { session, launcher, analysisArguments } = await fixture(
|
||||
`env-injection:${key}=${value}`,
|
||||
);
|
||||
const launched = await launcher.launch(session);
|
||||
expect(launched.ok).toBe(false);
|
||||
expect(analysisArguments).toEqual([]);
|
||||
if (launched.ok)
|
||||
throw new Error("Expected a JVM platform override failure");
|
||||
expect(launched.error.cause).toMatchObject({
|
||||
message: expect.stringContaining(
|
||||
"may not override os.arch or os.name",
|
||||
),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("admits a quoted non-platform option without mistaking its value for a platform argument", async () => {
|
||||
const { session, launcher, analysisArguments } = await fixture(
|
||||
'env-injection:JAVA_TOOL_OPTIONS=-Ddescription="text -Dos.arch=x86_64"',
|
||||
);
|
||||
const launched = await launcher.launch(session);
|
||||
if (!launched.ok) throw launched.error;
|
||||
expect(analysisArguments).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("bounds retained UTF-8 warnings and reports original byte counts", async () => {
|
||||
const { session, launcher } = await fixture("large");
|
||||
const launched = await launcher.launch(session);
|
||||
|
|
@ -290,6 +348,13 @@ describe.skipIf(process.platform === "win32")(
|
|||
"application.version=12.1.4\napplication.java.min=21\n",
|
||||
executable: () => true,
|
||||
probeJava: () => ({
|
||||
osArch: process.arch === "arm64" ? "aarch64" : "amd64",
|
||||
osName:
|
||||
process.platform === "darwin"
|
||||
? "Mac OS X"
|
||||
: process.platform === "win32"
|
||||
? "Windows 11"
|
||||
: "Linux",
|
||||
version: "21.0.12",
|
||||
major: 21,
|
||||
home: "/fixture/jdk",
|
||||
|
|
|
|||
|
|
@ -524,6 +524,13 @@ describe("Ghidra Windows launcher runtime", () => {
|
|||
),
|
||||
javaHome: "C:\\Java\\jdk-21",
|
||||
bridgeScriptPath: join(parent, "ReaGhidraBridge.java"),
|
||||
// The preflight validates the inspected JVM's LaunchSupport output;
|
||||
// these fixtures assert batch-spawn behavior without a working JDK.
|
||||
javaLaunch: async () => ({
|
||||
command: "C:\\Java\\jdk-21\\bin\\java.exe",
|
||||
arguments: [],
|
||||
environment: {},
|
||||
}),
|
||||
spawnProcess,
|
||||
});
|
||||
const launched = await launcher.launch({
|
||||
|
|
@ -569,6 +576,13 @@ describe("Ghidra Windows launcher runtime", () => {
|
|||
platform: "win32",
|
||||
javaHome,
|
||||
bridgeScriptPath: "/package/bridge/ReaGhidraBridge.java",
|
||||
// The preflight validates the inspected JVM's LaunchSupport output;
|
||||
// this fixture captures the batch launcher without a working JDK.
|
||||
javaLaunch: async () => ({
|
||||
command: `${javaHome}\\bin\\java.exe`,
|
||||
arguments: [],
|
||||
environment: {},
|
||||
}),
|
||||
});
|
||||
const launched = await launcher.launch({
|
||||
runtimeRoot,
|
||||
|
|
|
|||
Loading…
Reference in New Issue