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