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