View Javadoc
1   /*
2    * junixsocket
3    *
4    * Copyright 2009-2024 Christian Kohlschütter
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.newsclub.net.unix;
19  
20  import java.io.Closeable;
21  import java.io.File;
22  import java.io.FileNotFoundException;
23  import java.io.FileOutputStream;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.OutputStream;
27  import java.net.URL;
28  import java.nio.file.Files;
29  import java.util.ArrayList;
30  import java.util.Collections;
31  import java.util.List;
32  import java.util.Objects;
33  import java.util.Properties;
34  import java.util.concurrent.atomic.AtomicBoolean;
35  
36  import com.kohlschutter.annotations.compiletime.SuppressFBWarnings;
37  
38  @SuppressFBWarnings("RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE")
39  final class NativeLibraryLoader implements Closeable {
40    private static final String PROP_LIBRARY_DISABLE = "org.newsclub.net.unix.library.disable";
41    private static final String PROP_LIBRARY_OVERRIDE = "org.newsclub.net.unix.library.override";
42    private static final String PROP_LIBRARY_OVERRIDE_FORCE =
43        "org.newsclub.net.unix.library.override.force";
44    private static final String PROP_LIBRARY_TMPDIR = "org.newsclub.net.unix.library.tmpdir";
45  
46    private static final File TEMP_DIR;
47    private static final String OS_NAME_SIMPLIFIED = lookupArchProperty("os.name", "UnknownOS");
48  
49    private static final List<String> ARCHITECTURE_AND_OS = architectureAndOS();
50    private static final String LIBRARY_NAME = "junixsocket-native";
51  
52    private static final AtomicBoolean LOADED = new AtomicBoolean(false);
53    private static final boolean IS_ANDROID = checkAndroid();
54  
55    static {
56      String dir = System.getProperty(PROP_LIBRARY_TMPDIR, System.getProperty("java.io.tmpdir",
57          null));
58      TEMP_DIR = (dir == null) ? null : new File(dir);
59    }
60  
61    NativeLibraryLoader() {
62    }
63  
64    /**
65     * Returns the temporary directory where the native library is extracted to; debugging only.
66     *
67     * @return The temporary directory.
68     */
69    static File tempDir() {
70      return TEMP_DIR;
71    }
72  
73    private List<LibraryCandidate> tryProviderClass(String providerClassname, String artifactName)
74        throws IOException, ClassNotFoundException {
75      Class<?> providerClass = Class.forName(providerClassname);
76  
77      String version = getArtifactVersion(providerClass, artifactName);
78      String libraryNameAndVersion = LIBRARY_NAME + "-" + version;
79  
80      return findLibraryCandidates(artifactName, libraryNameAndVersion, providerClass);
81    }
82  
83    public static String getJunixsocketVersion() throws IOException {
84      // NOTE: This can't easily be tested from within the junixsocket-common Maven build
85  
86      String v = BuildProperties.getBuildProperties().get("git.build.version");
87      if (v != null && !v.startsWith("$")) {
88        return v;
89      }
90  
91      return getArtifactVersion(AFSocket.class, "junixsocket-common");
92    }
93  
94    private static String getArtifactVersion(Class<?> providerClass, String... artifactNames)
95        throws IOException {
96      for (String artifactName : artifactNames) {
97        Properties p = new Properties();
98        String resource = "/META-INF/maven/com.kohlschutter.junixsocket/" + artifactName
99            + "/pom.properties";
100       try (InputStream in = providerClass.getResourceAsStream(resource)) {
101         if (in == null) {
102           throw new FileNotFoundException("Could not find resource " + resource + " relative to "
103               + providerClass);
104         }
105         p.load(in);
106         String version = p.getProperty("version");
107 
108         Objects.requireNonNull(version, "Could not read version from pom.properties");
109         return version;
110       }
111     }
112     throw new IllegalStateException("No artifact names specified");
113   }
114 
115   private abstract static class LibraryCandidate implements Closeable {
116     protected final String libraryNameAndVersion;
117 
118     protected LibraryCandidate(String libraryNameAndVersion) {
119       this.libraryNameAndVersion = libraryNameAndVersion;
120     }
121 
122     @SuppressFBWarnings("THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION")
123     abstract String load() throws Exception;
124 
125     @Override
126     public abstract void close();
127 
128     @Override
129     public String toString() {
130       return super.toString() + "[" + libraryNameAndVersion + "]";
131     }
132   }
133 
134   private static final class StandardLibraryCandidate extends LibraryCandidate {
135     StandardLibraryCandidate(String version) {
136       super(version == null ? LIBRARY_NAME : LIBRARY_NAME + "-" + version);
137     }
138 
139     @Override
140     @SuppressFBWarnings("THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION")
141     String load() throws Exception, LinkageError {
142       if (libraryNameAndVersion != null) {
143         System.loadLibrary(libraryNameAndVersion);
144         return libraryNameAndVersion;
145       }
146       return null;
147     }
148 
149     @Override
150     public void close() {
151     }
152 
153     @Override
154     public String toString() {
155       return super.toString() + "(standard library path)";
156     }
157   }
158 
159   private static final class ClasspathLibraryCandidate extends LibraryCandidate {
160     private final String artifactName;
161     private final URL library;
162     private final String path;
163 
164     ClasspathLibraryCandidate(String artifactName, String libraryNameAndVersion, String path,
165         URL library) {
166       super(libraryNameAndVersion);
167       this.artifactName = artifactName;
168       this.path = path;
169       this.library = library;
170     }
171 
172     /**
173      * Even though we ask the JVM to delete the library file upon VM exit, this may not be honored
174      * in all cases (crash, Windows, etc.)
175      *
176      * Therefore, we attempt to delete these files whenever another JVM using junixsocket starts up.
177      * This is simplified by keeping empty marker files next to the temporary shared library file.
178      *
179      * @param libDir The directory to check.
180      */
181     private void deleteLibTmpDelFiles(File libDir) {
182       if (libDir == null) {
183         try {
184           File tempFile = File.createTempFile("libtmp", ".del");
185           libDir = tempFile.getParentFile();
186           tryDelete(tempFile);
187         } catch (IOException e) {
188           return;
189         }
190       }
191       File[] filesToDelete = libDir.listFiles((File f) -> {
192         if (!f.isFile()) {
193           return false;
194         }
195         String name = f.getName();
196         return name.startsWith("libtmp") && name.endsWith(".del");
197       });
198       if (filesToDelete == null || filesToDelete.length == 0) {
199         return;
200       }
201 
202       for (File f : filesToDelete) {
203         tryDelete(f);
204         String n = f.getName();
205         n = n.substring(0, n.length() - ".del".length());
206         File libFile = new File(f.getParentFile(), n);
207         tryDelete(libFile);
208       }
209     }
210 
211     @Override
212     @SuppressWarnings("PMD.CognitiveComplexity")
213     synchronized String load() throws IOException, LinkageError {
214       if (libraryNameAndVersion == null) {
215         return null;
216       }
217 
218       File libDir = TEMP_DIR;
219       File userHomeDir = new File(System.getProperty("user.home", "."));
220       File userDirOrNull = new File(System.getProperty("user.dir", "."));
221       if (userHomeDir.equals(userDirOrNull)) {
222         userDirOrNull = null;
223       }
224 
225       deleteLibTmpDelFiles(libDir);
226       deleteLibTmpDelFiles(userHomeDir);
227       if (userDirOrNull != null) {
228         deleteLibTmpDelFiles(userDirOrNull);
229       }
230 
231       for (int attempt = 0; attempt < 3; attempt++) {
232         File libFile;
233         try {
234           libFile = File.createTempFile("libtmp", System.mapLibraryName(libraryNameAndVersion),
235               libDir);
236           try (InputStream libraryIn = library.openStream();
237               OutputStream out = new FileOutputStream(libFile)) { // NOPMD UseTryWithResources
238             byte[] buf = new byte[4096];
239             int read;
240             while ((read = libraryIn.read(buf)) >= 0) {
241               out.write(buf, 0, read);
242             }
243           }
244         } catch (IOException e) {
245           throw e;
246         }
247 
248         try {
249           System.load(libFile.getAbsolutePath());
250         } catch (UnsatisfiedLinkError e) {
251           // Operation not permitted; permission denied; EPERM...
252           // -> tmp directory may be mounted with "noexec", try loading from user.home, user.dir
253 
254           switch (attempt) {
255             case 0:
256               libDir = userHomeDir;
257               break;
258             case 1:
259               if (userDirOrNull != null) {
260                 libDir = userDirOrNull;
261                 break;
262               }
263               // fall-through
264             default:
265               throw e;
266           }
267 
268           continue;
269         } finally {
270           if (!libFile.delete() && libFile.exists()) {
271             libFile.deleteOnExit();
272 
273             File markerFile = new File(libFile.getParentFile(), libFile.getName() + ".del");
274             try {
275               Files.createFile(markerFile.toPath());
276               Runtime.getRuntime().addShutdownHook(new Thread(() -> {
277                 if (!libFile.exists() || libFile.delete()) {
278                   tryDelete(markerFile);
279                 }
280               }));
281             } catch (IOException | UnsupportedOperationException e) {
282               // ignore
283             }
284           }
285         }
286 
287         // If we reach this, then we were able to load the library
288         break; // NOPMD.AvoidBranchingStatementAsLastInLoop
289       }
290       return artifactName + "/" + libraryNameAndVersion;
291     }
292 
293     @SuppressFBWarnings("RV_RETURN_VALUE_IGNORED_BAD_PRACTICE")
294     private static void tryDelete(File f) {
295       f.delete(); // NOPMD
296     }
297 
298     @Override
299     public void close() {
300     }
301 
302     @Override
303     public String toString() {
304       return super.toString() + "(" + artifactName + ":" + path + ")";
305     }
306   }
307 
308   private synchronized void setLoaded(String library) {
309     setLoaded0(library);
310   }
311 
312   @SuppressFBWarnings("THROWS_METHOD_THROWS_RUNTIMEEXCEPTION")
313   private static synchronized void setLoaded0(String library) {
314     if (LOADED.compareAndSet(false, true)) {
315       NativeUnixSocket.setLoaded(true);
316       AFSocket.loadedLibrary = library;
317       try {
318         NativeUnixSocket.init();
319       } catch (RuntimeException e) {
320         throw e;
321       } catch (Exception e) {
322         throw new IllegalStateException(e);
323       }
324     }
325   }
326 
327   private Throwable loadLibraryOverride() {
328     String libraryOverride = System.getProperty(PROP_LIBRARY_OVERRIDE, "");
329     String libraryOverrideForce = System.getProperty(PROP_LIBRARY_OVERRIDE_FORCE, "false");
330 
331     boolean overrideIsAbsolute;
332     try {
333       if (libraryOverrideForce.length() <= 5) { // reasonable simplification
334         overrideIsAbsolute = false;
335       } else {
336         overrideIsAbsolute = new File(libraryOverrideForce).isAbsolute();
337       }
338     } catch (Exception e) {
339       overrideIsAbsolute = false;
340       e.printStackTrace(); // NOPMD
341     }
342     if (libraryOverride.isEmpty() && overrideIsAbsolute) {
343       libraryOverride = libraryOverrideForce;
344       libraryOverrideForce = "true";
345     }
346 
347     if (!libraryOverride.isEmpty()) {
348       try {
349         System.load(libraryOverride);
350         setLoaded(libraryOverride);
351         return null;
352       } catch (Exception | LinkageError e) {
353         if (Boolean.parseBoolean(libraryOverrideForce)) {
354           throw e;
355         }
356         return e;
357       }
358     } else {
359       return new Exception("No library specified with -D" + PROP_LIBRARY_OVERRIDE + "=");
360     }
361   }
362 
363   private static Object loadLibrarySyncMonitor() {
364     Object monitor = NativeLibraryLoader.class.getClassLoader(); // NOPMD
365     if (monitor == null) {
366       // bootstrap classloader?
367       return NativeLibraryLoader.class;
368     } else {
369       return monitor;
370     }
371   }
372 
373   @SuppressWarnings("null")
374   public synchronized void loadLibrary() {
375     synchronized (loadLibrarySyncMonitor()) { // NOPMD We want to lock this class' classloader.
376       if (LOADED.get()) {
377         // Already loaded
378         return;
379       }
380 
381       NativeUnixSocket.initPre();
382 
383       // set -Dorg.newsclub.net.unix.library.override.force=provided to assume that
384       // we already have loaded the library via System.load, etc.
385       if ("provided".equals(System.getProperty(PROP_LIBRARY_OVERRIDE_FORCE, ""))) {
386         setLoaded("provided");
387         return;
388       }
389 
390       boolean provided = false;
391       try {
392         NativeUnixSocket.noop();
393         provided = true;
394       } catch (UnsatisfiedLinkError | Exception e) {
395         // expected unless we manually loaded the library
396       }
397       if (provided) {
398         setLoaded("provided");
399         return;
400       }
401 
402       if (Boolean.parseBoolean(System.getProperty(PROP_LIBRARY_DISABLE, "false"))) {
403         throw initCantLoadLibraryError(Collections.singletonList(new UnsupportedOperationException(
404             "junixsocket disabled by System.property " + PROP_LIBRARY_DISABLE)));
405       }
406 
407       List<Throwable> suppressedThrowables = new ArrayList<>();
408       Throwable ex = loadLibraryOverride();
409       if (ex == null) {
410         return;
411       }
412       suppressedThrowables.add(ex);
413 
414       List<LibraryCandidate> candidates = initLibraryCandidates(suppressedThrowables);
415 
416       String loadedLibraryId = null;
417       for (LibraryCandidate candidate : candidates) {
418         try {
419           if ((loadedLibraryId = candidate.load()) != null) {
420             break;
421           }
422         } catch (Exception | LinkageError e) {
423           suppressedThrowables.add(e);
424         }
425       }
426 
427       for (LibraryCandidate candidate : candidates) {
428         candidate.close();
429       }
430 
431       if (loadedLibraryId == null) {
432         throw initCantLoadLibraryError(suppressedThrowables);
433       }
434 
435       setLoaded(loadedLibraryId);
436     }
437   }
438 
439   private UnsatisfiedLinkError initCantLoadLibraryError(List<Throwable> suppressedThrowables) {
440     String message = "Could not load native library " + LIBRARY_NAME + " for architecture "
441         + ARCHITECTURE_AND_OS;
442 
443     String cp = System.getProperty("java.class.path", "");
444     if (cp.contains("junixsocket-native-custom/target-eclipse") || cp.contains(
445         "junixsocket-native-common/target-eclipse")) {
446       message += "\n\n*** ECLIPSE USERS ***\nIf you're running from within Eclipse, "
447           + "please close the projects \"junixsocket-native-common\" and \"junixsocket-native-custom\"\n";
448     }
449 
450     UnsatisfiedLinkError e = new UnsatisfiedLinkError(message);
451     if (suppressedThrowables != null) {
452       for (Throwable suppressed : suppressedThrowables) {
453         e.addSuppressed(suppressed);
454       }
455     }
456     throw e;
457   }
458 
459   private List<LibraryCandidate> initLibraryCandidates(List<Throwable> suppressedThrowables) {
460     List<LibraryCandidate> candidates = new ArrayList<>();
461     try {
462       String version = getArtifactVersion(getClass(), "junixsocket-common", "junixsocket-core");
463       if (version != null) {
464         candidates.add(new StandardLibraryCandidate(version));
465       }
466     } catch (Exception e) {
467       suppressedThrowables.add(e);
468     }
469 
470     try {
471       candidates.addAll(tryProviderClass("org.newsclub.lib.junixsocket.custom.NarMetadata",
472           "junixsocket-native-custom"));
473     } catch (Exception e) {
474       suppressedThrowables.add(e);
475     }
476     try {
477       candidates.addAll(tryProviderClass("org.newsclub.lib.junixsocket.common.NarMetadata",
478           "junixsocket-native-common"));
479     } catch (Exception e) {
480       suppressedThrowables.add(e);
481     }
482 
483     candidates.add(new StandardLibraryCandidate(null));
484 
485     return candidates;
486   }
487 
488   private static String lookupArchProperty(String key, String defaultVal) {
489     return System.getProperty(key, defaultVal).replaceAll("[ /\\\\'\";:\\$]", "");
490   }
491 
492   private static List<String> architectureAndOS() {
493     String arch = lookupArchProperty("os.arch", "UnknownArch");
494 
495     List<String> list = new ArrayList<>();
496     if (IS_ANDROID) {
497       // Android identifies itself as os.name="Linux"
498       // let's probe for an Android-specific library first
499       list.add(arch + "-Android");
500     }
501     list.add(arch + "-" + OS_NAME_SIMPLIFIED);
502     if (OS_NAME_SIMPLIFIED.startsWith("Windows") && !"Windows10".equals(OS_NAME_SIMPLIFIED)) {
503       list.add(arch + "-" + "Windows10");
504     }
505 
506     if ("MacOSX".equals(OS_NAME_SIMPLIFIED) && "x86_64".equals(arch)) {
507       list.add("aarch64-MacOSX"); // Rosetta 2
508     }
509 
510     return list;
511   }
512 
513   private static boolean checkAndroid() {
514     String vmName = lookupArchProperty("java.vm.name", "UnknownVM");
515     String vmSpecVendor = lookupArchProperty("java.vm.specification.vendor",
516         "UnknownSpecificationVendor");
517 
518     return ("Dalvik".equals(vmName) || vmSpecVendor.contains("Android"));
519   }
520 
521   static boolean isAndroid() {
522     return IS_ANDROID;
523   }
524 
525   static List<String> getArchitectureAndOS() {
526     return ARCHITECTURE_AND_OS;
527   }
528 
529   private static URL validateResourceURL(URL url) {
530     if (url == null) {
531       return null;
532     }
533     try (InputStream unused = url.openStream()) {
534       return url;
535     } catch (IOException e) {
536       return null;
537     }
538   }
539 
540   private static String mapLibraryName(String libraryNameAndVersion) {
541     String mappedName = System.mapLibraryName(libraryNameAndVersion);
542     if (mappedName.endsWith(".so")) {
543       // https://github.com/eclipse-openj9/openj9/issues/9788
544       // Many thanks to Fabrice Bourquin for finding this issue!
545       switch (OS_NAME_SIMPLIFIED) {
546         case "AIX":
547           mappedName = mappedName.substring(0, mappedName.length() - 3) + ".a";
548           break;
549         case "OS400":
550           mappedName = mappedName.substring(0, mappedName.length() - 3) + ".srvpgm";
551           break;
552         default:
553           break;
554       }
555     }
556     return mappedName;
557   }
558 
559   private List<LibraryCandidate> findLibraryCandidates(String artifactName,
560       String libraryNameAndVersion, Class<?> providerClass) {
561     String mappedName = mapLibraryName(libraryNameAndVersion);
562 
563     String[] prefixes = mappedName.startsWith("lib") ? new String[] {""} : new String[] {"", "lib"};
564 
565     List<LibraryCandidate> list = new ArrayList<>();
566     for (String archOs : ARCHITECTURE_AND_OS) {
567       for (String compiler : new String[] {"clang", "gcc"
568           // "gpp", "g++", "linker", "clang", "gcc", "cc", "CC", "icpc", "icc", "xlC", "xlC_r",
569           // "msvc",
570           // "icl", "ecpc", "ecc"
571       }) {
572         for (String prefix : prefixes) {
573           String path = "/lib/" + archOs + "-" + compiler + "/jni/" + prefix + mappedName;
574 
575           URL url;
576 
577           url = validateResourceURL(providerClass.getResource(path));
578           if (url != null) {
579             list.add(new ClasspathLibraryCandidate(artifactName, libraryNameAndVersion, path, url));
580           }
581 
582           // NOTE: we have to try .nodeps version _after_ trying the properly linked one.
583           // While the former may throw an UnsatisfiedLinkError, this one may just terminate the VM
584           // with a "symbol lookup error"
585           String nodepsPath = nodepsPath(path);
586           if (nodepsPath != null) {
587             url = validateResourceURL(providerClass.getResource(nodepsPath));
588             if (url != null) {
589               list.add(new ClasspathLibraryCandidate(artifactName, libraryNameAndVersion,
590                   nodepsPath, url));
591             }
592           }
593         }
594       }
595     }
596     return list;
597   }
598 
599   private String nodepsPath(String path) {
600     int lastDot = path.lastIndexOf('.');
601     if (lastDot == -1) {
602       return null;
603     } else {
604       return path.substring(0, lastDot) + ".nodeps" + path.substring(lastDot);
605     }
606   }
607 
608   @Override
609   public void close() {
610   }
611 }