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