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.lang.ProcessBuilder.Redirect;
29 import java.nio.file.FileVisitResult;
30 import java.nio.file.Files;
31 import java.nio.file.InvalidPathException;
32 import java.nio.file.Path;
33 import java.nio.file.Paths;
34 import java.nio.file.SimpleFileVisitor;
35 import java.nio.file.attribute.BasicFileAttributes;
36 import java.util.ArrayList;
37 import java.util.Arrays;
38 import java.util.HashMap;
39 import java.util.List;
40 import java.util.Locale;
41 import java.util.Map;
42 import java.util.MissingResourceException;
43 import java.util.Objects;
44 import java.util.Optional;
45 import java.util.ResourceBundle;
46 
47 import jdk.tools.jlink.plugin.Plugin;
48 import jdk.tools.jlink.plugin.PluginException;
49 import jdk.tools.jlink.plugin.ResourcePool;
50 import jdk.tools.jlink.plugin.ResourcePoolBuilder;
51 import jdk.tools.jlink.plugin.ResourcePoolEntry;
52 
53 /**
54  * Platform specific jlink plugin for stripping debug symbols from native
55  * libraries and binaries.
56  *
57  */
58 public final class StripNativeDebugSymbolsPlugin implements Plugin {
59 
60     public static final String NAME = "strip-native-debug-symbols";
61     private static final boolean DEBUG = Boolean.getBoolean("jlink.debug");
62     private static final String DEFAULT_STRIP_CMD = "objcopy";
63     private static final String STRIP_CMD_ARG = DEFAULT_STRIP_CMD;
64     private static final String KEEP_DEBUG_INFO_ARG = "keep-debuginfo-files";
65     private static final String EXCLUDE_DEBUG_INFO_ARG = "exclude-debuginfo-files";
66     private static final String DEFAULT_DEBUG_EXT = "debuginfo";
67     private static final String STRIP_DEBUG_SYMS_OPT = "-g";
68     private static final String ONLY_KEEP_DEBUG_SYMS_OPT = "--only-keep-debug";
69     private static final String ADD_DEBUG_LINK_OPT = "--add-gnu-debuglink";
70     private static final ResourceBundle resourceBundle;
71     private static final String SHARED_LIBS_EXT = ".so"; // for Linux/Unix
72 
73     static {
74         Locale locale = Locale.getDefault();
75         try {
76             resourceBundle = ResourceBundle.getBundle("jdk.tools.jlink."
77                     + "resources.strip_native_debug_symbols_plugin", locale);
78         } catch (MissingResourceException e) {
79             throw new InternalError("Cannot find jlink plugin resource bundle (" +
80                         NAME + ") for locale " + locale);
81         }
82     }
83 
84     private final ObjCopyCmdBuilder cmdBuilder;
85     private boolean includeDebugSymbols;
86     private String stripBin;
87     private String debuginfoExt;
88 
StripNativeDebugSymbolsPlugin()89     public StripNativeDebugSymbolsPlugin() {
90         this(new DefaultObjCopyCmdBuilder());
91     }
92 
StripNativeDebugSymbolsPlugin(ObjCopyCmdBuilder cmdBuilder)93     public StripNativeDebugSymbolsPlugin(ObjCopyCmdBuilder cmdBuilder) {
94         this.cmdBuilder = cmdBuilder;
95     }
96 
97     @Override
getName()98     public String getName() {
99         return NAME;
100     }
101 
102     @Override
transform(ResourcePool in, ResourcePoolBuilder out)103     public ResourcePool transform(ResourcePool in, ResourcePoolBuilder out) {
104         StrippedDebugInfoBinaryBuilder builder = new StrippedDebugInfoBinaryBuilder(
105                                                         includeDebugSymbols,
106                                                         debuginfoExt,
107                                                         cmdBuilder,
108                                                         stripBin);
109         in.transformAndCopy((resource) -> {
110             ResourcePoolEntry res = resource;
111             if ((resource.type() == ResourcePoolEntry.Type.NATIVE_LIB &&
112                  resource.path().endsWith(SHARED_LIBS_EXT)) ||
113                 resource.type() == ResourcePoolEntry.Type.NATIVE_CMD) {
114                 Optional<StrippedDebugInfoBinary> strippedBin = builder.build(resource);
115                 if (strippedBin.isPresent()) {
116                     StrippedDebugInfoBinary sb = strippedBin.get();
117                     res = sb.strippedBinary();
118                     if (includeDebugSymbols) {
119                         Optional<ResourcePoolEntry> debugInfo = sb.debugSymbols();
120                         if (debugInfo.isEmpty()) {
121                             String key = NAME + ".error.debugfile";
122                             logError(resource, key);
123                         } else {
124                             out.add(debugInfo.get());
125                         }
126                     }
127                 } else {
128                     String key = NAME + ".error.file";
129                     logError(resource, key);
130                 }
131             }
132             return res;
133         }, out);
134 
135         return out.build();
136     }
137 
logError(ResourcePoolEntry resource, String msgKey)138     private void logError(ResourcePoolEntry resource, String msgKey) {
139         String msg = PluginsResourceBundle.getMessage(resourceBundle,
140                                                       msgKey,
141                                                       NAME,
142                                                       resource.path());
143         System.err.println(msg);
144     }
145 
146     @Override
getType()147     public Category getType() {
148         return Category.TRANSFORMER;
149     }
150 
151     @Override
getDescription()152     public String getDescription() {
153         String key = NAME + ".description";
154         return PluginsResourceBundle.getMessage(resourceBundle, key);
155     }
156 
157     @Override
hasArguments()158     public boolean hasArguments() {
159         return true;
160     }
161 
162     @Override
getArgumentsDescription()163     public String getArgumentsDescription() {
164         String key = NAME + ".argument";
165         return PluginsResourceBundle.getMessage(resourceBundle, key);
166     }
167 
168     @Override
configure(Map<String, String> config)169     public void configure(Map<String, String> config) {
170         doConfigure(true, config);
171     }
172 
173     // For testing so that validation can be turned off
doConfigure(boolean withChecks, Map<String, String> orig)174     public void doConfigure(boolean withChecks, Map<String, String> orig) {
175         Map<String, String> config = new HashMap<>(orig);
176         String arg = config.remove(NAME);
177 
178         stripBin = DEFAULT_STRIP_CMD;
179         debuginfoExt = DEFAULT_DEBUG_EXT;
180 
181         // argument must never be null as it requires at least one
182         // argument, since hasArguments() == true. This might change once
183         // 8218761 is implemented.
184         if (arg == null) {
185             throw new InternalError();
186         }
187         boolean hasOmitDebugInfo = false;
188         boolean hasKeepDebugInfo = false;
189 
190         if (KEEP_DEBUG_INFO_ARG.equals(arg)) {
191             // Case: --strip-native-debug-symbols keep-debuginfo-files
192             hasKeepDebugInfo = true;
193         } else if (arg.startsWith(KEEP_DEBUG_INFO_ARG)) {
194             // Case: --strip-native-debug-symbols keep-debuginfo-files=foo
195             String[] tokens = arg.split("=");
196             if (tokens.length != 2 || !KEEP_DEBUG_INFO_ARG.equals(tokens[0])) {
197                 throw new IllegalArgumentException(
198                         PluginsResourceBundle.getMessage(resourceBundle,
199                                                          NAME + ".iae", NAME, arg));
200             }
201             hasKeepDebugInfo = true;
202             debuginfoExt = tokens[1];
203         }
204         if (EXCLUDE_DEBUG_INFO_ARG.equals(arg) || arg.startsWith(EXCLUDE_DEBUG_INFO_ARG + "=")) {
205             // Case: --strip-native-debug-symbols exclude-debuginfo-files[=something]
206             hasOmitDebugInfo = true;
207         }
208         if (arg.startsWith(STRIP_CMD_ARG)) {
209             // Case: --strip-native-debug-symbols objcopy=<path/to/objcopy
210             String[] tokens = arg.split("=");
211             if (tokens.length != 2 || !STRIP_CMD_ARG.equals(tokens[0])) {
212                 throw new IllegalArgumentException(
213                         PluginsResourceBundle.getMessage(resourceBundle,
214                                                          NAME + ".iae", NAME, arg));
215             }
216             if (withChecks) {
217                 validateStripArg(tokens[1]);
218             }
219             stripBin = tokens[1];
220         }
221         // Cases (combination of options):
222         //   --strip-native-debug-symbols keep-debuginfo-files:objcopy=</objcpy/path>
223         //   --strip-native-debug-symbols keep-debuginfo-files=ext:objcopy=</objcpy/path>
224         //   --strip-native-debug-symbols exclude-debuginfo-files:objcopy=</objcpy/path>
225         String stripArg = config.remove(STRIP_CMD_ARG);
226         if (stripArg != null && withChecks) {
227             validateStripArg(stripArg);
228         }
229         if (stripArg != null) {
230             stripBin = stripArg;
231         }
232         // Case (reversed combination)
233         //   --strip-native-debug-symbols objcopy=</objcpy/path>:keep-debuginfo-files=ext
234         // Note: cases like the following are not allowed by the parser
235         //   --strip-native-debug-symbols objcopy=</objcpy/path>:keep-debuginfo-files
236         //   --strip-native-debug-symbols objcopy=</objcpy/path>:exclude-debuginfo-files
237         String keepDebugInfo = config.remove(KEEP_DEBUG_INFO_ARG);
238         if (keepDebugInfo != null) {
239             hasKeepDebugInfo = true;
240             debuginfoExt = keepDebugInfo;
241         }
242         if ((hasKeepDebugInfo || includeDebugSymbols) && hasOmitDebugInfo) {
243             // Cannot keep and omit debug info at the same time. Note that
244             // includeDebugSymbols might already be true if configure is being run
245             // on the same plugin instance multiple times. Plugin option can
246             // repeat.
247             throw new IllegalArgumentException(
248                     PluginsResourceBundle.getMessage(resourceBundle,
249                                                      NAME + ".iae.conflict",
250                                                      NAME,
251                                                      EXCLUDE_DEBUG_INFO_ARG,
252                                                      KEEP_DEBUG_INFO_ARG));
253         }
254         if (!arg.startsWith(STRIP_CMD_ARG) &&
255             !arg.startsWith(KEEP_DEBUG_INFO_ARG) &&
256             !arg.startsWith(EXCLUDE_DEBUG_INFO_ARG)) {
257             // unknown arg value; case --strip-native-debug-symbols foobar
258             throw new IllegalArgumentException(
259                     PluginsResourceBundle.getMessage(resourceBundle,
260                                                      NAME + ".iae", NAME, arg));
261         }
262         if (!config.isEmpty()) {
263             // extraneous values; --strip-native-debug-symbols keep-debuginfo-files:foo=bar
264             throw new IllegalArgumentException(
265                     PluginsResourceBundle.getMessage(resourceBundle,
266                                                      NAME + ".iae", NAME,
267                                                      config.toString()));
268         }
269         includeDebugSymbols = hasKeepDebugInfo;
270     }
271 
validateStripArg(String stripArg)272     private void validateStripArg(String stripArg) throws IllegalArgumentException {
273         try {
274             Path strip = Paths.get(stripArg); // verify it's a resonable path
275             if (!Files.isExecutable(strip)) {
276                 throw new IllegalArgumentException(
277                         PluginsResourceBundle.getMessage(resourceBundle,
278                                                          NAME + ".invalidstrip",
279                                                          stripArg));
280             }
281         } catch (InvalidPathException e) {
282             throw new IllegalArgumentException(
283                     PluginsResourceBundle.getMessage(resourceBundle,
284                                                      NAME + ".invalidstrip",
285                                                      e.getInput()));
286         }
287     }
288 
289     private static class StrippedDebugInfoBinaryBuilder {
290 
291         private final boolean includeDebug;
292         private final String debugExt;
293         private final ObjCopyCmdBuilder cmdBuilder;
294         private final String strip;
295 
StrippedDebugInfoBinaryBuilder(boolean includeDebug, String debugExt, ObjCopyCmdBuilder cmdBuilder, String strip)296         private StrippedDebugInfoBinaryBuilder(boolean includeDebug,
297                                                String debugExt,
298                                                ObjCopyCmdBuilder cmdBuilder,
299                                                String strip) {
300             this.includeDebug = includeDebug;
301             this.debugExt = debugExt;
302             this.cmdBuilder = cmdBuilder;
303             this.strip = strip;
304         }
305 
build(ResourcePoolEntry resource)306         private Optional<StrippedDebugInfoBinary> build(ResourcePoolEntry resource) {
307             Path tempDir = null;
308             Optional<ResourcePoolEntry> debugInfo = Optional.empty();
309             try {
310                 Path resPath = Paths.get(resource.path());
311                 String relativeFileName = resPath.getFileName().toString();
312                 tempDir = Files.createTempDirectory(NAME + relativeFileName);
313                 Path resourceFileBinary = tempDir.resolve(relativeFileName);
314                 String relativeDbgFileName = relativeFileName + "." + debugExt;
315 
316                 Files.write(resourceFileBinary, resource.contentBytes());
317                 Path resourceFileDebugSymbols;
318                 if (includeDebug) {
319                     resourceFileDebugSymbols = tempDir.resolve(Paths.get(relativeDbgFileName));
320                     String debugEntryPath = resource.path() + "." + debugExt;
321                     byte[] debugInfoBytes = createDebugSymbolsFile(resourceFileBinary,
322                                            resourceFileDebugSymbols,
323                                            relativeDbgFileName);
324                     if (debugInfoBytes != null) {
325                         ResourcePoolEntry debugEntry = ResourcePoolEntry.create(
326                                                                 debugEntryPath,
327                                                                 resource.type(),
328                                                                 debugInfoBytes);
329                         debugInfo = Optional.of(debugEntry);
330                     }
331                 }
332                 if (!stripBinary(resourceFileBinary)) {
333                     if (DEBUG) {
334                         System.err.println("DEBUG: Stripping debug info failed.");
335                     }
336                     return Optional.empty();
337                 }
338                 if (includeDebug && !addGnuDebugLink(tempDir,
339                                                      relativeFileName,
340                                                      relativeDbgFileName)) {
341                     if (DEBUG) {
342                         System.err.println("DEBUG: Creating debug link failed.");
343                     }
344                     return Optional.empty();
345                 }
346                 byte[] strippedBytes = Files.readAllBytes(resourceFileBinary);
347                 ResourcePoolEntry strippedResource = resource.copyWithContent(strippedBytes);
348                 return Optional.of(new StrippedDebugInfoBinary(strippedResource, debugInfo));
349             } catch (IOException | InterruptedException e) {
350                 throw new PluginException(e);
351             } finally {
352                 if (tempDir != null) {
353                     deleteDirRecursivelyIgnoreResult(tempDir);
354                 }
355             }
356         }
357 
358         /*
359          *  Equivalent of 'objcopy -g binFile'. Returning true iff stripping of the binary
360          *  succeeded.
361          */
stripBinary(Path binFile)362         private boolean stripBinary(Path binFile)
363                 throws InterruptedException, IOException {
364             String filePath = binFile.toAbsolutePath().toString();
365             List<String> stripCmdLine = cmdBuilder.build(strip, STRIP_DEBUG_SYMS_OPT,
366                                                      filePath);
367             ProcessBuilder builder = createProcessBuilder(stripCmdLine);
368             Process stripProc = builder.start();
369             int retval = stripProc.waitFor();
370             return retval == 0;
371         }
372 
373         /*
374          *  Equivalent of 'objcopy --add-gnu-debuglink=relativeDbgFileName binFile'.
375          *  Returning true iff adding the debug link succeeded.
376          */
addGnuDebugLink(Path currDir, String binFile, String relativeDbgFileName)377         private boolean addGnuDebugLink(Path currDir,
378                                         String binFile,
379                                         String relativeDbgFileName)
380                                                 throws InterruptedException, IOException {
381             List<String> addDbgLinkCmdLine = cmdBuilder.build(strip, ADD_DEBUG_LINK_OPT +
382                                                      "=" + relativeDbgFileName,
383                                                      binFile);
384             ProcessBuilder builder = createProcessBuilder(addDbgLinkCmdLine);
385             builder.directory(currDir.toFile());
386             Process stripProc = builder.start();
387             int retval = stripProc.waitFor();
388             return retval == 0;
389 
390         }
391 
392         /*
393          *  Equivalent of 'objcopy --only-keep-debug binPath debugPath'.
394          *  Returning the bytes of the file containing debug symbols.
395          */
createDebugSymbolsFile(Path binPath, Path debugPath, String dbgFileName)396         private byte[] createDebugSymbolsFile(Path binPath,
397                                               Path debugPath,
398                                               String dbgFileName) throws InterruptedException,
399                                                                          IOException {
400             String filePath = binPath.toAbsolutePath().toString();
401             String dbgPath = debugPath.toAbsolutePath().toString();
402             List<String> createLinkCmdLine = cmdBuilder.build(strip,
403                                                      ONLY_KEEP_DEBUG_SYMS_OPT,
404                                                      filePath,
405                                                      dbgPath);
406             ProcessBuilder builder = createProcessBuilder(createLinkCmdLine);
407             Process stripProc = builder.start();
408             int retval = stripProc.waitFor();
409             if (retval != 0) {
410                 if (DEBUG) {
411                     System.err.println("DEBUG: Creating debuginfo file failed.");
412                 }
413                 return null;
414             } else {
415                 return Files.readAllBytes(debugPath);
416             }
417         }
418 
createProcessBuilder(List<String> cmd)419         private ProcessBuilder createProcessBuilder(List<String> cmd) {
420             ProcessBuilder builder = new ProcessBuilder(cmd);
421             builder.redirectError(Redirect.INHERIT);
422             builder.redirectOutput(Redirect.INHERIT);
423             return builder;
424         }
425 
deleteDirRecursivelyIgnoreResult(Path tempDir)426         private void deleteDirRecursivelyIgnoreResult(Path tempDir) {
427             try {
428                 Files.walkFileTree(tempDir, new SimpleFileVisitor<Path>() {
429                     @Override
430                     public FileVisitResult visitFile(Path file,
431                             BasicFileAttributes attrs) throws IOException {
432                         Files.delete(file);
433                         return FileVisitResult.CONTINUE;
434                     }
435 
436                     @Override
437                     public FileVisitResult postVisitDirectory(Path dir,
438                             IOException exc) throws IOException {
439                         Files.delete(dir);
440                         return FileVisitResult.CONTINUE;
441                     }
442                 });
443             } catch (IOException e) {
444                 // ignore deleting the temp dir
445             }
446         }
447 
448     }
449 
450     private static class StrippedDebugInfoBinary {
451         private final ResourcePoolEntry strippedBinary;
452         private final Optional<ResourcePoolEntry> debugSymbols;
453 
StrippedDebugInfoBinary(ResourcePoolEntry strippedBinary, Optional<ResourcePoolEntry> debugSymbols)454         private StrippedDebugInfoBinary(ResourcePoolEntry strippedBinary,
455                                         Optional<ResourcePoolEntry> debugSymbols) {
456             this.strippedBinary = Objects.requireNonNull(strippedBinary);
457             this.debugSymbols = Objects.requireNonNull(debugSymbols);
458         }
459 
strippedBinary()460         public ResourcePoolEntry strippedBinary() {
461             return strippedBinary;
462         }
463 
debugSymbols()464         public Optional<ResourcePoolEntry> debugSymbols() {
465             return debugSymbols;
466         }
467     }
468 
469     // For better testing using mocked objcopy
470     public static interface ObjCopyCmdBuilder {
build(String objCopy, String...options)471         List<String> build(String objCopy, String...options);
472     }
473 
474     private static final class DefaultObjCopyCmdBuilder implements ObjCopyCmdBuilder {
475 
476         @Override
build(String objCopy, String...options)477         public List<String> build(String objCopy, String...options) {
478             List<String> cmdList = new ArrayList<>();
479             cmdList.add(objCopy);
480             if (options.length > 0) {
481                 cmdList.addAll(Arrays.asList(options));
482             }
483             return cmdList;
484         }
485 
486     }
487 
488 }
489