Merge pull request #1297 from morluto/fix/ghidra-session-root-stability

fix(ghidra): use dot-free private session paths
This commit is contained in:
Kaoru0822 authored and GitHub committed 2026-10-09 07:59:06 -07:00
commit c3fc6c3aed
5 files changed
+140 -3

No files matched your search

+9
View File
@@ -25,6 +25,15 @@ const environment = () => ({
...(process.env.JAVA_HOME === undefined
? {}
: { JAVA_HOME: process.env.JAVA_HOME }),
...(process.env.GHIDRA_HEADLESS_MAXMEM === undefined
? {}
: { GHIDRA_HEADLESS_MAXMEM: process.env.GHIDRA_HEADLESS_MAXMEM }),
...(process.env.GHIDRA_MAXMEM === undefined
? {}
: { GHIDRA_MAXMEM: process.env.GHIDRA_MAXMEM }),
...(process.env._JAVA_OPTIONS === undefined
? {}
: { _JAVA_OPTIONS: process.env._JAVA_OPTIONS }),
});
const verifyGraph = (input, target, procedure, globalAddress) => {
+4 -1
View File
@@ -89,9 +89,12 @@ export async function verifyGhidraLargeResults({
length,
});
const readId = retainedReference(readError, "read_bytes");
// Two annotation strings repeated in text and structured content must
// exceed the receive budget even without obsolete Evidence envelopes.
// The request contains only the two strings and remains below that budget.
const comment =
"REA_BIG_COMMENT:" +
"x".repeat(Math.ceil(STDIO_DEFAULT_MAX_BUFFER_SIZE / 10));
"x".repeat(Math.ceil(STDIO_DEFAULT_MAX_BUFFER_SIZE / 4));
const annotationError = await reject("annotate_native_function", {
procedure: entry.address,
name: "rea_frame_probe",
+2 -2
View File
@@ -1,6 +1,5 @@
import { randomBytes, randomUUID } from "node:crypto";
import type { Socket } from "node:net";
import { tmpdir } from "node:os";
import { AnalysisError } from "../domain/analysisErrorBase.js";
import type { JsonValue } from "../domain/jsonValue.js";
@@ -48,9 +47,10 @@ import {
} from "./GhidraTransport.js";
import { GhidraWire } from "./GhidraClientWire.js";
import { completeGhidraStartupHandshake } from "./GhidraClientStartup.js";
import { ghidraSessionRoot } from "./GhidraSessionRoot.js";
const SHUTDOWN_TIMEOUT_MS = 10_000;
const SESSION_ROOT = tmpdir();
const SESSION_ROOT = ghidraSessionRoot();
/** Closed Java-bridge operation union callable after the exact handshake. */
export type GhidraOperation =
+70
View File
@@ -0,0 +1,70 @@
import { join, resolve, sep } from "node:path";
import { describe, expect, it } from "vitest";
import { ghidraSessionRoot } from "./GhidraSessionRoot.js";
const hasDotPrefixElement = (directory: string): boolean =>
directory.split(sep).some((element) => element.startsWith("."));
const POSIX = process.platform !== "win32";
const posixFallback = join(resolve(sep), "tmp");
const posixBase = (...segments: string[]) => join(resolve(sep), ...segments);
describe("ghidraSessionRoot", () => {
it("keeps an inherited base that Ghidra accepts", () => {
const base = posixBase("var", "tmp", "rea-run");
expect(ghidraSessionRoot({ base, fallback: posixFallback })).toBe(base);
});
it("keeps a base whose elements only contain an interior dot", () => {
const base = posixBase("opt", "rea.run", "session");
expect(ghidraSessionRoot({ base, fallback: posixFallback })).toBe(base);
});
it("relocates a dot-prefixed base to the supplied dot-free fallback", () => {
const base = posixBase("home", "operator", ".cache", "scratch");
expect(ghidraSessionRoot({ base, fallback: posixFallback })).toBe(
posixFallback,
);
});
it("relocates a base that is itself a hidden directory", () => {
const base = posixBase("home", "operator", ".tmp");
expect(ghidraSessionRoot({ base, fallback: posixBase("var", "tmp") })).toBe(
posixBase("var", "tmp"),
);
});
it("ignores a fallback that is itself dotted", () => {
const base = posixBase("home", "operator", ".cache", "scratch");
expect(
ghidraSessionRoot({
base,
fallback: posixBase("home", "operator", ".tmp"),
}),
).toBe(posixBase("home", "operator"));
});
it("never returns a path with a dot-prefixed element", () => {
const directory = ghidraSessionRoot({
base: posixBase("home", "operator", ".cache", "scratch"),
fallback: posixBase("home", "operator", ".cache"),
});
expect(hasDotPrefixElement(directory)).toBe(false);
});
it("uses the nearest safe ancestor when the platform has no fallback", () => {
expect(
ghidraSessionRoot({
base: posixBase("Users", "operator", ".cache", "scratch"),
platform: "win32",
}),
).toBe(posixBase("Users", "operator"));
});
it.skipIf(!POSIX)("prefers the platform temp directory by default", () => {
expect(
ghidraSessionRoot({ base: posixBase("home", "operator", ".cache") }),
).toBe(posixBase("tmp"));
});
});
+55
View File
@@ -0,0 +1,55 @@
import { dirname, isAbsolute, resolve, sep } from "node:path";
import { tmpdir } from "node:os";
/** Session roots may not contain a dot-prefixed path element. */
const hasDotPrefixElement = (directory: string): boolean =>
resolve(directory)
.split(sep)
.some((element) => element.startsWith("."));
/** Nearest ancestor of `directory`, inclusive, that has no dot-prefixed element. */
const nearestSafeAncestor = (directory: string): string | undefined => {
let current = resolve(directory);
for (;;) {
if (!hasDotPrefixElement(current)) return current;
const parent = dirname(current);
if (parent === current) return undefined;
current = parent;
}
};
/** Coordinates accepted by {@link ghidraSessionRoot}. */
export interface GhidraSessionRootOptions {
/** Inherited base directory, normally `os.tmpdir()`. */
readonly base?: string;
/** Platform-specific dot-free fallback directory passed to `mkdtemp`. */
readonly fallback?: string;
/** Platform override used by deterministic boundary tests. */
readonly platform?: NodeJS.Platform;
}
/**
* Select a session root usable as the parent of a Ghidra project path.
*
* Ghidra validates every project path element with `NamingUtilities.checkName`
* and aborts the headless analyzer on any element that starts with `.`, so an
* inherited `TMPDIR` such as `~/.cache/<tool>` fails every Ghidra operation
* with a generic analysis failure. Prefer the inherited base when it is usable,
* then a dot-free platform temp directory, then the nearest dot-free ancestor.
*/
export const ghidraSessionRoot = (
options: GhidraSessionRootOptions = {},
): string => {
const base = resolve(options.base ?? tmpdir());
if (!hasDotPrefixElement(base)) return base;
const platform = options.platform ?? process.platform;
const fallback =
options.fallback ?? (platform === "win32" ? undefined : "/tmp");
if (
fallback !== undefined &&
isAbsolute(fallback) &&
!hasDotPrefixElement(fallback)
)
return resolve(fallback);
return nearestSafeAncestor(base) ?? base;
};