1 /*
  2  * Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.  Oracle designates this
  8  * particular file as subject to the "Classpath" exception as provided
  9  * by Oracle in the LICENSE file that accompanied this code.
 10  *
 11  * This code is distributed in the hope that it will be useful, but WITHOUT
 12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 14  * version 2 for more details (a copy is included in the LICENSE file that
 15  * accompanied this code).
 16  *
 17  * You should have received a copy of the GNU General Public License version
 18  * 2 along with this work; if not, write to the Free Software Foundation,
 19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 20  *
 21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 22  * or visit www.oracle.com if you need additional information or have any
 23  * questions.
 24  */
 25 package jdk.internal.loader;
 26 
 27 import jdk.internal.misc.VM;
 28 import jdk.internal.ref.CleanerFactory;
 29 import jdk.internal.util.StaticProperty;
 30 
 31 import java.io.File;
 32 import java.io.IOException;
 33 import java.util.ArrayDeque;
 34 import java.util.Deque;
 35 import java.util.function.BiFunction;
 36 import java.util.function.Function;
 37 import java.util.Map;
 38 import java.util.Set;
 39 import java.util.concurrent.ConcurrentHashMap;
 40 import java.util.concurrent.locks.ReentrantLock;
 41 
 42 /**
 43  * Native libraries are loaded via {@link System#loadLibrary(String)},
 44  * {@link System#load(String)}, {@link Runtime#loadLibrary(String)} and
 45  * {@link Runtime#load(String)}.  They are caller-sensitive.
 46  *
 47  * Each class loader has a NativeLibraries instance to register all of its
 48  * loaded native libraries.  System::loadLibrary (and other APIs) only
 49  * allows a native library to be loaded by one class loader, i.e. one
 50  * NativeLibraries instance.  Any attempt to load a native library that
 51  * has already been loaded by a class loader with another class loader
 52  * will fail.
 53  */
 54 public final class NativeLibraries {
 55     private static final boolean loadLibraryOnlyIfPresent = ClassLoaderHelper.loadLibraryOnlyIfPresent();
 56     private final Map<String, NativeLibraryImpl> libraries = new ConcurrentHashMap<>();
 57     private final ClassLoader loader;
 58     // caller, if non-null, is the fromClass parameter for NativeLibraries::loadLibrary
 59     // unless specified
 60     private final Class<?> caller;      // may be null
 61     private final boolean searchJavaLibraryPath;
 62 
 63     /**
 64      * Creates a NativeLibraries instance for loading JNI native libraries
 65      * via for System::loadLibrary use.
 66      *
 67      * 1. Support of auto-unloading.  The loaded native libraries are unloaded
 68      *    when the class loader is reclaimed.
 69      * 2. Support of linking of native method.  See JNI spec.
 70      * 3. Restriction on a native library that can only be loaded by one class loader.
 71      *    Each class loader manages its own set of native libraries.
 72      *    The same JNI native library cannot be loaded into more than one class loader.
 73      *
 74      * This static factory method is intended only for System::loadLibrary use.
 75      *
 76      * @see <a href="${docroot}/specs/jni/invocation.html##library-and-version-management">
 77      *     JNI Specification: Library and Version Management</a>
 78      */
 79     public static NativeLibraries newInstance(ClassLoader loader) {
 80         return new NativeLibraries(loader, loader != null ? null : NativeLibraries.class, loader != null);
 81     }
 82 
 83     private NativeLibraries(ClassLoader loader, Class<?> caller, boolean searchJavaLibraryPath) {
 84         this.loader = loader;
 85         this.caller = caller;
 86         this.searchJavaLibraryPath = searchJavaLibraryPath;
 87     }
 88 
 89     /*
 90      * Find the address of the given symbol name from the native libraries
 91      * loaded in this NativeLibraries instance.
 92      */
 93     public long find(String name) {
 94         if (libraries.isEmpty())
 95             return 0;
 96 
 97         // the native libraries map may be updated in another thread
 98         // when a native library is being loaded.  No symbol will be
 99         // searched from it yet.
100         for (NativeLibrary lib : libraries.values()) {
101             long entry = lib.find(name);
102             if (entry != 0) return entry;
103         }
104         return 0;
105     }
106 
107     /*
108      * Load a native library from the given file.  Returns null if the given
109      * library is determined to be non-loadable, which is system-dependent.
110      *
111      * @param fromClass the caller class calling System::loadLibrary
112      * @param file the path of the native library
113      * @throws UnsatisfiedLinkError if any error in loading the native library
114      */
115     public NativeLibrary loadLibrary(Class<?> fromClass, File file) {
116         // Check to see if we're attempting to access a static library
117         String name = findBuiltinLib(file.getName());
118         boolean isBuiltin = (name != null);
119         if (!isBuiltin) {
120             try {
121                 if (loadLibraryOnlyIfPresent && !file.exists()) {
122                     return null;
123                 }
124                 name = file.getCanonicalPath();
125             } catch (IOException e) {
126                 return null;
127             }
128         }
129         return loadLibrary(fromClass, name, isBuiltin);
130     }
131 
132     /**
133      * Returns a NativeLibrary of the given name.
134      *
135      * @param fromClass the caller class calling System::loadLibrary
136      * @param name      library name
137      * @param isBuiltin built-in library
138      * @throws UnsatisfiedLinkError if the native library has already been loaded
139      *      and registered in another NativeLibraries
140      */
141     private NativeLibrary loadLibrary(Class<?> fromClass, String name, boolean isBuiltin) {
142         ClassLoader loader = (fromClass == null) ? null : fromClass.getClassLoader();
143         if (this.loader != loader) {
144             throw new InternalError(fromClass.getName() + " not allowed to load library");
145         }
146 
147         acquireNativeLibraryLock(name);
148         try {
149             // find if this library has already been loaded and registered in this NativeLibraries
150             NativeLibrary cached = libraries.get(name);
151             if (cached != null) {
152                 return cached;
153             }
154 
155             // cannot be loaded by other class loaders
156             if (loadedLibraryNames.contains(name)) {
157                 throw new UnsatisfiedLinkError("Native Library " + name +
158                         " already loaded in another classloader");
159             }
160 
161             /*
162              * When a library is being loaded, JNI_OnLoad function can cause
163              * another loadLibrary invocation that should succeed.
164              *
165              * Each thread maintains its own stack to hold the list of
166              * libraries it is loading.
167              *
168              * If there is a pending load operation for the library, we
169              * immediately return success; if the pending load is from
170              * a different class loader, we raise UnsatisfiedLinkError.
171              */
172             for (NativeLibraryImpl lib : NativeLibraryContext.current()) {
173                 if (name.equals(lib.name())) {
174                     if (loader == lib.fromClass.getClassLoader()) {
175                         return lib;
176                     } else {
177                         throw new UnsatisfiedLinkError("Native Library " +
178                                 name + " is being loaded in another classloader");
179                     }
180                 }
181             }
182 
183             NativeLibraryImpl lib = new NativeLibraryImpl(fromClass, name, isBuiltin);
184             // load the native library
185             NativeLibraryContext.push(lib);
186             try {
187                 if (!lib.open()) {
188                     return null;    // fail to open the native library
189                 }
190                 // auto unloading is only supported for JNI native libraries
191                 // loaded by custom class loaders that can be unloaded.
192                 // built-in class loaders are never unloaded.
193                 boolean autoUnload = !VM.isSystemDomainLoader(loader) && loader != ClassLoaders.appClassLoader();
194                 if (autoUnload) {
195                     // register the loaded native library for auto unloading
196                     // when the class loader is reclaimed, all native libraries
197                     // loaded that class loader will be unloaded.
198                     // The entries in the libraries map are not removed since
199                     // the entire map will be reclaimed altogether.
200                     CleanerFactory.cleaner().register(loader, lib.unloader());
201                 }
202             } finally {
203                 NativeLibraryContext.pop();
204             }
205             // register the loaded native library
206             loadedLibraryNames.add(name);
207             libraries.put(name, lib);
208             return lib;
209         } finally {
210             releaseNativeLibraryLock(name);
211         }
212     }
213 
214     /**
215      * Loads a native library from the system library path and java library path.
216      *
217      * @param name library name
218      *
219      * @throws UnsatisfiedLinkError if the native library has already been loaded
220      *      and registered in another NativeLibraries
221      */
222     public NativeLibrary loadLibrary(String name) {
223         assert name.indexOf(File.separatorChar) < 0;
224         return loadLibrary(caller, name);
225     }
226 
227     /**
228      * Loads a native library from the system library path and java library path.
229      *
230      * @param name library name
231      * @param fromClass the caller class calling System::loadLibrary
232      *
233      * @throws UnsatisfiedLinkError if the native library has already been loaded
234      *      and registered in another NativeLibraries
235      */
236     public NativeLibrary loadLibrary(Class<?> fromClass, String name) {
237         assert name.indexOf(File.separatorChar) < 0;
238 
239         NativeLibrary lib = findFromPaths(LibraryPaths.SYS_PATHS, fromClass, name);
240         if (lib == null && searchJavaLibraryPath) {
241             lib = findFromPaths(LibraryPaths.USER_PATHS, fromClass, name);
242         }
243         return lib;
244     }
245 
246     private NativeLibrary findFromPaths(String[] paths, Class<?> fromClass, String name) {
247         for (String path : paths) {
248             File libfile = new File(path, System.mapLibraryName(name));
249             NativeLibrary nl = loadLibrary(fromClass, libfile);
250             if (nl != null) {
251                 return nl;
252             }
253             libfile = ClassLoaderHelper.mapAlternativeName(libfile);
254             if (libfile != null) {
255                 nl = loadLibrary(fromClass, libfile);
256                 if (nl != null) {
257                     return nl;
258                 }
259             }
260         }
261         return null;
262     }
263 
264     /**
265      * NativeLibraryImpl denotes a loaded native library instance.
266      * Each NativeLibraries contains a map of loaded native libraries in the
267      * private field {@code libraries}.
268      *
269      * Every native library requires a particular version of JNI. This is
270      * denoted by the private {@code jniVersion} field.  This field is set by
271      * the VM when it loads the library, and used by the VM to pass the correct
272      * version of JNI to the native methods.
273      */
274     static class NativeLibraryImpl extends NativeLibrary {
275         // the class from which the library is loaded, also indicates
276         // the loader this native library belongs.
277         final Class<?> fromClass;
278         // the canonicalized name of the native library.
279         // or static library name
280         final String name;
281         // Indicates if the native library is linked into the VM
282         final boolean isBuiltin;
283 
284         // opaque handle to native library, used in native code.
285         long handle;
286         // the version of JNI environment the native library requires.
287         int jniVersion;
288 
289         NativeLibraryImpl(Class<?> fromClass, String name, boolean isBuiltin) {
290             this.fromClass = fromClass;
291             this.name = name;
292             this.isBuiltin = isBuiltin;
293         }
294 
295         @Override
296         public String name() {
297             return name;
298         }
299 
300         @Override
301         public long find(String name) {
302             return findEntry0(handle, name);
303         }
304 
305         /*
306          * Unloader::run method is invoked to unload the native library
307          * when this class loader becomes phantom reachable.
308          */
309         private Runnable unloader() {
310             return new Unloader(name, handle, isBuiltin);
311         }
312 
313         /*
314          * Loads the named native library
315          */
316         boolean open() {
317             if (handle != 0) {
318                 throw new InternalError("Native library " + name + " has been loaded");
319             }
320 
321             return load(this, name, isBuiltin, throwExceptionIfFail());
322         }
323 
324         private boolean throwExceptionIfFail() {
325             if (loadLibraryOnlyIfPresent) return true;
326 
327             // If the file exists but fails to load, UnsatisfiedLinkException thrown by the VM
328             // will include the error message from dlopen to provide diagnostic information
329             File file = new File(name);
330             return file.exists();
331         }
332 
333         /*
334          * Close this native library.
335          */
336         void close() {
337             unload(name, isBuiltin, handle);
338         }
339     }
340 
341     /*
342      * The run() method will be invoked when this class loader becomes
343      * phantom reachable to unload the native library.
344      */
345     static class Unloader implements Runnable {
346         // This represents the context when a native library is unloaded
347         // and getFromClass() will return null,
348         static final NativeLibraryImpl UNLOADER =
349                 new NativeLibraryImpl(null, "dummy", false);
350 
351         final String name;
352         final long handle;
353         final boolean isBuiltin;
354 
355         Unloader(String name, long handle, boolean isBuiltin) {
356             if (handle == 0) {
357                 throw new IllegalArgumentException(
358                         "Invalid handle for native library " + name);
359             }
360 
361             this.name = name;
362             this.handle = handle;
363             this.isBuiltin = isBuiltin;
364         }
365 
366         @Override
367         public void run() {
368             acquireNativeLibraryLock(name);
369             try {
370                 /* remove the native library name */
371                 if (!loadedLibraryNames.remove(name)) {
372                     throw new IllegalStateException(name + " has already been unloaded");
373                 }
374                 NativeLibraryContext.push(UNLOADER);
375                 try {
376                     unload(name, isBuiltin, handle);
377                 } finally {
378                     NativeLibraryContext.pop();
379                 }
380             } finally {
381                 releaseNativeLibraryLock(name);
382             }
383         }
384     }
385 
386     /*
387      * Holds system and user library paths derived from the
388      * {@code java.library.path} and {@code sun.boot.library.path} system
389      * properties. The system properties are eagerly read at bootstrap, then
390      * lazily parsed on first use to avoid initialization ordering issues.
391      */
392     static class LibraryPaths {
393         // The paths searched for libraries
394         static final String[] SYS_PATHS = ClassLoaderHelper.parsePath(StaticProperty.sunBootLibraryPath());
395         static final String[] USER_PATHS = ClassLoaderHelper.parsePath(StaticProperty.javaLibraryPath());
396     }
397 
398     // All native libraries we've loaded.
399     private static final Set<String> loadedLibraryNames =
400             ConcurrentHashMap.newKeySet();
401 
402     // reentrant lock class that allows exact counting (with external synchronization)
403     @SuppressWarnings("serial")
404     private static final class CountedLock extends ReentrantLock {
405 
406         private int counter = 0;
407 
408         public void increment() {
409             if (counter == Integer.MAX_VALUE) {
410                 // prevent overflow
411                 throw new Error("Maximum lock count exceeded");
412             }
413             ++counter;
414         }
415 
416         public void decrement() {
417             --counter;
418         }
419 
420         public int getCounter() {
421             return counter;
422         }
423     }
424 
425     // Maps native library name to the corresponding lock object
426     private static final Map<String, CountedLock> nativeLibraryLockMap =
427             new ConcurrentHashMap<>();
428 
429     private static void acquireNativeLibraryLock(String libraryName) {
430         nativeLibraryLockMap.compute(libraryName,
431             new BiFunction<>() {
432                 public CountedLock apply(String name, CountedLock currentLock) {
433                     if (currentLock == null) {
434                         currentLock = new CountedLock();
435                     }
436                     // safe as compute BiFunction<> is executed atomically
437                     currentLock.increment();
438                     return currentLock;
439                 }
440             }
441         ).lock();
442     }
443 
444     private static void releaseNativeLibraryLock(String libraryName) {
445         CountedLock lock = nativeLibraryLockMap.computeIfPresent(libraryName,
446             new BiFunction<>() {
447                 public CountedLock apply(String name, CountedLock currentLock) {
448                     if (currentLock.getCounter() == 1) {
449                         // unlock and release the object if no other threads are queued
450                         currentLock.unlock();
451                         // remove the element
452                         return null;
453                     } else {
454                         currentLock.decrement();
455                         return currentLock;
456                     }
457                 }
458             }
459         );
460         if (lock != null) {
461             lock.unlock();
462         }
463     }
464 
465     // native libraries being loaded
466     private static final class NativeLibraryContext {
467 
468         // Maps thread object to the native library context stack, maintained by each thread
469         private static Map<Thread, Deque<NativeLibraryImpl>> nativeLibraryThreadContext =
470                 new ConcurrentHashMap<>();
471 
472         // returns a context associated with the current thread
473         private static Deque<NativeLibraryImpl> current() {
474             return nativeLibraryThreadContext.computeIfAbsent(
475                     Thread.currentThread(),
476                     new Function<>() {
477                         public Deque<NativeLibraryImpl> apply(Thread t) {
478                             return new ArrayDeque<>(8);
479                         }
480                     });
481         }
482 
483         private static NativeLibraryImpl peek() {
484             return current().peek();
485         }
486 
487         private static void push(NativeLibraryImpl lib) {
488             current().push(lib);
489         }
490 
491         private static void pop() {
492             // this does not require synchronization since each
493             // thread has its own context
494             Deque<NativeLibraryImpl> libs = current();
495             libs.pop();
496             if (libs.isEmpty()) {
497                 // context can be safely removed once empty
498                 nativeLibraryThreadContext.remove(Thread.currentThread());
499             }
500         }
501 
502         private static boolean isEmpty() {
503             Deque<NativeLibraryImpl> context =
504                     nativeLibraryThreadContext.get(Thread.currentThread());
505             return (context == null || context.isEmpty());
506         }
507     }
508 
509     // Invoked in the VM to determine the context class in JNI_OnLoad
510     // and JNI_OnUnload
511     private static Class<?> getFromClass() {
512         if (NativeLibraryContext.isEmpty()) { // only default library
513             return Object.class;
514         }
515         return NativeLibraryContext.peek().fromClass;
516     }
517 
518     /*
519      * Return true if the given library is successfully loaded.
520      * If the given library cannot be loaded for any reason,
521      * if throwExceptionIfFail is false, then this method returns false;
522      * otherwise, UnsatisfiedLinkError will be thrown.
523      *
524      * JNI FindClass expects the caller class if invoked from JNI_OnLoad
525      * and JNI_OnUnload is NativeLibrary class.
526      */
527     private static native boolean load(NativeLibraryImpl impl, String name,
528                                        boolean isBuiltin,
529                                        boolean throwExceptionIfFail);
530     /*
531      * Unload the named library.  JNI_OnUnload, if present, will be invoked
532      * before the native library is unloaded.
533      */
534     private static native void unload(String name, boolean isBuiltin, long handle);
535     private static native String findBuiltinLib(String name);
536 }