fix(providers): preserve selected filesystem identities

This commit is contained in:
morluto committed 2026-10-27 09:31:17 +08:00
1 parent 9eb8ba2b29
commit 9be640a8b4
4 files changed
+106 -2

No files matched your search

+15 -1
View File
@@ -210,7 +210,21 @@ const resolveInside = async (
nodes.push({ path: absolute, metadata });
hops += 1;
if (hops > MAX_SYMLINK_HOPS) return { kind: "loop" };
const target = await readlink(absolute);
const rawTarget = await readlink(absolute, { encoding: "buffer" });
let target: string;
try {
// A replacement character can alias a different, valid sibling name.
// Preserve a literal UTF-8 BOM too: it is part of a filesystem name.
target = new TextDecoder("utf-8", {
fatal: true,
ignoreBOM: true,
}).decode(rawTarget);
} catch {
throw new ElectronFusePathFailure(
"invalid_value",
`Framework symlink target is not valid UTF-8 (raw target bytes: ${rawTarget.toString("hex")}): ${absolute}`,
);
}
// Lexical only: another alias of this root (/tmp vs /private/tmp)
// is still an escape. Later hops stay lstat- and containment-checked.
const absoluteTarget = isAbsolute(target);
+31
View File
@@ -41,6 +41,37 @@ const inspect = async (path: string) => {
return result.value;
};
it.skipIf(process.platform === "win32")(
"rejects undecodable symlink targets rather than selecting a replacement sibling",
async () => {
const root = await createTestTempDirectory("rea-electron-link-encoding-");
const framework = join(root, "Electron Framework.framework");
await mkdir(framework);
await writeFile(join(framework, "\uFFFD"), thinFuseImage([0x31]).bytes);
const alias = join(framework, "alias");
await symlink(Buffer.from([0xff]), alias);
await expect(stat(alias)).rejects.toMatchObject({ code: "ENOENT" });
const result = await provider.inspect({ path: alias });
expect(result.ok).toBe(false);
if (!result.ok)
expect(result.error).toMatchObject({
issues: [
{
reason: "invalid_value",
message: expect.stringContaining("not valid UTF-8"),
},
],
});
const validName = "\uFEFFbinary";
await writeFile(join(framework, validName), thinFuseImage([0x31]).bytes);
const validAlias = join(framework, "valid-alias");
await symlink(validName, validAlias);
expect(
(await inspect(validAlias)).slices[0]?.sentinels[0]?.fuses[0]?.state,
).toBe("enabled");
},
);
it.skipIf(process.platform === "win32")(
"rejects framework links whose terminal separator requires a directory",
async () => {
+7
View File
@@ -167,6 +167,13 @@ export const resolveGhidraProject = async (
"resolve_analysis_profile",
async () => {
assertArtifactReadActive(signal);
// Admit the caller's exact marker before canonicalizing its parent.
// Darwin realpath(file/..) can succeed despite native lookup rejecting
// that traversal; hashing the canonical sibling would select a new input.
if (!(await lstat(selection.path)).isFile())
throw selectionFailure(
`Existing Ghidra project marker must be a regular file: ${selection.path}`,
);
const projectName = basename(selection.path, ".gpr");
const projectPath = await realpath(dirname(selection.path));
const program = ghidraProgramNameSchema.safeParse(
@@ -1,4 +1,12 @@
import { mkdir, readFile, readdir, symlink, writeFile } from "node:fs/promises";
import {
mkdir,
readFile,
readdir,
realpath,
stat,
symlink,
writeFile,
} from "node:fs/promises";
import { join } from "node:path";
import { afterEach, beforeEach, expect, it } from "vitest";
import { ArtifactResourceScope } from "../../../../src/artifacts/ArtifactResourceScope.js";
@@ -170,3 +178,47 @@ it.each([null, "caller stopped", { kind: "stop-project-read" }])(
).rejects.toMatchObject({ _tag: "AnalysisCancelledError", cause: reason });
},
);
it.skipIf(process.platform === "win32")(
"rejects a selected marker with a non-directory traversal before canonicalizing its parent",
async () => {
const root = await createTestTempDirectory("rea-ghidra-project-traversal-");
await writeFile(join(root, "file"), "not a directory");
await writeFile(join(root, "project.gpr"), "marker");
await mkdir(join(root, "project.rep"));
const path = `${root}/file/../project.gpr`;
await expect(stat(path)).rejects.toMatchObject({ code: "ENOTDIR" });
await expect(
resolveGhidraProject({ path }, { path: join(root, "target") }, resources),
).rejects.toMatchObject({ diagnostics: { system_code: "ENOTDIR" } });
},
);
it.skipIf(process.platform === "win32")(
"follows native parent symlinks and preserves missing marker failures",
async () => {
const root = await createTestTempDirectory("rea-ghidra-project-selection-");
await mkdir(join(root, "actual", "nested"), { recursive: true });
await symlink("actual/nested", join(root, "link"));
await writeFile(join(root, "actual", "project.gpr"), "marker");
await mkdir(join(root, "actual", "project.rep"));
const path = `${root}/link/../project.gpr`;
const commitment = await resolveGhidraProject(
{ path },
{ path: join(root, "target") },
resources,
);
expect(commitment).toMatchObject({
configured_path: path,
project_path: await realpath(join(root, "actual")),
project_name: "project",
});
await expect(
resolveGhidraProject(
{ path: join(root, "missing.gpr") },
{ path: join(root, "target") },
resources,
),
).rejects.toMatchObject({ diagnostics: { system_code: "ENOENT" } });
},
);