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