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