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