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 String message = String.format(
227 "%s exists but cannot be executed even when execute permissions set; " +
228 "check volume for \"noexec\" flag; use -D%s=[path] " +
229 "to set native working directory separately.",
230 tmpFile.getPath(), "io.netty.native.workdir");
231 logger.info(message);
232 suppressed.add(ThrowableUtil.unknownStackTrace(
233 new UnsatisfiedLinkError(message), NativeLibraryLoader.class, "load"));
234 }
235 } catch (Throwable t) {
236 suppressed.add(t);
237 logger.debug("Error checking if {} is on a file store mounted with noexec", tmpFile, t);
238 }
239
240 ThrowableUtil.addSuppressedAndClear(e, suppressed);
241 throw e;
242 } catch (Exception e) {
243 UnsatisfiedLinkError ule = new UnsatisfiedLinkError("could not load a native library: " + name);
244 ule.initCause(e);
245 ThrowableUtil.addSuppressedAndClear(ule, suppressed);
246 throw ule;
247 } finally {
248 closeQuietly(in);
249 closeQuietly(out);
250
251
252
253 if (tmpFile != null && (!DELETE_NATIVE_LIB_AFTER_LOADING || !tmpFile.delete())) {
254 tmpFile.deleteOnExit();
255 }
256 }
257 }
258
259 private static URL getResource(String path, ClassLoader loader) {
260 final Enumeration<URL> urls;
261 try {
262 if (loader == null) {
263 urls = ClassLoader.getSystemResources(path);
264 } else {
265 urls = loader.getResources(path);
266 }
267 } catch (IOException iox) {
268 throw new RuntimeException("An error occurred while getting the resources for " + path, iox);
269 }
270
271 List<URL> urlsList = Collections.list(urls);
272 int size = urlsList.size();
273 switch (size) {
274 case 0:
275 return null;
276 case 1:
277 return urlsList.get(0);
278 default:
279 if (DETECT_NATIVE_LIBRARY_DUPLICATES) {
280 try {
281 MessageDigest md = MessageDigest.getInstance("SHA-256");
282
283
284 URL url = urlsList.get(0);
285 byte[] digest = digest(md, url);
286 boolean allSame = true;
287 if (digest != null) {
288 for (int i = 1; i < size; i++) {
289 byte[] digest2 = digest(md, urlsList.get(i));
290 if (digest2 == null || !Arrays.equals(digest, digest2)) {
291 allSame = false;
292 break;
293 }
294 }
295 } else {
296 allSame = false;
297 }
298 if (allSame) {
299 return url;
300 }
301 } catch (NoSuchAlgorithmException e) {
302 logger.debug("Don't support SHA-256, can't check if resources have same content.", e);
303 }
304
305 throw new IllegalStateException(
306 "Multiple resources found for '" + path + "' with different content: " + urlsList);
307 } else {
308 logger.warn("Multiple resources found for '" + path + "' with different content: " +
309 urlsList + ". Please fix your dependency graph.");
310 return urlsList.get(0);
311 }
312 }
313 }
314
315 private static byte[] digest(MessageDigest digest, URL url) {
316 InputStream in = null;
317 try {
318 in = url.openStream();
319 byte[] bytes = new byte[8192];
320 int i;
321 while ((i = in.read(bytes)) != -1) {
322 digest.update(bytes, 0, i);
323 }
324 return digest.digest();
325 } catch (IOException e) {
326 logger.debug("Can't read resource.", e);
327 return null;
328 } finally {
329 closeQuietly(in);
330 }
331 }
332
333 static void tryPatchShadedLibraryIdAndSign(File libraryFile, String originalName) {
334 if (!new File("/Library/Developer/CommandLineTools").exists()) {
335 logger.debug("Can't patch shaded library id as CommandLineTools are not installed." +
336 " Consider installing CommandLineTools with 'xcode-select --install'");
337 return;
338 }
339 String newId = new String(generateUniqueId(originalName.length()), CharsetUtil.UTF_8);
340 if (!tryExec("install_name_tool -id " + newId + " " + libraryFile.getAbsolutePath())) {
341 return;
342 }
343
344 tryExec("codesign -s - " + libraryFile.getAbsolutePath());
345 }
346
347 private static boolean tryExec(String cmd) {
348 try {
349 int exitValue = Runtime.getRuntime().exec(cmd).waitFor();
350 if (exitValue != 0) {
351 logger.debug("Execution of '{}' failed: {}", cmd, exitValue);
352 return false;
353 }
354 logger.debug("Execution of '{}' succeed: {}", cmd, exitValue);
355 return true;
356 } catch (InterruptedException e) {
357 Thread.currentThread().interrupt();
358 } catch (IOException e) {
359 logger.info("Execution of '{}' failed.", cmd, e);
360 } catch (SecurityException e) {
361 logger.error("Execution of '{}' failed.", cmd, e);
362 }
363 return false;
364 }
365
366 private static boolean shouldShadedLibraryIdBePatched(String packagePrefix) {
367 return TRY_TO_PATCH_SHADED_ID && PlatformDependent.isOsx() && !packagePrefix.isEmpty();
368 }
369
370 private static byte[] generateUniqueId(int length) {
371 byte[] idBytes = new byte[length];
372 for (int i = 0; i < idBytes.length; i++) {
373
374 idBytes[i] = UNIQUE_ID_BYTES[PlatformDependent.threadLocalRandom()
375 .nextInt(UNIQUE_ID_BYTES.length)];
376 }
377 return idBytes;
378 }
379
380
381
382
383
384
385
386 private static void loadLibrary(final ClassLoader loader, final String name, final boolean absolute) {
387 Throwable suppressed = null;
388 try {
389 try {
390
391 final Class<?> newHelper = tryToLoadClass(loader, NativeLibraryUtil.class);
392 loadLibraryByHelper(newHelper, name, absolute);
393 logger.debug("Successfully loaded the library {}", name);
394 return;
395 } catch (UnsatisfiedLinkError e) {
396 suppressed = e;
397 } catch (Exception e) {
398 suppressed = e;
399 }
400 NativeLibraryUtil.loadLibrary(name, absolute);
401 logger.debug("Successfully loaded the library {}", name);
402 } catch (NoSuchMethodError nsme) {
403 if (suppressed != null) {
404 ThrowableUtil.addSuppressed(nsme, suppressed);
405 }
406 rethrowWithMoreDetailsIfPossible(name, nsme);
407 } catch (UnsatisfiedLinkError ule) {
408 if (suppressed != null) {
409 ThrowableUtil.addSuppressed(ule, suppressed);
410 }
411 throw ule;
412 }
413 }
414
415 @SuppressJava6Requirement(reason = "Guarded by version check")
416 private static void rethrowWithMoreDetailsIfPossible(String name, NoSuchMethodError error) {
417 if (PlatformDependent.javaVersion() >= 7) {
418 throw new LinkageError(
419 "Possible multiple incompatible native libraries on the classpath for '" + name + "'?", error);
420 }
421 throw error;
422 }
423
424 private static void loadLibraryByHelper(final Class<?> helper, final String name, final boolean absolute)
425 throws UnsatisfiedLinkError {
426 Object ret = AccessController.doPrivileged(new PrivilegedAction<Object>() {
427 @Override
428 public Object run() {
429 try {
430
431
432 Method method = helper.getMethod("loadLibrary", String.class, boolean.class);
433 method.setAccessible(true);
434 return method.invoke(null, name, absolute);
435 } catch (Exception e) {
436 return e;
437 }
438 }
439 });
440 if (ret instanceof Throwable) {
441 Throwable t = (Throwable) ret;
442 assert !(t instanceof UnsatisfiedLinkError) : t + " should be a wrapper throwable";
443 Throwable cause = t.getCause();
444 if (cause instanceof UnsatisfiedLinkError) {
445 throw (UnsatisfiedLinkError) cause;
446 }
447 UnsatisfiedLinkError ule = new UnsatisfiedLinkError(t.getMessage());
448 ule.initCause(t);
449 throw ule;
450 }
451 }
452
453
454
455
456
457
458
459
460 private static Class<?> tryToLoadClass(final ClassLoader loader, final Class<?> helper)
461 throws ClassNotFoundException {
462 try {
463 return Class.forName(helper.getName(), false, loader);
464 } catch (ClassNotFoundException e1) {
465 if (loader == null) {
466
467 throw e1;
468 }
469 try {
470
471 final byte[] classBinary = classToByteArray(helper);
472 return AccessController.doPrivileged(new PrivilegedAction<Class<?>>() {
473 @Override
474 public Class<?> run() {
475 try {
476
477
478 Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class,
479 byte[].class, int.class, int.class);
480 defineClass.setAccessible(true);
481 return (Class<?>) defineClass.invoke(loader, helper.getName(), classBinary, 0,
482 classBinary.length);
483 } catch (Exception e) {
484 throw new IllegalStateException("Define class failed!", e);
485 }
486 }
487 });
488 } catch (ClassNotFoundException e2) {
489 ThrowableUtil.addSuppressed(e2, e1);
490 throw e2;
491 } catch (RuntimeException e2) {
492 ThrowableUtil.addSuppressed(e2, e1);
493 throw e2;
494 } catch (Error e2) {
495 ThrowableUtil.addSuppressed(e2, e1);
496 throw e2;
497 }
498 }
499 }
500
501
502
503
504
505
506
507 private static byte[] classToByteArray(Class<?> clazz) throws ClassNotFoundException {
508 String fileName = clazz.getName();
509 int lastDot = fileName.lastIndexOf('.');
510 if (lastDot > 0) {
511 fileName = fileName.substring(lastDot + 1);
512 }
513 URL classUrl = clazz.getResource(fileName + ".class");
514 if (classUrl == null) {
515 throw new ClassNotFoundException(clazz.getName());
516 }
517 byte[] buf = new byte[1024];
518 ByteArrayOutputStream out = new ByteArrayOutputStream(4096);
519 InputStream in = null;
520 try {
521 in = classUrl.openStream();
522 for (int r; (r = in.read(buf)) != -1;) {
523 out.write(buf, 0, r);
524 }
525 return out.toByteArray();
526 } catch (IOException ex) {
527 throw new ClassNotFoundException(clazz.getName(), ex);
528 } finally {
529 closeQuietly(in);
530 closeQuietly(out);
531 }
532 }
533
534 private static void closeQuietly(Closeable c) {
535 if (c != null) {
536 try {
537 c.close();
538 } catch (IOException ignore) {
539
540 }
541 }
542 }
543
544 private NativeLibraryLoader() {
545
546 }
547
548 private static final class NoexecVolumeDetector {
549
550 @SuppressJava6Requirement(reason = "Usage guarded by java version check")
551 private static boolean canExecuteExecutable(File file) throws IOException {
552 if (PlatformDependent.javaVersion() < 7) {
553
554
555 return true;
556 }
557
558
559 if (file.canExecute()) {
560 return true;
561 }
562
563
564
565
566
567
568
569 Set<java.nio.file.attribute.PosixFilePermission> existingFilePermissions =
570 java.nio.file.Files.getPosixFilePermissions(file.toPath());
571 Set<java.nio.file.attribute.PosixFilePermission> executePermissions =
572 EnumSet.of(java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE,
573 java.nio.file.attribute.PosixFilePermission.GROUP_EXECUTE,
574 java.nio.file.attribute.PosixFilePermission.OTHERS_EXECUTE);
575 if (existingFilePermissions.containsAll(executePermissions)) {
576 return false;
577 }
578
579 Set<java.nio.file.attribute.PosixFilePermission> newPermissions = EnumSet.copyOf(existingFilePermissions);
580 newPermissions.addAll(executePermissions);
581 java.nio.file.Files.setPosixFilePermissions(file.toPath(), newPermissions);
582 return file.canExecute();
583 }
584
585 private NoexecVolumeDetector() {
586
587 }
588 }
589 }