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.nio.file.Files;
33 import java.nio.file.attribute.PosixFilePermission;
34 import java.security.AccessController;
35 import java.security.MessageDigest;
36 import java.security.NoSuchAlgorithmException;
37 import java.security.PrivilegedAction;
38 import java.util.ArrayList;
39 import java.util.Arrays;
40 import java.util.Collections;
41 import java.util.EnumSet;
42 import java.util.Enumeration;
43 import java.util.List;
44 import java.util.Set;
45
46
47
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
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
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
101
102
103
104
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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143 private static String calculateMangledPackagePrefix() {
144 String maybeShaded = NativeLibraryLoader.class.getName();
145
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
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<Throwable>();
164 try {
165
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 InputStream in = null;
176 OutputStream out = null;
177 File tmpFile = null;
178 URL url = getResource(path, loader);
179 try {
180 if (url == null) {
181 if (PlatformDependent.isOsx()) {
182 String fileName = path.endsWith(".jnilib") ? NATIVE_RESOURCE_HOME + "lib" + name + ".dynlib" :
183 NATIVE_RESOURCE_HOME + "lib" + name + ".jnilib";
184 url = getResource(fileName, loader);
185 if (url == null) {
186 FileNotFoundException fnf = new FileNotFoundException(fileName);
187 ThrowableUtil.addSuppressedAndClear(fnf, suppressed);
188 throw fnf;
189 }
190 } else {
191 FileNotFoundException fnf = new FileNotFoundException(path);
192 ThrowableUtil.addSuppressedAndClear(fnf, suppressed);
193 throw fnf;
194 }
195 }
196
197 int index = libname.lastIndexOf('.');
198 String prefix = libname.substring(0, index);
199 String suffix = libname.substring(index);
200
201 tmpFile = PlatformDependent.createTempFile(prefix, suffix, WORKDIR);
202 in = url.openStream();
203 out = new FileOutputStream(tmpFile);
204
205 byte[] buffer = new byte[8192];
206 int length;
207 while ((length = in.read(buffer)) > 0) {
208 out.write(buffer, 0, length);
209 }
210 out.flush();
211
212 if (shouldShadedLibraryIdBePatched(mangledPackagePrefix)) {
213
214
215 tryPatchShadedLibraryIdAndSign(tmpFile, originalName);
216 }
217
218
219
220 closeQuietly(out);
221 out = null;
222
223 loadLibrary(loader, tmpFile.getPath(), true);
224 } catch (UnsatisfiedLinkError e) {
225 try {
226 if (tmpFile != null && tmpFile.isFile() && tmpFile.canRead() &&
227 !NoexecVolumeDetector.canExecuteExecutable(tmpFile)) {
228
229
230
231 logger.info("{} exists but cannot be executed even when execute permissions set; " +
232 "check volume for \"noexec\" flag; use -D{}=[path] " +
233 "to set native working directory separately.",
234 tmpFile.getPath(), "io.netty.native.workdir");
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
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 closeQuietly(in);
250 closeQuietly(out);
251
252
253
254 if (tmpFile != null && (!DELETE_NATIVE_LIB_AFTER_LOADING || !tmpFile.delete())) {
255 tmpFile.deleteOnExit();
256 }
257 }
258 }
259
260 private static URL getResource(String path, ClassLoader loader) {
261 final Enumeration<URL> urls;
262 try {
263 if (loader == null) {
264 urls = ClassLoader.getSystemResources(path);
265 } else {
266 urls = loader.getResources(path);
267 }
268 } catch (IOException iox) {
269 throw new RuntimeException("An error occurred while getting the resources for " + path, iox);
270 }
271
272 List<URL> urlsList = Collections.list(urls);
273 int size = urlsList.size();
274 switch (size) {
275 case 0:
276 return null;
277 case 1:
278 return urlsList.get(0);
279 default:
280 if (DETECT_NATIVE_LIBRARY_DUPLICATES) {
281 try {
282 MessageDigest md = MessageDigest.getInstance("SHA-256");
283
284
285 URL url = urlsList.get(0);
286 byte[] digest = digest(md, url);
287 boolean allSame = true;
288 if (digest != null) {
289 for (int i = 1; i < size; i++) {
290 byte[] digest2 = digest(md, urlsList.get(i));
291 if (digest2 == null || !Arrays.equals(digest, digest2)) {
292 allSame = false;
293 break;
294 }
295 }
296 } else {
297 allSame = false;
298 }
299 if (allSame) {
300 return url;
301 }
302 } catch (NoSuchAlgorithmException e) {
303 logger.debug("Don't support SHA-256, can't check if resources have same content.", e);
304 }
305
306 throw new IllegalStateException(
307 "Multiple resources found for '" + path + "' with different content: " + urlsList);
308 } else {
309 logger.warn("Multiple resources found for '" + path + "' with different content: " +
310 urlsList + ". Please fix your dependency graph.");
311 return urlsList.get(0);
312 }
313 }
314 }
315
316 private static byte[] digest(MessageDigest digest, URL url) {
317 InputStream in = null;
318 try {
319 in = url.openStream();
320 byte[] bytes = new byte[8192];
321 int i;
322 while ((i = in.read(bytes)) != -1) {
323 digest.update(bytes, 0, i);
324 }
325 return digest.digest();
326 } catch (IOException e) {
327 logger.debug("Can't read resource.", e);
328 return null;
329 } finally {
330 closeQuietly(in);
331 }
332 }
333
334 static void tryPatchShadedLibraryIdAndSign(File libraryFile, String originalName) {
335 if (!new File("/Library/Developer/CommandLineTools").exists()) {
336 logger.debug("Can't patch shaded library id as CommandLineTools are not installed." +
337 " Consider installing CommandLineTools with 'xcode-select --install'");
338 return;
339 }
340 String newId = new String(generateUniqueId(originalName.length()), CharsetUtil.UTF_8);
341 if (!tryExec("install_name_tool -id " + newId + " " + libraryFile.getAbsolutePath())) {
342 return;
343 }
344
345 tryExec("codesign -s - " + libraryFile.getAbsolutePath());
346 }
347
348 private static boolean tryExec(String cmd) {
349 try {
350 int exitValue = Runtime.getRuntime().exec(cmd).waitFor();
351 if (exitValue != 0) {
352 logger.debug("Execution of '{}' failed: {}", cmd, exitValue);
353 return false;
354 }
355 logger.debug("Execution of '{}' succeed: {}", cmd, exitValue);
356 return true;
357 } catch (InterruptedException e) {
358 Thread.currentThread().interrupt();
359 } catch (IOException e) {
360 logger.info("Execution of '{}' failed.", cmd, e);
361 } catch (SecurityException e) {
362 logger.error("Execution of '{}' failed.", cmd, e);
363 }
364 return false;
365 }
366
367 private static boolean shouldShadedLibraryIdBePatched(String packagePrefix) {
368 return TRY_TO_PATCH_SHADED_ID && PlatformDependent.isOsx() && !packagePrefix.isEmpty();
369 }
370
371 private static byte[] generateUniqueId(int length) {
372 byte[] idBytes = new byte[length];
373 for (int i = 0; i < idBytes.length; i++) {
374
375 idBytes[i] = UNIQUE_ID_BYTES[PlatformDependent.threadLocalRandom()
376 .nextInt(UNIQUE_ID_BYTES.length)];
377 }
378 return idBytes;
379 }
380
381
382
383
384
385
386
387 private static void loadLibrary(final ClassLoader loader, final String name, final boolean absolute) {
388 Throwable suppressed = null;
389 try {
390 try {
391
392 final Class<?> newHelper = tryToLoadClass(loader, NativeLibraryUtil.class);
393 loadLibraryByHelper(newHelper, name, absolute);
394 logger.debug("Successfully loaded the library {}", name);
395 return;
396 } catch (UnsatisfiedLinkError e) {
397 suppressed = e;
398 } catch (Exception e) {
399 suppressed = e;
400 }
401 NativeLibraryUtil.loadLibrary(name, absolute);
402 logger.debug("Successfully loaded the library {}", name);
403 } catch (NoSuchMethodError nsme) {
404 if (suppressed != null) {
405 ThrowableUtil.addSuppressed(nsme, suppressed);
406 }
407 throw new LinkageError(
408 "Possible multiple incompatible native libraries on the classpath for '" + name + "'?", nsme);
409 } catch (UnsatisfiedLinkError ule) {
410 if (suppressed != null) {
411 ThrowableUtil.addSuppressed(ule, suppressed);
412 }
413 throw ule;
414 }
415 }
416
417 private static void loadLibraryByHelper(final Class<?> helper, final String name, final boolean absolute)
418 throws UnsatisfiedLinkError {
419 Object ret = AccessController.doPrivileged(new PrivilegedAction<Object>() {
420 @Override
421 public Object run() {
422 try {
423
424
425 Method method = helper.getMethod("loadLibrary", String.class, boolean.class);
426 method.setAccessible(true);
427 return method.invoke(null, name, absolute);
428 } catch (Exception e) {
429 return e;
430 }
431 }
432 });
433 if (ret instanceof Throwable) {
434 Throwable t = (Throwable) ret;
435 assert !(t instanceof UnsatisfiedLinkError) : t + " should be a wrapper throwable";
436 Throwable cause = t.getCause();
437 if (cause instanceof UnsatisfiedLinkError) {
438 throw (UnsatisfiedLinkError) cause;
439 }
440 UnsatisfiedLinkError ule = new UnsatisfiedLinkError(t.getMessage());
441 ule.initCause(t);
442 throw ule;
443 }
444 }
445
446
447
448
449
450
451
452
453 private static Class<?> tryToLoadClass(final ClassLoader loader, final Class<?> helper)
454 throws ClassNotFoundException {
455 try {
456 return Class.forName(helper.getName(), false, loader);
457 } catch (ClassNotFoundException e1) {
458 if (loader == null) {
459
460 throw e1;
461 }
462 try {
463
464 final byte[] classBinary = classToByteArray(helper);
465 return AccessController.doPrivileged(new PrivilegedAction<Class<?>>() {
466 @Override
467 public Class<?> run() {
468 try {
469
470
471 Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class,
472 byte[].class, int.class, int.class);
473 defineClass.setAccessible(true);
474 return (Class<?>) defineClass.invoke(loader, helper.getName(), classBinary, 0,
475 classBinary.length);
476 } catch (Exception e) {
477 throw new IllegalStateException("Define class failed!", e);
478 }
479 }
480 });
481 } catch (ClassNotFoundException e2) {
482 ThrowableUtil.addSuppressed(e2, e1);
483 throw e2;
484 } catch (RuntimeException e2) {
485 ThrowableUtil.addSuppressed(e2, e1);
486 throw e2;
487 } catch (Error e2) {
488 ThrowableUtil.addSuppressed(e2, e1);
489 throw e2;
490 }
491 }
492 }
493
494
495
496
497
498
499
500 private static byte[] classToByteArray(Class<?> clazz) throws ClassNotFoundException {
501 String fileName = clazz.getName();
502 int lastDot = fileName.lastIndexOf('.');
503 if (lastDot > 0) {
504 fileName = fileName.substring(lastDot + 1);
505 }
506 URL classUrl = clazz.getResource(fileName + ".class");
507 if (classUrl == null) {
508 throw new ClassNotFoundException(clazz.getName());
509 }
510 byte[] buf = new byte[1024];
511 ByteArrayOutputStream out = new ByteArrayOutputStream(4096);
512 InputStream in = null;
513 try {
514 in = classUrl.openStream();
515 for (int r; (r = in.read(buf)) != -1;) {
516 out.write(buf, 0, r);
517 }
518 return out.toByteArray();
519 } catch (IOException ex) {
520 throw new ClassNotFoundException(clazz.getName(), ex);
521 } finally {
522 closeQuietly(in);
523 closeQuietly(out);
524 }
525 }
526
527 private static void closeQuietly(Closeable c) {
528 if (c != null) {
529 try {
530 c.close();
531 } catch (IOException ignore) {
532
533 }
534 }
535 }
536
537 private NativeLibraryLoader() {
538
539 }
540
541 private static final class NoexecVolumeDetector {
542
543 private static boolean canExecuteExecutable(File file) throws IOException {
544
545 if (file.canExecute()) {
546 return true;
547 }
548
549
550
551
552
553 Set<PosixFilePermission> existingFilePermissions = Files.getPosixFilePermissions(file.toPath());
554 Set<PosixFilePermission> executePermissions =
555 EnumSet.of(PosixFilePermission.OWNER_EXECUTE,
556 PosixFilePermission.GROUP_EXECUTE,
557 PosixFilePermission.OTHERS_EXECUTE);
558 if (existingFilePermissions.containsAll(executePermissions)) {
559 return false;
560 }
561
562 Set<PosixFilePermission> newPermissions = EnumSet.copyOf(existingFilePermissions);
563 newPermissions.addAll(executePermissions);
564 Files.setPosixFilePermissions(file.toPath(), newPermissions);
565 return file.canExecute();
566 }
567
568 private NoexecVolumeDetector() {
569
570 }
571 }
572 }