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