mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(apple): limit already-loaded reuse to rpath dependencies (#2292)
* fix(apple): limit already-loaded reuse to rpath dependencies dyld reuses an already-loaded image by install name only for @rpath/ dependencies (Loader.cpp:802-816) and evaluates other dependency forms against their expanded path for each loader. When multiple images in different directories depend on @loader_path/ libraries sharing an install name, checking loaded images before candidate expansion caused subsequent loaders to bind to the first loaded image's copy or appear resolved when their own copy was absent. Limit byInstallName registration and lookup to install names that start with @rpath/, leaving caller-relative and literal dependencies to expand and evaluate their own candidate paths. Signed-off-by: Vinícius DS Andrade <95101505+viniciusdsandrade@users.noreply.github.com> * test(apple): verify loader_path dependencies across isolated directories Assert that multiple images depending on @loader_path/ libraries with the same install name do not reuse already-loaded images across loaders. When the second loader's helper is absent, its candidate path is absent and dyld reports required-load-unresolved; when both helpers exist, each loader resolves to its own directory. Signed-off-by: Vinícius DS Andrade <95101505+viniciusdsandrade@users.noreply.github.com> --------- Signed-off-by: Vinícius DS Andrade <95101505+viniciusdsandrade@users.noreply.github.com>
This commit is contained in:
1 parent
650237efb0
commit
60f771a11e
4 files changed
+140
-4
No files matched your search
@@ -174,7 +174,7 @@ Resolution follows dyld:
|
||||
- `@rpath` tries each `LC_RPATH` of the loading image first, then those of the
|
||||
image that loaded it, up to the process root.
|
||||
- Dependencies load dependents-first in load-command order, and each image
|
||||
loads once per process. A later request whose install name matches an
|
||||
loads once per process. A later @rpath request whose install name matches an
|
||||
already loaded image reuses it.
|
||||
- Symlinks such as `Versions/Current` are followed inside the analyzed root.
|
||||
An escape through `..` or an absolute link is `escapes-target`.
|
||||
|
||||
@@ -597,6 +597,136 @@ describe("dyld load order", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("loader-relative dylib resolution", () => {
|
||||
it("does not reuse already loaded images by install name for @loader_path dependencies", async () => {
|
||||
const pluginA = "Contents/PlugIns/A/pluginA.dylib";
|
||||
const helperA = "Contents/PlugIns/A/libhelper.dylib";
|
||||
const pluginB = "Contents/PlugIns/B/pluginB.dylib";
|
||||
const trace = await traceDylibLoading(
|
||||
memoryView({
|
||||
[MAIN]: executable({
|
||||
dependencies: [
|
||||
dependency("@executable_path/../PlugIns/A/pluginA.dylib"),
|
||||
dependency("@executable_path/../PlugIns/B/pluginB.dylib"),
|
||||
],
|
||||
}),
|
||||
[pluginA]: parsed(
|
||||
slice({
|
||||
dependencies: [dependency("@loader_path/libhelper.dylib")],
|
||||
}),
|
||||
),
|
||||
[helperA]: parsed(
|
||||
slice({
|
||||
install_name: "@loader_path/libhelper.dylib",
|
||||
}),
|
||||
),
|
||||
[pluginB]: parsed(
|
||||
slice({
|
||||
dependencies: [dependency("@loader_path/libhelper.dylib")],
|
||||
}),
|
||||
),
|
||||
}),
|
||||
{ roots: [MAIN] },
|
||||
);
|
||||
|
||||
const edgeA = edgeFor(trace, pluginA, "@loader_path/libhelper.dylib");
|
||||
expect(edgeA.resolution).toEqual({
|
||||
status: "resolved",
|
||||
image: helperA,
|
||||
});
|
||||
expect(edgeA.candidates).toEqual([
|
||||
expect.objectContaining({
|
||||
source: "loader_path",
|
||||
outcome: "resolved",
|
||||
resolved_path: helperA,
|
||||
}),
|
||||
]);
|
||||
|
||||
const edgeB = edgeFor(trace, pluginB, "@loader_path/libhelper.dylib");
|
||||
expect(edgeB.resolution).toEqual({
|
||||
status: "unresolved",
|
||||
image: null,
|
||||
});
|
||||
expect(edgeB.candidates).toEqual([
|
||||
expect.objectContaining({
|
||||
path: "Contents/PlugIns/B/libhelper.dylib",
|
||||
source: "loader_path",
|
||||
outcome: "absent",
|
||||
resolved_path: null,
|
||||
}),
|
||||
]);
|
||||
expect(trace.findings).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "required-load-unresolved",
|
||||
image: pluginB,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves @loader_path dependencies to the loader's own directory when multiple helpers exist", async () => {
|
||||
const pluginA = "Contents/PlugIns/A/pluginA.dylib";
|
||||
const helperA = "Contents/PlugIns/A/libhelper.dylib";
|
||||
const pluginB = "Contents/PlugIns/B/pluginB.dylib";
|
||||
const helperB = "Contents/PlugIns/B/libhelper.dylib";
|
||||
const trace = await traceDylibLoading(
|
||||
memoryView({
|
||||
[MAIN]: executable({
|
||||
dependencies: [
|
||||
dependency("@executable_path/../PlugIns/A/pluginA.dylib"),
|
||||
dependency("@executable_path/../PlugIns/B/pluginB.dylib"),
|
||||
],
|
||||
}),
|
||||
[pluginA]: parsed(
|
||||
slice({
|
||||
dependencies: [dependency("@loader_path/libhelper.dylib")],
|
||||
}),
|
||||
),
|
||||
[helperA]: parsed(
|
||||
slice({
|
||||
install_name: "@loader_path/libhelper.dylib",
|
||||
}),
|
||||
),
|
||||
[pluginB]: parsed(
|
||||
slice({
|
||||
dependencies: [dependency("@loader_path/libhelper.dylib")],
|
||||
}),
|
||||
),
|
||||
[helperB]: parsed(
|
||||
slice({
|
||||
install_name: "@loader_path/libhelper.dylib",
|
||||
}),
|
||||
),
|
||||
}),
|
||||
{ roots: [MAIN] },
|
||||
);
|
||||
|
||||
expect(
|
||||
edgeFor(trace, pluginA, "@loader_path/libhelper.dylib"),
|
||||
).toMatchObject({
|
||||
resolution: { status: "resolved", image: helperA },
|
||||
candidates: [
|
||||
expect.objectContaining({
|
||||
source: "loader_path",
|
||||
outcome: "resolved",
|
||||
resolved_path: helperA,
|
||||
}),
|
||||
],
|
||||
});
|
||||
expect(
|
||||
edgeFor(trace, pluginB, "@loader_path/libhelper.dylib"),
|
||||
).toMatchObject({
|
||||
resolution: { status: "resolved", image: helperB },
|
||||
candidates: [
|
||||
expect.objectContaining({
|
||||
source: "loader_path",
|
||||
outcome: "resolved",
|
||||
resolved_path: helperB,
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("dyld slice compatibility and coverage", () => {
|
||||
it("lets an x86_64h process load generic x86_64 but not arm64e load arm64", async () => {
|
||||
const files = {
|
||||
|
||||
@@ -427,7 +427,9 @@ const resolveDependency = async (
|
||||
): Promise<Edge> => {
|
||||
const current = context.loaded.get(loader);
|
||||
const chain = current?.chain ?? [loader];
|
||||
const loaded = context.byInstallName.get(dependency.install_name);
|
||||
const loaded = dependency.install_name.startsWith("@rpath/")
|
||||
? context.byInstallName.get(dependency.install_name)
|
||||
: undefined;
|
||||
const candidates: Candidate[] = [];
|
||||
if (loaded !== undefined)
|
||||
candidates.push({
|
||||
@@ -506,7 +508,10 @@ const traceProcess = async (
|
||||
chain: [context.root],
|
||||
conditional: false,
|
||||
});
|
||||
if (rootSlice.install_name !== null)
|
||||
if (
|
||||
rootSlice.install_name !== null &&
|
||||
rootSlice.install_name.startsWith("@rpath/")
|
||||
)
|
||||
context.byInstallName.set(rootSlice.install_name, context.root);
|
||||
const pending = [context.root];
|
||||
while (pending.length > 0) {
|
||||
@@ -537,6 +542,7 @@ const traceProcess = async (
|
||||
});
|
||||
if (
|
||||
slice.install_name !== null &&
|
||||
slice.install_name.startsWith("@rpath/") &&
|
||||
!context.byInstallName.has(slice.install_name)
|
||||
)
|
||||
context.byInstallName.set(slice.install_name, image);
|
||||
|
||||
@@ -91,7 +91,7 @@ export const DYLIB_RESOLUTION_LIMITATIONS = [
|
||||
"Slices are matched by dyld's graded architectures (an x86_64h process also loads x86_64). arm64e processes that disable pointer authentication can also load arm64 slices; that fallback is not modeled.",
|
||||
"Absolute install names and rpaths are outside the analyzed root and are not evaluated, including /System and /usr/lib libraries that the dyld shared cache usually provides.",
|
||||
"Leaf and relative install names depend on dyld fallback paths, DYLD_* variables, and the working directory; they are undetermined.",
|
||||
"Load order follows dyld's dependents-first traversal in load-command order. An image reached through several chains is resolved once per process, with the rpath stack and conditionality of the first chain; a later request whose install name matches an already loaded image reuses it.",
|
||||
"Load order follows dyld's dependents-first traversal in load-command order. An image reached through several chains is resolved once per process, with the rpath stack and conditionality of the first chain; a later @rpath request whose install name matches an already loaded image reuses it.",
|
||||
"@loader_path uses each image's symlink-resolved path within the analyzed root.",
|
||||
"Code-signing checks that can reject a found image, such as library validation and the hardened runtime, are not evaluated.",
|
||||
"Dylib compatibility versions are not compared. Apple's linker documentation describes a load-time rejection when an image's compatibility version is older than the loader requires, but dyld on macOS 26 was observed to load such an image.",
|
||||
|
||||
Reference in new issue
Block a user