1
2
3
4
5
6
7
8
9
10
11
12
13
14
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.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
44
45
46
47
48 public final class NativeLibraryLoader {
49
50 private static final InternalLogger logger = InternalLoggerFactory.getInstance(NativeLibraryLoader.class);
51
52 private static final String NATIVE_RESOURCE_HOME = "META-INF/native/";
53 private static final File WORKDIR;
54 private static final boolean DELETE_NATIVE_LIB_AFTER_LOADING;
55 private static final boolean TRY_TO_PATCH_SHADED_ID;
56 private static final boolean DETECT_NATIVE_LIBRARY_DUPLICATES;
57
58
59 private static final byte[] UNIQUE_ID_BYTES =
60 "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes(CharsetUtil.US_ASCII);
61
62 static {
63 String workdir = SystemPropertyUtil.get("io.netty.native.workdir");
64 if (workdir != null) {
65 File f = new File(workdir);
66 f.mkdirs();
67
68 try {
69 f = f.getAbsoluteFile();
70 } catch (Exception ignored) {
71
72 }
73
74 WORKDIR = f;
75 logger.debug("-Dio.netty.native.workdir: " + WORKDIR);
76 } else {
77 WORKDIR = PlatformDependent.tmpdir();
78 logger.debug("-Dio.netty.native.workdir: " + WORKDIR + " (io.netty.tmpdir)");
79 }
80
81 DELETE_NATIVE_LIB_AFTER_LOADING = SystemPropertyUtil.getBoolean(
82 "io.netty.native.deleteLibAfterLoading", true);
83 logger.debug("-Dio.netty.native.deleteLibAfterLoading: {}", DELETE_NATIVE_LIB_AFTER_LOADING);
84
85 TRY_TO_PATCH_SHADED_ID = SystemPropertyUtil.getBoolean(
86 "io.netty.native.tryPatchShadedId", true);
87 logger.debug("-Dio.netty.native.tryPatchShadedId: {}", TRY_TO_PATCH_SHADED_ID);
88
89 DETECT_NATIVE_LIBRARY_DUPLICATES = SystemPropertyUtil.getBoolean(
90 "io.netty.native.detectNativeLibraryDuplicates", true);
91 logger.debug("-Dio.netty.native.detectNativeLibraryDuplicates: {}", DETECT_NATIVE_LIBRARY_DUPLICATES);
92 }
93
94
95
96
97
98
99
100
101 public static void loadFirstAvailable(ClassLoader loader, String... names) {
102 List<Throwable> suppressed = new ArrayList<Throwable>();
103 for (String name : names) {
104 try {
105 load(name, loader);
106 logger.debug("Loaded library with name '{}'", name);
107 return;
108 } catch (Throwable t) {
109 suppressed.add(t);
110 }
111 }
112
113 IllegalArgumentException iae =
114 new IllegalArgumentException("Failed to load any of the given libraries: " + Arrays.toString(names));
115 ThrowableUtil.addSuppressedAndClear(iae, suppressed);
116 throw iae;
117 }
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138 private static String calculateMangledPackagePrefix() {
139 String maybeShaded = NativeLibraryLoader.class.getName();
140
141 String expected = "io!netty!util!internal!NativeLibraryLoader".replace('!', '.');
142 if (!maybeShaded.endsWith(expected)) {
143 throw new UnsatisfiedLinkError(String.format(
144 "Could not find prefix added to %s to get %s. When shading, only adding a "
145 + "package prefix is supported", expected, maybeShaded));
146 }
147 return maybeShaded.substring(0, maybeShaded.length() - expected.length())
148 .replace("_", "_1")
149 .replace('.', '_');
150 }
151
152
153
154
155 public static void load(String originalName, ClassLoader loader) {
156 String mangledPackagePrefix = calculateMangledPackagePrefix();
157 String name = mangledPackagePrefix + originalName;
158 List<Throwable> suppressed = new ArrayList<Throwable>();
159 try {
160
161 loadLibrary(loader, name, false);
162 return;
163 } catch (Throwable ex) {
164 suppressed.add(ex);
165 }
166
167 String libname = System.mapLibraryName(name);
168 String path = NATIVE_RESOURCE_HOME + libname;
169
170 InputStream in = null;
171 OutputStream out = null;
172 File tmpFile = null;
173 URL url = getResource(path, loader);
174 try {
175 if (url == null) {
176 if (PlatformDependent.isOsx()) {
177 String fileName = path.endsWith(".jnilib") ? NATIVE_RESOURCE_HOME + "lib" + name + ".dynlib" :
178 NATIVE_RESOURCE_HOME + "lib" + name + ".jnilib";
179 url = getResource(fileName, loader);
180 if (url == null) {
181 FileNotFoundException fnf = new FileNotFoundException(fileName);
182 ThrowableUtil.addSuppressedAndClear(fnf, suppressed);
183 throw fnf;
184 }
185 } else {
186 FileNotFoundException fnf = new FileNotFoundException(path);
187 ThrowableUtil.addSuppressedAndClear(fnf, suppressed);
188 throw fnf;
189 }
190 }
191
192 int index = libname.lastIndexOf('.');
193 String prefix = libname.substring(0, index);
194 String suffix = libname.substring(index);
195
196 tmpFile = PlatformDependent.createTempFile(prefix, suffix, WORKDIR);
197 in = url.openStream();
198 out = new FileOutputStream(tmpFile);
199
200 byte[] buffer = new byte[8192];
201 int length;
202 while ((length = in.read(buffer)) > 0) {
203 out.write(buffer, 0, length);
204 }
205 out.flush();
206
207 if (shouldShadedLibraryIdBePatched(mangledPackagePrefix)) {
208
209
210 tryPatchShadedLibraryIdAndSign(tmpFile, originalName);
211 }
212
213
214
215 closeQuietly(out);
216 out = null;
217
218 loadLibrary(loader, tmpFile.getPath(), true);
219 } catch (UnsatisfiedLinkError e) {
220 try {
221 if (tmpFile != null && tmpFile.isFile() && tmpFile.canRead() &&
222 !NoexecVolumeDetector.canExecuteExecutable(tmpFile)) {
223
224
225
226 logger.info("{} exists but cannot be executed even when execute permissions set; " +
227 "check volume for \"noexec\" flag; use -D{}=[path] " +
228 "to set native working directory separately.",
229 tmpFile.getPath(), "io.netty.native.workdir");
230 }
231 } catch (Throwable t) {
232 suppressed.add(t);
233 logger.debug("Error checking if {} is on a file store mounted with noexec", tmpFile, t);
234 }
235
236 ThrowableUtil.addSuppressedAndClear(e, suppressed);
237 throw e;
238 } catch (Exception e) {
239 UnsatisfiedLinkError ule = new UnsatisfiedLinkError("could not load a native library: " + name);
240 ule.initCause(e);
241 ThrowableUtil.addSuppressedAndClear(ule, suppressed);
242 throw ule;
243 } finally {
244 closeQuietly(in);
245 closeQuietly(out);
246
247
248
249 if (tmpFile != null && (!DELETE_NATIVE_LIB_AFTER_LOADING || !tmpFile.delete())) {
250 tmpFile.deleteOnExit();
251 }
252 }
253 }
254
255 private static URL getResource(String path, ClassLoader loader) {
256 final Enumeration<URL> urls;
257 try {
258 if (loader == null) {
259 urls = ClassLoader.getSystemResources(path);
260 } else {
261 urls = loader.getResources(path);
262 }
263 } catch (IOException iox) {
264 throw new RuntimeException("An error occurred while getting the resources for " + path, iox);
265 }
266
267 List<URL> urlsList = Collections.list(urls);
268 int size = urlsList.size();
269 switch (size) {
270 case 0:
271 return null;
272 case 1:
273 return urlsList.get(0);
274 default:
275 if (DETECT_NATIVE_LIBRARY_DUPLICATES) {
276 try {
277 MessageDigest md = MessageDigest.getInstance("SHA-256");
278
279
280 URL url = urlsList.get(0);
281 byte[] digest = digest(md, url);
282 boolean allSame = true;
283 if (digest != null) {
284 for (int i = 1; i < size; i++) {
285 byte[] digest2 = digest(md, urlsList.get(i));
286 if (digest2 == null || !Arrays.equals(digest, digest2)) {
287 allSame = false;
288 break;
289 }
290 }
291 } else {
292 allSame = false;
293 }
294 if (allSame) {
295 return url;
296 }
297 } catch (NoSuchAlgorithmException e) {
298 logger.debug("Don't support SHA-256, can't check if resources have same content.", e);
299 }
300
301 throw new IllegalStateException(
302 "Multiple resources found for '" + path + "' with different content: " + urlsList);
303 } else {
304 logger.warn("Multiple resources found for '" + path + "' with different content: " +
305 urlsList + ". Please fix your dependency graph.");
306 return urlsList.get(0);
307 }
308 }
309 }
310
311 private static byte[] digest(MessageDigest digest, URL url) {
312 InputStream in = null;
313 try {
314 in = url.openStream();
315 byte[] bytes = new byte[8192];
316 int i;
317 while ((i = in.read(bytes)) != -1) {
318 digest.update(bytes, 0, i);
319 }
320 return digest.digest();
321 } catch (IOException e) {
322 logger.debug("Can't read resource.", e);
323 return null;
324 } finally {
325 closeQuietly(in);
326 }
327 }
328
329 static void tryPatchShadedLibraryIdAndSign(File libraryFile, String originalName) {
330 String newId = new String(generateUniqueId(originalName.length()), CharsetUtil.UTF_8);
331 if (!tryExec("install_name_tool -id " + newId + " " + libraryFile.getAbsolutePath())) {
332 return;
333 }
334
335 tryExec("codesign -s - " + libraryFile.getAbsolutePath());
336 }
337
338 private static boolean tryExec(String cmd) {
339 try {
340 int exitValue = Runtime.getRuntime().exec(cmd).waitFor();
341 if (exitValue != 0) {
342 logger.debug("Execution of '{}' failed: {}", cmd, exitValue);
343 return false;
344 }
345 logger.debug("Execution of '{}' succeed: {}", cmd, exitValue);
346 return true;
347 } catch (InterruptedException e) {
348 Thread.currentThread().interrupt();
349 } catch (IOException e) {
350 logger.info("Execution of '{}' failed.", cmd, e);
351 } catch (SecurityException e) {
352 logger.error("Execution of '{}' failed.", cmd, e);
353 }
354 return false;
355 }
356
357 private static boolean shouldShadedLibraryIdBePatched(String packagePrefix) {
358 return TRY_TO_PATCH_SHADED_ID && PlatformDependent.isOsx() && !packagePrefix.isEmpty();
359 }
360
361 private static byte[] generateUniqueId(int length) {
362 byte[] idBytes = new byte[length];
363 for (int i = 0; i < idBytes.length; i++) {
364
365 idBytes[i] = UNIQUE_ID_BYTES[PlatformDependent.threadLocalRandom()
366 .nextInt(UNIQUE_ID_BYTES.length)];
367 }
368 return idBytes;
369 }
370
371
372
373
374
375
376
377 private static void loadLibrary(final ClassLoader loader, final String name, final boolean absolute) {
378 Throwable suppressed = null;
379 try {
380 try {
381
382 final Class<?> newHelper = tryToLoadClass(loader, NativeLibraryUtil.class);
383 loadLibraryByHelper(newHelper, name, absolute);
384 logger.debug("Successfully loaded the library {}", name);
385 return;
386 } catch (UnsatisfiedLinkError e) {
387 suppressed = e;
388 } catch (Exception e) {
389 suppressed = e;
390 }
391 NativeLibraryUtil.loadLibrary(name, absolute);
392 logger.debug("Successfully loaded the library {}", name);
393 } catch (NoSuchMethodError nsme) {
394 if (suppressed != null) {
395 ThrowableUtil.addSuppressed(nsme, suppressed);
396 }
397 rethrowWithMoreDetailsIfPossible(name, nsme);
398 } catch (UnsatisfiedLinkError ule) {
399 if (suppressed != null) {
400 ThrowableUtil.addSuppressed(ule, suppressed);
401 }
402 throw ule;
403 }
404 }
405
406 @SuppressJava6Requirement(reason = "Guarded by version check")
407 private static void rethrowWithMoreDetailsIfPossible(String name, NoSuchMethodError error) {
408 if (PlatformDependent.javaVersion() >= 7) {
409 throw new LinkageError(
410 "Possible multiple incompatible native libraries on the classpath for '" + name + "'?", error);
411 }
412 throw error;
413 }
414
415 private static void loadLibraryByHelper(final Class<?> helper, final String name, final boolean absolute)
416 throws UnsatisfiedLinkError {
417 Object ret = AccessController.doPrivileged(new PrivilegedAction<Object>() {
418 @Override
419 public Object run() {
420 try {
421
422
423 Method method = helper.getMethod("loadLibrary", String.class, boolean.class);
424 method.setAccessible(true);
425 return method.invoke(null, name, absolute);
426 } catch (Exception e) {
427 return e;
428 }
429 }
430 });
431 if (ret instanceof Throwable) {
432 Throwable t = (Throwable) ret;
433 assert !(t instanceof UnsatisfiedLinkError) : t + " should be a wrapper throwable";
434 Throwable cause = t.getCause();
435 if (cause instanceof UnsatisfiedLinkError) {
436 throw (UnsatisfiedLinkError) cause;
437 }
438 UnsatisfiedLinkError ule = new UnsatisfiedLinkError(t.getMessage());
439 ule.initCause(t);
440 throw ule;
441 }
442 }
443
444
445
446
447
448
449
450
451 private static Class<?> tryToLoadClass(final ClassLoader loader, final Class<?> helper)
452 throws ClassNotFoundException {
453 try {
454 return Class.forName(helper.getName(), false, loader);
455 } catch (ClassNotFoundException e1) {
456 if (loader == null) {
457
458 throw e1;
459 }
460 try {
461
462 final byte[] classBinary = classToByteArray(helper);
463 return AccessController.doPrivileged(new PrivilegedAction<Class<?>>() {
464 @Override
465 public Class<?> run() {
466 try {
467
468
469 Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class,
470 byte[].class, int.class, int.class);
471 defineClass.setAccessible(true);
472 return (Class<?>) defineClass.invoke(loader, helper.getName(), classBinary, 0,
473 classBinary.length);
474 } catch (Exception e) {
475 throw new IllegalStateException("Define class failed!", e);
476 }
477 }
478 });
479 } catch (ClassNotFoundException e2) {
480 ThrowableUtil.addSuppressed(e2, e1);
481 throw e2;
482 } catch (RuntimeException e2) {
483 ThrowableUtil.addSuppressed(e2, e1);
484 throw e2;
485 } catch (Error e2) {
486 ThrowableUtil.addSuppressed(e2, e1);
487 throw e2;
488 }
489 }
490 }
491
492
493
494
495
496
497
498 private static byte[] classToByteArray(Class<?> clazz) throws ClassNotFoundException {
499 String fileName = clazz.getName();
500 int lastDot = fileName.lastIndexOf('.');
501 if (lastDot > 0) {
502 fileName = fileName.substring(lastDot + 1);
503 }
504 URL classUrl = clazz.getResource(fileName + ".class");
505 if (classUrl == null) {
506 throw new ClassNotFoundException(clazz.getName());
507 }
508 byte[] buf = new byte[1024];
509 ByteArrayOutputStream out = new ByteArrayOutputStream(4096);
510 InputStream in = null;
511 try {
512 in = classUrl.openStream();
513 for (int r; (r = in.read(buf)) != -1;) {
514 out.write(buf, 0, r);
515 }
516 return out.toByteArray();
517 } catch (IOException ex) {
518 throw new ClassNotFoundException(clazz.getName(), ex);
519 } finally {
520 closeQuietly(in);
521 closeQuietly(out);
522 }
523 }
524
525 private static void closeQuietly(Closeable c) {
526 if (c != null) {
527 try {
528 c.close();
529 } catch (IOException ignore) {
530
531 }
532 }
533 }
534
535 private NativeLibraryLoader() {
536
537 }
538
539 private static final class NoexecVolumeDetector {
540
541 @SuppressJava6Requirement(reason = "Usage guarded by java version check")
542 private static boolean canExecuteExecutable(File file) throws IOException {
543 if (PlatformDependent.javaVersion() < 7) {
544
545
546 return true;
547 }
548
549
550 if (file.canExecute()) {
551 return true;
552 }
553
554
555
556
557
558
559
560 Set<java.nio.file.attribute.PosixFilePermission> existingFilePermissions =
561 java.nio.file.Files.getPosixFilePermissions(file.toPath());
562 Set<java.nio.file.attribute.PosixFilePermission> executePermissions =
563 EnumSet.of(java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE,
564 java.nio.file.attribute.PosixFilePermission.GROUP_EXECUTE,
565 java.nio.file.attribute.PosixFilePermission.OTHERS_EXECUTE);
566 if (existingFilePermissions.containsAll(executePermissions)) {
567 return false;
568 }
569
570 Set<java.nio.file.attribute.PosixFilePermission> newPermissions = EnumSet.copyOf(existingFilePermissions);
571 newPermissions.addAll(executePermissions);
572 java.nio.file.Files.setPosixFilePermissions(file.toPath(), newPermissions);
573 return file.canExecute();
574 }
575
576 private NoexecVolumeDetector() {
577
578 }
579 }
580 }