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:
parent
5486860673
commit
a8b8288396
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
};
|
||||
Loading…
Reference in New Issue