Repository: llvm/llvm-project
Pull request #135241: [lld-macho]Fix bug in finding "chained" re-exported libs.
Author: login: oontvoo
Labels: name: lld
name: lld:MachO
Created: 2025-04-10T19:26:59Z | Merged: 2025-04-28T17:16:07Z

Details:
When we have the following scenario:

- lib_a re-exports lib_b
- lib_b re-exports @rpath/lib_c
  + lib_b contains LC_RPATH

Previously, lld-macho cannot find lib_c because it was attempting to resolve the '@rpath' from lib_b (which had no LC_RPATH defined). The change here is to also consider all the LC_RPATH rom lib_b when trying to find lib_c.

Inspired by real-life example when linking with libXCTestSwiftSupport.dylib (which re-exports XCTest, which re-exports XCTestCore)


Diff:
diff --git a/lld/MachO/InputFiles.cpp b/lld/MachO/InputFiles.cpp
index 9adfbc9d3f6f5..7a0f8e0d5c4d5 100644
--- a/lld/MachO/InputFiles.cpp
+++ b/lld/MachO/InputFiles.cpp
@@ -1633,6 +1633,17 @@ static DylibFile *findDylib(StringRef path, DylibFile *umbrella,
       if (std::optional<StringRef> dylibPath = resolveDylibPath(newPath.str()))
         return loadDylib(*dylibPath, umbrella);
     }
+    // If not found in umbrella, try the rpaths specified via -rpath too.
+    for (StringRef rpath : config->runtimePaths) {
+      newPath.clear();
+      if (rpath.consume_front("@loader_path/")) {
+        fs::real_path(umbrella->getName(), newPath);
+        path::remove_filename(newPath);
+      }
+      path::append(newPath, rpath, path.drop_front(strlen("@rpath/")));
+      if (std::optional<StringRef> dylibPath = resolveDylibPath(newPath.str()))
+        return loadDylib(*dylibPath, umbrella);
+    }
   }
 
   // FIXME: Should this be further up?
@@ -1678,9 +1689,16 @@ static bool isImplicitlyLinked(StringRef path) {
 void DylibFile::loadReexport(StringRef path, DylibFile *umbrella,
                          const InterfaceFile *currentTopLevelTapi) {
   DylibFile *reexport = findDylib(path, umbrella, currentTopLevelTapi);
-  if (!reexport)
-    error(toString(this) + ": unable to locate re-export with install name " +
-          path);
+  if (!reexport) {
+    // If not found in umbrella, retry since some rpaths might have been
+    // defined in "this" dylib (which contains the LC_REEXPORT_DYLIB cmd) and
+    // not in the umbrella.
+    DylibFile *reexport2 = findDylib(path, this, currentTopLevelTapi);
+    if (!reexport2) {
+      error(toString(this) + ": unable to locate re-export with install name " +
+            path);
+    }
+  }
 }
 
 DylibFile::DylibFile(MemoryBufferRef mb, DylibFile *umbrella,
diff --git a/lld/test/MachO/reexport-with-rpath.s b/lld/test/MachO/reexport-with-rpath.s
new file mode 100644
index 0000000000000..431962a9c2d8a
--- /dev/null
+++ b/lld/test/MachO/reexport-with-rpath.s
@@ -0,0 +1,35 @@
+# REQUIRES: x86
+# RUN: rm -rf %t; split-file %s %t
+# RUN: mkdir -p %t/cc/two/three
+# RUN: mkdir -p %t/bb/two/three
+# RUN: mkdir -p %t/aa/two/three
+
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/c.s -o %t/c.o
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/b.s -o %t/b.o
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/a.s -o %t/a.o
+
+# RUN: %lld -dylib -install_name @rpath/two/three/libCee.dylib %t/c.o -o %t/cc/two/three/libCee.dylib 
+# RUN: %lld -dylib -install_name @rpath/two/three/libBee.dylib -L%t/cc/two/three -sub_library libCee -lCee %t/b.o -o %t/bb/two/three/libBee.dylib -rpath %t/cc
+# RUN: %lld -dylib -install_name @rpath/two/three/libAee.dylib -L%t/bb/two/three -sub_library libBee -lBee %t/a.o -o %t/aa/two/three/libAee.dylib
+
+#--- c.s
+.text
+.global _c_func
+_c_func:
+  mov $0, %rax
+  ret
+
+#--- b.s
+.text
+.global _b_func
+
+_b_func:
+  mov $0, %rax
+  ret
+
+#--- a.s
+.text
+.global _a_func
+_a_func:
+  mov $0, %rax
+  ret


Reviews:
user: login: nico
body: #134925 recently was kind of similar. But that was about tbd files and this here is about mach-o files.

I wonder if our behavior here is consistent across .tbd and .dylib inputs.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T18:10:08Z
user: login: oontvoo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-12T04:34:52Z
user: login: googlewalt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T14:13:10Z
user: login: oontvoo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-28T17:15:32Z

Review comments:
diff_hunk: @@ -1678,9 +1689,16 @@ static bool isImplicitlyLinked(StringRef path) {
 void DylibFile::loadReexport(StringRef path, DylibFile *umbrella,
                          const InterfaceFile *currentTopLevelTapi) {
   DylibFile *reexport = findDylib(path, umbrella, currentTopLevelTapi);
-  if (!reexport)
-    error(toString(this) + ": unable to locate re-export with install name " +
-          path);
+  if (!reexport) {
+    // If not found in umbrella, retry since some rpaths might have been
+    // defined in "this" dyblib (which contains the LC_REEXPORT_DYLIB cmd) and
path: lld/MachO/InputFiles.cpp
user: login: nico
body: typo dyblib
created_at: 2025-04-11T18:05:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1678,9 +1689,16 @@ static bool isImplicitlyLinked(StringRef path) {
 void DylibFile::loadReexport(StringRef path, DylibFile *umbrella,
                          const InterfaceFile *currentTopLevelTapi) {
   DylibFile *reexport = findDylib(path, umbrella, currentTopLevelTapi);
-  if (!reexport)
-    error(toString(this) + ": unable to locate re-export with install name " +
-          path);
+  if (!reexport) {
+    // If not found in umbrella, retry since some rpaths might have been
+    // defined in "this" dyblib (which contains the LC_REEXPORT_DYLIB cmd) and
path: lld/MachO/InputFiles.cpp
user: login: oontvoo
body: done!
created_at: 2025-04-12T04:34:52Z
author_association: MEMBER
path: lld/test/MachO/reexport-with-rpath.s
user: login: googlewalt
body: Does this actually fail without the fix?  Do we need to link something with libAee.dylib to demonstrate the failure?
created_at: 2025-04-15T14:13:09Z
author_association: CONTRIBUTOR
path: lld/test/MachO/reexport-with-rpath.s
user: login: oontvoo
body: Done - updated the test to have a main which references the re-exported symbols.
W/o the change in this patch, the test failed with sth like this:


```
ld64.lld: error: /usr/local/google/home/vyng/repo/lld/oontvoo/llvm-project/build_lld/tools/lld/test/MachO/Output/reexport-with-rpath.s.tmp/aa/two/three/libAee.dylib: unable to locate re-export with install name @rpath/two/three/libBee.dylib
ld64.lld: error: undefined symbol: _b_func
>>> referenced by /usr/local/google/home/vyng/repo/lld/oontvoo/llvm-project/build_lld/tools/lld/test/MachO/Output/reexport-with-rpath.s.tmp/main.o:(symbol _main+0x6)
>>> did you mean: _a_func
>>> defined in: /usr/local/google/home/vyng/repo/lld/oontvoo/llvm-project/build_lld/tools/lld/test/MachO/Output/reexport-with-rpath.s.tmp/aa/two/three/libAee.dylib

ld64.lld: error: undefined symbol: _c_func
>>> referenced by /usr/local/google/home/vyng/repo/lld/oontvoo/llvm-project/build_lld/tools/lld/test/MachO/Output/reexport-with-rpath.s.tmp/main.o:(symbol _main+0x1)
>>> did you mean: _a_func
>>> defined in: /usr/local/google/home/vyng/repo/lld/oontvoo/llvm-project/build_lld/tools/lld/test/MachO/Output/reexport-with-rpath.s.tmp/aa/two/three/libAee.dylib

--

********************
********************
Failed Tests (1):
  lld :: MachO/reexport-with-rpath.s
```
created_at: 2025-04-28T17:15:32Z
author_association: MEMBER

PR comments:
user: login: llvmbot
created_at: 2025-04-10T19:27:36Z
body: 
<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-lld

Author: Vy Nguyen (oontvoo)

<details>
<summary>Changes</summary>

Details:
When we have the following scenario:

- lib_a re-exports lib_b
- lib_b re-exports @<!-- -->rpath/lib_c
  + lib_b contains LC_RPATH

Previously, lld-macho cannot find lib_c because it was attempting to resolve the '@<!-- -->rpath' from lib_b (which had no LC_RPATH defined). The change here is to also consider all the LC_RPATH rom lib_b when trying to find lib_c.

Inspired by real-life example when linking with libXCTestSwiftSupport.dylib (which re-exports XCTest, which re-exports XCTestCore)

---
Full diff: https://github.com/llvm/llvm-project/pull/135241.diff


2 Files Affected:

- (modified) lld/MachO/InputFiles.cpp (+21-3) 
- (added) lld/test/MachO/reexport-with-rpath.s (+37) 


``````````diff
diff --git a/lld/MachO/InputFiles.cpp b/lld/MachO/InputFiles.cpp
index 9adfbc9d3f6f5..9afa5feaa1693 100644
--- a/lld/MachO/InputFiles.cpp
+++ b/lld/MachO/InputFiles.cpp
@@ -1633,6 +1633,17 @@ static DylibFile *findDylib(StringRef path, DylibFile *umbrella,
       if (std::optional<StringRef> dylibPath = resolveDylibPath(newPath.str()))
         return loadDylib(*dylibPath, umbrella);
     }
+    // If not found in umbrella, try the rpaths specified via -rpath too.
+    for (StringRef rpath : config->runtimePaths) {
+      newPath.clear();
+      if (rpath.consume_front("@loader_path/")) {
+        fs::real_path(umbrella->getName(), newPath);
+        path::remove_filename(newPath);
+      }
+      path::append(newPath, rpath, path.drop_front(strlen("@rpath/")));
+      if (std::optional<StringRef> dylibPath = resolveDylibPath(newPath.str()))
+        return loadDylib(*dylibPath, umbrella);
+    }
   }
 
   // FIXME: Should this be further up?
@@ -1678,9 +1689,16 @@ static bool isImplicitlyLinked(StringRef path) {
 void DylibFile::loadReexport(StringRef path, DylibFile *umbrella,
                          const InterfaceFile *currentTopLevelTapi) {
   DylibFile *reexport = findDylib(path, umbrella, currentTopLevelTapi);
-  if (!reexport)
-    error(toString(this) + ": unable to locate re-export with install name " +
-          path);
+  if (!reexport) {
+    // If not found in umbrella, retry since some rpaths might have been
+    // defined in "this" dyblib (which contains the LC_REEXPORT_DYLIB cmd) and
+    // not in the umbrella.
+    DylibFile *reexport2 = findDylib(path, this, currentTopLevelTapi);
+    if (!reexport2) {
+      error(toString(this) + ": unable to locate re-export with install name " +
+            path);
+    }
+  }
 }
 
 DylibFile::DylibFile(MemoryBufferRef mb, DylibFile *umbrella,
diff --git a/lld/test/MachO/reexport-with-rpath.s b/lld/test/MachO/reexport-with-rpath.s
new file mode 100644
index 0000000000000..f1b00ddc9f7e5
--- /dev/null
+++ b/lld/test/MachO/reexport-with-rpath.s
@@ -0,0 +1,37 @@
+# REQUIRES: x86
+# RUN: rm -rf %t; split-file %s %t
+# RUN: mkdir -p %t/cc/two/three
+# RUN: mkdir -p %t/bb/two/three
+# RUN: mkdir -p %t/aa/two/three
+
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/c.s -o %t/c.o
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/b.s -o %t/b.o
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/a.s -o %t/a.o
+
+# RUN: %lld -dylib -install_name @rpath/two/three/libCee.dylib %t/c.o -o %t/cc/two/three/libCee.dylib 
+# RUN: %lld -dylib -install_name @rpath/two/three/libBee.dylib -L%t/cc/two/three -sub_library libCee -lCee %t/b.o -o %t/bb/two/three/libBee.dylib -rpath %t/cc
+# RUN: %lld -dylib -install_name @rpath/two/three/libAee.dylib -L%t/bb/two/three -sub_library libBee -lBee %t/a.o -o %t/aa/two/three/libAee.dylib
+##################
+
+
+#--- c.s
+.text
+.global _c_func
+_c_func:
+  mov $0, %rax
+  ret
+
+#--- b.s
+.text
+.global _b_func
+
+_b_func:
+  mov $0, %rax
+  ret
+
+#--- a.s
+.text
+.global _a_func
+_a_func:
+  mov $0, %rax
+  ret

``````````

</details>

author_association: MEMBER
user: login: llvmbot
created_at: 2025-04-10T19:27:37Z
body: 
<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-lld-macho

Author: Vy Nguyen (oontvoo)

<details>
<summary>Changes</summary>

Details:
When we have the following scenario:

- lib_a re-exports lib_b
- lib_b re-exports @<!-- -->rpath/lib_c
  + lib_b contains LC_RPATH

Previously, lld-macho cannot find lib_c because it was attempting to resolve the '@<!-- -->rpath' from lib_b (which had no LC_RPATH defined). The change here is to also consider all the LC_RPATH rom lib_b when trying to find lib_c.

Inspired by real-life example when linking with libXCTestSwiftSupport.dylib (which re-exports XCTest, which re-exports XCTestCore)

---
Full diff: https://github.com/llvm/llvm-project/pull/135241.diff


2 Files Affected:

- (modified) lld/MachO/InputFiles.cpp (+21-3) 
- (added) lld/test/MachO/reexport-with-rpath.s (+37) 


``````````diff
diff --git a/lld/MachO/InputFiles.cpp b/lld/MachO/InputFiles.cpp
index 9adfbc9d3f6f5..9afa5feaa1693 100644
--- a/lld/MachO/InputFiles.cpp
+++ b/lld/MachO/InputFiles.cpp
@@ -1633,6 +1633,17 @@ static DylibFile *findDylib(StringRef path, DylibFile *umbrella,
       if (std::optional<StringRef> dylibPath = resolveDylibPath(newPath.str()))
         return loadDylib(*dylibPath, umbrella);
     }
+    // If not found in umbrella, try the rpaths specified via -rpath too.
+    for (StringRef rpath : config->runtimePaths) {
+      newPath.clear();
+      if (rpath.consume_front("@loader_path/")) {
+        fs::real_path(umbrella->getName(), newPath);
+        path::remove_filename(newPath);
+      }
+      path::append(newPath, rpath, path.drop_front(strlen("@rpath/")));
+      if (std::optional<StringRef> dylibPath = resolveDylibPath(newPath.str()))
+        return loadDylib(*dylibPath, umbrella);
+    }
   }
 
   // FIXME: Should this be further up?
@@ -1678,9 +1689,16 @@ static bool isImplicitlyLinked(StringRef path) {
 void DylibFile::loadReexport(StringRef path, DylibFile *umbrella,
                          const InterfaceFile *currentTopLevelTapi) {
   DylibFile *reexport = findDylib(path, umbrella, currentTopLevelTapi);
-  if (!reexport)
-    error(toString(this) + ": unable to locate re-export with install name " +
-          path);
+  if (!reexport) {
+    // If not found in umbrella, retry since some rpaths might have been
+    // defined in "this" dyblib (which contains the LC_REEXPORT_DYLIB cmd) and
+    // not in the umbrella.
+    DylibFile *reexport2 = findDylib(path, this, currentTopLevelTapi);
+    if (!reexport2) {
+      error(toString(this) + ": unable to locate re-export with install name " +
+            path);
+    }
+  }
 }
 
 DylibFile::DylibFile(MemoryBufferRef mb, DylibFile *umbrella,
diff --git a/lld/test/MachO/reexport-with-rpath.s b/lld/test/MachO/reexport-with-rpath.s
new file mode 100644
index 0000000000000..f1b00ddc9f7e5
--- /dev/null
+++ b/lld/test/MachO/reexport-with-rpath.s
@@ -0,0 +1,37 @@
+# REQUIRES: x86
+# RUN: rm -rf %t; split-file %s %t
+# RUN: mkdir -p %t/cc/two/three
+# RUN: mkdir -p %t/bb/two/three
+# RUN: mkdir -p %t/aa/two/three
+
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/c.s -o %t/c.o
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/b.s -o %t/b.o
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/a.s -o %t/a.o
+
+# RUN: %lld -dylib -install_name @rpath/two/three/libCee.dylib %t/c.o -o %t/cc/two/three/libCee.dylib 
+# RUN: %lld -dylib -install_name @rpath/two/three/libBee.dylib -L%t/cc/two/three -sub_library libCee -lCee %t/b.o -o %t/bb/two/three/libBee.dylib -rpath %t/cc
+# RUN: %lld -dylib -install_name @rpath/two/three/libAee.dylib -L%t/bb/two/three -sub_library libBee -lBee %t/a.o -o %t/aa/two/three/libAee.dylib
+##################
+
+
+#--- c.s
+.text
+.global _c_func
+_c_func:
+  mov $0, %rax
+  ret
+
+#--- b.s
+.text
+.global _b_func
+
+_b_func:
+  mov $0, %rax
+  ret
+
+#--- a.s
+.text
+.global _a_func
+_a_func:
+  mov $0, %rax
+  ret

``````````

</details>

author_association: MEMBER
user: login: oontvoo
created_at: 2025-04-28T17:24:40Z
body: Whoops - the last commit didn't get pushed successfully - I've added it in PR/137672
author_association: MEMBER