mirror of
https://github.com/morluto/rea.git
synced 2026-10-11 21:39:55 +00:00
* feat(ghidra): admit soft-float MIPS32r2 ELF with integer-register float storage Soft-float o32 executables (Linux kernel builds, bare-metal firmware) were refused because their ABI flags record declares no CPR1 and FP ABI 3. Ghidra's MIPS32 compiler specifications model only hard-float o32, so typed float/double signatures (DWARF) would be bound to $f12/$f14/$f0 and the decompiler loses the real dataflow: `return sel ? a : b` decompiled to `return in_f0;`, and callers passed phantom FPU inputs. Admit ELF32 ET_EXEC o32 MIPS32r2 targets whose ABI record declares soft-float with no CPR1, and commit `mips_soft_float_storage: o32-integer-registers-v1` in their analysis profile only (hard-float profile digests are unchanged). ReaGhidraMipsSoftFloat.java runs after auto-analysis and, for each function with a float/double formal type, either keeps the types with the storage Ghidra's o32 model assigns to a same-size integer (pair alignment, stack spill), or, for auto-parameters such as hidden struct-return pointers, swaps float types for same-size integers under dynamic storage. No function is left on hard-float storage. The report is written last; the provider client requires it exactly when the profile commits the policy and refuses the session before any operation otherwise. Verification: real Ghidra 12.1.4 lane (verify:ghidra:mips) now builds hard- and soft-float fixtures in both byte orders and checks exact parameter/return storage through inspect_native_api, caller constants, snapshot profile and Evidence outcome; test:fast, verify:package and an installed-tarball soft-float run pass. * fix(ghidra): model soft-float MIPS as a prototype for every typed callable Review feedback on #1949: rewriting storage per Function left external functions and indirect calls through typed function pointers on Ghidra's hard-float o32 prototype, which still read $f12/$f14/$f0 while the session reported success. A negative control confirms it: without the change, `(*rea_mips_soft_callback)(8.5f, 9.5f, sel)` decompiles to `(*rea_mips_soft_callback)(in_f12, in_f14, sel)`. ReaGhidraMipsSoftFloat.java now derives a `__rea_soft_float` prototype from the program's own default o32 model (encoded, float pentries and float-consuming rules removed, so byte-order-specific aggregate rules and the hidden struct-return pointer are kept), installs it as a specification extension, and assigns it with dynamic storage to every non-thunk function, external function and function-pointer definition whose formal types use float or double. Thunks follow their targets. Float types are kept everywhere, so the integer-retype fallback is removed. The script fails without writing its report if the derived prototype or any reassigned function still uses an FPU register. The policy becomes `o32-soft-float-prototype-v1` with a {functions, definitions} report. The soft-float fixture adds an indirect call through a typed pointer; the real-Ghidra lane requires its constants with no FPU inputs and the struct return's `float scale` in $a1. * test(ghidra): pin ODDSPREG acceptance for soft-float MIPS ABI records Review on #1949 suggested requiring flags1 == 0 for soft-float, since MIPS_AFL_FLAGS1_ODDSPREG names CPR1 registers. GCC 5.2 kernel modules, LLVM/Rust firmware and Clang -msoft-float objects all record flags1 = 1 with FP ABI soft-float: it is the MIPS32 o32 toolchain default and selects nothing without CPR1. Keep accepting it, say why at the check, and test that soft-float with ODDSPREG is admitted while any other general flag is refused.
550 lines
19 KiB
TypeScript
550 lines
19 KiB
TypeScript
import { access, chmod, readFile, stat } from "node:fs/promises";
|
|
import { join, posix, win32 } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
|
import { z } from "zod";
|
|
|
|
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
|
|
import { createGhidraTestRuntime } from "../../../fixtures/ghidraRuntime.js";
|
|
import type { PrivateRuntimeRoot } from "../../../../src/process/PrivateRuntimeRoot.js";
|
|
|
|
import {
|
|
ghidraHeadlessArguments,
|
|
ghidraHeadlessCommand,
|
|
GhidraHeadlessLauncher,
|
|
} from "../../../../src/ghidra/GhidraLauncher.js";
|
|
|
|
const fixturePath = fileURLToPath(
|
|
new URL(
|
|
process.platform === "win32"
|
|
? "../../../fixtures/captureGhidraLaunch.cmd"
|
|
: "../../../fixtures/captureGhidraLaunch.mjs",
|
|
import.meta.url,
|
|
),
|
|
);
|
|
const runtimes: PrivateRuntimeRoot[] = [];
|
|
|
|
const launchCaptureSchema = z.object({
|
|
arguments: z.array(z.string()),
|
|
environment: z.record(z.string(), z.string()),
|
|
descriptor_mode: z.number().int(),
|
|
descriptor_has_token: z.boolean(),
|
|
});
|
|
|
|
const expectOptions = (
|
|
arguments_: readonly string[],
|
|
options: readonly (readonly [string, string])[],
|
|
): void => {
|
|
for (const [flag, value] of options) {
|
|
const index = arguments_.indexOf(flag);
|
|
expect(index, flag).toBeGreaterThanOrEqual(0);
|
|
expect(arguments_[index + 1], flag).toBe(value);
|
|
}
|
|
};
|
|
|
|
const expectIsolatedEnvironment = (
|
|
environment: Record<string, string>,
|
|
runtimeRoot: string,
|
|
javaHome: string,
|
|
) => {
|
|
expect(environment).toMatchObject({
|
|
HOME: join(runtimeRoot, "home"),
|
|
...(process.platform === "win32"
|
|
? {
|
|
USERPROFILE: join(runtimeRoot, "home"),
|
|
APPDATA: join(runtimeRoot, "config"),
|
|
LOCALAPPDATA: join(runtimeRoot, "cache"),
|
|
TEMP: join(runtimeRoot, "tmp"),
|
|
TMP: join(runtimeRoot, "tmp"),
|
|
}
|
|
: {}),
|
|
TMPDIR: join(runtimeRoot, "tmp"),
|
|
XDG_CACHE_HOME: join(runtimeRoot, "cache"),
|
|
XDG_CONFIG_HOME: join(runtimeRoot, "config"),
|
|
XDG_DATA_HOME: join(runtimeRoot, "data"),
|
|
GHIDRA_JAVA_OPTIONS: "",
|
|
JAVA_TOOL_OPTIONS: "",
|
|
JDK_JAVA_OPTIONS:
|
|
process.platform === "win32"
|
|
? `"-Duser.home=${join(runtimeRoot, "home")}" "-Djava.io.tmpdir=${join(runtimeRoot, "tmp")}" "-XX:-UsePerfData"`
|
|
: "",
|
|
_JAVA_OPTIONS: "",
|
|
JAVA_HOME: javaHome,
|
|
REA_PROCESS_RUN_ID: "d6fcbb66-e829-4ff6-a535-0035aec63139",
|
|
});
|
|
expect(environment.PATH).toBe(
|
|
`${join(javaHome, "bin")}${process.platform === "win32" ? ";" : ":"}${process.env.PATH ?? ""}`,
|
|
);
|
|
};
|
|
|
|
beforeAll(async () => {
|
|
if (process.platform !== "win32") await chmod(fixturePath, 0o755);
|
|
});
|
|
afterEach(async () => {
|
|
vi.unstubAllEnvs();
|
|
await Promise.all(runtimes.splice(0).map((runtime) => runtime.close()));
|
|
});
|
|
|
|
describe("Ghidra COM loader", () => {
|
|
it("loads explicit COM at its PSP-relative entry before auto-analysis", () => {
|
|
const arguments_ = ghidraHeadlessArguments({
|
|
projectRoot: "/tmp/project",
|
|
targetPath: "/tmp/target.com",
|
|
bridgeScriptPath: "/package/bridge/ReaGhidraBridge.java",
|
|
descriptorPath: "/tmp/session.json",
|
|
ghidraLogPath: "/tmp/ghidra.log",
|
|
scriptLogPath: "/tmp/script.log",
|
|
dosCom: true,
|
|
});
|
|
expectOptions(arguments_, [
|
|
["-loader", "BinaryLoader"],
|
|
["-loader-baseAddr", "1000:0100"],
|
|
["-processor", "x86:LE:16:Real Mode"],
|
|
["-cspec", "default"],
|
|
]);
|
|
const prepare = arguments_.indexOf("-preScript");
|
|
expect(arguments_[prepare + 1]).toBe(
|
|
join("/package/bridge", "ReaGhidraPrepareCom.java"),
|
|
);
|
|
expect(prepare).toBeLessThan(arguments_.indexOf("-postScript"));
|
|
expect(arguments_).toContain("-readOnly");
|
|
expect(arguments_).toContain("-deleteProject");
|
|
});
|
|
});
|
|
|
|
describe("Ghidra raw-binary loader", () => {
|
|
it("loads caller-supplied architecture and entry before auto-analysis", () => {
|
|
const arguments_ = ghidraHeadlessArguments({
|
|
projectRoot: "/tmp/project",
|
|
targetPath: "/tmp/ecu.bin",
|
|
bridgeScriptPath: "/package/bridge/ReaGhidraBridge.java",
|
|
descriptorPath: "/tmp/session.json",
|
|
ghidraLogPath: "/tmp/ghidra.log",
|
|
scriptLogPath: "/tmp/script.log",
|
|
rawBinary: {
|
|
languageId: "ARM:LE:32:v7",
|
|
compilerSpecId: "default",
|
|
baseAddr: "0x8000000",
|
|
},
|
|
});
|
|
expectOptions(arguments_, [
|
|
["-loader", "BinaryLoader"],
|
|
["-loader-baseAddr", "0x8000000"],
|
|
["-processor", "ARM:LE:32:v7"],
|
|
["-cspec", "default"],
|
|
]);
|
|
const prepare = arguments_.indexOf("-preScript");
|
|
expect(arguments_[prepare + 1]).toBe(
|
|
join("/package/bridge", "ReaGhidraPrepareRaw.java"),
|
|
);
|
|
expect(arguments_[prepare + 2]).toBe("ARM:LE:32:v7");
|
|
expect(arguments_[prepare + 3]).toBe("0x8000000");
|
|
expect(prepare).toBeLessThan(arguments_.indexOf("-postScript"));
|
|
expect(arguments_).toContain("-readOnly");
|
|
expect(arguments_).toContain("-deleteProject");
|
|
});
|
|
});
|
|
|
|
describe("Ghidra headless launcher", () => {
|
|
it("forces the admitted real-mode language and loader for DOS MZ", () => {
|
|
const arguments_ = ghidraHeadlessArguments({
|
|
projectRoot: "/tmp/project",
|
|
targetPath: "/tmp/target",
|
|
bridgeScriptPath: "/package/bridge/ReaGhidraBridge.java",
|
|
descriptorPath: "/tmp/session.json",
|
|
ghidraLogPath: "/tmp/ghidra.log",
|
|
scriptLogPath: "/tmp/script.log",
|
|
dosMz: true,
|
|
});
|
|
expectOptions(arguments_, [
|
|
["-loader", "MzLoader"],
|
|
["-processor", "x86:LE:16:Real Mode"],
|
|
["-cspec", "default"],
|
|
]);
|
|
expect(arguments_).toContain("-readOnly");
|
|
expect(arguments_).toContain("-deleteProject");
|
|
});
|
|
it("builds a read-only ephemeral import with its bridge descriptor", () => {
|
|
const arguments_ = ghidraHeadlessArguments({
|
|
platform: "darwin",
|
|
projectRoot: "/tmp/project",
|
|
targetPath: "/tmp/target",
|
|
bridgeScriptPath: "/package/bridge/ReaGhidraBridge.java",
|
|
descriptorPath: "/tmp/session.json",
|
|
ghidraLogPath: "/tmp/ghidra.log",
|
|
scriptLogPath: "/tmp/script.log",
|
|
});
|
|
expect(arguments_.slice(0, 2)).toEqual(["/tmp/project", "rea-project"]);
|
|
expectOptions(arguments_, [
|
|
["-import", "/tmp/target"],
|
|
["-log", "/tmp/ghidra.log"],
|
|
["-scriptlog", "/tmp/script.log"],
|
|
["-scriptPath", "/package/bridge"],
|
|
["-postScript", "/package/bridge/ReaGhidraNoReturnFix.java"],
|
|
]);
|
|
// The no-return repair must finish before the bridge starts serving.
|
|
const fix = arguments_.indexOf("/package/bridge/ReaGhidraNoReturnFix.java");
|
|
const bridge = arguments_.indexOf("/package/bridge/ReaGhidraBridge.java");
|
|
expect(fix).toBeLessThan(bridge);
|
|
expect(arguments_[bridge + 1]).toBe("/tmp/session.json");
|
|
expect(arguments_).toContain("-readOnly");
|
|
expect(arguments_).toContain("-deleteProject");
|
|
});
|
|
it("remaps soft-float storage after the no-return fix and before the bridge", () => {
|
|
const base = {
|
|
platform: "linux" as const,
|
|
projectRoot: "/tmp/project",
|
|
targetPath: "/tmp/target",
|
|
bridgeScriptPath: "/package/bridge/ReaGhidraBridge.java",
|
|
descriptorPath: "/tmp/session.json",
|
|
ghidraLogPath: "/tmp/ghidra.log",
|
|
scriptLogPath: "/tmp/script.log",
|
|
};
|
|
const soft = "/package/bridge/ReaGhidraMipsSoftFloat.java";
|
|
expect(ghidraHeadlessArguments(base)).not.toContain(soft);
|
|
const arguments_ = ghidraHeadlessArguments({
|
|
...base,
|
|
mipsSoftFloat: true,
|
|
});
|
|
expect(arguments_[arguments_.indexOf(soft) - 1]).toBe("-postScript");
|
|
expect(
|
|
arguments_.indexOf("/package/bridge/ReaGhidraNoReturnFix.java"),
|
|
).toBeLessThan(arguments_.indexOf(soft));
|
|
expect(arguments_.indexOf(soft)).toBeLessThan(
|
|
arguments_.indexOf("/package/bridge/ReaGhidraBridge.java"),
|
|
);
|
|
for (const conflict of [
|
|
{ psp: true as const },
|
|
{ dosMz: true as const },
|
|
{ dosCom: true as const },
|
|
{
|
|
rawBinary: {
|
|
languageId: "x86:LE:32:default",
|
|
compilerSpecId: "default",
|
|
baseAddr: "0x1000",
|
|
},
|
|
},
|
|
])
|
|
expect(() =>
|
|
ghidraHeadlessArguments({ ...base, mipsSoftFloat: true, ...conflict }),
|
|
).toThrow(/generic MIPS ELF/u);
|
|
});
|
|
});
|
|
|
|
describe("Ghidra configured import language and seeds", () => {
|
|
const base = {
|
|
platform: "darwin" as const,
|
|
projectRoot: "/tmp/project",
|
|
targetPath: "/tmp/target",
|
|
bridgeScriptPath: "/package/bridge/ReaGhidraBridge.java",
|
|
descriptorPath: "/tmp/session.json",
|
|
ghidraLogPath: "/tmp/ghidra.log",
|
|
scriptLogPath: "/tmp/script.log",
|
|
};
|
|
|
|
it("passes a configured language and compiler spec to the import", () => {
|
|
const arguments_ = ghidraHeadlessArguments({
|
|
...base,
|
|
languageOverride: {
|
|
languageId: "x86:LE:32:default",
|
|
compilerSpecId: "borlandcpp",
|
|
},
|
|
});
|
|
expectOptions(arguments_, [
|
|
["-processor", "x86:LE:32:default"],
|
|
["-cspec", "borlandcpp"],
|
|
]);
|
|
expect(arguments_).not.toContain("-loader");
|
|
expect(arguments_.indexOf("-processor")).toBeGreaterThan(
|
|
arguments_.indexOf("-import"),
|
|
);
|
|
});
|
|
|
|
it("lets Ghidra choose the language default compiler spec", () => {
|
|
const arguments_ = ghidraHeadlessArguments({
|
|
...base,
|
|
languageOverride: { languageId: "x86:LE:32:default" },
|
|
});
|
|
expectOptions(arguments_, [["-processor", "x86:LE:32:default"]]);
|
|
expect(arguments_).not.toContain("-cspec");
|
|
});
|
|
|
|
it("never combines a configured language with a DOS import", () => {
|
|
expect(() =>
|
|
ghidraHeadlessArguments({
|
|
...base,
|
|
dosMz: true,
|
|
languageOverride: { languageId: "x86:LE:32:default" },
|
|
}),
|
|
).toThrow(/cannot be combined with a DOS import/u);
|
|
});
|
|
|
|
it("never combines a configured language with a raw-binary import", () => {
|
|
expect(() =>
|
|
ghidraHeadlessArguments({
|
|
...base,
|
|
languageOverride: { languageId: "x86:LE:32:default" },
|
|
rawBinary: {
|
|
languageId: "ARM:LE:32:v7",
|
|
compilerSpecId: "default",
|
|
baseAddr: "0x8000000",
|
|
},
|
|
}),
|
|
).toThrow(/cannot be combined with a raw-binary import/u);
|
|
});
|
|
|
|
it.each(["darwin", "win32"] as const)(
|
|
"applies seeds after loader preparation and before analysis scripts on %s",
|
|
(platform) => {
|
|
const path = platform === "win32" ? win32 : posix;
|
|
const bridgeDirectory = path.join("/package", "bridge");
|
|
const seedPath = path.join("/tmp/runtime/seeds", "seeds.tsv");
|
|
const arguments_ = ghidraHeadlessArguments({
|
|
...base,
|
|
platform,
|
|
bridgeScriptPath: path.join(bridgeDirectory, "ReaGhidraBridge.java"),
|
|
dosCom: true,
|
|
seedPath,
|
|
});
|
|
const prepare = arguments_.indexOf(
|
|
path.join(bridgeDirectory, "ReaGhidraPrepareCom.java"),
|
|
);
|
|
const seeds = arguments_.indexOf(
|
|
path.join(bridgeDirectory, "ReaGhidraApplySeeds.java"),
|
|
);
|
|
expect(prepare).toBeGreaterThanOrEqual(0);
|
|
expect(arguments_[seeds - 1]).toBe("-preScript");
|
|
expect(arguments_[seeds + 1]).toBe(seedPath);
|
|
expect(prepare).toBeLessThan(seeds);
|
|
expect(seeds).toBeLessThan(arguments_.indexOf("-postScript"));
|
|
},
|
|
);
|
|
|
|
it("adds no seed script without a seed snapshot", () => {
|
|
expect(ghidraHeadlessArguments(base)).not.toContain(
|
|
posix.join("/package/bridge", "ReaGhidraApplySeeds.java"),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Ghidra disabled analyzers", () => {
|
|
const base = {
|
|
platform: "darwin" as const,
|
|
projectRoot: "/tmp/project",
|
|
targetPath: "/tmp/target",
|
|
bridgeScriptPath: "/package/bridge/ReaGhidraBridge.java",
|
|
descriptorPath: "/tmp/session.json",
|
|
ghidraLogPath: "/tmp/ghidra.log",
|
|
scriptLogPath: "/tmp/script.log",
|
|
};
|
|
|
|
it("applies the analyzer options after seeds and before analysis scripts", () => {
|
|
const arguments_ = ghidraHeadlessArguments({
|
|
...base,
|
|
dosCom: true,
|
|
seedPath: posix.join("/tmp/runtime/seeds", "seeds.tsv"),
|
|
disabledAnalyzers: [
|
|
"Objective-C Message Analyzer",
|
|
"Decompiler Switch Analysis",
|
|
],
|
|
});
|
|
const options = arguments_.indexOf(
|
|
posix.join("/package/bridge", "ReaGhidraAnalysisOptions.java"),
|
|
);
|
|
expect(arguments_[options - 1]).toBe("-preScript");
|
|
expect(arguments_[options + 1]).toBe(
|
|
"Objective-C Message Analyzer,Decompiler Switch Analysis",
|
|
);
|
|
const prepare = arguments_.indexOf(
|
|
posix.join("/package/bridge", "ReaGhidraPrepareCom.java"),
|
|
);
|
|
const seeds = arguments_.indexOf(
|
|
posix.join("/package/bridge", "ReaGhidraApplySeeds.java"),
|
|
);
|
|
expect(prepare).toBeLessThan(seeds);
|
|
expect(seeds).toBeLessThan(options);
|
|
expect(options).toBeLessThan(arguments_.indexOf("-postScript"));
|
|
});
|
|
|
|
it.each(["darwin", "win32"] as const)(
|
|
"uses the target platform's script path for the options script on %s",
|
|
(platform) => {
|
|
const path = platform === "win32" ? win32 : posix;
|
|
const arguments_ = ghidraHeadlessArguments({
|
|
...base,
|
|
platform,
|
|
bridgeScriptPath: path.join(
|
|
"/package",
|
|
"bridge",
|
|
"ReaGhidraBridge.java",
|
|
),
|
|
disabledAnalyzers: ["Objective-C Message Analyzer"],
|
|
});
|
|
const options = arguments_.indexOf(
|
|
path.join("/package", "bridge", "ReaGhidraAnalysisOptions.java"),
|
|
);
|
|
expect(arguments_[options - 1]).toBe("-preScript");
|
|
expect(arguments_[options + 1]).toBe("Objective-C Message Analyzer");
|
|
expect(options).toBeLessThan(arguments_.indexOf("-postScript"));
|
|
},
|
|
);
|
|
|
|
it("keeps the invocation byte-identical without an override", () => {
|
|
expect(ghidraHeadlessArguments(base)).toEqual([
|
|
"/tmp/project",
|
|
"rea-project",
|
|
"-import",
|
|
"/tmp/target",
|
|
"-readOnly",
|
|
"-deleteProject",
|
|
"-log",
|
|
"/tmp/ghidra.log",
|
|
"-scriptlog",
|
|
"/tmp/script.log",
|
|
"-scriptPath",
|
|
"/package/bridge",
|
|
"-postScript",
|
|
"/package/bridge/ReaGhidraNoReturnFix.java",
|
|
"-postScript",
|
|
"/package/bridge/ReaGhidraBridge.java",
|
|
"/tmp/session.json",
|
|
]);
|
|
});
|
|
|
|
it("adds no analyzer script for an empty override", () => {
|
|
expect(
|
|
ghidraHeadlessArguments({ ...base, disabledAnalyzers: [] }),
|
|
).not.toContain(
|
|
posix.join("/package/bridge", "ReaGhidraAnalysisOptions.java"),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Ghidra Windows launcher", () => {
|
|
it("keeps analysis repair outside Windows P0 authority", () => {
|
|
const args = ghidraHeadlessArguments({
|
|
platform: "win32",
|
|
projectRoot: "/tmp/project",
|
|
targetPath: "/tmp/target",
|
|
bridgeScriptPath: "/package/bridge/ReaGhidraBridge.java",
|
|
descriptorPath: "/tmp/session.json",
|
|
ghidraLogPath: "/tmp/ghidra.log",
|
|
scriptLogPath: "/tmp/script.log",
|
|
});
|
|
expect(args).not.toContain("/package/bridge/ReaGhidraNoReturnFix.java");
|
|
expect(args).toContain("/package/bridge/ReaGhidraBridge.java");
|
|
});
|
|
|
|
it("wraps the Windows batch launcher without enabling a Node shell", () => {
|
|
const command = ghidraHeadlessCommand({
|
|
environment: {},
|
|
platform: "win32",
|
|
analyzeHeadlessPath:
|
|
"C:\\Program Files\\Ghidra 12.1.4\\support\\analyzeHeadless.bat",
|
|
arguments: [
|
|
"C:\\REA Runtime\\project",
|
|
"rea-project",
|
|
"-import",
|
|
"C:\\REA Runtime\\target.exe",
|
|
],
|
|
comSpec: "C:\\Windows\\System32\\cmd.exe",
|
|
});
|
|
|
|
expect(command).toEqual({
|
|
command: "C:\\Windows\\System32\\cmd.exe",
|
|
arguments: [
|
|
"/d",
|
|
"/e:on",
|
|
"/v:off",
|
|
"/s",
|
|
"/c",
|
|
'""C:\\Program Files\\Ghidra 12.1.4\\support\\analyzeHeadless.bat" "C:\\REA Runtime\\project" "rea-project" "-import" "C:\\REA Runtime\\target.exe""',
|
|
],
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
"C:\\targets\\%TEMP%\\fixture.exe",
|
|
"C:\\targets\\fixture & calc.exe",
|
|
'C:\\targets\\fixture".exe',
|
|
])("rejects a Windows command-interpreter path: %s", (targetPath) => {
|
|
expect(() =>
|
|
ghidraHeadlessCommand({
|
|
environment: {},
|
|
platform: "win32",
|
|
analyzeHeadlessPath: "C:\\Ghidra\\support\\analyzeHeadless.bat",
|
|
arguments: ["-import", targetPath],
|
|
comSpec: "C:\\Windows\\System32\\cmd.exe",
|
|
}),
|
|
).toThrow(/metacharacters/u);
|
|
});
|
|
|
|
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");
|
|
vi.stubEnv("_JAVA_OPTIONS", "-Xmx99G");
|
|
const parent = await createTestTempDirectory("rea-launcher-test-");
|
|
const runtime = await createGhidraTestRuntime(parent);
|
|
runtimes.push(runtime);
|
|
const runtimeRoot = runtime.path;
|
|
const token = "secret-token-that-must-not-leak";
|
|
const javaHome = "C:\\Java\\jdk-21";
|
|
const launcher = new GhidraHeadlessLauncher({
|
|
// Select the fixture's Node PATH and injected JVM options explicitly.
|
|
environment: process.env,
|
|
analyzeHeadlessPath: fixturePath,
|
|
// 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",
|
|
});
|
|
const launched = await launcher.launch({
|
|
runtimeRoot,
|
|
transport: "unix-socket",
|
|
endpointPath: join(runtimeRoot, "bridge.sock"),
|
|
token,
|
|
runId: "d6fcbb66-e829-4ff6-a535-0035aec63139",
|
|
targetPath: "/tmp/fixture",
|
|
targetSha256: "b".repeat(64),
|
|
providerVersion: "12.1.4",
|
|
profileDigest: "a".repeat(64),
|
|
});
|
|
if (!launched.ok) throw launched.error;
|
|
try {
|
|
const capturePath = join(runtimeRoot, "launch-capture.json");
|
|
await vi.waitFor(() => access(`${capturePath}.ready`), {
|
|
timeout: 10_000,
|
|
});
|
|
const capture = launchCaptureSchema.parse(
|
|
JSON.parse(await readFile(capturePath, "utf8")),
|
|
);
|
|
const encodedArguments = JSON.stringify(capture.arguments);
|
|
const encodedEnvironment = JSON.stringify(capture.environment);
|
|
expect(encodedArguments).not.toContain(token);
|
|
expect(encodedEnvironment).not.toContain(token);
|
|
expect(capture).toMatchObject({
|
|
...(process.platform === "win32" ? {} : { descriptor_mode: 0o600 }),
|
|
descriptor_has_token: true,
|
|
});
|
|
expectIsolatedEnvironment(capture.environment, runtimeRoot, javaHome);
|
|
expect(capture.environment.GHIDRA_HEADLESS_JAVA_OPTIONS).toBe("");
|
|
if (process.platform !== "win32")
|
|
expect(
|
|
(await stat(join(runtimeRoot, "ownership.json"))).mode & 0o777,
|
|
).toBe(0o600);
|
|
} finally {
|
|
const cleaned = await launched.value.cleanup?.();
|
|
expect(cleaned).toMatchObject({ cleaned: true });
|
|
await expect(
|
|
access(join(runtimeRoot, "project")),
|
|
).resolves.toBeUndefined();
|
|
}
|
|
},
|
|
);
|
|
});
|