refactor(apple): normalize host paths at the filesystem boundary

This commit is contained in:
morluto committed 2026-10-08 19:09:40 +08:00
1 parent fe3cac8aa8
commit fb999f84b8
4 files changed
+103 -9

No files matched your search

+9 -2
View File
@@ -17,6 +17,10 @@ import {
import { ArtifactReaderFailure } from "../ArtifactReader.js";
import { DirectoryArtifactReader } from "../DirectoryArtifactReader.js";
import { DyldSharedCache } from "./DyldSharedCacheReader.js";
import {
filesystemDyldLinkTarget,
filesystemDyldLookupPath,
} from "./FilesystemDyldPaths.js";
import { hasMachoMagic, readMachoImage } from "./MachoLoadCommandReader.js";
const HASH_CHUNK_BYTES = 1024 * 1024;
@@ -63,11 +67,14 @@ class FilesystemTreeView implements DylibTreeView {
async #readEntry(path: string): Promise<DylibTreeEntry | undefined> {
cancelled(this.signal);
const absolute = join(this.root, path);
const absolute = filesystemDyldLookupPath(this.root, path);
try {
const metadata = await lstat(absolute);
if (metadata.isSymbolicLink())
return { kind: "symlink", target: await readlink(absolute) };
return {
kind: "symlink",
target: filesystemDyldLinkTarget(await readlink(absolute)),
};
if (metadata.isDirectory()) return { kind: "directory" };
return metadata.isFile() ? { kind: "file" } : undefined;
} catch (cause: unknown) {
@@ -0,0 +1,53 @@
import { expect, it } from "vitest";
import { resolveTreePath } from "../../domain/apple/dyldPaths.js";
import type { DylibTreeEntry } from "../../domain/apple/dylibResolution.js";
import {
filesystemDyldLinkTarget,
filesystemDyldLookupPath,
} from "./FilesystemDyldPaths.js";
it("normalizes Windows relative links before containment checks without probing an escaped path", async () => {
const requested: string[] = [];
const result = await resolveTreePath(
{
entry: async (path: string): Promise<DylibTreeEntry | undefined> => {
requested.push(path);
return path === "link"
? {
kind: "symlink",
target: filesystemDyldLinkTarget("..\\outside.dylib", "win32"),
}
: undefined;
},
image: async () => ({ status: "not-mach-o" }),
},
"link",
);
expect(result).toEqual({ kind: "escapes" });
expect(requested).toEqual(["link"]);
});
it("keeps POSIX backslash bytes and preserves Windows dot segments for segment-wise traversal", () => {
expect(filesystemDyldLinkTarget("..\\outside.dylib", "darwin")).toBe(
"..\\outside.dylib",
);
expect(filesystemDyldLinkTarget("nested\\..\\target", "win32")).toBe(
"nested/../target",
);
expect(filesystemDyldLinkTarget("C:\\outside", "win32")).toBe("/C:/outside");
expect(filesystemDyldLinkTarget("C:/literal-posix-directory", "darwin")).toBe(
"C:/literal-posix-directory",
);
expect(() => filesystemDyldLinkTarget("C:drive-relative", "win32")).toThrow(
"Windows drive working directory",
);
});
it("does not let Windows reinterpret a literal backslash-bearing loader name as traversal", () => {
expect(() =>
filesystemDyldLookupPath("C:\\root", "..\\outside.dylib", "win32"),
).toThrow("cannot be represented on Windows");
expect(
filesystemDyldLookupPath("/root", "literal\\name.dylib", "darwin"),
).toBe("/root/literal\\name.dylib");
});
@@ -0,0 +1,39 @@
import { posix, win32 } from "node:path";
import { ArtifactReaderFailure } from "../ArtifactReader.js";
/**
* Convert host filesystem separators into the tree view's POSIX traversal
* namespace. Dot segments stay intact so they apply after symlink expansion;
* POSIX backslash filename bytes are never reinterpreted as separators.
*/
export const filesystemDyldLinkTarget = (
reported: string,
platform: NodeJS.Platform = process.platform,
): string => {
if (platform !== "win32") return reported;
const portable = reported.replaceAll("\\", "/");
if (win32.isAbsolute(reported))
return portable.startsWith("/") ? portable : `/${portable}`;
if (/^[A-Za-z]:/u.test(reported))
throw new ArtifactReaderFailure(
"unavailable",
`Drive-relative symlink target depends on a Windows drive working directory: ${reported}`,
);
return portable;
};
/** Map a portable lookup to the host without allowing extra native separators to escape it. */
export const filesystemDyldLookupPath = (
root: string,
lookup: string,
platform: NodeJS.Platform = process.platform,
): string => {
if (platform === "win32" && lookup.includes("\\"))
throw new ArtifactReaderFailure(
"unavailable",
`Literal backslashes in a POSIX dyld lookup cannot be represented on Windows: ${lookup}. Analyze this tree on a POSIX host.`,
);
return platform === "win32"
? win32.join(root, lookup)
: posix.join(root, lookup);
};
+2 -7
View File
@@ -37,13 +37,8 @@ export const resolveTreePath = async (
if (entry.kind === "symlink") {
hops += 1;
if (hops > MAX_SYMLINK_HOPS) return { kind: "absent" };
if (
entry.target.startsWith("/") ||
/^[a-zA-Z]:[\\/]/u.test(entry.target) ||
entry.target.startsWith("\\\\") ||
entry.target.startsWith("//")
)
return { kind: "escapes" };
// Tree providers convert host link syntax to this POSIX namespace.
if (entry.target.startsWith("/")) return { kind: "escapes" };
pending.unshift(...segmentsOf(entry.target));
continue;
}