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 if (!new File("/Library/Developer/CommandLineTools").exists()) {
331 logger.debug("Can't patch shaded library id as CommandLineTools are not installed." +
332 " Consider installing CommandLineTools with 'xcode-select --install'");
333 return;
334 }
335 String newId = new String(generateUniqueId(originalName.length()), CharsetUtil.UTF_8);
336 if (!tryExec("install_name_tool -id " + newId + " " + libraryFile.getAbsolutePath())) {
337 return;
338 }
339
340 tryExec("codesign -s - " + libraryFile.getAbsolutePath());
341 }
342
343 private static boolean tryExec(String cmd) {
344 try {
345 int exitValue = Runtime.getRuntime().exec(cmd).waitFor();
346 if (exitValue != 0) {
347 logger.debug("Execution of '{}' failed: {}", cmd, exitValue);
348 return false;
349 }
350 logger.debug("Execution of '{}' succeed: {}", cmd, exitValue);
351 return true;
352 } catch (InterruptedException e) {
353 Thread.currentThread().interrupt();
354 } catch (IOException e) {
355 logger.info("Execution of '{}' failed.", cmd, e);
356 } catch (SecurityException e) {
357 logger.error("Execution of '{}' failed.", cmd, e);
358 }
359 return false;
360 }
361
362 private static boolean shouldShadedLibraryIdBePatched(String packagePrefix) {
363 return TRY_TO_PATCH_SHADED_ID && PlatformDependent.isOsx() && !packagePrefix.isEmpty();
364 }
365
366 private static byte[] generateUniqueId(int length) {
367 byte[] idBytes = new byte[length];
368 for (int i = 0; i < idBytes.length; i++) {
369
370 idBytes[i] = UNIQUE_ID_BYTES[PlatformDependent.threadLocalRandom()
371 .nextInt(UNIQUE_ID_BYTES.length)];
372 }
373 return idBytes;
374 }
375
376
377
378
379
380
381
382 private static void loadLibrary(final ClassLoader loader, final String name, final boolean absolute) {
383 Throwable suppressed = null;
384 try {
385 try {
386
387 final Class<?> newHelper = tryToLoadClass(loader, NativeLibraryUtil.class);
388 loadLibraryByHelper(newHelper, name, absolute);
389 logger.debug("Successfully loaded the library {}", name);
390 return;
391 } catch (UnsatisfiedLinkError e) {
392 suppressed = e;
393 } catch (Exception e) {
394 suppressed = e;
395 }
396 NativeLibraryUtil.loadLibrary(name, absolute);
397 logger.debug("Successfully loaded the library {}", name);
398 } catch (NoSuchMethodError nsme) {
399 if (suppressed != null) {
400 ThrowableUtil.addSuppressed(nsme, suppressed);
401 }
402 rethrowWithMoreDetailsIfPossible(name, nsme);
403 } catch (UnsatisfiedLinkError ule) {
404 if (suppressed != null) {
405 ThrowableUtil.addSuppressed(ule, suppressed);
406 }
407 throw ule;
408 }
409 }
410
411 @SuppressJava6Requirement(reason = "Guarded by version check")
412 private static void rethrowWithMoreDetailsIfPossible(String name, NoSuchMethodError error) {
413 if (PlatformDependent.javaVersion() >= 7) {
414 throw new LinkageError(
415 "Possible multiple incompatible native libraries on the classpath for '" + name + "'?", error);
416 }
417 throw error;
418 }
419
420 private static void loadLibraryByHelper(final Class<?> helper, final String name, final boolean absolute)
421 throws UnsatisfiedLinkError {
422 Object ret = AccessController.doPrivileged(new PrivilegedAction<Object>() {
423 @Override
424 public Object run() {
425 try {
426
427
428 Method method = helper.getMethod("loadLibrary", String.class, boolean.class);
429 method.setAccessible(true);
430 return method.invoke(null, name, absolute);
431 } catch (Exception e) {
432 return e;
433 }
434 }
435 });
436 if (ret instanceof Throwable) {
437 Throwable t = (Throwable) ret;
438 assert !(t instanceof UnsatisfiedLinkError) : t + " should be a wrapper throwable";
439 Throwable cause = t.getCause();
440 if (cause instanceof UnsatisfiedLinkError) {
441 throw (UnsatisfiedLinkError) cause;
442 }
443 UnsatisfiedLinkError ule = new UnsatisfiedLinkError(t.getMessage());
444 ule.initCause(t);
445 throw ule;
446 }
447 }
448
449
450
451
452
453
454
455
456 private static Class<?> tryToLoadClass(final ClassLoader loader, final Class<?> helper)
457 throws ClassNotFoundException {
458 try {
459 return Class.forName(helper.getName(), false, loader);
460 } catch (ClassNotFoundException e1) {
461 if (loader == null) {
462
463 throw e1;
464 }
465 try {
466
467 final byte[] classBinary = classToByteArray(helper);
468 return AccessController.doPrivileged(new PrivilegedAction<Class<?>>() {
469 @Override
470 public Class<?> run() {
471 try {
472
473
474 Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class,
475 byte[].class, int.class, int.class);
476 defineClass.setAccessible(true);
477 return (Class<?>) defineClass.invoke(loader, helper.getName(), classBinary, 0,
478 classBinary.length);
479 } catch (Exception e) {
480 throw new IllegalStateException("Define class failed!", e);
481 }
482 }
483 });
484 } catch (ClassNotFoundException e2) {
485 ThrowableUtil.addSuppressed(e2, e1);
486 throw e2;
487 } catch (RuntimeException e2) {
488 ThrowableUtil.addSuppressed(e2, e1);
489 throw e2;
490 } catch (Error e2) {
491 ThrowableUtil.addSuppressed(e2, e1);
492 throw e2;
493 }
494 }
495 }
496
497
498
499
500
501
502
503 private static byte[] classToByteArray(Class<?> clazz) throws ClassNotFoundException {
504 String fileName = clazz.getName();
505 int lastDot = fileName.lastIndexOf('.');
506 if (lastDot > 0) {
507 fileName = fileName.substring(lastDot + 1);
508 }
509 URL classUrl = clazz.getResource(fileName + ".class");
510 if (classUrl == null) {
511 throw new ClassNotFoundException(clazz.getName());
512 }
513 byte[] buf = new byte[1024];
514 ByteArrayOutputStream out = new ByteArrayOutputStream(4096);
515 InputStream in = null;
516 try {
517 in = classUrl.openStream();
518 for (int r; (r = in.read(buf)) != -1;) {
519 out.write(buf, 0, r);
520 }
521 return out.toByteArray();
522 } catch (IOException ex) {
523 throw new ClassNotFoundException(clazz.getName(), ex);
524 } finally {
525 closeQuietly(in);
526 closeQuietly(out);
527 }
528 }
529
530 private static void closeQuietly(Closeable c) {
531 if (c != null) {
532 try {
533 c.close();
534 } catch (IOException ignore) {
535
536 }
537 }
538 }
539
540 private NativeLibraryLoader() {
541
542 }
543
544 private static final class NoexecVolumeDetector {
545
546 @SuppressJava6Requirement(reason = "Usage guarded by java version check")
547 private static boolean canExecuteExecutable(File file) throws IOException {
548 if (PlatformDependent.javaVersion() < 7) {
549
550
551 return true;
552 }
553
554
555 if (file.canExecute()) {
556 return true;
557 }
558
559
560
561
562
563
564
565 Set<java.nio.file.attribute.PosixFilePermission> existingFilePermissions =
566 java.nio.file.Files.getPosixFilePermissions(file.toPath());
567 Set<java.nio.file.attribute.PosixFilePermission> executePermissions =
568 EnumSet.of(java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE,
569 java.nio.file.attribute.PosixFilePermission.GROUP_EXECUTE,
570 java.nio.file.attribute.PosixFilePermission.OTHERS_EXECUTE);
571 if (existingFilePermissions.containsAll(executePermissions)) {
572 return false;
573 }
574
575 Set<java.nio.file.attribute.PosixFilePermission> newPermissions = EnumSet.copyOf(existingFilePermissions);
576 newPermissions.addAll(executePermissions);
577 java.nio.file.Files.setPosixFilePermissions(file.toPath(), newPermissions);
578 return file.canExecute();
579 }
580
581 private NoexecVolumeDetector() {
582
583 }
584 }
585 }