fix: keep the Ghidra session root free of dot-prefixed path elements

Ghidra validates every project path element with NamingUtilities.checkName and aborts the headless analyzer on any element starting with '.', so an inherited TMPDIR such as ~/.cache/<tool> failed every Ghidra operation with a generic analysis failure. Select the session root through a helper that prefers the inherited base, then a dot-free platform temp directory, then the nearest dot-free ancestor.
This commit is contained in:
felipeatp 2026-10-09 04:26:40 -03:00 committed by N0zoM1z0
parent 5486860673
commit a8b8288396
3 changed files with 118 additions and 2 deletions

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";
@ -47,9 +46,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();
export type {
GhidraClientOptions,

View File

@ -0,0 +1,61 @@
import { join, 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(sep, "tmp");
const posixBase = (...segments: string[]) => join(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.skipIf(!POSIX)("prefers the platform temp directory by default", () => {
expect(
ghidraSessionRoot({ base: posixBase("home", "operator", ".cache") }),
).toBe(posixBase("tmp"));
});
});

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;
};