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    *   http://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.security.AccessController;
33  import java.security.PrivilegedAction;
34  import java.util.ArrayList;
35  import java.util.Arrays;
36  import java.util.EnumSet;
37  import java.util.List;
38  import java.util.Set;
39  
40  /**
41   * Helper class to load JNI resources.
42   *
43   */
44  public final class NativeLibraryLoader {
45  
46      private static final InternalLogger logger = InternalLoggerFactory.getInstance(NativeLibraryLoader.class);
47  
48      private static final String NATIVE_RESOURCE_HOME = "META-INF/native/";
49      private static final File WORKDIR;
50      private static final boolean DELETE_NATIVE_LIB_AFTER_LOADING;
51      private static final boolean TRY_TO_PATCH_SHADED_ID;
52  
53      // Just use a-Z and numbers as valid ID bytes.
54      private static final byte[] UNIQUE_ID_BYTES =
55              "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes(CharsetUtil.US_ASCII);
56  
57      static {
58          String workdir = SystemPropertyUtil.get("io.netty.native.workdir");
59          if (workdir != null) {
60              File f = new File(workdir);
61              f.mkdirs();
62  
63              try {
64                  f = f.getAbsoluteFile();
65              } catch (Exception ignored) {
66                  // Good to have an absolute path, but it's OK.
67              }
68  
69              WORKDIR = f;
70              logger.debug("-Dio.netty.native.workdir: " + WORKDIR);
71          } else {
72              WORKDIR = PlatformDependent.tmpdir();
73              logger.debug("-Dio.netty.native.workdir: " + WORKDIR + " (io.netty.tmpdir)");
74          }
75  
76          DELETE_NATIVE_LIB_AFTER_LOADING = SystemPropertyUtil.getBoolean(
77                  "io.netty.native.deleteLibAfterLoading", true);
78          logger.debug("-Dio.netty.native.deleteLibAfterLoading: {}", DELETE_NATIVE_LIB_AFTER_LOADING);
79  
80          TRY_TO_PATCH_SHADED_ID = SystemPropertyUtil.getBoolean(
81                  "io.netty.native.tryPatchShadedId", true);
82          logger.debug("-Dio.netty.native.tryPatchShadedId: {}", TRY_TO_PATCH_SHADED_ID);
83      }
84  
85      /**
86       * Loads the first available library in the collection with the specified
87       * {@link ClassLoader}.
88       *
89       * @throws IllegalArgumentException
90       *         if none of the given libraries load successfully.
91       */
92      public static void loadFirstAvailable(ClassLoader loader, String... names) {
93          List<Throwable> suppressed = new ArrayList<Throwable>();
94          for (String name : names) {
95              try {
96                  load(name, loader);
97                  return;
98              } catch (Throwable t) {
99                  suppressed.add(t);
100                 logger.debug("Unable to load the library '{}', trying next name...", name, t);
101             }
102         }
103         IllegalArgumentException iae =
104                 new IllegalArgumentException("Failed to load any of the given libraries: " + Arrays.toString(names));
105         ThrowableUtil.addSuppressedAndClear(iae, suppressed);
106         throw iae;
107     }
108 
109     /**
110      * The shading prefix added to this class's full name.
111      *
112      * @throws UnsatisfiedLinkError if the shader used something other than a prefix
113      */
114     private static String calculatePackagePrefix() {
115         String maybeShaded = NativeLibraryLoader.class.getName();
116         // Use ! instead of . to avoid shading utilities from modifying the string
117         String expected = "io!netty!util!internal!NativeLibraryLoader".replace('!', '.');
118         if (!maybeShaded.endsWith(expected)) {
119             throw new UnsatisfiedLinkError(String.format(
120                     "Could not find prefix added to %s to get %s. When shading, only adding a "
121                     + "package prefix is supported", expected, maybeShaded));
122         }
123         return maybeShaded.substring(0, maybeShaded.length() - expected.length());
124     }
125 
126     /**
127      * Load the given library with the specified {@link ClassLoader}
128      */
129     public static void load(String originalName, ClassLoader loader) {
130         // Adjust expected name to support shading of native libraries.
131         String packagePrefix = calculatePackagePrefix().replace('.', '_');
132         String name = packagePrefix + originalName;
133         List<Throwable> suppressed = new ArrayList<Throwable>();
134         try {
135             // first try to load from java.library.path
136             loadLibrary(loader, name, false);
137             return;
138         } catch (Throwable ex) {
139             suppressed.add(ex);
140             logger.debug(
141                     "{} cannot be loaded from java.libary.path, "
142                     + "now trying export to -Dio.netty.native.workdir: {}", name, WORKDIR, ex);
143         }
144 
145         String libname = System.mapLibraryName(name);
146         String path = NATIVE_RESOURCE_HOME + libname;
147 
148         InputStream in = null;
149         OutputStream out = null;
150         File tmpFile = null;
151         URL url;
152         if (loader == null) {
153             url = ClassLoader.getSystemResource(path);
154         } else {
155             url = loader.getResource(path);
156         }
157         try {
158             if (url == null) {
159                 if (PlatformDependent.isOsx()) {
160                     String fileName = path.endsWith(".jnilib") ? NATIVE_RESOURCE_HOME + "lib" + name + ".dynlib" :
161                             NATIVE_RESOURCE_HOME + "lib" + name + ".jnilib";
162                     if (loader == null) {
163                         url = ClassLoader.getSystemResource(fileName);
164                     } else {
165                         url = loader.getResource(fileName);
166                     }
167                     if (url == null) {
168                         FileNotFoundException fnf = new FileNotFoundException(fileName);
169                         ThrowableUtil.addSuppressedAndClear(fnf, suppressed);
170                         throw fnf;
171                     }
172                 } else {
173                     FileNotFoundException fnf = new FileNotFoundException(path);
174                     ThrowableUtil.addSuppressedAndClear(fnf, suppressed);
175                     throw fnf;
176                 }
177             }
178 
179             int index = libname.lastIndexOf('.');
180             String prefix = libname.substring(0, index);
181             String suffix = libname.substring(index, libname.length());
182 
183             tmpFile = File.createTempFile(prefix, suffix, WORKDIR);
184             in = url.openStream();
185             out = new FileOutputStream(tmpFile);
186 
187             byte[] buffer = new byte[8192];
188             int length;
189             if (TRY_TO_PATCH_SHADED_ID && PlatformDependent.isOsx() && !packagePrefix.isEmpty()) {
190                 // We read the whole native lib into memory to make it easier to monkey-patch the id.
191                 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(in.available());
192 
193                 while ((length = in.read(buffer)) > 0) {
194                     byteArrayOutputStream.write(buffer, 0, length);
195                 }
196                 byteArrayOutputStream.flush();
197                 byte[] bytes = byteArrayOutputStream.toByteArray();
198                 byteArrayOutputStream.close();
199 
200                 // Try to patch the library id.
201                 patchShadedLibraryId(bytes, originalName, name);
202 
203                 out.write(bytes);
204             } else {
205                 while ((length = in.read(buffer)) > 0) {
206                     out.write(buffer, 0, length);
207                 }
208             }
209             out.flush();
210 
211             // Close the output stream before loading the unpacked library,
212             // because otherwise Windows will refuse to load it when it's in use by other process.
213             closeQuietly(out);
214             out = null;
215             loadLibrary(loader, tmpFile.getPath(), true);
216         } catch (UnsatisfiedLinkError e) {
217             try {
218                 if (tmpFile != null && tmpFile.isFile() && tmpFile.canRead() &&
219                     !NoexecVolumeDetector.canExecuteExecutable(tmpFile)) {
220                     logger.info("{} exists but cannot be executed even when execute permissions set; " +
221                                 "check volume for \"noexec\" flag; use -Dio.netty.native.workdir=[path] " +
222                                 "to set native working directory separately.",
223                                 tmpFile.getPath());
224                 }
225             } catch (Throwable t) {
226                 suppressed.add(t);
227                 logger.debug("Error checking if {} is on a file store mounted with noexec", tmpFile, t);
228             }
229             // Re-throw to fail the load
230             ThrowableUtil.addSuppressedAndClear(e, suppressed);
231             throw e;
232         } catch (Exception e) {
233             UnsatisfiedLinkError ule = new UnsatisfiedLinkError("could not load a native library: " + name);
234             ule.initCause(e);
235             ThrowableUtil.addSuppressedAndClear(ule, suppressed);
236             throw ule;
237         } finally {
238             closeQuietly(in);
239             closeQuietly(out);
240             // After we load the library it is safe to delete the file.
241             // We delete the file immediately to free up resources as soon as possible,
242             // and if this fails fallback to deleting on JVM exit.
243             if (tmpFile != null && (!DELETE_NATIVE_LIB_AFTER_LOADING || !tmpFile.delete())) {
244                 tmpFile.deleteOnExit();
245             }
246         }
247     }
248 
249     /**
250      * Try to patch shaded library to ensure it uses a unique ID.
251      */
252     private static void patchShadedLibraryId(byte[] bytes, String originalName, String name) {
253         // Our native libs always have the name as part of their id so we can search for it and replace it
254         // to make the ID unique if shading is used.
255         byte[] nameBytes = originalName.getBytes(CharsetUtil.UTF_8);
256         int idIdx = -1;
257 
258         // Be aware this is a really raw way of patching a dylib but it does all we need without implementing
259         // a full mach-o parser and writer. Basically we just replace the the original bytes with some
260         // random bytes as part of the ID regeneration. The important thing here is that we need to use the same
261         // length to not corrupt the mach-o header.
262         outerLoop: for (int i = 0; i < bytes.length && bytes.length - i >= nameBytes.length; i++) {
263             int idx = i;
264             for (int j = 0; j < nameBytes.length;) {
265                 if (bytes[idx++] != nameBytes[j++]) {
266                     // Did not match the name, increase the index and try again.
267                     break;
268                 } else if (j == nameBytes.length) {
269                     // We found the index within the id.
270                     idIdx = i;
271                     break outerLoop;
272                 }
273             }
274         }
275 
276         if (idIdx == -1) {
277             logger.debug("Was not able to find the ID of the shaded native library {}, can't adjust it.", name);
278         } else {
279             // We found our ID... now monkey-patch it!
280             for (int i = 0; i < nameBytes.length; i++) {
281                 // We should only use bytes as replacement that are in our UNIQUE_ID_BYTES array.
282                 bytes[idIdx + i] = UNIQUE_ID_BYTES[PlatformDependent.threadLocalRandom()
283                                                                     .nextInt(UNIQUE_ID_BYTES.length)];
284             }
285 
286             if (logger.isDebugEnabled()) {
287                 logger.debug(
288                         "Found the ID of the shaded native library {}. Replacing ID part {} with {}",
289                         name, originalName, new String(bytes, idIdx, nameBytes.length, CharsetUtil.UTF_8));
290             }
291         }
292     }
293 
294     /**
295      * Loading the native library into the specified {@link ClassLoader}.
296      * @param loader - The {@link ClassLoader} where the native library will be loaded into
297      * @param name - The native library path or name
298      * @param absolute - Whether the native library will be loaded by path or by name
299      */
300     private static void loadLibrary(final ClassLoader loader, final String name, final boolean absolute) {
301         Throwable suppressed = null;
302         try {
303             try {
304                 // Make sure the helper is belong to the target ClassLoader.
305                 final Class<?> newHelper = tryToLoadClass(loader, NativeLibraryUtil.class);
306                 loadLibraryByHelper(newHelper, name, absolute);
307                 logger.debug("Successfully loaded the library {}", name);
308                 return;
309             } catch (UnsatisfiedLinkError e) { // Should by pass the UnsatisfiedLinkError here!
310                 suppressed = e;
311                 logger.debug("Unable to load the library '{}', trying other loading mechanism.", name, e);
312             } catch (Exception e) {
313                 suppressed = e;
314                 logger.debug("Unable to load the library '{}', trying other loading mechanism.", name, e);
315             }
316             NativeLibraryUtil.loadLibrary(name, absolute);  // Fallback to local helper class.
317             logger.debug("Successfully loaded the library {}", name);
318         } catch (UnsatisfiedLinkError ule) {
319             if (suppressed != null) {
320                 ThrowableUtil.addSuppressed(ule, suppressed);
321             }
322             throw ule;
323         }
324     }
325 
326     private static void loadLibraryByHelper(final Class<?> helper, final String name, final boolean absolute)
327             throws UnsatisfiedLinkError {
328         Object ret = AccessController.doPrivileged(new PrivilegedAction<Object>() {
329             @Override
330             public Object run() {
331                 try {
332                     // Invoke the helper to load the native library, if succeed, then the native
333                     // library belong to the specified ClassLoader.
334                     Method method = helper.getMethod("loadLibrary", String.class, boolean.class);
335                     method.setAccessible(true);
336                     return method.invoke(null, name, absolute);
337                 } catch (Exception e) {
338                     return e;
339                 }
340             }
341         });
342         if (ret instanceof Throwable) {
343             Throwable t = (Throwable) ret;
344             assert !(t instanceof UnsatisfiedLinkError) : t + " should be a wrapper throwable";
345             Throwable cause = t.getCause();
346             if (cause instanceof UnsatisfiedLinkError) {
347                 throw (UnsatisfiedLinkError) cause;
348             }
349             UnsatisfiedLinkError ule = new UnsatisfiedLinkError(t.getMessage());
350             ule.initCause(t);
351             throw ule;
352         }
353     }
354 
355     /**
356      * Try to load the helper {@link Class} into specified {@link ClassLoader}.
357      * @param loader - The {@link ClassLoader} where to load the helper {@link Class}
358      * @param helper - The helper {@link Class}
359      * @return A new helper Class defined in the specified ClassLoader.
360      * @throws ClassNotFoundException Helper class not found or loading failed
361      */
362     private static Class<?> tryToLoadClass(final ClassLoader loader, final Class<?> helper)
363             throws ClassNotFoundException {
364         try {
365             return Class.forName(helper.getName(), false, loader);
366         } catch (ClassNotFoundException e1) {
367             if (loader == null) {
368                 // cannot defineClass inside bootstrap class loader
369                 throw e1;
370             }
371             try {
372                 // The helper class is NOT found in target ClassLoader, we have to define the helper class.
373                 final byte[] classBinary = classToByteArray(helper);
374                 return AccessController.doPrivileged(new PrivilegedAction<Class<?>>() {
375                     @Override
376                     public Class<?> run() {
377                         try {
378                             // Define the helper class in the target ClassLoader,
379                             //  then we can call the helper to load the native library.
380                             Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class,
381                                     byte[].class, int.class, int.class);
382                             defineClass.setAccessible(true);
383                             return (Class<?>) defineClass.invoke(loader, helper.getName(), classBinary, 0,
384                                     classBinary.length);
385                         } catch (Exception e) {
386                             throw new IllegalStateException("Define class failed!", e);
387                         }
388                     }
389                 });
390             } catch (ClassNotFoundException e2) {
391                 ThrowableUtil.addSuppressed(e2, e1);
392                 throw e2;
393             } catch (RuntimeException e2) {
394                 ThrowableUtil.addSuppressed(e2, e1);
395                 throw e2;
396             } catch (Error e2) {
397                 ThrowableUtil.addSuppressed(e2, e1);
398                 throw e2;
399             }
400         }
401     }
402 
403     /**
404      * Load the helper {@link Class} as a byte array, to be redefined in specified {@link ClassLoader}.
405      * @param clazz - The helper {@link Class} provided by this bundle
406      * @return The binary content of helper {@link Class}.
407      * @throws ClassNotFoundException Helper class not found or loading failed
408      */
409     private static byte[] classToByteArray(Class<?> clazz) throws ClassNotFoundException {
410         String fileName = clazz.getName();
411         int lastDot = fileName.lastIndexOf('.');
412         if (lastDot > 0) {
413             fileName = fileName.substring(lastDot + 1);
414         }
415         URL classUrl = clazz.getResource(fileName + ".class");
416         if (classUrl == null) {
417             throw new ClassNotFoundException(clazz.getName());
418         }
419         byte[] buf = new byte[1024];
420         ByteArrayOutputStream out = new ByteArrayOutputStream(4096);
421         InputStream in = null;
422         try {
423             in = classUrl.openStream();
424             for (int r; (r = in.read(buf)) != -1;) {
425                 out.write(buf, 0, r);
426             }
427             return out.toByteArray();
428         } catch (IOException ex) {
429             throw new ClassNotFoundException(clazz.getName(), ex);
430         } finally {
431             closeQuietly(in);
432             closeQuietly(out);
433         }
434     }
435 
436     private static void closeQuietly(Closeable c) {
437         if (c != null) {
438             try {
439                 c.close();
440             } catch (IOException ignore) {
441                 // ignore
442             }
443         }
444     }
445 
446     private NativeLibraryLoader() {
447         // Utility
448     }
449 
450     private static final class NoexecVolumeDetector {
451 
452         private static boolean canExecuteExecutable(File file) throws IOException {
453             if (PlatformDependent.javaVersion() < 7) {
454                 // Pre-JDK7, the Java API did not directly support POSIX permissions; instead of implementing a custom
455                 // work-around, assume true, which disables the check.
456                 return true;
457             }
458 
459             // If we can already execute, there is nothing to do.
460             if (file.canExecute()) {
461                 return true;
462             }
463 
464             // On volumes, with noexec set, even files with the executable POSIX permissions will fail to execute.
465             // The File#canExecute() method honors this behavior, probaby via parsing the noexec flag when initializing
466             // the UnixFileStore, though the flag is not exposed via a public API.  To find out if library is being
467             // loaded off a volume with noexec, confirm or add executalbe permissions, then check File#canExecute().
468 
469             // Note: We use FQCN to not break when netty is used in java6
470             Set<java.nio.file.attribute.PosixFilePermission> existingFilePermissions =
471                     java.nio.file.Files.getPosixFilePermissions(file.toPath());
472             Set<java.nio.file.attribute.PosixFilePermission> executePermissions =
473                     EnumSet.of(java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE,
474                             java.nio.file.attribute.PosixFilePermission.GROUP_EXECUTE,
475                             java.nio.file.attribute.PosixFilePermission.OTHERS_EXECUTE);
476             if (existingFilePermissions.containsAll(executePermissions)) {
477                 return false;
478             }
479 
480             Set<java.nio.file.attribute.PosixFilePermission> newPermissions = EnumSet.copyOf(existingFilePermissions);
481             newPermissions.addAll(executePermissions);
482             java.nio.file.Files.setPosixFilePermissions(file.toPath(), newPermissions);
483             return file.canExecute();
484         }
485 
486         private NoexecVolumeDetector() {
487             // Utility
488         }
489     }
490 }