Files
reverse-engineering-anything/src/process/PrivateRuntimeRoot.ts
T
Kaoru0822andClaude Opus 5.5 b79d379c25 fix(ghidra): keep and name a runtime directory that close could not remove
When removing Ghidra's private runtime directory failed, close_binary
reported only a generic `provider-client` resource, and a later close,
after the permission was restored, returned success while the directory
and its contents remained. The client dropped its reference to the root
before removing it, and PrivateRuntimeRoot cached the rejected removal.

Keep each runtime or socket root until its removal succeeds, report a
failure as a ProviderCleanupError naming the leftover paths, let
PrivateRuntimeRoot retry a failed removal, and pass such typed cleanup
failures through closeAnalysisClient instead of replacing them.

Fixes #1190

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 04:19:10 +00:00

105 lines
3.5 KiB
TypeScript

import { chmod, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { WindowsPrivateRuntime } from "../windows/WindowsPrivateRuntime.js";
import {
windowsNativeCapabilities,
type WindowsNativeCapability,
} from "./WindowsAuthority.js";
/** Filesystem coordinates for one provider-owned private runtime directory. */
export interface PrivateRuntimeRootOptions {
readonly parent?: string;
readonly prefix?: string;
/** Platform override used by deterministic boundary tests. */
readonly platform?: NodeJS.Platform;
}
/** Provider-neutral security status for one runtime-root implementation. */
export type PrivateRuntimeRootCapability =
| WindowsNativeCapability
| {
readonly available: true;
readonly reason: null;
readonly proof: "posix-mode-0700";
};
/** Report whether this process can establish a private runtime root. */
export const privateRuntimeRootCapability = (
platform: NodeJS.Platform = process.platform,
): PrivateRuntimeRootCapability =>
platform === "win32"
? windowsNativeCapabilities(platform).private_runtime_dacl
: { available: true, reason: null, proof: "posix-mode-0700" };
/** Expected failure when a runtime root's privacy boundary is unavailable. */
export class PrivateRuntimeRootUnavailableError extends Error {
readonly code = "private-runtime-root-authority-unavailable";
constructor(message: string) {
super(message);
this.name = "PrivateRuntimeRootUnavailableError";
}
}
/**
* Owns a private temporary runtime root and removes it idempotently.
*
* Protocol adapters decide what belongs inside the directory; this primitive
* establishes a private filesystem boundary on POSIX and deterministic cleanup.
* Windows privacy requires the separately verified native DACL boundary.
*/
export class PrivateRuntimeRoot {
#closePromise: Promise<void> | undefined;
private constructor(
readonly path: string,
private readonly windowsRuntime?: WindowsPrivateRuntime,
) {}
/** Allocate a new private runtime root without performing protocol work. */
static async create(
options: PrivateRuntimeRootOptions = {},
): Promise<PrivateRuntimeRoot> {
const platform = options.platform ?? process.platform;
const capability = privateRuntimeRootCapability(platform);
if (!capability.available)
throw new PrivateRuntimeRootUnavailableError(capability.reason);
if (platform === "win32") {
const runtime = WindowsPrivateRuntime.create(
options.parent ?? tmpdir(),
options.prefix ?? "rea-provider-",
);
return new PrivateRuntimeRoot(runtime.observation.path, runtime);
}
const path = await mkdtemp(
join(options.parent ?? tmpdir(), options.prefix ?? "rea-provider-"),
);
try {
await chmod(path, 0o700);
return new PrivateRuntimeRoot(path);
} catch (cause: unknown) {
await rm(path, { recursive: true, force: true });
throw cause;
}
}
/**
* Remove the runtime root; concurrent callers share one attempt. A failed
* attempt is not cached, so a later close retries the removal.
*/
close(): Promise<void> {
this.#closePromise ??= (
this.windowsRuntime === undefined
? rm(this.path, { recursive: true, force: true })
: Promise.resolve().then(() => this.windowsRuntime?.close())
).catch((cause: unknown) => {
this.#closePromise = undefined;
throw cause;
});
return this.#closePromise;
}
}