Restore test fixture runtime prerequisites and align assertions with current Evidence and uncertainty contracts. Separate actual CLI publisher refusal from explicitly admitted Hopper socket/CLI fixtures, retaining exact wire identity and cleanup coverage. Validate retained EVM Evidence against exact unchanged source-carrier bytes. Validation: 88 targeted local test passes across three completed selections, two platform skips, full check:ci; hosted Windows probes and complete current-head CI. No production authority, authentication or schema change. Controlled fixtures do not qualify actual disassemblers or workers. Fixes #4690.
297 lines
9.8 KiB
TypeScript
297 lines
9.8 KiB
TypeScript
import { probeGhidraJava } from "../../../src/ghidra/GhidraJavaProbe.js";
|
|
import { spawnOwnedProviderProcess } from "../../../src/process/ProviderProcess.js";
|
|
import { createInspectionResources } from "../../fixtures/inspectionResources.js";
|
|
|
|
import { mkdir, writeFile, readFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { afterEach, expect, it } from "vitest";
|
|
|
|
import {
|
|
inspectGhidraInstallation,
|
|
systemGhidraInstallationHost,
|
|
ghidraInstallationDiagnostics,
|
|
} from "../../../src/ghidra/GhidraInstallation.js";
|
|
import { SUPPORTED_GHIDRA_VERSION } from "../../../src/ghidra/GhidraReleasePolicy.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
|
|
let inspectionResources = createInspectionResources();
|
|
afterEach(async () => {
|
|
await inspectionResources.close();
|
|
inspectionResources = createInspectionResources();
|
|
});
|
|
|
|
/** Controlled Java-shaped command exercising the real process owner, not a JVM. */
|
|
const writeJavaProbe = async (
|
|
home: string,
|
|
stderrText: string,
|
|
outputBytes?: number,
|
|
) => {
|
|
const executable = join(
|
|
home,
|
|
"bin",
|
|
process.platform === "win32" ? "java.exe" : "java",
|
|
);
|
|
await mkdir(join(home, "bin"), { recursive: true });
|
|
await writeFile(
|
|
process.platform === "win32" ? `${executable}.cjs` : executable,
|
|
`#!${process.execPath}\nconst fs = require('node:fs');\nfs.writeFileSync(${JSON.stringify(join(home, "probe-arguments.json"))}, JSON.stringify(process.argv.slice(2)));\nconst prefix = ${JSON.stringify(stderrText)};\nprocess.stderr.write(prefix${outputBytes === undefined ? "" : ` + 'X'.repeat(${outputBytes} - Buffer.byteLength(prefix))`});\n`,
|
|
{ mode: 0o700 },
|
|
);
|
|
};
|
|
|
|
// Windows executes the controlled JS prerequisite with the real owned Node exe;
|
|
// POSIX executes its selected shebang path. Neither fixture is a real JVM.
|
|
const probeHost =
|
|
process.platform !== "win32"
|
|
? undefined
|
|
: {
|
|
...systemGhidraInstallationHost(),
|
|
probeJava: (
|
|
command: string,
|
|
environment: NodeJS.ProcessEnv,
|
|
options: Parameters<typeof probeGhidraJava>[2],
|
|
) =>
|
|
probeGhidraJava(command, environment, {
|
|
...options,
|
|
launcher: (spawn) =>
|
|
spawnOwnedProviderProcess({
|
|
...spawn,
|
|
command: process.execPath,
|
|
arguments: [`${command}.cjs`, ...spawn.arguments],
|
|
// The controlled Node prerequisite needs Windows runtime state.
|
|
hostEnvironment: {
|
|
SystemRoot: process.env.SystemRoot,
|
|
TEMP: tmpdir(),
|
|
TMP: tmpdir(),
|
|
...spawn.hostEnvironment,
|
|
},
|
|
}),
|
|
}),
|
|
};
|
|
|
|
it.each([
|
|
{ major: 17, fullJdk: true, vmName: "Java HotSpot(TM) 64-Bit Server VM" },
|
|
{ major: 21, fullJdk: true, vmName: "Eclipse OpenJ9 VM" },
|
|
{ major: 21, fullJdk: false, vmName: undefined },
|
|
])(
|
|
"preserves the reported Unicode Java home and classifies JDK $major (full JDK: $fullJdk)",
|
|
async ({ major, fullJdk, vmName }) => {
|
|
const root = await createTestTempDirectory("rea-java-probe-");
|
|
const configuredHome = join(root, "selected-jdk");
|
|
const reportedHome = join(root, "Réa逆向", `JDK ${String(major)}`);
|
|
const installDir = join(root, "ghidra");
|
|
await mkdir(join(installDir, "Ghidra"), { recursive: true });
|
|
await writeFile(
|
|
join(installDir, "Ghidra", "application.properties"),
|
|
`application.version=${SUPPORTED_GHIDRA_VERSION}\napplication.java.min=17\n`,
|
|
);
|
|
if (fullJdk) {
|
|
await mkdir(join(reportedHome, "bin"), { recursive: true });
|
|
await writeFile(
|
|
join(
|
|
reportedHome,
|
|
"bin",
|
|
process.platform === "win32" ? "javac.exe" : "javac",
|
|
),
|
|
"compiler presence fixture",
|
|
{ mode: 0o755 },
|
|
);
|
|
}
|
|
const version = `${String(major)}.0.11`;
|
|
await writeJavaProbe(
|
|
configuredHome,
|
|
` java.version = ${version}\n java.home = ${reportedHome}\n sun.arch.data.model = 64\n${vmName === undefined ? "" : ` java.vm.name = ${vmName}\n`}`,
|
|
);
|
|
|
|
const inspection = await inspectGhidraInstallation(
|
|
{
|
|
resourceOwner: inspectionResources.owner,
|
|
environment: {},
|
|
installDir,
|
|
javaHome: configuredHome,
|
|
},
|
|
probeHost,
|
|
);
|
|
|
|
expect(inspection.javaHome).toBe(reportedHome);
|
|
expect(inspection.javaVersion).toBe(version);
|
|
expect(inspection.javaVmName).toBe(vmName);
|
|
expect(ghidraInstallationDiagnostics(inspection).java_vm_name).toBe(
|
|
vmName ?? null,
|
|
);
|
|
expect(inspection.checks.find(({ name }) => name === "java")).toMatchObject(
|
|
{
|
|
status: fullJdk ? "passed" : "failed",
|
|
detail: expect.stringContaining(fullJdk ? "; JDK;" : "; runtime only;"),
|
|
},
|
|
);
|
|
expect(
|
|
JSON.parse(
|
|
await readFile(join(configuredHome, "probe-arguments.json"), "utf8"),
|
|
),
|
|
).toEqual(
|
|
expect.arrayContaining([
|
|
"-Dstdout.encoding=UTF-8",
|
|
"-Dstderr.encoding=UTF-8",
|
|
"-Dsun.stdout.encoding=UTF-8",
|
|
"-Dsun.stderr.encoding=UTF-8",
|
|
]),
|
|
);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
{ label: "omitted", property: "", shown: "bitness unknown" },
|
|
{
|
|
label: "empty",
|
|
property: "sun.arch.data.model =\n",
|
|
shown: 'bitness unknown (sun.arch.data.model = "")',
|
|
},
|
|
{
|
|
label: "malformed",
|
|
property: "sun.arch.data.model = 64junk\n",
|
|
shown: 'bitness unknown (sun.arch.data.model = "64junk")',
|
|
},
|
|
{
|
|
label: "not exactly representable",
|
|
property: "sun.arch.data.model = 9007199254740993\n",
|
|
shown: 'bitness unknown (sun.arch.data.model = "9007199254740993")',
|
|
},
|
|
])("keeps $label Java bitness unresolved", async ({ property, shown }) => {
|
|
const root = await createTestTempDirectory("rea-java-bitness-");
|
|
const configuredHome = join(root, "selected-jdk");
|
|
const reportedHome = join(root, "reported-jdk");
|
|
const installDir = join(root, "ghidra");
|
|
await mkdir(join(installDir, "Ghidra"), { recursive: true });
|
|
await mkdir(join(reportedHome, "bin"), { recursive: true });
|
|
await writeFile(
|
|
join(
|
|
reportedHome,
|
|
"bin",
|
|
process.platform === "win32" ? "javac.exe" : "javac",
|
|
),
|
|
"fixture",
|
|
{
|
|
mode: 0o755,
|
|
},
|
|
);
|
|
await writeFile(
|
|
join(installDir, "Ghidra", "application.properties"),
|
|
`application.version=${SUPPORTED_GHIDRA_VERSION}\napplication.java.min=17\n`,
|
|
);
|
|
await writeJavaProbe(
|
|
configuredHome,
|
|
` java.version = 21.0.11\n java.home = ${reportedHome}\n${property}`,
|
|
);
|
|
|
|
const inspection = await inspectGhidraInstallation(
|
|
{
|
|
resourceOwner: inspectionResources.owner,
|
|
environment: {},
|
|
installDir,
|
|
javaHome: configuredHome,
|
|
},
|
|
probeHost,
|
|
);
|
|
|
|
expect(inspection.status).toBe("unavailable");
|
|
expect(inspection.checks.find(({ name }) => name === "java")).toMatchObject({
|
|
status: "failed",
|
|
code: "version_unresolved",
|
|
detail: expect.stringContaining(shown),
|
|
});
|
|
expect(
|
|
inspection.checks.find(({ name }) => name === "java")?.detail,
|
|
).not.toMatch(/(?:0-bit|9007199254740992-bit)/);
|
|
});
|
|
|
|
it("keeps a known 32-bit Java report classified as unsupported", async () => {
|
|
const root = await createTestTempDirectory("rea-java-bitness-known-");
|
|
const configuredHome = join(root, "selected-jdk");
|
|
const reportedHome = join(root, "reported-jdk");
|
|
const installDir = join(root, "ghidra");
|
|
await mkdir(join(installDir, "Ghidra"), { recursive: true });
|
|
await mkdir(join(reportedHome, "bin"), { recursive: true });
|
|
await writeFile(
|
|
join(
|
|
reportedHome,
|
|
"bin",
|
|
process.platform === "win32" ? "javac.exe" : "javac",
|
|
),
|
|
"fixture",
|
|
{
|
|
mode: 0o755,
|
|
},
|
|
);
|
|
await writeFile(
|
|
join(installDir, "Ghidra", "application.properties"),
|
|
`application.version=${SUPPORTED_GHIDRA_VERSION}\napplication.java.min=17\n`,
|
|
);
|
|
await writeJavaProbe(
|
|
configuredHome,
|
|
` java.version = 21.0.11\n java.home = ${reportedHome}\n sun.arch.data.model = 32\n`,
|
|
);
|
|
|
|
const inspection = await inspectGhidraInstallation(
|
|
{
|
|
resourceOwner: inspectionResources.owner,
|
|
environment: {},
|
|
installDir,
|
|
javaHome: configuredHome,
|
|
},
|
|
probeHost,
|
|
);
|
|
|
|
expect(inspection.checks.find(({ name }) => name === "java")).toMatchObject({
|
|
status: "failed",
|
|
code: "unsupported_version",
|
|
detail: expect.stringContaining("32-bit"),
|
|
});
|
|
});
|
|
|
|
it("bounds the Java version probe and reports an over-budget probe as a failed Java check", async () => {
|
|
const root = await createTestTempDirectory("rea-java-probe-bound-");
|
|
const configuredHome = join(root, "selected-jdk");
|
|
const installDir = join(root, "ghidra");
|
|
await mkdir(join(installDir, "Ghidra"), { recursive: true });
|
|
await writeFile(
|
|
join(installDir, "Ghidra", "application.properties"),
|
|
`application.version=${SUPPORTED_GHIDRA_VERSION}\napplication.java.min=17\n`,
|
|
);
|
|
// The real command owner observes output beyond its one-MiB aggregate budget.
|
|
await writeJavaProbe(
|
|
configuredHome,
|
|
"java.version = 21.0.11\n",
|
|
1024 * 1024 + 1,
|
|
);
|
|
|
|
const inspection = await inspectGhidraInstallation(
|
|
{
|
|
resourceOwner: inspectionResources.owner,
|
|
environment: {},
|
|
installDir,
|
|
javaHome: configuredHome,
|
|
},
|
|
probeHost,
|
|
);
|
|
|
|
expect(inspection.javaVersion).toBeNull();
|
|
expect(inspection.javaHome).toBe(configuredHome);
|
|
expect(inspection.checks.find(({ name }) => name === "java")).toMatchObject({
|
|
status: "failed",
|
|
code: "runtime_unavailable",
|
|
});
|
|
expect(
|
|
ghidraInstallationDiagnostics(inspection).java_probe_failure,
|
|
).toMatchObject({
|
|
code: "runtime_unavailable",
|
|
process_error_code: "ENOBUFS",
|
|
output: {
|
|
stderr: expect.stringContaining("java.version = 21.0.11\n"),
|
|
truncated: true,
|
|
signal: null,
|
|
},
|
|
});
|
|
});
|