Files
rea/tests/boundary/filesystem/ghidraJavaProbe.test.ts
ShuaixinHuang 1201c25542 fix(ci): align static checks and regression fixtures with current contracts (#7349)
* fix(ci): retire unused internal analysis declarations

* test: use an admitted app bundle for provider routing

(cherry picked from commit 00aa17f12a)

* test: keep bundle routing coverage outside oversized group

(cherry picked from commit 8658353b0d)

* fix(ci): align regression fixtures with current admission contracts

* test: bind extraction close fixtures to their ZIP digest

(cherry picked from commit 626d0250db)

* test(cli): match typed analysis outcomes in function detail fixture

* test(ghidra): preserve empty Java property separator framing

* test(electron): retain unknown IPC ownership for native module gaps

* fix(electron): share source-scoped call path identity

* test(captures): assert safe source identity on SAZ refusal

* test(apple): match root occurrence kind to inventory manifest

* fix(examples): align directory root occurrence identity

* test(electron): align equivalent fixture assertions with upstream

* test(process): assert retained capture root identity

* test(windows): declare the PE fixture optional header

* test(ci): refresh owned registration and verifier fixtures

* test(setup): use owned registrations for refresh fixtures

* test(process): control unavailable ownership in CLI cleanup fixture

* test(process): guard controlled compiler output arguments

* test(setup): align equivalent entrypoint guards with upstream

* test(process): release the cleanup target through an owned gate

* test(ci): align semantic identities and owned fixture facts

* test(pcap): retain the upstream DNS wire fixture branch

* test(pcap): use the canonical wire ownership fixture

* test(pcap): assert exact DNS wire query labels

* test(ci): preserve admission and ownership fixture prerequisites

* test(ci): align authenticated fixture and cleanup boundaries

* test(windows): align native artifact fixtures with upstream

* test(android): request complete evidence for inline MCP contracts

* test: avoid pinning diagnostic wording

* test: verify cleanup diagnostics at their source

* test: strengthen evidence and cleanup checks

* test: avoid pinning registration guidance

---------

Co-authored-by: Plastic-time <Plastic-time@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-27 21:24:30 +08:00

302 lines
9.9 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: "nonstandard framing",
property: "sun.arch.data.model =\n",
shown: "bitness unknown",
},
{
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,
},
});
});