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