[llvm-install-name-tool] Add support for -rpath option
authorAlexander Shaposhnikov <alexshap@fb.com>
Sat, 27 Jun 2020 00:22:15 +0000 (17:22 -0700)
committerAlexander Shaposhnikov <alexshap@fb.com>
Sat, 27 Jun 2020 00:23:03 +0000 (17:23 -0700)
This diff implements -rpath option for llvm-install-name-tool
which replaces the rpath value in the specified Mach-O binary.

Patch by Sameer Arora!

Test plan: make check-all

Differential revision: https://reviews.llvm.org/D82051

llvm/test/tools/llvm-objcopy/MachO/install-name-tool-rpath.test [new file with mode: 0644]
llvm/tools/llvm-objcopy/CopyConfig.cpp
llvm/tools/llvm-objcopy/CopyConfig.h
llvm/tools/llvm-objcopy/InstallNameToolOpts.td
llvm/tools/llvm-objcopy/MachO/MachOObjcopy.cpp

diff --git a/llvm/test/tools/llvm-objcopy/MachO/install-name-tool-rpath.test b/llvm/test/tools/llvm-objcopy/MachO/install-name-tool-rpath.test
new file mode 100644 (file)
index 0000000..ae92401
--- /dev/null
@@ -0,0 +1,142 @@
+## This test checks updating a LC_RPATH load command in a MachO binary.
+
+# RUN: yaml2obj %s -o %t
+
+## Updating a single RPath entry:
+# RUN: llvm-install-name-tool -rpath @executable_a/. @executable_A/. %t
+# RUN: llvm-objdump -p %t | \
+# RUN:   FileCheck %s --check-prefix=RPATHS --implicit-check-not=@executable
+
+# RPATHS: @executable_A/.
+# RPATHS: @executable_short_test
+# RPATHS: @executable_long_test/.
+# RPATHS: @executable_d/.
+
+## Updating multiple RPath entries:
+# RUN: llvm-install-name-tool -rpath @executable_short_test/. @executable_test/. \
+# RUN:                        -rpath @executable_long_test/. @executable_long_long_test/. %t
+# RUN: llvm-objdump -p %t | \
+# RUN:   FileCheck %s --check-prefix=RPATHS-MULTIPLE --implicit-check-not=@executable
+
+# RPATHS-MULTIPLE: @executable_A/.
+# RPATHS-MULTIPLE: @executable_test/.
+# RPATHS-MULTIPLE: @executable_long_long_test/.
+# RPATHS-MULTIPLE: @executable_d/.
+
+## Check that cmdsize accounts for NULL terminator:
+# RUN: llvm-install-name-tool -rpath @executable_A/. ABCD %t
+# RUN: llvm-objdump -p %t | FileCheck %s --check-prefix=RPATH-SIZE
+
+# RPATH-SIZE:      cmd LC_RPATH
+# RPATH-SIZE-NEXT: cmdsize 24
+# RPATH-SIZE-NEXT: path ABCD
+
+## Updating and adding different RPaths:
+# RUN: llvm-install-name-tool -rpath @executable_d/. @executable_D/. \
+# RUN:                        -add_rpath @executable_e/. %t
+# RUN: llvm-objdump -p %t | FileCheck %s --check-prefix=RPATHS-ADD --implicit-check-not=@executable
+
+# RPATHS-ADD: ABCD
+# RPATHS-ADD: @executable_test/.
+# RPATHS-ADD: @executable_long_long_test/.
+# RPATHS-ADD: @executable_D/.
+# RPATHS-ADD: @executable_e/.
+
+## Updating and deleting different RPaths:
+# RUN: llvm-install-name-tool -rpath @executable_D/. @executable_d/. \
+# RUN:                        -delete_rpath @executable_e/. %t
+# RUN: llvm-objdump -p %t | FileCheck %s --check-prefix=RPATHS-DELETE --implicit-check-not=@executable
+
+# RPATHS-DELETE: ABCD
+# RPATHS-DELETE: @executable_test/.
+# RPATHS-DELETE: @executable_long_long_test/.
+# RPATHS-DELETE: @executable_d/.
+
+# RUN: cp %t %t1
+
+## Updating multiple RPath entries where one exists and the other doesn't:
+# RUN: not llvm-install-name-tool -rpath @executable_test/. @executable/. \
+# RUN:                            -rpath @executable_long_test/. @executable_long_longest/. %t 2>&1 | \
+# RUN:   FileCheck %s --check-prefix=RPATHS-FAIL
+# RUN: cmp %t %t1
+
+# RPATHS-FAIL: no LC_RPATH load command with path: @executable_long_test/.
+
+## Updating a nonexistent RPath:
+# RUN: not llvm-install-name-tool -rpath @executable_a/. @executable_AA/. %t 2>&1 | \
+# RUN:   FileCheck %s --check-prefix=ABSENT-RPATH
+# RUN: cmp %t %t1
+
+# ABSENT-RPATH: no LC_RPATH load command with path: @executable_a/.
+
+## Updating to an existing RPath:
+# RUN: not llvm-install-name-tool -rpath @executable_d/. ABCD %t 2>&1 | \
+# RUN:   FileCheck %s --check-prefix=EXISTING
+# RUN: cmp %t %t1
+
+# EXISTING: rpath ABCD would create a duplicate load command
+
+## Duplicate RPath entries:
+# RUN: not llvm-install-name-tool -rpath DDD1/. @exec_d/. \
+# RUN:                            -rpath @exec_d/. DDD2/. %t 2>&1 | \
+# RUN:   FileCheck %s --check-prefix=DUPLICATE
+# RUN: cmp %t %t1
+
+# DUPLICATE: cannot specify both -rpath DDD1/. @exec_d/. and -rpath @exec_d/. DDD2/.
+
+## Updating and deleting RPath at the same time:
+# RUN: not llvm-install-name-tool -rpath @executable_d/. DD/. \
+# RUN:                            -delete_rpath @executable_d/. %t 2>&1 | \
+# RUN:   FileCheck %s --check-prefix=COMBINED-DELETE
+# RUN: cmp %t %t1
+
+# COMBINED-DELETE: cannot specify both -delete_rpath @executable_d/. and -rpath @executable_d/. DD/.
+
+## Updating and adding RPath at the same time:
+# RUN: not llvm-install-name-tool -rpath @executable_e/. EE/. \
+# RUN:                            -add_rpath @executable_e/. %t 2>&1 | \
+# RUN:   FileCheck %s --check-prefix=COMBINED-ADD
+# RUN: cmp %t %t1
+
+# COMBINED-ADD: cannot specify both -add_rpath @executable_e/. and -rpath @executable_e/. EE/.
+
+## Missing an RPath argument:
+# RUN: not llvm-install-name-tool %t -rpath @executable_e/. 2>&1 | \
+# RUN:   FileCheck %s --check-prefix=MISSING
+# RUN: cmp %t %t1
+
+## Missing both RPath arguments:
+# RUN: not llvm-install-name-tool %t -rpath 2>&1 | \
+# RUN:   FileCheck %s --check-prefix=MISSING
+# RUN: cmp %t %t1
+
+# MISSING: missing argument to -rpath option
+
+
+--- !mach-o
+FileHeader:
+  magic:           0xFEEDFACF
+  cputype:         0x01000007
+  cpusubtype:      0x00000003
+  filetype:        0x00000001
+  ncmds:           4
+  sizeofcmds:      144
+  flags:           0x00002000
+  reserved:        0x00000000
+LoadCommands:
+  - cmd:             LC_RPATH
+    cmdsize:         32
+    path:            12
+    PayloadString:   '@executable_a/.'
+  - cmd:             LC_RPATH
+    cmdsize:         40
+    path:            12
+    PayloadString:   '@executable_short_test/.'
+  - cmd:             LC_RPATH
+    cmdsize:         40
+    path:            12
+    PayloadString:   '@executable_long_test/.'
+  - cmd:             LC_RPATH
+    cmdsize:         32
+    path:            12
+    PayloadString:   '@executable_d/.'
index 749df08..b483116 100644 (file)
@@ -827,6 +827,13 @@ parseInstallNameToolOptions(ArrayRef<const char *> ArgsArr) {
   llvm::opt::InputArgList InputArgs =
       T.ParseArgs(ArgsArr, MissingArgumentIndex, MissingArgumentCount);
 
+  if (MissingArgumentCount)
+    return createStringError(
+        errc::invalid_argument,
+        "missing argument to " +
+            StringRef(InputArgs.getArgString(MissingArgumentIndex)) +
+            " option");
+
   if (InputArgs.size() == 0) {
     printHelp(T, errs(), ToolType::InstallNameTool);
     exit(1);
@@ -860,6 +867,43 @@ parseInstallNameToolOptions(ArrayRef<const char *> ArgsArr) {
     Config.RPathsToRemove.insert(RPath);
   }
 
+  for (auto *Arg : InputArgs.filtered(INSTALL_NAME_TOOL_rpath)) {
+    StringRef Old = Arg->getValue(0);
+    StringRef New = Arg->getValue(1);
+
+    auto Match = [=](StringRef RPath) { return RPath == Old || RPath == New; };
+
+    // Cannot specify duplicate -rpath entries
+    auto It1 = find_if(Config.RPathsToUpdate,
+                       [&Match](const std::pair<StringRef, StringRef> &OldNew) {
+                         return Match(OldNew.first) || Match(OldNew.second);
+                       });
+    if (It1 != Config.RPathsToUpdate.end())
+      return createStringError(
+          errc::invalid_argument,
+          "cannot specify both -rpath %s %s and -rpath %s %s",
+          It1->first.str().c_str(), It1->second.str().c_str(),
+          Old.str().c_str(), New.str().c_str());
+
+    // Cannot specify the same rpath under both -delete_rpath and -rpath
+    auto It2 = find_if(Config.RPathsToRemove, Match);
+    if (It2 != Config.RPathsToRemove.end())
+      return createStringError(
+          errc::invalid_argument,
+          "cannot specify both -delete_rpath %s and -rpath %s %s",
+          It2->str().c_str(), Old.str().c_str(), New.str().c_str());
+
+    // Cannot specify the same rpath under both -add_rpath and -rpath
+    auto It3 = find_if(Config.RPathToAdd, Match);
+    if (It3 != Config.RPathToAdd.end())
+      return createStringError(
+          errc::invalid_argument,
+          "cannot specify both -add_rpath %s and -rpath %s %s",
+          It3->str().c_str(), Old.str().c_str(), New.str().c_str());
+
+    Config.RPathsToUpdate.emplace_back(Old, New);
+  }
+
   SmallVector<StringRef, 2> Positional;
   for (auto Arg : InputArgs.filtered(INSTALL_NAME_TOOL_UNKNOWN))
     return createStringError(errc::invalid_argument, "unknown argument '%s'",
index 427016a..a492e46 100644 (file)
@@ -178,6 +178,7 @@ struct CopyConfig {
   std::vector<StringRef> DumpSection;
   std::vector<StringRef> SymbolsToAdd;
   std::vector<StringRef> RPathToAdd;
+  std::vector<std::pair<StringRef, StringRef>> RPathsToUpdate;
   DenseSet<StringRef> RPathsToRemove;
 
   // Section matchers
index b91aba0..cbdb878 100644 (file)
@@ -21,5 +21,8 @@ def add_rpath : Option<["-", "--"], "add_rpath", KIND_SEPARATE>,
 def delete_rpath: Option<["-", "--"], "delete_rpath", KIND_SEPARATE>,
                   HelpText<"Delete specified rpath">;
 
+def rpath: MultiArg<["-", "--"], "rpath", 2>,
+           HelpText<"Change rpath path name">;
+
 def version : Flag<["--"], "version">,
               HelpText<"Print the version and exit.">;
index 6a6f5e7..2586ef2 100644 (file)
@@ -22,16 +22,31 @@ using namespace object;
 using SectionPred = std::function<bool(const std::unique_ptr<Section> &Sec)>;
 using LoadCommandPred = std::function<bool(const LoadCommand &LC)>;
 
+static bool isLoadCommandWithPayloadString(const LoadCommand &LC) {
+  // TODO: Add support for LC_REEXPORT_DYLIB, LC_LOAD_UPWARD_DYLIB and
+  // LC_LAZY_LOAD_DYLIB
+  return LC.MachOLoadCommand.load_command_data.cmd == MachO::LC_RPATH ||
+         LC.MachOLoadCommand.load_command_data.cmd == MachO::LC_ID_DYLIB ||
+         LC.MachOLoadCommand.load_command_data.cmd == MachO::LC_LOAD_DYLIB ||
+         LC.MachOLoadCommand.load_command_data.cmd == MachO::LC_LOAD_WEAK_DYLIB;
+}
+
+static StringRef getPayloadString(const LoadCommand &LC) {
+  assert(isLoadCommandWithPayloadString(LC) &&
+         "unsupported load command encountered");
+
+  return StringRef(reinterpret_cast<const char *>(LC.Payload.data()),
+                   LC.Payload.size())
+      .rtrim('\0');
+}
+
 static Error removeLoadCommands(const CopyConfig &Config, Object &Obj) {
   DenseSet<StringRef> RPathsToRemove(Config.RPathsToRemove.begin(),
                                      Config.RPathsToRemove.end());
 
   LoadCommandPred RemovePred = [&RPathsToRemove](const LoadCommand &LC) {
     if (LC.MachOLoadCommand.load_command_data.cmd == MachO::LC_RPATH) {
-      StringRef RPath =
-          StringRef(reinterpret_cast<const char *>(LC.Payload.data()),
-                    LC.Payload.size())
-              .rtrim('\0');
+      StringRef RPath = getPayloadString(LC);
       if (RPathsToRemove.count(RPath)) {
         RPathsToRemove.erase(RPath);
         return true;
@@ -116,6 +131,18 @@ static void updateAndRemoveSymbols(const CopyConfig &Config, Object &Obj) {
   Obj.SymTable.removeSymbols(RemovePred);
 }
 
+template <typename LCType>
+static void updateLoadCommandPayloadString(LoadCommand &LC, StringRef S) {
+  assert(isLoadCommandWithPayloadString(LC) &&
+         "unsupported load command encountered");
+
+  uint32_t NewCmdsize = alignTo(sizeof(LCType) + S.size() + 1, 8);
+
+  LC.MachOLoadCommand.load_command_data.cmdsize = NewCmdsize;
+  LC.Payload.assign(NewCmdsize - sizeof(LCType), 0);
+  std::copy(S.begin(), S.end(), LC.Payload.begin());
+}
+
 static LoadCommand buildRPathLoadCommand(StringRef Path) {
   LoadCommand LC;
   MachO::rpath_command RPathLC;
@@ -257,12 +284,35 @@ static Error handleArgs(const CopyConfig &Config, Object &Obj) {
   if (Error E = removeLoadCommands(Config, Obj))
     return E;
 
+  StringRef Old, New;
+  for (const auto &OldNew : Config.RPathsToUpdate) {
+    std::tie(Old, New) = OldNew;
+
+    auto FindRPathLC = [&Obj](StringRef RPath) {
+      return find_if(Obj.LoadCommands, [=](const LoadCommand &LC) {
+        return LC.MachOLoadCommand.load_command_data.cmd == MachO::LC_RPATH &&
+               getPayloadString(LC) == RPath;
+      });
+    };
+
+    auto NewIt = FindRPathLC(New);
+    if (NewIt != Obj.LoadCommands.end())
+      return createStringError(errc::invalid_argument,
+                               "rpath " + New +
+                                   " would create a duplicate load command");
+
+    auto OldIt = FindRPathLC(Old);
+    if (OldIt == Obj.LoadCommands.end())
+      return createStringError(errc::invalid_argument,
+                               "no LC_RPATH load command with path: " + Old);
+
+    updateLoadCommandPayloadString<MachO::rpath_command>(*OldIt, New);
+  }
+
   for (StringRef RPath : Config.RPathToAdd) {
     for (LoadCommand &LC : Obj.LoadCommands) {
       if (LC.MachOLoadCommand.load_command_data.cmd == MachO::LC_RPATH &&
-          RPath == StringRef(reinterpret_cast<char *>(LC.Payload.data()),
-                             LC.Payload.size())
-                       .trim(0)) {
+          RPath == getPayloadString(LC)) {
         return createStringError(errc::invalid_argument,
                                  "rpath " + RPath +
                                      " would create a duplicate load command");