mirror of
https://github.com/morluto/rea.git
synced 2026-11-09 16:26:36 +00:00
fix(apple): resolve absolute dylib paths inside the selected root
Preserve load-command paths as evidence while passing a separate root-relative lookup path to the bounded tree resolver. Apply the same containment rule to absolute install names and rpath bases, including caller-selected root symlink spellings. Validation: 140 source tests, full check:ci, docs and runtime build passed. Compiler-built literal, rpath and relative fixtures passed CLI/stdio MCP parity, preserved both dependency edges and left all bytes unchanged. External system paths stayed outside-target; relative control was unchanged. Fixtures were not executed, and actual dyld execution was not qualified. Closes #5637
This commit is contained in:
1 parent
84c89d1107
commit
1502cfbc8c
6 files changed
+312
-19
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import type { BigIntStats } from "node:fs";
|
||||
import { lstat, readlink, realpath } from "node:fs/promises";
|
||||
import { join, relative, sep } from "node:path";
|
||||
import { isAbsolute, join, relative, sep } from "node:path";
|
||||
|
||||
import { AnalysisInputError } from "../../domain/analysisErrorCore.js";
|
||||
import { resolveTreePath } from "../../domain/apple/dyldPaths.js";
|
||||
@@ -36,10 +36,25 @@ class FilesystemTreeView implements DylibTreeView {
|
||||
|
||||
constructor(
|
||||
private readonly root: string,
|
||||
private readonly sourceRoot: string,
|
||||
private readonly files: DylibFileAccess,
|
||||
private readonly signal?: AbortSignal,
|
||||
) {}
|
||||
|
||||
rootRelativePath(absolutePath: string): string | undefined {
|
||||
const roots = isAbsolute(this.sourceRoot)
|
||||
? [this.root, this.sourceRoot]
|
||||
: [this.root];
|
||||
for (let root of roots) {
|
||||
while (root.length > 1 && root.endsWith(sep)) root = root.slice(0, -1);
|
||||
if (absolutePath === root) return "";
|
||||
const prefix = root === sep ? sep : `${root}${sep}`;
|
||||
if (absolutePath.startsWith(prefix))
|
||||
return absolutePath.slice(prefix.length).split(sep).join("/");
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
entry(path: string): Promise<DylibTreeEntry | undefined> {
|
||||
const cached = this.#entries.get(path);
|
||||
if (cached !== undefined) return cached;
|
||||
@@ -113,7 +128,12 @@ export const traceDylibResolution = async (options: {
|
||||
.split(sep)
|
||||
.join("/");
|
||||
assertArtifactReadActive(options.signal);
|
||||
const view = new FilesystemTreeView(root, files, options.signal);
|
||||
const view = new FilesystemTreeView(
|
||||
root,
|
||||
options.rootPath,
|
||||
files,
|
||||
options.signal,
|
||||
);
|
||||
const { roots: requested, unclassified } =
|
||||
parsed.data.roots !== undefined
|
||||
? { roots: parsed.data.roots, unclassified: [] }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, rename, writeFile } from "node:fs/promises";
|
||||
import { mkdir, realpath, rename, symlink, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
@@ -142,6 +142,241 @@ it("uses legacy simulator and device platform facts when resolving dylibs", asyn
|
||||
);
|
||||
});
|
||||
|
||||
describe("absolute dyld candidate paths at the filesystem reader boundary", () => {
|
||||
it.each(["install-name", "rpath"] as const)(
|
||||
"resolves an absolute in-root %s while preserving its source path",
|
||||
async (kind) => {
|
||||
const root = await createTestTempDirectory("rea-dylib-absolute-in-root-");
|
||||
const canonicalRoot = await realpath(root);
|
||||
const bridgePath = "Frameworks/libBridge.dylib";
|
||||
const leafPath = "Frameworks/libLeaf.dylib";
|
||||
const bridgeAbsolute = `${canonicalRoot}/${bridgePath}`;
|
||||
const leafName = "@loader_path/libLeaf.dylib";
|
||||
const executable = machoImage({
|
||||
commands: [
|
||||
buildVersionCommand(1),
|
||||
...(kind === "rpath"
|
||||
? [rpathCommand(`${canonicalRoot}/Frameworks`)]
|
||||
: []),
|
||||
dylibCommand(
|
||||
LC.LOAD_DYLIB,
|
||||
kind === "rpath" ? "@rpath/libBridge.dylib" : bridgeAbsolute,
|
||||
),
|
||||
],
|
||||
});
|
||||
const bridge = machoImage({
|
||||
fileType: FILE_TYPE.dylib,
|
||||
commands: [
|
||||
buildVersionCommand(1),
|
||||
dylibCommand(LC.LOAD_DYLIB, leafName),
|
||||
],
|
||||
});
|
||||
const leaf = machoImage({
|
||||
fileType: FILE_TYPE.dylib,
|
||||
commands: [buildVersionCommand(1)],
|
||||
});
|
||||
await mkdir(join(root, "Frameworks"), { recursive: true });
|
||||
await writeFile(join(root, "App"), executable);
|
||||
await writeFile(bridgeAbsolute, bridge);
|
||||
await writeFile(join(root, leafPath), leaf);
|
||||
|
||||
const trace = await traceDylibResolution({
|
||||
rootPath: root,
|
||||
targetPath: join(root, "App"),
|
||||
targetSha256: createHash("sha256").update(executable).digest("hex"),
|
||||
enumerateRoots: false,
|
||||
parameters: { roots: ["App"] },
|
||||
});
|
||||
|
||||
expect(trace.edges[0]).toMatchObject({
|
||||
install_name:
|
||||
kind === "rpath" ? "@rpath/libBridge.dylib" : bridgeAbsolute,
|
||||
candidates: [
|
||||
{
|
||||
path: bridgeAbsolute,
|
||||
...(kind === "rpath"
|
||||
? { rpath: `${canonicalRoot}/Frameworks` }
|
||||
: {}),
|
||||
outcome: "resolved",
|
||||
resolved_path: bridgePath,
|
||||
},
|
||||
],
|
||||
resolution: { status: "resolved", image: bridgePath },
|
||||
});
|
||||
expect(trace.edges).toContainEqual(
|
||||
expect.objectContaining({
|
||||
loader: bridgePath,
|
||||
install_name: leafName,
|
||||
resolution: { status: "resolved", image: leafPath },
|
||||
}),
|
||||
);
|
||||
expect(trace.images.map(({ path }) => path)).toEqual([
|
||||
"App",
|
||||
bridgePath,
|
||||
leafPath,
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps a genuinely external absolute candidate outside the analyzed root", async () => {
|
||||
const root = await createTestTempDirectory("rea-dylib-absolute-outside-");
|
||||
const canonicalRoot = await realpath(root);
|
||||
const executable = machoImage({
|
||||
commands: [
|
||||
buildVersionCommand(1),
|
||||
dylibCommand(LC.LOAD_DYLIB, "/usr/lib/libSystem.B.dylib"),
|
||||
dylibCommand(LC.LOAD_DYLIB, `${canonicalRoot}/../outside.dylib`),
|
||||
dylibCommand(
|
||||
LC.LOAD_DYLIB,
|
||||
`file://${canonicalRoot}/Frameworks/libBridge.dylib`,
|
||||
),
|
||||
],
|
||||
});
|
||||
await writeFile(join(root, "App"), executable);
|
||||
const trace = await traceDylibResolution({
|
||||
rootPath: root,
|
||||
targetPath: join(root, "App"),
|
||||
targetSha256: createHash("sha256").update(executable).digest("hex"),
|
||||
enumerateRoots: false,
|
||||
parameters: { roots: ["App"] },
|
||||
});
|
||||
|
||||
expect(trace.edges[0]).toMatchObject({
|
||||
candidates: [
|
||||
{
|
||||
path: "/usr/lib/libSystem.B.dylib",
|
||||
outcome: "outside-target",
|
||||
resolved_path: null,
|
||||
},
|
||||
],
|
||||
resolution: { status: "undetermined", image: null },
|
||||
});
|
||||
expect(trace.edges[1]).toMatchObject({
|
||||
candidates: [{ outcome: "escapes-target", resolved_path: null }],
|
||||
resolution: { status: "undetermined", image: null },
|
||||
});
|
||||
expect(trace.edges[2]).toMatchObject({
|
||||
install_name: `file://${canonicalRoot}/Frameworks/libBridge.dylib`,
|
||||
candidates: [
|
||||
{
|
||||
path: `file://${canonicalRoot}/Frameworks/libBridge.dylib`,
|
||||
outcome: "undetermined",
|
||||
resolved_path: null,
|
||||
},
|
||||
],
|
||||
resolution: { status: "undetermined", image: null },
|
||||
});
|
||||
expect(trace.images.map(({ path }) => path)).toEqual(["App"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("absolute dyld candidate traversal", () => {
|
||||
it("maps an absolute candidate spelled through the selected root symlink", async () => {
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-dylib-absolute-root-alias-",
|
||||
);
|
||||
const aliasRoot = join(root, "bundle-alias");
|
||||
const bridgePath = "Frameworks/libBridge.dylib";
|
||||
const bridgeAbsolute = `${aliasRoot}/${bridgePath}`;
|
||||
const executable = machoImage({
|
||||
commands: [
|
||||
buildVersionCommand(1),
|
||||
dylibCommand(LC.LOAD_DYLIB, bridgeAbsolute),
|
||||
],
|
||||
});
|
||||
const bridge = machoImage({
|
||||
fileType: FILE_TYPE.dylib,
|
||||
commands: [buildVersionCommand(1)],
|
||||
});
|
||||
await symlink(".", aliasRoot);
|
||||
await mkdir(join(root, "Frameworks"), { recursive: true });
|
||||
await writeFile(join(root, "App"), executable);
|
||||
await writeFile(join(root, bridgePath), bridge);
|
||||
|
||||
const trace = await traceDylibResolution({
|
||||
rootPath: aliasRoot,
|
||||
targetPath: `${aliasRoot}/App`,
|
||||
targetSha256: createHash("sha256").update(executable).digest("hex"),
|
||||
enumerateRoots: false,
|
||||
parameters: { roots: ["App"] },
|
||||
});
|
||||
|
||||
expect(trace.edges[0]).toMatchObject({
|
||||
install_name: bridgeAbsolute,
|
||||
candidates: [
|
||||
{
|
||||
path: bridgeAbsolute,
|
||||
outcome: "resolved",
|
||||
resolved_path: bridgePath,
|
||||
},
|
||||
],
|
||||
resolution: { status: "resolved", image: bridgePath },
|
||||
});
|
||||
});
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"preserves symlink-then-parent traversal for an absolute in-root path",
|
||||
async () => {
|
||||
const root = await createTestTempDirectory("rea-dylib-absolute-symlink-");
|
||||
const canonicalRoot = await realpath(root);
|
||||
const bridgePath = "Frameworks/Versions/libBridge.dylib";
|
||||
const decoyPath = "Frameworks/libBridge.dylib";
|
||||
const sourcePath = `${canonicalRoot}/Frameworks/Current/../libBridge.dylib`;
|
||||
const executable = machoImage({
|
||||
commands: [
|
||||
buildVersionCommand(1),
|
||||
dylibCommand(LC.LOAD_DYLIB, sourcePath),
|
||||
],
|
||||
});
|
||||
const bridge = machoImage({
|
||||
fileType: FILE_TYPE.dylib,
|
||||
commands: [
|
||||
buildVersionCommand(1),
|
||||
dylibCommand(LC.LOAD_DYLIB, "@loader_path/libLeaf.dylib"),
|
||||
],
|
||||
});
|
||||
const leaf = machoImage({
|
||||
fileType: FILE_TYPE.dylib,
|
||||
commands: [buildVersionCommand(1)],
|
||||
});
|
||||
await mkdir(join(root, "Frameworks/Versions/A"), { recursive: true });
|
||||
await symlink("Versions/A", join(root, "Frameworks/Current"));
|
||||
await writeFile(join(root, "App"), executable);
|
||||
await writeFile(join(root, bridgePath), bridge);
|
||||
await writeFile(join(root, decoyPath), new Uint8Array([0, 1, 2]));
|
||||
await writeFile(join(root, "Frameworks/Versions/libLeaf.dylib"), leaf);
|
||||
|
||||
const trace = await traceDylibResolution({
|
||||
rootPath: root,
|
||||
targetPath: join(root, "App"),
|
||||
targetSha256: createHash("sha256").update(executable).digest("hex"),
|
||||
enumerateRoots: false,
|
||||
parameters: { roots: ["App"] },
|
||||
});
|
||||
|
||||
expect(trace.edges[0]).toMatchObject({
|
||||
candidates: [
|
||||
{
|
||||
path: sourcePath,
|
||||
outcome: "resolved",
|
||||
resolved_path: bridgePath,
|
||||
},
|
||||
],
|
||||
resolution: { status: "resolved", image: bridgePath },
|
||||
});
|
||||
expect(trace.edges).toContainEqual(
|
||||
expect.objectContaining({
|
||||
loader: bridgePath,
|
||||
resolution: {
|
||||
status: "resolved",
|
||||
image: "Frameworks/Versions/libLeaf.dylib",
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("selects matching arm64e subtype slices when tracing a mixed FAT image", async () => {
|
||||
const root = await createTestTempDirectory("rea-macho-arm64e-subtypes-");
|
||||
const executablePath = "Contents/MacOS/App";
|
||||
|
||||
Reference in new issue
Block a user