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.File;
24  import java.io.FileNotFoundException;
25  import java.io.FileOutputStream;
26  import java.io.IOException;
27  import java.io.InputStream;
28  import java.io.OutputStream;
29  import java.lang.reflect.Method;
30  import java.net.URL;
31  import java.nio.file.Files;
32  import java.nio.file.attribute.PosixFilePermission;
33  import java.security.AccessController;
34  import java.security.MessageDigest;
35  import java.security.NoSuchAlgorithmException;
36  import java.security.PrivilegedAction;
37  import java.util.ArrayList;
38  import java.util.Arrays;
39  import java.util.Collections;
40  import java.util.EnumSet;
41  import java.util.Enumeration;
42  import java.util.List;
43  import java.util.Set;
44  import java.util.concurrent.ThreadLocalRandom;
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<>();
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         File tmpFile = null;
176         URL url = getResource(path, loader);
177         try {
178             if (url == null) {
179                 if (PlatformDependent.isOsx()) {
180                     String fileName = path.endsWith(".jnilib") ? NATIVE_RESOURCE_HOME + "lib" + name + ".dynlib" :
181                             NATIVE_RESOURCE_HOME + "lib" + name + ".jnilib";
182                     url = getResource(fileName, loader);
183                     if (url == null) {
184                         FileNotFoundException fnf = new FileNotFoundException(fileName);
185                         ThrowableUtil.addSuppressedAndClear(fnf, suppressed);
186                         throw fnf;
187                     }
188                 } else {
189                     FileNotFoundException fnf = new FileNotFoundException(path);
190                     ThrowableUtil.addSuppressedAndClear(fnf, suppressed);
191                     throw fnf;
192                 }
193             }
194 
195             int index = libname.lastIndexOf('.');
196             String prefix = libname.substring(0, index);
197             String suffix = libname.substring(index);
198 
199             tmpFile = PlatformDependent.createTempFile(prefix, suffix, WORKDIR);
200             try (InputStream in = url.openStream();
201                  OutputStream out = new FileOutputStream(tmpFile)) {
202 
203                 byte[] buffer = new byte[8192];
204                 int length;
205                 while ((length = in.read(buffer)) > 0) {
206                     out.write(buffer, 0, length);
207                 }
208                 out.flush();
209 
210                 if (shouldShadedLibraryIdBePatched(mangledPackagePrefix)) {
211                     // Let's try to patch the id and re-sign it. This is a best-effort and might fail if a
212                     // SecurityManager is setup or the right executables are not installed :/
213                     tryPatchShadedLibraryIdAndSign(tmpFile, originalName);
214                 }
215             }
216             // Close the output stream before loading the unpacked library,
217             // because otherwise Windows will refuse to load it when it's in use by other process.
218             loadLibrary(loader, tmpFile.getPath(), true);
219 
220         } catch (UnsatisfiedLinkError e) {
221             try {
222                 if (tmpFile != null && tmpFile.isFile() && tmpFile.canRead() &&
223                     !NoexecVolumeDetector.canExecuteExecutable(tmpFile)) {
224                     // Pass "io.netty.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                     String message = String.format(
228                             "%s exists but cannot be executed even when execute permissions set; " +
229                                     "check volume for \"noexec\" flag; use -D%s=[path] " +
230                                     "to set native working directory separately.",
231                             tmpFile.getPath(), "io.netty.native.workdir");
232                     logger.info(message);
233                     suppressed.add(ThrowableUtil.unknownStackTrace(
234                             new UnsatisfiedLinkError(message), NativeLibraryLoader.class, "load"));
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             // After we load the library it is safe to delete the file.
250             // We delete the file immediately to free up resources as soon as possible,
251             // and if this fails fallback to deleting on JVM exit.
252             if (tmpFile != null && (!DELETE_NATIVE_LIB_AFTER_LOADING || !tmpFile.delete())) {
253                 tmpFile.deleteOnExit();
254             }
255         }
256     }
257 
258     private static URL getResource(String path, ClassLoader loader) {
259         final Enumeration<URL> urls;
260         try {
261             if (loader == null) {
262                 urls = ClassLoader.getSystemResources(path);
263             } else {
264                 urls = loader.getResources(path);
265             }
266         } catch (IOException iox) {
267             throw new RuntimeException("An error occurred while getting the resources for " + path, iox);
268         }
269 
270         List<URL> urlsList = Collections.list(urls);
271         int size = urlsList.size();
272         switch (size) {
273             case 0:
274                 return null;
275             case 1:
276                 return urlsList.get(0);
277             default:
278                 if (DETECT_NATIVE_LIBRARY_DUPLICATES) {
279                     try {
280                         MessageDigest md = MessageDigest.getInstance("SHA-256");
281                         // We found more than 1 resource with the same name. Let's check if the content of the file is
282                         // the same as in this case it will not have any bad effect.
283                         URL url = urlsList.get(0);
284                         byte[] digest = digest(md, url);
285                         boolean allSame = true;
286                         if (digest != null) {
287                             for (int i = 1; i < size; i++) {
288                                 byte[] digest2 = digest(md, urlsList.get(i));
289                                 if (digest2 == null || !Arrays.equals(digest, digest2)) {
290                                     allSame = false;
291                                     break;
292                                 }
293                             }
294                         } else {
295                             allSame = false;
296                         }
297                         if (allSame) {
298                             return url;
299                         }
300                     } catch (NoSuchAlgorithmException e) {
301                         logger.debug("Don't support SHA-256, can't check if resources have same content.", e);
302                     }
303 
304                     throw new IllegalStateException(
305                             "Multiple resources found for '" + path + "' with different content: " + urlsList);
306                 } else {
307                     logger.warn("Multiple resources found for '" + path + "' with different content: " +
308                             urlsList + ". Please fix your dependency graph.");
309                     return urlsList.get(0);
310                 }
311         }
312     }
313 
314     private static byte[] digest(MessageDigest digest, URL url) {
315         try (InputStream 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         }
326     }
327 
328     static void tryPatchShadedLibraryIdAndSign(File libraryFile, String originalName) {
329         if (!new File("/Library/Developer/CommandLineTools").exists()) {
330             logger.debug("Can't patch shaded library id as CommandLineTools are not installed." +
331                     " Consider installing CommandLineTools with 'xcode-select --install'");
332             return;
333         }
334         String newId = new String(generateUniqueId(originalName.length()), CharsetUtil.UTF_8);
335         if (!tryExec("install_name_tool -id " + newId + " " + libraryFile.getAbsolutePath())) {
336             return;
337         }
338 
339         tryExec("codesign -s - " + libraryFile.getAbsolutePath());
340     }
341 
342     private static boolean tryExec(String cmd) {
343         try {
344             int exitValue = Runtime.getRuntime().exec(cmd).waitFor();
345             if (exitValue != 0) {
346                 logger.debug("Execution of '{}' failed: {}", cmd, exitValue);
347                 return false;
348             }
349             logger.debug("Execution of '{}' succeed: {}", cmd, exitValue);
350             return true;
351         } catch (InterruptedException e) {
352             Thread.currentThread().interrupt();
353         } catch (IOException e) {
354             logger.info("Execution of '{}' failed.", cmd, e);
355         } catch (SecurityException e) {
356             logger.error("Execution of '{}' failed.", cmd, e);
357         }
358         return false;
359     }
360 
361     private static boolean shouldShadedLibraryIdBePatched(String packagePrefix) {
362         return TRY_TO_PATCH_SHADED_ID && PlatformDependent.isOsx() && !packagePrefix.isEmpty();
363     }
364 
365     private static byte[] generateUniqueId(int length) {
366         byte[] idBytes = new byte[length];
367         for (int i = 0; i < idBytes.length; i++) {
368             // We should only use bytes as replacement that are in our UNIQUE_ID_BYTES array.
369             idBytes[i] = UNIQUE_ID_BYTES[ThreadLocalRandom.current()
370                     .nextInt(UNIQUE_ID_BYTES.length)];
371         }
372         return idBytes;
373     }
374 
375     /**
376      * Loading the native library into the specified {@link ClassLoader}.
377      * @param loader - The {@link ClassLoader} where the native library will be loaded into
378      * @param name - The native library path or name
379      * @param absolute - Whether the native library will be loaded by path or by name
380      */
381     private static void loadLibrary(final ClassLoader loader, final String name, final boolean absolute) {
382         Throwable suppressed = null;
383         try {
384             try {
385                 // Make sure the helper belongs to the target ClassLoader.
386                 final Class<?> newHelper = tryToLoadClass(loader, NativeLibraryUtil.class);
387                 loadLibraryByHelper(newHelper, name, absolute);
388                 logger.debug("Successfully loaded the library {}", name);
389                 return;
390             } catch (UnsatisfiedLinkError e) { // Should by pass the UnsatisfiedLinkError here!
391                 suppressed = e;
392             } catch (Exception e) {
393                 suppressed = e;
394             }
395             NativeLibraryUtil.loadLibrary(name, absolute);  // Fallback to local helper class.
396             logger.debug("Successfully loaded the library {}", name);
397         } catch (NoSuchMethodError nsme) {
398             if (suppressed != null) {
399                 ThrowableUtil.addSuppressed(nsme, suppressed);
400             }
401             throw new LinkageError(
402                     "Possible multiple incompatible native libraries on the classpath for '" + name + "'?", nsme);
403         } catch (UnsatisfiedLinkError ule) {
404             if (suppressed != null) {
405                 ThrowableUtil.addSuppressed(ule, suppressed);
406             }
407             throw ule;
408         }
409     }
410 
411     private static void loadLibraryByHelper(final Class<?> helper, final String name, final boolean absolute)
412             throws UnsatisfiedLinkError {
413         Object ret = AccessController.doPrivileged(new PrivilegedAction<Object>() {
414             @Override
415             public Object run() {
416                 try {
417                     // Invoke the helper to load the native library, if it succeeds, then the native
418                     // library belong to the specified ClassLoader.
419                     Method method = helper.getMethod("loadLibrary", String.class, boolean.class);
420                     method.setAccessible(true);
421                     return method.invoke(null, name, absolute);
422                 } catch (Exception e) {
423                     return e;
424                 }
425             }
426         });
427         if (ret instanceof Throwable) {
428             Throwable t = (Throwable) ret;
429             assert !(t instanceof UnsatisfiedLinkError) : t + " should be a wrapper throwable";
430             Throwable cause = t.getCause();
431             if (cause instanceof UnsatisfiedLinkError) {
432                 throw (UnsatisfiedLinkError) cause;
433             }
434             UnsatisfiedLinkError ule = new UnsatisfiedLinkError(t.getMessage());
435             ule.initCause(t);
436             throw ule;
437         }
438     }
439 
440     /**
441      * Try to load the helper {@link Class} into specified {@link ClassLoader}.
442      * @param loader - The {@link ClassLoader} where to load the helper {@link Class}
443      * @param helper - The helper {@link Class}
444      * @return A new helper Class defined in the specified ClassLoader.
445      * @throws ClassNotFoundException Helper class not found or loading failed
446      */
447     private static Class<?> tryToLoadClass(final ClassLoader loader, final Class<?> helper)
448             throws ClassNotFoundException {
449         try {
450             return Class.forName(helper.getName(), false, loader);
451         } catch (ClassNotFoundException e1) {
452             if (loader == null) {
453                 // cannot defineClass inside bootstrap class loader
454                 throw e1;
455             }
456             try {
457                 // The helper class is NOT found in target ClassLoader, we have to define the helper class.
458                 final byte[] classBinary = classToByteArray(helper);
459                 return AccessController.doPrivileged(new PrivilegedAction<Class<?>>() {
460                     @Override
461                     public Class<?> run() {
462                         try {
463                             // Define the helper class in the target ClassLoader,
464                             //  then we can call the helper to load the native library.
465                             Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class,
466                                     byte[].class, int.class, int.class);
467                             defineClass.setAccessible(true);
468                             return (Class<?>) defineClass.invoke(loader, helper.getName(), classBinary, 0,
469                                     classBinary.length);
470                         } catch (Exception e) {
471                             throw new IllegalStateException("Define class failed!", e);
472                         }
473                     }
474                 });
475             } catch (ClassNotFoundException | RuntimeException | Error e2) {
476                 ThrowableUtil.addSuppressed(e2, e1);
477                 throw e2;
478             }
479         }
480     }
481 
482     /**
483      * Load the helper {@link Class} as a byte array, to be redefined in specified {@link ClassLoader}.
484      * @param clazz - The helper {@link Class} provided by this bundle
485      * @return The binary content of helper {@link Class}.
486      * @throws ClassNotFoundException Helper class not found or loading failed
487      */
488     private static byte[] classToByteArray(Class<?> clazz) throws ClassNotFoundException {
489         String fileName = clazz.getName();
490         int lastDot = fileName.lastIndexOf('.');
491         if (lastDot > 0) {
492             fileName = fileName.substring(lastDot + 1);
493         }
494         URL classUrl = clazz.getResource(fileName + ".class");
495         if (classUrl == null) {
496             throw new ClassNotFoundException(clazz.getName());
497         }
498         byte[] buf = new byte[1024];
499         ByteArrayOutputStream out = new ByteArrayOutputStream(4096);
500         try (InputStream in = classUrl.openStream()) {
501             for (int r; (r = in.read(buf)) != -1;) {
502                 out.write(buf, 0, r);
503             }
504             return out.toByteArray();
505         } catch (IOException ex) {
506             throw new ClassNotFoundException(clazz.getName(), ex);
507         }
508     }
509 
510     private NativeLibraryLoader() {
511         // Utility
512     }
513 
514     private static final class NoexecVolumeDetector {
515 
516         private static boolean canExecuteExecutable(File file) throws IOException {
517             // If we can already execute, there is nothing to do.
518             if (file.canExecute()) {
519                 return true;
520             }
521 
522             // On volumes, with noexec set, even files with the executable POSIX permissions will fail to execute.
523             // The File#canExecute() method honors this behavior, probaby via parsing the noexec flag when initializing
524             // the UnixFileStore, though the flag is not exposed via a public API.  To find out if library is being
525             // loaded off a volume with noexec, confirm or add executalbe permissions, then check File#canExecute().
526             Set<PosixFilePermission> existingFilePermissions = Files.getPosixFilePermissions(file.toPath());
527             Set<PosixFilePermission> executePermissions =
528                     EnumSet.of(PosixFilePermission.OWNER_EXECUTE,
529                             PosixFilePermission.GROUP_EXECUTE,
530                             PosixFilePermission.OTHERS_EXECUTE);
531             if (existingFilePermissions.containsAll(executePermissions)) {
532                 return false;
533             }
534 
535             Set<PosixFilePermission> newPermissions = EnumSet.copyOf(existingFilePermissions);
536             newPermissions.addAll(executePermissions);
537             Files.setPosixFilePermissions(file.toPath(), newPermissions);
538             return file.canExecute();
539         }
540 
541         private NoexecVolumeDetector() {
542             // Utility
543         }
544     }
545 }