1 /*
   2  * Copyright (c) 2019, Red Hat, Inc.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package jdk.tools.jlink.internal.plugins;
  26 
  27 import java.io.IOException;
  28 import java.nio.file.FileVisitResult;
  29 import java.nio.file.Files;
  30 import java.nio.file.InvalidPathException;
  31 import java.nio.file.Path;
  32 import java.nio.file.Paths;
  33 import java.nio.file.SimpleFileVisitor;
  34 import java.nio.file.attribute.BasicFileAttributes;
  35 import java.util.ArrayList;
  36 import java.util.Arrays;
  37 import java.util.List;
  38 import java.util.Locale;
  39 import java.util.Map;
  40 import java.util.MissingResourceException;
  41 import java.util.ResourceBundle;
  42 
  43 import jdk.tools.jlink.plugin.Plugin;
  44 import jdk.tools.jlink.plugin.PluginException;
  45 import jdk.tools.jlink.plugin.ResourcePool;
  46 import jdk.tools.jlink.plugin.ResourcePoolBuilder;
  47 import jdk.tools.jlink.plugin.ResourcePoolEntry;
  48 
  49 /**
  50  * Platform specific jlink plugin for stripping debug symbols from native
  51  * libraries and binaries.
  52  *
  53  */
  54 public final class StripNativeDebugSymbolsPlugin implements Plugin {
  55 
  56     public static final String NAME = "strip-native-debug-symbols";
  57     private static final String DEFAULT_STRIP_CMD = "objcopy";
  58     private static final String STRIP_CMD_ARG = DEFAULT_STRIP_CMD;
  59     private static final String KEEP_DEBUG_INFO_ARG = "keep-debuginfo";
  60     private static final String OMIT_DEBUG_INFO_ARG = "omit-debuginfo";
  61     private static final String DEFAULT_DEBUG_EXT = "debuginfo";
  62     private static final String STRIP_DEBUG_SYMS_OPT = "-g";
  63     private static final String ONLY_KEEP_DEBUG_SYMS_OPT = "--only-keep-debug";
  64     private static final String ADD_DEBUG_LINK_OPT = "--add-gnu-debuglink";
  65     private static final ResourceBundle resourceBundle;
  66     private static final String SHARED_LIBS_EXT = ".so"; // for Linux/Unix
  67 
  68     static {
  69         Locale locale = Locale.getDefault();
  70         try {
  71             resourceBundle = ResourceBundle.getBundle("jdk.tools.jlink."
  72                     + "resources.strip_native_debug_symbols_plugin", locale);
  73         } catch (MissingResourceException e) {
  74             throw new InternalError("Cannot find jlink plugin resource bundle (" +
  75                         NAME + ") for locale " + locale);
  76         }
  77     }
  78 
  79     private final ObjCopyCmdBuilder cmdBuilder;
  80     private boolean includeDebugSymbols;
  81     private String stripBin;
  82     private String debuginfoExt;
  83 
  84     public StripNativeDebugSymbolsPlugin() {
  85         this(new DefaultObjCopyCmdBuilder());
  86     }
  87 
  88     public StripNativeDebugSymbolsPlugin(ObjCopyCmdBuilder cmdBuilder) {
  89         this.cmdBuilder = cmdBuilder;
  90     }
  91 
  92     @Override
  93     public String getName() {
  94         return NAME;
  95     }
  96 
  97     @Override
  98     public ResourcePool transform(ResourcePool in, ResourcePoolBuilder out) {
  99         in.transformAndCopy((resource) -> {
 100             ResourcePoolEntry res = resource;
 101             if (resource.type() == ResourcePoolEntry.Type.NATIVE_LIB ||
 102                 resource.type() == ResourcePoolEntry.Type.NATIVE_CMD) {
 103                 String moduleName = resource.moduleName();
 104                 if (resource.path().endsWith(SHARED_LIBS_EXT) ||
 105                     resource.path().startsWith("/" + moduleName + "/bin")) {
 106                     StrippedDebugInfoBinary strippedBin = stripBinary(resource);
 107                     if (strippedBin != null) {
 108                         res = resource.copyWithContent(strippedBin.strippedBinary);
 109                         if (includeDebugSymbols) {
 110                             String debugEntryPath = resource.path() +
 111                                                       "." + debuginfoExt;
 112                             ResourcePoolEntry debugEntry = ResourcePoolEntry.create(
 113                                                                 debugEntryPath,
 114                                                                 resource.type(),
 115                                                                 strippedBin.debugSymbols);
 116                             out.add(debugEntry);
 117                         }
 118                     }
 119                 }
 120             }
 121             return res;
 122         }, out);
 123 
 124         return out.build();
 125     }
 126 
 127     @Override
 128     public Category getType() {
 129         return Category.TRANSFORMER;
 130     }
 131 
 132     @Override
 133     public String getDescription() {
 134         String key = NAME + ".description";
 135         return PluginsResourceBundle.getMessage(resourceBundle, key);
 136     }
 137 
 138     @Override
 139     public boolean hasArguments() {
 140         return true;
 141     }
 142 
 143     @Override
 144     public String getArgumentsDescription() {
 145         String key = NAME + ".argument";
 146         return PluginsResourceBundle.getMessage(resourceBundle, key);
 147     }
 148 
 149     // --strip-native-debug-symbols=<defaults|options[:objcopy-cmd=/usr/bin/objcopy][:debuginfo-file-ext=dbg][:include-debug-syms=true]>
 150     @Override
 151     public void configure(Map<String, String> config) {
 152         doConfigure(true, config);
 153     }
 154 
 155     // For testing so that validation can be turned off
 156     public void doConfigure(boolean withChecks, Map<String, String> config) {
 157         String arg = config.get(NAME);
 158 
 159         stripBin = DEFAULT_STRIP_CMD;
 160         debuginfoExt = DEFAULT_DEBUG_EXT;
 161 
 162         if (arg == null) { // will this ever be null?
 163             return;
 164         }
 165         boolean hasOmitDebugInfo = false;
 166         boolean hasKeepDebugInfo = false;
 167 
 168         if (KEEP_DEBUG_INFO_ARG.equals(arg)) {
 169             // Case: --strip-native-debug-symbols keep-debug-info
 170             hasKeepDebugInfo = true;
 171         } else if (arg.startsWith(KEEP_DEBUG_INFO_ARG)) {
 172             // Case: --strip-native-debug-symbols keep-debug-info=foo
 173             String[] tokens = arg.split("=");
 174             if (tokens.length != 2 || !KEEP_DEBUG_INFO_ARG.equals(tokens[0])) {
 175                 throw new IllegalArgumentException(
 176                         PluginsResourceBundle.getMessage(resourceBundle,
 177                                                          NAME + ".iae", NAME, arg));
 178             }
 179             hasKeepDebugInfo = true;
 180             debuginfoExt = tokens[1];
 181         }
 182         if (OMIT_DEBUG_INFO_ARG.equals(arg)) {
 183             // Case: --strip-native-debug-symbols omit-debug-info
 184             hasOmitDebugInfo = true;
 185         }
 186         if (arg.startsWith(STRIP_CMD_ARG)) {
 187             // Case: --strip-native-debug-symbols objcopy=<path/to/objcopy
 188             String[] tokens = arg.split("=");
 189             if (tokens.length != 2 || !STRIP_CMD_ARG.equals(tokens[0])) {
 190                 throw new IllegalArgumentException(
 191                         PluginsResourceBundle.getMessage(resourceBundle,
 192                                                          NAME + ".iae", NAME, arg));
 193             }
 194             if (withChecks) {
 195                 validateStripArg(tokens[1]);
 196             }
 197             stripBin = tokens[1];
 198         }
 199         // Cases (combination of options):
 200         //   --strip-native-debug-symbols keep-debug-info:objcopy=</objcpy/path>
 201         //   --strip-native-debug-symbols keep-debug-info=ext:objcopy=</objcpy/path>
 202         //   --strip-native-debug-symbols omit-debug-info:objcopy=</objcpy/path>
 203         String stripArg = config.get(STRIP_CMD_ARG);
 204         if (stripArg != null && withChecks) {
 205             validateStripArg(stripArg);
 206         }
 207         if (stripArg != null) {
 208             stripBin = stripArg;
 209         }
 210         // Case (reversed combination)
 211         //   --strip-native-debug-symbols objcopy=</objcpy/path>:keep-debug-info=ext
 212         // Note: cases like the following are not allowed by the parser
 213         //   --strip-native-debug-symbols objcopy=</objcpy/path>:keep-debug-info
 214         //   --strip-native-debug-symbols objcopy=</objcpy/path>:omit-debug-info
 215         String keepDebugInfo = config.get(KEEP_DEBUG_INFO_ARG);
 216         if (keepDebugInfo != null) {
 217             hasKeepDebugInfo = true;
 218             debuginfoExt = keepDebugInfo;
 219         }
 220         if (hasKeepDebugInfo && hasOmitDebugInfo) {
 221             // cannot keep and omit debug info at the same time
 222             throw new IllegalArgumentException(
 223                     PluginsResourceBundle.getMessage(resourceBundle,
 224                                                      NAME + ".iae.conflict",
 225                                                      NAME,
 226                                                      OMIT_DEBUG_INFO_ARG,
 227                                                      KEEP_DEBUG_INFO_ARG));
 228         }
 229         includeDebugSymbols = hasKeepDebugInfo;
 230     }
 231 
 232     private StrippedDebugInfoBinary stripBinary(ResourcePoolEntry resource) {
 233         Path tempDir = null;
 234         try {
 235             Path resPath = Paths.get(resource.path());
 236             String relativeFileName = resPath.getFileName().toString();
 237             tempDir = Files.createTempDirectory(NAME + relativeFileName);
 238             Path resourceFileBinary = tempDir.resolve(relativeFileName);
 239             String relativeDbgFileName = relativeFileName + "." + debuginfoExt;
 240 
 241             Files.write(resourceFileBinary, resource.contentBytes());
 242             StrippedDebugInfoBinary strippedBin = new StrippedDebugInfoBinary();
 243             Path resourceFileDebugSymbols;
 244             if (includeDebugSymbols) {
 245                 resourceFileDebugSymbols = tempDir.resolve(Paths.get(relativeDbgFileName));
 246                 strippedBin.debugSymbols = createDebugSymbolsFile(resourceFileBinary,
 247                                                                   resourceFileDebugSymbols,
 248                                                                   relativeDbgFileName);
 249             }
 250             if (!stripBinary(resourceFileBinary)) {
 251                 return null;
 252             }
 253             if (includeDebugSymbols && !addGnuDebugLink(tempDir,
 254                                                         relativeFileName,
 255                                                         relativeDbgFileName)) {
 256                 return null;
 257             }
 258             strippedBin.strippedBinary = Files.readAllBytes(resourceFileBinary);
 259             return strippedBin;
 260         } catch (IOException | InterruptedException e) {
 261             throw new PluginException(e);
 262         } finally {
 263             if (tempDir != null) {
 264                 deleteDirRecursivelyIgnoreResult(tempDir);
 265             }
 266         }
 267     }
 268 
 269     /*
 270      *  Equivalent of 'objcopy -g binFile'. Returning true iff stripping of the binary
 271      *  succeeded.
 272      */
 273     private boolean stripBinary(Path binFile)
 274             throws InterruptedException, IOException {
 275         String filePath = binFile.toFile().getAbsolutePath();
 276         List<String> stripCmdLine = buildCmdLine(STRIP_DEBUG_SYMS_OPT,
 277                                                  filePath);
 278         ProcessBuilder builder = new ProcessBuilder(stripCmdLine);
 279         Process stripProc = builder.start();
 280         int retval = stripProc.waitFor();
 281         return retval == 0;
 282     }
 283 
 284     private List<String> buildCmdLine(String ...opts) {
 285         List<String> objCopyCmd = cmdBuilder.build(stripBin);
 286         objCopyCmd.addAll(Arrays.asList(opts));
 287         return objCopyCmd;
 288     }
 289 
 290     private void deleteDirRecursivelyIgnoreResult(Path tempDir) {
 291         try {
 292             Files.walkFileTree(tempDir, new SimpleFileVisitor<Path>() {
 293                 @Override
 294                 public FileVisitResult visitFile(Path file,
 295                         BasicFileAttributes attrs) throws IOException {
 296                     Files.delete(file);
 297                     return FileVisitResult.CONTINUE;
 298                 }
 299 
 300                 @Override
 301                 public FileVisitResult postVisitDirectory(Path dir,
 302                         IOException exc) throws IOException {
 303                     Files.delete(dir);
 304                     return FileVisitResult.CONTINUE;
 305                 }
 306             });
 307         } catch (IOException e) {
 308             // ignore deleting the temp dir
 309         }
 310     }
 311 
 312     /*
 313      *  Equivalent of 'objcopy --add-gnu-debuglink=relativeDbgFileName binFile'.
 314      *  Returning true iff adding the debug link succeeded.
 315      */
 316     private boolean addGnuDebugLink(Path currDir,
 317                                     String binFile,
 318                                     String relativeDbgFileName)
 319                                             throws InterruptedException, IOException {
 320         List<String> stripCmdLine = buildCmdLine(ADD_DEBUG_LINK_OPT +
 321                                                  "=" + relativeDbgFileName,
 322                                                  binFile);
 323         ProcessBuilder builder = new ProcessBuilder(stripCmdLine);
 324         builder.directory(currDir.toFile());
 325         Process stripProc = builder.start();
 326         int retval = stripProc.waitFor();
 327         return retval == 0;
 328 
 329     }
 330 
 331     /*
 332      *  Equivalent of 'objcopy --only-keep-debug binPath debugPath'.
 333      *  Returning the bytes of the file containing debug symbols.
 334      */
 335     private byte[] createDebugSymbolsFile(Path binPath,
 336                                           Path debugPath,
 337                                           String dbgFileName) throws InterruptedException,
 338                                                                      IOException {
 339         String filePath = binPath.toFile().getAbsolutePath();
 340         String dbgPath = debugPath.toFile().getAbsolutePath();
 341         List<String> stripCmdLine = buildCmdLine(ONLY_KEEP_DEBUG_SYMS_OPT,
 342                                                  filePath,
 343                                                  dbgPath);
 344         ProcessBuilder builder = new ProcessBuilder(stripCmdLine);
 345         Process stripProc = builder.start();
 346         int retval = stripProc.waitFor();
 347         if (retval != 0) {
 348             return null;
 349         } else {
 350             return Files.readAllBytes(debugPath);
 351         }
 352     }
 353 
 354     private void validateStripArg(String stripArg) throws IllegalArgumentException {
 355         try {
 356             Path strip = Paths.get(stripArg); // verify it's a resonable path
 357             if (!Files.isExecutable(strip)) {
 358                 throw new IllegalArgumentException(
 359                         PluginsResourceBundle.getMessage(resourceBundle,
 360                                                          NAME + ".invalidstrip",
 361                                                          stripArg));
 362             }
 363         } catch (InvalidPathException e) {
 364             throw new IllegalArgumentException(
 365                     PluginsResourceBundle.getMessage(resourceBundle,
 366                                                      NAME + ".invalidstrip",
 367                                                      e.getInput()));
 368         }
 369     }
 370 
 371     private static class StrippedDebugInfoBinary {
 372         byte[] strippedBinary;
 373         byte[] debugSymbols;
 374     }
 375 
 376     // For better testing using mocked objcopy
 377     public static interface ObjCopyCmdBuilder {
 378         List<String> build(String objCopy);
 379     }
 380 
 381     private static final class DefaultObjCopyCmdBuilder implements ObjCopyCmdBuilder {
 382 
 383         @Override
 384         public List<String> build(String objCopy) {
 385             List<String> cmdList = new ArrayList<>();
 386             cmdList.add(objCopy);
 387             return cmdList;
 388         }
 389 
 390     }
 391 
 392 }