mirror of
https://github.com/morluto/rea.git
synced 2026-11-06 23:06:18 +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,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