View Javadoc
1   /*
2    * Copyright 2014 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.util.internal;
17  
18  import io.netty.util.CharsetUtil;
19  import io.netty.util.internal.logging.InternalLogger;
20  import io.netty.util.internal.logging.InternalLoggerFactory;
21  
22  import java.io.ByteArrayOutputStream;
23  import java.io.Closeable;
24  import java.io.File;
25  import java.io.FileNotFoundException;
26  import java.io.FileOutputStream;
27  import java.io.IOException;
28  import java.io.InputStream;
29  import java.io.OutputStream;
30  import java.lang.reflect.Method;
31  import java.net.URL;
32  import java.nio.file.Files;
33  import java.nio.file.attribute.PosixFilePermission;
34  import java.security.AccessController;
35  import java.security.MessageDigest;
36  import java.security.NoSuchAlgorithmException;
37  import java.security.PrivilegedAction;
38  import java.util.ArrayList;
39  import java.util.Arrays;
40  import java.util.Collections;
41  import java.util.EnumSet;
42  import java.util.Enumeration;
43  import java.util.List;
44  import java.util.Set;
45  
46  /**
47   * Helper class to load JNI resources.
48   *
49   */
50  public final class NativeLibraryLoader {
51  
52      private static final InternalLogger logger = InternalLoggerFactory.getInstance(NativeLibraryLoader.class);
53  
54      private static final String NATIVE_RESOURCE_HOME = "META-INF/native/";
55      private static final File WORKDIR;
56      private static final boolean DELETE_NATIVE_LIB_AFTER_LOADING;
57      private static final boolean TRY_TO_PATCH_SHADED_ID;
58      private static final boolean DETECT_NATIVE_LIBRARY_DUPLICATES;
59  
60      // Just use a-Z and numbers as valid ID bytes.
61      private static final byte[] UNIQUE_ID_BYTES =
62              "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes(CharsetUtil.US_ASCII);
63  
64      static {
65          String workdir = SystemPropertyUtil.get("io.netty.native.workdir");
66          if (workdir != null) {
67              File f = new File(workdir);
68              if (!f.exists() && !f.mkdirs()) {
69                  throw new ExceptionInInitializerError(
70                      new IOException("Custom native workdir mkdirs failed: " + workdir));
71              }
72  
73              try {
74                  f = f.getAbsoluteFile();
75              } catch (Exception ignored) {
76                  // Good to have an absolute path, but it's OK.
77              }
78  
79              WORKDIR = f;
80              logger.debug("-Dio.netty.native.workdir: " + WORKDIR);
81          } else {
82              WORKDIR = PlatformDependent.tmpdir();
83              logger.debug("-Dio.netty.native.workdir: " + WORKDIR + " (io.netty.tmpdir)");
84          }
85  
86          DELETE_NATIVE_LIB_AFTER_LOADING = SystemPropertyUtil.getBoolean(
87                  "io.netty.native.deleteLibAfterLoading", true);
88          logger.debug("-Dio.netty.native.deleteLibAfterLoading: {}", DELETE_NATIVE_LIB_AFTER_LOADING);
89  
90          TRY_TO_PATCH_SHADED_ID = SystemPropertyUtil.getBoolean(
91                  "io.netty.native.tryPatchShadedId", true);
92          logger.debug("-Dio.netty.native.tryPatchShadedId: {}", TRY_TO_PATCH_SHADED_ID);
93  
94          DETECT_NATIVE_LIBRARY_DUPLICATES = SystemPropertyUtil.getBoolean(
95                  "io.netty.native.detectNativeLibraryDuplicates", true);
96          logger.debug("-Dio.netty.native.detectNativeLibraryDuplicates: {}", DETECT_NATIVE_LIBRARY_DUPLICATES);
97      }
98  
99      /**
100      * Loads the first available library in the collection with the specified
101      * {@link ClassLoader}.
102      *
103      * @throws IllegalArgumentException
104      *         if none of the given libraries load successfully.
105      */
106     public static void loadFirstAvailable(ClassLoader loader, String... names) {
107         List<Throwable> suppressed = new ArrayList<Throwable>();
108         for (String name : names) {
109             try {
110                 load(name, loader);
111                 logger.debug("Loaded library with name '{}'", name);
112                 return;
113             } catch (Throwable t) {
114                 suppressed.add(t);
115             }
116         }
117 
118         IllegalArgumentException iae =
119                 new IllegalArgumentException("Failed to load any of the given libraries: " + Arrays.toString(names));
120         ThrowableUtil.addSuppressedAndClear(iae, suppressed);
121         throw iae;
122     }
123 
124     /**
125      * Calculates the mangled shading prefix added to this class's full name.
126      *
127      * <p>This method mangles the package name as follows, so we can unmangle it back later:
128      * <ul>
129      *   <li>{@code _} to {@code _1}</li>
130      *   <li>{@code .} to {@code _}</li>
131      * </ul>
132      *
133      * <p>Note that we don't mangle non-ASCII characters here because it's extremely unlikely to have
134      * a non-ASCII character in a package name. For more information, see:
135      * <ul>
136      *   <li><a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html">JNI
137      *       specification</a></li>
138      *   <li>{@code parsePackagePrefix()} in {@code netty_jni_util.c}.</li>
139      * </ul>
140      *
141      * @throws UnsatisfiedLinkError if the shader used something other than a prefix
142      */
143     private static String calculateMangledPackagePrefix() {
144         String maybeShaded = NativeLibraryLoader.class.getName();
145         // Use ! instead of . to avoid shading utilities from modifying the string
146         String expected = "io!netty!util!internal!NativeLibraryLoader".replace('!', '.');
147         if (!maybeShaded.endsWith(expected)) {
148             throw new UnsatisfiedLinkError(String.format(
149                     "Could not find prefix added to %s to get %s. When shading, only adding a "
150                     + "package prefix is supported", expected, maybeShaded));
151         }
152         return maybeShaded.substring(0, maybeShaded.length() - expected.length())
153                           .replace("_", "_1")
154                           .replace('.', '_');
155     }
156 
157     /**
158      * Load the given library with the specified {@link ClassLoader}
159      */
160     public static void load(String originalName, ClassLoader loader) {
161         String mangledPackagePrefix = calculateMangledPackagePrefix();
162         String name = mangledPackagePrefix + originalName;
163         List<Throwable> suppressed = new ArrayList<Throwable>();
164         try {
165             // first try to load from java.library.path
166             loadLibrary(loader, name, false);
167             return;
168         } catch (Throwable ex) {
169             suppressed.add(ex);
170         }
171 
172         String libname = System.mapLibraryName(name);
173         String path = NATIVE_RESOURCE_HOME + libname;
174 
175         InputStream in = null;
176         OutputStream out = null;
177         File tmpFile = null;
178         URL url = getResource(path, loader);
179         try {
180             if (url == null) {
181                 if (PlatformDependent.isOsx()) {
182                     String fileName = path.endsWith(".jnilib") ? NATIVE_RESOURCE_HOME + "lib" + name + ".dynlib" :
183                             NATIVE_RESOURCE_HOME + "lib" + name + ".jnilib";
184                     url = getResource(fileName, loader);
185                     if (url == null) {
186                         FileNotFoundException fnf = new FileNotFoundException(fileName);
187                         ThrowableUtil.addSuppressedAndClear(fnf, suppressed);
188                         throw fnf;
189                     }
190                 } else {
191                     FileNotFoundException fnf = new FileNotFoundException(path);
192                     ThrowableUtil.addSuppressedAndClear(fnf, suppressed);
193                     throw fnf;
194                 }
195             }
196 
197             int index = libname.lastIndexOf('.');
198             String prefix = libname.substring(0, index);
199             String suffix = libname.substring(index);
200 
201             tmpFile = PlatformDependent.createTempFile(prefix, suffix, WORKDIR);
202             in = url.openStream();
203             out = new FileOutputStream(tmpFile);
204 
205             byte[] buffer = new byte[8192];
206             int length;
207             while ((length = in.read(buffer)) > 0) {
208                 out.write(buffer, 0, length);
209             }
210             out.flush();
211 
212             if (shouldShadedLibraryIdBePatched(mangledPackagePrefix)) {
213                 // Let's try to patch the id and re-sign it. This is a best-effort and might fail if a
214                 // SecurityManager is setup or the right executables are not installed :/
215                 tryPatchShadedLibraryIdAndSign(tmpFile, originalName);
216             }
217 
218             // Close the output stream before loading the unpacked library,
219             // because otherwise Windows will refuse to load it when it's in use by other process.
220             closeQuietly(out);
221             out = null;
222 
223             loadLibrary(loader, tmpFile.getPath(), true);
224         } catch (UnsatisfiedLinkError e) {
225             try {
226                 if (tmpFile != null && tmpFile.isFile() && tmpFile.canRead() &&
227                     !NoexecVolumeDetector.canExecuteExecutable(tmpFile)) {
228                     // Pass "io.netty.native.workdir" as an argument to allow shading tools to see
229                     // the string. Since this is printed out to users to tell them what to do next,
230                     // we want the value to be correct even when shading.
231                     logger.info("{} exists but cannot be executed even when execute permissions set; " +
232                                 "check volume for \"noexec\" flag; use -D{}=[path] " +
233                                 "to set native working directory separately.",
234                                 tmpFile.getPath(), "io.netty.native.workdir");
235                 }
236             } catch (Throwable t) {
237                 suppressed.add(t);
238                 logger.debug("Error checking if {} is on a file store mounted with noexec", tmpFile, t);
239             }
240             // Re-throw to fail the load
241             ThrowableUtil.addSuppressedAndClear(e, suppressed);
242             throw e;
243         } catch (Exception e) {
244             UnsatisfiedLinkError ule = new UnsatisfiedLinkError("could not load a native library: " + name);
245             ule.initCause(e);
246             ThrowableUtil.addSuppressedAndClear(ule, suppressed);
247             throw ule;
248         } finally {
249             closeQuietly(in);
250             closeQuietly(out);
251             // After we load the library it is safe to delete the file.
252             // We delete the file immediately to free up resources as soon as possible,
253             // and if this fails fallback to deleting on JVM exit.
254             if (tmpFile != null && (!DELETE_NATIVE_LIB_AFTER_LOADING || !tmpFile.delete())) {
255                 tmpFile.deleteOnExit();
256             }
257         }
258     }
259 
260     private static URL getResource(String path, ClassLoader loader) {
261         final Enumeration<URL> urls;
262         try {
263             if (loader == null) {
264                 urls = ClassLoader.getSystemResources(path);
265             } else {
266                 urls = loader.getResources(path);
267             }
268         } catch (IOException iox) {
269             throw new RuntimeException("An error occurred while getting the resources for " + path, iox);
270         }
271 
272         List<URL> urlsList = Collections.list(urls);
273         int size = urlsList.size();
274         switch (size) {
275             case 0:
276                 return null;
277             case 1:
278                 return urlsList.get(0);
279             default:
280                 if (DETECT_NATIVE_LIBRARY_DUPLICATES) {
281                     try {
282                         MessageDigest md = MessageDigest.getInstance("SHA-256");
283                         // We found more than 1 resource with the same name. Let's check if the content of the file is
284                         // the same as in this case it will not have any bad effect.
285                         URL url = urlsList.get(0);
286                         byte[] digest = digest(md, url);
287                         boolean allSame = true;
288                         if (digest != null) {
289                             for (int i = 1; i < size; i++) {
290                                 byte[] digest2 = digest(md, urlsList.get(i));
291                                 if (digest2 == null || !Arrays.equals(digest, digest2)) {
292                                     allSame = false;
293                                     break;
294                                 }
295                             }
296                         } else {
297                             allSame = false;
298                         }
299                         if (allSame) {
300                             return url;
301                         }
302                     } catch (NoSuchAlgorithmException e) {
303                         logger.debug("Don't support SHA-256, can't check if resources have same content.", e);
304                     }
305 
306                     throw new IllegalStateException(
307                             "Multiple resources found for '" + path + "' with different content: " + urlsList);
308                 } else {
309                     logger.warn("Multiple resources found for '" + path + "' with different content: " +
310                             urlsList + ". Please fix your dependency graph.");
311                     return urlsList.get(0);
312                 }
313         }
314     }
315 
316     private static byte[] digest(MessageDigest digest, URL url) {
317         InputStream in = null;
318         try {
319             in = url.openStream();
320             byte[] bytes = new byte[8192];
321             int i;
322             while ((i = in.read(bytes)) != -1) {
323                 digest.update(bytes, 0, i);
324             }
325             return digest.digest();
326         } catch (IOException e) {
327             logger.debug("Can't read resource.", e);
328             return null;
329         } finally {
330             closeQuietly(in);
331         }
332     }
333 
334     static void tryPatchShadedLibraryIdAndSign(File libraryFile, String originalName) {
335         if (!new File("/Library/Developer/CommandLineTools").exists()) {
336             logger.debug("Can't patch shaded library id as CommandLineTools are not installed." +
337                     " Consider installing CommandLineTools with 'xcode-select --install'");
338             return;
339         }
340         String newId = new String(generateUniqueId(originalName.length()), CharsetUtil.UTF_8);
341         if (!tryExec("install_name_tool -id " + newId + " " + libraryFile.getAbsolutePath())) {
342             return;
343         }
344 
345         tryExec("codesign -s - " + libraryFile.getAbsolutePath());
346     }
347 
348     private static boolean tryExec(String cmd) {
349         try {
350             int exitValue = Runtime.getRuntime().exec(cmd).waitFor();
351             if (exitValue != 0) {
352                 logger.debug("Execution of '{}' failed: {}", cmd, exitValue);
353                 return false;
354             }
355             logger.debug("Execution of '{}' succeed: {}", cmd, exitValue);
356             return true;
357         } catch (InterruptedException e) {
358             Thread.currentThread().interrupt();
359         } catch (IOException e) {
360             logger.info("Execution of '{}' failed.", cmd, e);
361         } catch (SecurityException e) {
362             logger.error("Execution of '{}' failed.", cmd, e);
363         }
364         return false;
365     }
366 
367     private static boolean shouldShadedLibraryIdBePatched(String packagePrefix) {
368         return TRY_TO_PATCH_SHADED_ID && PlatformDependent.isOsx() && !packagePrefix.isEmpty();
369     }
370 
371     private static byte[] generateUniqueId(int length) {
372         byte[] idBytes = new byte[length];
373         for (int i = 0; i < idBytes.length; i++) {
374             // We should only use bytes as replacement that are in our UNIQUE_ID_BYTES array.
375             idBytes[i] = UNIQUE_ID_BYTES[PlatformDependent.threadLocalRandom()
376                     .nextInt(UNIQUE_ID_BYTES.length)];
377         }
378         return idBytes;
379     }
380 
381     /**
382      * Loading the native library into the specified {@link ClassLoader}.
383      * @param loader - The {@link ClassLoader} where the native library will be loaded into
384      * @param name - The native library path or name
385      * @param absolute - Whether the native library will be loaded by path or by name
386      */
387     private static void loadLibrary(final ClassLoader loader, final String name, final boolean absolute) {
388         Throwable suppressed = null;
389         try {
390             try {
391                 // Make sure the helper belongs to the target ClassLoader.
392                 final Class<?> newHelper = tryToLoadClass(loader, NativeLibraryUtil.class);
393                 loadLibraryByHelper(newHelper, name, absolute);
394                 logger.debug("Successfully loaded the library {}", name);
395                 return;
396             } catch (UnsatisfiedLinkError e) { // Should by pass the UnsatisfiedLinkError here!
397                 suppressed = e;
398             } catch (Exception e) {
399                 suppressed = e;
400             }
401             NativeLibraryUtil.loadLibrary(name, absolute);  // Fallback to local helper class.
402             logger.debug("Successfully loaded the library {}", name);
403         } catch (NoSuchMethodError nsme) {
404             if (suppressed != null) {
405                 ThrowableUtil.addSuppressed(nsme, suppressed);
406             }
407             throw new LinkageError(
408                     "Possible multiple incompatible native libraries on the classpath for '" + name + "'?", nsme);
409         } catch (UnsatisfiedLinkError ule) {
410             if (suppressed != null) {
411                 ThrowableUtil.addSuppressed(ule, suppressed);
412             }
413             throw ule;
414         }
415     }
416 
417     private static void loadLibraryByHelper(final Class<?> helper, final String name, final boolean absolute)
418             throws UnsatisfiedLinkError {
419         Object ret = AccessController.doPrivileged(new PrivilegedAction<Object>() {
420             @Override
421             public Object run() {
422                 try {
423                     // Invoke the helper to load the native library, if succeed, then the native
424                     // library belong to the specified ClassLoader.
425                     Method method = helper.getMethod("loadLibrary", String.class, boolean.class);
426                     method.setAccessible(true);
427                     return method.invoke(null, name, absolute);
428                 } catch (Exception e) {
429                     return e;
430                 }
431             }
432         });
433         if (ret instanceof Throwable) {
434             Throwable t = (Throwable) ret;
435             assert !(t instanceof UnsatisfiedLinkError) : t + " should be a wrapper throwable";
436             Throwable cause = t.getCause();
437             if (cause instanceof UnsatisfiedLinkError) {
438                 throw (UnsatisfiedLinkError) cause;
439             }
440             UnsatisfiedLinkError ule = new UnsatisfiedLinkError(t.getMessage());
441             ule.initCause(t);
442             throw ule;
443         }
444     }
445 
446     /**
447      * Try to load the helper {@link Class} into specified {@link ClassLoader}.
448      * @param loader - The {@link ClassLoader} where to load the helper {@link Class}
449      * @param helper - The helper {@link Class}
450      * @return A new helper Class defined in the specified ClassLoader.
451      * @throws ClassNotFoundException Helper class not found or loading failed
452      */
453     private static Class<?> tryToLoadClass(final ClassLoader loader, final Class<?> helper)
454             throws ClassNotFoundException {
455         try {
456             return Class.forName(helper.getName(), false, loader);
457         } catch (ClassNotFoundException e1) {
458             if (loader == null) {
459                 // cannot defineClass inside bootstrap class loader
460                 throw e1;
461             }
462             try {
463                 // The helper class is NOT found in target ClassLoader, we have to define the helper class.
464                 final byte[] classBinary = classToByteArray(helper);
465                 return AccessController.doPrivileged(new PrivilegedAction<Class<?>>() {
466                     @Override
467                     public Class<?> run() {
468                         try {
469                             // Define the helper class in the target ClassLoader,
470                             //  then we can call the helper to load the native library.
471                             Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class,
472                                     byte[].class, int.class, int.class);
473                             defineClass.setAccessible(true);
474                             return (Class<?>) defineClass.invoke(loader, helper.getName(), classBinary, 0,
475                                     classBinary.length);
476                         } catch (Exception e) {
477                             throw new IllegalStateException("Define class failed!", e);
478                         }
479                     }
480                 });
481             } catch (ClassNotFoundException e2) {
482                 ThrowableUtil.addSuppressed(e2, e1);
483                 throw e2;
484             } catch (RuntimeException e2) {
485                 ThrowableUtil.addSuppressed(e2, e1);
486                 throw e2;
487             } catch (Error e2) {
488                 ThrowableUtil.addSuppressed(e2, e1);
489                 throw e2;
490             }
491         }
492     }
493 
494     /**
495      * Load the helper {@link Class} as a byte array, to be redefined in specified {@link ClassLoader}.
496      * @param clazz - The helper {@link Class} provided by this bundle
497      * @return The binary content of helper {@link Class}.
498      * @throws ClassNotFoundException Helper class not found or loading failed
499      */
500     private static byte[] classToByteArray(Class<?> clazz) throws ClassNotFoundException {
501         String fileName = clazz.getName();
502         int lastDot = fileName.lastIndexOf('.');
503         if (lastDot > 0) {
504             fileName = fileName.substring(lastDot + 1);
505         }
506         URL classUrl = clazz.getResource(fileName + ".class");
507         if (classUrl == null) {
508             throw new ClassNotFoundException(clazz.getName());
509         }
510         byte[] buf = new byte[1024];
511         ByteArrayOutputStream out = new ByteArrayOutputStream(4096);
512         InputStream in = null;
513         try {
514             in = classUrl.openStream();
515             for (int r; (r = in.read(buf)) != -1;) {
516                 out.write(buf, 0, r);
517             }
518             return out.toByteArray();
519         } catch (IOException ex) {
520             throw new ClassNotFoundException(clazz.getName(), ex);
521         } finally {
522             closeQuietly(in);
523             closeQuietly(out);
524         }
525     }
526 
527     private static void closeQuietly(Closeable c) {
528         if (c != null) {
529             try {
530                 c.close();
531             } catch (IOException ignore) {
532                 // ignore
533             }
534         }
535     }
536 
537     private NativeLibraryLoader() {
538         // Utility
539     }
540 
541     private static final class NoexecVolumeDetector {
542 
543         private static boolean canExecuteExecutable(File file) throws IOException {
544             // If we can already execute, there is nothing to do.
545             if (file.canExecute()) {
546                 return true;
547             }
548 
549             // On volumes, with noexec set, even files with the executable POSIX permissions will fail to execute.
550             // The File#canExecute() method honors this behavior, probaby via parsing the noexec flag when initializing
551             // the UnixFileStore, though the flag is not exposed via a public API.  To find out if library is being
552             // loaded off a volume with noexec, confirm or add executalbe permissions, then check File#canExecute().
553             Set<PosixFilePermission> existingFilePermissions = Files.getPosixFilePermissions(file.toPath());
554             Set<PosixFilePermission> executePermissions =
555                     EnumSet.of(PosixFilePermission.OWNER_EXECUTE,
556                             PosixFilePermission.GROUP_EXECUTE,
557                             PosixFilePermission.OTHERS_EXECUTE);
558             if (existingFilePermissions.containsAll(executePermissions)) {
559                 return false;
560             }
561 
562             Set<PosixFilePermission> newPermissions = EnumSet.copyOf(existingFilePermissions);
563             newPermissions.addAll(executePermissions);
564             Files.setPosixFilePermissions(file.toPath(), newPermissions);
565             return file.canExecute();
566         }
567 
568         private NoexecVolumeDetector() {
569             // Utility
570         }
571     }
572 }