fix(ida): canonicalize the headless workspace root before opening (#1481)

On macOS the default workspace root is os.tmpdir() under /var/folders,
and /var is a symlink to /private/var. idalib-mcp reports the canonical
input path from idb_open/idb_list, while REA compared it against the
uncanonicalized private copy with normalize(), so every headless IDA
analysis failed its identity check (surfacing as unreadable_output) and
the spawned idalib worker was never released.

Resolve the workspace root with realpath() when allocating the private
workspace so the input path REA sends already matches what upstream
reports.

Co-authored-by: Xingchuan Ma <xingchuan.ma@msn.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Max 2026-10-10 21:39:33 +08:00 committed by GitHub
parent 24d9337649
commit de619bd334
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 27 additions and 3 deletions

View File

@ -1,6 +1,6 @@
import { createHash, randomUUID } from "node:crypto";
import { createReadStream, constants } from "node:fs";
import { copyFile } from "node:fs/promises";
import { copyFile, realpath } from "node:fs/promises";
import { tmpdir } from "node:os";
import { basename, join, normalize } from "node:path";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
@ -39,8 +39,10 @@ export class IdaWorkspace {
target: BinaryTarget,
root = tmpdir(),
): Promise<IdaWorkspace> {
// Upstream reports canonical paths (macOS /var -> /private/var), so the
// private input path must already be canonical for identity checks.
const runtime = await PrivateRuntimeRoot.create({
parent: normalize(root),
parent: await realpath(normalize(root)),
prefix: "rea-ida-",
});
const inputPath = join(runtime.path, basename(target.path));

View File

@ -1,9 +1,11 @@
import { chmod, readdir, rm } from "node:fs/promises";
import { chmod, readdir, realpath, rm, symlink } from "node:fs/promises";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
createIdaTarget,
RecordingIdaMcp,
} from "../../tests/fixtures/idaMcp.js";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import { IdaSessionClient } from "./IdaSessionClient.js";
@ -33,6 +35,26 @@ const clientFor = (root: string, target: BinaryTarget) => {
};
describe("IDA workspace ownership before input preparation", () => {
it.skipIf(process.platform === "win32")(
"opens the private input through a canonical path when the workspace root is a symlink",
async () => {
const { root, target } = await createIdaTarget();
roots.push(root);
const linked = join(
await createTestTempDirectory("rea-ida-link-"),
"root",
);
await symlink(root, linked);
const { client, producer } = clientFor(linked, target);
expect((await client.execute("health", {})).ok).toBe(true);
const open = producer.calls.find(({ name }) => name === "idb_open");
expect(
String(open?.args.input_path).startsWith(`${await realpath(root)}/`),
).toBe(true);
expect((await client.close()).ok).toBe(true);
},
);
it("retains cleanup ownership when copying the admitted input fails", async () => {
const { root, target } = await createIdaTarget();
roots.push(root);